remove health checks from manifest

remove "restarting" bool on "starting" status

remove restarting attr
This commit is contained in:
Aiden McClelland
2023-02-17 12:28:40 -07:00
parent e4d283cc99
commit 4dfdf2f92f
21 changed files with 435 additions and 663 deletions

View File

@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { AppShowPage } from './app-show.page'
import { EmverPipesModule, ResponsiveColModule } from '@start9labs/shared'
import { EmverPipesModule, ResponsiveColModule, SharedPipesModule } from '@start9labs/shared'
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
import { AppConfigPageModule } from 'src/app/modals/app-config/app-config.module'
import { LaunchablePipeModule } from 'src/app/pipes/launchable/launchable.module'
@@ -16,7 +16,6 @@ import { AppShowMenuComponent } from './components/app-show-menu/app-show-menu.c
import { AppShowHealthChecksComponent } from './components/app-show-health-checks/app-show-health-checks.component'
import { AppShowAdditionalComponent } from './components/app-show-additional/app-show-additional.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'
@@ -34,7 +33,6 @@ const routes: Routes = [
AppShowPage,
HealthColorPipe,
ProgressDataPipe,
ToHealthChecksPipe,
ToButtonsPipe,
ToDependenciesPipe,
ToStatusPipe,
@@ -56,6 +54,7 @@ const routes: Routes = [
LaunchablePipeModule,
UiPipeModule,
ResponsiveColModule,
SharedPipesModule,
],
})
export class AppShowPageModule {}

View File

@@ -30,7 +30,7 @@
<!-- ** health checks ** -->
<app-show-health-checks
*ngIf="isRunning(status)"
[pkg]="pkg"
[pkgId]="pkgId"
></app-show-health-checks>
<!-- ** dependencies ** -->
<app-show-dependencies

View File

