rework status display in service show

This commit is contained in:
Matt Hill
2021-09-21 22:45:04 -06:00
committed by Matt Hill
parent 8cdd70090d
commit 17f58aabff
7 changed files with 147 additions and 112 deletions

View File

@@ -4,7 +4,7 @@
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Properties</ion-title>
<ion-buttons slot="end">
<ion-buttons *ngIf="!loading" slot="end">
<ion-button (click)="refresh()">
<ion-icon slot="start" name="refresh"></ion-icon>
Refresh
@@ -56,7 +56,7 @@
<ion-button *ngIf="prop.value.masked" fill="clear" (click)="toggleMask(prop.key)">
<ion-icon slot="icon-only" [name]="unmasked[prop.key] ? 'eye-off-outline' : 'eye-outline'" [color]="unmasked[prop.key] ? 'danger' : 'dark'" size="small"></ion-icon>
</ion-button>
<ion-button *ngIf="prop.value.qr" fill="clear" (click)="showQR(prop.value.value, $event)">
<ion-button *ngIf="prop.value.qr" fill="clear" (click)="showQR(prop.value.value)">
<ion-icon slot="icon-only" name="qr-code-outline" size="small"></ion-icon>
</ion-button>
<ion-button *ngIf="prop.value.copyable" fill="clear" (click)="copy(prop.value.value)">

View File

