health page, manifest page, watch unauth, begin redesign

This commit is contained in:
Matt Hill
2021-06-22 17:34:56 -06:00
committed by Aiden McClelland
parent 7013364ae8
commit 5d24d9324c
28 changed files with 614 additions and 302 deletions

View File

@@ -100,7 +100,7 @@ export class AppComponent {
}
})
this.http.watch401$().subscribe(() => {
this.http.watchUnauth$().subscribe(() => {
this.authService.setUnverified()
})
}

View File

@@ -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
}

View File

@@ -9,24 +9,19 @@
<ion-content class="ion-padding-top">
<ng-container *ngIf="patch.watch$('package-data', pkgId, 'installed') | ngrxPush as installed">
<ng-container *ngIf="installed.manifest as manifest">
<ion-item *ngIf="manifest.actions | empty; else actions">
<ion-item-group>
<ion-item button *ngFor="let action of installed.manifest.actions | keyvalue: asIsOrder" (click)="handleAction(installed, action)" >
<ion-label class="ion-text-wrap">
<p>No Actions for {{ manifest.title }} {{ manifest.versionInstalled }}.</p>
<h2><ion-text color="primary">{{ action.value.name }}</ion-text><ion-icon *ngIf="!(action.value['allowed-statuses'] | includes: installed.status.main.status)" color="danger" name="close-outline"></ion-icon></h2>
<p><ion-text color="dark">{{ action.value.description }}</ion-text></p>
</ion-label>
</ion-item>
<ng-template #actions>
<ion-item-group>
<ion-item button *ngFor="let action of manifest.actions | keyvalue: asIsOrder" (click)="handleAction(installed, action)" >
<ion-label class="ion-text-wrap">
<h2><ion-text color="primary">{{ action.value.name }}</ion-text><ion-icon *ngIf="!(action.value['allowed-statuses'] | includes: installed.status.main.status)" color="danger" name="close-outline"></ion-icon></h2>
<p><ion-text color="dark">{{ action.value.description }}</ion-text></p>
</ion-label>
</ion-item>
</ion-item-group>
</ng-template>
</ng-container>
<ion-item button (click)="uninstall(installed.manifest)" >
<ion-label class="ion-text-wrap">
<h2><ion-text color="primary">Uninstall</ion-text></h2>
<p><ion-text color="dark">This will uninstall the service from your Embassy and delete all data permanently.</ion-text></p>
</ion-label>
</ion-item>
</ion-item-group>
</ng-container>
</ion-content>

View File

