mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
health page, manifest page, watch unauth, begin redesign
This commit is contained in:
committed by
Aiden McClelland
parent
7013364ae8
commit
5d24d9324c
@@ -100,7 +100,7 @@ export class AppComponent {
|
||||
}
|
||||
})
|
||||
|
||||
this.http.watch401$().subscribe(() => {
|
||||
this.http.watchUnauth$().subscribe(() => {
|
||||
this.authService.setUnverified()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 { }
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.metric-note {
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 { }
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
.metric-note {
|
||||
font-size: 16px;
|
||||
}
|
||||
40
ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts
Normal file
40
ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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)">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"mocks": {
|
||||
"enabled": true,
|
||||
"connection": "poll",
|
||||
"connection": "ws",
|
||||
"rpcPort": "5959",
|
||||
"wsPort": "5960",
|
||||
"maskAs": "tor",
|
||||
|
||||
Reference in New Issue
Block a user