From 8942c29229b5bee89ae684f9f55cdcc1d3f2c9ba Mon Sep 17 00:00:00 2001 From: Alex Inkin Date: Tue, 15 Mar 2022 20:11:54 +0300 Subject: [PATCH] feat(marketplace): add separate package and move some entities in it (#1283) * feat(marketplace): add separate package and move some entities in it * feat(marketplace): refactor release notes and list * feat(marketplace): refactor showing a package * chore: fix install progress * chore: fix angular.json * chore: properly share stream --- frontend/angular.json | 36 ++- frontend/projects/marketplace/ng-package.json | 7 + frontend/projects/marketplace/package.json | 13 + .../src/pipes/filter-packages.pipe.ts | 65 ++++ .../src}/pipes/install-progress.pipe.ts | 0 .../src}/pipes/marketplace-pipes.module.ts | 5 +- .../src}/pipes/trust.pipe.ts | 0 .../projects/marketplace/src/public-api.ts | 17 + .../src/services/marketplace.service.ts | 17 + .../marketplace/src/types/local-pkg.ts | 8 + .../marketplace/src/types/marketplace-data.ts | 4 + .../src/types/marketplace-manifest.ts | 25 ++ .../marketplace/src/types/marketplace-pkg.ts | 17 + .../marketplace/src/types/marketplace.ts | 4 + .../marketplace/src/utils/spread-progress.ts | 5 + frontend/projects/marketplace/tsconfig.json | 13 + .../projects/marketplace/tsconfig.prod.json | 10 + .../setup-wizard/src/app/app.component.ts | 4 +- .../src/app/pages/embassy/embassy.page.ts | 10 +- .../src/app/pages/recover/recover.page.ts | 6 +- .../src/app/pages/success/success.page.ts | 2 +- .../src/app/services/error-toast.service.ts | 43 --- .../src/app/services/state.service.ts | 9 +- frontend/projects/shared/ng-package.json | 1 + frontend/projects/shared/package.json | 1 + .../directives/element/element.directive.ts | 11 + .../src/directives/element/element.module.ts | 9 + frontend/projects/shared/src/public-api.ts | 5 + .../src}/services/error-toast.service.ts | 9 +- frontend/projects/shared/src/types/url.ts | 1 + .../projects/shared/src/util/misc.util.ts | 3 +- .../{ => projects/shared}/styles/global.scss | 50 --- frontend/projects/shared/styles/shared.scss | 85 +++++ .../shared}/styles/variables.scss | 0 frontend/projects/ui/src/app/app.component.ts | 36 +-- frontend/projects/ui/src/app/app.module.ts | 4 +- .../backup-drives/backup-drives.component.ts | 2 +- .../backup-drives/backup.service.ts | 3 +- .../install-wizard/prebaked-wizards.ts | 28 +- .../ui/src/app/components/logs/logs.page.ts | 59 ++-- .../projects/ui/src/app/marketplace.module.ts | 13 + .../app/modals/app-config/app-config.page.ts | 12 +- .../app-recover-select.page.ts | 3 +- .../generic-input/generic-input.component.ts | 16 +- .../src/app/modals/markdown/markdown.page.ts | 3 +- .../app-actions/app-actions.page.ts | 3 +- .../app-list-rec/app-list-rec.component.ts | 24 +- .../app-metrics/app-metrics.page.ts | 3 +- .../app-properties/app-properties.page.ts | 2 +- .../app-show-status.component.ts | 7 +- .../dev-config/dev-config.page.ts | 2 +- .../dev-instructions/dev-instructions.page.ts | 2 +- .../developer-list/developer-list.page.ts | 3 +- .../developer-menu/developer-menu.page.ts | 2 +- .../app-release-notes.page.html | 35 --- .../app-release-notes.page.scss | 8 - .../app-release-notes.page.ts | 62 ---- .../marketplace-list-content.component.html | 62 ++++ .../marketplace-list-content.component.scss | 46 +++ .../marketplace-list-content.component.ts | 34 ++ .../marketplace-list-header.component.html | 7 + .../marketplace-list-header.component.ts | 8 + .../marketplace-list-skeleton.component.html | 38 +++ .../marketplace-list-skeleton.component.ts | 8 + .../marketplace-list.module.ts | 21 +- .../marketplace-list.page.html | 180 +---------- .../marketplace-list.page.scss | 30 -- .../marketplace-list/marketplace-list.page.ts | 167 +++------- .../marketplace-routing.module.ts | 17 +- .../marketplace-show-about.component.html | 23 ++ .../marketplace-show-about.component.scss | 9 + .../marketplace-show-about.component.ts | 13 + ...marketplace-show-additional.component.html | 76 +++++ .../marketplace-show-additional.component.ts | 71 +++++ .../marketplace-show-controls.component.html | 33 ++ .../marketplace-show-controls.component.ts | 92 ++++++ ...rketplace-show-dependencies.component.html | 32 ++ ...marketplace-show-dependencies.component.ts | 23 ++ .../marketplace-show-dependent.component.html | 30 ++ .../marketplace-show-dependent.component.scss | 18 ++ .../marketplace-show-dependent.component.ts | 24 ++ .../marketplace-show-header.component.html | 8 + .../marketplace-show-header.component.ts | 8 + .../marketplace-show.module.ts | 32 +- .../marketplace-show.page.html | 293 ++---------------- .../marketplace-show.page.scss | 29 +- .../marketplace-show/marketplace-show.page.ts | 255 ++++----------- .../marketplace-status.component.html | 28 ++ .../marketplace-status.component.ts | 18 ++ .../marketplace-status.module.ts | 19 ++ .../marketplace-routes/marketplace.service.ts | 161 ---------- .../release-notes-header.component.html | 8 + .../release-notes-header.component.ts | 13 + .../release-notes.module.ts} | 15 +- .../release-notes/release-notes.page.html | 34 ++ .../release-notes/release-notes.page.scss | 19 ++ .../release-notes/release-notes.page.ts | 38 +++ .../pages/notifications/notifications.page.ts | 2 +- .../marketplaces/marketplaces.page.html | 10 +- .../marketplaces/marketplaces.page.scss | 11 +- .../marketplaces/marketplaces.page.ts | 39 +-- .../server-metrics/server-metrics.page.ts | 3 +- .../server-show/server-show.page.ts | 3 +- .../server-routes/sessions/sessions.page.ts | 20 +- .../server-routes/ssh-keys/ssh-keys.page.ts | 34 +- .../app/pages/server-routes/wifi/wifi.page.ts | 3 +- .../ui/src/app/services/api/api.fixures.ts | 2 +- .../ui/src/app/services/api/api.types.ts | 23 +- .../services/api/embassy-mock-api.service.ts | 2 +- .../src/app/services/marketplace.service.ts | 220 +++++++++++++ .../src/app/services/patch-db/data-model.ts | 42 +-- .../app/services/patch-db/patch-db.service.ts | 1 + .../src/app/services/server-config.service.ts | 2 +- frontend/projects/ui/src/styles.scss | 20 -- frontend/tsconfig.json | 1 + 115 files changed, 1848 insertions(+), 1457 deletions(-) create mode 100644 frontend/projects/marketplace/ng-package.json create mode 100644 frontend/projects/marketplace/package.json create mode 100644 frontend/projects/marketplace/src/pipes/filter-packages.pipe.ts rename frontend/projects/{ui/src/app/pages/marketplace-routes => marketplace/src}/pipes/install-progress.pipe.ts (100%) rename frontend/projects/{ui/src/app/pages/marketplace-routes => marketplace/src}/pipes/marketplace-pipes.module.ts (50%) rename frontend/projects/{ui/src/app/pages/marketplace-routes => marketplace/src}/pipes/trust.pipe.ts (100%) create mode 100644 frontend/projects/marketplace/src/public-api.ts create mode 100644 frontend/projects/marketplace/src/services/marketplace.service.ts create mode 100644 frontend/projects/marketplace/src/types/local-pkg.ts create mode 100644 frontend/projects/marketplace/src/types/marketplace-data.ts create mode 100644 frontend/projects/marketplace/src/types/marketplace-manifest.ts create mode 100644 frontend/projects/marketplace/src/types/marketplace-pkg.ts create mode 100644 frontend/projects/marketplace/src/types/marketplace.ts create mode 100644 frontend/projects/marketplace/src/utils/spread-progress.ts create mode 100644 frontend/projects/marketplace/tsconfig.json create mode 100644 frontend/projects/marketplace/tsconfig.prod.json delete mode 100644 frontend/projects/setup-wizard/src/app/services/error-toast.service.ts create mode 100644 frontend/projects/shared/src/directives/element/element.directive.ts create mode 100644 frontend/projects/shared/src/directives/element/element.module.ts rename frontend/projects/{ui/src/app => shared/src}/services/error-toast.service.ts (84%) create mode 100644 frontend/projects/shared/src/types/url.ts rename frontend/{ => projects/shared}/styles/global.scss (55%) create mode 100644 frontend/projects/shared/styles/shared.scss rename frontend/{ => projects/shared}/styles/variables.scss (100%) create mode 100644 frontend/projects/ui/src/app/marketplace.module.ts delete mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.html delete mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.scss delete mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.ts create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.html create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.scss create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.ts create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-header/marketplace-list-header.component.html create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-header/marketplace-list-header.component.ts create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-skeleton/marketplace-list-skeleton.component.html create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-skeleton/marketplace-list-skeleton.component.ts delete mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.scss create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-about/marketplace-show-about.component.html create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-about/marketplace-show-about.component.scss create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-about/marketplace-show-about.component.ts create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-additional/marketplace-show-additional.component.html create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-additional/marketplace-show-additional.component.ts create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependencies/marketplace-show-dependencies.component.html create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependencies/marketplace-show-dependencies.component.ts create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.scss create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.ts create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-header/marketplace-show-header.component.html create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-header/marketplace-show-header.component.ts create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.html create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts delete mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/marketplace.service.ts create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes-header/release-notes-header.component.html create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes-header/release-notes-header.component.ts rename frontend/projects/ui/src/app/pages/marketplace-routes/{app-release-notes/app-release-notes.module.ts => release-notes/release-notes.module.ts} (55%) create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.page.html create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.page.scss create mode 100644 frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.page.ts create mode 100644 frontend/projects/ui/src/app/services/marketplace.service.ts diff --git a/frontend/angular.json b/frontend/angular.json index 30cee872d..d60e0a232 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -38,8 +38,9 @@ } ], "styles": [ - "styles/variables.scss", - "styles/global.scss", + "projects/shared/styles/variables.scss", + "projects/shared/styles/global.scss", + "projects/shared/styles/shared.scss", "projects/ui/src/styles.scss" ], "scripts": [] @@ -157,8 +158,8 @@ } ], "styles": [ - "styles/variables.scss", - "styles/global.scss", + "projects/shared/styles/variables.scss", + "projects/shared/styles/global.scss", "projects/setup-wizard/src/styles.scss" ], "scripts": [] @@ -276,8 +277,8 @@ } ], "styles": [ - "styles/variables.scss", - "styles/global.scss", + "projects/shared/styles/variables.scss", + "projects/shared/styles/global.scss", "projects/diagnostic-ui/src/styles.scss" ], "scripts": [] @@ -376,6 +377,29 @@ } } }, + "marketplace": { + "projectType": "library", + "root": "projects/marketplace", + "sourceRoot": "projects/marketplace/src", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:ng-packagr", + "options": { + "project": "projects/marketplace/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "projects/marketplace/tsconfig.prod.json" + }, + "development": { + "tsConfig": "projects/marketplace/tsconfig.json" + } + }, + "defaultConfiguration": "production" + } + } + }, "shared": { "projectType": "library", "root": "projects/shared", diff --git a/frontend/projects/marketplace/ng-package.json b/frontend/projects/marketplace/ng-package.json new file mode 100644 index 000000000..c38a11688 --- /dev/null +++ b/frontend/projects/marketplace/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/marketplace", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/frontend/projects/marketplace/package.json b/frontend/projects/marketplace/package.json new file mode 100644 index 000000000..04f3be2f4 --- /dev/null +++ b/frontend/projects/marketplace/package.json @@ -0,0 +1,13 @@ +{ + "name": "@start9labs/marketplace", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "^13.2.0", + "@angular/core": "^13.2.0", + "@start9labs/shared": "^0.0.1", + "fuse.js": "^6.4.6" + }, + "dependencies": { + "tslib": "^2.3.0" + } +} diff --git a/frontend/projects/marketplace/src/pipes/filter-packages.pipe.ts b/frontend/projects/marketplace/src/pipes/filter-packages.pipe.ts new file mode 100644 index 000000000..af8b9972d --- /dev/null +++ b/frontend/projects/marketplace/src/pipes/filter-packages.pipe.ts @@ -0,0 +1,65 @@ +import { Pipe, PipeTransform } from '@angular/core' +import Fuse from 'fuse.js/dist/fuse.min.js' + +import { LocalPkg } from '../types/local-pkg' +import { MarketplacePkg } from '../types/marketplace-pkg' + +const defaultOps = { + isCaseSensitive: false, + includeScore: true, + shouldSort: true, + includeMatches: false, + findAllMatches: false, + minMatchCharLength: 1, + location: 0, + threshold: 0.6, + distance: 100, + useExtendedSearch: false, + ignoreLocation: false, + ignoreFieldNorm: false, + keys: [ + 'manifest.id', + 'manifest.title', + 'manifest.description.short', + 'manifest.description.long', + ], +} + +@Pipe({ + name: 'filterPackages', +}) +export class FilterPackagesPipe implements PipeTransform { + transform( + packages: MarketplacePkg[] | null, + local: Record, + query: string, + category: string, + ): MarketplacePkg[] | null { + if (!packages) { + return null + } + + if (query) { + const fuse = new Fuse(packages, defaultOps) + + return fuse.search(query).map(p => p.item) + } + + if (category === 'updates') { + return packages.filter( + ({ manifest }) => + local[manifest.id] && + manifest.version !== local[manifest.id].manifest.version, + ) + } + + const pkgsToSort = packages.filter( + p => category === 'all' || p.categories.includes(category), + ) + const fuse = new Fuse(pkgsToSort, { ...defaultOps, threshold: 1 }) + + return fuse + .search(category !== 'all' ? category || '' : 'bit') + .map(p => p.item) + } +} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/pipes/install-progress.pipe.ts b/frontend/projects/marketplace/src/pipes/install-progress.pipe.ts similarity index 100% rename from frontend/projects/ui/src/app/pages/marketplace-routes/pipes/install-progress.pipe.ts rename to frontend/projects/marketplace/src/pipes/install-progress.pipe.ts diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/pipes/marketplace-pipes.module.ts b/frontend/projects/marketplace/src/pipes/marketplace-pipes.module.ts similarity index 50% rename from frontend/projects/ui/src/app/pages/marketplace-routes/pipes/marketplace-pipes.module.ts rename to frontend/projects/marketplace/src/pipes/marketplace-pipes.module.ts index 3c8659f10..bea2421d3 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/pipes/marketplace-pipes.module.ts +++ b/frontend/projects/marketplace/src/pipes/marketplace-pipes.module.ts @@ -1,9 +1,10 @@ import { NgModule } from '@angular/core' import { InstallProgressPipe } from './install-progress.pipe' import { TrustPipe } from './trust.pipe' +import { FilterPackagesPipe } from './filter-packages.pipe' @NgModule({ - declarations: [InstallProgressPipe, TrustPipe], - exports: [InstallProgressPipe, TrustPipe], + declarations: [InstallProgressPipe, TrustPipe, FilterPackagesPipe], + exports: [InstallProgressPipe, TrustPipe, FilterPackagesPipe], }) export class MarketplacePipesModule {} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/pipes/trust.pipe.ts b/frontend/projects/marketplace/src/pipes/trust.pipe.ts similarity index 100% rename from frontend/projects/ui/src/app/pages/marketplace-routes/pipes/trust.pipe.ts rename to frontend/projects/marketplace/src/pipes/trust.pipe.ts diff --git a/frontend/projects/marketplace/src/public-api.ts b/frontend/projects/marketplace/src/public-api.ts new file mode 100644 index 000000000..cf9062ed5 --- /dev/null +++ b/frontend/projects/marketplace/src/public-api.ts @@ -0,0 +1,17 @@ +/* + * Public API Surface of @start9labs/marketplace + */ + +export * from './pipes/install-progress.pipe' +export * from './pipes/trust.pipe' +export * from './pipes/marketplace-pipes.module' + +export * from './services/marketplace.service' + +export * from './types/local-pkg' +export * from './types/marketplace' +export * from './types/marketplace-data' +export * from './types/marketplace-manifest' +export * from './types/marketplace-pkg' + +export * from './utils/spread-progress' diff --git a/frontend/projects/marketplace/src/services/marketplace.service.ts b/frontend/projects/marketplace/src/services/marketplace.service.ts new file mode 100644 index 000000000..62c381b74 --- /dev/null +++ b/frontend/projects/marketplace/src/services/marketplace.service.ts @@ -0,0 +1,17 @@ +import { Observable } from 'rxjs' +import { MarketplacePkg } from '../types/marketplace-pkg' +import { Marketplace } from '../types/marketplace' + +export abstract class AbstractMarketplaceService { + abstract install(id: string, version?: string): Observable + + abstract getMarketplace(): Observable + + abstract getReleaseNotes(id: string): Observable> + + abstract getCategories(): Observable + + abstract getPackages(): Observable + + abstract getPackage(id: string, version: string): Observable +} diff --git a/frontend/projects/marketplace/src/types/local-pkg.ts b/frontend/projects/marketplace/src/types/local-pkg.ts new file mode 100644 index 000000000..6ccd43d3c --- /dev/null +++ b/frontend/projects/marketplace/src/types/local-pkg.ts @@ -0,0 +1,8 @@ +import { PackageState } from '@start9labs/shared' + +import { MarketplaceManifest } from './marketplace-manifest' + +export interface LocalPkg { + state: PackageState + manifest: MarketplaceManifest +} diff --git a/frontend/projects/marketplace/src/types/marketplace-data.ts b/frontend/projects/marketplace/src/types/marketplace-data.ts new file mode 100644 index 000000000..d2868b393 --- /dev/null +++ b/frontend/projects/marketplace/src/types/marketplace-data.ts @@ -0,0 +1,4 @@ +export interface MarketplaceData { + categories: string[] + name: string +} diff --git a/frontend/projects/marketplace/src/types/marketplace-manifest.ts b/frontend/projects/marketplace/src/types/marketplace-manifest.ts new file mode 100644 index 000000000..6e040efd4 --- /dev/null +++ b/frontend/projects/marketplace/src/types/marketplace-manifest.ts @@ -0,0 +1,25 @@ +import { Url } from '@start9labs/shared' + +export interface MarketplaceManifest { + id: string + title: string + version: string + description: { + short: string + long: string + } + 'release-notes': string + license: string // name + 'wrapper-repo': Url + 'upstream-repo': Url + 'support-site': Url + 'marketing-site': Url + 'donation-url': Url | null + alerts: { + install: string | null + uninstall: string | null + restore: string | null + start: string | null + stop: string | null + } +} diff --git a/frontend/projects/marketplace/src/types/marketplace-pkg.ts b/frontend/projects/marketplace/src/types/marketplace-pkg.ts new file mode 100644 index 000000000..259cd325e --- /dev/null +++ b/frontend/projects/marketplace/src/types/marketplace-pkg.ts @@ -0,0 +1,17 @@ +import { Url } from '@start9labs/shared' +import { MarketplaceManifest } from './marketplace-manifest' + +export interface MarketplacePkg { + icon: Url + license: Url + instructions: Url + manifest: MarketplaceManifest + categories: string[] + versions: string[] + 'dependency-metadata': { + [id: string]: { + title: string + icon: Url + } + } +} diff --git a/frontend/projects/marketplace/src/types/marketplace.ts b/frontend/projects/marketplace/src/types/marketplace.ts new file mode 100644 index 000000000..b610b83a0 --- /dev/null +++ b/frontend/projects/marketplace/src/types/marketplace.ts @@ -0,0 +1,4 @@ +export interface Marketplace { + url: string + name: string +} diff --git a/frontend/projects/marketplace/src/utils/spread-progress.ts b/frontend/projects/marketplace/src/utils/spread-progress.ts new file mode 100644 index 000000000..4347e1b08 --- /dev/null +++ b/frontend/projects/marketplace/src/utils/spread-progress.ts @@ -0,0 +1,5 @@ +import { LocalPkg } from '../types/local-pkg' + +export function spreadProgress(pkg: LocalPkg) { + pkg['install-progress'] = { ...pkg['install-progress'] } +} diff --git a/frontend/projects/marketplace/tsconfig.json b/frontend/projects/marketplace/tsconfig.json new file mode 100644 index 000000000..e3a6b521c --- /dev/null +++ b/frontend/projects/marketplace/tsconfig.json @@ -0,0 +1,13 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "../../out-tsc/lib", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": ["src/test.ts", "**/*.spec.ts"] +} diff --git a/frontend/projects/marketplace/tsconfig.prod.json b/frontend/projects/marketplace/tsconfig.prod.json new file mode 100644 index 000000000..06de549e1 --- /dev/null +++ b/frontend/projects/marketplace/tsconfig.prod.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/frontend/projects/setup-wizard/src/app/app.component.ts b/frontend/projects/setup-wizard/src/app/app.component.ts index 071c4788c..e5fbebf49 100644 --- a/frontend/projects/setup-wizard/src/app/app.component.ts +++ b/frontend/projects/setup-wizard/src/app/app.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core' import { NavController } from '@ionic/angular' import { ApiService } from './services/api/api.service' -import { ErrorToastService } from './services/error-toast.service' +import { ErrorToastService } from '@start9labs/shared' import { StateService } from './services/state.service' @Component({ @@ -30,7 +30,7 @@ export class AppComponent { await this.navCtrl.navigateForward(`/recover`) } } catch (e) { - this.errorToastService.present(e.message) + this.errorToastService.present(e) } } } diff --git a/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts b/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts index 46167230f..ee77a1f9a 100644 --- a/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts @@ -10,7 +10,7 @@ import { DiskInfo, DiskRecoverySource, } from 'src/app/services/api/api.service' -import { ErrorToastService } from 'src/app/services/error-toast.service' +import { ErrorToastService } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' import { PasswordPage } from '../../modals/password/password.page' @@ -74,7 +74,7 @@ export class EmbassyPage { await alert.present() } } catch (e) { - this.errorToastService.present(e.message) + this.errorToastService.present(e) } finally { this.loading = false } @@ -142,9 +142,9 @@ export class EmbassyPage { await this.navCtrl.navigateForward(`/success`) } } catch (e) { - this.errorToastService.present( - `${e.message}\n\nRestart Embassy to try again.`, - ) + this.errorToastService.present({ + message: `${e.message}\n\nRestart Embassy to try again.`, + }) console.error(e) } finally { loader.dismiss() diff --git a/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts b/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts index 3f0ca1e98..6906e46c6 100644 --- a/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts @@ -8,7 +8,7 @@ import { } from '@ionic/angular' import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page' import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service' -import { ErrorToastService } from 'src/app/services/error-toast.service' +import { ErrorToastService } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' import { PasswordPage } from '../../modals/password/password.page' import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page' @@ -120,7 +120,7 @@ export class RecoverPage { this.hasShownGuidAlert = true } } catch (e) { - this.errorToastService.present(e.message) + this.errorToastService.present(e) } finally { this.loading = false } @@ -206,7 +206,7 @@ export class RecoverPage { await this.stateService.importDrive(guid) await this.navCtrl.navigateForward(`/success`) } catch (e) { - this.errorToastService.present(e.message) + this.errorToastService.present(e) } finally { loader.dismiss() } diff --git a/frontend/projects/setup-wizard/src/app/pages/success/success.page.ts b/frontend/projects/setup-wizard/src/app/pages/success/success.page.ts index 5f3c596e5..ad087b2d4 100644 --- a/frontend/projects/setup-wizard/src/app/pages/success/success.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/success/success.page.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Output } from '@angular/core' import { ToastController } from '@ionic/angular' -import { ErrorToastService } from 'src/app/services/error-toast.service' +import { ErrorToastService } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' @Component({ diff --git a/frontend/projects/setup-wizard/src/app/services/error-toast.service.ts b/frontend/projects/setup-wizard/src/app/services/error-toast.service.ts deleted file mode 100644 index 81ef97265..000000000 --- a/frontend/projects/setup-wizard/src/app/services/error-toast.service.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Injectable } from '@angular/core' -import { ToastController } from '@ionic/angular' - -@Injectable({ - providedIn: 'root', -}) -export class ErrorToastService { - private toast: HTMLIonToastElement - - constructor ( - private readonly toastCtrl: ToastController, - ) { } - - async present (message: string): Promise { - if (this.toast) return - - this.toast = await this.toastCtrl.create({ - header: 'Error', - message, - duration: 0, - position: 'top', - cssClass: 'error-toast', - animated: true, - buttons: [ - { - side: 'end', - icon: 'close', - handler: () => { - this.dismiss() - }, - }, - ], - }) - await this.toast.present() - } - - async dismiss (): Promise { - if (this.toast) { - await this.toast.dismiss() - this.toast = undefined - } - } -} \ No newline at end of file diff --git a/frontend/projects/setup-wizard/src/app/services/state.service.ts b/frontend/projects/setup-wizard/src/app/services/state.service.ts index d79dd18ad..9d6aa9cad 100644 --- a/frontend/projects/setup-wizard/src/app/services/state.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/state.service.ts @@ -5,8 +5,7 @@ import { CifsRecoverySource, DiskRecoverySource, } from './api/api.service' -import { ErrorToastService } from './error-toast.service' -import { pauseFor } from '@start9labs/shared' +import { pauseFor, ErrorToastService } from '@start9labs/shared' @Injectable({ providedIn: 'root', @@ -51,9 +50,9 @@ export class StateService { try { progress = await this.apiService.getRecoveryStatus() } catch (e) { - this.errorToastService.present( - `${e.message}\n\nRestart Embassy to try again.`, - ) + this.errorToastService.present({ + message: `${e.message}\n\nRestart Embassy to try again.`, + }) } if (progress) { this.dataTransferProgress = { diff --git a/frontend/projects/shared/ng-package.json b/frontend/projects/shared/ng-package.json index 1aed3740f..d45aa83d9 100644 --- a/frontend/projects/shared/ng-package.json +++ b/frontend/projects/shared/ng-package.json @@ -1,6 +1,7 @@ { "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", "dest": "../../dist/shared", + "assets": ["styles"], "lib": { "entryFile": "src/public-api.ts" } diff --git a/frontend/projects/shared/package.json b/frontend/projects/shared/package.json index aa0b3923a..2154e301d 100644 --- a/frontend/projects/shared/package.json +++ b/frontend/projects/shared/package.json @@ -4,6 +4,7 @@ "peerDependencies": { "@angular/common": "^13.2.0", "@angular/core": "^13.2.0", + "@ionic/angular": "^6.0.3", "@start9labs/emver": "^0.1.5" }, "dependencies": { diff --git a/frontend/projects/shared/src/directives/element/element.directive.ts b/frontend/projects/shared/src/directives/element/element.directive.ts new file mode 100644 index 000000000..599d1ab16 --- /dev/null +++ b/frontend/projects/shared/src/directives/element/element.directive.ts @@ -0,0 +1,11 @@ +import { Directive, ElementRef, Inject } from '@angular/core' + +@Directive({ + selector: '[elementRef]', + exportAs: 'elementRef', +}) +export class ElementDirective extends ElementRef { + constructor(@Inject(ElementRef) { nativeElement }: ElementRef) { + super(nativeElement) + } +} diff --git a/frontend/projects/shared/src/directives/element/element.module.ts b/frontend/projects/shared/src/directives/element/element.module.ts new file mode 100644 index 000000000..85601d56a --- /dev/null +++ b/frontend/projects/shared/src/directives/element/element.module.ts @@ -0,0 +1,9 @@ +import { NgModule } from '@angular/core' + +import { ElementDirective } from './element.directive' + +@NgModule({ + declarations: [ElementDirective], + exports: [ElementDirective], +}) +export class ElementModule {} diff --git a/frontend/projects/shared/src/public-api.ts b/frontend/projects/shared/src/public-api.ts index dcb00fae0..81b5486b9 100644 --- a/frontend/projects/shared/src/public-api.ts +++ b/frontend/projects/shared/src/public-api.ts @@ -5,6 +5,9 @@ export * from './components/text-spinner/text-spinner.component.module' export * from './components/text-spinner/text-spinner.component' +export * from './directives/element/element.directive' +export * from './directives/element/element.module' + export * from './pipes/emver/emver.module' export * from './pipes/emver/emver.pipe' export * from './pipes/markdown/markdown.module' @@ -17,11 +20,13 @@ export * from './pipes/unit-conversion/unit-conversion.pipe' export * from './services/destroy.service' export * from './services/emver.service' +export * from './services/error-toast.service' export * from './types/dependent-info' export * from './types/install-progress' export * from './types/package-state' export * from './types/progress-data' +export * from './types/url' export * from './types/workspace-config' export * from './util/misc.util' diff --git a/frontend/projects/ui/src/app/services/error-toast.service.ts b/frontend/projects/shared/src/services/error-toast.service.ts similarity index 84% rename from frontend/projects/ui/src/app/services/error-toast.service.ts rename to frontend/projects/shared/src/services/error-toast.service.ts index 92970cd0c..e960713ac 100644 --- a/frontend/projects/ui/src/app/services/error-toast.service.ts +++ b/frontend/projects/shared/src/services/error-toast.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@angular/core' import { IonicSafeString, ToastController } from '@ionic/angular' -import { RequestError } from './http.service' @Injectable({ providedIn: 'root', @@ -10,7 +9,7 @@ export class ErrorToastService { constructor(private readonly toastCtrl: ToastController) {} - async present(e: RequestError, link?: string): Promise { + async present(e: { message: string }, link?: string): Promise { console.error(e) if (this.toast) return @@ -43,18 +42,16 @@ export class ErrorToastService { } export function getErrorMessage( - e: RequestError, + { message }: { message: string }, link?: string, ): string | IonicSafeString { - let message: string | IonicSafeString = e.message - if (!message) { message = 'Unknown Error.' link = 'https://start9.com/latest/support/FAQ' } if (link) { - message = new IonicSafeString( + return new IonicSafeString( `${message}

Get Help`, ) } diff --git a/frontend/projects/shared/src/types/url.ts b/frontend/projects/shared/src/types/url.ts new file mode 100644 index 000000000..41175d51a --- /dev/null +++ b/frontend/projects/shared/src/types/url.ts @@ -0,0 +1 @@ +export type Url = string diff --git a/frontend/projects/shared/src/util/misc.util.ts b/frontend/projects/shared/src/util/misc.util.ts index 94ac4b934..199e9572f 100644 --- a/frontend/projects/shared/src/util/misc.util.ts +++ b/frontend/projects/shared/src/util/misc.util.ts @@ -56,8 +56,7 @@ export function isObject(val: any): boolean { } export function isEmptyObject(obj: object): boolean { - if (obj === undefined) return true - return !Object.keys(obj).length + return obj === undefined || !Object.keys(obj).length } export function pauseFor(ms: number): Promise { diff --git a/frontend/styles/global.scss b/frontend/projects/shared/styles/global.scss similarity index 55% rename from frontend/styles/global.scss rename to frontend/projects/shared/styles/global.scss index e8ec3b1fc..d854de84a 100644 --- a/frontend/styles/global.scss +++ b/frontend/projects/shared/styles/global.scss @@ -24,53 +24,3 @@ @import "~@ionic/angular/css/text-alignment.css"; @import "~@ionic/angular/css/text-transformation.css"; @import "~@ionic/angular/css/flex-utils.css"; - -ion-input { - caret-color: gray; -} - -.item-interactive { - --highlight-background: var(--ion-color-light) !important; -} - -ion-modal::part(content) { - position: absolute; - height: 90% !important; - top: 5%; - width: 90% !important; - left: 5%; - display: block; - - border-radius: 6px; - border: 2px solid rgba(255,255,255,.03); - box-shadow: 0 0 70px 70px black; -} - -@media (min-width:1000px) { - ion-modal::part(content) { - position: absolute; - height: 80% !important; - top: 10%; - width: 50% !important; - left: 25%; - } -} - -.alertlike-modal { - &::part(content) { - max-height: 380px !important; - top: 25% !important; - width: 90% !important; - left: 5% !important; - --box-shadow: none !important; - } -} - -@media (min-width:1000px) { - .alertlike-modal { - &::part(content) { - width: 60% !important; - left: 20% !important; - } - } -} diff --git a/frontend/projects/shared/styles/shared.scss b/frontend/projects/shared/styles/shared.scss new file mode 100644 index 000000000..0d921e7dc --- /dev/null +++ b/frontend/projects/shared/styles/shared.scss @@ -0,0 +1,85 @@ +ion-input { + caret-color: gray; +} + +ion-modal::part(content) { + position: absolute; + height: 90% !important; + top: 5%; + width: 90% !important; + left: 5%; + display: block; + + border-radius: 6px; + border: 2px solid rgba(255, 255, 255, 0.03); + box-shadow: 0 0 70px 70px black; +} + +@media (min-width: 1000px) { + ion-modal::part(content) { + position: absolute; + height: 80% !important; + top: 10%; + width: 50% !important; + left: 25%; + } +} + +.alertlike-modal { + &::part(content) { + max-height: 380px !important; + top: 25% !important; + width: 90% !important; + left: 5% !important; + --box-shadow: none !important; + } +} + +@media (min-width: 1000px) { + .alertlike-modal { + &::part(content) { + width: 60% !important; + left: 20% !important; + } + } +} + +.item-interactive { + --highlight-background: var(--ion-color-light) !important; +} + +.hidden-scrollbar { + overflow: auto; + white-space: nowrap; + height: 60px; + + /* Hide scrollbar for Chrome, Safari and Opera */ + &::-webkit-scrollbar { + width: 0; + height: 0; + } + + /* Hide scrollbar for IE, Edge and Firefox */ + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +.divider { + background: linear-gradient( + 90deg, + var(--ion-color-light) 0, + var(--ion-color-dark) 50%, + var(--ion-color-light) 100% + ); + height: 1px; +} + +.loading-dots:after { + content: '...'; + overflow: hidden; + display: inline-block; + vertical-align: bottom; + animation: ellipsis-dot 1s infinite 0.3s; + animation-fill-mode: forwards; + width: 1em; +} diff --git a/frontend/styles/variables.scss b/frontend/projects/shared/styles/variables.scss similarity index 100% rename from frontend/styles/variables.scss rename to frontend/projects/shared/styles/variables.scss diff --git a/frontend/projects/ui/src/app/app.component.ts b/frontend/projects/ui/src/app/app.component.ts index ce08a3566..e5350cf48 100644 --- a/frontend/projects/ui/src/app/app.component.ts +++ b/frontend/projects/ui/src/app/app.component.ts @@ -1,14 +1,5 @@ -import { Component, HostListener, NgZone } from '@angular/core' -import { Storage } from '@ionic/storage-angular' -import { AuthService, AuthState } from './services/auth.service' -import { ApiService } from './services/api/embassy-api.service' +import { Component, HostListener, Inject, NgZone } from '@angular/core' import { Router, RoutesRecognized } from '@angular/router' -import { - debounceTime, - distinctUntilChanged, - filter, - take, -} from 'rxjs/operators' import { AlertController, IonicSafeString, @@ -16,21 +7,33 @@ import { ModalController, ToastController, } from '@ionic/angular' -import { SplitPaneTracker } from './services/split-pane.service' import { ToastButton } from '@ionic/core' +import { Storage } from '@ionic/storage-angular' +import { + debounce, + isEmptyObject, + Emver, + ErrorToastService, +} from '@start9labs/shared' +import { Subscription } from 'rxjs' +import { + debounceTime, + distinctUntilChanged, + filter, + take, +} from 'rxjs/operators' +import { AuthService, AuthState } from './services/auth.service' +import { ApiService } from './services/api/embassy-api.service' +import { SplitPaneTracker } from './services/split-pane.service' import { PatchDbService } from './services/patch-db/patch-db.service' import { ConnectionFailure, ConnectionService, } from './services/connection.service' import { ConfigService } from './services/config.service' -import { debounce, isEmptyObject, Emver } from '@start9labs/shared' import { ServerStatus, UIData } from 'src/app/services/patch-db/data-model' -import { ErrorToastService } from './services/error-toast.service' -import { Subscription } from 'rxjs' import { LocalStorageService } from './services/local-storage.service' import { EOSService } from './services/eos.service' -import { MarketplaceService } from './pages/marketplace-routes/marketplace.service' import { OSWelcomePage } from './modals/os-welcome/os-welcome.page' import { SnakePage } from './modals/snake/snake.page' @@ -128,7 +131,6 @@ export class AppComponent { private readonly emver: Emver, private readonly connectionService: ConnectionService, private readonly modalCtrl: ModalController, - private readonly marketplaceService: MarketplaceService, private readonly toastCtrl: ToastController, private readonly errToast: ErrorToastService, private readonly config: ConfigService, @@ -190,8 +192,6 @@ export class AppComponent { this.watchVersion(), // watch unread notification count to display toast this.watchNotifications(), - // watch marketplace URL for changes - this.marketplaceService.init(), ]) }) // UNVERIFIED diff --git a/frontend/projects/ui/src/app/app.module.ts b/frontend/projects/ui/src/app/app.module.ts index 1924b041f..a0416e613 100644 --- a/frontend/projects/ui/src/app/app.module.ts +++ b/frontend/projects/ui/src/app/app.module.ts @@ -23,6 +23,7 @@ import { MockApiService } from './services/api/embassy-mock-api.service' import { LiveApiService } from './services/api/embassy-live-api.service' import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor' import { SharedPipesModule, WorkspaceConfig } from '@start9labs/shared' +import { MarketplaceModule } from './marketplace.module' const { useMocks } = require('../../../../config.json') as WorkspaceConfig @@ -48,6 +49,7 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig GenericInputComponentModule, MonacoEditorModule, SharedPipesModule, + MarketplaceModule, ], providers: [ FormBuilder, @@ -79,4 +81,4 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig bootstrap: [AppComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) -export class AppModule { } +export class AppModule {} diff --git a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts index 4646c6087..bfdd17293 100644 --- a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts +++ b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts @@ -14,7 +14,7 @@ import { import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' import { ConfigSpec } from 'src/app/pkg-config/config-types' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService } from 'src/app/services/error-toast.service' +import { ErrorToastService } from '@start9labs/shared' import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' @Component({ diff --git a/frontend/projects/ui/src/app/components/backup-drives/backup.service.ts b/frontend/projects/ui/src/app/components/backup-drives/backup.service.ts index 3fde8693d..c50dbb5bf 100644 --- a/frontend/projects/ui/src/app/components/backup-drives/backup.service.ts +++ b/frontend/projects/ui/src/app/components/backup-drives/backup.service.ts @@ -1,14 +1,13 @@ import { Injectable } from '@angular/core' import { IonicSafeString } from '@ionic/core' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { getErrorMessage } from 'src/app/services/error-toast.service' import { BackupTarget, CifsBackupTarget, DiskBackupTarget, } from 'src/app/services/api/api.types' import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' -import { Emver } from '@start9labs/shared' +import { getErrorMessage, Emver } from '@start9labs/shared' @Injectable({ providedIn: 'root', diff --git a/frontend/projects/ui/src/app/components/install-wizard/prebaked-wizards.ts b/frontend/projects/ui/src/app/components/install-wizard/prebaked-wizards.ts index 02f578464..7febb4520 100644 --- a/frontend/projects/ui/src/app/components/install-wizard/prebaked-wizards.ts +++ b/frontend/projects/ui/src/app/components/install-wizard/prebaked-wizards.ts @@ -1,7 +1,8 @@ -import { Injectable } from '@angular/core' +import { Inject, Injectable } from '@angular/core' +import { exists } from '@start9labs/shared' +import { AbstractMarketplaceService } from '@start9labs/marketplace' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { Breakages } from 'src/app/services/api/api.types' -import { exists } from '@start9labs/shared' import { ApiService } from '../../services/api/embassy-api.service' import { InstallWizardComponent, @@ -9,13 +10,14 @@ import { TopbarParams, } from './install-wizard.component' import { ConfigService } from 'src/app/services/config.service' -import { MarketplaceService } from 'src/app/pages/marketplace-routes/marketplace.service' +import { MarketplaceService } from 'src/app/services/marketplace.service' @Injectable({ providedIn: 'root' }) export class WizardBaker { constructor( private readonly embassyApi: ApiService, private readonly config: ConfigService, + @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, ) {} @@ -77,10 +79,12 @@ export class WizardBaker { verb: 'beginning update for', title, executeAction: () => - this.marketplaceService.installPackage({ - id, - 'version-spec': version ? `=${version}` : undefined, - }), + this.marketplaceService + .installPackage({ + id, + 'version-spec': version ? `=${version}` : undefined, + }) + .toPromise(), }, }, bottomBar: { @@ -202,10 +206,12 @@ export class WizardBaker { verb: 'beginning downgrade for', title, executeAction: () => - this.marketplaceService.installPackage({ - id, - 'version-spec': version ? `=${version}` : undefined, - }), + this.marketplaceService + .installPackage({ + id, + 'version-spec': version ? `=${version}` : undefined, + }) + .toPromise(), }, }, bottomBar: { diff --git a/frontend/projects/ui/src/app/components/logs/logs.page.ts b/frontend/projects/ui/src/app/components/logs/logs.page.ts index 41da8652c..91e9df532 100644 --- a/frontend/projects/ui/src/app/components/logs/logs.page.ts +++ b/frontend/projects/ui/src/app/components/logs/logs.page.ts @@ -1,6 +1,6 @@ import { Component, Input, ViewChild } from '@angular/core' import { IonContent } from '@ionic/angular' -import { ErrorToastService } from 'src/app/services/error-toast.service' +import { ErrorToastService } from '@start9labs/shared' import { RR } from 'src/app/services/api/api.types' var Convert = require('ansi-to-html') var convert = new Convert({ @@ -14,7 +14,11 @@ var convert = new Convert({ }) export class LogsPage { @ViewChild(IonContent) private content: IonContent - @Input() fetchLogs: (params: { before_flag?: boolean, limit?: number, cursor?: string }) => Promise + @Input() fetchLogs: (params: { + before_flag?: boolean + limit?: number + cursor?: string + }) => Promise loading = true loadingMore = false logs: string @@ -25,15 +29,13 @@ export class LogsPage { scrollToBottomButton = false isOnBottom = true - constructor ( - private readonly errToast: ErrorToastService, - ) { } + constructor(private readonly errToast: ErrorToastService) {} - ngOnInit () { + ngOnInit() { this.getLogs() } - async fetch (isBefore: boolean = true) { + async fetch(isBefore: boolean = true) { try { const cursor = isBefore ? this.startCursor : this.endCursor const logsRes = await this.fetchLogs({ @@ -57,7 +59,7 @@ export class LogsPage { } } - async getLogs () { + async getLogs() { try { // get logs const logs = await this.fetch() @@ -65,47 +67,60 @@ export class LogsPage { const container = document.getElementById('container') const beforeContainerHeight = container.scrollHeight - const newLogs = document.getElementById('template').cloneNode(true) as HTMLElement - newLogs.innerHTML = logs.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`).join('\n') + (logs.length ? '\n' : '') + const newLogs = document + .getElementById('template') + .cloneNode(true) as HTMLElement + newLogs.innerHTML = + logs + .map(l => `${l.timestamp} ${convert.toHtml(l.message)}`) + .join('\n') + (logs.length ? '\n' : '') container.prepend(newLogs) const afterContainerHeight = container.scrollHeight // scroll down scrollBy(0, afterContainerHeight - beforeContainerHeight) - this.content.scrollToPoint(0, afterContainerHeight - beforeContainerHeight) + this.content.scrollToPoint( + 0, + afterContainerHeight - beforeContainerHeight, + ) if (logs.length < this.limit) { this.needInfinite = false } - - } catch (e) { } + } catch (e) {} } - async loadMore () { + async loadMore() { try { this.loadingMore = true const logs = await this.fetch(false) - if (!logs.length) return this.loadingMore = false + if (!logs.length) return (this.loadingMore = false) const container = document.getElementById('container') - const newLogs = document.getElementById('template').cloneNode(true) as HTMLElement - newLogs.innerHTML = logs.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`).join('\n') + (logs.length ? '\n' : '') + const newLogs = document + .getElementById('template') + .cloneNode(true) as HTMLElement + newLogs.innerHTML = + logs + .map(l => `${l.timestamp} ${convert.toHtml(l.message)}`) + .join('\n') + (logs.length ? '\n' : '') container.append(newLogs) this.loadingMore = false this.scrollEvent() - } catch (e) { } + } catch (e) {} } - scrollEvent () { + scrollEvent() { const buttonDiv = document.getElementById('button-div') - this.isOnBottom = buttonDiv && buttonDiv.getBoundingClientRect().top < window.innerHeight + this.isOnBottom = + buttonDiv && buttonDiv.getBoundingClientRect().top < window.innerHeight } - scrollToBottom () { + scrollToBottom() { this.content.scrollToBottom(500) } - async loadData (e: any): Promise { + async loadData(e: any): Promise { await this.getLogs() e.target.complete() } diff --git a/frontend/projects/ui/src/app/marketplace.module.ts b/frontend/projects/ui/src/app/marketplace.module.ts new file mode 100644 index 000000000..0a33eec32 --- /dev/null +++ b/frontend/projects/ui/src/app/marketplace.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core' +import { AbstractMarketplaceService } from '@start9labs/marketplace' +import { MarketplaceService } from 'src/app/services/marketplace.service' + +@NgModule({ + providers: [ + { + provide: AbstractMarketplaceService, + useClass: MarketplaceService, + }, + ], +}) +export class MarketplaceModule {} diff --git a/frontend/projects/ui/src/app/modals/app-config/app-config.page.ts b/frontend/projects/ui/src/app/modals/app-config/app-config.page.ts index 609acc4ac..48b7fcc11 100644 --- a/frontend/projects/ui/src/app/modals/app-config/app-config.page.ts +++ b/frontend/projects/ui/src/app/modals/app-config/app-config.page.ts @@ -7,16 +7,18 @@ import { IonicSafeString, } from '@ionic/angular' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { DependentInfo, isEmptyObject, isObject } from '@start9labs/shared' +import { + ErrorToastService, + getErrorMessage, + DependentInfo, + isEmptyObject, + isObject, +} from '@start9labs/shared' import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' import { ConfigSpec } from 'src/app/pkg-config/config-types' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' -import { - ErrorToastService, - getErrorMessage, -} from 'src/app/services/error-toast.service' import { FormGroup } from '@angular/forms' import { convertValuesRecursive, diff --git a/frontend/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts b/frontend/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts index 73b7fa7ad..61265b017 100644 --- a/frontend/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts +++ b/frontend/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts @@ -7,8 +7,7 @@ import { import { BackupInfo, PackageBackupInfo } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ConfigService } from 'src/app/services/config.service' -import { Emver } from '@start9labs/shared' -import { getErrorMessage } from 'src/app/services/error-toast.service' +import { getErrorMessage, Emver } from '@start9labs/shared' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' @Component({ diff --git a/frontend/projects/ui/src/app/modals/generic-input/generic-input.component.ts b/frontend/projects/ui/src/app/modals/generic-input/generic-input.component.ts index 2bb205611..aaa4b3109 100644 --- a/frontend/projects/ui/src/app/modals/generic-input/generic-input.component.ts +++ b/frontend/projects/ui/src/app/modals/generic-input/generic-input.component.ts @@ -1,6 +1,6 @@ import { Component, Input, ViewChild } from '@angular/core' import { ModalController, IonicSafeString, IonInput } from '@ionic/angular' -import { getErrorMessage } from 'src/app/services/error-toast.service' +import { getErrorMessage } from '@start9labs/shared' @Component({ selector: 'generic-input', @@ -14,11 +14,9 @@ export class GenericInputComponent { unmasked = false error: string | IonicSafeString - constructor ( - private readonly modalCtrl: ModalController, - ) { } + constructor(private readonly modalCtrl: ModalController) {} - ngOnInit () { + ngOnInit() { const defaultOptions: Partial = { buttonText: 'Submit', placeholder: 'Enter value', @@ -34,19 +32,19 @@ export class GenericInputComponent { this.value = this.options.initialValue } - ngAfterViewInit () { + ngAfterViewInit() { setTimeout(() => this.elem.setFocus(), 400) } - toggleMask () { + toggleMask() { this.unmasked = !this.unmasked } - cancel () { + cancel() { this.modalCtrl.dismiss() } - async submit () { + async submit() { const value = this.value.trim() if (!value && !this.options.nullable) return diff --git a/frontend/projects/ui/src/app/modals/markdown/markdown.page.ts b/frontend/projects/ui/src/app/modals/markdown/markdown.page.ts index e185272dd..0cc06dd1c 100644 --- a/frontend/projects/ui/src/app/modals/markdown/markdown.page.ts +++ b/frontend/projects/ui/src/app/modals/markdown/markdown.page.ts @@ -1,8 +1,7 @@ import { Component, Input } from '@angular/core' import { ModalController, IonicSafeString } from '@ionic/angular' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { getErrorMessage } from 'src/app/services/error-toast.service' -import { pauseFor } from '../../../../../shared/src/util/misc.util' +import { getErrorMessage, pauseFor } from '@start9labs/shared' @Component({ selector: 'markdown', diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts index b4032374a..be9458d6b 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts @@ -18,8 +18,7 @@ import { wizardModal } from 'src/app/components/install-wizard/install-wizard.co import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' import { Subscription } from 'rxjs' import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' -import { ErrorToastService } from 'src/app/services/error-toast.service' -import { isEmptyObject } from '@start9labs/shared' +import { isEmptyObject, ErrorToastService } from '@start9labs/shared' import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page' @Component({ diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list-rec/app-list-rec.component.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list-rec/app-list-rec.component.ts index 2e9006088..0c1a5fa37 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list-rec/app-list-rec.component.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list-rec/app-list-rec.component.ts @@ -2,16 +2,18 @@ import { ChangeDetectionStrategy, Component, EventEmitter, + Inject, Input, Output, } from '@angular/core' import { AlertController } from '@ionic/angular' +import { ErrorToastService } from '@start9labs/shared' +import { AbstractMarketplaceService } from '@start9labs/marketplace' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService } from 'src/app/services/error-toast.service' import { from, merge, OperatorFunction, pipe, Subject } from 'rxjs' import { catchError, mapTo, startWith, switchMap, tap } from 'rxjs/operators' import { RecoveredInfo } from 'src/app/util/parse-data-model' -import { MarketplaceService } from 'src/app/pages/marketplace-routes/marketplace.service' +import { MarketplaceService } from 'src/app/services/marketplace.service' @Component({ selector: 'app-list-rec', @@ -33,17 +35,14 @@ export class AppListRecComponent { readonly installing$ = this.install$.pipe( switchMap(({ id, version }) => // Mapping each installation to API request - from( - this.marketplaceService.installPackage({ - id, - 'version-spec': `>=${version}`, - 'version-priority': 'min', - }), - ).pipe( - // Mapping operation to true/false loading indication - loading(this.errToast), - ), + this.marketplaceService.installPackage({ + id, + 'version-spec': `>=${version}`, + 'version-priority': 'min', + }), ), + // Mapping operation to true/false loading indication + loading(this.errToast), ) // Deleting package @@ -66,6 +65,7 @@ export class AppListRecComponent { private readonly api: ApiService, private readonly errToast: ErrorToastService, private readonly alertCtrl: AlertController, + @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, ) {} diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts index 8cea346ff..41e9e7c09 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts @@ -4,9 +4,8 @@ import { IonContent } from '@ionic/angular' import { Subscription } from 'rxjs' import { Metric } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService } from 'src/app/services/error-toast.service' import { MainStatus } from 'src/app/services/patch-db/data-model' -import { pauseFor } from '@start9labs/shared' +import { pauseFor, ErrorToastService } from '@start9labs/shared' @Component({ selector: 'app-metrics', diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts index 949393d32..8b6e91ca6 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts @@ -15,7 +15,7 @@ import { PackageProperties } from 'src/app/util/properties.util' import { QRComponent } from 'src/app/components/qr/qr.component' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PackageMainStatus } from 'src/app/services/patch-db/data-model' -import { ErrorToastService } from 'src/app/services/error-toast.service' +import { ErrorToastService } from '@start9labs/shared' import { getValueByPointer } from 'fast-json-patch' @Component({ diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts index fea8943c5..70077795a 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts @@ -10,14 +10,17 @@ import { PackageDataEntry, Status, } from 'src/app/services/patch-db/data-model' -import { isEmptyObject, PackageState } from '@start9labs/shared' +import { + isEmptyObject, + ErrorToastService, + PackageState, +} from '@start9labs/shared' import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' import { AlertController, LoadingController, ModalController, } from '@ionic/angular' -import { ErrorToastService } from 'src/app/services/error-toast.service' import { ApiService } from 'src/app/services/api/embassy-api.service' import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts index e642c366d..816b63185 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts @@ -7,7 +7,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { debounce } from '../../../../../../shared/src/util/misc.util' import { GenericFormPage } from '../../../modals/generic-form/generic-form.page' -import { ErrorToastService } from '../../../services/error-toast.service' +import { ErrorToastService } from '@start9labs/shared' @Component({ selector: 'dev-config', diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts index be373843a..15cfc90ec 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts @@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router' import { ModalController } from '@ionic/angular' import { take } from 'rxjs/operators' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService } from 'src/app/services/error-toast.service' +import { ErrorToastService } from '@start9labs/shared' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { debounce } from '../../../../../../shared/src/util/misc.util' import { MarkdownPage } from '../../../modals/markdown/markdown.page' diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts index 1898e786b..636fcbb5e 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts @@ -17,9 +17,8 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types' import * as yaml from 'js-yaml' import { v4 } from 'uuid' import { DevData } from 'src/app/services/patch-db/data-model' -import { ErrorToastService } from 'src/app/services/error-toast.service' import { ActivatedRoute } from '@angular/router' -import { DestroyService } from '@start9labs/shared' +import { DestroyService, ErrorToastService } from '@start9labs/shared' import { takeUntil } from 'rxjs/operators' @Component({ diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts index 92d206981..16888c82d 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts @@ -5,7 +5,7 @@ import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' import { BasicInfo, getBasicInfoSpec } from './form-info' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService } from 'src/app/services/error-toast.service' +import { ErrorToastService } from '@start9labs/shared' import { takeUntil } from 'rxjs/operators' import { DevProjectData } from 'src/app/services/patch-db/data-model' import { DestroyService } from '../../../../../../shared/src/services/destroy.service' diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.html b/frontend/projects/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.html deleted file mode 100644 index c9573eb2e..000000000 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - Release Notes - - - - - - - -
- -