@@ -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(

View File

@@ -10,172 +10,129 @@
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-bottom">
<ion-content class="ion-padding-top">
<ion-item *ngIf="error" style="margin-bottom: 16px;">
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item>
<ng-container *ngrxLet="connectionService.monitor$() as connection">
<ng-container *ngIf="pkg | manifest as manifest">
<ng-container *ngIf="pkg | status : connection as status">
<div class="top-plate">
<ion-item class="no-cushion-item" lines="none">
<ion-label class="ion-text-wrap" style="
display: grid;
grid-template-columns: 80px auto;
margin: 0px;
margin-top: 15px;"
>
<ion-avatar style="justify-self: center; height: 55px; width: 55px" slot="start">
<img [src]="pkg['static-files'].icon" />
</ion-avatar>
<div style="display: flex; flex-direction: column;">
<ion-text style="font-family: 'Montserrat'; font-size: x-large; line-height: normal;" [class.less-large]="manifest.title.length > 20">
{{ manifest.title }}
</ion-text>
<ion-text style="margin-top: -5px; margin-left: 2px;">
{{ manifest.version | displayEmver }}
</ion-text>
</div>
</ion-label>
</ion-item>
<ion-item class="no-cushion-item" lines=none style="margin-bottom: 10px;">
<ion-label class="status-readout">
<status size="bold-large" [pkg]="pkg" [connection]="connection"></status>
<ion-button *ngIf="status === FeStatus.NeedsConfig" expand="block" fill="outline" [routerLink]="['config']">
Configure
</ion-button>
<ion-button *ngIf="status === FeStatus.Running" expand="block" fill="outline" color="danger" (click)="stop()">
Stop
</ion-button>
<ion-button *ngIf="status === FeStatus.DependencyIssue" expand="block" fill="outline" (click)="scrollToRequirements()">
Fix
</ion-button>
<ion-button *ngIf="status === FeStatus.Stopped" expand="block" fill="outline" color="success" (click)="tryStart()">
Start
</ion-button>
</ion-label>
</ion-item>
<ion-button size="small" *ngIf="pkg | hasUi" [disabled]="!(pkg | isLaunchable)" class="launch-button" expand="block" (click)="launchUiTab()">
Launch Web Interface
<ion-icon slot="end" name="rocket-outline"></ion-icon>
</ion-button>
</div>
<ng-container *ngIf="!([FeStatus.Installing, FeStatus.Updating, FeStatus.Removing] | includes : status)">
<ion-item-group class="ion-padding-bottom">
<!-- interfaces -->
<ion-item [routerLink]="['interfaces']">
<ion-icon slot="start" name="aperture-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Interfaces</ion-text></ion-label>
</ion-item>
<!-- instructions -->
<ion-item [routerLink]="['instructions']">
<ion-icon slot="start" name="list-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Instructions</ion-text></ion-label>
</ion-item>
<!-- config -->
<ion-item [disabled]="[FeStatus.Installing, FeStatus.Updating, FeStatus.Removing, FeStatus.BackingUp, FeStatus.Restoring] | includes : status" [routerLink]="['config']">
<ion-icon slot="start" name="construct-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Config</ion-text></ion-label>
</ion-item>
<!-- properties -->
<ion-item [routerLink]="['properties']">
<ion-icon slot="start" name="information-circle-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Properties</ion-text></ion-label>
</ion-item>
<!-- actions -->
<ion-item [routerLink]="['actions']">
<ion-icon slot="start" name="flash-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Actions</ion-text></ion-label>
</ion-item>
<!-- logs -->
<ion-item [routerLink]="['logs']">
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Logs</ion-text></ion-label>
</ion-item>
<!-- restore -->
<ion-item button [disabled]="[FeStatus.Connecting, FeStatus.Installing, FeStatus.Updating, FeStatus.Stopping, FeStatus.Removing, FeStatus.BackingUp, FeStatus.Restoring] | includes : status" [routerLink]="['restore']">
<ion-icon slot="start" name="color-wand-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Restore from Backup</ion-text></ion-label>
</ion-item>
<!-- donate -->
<ion-item button (click)="donate(manifest)">
<ion-icon slot="start" name="shapes-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Donate</ion-text></ion-label>
</ion-item>
<!-- marketplace -->
<ion-item [routerLink]="['/services', 'marketplace', manifest.id]">
<ion-icon slot="start" name="storefront-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Marketplace Listing</ion-text></ion-label>
</ion-item>
<!-- dependencies -->
<ng-container *ngIf="!(manifest.dependencies | empty)">
<ion-item-divider id="dependencies">
Dependencies
<ion-button style="position: relative; right: 10px;" size="small" fill="clear" color="medium" (click)="presentPopover(depDefinition, $event)">
<ion-icon name="help-circle-outline"></ion-icon>
<ng-container *ngIf="pkg | status : connection as status">
<!-- top plate -->
<div class="top-plate">
<ion-item class="no-cushion-item" lines="none">
<ion-label class="ion-text-wrap" style="
display: grid;
grid-template-columns: 80px auto;
margin: 0px;
margin-top: 15px;"
>
<ion-avatar style="justify-self: center; height: 55px; width: 55px" slot="start">
<img [src]="pkg['static-files'].icon" />
</ion-avatar>
<div style="display: flex; flex-direction: column;">
<ion-text style="font-family: 'Montserrat'; font-size: x-large; line-height: normal;" [class.less-large]="manifest.title.length > 20">
{{ manifest.title }}
</ion-text>
<ion-text style="margin-top: -5px; margin-left: 2px;">
{{ manifest.version | displayEmver }}
</ion-text>
</div>
</ion-label>
</ion-item>
<ion-item class="no-cushion-item" lines="none" style="margin-bottom: 10px;">
<ion-label class="status-readout">
<status size="bold-large" [pkg]="pkg" [connection]="connection"></status>
<ion-button *ngIf="status === FeStatus.NeedsConfig" expand="block" fill="outline" [routerLink]="['config']">
Configure
</ion-button>
<ion-button *ngIf="[FeStatus.Running, FeStatus.StartingUp, FeStatus.NeedsAttention] | includes : status" expand="block" fill="outline" color="danger" (click)="stop()">
Stop
</ion-button>
<ion-button *ngIf="status === FeStatus.DependencyIssue" expand="block" fill="outline" (click)="scrollToRequirements()">
Fix
</ion-button>
<ion-button *ngIf="status === FeStatus.Stopped" expand="block" fill="outline" color="success" (click)="tryStart()">
Start
</ion-button>
</ion-label>
</ion-item>
<ion-button size="small" *ngIf="pkg | hasUi" [disabled]="!(pkg | isLaunchable)" class="launch-button" expand="block" (click)="launchUiTab()">
Launch Web Interface
<ion-icon slot="end" name="rocket-outline"></ion-icon>
</ion-button>
</div>
<ng-container *ngIf="!([FeStatus.Installing, FeStatus.Updating, FeStatus.Removing] | includes : status)">
<ion-grid class="ion-text-center" style="margin: 0 6px;">
<ion-row>
<ion-col *ngFor="let button of buttons" sizeMd="4" sizeSm="6" sizeXs="6">
<ion-button style="width: 100% !important; min-height: 120px; --background: #2f4858;" [disabled]="button.disabled | includes : status" (click)="button.action()">
<div>
<ion-icon size="large" [name]="button.icon" style="color: #fbe6ea"></ion-icon>
<br/><br/>
<ion-text style="color: #fbe6ea">{{ button.title }}</ion-text>
</div>
</ion-button>
</ion-item-divider>
</ion-col>
</ion-row>
</ion-grid>
<div *ngFor="let dep of pkg.installed['current-dependencies'] | keyvalue">
<ion-item *ngrxLet="patch.watch$('package-data', dep.key) as localDep" class="dependency-item" lines="none">
<ion-avatar slot="start" style="position: relative; height: 5vh; width: 5vh; margin: 0px;">
<div class="dep-badge" [class]="pkg.installed.status['dependency-errors'][dep.key] ? 'dep-issue' : 'dep-sat'"></div>
<img [src]="localDep ? localDep['static-files'].icon : pkg.installed.status['dependency-errors'][dep.key]?.icon" />
</ion-avatar>
<ion-label class="ion-text-wrap" style="padding: 1vh; padding-left: 2vh">
<h4 style="font-family: 'Montserrat'">{{ localDep ? (localDep | manifest).title : pkg.installed.status['dependency-errors'][dep.key]?.title }}</h4>
<p style="font-size: small">{{ manifest.dependencies[dep.key].version | displayEmver }}</p>
<p style="padding-top: 2px; position: relative; font-style: italic; font-size: smaller"><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-item-group class="ion-padding-bottom">
<!-- dependencies -->
<ng-container *ngIf="!(pkg.installed['current-dependencies'] | empty)">
<ion-card id="dependencies" class="dep-card">
<ion-card-header>
<ion-card-title>Dependencies</ion-card-title>
</ion-card-header>
<ion-card-content>
<!-- A current-dependency is a subset of the manifest.dependencies that is currently required as determined by the service config. -->
<div *ngFor="let dep of pkg.installed['current-dependencies'] | keyvalue">
<ion-item *ngrxLet="patch.watch$('package-data', dep.key) as localDep" class="dependency-item">
<ion-avatar slot="start" style="position: relative; height: 5vh; width: 5vh; margin: 0px;">
<div class="dep-badge" [class]="pkg.installed.status['dependency-errors'][dep.key] ? 'dep-issue' : 'dep-sat'"></div>
<img [src]="localDep ? localDep['static-files'].icon : pkg.installed.status['dependency-errors'][dep.key]?.icon" />
</ion-avatar>
<ion-label class="ion-text-wrap" style="padding: 1vh; padding-left: 2vh">
<h4 style="font-family: 'Montserrat'">{{ localDep ? (localDep | manifest).title : pkg.installed.status['dependency-errors'][dep.key]?.title }}</h4>
<p style="font-size: small">{{ manifest.dependencies[dep.key].version | displayEmver }}</p>
<p style="padding-top: 2px; position: relative; font-style: italic; font-size: smaller"><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', 'installed', dep.key]" color="primary" fill="outline" style="font-size: x-small">
View
</ion-button>
<ng-container *ngIf="pkg.installed.status['dependency-errors'][dep.key]">
<ion-button *ngIf="!localDep" slot="end" size="small" (click)="fixDep('install', dep.key)" color="primary" fill="outline" style="font-size: x-small">
Install
<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', 'installed', dep.key]" color="primary" fill="outline" style="font-size: x-small">
View
</ion-button>
<ng-container *ngIf="localDep && localDep.state === PackageState.Installed">
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.NotRunning" slot="end" size="small" [routerLink]="['/services', 'installed', dep.key]" color="primary" fill="outline" style="font-size: x-small">
Start
<ng-container *ngIf="pkg.installed.status['dependency-errors'][dep.key]">
<ion-button *ngIf="!localDep" slot="end" size="small" (click)="fixDep('install', dep.key)" color="primary" fill="outline" style="font-size: x-small">
Install
</ion-button>
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.IncorrectVersion" slot="end" size="small" (click)="fixDep('update', dep.key)" color="primary" fill="outline" style="font-size: x-small">
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)" color="primary" fill="outline" style="font-size: x-small">
Configure
</ion-button>
</ng-container>
<div *ngIf="localDep && localDep.state !== PackageState.Installed" slot="end" class="spinner">
<ion-spinner [color]="localDep.state === PackageState.Removing ? 'danger' : 'primary'" style="height: 3vh; width: 3vh" name="dots"></ion-spinner>
</div>
<ng-container *ngIf="localDep && localDep.state === PackageState.Installed">
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.NotRunning" slot="end" size="small" [routerLink]="['/services', 'installed', dep.key]" color="primary" fill="outline" style="font-size: x-small">
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)" color="primary" fill="outline" style="font-size: x-small">
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)" color="primary" fill="outline" style="font-size: x-small">
Configure
</ion-button>
</ng-container>
</ng-container>
</ion-item>
</div>
</ng-container>
<ion-item-divider></ion-item-divider>
<ng-container *ngIf="!([FeStatus.Installing, FeStatus.Updating, FeStatus.BackingUp, FeStatus.Restoring] | includes : status)">
<!-- uninstall -->
<ion-item button (click)="uninstall()">
<ion-icon slot="start" name="trash-outline" color="danger"></ion-icon>
<ion-label><ion-text color="danger">Uninstall</ion-text></ion-label>
</ion-item>
</ng-container>
</ion-item-group>
</ng-container>
<div *ngIf="localDep && localDep.state !== PackageState.Installed" slot="end" class="spinner">
<ion-spinner [color]="localDep.state === PackageState.Removing ? 'danger' : 'primary'" style="height: 3vh; width: 3vh" name="dots"></ion-spinner>
</div>
</ng-container>
</ion-item>
</div>
</ion-card-content>
</ion-card>
</ng-container>
</ion-item-group>
</ng-container>
</ng-container>
</ng-container>

