From 26c37ba8249f9ba98937f68a012a8dd524ea9a56 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 27 Oct 2022 15:48:12 -0600 Subject: [PATCH] Feat/update tab (#1865) * implement updates tab for viewing all updates from all marketplaces in one place * remove auto-check-updates * feat: implement updates page (#1888) * feat: implement updates page * chore: comments * better styling in update tab * rework marketplace service (#1891) * rework marketplace service * remove unneeded ? * fix: refactor marketplace to cache requests Co-authored-by: waterplea Co-authored-by: Alex Inkin --- frontend/package-lock.json | 25 +- frontend/package.json | 1 + frontend/patchdb-ui-seed.json | 1 - .../src/pages/list/item/item.component.ts | 3 +- .../release-notes/release-notes.component.ts | 2 +- .../src/pages/show/about/about.component.ts | 3 +- .../show/additional/additional.component.ts | 4 +- .../dependencies/dependencies.component.ts | 3 +- .../pages/show/package/package.component.ts | 3 +- .../src/pipes/filter-packages.pipe.ts | 3 +- .../projects/marketplace/src/public-api.ts | 5 +- .../src/services/marketplace.service.ts | 23 +- frontend/projects/marketplace/src/types.ts | 76 ++++ .../marketplace/src/types/dependency.ts | 17 - .../marketplace/src/types/marketplace-info.ts | 4 - .../src/types/marketplace-manifest.ts | 27 -- .../marketplace/src/types/marketplace-pkg.ts | 18 - frontend/projects/shared/src/public-api.ts | 3 +- .../shared/src/util/get-new-entries.ts | 12 + .../projects/ui/src/app/app-routing.module.ts | 7 + .../ui/src/app/app/menu/menu.component.html | 4 +- .../ui/src/app/app/menu/menu.component.ts | 42 +- .../app/app/preloader/preloader.component.ts | 3 +- .../skeleton-list.component.html | 33 +- .../skeleton-list/skeleton-list.component.ts | 7 +- .../apps-routes/app-list/app-list.page.ts | 6 +- .../app-metrics/app-metrics.page.html | 2 +- .../marketplace-list.page.html | 8 +- .../marketplace-list/marketplace-list.page.ts | 30 +- .../marketplace-show-controls.component.ts | 2 +- .../marketplace-show/marketplace-show.page.ts | 2 +- .../marketplaces/marketplaces.page.ts | 2 +- .../preferences/preferences.module.ts | 24 -- .../preferences/preferences.page.html | 30 -- .../preferences/preferences.page.scss | 0 .../preferences/preferences.page.ts | 99 ----- .../server-routes/server-routing.module.ts | 7 - .../server-show/server-show.page.html | 5 +- .../server-show/server-show.page.ts | 108 ++++- .../src/app/pages/updates/updates.module.ts | 33 ++ .../src/app/pages/updates/updates.page.html | 84 ++++ .../src/app/pages/updates/updates.page.scss | 12 + .../ui/src/app/pages/updates/updates.page.ts | 95 +++++ .../install-progress.module.ts | 9 +- .../install-progress/install-progress.pipe.ts | 14 +- .../ui/src/app/services/api/api.fixures.ts | 6 +- .../ui/src/app/services/api/api.types.ts | 6 +- .../services/api/embassy-mock-api.service.ts | 1 - .../ui/src/app/services/api/mock-patch.ts | 11 +- .../src/app/services/marketplace.service.ts | 394 +++++++----------- .../ui/src/app/services/patch-data.service.ts | 11 +- .../src/app/services/patch-db/data-model.ts | 1 - .../src/app/services/server-config.service.ts | 116 ------ 53 files changed, 723 insertions(+), 724 deletions(-) create mode 100644 frontend/projects/marketplace/src/types.ts delete mode 100644 frontend/projects/marketplace/src/types/dependency.ts delete mode 100644 frontend/projects/marketplace/src/types/marketplace-info.ts delete mode 100644 frontend/projects/marketplace/src/types/marketplace-manifest.ts delete mode 100644 frontend/projects/marketplace/src/types/marketplace-pkg.ts create mode 100644 frontend/projects/shared/src/util/get-new-entries.ts delete mode 100644 frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.module.ts delete mode 100644 frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.html delete mode 100644 frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.scss delete mode 100644 frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.ts create mode 100644 frontend/projects/ui/src/app/pages/updates/updates.module.ts create mode 100644 frontend/projects/ui/src/app/pages/updates/updates.page.html create mode 100644 frontend/projects/ui/src/app/pages/updates/updates.page.scss create mode 100644 frontend/projects/ui/src/app/pages/updates/updates.page.ts delete mode 100644 frontend/projects/ui/src/app/services/server-config.service.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4424ad7d8..e34f32621 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "embassy-os", - "version": "0.3.2", + "version": "0.3.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "embassy-os", - "version": "0.3.2", + "version": "0.3.2.1", "dependencies": { "@angular/animations": "^14.1.0", "@angular/common": "^14.1.0", @@ -20,6 +20,7 @@ "@materia-ui/ngx-monaco-editor": "^6.0.0", "@start9labs/argon2": "^0.1.0", "@start9labs/emver": "^0.1.5", + "angular-svg-round-progressbar": "^9.0.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", "cbor": "npm:@jprochazk/cbor@^0.4.9", @@ -4127,6 +4128,18 @@ "ajv": "^8.8.2" } }, + "node_modules/angular-svg-round-progressbar": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/angular-svg-round-progressbar/-/angular-svg-round-progressbar-9.0.0.tgz", + "integrity": "sha512-q8d2AEG9u+GMAMrZY40NgejN5fHwR4iK+rRxtJ7NnMEvvuAMqt9UEtKe0SqVQHvZYE6W16L5J9yaO+TEtfRjpw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^14.0.0", + "@angular/core": "^14.0.0" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -17628,6 +17641,14 @@ "fast-deep-equal": "^3.1.3" } }, + "angular-svg-round-progressbar": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/angular-svg-round-progressbar/-/angular-svg-round-progressbar-9.0.0.tgz", + "integrity": "sha512-q8d2AEG9u+GMAMrZY40NgejN5fHwR4iK+rRxtJ7NnMEvvuAMqt9UEtKe0SqVQHvZYE6W16L5J9yaO+TEtfRjpw==", + "requires": { + "tslib": "^2.3.0" + } + }, "ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index d61e7efd1..27ad49f6a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -39,6 +39,7 @@ "@materia-ui/ngx-monaco-editor": "^6.0.0", "@start9labs/argon2": "^0.1.0", "@start9labs/emver": "^0.1.5", + "angular-svg-round-progressbar": "^9.0.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", "cbor": "npm:@jprochazk/cbor@^0.4.9", diff --git a/frontend/patchdb-ui-seed.json b/frontend/patchdb-ui-seed.json index 019505795..db671e9cb 100644 --- a/frontend/patchdb-ui-seed.json +++ b/frontend/patchdb-ui-seed.json @@ -1,6 +1,5 @@ { "name": null, - "auto-check-updates": true, "ack-welcome": "0.3.2.1", "marketplace": { "selected-url": "https://registry.start9.com/", diff --git a/frontend/projects/marketplace/src/pages/list/item/item.component.ts b/frontend/projects/marketplace/src/pages/list/item/item.component.ts index 2440bad6d..36398efe6 100644 --- a/frontend/projects/marketplace/src/pages/list/item/item.component.ts +++ b/frontend/projects/marketplace/src/pages/list/item/item.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' - -import { MarketplacePkg } from '../../../types/marketplace-pkg' +import { MarketplacePkg } from '../../../types' @Component({ selector: 'marketplace-item', diff --git a/frontend/projects/marketplace/src/pages/release-notes/release-notes.component.ts b/frontend/projects/marketplace/src/pages/release-notes/release-notes.component.ts index 891be5198..49da475d9 100644 --- a/frontend/projects/marketplace/src/pages/release-notes/release-notes.component.ts +++ b/frontend/projects/marketplace/src/pages/release-notes/release-notes.component.ts @@ -14,7 +14,7 @@ export class ReleaseNotesComponent { private selected: string | null = null - readonly notes$ = this.marketplaceService.fetchReleaseNotes(this.pkgId) + readonly notes$ = this.marketplaceService.fetchReleaseNotes$(this.pkgId) constructor( private readonly route: ActivatedRoute, diff --git a/frontend/projects/marketplace/src/pages/show/about/about.component.ts b/frontend/projects/marketplace/src/pages/show/about/about.component.ts index 3e2c1133e..6626d4fbe 100644 --- a/frontend/projects/marketplace/src/pages/show/about/about.component.ts +++ b/frontend/projects/marketplace/src/pages/show/about/about.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' - -import { MarketplacePkg } from '../../../types/marketplace-pkg' +import { MarketplacePkg } from '../../../types' @Component({ selector: 'marketplace-about', diff --git a/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts b/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts index a05d0779a..37c7efbbf 100644 --- a/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts +++ b/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts @@ -7,8 +7,8 @@ import { } from '@angular/core' import { AlertController, ModalController } from '@ionic/angular' import { displayEmver, Emver, MarkdownComponent } from '@start9labs/shared' +import { MarketplacePkg } from '../../../types' import { AbstractMarketplaceService } from '../../../services/marketplace.service' -import { MarketplacePkg } from '../../../types/marketplace-pkg' @Component({ selector: 'marketplace-additional', @@ -57,7 +57,7 @@ export class AdditionalComponent { } async presentModalMd(title: string) { - const content = this.marketplaceService.fetchPackageMarkdown( + const content = this.marketplaceService.fetchStatic$( this.pkg.manifest.id, title, ) diff --git a/frontend/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts b/frontend/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts index b383a1697..13ff86809 100644 --- a/frontend/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts +++ b/frontend/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' - -import { MarketplacePkg } from '../../../types/marketplace-pkg' +import { MarketplacePkg } from '../../../types' @Component({ selector: 'marketplace-dependencies', diff --git a/frontend/projects/marketplace/src/pages/show/package/package.component.ts b/frontend/projects/marketplace/src/pages/show/package/package.component.ts index 5e4ba2530..08da8aa51 100644 --- a/frontend/projects/marketplace/src/pages/show/package/package.component.ts +++ b/frontend/projects/marketplace/src/pages/show/package/package.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' - -import { MarketplacePkg } from '../../../types/marketplace-pkg' +import { MarketplacePkg } from '../../../types' @Component({ selector: 'marketplace-package', diff --git a/frontend/projects/marketplace/src/pipes/filter-packages.pipe.ts b/frontend/projects/marketplace/src/pipes/filter-packages.pipe.ts index 74b256ac5..733c05add 100644 --- a/frontend/projects/marketplace/src/pipes/filter-packages.pipe.ts +++ b/frontend/projects/marketplace/src/pipes/filter-packages.pipe.ts @@ -1,7 +1,6 @@ import { NgModule, Pipe, PipeTransform } from '@angular/core' -import { MarketplacePkg } from '../types/marketplace-pkg' -import { MarketplaceManifest } from '../types/marketplace-manifest' import { Emver } from '@start9labs/shared' +import { MarketplaceManifest, MarketplacePkg } from '../types' import Fuse from 'fuse.js' @Pipe({ diff --git a/frontend/projects/marketplace/src/public-api.ts b/frontend/projects/marketplace/src/public-api.ts index 677993ff0..b9e8c4686 100644 --- a/frontend/projects/marketplace/src/public-api.ts +++ b/frontend/projects/marketplace/src/public-api.ts @@ -25,7 +25,4 @@ export * from './pipes/filter-packages.pipe' export * from './services/marketplace.service' -export * from './types/dependency' -export * from './types/marketplace-info' -export * from './types/marketplace-manifest' -export * from './types/marketplace-pkg' +export * from './types' diff --git a/frontend/projects/marketplace/src/services/marketplace.service.ts b/frontend/projects/marketplace/src/services/marketplace.service.ts index 5813119da..649bddb66 100644 --- a/frontend/projects/marketplace/src/services/marketplace.service.ts +++ b/frontend/projects/marketplace/src/services/marketplace.service.ts @@ -1,24 +1,33 @@ import { Observable } from 'rxjs' -import { MarketplaceInfo } from '../types/marketplace-info' -import { MarketplacePkg } from '../types/marketplace-pkg' +import { + MarketplacePkg, + Marketplace, + MarketplaceURL, + MarketplaceName, + StoreData, +} from '../types' export abstract class AbstractMarketplaceService { - abstract getMarketplaceInfo$(): Observable + abstract getKnownHosts$(): Observable> - abstract getPackages$(): Observable + abstract getSelectedHost$(): Observable<{ url: string; name: string }> - abstract getPackage( + abstract getMarketplace$(): Observable + + abstract getSelectedStore$(): Observable + + abstract getPackage$( id: string, version: string, url?: string, ): Observable - abstract fetchReleaseNotes( + abstract fetchReleaseNotes$( id: string, url?: string, ): Observable> - abstract fetchPackageMarkdown( + abstract fetchStatic$( id: string, type: string, url?: string, diff --git a/frontend/projects/marketplace/src/types.ts b/frontend/projects/marketplace/src/types.ts new file mode 100644 index 000000000..a7072e507 --- /dev/null +++ b/frontend/projects/marketplace/src/types.ts @@ -0,0 +1,76 @@ +import { Url } from '@start9labs/shared' + +export type MarketplaceURL = string + +export type MarketplaceName = string + +export type Marketplace = Record + +export interface StoreData { + info: StoreInfo + packages: MarketplacePkg[] +} + +export interface StoreInfo { + name: MarketplaceName + categories: string[] +} + +export interface MarketplacePkg { + icon: Url + license: Url + instructions: Url + manifest: MarketplaceManifest + categories: string[] + versions: string[] + 'dependency-metadata': { + [id: string]: { + title: string + icon: Url + } + } + 'published-at': string +} + +export interface MarketplaceManifest { + id: string + title: string + version: string + description: { + short: string + long: string + } + 'release-notes': string + license: string // type of license + '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 + } + dependencies: Record> +} + +export interface Dependency { + version: string + requirement: + | { + type: 'opt-in' + how: string + } + | { + type: 'opt-out' + how: string + } + | { + type: 'required' + } + description: string | null + config: T +} diff --git a/frontend/projects/marketplace/src/types/dependency.ts b/frontend/projects/marketplace/src/types/dependency.ts deleted file mode 100644 index 22947aec4..000000000 --- a/frontend/projects/marketplace/src/types/dependency.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface Dependency { - version: string - requirement: - | { - type: 'opt-in' - how: string - } - | { - type: 'opt-out' - how: string - } - | { - type: 'required' - } - description: string | null - config: T -} diff --git a/frontend/projects/marketplace/src/types/marketplace-info.ts b/frontend/projects/marketplace/src/types/marketplace-info.ts deleted file mode 100644 index c97223305..000000000 --- a/frontend/projects/marketplace/src/types/marketplace-info.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface MarketplaceInfo { - name: string - categories: string[] -} diff --git a/frontend/projects/marketplace/src/types/marketplace-manifest.ts b/frontend/projects/marketplace/src/types/marketplace-manifest.ts deleted file mode 100644 index a8df045f3..000000000 --- a/frontend/projects/marketplace/src/types/marketplace-manifest.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Url } from '@start9labs/shared' -import { Dependency } from './dependency' - -export interface MarketplaceManifest { - id: string - title: string - version: string - description: { - short: string - long: string - } - 'release-notes': string - license: string // type of license - '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 - } - dependencies: Record> -} diff --git a/frontend/projects/marketplace/src/types/marketplace-pkg.ts b/frontend/projects/marketplace/src/types/marketplace-pkg.ts deleted file mode 100644 index 157ec16c0..000000000 --- a/frontend/projects/marketplace/src/types/marketplace-pkg.ts +++ /dev/null @@ -1,18 +0,0 @@ -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 - } - } - 'published-at': string -} diff --git a/frontend/projects/shared/src/public-api.ts b/frontend/projects/shared/src/public-api.ts index 75adead29..0745f5561 100644 --- a/frontend/projects/shared/src/public-api.ts +++ b/frontend/projects/shared/src/public-api.ts @@ -47,8 +47,9 @@ export * from './types/workspace-config' export * from './tokens/relative-url' -export * from './util/copy-to-clipboard' export * from './util/base-64' +export * from './util/copy-to-clipboard' +export * from './util/get-new-entries' export * from './util/get-pkg-id' export * from './util/misc.util' export * from './util/rpc.util' diff --git a/frontend/projects/shared/src/util/get-new-entries.ts b/frontend/projects/shared/src/util/get-new-entries.ts new file mode 100644 index 000000000..4afbe9975 --- /dev/null +++ b/frontend/projects/shared/src/util/get-new-entries.ts @@ -0,0 +1,12 @@ +export function getNewEntries>(prev: T, curr: T): T { + return Object.entries(curr).reduce( + (result, [key, value]) => + prev[key] + ? result + : { + ...result, + [key]: value, + }, + {} as T, + ) +} diff --git a/frontend/projects/ui/src/app/app-routing.module.ts b/frontend/projects/ui/src/app/app-routing.module.ts index 68cfd1044..1ca762699 100644 --- a/frontend/projects/ui/src/app/app-routing.module.ts +++ b/frontend/projects/ui/src/app/app-routing.module.ts @@ -24,6 +24,13 @@ const routes: Routes = [ m => m.ServerRoutingModule, ), }, + { + path: 'updates', + canActivate: [AuthGuard], + canActivateChild: [AuthGuard], + loadChildren: () => + import('./pages/updates/updates.module').then(m => m.UpdatesPageModule), + }, { path: 'marketplace', canActivate: [AuthGuard], diff --git a/frontend/projects/ui/src/app/app/menu/menu.component.html b/frontend/projects/ui/src/app/app/menu/menu.component.html index 7e3ca3ead..df11e72e3 100644 --- a/frontend/projects/ui/src/app/app/menu/menu.component.html +++ b/frontend/projects/ui/src/app/app/menu/menu.component.html @@ -29,9 +29,7 @@ name="rocket-outline" > {{ updateCount }} diff --git a/frontend/projects/ui/src/app/app/menu/menu.component.ts b/frontend/projects/ui/src/app/app/menu/menu.component.ts index 5ae4ba435..155a5c62a 100644 --- a/frontend/projects/ui/src/app/app/menu/menu.component.ts +++ b/frontend/projects/ui/src/app/app/menu/menu.component.ts @@ -1,12 +1,13 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' import { EOSService } from '../../services/eos.service' import { PatchDB } from 'patch-db-client' -import { iif, Observable } from 'rxjs' -import { filter, map, switchMap } from 'rxjs/operators' +import { combineLatest, map, Observable, of, startWith } from 'rxjs' import { AbstractMarketplaceService } from '@start9labs/marketplace' import { MarketplaceService } from 'src/app/services/marketplace.service' import { DataModel } from 'src/app/services/patch-db/data-model' import { SplitPaneTracker } from 'src/app/services/split-pane.service' +import { Emver } from '@start9labs/shared' +import { marketplaceSame, versionLower } from '../../pages/updates/updates.page' @Component({ selector: 'app-menu', @@ -26,6 +27,11 @@ export class MenuComponent { url: '/embassy', icon: 'cube-outline', }, + { + title: 'Updates', + url: '/updates', + icon: 'globe-outline', + }, { title: 'Marketplace', url: '/marketplace', @@ -47,21 +53,24 @@ export class MenuComponent { readonly showEOSUpdate$ = this.eosService.showUpdate$ - readonly updateCount$: Observable = this.patch - .watch$('ui', 'auto-check-updates') - .pipe( - filter(Boolean), - switchMap(() => - this.marketplaceService.getUpdates$().pipe( - map(arr => { - return arr.reduce( - (acc, marketplace) => acc + marketplace.pkgs.length, - 0, - ) - }), - ), + readonly updateCount$: Observable = combineLatest([ + this.marketplaceService.getMarketplace$(), + this.patch.watch$('package-data'), + ]).pipe( + map(([marketplace, local]) => + Object.entries(marketplace).reduce( + (length, [url, store]) => + length + + (store?.packages.filter( + ({ manifest }) => + marketplaceSame(manifest, local, url) && + versionLower(manifest, local, this.emver), + ).length || 0), + 0, ), - ) + ), + startWith(0), + ) readonly sidebarOpen$ = this.splitPane.sidebarOpen$ @@ -71,5 +80,6 @@ export class MenuComponent { @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, private readonly splitPane: SplitPaneTracker, + private readonly emver: Emver, ) {} } diff --git a/frontend/projects/ui/src/app/app/preloader/preloader.component.ts b/frontend/projects/ui/src/app/app/preloader/preloader.component.ts index 42767903f..75939bb62 100644 --- a/frontend/projects/ui/src/app/app/preloader/preloader.component.ts +++ b/frontend/projects/ui/src/app/app/preloader/preloader.component.ts @@ -25,7 +25,6 @@ const ICONS = [ 'cloud-offline-outline', 'cloud-upload-outline', 'code-outline', - 'cog-outline', 'color-wand-outline', 'construct-outline', 'copy-outline', @@ -56,12 +55,12 @@ const ICONS = [ 'newspaper-outline', 'notifications-outline', 'open-outline', - 'options-outline', 'pencil', 'phone-portrait-outline', 'play-circle-outline', 'play-outline', 'power', + 'pricetag-outline', 'pulse', 'push-outline', 'qr-code-outline', diff --git a/frontend/projects/ui/src/app/components/skeleton-list/skeleton-list.component.html b/frontend/projects/ui/src/app/components/skeleton-list/skeleton-list.component.html index 1ce63aa1c..5b875fa2c 100644 --- a/frontend/projects/ui/src/app/components/skeleton-list/skeleton-list.component.html +++ b/frontend/projects/ui/src/app/components/skeleton-list/skeleton-list.component.html @@ -2,14 +2,26 @@ - + + + + - + - + @@ -19,12 +31,21 @@ + + + - + - + - \ No newline at end of file + diff --git a/frontend/projects/ui/src/app/components/skeleton-list/skeleton-list.component.ts b/frontend/projects/ui/src/app/components/skeleton-list/skeleton-list.component.ts index 7042c0d05..d5e320caa 100644 --- a/frontend/projects/ui/src/app/components/skeleton-list/skeleton-list.component.ts +++ b/frontend/projects/ui/src/app/components/skeleton-list/skeleton-list.component.ts @@ -1,17 +1,18 @@ -import { Component, Input, OnChanges } from '@angular/core' +import { Component, Input } from '@angular/core' @Component({ selector: 'skeleton-list', templateUrl: './skeleton-list.component.html', styleUrls: ['./skeleton-list.component.scss'], }) -export class SkeletonListComponent implements OnChanges { +export class SkeletonListComponent { @Input() groups = 0 @Input() rows = 3 + @Input() showAvatar = false groupsArr: number[] = [] rowsArr: number[] = [] - ngOnChanges() { + ngOnInit() { this.groupsArr = Array(this.groups).fill(0) this.rowsArr = Array(this.rows).fill(0) } diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.ts index 834839ee1..f3aec5ee2 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.ts @@ -18,9 +18,9 @@ export class AppListPage { const length = next.length return !length || prev.length !== length }), - map(([_, pkgs]) => { - return pkgs.sort((a, b) => (b.manifest.title > a.manifest.title ? -1 : 1)) - }), + map(([_, pkgs]) => + pkgs.sort((a, b) => (b.manifest.title > a.manifest.title ? -1 : 1)), + ), ) constructor(private readonly patch: PatchDB) {} diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html b/frontend/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html index 5db570b23..d3b647673 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html @@ -11,7 +11,7 @@ - + {{ metric.key }} 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 aa2c40931..74578235c 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 @@ -29,13 +29,13 @@ - + @@ -43,7 +43,7 @@
{ + const categories = new Set() + if (info.categories.includes('featured')) categories.add('featured') + categories.add('updates') + info.categories.forEach(c => categories.add(c)) + categories.add('all') - private readonly categories$ = this.marketplaceService - .getMarketplaceInfo$() - .pipe( - map(({ categories }) => { - const set = new Set() - if (categories.includes('featured')) set.add('featured') - set.add('updates') - categories.forEach(c => set.add(c)) - set.add('all') - return set - }), - ) - - readonly marketplace$ = combineLatest([this.pkgs$, this.categories$]).pipe( - map(arr => { - return { pkgs: arr[0], categories: arr[1] } + return { categories, packages } }), ) readonly localPkgs$ = this.patch.watch$('package-data') - readonly details$ = this.marketplaceService.getUiMarketplace$().pipe( + readonly details$ = this.marketplaceService.getSelectedHost$().pipe( map(({ url, name }) => { let color: string let description: string 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 index 487486902..13b124187 100644 --- 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 @@ -62,7 +62,7 @@ export class MarketplaceShowControlsComponent { async tryInstall() { const currentMarketplace = await firstValueFrom( - this.marketplaceService.getUiMarketplace$(), + this.marketplaceService.getSelectedHost$(), ) const url = this.url || currentMarketplace.url 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 63a7c969c..51f376fd3 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 @@ -25,7 +25,7 @@ export class MarketplaceShowPage { readonly pkg$ = this.loadVersion$.pipe( switchMap(version => - this.marketplaceService.getPackage(this.pkgId, version, this.url), + this.marketplaceService.getPackage$(this.pkgId, version, this.url), ), ) diff --git a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts index 2eb19301b..914e3ec42 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts @@ -200,7 +200,7 @@ export class MarketplacesPage { loader.message = 'Validating marketplace...' await loader.present() - const name = await this.marketplaceService.validateMarketplace(url) + const name = await firstValueFrom(this.marketplaceService.fetchInfo$(url)) // Save loader.message = 'Saving...' diff --git a/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.module.ts b/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.module.ts deleted file mode 100644 index 7172c65a4..000000000 --- a/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { PreferencesPage } from './preferences.page' -import { Routes, RouterModule } from '@angular/router' -import { SharedPipesModule } from '@start9labs/shared' - -const routes: Routes = [ - { - path: '', - component: PreferencesPage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - SharedPipesModule, - ], - declarations: [PreferencesPage], -}) -export class PreferencesPageModule {} diff --git a/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.html b/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.html deleted file mode 100644 index 72159f3db..000000000 --- a/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - Preferences - - - - - - General - - Device Name - {{ name.current }} - - - Marketplace - - Auto Check for Updates - - {{ ui['auto-check-updates'] ? 'Enabled' : 'Disabled' }} - - - - diff --git a/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.scss b/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.ts b/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.ts deleted file mode 100644 index 03059ce67..000000000 --- a/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core' -import { PatchDB } from 'patch-db-client' -import { - LoadingController, - ModalController, - ToastController, -} from '@ionic/angular' -import { - GenericInputComponent, - GenericInputOptions, -} from 'src/app/modals/generic-input/generic-input.component' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ServerConfigService } from 'src/app/services/server-config.service' -import { ClientStorageService } from '../../../services/client-storage.service' -import { - ServerNameInfo, - ServerNameService, -} from 'src/app/services/server-name.service' -import { DataModel } from 'src/app/services/patch-db/data-model' - -@Component({ - selector: 'preferences', - templateUrl: './preferences.page.html', - styleUrls: ['./preferences.page.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class PreferencesPage { - clicks = 0 - - readonly ui$ = this.patch.watch$('ui') - readonly server$ = this.patch.watch$('server-info') - readonly name$ = this.serverNameService.name$ - - constructor( - private readonly loadingCtrl: LoadingController, - private readonly modalCtrl: ModalController, - private readonly api: ApiService, - private readonly toastCtrl: ToastController, - private readonly ClientStorageService: ClientStorageService, - private readonly patch: PatchDB, - private readonly serverNameService: ServerNameService, - readonly serverConfig: ServerConfigService, - ) {} - - async presentModalName(name: ServerNameInfo): Promise { - const options: GenericInputOptions = { - title: 'Edit Device Name', - message: 'This is for your reference only.', - label: 'Device Name', - useMask: false, - placeholder: name.default, - nullable: true, - initialValue: name.current, - buttonText: 'Save', - submitFn: (value: string) => - this.setDbValue('name', value || name.default), - } - - const modal = await this.modalCtrl.create({ - componentProps: { options }, - cssClass: 'alertlike-modal', - presentingElement: await this.modalCtrl.getTop(), - component: GenericInputComponent, - }) - - await modal.present() - } - - private async setDbValue(key: string, value: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Saving...', - }) - await loader.present() - - try { - await this.api.setDbValue([key], value) - } finally { - loader.dismiss() - } - } - - async addClick() { - this.clicks++ - if (this.clicks >= 5) { - this.clicks = 0 - const newVal = this.ClientStorageService.toggleShowDevTools() - const toast = await this.toastCtrl.create({ - header: newVal ? 'Dev tools unlocked' : 'Dev tools hidden', - position: 'bottom', - duration: 1000, - }) - - await toast.present() - } - setTimeout(() => { - this.clicks = Math.max(this.clicks - 1, 0) - }, 10000) - } -} diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-routing.module.ts b/frontend/projects/ui/src/app/pages/server-routes/server-routing.module.ts index 2549db866..7374d7d4c 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-routing.module.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-routing.module.ts @@ -48,13 +48,6 @@ const routes: Routes = [ m => m.ServerMetricsPageModule, ), }, - { - path: 'preferences', - loadChildren: () => - import('./preferences/preferences.module').then( - m => m.PreferencesPageModule, - ), - }, { path: 'restore', loadChildren: () => diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html index e231c9021..9f9884d41 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html @@ -24,10 +24,7 @@
- - {{ cat.key }} - - + {{ cat.key }} 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 6513a6321..11b59b72c 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 @@ -4,12 +4,13 @@ import { LoadingController, NavController, ModalController, + ToastController, } from '@ionic/angular' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ActivatedRoute } from '@angular/router' import { PatchDB } from 'patch-db-client' import { ServerNameService } from 'src/app/services/server-name.service' -import { Observable, of } from 'rxjs' +import { firstValueFrom, Observable, of } from 'rxjs' import { ErrorToastService } from '@start9labs/shared' import { EOSService } from 'src/app/services/eos.service' import { ClientStorageService } from 'src/app/services/client-storage.service' @@ -17,6 +18,10 @@ import { OSUpdatePage } from 'src/app/modals/os-update/os-update.page' import { getAllPackages } from '../../../util/get-package-data' import { AuthService } from 'src/app/services/auth.service' import { DataModel } from 'src/app/services/patch-db/data-model' +import { + GenericInputComponent, + GenericInputOptions, +} from 'src/app/modals/generic-input/generic-input.component' @Component({ selector: 'server-show', @@ -24,7 +29,8 @@ import { DataModel } from 'src/app/services/patch-db/data-model' styleUrls: ['server-show.page.scss'], }) export class ServerShowPage { - clicks = 0 + settingsClicks = 0 + powerClicks = 0 readonly server$ = this.patch.watch$('server-info') readonly name$ = this.serverNameService.name$ @@ -44,8 +50,35 @@ export class ServerShowPage { private readonly ClientStorageService: ClientStorageService, private readonly serverNameService: ServerNameService, private readonly authService: AuthService, + private readonly toastCtrl: ToastController, ) {} + async presentModalName(): Promise { + const name = await firstValueFrom(this.name$) + + const options: GenericInputOptions = { + title: 'Edit Device Name', + message: 'This is for your reference only.', + label: 'Device Name', + useMask: false, + placeholder: name.default, + nullable: true, + initialValue: name.current, + buttonText: 'Save', + submitFn: (value: string) => + this.setDbValue('name', value || name.default), + } + + const modal = await this.modalCtrl.create({ + componentProps: { options }, + cssClass: 'alertlike-modal', + presentingElement: await this.modalCtrl.getTop(), + component: GenericInputComponent, + }) + + await modal.present() + } + async updateEos(): Promise { const modal = await this.modalCtrl.create({ component: OSUpdatePage, @@ -170,6 +203,32 @@ export class ServerShowPage { await alert.present() } + addClick(title: string) { + switch (title) { + case 'Settings': + this.addSettingsClick() + break + case 'Power': + this.addPowerClick() + break + default: + return + } + } + + private async setDbValue(key: string, value: string): Promise { + const loader = await this.loadingCtrl.create({ + message: 'Saving...', + }) + await loader.present() + + try { + await this.embassyApi.setDbValue([key], value) + } finally { + loader.dismiss() + } + } + // should wipe cache independent of actual BE logout private logout() { this.embassyApi.logout({}).catch(e => console.error('Failed to log out', e)) @@ -311,7 +370,7 @@ export class ServerShowPage { { title: 'Software Update', description: 'Get the latest version of embassyOS', - icon: 'cog-outline', + icon: 'cloud-download-outline', action: () => this.eosService.updateAvailable$.getValue() ? this.updateEos() @@ -320,14 +379,11 @@ export class ServerShowPage { disabled$: this.eosService.updatingOrBackingUp$, }, { - title: 'Preferences', - description: 'Device name, background tasks', - icon: 'options-outline', - action: () => - this.navCtrl.navigateForward(['preferences'], { - relativeTo: this.route, - }), - detail: true, + title: 'Device Name', + description: 'Edit the local display name of your Embassy', + icon: 'pricetag-outline', + action: () => this.presentModalName(), + detail: false, disabled$: of(false), }, { @@ -504,19 +560,31 @@ export class ServerShowPage { ], } - asIsOrder() { - return 0 + private async addSettingsClick() { + this.settingsClicks++ + if (this.settingsClicks === 5) { + this.settingsClicks = 0 + const newVal = this.ClientStorageService.toggleShowDevTools() + const toast = await this.toastCtrl.create({ + header: newVal ? 'Dev tools unlocked' : 'Dev tools hidden', + position: 'bottom', + duration: 1000, + }) + + await toast.present() + } } - addClick() { - this.clicks++ - if (this.clicks >= 5) { - this.clicks = 0 + private addPowerClick() { + this.powerClicks++ + if (this.powerClicks === 5) { + this.powerClicks = 0 this.ClientStorageService.toggleShowDiskRepair() } - setTimeout(() => { - this.clicks = Math.max(this.clicks - 1, 0) - }, 10000) + } + + asIsOrder() { + return 0 } } diff --git a/frontend/projects/ui/src/app/pages/updates/updates.module.ts b/frontend/projects/ui/src/app/pages/updates/updates.module.ts new file mode 100644 index 000000000..4f247e2a8 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/updates/updates.module.ts @@ -0,0 +1,33 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { RouterModule, Routes } from '@angular/router' +import { FilterUpdatesPipe, UpdatesPage } from './updates.page' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' +import { MarkdownPipeModule, SharedPipesModule } from '@start9labs/shared' +import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module' +import { RoundProgressModule } from 'angular-svg-round-progressbar' +import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module' + +const routes: Routes = [ + { + path: '', + component: UpdatesPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + BadgeMenuComponentModule, + SharedPipesModule, + SkeletonListComponentModule, + MarkdownPipeModule, + RoundProgressModule, + InstallProgressPipeModule, + ], + declarations: [UpdatesPage, FilterUpdatesPipe], +}) +export class UpdatesPageModule {} diff --git a/frontend/projects/ui/src/app/pages/updates/updates.page.html b/frontend/projects/ui/src/app/pages/updates/updates.page.html new file mode 100644 index 000000000..467ee39c9 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/updates/updates.page.html @@ -0,0 +1,84 @@ + + + Updates + + + + + + + + + + {{ host.value }} + +
+ + Request Failed + + + + + + + + + + +

{{ pkg.manifest.title }}

+

+ {{ local.manifest.version }} +    + + {{ pkg.manifest.version }} + +

+

+
+ +
+ + + + + + Update + + + +
+
+
+ + +

All services are up to date!

+
+
+
+ + + + +
+
+
+
diff --git a/frontend/projects/ui/src/app/pages/updates/updates.page.scss b/frontend/projects/ui/src/app/pages/updates/updates.page.scss new file mode 100644 index 000000000..b1fcf9ea5 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/updates/updates.page.scss @@ -0,0 +1,12 @@ +ion-avatar { + position: absolute; + top: 6px; +} + +ion-label { + margin-left: 64px; +} + +.name:only-child { + display: none; +} diff --git a/frontend/projects/ui/src/app/pages/updates/updates.page.ts b/frontend/projects/ui/src/app/pages/updates/updates.page.ts new file mode 100644 index 000000000..80c292323 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/updates/updates.page.ts @@ -0,0 +1,95 @@ +import { Component, Inject } from '@angular/core' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { PatchDB } from 'patch-db-client' +import { + DataModel, + PackageDataEntry, + PackageState, +} from 'src/app/services/patch-db/data-model' +import { MarketplaceService } from 'src/app/services/marketplace.service' +import { + AbstractMarketplaceService, + Marketplace, + MarketplaceManifest, + MarketplacePkg, +} from '@start9labs/marketplace' +import { Emver } from '@start9labs/shared' +import { Pipe, PipeTransform } from '@angular/core' +import { combineLatest, Observable } from 'rxjs' +import { PrimaryRendering } from '../../services/pkg-status-rendering.service' + +interface UpdatesData { + hosts: Record + marketplace: Marketplace + localPkgs: Record + errors: string[] +} + +@Component({ + selector: 'updates', + templateUrl: 'updates.page.html', + styleUrls: ['updates.page.scss'], +}) +export class UpdatesPage { + queued: Record = {} + + readonly data$: Observable = combineLatest({ + hosts: this.marketplaceService.getKnownHosts$(), + marketplace: this.marketplaceService.getMarketplace$(), + localPkgs: this.patch.watch$('package-data'), + errors: this.marketplaceService.getRequestErrors$(), + }) + + readonly PackageState = PackageState + readonly rendering = PrimaryRendering[PackageState.Installing] + + constructor( + @Inject(AbstractMarketplaceService) + private readonly marketplaceService: MarketplaceService, + private readonly api: ApiService, + private readonly patch: PatchDB, + ) {} + + async update(id: string, url: string): Promise { + this.queued[id] = true + this.api.installPackage({ id, 'marketplace-url': url }) + } +} + +@Pipe({ + name: 'filterUpdates', +}) +export class FilterUpdatesPipe implements PipeTransform { + constructor(private readonly emver: Emver) {} + + transform( + pkgs: MarketplacePkg[], + local: Record = {}, + url: string, + ): MarketplacePkg[] { + return pkgs.filter( + ({ manifest }) => + marketplaceSame(manifest, local, url) && + versionLower(manifest, local, this.emver), + ) + } +} + +export function marketplaceSame( + { id }: MarketplaceManifest, + local: Record, + url: string, +): boolean { + return local[id]?.installed?.['marketplace-url'] === url +} + +export function versionLower( + { version, id }: MarketplaceManifest, + local: Record, + emver: Emver, +): boolean { + return ( + local[id].state === PackageState.Installing || + emver.compare(version, local[id].installed?.manifest.version || '') === 1 + ) +} diff --git a/frontend/projects/ui/src/app/pipes/install-progress/install-progress.module.ts b/frontend/projects/ui/src/app/pipes/install-progress/install-progress.module.ts index caf6a76f8..37bbd0744 100644 --- a/frontend/projects/ui/src/app/pipes/install-progress/install-progress.module.ts +++ b/frontend/projects/ui/src/app/pipes/install-progress/install-progress.module.ts @@ -1,8 +1,11 @@ import { NgModule } from '@angular/core' -import { InstallProgressPipe } from './install-progress.pipe' +import { + InstallProgressDisplayPipe, + InstallProgressPipe, +} from './install-progress.pipe' @NgModule({ - declarations: [InstallProgressPipe], - exports: [InstallProgressPipe], + declarations: [InstallProgressPipe, InstallProgressDisplayPipe], + exports: [InstallProgressPipe, InstallProgressDisplayPipe], }) export class InstallProgressPipeModule {} diff --git a/frontend/projects/ui/src/app/pipes/install-progress/install-progress.pipe.ts b/frontend/projects/ui/src/app/pipes/install-progress/install-progress.pipe.ts index e340c527d..459dc722b 100644 --- a/frontend/projects/ui/src/app/pipes/install-progress/install-progress.pipe.ts +++ b/frontend/projects/ui/src/app/pipes/install-progress/install-progress.pipe.ts @@ -3,11 +3,21 @@ import { InstallProgress } from 'src/app/services/patch-db/data-model' import { packageLoadingProgress } from 'src/app/util/package-loading-progress' @Pipe({ - name: 'installProgressDisplay', + name: 'installProgress', }) export class InstallProgressPipe implements PipeTransform { + transform(installProgress?: InstallProgress): number { + return packageLoadingProgress(installProgress)?.totalProgress || 0 + } +} + +@Pipe({ + name: 'installProgressDisplay', +}) +export class InstallProgressDisplayPipe implements PipeTransform { transform(installProgress?: InstallProgress): string { - const totalProgress = packageLoadingProgress(installProgress)?.totalProgress || 0 + const totalProgress = + packageLoadingProgress(installProgress)?.totalProgress || 0 return totalProgress < 99 ? totalProgress + '%' : 'finalizing' } 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 be9056f32..2b631f623 100644 --- a/frontend/projects/ui/src/app/services/api/api.fixures.ts +++ b/frontend/projects/ui/src/app/services/api/api.fixures.ts @@ -1787,7 +1787,7 @@ export module Mock { }, 'current-dependencies': {}, 'dependency-info': {}, - 'marketplace-url': 'https://marketplace-url.com', + 'marketplace-url': 'https://registry.start9.com/', 'developer-key': 'developer-key', }, 'install-progress': undefined, @@ -1836,7 +1836,7 @@ export module Mock { icon: 'assets/img/service-icons/bitcoind.png', }, }, - 'marketplace-url': 'https://marketplace-url.com', + 'marketplace-url': 'https://registry.start9.com/', 'developer-key': 'developer-key', }, 'install-progress': undefined, @@ -1896,7 +1896,7 @@ export module Mock { icon: 'assets/img/service-icons/btc-rpc-proxy.png', }, }, - 'marketplace-url': 'https://marketplace-url.com', + 'marketplace-url': 'https://registry.start9.com/', 'developer-key': 'developer-key', }, 'install-progress': undefined, 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 f1c6e85e1..96aa55f2f 100644 --- a/frontend/projects/ui/src/app/services/api/api.types.ts +++ b/frontend/projects/ui/src/app/services/api/api.types.ts @@ -1,5 +1,5 @@ import { Dump, Revision } from 'patch-db-client' -import { MarketplaceInfo, MarketplacePkg } from '@start9labs/marketplace' +import { MarketplacePkg, StoreInfo } from '@start9labs/marketplace' import { PackagePropertiesVersioned } from 'src/app/util/properties.util' import { ConfigSpec } from 'src/app/pkg-config/config-types' import { @@ -241,8 +241,8 @@ export module RR { // marketplace - export type GetMarketplaceDataReq = { 'server-id': string } - export type GetMarketplaceDataRes = MarketplaceInfo + export type GetMarketplaceInfoReq = { 'server-id': string } + export type GetMarketplaceInfoRes = StoreInfo export type GetMarketplaceEOSReq = { 'server-id': string 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 308d5b189..bb6c5f0cf 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 @@ -567,7 +567,6 @@ export class MockApiService extends ApiService { ...Mock.LocalPkgs[params.id], state: PackageState.Installing, 'install-progress': { ...PROGRESS }, - installed: undefined, }, }, ] diff --git a/frontend/projects/ui/src/app/services/api/mock-patch.ts b/frontend/projects/ui/src/app/services/api/mock-patch.ts index 83a938c97..752a5d8e5 100644 --- a/frontend/projects/ui/src/app/services/api/mock-patch.ts +++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts @@ -7,11 +7,11 @@ import { PackageMainStatus, PackageState, } from 'src/app/services/patch-db/data-model' +import { Mock } from './api.fixures' export const mockPatchData: DataModel = { ui: { name: `Matt's Embassy`, - 'auto-check-updates': true, 'pkg-order': [], 'ack-welcome': '1.0.0', marketplace: { @@ -382,7 +382,10 @@ export const mockPatchData: DataModel = { dependencies: {}, }, installed: { - manifest: {} as Manifest, + manifest: { + ...Mock.MockManifestBitcoind, + version: '0.20.0', + }, 'last-backup': null, status: { configured: true, @@ -434,7 +437,7 @@ export const mockPatchData: DataModel = { }, 'current-dependencies': {}, 'dependency-info': {}, - 'marketplace-url': 'https://marketplace-url.com', + 'marketplace-url': 'https://registry.start9.com/', 'developer-key': 'developer-key', }, }, @@ -648,7 +651,7 @@ export const mockPatchData: DataModel = { icon: 'assets/img/service-icons/btc-rpc-proxy.png', }, }, - 'marketplace-url': 'https://marketplace-url.com', + 'marketplace-url': 'https://registry.start9.com/', 'developer-key': 'developer-key', }, }, diff --git a/frontend/projects/ui/src/app/services/marketplace.service.ts b/frontend/projects/ui/src/app/services/marketplace.service.ts index eb643db9a..05ebdf34b 100644 --- a/frontend/projects/ui/src/app/services/marketplace.service.ts +++ b/frontend/projects/ui/src/app/services/marketplace.service.ts @@ -1,163 +1,131 @@ import { Injectable } from '@angular/core' -import { Emver } from '@start9labs/shared' import { MarketplacePkg, AbstractMarketplaceService, - MarketplaceInfo, + StoreData, + Marketplace, + StoreInfo, } from '@start9labs/marketplace' -import { combineLatest, from, Observable, of } from 'rxjs' +import { + BehaviorSubject, + combineLatest, + distinctUntilKeyChanged, + from, + mergeMap, + Observable, + of, + scan, +} from 'rxjs' import { RR } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { - DataModel, - Manifest, - PackageState, -} from 'src/app/services/patch-db/data-model' +import { DataModel } from 'src/app/services/patch-db/data-model' import { PatchDB } from 'patch-db-client' import { - distinctUntilChanged, + catchError, + filter, map, + pairwise, shareReplay, startWith, switchMap, - take, tap, } from 'rxjs/operators' -import { getServerInfo } from '../util/get-server-info' - -type MarketplaceURL = string -interface MarketplaceData { - info: MarketplaceInfo | null - packages: MarketplacePkg[] -} -type MasterCache = Map +import { getNewEntries } from '@start9labs/shared' @Injectable() export class MarketplaceService implements AbstractMarketplaceService { - private readonly cache: MasterCache = new Map() + private readonly knownHosts$ = this.patch.watch$( + 'ui', + 'marketplace', + 'known-hosts', + ) - private readonly uiMarketplace$: Observable<{ url: string; name: string }> = - this.patch.watch$('ui', 'marketplace').pipe( - distinctUntilChanged( - (prev, curr) => prev['selected-url'] === curr['selected-url'], - ), - map(data => { - const url = data['selected-url'] - return { - url, - name: data['known-hosts'][url], - } - }), - shareReplay(1), - ) - - private readonly marketplaceData$: Observable = - this.uiMarketplace$.pipe( - switchMap(({ url, name }) => - from(this.loadMarketplace(url)).pipe( - tap(data => { - this.updateName(url, name, data.info!.name) - }), - ), - ), - shareReplay(1), - ) - - private readonly marketplaceInfo$: Observable = - this.marketplaceData$.pipe(map(data => data.info!)) - - private readonly marketplacePkgs$: Observable = - this.marketplaceData$.pipe(map(data => data.packages)) - - private readonly updates$: Observable< - { url: string; pkgs: MarketplacePkg[] }[] - > = this.patch.watch$('package-data').pipe( - take(1), // check once per app instance - map(localPkgs => { - return Object.values(localPkgs) - .filter(localPkg => localPkg.state === PackageState.Installed) - .reduce((localPkgMap, pkg) => { - const url = pkg.installed!['marketplace-url'] || '' // side-laoded services will not have marketplace-url - const cached = this.cache - .get(url) - ?.packages.find(p => p.manifest.id === pkg.manifest.id) - if (url && !cached) { - const arr = localPkgMap.get(url) || [] - localPkgMap.set(url, arr.concat(pkg.manifest)) - } - return localPkgMap - }, new Map()) - }), - switchMap(localPkgMap => { - const urls = Array.from(localPkgMap.keys()) - const requests = urls.map(url => { - const ids = localPkgMap.get(url)?.map(({ id }) => { - return { id, version: '*' } - }) - return from(this.loadPackages({ ids }, url)).pipe( - map(pkgs => { - const manifests = localPkgMap.get(url)! - const filtered = pkgs.filter(pkg => { - const localVersion = manifests.find( - m => m.id === pkg.manifest.id, - )?.version - return ( - localVersion && - this.emver.compare(pkg.manifest.version, localVersion) === 1 - ) - }) - return { url, pkgs: filtered } - }), - startWith({ url, pkgs: [] }), // needed for combineLatest to emit right away - ) - }) - return combineLatest(requests) - }), + private readonly selectedHost$ = this.patch.watch$('ui', 'marketplace').pipe( + distinctUntilKeyChanged('selected-url'), + map(data => ({ + url: data['selected-url'], + name: data['known-hosts'][data['selected-url']], + })), shareReplay(1), ) + private readonly marketplace$ = this.knownHosts$.pipe( + startWith>({}), + pairwise(), + mergeMap(([prev, curr]) => from(Object.entries(getNewEntries(prev, curr)))), + mergeMap(([url, name]) => + this.fetchStore$(url).pipe( + map(data => { + if (data.info) this.updateName(url, name, data.info.name) + + return [url, data] + }), + startWith<[string, StoreData | null]>([url, null]), + ), + ), + scan<[string, StoreData | null], Record>( + (requests, [url, store]) => { + requests[url] = store + + return requests + }, + {}, + ), + shareReplay(1), + ) + + private readonly selectedStore$ = this.selectedHost$.pipe( + switchMap(({ url }) => this.marketplace$.pipe(map(m => m[url]))), + ) + + private readonly requestErrors$ = new BehaviorSubject([]) + constructor( private readonly api: ApiService, private readonly patch: PatchDB, - private readonly emver: Emver, ) {} - getUiMarketplace$(): Observable<{ url: string; name: string }> { - return this.uiMarketplace$ + getKnownHosts$(): Observable> { + return this.knownHosts$ } - getMarketplaceInfo$(): Observable { - return this.marketplaceInfo$ + getSelectedHost$(): Observable<{ url: string; name: string }> { + return this.selectedHost$ } - getPackages$(): Observable { - return this.marketplacePkgs$ + getMarketplace$(): Observable { + return this.marketplace$ } - getPackage( + getSelectedStore$(): Observable { + return this.selectedStore$ + } + + getPackage$( id: string, version: string, - url?: string, + optionalUrl?: string, ): Observable { - return this.uiMarketplace$.pipe( - switchMap(m => { - url = url || m.url - if (this.cache.has(url)) { - const pkg = this.getPkgFromCache(id, version, url) - if (pkg) return of(pkg) + return this.patch.watch$('ui', 'marketplace').pipe( + switchMap(marketplace => { + const url = optionalUrl || marketplace['selected-url'] + + if (version !== '*' || !marketplace['known-hosts'][url]) { + return this.fetchPackage$(id, version, url) } - if (version === '*') { - return from(this.loadPackage(id, url)) - } else { - return from(this.fetchPackage(id, version, url)) - } + return this.selectedStore$.pipe( + filter(Boolean), + map(s => s.packages.find(p => p.manifest.id === id)), + ) }), ) } - getUpdates$(): Observable<{ url: string; pkgs: MarketplacePkg[] }[]> { - return this.updates$ + // UI only + + getRequestErrors$(): Observable { + return this.requestErrors$ } async installPackage( @@ -174,16 +142,71 @@ export class MarketplaceService implements AbstractMarketplaceService { await this.api.installPackage(params) } - async validateMarketplace(url: string): Promise { - await this.loadInfo(url) - return this.cache.get(url)!.info!.name + fetchInfo$(url: string): Observable { + return this.patch + .watch$('server-info', 'id') + .pipe( + switchMap(id => + this.api.marketplaceProxy( + '/package/v0/info', + { 'server-d': id }, + url, + ), + ), + ) } - fetchReleaseNotes( + private fetchStore$(url: string): Observable { + return combineLatest([this.fetchInfo$(url), this.fetchPackages$(url)]).pipe( + map(([info, packages]) => ({ info, packages })), + catchError(e => { + this.requestErrors$.next(this.requestErrors$.value.concat(url)) + return of(e) + }), + ) + } + + private fetchPackages$( + url: string, + params: Omit< + RR.GetMarketplacePackagesReq, + 'eos-version-compat' | 'page' | 'per-page' + > = {}, + ): Observable { + return this.patch.watch$('server-info', 'eos-version-compat').pipe( + switchMap(versionCompat => { + const qp: RR.GetMarketplacePackagesReq = { + ...params, + 'eos-version-compat': versionCompat, + page: 1, + 'per-page': 100, + } + if (qp.ids) qp.ids = JSON.stringify(qp.ids) + + return this.api.marketplaceProxy( + '/package/v0/index', + qp, + url, + ) + }), + ) + } + + fetchPackage$( + id: string, + version: string, + url: string, + ): Observable { + return this.fetchPackages$(url, { ids: [{ id, version }] }).pipe( + map(pkgs => pkgs[0]), + ) + } + + fetchReleaseNotes$( id: string, url?: string, ): Observable> { - return this.uiMarketplace$.pipe( + return this.selectedHost$.pipe( switchMap(m => { return from( this.api.marketplaceProxy>( @@ -196,12 +219,8 @@ export class MarketplaceService implements AbstractMarketplaceService { ) } - fetchPackageMarkdown( - id: string, - type: string, - url?: string, - ): Observable { - return this.uiMarketplace$.pipe( + fetchStatic$(id: string, type: string, url?: string): Observable { + return this.selectedHost$.pipe( switchMap(m => { return from( this.api.marketplaceProxy( @@ -214,88 +233,6 @@ export class MarketplaceService implements AbstractMarketplaceService { ) } - private async loadMarketplace(url: string): Promise { - const cachedInfo = this.cache.get(url)?.info - const [info, packages] = await Promise.all([ - cachedInfo || this.loadInfo(url), - this.loadPackages({}, url), - ]) - return { info, packages } - } - - private async loadInfo(url: string): Promise { - const info = await this.fetchInfo(url) - this.updateCache(url, info) - return info - } - - private async loadPackage( - id: string, - url: string, - ): Promise { - const pkgs = await this.loadPackages({ ids: [{ id, version: '*' }] }, url) - return pkgs[0] - } - - private async loadPackages( - params: Omit< - RR.GetMarketplacePackagesReq, - 'eos-version-compat' | 'page' | 'per-page' - >, - url: string, - ): Promise { - const pkgs = await this.fetchPackages(params, url) - this.updateCache(url, undefined, pkgs) - return pkgs - } - - private async fetchInfo(url: string): Promise { - const { id } = await getServerInfo(this.patch) - - const params: RR.GetMarketplaceDataReq = { - 'server-id': id, - } - - return this.api.marketplaceProxy( - '/package/v0/info', - params, - url, - ) - } - - private async fetchPackage( - id: string, - version: string, - url: string, - ): Promise { - const pkgs = await this.fetchPackages({ ids: [{ id, version }] }, url) - return pkgs[0] - } - - private async fetchPackages( - params: Omit< - RR.GetMarketplacePackagesReq, - 'eos-version-compat' | 'page' | 'per-page' - >, - url: string, - ): Promise { - const qp: RR.GetMarketplacePackagesReq = { - ...params, - 'eos-version-compat': (await getServerInfo(this.patch))[ - 'eos-version-compat' - ], - page: 1, - 'per-page': 100, - } - if (qp.ids) qp.ids = JSON.stringify(qp.ids) - - return this.api.marketplaceProxy( - '/package/v0/index', - qp, - url, - ) - } - private async updateName( url: string, name: string, @@ -305,39 +242,4 @@ export class MarketplaceService implements AbstractMarketplaceService { this.api.setDbValue(['marketplace', 'known-hosts', url], newName) } } - - private getPkgFromCache( - id: string, - version: string, - url: string, - ): MarketplacePkg | undefined { - return this.cache.get(url)?.packages.find(p => { - const versionIsSame = - version === '*' || this.emver.compare(p.manifest.version, version) === 0 - - return p.manifest.id === id && versionIsSame - }) - } - - private updateCache( - url: string, - info?: MarketplaceInfo, - pkgs?: MarketplacePkg[], - ): void { - const cache = this.cache.get(url) - - let packages = cache?.packages || [] - if (pkgs) { - const filtered = packages.filter( - cachedPkg => - !pkgs.find(pkg => pkg.manifest.id === cachedPkg.manifest.id), - ) - packages = filtered.concat(pkgs) - } - - this.cache.set(url, { - info: info || cache?.info || null, - packages, - }) - } } diff --git a/frontend/projects/ui/src/app/services/patch-data.service.ts b/frontend/projects/ui/src/app/services/patch-data.service.ts index b624f213a..9eb6215c4 100644 --- a/frontend/projects/ui/src/app/services/patch-data.service.ts +++ b/frontend/projects/ui/src/app/services/patch-data.service.ts @@ -23,7 +23,7 @@ export class PatchDataService extends Observable { take(1), tap(({ ui }) => { // check for updates to EOS and services - this.checkForUpdates(ui) + this.checkForUpdates() // show eos welcome message this.showEosWelcome(ui['ack-welcome']) }), @@ -43,12 +43,9 @@ export class PatchDataService extends Observable { super(subscriber => this.stream$.subscribe(subscriber)) } - private checkForUpdates(ui: UIData): void { - if (ui['auto-check-updates'] !== false) { - this.eosService.getEOS() - this.marketplaceService.getMarketplaceInfo$().pipe(take(1)).subscribe() - this.marketplaceService.getUpdates$().pipe(take(1)).subscribe() - } + private checkForUpdates(): void { + this.eosService.getEOS() + this.marketplaceService.getMarketplace$().pipe(take(1)).subscribe() } private async showEosWelcome(ackVersion: string): Promise { 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 c861d8113..f6074011d 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 @@ -11,7 +11,6 @@ export interface DataModel { export interface UIData { name: string | null - 'auto-check-updates': boolean 'pkg-order': string[] 'ack-welcome': string // EOS emver marketplace: UIMarketplaceData diff --git a/frontend/projects/ui/src/app/services/server-config.service.ts b/frontend/projects/ui/src/app/services/server-config.service.ts deleted file mode 100644 index 7ee0d6cd3..000000000 --- a/frontend/projects/ui/src/app/services/server-config.service.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Injectable } from '@angular/core' -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 '@start9labs/shared' - -@Injectable({ - providedIn: 'root', -}) -export class ServerConfigService { - constructor( - private readonly loadingCtrl: LoadingController, - private readonly errToast: ErrorToastService, - private readonly alertCtrl: AlertController, - private readonly embassyApi: ApiService, - ) {} - - async presentAlert( - key: string, - current?: any, - ): Promise { - const spec = serverConfig[key] - - let inputs: AlertInput[] - let buttons: AlertButton[] = [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Save', - handler: async (data: any) => { - const loader = await this.loadingCtrl.create({ - message: 'Saving...', - }) - loader.present() - - try { - await this.saveFns[key](data) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - }, - cssClass: 'enter-click', - }, - ] - - switch (spec.type) { - case 'boolean': - inputs = [ - { - name: 'enabled', - type: 'radio', - label: 'Enabled', - value: true, - checked: current, - }, - { - name: 'disabled', - type: 'radio', - label: 'Disabled', - value: false, - checked: !current, - }, - ] - break - default: - return null - } - - const alert = await this.alertCtrl.create({ - header: spec.name, - message: spec.description, - inputs, - buttons, - }) - await alert.present() - return alert - } - - // async presentModalForm (key: string) { - // const modal = await this.modalCtrl.create({ - // component: AppActionInputPage, - // componentProps: { - // title: serverConfig[key].name, - // spec: (serverConfig[key] as ValueSpecObject).spec, - // }, - // }) - - // modal.onWillDismiss().then(res => { - // if (!res.data) return - // this.saveFns[key](res.data) - // }) - - // await modal.present() - // } - - saveFns: { [key: string]: (val: any) => Promise } = { - 'auto-check-updates': async (enabled: boolean) => { - return this.embassyApi.setDbValue(['auto-check-updates'], enabled) - }, - } -} - -export const serverConfig: ConfigSpec = { - 'auto-check-updates': { - type: 'boolean', - name: 'Auto Check for Updates', - description: - 'If enabled, embassyOS will automatically check for updates of itself and installed services. Updating will still require your approval and action. Updates will never be performed automatically.', - default: true, - }, -}