From cc38dab76fc893fe5a97f53fd0c50f7ee2706e43 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Tue, 19 Mar 2024 08:38:04 -0600 Subject: [PATCH] Rework PackageDataEntry for new strategy (#2573) * rework PackageDataEntry for new strategy * fix type error * fix issues with manifest fetching * mock installs working --- web/package-lock.json | 2 +- .../ui/src/app/app/menu/menu.component.ts | 22 +- .../components/status/status.component.html | 9 +- .../status/status.component.module.ts | 4 +- .../app/components/status/status.component.ts | 4 +- .../modals/app-config/app-config.page.html | 19 +- .../app/modals/app-config/app-config.page.ts | 17 +- .../backup-select/backup-select.page.ts | 9 +- .../app-actions/app-actions.page.html | 54 +- .../app-actions/app-actions.page.ts | 12 +- .../app-interfaces/app-interfaces.page.ts | 2 +- .../app-list-pkg/app-list-pkg.component.html | 16 +- .../app-list-pkg/app-list-pkg.component.ts | 9 +- .../apps-routes/app-list/app-list.page.html | 2 +- .../apps-routes/app-list/app-list.page.ts | 3 +- .../apps-routes/app-list/package-info.pipe.ts | 2 +- .../app-properties/app-properties.page.ts | 2 +- .../apps-routes/app-show/app-show.module.ts | 6 +- .../apps-routes/app-show/app-show.page.html | 17 +- .../apps-routes/app-show/app-show.page.ts | 73 +- .../app-show-additional.component.html | 2 +- .../app-show-additional.component.ts | 12 +- .../app-show-header.component.html | 13 +- .../app-show-health-checks.component.html | 12 +- .../app-show-health-checks.component.ts | 7 +- .../app-show-progress.component.html | 38 +- .../app-show-progress.component.ts | 29 +- .../app-show-status.component.html | 8 +- .../app-show-status.component.ts | 41 +- .../app-show/pipes/progress-data.pipe.ts | 13 - .../app-show/pipes/to-buttons.pipe.ts | 42 +- .../app-show/pipes/to-health-checks.pipe.ts | 8 +- .../marketplace-show-controls.component.html | 2 +- .../marketplace-show-controls.component.ts | 8 +- .../marketplace-status.component.html | 19 +- .../marketplace-status.component.ts | 19 +- .../marketplace-status.module.ts | 5 +- .../notifications/notifications.module.ts | 2 + .../notifications/notifications.page.html | 4 +- .../backing-up/backing-up.component.html | 9 +- .../backing-up/backing-up.component.ts | 11 +- .../server-backup/server-backup.module.ts | 2 + .../src/app/pages/updates/updates.module.ts | 4 +- .../src/app/pages/updates/updates.page.html | 8 +- .../ui/src/app/pages/updates/updates.page.ts | 36 +- .../built-in/health/health.component.ts | 3 +- .../install-progress.module.ts | 10 +- .../install-progress/install-progress.pipe.ts | 34 +- web/projects/ui/src/app/pipes/ui/ui.module.ts | 6 +- web/projects/ui/src/app/pipes/ui/ui.pipe.ts | 16 +- .../ui/src/app/services/api/api.fixures.ts | 1278 ++++++++------- .../services/api/embassy-mock-api.service.ts | 199 ++- .../ui/src/app/services/api/mock-patch.ts | 1395 ++++++----------- .../ui/src/app/services/config.service.ts | 6 +- .../ui/src/app/services/dep-error.service.ts | 42 +- .../src/app/services/patch-db/data-model.ts | 69 +- .../services/pkg-status-rendering.service.ts | 10 +- .../src/app/services/ui-launcher.service.ts | 4 +- .../ui/src/app/types/progress-data.ts | 7 - web/projects/ui/src/app/util/dry-update.ts | 8 +- .../ui/src/app/util/get-package-data.ts | 43 + .../ui/src/app/util/get-package-info.ts | 4 - web/projects/ui/src/app/util/has-deps.ts | 5 +- .../src/app/util/package-loading-progress.ts | 50 - 64 files changed, 1759 insertions(+), 2068 deletions(-) delete mode 100644 web/projects/ui/src/app/pages/apps-routes/app-show/pipes/progress-data.pipe.ts delete mode 100644 web/projects/ui/src/app/types/progress-data.ts delete mode 100644 web/projects/ui/src/app/util/package-loading-progress.ts diff --git a/web/package-lock.json b/web/package-lock.json index f1aab5e5b..a4cc53e5c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1973,7 +1973,7 @@ }, "../sdk/dist": { "name": "@start9labs/start-sdk", - "version": "0.4.0-rev0.lib0.rc8.beta7", + "version": "0.4.0-rev0.lib0.rc8.beta10", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/web/projects/ui/src/app/app/menu/menu.component.ts b/web/projects/ui/src/app/app/menu/menu.component.ts index 68de30c7d..540dee7b9 100644 --- a/web/projects/ui/src/app/app/menu/menu.component.ts +++ b/web/projects/ui/src/app/app/menu/menu.component.ts @@ -20,11 +20,18 @@ import { } 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 { DataModel, PackageState } from 'src/app/services/patch-db/data-model' import { SplitPaneTracker } from 'src/app/services/split-pane.service' import { Emver, THEME } from '@start9labs/shared' import { ConnectionService } from 'src/app/services/connection.service' import { ConfigService } from 'src/app/services/config.service' +import { + getManifest, + isInstalled, + isInstalling, + isRestoring, + isUpdating, +} from 'src/app/util/get-package-data' @Component({ selector: 'app-menu', @@ -79,8 +86,14 @@ export class MenuComponent { filter(([prev, curr]) => Object.values(prev).some( p => - p['install-progress'] && - !curr[p.manifest.id]?.['install-progress'], + [ + PackageState.Installing, + PackageState.Updating, + PackageState.Restoring, + ].includes(p['state-info'].state) && + [PackageState.Installed, PackageState.Removing].includes( + curr[getManifest(p).id]['state-info'].state, + ), ), ), map(([_, curr]) => curr), @@ -97,9 +110,10 @@ export class MenuComponent { Object.entries(marketplace).reduce((list, [_, store]) => { store?.packages.forEach(({ manifest: { id, version } }) => { if ( + local[id] && this.emver.compare( version, - local[id]?.installed?.manifest.version || '', + getManifest(local[id]).version || '', ) === 1 ) list.add(id) diff --git a/web/projects/ui/src/app/components/status/status.component.html b/web/projects/ui/src/app/components/status/status.component.html index e1829f4ca..db9e8f8f7 100644 --- a/web/projects/ui/src/app/components/status/status.component.html +++ b/web/projects/ui/src/app/components/status/status.component.html @@ -12,12 +12,9 @@ . This may take a while - - - {{ progress }} + + + {{ installingInfo.progress.overall | installingProgressString }} diff --git a/web/projects/ui/src/app/components/status/status.component.module.ts b/web/projects/ui/src/app/components/status/status.component.module.ts index ea093b8e9..04fa4690c 100644 --- a/web/projects/ui/src/app/components/status/status.component.module.ts +++ b/web/projects/ui/src/app/components/status/status.component.module.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { UnitConversionPipesModule } from '@start9labs/shared' import { StatusComponent } from './status.component' -import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module' +import { InstallingProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module' @NgModule({ declarations: [StatusComponent], @@ -11,7 +11,7 @@ import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/instal CommonModule, IonicModule, UnitConversionPipesModule, - InstallProgressPipeModule, + InstallingProgressPipeModule, ], exports: [StatusComponent], }) diff --git a/web/projects/ui/src/app/components/status/status.component.ts b/web/projects/ui/src/app/components/status/status.component.ts index 14ece402c..03894406e 100644 --- a/web/projects/ui/src/app/components/status/status.component.ts +++ b/web/projects/ui/src/app/components/status/status.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from '@angular/core' import { ConnectionService } from 'src/app/services/connection.service' -import { InstallProgress } from 'src/app/services/patch-db/data-model' +import { InstallingInfo } from 'src/app/services/patch-db/data-model' import { PrimaryRendering, PrimaryStatus, @@ -20,7 +20,7 @@ export class StatusComponent { @Input() size?: string @Input() style?: string = 'regular' @Input() weight?: string = 'normal' - @Input() installProgress?: InstallProgress + @Input() installingInfo?: InstallingInfo @Input() sigtermTimeout?: string | null = null readonly connected$ = this.connectionService.connected$ diff --git a/web/projects/ui/src/app/modals/app-config/app-config.page.html b/web/projects/ui/src/app/modals/app-config/app-config.page.html index 49eec5f5f..b781e8cad 100644 --- a/web/projects/ui/src/app/modals/app-config/app-config.page.html +++ b/web/projects/ui/src/app/modals/app-config/app-config.page.html @@ -9,7 +9,7 @@ - + - +

- {{ pkg.manifest.title }} has been automatically configured with + {{ manifest.title }} has been automatically configured with recommended defaults. Make whatever changes you want, then click "Save". @@ -59,19 +59,19 @@

- {{ pkg.manifest.title }} + {{ manifest.title }}

- The following modifications have been made to {{ - pkg.manifest.title }} to satisfy {{ dependentInfo.title }}: + The following modifications have been made to {{ manifest.title }} + to satisfy {{ dependentInfo.title }}:

@@ -85,8 +85,7 @@

- No config options for {{ pkg.manifest.title }} {{ - pkg.manifest.version }}. + No config options for {{ manifest.title }} {{ manifest.version }}.

diff --git a/web/projects/ui/src/app/modals/app-config/app-config.page.ts b/web/projects/ui/src/app/modals/app-config/app-config.page.ts index 35388b36c..c8f2186f0 100644 --- a/web/projects/ui/src/app/modals/app-config/app-config.page.ts +++ b/web/projects/ui/src/app/modals/app-config/app-config.page.ts @@ -16,6 +16,7 @@ import { DependentInfo } from 'src/app/types/dependent-info' import { ConfigSpec } from 'src/app/pkg-config/config-types' import { DataModel, + InstalledState, PackageDataEntry, } from 'src/app/services/patch-db/data-model' import { PatchDB } from 'patch-db-client' @@ -26,7 +27,12 @@ import { } from 'src/app/services/form.service' import { compare, Operation, getValueByPointer } from 'fast-json-patch' import { hasCurrentDeps } from 'src/app/util/has-deps' -import { getAllPackages, getPackage } from 'src/app/util/get-package-data' +import { + getAllPackages, + getManifest, + getPackage, + isInstalled, +} from 'src/app/util/get-package-data' import { Breakages } from 'src/app/services/api/api.types' @Component({ @@ -39,7 +45,7 @@ export class AppConfigPage { @Input() dependentInfo?: DependentInfo - pkg!: PackageDataEntry + pkg!: PackageDataEntry loadingText = '' configSpec?: ConfigSpec @@ -68,10 +74,11 @@ export class AppConfigPage { async ngOnInit() { try { const pkg = await getPackage(this.patch, this.pkgId) - if (!pkg) return + if (!pkg || !isInstalled(pkg)) return + this.pkg = pkg - if (!this.pkg.manifest.config) return + if (!this.pkg['state-info'].manifest.config) return let newConfig: object | undefined let patch: Operation[] | undefined @@ -210,7 +217,7 @@ export class AppConfigPage { 'As a result of this change, the following services will no longer work properly and may crash:
    ' const localPkgs = await getAllPackages(this.patch) const bullets = Object.keys(breakages).map(id => { - const title = localPkgs[id].manifest.title + const title = getManifest(localPkgs[id]).title return `
  • ${title}
  • ` }) message = `${message}${bullets}
` diff --git a/web/projects/ui/src/app/modals/backup-select/backup-select.page.ts b/web/projects/ui/src/app/modals/backup-select/backup-select.page.ts index 032c0c840..b1b6cf5e6 100644 --- a/web/projects/ui/src/app/modals/backup-select/backup-select.page.ts +++ b/web/projects/ui/src/app/modals/backup-select/backup-select.page.ts @@ -4,6 +4,7 @@ import { map, take } from 'rxjs/operators' import { DataModel, PackageState } from 'src/app/services/patch-db/data-model' import { PatchDB } from 'patch-db-client' import { firstValueFrom } from 'rxjs' +import { getManifest } from 'src/app/util/get-package-data' @Component({ selector: 'backup-select', @@ -32,13 +33,13 @@ export class BackupSelectPage { map(pkgs => { return Object.values(pkgs) .map(pkg => { - const { id, title } = pkg.manifest + const { id, title } = getManifest(pkg) return { id, title, - icon: pkg['static-files'].icon, - disabled: pkg.state !== PackageState.Installed, - checked: pkg.state === PackageState.Installed, + icon: pkg.icon, + disabled: pkg['state-info'].state !== PackageState.Installed, + checked: false, } }) .sort((a, b) => diff --git a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html index adcfcb829..c611f4a71 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html @@ -8,30 +8,34 @@ - - - Standard Actions - + + + + Standard Actions + - - - Actions for {{ pkg.manifest.title }} - - - + + + Actions for {{ manifest.title }} + + + +
diff --git a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts index bd25a1a42..0ea2f6bbe 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts @@ -11,13 +11,17 @@ import { PatchDB } from 'patch-db-client' import { Action, DataModel, + InstalledState, PackageDataEntry, PackageMainStatus, + StateInfo, + Status, } from 'src/app/services/patch-db/data-model' import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' import { isEmptyObject, ErrorToastService, getPkgId } from '@start9labs/shared' import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page' import { hasCurrentDeps } from 'src/app/util/has-deps' +import { getManifest } from 'src/app/util/get-package-data' @Component({ selector: 'app-actions', @@ -40,11 +44,7 @@ export class AppActionsPage { private readonly patch: PatchDB, ) {} - async handleAction( - pkg: PackageDataEntry, - action: { key: string; value: Action }, - ) { - const status = pkg.installed?.status + async handleAction(status: Status, action: { key: string; value: Action }) { if ( status && (action.value['allowed-statuses'] as PackageMainStatus[]).includes( @@ -120,7 +120,7 @@ export class AppActionsPage { } async tryUninstall(pkg: PackageDataEntry): Promise { - const { title, alerts } = pkg.manifest + const { title, alerts } = getManifest(pkg) let message = alerts.uninstall || diff --git a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts index 5200ebb78..5540866fd 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts @@ -29,7 +29,7 @@ export class AppInterfacesPage { readonly pkgId = getPkgId(this.route) readonly serviceInterfaces$ = this.patch - .watch$('package-data', this.pkgId, 'installed', 'service-interfaces') + .watch$('package-data', this.pkgId, 'service-interfaces') .pipe( map(interfaces => { const sorted = Object.values(interfaces) diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html index 71988ba5b..3849b4327 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html @@ -1,34 +1,34 @@ - +

{{ manifest.title }}

{{ manifest.version | displayEmver }}

diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts index 76559a8b1..95f8785f5 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { - InstalledPackageDataEntry, MainStatus, + PackageDataEntry, PackageMainStatus, } from 'src/app/services/patch-db/data-model' import { PkgInfo } from 'src/app/util/get-package-info' @@ -20,7 +20,7 @@ export class AppListPkgComponent { get pkgMainStatus(): MainStatus { return ( - this.pkg.entry.installed?.status.main || { + this.pkg.entry.status.main || { status: PackageMainStatus.Stopped, } ) @@ -32,10 +32,7 @@ export class AppListPkgComponent { : null } - launchUi( - e: Event, - interfaces: InstalledPackageDataEntry['service-interfaces'], - ): void { + launchUi(e: Event, interfaces: PackageDataEntry['service-interfaces']): void { e.stopPropagation() e.preventDefault() this.launcherService.launch(interfaces) diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.html b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.html index 3bc38f762..cc19bdff5 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.html @@ -27,7 +27,7 @@ sizeMd="6" > diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.ts index f7d7685ff..e88067ede 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.ts @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' import { PatchDB } from 'patch-db-client' import { DataModel } from 'src/app/services/patch-db/data-model' import { filter, map, pairwise, startWith } from 'rxjs/operators' +import { getManifest } from 'src/app/util/get-package-data' @Component({ selector: 'app-list', @@ -20,7 +21,7 @@ export class AppListPage { }), map(([_, pkgs]) => pkgs.sort((a, b) => - b.manifest.title.toLowerCase() > a.manifest.title.toLowerCase() + getManifest(b).title.toLowerCase() > getManifest(a).title.toLowerCase() ? -1 : 1, ), diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts b/web/projects/ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts index c0d00c2c9..17eda2827 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform } from '@angular/core' -import { Observable, combineLatest, firstValueFrom } from 'rxjs' +import { Observable, combineLatest } from 'rxjs' import { filter, map } from 'rxjs/operators' import { DataModel } from 'src/app/services/patch-db/data-model' import { getPackageInfo, PkgInfo } from '../../../util/get-package-info' diff --git a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts index 8a49bad52..15af441a7 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts @@ -41,7 +41,7 @@ export class AppPropertiesPage { unmasked: { [key: string]: boolean } = {} stopped$ = this.patch - .watch$('package-data', this.pkgId, 'installed', 'status', 'main', 'status') + .watch$('package-data', this.pkgId, 'status', 'main', 'status') .pipe(map(status => status === PackageMainStatus.Stopped)) @ViewChild(IonBackButtonDelegate, { static: false }) diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts index c69910c78..d76ae58d8 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts @@ -18,7 +18,7 @@ import { AppShowAdditionalComponent } from './components/app-show-additional/app import { HealthColorPipe } from './pipes/health-color.pipe' import { ToHealthChecksPipe } from './pipes/to-health-checks.pipe' import { ToButtonsPipe } from './pipes/to-buttons.pipe' -import { ProgressDataPipe } from './pipes/progress-data.pipe' +import { InstallingProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module' const routes: Routes = [ { @@ -31,7 +31,6 @@ const routes: Routes = [ declarations: [ AppShowPage, HealthColorPipe, - ProgressDataPipe, ToHealthChecksPipe, ToButtonsPipe, AppShowHeaderComponent, @@ -44,7 +43,7 @@ const routes: Routes = [ ], imports: [ CommonModule, - StatusComponentModule, + InstallingProgressPipeModule, IonicModule, RouterModule.forChild(routes), AppConfigPageModule, @@ -52,6 +51,7 @@ const routes: Routes = [ LaunchablePipeModule, UiPipeModule, ResponsiveColModule, + StatusComponentModule, ], }) export class AppShowPageModule {} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html index d2b7eaead..37ae25304 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html @@ -7,9 +7,8 @@ @@ -19,11 +18,13 @@ - + - + diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts index ab250e1a7..01a3067cd 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts @@ -3,16 +3,12 @@ import { NavController } from '@ionic/angular' import { PatchDB } from 'patch-db-client' import { DataModel, - InstalledPackageDataEntry, + InstallingState, Manifest, PackageDataEntry, - PackageState, + UpdatingState, } from 'src/app/services/patch-db/data-model' -import { - PackageStatus, - PrimaryStatus, - renderPkgStatus, -} from 'src/app/services/pkg-status-rendering.service' +import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' import { map, tap } from 'rxjs/operators' import { ActivatedRoute, NavigationExtras } from '@angular/router' import { getPkgId } from '@start9labs/shared' @@ -24,6 +20,13 @@ import { PkgDependencyErrors, } from 'src/app/services/dep-error.service' import { combineLatest } from 'rxjs' +import { + getManifest, + isInstalled, + isInstalling, + isRestoring, + isUpdating, +} from 'src/app/util/get-package-data' export interface DependencyInfo { id: string @@ -35,12 +38,6 @@ export interface DependencyInfo { action: () => any } -const STATES = [ - PackageState.Installing, - PackageState.Updating, - PackageState.Restoring, -] - @Component({ selector: 'app-show', templateUrl: './app-show.page.html', @@ -66,6 +63,8 @@ export class AppShowPage { }), ) + isInstalled = isInstalled + constructor( private readonly route: ActivatedRoute, private readonly navCtrl: NavController, @@ -74,55 +73,44 @@ export class AppShowPage { private readonly depErrorService: DepErrorService, ) {} - isInstalled({ state }: PackageDataEntry): boolean { - return state === PackageState.Installed - } - - isRunning({ primary }: PackageStatus): boolean { - return primary === PrimaryStatus.Running - } - - isBackingUp({ primary }: PackageStatus): boolean { - return primary === PrimaryStatus.BackingUp - } - - showProgress({ state }: PackageDataEntry): boolean { - return STATES.includes(state) + showProgress( + pkg: PackageDataEntry, + ): pkg is PackageDataEntry { + return isInstalling(pkg) || isUpdating(pkg) || isRestoring(pkg) } private getDepInfo( pkg: PackageDataEntry, depErrors: PkgDependencyErrors, ): DependencyInfo[] { - const pkgInstalled = pkg.installed + const manifest = getManifest(pkg) - if (!pkgInstalled) return [] - - return Object.keys(pkgInstalled['current-dependencies']) - .filter(id => !!pkgInstalled.manifest.dependencies[id]) - .map(id => this.getDepValues(pkgInstalled, id, depErrors)) + return Object.keys(pkg['current-dependencies']) + .filter(id => !!manifest.dependencies[id]) + .map(id => this.getDepValues(pkg, manifest, id, depErrors)) } private getDepValues( - pkgInstalled: InstalledPackageDataEntry, + pkg: PackageDataEntry, + manifest: Manifest, depId: string, depErrors: PkgDependencyErrors, ): DependencyInfo { const { errorText, fixText, fixAction } = this.getDepErrors( - pkgInstalled, + manifest, depId, depErrors, ) - const depInfo = pkgInstalled['dependency-info'][depId] + const depInfo = pkg['dependency-info'][depId] return { id: depId, - version: pkgInstalled.manifest.dependencies[depId].version, // do we want this version range? + version: manifest.dependencies[depId].version, // do we want this version range? title: depInfo?.title || depId, icon: depInfo?.icon || '', errorText: errorText - ? `${errorText}. ${pkgInstalled.manifest.title} will not work as expected.` + ? `${errorText}. ${manifest.title} will not work as expected.` : '', actionText: fixText || 'View', action: @@ -131,11 +119,10 @@ export class AppShowPage { } private getDepErrors( - pkgInstalled: InstalledPackageDataEntry, + manifest: Manifest, depId: string, depErrors: PkgDependencyErrors, ) { - const pkgManifest = pkgInstalled.manifest const depError = depErrors[depId] let errorText: string | null = null @@ -146,15 +133,15 @@ export class AppShowPage { if (depError.type === DependencyErrorType.NotInstalled) { errorText = 'Not installed' fixText = 'Install' - fixAction = () => this.fixDep(pkgManifest, 'install', depId) + fixAction = () => this.fixDep(manifest, 'install', depId) } else if (depError.type === DependencyErrorType.IncorrectVersion) { errorText = 'Incorrect version' fixText = 'Update' - fixAction = () => this.fixDep(pkgManifest, 'update', depId) + fixAction = () => this.fixDep(manifest, 'update', depId) } else if (depError.type === DependencyErrorType.ConfigUnsatisfied) { errorText = 'Config not satisfied' fixText = 'Auto config' - fixAction = () => this.fixDep(pkgManifest, 'configure', depId) + fixAction = () => this.fixDep(manifest, 'configure', depId) } else if (depError.type === DependencyErrorType.NotRunning) { errorText = 'Not running' fixText = 'Start' diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.html index 81fe8fb84..240eccff2 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.html @@ -1,5 +1,5 @@ Additional Info - + diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.ts index 501898428..3487093b5 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.ts @@ -3,7 +3,7 @@ import { ModalController, ToastController } from '@ionic/angular' import { copyToClipboard, MarkdownComponent } from '@start9labs/shared' import { from } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { PackageDataEntry } from 'src/app/services/patch-db/data-model' +import { Manifest } from 'src/app/services/patch-db/data-model' @Component({ selector: 'app-show-additional', @@ -12,7 +12,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model' }) export class AppShowAdditionalComponent { @Input() - pkg!: PackageDataEntry + manifest!: Manifest constructor( private readonly modalCtrl: ModalController, @@ -35,10 +35,16 @@ export class AppShowAdditionalComponent { } async presentModalLicense() { + const { id, version } = this.manifest + const modal = await this.modalCtrl.create({ componentProps: { title: 'License', - content: from(this.api.getStatic(this.pkg['static-files']['license'])), + content: from( + this.api.getStatic( + `/public/package-data/${id}/${version}/LICENSE.md`, + ), + ), }, component: MarkdownComponent, }) diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.html index 17dcd1be1..06f517222 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.html @@ -4,15 +4,12 @@
- - -

- {{ pkg.manifest.title }} + + +

+ {{ manifest.title }}

-

{{ pkg.manifest.version | displayEmver }}

+

{{ manifest.version | displayEmver }}

diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.html index fe854f8fc..9ea8d29ac 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.html @@ -1,5 +1,5 @@ Health Checks @@ -34,7 +34,7 @@ >

- {{ pkg.manifest['health-checks'][health.key].name }} + {{ manifest['health-checks'][health.key].name }}

@@ -49,13 +49,11 @@ : - {{ - pkg.manifest['health-checks'][health.key]['success-message'] - }} + {{ manifest['health-checks'][health.key]['success-message'] }}

@@ -70,7 +68,7 @@ >

- {{ pkg.manifest['health-checks'][health.key].name }} + {{ manifest['health-checks'][health.key].name }}

Awaiting result...

diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts index 5db1ab1ca..a6cbc4ab7 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts @@ -1,9 +1,6 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ConnectionService } from 'src/app/services/connection.service' -import { - HealthResult, - PackageDataEntry, -} from 'src/app/services/patch-db/data-model' +import { HealthResult, Manifest } from 'src/app/services/patch-db/data-model' @Component({ selector: 'app-show-health-checks', @@ -13,7 +10,7 @@ import { }) export class AppShowHealthChecksComponent { @Input() - pkg!: PackageDataEntry + manifest!: Manifest HealthResult = HealthResult diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html index 3134cee5e..e259f2792 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html @@ -1,20 +1,18 @@ -

Downloading: {{ progressData.downloadProgress }}%

- - -

Validating: {{ progressData.validateProgress }}%

- - -

Unpacking: {{ progressData.unpackProgress }}%

- + +

+ {{ phase.name }} + + : {{ progress * 100 }}% + +

+ +
diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.ts index 8ee7b750a..ab5c8e416 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.ts @@ -1,9 +1,5 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { - InstallProgress, - PackageDataEntry, -} from 'src/app/services/patch-db/data-model' -import { ProgressData } from 'src/app/types/progress-data' +import { FullProgress } from 'src/app/services/patch-db/data-model' @Component({ selector: 'app-show-progress', @@ -13,26 +9,5 @@ import { ProgressData } from 'src/app/types/progress-data' }) export class AppShowProgressComponent { @Input() - pkg!: PackageDataEntry - - @Input() - progressData!: ProgressData - - get unpackingBuffer(): number { - return this.progressData.validateProgress === 100 && - !this.progressData.unpackProgress - ? 0 - : 1 - } - - get validationBuffer(): number { - return this.progressData.downloadProgress === 100 && - !this.progressData.validateProgress - ? 0 - : 1 - } - - getColor(action: keyof InstallProgress): string { - return this.pkg['install-progress']?.[action] ? 'success' : 'secondary' - } + phases!: FullProgress['phases'] } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html index e2379713b..4521f8e5e 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html @@ -4,14 +4,14 @@
- + @@ -59,7 +59,9 @@ *ngIf="pkgStatus && interfaces && (interfaces | hasUi)" class="action-button" color="primary" - [disabled]="!(pkg.state | isLaunchable: pkgStatus.main.status)" + [disabled]=" + !(pkg['state-info'].state | isLaunchable: pkgStatus.main.status) + " (click)="launchUi(interfaces)" > diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts index dd8832c55..9bca7f2ac 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts @@ -6,7 +6,7 @@ import { PrimaryStatus, } from 'src/app/services/pkg-status-rendering.service' import { - InstalledPackageDataEntry, + Manifest, PackageDataEntry, PackageMainStatus, PackageState, @@ -18,6 +18,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' import { ModalService } from 'src/app/services/modal.service' import { hasCurrentDeps } from 'src/app/util/has-deps' import { ConnectionService } from 'src/app/services/connection.service' +import { isInstalled, getManifest } from 'src/app/util/get-package-data' @Component({ selector: 'app-show-status', @@ -34,6 +35,8 @@ export class AppShowStatusComponent { PR = PrimaryRendering + isInstalled = isInstalled + readonly connected$ = this.connectionService.connected$ constructor( @@ -46,18 +49,16 @@ export class AppShowStatusComponent { private readonly connectionService: ConnectionService, ) {} - get interfaces(): - | InstalledPackageDataEntry['service-interfaces'] - | undefined { - return this.pkg.installed?.['service-interfaces'] + get interfaces(): PackageDataEntry['service-interfaces'] { + return this.pkg['service-interfaces'] } - get pkgStatus(): Status | null { - return this.pkg.installed?.status || null + get pkgStatus(): Status { + return this.pkg.status } - get isInstalled(): boolean { - return this.pkg.state === PackageState.Installed + get manifest(): Manifest { + return getManifest(this.pkg) } get isRunning(): boolean { @@ -82,25 +83,25 @@ export class AppShowStatusComponent { : null } - launchUi(interfaces: InstalledPackageDataEntry['service-interfaces']): void { + launchUi(interfaces: PackageDataEntry['service-interfaces']): void { this.launcherService.launch(interfaces) } async presentModalConfig(): Promise { return this.modalService.presentModalConfig({ - pkgId: this.id, + pkgId: this.manifest.id, }) } async tryStart(): Promise { if (this.status.dependency === 'warning') { - const depErrMsg = `${this.pkg.manifest.title} has unmet dependencies. It will not work as expected.` + const depErrMsg = `${this.manifest.title} has unmet dependencies. It will not work as expected.` const proceed = await this.presentAlertStart(depErrMsg) if (!proceed) return } - const alertMsg = this.pkg.manifest.alerts.start + const alertMsg = this.manifest.alerts.start if (alertMsg) { const proceed = await this.presentAlertStart(alertMsg) @@ -112,7 +113,7 @@ export class AppShowStatusComponent { } async tryStop(): Promise { - const { title, alerts } = this.pkg.manifest + const { title, alerts } = this.manifest let message = alerts.stop || '' if (hasCurrentDeps(this.pkg)) { @@ -150,7 +151,7 @@ export class AppShowStatusComponent { if (hasCurrentDeps(this.pkg)) { const alert = await this.alertCtrl.create({ header: 'Warning', - message: `Services that depend on ${this.pkg.manifest.title} may temporarily experiences issues`, + message: `Services that depend on ${this.manifest.title} may temporarily experiences issues`, buttons: [ { text: 'Cancel', @@ -173,10 +174,6 @@ export class AppShowStatusComponent { } } - private get id(): string { - return this.pkg.manifest.id - } - private async start(): Promise { const loader = await this.loadingCtrl.create({ message: `Starting...`, @@ -184,7 +181,7 @@ export class AppShowStatusComponent { await loader.present() try { - await this.embassyApi.startPackage({ id: this.id }) + await this.embassyApi.startPackage({ id: this.manifest.id }) } catch (e: any) { this.errToast.present(e) } finally { @@ -199,7 +196,7 @@ export class AppShowStatusComponent { await loader.present() try { - await this.embassyApi.stopPackage({ id: this.id }) + await this.embassyApi.stopPackage({ id: this.manifest.id }) } catch (e: any) { this.errToast.present(e) } finally { @@ -214,7 +211,7 @@ export class AppShowStatusComponent { await loader.present() try { - await this.embassyApi.restartPackage({ id: this.id }) + await this.embassyApi.restartPackage({ id: this.manifest.id }) } catch (e: any) { this.errToast.present(e) } finally { diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/progress-data.pipe.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/progress-data.pipe.ts deleted file mode 100644 index 1e5397648..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/progress-data.pipe.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core' -import { PackageDataEntry } from 'src/app/services/patch-db/data-model' -import { ProgressData } from 'src/app/types/progress-data' -import { packageLoadingProgress } from 'src/app/util/package-loading-progress' - -@Pipe({ - name: 'progressData', -}) -export class ProgressDataPipe implements PipeTransform { - transform(pkg: PackageDataEntry): ProgressData | null { - return packageLoadingProgress(pkg['install-progress']) - } -} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts index eeb00b435..a25914367 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts @@ -4,12 +4,15 @@ import { ModalController, NavController } from '@ionic/angular' import { MarkdownComponent } from '@start9labs/shared' import { DataModel, + InstalledState, + Manifest, PackageDataEntry, } 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 { PatchDB } from 'patch-db-client' +import { getManifest } from 'src/app/util/get-package-data' export interface Button { title: string @@ -33,26 +36,26 @@ export class ToButtonsPipe implements PipeTransform { private readonly patch: PatchDB, ) {} - transform(pkg: PackageDataEntry): Button[] { - const pkgTitle = pkg.manifest.title + transform(pkg: PackageDataEntry): Button[] { + const manifest = pkg['state-info'].manifest return [ // instructions { - action: () => this.presentModalInstructions(pkg), + action: () => this.presentModalInstructions(manifest), title: 'Instructions', - description: `Understand how to use ${pkgTitle}`, + description: `Understand how to use ${manifest.title}`, icon: 'list-outline', highlighted$: this.patch - .watch$('ui', 'ack-instructions', pkg.manifest.id) + .watch$('ui', 'ack-instructions', manifest.id) .pipe(map(seen => !seen)), }, // config { action: async () => - this.modalService.presentModalConfig({ pkgId: pkg.manifest.id }), + this.modalService.presentModalConfig({ pkgId: manifest.id }), title: 'Config', - description: `Customize ${pkgTitle}`, + description: `Customize ${manifest.title}`, icon: 'options-outline', }, // properties @@ -71,7 +74,7 @@ export class ToButtonsPipe implements PipeTransform { action: () => this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }), title: 'Actions', - description: `Uninstall and other commands specific to ${pkgTitle}`, + description: `Uninstall and other commands specific to ${manifest.title}`, icon: 'flash-outline', }, // interfaces @@ -97,16 +100,18 @@ export class ToButtonsPipe implements PipeTransform { ] } - private async presentModalInstructions(pkg: PackageDataEntry) { + private async presentModalInstructions(manifest: Manifest) { this.apiService - .setDbValue(['ack-instructions', pkg.manifest.id], true) + .setDbValue(['ack-instructions', manifest.id], true) .catch(e => console.error('Failed to mark instructions as seen', e)) const modal = await this.modalCtrl.create({ componentProps: { title: 'Instructions', content: from( - this.apiService.getStatic(pkg['static-files']['instructions']), + this.apiService.getStatic( + `/public/package-data/${manifest.id}/${manifest.version}/INSTRUCTIONS.md`, + ), ), }, component: MarkdownComponent, @@ -115,17 +120,22 @@ export class ToButtonsPipe implements PipeTransform { await modal.present() } - private viewInMarketplaceButton(pkg: PackageDataEntry): Button { - const url = pkg.installed?.['marketplace-url'] + private viewInMarketplaceButton( + pkg: PackageDataEntry, + ): Button { + const url = pkg['marketplace-url'] const queryParams = url ? { url } : {} let button: Button = { title: 'Marketplace Listing', icon: 'storefront-outline', action: () => - this.navCtrl.navigateForward([`marketplace/${pkg.manifest.id}`], { - queryParams, - }), + this.navCtrl.navigateForward( + [`marketplace/${pkg['state-info'].manifest.id}`], + { + queryParams, + }, + ), disabled: false, description: 'View service in the marketplace', } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts index 8ba9bd4f3..066411eb0 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts @@ -2,7 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core' import { DataModel, HealthCheckResult, - PackageDataEntry, + Manifest, PackageMainStatus, } from 'src/app/services/patch-db/data-model' import { isEmptyObject } from '@start9labs/shared' @@ -17,15 +17,15 @@ export class ToHealthChecksPipe implements PipeTransform { constructor(private readonly patch: PatchDB) {} transform( - pkg: PackageDataEntry, + manifest: Manifest, ): Observable> | null { - const healthChecks = Object.keys(pkg.manifest['health-checks']).reduce( + const healthChecks = Object.keys(manifest['health-checks']).reduce( (obj, key) => ({ ...obj, [key]: null }), {}, ) const healthChecks$ = this.patch - .watch$('package-data', pkg.manifest.id, 'installed', 'status', 'main') + .watch$('package-data', manifest.id, 'status', 'main') .pipe( map(main => { // Question: is this ok or do we have to use Object.keys diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html index b150d2d4f..8d347ffa2 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html @@ -8,7 +8,7 @@ View Installed - + -
+ +
-
+ +
Removing
-
+ +
Installing diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts index 05e36471b..9db50dc29 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts @@ -1,8 +1,13 @@ import { Component, Input } from '@angular/core' +import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { - PackageDataEntry, - PackageState, -} from 'src/app/services/patch-db/data-model' + isInstalled, + isInstalling, + isUpdating, + isRemoving, + isRestoring, + getManifest, +} from 'src/app/util/get-package-data' @Component({ selector: 'marketplace-status', @@ -14,9 +19,13 @@ export class MarketplaceStatusComponent { @Input() localPkg?: PackageDataEntry - PackageState = PackageState + isInstalled = isInstalled + isInstalling = isInstalling + isUpdating = isUpdating + isRemoving = isRemoving + isRestoring = isRestoring get localVersion(): string { - return this.localPkg?.manifest.version || '' + return this.localPkg ? getManifest(this.localPkg).version : '' } } diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts index 8a7151f40..c31f4fd16 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts @@ -2,8 +2,7 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { IonicModule } from '@ionic/angular' import { EmverPipesModule } from '@start9labs/shared' - -import { InstallProgressPipeModule } from '../../../pipes/install-progress/install-progress.module' +import { InstallingProgressPipeModule } from '../../../pipes/install-progress/install-progress.module' import { MarketplaceStatusComponent } from './marketplace-status.component' @NgModule({ @@ -11,7 +10,7 @@ import { MarketplaceStatusComponent } from './marketplace-status.component' CommonModule, IonicModule, EmverPipesModule, - InstallProgressPipeModule, + InstallingProgressPipeModule, ], declarations: [MarketplaceStatusComponent], exports: [MarketplaceStatusComponent], diff --git a/web/projects/ui/src/app/pages/notifications/notifications.module.ts b/web/projects/ui/src/app/pages/notifications/notifications.module.ts index 5ff2b4572..4ec29702c 100644 --- a/web/projects/ui/src/app/pages/notifications/notifications.module.ts +++ b/web/projects/ui/src/app/pages/notifications/notifications.module.ts @@ -6,6 +6,7 @@ import { NotificationsPage } from './notifications.page' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { SharedPipesModule } from '@start9labs/shared' import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module' +import { UiPipeModule } from 'src/app/pipes/ui/ui.module' const routes: Routes = [ { @@ -22,6 +23,7 @@ const routes: Routes = [ BadgeMenuComponentModule, SharedPipesModule, BackupReportPageModule, + UiPipeModule, ], declarations: [NotificationsPage], }) diff --git a/web/projects/ui/src/app/pages/notifications/notifications.page.html b/web/projects/ui/src/app/pages/notifications/notifications.page.html index f9e697107..c53c140ff 100644 --- a/web/projects/ui/src/app/pages/notifications/notifications.page.html +++ b/web/projects/ui/src/app/pages/notifications/notifications.page.html @@ -87,8 +87,8 @@

- - {{ $any(packageData[pkgId])?.manifest.title || pkgId }} - + {{ packageData[pkgId] ? (packageData[pkgId] | + toManifest).title : pkgId }} - {{ not.title }} diff --git a/web/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.html b/web/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.html index 1bbd5aa86..7dcf4d793 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.html +++ b/web/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.html @@ -15,9 +15,9 @@ - + - {{ pkg.value.manifest.title }} + {{ (pkg.value | toManifest).title }} diff --git a/web/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.ts b/web/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.ts index 85ffbb2e1..125e9f28e 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.ts @@ -25,8 +25,6 @@ export class BackingUpComponent { 'backup-progress', ) - PackageMainStatus = PackageMainStatus - constructor(private readonly patch: PatchDB) {} } @@ -35,14 +33,7 @@ export class BackingUpComponent { }) export class PkgMainStatusPipe implements PipeTransform { transform(pkgId: string): Observable { - return this.patch.watch$( - 'package-data', - pkgId, - 'installed', - 'status', - 'main', - 'status', - ) + return this.patch.watch$('package-data', pkgId, 'status', 'main', 'status') } constructor(private readonly patch: PatchDB) {} diff --git a/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts b/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts index 6a1782985..d5b32cd6e 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts @@ -8,6 +8,7 @@ import { BackupDrivesComponentModule } from 'src/app/components/backup-drives/ba import { SharedPipesModule } from '@start9labs/shared' import { BackupSelectPageModule } from 'src/app/modals/backup-select/backup-select.module' import { PkgMainStatusPipe } from './backing-up/backing-up.component' +import { UiPipeModule } from 'src/app/pipes/ui/ui.module' const routes: Routes = [ { @@ -24,6 +25,7 @@ const routes: Routes = [ SharedPipesModule, BackupDrivesComponentModule, BackupSelectPageModule, + UiPipeModule, ], declarations: [ServerBackupPage, BackingUpComponent, PkgMainStatusPipe], }) diff --git a/web/projects/ui/src/app/pages/updates/updates.module.ts b/web/projects/ui/src/app/pages/updates/updates.module.ts index e07e798b4..62a6d2076 100644 --- a/web/projects/ui/src/app/pages/updates/updates.module.ts +++ b/web/projects/ui/src/app/pages/updates/updates.module.ts @@ -12,7 +12,7 @@ import { } 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' +import { InstallingProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module' import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module' import { MimeTypePipeModule } from '@start9labs/marketplace' @@ -34,7 +34,7 @@ const routes: Routes = [ SkeletonListComponentModule, MarkdownPipeModule, RoundProgressModule, - InstallProgressPipeModule, + InstallingProgressPipeModule, StoreIconComponentModule, EmverPipesModule, MimeTypePipeModule, diff --git a/web/projects/ui/src/app/pages/updates/updates.page.html b/web/projects/ui/src/app/pages/updates/updates.page.html index be48693f9..4ee0d53f4 100644 --- a/web/projects/ui/src/app/pages/updates/updates.page.html +++ b/web/projects/ui/src/app/pages/updates/updates.page.html @@ -39,8 +39,8 @@

{{ pkg.manifest.title }}

- {{ local.installed?.manifest?.version || '' | - displayEmver }} + {{ local['state-info'].manifest.version | displayEmver + }}   @@ -57,8 +57,8 @@
+ localPkgs: Record> errors: string[] } @@ -36,7 +42,14 @@ export class UpdatesPage { readonly data$: Observable = combineLatest({ hosts: this.marketplaceService.getKnownHosts$(true), marketplace: this.marketplaceService.getMarketplace$(), - localPkgs: this.patch.watch$('package-data'), + localPkgs: this.patch.watch$('package-data').pipe( + map(pkgs => + Object.values(pkgs).reduce((acc, curr) => { + if (isInstalled(curr) || isUpdating(curr)) return { ...acc, curr } + return acc + }, {} as Record>), + ), + ), errors: this.marketplaceService.getRequestErrors$(), }) @@ -154,14 +167,17 @@ export class FilterUpdatesPipe implements PipeTransform { transform( pkgs: MarketplacePkg[], - local: Record, + local: Record>, ): MarketplacePkg[] { - return pkgs.filter( - ({ manifest }) => + return pkgs.filter(({ manifest }) => { + const localPkg = local[manifest.id] + return ( + localPkg && this.emver.compare( manifest.version, - local[manifest.id]?.installed?.manifest.version || '', - ) === 1, - ) + localPkg['state-info'].manifest.version, + ) === 1 + ) + }) } } diff --git a/web/projects/ui/src/app/pages/widgets/built-in/health/health.component.ts b/web/projects/ui/src/app/pages/widgets/built-in/health/health.component.ts index e4f456c01..5a0f74767 100644 --- a/web/projects/ui/src/app/pages/widgets/built-in/health/health.component.ts +++ b/web/projects/ui/src/app/pages/widgets/built-in/health/health.component.ts @@ -9,6 +9,7 @@ import { PrimaryStatus } from 'src/app/services/pkg-status-rendering.service' import { getPackageInfo, PkgInfo } from '../../../../util/get-package-info' import { combineLatest } from 'rxjs' import { DepErrorService } from 'src/app/services/dep-error.service' +import { getManifest } from 'src/app/util/get-package-data' @Component({ selector: 'widget-health', @@ -31,7 +32,7 @@ export class HealthComponent { ]).pipe( map(([data, depErrors]) => { const pkgs = Object.values(data).map(pkg => - getPackageInfo(pkg, depErrors[pkg.manifest.id]), + getPackageInfo(pkg, depErrors[getManifest(pkg).id]), ) const result = this.labels.reduce>( (acc, label) => ({ diff --git a/web/projects/ui/src/app/pipes/install-progress/install-progress.module.ts b/web/projects/ui/src/app/pipes/install-progress/install-progress.module.ts index 37bbd0744..a0997f29c 100644 --- a/web/projects/ui/src/app/pipes/install-progress/install-progress.module.ts +++ b/web/projects/ui/src/app/pipes/install-progress/install-progress.module.ts @@ -1,11 +1,11 @@ import { NgModule } from '@angular/core' import { - InstallProgressDisplayPipe, - InstallProgressPipe, + InstallingProgressDisplayPipe, + InstallingProgressPipe, } from './install-progress.pipe' @NgModule({ - declarations: [InstallProgressPipe, InstallProgressDisplayPipe], - exports: [InstallProgressPipe, InstallProgressDisplayPipe], + declarations: [InstallingProgressPipe, InstallingProgressDisplayPipe], + exports: [InstallingProgressPipe, InstallingProgressDisplayPipe], }) -export class InstallProgressPipeModule {} +export class InstallingProgressPipeModule {} diff --git a/web/projects/ui/src/app/pipes/install-progress/install-progress.pipe.ts b/web/projects/ui/src/app/pipes/install-progress/install-progress.pipe.ts index 459dc722b..e6f87a127 100644 --- a/web/projects/ui/src/app/pipes/install-progress/install-progress.pipe.ts +++ b/web/projects/ui/src/app/pipes/install-progress/install-progress.pipe.ts @@ -1,24 +1,32 @@ import { Pipe, PipeTransform } from '@angular/core' -import { InstallProgress } from 'src/app/services/patch-db/data-model' -import { packageLoadingProgress } from 'src/app/util/package-loading-progress' +import { Progress } from 'src/app/services/patch-db/data-model' @Pipe({ - name: 'installProgress', + name: 'installingProgressString', }) -export class InstallProgressPipe implements PipeTransform { - transform(installProgress?: InstallProgress): number { - return packageLoadingProgress(installProgress)?.totalProgress || 0 +export class InstallingProgressDisplayPipe implements PipeTransform { + transform(progress: Progress): string { + if (progress === true) return 'finalizing' + if (progress === false || !progress.total) return 'unknown %' + const percentage = Math.round((100 * progress.done) / progress.total) + + return percentage < 99 ? String(percentage) + '%' : 'finalizing' } } @Pipe({ - name: 'installProgressDisplay', + name: 'installingProgress', }) -export class InstallProgressDisplayPipe implements PipeTransform { - transform(installProgress?: InstallProgress): string { - const totalProgress = - packageLoadingProgress(installProgress)?.totalProgress || 0 - - return totalProgress < 99 ? totalProgress + '%' : 'finalizing' +export class InstallingProgressPipe implements PipeTransform { + transform(progress: Progress): number | null { + if (progress === true) return 1 + if (progress === false || !progress.total) return null + return Number((progress.done / progress.total).toFixed(2)) } } + +function getProgress(progress: Progress): number | null { + if (progress === true) return 1 + if (progress === false || !progress.total) return null + return Number((progress.done / progress.total).toFixed(2)) +} diff --git a/web/projects/ui/src/app/pipes/ui/ui.module.ts b/web/projects/ui/src/app/pipes/ui/ui.module.ts index 9637306de..a031c2059 100644 --- a/web/projects/ui/src/app/pipes/ui/ui.module.ts +++ b/web/projects/ui/src/app/pipes/ui/ui.module.ts @@ -1,8 +1,8 @@ import { NgModule } from '@angular/core' -import { UiPipe } from './ui.pipe' +import { ToManifestPipe, UiPipe } from './ui.pipe' @NgModule({ - declarations: [UiPipe], - exports: [UiPipe], + declarations: [UiPipe, ToManifestPipe], + exports: [UiPipe, ToManifestPipe], }) export class UiPipeModule {} diff --git a/web/projects/ui/src/app/pipes/ui/ui.pipe.ts b/web/projects/ui/src/app/pipes/ui/ui.pipe.ts index 03ec11df0..2a81e4afd 100644 --- a/web/projects/ui/src/app/pipes/ui/ui.pipe.ts +++ b/web/projects/ui/src/app/pipes/ui/ui.pipe.ts @@ -1,14 +1,22 @@ import { Pipe, PipeTransform } from '@angular/core' -import { InstalledPackageDataEntry } from '../../services/patch-db/data-model' +import { Manifest, PackageDataEntry } from '../../services/patch-db/data-model' import { hasUi } from '../../services/config.service' +import { getManifest } from 'src/app/util/get-package-data' @Pipe({ name: 'hasUi', }) export class UiPipe implements PipeTransform { - transform( - interfaces: InstalledPackageDataEntry['service-interfaces'], - ): boolean { + transform(interfaces: PackageDataEntry['service-interfaces']): boolean { return interfaces ? hasUi(interfaces) : false } } + +@Pipe({ + name: 'toManifest', +}) +export class ToManifestPipe implements PipeTransform { + transform(pkg: PackageDataEntry): Manifest { + return getManifest(pkg) + } +} diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index 9caa257a4..46738041a 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -1,5 +1,6 @@ import { DockerIoFormat, + InstalledState, Manifest, PackageDataEntry, PackageMainStatus, @@ -1748,661 +1749,648 @@ export module Mock { export const MockDependencyConfig = MockConfig - export const bitcoind: PackageDataEntry = { - state: PackageState.Installed, - 'static-files': { - license: '/public/package-data/bitcoind/0.20.0/LICENSE.md', - icon: '/assets/img/service-icons/bitcoind.svg', - instructions: '/public/package-data/bitcoind/0.20.0/INSTRUCTIONS.md', - }, - manifest: MockManifestBitcoind, - installed: { + export const bitcoind: PackageDataEntry = { + 'state-info': { + state: PackageState.Installed, manifest: MockManifestBitcoind, - 'last-backup': null, - status: { - configured: true, - main: { - status: PackageMainStatus.Running, - started: new Date().toISOString(), - health: {}, - }, - 'dependency-config-errors': {}, - }, - 'service-interfaces': { - ui: { - id: 'ui', - hasPrimary: false, - disabled: false, - masked: false, - name: 'Web UI', - description: - 'A launchable web app for you to interact with your Bitcoin node', - type: 'ui', - addressInfo: { - username: null, - hostId: 'abcdefg', - bindOptions: { - scheme: 'http', - preferredExternalPort: 80, - addSsl: { - addXForwardedHeaders: false, - preferredExternalPort: 443, - scheme: 'https', - }, - secure: null, - }, - suffix: '', - }, - hostInfo: { - id: 'abcdefg', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 1234, - }, - }, - { - kind: 'onion', - hostname: { - value: 'bitcoin-ui-address.onion', - port: 80, - sslPort: 443, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: null, - sslPort: 1234, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: null, - sslPort: 1234, - }, - }, - ], - }, - }, - rpc: { - id: 'rpc', - hasPrimary: false, - disabled: false, - masked: false, - name: 'RPC', - description: - 'Used by dependent services and client wallets for connecting to your node', - type: 'api', - addressInfo: { - username: null, - hostId: 'bcdefgh', - bindOptions: { - scheme: 'http', - preferredExternalPort: 80, - addSsl: { - addXForwardedHeaders: false, - preferredExternalPort: 443, - scheme: 'https', - }, - secure: null, - }, - suffix: '', - }, - hostInfo: { - id: 'bcdefgh', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 2345, - }, - }, - { - kind: 'onion', - hostname: { - value: 'bitcoin-rpc-address.onion', - port: 80, - sslPort: 443, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: null, - sslPort: 2345, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: null, - sslPort: 2345, - }, - }, - ], - }, - }, - p2p: { - id: 'p2p', - hasPrimary: true, - disabled: false, - masked: false, - name: 'P2P', - description: - 'Used for connecting to other nodes on the Bitcoin network', - type: 'p2p', - addressInfo: { - username: null, - hostId: 'cdefghi', - bindOptions: { - scheme: 'bitcoin', - preferredExternalPort: 8333, - addSsl: null, - secure: { ssl: false }, - }, - suffix: '', - }, - hostInfo: { - id: 'cdefghi', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: 3456, - sslPort: null, - }, - }, - { - kind: 'onion', - hostname: { - value: 'bitcoin-p2p-address.onion', - port: 8333, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: 3456, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: 3456, - sslPort: null, - }, - }, - ], - }, - }, - }, - 'system-pointers': [], - 'current-dependents': { - lnd: { - pointers: [], - 'health-checks': [], - }, - }, - 'current-dependencies': {}, - 'dependency-info': {}, - 'marketplace-url': 'https://registry.start9.com/', - 'developer-key': 'developer-key', }, - 'install-progress': undefined, + icon: '/assets/img/service-icons/bitcoind.svg', + 'last-backup': null, + status: { + configured: true, + main: { + status: PackageMainStatus.Running, + started: new Date().toISOString(), + health: {}, + }, + 'dependency-config-errors': {}, + }, + 'service-interfaces': { + ui: { + id: 'ui', + hasPrimary: false, + disabled: false, + masked: false, + name: 'Web UI', + description: + 'A launchable web app for you to interact with your Bitcoin node', + type: 'ui', + addressInfo: { + username: null, + hostId: 'abcdefg', + bindOptions: { + scheme: 'http', + preferredExternalPort: 80, + addSsl: { + addXForwardedHeaders: false, + preferredExternalPort: 443, + scheme: 'https', + }, + secure: false, + ssl: false, + }, + suffix: '', + }, + hostInfo: { + id: 'abcdefg', + kind: 'multi', + hostnames: [ + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'onion', + hostname: { + value: 'bitcoin-ui-address.onion', + port: 80, + sslPort: 443, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.1.5', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + port: null, + sslPort: 1234, + }, + }, + ], + }, + }, + rpc: { + id: 'rpc', + hasPrimary: false, + disabled: false, + masked: false, + name: 'RPC', + description: + 'Used by dependent services and client wallets for connecting to your node', + type: 'api', + addressInfo: { + username: null, + hostId: 'bcdefgh', + bindOptions: { + scheme: 'http', + preferredExternalPort: 80, + addSsl: { + addXForwardedHeaders: false, + preferredExternalPort: 443, + scheme: 'https', + }, + secure: false, + ssl: false, + }, + suffix: '', + }, + hostInfo: { + id: 'bcdefgh', + kind: 'multi', + hostnames: [ + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 2345, + }, + }, + { + kind: 'onion', + hostname: { + value: 'bitcoin-rpc-address.onion', + port: 80, + sslPort: 443, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.1.5', + port: null, + sslPort: 2345, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + port: null, + sslPort: 2345, + }, + }, + ], + }, + }, + p2p: { + id: 'p2p', + hasPrimary: true, + disabled: false, + masked: false, + name: 'P2P', + description: + 'Used for connecting to other nodes on the Bitcoin network', + type: 'p2p', + addressInfo: { + username: null, + hostId: 'cdefghi', + bindOptions: { + scheme: 'bitcoin', + preferredExternalPort: 8333, + addSsl: null, + secure: true, + ssl: false, + }, + suffix: '', + }, + hostInfo: { + id: 'cdefghi', + kind: 'multi', + hostnames: [ + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: 3456, + sslPort: null, + }, + }, + { + kind: 'onion', + hostname: { + value: 'bitcoin-p2p-address.onion', + port: 8333, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.1.5', + port: 3456, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + port: 3456, + sslPort: null, + }, + }, + ], + }, + }, + }, + 'current-dependents': { + lnd: { + pointers: [], + 'health-checks': [], + }, + }, + 'current-dependencies': {}, + 'dependency-info': {}, + 'marketplace-url': 'https://registry.start9.com/', + 'developer-key': 'developer-key', } - export const bitcoinProxy: PackageDataEntry = { - state: PackageState.Installed, - 'static-files': { - license: '/public/package-data/btc-rpc-proxy/0.20.0/LICENSE.md', - icon: '/assets/img/service-icons/btc-rpc-proxy.png', - instructions: '/public/package-data/btc-rpc-proxy/0.20.0/INSTRUCTIONS.md', - }, - manifest: MockManifestBitcoinProxy, - installed: { - 'last-backup': null, - status: { - configured: false, - main: { - status: PackageMainStatus.Stopped, - }, - 'dependency-config-errors': {}, - }, + export const bitcoinProxy: PackageDataEntry = { + 'state-info': { + state: PackageState.Installed, manifest: MockManifestBitcoinProxy, - 'service-interfaces': { - ui: { - id: 'ui', - hasPrimary: false, - disabled: false, - masked: false, - name: 'Web UI', - description: 'A launchable web app for Bitcoin Proxy', - type: 'ui', - addressInfo: { - username: null, - hostId: 'hijklmnop', - bindOptions: { - scheme: 'http', - preferredExternalPort: 80, - addSsl: { - addXForwardedHeaders: false, - preferredExternalPort: 443, - scheme: 'https', - }, - secure: { ssl: true }, - }, - suffix: '', - }, - hostInfo: { - id: 'hijklmnop', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 4567, - }, - }, - { - kind: 'onion', - hostname: { - value: 'proxy-ui-address.onion', - port: 80, - sslPort: 443, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: null, - sslPort: 4567, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: null, - sslPort: 4567, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'wlan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 4567, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'wlan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.7', - port: null, - sslPort: 4567, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'wlan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: null, - sslPort: 4567, - }, - }, - ], - }, - }, - }, - 'system-pointers': [], - 'current-dependents': { - lnd: { - pointers: [], - 'health-checks': [], - }, - }, - 'current-dependencies': { - bitcoind: { - pointers: [], - 'health-checks': [], - }, - }, - 'dependency-info': { - bitcoind: { - title: Mock.MockManifestBitcoind.title, - icon: 'assets/img/service-icons/bitcoind.svg', - }, - }, - 'marketplace-url': 'https://registry.start9.com/', - 'developer-key': 'developer-key', }, - 'install-progress': undefined, + icon: '/assets/img/service-icons/btc-rpc-proxy.png', + 'last-backup': null, + status: { + configured: false, + main: { + status: PackageMainStatus.Stopped, + }, + 'dependency-config-errors': {}, + }, + 'service-interfaces': { + ui: { + id: 'ui', + hasPrimary: false, + disabled: false, + masked: false, + name: 'Web UI', + description: 'A launchable web app for Bitcoin Proxy', + type: 'ui', + addressInfo: { + username: null, + hostId: 'hijklmnop', + bindOptions: { + scheme: 'http', + preferredExternalPort: 80, + addSsl: { + addXForwardedHeaders: false, + preferredExternalPort: 443, + scheme: 'https', + }, + secure: true, + ssl: true, + }, + suffix: '', + }, + hostInfo: { + id: 'hijklmnop', + kind: 'multi', + hostnames: [ + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 4567, + }, + }, + { + kind: 'onion', + hostname: { + value: 'proxy-ui-address.onion', + port: 80, + sslPort: 443, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.1.5', + port: null, + sslPort: 4567, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + port: null, + sslPort: 4567, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 4567, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.1.7', + port: null, + sslPort: 4567, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + port: null, + sslPort: 4567, + }, + }, + ], + }, + }, + }, + 'current-dependents': { + lnd: { + pointers: [], + 'health-checks': [], + }, + }, + 'current-dependencies': { + bitcoind: { + pointers: [], + 'health-checks': [], + }, + }, + 'dependency-info': { + bitcoind: { + title: Mock.MockManifestBitcoind.title, + icon: 'assets/img/service-icons/bitcoind.svg', + }, + }, + 'marketplace-url': 'https://registry.start9.com/', + 'developer-key': 'developer-key', } - export const lnd: PackageDataEntry = { - state: PackageState.Installed, - 'static-files': { - license: '/public/package-data/lnd/0.11.0/LICENSE.md', - icon: '/assets/img/service-icons/lnd.png', - instructions: '/public/package-data/lnd/0.11.0/INSTRUCTIONS.md', - }, - manifest: MockManifestLnd, - installed: { - 'last-backup': null, - status: { - configured: true, - main: { - status: PackageMainStatus.Stopped, - }, - 'dependency-config-errors': { - 'btc-rpc-proxy': 'Username not found', - }, - }, + export const lnd: PackageDataEntry = { + 'state-info': { + state: PackageState.Installed, manifest: MockManifestLnd, - 'service-interfaces': { - grpc: { - id: 'grpc', - hasPrimary: false, - disabled: false, - masked: false, - name: 'GRPC', - description: - 'Used by dependent services and client wallets for connecting to your node', - type: 'api', - addressInfo: { - username: null, - hostId: 'qrstuv', - bindOptions: { - scheme: 'grpc', - preferredExternalPort: 10009, - addSsl: null, - secure: { ssl: true }, - }, - suffix: '', - }, - hostInfo: { - id: 'qrstuv', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: 5678, - sslPort: null, - }, - }, - { - kind: 'onion', - hostname: { - value: 'lnd-grpc-address.onion', - port: 10009, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: 5678, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: 5678, - sslPort: null, - }, - }, - ], - }, - }, - lndconnect: { - id: 'lndconnect', - hasPrimary: false, - disabled: false, - masked: true, - name: 'LND Connect', - description: - 'Used by client wallets adhering to LND Connect protocol to connect to your node', - type: 'api', - addressInfo: { - username: null, - hostId: 'qrstuv', - bindOptions: { - scheme: 'lndconnect', - preferredExternalPort: 10009, - addSsl: null, - secure: { ssl: true }, - }, - suffix: 'cert=askjdfbjadnaskjnd&macaroon=ksjbdfnhjasbndjksand', - }, - hostInfo: { - id: 'qrstuv', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: 5678, - sslPort: null, - }, - }, - { - kind: 'onion', - hostname: { - value: 'lnd-grpc-address.onion', - port: 10009, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: 5678, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: 5678, - sslPort: null, - }, - }, - ], - }, - }, - p2p: { - id: 'p2p', - hasPrimary: true, - disabled: false, - masked: false, - name: 'P2P', - description: - 'Used for connecting to other nodes on the Bitcoin network', - type: 'p2p', - addressInfo: { - username: null, - hostId: 'rstuvw', - bindOptions: { - scheme: null, - preferredExternalPort: 9735, - addSsl: null, - secure: { ssl: true }, - }, - suffix: '', - }, - hostInfo: { - id: 'rstuvw', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: 6789, - sslPort: null, - }, - }, - { - kind: 'onion', - hostname: { - value: 'lnd-p2p-address.onion', - port: 9735, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: 6789, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: 6789, - sslPort: null, - }, - }, - ], - }, - }, - }, - 'system-pointers': [], - 'current-dependents': {}, - 'current-dependencies': { - bitcoind: { - pointers: [], - 'health-checks': [], - }, - 'btc-rpc-proxy': { - pointers: [], - 'health-checks': [], - }, - }, - 'dependency-info': { - bitcoind: { - title: Mock.MockManifestBitcoind.title, - icon: 'assets/img/service-icons/bitcoind.svg', - }, - 'btc-rpc-proxy': { - title: Mock.MockManifestBitcoinProxy.title, - icon: 'assets/img/service-icons/btc-rpc-proxy.png', - }, - }, - 'marketplace-url': 'https://registry.start9.com/', - 'developer-key': 'developer-key', }, - 'install-progress': undefined, + icon: '/assets/img/service-icons/lnd.png', + 'last-backup': null, + status: { + configured: true, + main: { + status: PackageMainStatus.Stopped, + }, + 'dependency-config-errors': { + 'btc-rpc-proxy': 'Username not found', + }, + }, + 'service-interfaces': { + grpc: { + id: 'grpc', + hasPrimary: false, + disabled: false, + masked: false, + name: 'GRPC', + description: + 'Used by dependent services and client wallets for connecting to your node', + type: 'api', + addressInfo: { + username: null, + hostId: 'qrstuv', + bindOptions: { + scheme: 'grpc', + preferredExternalPort: 10009, + addSsl: null, + secure: true, + ssl: true, + }, + suffix: '', + }, + hostInfo: { + id: 'qrstuv', + kind: 'multi', + hostnames: [ + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: 5678, + sslPort: null, + }, + }, + { + kind: 'onion', + hostname: { + value: 'lnd-grpc-address.onion', + port: 10009, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.1.5', + port: 5678, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + port: 5678, + sslPort: null, + }, + }, + ], + }, + }, + lndconnect: { + id: 'lndconnect', + hasPrimary: false, + disabled: false, + masked: true, + name: 'LND Connect', + description: + 'Used by client wallets adhering to LND Connect protocol to connect to your node', + type: 'api', + addressInfo: { + username: null, + hostId: 'qrstuv', + bindOptions: { + scheme: 'lndconnect', + preferredExternalPort: 10009, + addSsl: null, + secure: true, + ssl: true, + }, + suffix: 'cert=askjdfbjadnaskjnd&macaroon=ksjbdfnhjasbndjksand', + }, + hostInfo: { + id: 'qrstuv', + kind: 'multi', + hostnames: [ + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: 5678, + sslPort: null, + }, + }, + { + kind: 'onion', + hostname: { + value: 'lnd-grpc-address.onion', + port: 10009, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.1.5', + port: 5678, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + port: 5678, + sslPort: null, + }, + }, + ], + }, + }, + p2p: { + id: 'p2p', + hasPrimary: true, + disabled: false, + masked: false, + name: 'P2P', + description: + 'Used for connecting to other nodes on the Bitcoin network', + type: 'p2p', + addressInfo: { + username: null, + hostId: 'rstuvw', + bindOptions: { + scheme: null, + preferredExternalPort: 9735, + addSsl: null, + secure: true, + ssl: true, + }, + suffix: '', + }, + hostInfo: { + id: 'rstuvw', + kind: 'multi', + hostnames: [ + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: 6789, + sslPort: null, + }, + }, + { + kind: 'onion', + hostname: { + value: 'lnd-p2p-address.onion', + port: 9735, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.1.5', + port: 6789, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + port: 6789, + sslPort: null, + }, + }, + ], + }, + }, + }, + 'current-dependents': {}, + 'current-dependencies': { + bitcoind: { + pointers: [], + 'health-checks': [], + }, + 'btc-rpc-proxy': { + pointers: [], + 'health-checks': [], + }, + }, + 'dependency-info': { + bitcoind: { + title: Mock.MockManifestBitcoind.title, + icon: 'assets/img/service-icons/bitcoind.svg', + }, + 'btc-rpc-proxy': { + title: Mock.MockManifestBitcoinProxy.title, + icon: 'assets/img/service-icons/btc-rpc-proxy.png', + }, + }, + 'marketplace-url': 'https://registry.start9.com/', + 'developer-key': 'developer-key', } - export const LocalPkgs: { [key: string]: PackageDataEntry } = { - bitcoind: bitcoind, - 'btc-rpc-proxy': bitcoinProxy, - lnd: lnd, - } + export const LocalPkgs: { [key: string]: PackageDataEntry } = + { + bitcoind: bitcoind, + 'btc-rpc-proxy': bitcoinProxy, + lnd: lnd, + } } diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 73cc71ef8..87c243628 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -10,11 +10,14 @@ import { } from 'patch-db-client' import { DataModel, - InstallProgress, + FullProgress, + InstallingState, PackageDataEntry, PackageMainStatus, PackageState, ServerStatus, + StateInfo, + UpdatingState, } from 'src/app/services/patch-db/data-model' import { CifsBackupTarget, RR } from './api.types' import { parsePropertiesPermissive } from 'src/app/util/properties.util' @@ -39,14 +42,34 @@ import { AuthService } from '../auth.service' import { ConnectionService } from '../connection.service' import { StoreInfo } from '@start9labs/marketplace' -const PROGRESS: InstallProgress = { - size: 120, - downloaded: 0, - 'download-complete': false, - validated: 0, - 'validation-complete': false, - unpacked: 0, - 'unpack-complete': false, +const PROGRESS: FullProgress = { + overall: { + done: 0, + total: 120, + }, + phases: [ + { + name: 'Downloading', + progress: { + done: 0, + total: 40, + }, + }, + { + name: 'Validating', + progress: { + done: 0, + total: 40, + }, + }, + { + name: 'Installing', + progress: { + done: 0, + total: 40, + }, + }, + ], } @Injectable() @@ -656,15 +679,30 @@ export class MockApiService extends ApiService { this.updateProgress(params.id) }, 1000) - const patch: Operation[] = [ + const manifest = Mock.LocalPkgs[params.id]['state-info'].manifest + + const patch: Operation< + PackageDataEntry + >[] = [ { op: PatchOp.ADD, path: `/package-data/${params.id}`, value: { ...Mock.LocalPkgs[params.id], - // state: PackageState.Installing, - state: PackageState.Updating, - 'install-progress': { ...PROGRESS }, + 'state-info': { + // if installing + state: PackageState.Installing, + + // if updating + // state: PackageState.Updating, + // manifest, + + // both + 'installing-info': { + 'new-manifest': manifest, + progress: PROGRESS, + }, + }, }, }, ] @@ -720,9 +758,13 @@ export class MockApiService extends ApiService { path: `/package-data/${id}`, value: { ...Mock.LocalPkgs[id], - state: PackageState.Restoring, - 'install-progress': { ...PROGRESS }, - installed: undefined, + 'state-info': { + state: PackageState.Restoring, + 'installing-info': { + 'new-manifest': Mock.LocalPkgs[id]['state-info'].manifest!, + progress: PROGRESS, + }, + }, }, } }) @@ -948,7 +990,7 @@ export class MockApiService extends ApiService { const patch = [ { op: PatchOp.REPLACE, - path: `/package-data/${params.id}/state`, + path: `/package-data/${params.id}/state-info/state`, value: PackageState.Removing, }, ] @@ -977,55 +1019,100 @@ export class MockApiService extends ApiService { } private async updateProgress(id: string): Promise { - const progress = { ...PROGRESS } - const phases = [ - { progress: 'downloaded', completion: 'download-complete' }, - { progress: 'validated', completion: 'validation-complete' }, - { progress: 'unpacked', completion: 'unpack-complete' }, - ] as const + const progress = JSON.parse(JSON.stringify(PROGRESS)) - for (let phase of phases) { - let i = progress[phase.progress] - const size = progress?.size || 0 - while (i < size) { - await pauseFor(250) - i = Math.min(i + 5, size) - progress[phase.progress] = i + for (let [i, phase] of progress.phases.entries()) { + if (typeof phase.progress !== 'object' || !phase.progress.total) { + await pauseFor(2000) - if (i === progress.size) { - progress[phase.completion] = true - } - - const patch = [ + const patches: Operation[] = [ { op: PatchOp.REPLACE, - path: `/package-data/${id}/install-progress`, - value: { ...progress }, + path: `/package-data/${id}/state-info/installing-info/progress/phases/${i}/progress`, + value: true, }, ] - this.mockRevision(patch) + + // overall + if (typeof progress.overall === 'object' && progress.overall.total) { + const step = progress.overall.total / progress.phases.length + + progress.overall.done += step + + patches.push({ + op: PatchOp.REPLACE, + path: `/package-data/${id}/state-info/installing-info/progress/overall/done`, + value: progress.overall.done, + }) + } + + this.mockRevision(patches) + } else { + const step = phase.progress.total / 4 + + while (phase.progress.done < phase.progress.total) { + await pauseFor(500) + + phase.progress.done += step + + const patches: Operation[] = [ + { + op: PatchOp.REPLACE, + path: `/package-data/${id}/state-info/installing-info/progress/phases/${i}/progress/done`, + value: phase.progress.done, + }, + ] + + // overall + if (typeof progress.overall === 'object' && progress.overall.total) { + const step = progress.overall.total / progress.phases.length / 4 + + progress.overall.done += step + + patches.push({ + op: PatchOp.REPLACE, + path: `/package-data/${id}/state-info/installing-info/progress/overall/done`, + value: progress.overall.done, + }) + } + + this.mockRevision(patches) + + if (phase.progress.done === phase.progress.total) { + await pauseFor(250) + this.mockRevision([ + { + op: PatchOp.REPLACE, + path: `/package-data/${id}/state-info/installing-info/progress/phases/${i}/progress`, + value: true, + }, + ]) + } + } } } - setTimeout(() => { - const patch2: Operation[] = [ - { - op: PatchOp.REPLACE, - path: `/package-data/${id}/state`, - value: PackageState.Installed, + await pauseFor(1000) + this.mockRevision([ + { + op: PatchOp.REPLACE, + path: `/package-data/${id}/state-info/installing-info/progress/overall`, + value: true, + }, + ]) + + await pauseFor(1000) + const patch2: Operation[] = [ + { + op: PatchOp.REPLACE, + path: `/package-data/${id}/state-info`, + value: { + state: PackageState.Installed, + manifest: Mock.LocalPkgs[id]['state-info'].manifest, }, - { - op: PatchOp.ADD, - path: `/package-data/${id}/installed`, - value: { ...Mock.LocalPkgs[id].installed }, - }, - { - op: PatchOp.REMOVE, - path: `/package-data/${id}/install-progress`, - }, - ] - this.mockRevision(patch2) - }, 1000) + }, + ] + this.mockRevision(patch2) } private async updateOSProgress() { diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index d9fcba0ef..ec4b5aac7 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -77,910 +77,521 @@ export const mockPatchData: DataModel = { }, 'package-data': { bitcoind: { - state: PackageState.Installed, - 'static-files': { - license: '/public/package-data/bitcoind/0.20.0/LICENSE.md', - icon: '/assets/img/service-icons/bitcoind.svg', - instructions: '/public/package-data/bitcoind/0.20.0/INSTRUCTIONS.md', - }, - manifest: { - id: 'bitcoind', - title: 'Bitcoin Core', - version: '0.20.0', - 'git-hash': 'abcdefgh', - description: { - short: 'A Bitcoin full node by Bitcoin Core.', - long: 'Bitcoin is a decentralized consensus protocol and settlement network.', - }, - 'release-notes': 'Taproot, Schnorr, and more.', - assets: { - icon: 'icon.png', - license: 'LICENSE.md', - instructions: 'INSTRUCTIONS.md', - docker_images: 'image.tar', - assets: './assets', - scripts: './scripts', - }, - license: 'MIT', - 'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper', - 'upstream-repo': 'https://github.com/bitcoin/bitcoin', - 'support-site': 'https://bitcoin.org', - 'marketing-site': 'https://bitcoin.org', - 'donation-url': 'https://start9.com', - alerts: { - install: 'Bitcoin can take over a week to sync.', - uninstall: - 'Chain state will be lost, as will any funds stored on your Bitcoin Core waller that have not been backed up.', - restore: null, - start: 'Starting Bitcoin is good for your health.', - stop: null, - }, - 'health-checks': { - 'chain-state': { - name: 'Chain State', - }, - 'ephemeral-health-check': { - name: 'Ephemeral Health Check', - }, - 'p2p-interface': { - name: 'P2P Interface', - 'success-message': 'the health check ran succesfully', - }, - 'rpc-interface': { - name: 'RPC Interface', - }, - 'unnecessary-health-check': { - name: 'Unneccessary Health Check', - }, - } as any, - config: { - get: {}, - set: {}, - } as any, - volumes: {}, - 'min-os-version': '0.2.12', - backup: { - create: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, - }, - restore: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, - }, - }, - migrations: null, - actions: { - resync: { - name: 'Resync Blockchain', - description: - 'Use this to resync the Bitcoin blockchain from genesis', - warning: 'This will take a couple of days.', - 'allowed-statuses': [ - PackageMainStatus.Running, - PackageMainStatus.Stopped, - ], - implementation: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, - }, - 'input-spec': { - reason: { - type: 'string', - name: 'Re-sync Reason', - description: - 'Your reason for re-syncing. Why are you doing this?', - nullable: false, - masked: false, - copyable: false, - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'Must contain only letters.', - }, - name: { - type: 'string', - name: 'Your Name', - description: 'Tell the class your name.', - nullable: true, - masked: false, - copyable: false, - warning: 'You may loose all your money by providing your name.', - }, - notifications: { - name: 'Notification Preferences', - type: 'list', - subtype: 'enum', - description: 'how you want to be notified', - range: '[1,3]', - default: ['email'], - spec: { - 'value-names': { - email: 'Email', - text: 'Text', - call: 'Call', - push: 'Push', - webhook: 'Webhook', - }, - values: ['email', 'text', 'call', 'push', 'webhook'], - }, - }, - 'days-ago': { - type: 'number', - name: 'Days Ago', - description: 'Number of days to re-sync.', - nullable: false, - default: 100, - range: '[0, 9999]', - integral: true, - }, - 'top-speed': { - type: 'number', - name: 'Top Speed', - description: 'The fastest you can possibly run.', - nullable: false, - range: '[-1000, 1000]', - integral: false, - units: 'm/s', - }, - testnet: { - name: 'Testnet', - type: 'boolean', - description: - '
  • determines whether your node is running on testnet or mainnet
', - warning: 'Chain will have to resync!', - default: false, - }, - randomEnum: { - name: 'Random Enum', - type: 'enum', - 'value-names': { - null: 'Null', - good: 'Good', - bad: 'Bad', - ugly: 'Ugly', - }, - default: 'null', - description: 'This is not even real.', - warning: 'Be careful changing this!', - values: ['null', 'good', 'bad', 'ugly'], - }, - 'emergency-contact': { - name: 'Emergency Contact', - type: 'object', - description: 'The person to contact in case of emergency.', - spec: { - name: { - type: 'string', - name: 'Name', - nullable: false, - masked: false, - copyable: false, - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'Must contain only letters.', - }, - email: { - type: 'string', - name: 'Email', - nullable: false, - masked: false, - copyable: true, - }, - }, - }, - ips: { - name: 'Whitelist IPs', - type: 'list', - subtype: 'string', - description: - 'external ip addresses that are authorized to access your Bitcoin node', - warning: - 'Any IP you allow here will have RPC access to your Bitcoin node.', - range: '[1,10]', - default: ['192.168.1.1'], - spec: { - pattern: '^[0-9]{1,3}([,.][0-9]{1,3})?$', - 'pattern-description': 'Must be a valid IP address', - masked: false, - copyable: false, - }, - }, - bitcoinNode: { - type: 'union', - default: 'internal', - tag: { - id: 'type', - 'variant-names': { - internal: 'Internal', - external: 'External', - }, - name: 'Bitcoin Node Settings', - description: 'The node settings', - warning: 'Careful changing this', - }, - variants: { - internal: { - 'lan-address': { - name: 'LAN Address', - type: 'pointer', - subtype: 'package', - target: 'lan-address', - 'package-id': 'bitcoind', - description: 'the lan address', - interface: '', - }, - 'friendly-name': { - name: 'Friendly Name', - type: 'string', - description: 'the lan address', - nullable: true, - masked: false, - copyable: false, - }, - }, - external: { - 'public-domain': { - name: 'Public Domain', - type: 'string', - description: 'the public address of the node', - nullable: false, - default: 'bitcoinnode.com', - pattern: '.*', - 'pattern-description': 'anything', - masked: false, - copyable: true, - }, - }, - }, - }, - }, - }, - }, - dependencies: {}, - }, - installed: { + 'state-info': { + state: PackageState.Installed, manifest: { ...Mock.MockManifestBitcoind, version: '0.20.0', }, - 'last-backup': null, - status: { - configured: true, - main: { - status: PackageMainStatus.Running, - started: '2021-06-14T20:49:17.774Z', - health: { - 'ephemeral-health-check': { - result: HealthResult.Starting, - }, - 'chain-state': { - result: HealthResult.Loading, - message: 'Bitcoin is syncing from genesis', - }, - 'p2p-interface': { - result: HealthResult.Success, - }, - 'rpc-interface': { - result: HealthResult.Failure, - error: 'RPC interface unreachable.', - }, - 'unnecessary-health-check': { - result: HealthResult.Disabled, - }, - }, - }, - 'dependency-config-errors': {}, - }, - 'service-interfaces': { - ui: { - id: 'ui', - hasPrimary: false, - disabled: false, - masked: false, - name: 'Web UI', - description: - 'A launchable web app for you to interact with your Bitcoin node', - type: 'ui', - addressInfo: { - username: null, - hostId: 'abcdefg', - bindOptions: { - scheme: 'http', - preferredExternalPort: 80, - addSsl: { - addXForwardedHeaders: false, - preferredExternalPort: 443, - scheme: 'https', - }, - secure: { ssl: false }, - }, - suffix: '', - }, - hostInfo: { - id: 'abcdefg', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 1234, - }, - }, - { - kind: 'onion', - hostname: { - value: 'bitcoin-ui-address.onion', - port: 80, - sslPort: 443, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: null, - sslPort: 1234, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: null, - sslPort: 1234, - }, - }, - ], - }, - }, - rpc: { - id: 'rpc', - hasPrimary: false, - disabled: false, - masked: false, - name: 'RPC', - description: - 'Used by dependent services and client wallets for connecting to your node', - type: 'api', - addressInfo: { - username: null, - hostId: 'bcdefgh', - bindOptions: { - scheme: 'http', - preferredExternalPort: 80, - addSsl: { - preferredExternalPort: 443, - scheme: 'https', - addXForwardedHeaders: null, - }, - secure: null, - }, - suffix: '', - }, - hostInfo: { - id: 'bcdefgh', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 2345, - }, - }, - { - kind: 'onion', - hostname: { - value: 'bitcoin-rpc-address.onion', - port: 80, - sslPort: 443, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: null, - sslPort: 2345, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: null, - sslPort: 2345, - }, - }, - ], - }, - }, - p2p: { - id: 'p2p', - hasPrimary: true, - disabled: false, - masked: false, - name: 'P2P', - description: - 'Used for connecting to other nodes on the Bitcoin network', - type: 'p2p', - addressInfo: { - username: null, - hostId: 'cdefghi', - bindOptions: { - scheme: 'bitcoin', - preferredExternalPort: 8333, - addSsl: null, - secure: { ssl: false }, - }, - suffix: '', - }, - hostInfo: { - id: 'cdefghi', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: 3456, - sslPort: null, - }, - }, - { - kind: 'onion', - hostname: { - value: 'bitcoin-p2p-address.onion', - port: 8333, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: 3456, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: 3456, - sslPort: null, - }, - }, - ], - }, - }, - }, - 'system-pointers': [], - 'current-dependents': { - lnd: { - pointers: [], - 'health-checks': [], - }, - }, - 'current-dependencies': {}, - 'dependency-info': {}, - 'marketplace-url': 'https://registry.start9.com/', - 'developer-key': 'developer-key', }, + icon: '/assets/img/service-icons/bitcoind.svg', + 'last-backup': null, + status: { + configured: true, + main: { + status: PackageMainStatus.Running, + started: '2021-06-14T20:49:17.774Z', + health: { + 'ephemeral-health-check': { + result: HealthResult.Starting, + }, + 'chain-state': { + result: HealthResult.Loading, + message: 'Bitcoin is syncing from genesis', + }, + 'p2p-interface': { + result: HealthResult.Success, + }, + 'rpc-interface': { + result: HealthResult.Failure, + error: 'RPC interface unreachable.', + }, + 'unnecessary-health-check': { + result: HealthResult.Disabled, + }, + }, + }, + 'dependency-config-errors': {}, + }, + 'service-interfaces': { + ui: { + id: 'ui', + hasPrimary: false, + disabled: false, + masked: false, + name: 'Web UI', + description: + 'A launchable web app for you to interact with your Bitcoin node', + type: 'ui', + addressInfo: { + username: null, + hostId: 'abcdefg', + bindOptions: { + scheme: 'http', + preferredExternalPort: 80, + addSsl: { + addXForwardedHeaders: false, + preferredExternalPort: 443, + scheme: 'https', + }, + secure: false, + ssl: false, + }, + suffix: '', + }, + hostInfo: { + id: 'abcdefg', + kind: 'multi', + hostnames: [ + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'onion', + hostname: { + value: 'bitcoin-ui-address.onion', + port: 80, + sslPort: 443, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.1.5', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + port: null, + sslPort: 1234, + }, + }, + ], + }, + }, + rpc: { + id: 'rpc', + hasPrimary: false, + disabled: false, + masked: false, + name: 'RPC', + description: + 'Used by dependent services and client wallets for connecting to your node', + type: 'api', + addressInfo: { + username: null, + hostId: 'bcdefgh', + bindOptions: { + scheme: 'http', + preferredExternalPort: 80, + addSsl: { + addXForwardedHeaders: false, + preferredExternalPort: 443, + scheme: 'https', + }, + secure: false, + ssl: false, + }, + suffix: '', + }, + hostInfo: { + id: 'bcdefgh', + kind: 'multi', + hostnames: [ + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 2345, + }, + }, + { + kind: 'onion', + hostname: { + value: 'bitcoin-rpc-address.onion', + port: 80, + sslPort: 443, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.1.5', + port: null, + sslPort: 2345, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + port: null, + sslPort: 2345, + }, + }, + ], + }, + }, + p2p: { + id: 'p2p', + hasPrimary: true, + disabled: false, + masked: false, + name: 'P2P', + description: + 'Used for connecting to other nodes on the Bitcoin network', + type: 'p2p', + addressInfo: { + username: null, + hostId: 'cdefghi', + bindOptions: { + scheme: 'bitcoin', + preferredExternalPort: 8333, + addSsl: null, + secure: true, + ssl: false, + }, + suffix: '', + }, + hostInfo: { + id: 'cdefghi', + kind: 'multi', + hostnames: [ + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: 3456, + sslPort: null, + }, + }, + { + kind: 'onion', + hostname: { + value: 'bitcoin-p2p-address.onion', + port: 8333, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.1.5', + port: 3456, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + port: 3456, + sslPort: null, + }, + }, + ], + }, + }, + }, + 'current-dependents': { + lnd: { + pointers: [], + 'health-checks': [], + }, + }, + 'current-dependencies': {}, + 'dependency-info': {}, + 'marketplace-url': 'https://registry.start9.com/', + 'developer-key': 'developer-key', }, lnd: { - state: PackageState.Installed, - 'static-files': { - license: '/public/package-data/lnd/0.11.1/LICENSE.md', - icon: '/assets/img/service-icons/lnd.png', - instructions: '/public/package-data/lnd/0.11.1/INSTRUCTIONS.md', - }, - manifest: { - id: 'lnd', - title: 'Lightning Network Daemon', - version: '0.11.0', - description: { - short: 'A bolt spec compliant client.', - long: 'More info about LND. More info about LND. More info about LND.', - }, - 'release-notes': 'Dual funded channels!', - assets: { - icon: 'icon.png', - license: 'LICENSE.md', - instructions: 'INSTRUCTIONS.md', - docker_images: 'image.tar', - assets: './assets', - scripts: './scripts', - }, - license: 'MIT', - 'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper', - 'upstream-repo': 'https://github.com/lightningnetwork/lnd', - 'support-site': 'https://lightning.engineering/', - 'marketing-site': 'https://lightning.engineering/', - 'donation-url': null, - alerts: { - install: null, - uninstall: null, - restore: - 'If this is a duplicate instance of the same LND node, you may loose your funds.', - start: 'Starting LND is good for your health.', - stop: null, - }, - 'health-checks': {}, - config: { - get: null, - set: null, - }, - volumes: {}, - 'min-os-version': '0.2.12', - backup: { - create: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, - }, - restore: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, - }, - }, - migrations: null, - actions: { - resync: { - name: 'Resync Network Graph', - description: 'Your node will resync its network graph.', - warning: 'This will take a couple hours.', - 'allowed-statuses': [PackageMainStatus.Running], - implementation: { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [], - mounts: {}, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - 'sigterm-timeout': null, - }, - 'input-spec': null, - }, - }, - dependencies: { - bitcoind: { - version: '=0.21.0', - description: 'LND needs bitcoin to live.', - requirement: { - type: 'opt-out', - how: 'You can use an external node from your server if you prefer.', - }, - config: null, - }, - 'btc-rpc-proxy': { - version: '>=0.2.2', - description: - 'As long as Bitcoin is pruned, LND needs Bitcoin Proxy to fetch block over the P2P network.', - requirement: { - type: 'opt-in', - how: `To use Proxy's user management system, go to LND config and select Bitcoin Proxy under Bitcoin config.`, - }, - config: null, - }, - }, - }, - installed: { + 'state-info': { + state: PackageState.Installed, manifest: { ...Mock.MockManifestLnd, version: '0.11.0', }, - 'last-backup': null, - status: { - configured: true, - main: { - status: PackageMainStatus.Stopped, - }, - 'dependency-config-errors': { - 'btc-rpc-proxy': 'This is a config unsatisfied error', - }, - }, - 'service-interfaces': { - grpc: { - id: 'grpc', - hasPrimary: false, - disabled: false, - masked: false, - name: 'GRPC', - description: - 'Used by dependent services and client wallets for connecting to your node', - type: 'api', - addressInfo: { - username: null, - hostId: 'qrstuv', - bindOptions: { - scheme: 'grpc', - preferredExternalPort: 10009, - addSsl: null, - secure: { ssl: true }, - }, - suffix: '', - }, - hostInfo: { - id: 'qrstuv', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: 5678, - sslPort: null, - }, - }, - { - kind: 'onion', - hostname: { - value: 'lnd-grpc-address.onion', - port: 10009, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: 5678, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: 5678, - sslPort: null, - }, - }, - ], - }, - }, - lndconnect: { - id: 'lndconnect', - hasPrimary: false, - disabled: false, - masked: true, - name: 'LND Connect', - description: - 'Used by client wallets adhering to LND Connect protocol to connect to your node', - type: 'api', - addressInfo: { - username: null, - hostId: 'qrstuv', - bindOptions: { - scheme: 'lndconnect', - preferredExternalPort: 10009, - addSsl: null, - secure: { ssl: true }, - }, - suffix: 'cert=askjdfbjadnaskjnd&macaroon=ksjbdfnhjasbndjksand', - }, - hostInfo: { - id: 'qrstuv', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: 5678, - sslPort: null, - }, - }, - { - kind: 'onion', - hostname: { - value: 'lnd-grpc-address.onion', - port: 10009, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: 5678, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: 5678, - sslPort: null, - }, - }, - ], - }, - }, - p2p: { - id: 'p2p', - hasPrimary: true, - disabled: false, - masked: false, - name: 'P2P', - description: - 'Used for connecting to other nodes on the Bitcoin network', - type: 'p2p', - addressInfo: { - username: null, - hostId: 'rstuvw', - bindOptions: { - scheme: null, - preferredExternalPort: 9735, - addSsl: null, - secure: { ssl: true }, - }, - suffix: '', - }, - hostInfo: { - id: 'rstuvw', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: 6789, - sslPort: null, - }, - }, - { - kind: 'onion', - hostname: { - value: 'lnd-p2p-address.onion', - port: 9735, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: 6789, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: 6789, - sslPort: null, - }, - }, - ], - }, - }, - }, - 'system-pointers': [], - 'current-dependents': {}, - 'current-dependencies': { - bitcoind: { - pointers: [], - 'health-checks': [], - }, - 'btc-rpc-proxy': { - pointers: [], - 'health-checks': [], - }, - }, - 'dependency-info': { - bitcoind: { - title: 'Bitcoin Core', - icon: 'assets/img/service-icons/bitcoind.svg', - }, - 'btc-rpc-proxy': { - title: 'Bitcoin Proxy', - icon: 'assets/img/service-icons/btc-rpc-proxy.png', - }, - }, - 'marketplace-url': 'https://registry.start9.com/', - 'developer-key': 'developer-key', }, + icon: '/assets/img/service-icons/lnd.png', + 'last-backup': null, + status: { + configured: true, + main: { + status: PackageMainStatus.Stopped, + }, + 'dependency-config-errors': { + 'btc-rpc-proxy': 'This is a config unsatisfied error', + }, + }, + 'service-interfaces': { + grpc: { + id: 'grpc', + hasPrimary: false, + disabled: false, + masked: false, + name: 'GRPC', + description: + 'Used by dependent services and client wallets for connecting to your node', + type: 'api', + addressInfo: { + username: null, + hostId: 'qrstuv', + bindOptions: { + scheme: 'grpc', + preferredExternalPort: 10009, + addSsl: null, + secure: true, + ssl: true, + }, + suffix: '', + }, + hostInfo: { + id: 'qrstuv', + kind: 'multi', + hostnames: [ + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: 5678, + sslPort: null, + }, + }, + { + kind: 'onion', + hostname: { + value: 'lnd-grpc-address.onion', + port: 10009, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.1.5', + port: 5678, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + port: 5678, + sslPort: null, + }, + }, + ], + }, + }, + lndconnect: { + id: 'lndconnect', + hasPrimary: false, + disabled: false, + masked: true, + name: 'LND Connect', + description: + 'Used by client wallets adhering to LND Connect protocol to connect to your node', + type: 'api', + addressInfo: { + username: null, + hostId: 'qrstuv', + bindOptions: { + scheme: 'lndconnect', + preferredExternalPort: 10009, + addSsl: null, + secure: true, + ssl: true, + }, + suffix: 'cert=askjdfbjadnaskjnd&macaroon=ksjbdfnhjasbndjksand', + }, + hostInfo: { + id: 'qrstuv', + kind: 'multi', + hostnames: [ + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: 5678, + sslPort: null, + }, + }, + { + kind: 'onion', + hostname: { + value: 'lnd-grpc-address.onion', + port: 10009, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.1.5', + port: 5678, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + port: 5678, + sslPort: null, + }, + }, + ], + }, + }, + p2p: { + id: 'p2p', + hasPrimary: true, + disabled: false, + masked: false, + name: 'P2P', + description: + 'Used for connecting to other nodes on the Bitcoin network', + type: 'p2p', + addressInfo: { + username: null, + hostId: 'rstuvw', + bindOptions: { + scheme: null, + preferredExternalPort: 9735, + addSsl: null, + secure: true, + ssl: true, + }, + suffix: '', + }, + hostInfo: { + id: 'rstuvw', + kind: 'multi', + hostnames: [ + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: 6789, + sslPort: null, + }, + }, + { + kind: 'onion', + hostname: { + value: 'lnd-p2p-address.onion', + port: 9735, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.1.5', + port: 6789, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + port: 6789, + sslPort: null, + }, + }, + ], + }, + }, + }, + 'current-dependents': {}, + 'current-dependencies': { + bitcoind: { + pointers: [], + 'health-checks': [], + }, + 'btc-rpc-proxy': { + pointers: [], + 'health-checks': [], + }, + }, + 'dependency-info': { + bitcoind: { + title: 'Bitcoin Core', + icon: 'assets/img/service-icons/bitcoind.svg', + }, + 'btc-rpc-proxy': { + title: 'Bitcoin Proxy', + icon: 'assets/img/service-icons/btc-rpc-proxy.png', + }, + }, + 'marketplace-url': 'https://registry.start9.com/', + 'developer-key': 'developer-key', }, }, } diff --git a/web/projects/ui/src/app/services/config.service.ts b/web/projects/ui/src/app/services/config.service.ts index d6ad17019..71719ae4a 100644 --- a/web/projects/ui/src/app/services/config.service.ts +++ b/web/projects/ui/src/app/services/config.service.ts @@ -3,7 +3,7 @@ import { Inject, Injectable } from '@angular/core' import { WorkspaceConfig } from '@start9labs/shared' import { types } from '@start9labs/start-sdk' import { - InstalledPackageDataEntry, + PackageDataEntry, PackageMainStatus, PackageState, } from 'src/app/services/patch-db/data-model' @@ -64,7 +64,7 @@ export class ConfigService { /** ${scheme}://${username}@${host}:${externalPort}${suffix} */ launchableAddress( - interfaces: InstalledPackageDataEntry['service-interfaces'], + interfaces: PackageDataEntry['service-interfaces'], ): string { const ui = Object.values(interfaces).find(i => i.type === 'ui') @@ -128,7 +128,7 @@ export class ConfigService { } export function hasUi( - interfaces: InstalledPackageDataEntry['service-interfaces'], + interfaces: PackageDataEntry['service-interfaces'], ): boolean { return Object.values(interfaces).some(iface => iface.type === 'ui') } diff --git a/web/projects/ui/src/app/services/dep-error.service.ts b/web/projects/ui/src/app/services/dep-error.service.ts index 4762f2df1..8d0d93ead 100644 --- a/web/projects/ui/src/app/services/dep-error.service.ts +++ b/web/projects/ui/src/app/services/dep-error.service.ts @@ -4,12 +4,14 @@ import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators' import { PatchDB } from 'patch-db-client' import { DataModel, - HealthCheckResult, HealthResult, - InstalledPackageDataEntry, + InstalledState, + PackageDataEntry, PackageMainStatus, + PackageState, } from './patch-db/data-model' import * as deepEqual from 'fast-deep-equal' +import { isInstalled } from '../util/get-package-data' export type AllDependencyErrors = Record export type PkgDependencyErrors = Record @@ -55,14 +57,14 @@ export class DepErrorService { pkgId: string, outerErrors: AllDependencyErrors, ): PkgDependencyErrors { - const pkgInstalled = pkgs[pkgId].installed + const pkg = pkgs[pkgId] - if (!pkgInstalled) return {} + if (!isInstalled(pkg)) return {} return currentDeps(pkgs, pkgId).reduce( (innerErrors, depId): PkgDependencyErrors => ({ ...innerErrors, - [depId]: this.getDepError(pkgs, pkgInstalled, depId, outerErrors), + [depId]: this.getDepError(pkgs, pkg, depId, outerErrors), }), {} as PkgDependencyErrors, ) @@ -70,21 +72,21 @@ export class DepErrorService { private getDepError( pkgs: DataModel['package-data'], - pkgInstalled: InstalledPackageDataEntry, + pkg: PackageDataEntry, depId: string, outerErrors: AllDependencyErrors, ): DependencyError | null { - const depInstalled = pkgs[depId]?.installed + const dep = pkgs[depId] // not installed - if (!depInstalled) { + if (!dep || dep['state-info'].state !== PackageState.Installed) { return { type: DependencyErrorType.NotInstalled, } } - const pkgManifest = pkgInstalled.manifest - const depManifest = depInstalled.manifest + const pkgManifest = pkg['state-info'].manifest + const depManifest = dep['state-info'].manifest // incorrect version if ( @@ -102,16 +104,14 @@ export class DepErrorService { // invalid config if ( - Object.values(pkgInstalled.status['dependency-config-errors']).some( - err => !!err, - ) + Object.values(pkg.status['dependency-config-errors']).some(err => !!err) ) { return { type: DependencyErrorType.ConfigUnsatisfied, } } - const depStatus = depInstalled.status.main.status + const depStatus = dep.status.main.status // not running if ( @@ -125,12 +125,8 @@ export class DepErrorService { // health check failure if (depStatus === PackageMainStatus.Running) { - for (let id of pkgInstalled['current-dependencies'][depId][ - 'health-checks' - ]) { - if ( - depInstalled.status.main.health[id]?.result !== HealthResult.Success - ) { + for (let id of pkg['current-dependencies'][depId]['health-checks']) { + if (dep.status.main.health[id]?.result !== HealthResult.Success) { return { type: DependencyErrorType.HealthChecksFailed, } @@ -154,9 +150,9 @@ export class DepErrorService { } function currentDeps(pkgs: DataModel['package-data'], id: string): string[] { - return Object.keys( - pkgs[id]?.installed?.['current-dependencies'] || {}, - ).filter(depId => depId !== id) + return Object.keys(pkgs[id]?.['current-dependencies'] || {}).filter( + depId => depId !== id, + ) } function dependencyDepth( diff --git a/web/projects/ui/src/app/services/patch-db/data-model.ts b/web/projects/ui/src/app/services/patch-db/data-model.ts index 931b3b949..363fc8b89 100644 --- a/web/projects/ui/src/app/services/patch-db/data-model.ts +++ b/web/projects/ui/src/app/services/patch-db/data-model.ts @@ -107,31 +107,11 @@ export enum ServerStatus { BackingUp = 'backing-up', } -export interface PackageDataEntry { - state: PackageState - 'static-files': { - license: Url - instructions: Url - icon: Url - } - manifest: Manifest - installed?: InstalledPackageDataEntry // exists when: installed, updating - 'install-progress'?: InstallProgress // exists when: installing, updating -} - -export enum PackageState { - Installing = 'installing', - Installed = 'installed', - Updating = 'updating', - Removing = 'removing', - Restoring = 'restoring', -} - -export interface InstalledPackageDataEntry { +export type PackageDataEntry = { + 'state-info': T + icon: Url status: Status - manifest: Manifest 'last-backup': string | null - 'system-pointers': any[] 'current-dependents': { [id: string]: CurrentDependencyInfo } 'current-dependencies': { [id: string]: CurrentDependencyInfo } 'dependency-info': { @@ -145,6 +125,32 @@ export interface InstalledPackageDataEntry { 'developer-key': string } +export type StateInfo = InstalledState | InstallingState | UpdatingState + +export type InstalledState = { + state: PackageState.Installed | PackageState.Removing + manifest: Manifest +} + +export type InstallingState = { + state: PackageState.Installing | PackageState.Restoring + 'installing-info': InstallingInfo +} + +export type UpdatingState = { + state: PackageState.Updating + 'installing-info': InstallingInfo + manifest: Manifest +} + +export enum PackageState { + Installing = 'installing', + Installed = 'installed', + Updating = 'updating', + Removing = 'removing', + Restoring = 'restoring', +} + export interface CurrentDependencyInfo { pointers: any[] 'health-checks': string[] // array of health check IDs @@ -354,12 +360,13 @@ export interface HealthCheckResultFailure { error: string } -export interface InstallProgress { - readonly size: number | null - readonly downloaded: number - readonly 'download-complete': boolean - readonly validated: number - readonly 'validation-complete': boolean - readonly unpacked: number - readonly 'unpack-complete': boolean +export type InstallingInfo = { + progress: FullProgress + 'new-manifest': Manifest } + +export type FullProgress = { + overall: Progress + phases: { name: string; progress: Progress }[] +} +export type Progress = boolean | { done: number; total: number | null } // false means indeterminate. true means complete diff --git a/web/projects/ui/src/app/services/pkg-status-rendering.service.ts b/web/projects/ui/src/app/services/pkg-status-rendering.service.ts index 28c58a809..a041b5e7c 100644 --- a/web/projects/ui/src/app/services/pkg-status-rendering.service.ts +++ b/web/projects/ui/src/app/services/pkg-status-rendering.service.ts @@ -22,15 +22,15 @@ export function renderPkgStatus( let dependency: DependencyStatus | null = null let health: HealthStatus | null = null - if (pkg.state === PackageState.Installed && pkg.installed) { - primary = getPrimaryStatus(pkg.installed.status) + if (pkg['state-info'].state === PackageState.Installed) { + primary = getPrimaryStatus(pkg.status) dependency = getDependencyStatus(depErrors) health = getHealthStatus( - pkg.installed.status, - !isEmptyObject(pkg.manifest['health-checks']), + pkg.status, + !isEmptyObject(pkg['state-info'].manifest['health-checks']), ) } else { - primary = pkg.state as string as PrimaryStatus + primary = pkg['state-info'].state as string as PrimaryStatus } return { primary, dependency, health } diff --git a/web/projects/ui/src/app/services/ui-launcher.service.ts b/web/projects/ui/src/app/services/ui-launcher.service.ts index 70666e264..82250048a 100644 --- a/web/projects/ui/src/app/services/ui-launcher.service.ts +++ b/web/projects/ui/src/app/services/ui-launcher.service.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@angular/core' import { WINDOW } from '@ng-web-apis/common' -import { InstalledPackageDataEntry } from 'src/app/services/patch-db/data-model' +import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { ConfigService } from './config.service' @Injectable({ @@ -12,7 +12,7 @@ export class UiLauncherService { private readonly config: ConfigService, ) {} - launch(interfaces: InstalledPackageDataEntry['service-interfaces']): void { + launch(interfaces: PackageDataEntry['service-interfaces']): void { this.windowRef.open( this.config.launchableAddress(interfaces), '_blank', diff --git a/web/projects/ui/src/app/types/progress-data.ts b/web/projects/ui/src/app/types/progress-data.ts deleted file mode 100644 index a05e475a1..000000000 --- a/web/projects/ui/src/app/types/progress-data.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface ProgressData { - totalProgress: number - downloadProgress: number - validateProgress: number - unpackProgress: number - isComplete: boolean -} diff --git a/web/projects/ui/src/app/util/dry-update.ts b/web/projects/ui/src/app/util/dry-update.ts index 9b7ba44a3..2a37c9160 100644 --- a/web/projects/ui/src/app/util/dry-update.ts +++ b/web/projects/ui/src/app/util/dry-update.ts @@ -1,5 +1,6 @@ import { Emver } from '@start9labs/shared' import { DataModel } from '../services/patch-db/data-model' +import { getManifest } from './get-package-data' export function dryUpdate( { id, version }: { id: string; version: string }, @@ -9,9 +10,10 @@ export function dryUpdate( return Object.values(pkgs) .filter( pkg => - Object.keys(pkg.installed?.['current-dependencies'] || {}).some( + Object.keys(pkg['current-dependencies'] || {}).some( pkgId => pkgId === id, - ) && !emver.satisfies(version, pkg.manifest.dependencies[id].version), + ) && + !emver.satisfies(version, getManifest(pkg).dependencies[id].version), ) - .map(pkg => pkg.manifest.title) + .map(pkg => getManifest(pkg).title) } diff --git a/web/projects/ui/src/app/util/get-package-data.ts b/web/projects/ui/src/app/util/get-package-data.ts index 0645f3b3c..472a549d5 100644 --- a/web/projects/ui/src/app/util/get-package-data.ts +++ b/web/projects/ui/src/app/util/get-package-data.ts @@ -1,7 +1,12 @@ import { PatchDB } from 'patch-db-client' import { DataModel, + InstalledState, + InstallingState, + Manifest, PackageDataEntry, + PackageState, + UpdatingState, } from 'src/app/services/patch-db/data-model' import { firstValueFrom } from 'rxjs' @@ -17,3 +22,41 @@ export async function getAllPackages( ): Promise { return firstValueFrom(patch.watch$('package-data')) } + +export function getManifest(pkg: PackageDataEntry): Manifest { + if (isInstalled(pkg) || isRemoving(pkg)) return pkg['state-info'].manifest + + return (pkg['state-info'] as InstallingState)['installing-info'][ + 'new-manifest' + ] +} + +export function isInstalled( + pkg: PackageDataEntry, +): pkg is PackageDataEntry { + return pkg['state-info'].state === PackageState.Installed +} + +export function isRemoving( + pkg: PackageDataEntry, +): pkg is PackageDataEntry { + return pkg['state-info'].state === PackageState.Removing +} + +export function isInstalling( + pkg: PackageDataEntry, +): pkg is PackageDataEntry { + return pkg['state-info'].state === PackageState.Installing +} + +export function isRestoring( + pkg: PackageDataEntry, +): pkg is PackageDataEntry { + return pkg['state-info'].state === PackageState.Restoring +} + +export function isUpdating( + pkg: PackageDataEntry, +): pkg is PackageDataEntry { + return pkg['state-info'].state === PackageState.Updating +} diff --git a/web/projects/ui/src/app/util/get-package-info.ts b/web/projects/ui/src/app/util/get-package-info.ts index 14901f201..936ed9954 100644 --- a/web/projects/ui/src/app/util/get-package-info.ts +++ b/web/projects/ui/src/app/util/get-package-info.ts @@ -7,9 +7,7 @@ import { renderPkgStatus, StatusRendering, } from '../services/pkg-status-rendering.service' -import { ProgressData } from 'src/app/types/progress-data' import { Subscription } from 'rxjs' -import { packageLoadingProgress } from './package-loading-progress' import { PkgDependencyErrors } from '../services/dep-error.service' export function getPackageInfo( @@ -23,7 +21,6 @@ export function getPackageInfo( entry, primaryRendering, primaryStatus: statuses.primary, - installProgress: packageLoadingProgress(entry['install-progress']), error: statuses.health === HealthStatus.Failure || statuses.dependency === DependencyStatus.Warning, @@ -40,7 +37,6 @@ export interface PkgInfo { entry: PackageDataEntry primaryRendering: StatusRendering primaryStatus: PrimaryStatus - installProgress: ProgressData | null error: boolean warning: boolean transitioning: boolean diff --git a/web/projects/ui/src/app/util/has-deps.ts b/web/projects/ui/src/app/util/has-deps.ts index 94b3fa4cd..3699ef916 100644 --- a/web/projects/ui/src/app/util/has-deps.ts +++ b/web/projects/ui/src/app/util/has-deps.ts @@ -1,7 +1,8 @@ import { PackageDataEntry } from '../services/patch-db/data-model' +import { getManifest } from './get-package-data' export function hasCurrentDeps(pkg: PackageDataEntry): boolean { - return !!Object.keys(pkg.installed?.['current-dependents'] || {}).filter( - depId => depId !== pkg.manifest.id, + return !!Object.keys(pkg['current-dependents']).filter( + depId => depId !== getManifest(pkg).id, ).length } diff --git a/web/projects/ui/src/app/util/package-loading-progress.ts b/web/projects/ui/src/app/util/package-loading-progress.ts deleted file mode 100644 index 3c699dfd3..000000000 --- a/web/projects/ui/src/app/util/package-loading-progress.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { isEmptyObject } from '@start9labs/shared' -import { ProgressData } from 'src/app/types/progress-data' -import { InstallProgress } from '../services/patch-db/data-model' - -export function packageLoadingProgress( - loadData?: InstallProgress, -): ProgressData | null { - if (!loadData || isEmptyObject(loadData)) { - return null - } - - let { - downloaded, - validated, - unpacked, - size, - 'download-complete': downloadComplete, - 'validation-complete': validationComplete, - 'unpack-complete': unpackComplete, - } = loadData - - // only permit 100% when "complete" == true - size = size || 0 - downloaded = downloadComplete ? size : Math.max(downloaded - 1, 0) - validated = validationComplete ? size : Math.max(validated - 1, 0) - unpacked = unpackComplete ? size : Math.max(unpacked - 1, 0) - - const downloadWeight = 1 - const validateWeight = 0.2 - const unpackWeight = 0.7 - - const numerator = Math.floor( - downloadWeight * downloaded + - validateWeight * validated + - unpackWeight * unpacked, - ) - - const denominator = Math.floor( - size * (downloadWeight + validateWeight + unpackWeight), - ) - const totalProgress = Math.floor((100 * numerator) / denominator) - - return { - totalProgress, - downloadProgress: Math.floor((100 * downloaded) / size), - validateProgress: Math.floor((100 * validated) / size), - unpackProgress: Math.floor((100 * unpacked) / size), - isComplete: downloadComplete && validationComplete && unpackComplete, - } -}