View File

@@ -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;

View File

@@ -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 = '<span style="font-style: italic">Service Dependencies</span> 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<void> {
const url = manifest['donation-url']
async donate (): Promise<void> {
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<void> {
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
}

View File

@@ -40,11 +40,6 @@
<ion-icon size="small" slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
<ion-item button detail="true">
<ion-label class="ion-text-wrap">
Advanced
</ion-label>
</ion-item>
</ng-container>
</ion-card-content>
</ion-card>

View File

@@ -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 { }

View File

@@ -0,0 +1,45 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Manifest</ion-title>
</ion-toolbar>
</ion-header>
<ion-content *ngIf="pkg">
<ion-item *ngIf="error" style="margin-bottom: 16px;">
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item>
<ion-item-group *ngIf="node | typeof === 'object'">
<div *ngFor="let prop of node | keyvalue : asIsOrder">
<!-- object/array -->
<ng-container *ngIf="['object', 'array'] | includes : (prop.value | typeof); else notObj">
<ion-item button detail="true" *ngIf="!(prop.value | empty)" (click)="goToNested(prop.key)">
<ion-label class="ion-text-wrap">
<h2>{{ prop.key }}</h2>
</ion-label>
</ion-item>
</ng-container>
<!-- not object/array -->
<ng-template #notObj>
<ion-item *ngIf="prop.value">
<ion-label class="ion-text-wrap">
<h2>{{ prop.key }}</h2>
<p>{{ prop.value }}</p>
</ion-label>
</ion-item>
</ng-template>
</div>
</ion-item-group>
<!-- Arrays -->
<ion-item-group *ngIf="node | typeof === 'array'">
<ion-item *ngFor="let prop of node">
<ion-label class="ion-text-wrap">
{{ prop }}
</ion-label>
</ion-item>
</ion-item-group>
</ion-content>

View File

@@ -0,0 +1,3 @@
.metric-note {
font-size: 16px;
}

View File

@@ -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<any> {
this.navCtrl.navigateForward(`/services/installed/${this.pkgId}/manifest`, {
queryParams: {
pointer: `${this.pointer || ''}/${key}`,
},
})
}
asIsOrder (a: any, b: any) {
return 0
}
}

