diff --git a/frontend/config-sample.json b/frontend/config-sample.json index 26699cc4b..51feaef8b 100644 --- a/frontend/config-sample.json +++ b/frontend/config-sample.json @@ -9,10 +9,6 @@ "mocks": { "maskAs": "tor", "skipStartupAlerts": true - }, - "marketplace": { - "url": "https://registry.start9.com/", - "name": "Start9 Marketplace" } }, "gitHash": "" diff --git a/frontend/patchdb-ui-seed.json b/frontend/patchdb-ui-seed.json index 68d5e0831..ac93a8b88 100644 --- a/frontend/patchdb-ui-seed.json +++ b/frontend/patchdb-ui-seed.json @@ -1,11 +1,13 @@ { "name": null, "auto-check-updates": true, - "pkg-order": [], - "ack-welcome": "0.3.2.1", + "ack-welcome": "0.3.3", "marketplace": { - "selected-id": null, - "known-hosts": {} + "selected-url": "https://registry.start9.com/", + "known-hosts": { + "https://registry.start9.com/": "Start9 Marketplace", + "https://community-registry.start9.com/": "Community Marketplace" + } }, "dev": {}, "gaming": { diff --git a/frontend/projects/marketplace/src/pages/list/categories/categories.component.ts b/frontend/projects/marketplace/src/pages/list/categories/categories.component.ts index 3647bd23b..d6ce907a2 100644 --- a/frontend/projects/marketplace/src/pages/list/categories/categories.component.ts +++ b/frontend/projects/marketplace/src/pages/list/categories/categories.component.ts @@ -17,13 +17,13 @@ import { }) export class CategoriesComponent { @Input() - categories = new Set() + categories!: Set @Input() - category = '' + category!: string @Input() - updatesAvailable = 0 + updatesAvailable!: number @Output() readonly categoryChange = new EventEmitter() 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 d841461bd..891be5198 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.getReleaseNotes(this.pkgId) + readonly notes$ = this.marketplaceService.fetchReleaseNotes(this.pkgId) constructor( private readonly route: ActivatedRoute, 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 ba38cc80b..a05d0779a 100644 --- a/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts +++ b/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts @@ -7,7 +7,6 @@ import { } from '@angular/core' import { AlertController, ModalController } from '@ionic/angular' import { displayEmver, Emver, MarkdownComponent } from '@start9labs/shared' - import { AbstractMarketplaceService } from '../../../services/marketplace.service' import { MarketplacePkg } from '../../../types/marketplace-pkg' @@ -58,9 +57,9 @@ export class AdditionalComponent { } async presentModalMd(title: string) { - const content = this.marketplaceService.getPackageMarkdown( - title, + const content = this.marketplaceService.fetchPackageMarkdown( this.pkg.manifest.id, + title, ) const modal = await this.modalCtrl.create({ diff --git a/frontend/projects/marketplace/src/pages/show/package/package.component.html b/frontend/projects/marketplace/src/pages/show/package/package.component.html index 91b969292..6c1cdf677 100644 --- a/frontend/projects/marketplace/src/pages/show/package/package.component.html +++ b/frontend/projects/marketplace/src/pages/show/package/package.component.html @@ -7,8 +7,7 @@

{{ pkg.manifest.title }}

{{ pkg.manifest.version | displayEmver }}

- -

+

Released: {{ pkg['published-at'] | date: 'medium' }}

diff --git a/frontend/projects/marketplace/src/pipes/filter-packages.pipe.ts b/frontend/projects/marketplace/src/pipes/filter-packages.pipe.ts index 3626b1e29..74b256ac5 100644 --- a/frontend/projects/marketplace/src/pipes/filter-packages.pipe.ts +++ b/frontend/projects/marketplace/src/pipes/filter-packages.pipe.ts @@ -1,9 +1,8 @@ import { NgModule, Pipe, PipeTransform } from '@angular/core' -import Fuse from 'fuse.js' - import { MarketplacePkg } from '../types/marketplace-pkg' import { MarketplaceManifest } from '../types/marketplace-manifest' import { Emver } from '@start9labs/shared' +import Fuse from 'fuse.js' @Pipe({ name: 'filterPackages', diff --git a/frontend/projects/marketplace/src/public-api.ts b/frontend/projects/marketplace/src/public-api.ts index 2bc7bee38..677993ff0 100644 --- a/frontend/projects/marketplace/src/public-api.ts +++ b/frontend/projects/marketplace/src/public-api.ts @@ -26,7 +26,6 @@ export * from './pipes/filter-packages.pipe' export * from './services/marketplace.service' export * from './types/dependency' -export * from './types/marketplace' -export * from './types/marketplace-data' +export * from './types/marketplace-info' export * from './types/marketplace-manifest' export * from './types/marketplace-pkg' diff --git a/frontend/projects/marketplace/src/services/marketplace.service.ts b/frontend/projects/marketplace/src/services/marketplace.service.ts index 12d97f150..5813119da 100644 --- a/frontend/projects/marketplace/src/services/marketplace.service.ts +++ b/frontend/projects/marketplace/src/services/marketplace.service.ts @@ -1,20 +1,26 @@ import { Observable } from 'rxjs' +import { MarketplaceInfo } from '../types/marketplace-info' import { MarketplacePkg } from '../types/marketplace-pkg' -import { Marketplace } from '../types/marketplace' export abstract class AbstractMarketplaceService { - abstract getMarketplace(): Observable + abstract getMarketplaceInfo$(): Observable - abstract getReleaseNotes(id: string): Observable> - - abstract getCategories(): Observable> - - abstract getPackages(): Observable - - abstract getPackageMarkdown(type: string, pkgId: string): Observable + abstract getPackages$(): Observable abstract getPackage( id: string, version: string, - ): Observable + url?: string, + ): Observable + + abstract fetchReleaseNotes( + id: string, + url?: string, + ): Observable> + + abstract fetchPackageMarkdown( + id: string, + type: string, + url?: string, + ): Observable } diff --git a/frontend/projects/marketplace/src/types/marketplace-data.ts b/frontend/projects/marketplace/src/types/marketplace-info.ts similarity index 53% rename from frontend/projects/marketplace/src/types/marketplace-data.ts rename to frontend/projects/marketplace/src/types/marketplace-info.ts index d2868b393..c97223305 100644 --- a/frontend/projects/marketplace/src/types/marketplace-data.ts +++ b/frontend/projects/marketplace/src/types/marketplace-info.ts @@ -1,4 +1,4 @@ -export interface MarketplaceData { - categories: string[] +export interface MarketplaceInfo { name: string + categories: string[] } diff --git a/frontend/projects/marketplace/src/types/marketplace.ts b/frontend/projects/marketplace/src/types/marketplace.ts deleted file mode 100644 index b610b83a0..000000000 --- a/frontend/projects/marketplace/src/types/marketplace.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface Marketplace { - url: string - name: string -} diff --git a/frontend/projects/shared/src/pipes/emver/emver.pipe.ts b/frontend/projects/shared/src/pipes/emver/emver.pipe.ts index 5c32f1675..183c4a0a8 100644 --- a/frontend/projects/shared/src/pipes/emver/emver.pipe.ts +++ b/frontend/projects/shared/src/pipes/emver/emver.pipe.ts @@ -26,7 +26,7 @@ export class EmverComparesPipe implements PipeTransform { try { return this.emver.compare(first, second) as SemverResult } catch (e) { - console.warn(`emver comparison failed`, e, first, second) + console.error(`emver comparison failed`, e, first, second) return 'comparison-impossible' } } diff --git a/frontend/projects/shared/src/services/http.service.ts b/frontend/projects/shared/src/services/http.service.ts index 3357ed7ff..99d3647b7 100644 --- a/frontend/projects/shared/src/services/http.service.ts +++ b/frontend/projects/shared/src/services/http.service.ts @@ -36,8 +36,7 @@ export class HttpService { @Inject(DOCUMENT) private readonly document: Document, private readonly http: HttpClient, ) { - const { protocol, hostname, port } = this.document.location - this.fullUrl = `${protocol}//${hostname}:${port}` + this.fullUrl = this.document.location.origin } async rpcRequest( diff --git a/frontend/projects/shared/src/types/workspace-config.ts b/frontend/projects/shared/src/types/workspace-config.ts index ed3a1e269..1ce18c575 100644 --- a/frontend/projects/shared/src/types/workspace-config.ts +++ b/frontend/projects/shared/src/types/workspace-config.ts @@ -12,9 +12,5 @@ export type WorkspaceConfig = { maskAs: 'tor' | 'lan' skipStartupAlerts: boolean } - marketplace: { - url: string - name: string - } } } 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 23aefcc25..5ae4ba435 100644 --- a/frontend/projects/ui/src/app/app/menu/menu.component.ts +++ b/frontend/projects/ui/src/app/app/menu/menu.component.ts @@ -1,8 +1,8 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' import { EOSService } from '../../services/eos.service' import { PatchDB } from 'patch-db-client' -import { Observable } from 'rxjs' -import { map } from 'rxjs/operators' +import { iif, Observable } from 'rxjs' +import { filter, map, switchMap } from 'rxjs/operators' import { AbstractMarketplaceService } from '@start9labs/marketplace' import { MarketplaceService } from 'src/app/services/marketplace.service' import { DataModel } from 'src/app/services/patch-db/data-model' @@ -47,9 +47,21 @@ export class MenuComponent { readonly showEOSUpdate$ = this.eosService.showUpdate$ - readonly updateCount$: Observable = this.marketplaceService - .getUpdates() - .pipe(map(pkgs => pkgs.length)) + 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 sidebarOpen$ = this.splitPane.sidebarOpen$ diff --git a/frontend/projects/ui/src/app/app/snek/snek.directive.ts b/frontend/projects/ui/src/app/app/snek/snek.directive.ts index 7d167d236..af9d26d46 100644 --- a/frontend/projects/ui/src/app/app/snek/snek.directive.ts +++ b/frontend/projects/ui/src/app/app/snek/snek.directive.ts @@ -38,10 +38,10 @@ export class SnekDirective { await loader.present() try { - await this.embassyApi.setDbValue({ - pointer: '/gaming/snake/high-score', - value: data.highScore, - }) + await this.embassyApi.setDbValue( + ['gaming', 'snake', 'high-score'], + data.highScore, + ) } catch (e: any) { this.errToast.present(e) } finally { diff --git a/frontend/projects/ui/src/app/modals/action-marketplace/action-marketplace.component.html b/frontend/projects/ui/src/app/modals/action-marketplace/action-marketplace.component.html deleted file mode 100644 index cf7c1ad46..000000000 --- a/frontend/projects/ui/src/app/modals/action-marketplace/action-marketplace.component.html +++ /dev/null @@ -1,38 +0,0 @@ - - - {{ title }} - - - - - - - - - -
-
-

This service was installed from:

-

{{ packageMarketplace }}

-

But you are currently connected to:

-

{{ currentMarketplace }}

-
-
-

Switch to {{ packageMarketplace }} in

- Marketplace Settings -

Or you can

- Continue to {{ currentMarketplace }} -
-
-
-
diff --git a/frontend/projects/ui/src/app/modals/action-marketplace/action-marketplace.component.module.ts b/frontend/projects/ui/src/app/modals/action-marketplace/action-marketplace.component.module.ts deleted file mode 100644 index fb8936cf4..000000000 --- a/frontend/projects/ui/src/app/modals/action-marketplace/action-marketplace.component.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { RouterModule } from '@angular/router' -import { ActionMarketplaceComponent } from './action-marketplace.component' - -@NgModule({ - declarations: [ActionMarketplaceComponent], - imports: [CommonModule, IonicModule, RouterModule.forChild([])], - exports: [ActionMarketplaceComponent], -}) -export class ActionMarketplaceComponentModule {} diff --git a/frontend/projects/ui/src/app/modals/action-marketplace/action-marketplace.component.scss b/frontend/projects/ui/src/app/modals/action-marketplace/action-marketplace.component.scss deleted file mode 100644 index 1ad7fb8e6..000000000 --- a/frontend/projects/ui/src/app/modals/action-marketplace/action-marketplace.component.scss +++ /dev/null @@ -1,17 +0,0 @@ -.center { - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; -} - -.text-center { - text-align: center; -} - -.card { - background: rgba(53, 56, 62, 0.768); - border-radius: 7px; - padding: 27px; - margin-bottom: 24px; -} \ No newline at end of file diff --git a/frontend/projects/ui/src/app/modals/action-marketplace/action-marketplace.component.ts b/frontend/projects/ui/src/app/modals/action-marketplace/action-marketplace.component.ts deleted file mode 100644 index 6759f4b89..000000000 --- a/frontend/projects/ui/src/app/modals/action-marketplace/action-marketplace.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Component, Input } from '@angular/core' -import { ModalController } from '@ionic/angular' - -@Component({ - selector: 'action-marketplace', - templateUrl: './action-marketplace.component.html', - styleUrls: ['./action-marketplace.component.scss'], -}) -export class ActionMarketplaceComponent { - @Input() title!: string - @Input() packageMarketplace!: string - @Input() currentMarketplace!: string - @Input() pkgId!: string - - constructor(private readonly modalCtrl: ModalController) {} - - dismiss() { - this.modalCtrl.dismiss() - } -} diff --git a/frontend/projects/ui/src/app/modals/os-update/os-update.page.ts b/frontend/projects/ui/src/app/modals/os-update/os-update.page.ts index 6d330a861..c60da3c32 100644 --- a/frontend/projects/ui/src/app/modals/os-update/os-update.page.ts +++ b/frontend/projects/ui/src/app/modals/os-update/os-update.page.ts @@ -1,35 +1,39 @@ -import { Component, Input } from '@angular/core' +import { ChangeDetectionStrategy, Component } from '@angular/core' import { LoadingController, ModalController } from '@ionic/angular' -import { ConfigService } from '../../services/config.service' import { ApiService } from '../../services/api/embassy-api.service' import { ErrorToastService } from '@start9labs/shared' +import { EOSService } from 'src/app/services/eos.service' +import { PatchDB } from 'patch-db-client' +import { DataModel } from 'src/app/services/patch-db/data-model' @Component({ selector: 'os-update', templateUrl: './os-update.page.html', styleUrls: ['./os-update.page.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class OSUpdatePage { - @Input() releaseNotes!: { [version: string]: string } - versions: { version: string; notes: string }[] = [] constructor( private readonly modalCtrl: ModalController, private readonly loadingCtrl: LoadingController, - private readonly config: ConfigService, private readonly errToast: ErrorToastService, private readonly embassyApi: ApiService, + private readonly eosService: EOSService, + private readonly patch: PatchDB, ) {} ngOnInit() { - this.versions = Object.keys(this.releaseNotes) + const releaseNotes = this.eosService.eos?.['release-notes']! + + this.versions = Object.keys(releaseNotes) .sort() .reverse() .map(version => { return { version, - notes: this.releaseNotes[version], + notes: releaseNotes[version], } }) } @@ -45,9 +49,7 @@ export class OSUpdatePage { await loader.present() try { - await this.embassyApi.updateServer({ - 'marketplace-url': this.config.marketplace.url, - }) + await this.embassyApi.updateServer() this.dismiss() } catch (e: any) { this.errToast.present(e) 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 ce2c4048d..2d279c735 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 @@ -161,10 +161,7 @@ export class AppActionsPage { try { await this.embassyApi.uninstallPackage({ id: this.pkgId }) this.embassyApi - .setDbValue({ - pointer: `/ack-instructions/${this.pkgId}`, - value: false, - }) + .setDbValue(['ack-instructions', this.pkgId], false) .catch(e => console.error('Failed to mark instructions as unseen', e)) this.navCtrl.navigateRoot('/services') } catch (e: any) { diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.html b/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.html index b6e7b9550..e2ef17910 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.html +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.html @@ -8,15 +8,10 @@ - - - - - - + @@ -36,5 +31,10 @@ + + + + + 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 1776c80e0..834839ee1 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 @@ -1,46 +1,27 @@ -import { Component } from '@angular/core' +import { ChangeDetectionStrategy, Component } from '@angular/core' import { PatchDB } from 'patch-db-client' -import { - DataModel, - PackageDataEntry, -} from 'src/app/services/patch-db/data-model' -import { filter, takeUntil, tap } from 'rxjs/operators' -import { DestroyService } from '@start9labs/shared' -import { ApiService } from 'src/app/services/api/embassy-api.service' +import { DataModel } from 'src/app/services/patch-db/data-model' +import { filter, map, pairwise, startWith } from 'rxjs/operators' @Component({ selector: 'app-list', templateUrl: './app-list.page.html', styleUrls: ['./app-list.page.scss'], - providers: [DestroyService], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppListPage { - loading = true - pkgs: readonly PackageDataEntry[] = [] + readonly pkgs$ = this.patch.watch$('package-data').pipe( + map(pkgs => Object.values(pkgs)), + startWith([]), + pairwise(), + filter(([prev, next]) => { + const length = next.length + return !length || prev.length !== length + }), + map(([_, pkgs]) => { + return pkgs.sort((a, b) => (b.manifest.title > a.manifest.title ? -1 : 1)) + }), + ) - constructor( - private readonly api: ApiService, - private readonly destroy$: DestroyService, - private readonly patch: PatchDB, - ) {} - - get empty(): boolean { - return !this.pkgs.length - } - - ngOnInit() { - this.patch - .watch$('package-data') - .pipe( - filter(pkgs => Object.keys(pkgs).length !== this.pkgs.length), - tap(pkgs => { - this.loading = false - this.pkgs = Object.values(pkgs).sort((a, b) => - b.manifest.title > a.manifest.title ? -1 : 1, - ) - }), - takeUntil(this.destroy$), - ) - .subscribe() - } + constructor(private readonly patch: PatchDB) {} } diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts index dfce4cadc..6f6a2dcff 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts @@ -20,7 +20,6 @@ import { ToButtonsPipe } from './pipes/to-buttons.pipe' import { ToDependenciesPipe } from './pipes/to-dependencies.pipe' import { ToStatusPipe } from './pipes/to-status.pipe' import { ProgressDataPipe } from './pipes/progress-data.pipe' -import { ActionMarketplaceComponentModule } from 'src/app/modals/action-marketplace/action-marketplace.component.module' const routes: Routes = [ { @@ -54,7 +53,6 @@ const routes: Routes = [ EmverPipesModule, LaunchablePipeModule, UiPipeModule, - ActionMarketplaceComponentModule, ], }) export class AppShowPageModule {} diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html index 7775dafe0..09797f608 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html @@ -22,9 +22,7 @@ [dependencies]="dependencies" > - + diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts index a65072a9f..878c8539f 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' +import { ChangeDetectionStrategy, Component } from '@angular/core' import { NavController } from '@ionic/angular' import { PatchDB } from 'patch-db-client' import { @@ -14,8 +14,6 @@ import { import { filter, tap } from 'rxjs/operators' import { ActivatedRoute } from '@angular/router' import { getPkgId } from '@start9labs/shared' -import { MarketplaceService } from 'src/app/services/marketplace.service' -import { AbstractMarketplaceService } from '@start9labs/marketplace' const STATES = [ PackageState.Installing, @@ -49,16 +47,10 @@ export class AppShowPage { ), ) - readonly currentMarketplace$ = this.marketplaceService.getMarketplace() - - readonly altMarketplaceData$ = this.marketplaceService.getAltMarketplaceData() - constructor( private readonly route: ActivatedRoute, private readonly navCtrl: NavController, private readonly patch: PatchDB, - @Inject(AbstractMarketplaceService) - private readonly marketplaceService: MarketplaceService, ) {} isInstalled({ state }: PackageDataEntry): boolean { diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts index ba6c0b153..f8811ad6d 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts @@ -2,17 +2,14 @@ import { Inject, Pipe, PipeTransform } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { DOCUMENT } from '@angular/common' import { AlertController, ModalController, NavController } from '@ionic/angular' -import { getUrlHostname, MarkdownComponent } from '@start9labs/shared' +import { MarkdownComponent } from '@start9labs/shared' import { DataModel, PackageDataEntry, - UIMarketplaceData, } from 'src/app/services/patch-db/data-model' import { ModalService } from 'src/app/services/modal.service' import { ApiService } from 'src/app/services/api/embassy-api.service' import { from, map, Observable } from 'rxjs' -import { Marketplace } from '@start9labs/marketplace' -import { ActionMarketplaceComponent } from 'src/app/modals/action-marketplace/action-marketplace.component' import { PatchDB } from 'patch-db-client' export interface Button { @@ -39,11 +36,7 @@ export class ToButtonsPipe implements PipeTransform { private readonly patch: PatchDB, ) {} - transform( - pkg: PackageDataEntry, - currentMarketplace: Marketplace | null, - altMarketplaces: UIMarketplaceData | null | undefined, - ): Button[] { + transform(pkg: PackageDataEntry): Button[] { const pkgTitle = pkg.manifest.title return [ @@ -103,7 +96,7 @@ export class ToButtonsPipe implements PipeTransform { icon: 'receipt-outline', }, // view in marketplace - this.viewInMarketplaceButton(pkg, currentMarketplace, altMarketplaces), + this.viewInMarketplaceButton(pkg), // donate { action: () => this.donate(pkg), @@ -116,10 +109,7 @@ export class ToButtonsPipe implements PipeTransform { private async presentModalInstructions(pkg: PackageDataEntry) { this.apiService - .setDbValue({ - pointer: `/ack-instructions/${pkg.manifest.id}`, - value: true, - }) + .setDbValue(['ack-instructions', pkg.manifest.id], true) .catch(e => console.error('Failed to mark instructions as seen', e)) const modal = await this.modalCtrl.create({ @@ -135,51 +125,27 @@ export class ToButtonsPipe implements PipeTransform { await modal.present() } - private viewInMarketplaceButton( - pkg: PackageDataEntry, - currentMarketplace: Marketplace | null, - altMarketplaces: UIMarketplaceData | null | undefined, - ): Button { - const pkgMarketplaceUrl = pkg.installed?.['marketplace-url'] - // default button if package marketplace and current marketplace are the same + private viewInMarketplaceButton(pkg: PackageDataEntry): Button { + const url = pkg.installed?.['marketplace-url'] + const queryParams = url ? { url } : {} + let button: Button = { title: 'Marketplace', icon: 'storefront-outline', action: () => - this.navCtrl.navigateForward([`marketplace/${pkg.manifest.id}`]), + this.navCtrl.navigateForward([`marketplace/${pkg.manifest.id}`], { + queryParams, + }), disabled: false, description: 'View service in marketplace', } - if (!pkgMarketplaceUrl) { + + if (!url) { button.disabled = true button.description = 'This package was not installed from a marketplace.' button.action = () => {} - } else if ( - pkgMarketplaceUrl && - currentMarketplace && - getUrlHostname(pkgMarketplaceUrl) !== - getUrlHostname(currentMarketplace.url) - ) { - // attempt to get name for pkg marketplace - let pkgMarketplaceName = getUrlHostname(pkgMarketplaceUrl) - if (altMarketplaces) { - const pkgMarketplaces = Object.values( - altMarketplaces['known-hosts'], - ).filter(m => getUrlHostname(m.url) === pkgMarketplaceName) - if (pkgMarketplaces.length) { - // if multiple of the same url exist, they will have the same name, so fine to grab first - pkgMarketplaceName = pkgMarketplaces[0].name - } - } - - button.action = () => - this.differentMarketplaceAction( - pkgMarketplaceName, - currentMarketplace.name, - pkg.manifest.id, - ) - button.description = 'Service was installed from a different marketplace' } + return button } @@ -195,22 +161,4 @@ export class ToButtonsPipe implements PipeTransform { await alert.present() } } - - private async differentMarketplaceAction( - packageMarketplace: string, - currentMarketplace: string, - pkgId: string, - ) { - const modal = await this.modalCtrl.create({ - component: ActionMarketplaceComponent, - componentProps: { - title: 'Marketplace Conflict', - packageMarketplace, - currentMarketplace, - pkgId, - }, - cssClass: 'medium-modal', - }) - await modal.present() - } } diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts index e8c34844a..8ba9bd4f3 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts @@ -5,8 +5,8 @@ import { PackageDataEntry, PackageMainStatus, } from 'src/app/services/patch-db/data-model' -import { exists, isEmptyObject } from '@start9labs/shared' -import { filter, map, startWith } from 'rxjs/operators' +import { isEmptyObject } from '@start9labs/shared' +import { map, startWith } from 'rxjs/operators' import { PatchDB } from 'patch-db-client' import { Observable } from 'rxjs' @@ -27,7 +27,6 @@ export class ToHealthChecksPipe implements PipeTransform { const healthChecks$ = this.patch .watch$('package-data', pkg.manifest.id, 'installed', 'status', 'main') .pipe( - filter(obj => exists(obj)), map(main => { // Question: is this ok or do we have to use Object.keys // to maintain order and the keys initially present in pkg? 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 8a4a5ceec..c0cf5f40a 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 @@ -69,10 +69,7 @@ export class DevConfigPage { async save() { this.saving = true try { - await this.api.setDbValue({ - pointer: `/dev/${this.projectId}/config`, - value: this.code, - }) + await this.api.setDbValue(['dev', this.projectId, 'config'], this.code) } catch (e: any) { this.errToast.present(e) } finally { 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 7686f6542..e3b5c23c0 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 @@ -56,10 +56,10 @@ export class DevInstructionsPage { async save() { this.saving = true try { - await this.api.setDbValue({ - pointer: `/dev/${this.projectId}/instructions`, - value: this.code, - }) + await this.api.setDbValue( + ['dev', this.projectId, 'instructions'], + this.code, + ) } catch (e: any) { this.errToast.present(e) } finally { 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 1c9bd14d0..ee1035135 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 @@ -148,7 +148,7 @@ export class DeveloperListPage { .replace(/warning:/g, '# Optional\n warning:') const def = { name, config, instructions: SAMPLE_INSTUCTIONS } - await this.api.setDbValue({ pointer: `/dev/${id}`, value: def }) + await this.api.setDbValue(['dev', id], def) } catch (e: any) { this.errToast.present(e) } finally { @@ -184,7 +184,7 @@ export class DeveloperListPage { await loader.present() try { - await this.api.setDbValue({ pointer: `/dev/${id}/name`, value: newName }) + await this.api.setDbValue(['dev', id, 'name'], newName) } catch (e: any) { this.errToast.present(e) } finally { @@ -201,7 +201,7 @@ export class DeveloperListPage { try { const devDataToSave: DevData = JSON.parse(JSON.stringify(this.devData)) delete devDataToSave[id] - await this.api.setDbValue({ pointer: `/dev`, value: devDataToSave }) + await this.api.setDbValue(['dev'], devDataToSave) } catch (e: any) { this.errToast.present(e) } finally { 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 168933823..a992a9056 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 @@ -55,10 +55,10 @@ export class DeveloperMenuPage { await loader.present() try { - await this.api.setDbValue({ - pointer: `/dev/${this.projectId}/basic-info`, - value: basicInfo, - }) + await this.api.setDbValue( + ['dev', this.projectId, 'basic-info'], + basicInfo, + ) } catch (e: any) { this.errToast.present(e) } finally { 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 dd15dcc53..aa2c40931 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,5 +1,6 @@ + Marketplace @@ -28,14 +29,13 @@ - + @@ -43,7 +43,7 @@
{ + 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] } + }), + ) + readonly localPkgs$ = this.patch.watch$('package-data') - readonly categories$ = this.marketplaceService.getCategories() - readonly pkgs$ = this.marketplaceService.getPackages() - readonly details$ = this.marketplaceService.getMarketplace().pipe( - map(d => { + + readonly details$ = this.marketplaceService.getUiMarketplace$().pipe( + map(({ url, name }) => { let color: string let description: string - switch (getUrlHostname(d.url)) { - case 'registry.start9.com': + switch (url) { + case 'https://registry.start9.com/': color = 'success' description = 'Services in this marketplace are packaged and maintained by the Start9 team. If you experience an issue or have a questions related to a service in this marketplace, one of our dedicated support staff will be happy to assist you.' break - case 'beta-registry-0-3.start9labs.com': + case 'https://beta-registry-0-3.start9labs.com/': color = 'primary' description = 'Services in this marketplace are undergoing active testing and may contain bugs. Install at your own risk. If you discover a bug or have a suggestion for improvement, please report it to the Start9 team in our community testing channel on Matrix.' break - case 'community.start9labs.com': + case 'https://community.start9labs.com/': color = 'tertiary' description = 'Services in this marketplace are packaged and maintained by members of the Start9 community. Install at your own risk. If you experience an issue or have a question related to a service in this marketplace, please reach out to the package developer for assistance.' @@ -43,7 +63,8 @@ export class MarketplaceListPage { } return { - ...d, + name, + url, color, description, } @@ -52,7 +73,8 @@ export class MarketplaceListPage { constructor( private readonly patch: PatchDB, - private readonly marketplaceService: AbstractMarketplaceService, + @Inject(AbstractMarketplaceService) + private readonly marketplaceService: MarketplaceService, ) {} category = 'featured' 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 378b5f861..487486902 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 @@ -31,6 +31,9 @@ import { firstValueFrom } from 'rxjs' changeDetection: ChangeDetectionStrategy.OnPush, }) export class MarketplaceShowControlsComponent { + @Input() + url?: string + @Input() pkg!: MarketplacePkg @@ -58,22 +61,81 @@ export class MarketplaceShowControlsComponent { } async tryInstall() { + const currentMarketplace = await firstValueFrom( + this.marketplaceService.getUiMarketplace$(), + ) + const url = this.url || currentMarketplace.url + if (!this.localPkg) { - this.alertInstall() + this.alertInstall(url) } else { + const originalUrl = this.localPkg.installed?.['marketplace-url'] + + if (url !== originalUrl) { + const proceed = await this.presentAlertDifferentMarketplace( + url, + originalUrl, + ) + if (!proceed) return + } + if ( this.emver.compare(this.localVersion, this.pkg.manifest.version) !== 0 && hasCurrentDeps(this.localPkg) ) { - this.dryInstall() + this.dryInstall(url) } else { - this.install() + this.install(url) } } } - private async dryInstall() { + private async presentAlertDifferentMarketplace( + url: string, + originalUrl: string | null | undefined, + ): Promise { + const marketplaces = await firstValueFrom( + this.patch.watch$('ui', 'marketplace'), + ) + + const name = marketplaces['known-hosts'][url] || url + + let originalName: string | undefined + if (originalUrl) { + originalName = marketplaces['known-hosts'][originalUrl] || originalUrl + } + + return new Promise(async resolve => { + const alert = await this.alertCtrl.create({ + header: 'Warning', + message: `This service was originally ${ + originalName ? 'installed from ' + originalName : 'side loaded' + }, but you are currently connected to ${name}. To install from ${name} anyway, click "Continue".`, + buttons: [ + { + text: 'Cancel', + role: 'cancel', + handler: () => { + resolve(false) + }, + }, + { + text: 'Continue', + handler: () => { + resolve(true) + }, + cssClass: 'enter-click', + }, + ], + cssClass: 'alert-warning-message', + }) + + await alert.present() + }) + } + + private async dryInstall(url: string) { const loader = await this.loadingCtrl.create({ message: 'Checking dependent services...', }) @@ -88,12 +150,12 @@ export class MarketplaceShowControlsComponent { }) if (isEmptyObject(breakages)) { - this.install(loader) + this.install(url, loader) } else { await loader.dismiss() const proceed = await this.presentAlertBreakages(breakages) if (proceed) { - this.install() + this.install(url) } } } catch (e: any) { @@ -101,10 +163,10 @@ export class MarketplaceShowControlsComponent { } } - private async alertInstall() { + private async alertInstall(url: string) { const installAlert = this.pkg.manifest.alerts.install - if (!installAlert) return this.install() + if (!installAlert) return this.install(url) const alert = await this.alertCtrl.create({ header: 'Alert', @@ -117,7 +179,7 @@ export class MarketplaceShowControlsComponent { { text: 'Install', handler: () => { - this.install() + this.install(url) }, cssClass: 'enter-click', }, @@ -126,7 +188,7 @@ export class MarketplaceShowControlsComponent { await alert.present() } - private async install(loader?: HTMLIonLoadingElement) { + private async install(url: string, loader?: HTMLIonLoadingElement) { const message = 'Beginning Install...' if (loader) { loader.message = message @@ -138,12 +200,7 @@ export class MarketplaceShowControlsComponent { const { id, version } = this.pkg.manifest try { - await firstValueFrom( - this.marketplaceService.installPackage({ - id, - 'version-spec': `=${version}`, - }), - ) + await this.marketplaceService.installPackage(id, version, url) } catch (e: any) { this.errToast.present(e) } finally { 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 b7078bd0d..fa1e5f38f 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 @@ -5,6 +5,7 @@ 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 298c17af2..63a7c969c 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,13 +1,10 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { ErrorToastService, getPkgId } from '@start9labs/shared' -import { - MarketplacePkg, - AbstractMarketplaceService, -} from '@start9labs/marketplace' +import { getPkgId } from '@start9labs/shared' +import { AbstractMarketplaceService } from '@start9labs/marketplace' import { PatchDB } from 'patch-db-client' -import { BehaviorSubject, Observable, of } from 'rxjs' -import { catchError, filter, shareReplay, switchMap } from 'rxjs/operators' +import { BehaviorSubject } from 'rxjs' +import { filter, shareReplay, switchMap } from 'rxjs/operators' import { DataModel } from 'src/app/services/patch-db/data-model' @Component({ @@ -18,6 +15,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model' }) export class MarketplaceShowPage { private readonly pkgId = getPkgId(this.route) + readonly url = this.route.snapshot.queryParamMap.get('url') || undefined readonly loadVersion$ = new BehaviorSubject('*') @@ -25,26 +23,15 @@ export class MarketplaceShowPage { .watch$('package-data', this.pkgId) .pipe(filter(Boolean), shareReplay({ bufferSize: 1, refCount: true })) - readonly pkg$: Observable = this.loadVersion$.pipe( + readonly pkg$ = this.loadVersion$.pipe( switchMap(version => - this.marketplaceService.getPackage(this.pkgId, version), + this.marketplaceService.getPackage(this.pkgId, version, this.url), ), - // TODO: Better fallback - catchError(e => { - this.errToast.present(e) - - return of({} as MarketplacePkg) - }), ) constructor( private readonly route: ActivatedRoute, - private readonly errToast: ErrorToastService, private readonly patch: PatchDB, private readonly marketplaceService: AbstractMarketplaceService, ) {} - - getIcon(icon: string): string { - return `data:image/png;base64,${icon}` - } } 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 421b0ef10..45ecdfe14 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 @@ -8,8 +8,34 @@ - - Saved Marketplaces + + + + Connect to a standard marketplaces or an alternative marketplace. + + + + Standard Marketplaces + + + +

{{ s.name }}

+

{{ s.url }}

+
+
+ + Alt Marketplaces @@ -20,22 +46,21 @@ -
-

{{ mp.name }}

-

{{ mp.url }}

+

{{ a.name }}

+

{{ a.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 2cf5e8315..2eb19301b 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 @@ -1,4 +1,4 @@ -import { Component, Inject } from '@angular/core' +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' import { ActionSheetController, AlertController, @@ -6,47 +6,44 @@ import { ModalController, } from '@ionic/angular' import { ActionSheetButton } from '@ionic/core' -import { - DestroyService, - ErrorToastService, - getUrlHostname, -} from '@start9labs/shared' +import { ErrorToastService } from '@start9labs/shared' import { AbstractMarketplaceService } from '@start9labs/marketplace' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ValueSpecObject } from 'src/app/pkg-config/config-types' import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' import { PatchDB } from 'patch-db-client' -import { v4 } from 'uuid' -import { - DataModel, - UIMarketplaceData, -} from '../../../services/patch-db/data-model' -import { ConfigService } from '../../../services/config.service' +import { DataModel } from '../../../services/patch-db/data-model' import { MarketplaceService } from 'src/app/services/marketplace.service' -import { - distinctUntilChanged, - finalize, - first, - takeUntil, -} from 'rxjs/operators' -import { getServerInfo } from '../../../util/get-server-info' -import { getMarketplace } from '../../../util/get-marketplace' - -type Marketplaces = { - id: string | null - name: string - url: string -}[] +import { map } from 'rxjs/operators' +import { firstValueFrom } from 'rxjs' @Component({ selector: 'marketplaces', templateUrl: 'marketplaces.page.html', styleUrls: ['marketplaces.page.scss'], - providers: [DestroyService], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class MarketplacesPage { - selectedId: string | null = null - marketplaces: Marketplaces = [] + marketplace$ = this.patch.watch$('ui', 'marketplace').pipe( + map(m => { + const selected = m['selected-url'] + const hosts = Object.entries(m['known-hosts']) + + const standard = hosts + .map(([url, name]) => { + return { url, name } + }) + .slice(0, 2) // 0 and 1 will always be prod and community + + const alt = hosts + .map(([url, name]) => { + return { url, name } + }) + .slice(2) // 2 and beyond will always be alts + + return { selected, standard, alt } + }), + ) constructor( private readonly api: ApiService, @@ -56,55 +53,28 @@ export class MarketplacesPage { private readonly actionCtrl: ActionSheetController, @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, - private readonly config: ConfigService, private readonly patch: PatchDB, - private readonly destroy$: DestroyService, private readonly alertCtrl: AlertController, ) {} - ngOnInit() { - this.patch - .watch$('ui', 'marketplace') - .pipe(distinctUntilChanged(), takeUntil(this.destroy$)) - .subscribe((mp: UIMarketplaceData) => { - let marketplaces: Marketplaces = [ - { - id: null, - name: this.config.marketplace.name, - url: this.config.marketplace.url, - }, - ] - this.selectedId = mp['selected-id'] - const alts = Object.entries(mp['known-hosts']).map(([k, v]) => { - return { - id: k, - name: v.name, - url: v.url, - } - }) - marketplaces = marketplaces.concat(alts) - this.marketplaces = marketplaces - }) - } - async presentModalAdd() { - const marketplaceSpec = getMarketplaceValueSpec() + const { name, spec } = getMarketplaceValueSpec() const modal = await this.modalCtrl.create({ component: GenericFormPage, componentProps: { - title: marketplaceSpec.name, - spec: marketplaceSpec.spec, + title: name, + spec, buttons: [ { text: 'Save for Later', handler: (value: { url: string }) => { - this.save(value.url) + this.saveOnly(new URL(value.url)) }, }, { text: 'Save and Connect', handler: (value: { url: string }) => { - this.saveAndConnect(value.url) + this.saveAndConnect(new URL(value.url)) }, isSubmit: true, }, @@ -116,32 +86,31 @@ export class MarketplacesPage { await modal.present() } - async presentAction(id: string | null) { - // no need to view actions if is selected marketplace - const marketplace = await getMarketplace(this.patch) - if (id === marketplace['selected-id']) return - + async presentAction( + { url, name }: { url: string; name: string }, + canDelete = false, + ) { const buttons: ActionSheetButton[] = [ { text: 'Connect', handler: () => { - this.connect(id) + this.connect(url) }, }, ] - if (id) { + if (canDelete) { buttons.unshift({ text: 'Delete', role: 'destructive', handler: () => { - this.presentAlertDelete(id) + this.presentAlertDelete(url, name) }, }) } const action = await this.actionCtrl.create({ - header: this.marketplaces.find(mp => mp.id === id)?.name, + header: name, mode: 'ios', buttons, }) @@ -149,55 +118,7 @@ export class MarketplacesPage { await action.present() } - private async connect(id: string | null): Promise { - const marketplace = await getMarketplace(this.patch) - - const url = id - ? marketplace['known-hosts'][id].url - : this.config.marketplace.url - - const loader = await this.loadingCtrl.create({ - message: 'Validating Marketplace...', - }) - await loader.present() - - try { - const { id } = await getServerInfo(this.patch) - await this.marketplaceService.getMarketplaceData({ 'server-id': id }, url) - } catch (e: any) { - this.errToast.present(e) - loader.dismiss() - return - } - - loader.message = 'Changing Marketplace...' - - const value: UIMarketplaceData = { - ...marketplace, - 'selected-id': id, - } - - try { - await this.api.setDbValue({ pointer: `/marketplace`, value }) - } catch (e: any) { - this.errToast.present(e) - loader.dismiss() - } - - loader.message = 'Syncing store...' - - this.marketplaceService - .getPackages() - .pipe( - first(), - finalize(() => loader.dismiss()), - ) - .subscribe() - } - - private async presentAlertDelete(id: string) { - const name = this.marketplaces.find(m => m.id === id)?.name - + private async presentAlertDelete(url: string, name: string) { const alert = await this.alertCtrl.create({ header: 'Confirm', message: `Are you sure you want to delete ${name}?`, @@ -208,7 +129,7 @@ export class MarketplacesPage { }, { text: 'Delete', - handler: () => this.delete(id), + handler: () => this.delete(url), cssClass: 'enter-click', }, ], @@ -217,125 +138,104 @@ export class MarketplacesPage { await alert.present() } - private async delete(id: string): Promise { - const data = await getMarketplace(this.patch) - const marketplace: UIMarketplaceData = JSON.parse(JSON.stringify(data)) + private async connect( + url: string, + loader?: HTMLIonLoadingElement, + ): Promise { + const message = 'Changing Marketplace...' + if (!loader) { + loader = await this.loadingCtrl.create({ message }) + await loader.present() + } else { + loader.message = message + } + try { + await this.api.setDbValue(['marketplace', 'selected-url'], url) + } catch (e: any) { + this.errToast.present(e) + } finally { + loader.dismiss() + } + } + + private async saveOnly(url: URL): Promise { + const loader = await this.loadingCtrl.create() + + try { + await this.validateAndSave(url, loader) + } catch (e: any) { + this.errToast.present(e) + } finally { + loader.dismiss() + } + } + + private async saveAndConnect(url: URL): Promise { + const loader = await this.loadingCtrl.create() + + try { + await this.validateAndSave(url, loader) + await this.connect(url.toString(), loader) + } catch (e: any) { + this.errToast.present(e) + } finally { + loader.dismiss() + } + } + + private async validateAndSave( + urlObj: URL, + loader: HTMLIonLoadingElement, + ): Promise { + const url = urlObj.toString() + // Error on duplicates + const hosts = await firstValueFrom( + this.patch.watch$('ui', 'marketplace', 'known-hosts'), + ) + const currentUrls = Object.keys(hosts) + if (currentUrls.includes(url)) throw new Error('marketplace already added') + + // Validate + loader.message = 'Validating marketplace...' + await loader.present() + + const name = await this.marketplaceService.validateMarketplace(url) + + // Save + loader.message = 'Saving...' + + await this.api.setDbValue(['marketplace', 'known-hosts', url], name) + } + + private async delete(url: string): Promise { const loader = await this.loadingCtrl.create({ message: 'Deleting...', }) await loader.present() + const hosts = await firstValueFrom( + this.patch.watch$('ui', 'marketplace', 'known-hosts'), + ) + + const filtered = Object.keys(hosts) + .filter(key => key !== url) + .reduce((prev, curr) => { + const name = hosts[curr] + return { + ...prev, + [curr]: name, + } + }, {}) + try { - delete marketplace['known-hosts'][id] - await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace }) + await this.api.setDbValue(['marketplace', 'known-hosts'], filtered) } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() } } - - private async save(url: string): Promise { - const data = await getMarketplace(this.patch) - const marketplace: UIMarketplaceData = data - ? JSON.parse(JSON.stringify(data)) - : { - 'selected-id': null, - 'known-hosts': {}, - } - - // no-op on duplicates - const currentUrls = this.marketplaces.map(mp => getUrlHostname(mp.url)) - if (currentUrls.includes(getUrlHostname(url))) return - - const loader = await this.loadingCtrl.create({ - message: 'Validating Marketplace...', - }) - - await loader.present() - - try { - const id = v4() - const { id: serverId } = await getServerInfo(this.patch) - const { name } = await this.marketplaceService.getMarketplaceData( - { 'server-id': serverId }, - url, - ) - marketplace['known-hosts'][id] = { name, url } - } catch (e: any) { - this.errToast.present(e) - loader.dismiss() - return - } - - loader.message = 'Saving...' - - try { - await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace }) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - } - - private async saveAndConnect(url: string): Promise { - const data = await getMarketplace(this.patch) - const marketplace: UIMarketplaceData = data - ? JSON.parse(JSON.stringify(data)) - : { - 'selected-id': null, - 'known-hosts': {}, - } - - // no-op on duplicates - const currentUrls = this.marketplaces.map(mp => getUrlHostname(mp.url)) - if (currentUrls.includes(getUrlHostname(url))) { - this.errToast.present({ message: 'Marketplace already added' }) - return - } - - const loader = await this.loadingCtrl.create({ - message: 'Validating Marketplace...', - }) - await loader.present() - - try { - const id = v4() - const { id: serverId } = await getServerInfo(this.patch) - const { name } = await this.marketplaceService.getMarketplaceData( - { 'server-id': serverId }, - url, - ) - marketplace['known-hosts'][id] = { name, url } - marketplace['selected-id'] = id - } catch (e: any) { - this.errToast.present(e) - loader.dismiss() - return - } - - loader.message = 'Saving...' - - try { - await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace }) - } catch (e: any) { - this.errToast.present(e) - loader.dismiss() - return - } - - loader.message = 'Syncing marketplace data...' - - this.marketplaceService - .getPackages() - .pipe( - first(), - finalize(() => loader.dismiss()), - ) - .subscribe() - } } function getMarketplaceValueSpec(): ValueSpecObject { 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 index 0deeea054..03059ce67 100644 --- 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 @@ -73,7 +73,7 @@ export class PreferencesPage { await loader.present() try { - await this.api.setDbValue({ pointer: `/${key}`, value }) + await this.api.setDbValue([key], value) } finally { loader.dismiss() } 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 90b4de06b..6513a6321 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 @@ -48,9 +48,6 @@ export class ServerShowPage { async updateEos(): Promise { const modal = await this.modalCtrl.create({ - componentProps: { - releaseNotes: this.eosService.eos?.['release-notes'], - }, component: OSUpdatePage, }) modal.present() 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 e4264fbd7..f1c6e85e1 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 { MarketplaceData, MarketplacePkg } from '@start9labs/marketplace' +import { MarketplaceInfo, MarketplacePkg } from '@start9labs/marketplace' import { PackagePropertiesVersioned } from 'src/app/util/properties.util' import { ConfigSpec } from 'src/app/pkg-config/config-types' import { @@ -242,7 +242,7 @@ export module RR { // marketplace export type GetMarketplaceDataReq = { 'server-id': string } - export type GetMarketplaceDataRes = MarketplaceData + export type GetMarketplaceDataRes = MarketplaceInfo export type GetMarketplaceEOSReq = { 'server-id': string diff --git a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts index 14fa77bc5..f21603e43 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts @@ -18,7 +18,10 @@ export abstract class ApiService { // db - abstract setDbValue(params: RR.SetDBValueReq): Promise + abstract setDbValue( + pathArr: Array, + value: any, + ): Promise // auth @@ -64,7 +67,7 @@ export abstract class ApiService { params: RR.GetPackageMetricsReq, ): Promise - abstract updateServer(params: RR.UpdateServerReq): Promise + abstract updateServer(url?: string): Promise abstract restartServer( params: RR.RestartServerReq, diff --git a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts index 2b09e863e..1ab83aec6 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -18,10 +18,12 @@ import { Observable } from 'rxjs' import { AuthService } from '../auth.service' import { DOCUMENT } from '@angular/common' import { DataModel } from '../patch-db/data-model' -import { PatchDB, Update } from 'patch-db-client' +import { PatchDB, pathFromArray, Update } from 'patch-db-client' @Injectable() export class LiveApiService extends ApiService { + readonly eosMarketplaceUrl = 'https://registry.start9.com/' + constructor( @Inject(DOCUMENT) private readonly document: Document, private readonly http: HttpService, @@ -52,7 +54,12 @@ export class LiveApiService extends ApiService { // db - async setDbValue(params: RR.SetDBValueReq): Promise { + async setDbValue( + pathArr: Array, + value: any, + ): Promise { + const pointer = pathFromArray(pathArr) + const params: RR.SetDBValueReq = { pointer, value } return this.rpcRequest({ method: 'db.put.ui', params }) } @@ -127,13 +134,11 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'server.metrics', params }) } - async updateServer(params: RR.UpdateServerReq): Promise { + async updateServer(url?: string): Promise { + const params = { + 'marketplace-url': url || this.eosMarketplaceUrl, + } return this.rpcRequest({ method: 'server.update', params }) - // const res = await this.updateServer(params) - // if (res.response === 'no-updates') { - // throw new Error('Could not find a newer version of embassyOS') - // } - // return res } async restartServer( @@ -160,12 +165,12 @@ export class LiveApiService extends ApiService { // marketplace URLs - async marketplaceProxy(path: string, qp: {}, url: string): Promise { + async marketplaceProxy(path: string, qp: {}, baseUrl: string): Promise { Object.assign(qp, { arch: this.config.targetArch }) - const fullURL = `${url}${path}?${new URLSearchParams(qp).toString()}` + const fullUrl = `${baseUrl}${path}?${new URLSearchParams(qp).toString()}` return this.rpcRequest({ method: 'marketplace.get', - params: { url: fullURL }, + params: { url: fullUrl }, }) } @@ -175,7 +180,7 @@ export class LiveApiService extends ApiService { return this.marketplaceProxy( '/eos/v0/latest', params, - this.config.marketplace.url, + this.eosMarketplaceUrl, ) } 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 8b012f154..d2e88c73b 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 @@ -1,7 +1,13 @@ import { Injectable } from '@angular/core' import { pauseFor, Log } from '@start9labs/shared' import { ApiService } from './embassy-api.service' -import { PatchOp, Update, Operation, RemoveOperation } from 'patch-db-client' +import { + PatchOp, + Update, + Operation, + RemoveOperation, + pathFromArray, +} from 'patch-db-client' import { DataModel, DependencyErrorType, @@ -15,11 +21,22 @@ import { CifsBackupTarget, RR } from './api.types' import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { Mock } from './api.fixures' import markdown from 'raw-loader!../../../../../../assets/markdown/md-sample.md' -import { BehaviorSubject, interval, map, Observable } from 'rxjs' +import { + EMPTY, + iif, + interval, + map, + Observable, + ReplaySubject, + switchMap, + tap, + timer, +} from 'rxjs' import { LocalStorageBootstrap } from '../patch-db/local-storage-bootstrap' import { mockPatchData } from './mock-patch' import { WebSocketSubjectConfig } from 'rxjs/webSocket' import { AuthService } from '../auth.service' +import { ConnectionService } from '../connection.service' const PROGRESS: InstallProgress = { size: 120, @@ -33,28 +50,35 @@ const PROGRESS: InstallProgress = { @Injectable() export class MockApiService extends ApiService { - readonly mockWsSource$ = new BehaviorSubject>({ - id: 1, - value: mockPatchData, - }) + readonly mockWsSource$ = new ReplaySubject>() private readonly revertTime = 2000 sequence = 0 constructor( private readonly bootstrapper: LocalStorageBootstrap, + private readonly connectionService: ConnectionService, private readonly auth: AuthService, ) { super() - this.auth.isVerified$.subscribe(verified => { - if (!verified) { - this.patchStream$.next([]) - this.mockWsSource$.next({ - id: 1, - value: mockPatchData, - }) - this.sequence = 0 - } - }) + this.auth.isVerified$ + .pipe( + tap(() => { + this.sequence = 0 + this.patchStream$.next([]) + }), + switchMap(verified => + iif( + () => verified, + timer(2000).pipe( + tap(() => { + this.connectionService.websocketConnected$.next(true) + }), + ), + EMPTY, + ), + ), + ) + .subscribe() } async getStatic(url: string): Promise { @@ -69,7 +93,12 @@ export class MockApiService extends ApiService { // db - async setDbValue(params: RR.SetDBValueReq): Promise { + async setDbValue( + pathArr: Array, + value: any, + ): Promise { + const pointer = pathFromArray(pathArr) + const params: RR.SetDBValueReq = { pointer, value } await pauseFor(2000) const patch = [ { @@ -198,7 +227,7 @@ export class MockApiService extends ApiService { return Mock.getAppMetrics() } - async updateServer(params: RR.UpdateServerReq): Promise { + async updateServer(url?: string): Promise { await pauseFor(2000) const initialProgress = { size: 10000, @@ -254,10 +283,10 @@ export class MockApiService extends ApiService { return { name: 'Dark69', categories: [ - 'featured', 'bitcoin', 'lightning', 'data', + 'featured', 'messaging', 'social', 'alt coin', 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 e6347e4b9..83a938c97 100644 --- a/frontend/projects/ui/src/app/services/api/mock-patch.ts +++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts @@ -15,12 +15,11 @@ export const mockPatchData: DataModel = { 'pkg-order': [], 'ack-welcome': '1.0.0', marketplace: { - 'selected-id': '1234', + 'selected-url': 'https://registry.start9.com/', 'known-hosts': { - '1234': { - name: 'Dark9', - url: 'https://test-marketplace.com', - }, + 'https://registry.start9.com/': 'Start9 Marketplace', + 'https://community-registry.start9.com/': 'Community Marketplace', + 'https://dark9-marketplace.com/': 'Dark9', }, }, dev: {}, diff --git a/frontend/projects/ui/src/app/services/config.service.ts b/frontend/projects/ui/src/app/services/config.service.ts index 5769bd69d..55a8f67b1 100644 --- a/frontend/projects/ui/src/app/services/config.service.ts +++ b/frontend/projects/ui/src/app/services/config.service.ts @@ -11,7 +11,7 @@ const { targetArch, gitHash, useMocks, - ui: { api, mocks, marketplace }, + ui: { api, mocks }, } = require('../../../../../config.json') as WorkspaceConfig @Injectable({ @@ -25,7 +25,6 @@ export class ConfigService { targetArch = targetArch gitHash = gitHash api = api - marketplace = marketplace skipStartupAlerts = useMocks && mocks.skipStartupAlerts isConsulate = (window as any)['platform'] === 'ios' supportsWebSockets = !!window.WebSocket || this.isConsulate diff --git a/frontend/projects/ui/src/app/services/marketplace.service.ts b/frontend/projects/ui/src/app/services/marketplace.service.ts index c85f38413..eb643db9a 100644 --- a/frontend/projects/ui/src/app/services/marketplace.service.ts +++ b/frontend/projects/ui/src/app/services/marketplace.service.ts @@ -1,26 +1,21 @@ import { Injectable } from '@angular/core' -import { Emver, ErrorToastService } from '@start9labs/shared' +import { Emver } from '@start9labs/shared' import { MarketplacePkg, AbstractMarketplaceService, - Marketplace, - FilterPackagesPipe, - MarketplaceData, + MarketplaceInfo, } from '@start9labs/marketplace' -import { from, Observable, of, Subject } from 'rxjs' +import { combineLatest, 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 { DataModel, - ServerInfo, - UIMarketplaceData, + Manifest, + PackageState, } from 'src/app/services/patch-db/data-model' import { PatchDB } from 'patch-db-client' import { - catchError, distinctUntilChanged, - filter, map, shareReplay, startWith, @@ -28,275 +23,321 @@ import { 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 @Injectable() -export class MarketplaceService extends AbstractMarketplaceService { - private readonly notes = new Map>() - private readonly hasPackages$ = new Subject() +export class MarketplaceService implements AbstractMarketplaceService { + private readonly cache: MasterCache = new Map() - private readonly uiMarketplaceData$ = this.patch - .watch$('ui', 'marketplace') - .pipe( + private readonly uiMarketplace$: Observable<{ url: string; name: string }> = + this.patch.watch$('ui', 'marketplace').pipe( distinctUntilChanged( - (prev, curr) => prev['selected-id'] === curr['selected-id'], + (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 marketplace$ = this.uiMarketplaceData$.pipe( - map(data => this.toMarketplace(data)), - ) - - private readonly serverInfo$: Observable = this.patch - .watch$('server-info') - .pipe(take(1), shareReplay()) - - private readonly registryData$: Observable = - this.uiMarketplaceData$.pipe( - switchMap(data => - this.serverInfo$.pipe( - switchMap(({ id }) => - from( - this.getMarketplaceData( - { 'server-id': id }, - this.toMarketplace(data).url, - ), - ).pipe(tap(({ name }) => this.updateName(data, name))), - ), + 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 categories$: Observable> = - this.registryData$.pipe( - map( - ({ categories }) => - new Set(['featured', 'updates', ...categories, 'all']), - ), - ) + private readonly marketplaceInfo$: Observable = + this.marketplaceData$.pipe(map(data => data.info!)) - private readonly pkgs$: Observable = this.marketplace$.pipe( - switchMap(({ url }) => - this.serverInfo$.pipe( - switchMap(info => - from( - this.getMarketplacePkgs( - { page: 1, 'per-page': 100 }, - url, - info['eos-version-compat'], - ), - ).pipe(tap(() => this.hasPackages$.next(true))), - ), - ), - ), - catchError(e => { - this.errToast.present(e) + private readonly marketplacePkgs$: Observable = + this.marketplaceData$.pipe(map(data => data.packages)) - return of([]) + 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) }), shareReplay(1), ) - private readonly updates$: Observable = - this.hasPackages$.pipe( - switchMap(() => - this.patch.watch$('package-data').pipe( - switchMap(localPkgs => - this.pkgs$.pipe( - map(pkgs => { - return this.filterPkgsPipe.transform( - pkgs, - '', - 'updates', - localPkgs, - ) - }), - ), - ), - ), - ), - ) - constructor( private readonly api: ApiService, private readonly patch: PatchDB, - private readonly config: ConfigService, - private readonly errToast: ErrorToastService, private readonly emver: Emver, - private readonly filterPkgsPipe: FilterPackagesPipe, - ) { - super() + ) {} + + getUiMarketplace$(): Observable<{ url: string; name: string }> { + return this.uiMarketplace$ } - getMarketplace(): Observable { - return this.marketplace$ + getMarketplaceInfo$(): Observable { + return this.marketplaceInfo$ } - getAltMarketplaceData() { - return this.uiMarketplaceData$ + getPackages$(): Observable { + return this.marketplacePkgs$ } - getCategories(): Observable> { - return this.categories$ - } - - getPackages(): Observable { - return this.pkgs$ - } - - getPackage(id: string, version: string): Observable { - const params = { ids: [{ id, version }] } - const fallback$ = this.marketplace$.pipe( - switchMap(({ url }) => - this.serverInfo$.pipe( - switchMap(info => - from( - this.getMarketplacePkgs(params, url, info['eos-version-compat']), - ), - ), - ), - ), - map(pkgs => this.findPackage(pkgs, id, version)), - startWith(null), - ) - - return this.getPackages().pipe( - map(pkgs => this.findPackage(pkgs, id, version)), - switchMap(pkg => (pkg ? of(pkg) : fallback$)), - filter((pkg): pkg is MarketplacePkg | null => { - if (pkg === undefined) { - throw new Error(`No results for ${id}${version ? ' ' + version : ''}`) + getPackage( + id: string, + version: string, + url?: 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 true + if (version === '*') { + return from(this.loadPackage(id, url)) + } else { + return from(this.fetchPackage(id, version, url)) + } }), ) } - getUpdates(): Observable { + getUpdates$(): Observable<{ url: string; pkgs: MarketplacePkg[] }[]> { return this.updates$ } - getReleaseNotes(id: string): Observable> { - if (this.notes.has(id)) { - return of(this.notes.get(id) || {}) + async installPackage( + id: string, + version: string, + url: string, + ): Promise { + const params: RR.InstallPackageReq = { + id, + 'version-spec': `=${version}`, + 'marketplace-url': url, } - return this.marketplace$.pipe( - switchMap(({ url }) => this.loadReleaseNotes(id, url)), - tap(response => this.notes.set(id, response)), - catchError(e => { - this.errToast.present(e) + await this.api.installPackage(params) + } - return of({}) + async validateMarketplace(url: string): Promise { + await this.loadInfo(url) + return this.cache.get(url)!.info!.name + } + + fetchReleaseNotes( + id: string, + url?: string, + ): Observable> { + return this.uiMarketplace$.pipe( + switchMap(m => { + return from( + this.api.marketplaceProxy>( + `/package/v0/release-notes/${id}`, + {}, + url || m.url, + ), + ) }), ) } - installPackage( - req: Omit, - ): Observable { - return this.getMarketplace().pipe( - take(1), - switchMap(({ url }) => - from( - this.api.installPackage({ - ...req, - 'marketplace-url': url, - }), - ), - ), - ) - } - - getPackageMarkdown(type: string, pkgId: string): Observable { - return this.getMarketplace().pipe( - switchMap(({ url }) => - from( + fetchPackageMarkdown( + id: string, + type: string, + url?: string, + ): Observable { + return this.uiMarketplace$.pipe( + switchMap(m => { + return from( this.api.marketplaceProxy( - `/package/v0/${type}/${pkgId}`, + `/package/v0/${type}/${id}`, {}, - url, + url || m.url, ), - ), - ), + ) + }), ) } - async getMarketplaceData( - params: RR.GetMarketplaceDataReq, - url: string, - ): Promise { - return this.api.marketplaceProxy('/package/v0/info', params, url) + 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 } } - async getMarketplacePkgs( - params: Omit, - url: string, - eosVersionCompat: string, - ): Promise { - let clonedParams = { ...params } - if (params.query) delete params.category - if (clonedParams.ids) clonedParams.ids = JSON.stringify(clonedParams.ids) - - const qp: RR.GetMarketplacePackagesReq = { - ...clonedParams, - 'eos-version-compat': eosVersionCompat, - } - - return this.api.marketplaceProxy('/package/v0/index', qp, url) + private async loadInfo(url: string): Promise { + const info = await this.fetchInfo(url) + this.updateCache(url, info) + return info } - private loadReleaseNotes( + private async loadPackage( id: string, url: string, - ): Observable> { - return from( - this.api.marketplaceProxy>( - `/package/v0/release-notes/${id}`, - {}, - url, - ), + ): 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 updateName( - uiMarketplaceData: UIMarketplaceData | undefined, - name: string, - ) { - if (!uiMarketplaceData?.['selected-id']) { - return - } - - const selectedId = uiMarketplaceData['selected-id'] - const knownHosts = uiMarketplaceData['known-hosts'] - - if (knownHosts[selectedId].name !== name) { - this.api.setDbValue({ - pointer: `/marketplace/known-hosts/${selectedId}/name`, - value: name, - }) - } - } - - private toMarketplace(marketplace: UIMarketplaceData): Marketplace { - return marketplace['selected-id'] - ? marketplace['known-hosts'][marketplace['selected-id']] - : this.config.marketplace - } - - private findPackage( - pkgs: readonly MarketplacePkg[], + private async fetchPackage( id: string, version: string, - ): MarketplacePkg | undefined { - return pkgs.find(pkg => { - const versionIsSame = - version === '*' || - this.emver.compare(pkg.manifest.version, version) === 0 + url: string, + ): Promise { + const pkgs = await this.fetchPackages({ ids: [{ id, version }] }, url) + return pkgs[0] + } - return pkg.manifest.id === id && versionIsSame + 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, + newName: string, + ): Promise { + if (name !== newName) { + 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 f11355dfb..b624f213a 100644 --- a/frontend/projects/ui/src/app/services/patch-data.service.ts +++ b/frontend/projects/ui/src/app/services/patch-data.service.ts @@ -2,7 +2,6 @@ import { Inject, Injectable } from '@angular/core' import { ModalController } from '@ionic/angular' import { Observable } from 'rxjs' import { filter, share, switchMap, take, tap } from 'rxjs/operators' -import { exists, isEmptyObject } from '@start9labs/shared' import { PatchDB } from 'patch-db-client' import { DataModel, UIData } from 'src/app/services/patch-db/data-model' import { EOSService } from 'src/app/services/eos.service' @@ -21,7 +20,6 @@ export class PatchDataService extends Observable { private readonly stream$ = this.connectionService.connected$.pipe( filter(Boolean), switchMap(() => this.patch.watch$()), - filter(obj => exists(obj) && !isEmptyObject(obj)), take(1), tap(({ ui }) => { // check for updates to EOS and services @@ -48,8 +46,8 @@ export class PatchDataService extends Observable { private checkForUpdates(ui: UIData): void { if (ui['auto-check-updates'] !== false) { this.eosService.getEOS() - this.marketplaceService.getPackages().pipe(take(1)).subscribe() - this.marketplaceService.getCategories().pipe(take(1)).subscribe() + this.marketplaceService.getMarketplaceInfo$().pipe(take(1)).subscribe() + this.marketplaceService.getUpdates$().pipe(take(1)).subscribe() } } @@ -64,9 +62,7 @@ export class PatchDataService extends Observable { backdropDismiss: false, }) modal.onWillDismiss().then(() => { - this.embassyApi - .setDbValue({ pointer: '/ack-welcome', value: this.config.version }) - .catch() + this.embassyApi.setDbValue(['ack-welcome'], this.config.version).catch() }) await modal.present() 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 2ed54203d..c861d8113 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 @@ -25,12 +25,11 @@ export interface UIData { } export interface UIMarketplaceData { - 'selected-id': string | null + 'selected-url': string 'known-hosts': { - [id: string]: { - url: string - name: string - } + 'https://registry.start9.com/': string + 'https://community-registry.start9.com/': string + [url: string]: string } } 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 f57eee5cf..7ee0d6cd3 100644 --- a/frontend/projects/ui/src/app/services/server-config.service.ts +++ b/frontend/projects/ui/src/app/services/server-config.service.ts @@ -100,10 +100,7 @@ export class ServerConfigService { saveFns: { [key: string]: (val: any) => Promise } = { 'auto-check-updates': async (enabled: boolean) => { - return this.embassyApi.setDbValue({ - pointer: '/auto-check-updates', - value: enabled, - }) + return this.embassyApi.setDbValue(['auto-check-updates'], enabled) }, } } diff --git a/frontend/projects/ui/src/app/util/get-marketplace.ts b/frontend/projects/ui/src/app/util/get-marketplace.ts deleted file mode 100644 index dd65c8839..000000000 --- a/frontend/projects/ui/src/app/util/get-marketplace.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { PatchDB } from 'patch-db-client' -import { - DataModel, - UIMarketplaceData, -} from 'src/app/services/patch-db/data-model' -import { firstValueFrom, map } from 'rxjs' - -export function getMarketplace( - patch: PatchDB, -): Promise { - return firstValueFrom(patch.watch$('ui', 'marketplace')) -} diff --git a/frontend/projects/ui/src/app/util/get-package-data.ts b/frontend/projects/ui/src/app/util/get-package-data.ts index f83d90ab5..816471832 100644 --- a/frontend/projects/ui/src/app/util/get-package-data.ts +++ b/frontend/projects/ui/src/app/util/get-package-data.ts @@ -3,16 +3,16 @@ import { DataModel, PackageDataEntry, } from 'src/app/services/patch-db/data-model' -import { filter, firstValueFrom } from 'rxjs' +import { firstValueFrom } from 'rxjs' -export function getPackage( +export async function getPackage( patch: PatchDB, id: string, ): Promise { return firstValueFrom(patch.watch$('package-data', id)) } -export function getAllPackages( +export async function getAllPackages( patch: PatchDB, ): Promise> { return firstValueFrom(patch.watch$('package-data')) diff --git a/frontend/projects/ui/src/app/util/get-server-info.ts b/frontend/projects/ui/src/app/util/get-server-info.ts index f43dfd614..4500c4305 100644 --- a/frontend/projects/ui/src/app/util/get-server-info.ts +++ b/frontend/projects/ui/src/app/util/get-server-info.ts @@ -2,6 +2,8 @@ import { PatchDB } from 'patch-db-client' import { DataModel, ServerInfo } from 'src/app/services/patch-db/data-model' import { firstValueFrom } from 'rxjs' -export function getServerInfo(patch: PatchDB): Promise { +export async function getServerInfo( + patch: PatchDB, +): Promise { return firstValueFrom(patch.watch$('server-info')) }