mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
Convert properties to an action (#2751)
* update actions response types and partially implement in UI * further remove diagnostic ui * convert action response nested to array * prepare action res modal for Alex * ad dproperties action for Bitcoin * feat: add action success dialog (#2753) * feat: add action success dialog * mocks for string action res and hide properties from actions page --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> * return null * remove properties from backend * misc fixes * make severity separate argument * rename ActionRequest to ActionRequestOptions * add clearRequests * fix s9pk build * remove config and properties, introduce action requests * better ux, better moocks, include icons * fix dependency types * add variant for versionCompat * fix dep icon display and patch operation display * misc fixes * misc fixes * alpha 12 * honor provided input to set values in action * fix: show full descriptions of action success items (#2758) * fix type * fix: fix build:deps command on Windows (#2752) * fix: fix build:deps command on Windows * fix: add escaped quotes --------- Co-authored-by: Aiden McClelland <me@drbonez.dev> * misc db compatibility fixes --------- Co-authored-by: Alex Inkin <alexander@inkin.ru> Co-authored-by: Aiden McClelland <me@drbonez.dev> Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
This commit is contained in:
@@ -65,7 +65,6 @@ const ICONS = [
|
||||
'options-outline',
|
||||
'pencil',
|
||||
'phone-portrait-outline',
|
||||
'play-circle-outline',
|
||||
'play-outline',
|
||||
'power',
|
||||
'pricetag-outline',
|
||||
|
||||
@@ -149,14 +149,15 @@ export class FormComponent<T extends Record<string, any>> implements OnInit {
|
||||
}
|
||||
|
||||
private process(operations: Operation[]) {
|
||||
operations.forEach(({ op, path }) => {
|
||||
const control = this.form.get(path.substring(1).split('/'))
|
||||
operations.forEach(operation => {
|
||||
const control = this.form.get(operation.path.substring(1).split('/'))
|
||||
|
||||
if (!control || !control.parent) return
|
||||
|
||||
if (op !== 'remove') {
|
||||
if (operation.op === 'add' || operation.op === 'replace') {
|
||||
control.markAsDirty()
|
||||
control.markAsTouched()
|
||||
control.setValue(operation.value)
|
||||
}
|
||||
|
||||
control.parent.markAsDirty()
|
||||
|
||||
@@ -16,7 +16,7 @@ import { compare } from 'fast-json-patch'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { catchError, defer, EMPTY, endWith, firstValueFrom, map } from 'rxjs'
|
||||
import { InvalidService } from 'src/app/components/form/invalid.service'
|
||||
import { ActionDepComponent } from 'src/app/modals/action-dep.component'
|
||||
import { ActionRequestInfoComponent } from 'src/app/modals/action-request-input.component'
|
||||
import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
@@ -25,23 +25,29 @@ import * as json from 'fast-json-patch'
|
||||
import { ActionService } from '../services/action.service'
|
||||
import { ActionButton, FormComponent } from '../components/form.component'
|
||||
|
||||
export interface PackageActionData {
|
||||
readonly pkgInfo: {
|
||||
export type PackageActionData = {
|
||||
pkgInfo: {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
mainStatus: T.MainStatus['main']
|
||||
}
|
||||
readonly actionInfo: {
|
||||
actionInfo: {
|
||||
id: string
|
||||
warning: string | null
|
||||
metadata: T.ActionMetadata
|
||||
}
|
||||
readonly dependentInfo?: {
|
||||
title: string
|
||||
requestInfo?: {
|
||||
dependentId?: string
|
||||
request: T.ActionRequest
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div class="service-title">
|
||||
<img [src]="pkgInfo.icon" alt="" />
|
||||
<h4>{{ pkgInfo.title }}</h4>
|
||||
</div>
|
||||
<ng-container *ngIf="res$ | async as res; else loading">
|
||||
<tui-notification *ngIf="error" status="error">
|
||||
<div [innerHTML]="error"></div>
|
||||
@@ -52,13 +58,11 @@ export interface PackageActionData {
|
||||
<div [innerHTML]="warning"></div>
|
||||
</tui-notification>
|
||||
|
||||
<action-dep
|
||||
*ngIf="dependentInfo"
|
||||
[pkgTitle]="pkgInfo.title"
|
||||
[depTitle]="dependentInfo.title"
|
||||
<action-request-info
|
||||
*ngIf="requestInfo"
|
||||
[originalValue]="res.originalValue || {}"
|
||||
[operations]="res.operations || []"
|
||||
></action-dep>
|
||||
></action-request-info>
|
||||
|
||||
<app-form
|
||||
tuiMode="onDark"
|
||||
@@ -87,7 +91,19 @@ export interface PackageActionData {
|
||||
`
|
||||
tui-notification {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 1.4rem;
|
||||
}
|
||||
.service-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1.4rem;
|
||||
img {
|
||||
height: 20px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
`,
|
||||
],
|
||||
@@ -98,7 +114,7 @@ export interface PackageActionData {
|
||||
TuiNotificationModule,
|
||||
TuiButtonModule,
|
||||
TuiModeModule,
|
||||
ActionDepComponent,
|
||||
ActionRequestInfoComponent,
|
||||
UiPipeModule,
|
||||
FormComponent,
|
||||
],
|
||||
@@ -106,9 +122,9 @@ export interface PackageActionData {
|
||||
})
|
||||
export class ActionInputModal {
|
||||
readonly actionId = this.context.data.actionInfo.id
|
||||
readonly warning = this.context.data.actionInfo.warning
|
||||
readonly warning = this.context.data.actionInfo.metadata.warning
|
||||
readonly pkgInfo = this.context.data.pkgInfo
|
||||
readonly dependentInfo = this.context.data.dependentInfo
|
||||
readonly requestInfo = this.context.data.requestInfo
|
||||
|
||||
buttons: ActionButton<any>[] = [
|
||||
{
|
||||
@@ -131,12 +147,12 @@ export class ActionInputModal {
|
||||
return {
|
||||
spec: res.spec,
|
||||
originalValue,
|
||||
operations: this.dependentInfo?.request.input
|
||||
operations: this.requestInfo?.request.input
|
||||
? compare(
|
||||
originalValue,
|
||||
JSON.parse(JSON.stringify(originalValue)),
|
||||
utils.deepMerge(
|
||||
originalValue,
|
||||
this.dependentInfo.request.input.value,
|
||||
JSON.parse(JSON.stringify(originalValue)),
|
||||
this.requestInfo.request.input.value,
|
||||
) as object,
|
||||
)
|
||||
: null,
|
||||
@@ -159,15 +175,7 @@ export class ActionInputModal {
|
||||
|
||||
async execute(input: object) {
|
||||
if (await this.checkConflicts(input)) {
|
||||
const res = await firstValueFrom(this.res$)
|
||||
|
||||
return this.actionService.execute(this.pkgInfo.id, this.actionId, {
|
||||
prev: {
|
||||
spec: res.spec,
|
||||
value: res.originalValue,
|
||||
},
|
||||
curr: input,
|
||||
})
|
||||
return this.actionService.execute(this.pkgInfo.id, this.actionId, input)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +189,7 @@ export class ActionInputModal {
|
||||
Object.values(packages[id].requestedActions).some(
|
||||
({ request, active }) =>
|
||||
!active &&
|
||||
request.severity === 'critical' &&
|
||||
request.packageId === this.pkgInfo.id &&
|
||||
request.actionId === this.actionId &&
|
||||
request.when?.condition === 'input-not-matches' &&
|
||||
|
||||
@@ -11,14 +11,10 @@ import { CommonModule } from '@angular/common'
|
||||
import { TuiNotificationModule } from '@taiga-ui/core'
|
||||
|
||||
@Component({
|
||||
selector: 'action-dep',
|
||||
selector: 'action-request-info',
|
||||
template: `
|
||||
<tui-notification>
|
||||
<h3 style="margin: 0 0 0.5rem; font-size: 1.25rem;">
|
||||
{{ pkgTitle }}
|
||||
</h3>
|
||||
The following modifications have been made to {{ pkgTitle }} to satisfy
|
||||
{{ depTitle }}:
|
||||
<tui-notification *ngIf="diff.length">
|
||||
The following modifications were made:
|
||||
<ul>
|
||||
<li *ngFor="let d of diff" [innerHTML]="d"></li>
|
||||
</ul>
|
||||
@@ -27,14 +23,15 @@ import { TuiNotificationModule } from '@taiga-ui/core'
|
||||
standalone: true,
|
||||
imports: [CommonModule, TuiNotificationModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
styles: [
|
||||
`
|
||||
tui-notification {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class ActionDepComponent implements OnInit {
|
||||
@Input()
|
||||
pkgTitle = ''
|
||||
|
||||
@Input()
|
||||
depTitle = ''
|
||||
|
||||
export class ActionRequestInfoComponent implements OnInit {
|
||||
@Input()
|
||||
originalValue: object = {}
|
||||
|
||||
@@ -68,15 +65,15 @@ export class ActionDepComponent implements OnInit {
|
||||
private getMessage(operation: Operation): string {
|
||||
switch (operation.op) {
|
||||
case 'add':
|
||||
return `Added ${this.getNewValue(operation.value)}`
|
||||
return `added ${this.getNewValue(operation.value)}`
|
||||
case 'remove':
|
||||
return `Removed ${this.getOldValue(operation.path)}`
|
||||
return `removed ${this.getOldValue(operation.path)}`
|
||||
case 'replace':
|
||||
return `Changed from ${this.getOldValue(
|
||||
return `changed from ${this.getOldValue(
|
||||
operation.path,
|
||||
)} to ${this.getNewValue(operation.value)}`
|
||||
default:
|
||||
return `Unknown operation`
|
||||
return `Unknown operation` // unreachable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiFadeModule, TuiTitleModule } from '@taiga-ui/experimental'
|
||||
import { TuiAccordionModule } from '@taiga-ui/kit'
|
||||
import { ActionSuccessItemComponent } from './action-success-item.component'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-action-success-group',
|
||||
template: `
|
||||
<p *ngFor="let item of value?.value">
|
||||
<app-action-success-item
|
||||
*ngIf="isSingle(item)"
|
||||
[value]="item"
|
||||
></app-action-success-item>
|
||||
<tui-accordion-item *ngIf="!isSingle(item)" size="s">
|
||||
<div tuiFade>{{ item.name }}</div>
|
||||
<ng-template tuiAccordionItemContent>
|
||||
<app-action-success-group [value]="item"></app-action-success-group>
|
||||
</ng-template>
|
||||
</tui-accordion-item>
|
||||
</p>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
TuiTitleModule,
|
||||
ActionSuccessItemComponent,
|
||||
TuiAccordionModule,
|
||||
TuiFadeModule,
|
||||
],
|
||||
})
|
||||
export class ActionSuccessGroupComponent {
|
||||
@Input()
|
||||
value?: T.ActionResultV1 & { type: 'object' }
|
||||
|
||||
isSingle(
|
||||
value: T.ActionResultV1,
|
||||
): value is T.ActionResultV1 & { type: 'string' } {
|
||||
return value.type === 'string'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
inject,
|
||||
Input,
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
TuiDialogService,
|
||||
TuiLabelModule,
|
||||
TuiTextfieldComponent,
|
||||
TuiTextfieldControllerModule,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||
import { TuiInputModule } from '@taiga-ui/kit'
|
||||
import { QrCodeModule } from 'ng-qrcode'
|
||||
import { ActionSuccessGroupComponent } from './action-success-group.component'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-action-success-item',
|
||||
template: `
|
||||
<p *ngIf="!parent" class="qr">
|
||||
<ng-container *ngTemplateOutlet="qr"></ng-container>
|
||||
</p>
|
||||
<label [tuiLabel]="value.description">
|
||||
<tui-input
|
||||
[readOnly]="true"
|
||||
[ngModel]="value.value"
|
||||
[tuiTextfieldCustomContent]="actions"
|
||||
>
|
||||
<input
|
||||
tuiTextfield
|
||||
[style.border-inline-end-width.rem]="border"
|
||||
[type]="value.masked && masked ? 'password' : 'text'"
|
||||
/>
|
||||
</tui-input>
|
||||
</label>
|
||||
<ng-template #actions>
|
||||
<button
|
||||
*ngIf="value.masked"
|
||||
tuiIconButton
|
||||
appearance="icon"
|
||||
size="s"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
[iconLeft]="masked ? 'tuiIconEye' : 'tuiIconEyeOff'"
|
||||
[style.pointer-events]="'auto'"
|
||||
(click)="masked = !masked"
|
||||
>
|
||||
Reveal/Hide
|
||||
</button>
|
||||
<button
|
||||
*ngIf="value.copyable"
|
||||
tuiIconButton
|
||||
appearance="icon"
|
||||
size="s"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
iconLeft="tuiIconCopy"
|
||||
[style.pointer-events]="'auto'"
|
||||
(click)="copy()"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
<button
|
||||
*ngIf="value.qr && parent"
|
||||
tuiIconButton
|
||||
appearance="icon"
|
||||
size="s"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
iconLeft="tuiIconGrid"
|
||||
[style.pointer-events]="'auto'"
|
||||
(click)="show(qr)"
|
||||
>
|
||||
Show QR
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template #qr>
|
||||
<qr-code
|
||||
[value]="value.value"
|
||||
[style.filter]="value.masked && masked ? 'blur(0.5rem)' : null"
|
||||
size="350"
|
||||
></qr-code>
|
||||
<button
|
||||
*ngIf="value.masked && masked"
|
||||
tuiIconButton
|
||||
class="reveal"
|
||||
iconLeft="tuiIconEye"
|
||||
[style.border-radius.%]="100"
|
||||
(click)="masked = false"
|
||||
>
|
||||
Reveal
|
||||
</button>
|
||||
</ng-template>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
styles: [
|
||||
`
|
||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||
|
||||
.reveal {
|
||||
@include center-all();
|
||||
}
|
||||
|
||||
.qr {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
TuiInputModule,
|
||||
TuiTextfieldControllerModule,
|
||||
TuiButtonModule,
|
||||
QrCodeModule,
|
||||
TuiLabelModule,
|
||||
],
|
||||
})
|
||||
export class ActionSuccessItemComponent {
|
||||
@ViewChild(TuiTextfieldComponent, { read: ElementRef })
|
||||
private readonly input!: ElementRef<HTMLInputElement>
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
|
||||
readonly parent = inject(ActionSuccessGroupComponent, {
|
||||
optional: true,
|
||||
})
|
||||
|
||||
@Input()
|
||||
value!: T.ActionResultV1 & { type: 'string' }
|
||||
|
||||
masked = true
|
||||
|
||||
get border(): number {
|
||||
let border = 0
|
||||
|
||||
if (this.value.masked) {
|
||||
border += 2
|
||||
}
|
||||
|
||||
if (this.value.copyable) {
|
||||
border += 2
|
||||
}
|
||||
|
||||
if (this.value.qr && this.parent) {
|
||||
border += 2
|
||||
}
|
||||
|
||||
return border
|
||||
}
|
||||
|
||||
show(template: TemplateRef<any>) {
|
||||
const masked = this.masked
|
||||
|
||||
this.masked = this.value.masked
|
||||
this.dialogs
|
||||
.open(template, { label: 'Scan this QR', size: 's' })
|
||||
.subscribe({
|
||||
complete: () => (this.masked = masked),
|
||||
})
|
||||
}
|
||||
|
||||
copy() {
|
||||
const el = this.input.nativeElement
|
||||
|
||||
if (!el) {
|
||||
return
|
||||
}
|
||||
|
||||
el.type = 'text'
|
||||
el.focus()
|
||||
el.select()
|
||||
el.ownerDocument.execCommand('copy')
|
||||
el.type = this.masked && this.value.masked ? 'password' : 'text'
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { ActionSuccessPage } from './action-success.page'
|
||||
import { QrCodeModule } from 'ng-qrcode'
|
||||
|
||||
@NgModule({
|
||||
declarations: [ActionSuccessPage],
|
||||
imports: [CommonModule, IonicModule, QrCodeModule],
|
||||
exports: [ActionSuccessPage],
|
||||
})
|
||||
export class ActionSuccessPageModule {}
|
||||
@@ -1,35 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Execution Complete</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="dismiss()" class="enter-click">
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<h2 class="ion-padding">{{ actionRes.message }}</h2>
|
||||
|
||||
<div *ngIf="actionRes.value" class="ion-text-center" style="padding: 48px 0">
|
||||
<div *ngIf="actionRes.qr" class="ion-padding-bottom">
|
||||
<qr-code [value]="actionRes.value" size="240"></qr-code>
|
||||
</div>
|
||||
|
||||
<p *ngIf="!actionRes.copyable">{{ actionRes.value }}</p>
|
||||
<a
|
||||
*ngIf="actionRes.copyable"
|
||||
style="cursor: copy"
|
||||
(click)="copy(actionRes.value)"
|
||||
>
|
||||
<b>{{ actionRes.value }}</b>
|
||||
<sup>
|
||||
<ion-icon
|
||||
name="copy-outline"
|
||||
style="padding-left: 6px; font-size: small"
|
||||
></ion-icon>
|
||||
</sup>
|
||||
</a>
|
||||
</div>
|
||||
</ion-content>
|
||||
@@ -1,39 +1,36 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController, ToastController } from '@ionic/angular'
|
||||
import { copyToClipboard } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { TuiDialogContext, TuiTextfieldControllerModule } from '@taiga-ui/core'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ActionSuccessGroupComponent } from './action-success-group.component'
|
||||
import { ActionSuccessItemComponent } from './action-success-item.component'
|
||||
|
||||
@Component({
|
||||
selector: 'action-success',
|
||||
templateUrl: './action-success.page.html',
|
||||
styleUrls: ['./action-success.page.scss'],
|
||||
standalone: true,
|
||||
template: `
|
||||
<ng-container tuiTextfieldSize="m" [tuiTextfieldLabelOutside]="true">
|
||||
<app-action-success-item
|
||||
*ngIf="item"
|
||||
[value]="item"
|
||||
></app-action-success-item>
|
||||
<app-action-success-group
|
||||
*ngIf="group"
|
||||
[value]="group"
|
||||
></app-action-success-group>
|
||||
</ng-container>
|
||||
`,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ActionSuccessGroupComponent,
|
||||
ActionSuccessItemComponent,
|
||||
TuiTextfieldControllerModule,
|
||||
],
|
||||
})
|
||||
export class ActionSuccessPage {
|
||||
@Input()
|
||||
actionRes!: T.ActionResult
|
||||
readonly data =
|
||||
inject<TuiDialogContext<void, RR.ActionRes>>(POLYMORPHEUS_CONTEXT).data
|
||||
|
||||
constructor(
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly toastCtrl: ToastController,
|
||||
) {}
|
||||
|
||||
async copy(address: string) {
|
||||
let message = ''
|
||||
await copyToClipboard(address || '').then(success => {
|
||||
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 dismiss() {
|
||||
return this.modalCtrl.dismiss()
|
||||
}
|
||||
readonly item = this.data?.type === 'string' ? this.data : null
|
||||
readonly group = this.data?.type === 'object' ? this.data : null
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { AppActionsPage, AppActionsItemComponent } from './app-actions.page'
|
||||
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
|
||||
import { SharedPipesModule } from '@start9labs/shared'
|
||||
import { ActionSuccessPageModule } from 'src/app/modals/action-success/action-success.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -21,7 +20,6 @@ const routes: Routes = [
|
||||
RouterModule.forChild(routes),
|
||||
QRComponentModule,
|
||||
SharedPipesModule,
|
||||
ActionSuccessPageModule,
|
||||
],
|
||||
declarations: [AppActionsPage, AppActionsItemComponent],
|
||||
})
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
<app-actions-item
|
||||
*ngFor="let action of pkg.actions"
|
||||
[action]="action"
|
||||
icon="play-circle-outline"
|
||||
(click)="handleAction(pkg.mainStatus, pkg.manifest, action)"
|
||||
icon="play-outline"
|
||||
(click)="handleAction(pkg.mainStatus, pkg.icon, pkg.manifest, action)"
|
||||
></app-actions-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
|
||||
@@ -21,13 +21,12 @@ export class AppActionsPage {
|
||||
filter(pkg => pkg.stateInfo.state === 'installed'),
|
||||
map(pkg => ({
|
||||
mainStatus: pkg.status.main,
|
||||
icon: pkg.icon,
|
||||
manifest: getManifest(pkg),
|
||||
actions: Object.keys(pkg.actions)
|
||||
.filter(id => id !== 'config')
|
||||
.map(id => ({
|
||||
id,
|
||||
...pkg.actions[id],
|
||||
})),
|
||||
actions: Object.keys(pkg.actions).map(id => ({
|
||||
id,
|
||||
...pkg.actions[id],
|
||||
})),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -40,13 +39,14 @@ export class AppActionsPage {
|
||||
|
||||
async handleAction(
|
||||
mainStatus: T.MainStatus['main'],
|
||||
icon: string,
|
||||
manifest: T.Manifest,
|
||||
action: T.ActionMetadata & { id: string },
|
||||
) {
|
||||
this.actionService.present(
|
||||
{ id: manifest.id, title: manifest.title, mainStatus },
|
||||
{ id: action.id, metadata: action },
|
||||
)
|
||||
this.actionService.present({
|
||||
pkgInfo: { id: manifest.id, title: manifest.title, icon, mainStatus },
|
||||
actionInfo: { id: action.id, metadata: action },
|
||||
})
|
||||
}
|
||||
|
||||
async rebuild(id: string) {
|
||||
@@ -76,7 +76,7 @@ export class AppActionsItemComponent {
|
||||
get disabledText() {
|
||||
return (
|
||||
typeof this.action.visibility === 'object' &&
|
||||
this.action.visibility.disabled.reason
|
||||
this.action.visibility.disabled
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppMetricsPage } from './app-metrics.page'
|
||||
import { SharedPipesModule } from '@start9labs/shared'
|
||||
import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AppMetricsPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
SharedPipesModule,
|
||||
SkeletonListComponentModule,
|
||||
],
|
||||
declarations: [AppMetricsPage],
|
||||
})
|
||||
export class AppMetricsPageModule {}
|
||||
@@ -1,25 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [defaultHref]="'/services/' + pkgId"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Monitor</ion-title>
|
||||
<ion-title slot="end">
|
||||
<ion-spinner name="dots" class="fader"></ion-spinner>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding with-widgets">
|
||||
<skeleton-list *ngIf="loading"></skeleton-list>
|
||||
<ion-item-group *ngIf="!loading">
|
||||
<ion-item *ngFor="let metric of metrics | keyvalue : asIsOrder">
|
||||
<ion-label>{{ metric.key }}</ion-label>
|
||||
<ion-note *ngIf="metric.value" slot="end" class="metric-note">
|
||||
<ion-text style="color: white">
|
||||
{{ metric.value.value }} {{ metric.value.unit }}
|
||||
</ion-text>
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
@@ -1,3 +0,0 @@
|
||||
.metric-note {
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ErrorService, getPkgId, pauseFor } from '@start9labs/shared'
|
||||
import { Metric } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-metrics',
|
||||
templateUrl: './app-metrics.page.html',
|
||||
styleUrls: ['./app-metrics.page.scss'],
|
||||
})
|
||||
export class AppMetricsPage {
|
||||
loading = true
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
going = false
|
||||
metrics?: Metric
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly embassyApi: ApiService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.startDaemon()
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.stopDaemon()
|
||||
}
|
||||
|
||||
async startDaemon(): Promise<void> {
|
||||
this.going = true
|
||||
while (this.going) {
|
||||
const startTime = Date.now()
|
||||
await this.getMetrics()
|
||||
await pauseFor(Math.max(4000 - (Date.now() - startTime), 0))
|
||||
}
|
||||
}
|
||||
|
||||
stopDaemon() {
|
||||
this.going = false
|
||||
}
|
||||
|
||||
async getMetrics(): Promise<void> {
|
||||
try {
|
||||
this.metrics = await this.embassyApi.getPkgMetrics({ id: this.pkgId })
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
this.stopDaemon()
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
asIsOrder(a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppPropertiesPage } from './app-properties.page'
|
||||
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
|
||||
import { MaskPipeModule } from 'src/app/pipes/mask/mask.module'
|
||||
import {
|
||||
SharedPipesModule,
|
||||
TextSpinnerComponentModule,
|
||||
} from '@start9labs/shared'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AppPropertiesPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
QRComponentModule,
|
||||
SharedPipesModule,
|
||||
TextSpinnerComponentModule,
|
||||
MaskPipeModule,
|
||||
],
|
||||
declarations: [AppPropertiesPage],
|
||||
})
|
||||
export class AppPropertiesPageModule {}
|
||||
@@ -1,119 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [defaultHref]="'/services/' + pkgId"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Properties</ion-title>
|
||||
<ion-buttons *ngIf="!loading" slot="end">
|
||||
<ion-button (click)="refresh()">
|
||||
<ion-icon slot="start" name="refresh"></ion-icon>
|
||||
Refresh
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top with-widgets">
|
||||
<text-spinner
|
||||
*ngIf="loading; else loaded"
|
||||
text="Loading Properties"
|
||||
></text-spinner>
|
||||
|
||||
<ng-template #loaded>
|
||||
<!-- not running -->
|
||||
<ion-item *ngIf="stopped$ | async" class="ion-margin-bottom">
|
||||
<ion-label>
|
||||
<p>
|
||||
<ion-text color="warning">
|
||||
Service is stopped. Information on this page could be inaccurate.
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- no properties -->
|
||||
<ion-item *ngIf="properties | empty">
|
||||
<ion-label>
|
||||
<p>No properties.</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- properties -->
|
||||
<ion-item-group *ngIf="!(properties | empty)">
|
||||
<div *ngFor="let prop of node | keyvalue: asIsOrder">
|
||||
<!-- object -->
|
||||
<ion-item
|
||||
button
|
||||
detail="true"
|
||||
*ngIf="prop.value.type === 'object'"
|
||||
(click)="goToNested(prop.key)"
|
||||
>
|
||||
<ion-button
|
||||
*ngIf="prop.value.description"
|
||||
fill="clear"
|
||||
slot="start"
|
||||
(click)="presentDescription(prop, $event)"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="help-circle-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-label>
|
||||
<h2>{{ prop.key }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- not object -->
|
||||
<ion-item *ngIf="prop.value.type === 'string'">
|
||||
<ion-button
|
||||
*ngIf="prop.value.description"
|
||||
fill="clear"
|
||||
slot="start"
|
||||
(click)="presentDescription(prop, $event)"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="help-circle-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-label>
|
||||
<h2>{{ prop.key }}</h2>
|
||||
<p class="courier-new">
|
||||
{{ prop.value.masked && !unmasked[prop.key] ? (prop.value.value |
|
||||
mask : 64) : prop.value.value }}
|
||||
</p>
|
||||
</ion-label>
|
||||
<div slot="end" *ngIf="prop.value.copyable || prop.value.qr">
|
||||
<ion-button
|
||||
*ngIf="prop.value.masked"
|
||||
fill="clear"
|
||||
(click)="toggleMask(prop.key)"
|
||||
>
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
[name]="unmasked[prop.key] ? 'eye-off-outline' : 'eye-outline'"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button
|
||||
*ngIf="prop.value.qr"
|
||||
fill="clear"
|
||||
(click)="showQR(prop.value.value)"
|
||||
>
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
name="qr-code-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button
|
||||
*ngIf="prop.value.copyable"
|
||||
fill="clear"
|
||||
(click)="copy(prop.value.value)"
|
||||
>
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
name="copy-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
@@ -1,3 +0,0 @@
|
||||
.metric-note {
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import {
|
||||
AlertController,
|
||||
IonBackButtonDelegate,
|
||||
ModalController,
|
||||
NavController,
|
||||
ToastController,
|
||||
} from '@ionic/angular'
|
||||
import { copyToClipboard, ErrorService, getPkgId } from '@start9labs/shared'
|
||||
import { TuiDestroyService } from '@taiga-ui/cdk'
|
||||
import { getValueByPointer } from 'fast-json-patch'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map, takeUntil } from 'rxjs/operators'
|
||||
import { QRComponent } from 'src/app/components/qr/qr.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { PackageProperties } from 'src/app/util/properties.util'
|
||||
|
||||
@Component({
|
||||
selector: 'app-properties',
|
||||
templateUrl: './app-properties.page.html',
|
||||
styleUrls: ['./app-properties.page.scss'],
|
||||
providers: [TuiDestroyService],
|
||||
})
|
||||
export class AppPropertiesPage {
|
||||
loading = true
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
|
||||
pointer = ''
|
||||
node: PackageProperties = {}
|
||||
|
||||
properties: PackageProperties = {}
|
||||
unmasked: { [key: string]: boolean } = {}
|
||||
|
||||
stopped$ = this.patch
|
||||
.watch$('packageData', this.pkgId, 'status', 'main')
|
||||
.pipe(map(status => status === 'stopped'))
|
||||
|
||||
@ViewChild(IonBackButtonDelegate, { static: false })
|
||||
backButton?: IonBackButtonDelegate
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly destroy$: TuiDestroyService,
|
||||
) {}
|
||||
|
||||
ionViewDidEnter() {
|
||||
if (!this.backButton) return
|
||||
this.backButton.onClick = () => {
|
||||
history.back()
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.getProperties()
|
||||
|
||||
this.route.queryParams
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(queryParams => {
|
||||
if (queryParams['pointer'] === this.pointer) return
|
||||
this.pointer = queryParams['pointer'] || ''
|
||||
this.node = getValueByPointer(this.properties, this.pointer)
|
||||
})
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await this.getProperties()
|
||||
}
|
||||
|
||||
async presentDescription(
|
||||
property: { key: string; value: PackageProperties[''] },
|
||||
e: Event,
|
||||
) {
|
||||
e.stopPropagation()
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: property.key,
|
||||
message: property.value.description || undefined,
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async goToNested(key: string): Promise<any> {
|
||||
this.navCtrl.navigateForward(`/services/${this.pkgId}/properties`, {
|
||||
queryParams: {
|
||||
pointer: `${this.pointer}/${key}/value`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async copy(text: string): Promise<void> {
|
||||
let message = ''
|
||||
await copyToClipboard(text).then(success => {
|
||||
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 showQR(text: string): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: QRComponent,
|
||||
componentProps: {
|
||||
text,
|
||||
},
|
||||
cssClass: 'qr-modal',
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
toggleMask(key: string) {
|
||||
this.unmasked[key] = !this.unmasked[key]
|
||||
}
|
||||
|
||||
private async getProperties(): Promise<void> {
|
||||
this.loading = true
|
||||
try {
|
||||
this.properties = await this.embassyApi.getPackageProperties({
|
||||
id: this.pkgId,
|
||||
})
|
||||
this.node = getValueByPointer(this.properties, this.pointer)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
asIsOrder(a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ 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 { AppShowErrorComponent } from './components/app-show-error/app-show-error.component'
|
||||
import { AppShowActionRequestsComponent } from './components/app-show-action-requests/app-show-action-requests.component'
|
||||
import { HealthColorPipe } from './pipes/health-color.pipe'
|
||||
import { ToHealthChecksPipe } from './pipes/to-health-checks.pipe'
|
||||
import { ToButtonsPipe } from './pipes/to-buttons.pipe'
|
||||
@@ -45,6 +46,7 @@ const routes: Routes = [
|
||||
AppShowHealthChecksComponent,
|
||||
AppShowAdditionalComponent,
|
||||
AppShowErrorComponent,
|
||||
AppShowActionRequestsComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
<ng-container
|
||||
*ngIf="isInstalled(pkg) && status.primary !== 'backingUp' && status.primary !== 'error'"
|
||||
>
|
||||
<!-- ** action requests ** -->
|
||||
<app-show-action-requests
|
||||
[allPkgs]="pkgPlus.allPkgs"
|
||||
[pkg]="pkg"
|
||||
[manifest]="pkgPlus.manifest"
|
||||
></app-show-action-requests>
|
||||
<!-- ** health checks ** -->
|
||||
<app-show-health-checks
|
||||
*ngIf="pkg.status.main === 'running'"
|
||||
|
||||
@@ -20,14 +20,13 @@ import {
|
||||
import { combineLatest } from 'rxjs'
|
||||
import {
|
||||
getManifest,
|
||||
getPackage,
|
||||
isInstalled,
|
||||
isInstalling,
|
||||
isRestoring,
|
||||
isUpdating,
|
||||
} from 'src/app/util/get-package-data'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { ActionService } from 'src/app/services/action.service'
|
||||
import { getDepDetails } from 'src/app/util/dep-info'
|
||||
|
||||
export interface DependencyInfo {
|
||||
id: string
|
||||
@@ -35,7 +34,7 @@ export interface DependencyInfo {
|
||||
icon: string
|
||||
version: string
|
||||
errorText: string
|
||||
actionText: string
|
||||
actionText: string | null
|
||||
action: () => any
|
||||
}
|
||||
|
||||
@@ -60,6 +59,7 @@ export class AppShowPage {
|
||||
const pkg = allPkgs[this.pkgId]
|
||||
const manifest = getManifest(pkg)
|
||||
return {
|
||||
allPkgs,
|
||||
pkg,
|
||||
manifest,
|
||||
dependencies: this.getDepInfo(pkg, manifest, allPkgs, depErrors),
|
||||
@@ -75,7 +75,6 @@ export class AppShowPage {
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly depErrorService: DepErrorService,
|
||||
private readonly actionService: ActionService,
|
||||
) {}
|
||||
|
||||
showProgress(
|
||||
@@ -95,32 +94,6 @@ export class AppShowPage {
|
||||
)
|
||||
}
|
||||
|
||||
private getDepDetails(
|
||||
pkg: PackageDataEntry,
|
||||
allPkgs: AllPackageData,
|
||||
depId: string,
|
||||
) {
|
||||
const { title, icon, versionRange } = pkg.currentDependencies[depId]
|
||||
|
||||
if (
|
||||
allPkgs[depId] &&
|
||||
(allPkgs[depId].stateInfo.state === 'installed' ||
|
||||
allPkgs[depId].stateInfo.state === 'updating')
|
||||
) {
|
||||
return {
|
||||
title: allPkgs[depId].stateInfo.manifest!.title,
|
||||
icon: allPkgs[depId].icon,
|
||||
versionRange,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
title: title || depId,
|
||||
icon: icon || 'assets/img/service-icons/fallback.png',
|
||||
versionRange,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getDepValues(
|
||||
pkg: PackageDataEntry,
|
||||
allPkgs: AllPackageData,
|
||||
@@ -135,23 +108,16 @@ export class AppShowPage {
|
||||
depErrors,
|
||||
)
|
||||
|
||||
const { title, icon, versionRange } = this.getDepDetails(
|
||||
pkg,
|
||||
allPkgs,
|
||||
depId,
|
||||
)
|
||||
const { title, icon, versionRange } = getDepDetails(pkg, allPkgs, depId)
|
||||
|
||||
return {
|
||||
id: depId,
|
||||
version: versionRange,
|
||||
title,
|
||||
icon,
|
||||
errorText: errorText
|
||||
? `${errorText}. ${manifest.title} will not work as expected.`
|
||||
: '',
|
||||
actionText: fixText || 'View',
|
||||
action:
|
||||
fixAction || (() => this.navCtrl.navigateForward(`/services/${depId}`)),
|
||||
errorText: errorText ? errorText : '',
|
||||
actionText: fixText,
|
||||
action: fixAction,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,28 +131,31 @@ export class AppShowPage {
|
||||
|
||||
let errorText: string | null = null
|
||||
let fixText: string | null = null
|
||||
let fixAction: (() => any) | null = null
|
||||
let fixAction: () => any = () => {}
|
||||
|
||||
if (depError) {
|
||||
if (depError.type === 'notInstalled') {
|
||||
errorText = 'Not installed'
|
||||
fixText = 'Install'
|
||||
fixAction = () => this.fixDep(pkg, manifest, 'install', depId)
|
||||
fixAction = () => this.installDep(pkg, manifest, depId)
|
||||
} else if (depError.type === 'incorrectVersion') {
|
||||
errorText = 'Incorrect version'
|
||||
fixText = 'Update'
|
||||
fixAction = () => this.fixDep(pkg, manifest, 'update', depId)
|
||||
} else if (depError.type === 'configUnsatisfied') {
|
||||
errorText = 'Config not satisfied'
|
||||
fixText = 'Auto config'
|
||||
fixAction = () => this.fixDep(pkg, manifest, 'configure', depId)
|
||||
fixAction = () => this.installDep(pkg, manifest, depId)
|
||||
} else if (depError.type === 'actionRequired') {
|
||||
errorText = 'Action Required (see above)'
|
||||
} else if (depError.type === 'notRunning') {
|
||||
errorText = 'Not running'
|
||||
fixText = 'Start'
|
||||
fixAction = () => this.navCtrl.navigateForward(`/services/${depId}`)
|
||||
} else if (depError.type === 'healthChecksFailed') {
|
||||
errorText = 'Required health check not passing'
|
||||
fixText = 'View'
|
||||
fixAction = () => this.navCtrl.navigateForward(`/services/${depId}`)
|
||||
} else if (depError.type === 'transitive') {
|
||||
errorText = 'Dependency has a dependency issue'
|
||||
fixText = 'View'
|
||||
fixAction = () => this.navCtrl.navigateForward(`/services/${depId}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,41 +166,6 @@ export class AppShowPage {
|
||||
}
|
||||
}
|
||||
|
||||
private async fixDep(
|
||||
pkg: PackageDataEntry,
|
||||
pkgManifest: T.Manifest,
|
||||
action: 'install' | 'update' | 'configure',
|
||||
depId: string,
|
||||
) {
|
||||
switch (action) {
|
||||
case 'install':
|
||||
case 'update':
|
||||
return this.installDep(pkg, pkgManifest, depId)
|
||||
case 'configure':
|
||||
const depPkg = await getPackage(this.patch, depId)
|
||||
if (!depPkg) return
|
||||
|
||||
const depManifest = getManifest(depPkg)
|
||||
return this.actionService.present(
|
||||
{
|
||||
id: depId,
|
||||
title: depManifest.title,
|
||||
mainStatus: depPkg.status.main,
|
||||
},
|
||||
{ id: 'config', metadata: pkg.actions['config'] },
|
||||
{
|
||||
title: pkgManifest.title,
|
||||
request: Object.values(pkg.requestedActions).find(
|
||||
r =>
|
||||
r.active &&
|
||||
r.request.packageId === depId &&
|
||||
r.request.actionId === 'config',
|
||||
)!.request,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private async installDep(
|
||||
pkg: PackageDataEntry,
|
||||
pkgManifest: T.Manifest,
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<ng-container *ngIf="actionRequests.critical.length">
|
||||
<ion-item-divider>Required Actions</ion-item-divider>
|
||||
<ion-item
|
||||
*ngFor="let request of actionRequests.critical"
|
||||
button
|
||||
(click)="handleAction(request)"
|
||||
>
|
||||
<ion-icon slot="start" name="warning-outline" color="warning"></ion-icon>
|
||||
<ion-label>
|
||||
<h2 class="highlighted">{{ request.actionName }}</h2>
|
||||
<p *ngIf="request.dependency" class="dependency">
|
||||
<span class="light">Service:</span>
|
||||
<img [src]="request.dependency.icon" alt="" />
|
||||
{{ request.dependency.title }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="light">Reason:</span>
|
||||
{{ request.reason || 'no reason provided' }}
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="actionRequests.important.length">
|
||||
<ion-item-divider>Requested Actions</ion-item-divider>
|
||||
<ion-item
|
||||
*ngFor="let request of actionRequests.important"
|
||||
button
|
||||
(click)="handleAction(request)"
|
||||
>
|
||||
<ion-icon slot="start" name="play-outline" color="warning"></ion-icon>
|
||||
<ion-label>
|
||||
<h2 class="highlighted">{{ request.actionName }}</h2>
|
||||
<p *ngIf="request.dependency" class="dependency">
|
||||
<span class="light">Service:</span>
|
||||
<img [src]="request.dependency.icon" alt="" />
|
||||
{{ request.dependency.title }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="light">Reason:</span>
|
||||
{{ request.reason || 'no reason provided' }}
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,16 @@
|
||||
.light {
|
||||
color: var(--ion-color-dark);
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
color: var(--ion-color-dark);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dependency {
|
||||
display: inline-flex;
|
||||
img {
|
||||
max-width: 16px;
|
||||
margin: 0 2px 0 5px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { ActionService } from 'src/app/services/action.service'
|
||||
import { getDepDetails } from 'src/app/util/dep-info'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-action-requests',
|
||||
templateUrl: './app-show-action-requests.component.html',
|
||||
styleUrls: ['./app-show-action-requests.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowActionRequestsComponent {
|
||||
@Input()
|
||||
allPkgs!: Record<string, T.PackageDataEntry>
|
||||
|
||||
@Input()
|
||||
pkg!: T.PackageDataEntry
|
||||
|
||||
@Input()
|
||||
manifest!: T.Manifest
|
||||
|
||||
get actionRequests() {
|
||||
const critical: (T.ActionRequest & {
|
||||
actionName: string
|
||||
dependency: {
|
||||
title: string
|
||||
icon: string
|
||||
} | null
|
||||
})[] = []
|
||||
const important: (T.ActionRequest & {
|
||||
actionName: string
|
||||
dependency: {
|
||||
title: string
|
||||
icon: string
|
||||
} | null
|
||||
})[] = []
|
||||
|
||||
Object.values(this.pkg.requestedActions)
|
||||
.filter(r => r.active)
|
||||
.forEach(r => {
|
||||
const self = r.request.packageId === this.manifest.id
|
||||
const toReturn = {
|
||||
...r.request,
|
||||
actionName: self
|
||||
? this.pkg.actions[r.request.actionId].name
|
||||
: this.allPkgs[r.request.packageId]?.actions[r.request.actionId]
|
||||
.name || 'Unknown Action',
|
||||
dependency: self
|
||||
? null
|
||||
: getDepDetails(this.pkg, this.allPkgs, r.request.packageId),
|
||||
}
|
||||
|
||||
if (r.request.severity === 'critical') {
|
||||
critical.push(toReturn)
|
||||
} else {
|
||||
important.push(toReturn)
|
||||
}
|
||||
})
|
||||
|
||||
return { critical, important }
|
||||
}
|
||||
|
||||
constructor(private readonly actionService: ActionService) {}
|
||||
|
||||
async handleAction(request: T.ActionRequest) {
|
||||
const self = request.packageId === this.manifest.id
|
||||
this.actionService.present({
|
||||
pkgInfo: {
|
||||
id: request.packageId,
|
||||
title: self
|
||||
? this.manifest.title
|
||||
: getDepDetails(this.pkg, this.allPkgs, request.packageId).title,
|
||||
mainStatus: self
|
||||
? this.pkg.status.main
|
||||
: this.allPkgs[request.packageId].status.main,
|
||||
icon: self
|
||||
? this.pkg.icon
|
||||
: getDepDetails(this.pkg, this.allPkgs, request.packageId).icon,
|
||||
},
|
||||
actionInfo: {
|
||||
id: request.actionId,
|
||||
metadata:
|
||||
request.packageId === this.manifest.id
|
||||
? this.pkg.actions[request.actionId]
|
||||
: this.allPkgs[request.packageId].actions[request.actionId],
|
||||
},
|
||||
requestInfo: {
|
||||
request,
|
||||
dependentId:
|
||||
request.packageId === this.manifest.id ? undefined : this.manifest.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
<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-item
|
||||
[button]="!!dep.actionText"
|
||||
*ngFor="let dep of dependencies"
|
||||
(click)="dep.action()"
|
||||
>
|
||||
<ion-thumbnail slot="start">
|
||||
<img [src]="dep.icon" alt="" />
|
||||
</ion-thumbnail>
|
||||
|
||||
@@ -52,16 +52,6 @@
|
||||
Start
|
||||
</ion-button>
|
||||
|
||||
<ion-button
|
||||
*ngIf="needsConfig(manifest.id, pkg.requestedActions)"
|
||||
class="action-button"
|
||||
color="warning"
|
||||
(click)="presentModalConfig()"
|
||||
>
|
||||
<ion-icon slot="start" name="construct-outline"></ion-icon>
|
||||
Configure
|
||||
</ion-button>
|
||||
|
||||
<ion-button
|
||||
*ngIf="pkgStatus && interfaces && (interfaces | hasUi) && hosts"
|
||||
class="action-button"
|
||||
|
||||
@@ -3,7 +3,6 @@ import { AlertController } from '@ionic/angular'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { ActionService } from 'src/app/services/action.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import {
|
||||
@@ -19,7 +18,6 @@ import {
|
||||
getAllPackages,
|
||||
getManifest,
|
||||
isInstalled,
|
||||
needsConfig,
|
||||
} from 'src/app/util/get-package-data'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
|
||||
@@ -39,7 +37,6 @@ export class AppShowStatusComponent {
|
||||
PR = PrimaryRendering
|
||||
|
||||
isInstalled = isInstalled
|
||||
needsConfig = needsConfig
|
||||
|
||||
constructor(
|
||||
private readonly alertCtrl: AlertController,
|
||||
@@ -49,7 +46,6 @@ export class AppShowStatusComponent {
|
||||
private readonly launcherService: UiLauncherService,
|
||||
readonly connection$: ConnectionService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly actionService: ActionService,
|
||||
) {}
|
||||
|
||||
get interfaces(): PackageDataEntry['serviceInterfaces'] {
|
||||
@@ -77,14 +73,11 @@ export class AppShowStatusComponent {
|
||||
}
|
||||
|
||||
get canStart(): boolean {
|
||||
return (
|
||||
this.status.primary === 'stopped' &&
|
||||
!Object.keys(this.pkg.requestedActions).length
|
||||
)
|
||||
return this.status.primary === 'stopped'
|
||||
}
|
||||
|
||||
get sigtermTimeout(): string | null {
|
||||
return this.pkgStatus?.main === 'stopping' ? '30s' : null // @dr-bonez TODO
|
||||
return this.pkgStatus?.main === 'stopping' ? '30s' : null // @TODO Aiden
|
||||
}
|
||||
|
||||
launchUi(
|
||||
@@ -94,17 +87,6 @@ export class AppShowStatusComponent {
|
||||
this.launcherService.launch(interfaces, hosts)
|
||||
}
|
||||
|
||||
async presentModalConfig(): Promise<void> {
|
||||
return this.actionService.present(
|
||||
{
|
||||
id: this.manifest.id,
|
||||
title: this.manifest.title,
|
||||
mainStatus: this.pkg.status.main,
|
||||
},
|
||||
{ id: 'config', metadata: this.pkg.actions['config'] },
|
||||
)
|
||||
}
|
||||
|
||||
async tryStart(): Promise<void> {
|
||||
if (this.status.dependency === 'warning') {
|
||||
const depErrMsg = `${this.manifest.title} has unmet dependencies. It will not work as expected.`
|
||||
@@ -221,6 +203,7 @@ export class AppShowStatusComponent {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async presentAlertStart(message: string): Promise<boolean> {
|
||||
return new Promise(async resolve => {
|
||||
const alert = await this.alertCtrl.create({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { AlertController, ModalController, NavController } from '@ionic/angular'
|
||||
import { ModalController, NavController } from '@ionic/angular'
|
||||
import { MarkdownComponent } from '@start9labs/shared'
|
||||
import {
|
||||
DataModel,
|
||||
@@ -8,10 +8,8 @@ import {
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { from, map, Observable, of } from 'rxjs'
|
||||
import { from, map, Observable } from 'rxjs'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { ActionService } from 'src/app/services/action.service'
|
||||
import { needsConfig } from 'src/app/util/get-package-data'
|
||||
|
||||
export interface Button {
|
||||
title: string
|
||||
@@ -33,8 +31,6 @@ export class ToButtonsPipe implements PipeTransform {
|
||||
private readonly apiService: ApiService,
|
||||
private readonly api: ApiService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly actionService: ActionService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
) {}
|
||||
|
||||
transform(pkg: PackageDataEntry<InstalledState>): Button[] {
|
||||
@@ -51,50 +47,12 @@ export class ToButtonsPipe implements PipeTransform {
|
||||
.watch$('ui', 'ackInstructions', manifest.id)
|
||||
.pipe(map(seen => !seen)),
|
||||
},
|
||||
// config
|
||||
{
|
||||
action: async () =>
|
||||
pkg.actions['config']
|
||||
? this.actionService.present(
|
||||
{
|
||||
id: manifest.id,
|
||||
title: manifest.title,
|
||||
mainStatus: pkg.status.main,
|
||||
},
|
||||
{
|
||||
id: 'config',
|
||||
metadata: pkg.actions['config'],
|
||||
},
|
||||
)
|
||||
: this.alertCtrl
|
||||
.create({
|
||||
header: 'No Config',
|
||||
message: `No config options for ${manifest.title} v${manifest.version}`,
|
||||
buttons: ['OK'],
|
||||
})
|
||||
.then(a => a.present()),
|
||||
title: 'Config',
|
||||
description: `Customize ${manifest.title}`,
|
||||
icon: 'options-outline',
|
||||
highlighted$: of(needsConfig(manifest.id, pkg.requestedActions)),
|
||||
},
|
||||
// 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 ${manifest.title}`,
|
||||
description: `All actions for ${manifest.title}`,
|
||||
icon: 'flash-outline',
|
||||
},
|
||||
// interfaces
|
||||
|
||||
@@ -36,20 +36,6 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule),
|
||||
},
|
||||
{
|
||||
path: ':pkgId/metrics',
|
||||
loadChildren: () =>
|
||||
import('./app-metrics/app-metrics.module').then(
|
||||
m => m.AppMetricsPageModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: ':pkgId/properties',
|
||||
loadChildren: () =>
|
||||
import('./app-properties/app-properties.module').then(
|
||||
m => m.AppPropertiesPageModule,
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AlertController, ModalController } from '@ionic/angular'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import {
|
||||
@@ -31,28 +31,16 @@ const allowedStatuses = {
|
||||
export class ActionService {
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly formDialog: FormDialogService,
|
||||
) {}
|
||||
|
||||
async present(
|
||||
pkgInfo: {
|
||||
id: string
|
||||
title: string
|
||||
mainStatus: T.MainStatus['main']
|
||||
},
|
||||
actionInfo: {
|
||||
id: string
|
||||
metadata: T.ActionMetadata
|
||||
},
|
||||
dependentInfo?: {
|
||||
title: string
|
||||
request: T.ActionRequest
|
||||
},
|
||||
) {
|
||||
async present(data: PackageActionData) {
|
||||
const { pkgInfo, actionInfo } = data
|
||||
|
||||
if (
|
||||
allowedStatuses[actionInfo.metadata.allowedStatuses].has(
|
||||
pkgInfo.mainStatus,
|
||||
@@ -61,36 +49,32 @@ export class ActionService {
|
||||
if (actionInfo.metadata.hasInput) {
|
||||
this.formDialog.open<PackageActionData>(ActionInputModal, {
|
||||
label: actionInfo.metadata.name,
|
||||
data: {
|
||||
pkgInfo,
|
||||
actionInfo: {
|
||||
id: actionInfo.id,
|
||||
warning: actionInfo.metadata.warning,
|
||||
},
|
||||
dependentInfo,
|
||||
},
|
||||
data,
|
||||
})
|
||||
} else {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Confirm',
|
||||
message: `Are you sure you want to execute action "${
|
||||
actionInfo.metadata.name
|
||||
}"? ${actionInfo.metadata.warning || ''}`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Execute',
|
||||
handler: () => {
|
||||
this.execute(pkgInfo.id, actionInfo.id)
|
||||
if (actionInfo.metadata.warning) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message: actionInfo.metadata.warning,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
{
|
||||
text: 'Run',
|
||||
handler: () => {
|
||||
this.execute(pkgInfo.id, actionInfo.id)
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
cssClass: 'alert-warning-message',
|
||||
})
|
||||
await alert.present()
|
||||
} else {
|
||||
this.execute(pkgInfo.id, actionInfo.id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const statuses = [...allowedStatuses[actionInfo.metadata.allowedStatuses]]
|
||||
@@ -123,30 +107,24 @@ export class ActionService {
|
||||
async execute(
|
||||
packageId: string,
|
||||
actionId: string,
|
||||
inputs?: {
|
||||
prev: RR.GetActionInputRes
|
||||
curr: object
|
||||
},
|
||||
input?: object,
|
||||
): Promise<boolean> {
|
||||
const loader = this.loader.open('Executing action...').subscribe()
|
||||
const loader = this.loader.open('Loading...').subscribe()
|
||||
|
||||
try {
|
||||
const res = await this.api.runAction({
|
||||
packageId,
|
||||
actionId,
|
||||
prev: inputs?.prev || null,
|
||||
input: inputs?.curr || null,
|
||||
input: input || null,
|
||||
})
|
||||
|
||||
if (res) {
|
||||
const successModal = await this.modalCtrl.create({
|
||||
component: ActionSuccessPage,
|
||||
componentProps: {
|
||||
actionRes: res,
|
||||
},
|
||||
})
|
||||
|
||||
setTimeout(() => successModal.present(), 500)
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(ActionSuccessPage), {
|
||||
label: res.name,
|
||||
data: res,
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
return true // needed to dismiss original modal/alert
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -2,18 +2,13 @@ import {
|
||||
InstalledState,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { Metric, NotificationLevel, RR, ServerNotifications } from './api.types'
|
||||
import { NotificationLevel, RR, ServerNotifications } from './api.types'
|
||||
import { BTC_ICON, LND_ICON, PROXY_ICON, REGISTRY_ICON } from './api-icons'
|
||||
import { Log } from '@start9labs/shared'
|
||||
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
|
||||
import { T, ISB, IST } from '@start9labs/start-sdk'
|
||||
import { GetPackagesRes } from '@start9labs/marketplace'
|
||||
|
||||
const mockBlake3Commitment: T.Blake3Commitment = {
|
||||
hash: 'fakehash',
|
||||
size: 0,
|
||||
}
|
||||
|
||||
const mockMerkleArchiveCommitment: T.MerkleArchiveCommitment = {
|
||||
rootSighash: 'fakehash',
|
||||
rootMaxsize: 0,
|
||||
@@ -880,25 +875,6 @@ export module Mock {
|
||||
}
|
||||
}
|
||||
|
||||
export function getAppMetrics() {
|
||||
const metr: Metric = {
|
||||
Metric1: {
|
||||
value: Math.random(),
|
||||
unit: 'mi/b',
|
||||
},
|
||||
Metric2: {
|
||||
value: Math.random(),
|
||||
unit: '%',
|
||||
},
|
||||
Metric3: {
|
||||
value: 10.1,
|
||||
unit: '%',
|
||||
},
|
||||
}
|
||||
|
||||
return metr
|
||||
}
|
||||
|
||||
export const ServerLogs: Log[] = [
|
||||
{
|
||||
timestamp: '2022-07-28T03:52:54.808769Z',
|
||||
@@ -946,15 +922,6 @@ export module Mock {
|
||||
},
|
||||
}
|
||||
|
||||
export const ActionResponse: T.ActionResult = {
|
||||
version: '0',
|
||||
message:
|
||||
'Password changed successfully. If you lose your new password, you will be lost forever.',
|
||||
value: 'NewPassword1234!',
|
||||
copyable: true,
|
||||
qr: true,
|
||||
}
|
||||
|
||||
export const SshKeys: RR.GetSSHKeysRes = [
|
||||
{
|
||||
createdAt: new Date().toISOString(),
|
||||
@@ -1082,11 +1049,26 @@ export module Mock {
|
||||
},
|
||||
}
|
||||
|
||||
export const PackageProperties: RR.GetPackagePropertiesRes<2> = {
|
||||
version: 2,
|
||||
data: {
|
||||
lndconnect: {
|
||||
export const ActionRes: RR.ActionRes = {
|
||||
version: '1',
|
||||
type: 'string',
|
||||
name: 'New Password',
|
||||
description:
|
||||
'Action was run successfully Action was run successfully Action was run successfully Action was run successfully Action was run successfully',
|
||||
copyable: true,
|
||||
qr: true,
|
||||
masked: true,
|
||||
value: 'iwejdoiewdhbew',
|
||||
}
|
||||
|
||||
export const ActionProperties: RR.ActionRes = {
|
||||
version: '1',
|
||||
type: 'object',
|
||||
name: 'Properties',
|
||||
value: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'LND Connect',
|
||||
description: 'This is some information about the thing.',
|
||||
copyable: true,
|
||||
qr: true,
|
||||
@@ -1094,45 +1076,50 @@ export module Mock {
|
||||
value:
|
||||
'lndconnect://udlyfq2mxa4355pt7cqlrdipnvk2tsl4jtsdw7zaeekenufwcev2wlad.onion:10009?cert=MIICJTCCAcugAwIBAgIRAOyq85fqAiA3U3xOnwhH678wCgYIKoZIzj0EAwIwODEfMB0GAkUEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMB4XDTIwMTAyNjA3MzEyN1oXDTIxMTIyMTA3MzEyN1owODEfMB0GA1UEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKqfhAMMZdY-eFnU5P4bGrQTSx0lo7m8u4V0yYkzUM6jlql_u31_mU2ovLTj56wnZApkEjoPl6fL2yasZA2wiy6OBtTCBsjAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH_BAUwAwEB_zAdBgNVHQ4EFgQUYQ9uIO6spltnVCx4rLFL5BvBF9IwWwYDVR0RBFQwUoIMNTc0OTkwMzIyYzZlgglsb2NhbGhvc3SCBHVuaXiCCnVuaXhwYWNrZXSCB2J1ZmNvbm6HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAGHBKwSAAswCgYIKoZIzj0EAwIDSAAwRQIgVZH2Z2KlyAVY2Q2aIQl0nsvN-OEN49wreFwiBqlxNj4CIQD5_JbpuBFJuf81I5J0FQPtXY-4RppWOPZBb-y6-rkIUQ&macaroon=AgEDbG5kAusBAwoQuA8OUMeQ8Fr2h-f65OdXdRIBMBoWCgdhZGRyZXNzEgRyZWFkEgV3cml0ZRoTCgRpbmZvEgRyZWFkEgV3cml0ZRoXCghpbnZvaWNlcxIEcmVhZBIFd3JpdGUaFAoIbWFjYXJvb24SCGdlbmVyYXRlGhYKB21lc3NhZ2USBHJlYWQSBXdyaXRlGhcKCG9mZmNoYWluEgRyZWFkEgV3cml0ZRoWCgdvbmNoYWluEgRyZWFkEgV3cml0ZRoUCgVwZWVycxIEcmVhZBIFd3JpdGUaGAoGc2lnbmVyEghnZW5lcmF0ZRIEcmVhZAAABiCYsRUoUWuAHAiCSLbBR7b_qULDSl64R8LIU2aqNIyQfA',
|
||||
},
|
||||
Nested: {
|
||||
{
|
||||
type: 'object',
|
||||
name: 'Nested Stuff',
|
||||
description: 'This is a nested thing metric',
|
||||
value: {
|
||||
'Last Name': {
|
||||
value: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'Last Name',
|
||||
description: 'The last name of the user',
|
||||
copyable: true,
|
||||
qr: true,
|
||||
masked: false,
|
||||
value: 'Hill',
|
||||
},
|
||||
Age: {
|
||||
{
|
||||
type: 'string',
|
||||
name: 'Age',
|
||||
description: 'The age of the user',
|
||||
copyable: false,
|
||||
qr: false,
|
||||
masked: false,
|
||||
value: '35',
|
||||
},
|
||||
Password: {
|
||||
{
|
||||
type: 'string',
|
||||
name: 'Password',
|
||||
description: 'A secret password',
|
||||
copyable: true,
|
||||
qr: false,
|
||||
masked: true,
|
||||
value: 'password123',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
'Another Value': {
|
||||
{
|
||||
type: 'string',
|
||||
name: 'Another Value',
|
||||
description: 'Some more information about the service.',
|
||||
copyable: false,
|
||||
qr: true,
|
||||
masked: false,
|
||||
value: 'https://guessagain.com',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const getActionInputSpec = async (): Promise<IST.InputSpec> =>
|
||||
@@ -1692,7 +1679,7 @@ export module Mock {
|
||||
},
|
||||
actions: {
|
||||
config: {
|
||||
name: 'Bitcoin Config',
|
||||
name: 'Set Config',
|
||||
description: 'edit bitcoin.conf',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
@@ -1700,6 +1687,25 @@ export module Mock {
|
||||
hasInput: true,
|
||||
group: null,
|
||||
},
|
||||
properties: {
|
||||
name: 'View Properties',
|
||||
description: 'view important information about Bitcoin',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'any',
|
||||
hasInput: false,
|
||||
group: null,
|
||||
},
|
||||
test: {
|
||||
name: 'Do Another Thing',
|
||||
description:
|
||||
'An example of an action that shows a warning and takes no input',
|
||||
warning: 'careful running this action',
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'only-running',
|
||||
hasInput: false,
|
||||
group: null,
|
||||
},
|
||||
},
|
||||
serviceInterfaces: {
|
||||
ui: {
|
||||
@@ -1859,7 +1865,27 @@ export module Mock {
|
||||
storeExposedDependents: [],
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
requestedActions: {},
|
||||
requestedActions: {
|
||||
'bitcoind-config': {
|
||||
request: {
|
||||
packageId: 'bitcoind',
|
||||
actionId: 'config',
|
||||
severity: 'critical',
|
||||
reason:
|
||||
'You must run Config before starting Bitcoin for the first time',
|
||||
},
|
||||
active: true,
|
||||
},
|
||||
'bitcoind-properties': {
|
||||
request: {
|
||||
packageId: 'bitcoind',
|
||||
actionId: 'properties',
|
||||
severity: 'important',
|
||||
reason: 'Check out all the info about your Bitcoin node',
|
||||
},
|
||||
active: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const bitcoinProxy: PackageDataEntry<InstalledState> = {
|
||||
@@ -1992,7 +2018,27 @@ export module Mock {
|
||||
storeExposedDependents: [],
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
requestedActions: {},
|
||||
requestedActions: {
|
||||
'bitcoind/config': {
|
||||
active: true,
|
||||
request: {
|
||||
packageId: 'bitcoind',
|
||||
actionId: 'config',
|
||||
severity: 'critical',
|
||||
reason: 'LND likes BTC a certain way',
|
||||
input: {
|
||||
kind: 'partial',
|
||||
value: {
|
||||
color: '#ffffff',
|
||||
rpcsettings: {
|
||||
rpcuser: 'lnd',
|
||||
},
|
||||
testnet: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const LocalPkgs: { [key: string]: PackageDataEntry<InstalledState> } =
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Dump } from 'patch-db-client'
|
||||
import { PackagePropertiesVersioned } from 'src/app/util/properties.util'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'
|
||||
import { IST, T } from '@start9labs/start-sdk'
|
||||
@@ -209,19 +208,12 @@ export module RR {
|
||||
|
||||
// package
|
||||
|
||||
export type GetPackagePropertiesReq = { id: string } // package.properties
|
||||
export type GetPackagePropertiesRes<T extends number> =
|
||||
PackagePropertiesVersioned<T>
|
||||
|
||||
export type GetPackageLogsReq = ServerLogsReq & { id: string } // package.logs
|
||||
export type GetPackageLogsRes = LogsRes
|
||||
|
||||
export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow
|
||||
export type FollowPackageLogsRes = FollowServerLogsRes
|
||||
|
||||
export type GetPackageMetricsReq = { id: string } // package.metrics
|
||||
export type GetPackageMetricsRes = Metric
|
||||
|
||||
export type InstallPackageReq = T.InstallParams
|
||||
export type InstallPackageRes = null
|
||||
|
||||
@@ -231,13 +223,12 @@ export module RR {
|
||||
value: object | null
|
||||
}
|
||||
|
||||
export type RunActionReq = {
|
||||
export type ActionReq = {
|
||||
packageId: string
|
||||
actionId: string
|
||||
prev: GetActionInputRes | null
|
||||
input: object | null
|
||||
} // package.action.run
|
||||
export type RunActionRes = T.ActionResult | null
|
||||
export type ActionRes = (T.ActionResult & { version: '1' }) | null
|
||||
|
||||
export type RestorePackagesReq = {
|
||||
// package.backup.restore
|
||||
@@ -494,7 +485,7 @@ export type DependencyError =
|
||||
| DependencyErrorNotInstalled
|
||||
| DependencyErrorNotRunning
|
||||
| DependencyErrorIncorrectVersion
|
||||
| DependencyErrorConfigUnsatisfied
|
||||
| DependencyErrorActionRequired
|
||||
| DependencyErrorHealthChecksFailed
|
||||
| DependencyErrorTransitive
|
||||
|
||||
@@ -512,8 +503,8 @@ export interface DependencyErrorIncorrectVersion {
|
||||
received: string // version
|
||||
}
|
||||
|
||||
export interface DependencyErrorConfigUnsatisfied {
|
||||
type: 'configUnsatisfied'
|
||||
export interface DependencyErrorActionRequired {
|
||||
type: 'actionRequired'
|
||||
}
|
||||
|
||||
export interface DependencyErrorHealthChecksFailed {
|
||||
|
||||
@@ -113,10 +113,6 @@ export abstract class ApiService {
|
||||
params: RR.GetServerMetricsReq,
|
||||
): Promise<RR.GetServerMetricsRes>
|
||||
|
||||
abstract getPkgMetrics(
|
||||
params: RR.GetPackageMetricsReq,
|
||||
): Promise<RR.GetPackageMetricsRes>
|
||||
|
||||
abstract updateServer(url?: string): Promise<RR.UpdateServerRes>
|
||||
|
||||
abstract restartServer(
|
||||
@@ -215,10 +211,6 @@ export abstract class ApiService {
|
||||
|
||||
// package
|
||||
|
||||
abstract getPackageProperties(
|
||||
params: RR.GetPackagePropertiesReq,
|
||||
): Promise<RR.GetPackagePropertiesRes<2>['data']>
|
||||
|
||||
abstract getPackageLogs(
|
||||
params: RR.GetPackageLogsReq,
|
||||
): Promise<RR.GetPackageLogsRes>
|
||||
@@ -235,7 +227,7 @@ export abstract class ApiService {
|
||||
params: RR.GetActionInputReq,
|
||||
): Promise<RR.GetActionInputRes>
|
||||
|
||||
abstract runAction(params: RR.RunActionReq): Promise<RR.RunActionRes>
|
||||
abstract runAction(params: RR.ActionReq): Promise<RR.ActionRes>
|
||||
|
||||
abstract restorePackages(
|
||||
params: RR.RestorePackagesReq,
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import { PATCH_CACHE } from 'src/app/services/patch-db/patch-db-source'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import { RR } from './api.types'
|
||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||
import { ConfigService } from '../config.service'
|
||||
import { webSocket } from 'rxjs/webSocket'
|
||||
import { Observable, filter, firstValueFrom } from 'rxjs'
|
||||
@@ -436,14 +435,6 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
// package
|
||||
|
||||
async getPackageProperties(
|
||||
params: RR.GetPackagePropertiesReq,
|
||||
): Promise<RR.GetPackagePropertiesRes<2>['data']> {
|
||||
return this.rpcRequest({ method: 'package.properties', params }).then(
|
||||
parsePropertiesPermissive,
|
||||
)
|
||||
}
|
||||
|
||||
async getPackageLogs(
|
||||
params: RR.GetPackageLogsReq,
|
||||
): Promise<RR.GetPackageLogsRes> {
|
||||
@@ -456,12 +447,6 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'package.logs.follow', params })
|
||||
}
|
||||
|
||||
async getPkgMetrics(
|
||||
params: RR.GetPackageMetricsReq,
|
||||
): Promise<RR.GetPackageMetricsRes> {
|
||||
return this.rpcRequest({ method: 'package.metrics', params })
|
||||
}
|
||||
|
||||
async installPackage(
|
||||
params: RR.InstallPackageReq,
|
||||
): Promise<RR.InstallPackageRes> {
|
||||
@@ -474,7 +459,7 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'package.action.get-input', params })
|
||||
}
|
||||
|
||||
async runAction(params: RR.RunActionReq): Promise<RR.RunActionRes> {
|
||||
async runAction(params: RR.ActionReq): Promise<RR.ActionRes> {
|
||||
return this.rpcRequest({ method: 'package.action.run', params })
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
UpdatingState,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { CifsBackupTarget, RR } from './api.types'
|
||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||
import { Mock } from './api.fixures'
|
||||
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
|
||||
import {
|
||||
@@ -368,13 +367,6 @@ export class MockApiService extends ApiService {
|
||||
return Mock.getServerMetrics()
|
||||
}
|
||||
|
||||
async getPkgMetrics(
|
||||
params: RR.GetServerMetricsReq,
|
||||
): Promise<RR.GetPackageMetricsRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.getAppMetrics()
|
||||
}
|
||||
|
||||
async updateServer(url?: string): Promise<RR.UpdateServerRes> {
|
||||
await pauseFor(2000)
|
||||
const initialProgress = {
|
||||
@@ -707,13 +699,6 @@ export class MockApiService extends ApiService {
|
||||
|
||||
// package
|
||||
|
||||
async getPackageProperties(
|
||||
params: RR.GetPackagePropertiesReq,
|
||||
): Promise<RR.GetPackagePropertiesRes<2>['data']> {
|
||||
await pauseFor(2000)
|
||||
return parsePropertiesPermissive(Mock.PackageProperties)
|
||||
}
|
||||
|
||||
async getPackageLogs(
|
||||
params: RR.GetPackageLogsReq,
|
||||
): Promise<RR.GetPackageLogsRes> {
|
||||
@@ -795,9 +780,23 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async runAction(params: RR.RunActionReq): Promise<RR.RunActionRes> {
|
||||
async runAction(params: RR.ActionReq): Promise<RR.ActionRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.ActionResponse
|
||||
|
||||
if (params.actionId === 'properties') {
|
||||
return Mock.ActionProperties
|
||||
} else if (params.actionId === 'config') {
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.packageId}/requestedActions/${params.packageId}-config`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
return null
|
||||
} else {
|
||||
return Mock.ActionRes
|
||||
}
|
||||
}
|
||||
|
||||
async restorePackages(
|
||||
|
||||
@@ -61,6 +61,7 @@ export const mockPatchData: DataModel = {
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
versionCompat: '>=0.3.0 <=0.3.6',
|
||||
postInitMigrationTodos: [],
|
||||
statusInfo: {
|
||||
backupProgress: null,
|
||||
updated: false,
|
||||
@@ -82,7 +83,6 @@ export const mockPatchData: DataModel = {
|
||||
selected: null,
|
||||
lastRegion: null,
|
||||
},
|
||||
postInitMigrationTodos: [],
|
||||
},
|
||||
packageData: {
|
||||
bitcoind: {
|
||||
@@ -107,7 +107,7 @@ export const mockPatchData: DataModel = {
|
||||
// },
|
||||
actions: {
|
||||
config: {
|
||||
name: 'Bitcoin Config',
|
||||
name: 'Set Config',
|
||||
description: 'edit bitcoin.conf',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
@@ -115,6 +115,25 @@ export const mockPatchData: DataModel = {
|
||||
hasInput: true,
|
||||
group: null,
|
||||
},
|
||||
properties: {
|
||||
name: 'View Properties',
|
||||
description: 'view important information about Bitcoin',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'any',
|
||||
hasInput: false,
|
||||
group: null,
|
||||
},
|
||||
test: {
|
||||
name: 'Do Another Thing',
|
||||
description:
|
||||
'An example of an action that shows a warning and takes no input',
|
||||
warning: 'careful running this action',
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'only-running',
|
||||
hasInput: false,
|
||||
group: null,
|
||||
},
|
||||
},
|
||||
serviceInterfaces: {
|
||||
ui: {
|
||||
@@ -274,7 +293,27 @@ export const mockPatchData: DataModel = {
|
||||
storeExposedDependents: [],
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
requestedActions: {},
|
||||
requestedActions: {
|
||||
'bitcoind-config': {
|
||||
request: {
|
||||
packageId: 'bitcoind',
|
||||
actionId: 'config',
|
||||
severity: 'critical',
|
||||
reason:
|
||||
'You must run Config before starting Bitcoin for the first time',
|
||||
},
|
||||
active: true,
|
||||
},
|
||||
'bitcoind-properties': {
|
||||
request: {
|
||||
packageId: 'bitcoind',
|
||||
actionId: 'properties',
|
||||
severity: 'important',
|
||||
reason: 'Check out all the info about your Bitcoin node',
|
||||
},
|
||||
active: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
lnd: {
|
||||
stateInfo: {
|
||||
@@ -364,7 +403,27 @@ export const mockPatchData: DataModel = {
|
||||
storeExposedDependents: [],
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
requestedActions: {},
|
||||
requestedActions: {
|
||||
'bitcoind/config': {
|
||||
active: true,
|
||||
request: {
|
||||
packageId: 'bitcoind',
|
||||
actionId: 'config',
|
||||
severity: 'critical',
|
||||
reason: 'LND likes BTC a certain way',
|
||||
input: {
|
||||
kind: 'partial',
|
||||
value: {
|
||||
color: '#ffffff',
|
||||
rpcsettings: {
|
||||
rpcuser: 'lnd',
|
||||
},
|
||||
testnet: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -101,17 +101,17 @@ export class DepErrorService {
|
||||
}
|
||||
}
|
||||
|
||||
// invalid config
|
||||
// action required
|
||||
if (
|
||||
Object.values(pkg.requestedActions).some(
|
||||
a =>
|
||||
a.active &&
|
||||
a.request.packageId === depId &&
|
||||
a.request.actionId === 'config',
|
||||
a.request.severity === 'critical',
|
||||
)
|
||||
) {
|
||||
return {
|
||||
type: 'configUnsatisfied',
|
||||
type: 'actionRequired',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { PkgDependencyErrors } from './dep-error.service'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { getManifest, needsConfig } from '../util/get-package-data'
|
||||
|
||||
export interface PackageStatus {
|
||||
primary: PrimaryStatus
|
||||
@@ -29,8 +28,12 @@ export function renderPkgStatus(
|
||||
}
|
||||
|
||||
function getInstalledPrimaryStatus(pkg: T.PackageDataEntry): PrimaryStatus {
|
||||
if (needsConfig(getManifest(pkg).id, pkg.requestedActions)) {
|
||||
return 'needsConfig'
|
||||
if (
|
||||
Object.values(pkg.requestedActions).some(
|
||||
r => r.active && r.request.severity === 'critical',
|
||||
)
|
||||
) {
|
||||
return 'actionRequired'
|
||||
} else {
|
||||
return pkg.status.main
|
||||
}
|
||||
@@ -79,7 +82,7 @@ export type PrimaryStatus =
|
||||
| 'restarting'
|
||||
| 'stopped'
|
||||
| 'backingUp'
|
||||
| 'needsConfig'
|
||||
| 'actionRequired'
|
||||
| 'error'
|
||||
|
||||
export type DependencyStatus = 'warning' | 'satisfied'
|
||||
@@ -135,8 +138,8 @@ export const PrimaryRendering: Record<PrimaryStatus, StatusRendering> = {
|
||||
color: 'success',
|
||||
showDots: false,
|
||||
},
|
||||
needsConfig: {
|
||||
display: 'Needs Config',
|
||||
actionRequired: {
|
||||
display: 'Action Required',
|
||||
color: 'warning',
|
||||
showDots: false,
|
||||
},
|
||||
|
||||
30
web/projects/ui/src/app/util/dep-info.ts
Normal file
30
web/projects/ui/src/app/util/dep-info.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
AllPackageData,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
|
||||
export function getDepDetails(
|
||||
pkg: PackageDataEntry,
|
||||
allPkgs: AllPackageData,
|
||||
depId: string,
|
||||
) {
|
||||
const { title, icon, versionRange } = pkg.currentDependencies[depId]
|
||||
|
||||
if (
|
||||
allPkgs[depId] &&
|
||||
(allPkgs[depId].stateInfo.state === 'installed' ||
|
||||
allPkgs[depId].stateInfo.state === 'updating')
|
||||
) {
|
||||
return {
|
||||
title: allPkgs[depId].stateInfo.manifest!.title,
|
||||
icon: allPkgs[depId].icon,
|
||||
versionRange,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
title: title || depId,
|
||||
icon: icon || 'assets/img/service-icons/fallback.png',
|
||||
versionRange,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,18 +28,6 @@ export function getManifest(pkg: PackageDataEntry): T.Manifest {
|
||||
return (pkg.stateInfo as InstallingState).installingInfo.newManifest
|
||||
}
|
||||
|
||||
export function needsConfig(
|
||||
pkgId: string,
|
||||
requestedActions: PackageDataEntry['requestedActions'],
|
||||
): boolean {
|
||||
return Object.values(requestedActions).some(
|
||||
r =>
|
||||
r.active &&
|
||||
r.request.packageId === pkgId &&
|
||||
r.request.actionId === 'config',
|
||||
)
|
||||
}
|
||||
|
||||
export function isInstalled(
|
||||
pkg: PackageDataEntry,
|
||||
): pkg is PackageDataEntry<InstalledState> {
|
||||
|
||||
@@ -20,7 +20,7 @@ export function getPackageInfo(
|
||||
primaryRendering,
|
||||
primaryStatus: statuses.primary,
|
||||
error: statuses.health === 'failure' || statuses.dependency === 'warning',
|
||||
warning: statuses.primary === 'needsConfig',
|
||||
warning: statuses.primary === 'actionRequired',
|
||||
transitioning:
|
||||
primaryRendering.showDots ||
|
||||
statuses.health === 'loading' ||
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import { applyOperation } from 'fast-json-patch'
|
||||
import matches, {
|
||||
Parser,
|
||||
shape,
|
||||
string,
|
||||
literal,
|
||||
boolean,
|
||||
deferred,
|
||||
dictionary,
|
||||
anyOf,
|
||||
number,
|
||||
arrayOf,
|
||||
} from 'ts-matches'
|
||||
|
||||
type PropertiesV1 = typeof matchPropertiesV1._TYPE
|
||||
type PackagePropertiesV1 = PropertiesV1[]
|
||||
type PackagePropertiesV2 = {
|
||||
[name: string]: PackagePropertyString | PackagePropertyObject
|
||||
}
|
||||
type PackagePropertiesVersionedData<T extends number> = T extends 1
|
||||
? PackagePropertiesV1
|
||||
: T extends 2
|
||||
? PackagePropertiesV2
|
||||
: never
|
||||
|
||||
type PackagePropertyString = typeof matchPackagePropertyString._TYPE
|
||||
|
||||
export type PackagePropertiesVersioned<T extends number> = {
|
||||
version: T
|
||||
data: PackagePropertiesVersionedData<T>
|
||||
}
|
||||
export type PackageProperties = PackagePropertiesV2
|
||||
|
||||
const matchPropertiesV1 = shape(
|
||||
{
|
||||
name: string,
|
||||
value: string,
|
||||
description: string,
|
||||
copyable: boolean,
|
||||
qr: boolean,
|
||||
},
|
||||
['description', 'copyable', 'qr'],
|
||||
{ copyable: false, qr: false } as const,
|
||||
)
|
||||
|
||||
const [matchPackagePropertiesV2, setPPV2] = deferred<PackagePropertiesV2>()
|
||||
const matchPackagePropertyString = shape(
|
||||
{
|
||||
type: literal('string'),
|
||||
description: string,
|
||||
value: string,
|
||||
copyable: boolean,
|
||||
qr: boolean,
|
||||
masked: boolean,
|
||||
},
|
||||
['description', 'copyable', 'qr', 'masked'],
|
||||
{
|
||||
copyable: false,
|
||||
qr: false,
|
||||
masked: false,
|
||||
} as const,
|
||||
)
|
||||
const matchPackagePropertyObject = shape(
|
||||
{
|
||||
type: literal('object'),
|
||||
value: matchPackagePropertiesV2,
|
||||
description: string,
|
||||
},
|
||||
['description'],
|
||||
)
|
||||
|
||||
const matchPropertyV2 = anyOf(
|
||||
matchPackagePropertyString,
|
||||
matchPackagePropertyObject,
|
||||
)
|
||||
type PackagePropertyObject = typeof matchPackagePropertyObject._TYPE
|
||||
setPPV2(dictionary([string, matchPropertyV2]))
|
||||
|
||||
const matchPackagePropertiesVersionedV1 = shape({
|
||||
version: number,
|
||||
data: arrayOf(matchPropertiesV1),
|
||||
})
|
||||
const matchPackagePropertiesVersionedV2 = shape({
|
||||
version: number,
|
||||
data: dictionary([string, matchPropertyV2]),
|
||||
})
|
||||
|
||||
export function parsePropertiesPermissive(
|
||||
properties: unknown,
|
||||
errorCallback: (err: Error) => any = console.warn,
|
||||
): PackageProperties {
|
||||
return matches(properties)
|
||||
.when(matchPackagePropertiesVersionedV1, prop =>
|
||||
parsePropertiesV1Permissive(prop.data, errorCallback),
|
||||
)
|
||||
.when(matchPackagePropertiesVersionedV2, prop => prop.data)
|
||||
.when(matches.nill, {})
|
||||
.defaultToLazy(() => {
|
||||
errorCallback(new TypeError(`value is not valid`))
|
||||
return {}
|
||||
})
|
||||
}
|
||||
|
||||
function parsePropertiesV1Permissive(
|
||||
properties: unknown,
|
||||
errorCallback: (err: Error) => any,
|
||||
): PackageProperties {
|
||||
if (!Array.isArray(properties)) {
|
||||
errorCallback(new TypeError(`${properties} is not an array`))
|
||||
return {}
|
||||
}
|
||||
return properties.reduce(
|
||||
(prev: PackagePropertiesV2, cur: unknown, idx: number) => {
|
||||
const result = matchPropertiesV1.enumParsed(cur)
|
||||
if ('value' in result) {
|
||||
const value = result.value
|
||||
prev[value.name] = {
|
||||
type: 'string',
|
||||
value: value.value,
|
||||
description: value.description,
|
||||
copyable: value.copyable,
|
||||
qr: value.qr,
|
||||
masked: false,
|
||||
}
|
||||
} else {
|
||||
const error = result.error
|
||||
const message = Parser.validatorErrorAsString(error)
|
||||
const dataPath = error.keys.map(removeQuotes).join('/')
|
||||
errorCallback(new Error(`/data/${idx}: ${message}`))
|
||||
if (dataPath) {
|
||||
applyOperation(cur, {
|
||||
op: 'replace',
|
||||
path: `/${dataPath}`,
|
||||
value: undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
return prev
|
||||
},
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
const removeRegex = /('|")/
|
||||
function removeQuotes(x: string) {
|
||||
while (removeRegex.test(x)) {
|
||||
x = x.replace(removeRegex, '')
|
||||
}
|
||||
return x
|
||||
}
|
||||
Reference in New Issue
Block a user