View File

@@ -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 { }

View File

@@ -0,0 +1,39 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Monitor</ion-title>
</ion-toolbar>
</ion-header>
<ion-content *ngIf="pkg">
<ion-item *ngIf="error" style="margin-bottom: 16px;">
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item>
<ion-card>
<ion-card-header>
<ion-card-title>
<ion-icon style="vertical-align: middle; padding-right: 12px;" name="medkit-outline"></ion-icon>
<span style="vertical-align: middle;">Health Checks</span>
</ion-card-title>
</ion-card-header>
<ion-card-content>
<ion-item *ngIf="!(pkg.installed?.status.main.health | empty); else noHealth" color="light" style="margin: 10px;">
<ion-label>
<div *ngFor="let health of pkg.installed.status.main.health | keyvalue : asIsOrder" class="align" style="margin-left: 12px;">
<ion-icon *ngIf="health.value.result === 'success'" name="checkmark-outline" color="success"></ion-icon>
<ion-icon *ngIf="health.value.result === 'warming-up'" name="timer-outline" color="warning"></ion-icon>
<ion-icon *ngIf="health.value.result === 'failure'" name="close-outline" color="danger"></ion-icon>
<ion-icon *ngIf="health.value.result === 'disabled'" name="remove-outline" color="medium"></ion-icon>
<h2>{{ health.key }}</h2>
<p style="margin-left: 24px;" *ngIf="health.value.result === 'failure'"><ion-text color="danger">{{ health.value.error }}</ion-text></p>
</div>
</ion-label>
</ion-item>
<ng-template #noHealth>
No health checks
</ng-template>
</ion-card-content>
</ion-card>
</ion-content>