{{ note.key | displayEmver }}

-
- - - -
-
-
diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.scss b/frontend/projects/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.scss deleted file mode 100644 index e122c8b4b..000000000 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.scss +++ /dev/null @@ -1,8 +0,0 @@ -.panel { - margin: 0px; - padding: 0px 24px; -} - -.active { - border: 5px solid #4d4d4d; -} \ No newline at end of file diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.ts deleted file mode 100644 index b3b7c438d..000000000 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Component, ViewChild } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { IonContent } from '@ionic/angular' -import { ErrorToastService } from 'src/app/services/error-toast.service' -import { MarketplaceService } from '../marketplace.service' - -@Component({ - selector: 'app-release-notes', - templateUrl: './app-release-notes.page.html', - styleUrls: ['./app-release-notes.page.scss'], -}) -export class AppReleaseNotes { - @ViewChild(IonContent) content: IonContent - selected: string - pkgId: string - loading = true - - constructor( - private readonly route: ActivatedRoute, - public marketplaceService: MarketplaceService, - public errToast: ErrorToastService, - ) {} - - async ngOnInit() { - this.pkgId = this.route.snapshot.paramMap.get('pkgId') - try { - const promises = [] - if (!this.marketplaceService.releaseNotes[this.pkgId]) { - promises.push(this.marketplaceService.cacheReleaseNotes(this.pkgId)) - } - if (!this.marketplaceService.pkgs.length) { - promises.push(this.marketplaceService.load()) - } - await Promise.all(promises) - } catch (e) { - this.errToast.present(e) - } finally { - this.loading = false - } - } - - ngAfterViewInit() { - this.content.scrollToPoint(undefined, 1) - } - - setSelected(selected: string) { - if (this.selected === selected) { - this.selected = null - } else { - this.selected = selected - } - } - - getDocSize(selected: string) { - const element = document.getElementById(selected) - return `${element.scrollHeight}px` - } - - asIsOrder(a: any, b: any) { - return 0 - } -} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.html new file mode 100644 index 000000000..1d3dc074c --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.html @@ -0,0 +1,62 @@ +

{{ name }}

+ + + + + + + + + + + + +
+ + {{ cat }} + +
+ +
+ + +
+

All services are up to date!

+
+ + + + + + + + +

+ {{ pkg.manifest.title }} +

+

{{ pkg.manifest.description.short }}

+ +
+
+
+
+
+
+ + + + diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.scss b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.scss new file mode 100644 index 000000000..960ac8c80 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.scss @@ -0,0 +1,46 @@ +.heading { + font-family: 'Montserrat', sans-serif; + font-size: 42px; + margin: 32px 0; +} + +.divider { + margin: 24px; +} + +.ion-padding { + text-align: center; +} + +.grid { + padding-bottom: 32px; +} + +.pkg-title { + font-family: 'Montserrat', sans-serif; + font-weight: bold; +} + +.eos-item { + --border-style: none; + --background: linear-gradient( + 45deg, + var(--ion-color-dark) -380%, + var(--ion-color-medium) 100% + ); +} + +.category { + font-weight: 300; + color: var(--ion-color-dark-shade); + + &_selected { + font-weight: bold; + font-size: 17px; + color: var(--color); + } +} + +.status { + font-size: 14px; +} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.ts new file mode 100644 index 000000000..e9555f279 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-content/marketplace-list-content.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { LocalPkg, MarketplacePkg } from '@start9labs/marketplace' + +@Component({ + selector: 'marketplace-list-content', + templateUrl: 'marketplace-list-content.component.html', + styleUrls: ['./marketplace-list-content.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MarketplaceListContentComponent { + @Input() + pkgs: MarketplacePkg[] | null = null + + @Input() + localPkgs: Record = {} + + @Input() + categories: Set | null = null + + @Input() + name = '' + + category = 'featured' + query = '' + + isSelected(category: string) { + return category === this.category && !this.query + } + + switchCategory(category: string): void { + this.category = category + this.query = '' + } +} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-header/marketplace-list-header.component.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-header/marketplace-list-header.component.html new file mode 100644 index 000000000..33e24d7b9 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-header/marketplace-list-header.component.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-header/marketplace-list-header.component.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-header/marketplace-list-header.component.ts new file mode 100644 index 000000000..c13ea6f63 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-header/marketplace-list-header.component.ts @@ -0,0 +1,8 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' + +@Component({ + selector: 'marketplace-list-header', + templateUrl: 'marketplace-list-header.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MarketplaceListHeaderComponent {} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-skeleton/marketplace-list-skeleton.component.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-skeleton/marketplace-list-skeleton.component.html new file mode 100644 index 000000000..664f18626 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-skeleton/marketplace-list-skeleton.component.html @@ -0,0 +1,38 @@ +
+ + + +
+ +
+ + + + + + + + + + + + + + + + + diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-skeleton/marketplace-list-skeleton.component.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-skeleton/marketplace-list-skeleton.component.ts new file mode 100644 index 000000000..fe08787bf --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list-skeleton/marketplace-list-skeleton.component.ts @@ -0,0 +1,8 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' + +@Component({ + selector: 'marketplace-list-skeleton', + templateUrl: 'marketplace-list-skeleton.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MarketplaceListSkeletonComponent {} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts index 425c2ac03..32dd3b956 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts @@ -8,9 +8,14 @@ import { EmverPipesModule, TextSpinnerComponentModule, } from '@start9labs/shared' +import { MarketplacePipesModule } from '@start9labs/marketplace' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' -import { MarketplacePipesModule } from '../pipes/marketplace-pipes.module' + +import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module' import { MarketplaceListPage } from './marketplace-list.page' +import { MarketplaceListHeaderComponent } from './marketplace-list-header/marketplace-list-header.component' +import { MarketplaceListSkeletonComponent } from './marketplace-list-skeleton/marketplace-list-skeleton.component' +import { MarketplaceListContentComponent } from './marketplace-list-content/marketplace-list-content.component' const routes: Routes = [ { @@ -29,8 +34,20 @@ const routes: Routes = [ SharedPipesModule, EmverPipesModule, MarketplacePipesModule, + MarketplaceStatusModule, BadgeMenuComponentModule, ], - declarations: [MarketplaceListPage], + declarations: [ + MarketplaceListPage, + MarketplaceListHeaderComponent, + MarketplaceListContentComponent, + MarketplaceListSkeletonComponent, + ], + exports: [ + MarketplaceListPage, + MarketplaceListHeaderComponent, + MarketplaceListContentComponent, + MarketplaceListSkeletonComponent, + ], }) export class MarketplaceListPageModule {} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html index 2f6704847..67955c0bb 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html @@ -1,175 +1,15 @@ - - - - - - - + - - + - - -

- {{ marketplaceService.marketplace.name }} -

- - - - - - - - - - - - - -
- - - -
- -
-
- - - -
- - {{ cat }} - -
- -
-
- - - - - - - - - - - - - - - - - - - - - - - -
-

All services are up to date!

-
- - - - - - - - -

- {{ pkg.manifest.title }} -

-

{{ pkg.manifest.description.short }}

- -

- Installed - Update Available -

-

- - Installing - {{ progress }} - -

-

- - Removing - - -

-
- -

Not Installed

-
-
-
-
-
-
-
+ +
diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.scss b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.scss deleted file mode 100644 index cd586765d..000000000 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.scss +++ /dev/null @@ -1,30 +0,0 @@ -.scrollable { - overflow: auto; - white-space: nowrap; - // background-color: var(--ion-color-light); - height: 60px; - - /* Hide scrollbar for Chrome, Safari and Opera */ - ::-webkit-scrollbar { - display: none; - } - - /* Hide scrollbar for IE, Edge and Firefox */ - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ -} - -.eos-item { - --border-style: none; - --background: linear-gradient(45deg, var(--ion-color-dark) -380%, var(--ion-color-medium) 100%) -} - -.selected { - font-weight: bold; - font-size: 17px; -} - -.dim { - font-weight: 300; - color: var(--ion-color-dark-shade); -} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts index 91d3f164a..d5398d4a9 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts @@ -1,146 +1,53 @@ -import { Component, ViewChild } from '@angular/core' -import { MarketplacePkg } from 'src/app/services/api/api.types' -import { IonContent } from '@ionic/angular' -import { Subscription } from 'rxjs' -import { ErrorToastService } from 'src/app/services/error-toast.service' -import { MarketplaceService } from '../marketplace.service' -import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' -import Fuse from 'fuse.js/dist/fuse.min.js' -import { exists, isEmptyObject, PackageState } from '@start9labs/shared' -import { PackageDataEntry } from 'src/app/services/patch-db/data-model' -import { filter, first } from 'rxjs/operators' +import { Component } from '@angular/core' +import { defer, Observable } from 'rxjs' +import { filter, first, map, startWith, switchMapTo, tap } from 'rxjs/operators' +import { exists, isEmptyObject } from '@start9labs/shared' +import { + AbstractMarketplaceService, + LocalPkg, + MarketplacePkg, + spreadProgress, +} from '@start9labs/marketplace' -const defaultOps = { - isCaseSensitive: false, - includeScore: true, - shouldSort: true, - includeMatches: false, - findAllMatches: false, - minMatchCharLength: 1, - location: 0, - threshold: 0.6, - distance: 100, - useExtendedSearch: false, - ignoreLocation: false, - ignoreFieldNorm: false, - keys: [ - 'manifest.id', - 'manifest.title', - 'manifest.description.short', - 'manifest.description.long', - ], -} +import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' @Component({ selector: 'marketplace-list', templateUrl: './marketplace-list.page.html', - styleUrls: ['./marketplace-list.page.scss'], }) export class MarketplaceListPage { - PackageState = PackageState + readonly localPkgs$: Observable> = defer(() => + this.patch.watch$('package-data'), + ).pipe( + filter(data => exists(data) && !isEmptyObject(data)), + tap(pkgs => Object.values(pkgs).forEach(spreadProgress)), + startWith({}), + ) - @ViewChild(IonContent) content: IonContent + readonly categories$ = this.marketplaceService + .getCategories() + .pipe( + map(categories => new Set(['featured', 'updates', ...categories, 'all'])), + ) - pkgs: MarketplacePkg[] = [] - categories: string[] - localPkgs: Record = {} - category = 'featured' - query: string - loading = true + readonly pkgs$: Observable = defer(() => + this.patch.watch$('server-info'), + ).pipe( + filter(data => exists(data) && !isEmptyObject(data)), + first(), + switchMapTo(this.marketplaceService.getPackages()), + ) - subs: Subscription[] = [] + readonly name$: Observable = this.marketplaceService + .getMarketplace() + .pipe(map(({ name }) => name)) constructor( - private readonly errToast: ErrorToastService, - public readonly patch: PatchDbService, - public readonly marketplaceService: MarketplaceService, + private readonly patch: PatchDbService, + private readonly marketplaceService: AbstractMarketplaceService, ) {} - async ngOnInit() { - this.subs = [ - this.patch - .watch$('package-data') - .pipe(filter(data => exists(data) && !isEmptyObject(data))) - .subscribe(pkgs => { - this.localPkgs = pkgs - Object.values(this.localPkgs).forEach(pkg => { - pkg['install-progress'] = { ...pkg['install-progress'] } - }) - }), - ] - - this.patch - .watch$('server-info') - .pipe( - filter(data => exists(data) && !isEmptyObject(data)), - first(), - ) - .subscribe(async _ => { - try { - if (!this.marketplaceService.pkgs.length) { - await this.marketplaceService.load() - } - - // category should start as first item in array - // remove here then add at beginning - const filterdCategories = - this.marketplaceService.data.categories.filter( - cat => this.category !== cat, - ) - this.categories = [this.category, 'updates'] - .concat(filterdCategories) - .concat(['all']) - - this.filterPkgs() - } catch (e) { - this.errToast.present(e) - } finally { - this.loading = false - } - }) - } - - ngAfterViewInit() { - this.content.scrollToPoint(undefined, 1) - } - - ngOnDestroy() { - this.subs.forEach(sub => sub.unsubscribe()) - } - - search(): void { - if (this.query) { - this.category = undefined - } - this.filterPkgs() - } - - switchCategory(category: string): void { - this.category = category - this.query = undefined - this.filterPkgs() - } - - private filterPkgs(): void { - if (this.category === 'updates') { - this.pkgs = this.marketplaceService.pkgs.filter(pkg => { - const { id, version } = pkg.manifest - return ( - this.localPkgs[id] && version !== this.localPkgs[id].manifest.version - ) - }) - } else if (this.query) { - const fuse = new Fuse(this.marketplaceService.pkgs, defaultOps) - this.pkgs = fuse.search(this.query).map(p => p.item) - } else { - const pkgsToSort = this.marketplaceService.pkgs.filter(p => { - return this.category === 'all' || p.categories.includes(this.category) - }) - - const fuse = new Fuse(pkgsToSort, { ...defaultOps, threshold: 1 }) - this.pkgs = fuse - .search(this.category !== 'all' ? this.category || '' : 'bit') - .map(p => p.item) - } + get loaded(): boolean { + return this.patch.loaded } } diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-routing.module.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-routing.module.ts index ea4fa17b7..7e074fddf 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-routing.module.ts +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-routing.module.ts @@ -9,15 +9,24 @@ const routes: Routes = [ }, { path: 'browse', - loadChildren: () => import('./marketplace-list/marketplace-list.module').then(m => m.MarketplaceListPageModule), + loadChildren: () => + import('./marketplace-list/marketplace-list.module').then( + m => m.MarketplaceListPageModule, + ), }, { path: ':pkgId', - loadChildren: () => import('./marketplace-show/marketplace-show.module').then(m => m.MarketplaceShowPageModule), + loadChildren: () => + import('./marketplace-show/marketplace-show.module').then( + m => m.MarketplaceShowPageModule, + ), }, { path: ':pkgId/notes', - loadChildren: () => import('./app-release-notes/app-release-notes.module').then(m => m.ReleaseNotesModule), + loadChildren: () => + import('./release-notes/release-notes.module').then( + m => m.ReleaseNotesPageModule, + ), }, ] @@ -25,4 +34,4 @@ const routes: Routes = [ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) -export class MarketplaceRoutingModule { } \ No newline at end of file +export class MarketplaceRoutingModule {} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-about/marketplace-show-about.component.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-about/marketplace-show-about.component.html new file mode 100644 index 000000000..61fc0b24f --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-about/marketplace-show-about.component.html @@ -0,0 +1,23 @@ + + + New in {{ pkg.manifest.version | displayEmver }} + + All Release Notes + + + + + +
+
+
+ +Description + + +
{{ pkg.manifest.description.long }}
+
+
diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-about/marketplace-show-about.component.scss b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-about/marketplace-show-about.component.scss new file mode 100644 index 000000000..5cefebfe1 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-about/marketplace-show-about.component.scss @@ -0,0 +1,9 @@ +.all-notes { + position: absolute; + right: 10px; +} + +.release-notes { + overflow: auto; + max-height: 120px; +} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-about/marketplace-show-about.component.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-about/marketplace-show-about.component.ts new file mode 100644 index 000000000..257898dfa --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-about/marketplace-show-about.component.ts @@ -0,0 +1,13 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { MarketplacePkg } from '@start9labs/marketplace' + +@Component({ + selector: 'marketplace-show-about', + templateUrl: 'marketplace-show-about.component.html', + styleUrls: ['marketplace-show-about.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MarketplaceShowAboutComponent { + @Input() + pkg: MarketplacePkg +} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-additional/marketplace-show-additional.component.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-additional/marketplace-show-additional.component.html new file mode 100644 index 000000000..524952f40 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-additional/marketplace-show-additional.component.html @@ -0,0 +1,76 @@ +Additional Info + + + + + + + +

Other Versions

+

Click to view other versions

+
+ +
+ + +

License

+

{{ pkg.manifest.license }}

+
+ +
+ + +

Instructions

+

Click to view instructions

+
+ +
+
+
+ + + + +

Source Repository

+

{{ pkg.manifest['upstream-repo'] }}

+
+ +
+ + +

Wrapper Repository

+

{{ pkg.manifest['wrapper-repo'] }}

+
+ +
+ + +

Support Site

+

{{ pkg.manifest['support-site'] }}

+
+ +
+
+
+
+
+
diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-additional/marketplace-show-additional.component.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-additional/marketplace-show-additional.component.ts new file mode 100644 index 000000000..799696660 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-additional/marketplace-show-additional.component.ts @@ -0,0 +1,71 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core' +import { AlertController, ModalController } from '@ionic/angular' +import { MarketplacePkg } from '@start9labs/marketplace' +import { displayEmver, Emver } from '@start9labs/shared' + +import { MarkdownPage } from 'src/app/modals/markdown/markdown.page' + +@Component({ + selector: 'marketplace-show-additional', + templateUrl: 'marketplace-show-additional.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MarketplaceShowAdditionalComponent { + @Input() + pkg: MarketplacePkg + + @Output() + version = new EventEmitter() + + constructor( + private readonly alertCtrl: AlertController, + private readonly modalCtrl: ModalController, + private readonly emver: Emver, + ) {} + + async presentAlertVersions() { + const alert = await this.alertCtrl.create({ + header: 'Versions', + inputs: this.pkg.versions + .sort((a, b) => -1 * this.emver.compare(a, b)) + .map(v => ({ + name: v, // for CSS + type: 'radio', + label: displayEmver(v), // appearance on screen + value: v, // literal SEM version value + checked: this.pkg.manifest.version === v, + })), + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Ok', + handler: (version: string) => this.version.emit(version), + cssClass: 'enter-click', + }, + ], + }) + + await alert.present() + } + + async presentModalMd(title: string) { + const modal = await this.modalCtrl.create({ + componentProps: { + title, + contentUrl: `/marketplace${this.pkg[title]}`, + }, + component: MarkdownPage, + }) + + await modal.present() + } +} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html new file mode 100644 index 000000000..db4b41e77 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html @@ -0,0 +1,33 @@ + + + + + Update + + + Downgrade + + + + Reinstall + + + + + + + Install + diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts new file mode 100644 index 000000000..9a04022a1 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts @@ -0,0 +1,92 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { AlertController, ModalController, NavController } from '@ionic/angular' +import { + AbstractMarketplaceService, + MarketplacePkg, + LocalPkg, +} from '@start9labs/marketplace' +import { pauseFor, PackageState } from '@start9labs/shared' + +import { Manifest } from 'src/app/services/patch-db/data-model' +import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' +import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' +import { LocalStorageService } from 'src/app/services/local-storage.service' + +@Component({ + selector: 'marketplace-show-controls', + templateUrl: 'marketplace-show-controls.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MarketplaceShowControlsComponent { + @Input() + pkg: MarketplacePkg + + @Input() + localPkg: LocalPkg + + readonly PackageState = PackageState + + constructor( + private readonly alertCtrl: AlertController, + private readonly modalCtrl: ModalController, + private readonly wizardBaker: WizardBaker, + private readonly navCtrl: NavController, + private readonly marketplaceService: AbstractMarketplaceService, + public readonly localStorageService: LocalStorageService, + ) {} + + get version(): string { + return this.localPkg?.manifest.version || '' + } + + async tryInstall() { + const { id, title, version, alerts } = this.pkg.manifest + + if (!alerts.install) { + this.marketplaceService.install(id, version).subscribe() + } else { + const alert = await this.alertCtrl.create({ + header: title, + subHeader: version, + message: alerts.install, + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Install', + handler: () => + this.marketplaceService.install(id, version).subscribe(), + }, + ], + }) + await alert.present() + } + } + + async presentModal(action: 'update' | 'downgrade') { + // TODO: Fix type + const { id, title, version, dependencies, alerts } = this.pkg + .manifest as Manifest + const value = { + id, + title, + version, + serviceRequirements: dependencies, + installAlert: alerts.install, + } + + const { cancelled } = await wizardModal( + this.modalCtrl, + action === 'update' + ? this.wizardBaker.update(value) + : this.wizardBaker.downgrade(value), + ) + + if (cancelled) return + + await pauseFor(250) + this.navCtrl.back() + } +} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependencies/marketplace-show-dependencies.component.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependencies/marketplace-show-dependencies.component.html new file mode 100644 index 000000000..71af3a9d3 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependencies/marketplace-show-dependencies.component.html @@ -0,0 +1,32 @@ + + Dependencies + + + + + + + + +

+ {{ pkg['dependency-metadata'][dep.key].title }} + + (required) + (required by default) + (optional) + +

+

+ {{ dep.value.version | displayEmver }} +

+

{{ dep.value.description }}

+
+
+
+
+
+
diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependencies/marketplace-show-dependencies.component.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependencies/marketplace-show-dependencies.component.ts new file mode 100644 index 000000000..450149c95 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependencies/marketplace-show-dependencies.component.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { MarketplacePkg } from '@start9labs/marketplace' + +import { DependencyInfo, Manifest } from 'src/app/services/patch-db/data-model' + +@Component({ + selector: 'marketplace-show-dependencies', + templateUrl: 'marketplace-show-dependencies.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MarketplaceShowDependenciesComponent { + @Input() + pkg: MarketplacePkg + + get dependencies(): DependencyInfo { + // TODO: Fix type + return (this.pkg.manifest as Manifest).dependencies + } + + getImg(key: string): string { + return 'data:image/png;base64,' + this.pkg['dependency-metadata'][key].icon + } +} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html new file mode 100644 index 000000000..99722c191 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html @@ -0,0 +1,30 @@ + + + +

+ + {{ title }} + +

+

+ + {{ dependentInfo.title }} requires an install of {{ title }} satisfying + {{ dependentInfo.version }}. +
+
+ + {{ title }} version {{ version | displayEmver }} is compatible. + + + {{ title }} version {{ version | displayEmver }} is NOT compatible. + +
+

+
+
diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.scss b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.scss new file mode 100644 index 000000000..83c4d2737 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.scss @@ -0,0 +1,18 @@ +.heading { + display: flex; + align-items: center; +} + +.title { + margin: 5px; + font-family: 'Montserrat', sans-serif; + font-size: 18px; +} + +.text { + font-style: italic; + + &_error { + color: var(--ion-color-danger); + } +} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.ts new file mode 100644 index 000000000..224bc4001 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.ts @@ -0,0 +1,24 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { MarketplacePkg } from '@start9labs/marketplace' +import { DependentInfo } from '@start9labs/shared' + +@Component({ + selector: 'marketplace-show-dependent', + templateUrl: 'marketplace-show-dependent.component.html', + styleUrls: ['marketplace-show-dependent.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MarketplaceShowDependentComponent { + @Input() + pkg: MarketplacePkg + + readonly dependentInfo?: DependentInfo = history.state?.dependentInfo + + get title(): string { + return this.pkg?.manifest.title || '' + } + + get version(): string { + return this.pkg?.manifest.version || '' + } +} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-header/marketplace-show-header.component.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-header/marketplace-show-header.component.html new file mode 100644 index 000000000..26bddfbcb --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-header/marketplace-show-header.component.html @@ -0,0 +1,8 @@ + + + + + + Marketplace Listing + + diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-header/marketplace-show-header.component.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-header/marketplace-show-header.component.ts new file mode 100644 index 000000000..efe309db7 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-header/marketplace-show-header.component.ts @@ -0,0 +1,8 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' + +@Component({ + selector: 'marketplace-show-header', + templateUrl: 'marketplace-show-header.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MarketplaceShowHeaderComponent {} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts index c584f4107..40c5aeb60 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts @@ -2,15 +2,23 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' -import { MarketplaceShowPage } from './marketplace-show.page' import { SharedPipesModule, EmverPipesModule, MarkdownPipeModule, TextSpinnerComponentModule, } from '@start9labs/shared' +import { MarketplacePipesModule } from '@start9labs/marketplace' import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module' -import { MarketplacePipesModule } from '../pipes/marketplace-pipes.module' + +import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module' +import { MarketplaceShowPage } from './marketplace-show.page' +import { MarketplaceShowHeaderComponent } from './marketplace-show-header/marketplace-show-header.component' +import { MarketplaceShowDependentComponent } from './marketplace-show-dependent/marketplace-show-dependent.component' +import { MarketplaceShowDependenciesComponent } from './marketplace-show-dependencies/marketplace-show-dependencies.component' +import { MarketplaceShowAdditionalComponent } from './marketplace-show-additional/marketplace-show-additional.component' +import { MarketplaceShowAboutComponent } from './marketplace-show-about/marketplace-show-about.component' +import { MarketplaceShowControlsComponent } from './marketplace-show-controls/marketplace-show-controls.component' const routes: Routes = [ { @@ -29,8 +37,26 @@ const routes: Routes = [ EmverPipesModule, MarkdownPipeModule, MarketplacePipesModule, + MarketplaceStatusModule, InstallWizardComponentModule, ], - declarations: [MarketplaceShowPage], + declarations: [ + MarketplaceShowPage, + MarketplaceShowHeaderComponent, + MarketplaceShowControlsComponent, + MarketplaceShowDependentComponent, + MarketplaceShowAboutComponent, + MarketplaceShowDependenciesComponent, + MarketplaceShowAdditionalComponent, + ], + exports: [ + MarketplaceShowPage, + MarketplaceShowHeaderComponent, + MarketplaceShowControlsComponent, + MarketplaceShowDependentComponent, + MarketplaceShowAboutComponent, + MarketplaceShowDependenciesComponent, + MarketplaceShowAdditionalComponent, + ], }) export class MarketplaceShowPageModule {} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html index e3fed3cf0..78af9db43 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html @@ -1,68 +1,21 @@ - - - - - - Marketplace Listing - - + - - - +
- +

{{ pkg.manifest.title }}

{{ pkg.manifest.version | displayEmver }}

-
- -

Not Installed

- - - -

- Installed - Update Available -

- -

- - Installing - {{ progress }} - -

- -

- - Removing - - -

-
-
+
@@ -74,41 +27,13 @@ sizeXs="12" class="ion-align-self-center" > - - - Install - - - - - - - Update - - - Reinstall - - - Downgrade - - - +
- +
- - - -

- {{ pkg.manifest.title }} -

-

- - {{ dependentInfo.title }} requires an install of {{ - pkg.manifest.title }} satisfying {{ dependentInfo.version }}. -
-
- {{ pkg.manifest.title }} version {{ pkg.manifest.version | - displayEmver }} is compatible. - {{ pkg.manifest.title }} version {{ pkg.manifest.version | - displayEmver }} is NOT compatible. -
-

-
-
+ - - - New in {{ pkg.manifest.version | displayEmver }} - - All Release Notes - - - - - -
-
-
- - Description - - -
- {{ pkg.manifest.description.long }} -
-
-
- - - Dependencies - - - - - - - - -

- {{ pkg['dependency-metadata'][dep.key].title }} - - (required) - - (required by default) - - (optional) -

-

- {{ dep.value.version | displayEmver }} -

-

{{ dep.value.description }}

-
-
-
-
-
-
+ + +
- Additional Info - - - - - - - -

Other Versions

-

Click to view other versions

-
- -
- - -

License

-

{{ pkg.manifest.license }}

-
- -
- - -

Instructions

-

Click to view instructions

-
- -
-
-
- - - - -

Source Repository

-

{{ pkg.manifest['upstream-repo'] }}

-
- -
- - -

Wrapper Repository

-

{{ pkg.manifest['wrapper-repo'] }}

-
- -
- - -

Support Site

-

{{ pkg.manifest['support-site'] }}

-
- -
-
-
-
-
-
+ +
+ + +
diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.scss b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.scss index 1ac08994f..5f2fdf00f 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.scss +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.scss @@ -1,41 +1,30 @@ .header { - font-family: 'Montserrat'; + font-family: 'Montserrat', sans-serif; padding: 2%; + img { min-width: 15%; max-width: 18%; } + .header-text { margin-left: 5%; display: inline-block; vertical-align: top; + .header-title { margin: 0 0 0 -2px; - font-size: calc(20px + 3vw) + font-size: calc(20px + 3vw); } + .header-version { padding: 4px 0 12px 0; margin: 0; - font-size: calc(10px + 1vw) + font-size: calc(10px + 1vw); } + .header-status { - p { - margin: 0; - font-size: calc(16px + 1vw) - } + font-size: calc(16px + 1vw); } } } - -.recommendation-text { - font-style: italic; -} - -.recommendation-error { - color: var(--ion-color-danger); -} - -#release-notes { - overflow: auto; - max-height: 120px; -} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts index e7f23962b..49a1df720 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts @@ -1,223 +1,72 @@ -import { Component, ViewChild } from '@angular/core' +import { ChangeDetectionStrategy, Component } from '@angular/core' import { ActivatedRoute } from '@angular/router' +import { ErrorToastService } from '@start9labs/shared' import { - AlertController, - IonContent, - LoadingController, - ModalController, - NavController, -} from '@ionic/angular' -import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' -import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' -import { - displayEmver, - Emver, - DependentInfo, - pauseFor, - PackageState, -} from '@start9labs/shared' -import { PackageDataEntry } from 'src/app/services/patch-db/data-model' + LocalPkg, + MarketplacePkg, + AbstractMarketplaceService, + spreadProgress, +} from '@start9labs/marketplace' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' -import { ErrorToastService } from 'src/app/services/error-toast.service' -import { MarketplaceService } from '../marketplace.service' -import { Subscription } from 'rxjs' -import { MarkdownPage } from 'src/app/modals/markdown/markdown.page' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { MarketplacePkg } from 'src/app/services/api/api.types' -import { LocalStorageService } from 'src/app/services/local-storage.service' +import { BehaviorSubject, defer, Observable, of } from 'rxjs' +import { + catchError, + filter, + shareReplay, + startWith, + switchMap, + tap, +} from 'rxjs/operators' @Component({ selector: 'marketplace-show', templateUrl: './marketplace-show.page.html', styleUrls: ['./marketplace-show.page.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class MarketplaceShowPage { - @ViewChild(IonContent) content: IonContent - loading = true - pkgId: string - pkg: MarketplacePkg - localPkg: PackageDataEntry - PackageState = PackageState - dependentInfo: DependentInfo - subs: Subscription[] = [] + private readonly pkgId = this.route.snapshot.paramMap.get('pkgId') + + readonly loadVersion$ = new BehaviorSubject('*') + + readonly localPkg$ = defer(() => + this.patch.watch$('package-data', this.pkgId), + ).pipe( + filter(Boolean), + tap(spreadProgress), + shareReplay({ bufferSize: 1, refCount: true }), + ) + + readonly pkg$: Observable = this.loadVersion$.pipe( + switchMap(version => + this.marketplaceService + .getPackage(this.pkgId, version) + .pipe(startWith(null)), + ), + // TODO: Better fallback + catchError(e => this.errToast.present(e) && of({} as MarketplacePkg)), + ) constructor( private readonly route: ActivatedRoute, - private readonly alertCtrl: AlertController, - private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, private readonly errToast: ErrorToastService, - private readonly wizardBaker: WizardBaker, - private readonly navCtrl: NavController, - private readonly emver: Emver, private readonly patch: PatchDbService, - private readonly embassyApi: ApiService, - private readonly marketplaceService: MarketplaceService, - public readonly localStorageService: LocalStorageService, + private readonly marketplaceService: AbstractMarketplaceService, ) {} - async ngOnInit() { - this.pkgId = this.route.snapshot.paramMap.get('pkgId') - this.dependentInfo = - history.state && (history.state.dependentInfo as DependentInfo) - - this.subs = [ - this.patch.watch$('package-data', this.pkgId).subscribe(pkg => { - if (!pkg) return - this.localPkg = pkg - this.localPkg['install-progress'] = { - ...this.localPkg['install-progress'], - } - }), - ] - - try { - if (!this.marketplaceService.pkgs.length) { - await this.marketplaceService.load() - } - this.pkg = this.marketplaceService.pkgs.find( - pkg => pkg.manifest.id === this.pkgId, - ) - if (!this.pkg) { - throw new Error(`Service with ID "${this.pkgId}" not found.`) - } - } catch (e) { - this.errToast.present(e) - } finally { - this.loading = false - } + getIcon(icon: string): string { + return `data:image/png;base64,${icon}` } - ngAfterViewInit() { - this.content.scrollToPoint(undefined, 1) - } - - ngOnDestroy() { - this.subs.forEach(sub => sub.unsubscribe()) - } - - async presentAlertVersions() { - const alert = await this.alertCtrl.create({ - header: 'Versions', - inputs: this.pkg.versions - .sort((a, b) => -1 * this.emver.compare(a, b)) - .map(v => { - return { - name: v, // for CSS - type: 'radio', - label: displayEmver(v), // appearance on screen - value: v, // literal SEM version value - checked: this.pkg.manifest.version === v, - } - }), - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Ok', - handler: (version: string) => { - this.getPkg(version) - }, - cssClass: 'enter-click', - }, - ], - }) - - await alert.present() - } - - async presentModalMd(title: string) { - const modal = await this.modalCtrl.create({ - componentProps: { - title, - contentUrl: `/marketplace${this.pkg[title]}`, - }, - component: MarkdownPage, - }) - - await modal.present() - } - - async tryInstall() { - const { id, title, version, alerts } = this.pkg.manifest - - if (!alerts.install) { - await this.install(id, version) - } else { - const alert = await this.alertCtrl.create({ - header: title, - subHeader: version, - message: alerts.install, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Install', - handler: () => { - this.install(id, version) - }, - }, - ], - }) - await alert.present() - } - } - - async presentModal(action: 'update' | 'downgrade') { - const { id, title, version, dependencies, alerts } = this.pkg.manifest - const value = { - id, - title, - version, - serviceRequirements: dependencies, - installAlert: alerts.install, - } - - const { cancelled } = await wizardModal( - this.modalCtrl, - action === 'update' - ? this.wizardBaker.update(value) - : this.wizardBaker.downgrade(value), - ) - - if (cancelled) return - await pauseFor(250) - this.navCtrl.back() - } - - private async getPkg(version?: string): Promise { - this.loading = true - try { - this.pkg = await this.marketplaceService.getPkg(this.pkgId, version) - } catch (e) { - this.errToast.present(e) - } finally { - await pauseFor(100) - this.loading = false - } - } - - private async install(id: string, version?: string): Promise { - const loader = await this.loadingCtrl.create({ - spinner: 'lines', - message: 'Beginning Installation', - cssClass: 'loader', - }) - loader.present() - - try { - await this.marketplaceService.installPackage({ - id, - 'version-spec': version ? `=${version}` : undefined, - }) - } catch (e) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - } + // async getPkg(version: string): Promise { + // this.loading = true + // try { + // this.pkg = await this.marketplaceService.getPkg(this.pkgId, version) + // } catch (e) { + // this.errToast.present(e) + // } finally { + // await pauseFor(100) + // this.loading = false + // } + // } } diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.html new file mode 100644 index 000000000..d1bcee7ab --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.html @@ -0,0 +1,28 @@ + +
+ + Installed + + + Update Available + +
+
+ + Removing + + +
+
+ + Installing + {{ progress }} + +
+
+ +
Not Installed
+
diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts new file mode 100644 index 000000000..2827d5afe --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts @@ -0,0 +1,18 @@ +import { Component, Input } from '@angular/core' +import { LocalPkg } from '@start9labs/marketplace' +import { PackageState } from '@start9labs/shared' + +@Component({ + selector: 'marketplace-status', + templateUrl: 'marketplace-status.component.html', +}) +export class MarketplaceStatusComponent { + @Input() + pkg?: LocalPkg + + PackageState = PackageState + + get version(): string { + return this.pkg?.manifest.version || '' + } +} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts new file mode 100644 index 000000000..783266bb7 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common' +import { NgModule } from '@angular/core' +import { IonicModule } from '@ionic/angular' +import { EmverPipesModule } from '@start9labs/shared' +import { MarketplacePipesModule } from '@start9labs/marketplace' + +import { MarketplaceStatusComponent } from './marketplace-status.component' + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + EmverPipesModule, + MarketplacePipesModule, + ], + declarations: [MarketplaceStatusComponent], + exports: [MarketplaceStatusComponent], +}) +export class MarketplaceStatusModule {} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace.service.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace.service.ts deleted file mode 100644 index b05c57631..000000000 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace.service.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { Injectable } from '@angular/core' -import { Subscription } from 'rxjs' -import { - MarketplaceData, - MarketplacePkg, - RR, -} from 'src/app/services/api/api.types' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ConfigService } from 'src/app/services/config.service' -import { Emver } from '@start9labs/shared' -import { PackageDataEntry } from 'src/app/services/patch-db/data-model' -import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' - -@Injectable({ - providedIn: 'root', -}) -export class MarketplaceService { - data: MarketplaceData - pkgs: MarketplacePkg[] = [] - releaseNotes: { - [id: string]: { - [version: string]: string - } - } = {} - marketplace: { - url: string - name: string - } - - constructor( - private readonly api: ApiService, - private readonly emver: Emver, - private readonly patch: PatchDbService, - private readonly config: ConfigService, - ) {} - - init(): Subscription { - return this.patch.watch$('ui', 'marketplace').subscribe(marketplace => { - if (!marketplace || !marketplace['selected-id']) { - this.marketplace = this.config.marketplace - } else { - this.marketplace = - marketplace['known-hosts'][marketplace['selected-id']] - } - }) - } - - async load(): Promise { - try { - const [data, pkgs] = await Promise.all([ - this.getMarketplaceData({ - 'server-id': this.patch.getData()['server-info'].id, - }), - this.getMarketplacePkgs({ page: 1, 'per-page': 100 }), - ]) - this.data = data - this.pkgs = pkgs - if (this.patch.getData().ui.marketplace?.['selected-id']) { - const { 'selected-id': selectedId, 'known-hosts': knownHosts } = - this.patch.getData().ui.marketplace - if (knownHosts[selectedId].name !== this.data.name) { - this.api.setDbValue({ - pointer: `/marketplace/known-hosts/${selectedId}/name`, - value: this.data.name, - }) - } - } - } catch (e) { - this.data = undefined - this.pkgs = [] - throw e - } - } - - async installPackage(req: Omit) { - req['marketplace-url'] = this.marketplace.url - return this.api.installPackage(req as RR.InstallPackageReq) - } - - async getUpdates( - localPkgs: Record, - ): Promise { - const id = this.patch.getData().ui.marketplace?.['selected-id'] - const url = id - ? this.patch.getData().ui.marketplace['known-hosts'][id].url - : this.config.marketplace.url - - const idAndCurrentVersions = Object.keys(localPkgs) - .map(key => ({ - id: key, - version: localPkgs[key].manifest.version, - marketplaceUrl: localPkgs[key].installed['marketplace-url'], - })) - .filter(pkg => { - return pkg.marketplaceUrl === url - }) - const latestPkgs = await this.getMarketplacePkgs({ - ids: idAndCurrentVersions, - }) - - return latestPkgs.filter(latestPkg => { - const latestVersion = latestPkg.manifest.version - const curVersion = localPkgs[latestPkg.manifest.id]?.manifest.version - return !!curVersion && this.emver.compare(latestVersion, curVersion) === 1 - }) - } - - async getPkg(id: string, version = '*'): Promise { - const pkgs = await this.getMarketplacePkgs({ - ids: [{ id, version }], - }) - const pkg = pkgs.find(pkg => pkg.manifest.id == id) - - if (!pkg) { - throw new Error(`No results for ${id}${version ? ' ' + version : ''}`) - } else { - return pkg - } - } - - async cacheReleaseNotes(id: string): Promise { - this.releaseNotes[id] = await this.getReleaseNotes({ id }) - } - - async getMarketplaceData( - params: RR.GetMarketplaceDataReq, - url?: string, - ): Promise { - url = url || this.marketplace.url - return this.api.marketplaceProxy('/package/v0/info', params, url) - } - - async getMarketplacePkgs( - params: Omit, - ): Promise { - if (params.query) delete params.category - if (params.ids) params.ids = JSON.stringify(params.ids) as any - - const qp: RR.GetMarketplacePackagesReq = { - ...params, - 'eos-version-compat': - this.patch.getData()['server-info']['eos-version-compat'], - } - - return this.api.marketplaceProxy( - '/package/v0/index', - qp, - this.marketplace.url, - ) - } - - async getReleaseNotes( - params: RR.GetReleaseNotesReq, - ): Promise { - return this.api.marketplaceProxy( - `/package/v0/release-notes/${params.id}`, - {}, - this.marketplace.url, - ) - } -} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes-header/release-notes-header.component.html b/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes-header/release-notes-header.component.html new file mode 100644 index 000000000..3c8c19d7f --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes-header/release-notes-header.component.html @@ -0,0 +1,8 @@ + + + + + + Release Notes + + diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes-header/release-notes-header.component.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes-header/release-notes-header.component.ts new file mode 100644 index 000000000..7e4e405a7 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes-header/release-notes-header.component.ts @@ -0,0 +1,13 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' +import { ActivatedRoute } from '@angular/router' + +@Component({ + selector: 'release-notes-header', + templateUrl: 'release-notes-header.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReleaseNotesHeaderComponent { + readonly href = `/marketplace/${this.route.snapshot.paramMap.get('pkgId')}` + + constructor(private readonly route: ActivatedRoute) {} +} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.module.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.module.ts similarity index 55% rename from frontend/projects/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.module.ts rename to frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.module.ts index 5082481ca..0788c5b60 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.module.ts +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.module.ts @@ -6,14 +6,17 @@ import { EmverPipesModule, MarkdownPipeModule, TextSpinnerComponentModule, + ElementModule, } from '@start9labs/shared' -import { AppReleaseNotes } from './app-release-notes.page' -import { MarketplacePipesModule } from '../pipes/marketplace-pipes.module' +import { MarketplacePipesModule } from '@start9labs/marketplace' + +import { ReleaseNotesPage } from './release-notes.page' +import { ReleaseNotesHeaderComponent } from './release-notes-header/release-notes-header.component' const routes: Routes = [ { path: '', - component: AppReleaseNotes, + component: ReleaseNotesPage, }, ] @@ -26,7 +29,9 @@ const routes: Routes = [ EmverPipesModule, MarkdownPipeModule, MarketplacePipesModule, + ElementModule, ], - declarations: [AppReleaseNotes], + declarations: [ReleaseNotesPage, ReleaseNotesHeaderComponent], + exports: [ReleaseNotesPage, ReleaseNotesHeaderComponent], }) -export class ReleaseNotesModule {} +export class ReleaseNotesPageModule {} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.page.html b/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.page.html new file mode 100644 index 000000000..1b6666027 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.page.html @@ -0,0 +1,34 @@ + + + + +
+ +

{{ note.key | displayEmver }}

+
+ + + +
+
+ + + + +
diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.page.scss b/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.page.scss new file mode 100644 index 000000000..d822599c3 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.page.scss @@ -0,0 +1,19 @@ +.panel { + margin: 0; + padding: 0 24px; + transition: max-height 0.2s ease-out; +} + +.active { + border: 5px solid #4d4d4d; +} + +.version-button { + height: 50px; + margin: 1px; +} + +.version { + position: absolute; + left: 10px; +} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.page.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.page.ts new file mode 100644 index 000000000..1139c07eb --- /dev/null +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.page.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core' +import { ActivatedRoute } from '@angular/router' +import { AbstractMarketplaceService } from '@start9labs/marketplace' + +@Component({ + selector: 'release-notes', + templateUrl: './release-notes.page.html', + styleUrls: ['./release-notes.page.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReleaseNotesPage { + private readonly pkgId = this.route.snapshot.paramMap.get('pkgId') + + private selected: string | null = null + + readonly notes$ = this.marketplaceService.getReleaseNotes(this.pkgId) + + constructor( + private readonly route: ActivatedRoute, + private readonly marketplaceService: AbstractMarketplaceService, + ) {} + + isSelected(key: string): boolean { + return this.selected === key + } + + setSelected(selected: string) { + this.selected = this.isSelected(selected) ? null : selected + } + + getDocSize(key: string, { nativeElement }: ElementRef) { + return this.isSelected(key) ? nativeElement.scrollHeight : 0 + } + + asIsOrder(a: any, b: any) { + return 0 + } +} diff --git a/frontend/projects/ui/src/app/pages/notifications/notifications.page.ts b/frontend/projects/ui/src/app/pages/notifications/notifications.page.ts index fd6a12c74..e7697a827 100644 --- a/frontend/projects/ui/src/app/pages/notifications/notifications.page.ts +++ b/frontend/projects/ui/src/app/pages/notifications/notifications.page.ts @@ -11,7 +11,7 @@ import { ModalController, } from '@ionic/angular' import { ActivatedRoute } from '@angular/router' -import { ErrorToastService } from 'src/app/services/error-toast.service' +import { ErrorToastService } from '@start9labs/shared' import { BackupReportPage } from 'src/app/modals/backup-report/backup-report.page' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' diff --git a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.html b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.html index 744c10c40..1f2996e59 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.html @@ -20,16 +20,12 @@ -
+
loader.dismiss()), + ) + .subscribe() } private async delete(id: string): Promise { @@ -281,13 +284,13 @@ export class MarketplacesPage { loader.message = 'Syncing marketplace data...' - try { - await this.marketplaceService.load() - } catch (e) { - this.errToast.present(e) - } finally { - loader.dismiss() - } + this.marketplaceService + .getPackages() + .pipe( + first(), + finalize(() => loader.dismiss()), + ) + .subscribe() } } diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts index ead7cfca2..22013b4f3 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts @@ -1,8 +1,7 @@ import { Component } from '@angular/core' import { Metrics } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService } from 'src/app/services/error-toast.service' -import { pauseFor } from '@start9labs/shared' +import { pauseFor, ErrorToastService } from '@start9labs/shared' @Component({ selector: 'server-metrics', diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts index df401cdbf..927755b44 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -8,13 +8,12 @@ import { } from '@ionic/angular' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ActivatedRoute } from '@angular/router' -import { ErrorToastService } from 'src/app/services/error-toast.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { Observable, of } from 'rxjs' import { filter, map, take } from 'rxjs/operators' import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' -import { exists, isEmptyObject } from '@start9labs/shared' +import { exists, isEmptyObject, ErrorToastService } from '@start9labs/shared' import { EOSService } from 'src/app/services/eos.service' import { ServerStatus } from 'src/app/services/patch-db/data-model' diff --git a/frontend/projects/ui/src/app/pages/server-routes/sessions/sessions.page.ts b/frontend/projects/ui/src/app/pages/server-routes/sessions/sessions.page.ts index f3af1ed6e..f8365a6e1 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/sessions/sessions.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/sessions/sessions.page.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core' import { AlertController, LoadingController } from '@ionic/angular' -import { ErrorToastService } from 'src/app/services/error-toast.service' +import { ErrorToastService } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/embassy-api.service' import { PlatformType, RR } from 'src/app/services/api/api.types' @@ -13,16 +13,16 @@ export class SessionsPage { loading = true sessionInfo: RR.GetSessionsRes - constructor ( + constructor( private readonly loadingCtrl: LoadingController, private readonly errToast: ErrorToastService, private readonly alertCtrl: AlertController, private readonly embassyApi: ApiService, - ) { } + ) {} - async ngOnInit () { + async ngOnInit() { try { - this.sessionInfo = await this.embassyApi.getSessions({ }) + this.sessionInfo = await this.embassyApi.getSessions({}) } catch (e) { this.errToast.present(e) } finally { @@ -30,7 +30,7 @@ export class SessionsPage { } } - async presentAlertKill (id: string) { + async presentAlertKill(id: string) { const alert = await this.alertCtrl.create({ header: 'Caution', message: `Are you sure you want to kill this session?`, @@ -51,7 +51,7 @@ export class SessionsPage { await alert.present() } - async kill (id: string): Promise { + async kill(id: string): Promise { const loader = await this.loadingCtrl.create({ spinner: 'lines', message: 'Killing session...', @@ -69,7 +69,7 @@ export class SessionsPage { } } - getPlatformIcon (platforms: PlatformType[]): string { + getPlatformIcon(platforms: PlatformType[]): string { if (platforms.includes('cli')) { return 'terminal-outline' } else if (platforms.includes('desktop')) { @@ -79,7 +79,7 @@ export class SessionsPage { } } - getPlatformName (platforms: PlatformType[]): string { + getPlatformName(platforms: PlatformType[]): string { if (platforms.includes('cli')) { return 'CLI' } else if (platforms.includes('desktop')) { @@ -97,7 +97,7 @@ export class SessionsPage { } } - asIsOrder (a: any, b: any) { + asIsOrder(a: any, b: any) { return 0 } } diff --git a/frontend/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts b/frontend/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts index 9c7b5fb08..a4a77bb86 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts @@ -1,9 +1,16 @@ import { Component } from '@angular/core' -import { AlertController, LoadingController, ModalController } from '@ionic/angular' +import { + AlertController, + LoadingController, + ModalController, +} from '@ionic/angular' import { SSHKey } from 'src/app/services/api/api.types' -import { ErrorToastService } from 'src/app/services/error-toast.service' +import { ErrorToastService } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { GenericInputComponent, GenericInputOptions } from 'src/app/modals/generic-input/generic-input.component' +import { + GenericInputComponent, + GenericInputOptions, +} from 'src/app/modals/generic-input/generic-input.component' @Component({ selector: 'ssh-keys', @@ -15,21 +22,21 @@ export class SSHKeysPage { sshKeys: SSHKey[] = [] readonly docsUrl = 'https://start9.com/latest/user-manual/ssh' - constructor ( + constructor( private readonly loadingCtrl: LoadingController, private readonly modalCtrl: ModalController, private readonly errToast: ErrorToastService, private readonly alertCtrl: AlertController, private readonly embassyApi: ApiService, - ) { } + ) {} - async ngOnInit () { + async ngOnInit() { await this.getKeys() } - async getKeys (): Promise { + async getKeys(): Promise { try { - this.sshKeys = await this.embassyApi.getSshKeys({ }) + this.sshKeys = await this.embassyApi.getSshKeys({}) } catch (e) { this.errToast.present(e) } finally { @@ -37,7 +44,7 @@ export class SSHKeysPage { } } - async presentModalAdd () { + async presentModalAdd() { const { name, description } = sshSpec const options: GenericInputOptions = { @@ -55,7 +62,7 @@ export class SSHKeysPage { await modal.present() } - async add (pubkey: string): Promise { + async add(pubkey: string): Promise { const loader = await this.loadingCtrl.create({ spinner: 'lines', message: 'Saving...', @@ -71,7 +78,7 @@ export class SSHKeysPage { } } - async presentAlertDelete (i: number) { + async presentAlertDelete(i: number) { const alert = await this.alertCtrl.create({ header: 'Caution', message: `Are you sure you want to delete this key?`, @@ -92,7 +99,7 @@ export class SSHKeysPage { await alert.present() } - async delete (i: number): Promise { + async delete(i: number): Promise { const loader = await this.loadingCtrl.create({ spinner: 'lines', message: 'Deleting...', @@ -115,7 +122,8 @@ export class SSHKeysPage { const sshSpec = { type: 'string', name: 'SSH Key', - description: 'Enter the SSH public key of you would like to authorize for root access to your Embassy.', + description: + 'Enter the SSH public key of you would like to authorize for root access to your Embassy.', nullable: false, masked: false, copyable: false, diff --git a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts index 7e4432a86..35ea974da 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts @@ -9,10 +9,9 @@ import { import { AlertInput } from '@ionic/core' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ActionSheetButton } from '@ionic/core' -import { ErrorToastService } from 'src/app/services/error-toast.service' import { ValueSpecObject } from 'src/app/pkg-config/config-types' import { RR } from 'src/app/services/api/api.types' -import { pauseFor } from '@start9labs/shared' +import { pauseFor, ErrorToastService } from '@start9labs/shared' import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' import { ConfigService } from 'src/app/services/config.service' diff --git a/frontend/projects/ui/src/app/services/api/api.fixures.ts b/frontend/projects/ui/src/app/services/api/api.fixures.ts index be8dd6580..882252e14 100644 --- a/frontend/projects/ui/src/app/services/api/api.fixures.ts +++ b/frontend/projects/ui/src/app/services/api/api.fixures.ts @@ -9,7 +9,6 @@ import { } from 'src/app/services/patch-db/data-model' import { Log, - MarketplacePkg, Metric, RR, NotificationLevel, @@ -17,6 +16,7 @@ import { } from './api.types' import { BTC_ICON, LND_ICON, PROXY_ICON } from './api-icons' +import { MarketplacePkg } from '@start9labs/marketplace' export module Mock { export const MarketplaceEos: RR.GetMarketplaceEOSRes = { diff --git a/frontend/projects/ui/src/app/services/api/api.types.ts b/frontend/projects/ui/src/app/services/api/api.types.ts index ff0b59f54..9c312744a 100644 --- a/frontend/projects/ui/src/app/services/api/api.types.ts +++ b/frontend/projects/ui/src/app/services/api/api.types.ts @@ -1,11 +1,10 @@ import { Dump, Revision } from 'patch-db-client' +import { MarketplaceData, MarketplacePkg } from '@start9labs/marketplace' import { PackagePropertiesVersioned } from 'src/app/util/properties.util' import { ConfigSpec } from 'src/app/pkg-config/config-types' import { DataModel, DependencyError, - Manifest, - URL, } from 'src/app/services/patch-db/data-model' export module RR { @@ -268,32 +267,12 @@ export module RR { export type WithExpire = { 'expire-id'?: string } & T export type WithRevision = { response: T; revision?: Revision } -export interface MarketplaceData { - categories: string[] - name: string -} - export interface MarketplaceEOS { version: string headline: string 'release-notes': { [version: string]: string } } -export interface MarketplacePkg { - icon: URL - license: URL - instructions: URL - manifest: Manifest - categories: string[] - versions: string[] - 'dependency-metadata': { - [id: string]: { - title: string - icon: URL - } - } -} - export interface Breakages { [id: string]: TaggedDependencyError } diff --git a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts index e8c46f097..174cc0293 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -190,7 +190,7 @@ export class MockApiService extends ApiService { } } else if (path === '/package/v0/index') { return Mock.MarketplacePkgsList - } else if (path === '/package/v0/release-notes') { + } else if (path.startsWith('/package/v0/release-notes')) { return Mock.ReleaseNotes } } diff --git a/frontend/projects/ui/src/app/services/marketplace.service.ts b/frontend/projects/ui/src/app/services/marketplace.service.ts new file mode 100644 index 000000000..edf938fbd --- /dev/null +++ b/frontend/projects/ui/src/app/services/marketplace.service.ts @@ -0,0 +1,220 @@ +import { Injectable } from '@angular/core' +import { LoadingController } from '@ionic/angular' +import { Emver, ErrorToastService } from '@start9labs/shared' +import { + MarketplacePkg, + AbstractMarketplaceService, + Marketplace, + MarketplaceData, +} from '@start9labs/marketplace' +import { defer, from, Observable, of } from 'rxjs' +import { RR } from 'src/app/services/api/api.types' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { ConfigService } from 'src/app/services/config.service' +import { ServerInfo } from 'src/app/services/patch-db/data-model' +import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' +import { + catchError, + finalize, + map, + shareReplay, + switchMap, + tap, +} from 'rxjs/operators' + +@Injectable() +export class MarketplaceService extends AbstractMarketplaceService { + private readonly notes = new Map>() + + private readonly init$: Observable = defer(() => + this.patch.watch$('ui', 'marketplace'), + ).pipe( + map(marketplace => + marketplace?.['selected-id'] + ? marketplace['known-hosts'][marketplace['selected-id']] + : this.config.marketplace, + ), + shareReplay(), + ) + + private readonly data$: Observable = this.init$.pipe( + switchMap(({ url }) => + from(this.getMarketplaceData({ 'server-id': this.serverInfo.id }, url)), + ), + shareReplay(), + ) + + private readonly pkg$: Observable = this.init$.pipe( + switchMap(({ url, name }) => + from(this.getMarketplacePkgs({ page: 1, 'per-page': 100 }, url)).pipe( + tap(() => this.onPackages(name)), + ), + ), + shareReplay(), + catchError(e => this.errToast.present(e) && of([])), + ) + + constructor( + private readonly api: ApiService, + private readonly emver: Emver, + private readonly patch: PatchDbService, + private readonly config: ConfigService, + private readonly loadingCtrl: LoadingController, + private readonly errToast: ErrorToastService, + ) { + super() + } + + getMarketplace(): Observable { + return this.init$ + } + + getCategories(): Observable { + return this.data$.pipe(map(({ categories }) => categories)) + } + + getPackages(): Observable { + return this.pkg$ + } + + getPackage(id: string, version: string): Observable { + const params = { ids: [{ id, version }] } + + return this.init$.pipe( + switchMap(({ url }) => from(this.getMarketplacePkgs(params, url))), + map(pkgs => pkgs.find(pkg => pkg.manifest.id == id)), + tap(pkg => { + if (!pkg) { + throw new Error(`No results for ${id}${version ? ' ' + version : ''}`) + } + }), + ) + } + + getReleaseNotes(id: string): Observable> { + if (this.notes.has(id)) { + return of(this.notes.get(id)) + } + + return this.init$.pipe( + switchMap(({ url }) => this.loadReleaseNotes(id, url)), + tap(response => this.notes.set(id, response)), + catchError(e => this.errToast.present(e) && of({})), + ) + } + + // async install(id: string, version?: string): Promise { + // const loader = await this.loadingCtrl.create({ + // spinner: 'lines', + // message: 'Beginning Installation', + // cssClass: 'loader', + // }) + // loader.present() + // + // try { + // await this.installPackage({ + // id, + // 'version-spec': version ? `=${version}` : undefined, + // }) + // } catch (e) { + // this.errToast.present(e) + // } finally { + // loader.dismiss() + // } + // } + + install(id: string, version?: string): Observable { + return defer(() => + from( + this.loadingCtrl.create({ + spinner: 'lines', + message: 'Beginning Installation', + cssClass: 'loader', + }), + ), + ).pipe( + tap(loader => loader.present()), + switchMap(loader => + this.installPackage({ + id, + 'version-spec': version ? `=${version}` : undefined, + }).pipe( + catchError(e => from(this.errToast.present(e))), + tap(() => loader.dismiss()), + ), + ), + ) + } + + installPackage( + req: Omit, + ): Observable { + return this.getMarketplace().pipe( + switchMap(({ url }) => + from( + this.api.installPackage({ + ...req, + 'marketplace-url': url, + }), + ), + ), + ) + } + + async getMarketplaceData( + params: RR.GetMarketplaceDataReq, + url: string, + ): Promise { + return this.api.marketplaceProxy('/package/v0/info', params, url) + } + + async getMarketplacePkgs( + params: Omit, + url: string, + ): Promise { + if (params.query) delete params.category + if (params.ids) params.ids = JSON.stringify(params.ids) as any + + const qp: RR.GetMarketplacePackagesReq = { + ...params, + 'eos-version-compat': this.serverInfo['eos-version-compat'], + } + + return this.api.marketplaceProxy('/package/v0/index', qp, url) + } + + private get serverInfo(): ServerInfo { + return this.patch.getData()['server-info'] + } + + private loadReleaseNotes( + id: string, + url: string, + ): Observable> { + return from( + this.api.marketplaceProxy>( + `/package/v0/release-notes/${id}`, + {}, + url, + ), + ) + } + + private onPackages(name: string) { + const { marketplace } = this.patch.getData().ui + + if (!marketplace?.['selected-id']) { + return + } + + const selectedId = marketplace['selected-id'] + const knownHosts = marketplace['known-hosts'] + + if (knownHosts[selectedId].name !== name) { + this.api.setDbValue({ + pointer: `/marketplace/known-hosts/${selectedId}/name`, + value: name, + }) + } + } +} diff --git a/frontend/projects/ui/src/app/services/patch-db/data-model.ts b/frontend/projects/ui/src/app/services/patch-db/data-model.ts index e661f32f6..55268a846 100644 --- a/frontend/projects/ui/src/app/services/patch-db/data-model.ts +++ b/frontend/projects/ui/src/app/services/patch-db/data-model.ts @@ -1,5 +1,6 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types' -import { InstallProgress, PackageState } from '@start9labs/shared' +import { InstallProgress, PackageState, Url } from '@start9labs/shared' +import { MarketplaceManifest } from '@start9labs/marketplace' import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info' export interface DataModel { @@ -50,8 +51,8 @@ export interface ServerInfo { id: string version: string 'last-backup': string | null - 'lan-address': URL - 'tor-address': URL + 'lan-address': Url + 'tor-address': Url 'unread-notification-count': number 'status-info': { 'backing-up': boolean @@ -69,16 +70,16 @@ export enum ServerStatus { } export interface RecoveredPackageDataEntry { title: string - icon: URL + icon: Url version: string } export interface PackageDataEntry { state: PackageState 'static-files': { - license: URL - instructions: URL - icon: URL + license: Url + instructions: Url + icon: Url } manifest: Manifest installed?: InstalledPackageDataEntry // exists when: installed, updating @@ -95,7 +96,7 @@ export interface InstalledPackageDataEntry { 'dependency-info': { [id: string]: { manifest: Manifest - icon: URL + icon: Url } } 'interface-addresses': { @@ -110,28 +111,7 @@ export interface CurrentDependencyInfo { 'health-checks': string[] // array of health check IDs } -export interface Manifest { - id: string - title: string - version: string - description: { - short: string - long: string - } - 'release-notes': string - license: string // name - 'wrapper-repo': URL - 'upstream-repo': URL - 'support-site': URL - 'marketing-site': URL - 'donation-url': URL | null - alerts: { - install: string | null - uninstall: string | null - restore: string | null - start: string | null - stop: string | null - } +export interface Manifest extends MarketplaceManifest { main: ActionImpl 'health-checks': Record< string, @@ -397,5 +377,3 @@ export interface DependencyEntry { 'auto-configure': ActionImpl } } - -export type URL = string diff --git a/frontend/projects/ui/src/app/services/patch-db/patch-db.service.ts b/frontend/projects/ui/src/app/services/patch-db/patch-db.service.ts index fb6771011..9c6b0726a 100644 --- a/frontend/projects/ui/src/app/services/patch-db/patch-db.service.ts +++ b/frontend/projects/ui/src/app/services/patch-db/patch-db.service.ts @@ -48,6 +48,7 @@ export class PatchDbService { return this.patchDb.store.cache.data } + // TODO: Refactor to use `Observable` so that we can react to PatchDb becoming loaded get loaded(): boolean { return ( this.patchDb?.store?.cache?.data && diff --git a/frontend/projects/ui/src/app/services/server-config.service.ts b/frontend/projects/ui/src/app/services/server-config.service.ts index 386d4bbbf..9793b5f76 100644 --- a/frontend/projects/ui/src/app/services/server-config.service.ts +++ b/frontend/projects/ui/src/app/services/server-config.service.ts @@ -3,7 +3,7 @@ import { AlertInput, AlertButton } from '@ionic/core' import { ApiService } from './api/embassy-api.service' import { ConfigSpec } from 'src/app/pkg-config/config-types' import { AlertController, LoadingController } from '@ionic/angular' -import { ErrorToastService } from './error-toast.service' +import { ErrorToastService } from '@start9labs/shared' @Injectable({ providedIn: 'root', diff --git a/frontend/projects/ui/src/styles.scss b/frontend/projects/ui/src/styles.scss index c0b35bdb5..2ade488eb 100644 --- a/frontend/projects/ui/src/styles.scss +++ b/frontend/projects/ui/src/styles.scss @@ -278,30 +278,10 @@ ion-loading { --padding-start: 10px; } -.divider { - background: linear-gradient( - 90deg, - var(--ion-color-light) 0, - var(--ion-color-dark) 50%, - var(--ion-color-light) 100% - ); - height: 1px; -} - h2 { line-height: unset; } -.loading-dots:after { - content: '...'; - overflow: hidden; - display: inline-block; - vertical-align: bottom; - animation: ellipsis-dot 1s infinite 0.3s; - animation-fill-mode: forwards; - width: 1em; -} - @keyframes ellipsis-dot { 25% { content: ''; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 34ca180ae..aac9e859b 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -22,6 +22,7 @@ "crypto": ["./node_modules/crypto-browserify"], "vm": ["./node_modules/vm-browserify"], /* These paths are relative to each app base folder */ + "@start9labs/marketplace": ["../marketplace/src/public-api"], "@start9labs/shared": ["../shared/src/public-api"] }, "typeRoots": ["node_modules/@types"],