mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
add error status (#2746)
* add error status * update types * ṗ̶̰̙̓͒̈́ͅü̵̢̙̫̣ŗ̷̪̺̺͛g̴̲͉͎̬̒̇e̵̪̎̅͌ ̶̡̜̘͐͛t̶͎͍̣̿̍̐h̴͕̩͗̈́̎̑e̵͚͒̂͝ ̸̛͙̦͈͝v̶̱͙̬̽̔ọ̶̧̡̒̓i̸̬̲͍̋̈́d̴͉̀ * fix some extra voids * add `package.rebuild` * introduce error status and pkg rebuild and fix mocks * minor fixes * fix build --------- Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
@@ -11,10 +11,19 @@
|
||||
<ion-item-group *ngIf="pkg$ | async as pkg">
|
||||
<!-- ** standard actions ** -->
|
||||
<ion-item-divider>Standard Actions</ion-item-divider>
|
||||
<app-actions-item
|
||||
[action]="{
|
||||
name: 'Rebuild Service',
|
||||
description: 'Rebuilds the service container. It is harmless and only takes a few seconds to complete, but it should only be necessary if a StartOS bug is preventing dependencies, interfaces, or actions from synchronizing.',
|
||||
visibility: 'enabled'
|
||||
}"
|
||||
icon="construct-outline"
|
||||
(click)="rebuild(pkg.manifest.id)"
|
||||
></app-actions-item>
|
||||
<app-actions-item
|
||||
[action]="{
|
||||
name: 'Uninstall',
|
||||
description: 'This will uninstall the service from StartOS and delete all data permanently.',
|
||||
description: 'Uninstalls this service from StartOS and delete all data permanently.',
|
||||
visibility: 'enabled'
|
||||
}"
|
||||
icon="trash-outline"
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { AlertController, NavController } from '@ionic/angular'
|
||||
import { ErrorService, getPkgId, LoadingService } from '@start9labs/shared'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ActionService } from 'src/app/services/action.service'
|
||||
import { StandardActionsService } from 'src/app/services/standard-actions.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { getAllPackages, getManifest } from 'src/app/util/get-package-data'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
import { getManifest } from 'src/app/util/get-package-data'
|
||||
import { filter, map } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
@@ -35,13 +33,9 @@ export class AppActionsPage {
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly api: ApiService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly actionService: ActionService,
|
||||
private readonly standardActionsService: StandardActionsService,
|
||||
) {}
|
||||
|
||||
async handleAction(
|
||||
@@ -55,51 +49,12 @@ export class AppActionsPage {
|
||||
)
|
||||
}
|
||||
|
||||
async tryUninstall(manifest: T.Manifest): Promise<void> {
|
||||
let message =
|
||||
manifest.alerts.uninstall ||
|
||||
`Uninstalling ${manifest.title} will permanently delete its data`
|
||||
|
||||
if (hasCurrentDeps(this.pkgId, await getAllPackages(this.patch))) {
|
||||
message = `${message}. Services that depend on ${manifest.title} will no longer work properly and may crash`
|
||||
}
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Uninstall',
|
||||
handler: () => {
|
||||
this.uninstall()
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
cssClass: 'alert-warning-message',
|
||||
})
|
||||
|
||||
await alert.present()
|
||||
async rebuild(id: string) {
|
||||
return this.standardActionsService.rebuild(id)
|
||||
}
|
||||
|
||||
private async uninstall() {
|
||||
const loader = this.loader.open(`Beginning uninstall...`).subscribe()
|
||||
|
||||
try {
|
||||
await this.api.uninstallPackage({ id: this.pkgId })
|
||||
this.api
|
||||
.setDbValue<boolean>(['ackInstructions', this.pkgId], false)
|
||||
.catch(e => console.error('Failed to mark instructions as unseen', e))
|
||||
this.navCtrl.navigateRoot('/services')
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
async tryUninstall(manifest: T.Manifest) {
|
||||
return this.standardActionsService.tryUninstall(manifest)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { AppShowDependenciesComponent } from './components/app-show-dependencies
|
||||
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 { AppShowAdditionalComponent } from './components/app-show-additional/app-show-additional.component'
|
||||
import { AppShowErrorComponent } from './components/app-show-error/app-show-error.component'
|
||||
import { HealthColorPipe } from './pipes/health-color.pipe'
|
||||
import { ToHealthChecksPipe } from './pipes/to-health-checks.pipe'
|
||||
import { ToButtonsPipe } from './pipes/to-buttons.pipe'
|
||||
@@ -43,6 +44,7 @@ const routes: Routes = [
|
||||
AppShowMenuComponent,
|
||||
AppShowHealthChecksComponent,
|
||||
AppShowAdditionalComponent,
|
||||
AppShowErrorComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
<ion-item-group *ngIf="pkgPlus.status as status">
|
||||
<!-- ** status ** -->
|
||||
<app-show-status [pkg]="pkg" [status]="status"></app-show-status>
|
||||
<!-- ** installed && !backingUp ** -->
|
||||
<!-- ** installed && !backingUp && !error ** -->
|
||||
<ng-container
|
||||
*ngIf="isInstalled(pkg) && status.primary !== 'backingUp'"
|
||||
*ngIf="isInstalled(pkg) && status.primary !== 'backingUp' && status.primary !== 'error'"
|
||||
>
|
||||
<!-- ** health checks ** -->
|
||||
<app-show-health-checks
|
||||
@@ -35,6 +35,11 @@
|
||||
<!-- ** additional ** -->
|
||||
<app-show-additional [pkg]="pkg"></app-show-additional>
|
||||
</ng-container>
|
||||
<app-show-error
|
||||
*ngIf="pkg.status.main === 'error'"
|
||||
[manifest]="pkgPlus.manifest"
|
||||
[error]="pkg.status"
|
||||
></app-show-error>
|
||||
</ion-item-group>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
|
||||
@@ -45,7 +45,7 @@ export interface DependencyInfo {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowPage {
|
||||
private readonly pkgId = getPkgId(this.route)
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
|
||||
readonly pkgPlus$ = combineLatest([
|
||||
this.patch.watch$('packageData'),
|
||||
@@ -58,9 +58,11 @@ export class AppShowPage {
|
||||
}),
|
||||
map(([allPkgs, depErrors]) => {
|
||||
const pkg = allPkgs[this.pkgId]
|
||||
const manifest = getManifest(pkg)
|
||||
return {
|
||||
pkg,
|
||||
dependencies: this.getDepInfo(pkg, allPkgs, depErrors),
|
||||
manifest,
|
||||
dependencies: this.getDepInfo(pkg, manifest, allPkgs, depErrors),
|
||||
status: renderPkgStatus(pkg, depErrors),
|
||||
}
|
||||
}),
|
||||
@@ -84,11 +86,10 @@ export class AppShowPage {
|
||||
|
||||
private getDepInfo(
|
||||
pkg: PackageDataEntry,
|
||||
manifest: T.Manifest,
|
||||
allPkgs: AllPackageData,
|
||||
depErrors: PkgDependencyErrors,
|
||||
): DependencyInfo[] {
|
||||
const manifest = getManifest(pkg)
|
||||
|
||||
return Object.keys(pkg.currentDependencies).map(id =>
|
||||
this.getDepValues(pkg, allPkgs, manifest, id, depErrors),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<ion-item-divider>Message</ion-item-divider>
|
||||
<div class="code-block ion-margin">
|
||||
<code>
|
||||
<ion-text color="warning">{{ error.message }}</ion-text>
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<ion-item-divider>Actions</ion-item-divider>
|
||||
<div class="ion-margin">
|
||||
<p>
|
||||
<b>Rebuild Container</b>
|
||||
is harmless action that and only takes a few seconds to complete. It will
|
||||
likely resolve this issue.
|
||||
<b>Uninstall Service</b>
|
||||
is a dangerous action that will remove the service from StartOS and wipe all
|
||||
its data.
|
||||
</p>
|
||||
<ion-button class="ion-margin-end" (click)="rebuild()">
|
||||
Rebuild Container
|
||||
</ion-button>
|
||||
<ion-button (click)="tryUninstall()" color="danger">
|
||||
Uninstall Service
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="error.debug">
|
||||
<ion-item-divider>Full Stack Trace</ion-item-divider>
|
||||
<div class="code-block ion-margin">
|
||||
<code>{{ error.message }}</code>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { ToastController } from '@ionic/angular'
|
||||
import { copyToClipboard } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { StandardActionsService } from 'src/app/services/standard-actions.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-error',
|
||||
templateUrl: 'app-show-error.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowErrorComponent {
|
||||
@Input()
|
||||
manifest!: T.Manifest
|
||||
|
||||
@Input()
|
||||
error!: T.MainStatus & { main: 'error' }
|
||||
|
||||
constructor(
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly standardActionsService: StandardActionsService,
|
||||
) {}
|
||||
|
||||
async copy(text: string): Promise<void> {
|
||||
const success = await copyToClipboard(text)
|
||||
const message = success
|
||||
? 'Copied to clipboard!'
|
||||
: 'Failed to copy to clipboard.'
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
async rebuild() {
|
||||
return this.standardActionsService.rebuild(this.manifest.id)
|
||||
}
|
||||
|
||||
async tryUninstall() {
|
||||
return this.standardActionsService.tryUninstall(this.manifest)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,14 @@
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="isInstalled(pkg) && (connection$ | async)">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
isInstalled(pkg) &&
|
||||
pkg.status.main !== 'backingUp' &&
|
||||
pkg.status.main !== 'error' &&
|
||||
(connection$ | async)
|
||||
"
|
||||
>
|
||||
<ion-grid>
|
||||
<ion-row style="padding-left: 12px">
|
||||
<ion-col>
|
||||
|
||||
@@ -257,6 +257,9 @@ export module RR {
|
||||
export type StopPackageReq = { id: string } // package.stop
|
||||
export type StopPackageRes = null
|
||||
|
||||
export type RebuildPackageReq = { id: string } // package.rebuild
|
||||
export type RebuildPackageRes = null
|
||||
|
||||
export type UninstallPackageReq = { id: string } // package.uninstall
|
||||
export type UninstallPackageRes = null
|
||||
|
||||
|
||||
@@ -249,6 +249,10 @@ export abstract class ApiService {
|
||||
|
||||
abstract stopPackage(params: RR.StopPackageReq): Promise<RR.StopPackageRes>
|
||||
|
||||
abstract rebuildPackage(
|
||||
params: RR.RebuildPackageReq,
|
||||
): Promise<RR.RebuildPackageRes>
|
||||
|
||||
abstract uninstallPackage(
|
||||
params: RR.UninstallPackageReq,
|
||||
): Promise<RR.UninstallPackageRes>
|
||||
|
||||
@@ -498,6 +498,12 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'package.stop', params })
|
||||
}
|
||||
|
||||
async rebuildPackage(
|
||||
params: RR.RebuildPackageReq,
|
||||
): Promise<RR.RebuildPackageRes> {
|
||||
return this.rpcRequest({ method: 'package.rebuild', params })
|
||||
}
|
||||
|
||||
async uninstallPackage(
|
||||
params: RR.UninstallPackageReq,
|
||||
): Promise<RR.UninstallPackageRes> {
|
||||
|
||||
@@ -2,10 +2,12 @@ import { Injectable } from '@angular/core'
|
||||
import { Log, RPCErrorDetails, RPCOptions, pauseFor } from '@start9labs/shared'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import {
|
||||
AddOperation,
|
||||
Operation,
|
||||
PatchOp,
|
||||
pathFromArray,
|
||||
RemoveOperation,
|
||||
ReplaceOperation,
|
||||
Revision,
|
||||
} from 'patch-db-client'
|
||||
import {
|
||||
@@ -636,14 +638,14 @@ export class MockApiService extends ApiService {
|
||||
|
||||
async createBackup(params: RR.CreateBackupReq): Promise<RR.CreateBackupRes> {
|
||||
await pauseFor(2000)
|
||||
const path = '/serverInfo/statusInfo/backupProgress'
|
||||
const serverPath = '/serverInfo/statusInfo/backupProgress'
|
||||
const ids = params.packageIds
|
||||
|
||||
setTimeout(async () => {
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i]
|
||||
const appPath = `/packageData/${id}/status/main/status`
|
||||
const appPatch = [
|
||||
const appPath = `/packageData/${id}/status/main/`
|
||||
const appPatch: ReplaceOperation<T.MainStatus['main']>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: appPath,
|
||||
@@ -660,40 +662,43 @@ export class MockApiService extends ApiService {
|
||||
value: 'stopped',
|
||||
},
|
||||
])
|
||||
this.mockRevision([
|
||||
|
||||
const serverPatch: ReplaceOperation<T.BackupProgress['complete']>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `${path}/${id}/complete`,
|
||||
path: `${serverPath}/${id}/complete`,
|
||||
value: true,
|
||||
},
|
||||
])
|
||||
]
|
||||
this.mockRevision(serverPatch)
|
||||
}
|
||||
|
||||
await pauseFor(1000)
|
||||
|
||||
// set server back to running
|
||||
const lastPatch = [
|
||||
// remove backupProgress
|
||||
const lastPatch: ReplaceOperation<T.ServerStatus['backupProgress']>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path,
|
||||
path: serverPath,
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
this.mockRevision(lastPatch)
|
||||
}, 500)
|
||||
|
||||
const originalPatch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path,
|
||||
value: ids.reduce((acc, val) => {
|
||||
return {
|
||||
...acc,
|
||||
[val]: { complete: false },
|
||||
}
|
||||
}, {}),
|
||||
},
|
||||
]
|
||||
const originalPatch: ReplaceOperation<T.ServerStatus['backupProgress']>[] =
|
||||
[
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: serverPath,
|
||||
value: ids.reduce((acc, val) => {
|
||||
return {
|
||||
...acc,
|
||||
[val]: { complete: false },
|
||||
}
|
||||
}, {}),
|
||||
},
|
||||
]
|
||||
|
||||
this.mockRevision(originalPatch)
|
||||
|
||||
@@ -750,7 +755,7 @@ export class MockApiService extends ApiService {
|
||||
this.installProgress(params.id)
|
||||
}, 1000)
|
||||
|
||||
const patch: Operation<
|
||||
const patch: AddOperation<
|
||||
PackageDataEntry<InstallingState | UpdatingState>
|
||||
>[] = [
|
||||
{
|
||||
@@ -799,7 +804,7 @@ export class MockApiService extends ApiService {
|
||||
params: RR.RestorePackagesReq,
|
||||
): Promise<RR.RestorePackagesRes> {
|
||||
await pauseFor(2000)
|
||||
const patch: Operation<PackageDataEntry>[] = params.ids.map(id => {
|
||||
const patch: AddOperation<PackageDataEntry>[] = params.ids.map(id => {
|
||||
setTimeout(async () => {
|
||||
this.installProgress(id)
|
||||
}, 2000)
|
||||
@@ -826,76 +831,61 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
|
||||
async startPackage(params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
|
||||
const path = `/packageData/${params.id}/status/main`
|
||||
const path = `/packageData/${params.id}/status`
|
||||
|
||||
await pauseFor(2000)
|
||||
|
||||
setTimeout(async () => {
|
||||
const patch2 = [
|
||||
const patch2: ReplaceOperation<T.MainStatus & { main: 'running' }>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/status',
|
||||
value: 'running',
|
||||
},
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/started',
|
||||
value: new Date().toISOString(),
|
||||
path,
|
||||
value: {
|
||||
main: 'running',
|
||||
started: new Date().toISOString(),
|
||||
health: {
|
||||
'ephemeral-health-check': {
|
||||
name: 'Ephemeral Health Check',
|
||||
result: 'starting',
|
||||
message: null,
|
||||
},
|
||||
'unnecessary-health-check': {
|
||||
name: 'Unnecessary Health Check',
|
||||
result: 'disabled',
|
||||
message: 'Custom disabled message',
|
||||
},
|
||||
'chain-state': {
|
||||
name: 'Chain State',
|
||||
result: 'loading',
|
||||
message: 'Bitcoin is syncing from genesis',
|
||||
},
|
||||
'p2p-interface': {
|
||||
name: 'P2P Interface',
|
||||
result: 'success',
|
||||
message: null,
|
||||
},
|
||||
'rpc-interface': {
|
||||
name: 'RPC Interface',
|
||||
result: 'failure',
|
||||
message: 'Custom failure message',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
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 = [
|
||||
const originalPatch: ReplaceOperation<
|
||||
T.MainStatus & { main: 'starting' }
|
||||
>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/status',
|
||||
value: 'starting',
|
||||
path,
|
||||
value: {
|
||||
main: 'starting',
|
||||
health: {},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -907,74 +897,57 @@ export class MockApiService extends ApiService {
|
||||
async restartPackage(
|
||||
params: RR.RestartPackageReq,
|
||||
): Promise<RR.RestartPackageRes> {
|
||||
// first enact stop
|
||||
await pauseFor(2000)
|
||||
const path = `/packageData/${params.id}/status/main`
|
||||
const path = `/packageData/${params.id}/status`
|
||||
|
||||
setTimeout(async () => {
|
||||
const patch2: Operation<any>[] = [
|
||||
const patch2: ReplaceOperation<T.MainStatus & { main: 'running' }>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/status',
|
||||
value: 'starting',
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: path + '/restarting',
|
||||
value: true,
|
||||
path,
|
||||
value: {
|
||||
main: 'running',
|
||||
started: new Date().toISOString(),
|
||||
health: {
|
||||
'ephemeral-health-check': {
|
||||
name: 'Ephemeral Health Check',
|
||||
result: 'starting',
|
||||
message: null,
|
||||
},
|
||||
'unnecessary-health-check': {
|
||||
name: 'Unnecessary Health Check',
|
||||
result: 'disabled',
|
||||
message: 'Custom disabled message',
|
||||
},
|
||||
'chain-state': {
|
||||
name: 'Chain State',
|
||||
result: 'loading',
|
||||
message: 'Bitcoin is syncing from genesis',
|
||||
},
|
||||
'p2p-interface': {
|
||||
name: 'P2P Interface',
|
||||
result: 'success',
|
||||
message: null,
|
||||
},
|
||||
'rpc-interface': {
|
||||
name: 'RPC Interface',
|
||||
result: 'failure',
|
||||
message: 'Custom failure message',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch2)
|
||||
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch3: Operation<any>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/status',
|
||||
value: 'running',
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: path + '/restarting',
|
||||
},
|
||||
{
|
||||
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.',
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
]
|
||||
this.mockRevision(patch3)
|
||||
}, this.revertTime)
|
||||
|
||||
const patch = [
|
||||
const patch: ReplaceOperation<T.MainStatus & { main: 'restarting' }>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/status',
|
||||
value: 'restarting',
|
||||
},
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/health',
|
||||
value: {},
|
||||
path,
|
||||
value: {
|
||||
main: 'restarting',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -985,29 +958,24 @@ export class MockApiService extends ApiService {
|
||||
|
||||
async stopPackage(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
|
||||
await pauseFor(2000)
|
||||
const path = `/packageData/${params.id}/status/main`
|
||||
const path = `/packageData/${params.id}/status`
|
||||
|
||||
setTimeout(() => {
|
||||
const patch2 = [
|
||||
const patch2: ReplaceOperation<T.MainStatus & { main: 'stopped' }>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path,
|
||||
value: {
|
||||
status: 'stopped',
|
||||
},
|
||||
value: { main: 'stopped' },
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch2)
|
||||
}, this.revertTime)
|
||||
|
||||
const patch = [
|
||||
const patch: ReplaceOperation<T.MainStatus & { main: 'stopping' }>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path,
|
||||
value: {
|
||||
status: 'stopping',
|
||||
timeout: '35s',
|
||||
},
|
||||
value: { main: 'stopping' },
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1016,6 +984,12 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async rebuildPackage(
|
||||
params: RR.RebuildPackageReq,
|
||||
): Promise<RR.RebuildPackageRes> {
|
||||
return this.restartPackage(params)
|
||||
}
|
||||
|
||||
async uninstallPackage(
|
||||
params: RR.UninstallPackageReq,
|
||||
): Promise<RR.UninstallPackageRes> {
|
||||
@@ -1031,7 +1005,7 @@ export class MockApiService extends ApiService {
|
||||
this.mockRevision(patch2)
|
||||
}, this.revertTime)
|
||||
|
||||
const patch = [
|
||||
const patch: ReplaceOperation<T.PackageState['state']>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/packageData/${params.id}/stateInfo/state`,
|
||||
|
||||
@@ -96,36 +96,14 @@ export const mockPatchData: DataModel = {
|
||||
icon: '/assets/img/service-icons/bitcoind.svg',
|
||||
lastBackup: null,
|
||||
status: {
|
||||
main: 'running',
|
||||
started: '2021-06-14T20:49:17.774Z',
|
||||
health: {
|
||||
'ephemeral-health-check': {
|
||||
name: 'Ephemeral Health Check',
|
||||
result: 'starting',
|
||||
message: null,
|
||||
},
|
||||
'chain-state': {
|
||||
name: 'Chain State',
|
||||
result: 'loading',
|
||||
message: 'Bitcoin is syncing from genesis',
|
||||
},
|
||||
'p2p-interface': {
|
||||
name: 'P2P',
|
||||
result: 'success',
|
||||
message: 'Health check successful',
|
||||
},
|
||||
'rpc-interface': {
|
||||
name: 'RPC',
|
||||
result: 'failure',
|
||||
message: 'RPC interface unreachable.',
|
||||
},
|
||||
'unnecessary-health-check': {
|
||||
name: 'Unnecessary Health Check',
|
||||
result: 'disabled',
|
||||
message: null,
|
||||
},
|
||||
},
|
||||
main: 'stopped',
|
||||
},
|
||||
// status: {
|
||||
// main: 'error',
|
||||
// message: 'Bitcoin is erroring out',
|
||||
// debug: 'This is a complete stack trace for bitcoin',
|
||||
// onRebuild: 'start',
|
||||
// },
|
||||
actions: {
|
||||
config: {
|
||||
name: 'Bitcoin Config',
|
||||
|
||||
@@ -80,6 +80,7 @@ export type PrimaryStatus =
|
||||
| 'stopped'
|
||||
| 'backingUp'
|
||||
| 'needsConfig'
|
||||
| 'error'
|
||||
|
||||
export type DependencyStatus = 'warning' | 'satisfied'
|
||||
|
||||
@@ -139,6 +140,11 @@ export const PrimaryRendering: Record<PrimaryStatus, StatusRendering> = {
|
||||
color: 'warning',
|
||||
showDots: false,
|
||||
},
|
||||
error: {
|
||||
display: 'Service Launch Error',
|
||||
color: 'danger',
|
||||
showDots: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const DependencyRendering: Record<DependencyStatus, StatusRendering> = {
|
||||
|
||||
85
web/projects/ui/src/app/services/standard-actions.service.ts
Normal file
85
web/projects/ui/src/app/services/standard-actions.service.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { hasCurrentDeps } from '../util/has-deps'
|
||||
import { getAllPackages } from '../util/get-package-data'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from './patch-db/data-model'
|
||||
import { AlertController, NavController } from '@ionic/angular'
|
||||
import { ApiService } from './api/embassy-api.service'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StandardActionsService {
|
||||
constructor(
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly api: ApiService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly navCtrl: NavController,
|
||||
) {}
|
||||
|
||||
async rebuild(id: string) {
|
||||
const loader = this.loader.open(`Rebuilding Container...`).subscribe()
|
||||
|
||||
try {
|
||||
await this.api.rebuildPackage({ id })
|
||||
this.navCtrl.navigateBack('/services/' + id)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
async tryUninstall(manifest: T.Manifest): Promise<void> {
|
||||
const { id, title, alerts } = manifest
|
||||
|
||||
let message =
|
||||
alerts.uninstall ||
|
||||
`Uninstalling ${title} will permanently delete its data`
|
||||
|
||||
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
|
||||
message = `${message}. Services that depend on ${title} will no longer work properly and may crash`
|
||||
}
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Uninstall',
|
||||
handler: () => {
|
||||
this.uninstall(id)
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
cssClass: 'alert-warning-message',
|
||||
})
|
||||
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async uninstall(id: string) {
|
||||
const loader = this.loader.open(`Beginning uninstall...`).subscribe()
|
||||
|
||||
try {
|
||||
await this.api.uninstallPackage({ id })
|
||||
this.api
|
||||
.setDbValue<boolean>(['ackInstructions', id], false)
|
||||
.catch(e => console.error('Failed to mark instructions as unseen', e))
|
||||
this.navCtrl.navigateRoot('/services')
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user