mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
refactor(app-show): refactor component (#961)
* refactor(app-show): refactor component * chore: remove precommit hook for the time being * chore: fix mutation by spreading Co-authored-by: Drew Ansbacher <drew.ansbacher@spiredigital.com>
This commit is contained in:
committed by
Aiden McClelland
parent
dbc159c82e
commit
5e681aa3fb
6
ui/.prettierrc
Normal file
6
ui/.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"arrowParens": "avoid",
|
||||
"trailingComma": "all"
|
||||
}
|
||||
@@ -94,10 +94,7 @@
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.html"
|
||||
]
|
||||
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
|
||||
}
|
||||
},
|
||||
"ionic-cordova-build": {
|
||||
|
||||
1356
ui/package-lock.json
generated
1356
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,13 @@
|
||||
"uuid": "^8.3.2",
|
||||
"zone.js": "^0.11.4"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts,html,md,less,json}": [
|
||||
"prettier --write",
|
||||
"git add"
|
||||
],
|
||||
"*.ts": "tslint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^12.2.4",
|
||||
"@angular/cli": "^12.2.4",
|
||||
@@ -49,7 +56,10 @@
|
||||
"@types/mustache": "^4.1.2",
|
||||
"@types/node": "^16.7.13",
|
||||
"@types/uuid": "^8.3.1",
|
||||
"husky": "^4.3.8",
|
||||
"lint-staged": "^12.1.2",
|
||||
"node-html-parser": "^4.1.4",
|
||||
"prettier": "^2.5.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"ts-node": "^10.2.0",
|
||||
"tslint": "^6.1.3",
|
||||
|
||||
@@ -8,9 +8,7 @@ import { HttpClientModule } from '@angular/common/http'
|
||||
import { AppComponent } from './app.component'
|
||||
import { AppRoutingModule } from './app-routing.module'
|
||||
import { ApiService } from './services/api/embassy-api.service'
|
||||
import { ApiServiceFactory } from './services/api/api.service.factory'
|
||||
import { PatchDbServiceFactory } from './services/patch-db/patch-db.factory'
|
||||
import { HttpService } from './services/http.service'
|
||||
import { ConfigService } from './services/config.service'
|
||||
import { QrCodeModule } from 'ng-qrcode'
|
||||
import { OSWelcomePageModule } from './modals/os-welcome/os-welcome.module'
|
||||
@@ -22,6 +20,10 @@ import { FormBuilder } from '@angular/forms'
|
||||
import { GenericInputComponentModule } from './modals/generic-input/generic-input.component.module'
|
||||
import { AuthService } from './services/auth.service'
|
||||
import { GlobalErrorHandler } from './services/global-error-handler.service'
|
||||
import { MockApiService } from './services/api/embassy-mock-api.service'
|
||||
import { LiveApiService } from './services/api/embassy-live-api.service'
|
||||
|
||||
const { mocks } = require('../../config.json')
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
@@ -54,8 +56,7 @@ import { GlobalErrorHandler } from './services/global-error-handler.service'
|
||||
},
|
||||
{
|
||||
provide: ApiService,
|
||||
useFactory: ApiServiceFactory,
|
||||
deps: [ConfigService, HttpService, LocalStorageBootstrap],
|
||||
useClass: mocks.enabled ? MockApiService : LiveApiService,
|
||||
},
|
||||
{
|
||||
provide: PatchDbService,
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
slot="end"
|
||||
fill="clear"
|
||||
color="primary"
|
||||
(click)="launchUi(pkg.entry)"
|
||||
(click)="launchUi()"
|
||||
[disabled]="!(pkg.entry.state | isLaunchable: status:manifest.interfaces)"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
|
||||
|
||||
@@ -1,47 +1,40 @@
|
||||
import { DOCUMENT } from "@angular/common";
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Inject,
|
||||
Input,
|
||||
} from "@angular/core";
|
||||
} from '@angular/core'
|
||||
import {
|
||||
PackageMainStatus,
|
||||
PackageDataEntry, Manifest,
|
||||
} from "src/app/services/patch-db/data-model";
|
||||
import { ConfigService } from "src/app/services/config.service";
|
||||
import { PkgInfo } from "src/app/util/get-package-info";
|
||||
PackageDataEntry,
|
||||
Manifest,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { PkgInfo } from 'src/app/util/get-package-info'
|
||||
import { UiLauncherService } from 'src/app/services/ui-launcher.service'
|
||||
|
||||
@Component({
|
||||
selector: "app-list-pkg",
|
||||
templateUrl: "app-list-pkg.component.html",
|
||||
selector: 'app-list-pkg',
|
||||
templateUrl: 'app-list-pkg.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppListPkgComponent {
|
||||
@Input()
|
||||
pkg: PkgInfo;
|
||||
pkg: PkgInfo
|
||||
|
||||
@Input()
|
||||
connectionFailure = false;
|
||||
connectionFailure = false
|
||||
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
private readonly config: ConfigService
|
||||
) {}
|
||||
constructor(private readonly launcherService: UiLauncherService) {}
|
||||
|
||||
get status(): PackageMainStatus {
|
||||
return this.pkg.entry.installed?.status.main.status;
|
||||
return this.pkg.entry.installed?.status.main.status
|
||||
}
|
||||
|
||||
get manifest(): Manifest {
|
||||
return this.pkg.entry.manifest;
|
||||
return this.pkg.entry.manifest
|
||||
}
|
||||
|
||||
launchUi(pkg: PackageDataEntry): void {
|
||||
this.document.defaultView.open(
|
||||
this.config.launchableURL(pkg),
|
||||
"_blank",
|
||||
"noreferrer"
|
||||
);
|
||||
launchUi(): void {
|
||||
this.launcherService.launch(this.pkg.entry)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,23 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppShowPage, HealthColorPipe } from './app-show.page'
|
||||
import { AppShowPage } from './app-show.page'
|
||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
|
||||
import { AppConfigPageModule } from 'src/app/modals/app-config/app-config.module'
|
||||
import { MarkdownPageModule } from 'src/app/modals/markdown/markdown.module'
|
||||
import { AppShowHeaderComponent } from './components/app-show-header/app-show-header.component'
|
||||
import { AppShowProgressComponent } from './components/app-show-progress/app-show-progress.component'
|
||||
import { AppShowStatusComponent } from './components/app-show-status/app-show-status.component'
|
||||
import { AppShowDependenciesComponent } from './components/app-show-dependencies/app-show-dependencies.component'
|
||||
import { AppShowMenuComponent } from './components/app-show-menu/app-show-menu.component'
|
||||
import { AppShowHealthChecksComponent } from './components/app-show-health-checks/app-show-health-checks.component'
|
||||
import { HealthColorPipe } from './pipes/health-color.pipe'
|
||||
import { ToHealthChecksPipe } from './pipes/to-health-checks.pipe'
|
||||
import { ToButtonsPipe } from './pipes/to-buttons.pipe'
|
||||
import { ToDependenciesPipe } from './pipes/to-dependencies.pipe'
|
||||
import { ToStatusPipe } from './pipes/to-status.pipe'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -20,6 +31,16 @@ const routes: Routes = [
|
||||
declarations: [
|
||||
AppShowPage,
|
||||
HealthColorPipe,
|
||||
ToHealthChecksPipe,
|
||||
ToButtonsPipe,
|
||||
ToDependenciesPipe,
|
||||
ToStatusPipe,
|
||||
AppShowHeaderComponent,
|
||||
AppShowProgressComponent,
|
||||
AppShowStatusComponent,
|
||||
AppShowDependenciesComponent,
|
||||
AppShowMenuComponent,
|
||||
AppShowHealthChecksComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -32,5 +53,4 @@ const routes: Routes = [
|
||||
MarkdownPageModule,
|
||||
],
|
||||
})
|
||||
export class AppShowPageModule { }
|
||||
|
||||
export class AppShowPageModule {}
|
||||
|
||||
@@ -1,158 +1,40 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="services"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-item lines="none" color="light">
|
||||
<ion-avatar slot="start">
|
||||
<img [src]="pkg['static-files'].icon" />
|
||||
</ion-avatar>
|
||||
<ion-label style="text-overflow: ellipsis;">
|
||||
<h1 style="font-family: 'Montserrat';" [class.less-large]="pkg.manifest.title.length > 20">
|
||||
{{ pkg.manifest.title }}
|
||||
</h1>
|
||||
<h2>{{ pkg.manifest.version | displayEmver }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ng-container *ngIf="pkg$ | async as pkg">
|
||||
<app-show-header [pkg]="pkg"></app-show-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-item-group>
|
||||
<!-- ** status ** -->
|
||||
<ion-item-divider>Status</ion-item-divider>
|
||||
<ion-item>
|
||||
<ion-label style="overflow: visible;">
|
||||
<status
|
||||
[disconnected]="connectionFailure"
|
||||
size="x-large"
|
||||
weight="500"
|
||||
[installProgress]="installProgress?.totalProgress"
|
||||
[rendering]="PR[statuses.primary]"
|
||||
[sigtermTimeout]="pkg.manifest.main['sigterm-timeout']"
|
||||
></status>
|
||||
</ion-label>
|
||||
<ng-container *ngIf="pkg.state === PackageState.Installed && !connectionFailure">
|
||||
<ion-button slot="end" class="action-button" *ngIf="pkg.manifest.interfaces | hasUi" [disabled]="!(pkg.state | isLaunchable : pkg.installed.status.main.status : pkg.manifest.interfaces)" (click)="launchUi()">
|
||||
Launch UI
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button slot="end" class="action-button" *ngIf="!pkg.installed.status.configured" (click)="presentModalConfig({ pkgId })">
|
||||
Configure
|
||||
</ion-button>
|
||||
<ion-button slot="end" class="action-button" *ngIf="statuses.primary === PS.Running" color="danger" (click)="stop()">
|
||||
Stop
|
||||
</ion-button>
|
||||
<ion-button slot="end" class="action-button" *ngIf="statuses.primary === PS.Stopped && pkg.installed.status.configured" color="success" (click)="tryStart()">
|
||||
Start
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
</ion-item>
|
||||
<!-- ** installed ** -->
|
||||
<ng-container *ngIf="pkg.state === PackageState.Installed">
|
||||
<!-- ** !backing-up ** -->
|
||||
<ng-container *ngIf="statuses.primary !== PS.BackingUp">
|
||||
<ion-content *ngIf="pkg | toDependencies | async as dependencies">
|
||||
<ion-item-group *ngIf="pkg | toStatus as status">
|
||||
<!-- ** status ** -->
|
||||
<app-show-status
|
||||
[pkg]="pkg"
|
||||
[connectionFailure]="connectionFailure$ | async"
|
||||
[dependencies]="dependencies"
|
||||
[status]="status"
|
||||
></app-show-status>
|
||||
<!-- ** installed && !backing-up ** -->
|
||||
<ng-container *ngIf="isInstalled(pkg, status)">
|
||||
<!-- ** health checks ** -->
|
||||
<ng-container *ngIf="statuses.primary === PS.Running && !(healthChecks | empty)">
|
||||
<ion-item-divider>Health Checks</ion-item-divider>
|
||||
<ng-container *ngIf="connectionFailure">
|
||||
<ion-item *ngFor="let health of healthChecks | keyvalue">
|
||||
<ion-avatar slot="start">
|
||||
<ion-skeleton-text style="width: 20px; height: 20px; border-radius: 0;"></ion-skeleton-text>
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<ion-skeleton-text style="width: 100px; margin-bottom: 10px;"></ion-skeleton-text>
|
||||
<ion-skeleton-text style="width: 150px; margin-bottom: 10px;"></ion-skeleton-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!connectionFailure">
|
||||
<ion-item button *ngFor="let health of healthChecks | keyvalue : asIsOrder" (click)="presentAlertDescription(health.key)">
|
||||
<ng-container *ngIf="health.value?.result as result; else noResult">
|
||||
<ion-spinner class="icon-spinner" color="primary" slot="start" *ngIf="[HealthResult.Starting, HealthResult.Loading] | includes : result"></ion-spinner>
|
||||
<ion-icon slot="start" *ngIf="result === HealthResult.Success" name="checkmark" color="success"></ion-icon>
|
||||
<ion-icon slot="start" *ngIf="result === HealthResult.Failure" name="warning-outline" color="warning"></ion-icon>
|
||||
<ion-icon slot="start" *ngIf="result === HealthResult.Disabled" name="remove" color="dark"></ion-icon>
|
||||
<ion-label>
|
||||
<h2 style="font-weight: bold;">{{ pkg.manifest['health-checks'][health.key].name }}</h2>
|
||||
<ion-text [color]="result | healthColor">
|
||||
<p>
|
||||
<span *ngIf="!([HealthResult.Failure, HealthResult.Loading] | includes : result)">{{ result | titlecase }}</span>
|
||||
<span *ngIf="result === HealthResult.Starting">...</span>
|
||||
<span *ngIf="result === HealthResult.Failure">{{ $any(health.value).error }}</span>
|
||||
<span *ngIf="result === HealthResult.Loading">{{ $any(health.value).message }}</span>
|
||||
</p>
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ng-container>
|
||||
<ng-template #noResult>
|
||||
<ion-spinner class="icon-spinner" color="dark" slot="start"></ion-spinner>
|
||||
<ion-label>
|
||||
<h2 style="font-weight: bold;">{{ health.key }}</h2>
|
||||
<p>Awaiting result...</p>
|
||||
</ion-label>
|
||||
</ng-template>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<app-show-health-checks
|
||||
*ngIf="isRunning(status)"
|
||||
[pkg]="pkg"
|
||||
[connectionFailure]="connectionFailure$ | async"
|
||||
></app-show-health-checks>
|
||||
<!-- ** dependencies ** -->
|
||||
<ng-container *ngIf="dependencies.length">
|
||||
<ion-item-divider>Dependencies</ion-item-divider>
|
||||
<!-- dependencies are a subset of the pkg.manifest.dependencies that are currently required as determined by the service config -->
|
||||
<ion-item button *ngFor="let dep of dependencies" (click)="dep.action()">
|
||||
<ion-thumbnail slot="start">
|
||||
<img [src]="dep.icon" />
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2 class="inline" style="font-family: 'Montserrat'">
|
||||
<ion-icon style="padding-right: 4px;" *ngIf="!!dep.errorText" slot="start" name="warning-outline" color="warning"></ion-icon>
|
||||
{{ dep.title }}
|
||||
</h2>
|
||||
<p>{{ dep.version | displayEmver }}</p>
|
||||
<p>
|
||||
<ion-text [color]="!!dep.errorText ? 'warning' : 'success'">{{ dep.errorText || 'satisfied' }}</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-button *ngIf="dep.actionText" slot="end" fill="clear">
|
||||
{{ dep.actionText }}
|
||||
<ion-icon slot="end" name="arrow-forward"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<app-show-dependencies
|
||||
*ngIf="dependencies.length"
|
||||
[dependencies]="dependencies"
|
||||
></app-show-dependencies>
|
||||
<!-- ** menu ** -->
|
||||
<ion-item-divider>Menu</ion-item-divider>
|
||||
<ion-item button detail *ngFor="let button of buttons" (click)="button.action()">
|
||||
<ion-icon slot="start" [name]="button.icon"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ button.title }}</h2>
|
||||
<p *ngIf="button.description">{{ button.description }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- ** installing, updating, restoring ** -->
|
||||
<ng-container *ngIf="showProgress(pkg)">
|
||||
<app-show-progress
|
||||
*ngIf="pkg['install-progress'] | installState as installProgress"
|
||||
[pkg]="pkg"
|
||||
[installProgress]="installProgress"
|
||||
></app-show-progress>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- ** installing, updating, restoring ** -->
|
||||
<div *ngIf="([PackageState.Installing, PackageState.Updating, PackageState.Restoring] | includes : pkg.state) && installProgress" style="padding: 16px;">
|
||||
<p>Downloading: {{ installProgress.downloadProgress }}%</p>
|
||||
<ion-progress-bar
|
||||
[color]="pkg['install-progress']['download-complete'] ? 'success' : 'secondary'"
|
||||
[value]="installProgress.downloadProgress / 100"
|
||||
[buffer]="!installProgress.downloadProgress ? 0 : 1"
|
||||
></ion-progress-bar>
|
||||
|
||||
<p>Validating: {{ installProgress.validateProgress }}%</p>
|
||||
<ion-progress-bar
|
||||
[color]="pkg['install-progress']['validation-complete'] ? 'success' : 'secondary'"
|
||||
[value]="installProgress.validateProgress / 100"
|
||||
[buffer]="installProgress.downloadProgress === 100 && !installProgress.validateProgress ? 0 : 1"
|
||||
></ion-progress-bar>
|
||||
|
||||
<p>Unpacking: {{ installProgress.unpackProgress }}%</p>
|
||||
<ion-progress-bar
|
||||
[color]="pkg['install-progress']['unpack-complete'] ? 'success' : 'secondary'"
|
||||
[value]="installProgress.unpackProgress / 100"
|
||||
[buffer]="installProgress.validateProgress === 100 && !installProgress.unpackProgress ? 0 : 1"
|
||||
></ion-progress-bar>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-content>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
.less-large {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin: 10px;
|
||||
min-height: 36px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.icon-spinner {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
@@ -1,469 +1,73 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { AlertController, NavController, ModalController, IonContent, LoadingController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ActivatedRoute, NavigationExtras } from '@angular/router'
|
||||
import { DependentInfo, exists, isEmptyObject } from 'src/app/util/misc.util'
|
||||
import { combineLatest, 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 { ConfigService } from 'src/app/services/config.service'
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { DependencyError, DependencyErrorType, HealthCheckResult, HealthResult, PackageDataEntry, PackageMainStatus, PackageState } from 'src/app/services/patch-db/data-model'
|
||||
import { DependencyStatus, HealthStatus, PrimaryRendering, PrimaryStatus, renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { ConnectionFailure, ConnectionService } from 'src/app/services/connection.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { AppConfigPage } from 'src/app/modals/app-config/app-config.page'
|
||||
import { filter } from 'rxjs/operators'
|
||||
import { MarkdownPage } from 'src/app/modals/markdown/markdown.page'
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { packageLoadingProgress, ProgressData } from 'src/app/util/package-loading-progress'
|
||||
import {
|
||||
PackageDataEntry,
|
||||
PackageState,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
PackageStatus,
|
||||
PrimaryStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
import {
|
||||
ConnectionFailure,
|
||||
ConnectionService,
|
||||
} from 'src/app/services/connection.service'
|
||||
import { map, startWith } from 'rxjs/operators'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
|
||||
const STATES = [
|
||||
PackageState.Installing,
|
||||
PackageState.Updating,
|
||||
PackageState.Restoring,
|
||||
]
|
||||
|
||||
@Component({
|
||||
selector: 'app-show',
|
||||
templateUrl: './app-show.page.html',
|
||||
styleUrls: ['./app-show.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowPage {
|
||||
PackageState = PackageState
|
||||
DependencyErrorType = DependencyErrorType
|
||||
Math = Math
|
||||
HealthResult = HealthResult
|
||||
PS = PrimaryStatus
|
||||
DS = DependencyStatus
|
||||
PR = PrimaryRendering
|
||||
private readonly pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
|
||||
pkgId: string
|
||||
pkg: PackageDataEntry
|
||||
hideLAN: boolean
|
||||
buttons: Button[] = []
|
||||
dependencies: DependencyInfo[] = []
|
||||
statuses: {
|
||||
primary: PrimaryStatus
|
||||
dependency: DependencyStatus
|
||||
health: HealthStatus
|
||||
} = { } as any
|
||||
connectionFailure: boolean
|
||||
loading = true
|
||||
healthChecks: { [id: string]: HealthCheckResult | null }
|
||||
installProgress: ProgressData
|
||||
readonly pkg$ = this.patch.watch$('package-data', this.pkgId).pipe(
|
||||
map(pkg => {
|
||||
// if package disappears, navigate to list page
|
||||
if (!pkg) {
|
||||
this.navCtrl.navigateRoot('/services')
|
||||
}
|
||||
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
subs: Subscription[] = []
|
||||
return { ...pkg }
|
||||
}),
|
||||
startWith(this.patch.getData()['package-data'][this.pkgId]),
|
||||
)
|
||||
|
||||
constructor (
|
||||
private readonly alertCtrl: AlertController,
|
||||
readonly connectionFailure$ = this.connectionService
|
||||
.watchFailure$()
|
||||
.pipe(map(failure => failure !== ConnectionFailure.None))
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly config: ConfigService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly connectionService: ConnectionService,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
async ngOnInit () {
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
this.pkg = this.patch.getData()['package-data'][this.pkgId]
|
||||
this.statuses = renderPkgStatus(this.pkg)
|
||||
this.healthChecks = Object.keys(this.pkg.manifest['health-checks']).reduce((obj, key) => {
|
||||
obj[key] = null
|
||||
return obj
|
||||
}, { })
|
||||
|
||||
this.subs = [
|
||||
// 1
|
||||
this.patch.watch$('package-data', this.pkgId)
|
||||
.subscribe(pkg => {
|
||||
// if package disappears, navigate to list page
|
||||
if (!pkg) {
|
||||
this.navCtrl.navigateRoot('/services')
|
||||
return
|
||||
}
|
||||
|
||||
this.pkg = pkg
|
||||
this.statuses = renderPkgStatus(pkg)
|
||||
this.installProgress = !isEmptyObject(pkg['install-progress']) ? packageLoadingProgress(pkg['install-progress']) : undefined
|
||||
}),
|
||||
|
||||
// 2
|
||||
combineLatest([
|
||||
this.patch.watch$('package-data', this.pkgId, 'installed', 'current-dependencies'),
|
||||
this.patch.watch$('package-data', this.pkgId, 'installed', 'status', 'dependency-errors'),
|
||||
])
|
||||
.pipe(
|
||||
filter(([currentDeps, depErrors]) => exists(currentDeps) && exists(depErrors)),
|
||||
)
|
||||
.subscribe(([currentDeps, depErrors]) => {
|
||||
this.dependencies = Object.keys(currentDeps)
|
||||
.filter(id => !!this.pkg.manifest.dependencies[id])
|
||||
.map(id => {
|
||||
return this.setDepValues(id, depErrors)
|
||||
})
|
||||
}),
|
||||
|
||||
// 3
|
||||
this.patch.watch$('package-data', this.pkgId, 'installed', 'status', 'main')
|
||||
.pipe(
|
||||
filter(obj => exists(obj)),
|
||||
)
|
||||
.subscribe(main => {
|
||||
if (main.status === PackageMainStatus.Running) {
|
||||
Object.keys(this.healthChecks).forEach(key => {
|
||||
this.healthChecks[key] = main.health[key]
|
||||
})
|
||||
} else {
|
||||
Object.keys(this.healthChecks).forEach(key => {
|
||||
this.healthChecks[key] = null
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
||||
// 4
|
||||
this.connectionService.watchFailure$()
|
||||
.subscribe(connectionFailure => {
|
||||
this.connectionFailure = connectionFailure !== ConnectionFailure.None
|
||||
}),
|
||||
]
|
||||
this.setButtons()
|
||||
isInstalled(
|
||||
{ state }: PackageDataEntry,
|
||||
{ primary }: PackageStatus,
|
||||
): boolean {
|
||||
return (
|
||||
state === PackageState.Installed && primary !== PrimaryStatus.BackingUp
|
||||
)
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
isRunning({ primary }: PackageStatus): boolean {
|
||||
return primary === PrimaryStatus.Running
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.subs.forEach(sub => sub.unsubscribe())
|
||||
}
|
||||
|
||||
launchUi (): void {
|
||||
window.open(this.config.launchableURL(this.pkg), '_blank', 'noreferrer')
|
||||
}
|
||||
|
||||
async stop (): Promise<void> {
|
||||
const { id, title, version } = this.pkg.manifest
|
||||
|
||||
if (isEmptyObject(this.pkg.installed['current-dependents'])) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: `Stopping...`,
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.stopPackage({ id })
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
} else {
|
||||
wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizardBaker.stop({
|
||||
id,
|
||||
title,
|
||||
version,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async tryStart (): Promise<void> {
|
||||
if (this.dependencies.some(d => !!d.errorText)) {
|
||||
const depErrMsg = `${this.pkg.manifest.title} has unmet dependencies. It will not work as expected.`
|
||||
const proceed = await this.presentAlertStart(depErrMsg)
|
||||
if (!proceed) return
|
||||
}
|
||||
|
||||
const alertMsg = this.pkg.manifest.alerts.start
|
||||
if (!!alertMsg) {
|
||||
const proceed = await this.presentAlertStart(alertMsg)
|
||||
if (!proceed) return
|
||||
}
|
||||
|
||||
this.start()
|
||||
}
|
||||
|
||||
async donate (): Promise<void> {
|
||||
const url = this.pkg.manifest['donation-url']
|
||||
if (url) {
|
||||
window.open(url, '_blank', 'noreferrer')
|
||||
} else {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Not Accepting Donations',
|
||||
message: `The developers of ${this.pkg.manifest.title} have not provided a donation URL. Please contact them directly if you insist on giving them money.`,
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
}
|
||||
|
||||
async fixDep (action: 'install' | 'update' | 'configure', id: string): Promise<void> {
|
||||
switch (action) {
|
||||
case 'install':
|
||||
case 'update':
|
||||
return this.installDep(id)
|
||||
case 'configure':
|
||||
return this.configureDep(id)
|
||||
}
|
||||
}
|
||||
|
||||
async presentModalConfig (props: { pkgId: string, dependentInfo?: DependentInfo }): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: AppConfigPage,
|
||||
componentProps: props,
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async presentModalInstructions () {
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
title: 'Instructions',
|
||||
contentUrl: this.pkg['static-files']['instructions'],
|
||||
},
|
||||
component: MarkdownPage,
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async presentAlertDescription (id: string) {
|
||||
const health = this.pkg.manifest['health-checks'][id]
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Health Check',
|
||||
subHeader: health.name,
|
||||
message: health.description,
|
||||
buttons: [
|
||||
{
|
||||
text: `OK`,
|
||||
handler: () => {
|
||||
alert.dismiss()
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private setDepValues (id: string, errors: { [id: string]: DependencyError }): DependencyInfo {
|
||||
let errorText = ''
|
||||
let actionText = 'View'
|
||||
let action: () => any = () => this.navCtrl.navigateForward(`/services/${id}`)
|
||||
|
||||
const error = errors[id]
|
||||
|
||||
if (error) {
|
||||
// health checks failed
|
||||
if ([DependencyErrorType.InterfaceHealthChecksFailed, DependencyErrorType.HealthChecksFailed].includes(error.type)) {
|
||||
errorText = 'Health check failed'
|
||||
// not installed
|
||||
} else if (error.type === DependencyErrorType.NotInstalled) {
|
||||
errorText = 'Not installed'
|
||||
actionText = 'Install'
|
||||
action = () => this.fixDep('install', id)
|
||||
// incorrect version
|
||||
} else if (error.type === DependencyErrorType.IncorrectVersion) {
|
||||
errorText = 'Incorrect version'
|
||||
actionText = 'Update'
|
||||
action = () => this.fixDep('update', id)
|
||||
// not running
|
||||
} else if (error.type === DependencyErrorType.NotRunning) {
|
||||
errorText = 'Not running'
|
||||
actionText = 'Start'
|
||||
// config unsatisfied
|
||||
} else if (error.type === DependencyErrorType.ConfigUnsatisfied) {
|
||||
errorText = 'Config not satisfied'
|
||||
actionText = 'Auto config'
|
||||
action = () => this.fixDep('configure', id)
|
||||
} else if (error.type === DependencyErrorType.Transitive) {
|
||||
errorText = 'Dependency has a dependency issue'
|
||||
}
|
||||
errorText = `${errorText}. ${ this.pkg.manifest.title} will not work as expected.`
|
||||
}
|
||||
|
||||
const depInfo = this.pkg.installed['dependency-info'][id]
|
||||
|
||||
return {
|
||||
id,
|
||||
version: this.pkg.manifest.dependencies[id].version,
|
||||
title: depInfo.manifest.title,
|
||||
icon: depInfo.icon,
|
||||
errorText,
|
||||
actionText,
|
||||
action,
|
||||
}
|
||||
}
|
||||
|
||||
private async installDep (depId: string): Promise<void> {
|
||||
const version = this.pkg.manifest.dependencies[depId].version
|
||||
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: this.pkgId,
|
||||
title: this.pkg.manifest.title,
|
||||
version,
|
||||
}
|
||||
const navigationExtras: NavigationExtras = {
|
||||
state: { dependentInfo },
|
||||
}
|
||||
|
||||
await this.navCtrl.navigateForward(`/marketplace/${depId}`, navigationExtras)
|
||||
}
|
||||
|
||||
private async configureDep (dependencyId: string): Promise<void> {
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: this.pkgId,
|
||||
title: this.pkg.manifest.title,
|
||||
}
|
||||
|
||||
await this.presentModalConfig({
|
||||
pkgId: dependencyId,
|
||||
dependentInfo,
|
||||
})
|
||||
}
|
||||
|
||||
private async presentAlertStart (message: string): Promise<boolean> {
|
||||
return new Promise(async resolve => {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
handler: () => {
|
||||
resolve(false)
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Continue',
|
||||
handler: () => {
|
||||
resolve(true)
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
})
|
||||
}
|
||||
|
||||
private async start (): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: `Starting...`,
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.startPackage({ id: this.pkgId })
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private setButtons (): void {
|
||||
const pkgTitle = this.pkg.manifest.title
|
||||
|
||||
this.buttons = [
|
||||
// instructions
|
||||
{
|
||||
action: () => this.presentModalInstructions(),
|
||||
title: 'Instructions',
|
||||
description: `Understand how to use ${pkgTitle}`,
|
||||
icon: 'list-outline',
|
||||
},
|
||||
// config
|
||||
{
|
||||
action: async () => this.presentModalConfig({ pkgId: this.pkgId }),
|
||||
title: 'Config',
|
||||
description: `Customize ${pkgTitle}`,
|
||||
icon: 'construct-outline',
|
||||
},
|
||||
// properties
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['properties'], { relativeTo: this.route }),
|
||||
title: 'Properties',
|
||||
description: 'Runtime information, credentials, and other values of interest',
|
||||
icon: 'briefcase-outline',
|
||||
},
|
||||
// actions
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }),
|
||||
title: 'Actions',
|
||||
description: `Uninstall and other commands specific to ${pkgTitle}`,
|
||||
icon: 'flash-outline',
|
||||
},
|
||||
// interfaces
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['interfaces'], { relativeTo: this.route }),
|
||||
title: 'Interfaces',
|
||||
description: 'User and machine access points',
|
||||
icon: 'desktop-outline',
|
||||
},
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }),
|
||||
title: 'Logs',
|
||||
description: 'Raw, unfiltered service logs',
|
||||
icon: 'receipt-outline',
|
||||
},
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward([`marketplace/${this.pkgId}`]),
|
||||
title: 'Marketplace',
|
||||
description: 'View service in marketplace',
|
||||
icon: 'storefront-outline',
|
||||
},
|
||||
{
|
||||
action: () => this.donate(),
|
||||
title: 'Donate',
|
||||
description: `Support ${pkgTitle}`,
|
||||
icon: 'logo-bitcoin',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
asIsOrder () {
|
||||
return 0
|
||||
showProgress({ state }: PackageDataEntry): boolean {
|
||||
return STATES.includes(state)
|
||||
}
|
||||
}
|
||||
|
||||
interface DependencyInfo {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
version: string
|
||||
errorText: string
|
||||
actionText: string
|
||||
action: () => any
|
||||
}
|
||||
|
||||
interface Button {
|
||||
title: string
|
||||
description: string
|
||||
icon: string
|
||||
action: Function
|
||||
}
|
||||
|
||||
|
||||
@Pipe({
|
||||
name: 'healthColor',
|
||||
})
|
||||
export class HealthColorPipe implements PipeTransform {
|
||||
transform (val: HealthResult): string {
|
||||
switch (val) {
|
||||
case HealthResult.Success: return 'success'
|
||||
case HealthResult.Failure: return 'warning'
|
||||
case HealthResult.Disabled: return 'dark'
|
||||
case HealthResult.Starting:
|
||||
case HealthResult.Loading: return 'primary'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<ion-item-divider>Dependencies</ion-item-divider>
|
||||
<!-- dependencies are a subset of the pkg.manifest.dependencies that are currently required as determined by the service config -->
|
||||
<ion-item button *ngFor="let dep of dependencies" (click)="dep.action()">
|
||||
<ion-thumbnail slot="start">
|
||||
<img [src]="dep.icon" alt="" />
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2 class="inline">
|
||||
<ion-icon
|
||||
*ngIf="!!dep.errorText"
|
||||
class="icon"
|
||||
slot="start"
|
||||
name="warning-outline"
|
||||
color="warning"
|
||||
></ion-icon>
|
||||
{{ dep.title }}
|
||||
</h2>
|
||||
<p>{{ dep.version | displayEmver }}</p>
|
||||
<p>
|
||||
<ion-text [color]="dep.errorText ? 'warning' : 'success'">
|
||||
{{ dep.errorText || 'satisfied' }}
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-button *ngIf="dep.actionText" slot="end" fill="clear">
|
||||
{{ dep.actionText }}
|
||||
<ion-icon slot="end" name="arrow-forward"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
@@ -0,0 +1,7 @@
|
||||
.inline {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding-right: 4px;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { DependencyInfo } from '../../pipes/to-dependencies.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-dependencies',
|
||||
templateUrl: './app-show-dependencies.component.html',
|
||||
styleUrls: ['./app-show-dependencies.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowDependenciesComponent {
|
||||
@Input()
|
||||
dependencies: DependencyInfo[] = []
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="services"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-item lines="none" color="light">
|
||||
<ion-avatar slot="start">
|
||||
<img [src]="pkg['static-files'].icon" alt="" />
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<h1 class="name" [class.less-large]="pkg.manifest.title.length > 20">
|
||||
{{ pkg.manifest.title }}
|
||||
</h1>
|
||||
<h2>{{ pkg.manifest.version | displayEmver }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
@@ -0,0 +1,7 @@
|
||||
.name {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
|
||||
.less-large {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-header',
|
||||
templateUrl: './app-show-header.component.html',
|
||||
styleUrls: ['./app-show-header.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowHeaderComponent {
|
||||
@Input()
|
||||
pkg: PackageDataEntry
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<ng-container
|
||||
*ngIf="pkg | toHealthChecks | async | keyvalue: asIsOrder as checks"
|
||||
>
|
||||
<ion-item-divider>Health Checks</ion-item-divider>
|
||||
<ng-container *ngIf="connectionFailure; else connected">
|
||||
<ion-item *ngFor="let health of checks">
|
||||
<ion-avatar slot="start">
|
||||
<ion-skeleton-text class="avatar"></ion-skeleton-text>
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<ion-skeleton-text class="label"></ion-skeleton-text>
|
||||
<ion-skeleton-text class="description"></ion-skeleton-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ng-template #connected>
|
||||
<ion-item
|
||||
*ngFor="let health of checks"
|
||||
button
|
||||
(click)="presentAlertDescription(health.key)"
|
||||
>
|
||||
<ng-container *ngIf="health.value?.result as result; else noResult">
|
||||
<ion-spinner
|
||||
*ngIf="isLoading(result)"
|
||||
class="icon-spinner"
|
||||
color="primary"
|
||||
slot="start"
|
||||
></ion-spinner>
|
||||
<ion-icon
|
||||
*ngIf="result === HealthResult.Success"
|
||||
slot="start"
|
||||
name="checkmark"
|
||||
color="success"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="result === HealthResult.Failure"
|
||||
slot="start"
|
||||
name="warning-outline"
|
||||
color="warning"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="result === HealthResult.Disabled"
|
||||
slot="start"
|
||||
name="remove"
|
||||
color="dark"
|
||||
></ion-icon>
|
||||
<ion-label>
|
||||
<h2 class="bold">
|
||||
{{ pkg.manifest['health-checks'][health.key].name }}
|
||||
</h2>
|
||||
<ion-text [color]="result | healthColor">
|
||||
<p>
|
||||
<span *ngIf="isReady(result)">{{ result | titlecase }}</span>
|
||||
<span *ngIf="result === HealthResult.Starting">...</span>
|
||||
<span *ngIf="result === HealthResult.Failure">
|
||||
{{ $any(health.value).error }}
|
||||
</span>
|
||||
<span *ngIf="result === HealthResult.Loading">
|
||||
{{ $any(health.value).message }}
|
||||
</span>
|
||||
</p>
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ng-container>
|
||||
<ng-template #noResult>
|
||||
<ion-spinner
|
||||
class="icon-spinner"
|
||||
color="dark"
|
||||
slot="start"
|
||||
></ion-spinner>
|
||||
<ion-label>
|
||||
<h2 class="bold">{{ health.key }}</h2>
|
||||
<p>Awaiting result...</p>
|
||||
</ion-label>
|
||||
</ng-template>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,24 @@
|
||||
.icon-spinner {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 100px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.description {
|
||||
width: 150px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
import {
|
||||
HealthResult,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-health-checks',
|
||||
templateUrl: './app-show-health-checks.component.html',
|
||||
styleUrls: ['./app-show-health-checks.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowHealthChecksComponent {
|
||||
@Input()
|
||||
pkg: PackageDataEntry
|
||||
|
||||
@Input()
|
||||
connectionFailure = false
|
||||
|
||||
HealthResult = HealthResult
|
||||
|
||||
constructor(private readonly alertCtrl: AlertController) {}
|
||||
|
||||
isLoading(result: HealthResult): boolean {
|
||||
return result === HealthResult.Starting || result === HealthResult.Loading
|
||||
}
|
||||
|
||||
isReady(result: HealthResult): boolean {
|
||||
return result !== HealthResult.Failure && result !== HealthResult.Loading
|
||||
}
|
||||
|
||||
async presentAlertDescription(id: string) {
|
||||
const health = this.pkg.manifest['health-checks'][id]
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Health Check',
|
||||
subHeader: health.name,
|
||||
message: health.description,
|
||||
buttons: [
|
||||
{
|
||||
text: `OK`,
|
||||
handler: () => {
|
||||
alert.dismiss()
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
asIsOrder() {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<ion-item-divider>Menu</ion-item-divider>
|
||||
<ion-item
|
||||
*ngFor="let button of buttons"
|
||||
button
|
||||
detail
|
||||
(click)="button.action()"
|
||||
>
|
||||
<ion-icon slot="start" [name]="button.icon"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ button.title }}</h2>
|
||||
<p *ngIf="button.description">{{ button.description }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { Button } from '../../pipes/to-buttons.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-menu',
|
||||
templateUrl: './app-show-menu.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowMenuComponent {
|
||||
@Input()
|
||||
buttons: Button[] = []
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<p>Downloading: {{ installProgress.downloadProgress }}%</p>
|
||||
<ion-progress-bar
|
||||
[color]="getColor('download-complete')"
|
||||
[value]="installProgress.downloadProgress / 100"
|
||||
[buffer]="!installProgress.downloadProgress ? 0 : 1"
|
||||
></ion-progress-bar>
|
||||
|
||||
<p>Validating: {{ installProgress.validateProgress }}%</p>
|
||||
<ion-progress-bar
|
||||
[color]="getColor('validation-complete')"
|
||||
[value]="installProgress.validateProgress / 100"
|
||||
[buffer]="validationBuffer"
|
||||
></ion-progress-bar>
|
||||
|
||||
<p>Unpacking: {{ installProgress.unpackProgress }}%</p>
|
||||
<ion-progress-bar
|
||||
[color]="getColor('unpack-complete')"
|
||||
[value]="installProgress.unpackProgress / 100"
|
||||
[buffer]="unpackingBuffer"
|
||||
></ion-progress-bar>
|
||||
@@ -0,0 +1,4 @@
|
||||
:host {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import {
|
||||
InstallProgress,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { ProgressData } from 'src/app/util/package-loading-progress'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-progress',
|
||||
templateUrl: './app-show-progress.component.html',
|
||||
styleUrls: ['./app-show-progress.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowProgressComponent {
|
||||
@Input()
|
||||
pkg: PackageDataEntry
|
||||
|
||||
@Input()
|
||||
installProgress: ProgressData
|
||||
|
||||
get unpackingBuffer(): number {
|
||||
return this.installProgress.validateProgress === 100 &&
|
||||
!this.installProgress.unpackProgress
|
||||
? 0
|
||||
: 1
|
||||
}
|
||||
|
||||
get validationBuffer(): number {
|
||||
return this.installProgress.downloadProgress === 100 &&
|
||||
!this.installProgress.validateProgress
|
||||
? 0
|
||||
: 1
|
||||
}
|
||||
|
||||
getColor(action: keyof InstallProgress): string {
|
||||
return this.pkg['install-progress'][action] ? 'success' : 'secondary'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<ion-item-divider>Status</ion-item-divider>
|
||||
<ion-item>
|
||||
<ion-label class="label">
|
||||
<status
|
||||
size="x-large"
|
||||
weight="500"
|
||||
[disconnected]="connectionFailure"
|
||||
[installProgress]="(progress | installState)?.totalProgress"
|
||||
[rendering]="PR[status.primary]"
|
||||
[sigtermTimeout]="pkg.manifest.main['sigterm-timeout']"
|
||||
></status>
|
||||
</ion-label>
|
||||
<ng-container *ngIf="isInstalled">
|
||||
<ion-button
|
||||
*ngIf="interfaces | hasUi"
|
||||
slot="end"
|
||||
class="action-button"
|
||||
[disabled]="!(pkg.state | isLaunchable: pkgStatus.main.status:interfaces)"
|
||||
(click)="launchUi()"
|
||||
>
|
||||
Launch UI
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button
|
||||
*ngIf="!pkgStatus.configured"
|
||||
slot="end"
|
||||
class="action-button"
|
||||
(click)="presentModalConfig()"
|
||||
>
|
||||
Configure
|
||||
</ion-button>
|
||||
<ion-button
|
||||
*ngIf="isRunning"
|
||||
slot="end"
|
||||
class="action-button"
|
||||
color="danger"
|
||||
(click)="stop()"
|
||||
>
|
||||
Stop
|
||||
</ion-button>
|
||||
<ion-button
|
||||
*ngIf="isStopped"
|
||||
slot="end"
|
||||
class="action-button"
|
||||
color="success"
|
||||
(click)="tryStart()"
|
||||
>
|
||||
Start
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
</ion-item>
|
||||
@@ -0,0 +1,9 @@
|
||||
.label {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin: 10px;
|
||||
min-height: 36px;
|
||||
min-width: 120px;
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { UiLauncherService } from 'src/app/services/ui-launcher.service'
|
||||
import {
|
||||
InstallProgress,
|
||||
InterfaceDef,
|
||||
PackageDataEntry,
|
||||
PackageState,
|
||||
Status,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
PackageStatus,
|
||||
PrimaryRendering,
|
||||
PrimaryStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
import { isEmptyObject } from 'src/app/util/misc.util'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import {
|
||||
AlertController,
|
||||
LoadingController,
|
||||
ModalController,
|
||||
} from '@ionic/angular'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ModalService } from 'src/app/services/modal.service'
|
||||
import { DependencyInfo } from '../../pipes/to-dependencies.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-status',
|
||||
templateUrl: './app-show-status.component.html',
|
||||
styleUrls: ['./app-show-status.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowStatusComponent {
|
||||
@Input()
|
||||
pkg: PackageDataEntry
|
||||
|
||||
@Input()
|
||||
connectionFailure = false
|
||||
|
||||
@Input()
|
||||
status: PackageStatus
|
||||
|
||||
@Input()
|
||||
dependencies: DependencyInfo[] = []
|
||||
|
||||
PR = PrimaryRendering
|
||||
|
||||
constructor(
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly launcherService: UiLauncherService,
|
||||
private readonly modalService: ModalService,
|
||||
) {}
|
||||
|
||||
get interfaces(): Record<string, InterfaceDef> {
|
||||
return this.pkg.manifest.interfaces
|
||||
}
|
||||
|
||||
get progress(): InstallProgress | undefined {
|
||||
return this.pkg['install-progress']
|
||||
}
|
||||
|
||||
get pkgStatus(): Status {
|
||||
return this.pkg.installed.status
|
||||
}
|
||||
|
||||
get isInstalled(): boolean {
|
||||
return this.pkg.state === PackageState.Installed && !this.connectionFailure
|
||||
}
|
||||
|
||||
get isRunning(): boolean {
|
||||
return this.status.primary === PrimaryStatus.Running
|
||||
}
|
||||
|
||||
get isStopped(): boolean {
|
||||
return (
|
||||
this.status.primary === PrimaryStatus.Stopped && this.pkgStatus.configured
|
||||
)
|
||||
}
|
||||
|
||||
launchUi(): void {
|
||||
this.launcherService.launch(this.pkg)
|
||||
}
|
||||
|
||||
async presentModalConfig(): Promise<void> {
|
||||
return this.modalService.presentModalConfig({ pkgId: this.pkg.manifest.id })
|
||||
}
|
||||
|
||||
async tryStart(): Promise<void> {
|
||||
if (this.dependencies.some(d => !!d.errorText)) {
|
||||
const depErrMsg = `${this.pkg.manifest.title} has unmet dependencies. It will not work as expected.`
|
||||
const proceed = await this.presentAlertStart(depErrMsg)
|
||||
|
||||
if (!proceed) return
|
||||
}
|
||||
|
||||
const alertMsg = this.pkg.manifest.alerts.start
|
||||
|
||||
if (!!alertMsg) {
|
||||
const proceed = await this.presentAlertStart(alertMsg)
|
||||
|
||||
if (!proceed) return
|
||||
}
|
||||
|
||||
this.start()
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
const { id, title, version } = this.pkg.manifest
|
||||
|
||||
if (isEmptyObject(this.pkg.installed['current-dependents'])) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: `Stopping...`,
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.stopPackage({ id })
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
} else {
|
||||
wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizardBaker.stop({
|
||||
id,
|
||||
title,
|
||||
version,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private async start(): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: `Starting...`,
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.startPackage({ id: this.pkg.manifest.id })
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async presentAlertStart(message: string): Promise<boolean> {
|
||||
return new Promise(async resolve => {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
handler: () => {
|
||||
resolve(false)
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Continue',
|
||||
handler: () => {
|
||||
resolve(true)
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await alert.present()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { HealthResult } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Pipe({
|
||||
name: 'healthColor',
|
||||
})
|
||||
export class HealthColorPipe implements PipeTransform {
|
||||
transform(val: HealthResult): string {
|
||||
switch (val) {
|
||||
case HealthResult.Success:
|
||||
return 'success'
|
||||
case HealthResult.Failure:
|
||||
return 'warning'
|
||||
case HealthResult.Disabled:
|
||||
return 'dark'
|
||||
case HealthResult.Starting:
|
||||
case HealthResult.Loading:
|
||||
return 'primary'
|
||||
}
|
||||
}
|
||||
}
|
||||
125
ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts
Normal file
125
ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Inject, Pipe, PipeTransform } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { AlertController, ModalController, NavController } from '@ionic/angular'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { MarkdownPage } from 'src/app/modals/markdown/markdown.page'
|
||||
import { ModalService } from 'src/app/services/modal.service'
|
||||
|
||||
export interface Button {
|
||||
title: string
|
||||
description: string
|
||||
icon: string
|
||||
action: Function
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'toButtons',
|
||||
})
|
||||
export class ToButtonsPipe implements PipeTransform {
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly modalService: ModalService,
|
||||
) {}
|
||||
|
||||
transform(pkg: PackageDataEntry): Button[] {
|
||||
const pkgTitle = pkg.manifest.title
|
||||
|
||||
return [
|
||||
// instructions
|
||||
{
|
||||
action: () => this.presentModalInstructions(pkg),
|
||||
title: 'Instructions',
|
||||
description: `Understand how to use ${pkgTitle}`,
|
||||
icon: 'list-outline',
|
||||
},
|
||||
// config
|
||||
{
|
||||
action: async () =>
|
||||
this.modalService.presentModalConfig({ pkgId: pkg.manifest.id }),
|
||||
title: 'Config',
|
||||
description: `Customize ${pkgTitle}`,
|
||||
icon: 'construct-outline',
|
||||
},
|
||||
// properties
|
||||
{
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['properties'], {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
title: 'Properties',
|
||||
description:
|
||||
'Runtime information, credentials, and other values of interest',
|
||||
icon: 'briefcase-outline',
|
||||
},
|
||||
// actions
|
||||
{
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }),
|
||||
title: 'Actions',
|
||||
description: `Uninstall and other commands specific to ${pkgTitle}`,
|
||||
icon: 'flash-outline',
|
||||
},
|
||||
// interfaces
|
||||
{
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['interfaces'], {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
title: 'Interfaces',
|
||||
description: 'User and machine access points',
|
||||
icon: 'desktop-outline',
|
||||
},
|
||||
// logs
|
||||
{
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }),
|
||||
title: 'Logs',
|
||||
description: 'Raw, unfiltered service logs',
|
||||
icon: 'receipt-outline',
|
||||
},
|
||||
// view in marketplace
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward([`marketplace/${pkg.manifest.id}`]),
|
||||
title: 'Marketplace',
|
||||
description: 'View service in marketplace',
|
||||
icon: 'storefront-outline',
|
||||
},
|
||||
{
|
||||
action: () => this.donate(pkg),
|
||||
title: 'Donate',
|
||||
description: `Support ${pkgTitle}`,
|
||||
icon: 'logo-bitcoin',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
private async presentModalInstructions(pkg: PackageDataEntry) {
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
title: 'Instructions',
|
||||
contentUrl: pkg['static-files']['instructions'],
|
||||
},
|
||||
component: MarkdownPage,
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private async donate({ manifest }: PackageDataEntry): Promise<void> {
|
||||
const url = manifest['donation-url']
|
||||
if (url) {
|
||||
this.document.defaultView.open(url, '_blank', 'noreferrer')
|
||||
} 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.`,
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { NavigationExtras } from '@angular/router'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { combineLatest, Observable } from 'rxjs'
|
||||
import { filter, map } from 'rxjs/operators'
|
||||
import { DependentInfo, exists } from 'src/app/util/misc.util'
|
||||
import {
|
||||
DependencyError,
|
||||
DependencyErrorType,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ModalService } from 'src/app/services/modal.service'
|
||||
|
||||
export interface DependencyInfo {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
version: string
|
||||
errorText: string
|
||||
actionText: string
|
||||
action: () => any
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'toDependencies',
|
||||
})
|
||||
export class ToDependenciesPipe implements PipeTransform {
|
||||
constructor(
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly modalService: ModalService,
|
||||
) {}
|
||||
|
||||
transform(pkg: PackageDataEntry): Observable<DependencyInfo[]> {
|
||||
return combineLatest([
|
||||
this.patch.watch$(
|
||||
'package-data',
|
||||
pkg.manifest.id,
|
||||
'installed',
|
||||
'current-dependencies',
|
||||
),
|
||||
this.patch.watch$(
|
||||
'package-data',
|
||||
pkg.manifest.id,
|
||||
'installed',
|
||||
'status',
|
||||
'dependency-errors',
|
||||
),
|
||||
]).pipe(
|
||||
filter(deps => deps.every(exists)),
|
||||
map(([currentDeps, depErrors]) =>
|
||||
Object.keys(currentDeps)
|
||||
.filter(id => !!pkg.manifest.dependencies[id])
|
||||
.map(id => this.setDepValues(pkg, id, depErrors)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private setDepValues(
|
||||
pkg: PackageDataEntry,
|
||||
id: string,
|
||||
errors: { [id: string]: DependencyError },
|
||||
): DependencyInfo {
|
||||
let errorText = ''
|
||||
let actionText = 'View'
|
||||
let action: () => any = () =>
|
||||
this.navCtrl.navigateForward(`/services/${id}`)
|
||||
|
||||
const error = errors[id]
|
||||
|
||||
if (error) {
|
||||
// health checks failed
|
||||
if (
|
||||
[
|
||||
DependencyErrorType.InterfaceHealthChecksFailed,
|
||||
DependencyErrorType.HealthChecksFailed,
|
||||
].includes(error.type)
|
||||
) {
|
||||
errorText = 'Health check failed'
|
||||
// not installed
|
||||
} else if (error.type === DependencyErrorType.NotInstalled) {
|
||||
errorText = 'Not installed'
|
||||
actionText = 'Install'
|
||||
action = () => this.fixDep(pkg, 'install', id)
|
||||
// incorrect version
|
||||
} else if (error.type === DependencyErrorType.IncorrectVersion) {
|
||||
errorText = 'Incorrect version'
|
||||
actionText = 'Update'
|
||||
action = () => this.fixDep(pkg, 'update', id)
|
||||
// not running
|
||||
} else if (error.type === DependencyErrorType.NotRunning) {
|
||||
errorText = 'Not running'
|
||||
actionText = 'Start'
|
||||
// config unsatisfied
|
||||
} else if (error.type === DependencyErrorType.ConfigUnsatisfied) {
|
||||
errorText = 'Config not satisfied'
|
||||
actionText = 'Auto config'
|
||||
action = () => this.fixDep(pkg, 'configure', id)
|
||||
} else if (error.type === DependencyErrorType.Transitive) {
|
||||
errorText = 'Dependency has a dependency issue'
|
||||
}
|
||||
errorText = `${errorText}. ${pkg.manifest.title} will not work as expected.`
|
||||
}
|
||||
|
||||
const depInfo = pkg.installed['dependency-info'][id]
|
||||
|
||||
return {
|
||||
id,
|
||||
version: pkg.manifest.dependencies[id].version,
|
||||
title: depInfo.manifest.title,
|
||||
icon: depInfo.icon,
|
||||
errorText,
|
||||
actionText,
|
||||
action,
|
||||
}
|
||||
}
|
||||
|
||||
async fixDep(
|
||||
pkg: PackageDataEntry,
|
||||
action: 'install' | 'update' | 'configure',
|
||||
id: string,
|
||||
): Promise<void> {
|
||||
switch (action) {
|
||||
case 'install':
|
||||
case 'update':
|
||||
return this.installDep(pkg, id)
|
||||
case 'configure':
|
||||
return this.configureDep(pkg, id)
|
||||
}
|
||||
}
|
||||
|
||||
private async installDep(
|
||||
pkg: PackageDataEntry,
|
||||
depId: string,
|
||||
): Promise<void> {
|
||||
const version = pkg.manifest.dependencies[depId].version
|
||||
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: pkg.manifest.id,
|
||||
title: pkg.manifest.title,
|
||||
version,
|
||||
}
|
||||
const navigationExtras: NavigationExtras = {
|
||||
state: { dependentInfo },
|
||||
}
|
||||
|
||||
await this.navCtrl.navigateForward(
|
||||
`/marketplace/${depId}`,
|
||||
navigationExtras,
|
||||
)
|
||||
}
|
||||
|
||||
private async configureDep(
|
||||
pkg: PackageDataEntry,
|
||||
dependencyId: string,
|
||||
): Promise<void> {
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: pkg.manifest.id,
|
||||
title: pkg.manifest.title,
|
||||
}
|
||||
|
||||
await this.modalService.presentModalConfig({
|
||||
pkgId: dependencyId,
|
||||
dependentInfo,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Inject, Pipe, PipeTransform } from '@angular/core'
|
||||
import {
|
||||
HealthCheckResult,
|
||||
PackageDataEntry,
|
||||
PackageMainStatus,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { exists, isEmptyObject } from 'src/app/util/misc.util'
|
||||
import { filter, map, startWith } from 'rxjs/operators'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
@Pipe({
|
||||
name: 'toHealthChecks',
|
||||
})
|
||||
export class ToHealthChecksPipe implements PipeTransform {
|
||||
constructor(private readonly patch: PatchDbService) {}
|
||||
|
||||
transform(
|
||||
pkg: PackageDataEntry,
|
||||
): Observable<Record<string, HealthCheckResult | null>> | null {
|
||||
const healthChecks = Object.keys(pkg.manifest['health-checks']).reduce(
|
||||
(obj, key) => ({ ...obj, [key]: null }),
|
||||
{},
|
||||
)
|
||||
|
||||
const healthChecks$ = this.patch
|
||||
.watch$('package-data', pkg.manifest.id, 'installed', 'status', 'main')
|
||||
.pipe(
|
||||
filter(obj => exists(obj)),
|
||||
map(main =>
|
||||
// Question: is this ok or do we have to use Object.keys
|
||||
// to maintain order and the keys initially present in pkg?
|
||||
main.status === PackageMainStatus.Running
|
||||
? main.health
|
||||
: healthChecks,
|
||||
),
|
||||
startWith(healthChecks),
|
||||
)
|
||||
|
||||
return isEmptyObject(healthChecks) ? null : healthChecks$
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
PackageStatus,
|
||||
renderPkgStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
|
||||
@Pipe({
|
||||
name: 'toStatus',
|
||||
})
|
||||
export class ToStatusPipe implements PipeTransform {
|
||||
transform(pkg: PackageDataEntry): PackageStatus {
|
||||
return renderPkgStatus(pkg)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { InstallProgress } from '../services/patch-db/data-model'
|
||||
import { packageLoadingProgress, ProgressData } from '../util/package-loading-progress'
|
||||
import {
|
||||
packageLoadingProgress,
|
||||
ProgressData,
|
||||
} from '../util/package-loading-progress'
|
||||
|
||||
@Pipe({
|
||||
name: 'installState',
|
||||
})
|
||||
export class InstallState implements PipeTransform {
|
||||
transform (loadData: InstallProgress): ProgressData {
|
||||
transform(loadData: InstallProgress): ProgressData | null {
|
||||
return packageLoadingProgress(loadData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { HttpService } from '../http.service'
|
||||
import { MockApiService } from './embassy-mock-api.service'
|
||||
import { LiveApiService } from './embassy-live-api.service'
|
||||
import { ConfigService } from '../config.service'
|
||||
import { LocalStorageBootstrap } from '../patch-db/local-storage-bootstrap'
|
||||
|
||||
export function ApiServiceFactory (config: ConfigService, http: HttpService, bootstrapper: LocalStorageBootstrap) {
|
||||
if (config.mocks.enabled) {
|
||||
return new MockApiService(bootstrapper)
|
||||
} else {
|
||||
return new LiveApiService(http)
|
||||
}
|
||||
}
|
||||
28
ui/src/app/services/modal.service.ts
Normal file
28
ui/src/app/services/modal.service.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { DependentInfo } from 'src/app/util/misc.util'
|
||||
import { AppConfigPage } from 'src/app/modals/app-config/app-config.page'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ModalService {
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly modalCtrl: ModalController,
|
||||
) {}
|
||||
|
||||
async presentModalConfig(componentProps: ComponentProps): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: AppConfigPage,
|
||||
componentProps,
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
}
|
||||
|
||||
interface ComponentProps {
|
||||
pkgId: string
|
||||
dependentInfo?: DependentInfo
|
||||
}
|
||||
@@ -122,14 +122,14 @@ export interface Manifest {
|
||||
stop: string | null
|
||||
}
|
||||
main: ActionImpl
|
||||
'health-checks': { [id: string]: ActionImpl & { name: string, description: string } }
|
||||
'health-checks': Record<string, ActionImpl & { name: string, description: string }>
|
||||
config: ConfigActions | null
|
||||
volumes: { [id: string]: Volume }
|
||||
volumes: Record<string, Volume>
|
||||
'min-os-version': string
|
||||
interfaces: { [id: string]: InterfaceDef }
|
||||
interfaces: Record<string, InterfaceDef>
|
||||
backup: BackupActions
|
||||
migrations: Migrations
|
||||
actions: { [id: string]: Action }
|
||||
actions: Record<string, Action>
|
||||
permissions: any // @TODO 0.3.1
|
||||
dependencies: DependencyInfo
|
||||
}
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { isEmptyObject } from '../util/misc.util'
|
||||
import { PackageDataEntry, PackageMainStatus, PackageState, Status } from './patch-db/data-model'
|
||||
import {
|
||||
PackageDataEntry,
|
||||
PackageMainStatus,
|
||||
PackageState,
|
||||
Status,
|
||||
} from './patch-db/data-model'
|
||||
|
||||
export function renderPkgStatus (pkg: PackageDataEntry): {
|
||||
primary: PrimaryStatus,
|
||||
dependency: DependencyStatus | null,
|
||||
export interface PackageStatus {
|
||||
primary: PrimaryStatus
|
||||
dependency: DependencyStatus | null
|
||||
health: HealthStatus | null
|
||||
} {
|
||||
}
|
||||
|
||||
export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus {
|
||||
let primary: PrimaryStatus
|
||||
let dependency: DependencyStatus | null = null
|
||||
let health: HealthStatus | null = null
|
||||
@@ -21,7 +28,7 @@ export function renderPkgStatus (pkg: PackageDataEntry): {
|
||||
return { primary, dependency, health }
|
||||
}
|
||||
|
||||
function getPrimaryStatus (status: Status): PrimaryStatus {
|
||||
function getPrimaryStatus(status: Status): PrimaryStatus {
|
||||
if (!status.configured) {
|
||||
return PrimaryStatus.NeedsConfig
|
||||
} else {
|
||||
@@ -29,7 +36,7 @@ function getPrimaryStatus (status: Status): PrimaryStatus {
|
||||
}
|
||||
}
|
||||
|
||||
function getDependencyStatus (pkg: PackageDataEntry): DependencyStatus {
|
||||
function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus {
|
||||
const installed = pkg.installed
|
||||
if (isEmptyObject(installed['current-dependencies'])) return null
|
||||
|
||||
@@ -39,7 +46,7 @@ function getDependencyStatus (pkg: PackageDataEntry): DependencyStatus {
|
||||
return depIds.length ? DependencyStatus.Warning : DependencyStatus.Satisfied
|
||||
}
|
||||
|
||||
function getHealthStatus (status: Status): HealthStatus {
|
||||
function getHealthStatus(status: Status): HealthStatus {
|
||||
if (status.main.status === PackageMainStatus.Running) {
|
||||
const values = Object.values(status.main.health)
|
||||
if (values.some(h => h.result === 'failure')) {
|
||||
|
||||
22
ui/src/app/services/ui-launcher.service.ts
Normal file
22
ui/src/app/services/ui-launcher.service.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { PackageDataEntry } from './patch-db/data-model'
|
||||
import { ConfigService } from './config.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UiLauncherService {
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
private readonly config: ConfigService,
|
||||
) {}
|
||||
|
||||
launch(pkg: PackageDataEntry): void {
|
||||
this.document.defaultView.open(
|
||||
this.config.launchableURL(pkg),
|
||||
'_blank',
|
||||
'noreferrer',
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -13,15 +13,13 @@ import {
|
||||
} from './package-loading-progress'
|
||||
import { Subscription } from 'rxjs'
|
||||
|
||||
export function getPackageInfo (entry: PackageDataEntry): PkgInfo {
|
||||
export function getPackageInfo(entry: PackageDataEntry): PkgInfo {
|
||||
const statuses = renderPkgStatus(entry)
|
||||
|
||||
return {
|
||||
entry,
|
||||
primaryRendering: PrimaryRendering[statuses.primary],
|
||||
installProgress: !isEmptyObject(entry['install-progress'])
|
||||
? packageLoadingProgress(entry['install-progress'])
|
||||
: undefined,
|
||||
installProgress: packageLoadingProgress(entry['install-progress']),
|
||||
error:
|
||||
statuses.health === HealthStatus.Failure ||
|
||||
statuses.dependency === DependencyStatus.Warning,
|
||||
@@ -31,7 +29,7 @@ export function getPackageInfo (entry: PackageDataEntry): PkgInfo {
|
||||
export interface PkgInfo {
|
||||
entry: PackageDataEntry
|
||||
primaryRendering: StatusRendering
|
||||
installProgress: ProgressData
|
||||
installProgress: ProgressData | null
|
||||
error: boolean
|
||||
sub?: Subscription | null
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { InstallProgress } from 'src/app/services/patch-db/data-model'
|
||||
import { isEmptyObject } from './misc.util'
|
||||
|
||||
export function packageLoadingProgress (
|
||||
export function packageLoadingProgress(
|
||||
loadData: InstallProgress,
|
||||
): ProgressData {
|
||||
): ProgressData | null {
|
||||
if (isEmptyObject(loadData)) {
|
||||
return null
|
||||
}
|
||||
|
||||
let {
|
||||
downloaded,
|
||||
validated,
|
||||
@@ -28,11 +33,10 @@ export function packageLoadingProgress (
|
||||
unpackWeight * unpacked,
|
||||
)
|
||||
|
||||
|
||||
const denominator = Math.floor(
|
||||
size * (downloadWeight + validateWeight + unpackWeight),
|
||||
)
|
||||
const totalProgress = Math.floor(100 * numerator / denominator)
|
||||
const totalProgress = Math.floor((100 * numerator) / denominator)
|
||||
|
||||
return {
|
||||
totalProgress,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"rules": {
|
||||
"no-unused-variable": true,
|
||||
"no-unused-expression": true,
|
||||
"space-before-function-paren": true,
|
||||
"semicolon": [
|
||||
true,
|
||||
"never"
|
||||
@@ -24,8 +23,7 @@
|
||||
"check-type",
|
||||
"check-typecast",
|
||||
"check-type-operator",
|
||||
"check-preblock",
|
||||
"check-postbrace"
|
||||
"check-preblock"
|
||||
],
|
||||
"trailing-comma": [
|
||||
true,
|
||||
|
||||
Reference in New Issue
Block a user