diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index 83d1dd8a7..714845a21 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -100,7 +100,7 @@ export class AppComponent { } }) - this.http.watch401$().subscribe(() => { + this.http.watchUnauth$().subscribe(() => { this.authService.setUnverified() }) } diff --git a/ui/src/app/models/patch-db/data-model.ts b/ui/src/app/models/patch-db/data-model.ts index 5d3757ef4..3a8b10b65 100644 --- a/ui/src/app/models/patch-db/data-model.ts +++ b/ui/src/app/models/patch-db/data-model.ts @@ -104,7 +104,7 @@ export interface Manifest { stop: string | null } main: ActionImpl - 'health-check': ActionImpl + 'health-checks': { [id: string]: ActionImpl & { critical: boolean } } config: ConfigActions | null volumes: { [id: string]: Volume } 'min-os-version': string @@ -257,24 +257,31 @@ export enum PackageMainStatus { export type HealthCheckResult = HealthCheckResultWarmingUp | HealthCheckResultDisabled | HealthCheckResultSuccess | HealthCheckResultFailure +export enum HealthResult { + WarmingUp = 'warming-up', + Disabled = 'disabled', + Success = 'success', + Failure = 'failure', +} + export interface HealthCheckResultWarmingUp { time: string // UTC date string - result: 'warming-up' + result: HealthResult.WarmingUp } export interface HealthCheckResultDisabled { time: string // UTC date string - result: 'disabled' + result: HealthResult.Disabled } export interface HealthCheckResultSuccess { time: string // UTC date string - result: 'success' + result: HealthResult.Success } export interface HealthCheckResultFailure { time: string // UTC date string - result: 'failure' + result: HealthResult.Failure error: string } diff --git a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html index 9b43dfc15..f39ebb761 100644 --- a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html +++ b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html @@ -9,24 +9,19 @@ - - - + + - No Actions for {{ manifest.title }} {{ manifest.versionInstalled }}. + {{ action.value.name }} + {{ action.value.description }} - - - - - - {{ action.value.name }} - {{ action.value.description }} - - - - - + + + Uninstall + This will uninstall the service from your Embassy and delete all data permanently. + + + \ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts index e74f9223e..ce1e4a2fc 100644 --- a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts +++ b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts @@ -1,11 +1,13 @@ import { Component } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { ApiService } from 'src/app/services/api/api.service' -import { AlertController } from '@ionic/angular' +import { AlertController, ModalController, NavController } from '@ionic/angular' import { LoaderService } from 'src/app/services/loader.service' import { HttpErrorResponse } from '@angular/common/http' import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model' -import { Action, InstalledPackageDataEntry, PackageMainStatus } from 'src/app/models/patch-db/data-model' +import { Action, InstalledPackageDataEntry, Manifest, PackageMainStatus } from 'src/app/models/patch-db/data-model' +import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' +import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' @Component({ selector: 'app-actions', @@ -18,8 +20,11 @@ export class AppActionsPage { constructor ( private readonly route: ActivatedRoute, private readonly apiService: ApiService, + private readonly modalCtrl: ModalController, private readonly alertCtrl: AlertController, private readonly loaderService: LoaderService, + private readonly wizardBaker: WizardBaker, + private readonly navCtrl: NavController, public readonly patch: PatchDbModel, ) { } @@ -71,6 +76,22 @@ export class AppActionsPage { } } + async uninstall (manifest: Manifest) { + const { id, title, version, alerts } = manifest + const data = await wizardModal( + this.modalCtrl, + this.wizardBaker.uninstall({ + id, + title, + version, + uninstallAlert: alerts.uninstall, + }), + ) + + if (data.cancelled) return + return this.navCtrl.navigateRoot('/services/installed') + } + private async executeAction (pkgId: string, actionId: string) { try { const res = await this.loaderService.displayDuringP( diff --git a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.html b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.html index ede0211e6..a55dfb715 100644 --- a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.html +++ b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.html @@ -10,172 +10,129 @@ - + {{ error }} - - - - - - - - - - 20"> - {{ manifest.title }} - - - {{ manifest.version | displayEmver }} - - - - - - - - - - Configure - - - Stop - - - Fix - - - Start - - - - - - Launch Web Interface - - - - - - - - - - - Interfaces - - - - - Instructions - - - - - Config - - - - - Properties - - - - - Actions - - - - - Logs - - - - - Restore from Backup - - - - - Donate - - - - - Marketplace Listing - - - - - - Dependencies - - + + + + + + + + + + 20"> + {{ manifest.title }} + + + {{ manifest.version | displayEmver }} + + + + + + + + + + Configure + + + Stop + + + Fix + + + Start + + + + + + Launch Web Interface + + + + + + + + + + + + + {{ button.title }} + - + + + - - - - - - - - {{ localDep ? (localDep | manifest).title : pkg.installed.status['dependency-errors'][dep.key]?.title }} - {{ manifest.dependencies[dep.key].version | displayEmver }} - {{ pkg.installed.status['dependency-errors'][dep.key] ? pkg.installed.status['dependency-errors'][dep.key].type : 'satisfied' }} - + + + + + + Dependencies + + + + + + + + + + + {{ localDep ? (localDep | manifest).title : pkg.installed.status['dependency-errors'][dep.key]?.title }} + {{ manifest.dependencies[dep.key].version | displayEmver }} + {{ pkg.installed.status['dependency-errors'][dep.key] ? pkg.installed.status['dependency-errors'][dep.key].type : 'satisfied' }} + - - View - - - - - Install + + View - - - Start + + + Install - - Update - - - Configure - - - - - + + + Start + + + Update + + + Configure + + - - - - - - - - - - - - Uninstall - - - - + + + + + + + + + + + diff --git a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.scss b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.scss index d2e05baf4..79e83f0f8 100644 --- a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.scss +++ b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.scss @@ -16,7 +16,7 @@ .top-plate { background: var(--ion-item-background); - margin: 20px 10px; + margin: 0 16px; border-radius: 10px; border-style: solid; border-color: #373737; @@ -49,6 +49,12 @@ margin: 12px 10px; } +.dep-card { + border-radius: 10px; + border-style: solid; + border-color: #404040; +} + .dep-badge { position: absolute; width: 2.5vh; height: 2.5vh; diff --git a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.ts b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.ts index 5eb140294..5d7a2669d 100644 --- a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.ts +++ b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.ts @@ -1,14 +1,14 @@ import { Component, ViewChild } from '@angular/core' import { AlertController, NavController, ModalController, IonContent, PopoverController } from '@ionic/angular' import { ApiService } from 'src/app/services/api/api.service' -import { ActivatedRoute, NavigationExtras } from '@angular/router' -import { chill, isEmptyObject, pauseFor } from 'src/app/util/misc.util' +import { ActivatedRoute, NavigationExtras, Router } from '@angular/router' +import { chill, isEmptyObject } from 'src/app/util/misc.util' import { LoaderService } from 'src/app/services/loader.service' import { Observable, of, Subscription } from 'rxjs' import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component' -import { ConfigService } from 'src/app/services/config.service' +import { ConfigService, getManifest } from 'src/app/services/config.service' import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model' import { DependencyErrorConfigUnsatisfied, DependencyErrorNotInstalled, DependencyErrorType, Manifest, PackageDataEntry, PackageState } from 'src/app/models/patch-db/data-model' import { FEStatus } from 'src/app/services/pkg-status-rendering.service' @@ -26,13 +26,13 @@ export class AppInstalledShowPage { pkg: PackageDataEntry pkgSub: Subscription hideLAN: boolean + buttons: Button[] = [] + manifest: Manifest = { } as Manifest FeStatus = FEStatus PackageState = PackageState DependencyErrorType = DependencyErrorType - depDefinition = 'Service Dependencies are other services that this service recommends or requires in order to run.' - @ViewChild(IonContent) content: IonContent constructor ( @@ -51,7 +51,11 @@ export class AppInstalledShowPage { async ngOnInit () { this.pkgId = this.route.snapshot.paramMap.get('pkgId') - this.pkgSub = this.patch.watch$('package-data', this.pkgId).subscribe(pkg => this.pkg = pkg) + this.pkgSub = this.patch.watch$('package-data', this.pkgId).subscribe(pkg => { + this.pkg = pkg + this.manifest = getManifest(this.pkg) + }) + this.setButtons() } async ngAfterViewInit () { @@ -102,30 +106,14 @@ export class AppInstalledShowPage { } } - async uninstall () { - const { id, title, version, alerts } = this.pkg.installed.manifest - const data = await wizardModal( - this.modalCtrl, - this.wizardBaker.uninstall({ - id, - title, - version, - uninstallAlert: alerts.uninstall, - }), - ) - - if (data.cancelled) return - return this.navCtrl.navigateRoot('/services/installed') - } - - async donate (manifest: Manifest): Promise { - const url = manifest['donation-url'] + async donate (): Promise { + const url = this.manifest['donation-url'] if (url) { window.open(url, '_blank') } else { const alert = await this.alertCtrl.create({ header: 'Not Accepting Donations', - message: `The developers of ${manifest.title} have not provided a donation URL. Please contact them directly if you insist on giving them money.`, + message: `The developers of ${this.manifest.title} have not provided a donation URL. Please contact them directly if you insist on giving them money.`, buttons: ['OK'], }) await alert.present() @@ -163,6 +151,10 @@ export class AppInstalledShowPage { } } + asIsOrder () { + return 0 + } + private async installDep (depId: string): Promise { const version = this.pkg.installed.manifest.dependencies[depId].version const dependentTitle = this.pkg.installed.manifest.title @@ -234,4 +226,94 @@ export class AppInstalledShowPage { this.error = e.message return of() } + + setButtons (): void { + this.buttons = [ + { + action: () => this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }), + title: 'Health', + icon: 'medkit-outline', + color: 'danger', + disabled: [], + }, + { + action: () => this.navCtrl.navigateForward(['instructions'], { relativeTo: this.route }), + title: 'Instructions', + icon: 'list-outline', + color: 'danger', + disabled: [], + }, + { + action: () => this.navCtrl.navigateForward(['config'], { relativeTo: this.route }), + title: 'Configure', + icon: 'construct-outline', + color: 'danger', + disabled: [FEStatus.Installing, FEStatus.Updating, FEStatus.Removing, FEStatus.BackingUp, FEStatus.Restoring], + }, + { + action: () => this.navCtrl.navigateForward(['properties'], { relativeTo: this.route }), + title: 'Values', + icon: 'briefcase-outline', + color: 'danger', + disabled: [], + }, + { + action: () => this.navCtrl.navigateForward(['interfaces'], { relativeTo: this.route }), + title: 'Interfaces', + icon: 'desktop-outline', + color: 'danger', + disabled: [], + }, + { + action: () => this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }), + title: 'Actions', + icon: 'flash-outline', + color: 'danger', + disabled: [], + }, + { + action: () => this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }), + title: 'Logs', + icon: 'receipt-outline', + color: 'danger', + disabled: [], + }, + { + action: () => this.navCtrl.navigateForward(['restore'], { relativeTo: this.route }), + title: 'Restore Backup', + icon: 'color-wand-outline', + color: 'danger', + disabled: [FEStatus.Connecting, FEStatus.Installing, FEStatus.Updating, FEStatus.Stopping, FEStatus.Removing, FEStatus.BackingUp, FEStatus.Restoring], + }, + { + action: () => this.navCtrl.navigateForward(['manifest'], { relativeTo: this.route }), + title: 'Package Manifest', + icon: 'finger-print-outline', + color: 'danger', + disabled: [], + }, + { + action: () => this.donate(), + title: 'Support Project', + icon: 'logo-bitcoin', + color: 'danger', + disabled: [], + }, + { + action: () => this.navCtrl.navigateForward(['/services', 'marketplace', this.pkgId], { relativeTo: this.route }), + title: 'Marketplace Listing', + icon: 'storefront-outline', + color: 'danger', + disabled: [], + }, + ] + } +} + +interface Button { + title: string + icon: string + color: string + disabled: FEStatus[] + action: Function } diff --git a/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html index bb2091b6d..295a17cb6 100644 --- a/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html +++ b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html @@ -40,11 +40,6 @@ - - - Advanced - - diff --git a/ui/src/app/pages/apps-routes/app-manifest/app-manifest.module.ts b/ui/src/app/pages/apps-routes/app-manifest/app-manifest.module.ts new file mode 100644 index 000000000..ebdb23287 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-manifest/app-manifest.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { Routes, RouterModule } from '@angular/router' +import { IonicModule } from '@ionic/angular' +import { AppManifestPage } from './app-manifest.page' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { SharingModule } from 'src/app/modules/sharing.module' + +const routes: Routes = [ + { + path: '', + component: AppManifestPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + PwaBackComponentModule, + SharingModule, + ], + declarations: [AppManifestPage], +}) +export class AppManifestPageModule { } diff --git a/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.html b/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.html new file mode 100644 index 000000000..56d21aa7d --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.html @@ -0,0 +1,45 @@ + + + + + + Manifest + + + + + + {{ error }} + + + + + + + + + {{ prop.key }} + + + + + + + + {{ prop.key }} + {{ prop.value }} + + + + + + + + + + {{ prop }} + + + + + diff --git a/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.scss b/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.scss new file mode 100644 index 000000000..eea898305 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.scss @@ -0,0 +1,3 @@ +.metric-note { + font-size: 16px; +} \ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.ts b/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.ts new file mode 100644 index 000000000..52e3280e2 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.ts @@ -0,0 +1,68 @@ +import { Component } from '@angular/core' +import { ActivatedRoute } from '@angular/router' +import { NavController } from '@ionic/angular' +import * as JsonPointer from 'json-pointer' +import { Subscription } from 'rxjs' +import { distinctUntilChanged } from 'rxjs/operators' +import { PackageDataEntry } from 'src/app/models/patch-db/data-model' +import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model' +import { getManifest } from 'src/app/services/config.service' + +@Component({ + selector: 'app-manifest', + templateUrl: './app-manifest.page.html', + styleUrls: ['./app-manifest.page.scss'], +}) +export class AppManifestPage { + pkgId: string + pkg: PackageDataEntry + pointer: string + node: object + subs: Subscription[] + + constructor ( + private readonly route: ActivatedRoute, + private readonly patch: PatchDbModel, + private readonly navCtrl: NavController, + ) { } + + ngOnInit () { + this.pkgId = this.route.snapshot.paramMap.get('pkgId') + + this.subs = [ + this.patch.watch$('package-data', this.pkgId) + .subscribe(pkg => { + this.pkg = pkg + this.setNode() + }), + this.route.queryParams + .pipe(distinctUntilChanged()) + .subscribe(queryParams => { + this.pointer = queryParams['pointer'] + this.setNode() + }), + ] + + this.setNode() + } + + ngOnDestroy () { + this.subs.forEach(sub => sub.unsubscribe()) + } + + setNode () { + this.node = JsonPointer.get(getManifest(this.pkg), this.pointer || '') + } + + async goToNested (key: string): Promise { + this.navCtrl.navigateForward(`/services/installed/${this.pkgId}/manifest`, { + queryParams: { + pointer: `${this.pointer || ''}/${key}`, + }, + }) + } + + asIsOrder (a: any, b: any) { + return 0 + } +} diff --git a/ui/src/app/pages/apps-routes/app-metrics/app-metrics.module.ts b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.module.ts new file mode 100644 index 000000000..b15526a5d --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { Routes, RouterModule } from '@angular/router' +import { IonicModule } from '@ionic/angular' +import { AppMetricsPage } from './app-metrics.page' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { SharingModule } from 'src/app/modules/sharing.module' + +const routes: Routes = [ + { + path: '', + component: AppMetricsPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + PwaBackComponentModule, + SharingModule, + ], + declarations: [AppMetricsPage], +}) +export class AppMetricsPageModule { } diff --git a/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html new file mode 100644 index 000000000..fb91e66cc --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html @@ -0,0 +1,39 @@ + + + + + + Monitor + + + + + + {{ error }} + + + + + + Health Checks + + + + + + + + + + + {{ health.key }} + {{ health.value.error }} + + + + + No health checks + + + + diff --git a/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.scss b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.scss new file mode 100644 index 000000000..eea898305 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.scss @@ -0,0 +1,3 @@ +.metric-note { + font-size: 16px; +} \ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts new file mode 100644 index 000000000..9c1df721a --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core' +import { ActivatedRoute } from '@angular/router' +import { Subscription } from 'rxjs' +import { PackageDataEntry } from 'src/app/models/patch-db/data-model' +import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model' + +@Component({ + selector: 'app-metrics', + templateUrl: './app-metrics.page.html', + styleUrls: ['./app-metrics.page.scss'], +}) +export class AppMetricsPage { + pkgId: string + pkg: PackageDataEntry + subs: Subscription[] + + constructor ( + private readonly route: ActivatedRoute, + private readonly patch: PatchDbModel, + ) { } + + ngOnInit () { + this.pkgId = this.route.snapshot.paramMap.get('pkgId') + + this.subs = [ + this.patch.watch$('package-data', this.pkgId) + .subscribe(pkg => { + this.pkg = pkg + }), + ] + } + + ngOnDestroy () { + this.subs.forEach(sub => sub.unsubscribe()) + } + + asIsOrder (a: any, b: any) { + return 0 + } +} diff --git a/ui/src/app/pages/apps-routes/app-properties/app-properties.page.html b/ui/src/app/pages/apps-routes/app-properties/app-properties.page.html index 53d62db29..f55fd96f9 100644 --- a/ui/src/app/pages/apps-routes/app-properties/app-properties.page.html +++ b/ui/src/app/pages/apps-routes/app-properties/app-properties.page.html @@ -3,7 +3,7 @@ - Properties + Values @@ -28,15 +28,15 @@ - + - No properties. + No values. - - + + diff --git a/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts b/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts index 5efc1ee9e..f6c81ffd5 100644 --- a/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts +++ b/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts @@ -1,15 +1,14 @@ import { Component } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { ApiService } from 'src/app/services/api/api.service' -import { isEmptyObject, pauseFor } from 'src/app/util/misc.util' -import { BehaviorSubject, Subject } from 'rxjs' +import { Subscription } from 'rxjs' import { copyToClipboard } from 'src/app/util/web.util' import { AlertController, NavController, PopoverController, ToastController } from '@ionic/angular' import { PackageProperties } from 'src/app/util/properties.util' import { QRComponent } from 'src/app/components/qr/qr.component' import { PropertyStore } from './property-store' import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model' -import * as JSONpointer from 'json-pointer' +import * as JsonPointer from 'json-pointer' import { FEStatus } from 'src/app/services/pkg-status-rendering.service' @Component({ @@ -23,10 +22,11 @@ export class AppPropertiesPage { pkgId: string pointer: string qrCode: string - properties$ = new BehaviorSubject({ }) - hasProperties$ = new BehaviorSubject(null) + properties: PackageProperties + node: PackageProperties unmasked: { [key: string]: boolean } = { } FeStatus = FEStatus + subs: Subscription[] constructor ( private readonly route: ActivatedRoute, @@ -36,28 +36,27 @@ export class AppPropertiesPage { private readonly popoverCtrl: PopoverController, private readonly propertyStore: PropertyStore, private readonly navCtrl: NavController, - public patch: PatchDbModel, + public readonly patch: PatchDbModel, ) { } - ngOnInit () { + async ngOnInit () { this.pkgId = this.route.snapshot.paramMap.get('pkgId') - this.pointer = this.route.queryParams['pointer'] - this.getProperties().then(() => this.loading = false) + await this.getProperties() - this.propertyStore.watch$().subscribe(m => { - const properties = JSONpointer.get(m, this.pointer || '') - this.properties$.next(properties) - }) - this.properties$.subscribe(m => { - this.hasProperties$.next(!isEmptyObject(m)) - }) - this.route.queryParams.subscribe(queryParams => { - if (queryParams['pointer'] === this.pointer) return - this.pointer = queryParams['pointer'] - const properties = JSONpointer.get(this.propertyStore.properties$.getValue(), this.pointer || '') - this.properties$.next(properties) - }) + this.subs = [ + this.route.queryParams.subscribe(queryParams => { + if (queryParams['pointer'] === this.pointer) return + this.pointer = queryParams['pointer'] + this.node = JsonPointer.get(this.properties, this.pointer || '') + }), + ] + + this.loading = false + } + + ngOnDestroy () { + this.subs.forEach(sub => sub.unsubscribe()) } async doRefresh (event: any) { @@ -118,8 +117,8 @@ export class AppPropertiesPage { private async getProperties (): Promise { try { - const properties = await this.apiService.getPackageProperties({ id: this.pkgId }) - this.propertyStore.update(properties) + this.properties = await this.apiService.getPackageProperties({ id: this.pkgId }) + this.node = JsonPointer.get(this.properties, this.pointer || '') } catch (e) { console.error(e) this.error = e.message diff --git a/ui/src/app/pages/apps-routes/apps-routing.module.ts b/ui/src/app/pages/apps-routes/apps-routing.module.ts index dbbad4726..1fe38bda2 100644 --- a/ui/src/app/pages/apps-routes/apps-routing.module.ts +++ b/ui/src/app/pages/apps-routes/apps-routing.module.ts @@ -39,6 +39,14 @@ const routes: Routes = [ path: 'installed/:pkgId/logs', loadChildren: () => import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule), }, + { + path: 'installed/:pkgId/manifest', + loadChildren: () => import('./app-manifest/app-manifest.module').then(m => m.AppManifestPageModule), + }, + { + path: 'installed/:pkgId/metrics', + loadChildren: () => import('./app-metrics/app-metrics.module').then(m => m.AppMetricsPageModule), + }, { path: 'installed/:pkgId/properties', loadChildren: () => import('./app-properties/app-properties.module').then(m => m.AppPropertiesPageModule), diff --git a/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts index 5b3c085c3..e2c91f316 100644 --- a/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts +++ b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core' -import { ServerMetrics } from 'src/app/services/api/api-types' +import { Metrics } from 'src/app/services/api/api-types' import { ApiService } from 'src/app/services/api/api.service' import { pauseFor } from 'src/app/util/misc.util' @@ -12,7 +12,7 @@ export class ServerMetricsPage { error = '' loading = true going = false - metrics: ServerMetrics = { } + metrics: Metrics = { } constructor ( private readonly apiService: ApiService, diff --git a/ui/src/app/pages/server-routes/server-show/server-show.page.html b/ui/src/app/pages/server-routes/server-show/server-show.page.html index 92f085640..14e903d59 100644 --- a/ui/src/app/pages/server-routes/server-show/server-show.page.html +++ b/ui/src/app/pages/server-routes/server-show/server-show.page.html @@ -12,60 +12,60 @@ Backups - - Create Backup + + Create Backup Insights - - About + + About - - Monitor + + Monitor - - Logs + + Logs Settings - - General + + General - - LAN + + LAN - - WiFi + + WiFi - - Developer Options + + Developer Options Power - - Restart + + Restart - - Shutdown + + Shutdown \ No newline at end of file diff --git a/ui/src/app/pipes/empty.pipe.ts b/ui/src/app/pipes/empty.pipe.ts index e78c1581f..bc72cf602 100644 --- a/ui/src/app/pipes/empty.pipe.ts +++ b/ui/src/app/pipes/empty.pipe.ts @@ -5,7 +5,8 @@ import { isEmptyObject } from '../util/misc.util' name: 'empty', }) export class EmptyPipe implements PipeTransform { - transform (obj: { }): boolean { - return isEmptyObject(obj) + transform (val: object | [] = { }): boolean { + if (Array.isArray(val)) return !val.length + return isEmptyObject(val) } } \ No newline at end of file diff --git a/ui/src/app/pipes/typeof.pipe.ts b/ui/src/app/pipes/typeof.pipe.ts index 6881690bf..46060c9a1 100644 --- a/ui/src/app/pipes/typeof.pipe.ts +++ b/ui/src/app/pipes/typeof.pipe.ts @@ -5,6 +5,12 @@ import { Pipe, PipeTransform } from '@angular/core' }) export class TypeofPipe implements PipeTransform { transform (value: any): any { + if (value === null) { + return 'null' + } else if (Array.isArray(value)) { + return 'array' + } + return typeof value } } \ No newline at end of file diff --git a/ui/src/app/services/api/api-types.ts b/ui/src/app/services/api/api-types.ts index 65d8f13df..5e4620558 100644 --- a/ui/src/app/services/api/api-types.ts +++ b/ui/src/app/services/api/api-types.ts @@ -28,7 +28,7 @@ export module RR { export type GetServerLogsRes = Log[] export type GetServerMetricsReq = { } // server.metrics - export type GetServerMetricsRes = ServerMetrics + export type GetServerMetricsRes = Metrics export type UpdateServerReq = WithExpire<{ }> // server.update export type UpdateServerRes = WithRevision @@ -219,7 +219,7 @@ export interface ActionResponse { qr: boolean } -export interface ServerMetrics { +export interface Metrics { [key: string]: { [key: string]: { value: string | number | null diff --git a/ui/src/app/services/api/mock-app-fixures.ts b/ui/src/app/services/api/mock-app-fixures.ts index a33706055..859335bae 100644 --- a/ui/src/app/services/api/mock-app-fixures.ts +++ b/ui/src/app/services/api/mock-app-fixures.ts @@ -67,17 +67,7 @@ export module Mock { inject: false, 'shm-size': '', }, - 'health-check': { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [''], - mounts: { }, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - }, + 'health-checks': { }, config: null, volumes: { }, 'min-os-version': '0.2.12', @@ -198,17 +188,7 @@ export module Mock { inject: false, 'shm-size': '', }, - 'health-check': { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [''], - mounts: { }, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - }, + 'health-checks': { }, config: null, volumes: { }, 'min-os-version': '0.2.12', @@ -353,17 +333,7 @@ export module Mock { inject: false, 'shm-size': '', }, - 'health-check': { - type: 'docker', - image: '', - system: true, - entrypoint: '', - args: [''], - mounts: { }, - 'io-format': DockerIoFormat.Yaml, - inject: false, - 'shm-size': '', - }, + 'health-checks': { }, config: null, volumes: { }, 'min-os-version': '0.2.12', @@ -991,7 +961,7 @@ export module Mock { }, }, }, - 'Another Property': { + 'Another Value': { type: 'string', description: 'Some more information about the service.', copyable: false, diff --git a/ui/src/app/services/http.service.ts b/ui/src/app/services/http.service.ts index fedde0893..428cf220f 100644 --- a/ui/src/app/services/http.service.ts +++ b/ui/src/app/services/http.service.ts @@ -22,7 +22,7 @@ export class HttpService { this.fullUrl = `${window.location.protocol}//${window.location.hostname}:${port}/${url}/${version}` } - watch401$ (): Observable<{ }> { + watchUnauth$ (): Observable<{ }> { return this.unauthorizedApiResponse$.asObservable() } @@ -36,7 +36,10 @@ export class HttpService { const res = await this.httpRequest>(httpOpts) - if (isRpcError(res)) throw new RpcError(res.error) + if (isRpcError(res)) { + if (res.error.code === 34) this.unauthorizedApiResponse$.next(true) + throw new RpcError(res.error) + } if (isRpcSuccess(res)) return res.result } diff --git a/ui/src/app/services/pkg-status-rendering.service.ts b/ui/src/app/services/pkg-status-rendering.service.ts index 7c5ffc731..d1545359f 100644 --- a/ui/src/app/services/pkg-status-rendering.service.ts +++ b/ui/src/app/services/pkg-status-rendering.service.ts @@ -1,4 +1,4 @@ -import { PackageDataEntry, PackageMainStatus, PackageState, Status } from '../models/patch-db/data-model' +import { HealthCheckResultWarmingUp, MainStatus, MainStatusRunning, PackageDataEntry, PackageMainStatus, PackageState, Status } from '../models/patch-db/data-model' import { ConnectionState } from './connection.service' export function renderPkgStatus (pkg: PackageDataEntry, connection: ConnectionState): PkgStatusRendering { @@ -19,16 +19,26 @@ function handleInstalledState (status: Status): PkgStatusRendering { return { display: 'Needs Config', color: 'warning', showDots: false, feStatus: FEStatus.NeedsConfig } } - if (Object.values(status['dependency-errors']).length) { + if (Object.keys(status['dependency-errors']).length) { return { display: 'Dependency Issue', color: 'warning', showDots: false, feStatus: FEStatus.DependencyIssue } } switch (status.main.status) { - case PackageMainStatus.Running: return { display: 'Running', color: 'success', showDots: false, feStatus: FEStatus.Running } case PackageMainStatus.Stopping: return { display: 'Stopping', color: 'dark', showDots: true, feStatus: FEStatus.Stopping } case PackageMainStatus.Stopped: return { display: 'Stopped', color: 'medium', showDots: false, feStatus: FEStatus.Stopped } case PackageMainStatus.BackingUp: return { display: 'Backing Up', color: 'warning', showDots: true, feStatus: FEStatus.BackingUp } case PackageMainStatus.Restoring: return { display: 'Restoring', color: 'primary', showDots: true, feStatus: FEStatus.Restoring } + case PackageMainStatus.Running: return handleRunningState(status.main) + } +} + +function handleRunningState (status: MainStatusRunning): PkgStatusRendering { + if (Object.values(status.health).some(h => h.result === 'failure')) { + return { display: 'Needs Attention', color: 'danger', showDots: false, feStatus: FEStatus.NeedsAttention } + } else if (Object.values(status.health).some(h => h.result === 'warming-up')) { + return { display: 'Starting Up', color: 'warning', showDots: true, feStatus: FEStatus.StartingUp } + } else { + return { display: 'Running', color: 'success', showDots: false, feStatus: FEStatus.Running } } } @@ -52,6 +62,8 @@ export enum FEStatus { BackingUp = 'backing-up', Restoring = 'restoring', // FE + NeedsAttention = 'needs-attention', + StartingUp = 'starting-up', Connecting = 'connecting', DependencyIssue = 'dependency-issue', NeedsConfig = 'needs-config', diff --git a/ui/ui-config.json b/ui/ui-config.json index 7f06abd3a..65a58f496 100644 --- a/ui/ui-config.json +++ b/ui/ui-config.json @@ -10,7 +10,7 @@ }, "mocks": { "enabled": true, - "connection": "poll", + "connection": "ws", "rpcPort": "5959", "wsPort": "5960", "maskAs": "tor",
No Actions for {{ manifest.title }} {{ manifest.versionInstalled }}.
{{ action.value.description }}
This will uninstall the service from your Embassy and delete all data permanently.
{{ manifest.dependencies[dep.key].version | displayEmver }}
{{ pkg.installed.status['dependency-errors'][dep.key] ? pkg.installed.status['dependency-errors'][dep.key].type : 'satisfied' }}
{{ prop.value }}
{{ health.value.error }}
No properties.
No values.