View File

@@ -0,0 +1,3 @@
.metric-note {
font-size: 16px;
}

View File

@@ -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
}
}

View File

@@ -3,7 +3,7 @@
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Properties</ion-title>
<ion-title>Values</ion-title>
</ion-toolbar>
</ion-header>
@@ -28,15 +28,15 @@
</ion-item>
<!-- no properties -->
<ion-item *ngIf="(hasProperties$ | ngrxPush) === false">
<ion-item *ngIf="properties | empty">
<ion-label class="ion-text-wrap">
<p>No properties.</p>
<p>No values.</p>
</ion-label>
</ion-item>
<!-- properties -->
<ion-item-group *ngIf="(hasProperties$ | ngrxPush) === true">
<div *ngFor="let prop of properties$ | ngrxPush | keyvalue: asIsOrder">
<ion-item-group *ngIf="!(properties | empty)">
<div *ngFor="let prop of node | keyvalue: asIsOrder">
<!-- object -->
<ion-item button detail="true" *ngIf="prop.value.type === 'object'" (click)="goToNested(prop.key)">
<ion-button *ngIf="prop.value.description" class="help-button" fill="clear" slot="start" (click)="presentDescription(prop, $event)">

View File

@@ -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<PackageProperties>({ })
hasProperties$ = new BehaviorSubject<boolean>(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<void> {
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

View File

@@ -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),

View File

@@ -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,

View File

@@ -12,60 +12,60 @@
<ion-item-divider>Backups</ion-item-divider>
<ion-item [routerLink]="['backup']">
<ion-icon slot="start" name="save-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Create Backup</ion-text></ion-label>
<ion-icon slot="start" name="save-outline"></ion-icon>
<ion-label>Create Backup</ion-label>
</ion-item>
<ion-item-divider>Insights</ion-item-divider>
<ion-item-group>
<ion-item [routerLink]="['specs']">
<ion-icon slot="start" name="information-circle-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">About</ion-text></ion-label>
<ion-icon slot="start" name="information-circle-outline"></ion-icon>
<ion-label>About</ion-label>
</ion-item>
<ion-item [routerLink]="['metrics']">
<ion-icon slot="start" name="pulse" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Monitor</ion-text></ion-label>
<ion-icon slot="start" name="pulse"></ion-icon>
<ion-label>Monitor</ion-label>
</ion-item>
<ion-item [routerLink]="['logs']">
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Logs</ion-text></ion-label>
<ion-icon slot="start" name="newspaper-outline"></ion-icon>
<ion-label>Logs</ion-label>
</ion-item>
<ion-item-divider>Settings</ion-item-divider>
<ion-item lines="none" [routerLink]="['settings']">
<ion-icon slot="start" name="cog-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">General</ion-text></ion-label>
<ion-icon slot="start" name="cog-outline"></ion-icon>
<ion-label>General</ion-label>
</ion-item>
<ion-item [routerLink]="['lan']">
<ion-icon slot="start" name="home-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">LAN</ion-text></ion-label>
<ion-icon slot="start" name="home-outline"></ion-icon>
<ion-label>LAN</ion-label>
</ion-item>
<ion-item [routerLink]="['wifi']">
<ion-icon slot="start" name="wifi" color="primary"></ion-icon>
<ion-label><ion-text color="primary">WiFi</ion-text></ion-label>
<ion-icon slot="start" name="wifi"></ion-icon>
<ion-label>WiFi</ion-label>
</ion-item>
<ion-item lines="none" [routerLink]="['developer']">
<ion-icon slot="start" name="terminal-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Developer Options</ion-text></ion-label>
<ion-icon slot="start" name="terminal-outline"></ion-icon>
<ion-label>Developer Options</ion-label>
</ion-item>
<ion-item-divider>Power</ion-item-divider>
<ion-item button (click)="presentAlertRestart()">
<ion-icon slot="start" name="reload-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Restart</ion-text></ion-label>
<ion-icon slot="start" name="reload-outline"></ion-icon>
<ion-label>Restart</ion-label>
</ion-item>
<ion-item button lines="none" (click)="presentAlertShutdown()">
<ion-icon slot="start" name="power" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Shutdown</ion-text></ion-label>
<ion-icon slot="start" name="power"></ion-icon>
<ion-label>Shutdown</ion-label>
</ion-item>
</ion-item-group>
</ion-content>

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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<null>
@@ -219,7 +219,7 @@ export interface ActionResponse {
qr: boolean
}
export interface ServerMetrics {
export interface Metrics {
[key: string]: {
[key: string]: {
value: string | number | null

View File

@@ -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,

View File

@@ -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<RPCResponse<T>>(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
}

View File

@@ -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',

View File

@@ -10,7 +10,7 @@
},
"mocks": {
"enabled": true,
"connection": "poll",
"connection": "ws",
"rpcPort": "5959",
"wsPort": "5960",
"maskAs": "tor",