@@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { Subscription } from 'rxjs'
import { copyToClipboard } from 'src/app/util/web.util'
import { AlertController, IonContent, NavController, PopoverController, ToastController } from '@ionic/angular'
import { AlertController, IonContent, ModalController, NavController, ToastController } from '@ionic/angular'
import { PackageProperties } from 'src/app/util/properties.util'
import { QRComponent } from 'src/app/components/qr/qr.component'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
@@ -34,7 +34,7 @@ export class AppPropertiesPage {
private readonly errToast: ErrorToastService,
private readonly alertCtrl: AlertController,
private readonly toastCtrl: ToastController,
private readonly popoverCtrl: PopoverController,
private readonly modalCtrl: ModalController,
private readonly navCtrl: NavController,
private readonly patch: PatchDbService,
) { }
@@ -100,16 +100,15 @@ export class AppPropertiesPage {
await toast.present()
}
async showQR (text: string, ev: any): Promise<void> {
const popover = await this.popoverCtrl.create({
async showQR (text: string): Promise<void> {
const modal = await this.modalCtrl.create({
component: QRComponent,
cssClass: 'qr-popover',
event: ev,
componentProps: {
text,
},
cssClass: 'qr-modal',
})
return await popover.present()
await modal.present()
}
toggleMask (key: string) {

View File

@@ -19,19 +19,11 @@
<ion-content>
<ion-item-group>
<!-- ** always ** -->
<!-- ** status ** -->
<ion-item-divider>Status</ion-item-divider>
<ion-item>
<ion-label style="overflow: visible;">
<status [disconnected]="connectionFailure" size="x-large" weight="500" [rendering]="PR[statuses.primary]"></status>
<span *ngIf="statuses.dependency">
Dependencies:
<status [disconnected]="connectionFailure" size="medium" weight="500" [rendering]="DR[statuses.dependency]"></status>
</span>
<span *ngIf="statuses.health">
Health:
<status [disconnected]="connectionFailure" size="medium" weight="500" [rendering]="HR[statuses.health]"></status>
</span>
</ion-label>
<ng-container *ngIf="pkg.state === PackageState.Installed && !connectionFailure">
<ion-button slot="end" class="action-button" *ngIf="pkg.manifest.interfaces | hasUi" [disabled]="!(pkg.state | isLaunchable : pkg.installed.status.main.status : pkg.manifest.interfaces)" (click)="launchUi()">
@@ -44,15 +36,11 @@
<ion-button slot="end" class="action-button" *ngIf="statuses.primary === PS.Running" color="danger" (click)="stop()">
Stop
</ion-button>
<ion-button slot="end" class="action-button" *ngIf="statuses.dependency && status.dependency !== DS.Satisfied" (click)="scrollToRequirements()">
Fix
</ion-button>
<ion-button slot="end" class="action-button" *ngIf="statuses.primary === PS.Stopped && statuses.dependency !== DS.Critical" color="success" (click)="tryStart()">
<ion-button slot="end" class="action-button" *ngIf="statuses.primary === PS.Stopped && pkg.installed.status.configured && statuses.dependency !== DS.Critical" color="success" (click)="tryStart()">
Start
</ion-button>
</ng-container>
</ion-item>
<!-- ** installed ** -->
<ng-container *ngIf="pkg.state === PackageState.Installed">
<!-- ** !restoring/backing-up ** -->
@@ -87,55 +75,32 @@
</ion-item>
</ng-container>
</ng-container>
<!-- ** dependencies ** -->
<ng-container *ngIf="!(dependencies | empty)">
<ion-item-divider>Dependencies</ion-item-divider>
<!-- dependencies are a subset of the pkg.manifest.dependencies that are currently required as determined by the service config -->
<ion-item button *ngFor="let dep of dependencies | keyvalue" (click)="dep.value.action()">
<ion-thumbnail slot="start">
<img [src]="dep.value.icon" />
</ion-thumbnail>
<ion-label>
<h2 style="font-family: 'Montserrat'">{{ dep.value.title }}</h2>
<p>{{ dep.value.version | displayEmver }}</p>
<p><ion-text [color]="!!dep.value.errorText ? 'warning' : 'success'">{{ dep.value.errorText || 'satisfied' }}</ion-text></p>
</ion-label>
<ion-spinner *ngIf="dep.value.spinnerColor" slot="end" [color]="dep.value.spinnerColor" style="height: 3vh; width: 3vh"></ion-spinner>
<ion-button *ngIf="dep.value.actionText" slot="end" fill="clear">
{{ dep.value.actionText }}
<ion-icon slot="end" name="arrow-forward"></ion-icon>
</ion-button>
</ion-item>
</ng-container>
<!-- ** menu ** -->
<ion-item-divider>Menu</ion-item-divider>
<ion-item button detail *ngFor="let button of buttons" (click)="button.action()">
<ion-icon slot="start" [name]="button.icon"></ion-icon>
<ion-label>{{ button.title }}</ion-label>
</ion-item>
<!-- ** dependencies ** -->
<ng-container *ngIf="!(currentDependencies | empty)">
<ion-item-divider id="dependencies">Dependencies</ion-item-divider>
<!-- A current-dependency is a subset of the pkg.manifest.dependencies that is currently required as determined by the service config. -->
<ion-item *ngFor="let dep of currentDependencies | keyvalue">
<ion-thumbnail slot="start">
<img [src]="pkg.installed['dependency-info'][dep.key].icon" />
</ion-thumbnail>
<ion-label>
<h2 style="font-family: 'Montserrat'">{{ pkg.installed['dependency-info'][dep.key].manifest.title }}</h2>
<p>{{ pkg.manifest.dependencies[dep.key].version | displayEmver }}</p>
<p><ion-text [color]="pkg.installed.status['dependency-errors'][dep.key] ? 'warning' : 'success'">{{ pkg.installed.status['dependency-errors'][dep.key] ? pkg.installed.status['dependency-errors'][dep.key].type : 'satisfied' }}</ion-text></p>
</ion-label>
<ion-button *ngIf="!pkg.installed.status['dependency-errors'][dep.key] || (pkg.installed.status['dependency-errors'][dep.key] && [DependencyErrorType.InterfaceHealthChecksFailed, DependencyErrorType.HealthChecksFailed] | includes : pkg.installed.status['dependency-errors'][dep.key].type)" slot="end" size="small" [routerLink]="['/services', dep.key]">
View
</ion-button>
<ng-container *ngIf="pkg.installed.status['dependency-errors'][dep.key]">
<ion-button *ngIf="!patch.data['package-data'][dep.key]" slot="end" size="small" (click)="fixDep('install', dep.key)">
Install
</ion-button>
<ng-container *ngIf="patch.data['package-data'][dep.key] && patch.data['package-data'][dep.key].state === PackageState.Installed">
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.NotRunning" slot="end" size="small" [routerLink]="['/services', dep.key]">
Start
</ion-button>
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.IncorrectVersion" slot="end" size="small" (click)="fixDep('update', dep.key)">
Update
</ion-button>
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.ConfigUnsatisfied" slot="end" size="small" (click)="fixDep('configure', dep.key)">
Configure
</ion-button>
</ng-container>
<div *ngIf="patch.data['package-data'][dep.key] && patch.data['package-data'][dep.key].state !== PackageState.Installed" slot="end">
<ion-spinner [color]="patch.data['package-data'][dep.key].state === PackageState.Removing ? 'danger' : 'primary'" style="height: 3vh; width: 3vh" name="dots"></ion-spinner>
</div>
</ng-container>
</ion-item>
</ng-container>
</ng-container>
<!-- @TODO better maintenance messaging -->
<ng-template #maintenance>

View File

@@ -8,8 +8,8 @@ import { wizardModal } from 'src/app/components/install-wizard/install-wizard.co
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { ConfigService } from 'src/app/services/config.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { CurrentDependencyInfo, DependencyErrorConfigUnsatisfied, DependencyErrorType, HealthCheckResult, PackageDataEntry, PackageMainStatus, PackageState } from 'src/app/services/patch-db/data-model'
import { DependencyRendering, DependencyStatus, HealthRendering, HealthStatus, PrimaryRendering, PrimaryStatus, renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
import { DependencyErrorConfigUnsatisfied, DependencyErrorType, HealthCheckResult, PackageDataEntry, PackageMainStatus, PackageState } from 'src/app/services/patch-db/data-model'
import { DependencyStatus, HealthStatus, PrimaryRendering, PrimaryStatus, renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
import { ConnectionFailure, ConnectionService } from 'src/app/services/connection.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { AppConfigPage } from 'src/app/modals/app-config/app-config.page'
@@ -27,16 +27,14 @@ export class AppShowPage {
Math = Math
PS = PrimaryStatus
DS = DependencyStatus
HS = HealthStatus
PR = PrimaryRendering
DR = DependencyRendering
HR = HealthRendering
pkgId: string
pkg: PackageDataEntry
hideLAN: boolean
buttons: Button[] = []
currentDependencies: { [id: string]: CurrentDependencyInfo }
// currentDependencies: { [id: string]: CurrentDependencyInfo }
dependencies: { [id: string]: DependencyInfo } = { }
statuses: {
primary: PrimaryStatus
dependency: DependencyStatus
@@ -61,19 +59,19 @@ export class AppShowPage {
private readonly wizardBaker: WizardBaker,
private readonly config: ConfigService,
private readonly packageLoadingService: PackageLoadingService,
public readonly patch: PatchDbService,
public readonly connectionService: ConnectionService,
private readonly patch: PatchDbService,
private readonly connectionService: ConnectionService,
) { }
async ngOnInit () {
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
this.setValues(this.patch.data['package-data'][this.pkgId])
this.setValues(this.patch.data['package-data'])
this.subs = [
// 1
this.patch.watch$('package-data', this.pkgId)
.subscribe(pkg => {
this.setValues(pkg)
this.patch.watch$('package-data')
.subscribe(pkgs => {
this.setValues(pkgs)
}),
// 2
this.connectionService.watchFailure$()
@@ -148,13 +146,6 @@ export class AppShowPage {
}
}
scrollToRequirements () {
const el = document.getElementById('dependencies')
if (!el) return
let y = el.offsetTop
return this.content.scrollToPoint(0, y, 1000)
}
async fixDep (action: 'install' | 'update' | 'configure', id: string): Promise<void> {
switch (action) {
case 'install':
@@ -175,22 +166,87 @@ export class AppShowPage {
await modal.present()
}
private setValues (pkg: PackageDataEntry): void {
this.pkg = pkg
this.installProgress = !isEmptyObject(pkg['install-progress']) ? this.packageLoadingService.transform(pkg['install-progress']) : undefined
// we can safely ignore any current dependencies that are not defined in the service manifest
this.currentDependencies = { }
Object.entries(pkg.installed?.['current-dependencies'] || { }).forEach(([id, value]) => {
if (pkg.manifest.dependencies[id]) {
this.currentDependencies[id] = value
}
})
if (pkg.installed?.status.main.status === PackageMainStatus.Running) {
this.healthChecks = { ...pkg.installed.status.main.health }
} else {
private setValues (pkgs: { [id: string]: PackageDataEntry }): void {
this.pkg = pkgs[this.pkgId]
this.installProgress = !isEmptyObject(this.pkg['install-progress']) ? this.packageLoadingService.transform(this.pkg['install-progress']) : undefined
this.statuses = renderPkgStatus(this.pkg)
if (!this.pkg.installed) {
this.dependencies = { }
this.healthChecks = { }
} else {
// ** dependencies
Object.keys(this.pkg.installed['current-dependencies'] || { })
.forEach(id => {
// we can safely ignore any current dependencies that are not defined in the service manifest
const manifestDep = this.pkg.manifest.dependencies[id]
if (manifestDep) {
let errorText = ''
let spinnerColor = ''
let actionText = 'View'
let action: () => any = () => this.navCtrl.navigateForward(`/services/${id}`)
const error = this.pkg.installed.status['dependency-errors'][id] || null
if (error) {
const localDep = pkgs[id]
// health checks failed
if ([DependencyErrorType.InterfaceHealthChecksFailed, DependencyErrorType.HealthChecksFailed].includes(error.type)) {
errorText = 'Health Check Failed'
// not fully installed (same as !localDep?.installed)
} else if (error.type === DependencyErrorType.NotInstalled) {
if (localDep) {
errorText = localDep.state // 'Installing' | 'Removing'
} else {
errorText = 'Not Installed'
actionText = 'Install'
action = () => this.fixDep('install', id)
}
// incorrect version
} else if (error.type === DependencyErrorType.IncorrectVersion) {
if (localDep) {
errorText = localDep.state // 'Updating' | 'Removing'
} else {
errorText = 'Incorrect Version'
actionText = 'Update'
action = () => this.fixDep('update', id)
}
// not running
} else if (error.type === DependencyErrorType.NotRunning) {
errorText = 'Not Running'
actionText = 'Start'
// config unsatisfied
} else if (error.type === DependencyErrorType.ConfigUnsatisfied) {
errorText = 'Config Not Satisfied'
actionText = 'Auto Config'
action = () => this.fixDep('configure', id)
}
if (localDep && localDep.state !== PackageState.Installed) {
spinnerColor = localDep.state === PackageState.Removing ? 'danger' : 'primary'
}
}
if (!this.dependencies[id]) this.dependencies[id] = { } as any
const depInfo = this.pkg.installed['dependency-info'][id]
this.dependencies[id].title = depInfo.manifest.title
this.dependencies[id].icon = depInfo.icon
this.dependencies[id].version = manifestDep.version
this.dependencies[id].errorText = errorText
this.dependencies[id].actionText = actionText
this.dependencies[id].spinnerColor = spinnerColor
this.dependencies[id].action = action
}
})
// ** health
if (this.pkg.installed.status.main.status === PackageMainStatus.Running) {
this.healthChecks = { ...this.pkg.installed.status.main.health }
} else {
this.healthChecks = { }
}
}
this.statuses = renderPkgStatus(pkg)
}
private async installDep (depId: string): Promise<void> {
@@ -334,6 +390,16 @@ export class AppShowPage {
}
}
interface DependencyInfo {
title: string
icon: string
version: string
errorText: string
spinnerColor: string
actionText: string
action: () => any
}
interface Button {
title: string
icon: string

View File

@@ -3,7 +3,7 @@
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Listing</ion-title>
<ion-title>Marketplace Listing</ion-title>
</ion-toolbar>
</ion-header>

View File

@@ -6,13 +6,14 @@ export function renderPkgStatus (pkg: PackageDataEntry): {
dependency: DependencyStatus | null,
health: HealthStatus | null
} {
console.log('PKGPKG', pkg)
let primary: PrimaryStatus
let dependency: DependencyStatus | null = null
let health: HealthStatus | null = null
if (pkg.state === PackageState.Installed) {
primary = pkg.installed.status.main.status as string as PrimaryStatus
dependency = getDependencyStatus(pkg.installed)
dependency = getDependencyStatus(pkg)
health = getHealthStatus(pkg.installed.status)
} else {
primary = pkg.state as string as PrimaryStatus
@@ -21,11 +22,11 @@ export function renderPkgStatus (pkg: PackageDataEntry): {
return { primary, dependency, health }
}
function getDependencyStatus (pkg: InstalledPackageDataEntry): DependencyStatus {
console.log('pkg', pkg)
if (isEmptyObject(pkg['current-dependencies'])) return null
function getDependencyStatus (pkg: PackageDataEntry): DependencyStatus {
const installed = pkg.installed
if (isEmptyObject(installed['current-dependencies'])) return null
const pkgIds = Object.keys(pkg.status['dependency-errors'])
const pkgIds = Object.keys(installed.status['dependency-errors'])
for (let pkgId of pkgIds) {
if (pkg.manifest.dependencies[pkgId].critical) {
@@ -92,8 +93,8 @@ export const PrimaryRendering: { [key: string]: StatusRendering } = {
[PrimaryStatus.Installing]: { display: 'Installing', color: 'primary', showDots: true },
[PrimaryStatus.Updating]: { display: 'Updating', color: 'primary', showDots: true },
[PrimaryStatus.Removing]: { display: 'Removing', color: 'warning', showDots: true },
[PrimaryStatus.Stopping]: { display: 'Stopping', color: 'dark', showDots: true },
[PrimaryStatus.Stopped]: { display: 'Stopped', color: 'dark', showDots: false },
[PrimaryStatus.Stopping]: { display: 'Stopping', color: 'dark-shade', showDots: true },
[PrimaryStatus.Stopped]: { display: 'Stopped', color: 'dark-shade', showDots: false },
[PrimaryStatus.BackingUp]: { display: 'Backing Up', color: 'warning', showDots: true },
[PrimaryStatus.Restoring]: { display: 'Restoring', color: 'primary', showDots: true },
[PrimaryStatus.Running]: { display: 'Running', color: 'success', showDots: false },

View File

@@ -174,6 +174,15 @@ ion-button {
box-shadow: 0 0 70px 70px black;
}
.qr-modal {
.modal-wrapper {
width: 400px !important;
height: 400px !important;
top: unset !important;
left: unset !important;
}
}
.modal-wrapper {
position: absolute;
height: 90% !important;
@@ -236,11 +245,6 @@ ion-slides {
}
}
.qr-popover {
--width: auto;
--background: transparent !important;
}
ion-item-divider {
text-transform: uppercase;
--padding-top: 24px;