@@ -31,7 +31,7 @@ const STATES = [
export class AppShowPage {
readonly secure = this.config.isSecure()
private readonly pkgId = getPkgId(this.route)
readonly pkgId = getPkgId(this.route)
readonly pkg$ = this.patch.watch$('package-data', this.pkgId).pipe(
tap(pkg => {

View File

@@ -1,92 +1,82 @@
<ng-container
*ngIf="pkg | toHealthChecks | async | keyvalue: asIsOrder as checks"
>
<ng-container *ngIf="checks.length">
<ion-item-divider>Health Checks</ion-item-divider>
<!-- connected -->
<ng-container *ngIf="connected$ | async; else disconnected">
<ion-item *ngFor="let health of checks">
<!-- result -->
<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>
<span
*ngIf="
result === HealthResult.Success &&
pkg.manifest['health-checks'][health.key]['success-message']
"
>:
{{
pkg.manifest['health-checks'][health.key]['success-message']
}}
</span>
</p>
</ion-text>
</ion-label>
</ng-container>
<!-- no result -->
<ng-template #noResult>
<ion-spinner
class="icon-spinner"
color="dark"
slot="start"
></ion-spinner>
<ion-label>
<h2 class="bold">
{{ pkg.manifest['health-checks'][health.key].name }}
</h2>
<p class="primary">Awaiting result...</p>
</ion-label>
</ng-template>
</ion-item>
</ng-container>
<!-- disconnected -->
<ng-template #disconnected>
<ion-item *ngFor="let health of checks">
<ion-avatar slot="start">
<ion-skeleton-text class="avatar"></ion-skeleton-text>
</ion-avatar>
<ng-container *ngIf="healthChecks$ | async as checks">
<ion-item-divider>Health Checks</ion-item-divider>
<!-- connected -->
<ng-container *ngIf="connected$ | async; else disconnected">
<ion-item *ngFor="let check of checks">
<!-- result -->
<ng-container *ngIf="check.result as result; else noResult">
<ion-spinner
*ngIf="isLoading(result)"
class="icon-spinner"
color="primary"
slot="start"
></ion-spinner>
<ion-icon
*ngIf="result === 'success'"
slot="start"
name="checkmark"
color="success"
></ion-icon>
<ion-icon
*ngIf="result === 'failure'"
slot="start"
name="warning-outline"
color="warning"
></ion-icon>
<ion-icon
*ngIf="result === 'disabled'"
slot="start"
name="remove"
color="dark"
></ion-icon>
<ion-label>
<ion-skeleton-text class="label"></ion-skeleton-text>
<ion-skeleton-text class="description"></ion-skeleton-text>
<h2 class="bold">
{{ check.name }}
</h2>
<ion-text [color]="result | healthColor">
<p>
<span *ngIf="isReady(result)">{{ result | titlecase }}</span>
<span *ngIf="result === 'starting'">...</span>
<span *ngIf="result === 'failure'">
{{ $any(check).error }}
</span>
<span *ngIf="result === 'loading'">
{{ $any(check).message }}
</span>
<span *ngIf="result === 'success'"
>:
{{ $any(check).message }}
</span>
</p>
</ion-text>
</ion-label>
</ion-item>
</ng-template>
</ng-container>
<!-- no result -->
<ng-template #noResult>
<ion-spinner
class="icon-spinner"
color="dark"
slot="start"
></ion-spinner>
<ion-label>
<h2 class="bold">
{{ check.name }}
</h2>
<p class="primary">Awaiting result...</p>
</ion-label>
</ng-template>
</ion-item>
</ng-container>
<!-- disconnected -->
<ng-template #disconnected>
<ion-item *ngFor="let check 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-template>
</ng-container>

View File

@@ -1,9 +1,9 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { PatchDB } from 'patch-db-client'
import { map } from 'rxjs'
import { ConnectionService } from 'src/app/services/connection.service'
import {
HealthResult,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { DataModel, HealthResult } from 'src/app/services/patch-db/data-model'
import { isEmptyObject } from '@start9labs/shared'
@Component({
selector: 'app-show-health-checks',
@@ -12,14 +12,26 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppShowHealthChecksComponent {
@Input()
pkg!: PackageDataEntry
HealthResult = HealthResult
@Input() pkgId!: string
readonly connected$ = this.connectionService.connected$
constructor(private readonly connectionService: ConnectionService) {}
get healthChecks$() {
return this.patch
.watch$('package-data', this.pkgId, 'installed', 'status', 'main')
.pipe(
map(main => {
if (main.status !== 'running' || isEmptyObject(main.health))
return null
return Object.values(main.health)
}),
)
}
constructor(
private readonly connectionService: ConnectionService,
private readonly patch: PatchDB<DataModel>,
) {}
isLoading(result: HealthResult): boolean {
return result === HealthResult.Starting || result === HealthResult.Loading

View File

@@ -1,43 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core'
import {
DataModel,
HealthCheckResult,
PackageDataEntry,
PackageMainStatus,
} from 'src/app/services/patch-db/data-model'
import { isEmptyObject } from '@start9labs/shared'
import { map, startWith } from 'rxjs/operators'
import { PatchDB } from 'patch-db-client'
import { Observable } from 'rxjs'
@Pipe({
name: 'toHealthChecks',
})
export class ToHealthChecksPipe implements PipeTransform {
constructor(private readonly patch: PatchDB<DataModel>) {}
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(
map(main => {
// Question: is this ok or do we have to use Object.keys
// to maintain order and the keys initially present in pkg?
return main.status === PackageMainStatus.Running &&
!isEmptyObject(main.health)
? main.health
: healthChecks
}),
startWith(healthChecks),
)
return isEmptyObject(healthChecks) ? null : healthChecks$
}
}

View File

@@ -87,7 +87,6 @@ export module Mock {
'shm-size': '',
'sigterm-timeout': '1ms',
},
'health-checks': {},
config: {
get: null,
set: null,
@@ -382,7 +381,6 @@ export module Mock {
'shm-size': '',
'sigterm-timeout': '10000µs',
},
'health-checks': {},
config: {
get: null,
set: null,
@@ -535,7 +533,6 @@ export module Mock {
'shm-size': '',
'sigterm-timeout': '1m',
},
'health-checks': {},
config: { get: {} as any, set: {} as any },
volumes: {},
'min-os-version': '0.2.12',

View File

@@ -727,7 +727,18 @@ export class MockApiService extends ApiService {
await pauseFor(2000)
setTimeout(async () => {
const patch2 = [
if (params.id !== 'bitcoind') {
const patch2 = [
{
op: PatchOp.REPLACE,
path: path + '/health',
value: {},
},
]
this.mockRevision(patch2)
}
const patch3 = [
{
op: PatchOp.REPLACE,
path: path + '/status',
@@ -739,52 +750,7 @@ export class MockApiService extends ApiService {
value: new Date().toISOString(),
},
]
this.mockRevision(patch2)
const patch3 = [
{
op: PatchOp.REPLACE,
path: path + '/health',
value: {
'ephemeral-health-check': {
result: 'starting',
},
'unnecessary-health-check': {
result: 'disabled',
},
},
},
]
this.mockRevision(patch3)
await pauseFor(2000)
const patch4 = [
{
op: PatchOp.REPLACE,
path: path + '/health',
value: {
'ephemeral-health-check': {
result: 'starting',
},
'unnecessary-health-check': {
result: 'disabled',
},
'chain-state': {
result: 'loading',
message: 'Bitcoin is syncing from genesis',
},
'p2p-interface': {
result: 'success',
},
'rpc-interface': {
result: 'failure',
error: 'RPC interface unreachable.',
},
},
},
]
this.mockRevision(patch4)
}, 2000)
const originalPatch = [
@@ -896,11 +862,6 @@ export class MockApiService extends ApiService {
path: path + '/status',
value: PackageMainStatus.Stopping,
},
{
op: PatchOp.REPLACE,
path: path + '/health',
value: {},
},
]
return this.withRevision(patch)

View File

@@ -127,24 +127,6 @@ export const mockPatchData: DataModel = {
'shm-size': '',
'sigterm-timeout': '.49m',
},
'health-checks': {
'chain-state': {
name: 'Chain State',
},
'ephemeral-health-check': {
name: 'Ephemeral Health Check',
},
'p2p-interface': {
name: 'P2P Interface',
'success-message': 'the health check ran succesfully',
},
'rpc-interface': {
name: 'RPC Interface',
},
'unnecessary-health-check': {
name: 'Unneccessary Health Check',
},
} as any,
config: {
get: {},
set: {},
@@ -243,9 +225,12 @@ export const mockPatchData: DataModel = {
'Your reason for re-syncing. Why are you doing this?',
nullable: false,
masked: false,
copyable: false,
pattern: '^[a-zA-Z]+$',
'pattern-description': 'Must contain only letters.',
placeholder: null,
textarea: false,
warning: null,
default: null,
},
name: {
type: 'string',
@@ -253,14 +238,19 @@ export const mockPatchData: DataModel = {
description: 'Tell the class your name.',
nullable: true,
masked: false,
copyable: false,
warning: 'You may loose all your money by providing your name.',
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
default: null,
},
notifications: {
name: 'Notification Preferences',
type: 'list',
subtype: 'enum',
description: 'how you want to be notified',
warning: null,
range: '[1,3]',
default: ['email'],
spec: {
@@ -282,6 +272,9 @@ export const mockPatchData: DataModel = {
default: 100,
range: '[0, 9999]',
integral: true,
units: null,
placeholder: null,
warning: null,
},
'top-speed': {
type: 'number',
@@ -291,6 +284,9 @@ export const mockPatchData: DataModel = {
range: '[-1000, 1000]',
integral: false,
units: 'm/s',
placeholder: null,
warning: null,
default: null,
},
testnet: {
name: 'Testnet',
@@ -318,22 +314,33 @@ export const mockPatchData: DataModel = {
name: 'Emergency Contact',
type: 'object',
description: 'The person to contact in case of emergency.',
warning: null,
spec: {
name: {
type: 'string',
name: 'Name',
description: null,
nullable: false,
masked: false,
copyable: false,
pattern: '^[a-zA-Z]+$',
'pattern-description': 'Must contain only letters.',
placeholder: null,
textarea: false,
warning: null,
default: null,
},
email: {
type: 'string',
name: 'Email',
description: null,
nullable: false,
masked: false,
copyable: true,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
},
},
@@ -351,7 +358,7 @@ export const mockPatchData: DataModel = {
pattern: '^[0-9]{1,3}([,.][0-9]{1,3})?$',
'pattern-description': 'Must be a valid IP address',
masked: false,
copyable: false,
placeholder: null,
},
},
bitcoinNode: {
@@ -375,7 +382,12 @@ export const mockPatchData: DataModel = {
description: 'the lan address',
nullable: true,
masked: false,
copyable: false,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
},
external: {
@@ -388,7 +400,9 @@ export const mockPatchData: DataModel = {
pattern: '.*',
'pattern-description': 'anything',
masked: false,
copyable: true,
placeholder: null,
textarea: false,
warning: null,
},
},
},
@@ -411,20 +425,26 @@ export const mockPatchData: DataModel = {
started: '2021-06-14T20:49:17.774Z',
health: {
'ephemeral-health-check': {
name: 'Ephemeral Health Check',
result: HealthResult.Starting,
},
'chain-state': {
name: 'Chain State',
result: HealthResult.Loading,
message: 'Bitcoin is syncing from genesis',
},
'p2p-interface': {
name: 'P2P Interface',
result: HealthResult.Success,
message: 'the health check ran successfully',
},
'rpc-interface': {
name: 'RPC Interface',
result: HealthResult.Failure,
error: 'RPC interface unreachable.',
},
'unnecessary-health-check': {
name: 'Totally Unnecessary',
result: HealthResult.Disabled,
},
},
@@ -461,7 +481,7 @@ export const mockPatchData: DataModel = {
manifest: {
id: 'lnd',
title: 'Lightning Network Daemon',
version: '0.11.0',
version: '0.11.1',
description: {
short: 'A bolt spec compliant client.',
long: 'More info about LND. More info about LND. More info about LND.',
@@ -501,7 +521,6 @@ export const mockPatchData: DataModel = {
'shm-size': '',
'sigterm-timeout': '0.5s',
},
'health-checks': {},
config: {
get: null,
set: null,

View File

@@ -155,10 +155,6 @@ export interface Manifest extends MarketplaceManifest<DependencyConfig | null> {
scripts: string // path to scripts folder
}
main: ActionImpl
'health-checks': Record<
string,
ActionImpl & { name: string; 'success-message': string | null }
>
config: ConfigActions | null
volumes: Record<string, Volume>
'min-os-version': string
@@ -295,7 +291,6 @@ export interface MainStatusStopping {
export interface MainStatusStarting {
status: PackageMainStatus.Starting
restarting: boolean
}
export interface MainStatusRunning {
@@ -322,12 +317,13 @@ export enum PackageMainStatus {
Restarting = 'restarting',
}
export type HealthCheckResult =
export type HealthCheckResult = { name: string } & (
| HealthCheckResultStarting
| HealthCheckResultLoading
| HealthCheckResultDisabled
| HealthCheckResultSuccess
| HealthCheckResultFailure
)
export enum HealthResult {
Starting = 'starting',
@@ -347,6 +343,7 @@ export interface HealthCheckResultDisabled {
export interface HealthCheckResultSuccess {
result: HealthResult.Success
message: string
}
export interface HealthCheckResultLoading {

View File

@@ -1,5 +1,6 @@
import { isEmptyObject } from '@start9labs/shared'
import {
InstalledPackageDataEntry,
MainStatusStarting,
PackageDataEntry,
PackageMainStatus,
@@ -8,42 +9,39 @@ import {
} from 'src/app/services/patch-db/data-model'
export interface PackageStatus {
primary: PrimaryStatus
primary: PrimaryStatus | PackageState | PackageMainStatus
dependency: DependencyStatus | null
health: HealthStatus | null
}
export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus {
let primary: PrimaryStatus
let primary: PrimaryStatus | PackageState | PackageMainStatus
let dependency: DependencyStatus | null = null
let health: HealthStatus | null = null
const hasHealthChecks = !isEmptyObject(pkg.manifest['health-checks'])
if (pkg.state === PackageState.Installed && pkg.installed) {
primary = getPrimaryStatus(pkg.installed.status)
dependency = getDependencyStatus(pkg)
health = getHealthStatus(pkg.installed.status, hasHealthChecks)
dependency = getDependencyStatus(pkg.installed)
health = getHealthStatus(pkg.installed.status)
} else {
primary = pkg.state as string as PrimaryStatus
primary = pkg.state
}
return { primary, dependency, health }
}
function getPrimaryStatus(status: Status): PrimaryStatus {
function getPrimaryStatus(status: Status): PrimaryStatus | PackageMainStatus {
if (!status.configured) {
return PrimaryStatus.NeedsConfig
} else if ((status.main as MainStatusStarting).restarting) {
return PrimaryStatus.Restarting
} else {
return status.main.status as any as PrimaryStatus
return status.main.status
}
}
function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus | null {
const installed = pkg.installed
if (!installed || isEmptyObject(installed['current-dependencies']))
return null
function getDependencyStatus(
installed: InstalledPackageDataEntry,
): DependencyStatus | null {
if (isEmptyObject(installed['current-dependencies'])) return null
const depErrors = installed.status['dependency-errors']
const depIds = Object.keys(depErrors).filter(key => !!depErrors[key])
@@ -51,11 +49,8 @@ function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus | null {
return depIds.length ? DependencyStatus.Warning : DependencyStatus.Satisfied
}
function getHealthStatus(
status: Status,
hasHealthChecks: boolean,
): HealthStatus | null {
if (status.main.status !== PackageMainStatus.Running || !status.main.health) {
function getHealthStatus(status: Status): HealthStatus | null {
if (status.main.status !== PackageMainStatus.Running) {
return null
}
@@ -65,10 +60,6 @@ function getHealthStatus(
return HealthStatus.Failure
}
if (!values.length && hasHealthChecks) {
return HealthStatus.Waiting
}
if (values.some(h => h.result === 'loading')) {
return HealthStatus.Loading
}