mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-02 05:23:14 +00:00
Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major
This commit is contained in:
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { endWith, ReplaySubject, shareReplay, Subject, switchMap } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SideloadService {
|
||||
private readonly guid$ = new Subject<string>()
|
||||
|
||||
readonly websocketConnected$ = new ReplaySubject()
|
||||
|
||||
readonly progress$ = this.guid$.pipe(
|
||||
switchMap(guid =>
|
||||
this.api
|
||||
.openWebsocket$<T.FullProgress>(guid, {
|
||||
openObserver: {
|
||||
next: () => this.websocketConnected$.next(''),
|
||||
},
|
||||
})
|
||||
.pipe(endWith(null)),
|
||||
),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
constructor(private readonly api: ApiService) {}
|
||||
|
||||
followProgress(guid: string) {
|
||||
this.guid$.next(guid)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { LoadingService } from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { filter } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
|
||||
@Component({
|
||||
selector: 'diagnostic-home',
|
||||
@@ -25,6 +26,7 @@ export class HomePage {
|
||||
private readonly api: ApiService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
@Inject(WA_WINDOW) private readonly window: Window,
|
||||
readonly config: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
||||
@@ -8,8 +8,7 @@ import {
|
||||
} from '@angular/core'
|
||||
import { FormGroup, ReactiveFormsModule } from '@angular/forms'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import {
|
||||
tuiMarkControlAsTouchedAndValidate,
|
||||
TuiValueChanges,
|
||||
@@ -29,10 +28,10 @@ export interface ActionButton<T> {
|
||||
}
|
||||
|
||||
export interface FormContext<T> {
|
||||
spec: CT.InputSpec
|
||||
spec: IST.InputSpec
|
||||
buttons: ActionButton<T>[]
|
||||
value?: T
|
||||
patch?: Operation[]
|
||||
operations?: Operation[]
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -110,7 +109,7 @@ export class FormComponent<T extends Record<string, any>> implements OnInit {
|
||||
|
||||
@Input() spec = this.context?.data.spec || {}
|
||||
@Input() buttons = this.context?.data.buttons || []
|
||||
@Input() patch = this.context?.data.patch || []
|
||||
@Input() operations = this.context?.data.operations || []
|
||||
@Input() value?: T = this.context?.data.value
|
||||
|
||||
form = new FormGroup({})
|
||||
@@ -118,7 +117,7 @@ export class FormComponent<T extends Record<string, any>> implements OnInit {
|
||||
ngOnInit() {
|
||||
this.confirmService.markAsPristine()
|
||||
this.form = this.formService.createForm(this.spec, this.value)
|
||||
this.process(this.patch)
|
||||
this.process(this.operations)
|
||||
}
|
||||
|
||||
onReset() {
|
||||
@@ -147,15 +146,16 @@ export class FormComponent<T extends Record<string, any>> implements OnInit {
|
||||
this.context?.$implicit.complete()
|
||||
}
|
||||
|
||||
private process(patch: Operation[]) {
|
||||
patch.forEach(({ op, path }) => {
|
||||
const control = this.form.get(path.substring(1).split('/'))
|
||||
private process(operations: Operation[]) {
|
||||
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()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { inject } from '@angular/core'
|
||||
import { FormControlComponent } from './form-control/form-control.component'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
|
||||
export abstract class Control<Spec extends CT.ValueSpec, Value> {
|
||||
export abstract class Control<Spec extends Exclude<IST.ValueSpec, IST.ValueSpecHidden>, Value> {
|
||||
private readonly control: FormControlComponent<Spec, Value> =
|
||||
inject(FormControlComponent)
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { KeyValue } from '@angular/common'
|
||||
|
||||
@Pipe({
|
||||
name: 'filterHidden',
|
||||
})
|
||||
export class FilterHiddenPipe implements PipeTransform {
|
||||
transform(value: KeyValue<string, IST.ValueSpec>[]) {
|
||||
return value.filter(x => x.value.type !== 'hidden') as KeyValue<
|
||||
string,
|
||||
Exclude<IST.ValueSpec, IST.ValueSpecHidden>
|
||||
>[]
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '@taiga-ui/core'
|
||||
import { TUI_CONFIRM } from '@taiga-ui/kit'
|
||||
import { filter } from 'rxjs'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
import { ERRORS } from '../form-group/form-group.component'
|
||||
|
||||
@@ -29,7 +30,7 @@ import { ERRORS } from '../form-group/form-group.component'
|
||||
})
|
||||
export class FormArrayComponent {
|
||||
@Input({ required: true })
|
||||
spec!: CT.ValueSpecList
|
||||
spec!: IST.ValueSpecList
|
||||
|
||||
@HostBinding('@tuiParentStop')
|
||||
readonly animation = tuiToAnimationOptions(inject(TUI_ANIMATIONS_SPEED))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
import { MaskitoOptions } from '@maskito/core'
|
||||
|
||||
@@ -8,7 +8,7 @@ import { MaskitoOptions } from '@maskito/core'
|
||||
templateUrl: './form-color.component.html',
|
||||
styleUrls: ['./form-color.component.scss'],
|
||||
})
|
||||
export class FormColorComponent extends Control<CT.ValueSpecColor, string> {
|
||||
export class FormColorComponent extends Control<IST.ValueSpecColor, string> {
|
||||
readonly mask: MaskitoOptions = {
|
||||
mask: ['#', ...Array(6).fill(/[0-9a-f]/i)],
|
||||
}
|
||||
|
||||
@@ -37,4 +37,4 @@
|
||||
Accept
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { TuiAlertService, TuiDialogContext } from '@taiga-ui/core'
|
||||
import { AbstractTuiNullableControl } from '@taiga-ui/legacy'
|
||||
import { filter } from 'rxjs'
|
||||
import { TuiAlertService, TuiDialogContext } from '@taiga-ui/core'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { ERRORS } from '../form-group/form-group.component'
|
||||
import { FORM_CONTROL_PROVIDERS } from './form-control.providers'
|
||||
|
||||
@@ -22,7 +22,7 @@ import { FORM_CONTROL_PROVIDERS } from './form-control.providers'
|
||||
providers: FORM_CONTROL_PROVIDERS,
|
||||
})
|
||||
export class FormControlComponent<
|
||||
T extends CT.ValueSpec,
|
||||
T extends Exclude<IST.ValueSpec, IST.ValueSpecHidden>,
|
||||
V,
|
||||
> extends AbstractTuiNullableControl<V> {
|
||||
@Input({ required: true })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { forwardRef, Provider } from '@angular/core'
|
||||
import { TUI_VALIDATION_ERRORS } from '@taiga-ui/kit'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { FormControlComponent } from './form-control.component'
|
||||
|
||||
interface ValidatorsPatternError {
|
||||
@@ -12,7 +12,7 @@ export const FORM_CONTROL_PROVIDERS: Provider[] = [
|
||||
{
|
||||
provide: TUI_VALIDATION_ERRORS,
|
||||
deps: [forwardRef(() => FormControlComponent)],
|
||||
useFactory: (control: FormControlComponent<CT.ValueSpec, string>) => ({
|
||||
useFactory: (control: FormControlComponent<Exclude<IST.ValueSpec, IST.ValueSpecHidden>, string>) => ({
|
||||
required: 'Required',
|
||||
pattern: ({ requiredPattern }: ValidatorsPatternError) =>
|
||||
('patterns' in control.spec &&
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
tuiPure,
|
||||
TuiTime,
|
||||
} from '@taiga-ui/cdk'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
@@ -14,7 +14,7 @@ import { Control } from '../control'
|
||||
templateUrl: './form-datetime.component.html',
|
||||
})
|
||||
export class FormDatetimeComponent extends Control<
|
||||
CT.ValueSpecDatetime,
|
||||
IST.ValueSpecDatetime,
|
||||
string
|
||||
> {
|
||||
readonly min = TUI_FIRST_DAY
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<ng-container
|
||||
*ngFor="let entry of spec | keyvalue: asIsOrder"
|
||||
*ngFor="let entry of spec | keyvalue: asIsOrder | filterHidden"
|
||||
[ngSwitch]="entry.value.type"
|
||||
[tuiTextfieldCleaner]="true"
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
Input,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { FORM_GROUP_PROVIDERS } from './form-group.providers'
|
||||
|
||||
export const ERRORS = [
|
||||
@@ -27,7 +27,7 @@ export const ERRORS = [
|
||||
viewProviders: [FORM_GROUP_PROVIDERS],
|
||||
})
|
||||
export class FormGroupComponent {
|
||||
@Input() spec: CT.InputSpec = {}
|
||||
@Input() spec: IST.InputSpec = {}
|
||||
|
||||
asIsOrder() {
|
||||
return 0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
import { invert } from '@start9labs/shared'
|
||||
@@ -9,7 +9,7 @@ import { invert } from '@start9labs/shared'
|
||||
templateUrl: './form-multiselect.component.html',
|
||||
})
|
||||
export class FormMultiselectComponent extends Control<
|
||||
CT.ValueSpecMultiselect,
|
||||
IST.ValueSpecMultiselect,
|
||||
readonly string[]
|
||||
> {
|
||||
private readonly inverted = invert(this.spec.values)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
selector: 'form-number',
|
||||
templateUrl: './form-number.component.html',
|
||||
})
|
||||
export class FormNumberComponent extends Control<CT.ValueSpecNumber, number> {
|
||||
export class FormNumberComponent extends Control<IST.ValueSpecNumber, number> {
|
||||
protected readonly Infinity = Infinity
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import { ControlContainer } from '@angular/forms'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
|
||||
@Component({
|
||||
selector: 'form-object',
|
||||
@@ -17,7 +17,7 @@ import { CT } from '@start9labs/start-sdk'
|
||||
})
|
||||
export class FormObjectComponent {
|
||||
@Input({ required: true })
|
||||
spec!: CT.ValueSpecObject
|
||||
spec!: IST.ValueSpecObject
|
||||
|
||||
@Input()
|
||||
open = false
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="disabled"
|
||||
[readOnly]="readOnly"
|
||||
[tuiTextfieldCleaner]="!spec.required"
|
||||
[tuiTextfieldCleaner]="false"
|
||||
[pseudoInvalid]="invalid"
|
||||
[(ngModel)]="selected"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
{{ spec.name }}*
|
||||
<select
|
||||
tuiSelect
|
||||
[placeholder]="spec.name"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { invert } from '@start9labs/shared'
|
||||
import { Control } from '../control'
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Control } from '../control'
|
||||
selector: 'form-select',
|
||||
templateUrl: './form-select.component.html',
|
||||
})
|
||||
export class FormSelectComponent extends Control<CT.ValueSpecSelect, string> {
|
||||
export class FormSelectComponent extends Control<IST.ValueSpecSelect, string> {
|
||||
private readonly inverted = invert(this.spec.values)
|
||||
|
||||
readonly items = Object.values(this.spec.values)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT, utils } from '@start9labs/start-sdk'
|
||||
import { IST, utils } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
@@ -7,7 +7,7 @@ import { Control } from '../control'
|
||||
templateUrl: './form-text.component.html',
|
||||
styleUrls: ['./form-text.component.scss'],
|
||||
})
|
||||
export class FormTextComponent extends Control<CT.ValueSpecText, string> {
|
||||
export class FormTextComponent extends Control<IST.ValueSpecText, string> {
|
||||
masked = true
|
||||
|
||||
generate() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
@@ -7,6 +7,6 @@ import { Control } from '../control'
|
||||
templateUrl: './form-textarea.component.html',
|
||||
})
|
||||
export class FormTextareaComponent extends Control<
|
||||
CT.ValueSpecTextarea,
|
||||
IST.ValueSpecTextarea,
|
||||
string
|
||||
> {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
@@ -7,4 +7,7 @@ import { Control } from '../control'
|
||||
templateUrl: './form-toggle.component.html',
|
||||
host: { class: 'g-toggle' },
|
||||
})
|
||||
export class FormToggleComponent extends Control<CT.ValueSpecToggle, boolean> {}
|
||||
export class FormToggleComponent extends Control<
|
||||
IST.ValueSpecToggle,
|
||||
boolean
|
||||
> {}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
OnChanges,
|
||||
} from '@angular/core'
|
||||
import { ControlContainer, FormGroupName } from '@angular/forms'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
|
||||
@@ -24,9 +24,9 @@ import { tuiPure } from '@taiga-ui/cdk'
|
||||
})
|
||||
export class FormUnionComponent implements OnChanges {
|
||||
@Input({ required: true })
|
||||
spec!: CT.ValueSpecUnion
|
||||
spec!: IST.ValueSpecUnion
|
||||
|
||||
selectSpec!: CT.ValueSpecSelect
|
||||
selectSpec!: IST.ValueSpecSelect
|
||||
|
||||
private readonly form = inject(FormGroupName)
|
||||
private readonly formService = inject(FormService)
|
||||
|
||||
@@ -49,6 +49,7 @@ import { FormToggleComponent } from './form-toggle/form-toggle.component'
|
||||
import { FormUnionComponent } from './form-union/form-union.component'
|
||||
import { HintPipe } from './hint.pipe'
|
||||
import { MustachePipe } from './mustache.pipe'
|
||||
import { FilterHiddenPipe } from './filter-hidden.pipe'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -100,6 +101,7 @@ import { MustachePipe } from './mustache.pipe'
|
||||
MustachePipe,
|
||||
HintPipe,
|
||||
ControlDirective,
|
||||
FilterHiddenPipe,
|
||||
],
|
||||
exports: [FormGroupComponent],
|
||||
})
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
|
||||
@Pipe({
|
||||
name: 'hint',
|
||||
})
|
||||
export class HintPipe implements PipeTransform {
|
||||
transform(spec: CT.ValueSpec): string {
|
||||
transform(spec: Exclude<IST.ValueSpec, IST.ValueSpecHidden>): string {
|
||||
const hint = []
|
||||
|
||||
if (spec.description) {
|
||||
|
||||
@@ -2,50 +2,46 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
} from '@angular/core'
|
||||
import { TuiNotification } from '@taiga-ui/core'
|
||||
import { compare, getValueByPointer, Operation } from 'fast-json-patch'
|
||||
import { getValueByPointer, Operation } from 'fast-json-patch'
|
||||
import { isObject } from '@start9labs/shared'
|
||||
import { tuiIsNumber } from '@taiga-ui/cdk'
|
||||
import { CommonModule } from '@angular/common'
|
||||
|
||||
@Component({
|
||||
selector: 'config-dep',
|
||||
selector: 'action-request-info',
|
||||
template: `
|
||||
<tui-notification>
|
||||
<h3 style="margin: 0 0 0.5rem; font-size: 1.25rem;">
|
||||
{{ package }}
|
||||
</h3>
|
||||
The following modifications have been made to {{ package }} to satisfy
|
||||
{{ dep }}:
|
||||
<tui-notification *ngIf="diff.length">
|
||||
The following modifications were made:
|
||||
<ul>
|
||||
<li *ngFor="let d of diff" [innerHTML]="d"></li>
|
||||
</ul>
|
||||
To accept these modifications, click "Save".
|
||||
</tui-notification>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [CommonModule, TuiNotification],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
styles: [
|
||||
`
|
||||
tui-notification {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class ConfigDepComponent implements OnChanges {
|
||||
export class ActionRequestInfoComponent implements OnInit {
|
||||
@Input()
|
||||
package = ''
|
||||
originalValue: object = {}
|
||||
|
||||
@Input()
|
||||
dep = ''
|
||||
|
||||
@Input()
|
||||
original: object = {}
|
||||
|
||||
@Input()
|
||||
value: object = {}
|
||||
operations: Operation[] = []
|
||||
|
||||
diff: string[] = []
|
||||
|
||||
ngOnChanges() {
|
||||
this.diff = compare(this.original, this.value).map(
|
||||
ngOnInit() {
|
||||
this.diff = this.operations.map(
|
||||
op => `${this.getPath(op)}: ${this.getMessage(op)}`,
|
||||
)
|
||||
}
|
||||
@@ -69,20 +65,20 @@ export class ConfigDepComponent implements OnChanges {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private getOldValue(path: any): string {
|
||||
const val = getValueByPointer(this.original, path)
|
||||
const val = getValueByPointer(this.originalValue, path)
|
||||
if (['string', 'number', 'boolean'].includes(typeof val)) {
|
||||
return val
|
||||
} else if (isObject(val)) {
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, Inject, ViewChild } from '@angular/core'
|
||||
import {
|
||||
ErrorService,
|
||||
getErrorMessage,
|
||||
isEmptyObject,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { CT, T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
TuiDialogContext,
|
||||
TuiDialogService,
|
||||
TuiLoader,
|
||||
TuiButton,
|
||||
TuiNotification,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiConfirmData, TUI_CONFIRM } from '@taiga-ui/kit'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@taiga-ui/polymorpheus'
|
||||
import { compare, Operation } from 'fast-json-patch'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { endWith, firstValueFrom, Subscription } from 'rxjs'
|
||||
import { ConfigDepComponent } from 'src/app/routes/portal/modals/config-dep.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { hasCurrentDeps } from 'src/app/utils/has-deps'
|
||||
import {
|
||||
getAllPackages,
|
||||
getManifest,
|
||||
getPackage,
|
||||
} from 'src/app/utils/get-package-data'
|
||||
import { Breakages } from 'src/app/services/api/api.types'
|
||||
import { InvalidService } from 'src/app/routes/portal/components/form/invalid.service'
|
||||
import {
|
||||
ActionButton,
|
||||
FormComponent,
|
||||
} from 'src/app/routes/portal/components/form.component'
|
||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||
import { ToManifestPipe } from '../pipes/to-manifest'
|
||||
|
||||
export interface PackageConfigData {
|
||||
readonly pkgId: string
|
||||
readonly dependentInfo?: DependentInfo
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<tui-loader *ngIf="loadingText" size="l" [textContent]="loadingText" />
|
||||
|
||||
<tui-notification
|
||||
*ngIf="!loadingText && (loadingError || !pkg)"
|
||||
status="error"
|
||||
>
|
||||
<div [innerHTML]="loadingError"></div>
|
||||
</tui-notification>
|
||||
|
||||
<ng-container
|
||||
*ngIf="
|
||||
!loadingText && !loadingError && pkg && (pkg | toManifest) as manifest
|
||||
"
|
||||
>
|
||||
<tui-notification *ngIf="success" status="success">
|
||||
{{ manifest.title }} has been automatically configured with recommended
|
||||
defaults. Make whatever changes you want, then click "Save".
|
||||
</tui-notification>
|
||||
|
||||
<config-dep
|
||||
*ngIf="dependentInfo && value && original"
|
||||
[package]="manifest.title"
|
||||
[dep]="dependentInfo.title"
|
||||
[original]="original"
|
||||
[value]="value"
|
||||
/>
|
||||
|
||||
<tui-notification *ngIf="!manifest.hasConfig" status="warning">
|
||||
No config options for {{ manifest.title }} {{ manifest.version }}.
|
||||
</tui-notification>
|
||||
|
||||
<app-form
|
||||
[spec]="spec"
|
||||
[value]="value || {}"
|
||||
[buttons]="buttons"
|
||||
[patch]="patch"
|
||||
>
|
||||
<button
|
||||
tuiButton
|
||||
appearance="flat"
|
||||
type="reset"
|
||||
[style.margin-right]="'auto'"
|
||||
>
|
||||
Reset Defaults
|
||||
</button>
|
||||
</app-form>
|
||||
</ng-container>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
tui-notification {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
`,
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormComponent,
|
||||
TuiLoader,
|
||||
TuiNotification,
|
||||
TuiButton,
|
||||
ConfigDepComponent,
|
||||
ToManifestPipe,
|
||||
],
|
||||
providers: [InvalidService],
|
||||
})
|
||||
export class ConfigModal {
|
||||
@ViewChild(FormComponent)
|
||||
private readonly form?: FormComponent<Record<string, any>>
|
||||
|
||||
readonly pkgId = this.context.data.pkgId
|
||||
readonly dependentInfo = this.context.data.dependentInfo
|
||||
|
||||
loadingError = ''
|
||||
loadingText = this.dependentInfo
|
||||
? `Setting properties to accommodate ${this.dependentInfo.title}`
|
||||
: 'Loading Config'
|
||||
|
||||
pkg?: PackageDataEntry
|
||||
spec: CT.InputSpec = {}
|
||||
patch: Operation[] = []
|
||||
buttons: ActionButton<any>[] = [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: value => this.save(value),
|
||||
},
|
||||
]
|
||||
|
||||
original: object | null = null
|
||||
value: object | null = null
|
||||
|
||||
constructor(
|
||||
@Inject(POLYMORPHEUS_CONTEXT)
|
||||
private readonly context: TuiDialogContext<void, PackageConfigData>,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly patchDb: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
get success(): boolean {
|
||||
return (
|
||||
!!this.form &&
|
||||
!this.form.form.dirty &&
|
||||
!this.original &&
|
||||
!this.pkg?.status?.configured
|
||||
)
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
this.pkg = await getPackage(this.patchDb, this.pkgId)
|
||||
|
||||
if (!this.pkg) {
|
||||
this.loadingError = 'This service does not exist'
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (this.dependentInfo) {
|
||||
const depConfig = await this.embassyApi.dryConfigureDependency({
|
||||
dependencyId: this.pkgId,
|
||||
dependentId: this.dependentInfo.id,
|
||||
})
|
||||
|
||||
this.original = depConfig.oldConfig
|
||||
this.value = depConfig.newConfig || this.original
|
||||
this.spec = depConfig.spec
|
||||
this.patch = compare(this.original, this.value)
|
||||
} else {
|
||||
const { config, spec } = await this.embassyApi.getPackageConfig({
|
||||
id: this.pkgId,
|
||||
})
|
||||
|
||||
this.original = config
|
||||
this.value = config
|
||||
this.spec = spec
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.loadingError = getErrorMessage(e)
|
||||
} finally {
|
||||
this.loadingText = ''
|
||||
}
|
||||
}
|
||||
|
||||
private async save(config: any) {
|
||||
const loader = new Subscription()
|
||||
|
||||
try {
|
||||
if (hasCurrentDeps(this.pkgId, await getAllPackages(this.patchDb))) {
|
||||
await this.configureDeps(config, loader)
|
||||
} else {
|
||||
await this.configure(config, loader)
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async configureDeps(
|
||||
config: Record<string, any>,
|
||||
loader: Subscription,
|
||||
) {
|
||||
loader.unsubscribe()
|
||||
loader.closed = false
|
||||
loader.add(this.loader.open('Checking dependent services...').subscribe())
|
||||
|
||||
const breakages = await this.embassyApi.drySetPackageConfig({
|
||||
id: this.pkgId,
|
||||
config,
|
||||
})
|
||||
|
||||
loader.unsubscribe()
|
||||
loader.closed = false
|
||||
|
||||
if (isEmptyObject(breakages) || (await this.approveBreakages(breakages))) {
|
||||
await this.configure(config, loader)
|
||||
}
|
||||
}
|
||||
|
||||
private async configure(config: Record<string, any>, loader: Subscription) {
|
||||
loader.unsubscribe()
|
||||
loader.closed = false
|
||||
loader.add(this.loader.open('Saving...').subscribe())
|
||||
|
||||
await this.embassyApi.setPackageConfig({ id: this.pkgId, config })
|
||||
this.context.$implicit.complete()
|
||||
}
|
||||
|
||||
private async approveBreakages(breakages: T.PackageId[]): Promise<boolean> {
|
||||
const packages = await getAllPackages(this.patchDb)
|
||||
const message =
|
||||
'As a result of this change, the following services will no longer work properly and may crash:<ul>'
|
||||
const content = `${message}${breakages.map(
|
||||
id => `<li><b>${getManifest(packages[id]).title}</b></li>`,
|
||||
)}</ul>`
|
||||
const data: TuiConfirmData = { content, yes: 'Continue', no: 'Cancel' }
|
||||
|
||||
return firstValueFrom(
|
||||
this.dialogs.open<boolean>(TUI_CONFIRM, { data }).pipe(endWith(false)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,14 @@ import { PackageBackupInfo } from 'src/app/services/api/api.types'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { RecoverOption } from '../types/recover-option'
|
||||
import { Version } from '@start9labs/start-sdk'
|
||||
|
||||
export interface AppRecoverOption extends PackageBackupInfo {
|
||||
id: string
|
||||
checked: boolean
|
||||
installed: boolean
|
||||
newerOS: boolean
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'toOptions',
|
||||
@@ -26,7 +34,10 @@ export class ToOptionsPipe implements PipeTransform {
|
||||
id,
|
||||
installed: !!packageData[id],
|
||||
checked: false,
|
||||
newerStartOs: this.compare(packageBackups[id].osVersion),
|
||||
newerOS:
|
||||
Version.parse(packageBackups[id].osVersion).compare(
|
||||
Version.parse(this.config.version),
|
||||
) === 'greater',
|
||||
}))
|
||||
.sort((a, b) =>
|
||||
b.title.toLowerCase() > a.title.toLowerCase() ? -1 : 1,
|
||||
@@ -34,11 +45,4 @@ export class ToOptionsPipe implements PipeTransform {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private compare(version: string): boolean {
|
||||
// checks to see if backup was made on a newer version of startOS
|
||||
return (
|
||||
this.exver.compareOsVersion(version, this.config.version) === 'greater'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ export interface RecoverOption extends PackageBackupInfo {
|
||||
id: string
|
||||
checked: boolean
|
||||
installed: boolean
|
||||
newerStartOs: boolean
|
||||
newerOs: boolean
|
||||
}
|
||||
|
||||
141
web/projects/ui/src/app/services/action.service.ts
Normal file
141
web/projects/ui/src/app/services/action.service.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import {
|
||||
ActionInputModal,
|
||||
PackageActionData,
|
||||
} from '../modals/action-input.component'
|
||||
|
||||
const allowedStatuses = {
|
||||
'only-running': new Set(['running']),
|
||||
'only-stopped': new Set(['stopped']),
|
||||
any: new Set([
|
||||
'running',
|
||||
'stopped',
|
||||
'restarting',
|
||||
'restoring',
|
||||
'stopping',
|
||||
'starting',
|
||||
'backingUp',
|
||||
]),
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ActionService {
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly formDialog: FormDialogService,
|
||||
) {}
|
||||
|
||||
async present(data: PackageActionData) {
|
||||
const { pkgInfo, actionInfo } = data
|
||||
|
||||
if (
|
||||
allowedStatuses[actionInfo.metadata.allowedStatuses].has(
|
||||
pkgInfo.mainStatus,
|
||||
)
|
||||
) {
|
||||
if (actionInfo.metadata.hasInput) {
|
||||
this.formDialog.open<PackageActionData>(ActionInputModal, {
|
||||
label: actionInfo.metadata.name,
|
||||
data,
|
||||
})
|
||||
} else {
|
||||
if (actionInfo.metadata.warning) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message: actionInfo.metadata.warning,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
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]]
|
||||
const last = statuses.pop()
|
||||
let statusesStr = statuses.join(', ')
|
||||
let error = ''
|
||||
if (statuses.length) {
|
||||
if (statuses.length > 1) {
|
||||
// oxford comma
|
||||
statusesStr += ','
|
||||
}
|
||||
statusesStr += ` or ${last}`
|
||||
} else if (last) {
|
||||
statusesStr = `${last}`
|
||||
} else {
|
||||
error = `There is no status for which this action may be run. This is a bug. Please file an issue with the service maintainer.`
|
||||
}
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Forbidden',
|
||||
message:
|
||||
error ||
|
||||
`Action "${actionInfo.metadata.name}" can only be executed when service is ${statusesStr}`,
|
||||
buttons: ['OK'],
|
||||
cssClass: 'alert-error-message enter-click',
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
}
|
||||
|
||||
async execute(
|
||||
packageId: string,
|
||||
actionId: string,
|
||||
input?: object,
|
||||
): Promise<boolean> {
|
||||
const loader = this.loader.open('Loading...').subscribe()
|
||||
|
||||
try {
|
||||
const res = await this.api.runAction({
|
||||
packageId,
|
||||
actionId,
|
||||
input: input || null,
|
||||
})
|
||||
|
||||
if (!res) return true
|
||||
|
||||
if (res.result) {
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(ActionSuccessPage), {
|
||||
label: res.title,
|
||||
data: res,
|
||||
})
|
||||
.subscribe()
|
||||
} else if (res.message) {
|
||||
this.dialogs.open(res.message, { label: res.title }).subscribe()
|
||||
}
|
||||
return true // needed to dismiss original modal/alert
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false // don't dismiss original modal/alert
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,13 @@ import {
|
||||
PackageDataEntry,
|
||||
ServerStatusInfo,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { RR, ServerMetrics, ServerNotifications } from './api.types'
|
||||
import { 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/utils/configBuilderToSpec'
|
||||
import { T, CB } from '@start9labs/start-sdk'
|
||||
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,
|
||||
@@ -113,7 +108,6 @@ export module Mock {
|
||||
},
|
||||
osVersion: '0.2.12',
|
||||
dependencies: {},
|
||||
hasConfig: true,
|
||||
images: {
|
||||
main: {
|
||||
source: 'packed',
|
||||
@@ -124,7 +118,7 @@ export module Mock {
|
||||
assets: [],
|
||||
volumes: ['main'],
|
||||
hardwareRequirements: {
|
||||
device: {},
|
||||
device: [],
|
||||
arch: null,
|
||||
ram: null,
|
||||
},
|
||||
@@ -171,7 +165,6 @@ export module Mock {
|
||||
s9pk: '',
|
||||
},
|
||||
},
|
||||
hasConfig: true,
|
||||
images: {
|
||||
main: {
|
||||
source: 'packed',
|
||||
@@ -182,7 +175,7 @@ export module Mock {
|
||||
assets: [],
|
||||
volumes: ['main'],
|
||||
hardwareRequirements: {
|
||||
device: {},
|
||||
device: [],
|
||||
arch: null,
|
||||
ram: null,
|
||||
},
|
||||
@@ -222,7 +215,6 @@ export module Mock {
|
||||
s9pk: '',
|
||||
},
|
||||
},
|
||||
hasConfig: false,
|
||||
images: {
|
||||
main: {
|
||||
source: 'packed',
|
||||
@@ -233,7 +225,7 @@ export module Mock {
|
||||
assets: [],
|
||||
volumes: ['main'],
|
||||
hardwareRequirements: {
|
||||
device: {},
|
||||
device: [],
|
||||
arch: null,
|
||||
ram: null,
|
||||
},
|
||||
@@ -262,7 +254,7 @@ export module Mock {
|
||||
'26.1.0:0.1.0': {
|
||||
title: 'Bitcoin Core',
|
||||
description: mockDescription,
|
||||
hardwareRequirements: { arch: null, device: {}, ram: null },
|
||||
hardwareRequirements: { arch: null, device: [], ram: null },
|
||||
license: 'mit',
|
||||
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
|
||||
upstreamRepo: 'https://github.com/bitcoin/bitcoin',
|
||||
@@ -295,7 +287,7 @@ export module Mock {
|
||||
short: 'An alternate fully verifying implementation of Bitcoin',
|
||||
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
|
||||
},
|
||||
hardwareRequirements: { arch: null, device: {}, ram: null },
|
||||
hardwareRequirements: { arch: null, device: [], ram: null },
|
||||
license: 'mit',
|
||||
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
|
||||
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
|
||||
@@ -338,7 +330,7 @@ export module Mock {
|
||||
'26.1.0:0.1.0': {
|
||||
title: 'Bitcoin Core',
|
||||
description: mockDescription,
|
||||
hardwareRequirements: { arch: null, device: {}, ram: null },
|
||||
hardwareRequirements: { arch: null, device: [], ram: null },
|
||||
license: 'mit',
|
||||
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
|
||||
upstreamRepo: 'https://github.com/bitcoin/bitcoin',
|
||||
@@ -371,7 +363,7 @@ export module Mock {
|
||||
short: 'An alternate fully verifying implementation of Bitcoin',
|
||||
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
|
||||
},
|
||||
hardwareRequirements: { arch: null, device: {}, ram: null },
|
||||
hardwareRequirements: { arch: null, device: [], ram: null },
|
||||
license: 'mit',
|
||||
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
|
||||
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
|
||||
@@ -416,7 +408,7 @@ export module Mock {
|
||||
'0.17.5:0': {
|
||||
title: 'LND',
|
||||
description: mockDescription,
|
||||
hardwareRequirements: { arch: null, device: {}, ram: null },
|
||||
hardwareRequirements: { arch: null, device: [], ram: null },
|
||||
license: 'mit',
|
||||
wrapperRepo: 'https://github.com/start9labs/lnd-startos',
|
||||
upstreamRepo: 'https://github.com/lightningnetwork/lnd',
|
||||
@@ -472,7 +464,7 @@ export module Mock {
|
||||
'0.17.4-beta:1.0-alpha': {
|
||||
title: 'LND',
|
||||
description: mockDescription,
|
||||
hardwareRequirements: { arch: null, device: {}, ram: null },
|
||||
hardwareRequirements: { arch: null, device: [], ram: null },
|
||||
license: 'mit',
|
||||
wrapperRepo: 'https://github.com/start9labs/lnd-startos',
|
||||
upstreamRepo: 'https://github.com/lightningnetwork/lnd',
|
||||
@@ -530,7 +522,7 @@ export module Mock {
|
||||
'0.3.2.6:0': {
|
||||
title: 'Bitcoin Proxy',
|
||||
description: mockDescription,
|
||||
hardwareRequirements: { arch: null, device: {}, ram: null },
|
||||
hardwareRequirements: { arch: null, device: [], ram: null },
|
||||
license: 'mit',
|
||||
wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers',
|
||||
upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy',
|
||||
@@ -581,7 +573,7 @@ export module Mock {
|
||||
'27.0.0:1.0.0': {
|
||||
title: 'Bitcoin Core',
|
||||
description: mockDescription,
|
||||
hardwareRequirements: { arch: null, device: {}, ram: null },
|
||||
hardwareRequirements: { arch: null, device: [], ram: null },
|
||||
license: 'mit',
|
||||
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
|
||||
upstreamRepo: 'https://github.com/bitcoin/bitcoin',
|
||||
@@ -614,7 +606,7 @@ export module Mock {
|
||||
short: 'An alternate fully verifying implementation of Bitcoin',
|
||||
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
|
||||
},
|
||||
hardwareRequirements: { arch: null, device: {}, ram: null },
|
||||
hardwareRequirements: { arch: null, device: [], ram: null },
|
||||
license: 'mit',
|
||||
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
|
||||
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
|
||||
@@ -657,7 +649,7 @@ export module Mock {
|
||||
'0.18.0:0.0.1': {
|
||||
title: 'LND',
|
||||
description: mockDescription,
|
||||
hardwareRequirements: { arch: null, device: {}, ram: null },
|
||||
hardwareRequirements: { arch: null, device: [], ram: null },
|
||||
license: 'mit',
|
||||
wrapperRepo: 'https://github.com/start9labs/lnd-startos',
|
||||
upstreamRepo: 'https://github.com/lightningnetwork/lnd',
|
||||
@@ -713,7 +705,7 @@ export module Mock {
|
||||
'0.3.2.7:0': {
|
||||
title: 'Bitcoin Proxy',
|
||||
description: mockDescription,
|
||||
hardwareRequirements: { arch: null, device: {}, ram: null },
|
||||
hardwareRequirements: { arch: null, device: [], ram: null },
|
||||
license: 'mit',
|
||||
wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers',
|
||||
upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy',
|
||||
@@ -957,14 +949,6 @@ export module Mock {
|
||||
},
|
||||
}
|
||||
|
||||
export const ActionResponse: RR.ExecutePackageActionRes = {
|
||||
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(),
|
||||
@@ -1160,45 +1144,128 @@ export module Mock {
|
||||
},
|
||||
}
|
||||
|
||||
export const getInputSpec = async (): Promise<
|
||||
RR.GetPackageConfigRes['spec']
|
||||
> =>
|
||||
export const ActionResMessage: RR.ActionRes = {
|
||||
version: '1',
|
||||
title: 'New Password',
|
||||
message:
|
||||
'Action was run successfully and smoothly and fully and all is good on the western front.',
|
||||
result: null,
|
||||
}
|
||||
|
||||
export const ActionResSingle: RR.ActionRes = {
|
||||
version: '1',
|
||||
title: 'New Password',
|
||||
message:
|
||||
'Action was run successfully and smoothly and fully and all is good on the western front.',
|
||||
result: {
|
||||
type: 'single',
|
||||
copyable: true,
|
||||
qr: true,
|
||||
masked: true,
|
||||
value: 'iwejdoiewdhbew',
|
||||
},
|
||||
}
|
||||
|
||||
export const ActionResGroup: RR.ActionRes = {
|
||||
version: '1',
|
||||
title: 'Properties',
|
||||
message:
|
||||
'Successfully retrieved properties. Here is a bunch of useful information about this service.',
|
||||
result: {
|
||||
type: 'group',
|
||||
value: [
|
||||
{
|
||||
type: 'single',
|
||||
name: 'LND Connect',
|
||||
description: 'This is some information about the thing.',
|
||||
copyable: true,
|
||||
qr: true,
|
||||
masked: true,
|
||||
value:
|
||||
'lndconnect://udlyfq2mxa4355pt7cqlrdipnvk2tsl4jtsdw7zaeekenufwcev2wlad.onion:10009?cert=MIICJTCCAcugAwIBAgIRAOyq85fqAiA3U3xOnwhH678wCgYIKoZIzj0EAwIwODEfMB0GAkUEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMB4XDTIwMTAyNjA3MzEyN1oXDTIxMTIyMTA3MzEyN1owODEfMB0GA1UEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKqfhAMMZdY-eFnU5P4bGrQTSx0lo7m8u4V0yYkzUM6jlql_u31_mU2ovLTj56wnZApkEjoPl6fL2yasZA2wiy6OBtTCBsjAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH_BAUwAwEB_zAdBgNVHQ4EFgQUYQ9uIO6spltnVCx4rLFL5BvBF9IwWwYDVR0RBFQwUoIMNTc0OTkwMzIyYzZlgglsb2NhbGhvc3SCBHVuaXiCCnVuaXhwYWNrZXSCB2J1ZmNvbm6HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAGHBKwSAAswCgYIKoZIzj0EAwIDSAAwRQIgVZH2Z2KlyAVY2Q2aIQl0nsvN-OEN49wreFwiBqlxNj4CIQD5_JbpuBFJuf81I5J0FQPtXY-4RppWOPZBb-y6-rkIUQ&macaroon=AgEDbG5kAusBAwoQuA8OUMeQ8Fr2h-f65OdXdRIBMBoWCgdhZGRyZXNzEgRyZWFkEgV3cml0ZRoTCgRpbmZvEgRyZWFkEgV3cml0ZRoXCghpbnZvaWNlcxIEcmVhZBIFd3JpdGUaFAoIbWFjYXJvb24SCGdlbmVyYXRlGhYKB21lc3NhZ2USBHJlYWQSBXdyaXRlGhcKCG9mZmNoYWluEgRyZWFkEgV3cml0ZRoWCgdvbmNoYWluEgRyZWFkEgV3cml0ZRoUCgVwZWVycxIEcmVhZBIFd3JpdGUaGAoGc2lnbmVyEghnZW5lcmF0ZRIEcmVhZAAABiCYsRUoUWuAHAiCSLbBR7b_qULDSl64R8LIU2aqNIyQfA',
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
name: 'Nested Stuff',
|
||||
description: 'This is a nested thing metric',
|
||||
value: [
|
||||
{
|
||||
type: 'single',
|
||||
name: 'Last Name',
|
||||
description: 'The last name of the user',
|
||||
copyable: true,
|
||||
qr: true,
|
||||
masked: false,
|
||||
value: 'Hill',
|
||||
},
|
||||
{
|
||||
type: 'single',
|
||||
name: 'Age',
|
||||
description: 'The age of the user',
|
||||
copyable: false,
|
||||
qr: false,
|
||||
masked: false,
|
||||
value: '35',
|
||||
},
|
||||
{
|
||||
type: 'single',
|
||||
name: 'Password',
|
||||
description: 'A secret password',
|
||||
copyable: true,
|
||||
qr: false,
|
||||
masked: true,
|
||||
value: 'password123',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'single',
|
||||
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> =>
|
||||
configBuilderToSpec(
|
||||
CB.Config.of({
|
||||
bitcoin: CB.Value.object(
|
||||
ISB.InputSpec.of({
|
||||
bitcoin: ISB.Value.object(
|
||||
{
|
||||
name: 'Bitcoin Settings',
|
||||
description:
|
||||
'RPC and P2P interface configuration options for Bitcoin Core',
|
||||
},
|
||||
CB.Config.of({
|
||||
'bitcoind-p2p': CB.Value.union(
|
||||
ISB.InputSpec.of({
|
||||
'bitcoind-p2p': ISB.Value.union(
|
||||
{
|
||||
name: 'P2P Settings',
|
||||
description:
|
||||
'<p>The Bitcoin Core node to connect to over the peer-to-peer (P2P) interface:</p><ul><li><strong>Bitcoin Core</strong>: The Bitcoin Core service installed on this device</li><li><strong>External Node</strong>: A Bitcoin node running on a different device</li></ul>',
|
||||
required: { default: 'internal' },
|
||||
default: 'internal',
|
||||
},
|
||||
CB.Variants.of({
|
||||
internal: { name: 'Bitcoin Core', spec: CB.Config.of({}) },
|
||||
ISB.Variants.of({
|
||||
internal: { name: 'Bitcoin Core', spec: ISB.InputSpec.of({}) },
|
||||
external: {
|
||||
name: 'External Node',
|
||||
spec: CB.Config.of({
|
||||
'p2p-host': CB.Value.text({
|
||||
spec: ISB.InputSpec.of({
|
||||
'p2p-host': ISB.Value.text({
|
||||
name: 'Public Address',
|
||||
required: {
|
||||
default: null,
|
||||
},
|
||||
required: false,
|
||||
default: null,
|
||||
description:
|
||||
'The public address of your Bitcoin Core server',
|
||||
}),
|
||||
'p2p-port': CB.Value.number({
|
||||
'p2p-port': ISB.Value.number({
|
||||
name: 'P2P Port',
|
||||
description:
|
||||
'The port that your Bitcoin Core P2P server is bound to',
|
||||
required: {
|
||||
default: 8333,
|
||||
},
|
||||
required: true,
|
||||
default: 8333,
|
||||
min: 0,
|
||||
max: 65535,
|
||||
integer: true,
|
||||
@@ -1209,24 +1276,25 @@ export module Mock {
|
||||
),
|
||||
}),
|
||||
),
|
||||
color: CB.Value.color({
|
||||
color: ISB.Value.color({
|
||||
name: 'Color',
|
||||
required: false,
|
||||
default: null,
|
||||
}),
|
||||
datetime: CB.Value.datetime({
|
||||
datetime: ISB.Value.datetime({
|
||||
name: 'Datetime',
|
||||
required: false,
|
||||
default: null,
|
||||
}),
|
||||
file: CB.Value.file({
|
||||
name: 'File',
|
||||
required: false,
|
||||
extensions: ['png', 'pdf'],
|
||||
}),
|
||||
users: CB.Value.multiselect({
|
||||
// file: ISB.Value.file({
|
||||
// name: 'File',
|
||||
// required: false,
|
||||
// extensions: ['png', 'pdf'],
|
||||
// }),
|
||||
users: ISB.Value.multiselect({
|
||||
name: 'Users',
|
||||
default: [],
|
||||
maxLength: 2,
|
||||
disabled: ['matt'],
|
||||
values: {
|
||||
matt: 'Matt Hill',
|
||||
alex: 'Alex Inkin',
|
||||
@@ -1234,25 +1302,22 @@ export module Mock {
|
||||
lucy: 'Lucy',
|
||||
},
|
||||
}),
|
||||
advanced: CB.Value.object(
|
||||
advanced: ISB.Value.object(
|
||||
{
|
||||
name: 'Advanced',
|
||||
description: 'Advanced settings',
|
||||
},
|
||||
CB.Config.of({
|
||||
rpcsettings: CB.Value.object(
|
||||
ISB.InputSpec.of({
|
||||
rpcsettings: ISB.Value.object(
|
||||
{
|
||||
name: 'RPC Settings',
|
||||
description: 'rpc username and password',
|
||||
warning:
|
||||
'Adding RPC users gives them special permissions on your node.',
|
||||
},
|
||||
CB.Config.of({
|
||||
rpcuser2: CB.Value.text({
|
||||
ISB.InputSpec.of({
|
||||
rpcuser2: ISB.Value.text({
|
||||
name: 'RPC Username',
|
||||
required: {
|
||||
default: 'defaultrpcusername',
|
||||
},
|
||||
required: false,
|
||||
default: 'defaultrpcusername',
|
||||
description: 'rpc username',
|
||||
patterns: [
|
||||
{
|
||||
@@ -1261,11 +1326,10 @@ export module Mock {
|
||||
},
|
||||
],
|
||||
}),
|
||||
rpcuser: CB.Value.text({
|
||||
rpcuser: ISB.Value.text({
|
||||
name: 'RPC Username',
|
||||
required: {
|
||||
default: 'defaultrpcusername',
|
||||
},
|
||||
required: true,
|
||||
default: 'defaultrpcusername',
|
||||
description: 'rpc username',
|
||||
patterns: [
|
||||
{
|
||||
@@ -1274,23 +1338,21 @@ export module Mock {
|
||||
},
|
||||
],
|
||||
}),
|
||||
rpcpass: CB.Value.text({
|
||||
rpcpass: ISB.Value.text({
|
||||
name: 'RPC User Password',
|
||||
required: {
|
||||
default: {
|
||||
charset: 'a-z,A-Z,2-9',
|
||||
len: 20,
|
||||
},
|
||||
required: true,
|
||||
default: {
|
||||
charset: 'a-z,A-Z,2-9',
|
||||
len: 20,
|
||||
},
|
||||
description: 'rpc password',
|
||||
}),
|
||||
rpcpass2: CB.Value.text({
|
||||
rpcpass2: ISB.Value.text({
|
||||
name: 'RPC User Password',
|
||||
required: {
|
||||
default: {
|
||||
charset: 'a-z,A-Z,2-9',
|
||||
len: 20,
|
||||
},
|
||||
required: true,
|
||||
default: {
|
||||
charset: 'a-z,A-Z,2-9',
|
||||
len: 20,
|
||||
},
|
||||
description: 'rpc password',
|
||||
}),
|
||||
@@ -1298,15 +1360,15 @@ export module Mock {
|
||||
),
|
||||
}),
|
||||
),
|
||||
testnet: CB.Value.toggle({
|
||||
testnet: ISB.Value.toggle({
|
||||
name: 'Testnet',
|
||||
default: true,
|
||||
description:
|
||||
'<ul><li>determines whether your node is running on testnet or mainnet</li></ul><script src="fake"></script>',
|
||||
warning: 'Chain will have to resync!',
|
||||
}),
|
||||
'object-list': CB.Value.list(
|
||||
CB.List.obj(
|
||||
'object-list': ISB.Value.list(
|
||||
ISB.List.obj(
|
||||
{
|
||||
name: 'Object List',
|
||||
minLength: 0,
|
||||
@@ -1318,19 +1380,19 @@ export module Mock {
|
||||
description: 'This is a list of objects, like users or something',
|
||||
},
|
||||
{
|
||||
spec: CB.Config.of({
|
||||
'first-name': CB.Value.text({
|
||||
spec: ISB.InputSpec.of({
|
||||
'first-name': ISB.Value.text({
|
||||
name: 'First Name',
|
||||
required: false,
|
||||
description: 'User first name',
|
||||
default: 'Matt',
|
||||
}),
|
||||
'last-name': CB.Value.text({
|
||||
'last-name': ISB.Value.text({
|
||||
name: 'Last Name',
|
||||
required: {
|
||||
default: {
|
||||
charset: 'a-g,2-9',
|
||||
len: 12,
|
||||
},
|
||||
required: true,
|
||||
default: {
|
||||
charset: 'a-g,2-9',
|
||||
len: 12,
|
||||
},
|
||||
description: 'User first name',
|
||||
patterns: [
|
||||
@@ -1340,11 +1402,12 @@ export module Mock {
|
||||
},
|
||||
],
|
||||
}),
|
||||
age: CB.Value.number({
|
||||
age: ISB.Value.number({
|
||||
name: 'Age',
|
||||
description: 'The age of the user',
|
||||
warning: 'User must be at least 18.',
|
||||
required: false,
|
||||
default: null,
|
||||
min: 18,
|
||||
integer: false,
|
||||
}),
|
||||
@@ -1354,8 +1417,8 @@ export module Mock {
|
||||
},
|
||||
),
|
||||
),
|
||||
'union-list': CB.Value.list(
|
||||
CB.List.obj(
|
||||
'union-list': ISB.Value.list(
|
||||
ISB.List.obj(
|
||||
{
|
||||
name: 'Union List',
|
||||
minLength: 0,
|
||||
@@ -1365,32 +1428,29 @@ export module Mock {
|
||||
warning: 'If you change this, things may work.',
|
||||
},
|
||||
{
|
||||
spec: CB.Config.of({
|
||||
spec: ISB.InputSpec.of({
|
||||
/* TODO: Convert range for this value ([0, 2])*/
|
||||
union: CB.Value.union(
|
||||
union: ISB.Value.union(
|
||||
{
|
||||
name: 'Preference',
|
||||
description: null,
|
||||
warning: null,
|
||||
required: { default: 'summer' },
|
||||
default: 'summer',
|
||||
},
|
||||
CB.Variants.of({
|
||||
ISB.Variants.of({
|
||||
summer: {
|
||||
name: 'summer',
|
||||
spec: CB.Config.of({
|
||||
'favorite-tree': CB.Value.text({
|
||||
spec: ISB.InputSpec.of({
|
||||
'favorite-tree': ISB.Value.text({
|
||||
name: 'Favorite Tree',
|
||||
required: {
|
||||
default: 'Maple',
|
||||
},
|
||||
required: true,
|
||||
default: 'Maple',
|
||||
description: 'What is your favorite tree?',
|
||||
}),
|
||||
'favorite-flower': CB.Value.select({
|
||||
'favorite-flower': ISB.Value.select({
|
||||
name: 'Favorite Flower',
|
||||
description: 'Select your favorite flower',
|
||||
required: {
|
||||
default: 'none',
|
||||
},
|
||||
default: 'none',
|
||||
values: {
|
||||
none: 'none',
|
||||
red: 'red',
|
||||
@@ -1402,8 +1462,8 @@ export module Mock {
|
||||
},
|
||||
winter: {
|
||||
name: 'winter',
|
||||
spec: CB.Config.of({
|
||||
'like-snow': CB.Value.toggle({
|
||||
spec: ISB.InputSpec.of({
|
||||
'like-snow': ISB.Value.toggle({
|
||||
name: 'Like Snow?',
|
||||
default: true,
|
||||
description: 'Do you like snow or not?',
|
||||
@@ -1417,60 +1477,59 @@ export module Mock {
|
||||
},
|
||||
),
|
||||
),
|
||||
'random-select': CB.Value.select({
|
||||
'random-select': ISB.Value.dynamicSelect(() => ({
|
||||
name: 'Random select',
|
||||
description: 'This is not even real.',
|
||||
warning: 'Be careful changing this!',
|
||||
required: {
|
||||
default: null,
|
||||
},
|
||||
default: 'option1',
|
||||
values: {
|
||||
option1: 'option1',
|
||||
option2: 'option2',
|
||||
option3: 'option3',
|
||||
},
|
||||
disabled: ['option2'],
|
||||
}),
|
||||
})),
|
||||
'favorite-number':
|
||||
/* TODO: Convert range for this value ((-100,100])*/ CB.Value.number({
|
||||
name: 'Favorite Number',
|
||||
description: 'Your favorite number of all time',
|
||||
warning:
|
||||
'Once you set this number, it can never be changed without severe consequences.',
|
||||
required: {
|
||||
/* TODO: Convert range for this value ((-100,100])*/ ISB.Value.number(
|
||||
{
|
||||
name: 'Favorite Number',
|
||||
description: 'Your favorite number of all time',
|
||||
warning:
|
||||
'Once you set this number, it can never be changed without severe consequences.',
|
||||
required: false,
|
||||
default: 7,
|
||||
integer: false,
|
||||
units: 'BTC',
|
||||
},
|
||||
integer: false,
|
||||
units: 'BTC',
|
||||
}),
|
||||
rpcsettings: CB.Value.object(
|
||||
),
|
||||
rpcsettings: ISB.Value.object(
|
||||
{
|
||||
name: 'RPC Settings',
|
||||
description: 'rpc username and password',
|
||||
warning:
|
||||
'Adding RPC users gives them special permissions on your node.',
|
||||
},
|
||||
CB.Config.of({
|
||||
laws: CB.Value.object(
|
||||
ISB.InputSpec.of({
|
||||
laws: ISB.Value.object(
|
||||
{
|
||||
name: 'Laws',
|
||||
description: 'the law of the realm',
|
||||
},
|
||||
CB.Config.of({
|
||||
law1: CB.Value.text({
|
||||
ISB.InputSpec.of({
|
||||
law1: ISB.Value.text({
|
||||
name: 'First Law',
|
||||
required: false,
|
||||
description: 'the first law',
|
||||
default: null,
|
||||
}),
|
||||
law2: CB.Value.text({
|
||||
law2: ISB.Value.text({
|
||||
name: 'Second Law',
|
||||
required: false,
|
||||
description: 'the second law',
|
||||
default: null,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
rulemakers: CB.Value.list(
|
||||
CB.List.obj(
|
||||
rulemakers: ISB.Value.list(
|
||||
ISB.List.obj(
|
||||
{
|
||||
name: 'Rule Makers',
|
||||
minLength: 0,
|
||||
@@ -1478,22 +1537,20 @@ export module Mock {
|
||||
description: 'the people who make the rules',
|
||||
},
|
||||
{
|
||||
spec: CB.Config.of({
|
||||
rulemakername: CB.Value.text({
|
||||
spec: ISB.InputSpec.of({
|
||||
rulemakername: ISB.Value.text({
|
||||
name: 'Rulemaker Name',
|
||||
required: {
|
||||
default: {
|
||||
charset: 'a-g,2-9',
|
||||
len: 12,
|
||||
},
|
||||
required: true,
|
||||
default: {
|
||||
charset: 'a-g,2-9',
|
||||
len: 12,
|
||||
},
|
||||
description: 'the name of the rule maker',
|
||||
}),
|
||||
rulemakerip: CB.Value.text({
|
||||
rulemakerip: ISB.Value.text({
|
||||
name: 'Rulemaker IP',
|
||||
required: {
|
||||
default: '192.168.1.0',
|
||||
},
|
||||
required: true,
|
||||
default: '192.168.1.0',
|
||||
description: 'the ip of the rule maker',
|
||||
patterns: [
|
||||
{
|
||||
@@ -1507,11 +1564,10 @@ export module Mock {
|
||||
},
|
||||
),
|
||||
),
|
||||
rpcuser: CB.Value.text({
|
||||
rpcuser: ISB.Value.text({
|
||||
name: 'RPC Username',
|
||||
required: {
|
||||
default: 'defaultrpcusername',
|
||||
},
|
||||
required: true,
|
||||
default: 'defaultrpcusername',
|
||||
description: 'rpc username',
|
||||
patterns: [
|
||||
{
|
||||
@@ -1520,50 +1576,47 @@ export module Mock {
|
||||
},
|
||||
],
|
||||
}),
|
||||
rpcpass: CB.Value.text({
|
||||
rpcpass: ISB.Value.text({
|
||||
name: 'RPC User Password',
|
||||
required: {
|
||||
default: {
|
||||
charset: 'a-z,A-Z,2-9',
|
||||
len: 20,
|
||||
},
|
||||
required: true,
|
||||
default: {
|
||||
charset: 'a-z,A-Z,2-9',
|
||||
len: 20,
|
||||
},
|
||||
description: 'rpc password',
|
||||
masked: true,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
'bitcoin-node': CB.Value.union(
|
||||
'bitcoin-node': ISB.Value.union(
|
||||
{
|
||||
name: 'Bitcoin Node',
|
||||
description: 'Options<ul><li>Item 1</li><li>Item 2</li></ul>',
|
||||
warning: 'Careful changing this',
|
||||
required: { default: 'internal' },
|
||||
disabled: ['fake'],
|
||||
default: 'internal',
|
||||
},
|
||||
CB.Variants.of({
|
||||
ISB.Variants.of({
|
||||
fake: {
|
||||
name: 'Fake',
|
||||
spec: CB.Config.of({}),
|
||||
spec: ISB.InputSpec.of({}),
|
||||
},
|
||||
internal: {
|
||||
name: 'Internal',
|
||||
spec: CB.Config.of({}),
|
||||
spec: ISB.InputSpec.of({}),
|
||||
},
|
||||
external: {
|
||||
name: 'External',
|
||||
spec: CB.Config.of({
|
||||
'emergency-contact': CB.Value.object(
|
||||
spec: ISB.InputSpec.of({
|
||||
'emergency-contact': ISB.Value.object(
|
||||
{
|
||||
name: 'Emergency Contact',
|
||||
description: 'The person to contact in case of emergency.',
|
||||
},
|
||||
CB.Config.of({
|
||||
name: CB.Value.text({
|
||||
ISB.InputSpec.of({
|
||||
name: ISB.Value.text({
|
||||
name: 'Name',
|
||||
required: {
|
||||
default: null,
|
||||
},
|
||||
required: false,
|
||||
default: null,
|
||||
patterns: [
|
||||
{
|
||||
regex: '^[a-zA-Z]+$',
|
||||
@@ -1571,20 +1624,18 @@ export module Mock {
|
||||
},
|
||||
],
|
||||
}),
|
||||
email: CB.Value.text({
|
||||
email: ISB.Value.text({
|
||||
name: 'Email',
|
||||
inputmode: 'email',
|
||||
required: {
|
||||
default: null,
|
||||
},
|
||||
required: false,
|
||||
default: null,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
'public-domain': CB.Value.text({
|
||||
'public-domain': ISB.Value.text({
|
||||
name: 'Public Domain',
|
||||
required: {
|
||||
default: 'bitcoinnode.com',
|
||||
},
|
||||
required: true,
|
||||
default: 'bitcoinnode.com',
|
||||
description: 'the public address of the node',
|
||||
patterns: [
|
||||
{
|
||||
@@ -1593,11 +1644,10 @@ export module Mock {
|
||||
},
|
||||
],
|
||||
}),
|
||||
'private-domain': CB.Value.text({
|
||||
'private-domain': ISB.Value.text({
|
||||
name: 'Private Domain',
|
||||
required: {
|
||||
default: null,
|
||||
},
|
||||
required: false,
|
||||
default: null,
|
||||
description: 'the private address of the node',
|
||||
masked: true,
|
||||
inputmode: 'url',
|
||||
@@ -1606,31 +1656,31 @@ export module Mock {
|
||||
},
|
||||
}),
|
||||
),
|
||||
port: CB.Value.number({
|
||||
port: ISB.Value.number({
|
||||
name: 'Port',
|
||||
description:
|
||||
'the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444',
|
||||
required: {
|
||||
default: 8333,
|
||||
},
|
||||
required: true,
|
||||
default: 8333,
|
||||
min: 1,
|
||||
max: 9998,
|
||||
step: 1,
|
||||
integer: true,
|
||||
}),
|
||||
'favorite-slogan': CB.Value.text({
|
||||
'favorite-slogan': ISB.Value.text({
|
||||
name: 'Favorite Slogan',
|
||||
generate: {
|
||||
charset: 'a-z,A-Z,2-9',
|
||||
len: 20,
|
||||
},
|
||||
required: false,
|
||||
default: null,
|
||||
description:
|
||||
'You most favorite slogan in the whole world, used for paying you.',
|
||||
masked: true,
|
||||
}),
|
||||
rpcallowip: CB.Value.list(
|
||||
CB.List.text(
|
||||
rpcallowip: ISB.Value.list(
|
||||
ISB.List.text(
|
||||
{
|
||||
name: 'RPC Allowed IPs',
|
||||
minLength: 1,
|
||||
@@ -1652,8 +1702,8 @@ export module Mock {
|
||||
},
|
||||
),
|
||||
),
|
||||
rpcauth: CB.Value.list(
|
||||
CB.List.text(
|
||||
rpcauth: ISB.Value.list(
|
||||
ISB.List.text(
|
||||
{
|
||||
name: 'RPC Auth',
|
||||
description:
|
||||
@@ -1719,14 +1769,40 @@ export module Mock {
|
||||
lastBackup: null,
|
||||
nextBackup: null,
|
||||
status: {
|
||||
configured: true,
|
||||
main: {
|
||||
status: 'running',
|
||||
started: new Date().toISOString(),
|
||||
health: {},
|
||||
main: 'running',
|
||||
started: new Date().toISOString(),
|
||||
health: {},
|
||||
},
|
||||
actions: {
|
||||
config: {
|
||||
name: 'Set Config',
|
||||
description: 'edit bitcoin.conf',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'any',
|
||||
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: { disabled: 'This is temporarily disabled' },
|
||||
allowedStatuses: 'only-running',
|
||||
hasInput: false,
|
||||
group: null,
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
serviceInterfaces: {
|
||||
ui: {
|
||||
id: 'ui',
|
||||
@@ -1886,6 +1962,27 @@ export module Mock {
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
outboundProxy: null,
|
||||
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> = {
|
||||
@@ -1899,10 +1996,7 @@ export module Mock {
|
||||
lastBackup: null,
|
||||
nextBackup: null,
|
||||
status: {
|
||||
configured: false,
|
||||
main: {
|
||||
status: 'stopped',
|
||||
},
|
||||
main: 'stopped',
|
||||
},
|
||||
actions: {},
|
||||
serviceInterfaces: {
|
||||
@@ -1930,7 +2024,6 @@ export module Mock {
|
||||
kind: 'running',
|
||||
versionRange: '>=26.0.0',
|
||||
healthChecks: [],
|
||||
configSatisfied: true,
|
||||
},
|
||||
},
|
||||
hosts: {},
|
||||
@@ -1938,6 +2031,7 @@ export module Mock {
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
outboundProxy: null,
|
||||
requestedActions: {},
|
||||
}
|
||||
|
||||
export const lnd: PackageDataEntry<InstalledState> = {
|
||||
@@ -1951,10 +2045,7 @@ export module Mock {
|
||||
lastBackup: null,
|
||||
nextBackup: null,
|
||||
status: {
|
||||
configured: true,
|
||||
main: {
|
||||
status: 'stopped',
|
||||
},
|
||||
main: 'stopped',
|
||||
},
|
||||
actions: {},
|
||||
serviceInterfaces: {
|
||||
@@ -2017,14 +2108,12 @@ export module Mock {
|
||||
kind: 'running',
|
||||
versionRange: '>=26.0.0',
|
||||
healthChecks: [],
|
||||
configSatisfied: true,
|
||||
},
|
||||
'btc-rpc-proxy': {
|
||||
title: Mock.MockManifestBitcoinProxy.title,
|
||||
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
|
||||
kind: 'exists',
|
||||
versionRange: '>2.0.0',
|
||||
configSatisfied: false,
|
||||
},
|
||||
},
|
||||
hosts: {},
|
||||
@@ -2032,6 +2121,27 @@ export module Mock {
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
outboundProxy: null,
|
||||
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> } =
|
||||
|
||||
@@ -6,8 +6,8 @@ import { FetchLogsReq, FetchLogsRes } from '@start9labs/shared'
|
||||
import { config } from '@start9labs/start-sdk'
|
||||
import { Dump } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { StartOSDiskInfo } from '@start9labs/shared'
|
||||
import { CT, T } from '@start9labs/start-sdk'
|
||||
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'
|
||||
import { IST, T } from '@start9labs/start-sdk'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
|
||||
export module RR {
|
||||
@@ -77,11 +77,10 @@ export module RR {
|
||||
export type GetServerLogsReq = FetchLogsReq // server.logs & server.kernel-logs & server.tor-logs
|
||||
export type GetServerLogsRes = FetchLogsRes
|
||||
|
||||
// @param limit: BE default is 50
|
||||
// @param boot: number is offset (0: current, -1 prev, +1 first), string is a specific boot id, and null is all
|
||||
export type FollowServerLogsReq = {
|
||||
limit?: number
|
||||
boot?: number | string | null
|
||||
limit?: number // (optional) default is 50. Ignored if cursor provided
|
||||
boot?: number | string | null // (optional) number is offset (0: current, -1 prev, +1 first), string is a specific boot id, null is all. Default is undefined
|
||||
cursor?: string // the last known log. Websocket will return all logs since this log
|
||||
} // server.logs.follow & server.kernel-logs.follow
|
||||
export type FollowServerLogsRes = {
|
||||
startCursor: string
|
||||
@@ -330,32 +329,27 @@ export module RR {
|
||||
|
||||
// package
|
||||
|
||||
export type GetPackagePropertiesReq = { id: string } // package.properties
|
||||
export type GetPackagePropertiesRes = Record<string, string>
|
||||
|
||||
export type GetPackageLogsReq = FetchLogsReq & { id: string } // package.logs
|
||||
export type GetPackageLogsRes = FetchLogsRes
|
||||
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 FollowPackageMetricsReq = { id: string } // package.metrics.follow
|
||||
export type FollowPackageMetricsRes = {
|
||||
guid: string
|
||||
metrics: AppMetrics
|
||||
}
|
||||
|
||||
export type InstallPackageReq = T.InstallParams
|
||||
export type InstallPackageRes = null
|
||||
|
||||
export type GetPackageConfigReq = { id: string } // package.config.get
|
||||
export type GetPackageConfigRes = { spec: CT.InputSpec; config: object }
|
||||
export type GetActionInputReq = { packageId: string; actionId: string } // package.action.get-input
|
||||
export type GetActionInputRes = {
|
||||
spec: IST.InputSpec
|
||||
value: object | null
|
||||
}
|
||||
|
||||
export type DrySetPackageConfigReq = { id: string; config: object } // package.config.set.dry
|
||||
export type DrySetPackageConfigRes = T.PackageId[]
|
||||
|
||||
export type SetPackageConfigReq = DrySetPackageConfigReq // package.config.set
|
||||
export type SetPackageConfigRes = null
|
||||
export type ActionReq = {
|
||||
packageId: string
|
||||
actionId: string
|
||||
input: object | null
|
||||
} // package.action.run
|
||||
export type ActionRes = (T.ActionResult & { version: '1' }) | null
|
||||
|
||||
export type RestorePackagesReq = {
|
||||
// package.backup.restore
|
||||
@@ -366,13 +360,6 @@ export module RR {
|
||||
}
|
||||
export type RestorePackagesRes = null
|
||||
|
||||
export type ExecutePackageActionReq = {
|
||||
id: string
|
||||
actionId: string
|
||||
input?: object
|
||||
} // package.action
|
||||
export type ExecutePackageActionRes = ActionResponse
|
||||
|
||||
export type StartPackageReq = { id: string } // package.start
|
||||
export type StartPackageRes = null
|
||||
|
||||
@@ -382,19 +369,12 @@ 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
|
||||
|
||||
export type DryConfigureDependencyReq = {
|
||||
dependencyId: string
|
||||
dependentId: string
|
||||
} // package.dependency.configure.dry
|
||||
export type DryConfigureDependencyRes = {
|
||||
oldConfig: object
|
||||
newConfig: object
|
||||
spec: CT.InputSpec
|
||||
}
|
||||
|
||||
export type SideloadPackageReq = {
|
||||
manifest: T.Manifest
|
||||
icon: string // base64
|
||||
@@ -441,14 +421,7 @@ export type TaggedDependencyError = {
|
||||
error: DependencyError
|
||||
}
|
||||
|
||||
export type ActionResponse = {
|
||||
message: string
|
||||
value: string | null
|
||||
copyable: boolean
|
||||
qr: boolean
|
||||
}
|
||||
|
||||
type MetricData = {
|
||||
interface MetricData {
|
||||
value: string
|
||||
unit: string
|
||||
}
|
||||
@@ -667,7 +640,7 @@ export type DependencyError =
|
||||
| DependencyErrorNotInstalled
|
||||
| DependencyErrorNotRunning
|
||||
| DependencyErrorIncorrectVersion
|
||||
| DependencyErrorConfigUnsatisfied
|
||||
| DependencyErrorActionRequired
|
||||
| DependencyErrorHealthChecksFailed
|
||||
| DependencyErrorTransitive
|
||||
|
||||
@@ -685,8 +658,8 @@ export type DependencyErrorIncorrectVersion = {
|
||||
received: string // version
|
||||
}
|
||||
|
||||
export type DependencyErrorConfigUnsatisfied = {
|
||||
type: 'configUnsatisfied'
|
||||
export interface DependencyErrorActionRequired {
|
||||
type: 'actionRequired'
|
||||
}
|
||||
|
||||
export type DependencyErrorHealthChecksFailed = {
|
||||
|
||||
@@ -12,7 +12,7 @@ export abstract class ApiService {
|
||||
// http
|
||||
|
||||
// for sideloading packages
|
||||
abstract uploadPackage(guid: string, body: Blob): Promise<string>
|
||||
abstract uploadPackage(guid: string, body: Blob): Promise<void>
|
||||
|
||||
abstract uploadFile(body: Blob): Promise<string>
|
||||
|
||||
@@ -295,10 +295,6 @@ export abstract class ApiService {
|
||||
|
||||
// package
|
||||
|
||||
abstract getPackageProperties(
|
||||
params: RR.GetPackagePropertiesReq,
|
||||
): Promise<RR.GetPackagePropertiesRes>
|
||||
|
||||
abstract getPackageLogs(
|
||||
params: RR.GetPackageLogsReq,
|
||||
): Promise<RR.GetPackageLogsRes>
|
||||
@@ -311,26 +307,16 @@ export abstract class ApiService {
|
||||
params: RR.InstallPackageReq,
|
||||
): Promise<RR.InstallPackageRes>
|
||||
|
||||
abstract getPackageConfig(
|
||||
params: RR.GetPackageConfigReq,
|
||||
): Promise<RR.GetPackageConfigRes>
|
||||
abstract getActionInput(
|
||||
params: RR.GetActionInputReq,
|
||||
): Promise<RR.GetActionInputRes>
|
||||
|
||||
abstract drySetPackageConfig(
|
||||
params: RR.DrySetPackageConfigReq,
|
||||
): Promise<RR.DrySetPackageConfigRes>
|
||||
|
||||
abstract setPackageConfig(
|
||||
params: RR.SetPackageConfigReq,
|
||||
): Promise<RR.SetPackageConfigRes>
|
||||
abstract runAction(params: RR.ActionReq): Promise<RR.ActionRes>
|
||||
|
||||
abstract restorePackages(
|
||||
params: RR.RestorePackagesReq,
|
||||
): Promise<RR.RestorePackagesRes>
|
||||
|
||||
abstract executePackageAction(
|
||||
params: RR.ExecutePackageActionReq,
|
||||
): Promise<RR.ExecutePackageActionRes>
|
||||
|
||||
abstract startPackage(params: RR.StartPackageReq): Promise<RR.StartPackageRes>
|
||||
|
||||
abstract restartPackage(
|
||||
@@ -339,14 +325,14 @@ 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>
|
||||
|
||||
abstract dryConfigureDependency(
|
||||
params: RR.DryConfigureDependencyReq,
|
||||
): Promise<RR.DryConfigureDependencyRes>
|
||||
|
||||
abstract sideloadPackage(): Promise<RR.SideloadPackageRes>
|
||||
|
||||
abstract setInterfaceClearnetAddress(
|
||||
|
||||
@@ -44,12 +44,11 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
// for sideloading packages
|
||||
|
||||
async uploadPackage(guid: string, body: Blob): Promise<string> {
|
||||
return this.httpRequest({
|
||||
async uploadPackage(guid: string, body: Blob): Promise<void> {
|
||||
await this.httpRequest({
|
||||
method: Method.POST,
|
||||
body,
|
||||
url: `/rest/rpc/${guid}`,
|
||||
responseType: 'text',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -562,12 +561,6 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
// package
|
||||
|
||||
async getPackageProperties(
|
||||
params: RR.GetPackagePropertiesReq,
|
||||
): Promise<RR.GetPackagePropertiesRes> {
|
||||
return this.rpcRequest({ method: 'package.properties', params })
|
||||
}
|
||||
|
||||
async getPackageLogs(
|
||||
params: RR.GetPackageLogsReq,
|
||||
): Promise<RR.GetPackageLogsRes> {
|
||||
@@ -586,22 +579,14 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'package.install', params })
|
||||
}
|
||||
|
||||
async getPackageConfig(
|
||||
params: RR.GetPackageConfigReq,
|
||||
): Promise<RR.GetPackageConfigRes> {
|
||||
return this.rpcRequest({ method: 'package.config.get', params })
|
||||
async getActionInput(
|
||||
params: RR.GetActionInputReq,
|
||||
): Promise<RR.GetActionInputRes> {
|
||||
return this.rpcRequest({ method: 'package.action.get-input', params })
|
||||
}
|
||||
|
||||
async drySetPackageConfig(
|
||||
params: RR.DrySetPackageConfigReq,
|
||||
): Promise<RR.DrySetPackageConfigRes> {
|
||||
return this.rpcRequest({ method: 'package.config.set.dry', params })
|
||||
}
|
||||
|
||||
async setPackageConfig(
|
||||
params: RR.SetPackageConfigReq,
|
||||
): Promise<RR.SetPackageConfigRes> {
|
||||
return this.rpcRequest({ method: 'package.config.set', params })
|
||||
async runAction(params: RR.ActionReq): Promise<RR.ActionRes> {
|
||||
return this.rpcRequest({ method: 'package.action.run', params })
|
||||
}
|
||||
|
||||
async restorePackages(
|
||||
@@ -610,12 +595,6 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'package.backup.restore', params })
|
||||
}
|
||||
|
||||
async executePackageAction(
|
||||
params: RR.ExecutePackageActionReq,
|
||||
): Promise<RR.ExecutePackageActionRes> {
|
||||
return this.rpcRequest({ method: 'package.action', params })
|
||||
}
|
||||
|
||||
async startPackage(params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
|
||||
return this.rpcRequest({ method: 'package.start', params })
|
||||
}
|
||||
@@ -630,21 +609,18 @@ 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> {
|
||||
return this.rpcRequest({ method: 'package.uninstall', params })
|
||||
}
|
||||
|
||||
async dryConfigureDependency(
|
||||
params: RR.DryConfigureDependencyReq,
|
||||
): Promise<RR.DryConfigureDependencyRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.dependency.configure.dry',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async sideloadPackage(): Promise<RR.SideloadPackageRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.sideload',
|
||||
|
||||
@@ -2,10 +2,12 @@ import { Injectable } from '@angular/core'
|
||||
import { pauseFor, Log, RPCErrorDetails, RPCOptions } from '@start9labs/shared'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import {
|
||||
AddOperation,
|
||||
Operation,
|
||||
PatchOp,
|
||||
pathFromArray,
|
||||
RemoveOperation,
|
||||
ReplaceOperation,
|
||||
Revision,
|
||||
} from 'patch-db-client'
|
||||
import {
|
||||
@@ -81,10 +83,8 @@ export class MockApiService extends ApiService {
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
async uploadPackage(guid: string, body: Blob): Promise<string> {
|
||||
async uploadPackage(guid: string, body: Blob): Promise<void> {
|
||||
await pauseFor(2000)
|
||||
// @TODO Aiden confirm this is correct
|
||||
return 'success'
|
||||
}
|
||||
|
||||
async uploadFile(body: Blob): Promise<string> {
|
||||
@@ -112,7 +112,7 @@ export class MockApiService extends ApiService {
|
||||
|
||||
openWebsocket$<T>(
|
||||
guid: string,
|
||||
config: RR.WebsocketConfig<T>,
|
||||
config: RR.WebsocketConfig<T> = {},
|
||||
): Observable<T> {
|
||||
if (guid === 'db-guid') {
|
||||
return this.mockWsSource$.pipe<any>(
|
||||
@@ -131,6 +131,11 @@ export class MockApiService extends ApiService {
|
||||
return from(this.initProgress()).pipe(
|
||||
startWith(PROGRESS),
|
||||
) as Observable<T>
|
||||
} else if (guid === 'sideload-progress-guid') {
|
||||
config.openObserver?.next(new Event(''))
|
||||
return from(this.initProgress()).pipe(
|
||||
startWith(PROGRESS),
|
||||
) as Observable<T>
|
||||
} else {
|
||||
throw new Error('invalid guid type')
|
||||
}
|
||||
@@ -909,14 +914,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,
|
||||
@@ -933,40 +938,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)
|
||||
|
||||
@@ -975,15 +983,6 @@ export class MockApiService extends ApiService {
|
||||
|
||||
// package
|
||||
|
||||
async getPackageProperties(
|
||||
params: RR.GetPackagePropertiesReq,
|
||||
): Promise<RR.GetPackagePropertiesRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
password: 'specialPassword$',
|
||||
}
|
||||
}
|
||||
|
||||
async getPackageLogs(
|
||||
params: RR.GetPackageLogsReq,
|
||||
): Promise<RR.GetPackageLogsRes> {
|
||||
@@ -1025,7 +1024,7 @@ export class MockApiService extends ApiService {
|
||||
this.installProgress(params.id)
|
||||
}, 1000)
|
||||
|
||||
const patch: Operation<
|
||||
const patch: AddOperation<
|
||||
PackageDataEntry<InstallingState | UpdatingState>
|
||||
>[] = [
|
||||
{
|
||||
@@ -1055,44 +1054,43 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async getPackageConfig(
|
||||
params: RR.GetPackageConfigReq,
|
||||
): Promise<RR.GetPackageConfigRes> {
|
||||
async getActionInput(
|
||||
params: RR.GetActionInputReq,
|
||||
): Promise<RR.GetActionInputRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
config: Mock.MockConfig,
|
||||
spec: await Mock.getInputSpec(),
|
||||
value: Mock.MockConfig,
|
||||
spec: await Mock.getActionInputSpec(),
|
||||
}
|
||||
}
|
||||
|
||||
async drySetPackageConfig(
|
||||
params: RR.DrySetPackageConfigReq,
|
||||
): Promise<RR.DrySetPackageConfigRes> {
|
||||
async runAction(params: RR.ActionReq): Promise<RR.ActionRes> {
|
||||
await pauseFor(2000)
|
||||
return []
|
||||
}
|
||||
|
||||
async setPackageConfig(
|
||||
params: RR.SetPackageConfigReq,
|
||||
): Promise<RR.SetPackageConfigRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/packageData/${params.id}/status/configured`,
|
||||
value: true,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
if (params.actionId === 'properties') {
|
||||
// return Mock.ActionResGroup
|
||||
return Mock.ActionResMessage
|
||||
// return Mock.ActionResSingle
|
||||
} 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.ActionResMessage
|
||||
// return Mock.ActionResSingle
|
||||
}
|
||||
}
|
||||
|
||||
async restorePackages(
|
||||
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)
|
||||
@@ -1118,50 +1116,62 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async executePackageAction(
|
||||
params: RR.ExecutePackageActionReq,
|
||||
): Promise<RR.ExecutePackageActionRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.ActionResponse
|
||||
}
|
||||
|
||||
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 () => {
|
||||
if (params.id !== 'bitcoind') {
|
||||
const patch2 = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/health',
|
||||
value: {},
|
||||
const patch2: ReplaceOperation<T.MainStatus & { main: 'running' }>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
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 + '/status',
|
||||
value: 'running',
|
||||
},
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/started',
|
||||
value: new Date().toISOString(),
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch3)
|
||||
this.mockRevision(patch2)
|
||||
}, 2000)
|
||||
|
||||
const originalPatch = [
|
||||
const originalPatch: ReplaceOperation<
|
||||
T.MainStatus & { main: 'starting' }
|
||||
>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/status',
|
||||
value: 'starting',
|
||||
path,
|
||||
value: {
|
||||
main: 'starting',
|
||||
health: {},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1173,74 +1183,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',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1251,29 +1244,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' },
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1282,6 +1270,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> {
|
||||
@@ -1297,7 +1291,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`,
|
||||
@@ -1310,22 +1304,11 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async dryConfigureDependency(
|
||||
params: RR.DryConfigureDependencyReq,
|
||||
): Promise<RR.DryConfigureDependencyRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
oldConfig: Mock.MockConfig,
|
||||
newConfig: Mock.MockDependencyConfig,
|
||||
spec: await Mock.getInputSpec(),
|
||||
}
|
||||
}
|
||||
|
||||
async sideloadPackage(): Promise<RR.SideloadPackageRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
upload: '4120e092-05ab-4de2-9fbd-c3f1f4b1df9e', // no significance, randomly generated
|
||||
progress: '5120e092-05ab-4de2-9fbd-c3f1f4b1df9e', // no significance, randomly generated
|
||||
upload: 'sideload-upload-guid', // no significance, randomly generated
|
||||
progress: 'sideload-progress-guid', // no significance, randomly generated
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -154,40 +154,44 @@ export const mockPatchData: DataModel = {
|
||||
lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(),
|
||||
nextBackup: new Date(new Date().valueOf() + 100000000).toISOString(),
|
||||
status: {
|
||||
configured: true,
|
||||
main: {
|
||||
status: '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: 'Set Config',
|
||||
description: 'edit bitcoin.conf',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'any',
|
||||
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: { disabled: 'This is temporarily disabled' },
|
||||
allowedStatuses: 'only-running',
|
||||
hasInput: false,
|
||||
group: null,
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
serviceInterfaces: {
|
||||
ui: {
|
||||
id: 'ui',
|
||||
@@ -347,6 +351,27 @@ export const mockPatchData: DataModel = {
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
outboundProxy: null,
|
||||
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: {
|
||||
@@ -362,10 +387,7 @@ export const mockPatchData: DataModel = {
|
||||
lastBackup: null,
|
||||
nextBackup: null,
|
||||
status: {
|
||||
configured: true,
|
||||
main: {
|
||||
status: 'stopped',
|
||||
},
|
||||
main: 'stopped',
|
||||
},
|
||||
actions: {},
|
||||
serviceInterfaces: {
|
||||
@@ -428,7 +450,6 @@ export const mockPatchData: DataModel = {
|
||||
kind: 'running',
|
||||
versionRange: '>=26.0.0',
|
||||
healthChecks: [],
|
||||
configSatisfied: true,
|
||||
},
|
||||
'btc-rpc-proxy': {
|
||||
title: 'Bitcoin Proxy',
|
||||
@@ -436,7 +457,6 @@ export const mockPatchData: DataModel = {
|
||||
kind: 'running',
|
||||
versionRange: '>2.0.0',
|
||||
healthChecks: [],
|
||||
configSatisfied: false,
|
||||
},
|
||||
},
|
||||
hosts: {},
|
||||
@@ -444,6 +464,27 @@ export const mockPatchData: DataModel = {
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
outboundProxy: null,
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export class ConfigService {
|
||||
|
||||
isLaunchable(
|
||||
state: T.PackageState['state'],
|
||||
status: T.MainStatus['status'],
|
||||
status: T.MainStatus['main'],
|
||||
): boolean {
|
||||
return state === 'installed' && status === 'running'
|
||||
}
|
||||
|
||||
@@ -104,14 +104,21 @@ export class DepErrorService {
|
||||
}
|
||||
}
|
||||
|
||||
// invalid config
|
||||
if (!currentDep.configSatisfied) {
|
||||
// action required
|
||||
if (
|
||||
Object.values(pkg.requestedActions).some(
|
||||
a =>
|
||||
a.active &&
|
||||
a.request.packageId === depId &&
|
||||
a.request.severity === 'critical',
|
||||
)
|
||||
) {
|
||||
return {
|
||||
type: 'configUnsatisfied',
|
||||
type: 'actionRequired',
|
||||
}
|
||||
}
|
||||
|
||||
const depStatus = dep.status.main.status
|
||||
const depStatus = dep.status.main
|
||||
|
||||
// not running
|
||||
if (depStatus !== 'running' && depStatus !== 'starting') {
|
||||
@@ -123,7 +130,7 @@ export class DepErrorService {
|
||||
// health check failure
|
||||
if (depStatus === 'running' && currentDep.kind === 'running') {
|
||||
for (let id of currentDep.healthChecks) {
|
||||
const check = dep.status.main.health[id]
|
||||
const check = dep.status.health[id]
|
||||
if (check?.result !== 'success') {
|
||||
return {
|
||||
type: 'healthChecksFailed',
|
||||
|
||||
@@ -5,7 +5,7 @@ import { OSUpdate } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { getServerInfo } from 'src/app/utils/get-server-info'
|
||||
import { DataModel } from './patch-db/data-model'
|
||||
import { Exver } from '@start9labs/shared'
|
||||
import { Version } from '@start9labs/start-sdk'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -47,14 +47,14 @@ export class EOSService {
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly exver: Exver,
|
||||
) {}
|
||||
|
||||
async loadEos(): Promise<void> {
|
||||
const { version, id } = await getServerInfo(this.patch)
|
||||
this.osUpdate = await this.api.checkOSUpdate({ serverId: id })
|
||||
const updateAvailable =
|
||||
this.exver.compareOsVersion(this.osUpdate.version, version) === 'greater'
|
||||
Version.parse(this.osUpdate.version).compare(Version.parse(version)) ===
|
||||
'greater'
|
||||
this.updateAvailable$.next(updateAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ValidatorFn,
|
||||
Validators,
|
||||
} from '@angular/forms'
|
||||
import { CT, utils } from '@start9labs/start-sdk'
|
||||
import { IST, utils } from '@start9labs/start-sdk'
|
||||
const Mustache = require('mustache')
|
||||
|
||||
@Injectable({
|
||||
@@ -17,16 +17,16 @@ export class FormService {
|
||||
constructor(private readonly formBuilder: UntypedFormBuilder) {}
|
||||
|
||||
createForm(
|
||||
spec: CT.InputSpec,
|
||||
spec: IST.InputSpec,
|
||||
current: Record<string, any> = {},
|
||||
): UntypedFormGroup {
|
||||
return this.getFormGroup(spec, [], current)
|
||||
}
|
||||
|
||||
getUnionSelectSpec(
|
||||
spec: CT.ValueSpecUnion,
|
||||
spec: IST.ValueSpecUnion,
|
||||
selection: string | null,
|
||||
): CT.ValueSpecSelect {
|
||||
): IST.ValueSpecSelect {
|
||||
return {
|
||||
...spec,
|
||||
type: 'select',
|
||||
@@ -38,7 +38,7 @@ export class FormService {
|
||||
}
|
||||
|
||||
getUnionObject(
|
||||
spec: CT.ValueSpecUnion,
|
||||
spec: IST.ValueSpecUnion,
|
||||
selected: string | null,
|
||||
): UntypedFormGroup {
|
||||
const group = this.getFormGroup({
|
||||
@@ -53,16 +53,16 @@ export class FormService {
|
||||
return group
|
||||
}
|
||||
|
||||
getListItem(spec: CT.ValueSpecList, entry?: any) {
|
||||
if (CT.isValueSpecListOf(spec, 'text')) {
|
||||
getListItem(spec: IST.ValueSpecList, entry?: any) {
|
||||
if (IST.isValueSpecListOf(spec, 'text')) {
|
||||
return this.formBuilder.control(entry, stringValidators(spec.spec))
|
||||
} else if (CT.isValueSpecListOf(spec, 'object')) {
|
||||
} else if (IST.isValueSpecListOf(spec, 'object')) {
|
||||
return this.getFormGroup(spec.spec.spec, [], entry)
|
||||
}
|
||||
}
|
||||
|
||||
getFormGroup(
|
||||
config: CT.InputSpec,
|
||||
config: IST.InputSpec,
|
||||
validators: ValidatorFn[] = [],
|
||||
current?: Record<string, any> | null,
|
||||
): UntypedFormGroup {
|
||||
@@ -77,7 +77,7 @@ export class FormService {
|
||||
}
|
||||
|
||||
private getFormEntry(
|
||||
spec: CT.ValueSpec,
|
||||
spec: IST.ValueSpec,
|
||||
currentValue?: any,
|
||||
): UntypedFormGroup | UntypedFormArray | UntypedFormControl {
|
||||
let value: any
|
||||
@@ -140,7 +140,7 @@ export class FormService {
|
||||
return this.formBuilder.control(value)
|
||||
case 'select':
|
||||
value = currentValue === undefined ? spec.default : currentValue
|
||||
return this.formBuilder.control(value, selectValidators(spec))
|
||||
return this.formBuilder.control(value)
|
||||
case 'multiselect':
|
||||
value = currentValue === undefined ? spec.default : currentValue
|
||||
return this.formBuilder.control(value, multiselectValidators(spec))
|
||||
@@ -150,18 +150,18 @@ export class FormService {
|
||||
}
|
||||
}
|
||||
|
||||
// function getListItemValidators(spec: CT.ValueSpecList) {
|
||||
// if (CT.isValueSpecListOf(spec, 'text')) {
|
||||
// function getListItemValidators(spec: IST.ValueSpecList) {
|
||||
// if (IST.isValueSpecListOf(spec, 'text')) {
|
||||
// return stringValidators(spec.spec)
|
||||
// }
|
||||
// }
|
||||
|
||||
function stringValidators(
|
||||
spec: CT.ValueSpecText | CT.ListValueSpecText,
|
||||
spec: IST.ValueSpecText | IST.ListValueSpecText,
|
||||
): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
if ((spec as CT.ValueSpecText).required) {
|
||||
if ((spec as IST.ValueSpecText).required) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ function stringValidators(
|
||||
return validators
|
||||
}
|
||||
|
||||
function textareaValidators(spec: CT.ValueSpecTextarea): ValidatorFn[] {
|
||||
function textareaValidators(spec: IST.ValueSpecTextarea): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
if (spec.required) {
|
||||
@@ -186,7 +186,7 @@ function textareaValidators(spec: CT.ValueSpecTextarea): ValidatorFn[] {
|
||||
return validators
|
||||
}
|
||||
|
||||
function colorValidators({ required }: CT.ValueSpecColor): ValidatorFn[] {
|
||||
function colorValidators({ required }: IST.ValueSpecColor): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = [Validators.pattern(/^#[0-9a-f]{6}$/i)]
|
||||
|
||||
if (required) {
|
||||
@@ -200,7 +200,7 @@ function datetimeValidators({
|
||||
required,
|
||||
min,
|
||||
max,
|
||||
}: CT.ValueSpecDatetime): ValidatorFn[] {
|
||||
}: IST.ValueSpecDatetime): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
if (required) {
|
||||
@@ -218,12 +218,12 @@ function datetimeValidators({
|
||||
return validators
|
||||
}
|
||||
|
||||
function numberValidators(spec: CT.ValueSpecNumber): ValidatorFn[] {
|
||||
function numberValidators(spec: IST.ValueSpecNumber): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
validators.push(isNumber())
|
||||
|
||||
if ((spec as CT.ValueSpecNumber).required) {
|
||||
if ((spec as IST.ValueSpecNumber).required) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
@@ -236,23 +236,13 @@ function numberValidators(spec: CT.ValueSpecNumber): ValidatorFn[] {
|
||||
return validators
|
||||
}
|
||||
|
||||
function selectValidators(spec: CT.ValueSpecSelect): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
if (spec.required) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
return validators
|
||||
}
|
||||
|
||||
function multiselectValidators(spec: CT.ValueSpecMultiselect): ValidatorFn[] {
|
||||
function multiselectValidators(spec: IST.ValueSpecMultiselect): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
validators.push(listInRange(spec.minLength, spec.maxLength))
|
||||
return validators
|
||||
}
|
||||
|
||||
function listValidators(spec: CT.ValueSpecList): ValidatorFn[] {
|
||||
function listValidators(spec: IST.ValueSpecList): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
validators.push(listInRange(spec.minLength, spec.maxLength))
|
||||
validators.push(listItemIssue())
|
||||
@@ -367,7 +357,7 @@ export function listItemIssue(): ValidatorFn {
|
||||
}
|
||||
}
|
||||
|
||||
export function listUnique(spec: CT.ValueSpecList): ValidatorFn {
|
||||
export function listUnique(spec: IST.ValueSpecList): ValidatorFn {
|
||||
return control => {
|
||||
const list = control.value
|
||||
for (let idx = 0; idx < list.length; idx++) {
|
||||
@@ -404,7 +394,11 @@ export function listUnique(spec: CT.ValueSpecList): ValidatorFn {
|
||||
}
|
||||
}
|
||||
|
||||
function listItemEquals(spec: CT.ValueSpecList, val1: any, val2: any): boolean {
|
||||
function listItemEquals(
|
||||
spec: IST.ValueSpecList,
|
||||
val1: any,
|
||||
val2: any,
|
||||
): boolean {
|
||||
// TODO: fix types
|
||||
switch (spec.spec.type) {
|
||||
case 'text':
|
||||
@@ -417,7 +411,7 @@ function listItemEquals(spec: CT.ValueSpecList, val1: any, val2: any): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function itemEquals(spec: CT.ValueSpec, val1: any, val2: any): boolean {
|
||||
function itemEquals(spec: IST.ValueSpec, val1: any, val2: any): boolean {
|
||||
switch (spec.type) {
|
||||
case 'text':
|
||||
case 'textarea':
|
||||
@@ -429,15 +423,15 @@ function itemEquals(spec: CT.ValueSpec, val1: any, val2: any): boolean {
|
||||
// TODO: 'unique-by' does not exist on ValueSpecObject, fix types
|
||||
return objEquals(
|
||||
(spec as any)['unique-by'],
|
||||
spec as CT.ValueSpecObject,
|
||||
spec as IST.ValueSpecObject,
|
||||
val1,
|
||||
val2,
|
||||
)
|
||||
case 'union':
|
||||
// TODO: 'unique-by' does not exist on CT.ValueSpecUnion, fix types
|
||||
// TODO: 'unique-by' does not exist onIST.ValueSpecUnion, fix types
|
||||
return unionEquals(
|
||||
(spec as any)['unique-by'],
|
||||
spec as CT.ValueSpecUnion,
|
||||
spec as IST.ValueSpecUnion,
|
||||
val1,
|
||||
val2,
|
||||
)
|
||||
@@ -457,8 +451,8 @@ function itemEquals(spec: CT.ValueSpec, val1: any, val2: any): boolean {
|
||||
}
|
||||
|
||||
function listObjEquals(
|
||||
uniqueBy: CT.UniqueBy,
|
||||
spec: CT.ListValueSpecObject,
|
||||
uniqueBy: IST.UniqueBy,
|
||||
spec: IST.ListValueSpecObject,
|
||||
val1: any,
|
||||
val2: any,
|
||||
): boolean {
|
||||
@@ -485,8 +479,8 @@ function listObjEquals(
|
||||
}
|
||||
|
||||
function objEquals(
|
||||
uniqueBy: CT.UniqueBy,
|
||||
spec: CT.ValueSpecObject,
|
||||
uniqueBy: IST.UniqueBy,
|
||||
spec: IST.ValueSpecObject,
|
||||
val1: any,
|
||||
val2: any,
|
||||
): boolean {
|
||||
@@ -514,8 +508,8 @@ function objEquals(
|
||||
}
|
||||
|
||||
function unionEquals(
|
||||
uniqueBy: CT.UniqueBy,
|
||||
spec: CT.ValueSpecUnion,
|
||||
uniqueBy: IST.UniqueBy,
|
||||
spec: IST.ValueSpecUnion,
|
||||
val1: any,
|
||||
val2: any,
|
||||
): boolean {
|
||||
@@ -547,8 +541,8 @@ function unionEquals(
|
||||
}
|
||||
|
||||
function uniqueByMessageWrapper(
|
||||
uniqueBy: CT.UniqueBy,
|
||||
spec: CT.ListValueSpecObject,
|
||||
uniqueBy: IST.UniqueBy,
|
||||
spec: IST.ListValueSpecObject,
|
||||
) {
|
||||
let configSpec = spec.spec
|
||||
|
||||
@@ -559,8 +553,8 @@ function uniqueByMessageWrapper(
|
||||
}
|
||||
|
||||
function uniqueByMessage(
|
||||
uniqueBy: CT.UniqueBy,
|
||||
configSpec: CT.InputSpec,
|
||||
uniqueBy: IST.UniqueBy,
|
||||
configSpec: IST.InputSpec,
|
||||
outermost = true,
|
||||
): string {
|
||||
let joinFunc
|
||||
@@ -569,7 +563,7 @@ function uniqueByMessage(
|
||||
return ''
|
||||
} else if (typeof uniqueBy === 'string') {
|
||||
return configSpec[uniqueBy]
|
||||
? (configSpec[uniqueBy] as CT.ValueSpecObject).name
|
||||
? (configSpec[uniqueBy] as IST.ValueSpecObject).name
|
||||
: uniqueBy
|
||||
} else if ('any' in uniqueBy) {
|
||||
joinFunc = ' OR '
|
||||
@@ -589,14 +583,14 @@ function uniqueByMessage(
|
||||
}
|
||||
|
||||
function isObject(
|
||||
spec: CT.ListValueSpecOf<any>,
|
||||
): spec is CT.ListValueSpecObject {
|
||||
spec: IST.ListValueSpecOf<any>,
|
||||
): spec is IST.ListValueSpecObject {
|
||||
// only lists of objects have uniqueBy
|
||||
return 'uniqueBy' in spec
|
||||
}
|
||||
|
||||
export function convertValuesRecursive(
|
||||
configSpec: CT.InputSpec,
|
||||
configSpec: IST.InputSpec,
|
||||
group: UntypedFormGroup,
|
||||
) {
|
||||
Object.entries(configSpec).forEach(([key, valueSpec]) => {
|
||||
@@ -626,7 +620,7 @@ export function convertValuesRecursive(
|
||||
})
|
||||
} else if (valueSpec.spec.type === 'object') {
|
||||
controls.forEach(formGroup => {
|
||||
const objectSpec = valueSpec.spec as CT.ListValueSpecObject
|
||||
const objectSpec = valueSpec.spec as IST.ListValueSpecObject
|
||||
convertValuesRecursive(objectSpec.spec, formGroup as UntypedFormGroup)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -231,19 +231,6 @@ export class MarketplaceService {
|
||||
}
|
||||
}
|
||||
|
||||
private async updateStoreName(
|
||||
url: string,
|
||||
oldName: string | undefined,
|
||||
newName: string,
|
||||
): Promise<void> {
|
||||
if (oldName !== newName) {
|
||||
this.api.setDbValue<string>(
|
||||
['marketplace', 'knownHosts', url, 'name'],
|
||||
newName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// UI only
|
||||
readonly updateErrors: Record<string, string> = {}
|
||||
readonly updateQueue: Record<string, boolean> = {}
|
||||
|
||||
@@ -33,7 +33,7 @@ export class PatchDbSource extends Observable<Update<DataModel>[]> {
|
||||
private readonly stream$ = inject(AuthService).isVerified$.pipe(
|
||||
switchMap(verified => (verified ? this.api.subscribeToPatchDB({}) : EMPTY)),
|
||||
switchMap(({ dump, guid }) =>
|
||||
this.api.openWebsocket$<Revision>(guid, {}).pipe(
|
||||
this.api.openWebsocket$<Revision>(guid).pipe(
|
||||
bufferTime(250),
|
||||
filter(revisions => !!revisions.length),
|
||||
startWith([dump]),
|
||||
|
||||
@@ -17,7 +17,7 @@ export function renderPkgStatus(
|
||||
let health: T.HealthStatus | null = null
|
||||
|
||||
if (pkg.stateInfo.state === 'installed') {
|
||||
primary = getInstalledPrimaryStatus(pkg.status)
|
||||
primary = getInstalledPrimaryStatus(pkg)
|
||||
dependency = getDependencyStatus(depErrors)
|
||||
health = getHealthStatus(pkg.status)
|
||||
} else {
|
||||
@@ -27,11 +27,15 @@ export function renderPkgStatus(
|
||||
return { primary, dependency, health }
|
||||
}
|
||||
|
||||
function getInstalledPrimaryStatus(status: T.Status): PrimaryStatus {
|
||||
if (!status.configured) {
|
||||
return 'needsConfig'
|
||||
function getInstalledPrimaryStatus(pkg: T.PackageDataEntry): PrimaryStatus {
|
||||
if (
|
||||
Object.values(pkg.requestedActions).some(
|
||||
r => r.active && r.request.severity === 'critical',
|
||||
)
|
||||
) {
|
||||
return 'actionRequired'
|
||||
} else {
|
||||
return status.main.status
|
||||
return pkg.status.main
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,12 +43,12 @@ function getDependencyStatus(depErrors: PkgDependencyErrors): DependencyStatus {
|
||||
return Object.values(depErrors).some(err => !!err) ? 'warning' : 'satisfied'
|
||||
}
|
||||
|
||||
function getHealthStatus(status: T.Status): T.HealthStatus | null {
|
||||
if (status.main.status !== 'running' || !status.main.health) {
|
||||
function getHealthStatus(status: T.MainStatus): T.HealthStatus | null {
|
||||
if (status.main !== 'running' || !status.main) {
|
||||
return null
|
||||
}
|
||||
|
||||
const values = Object.values(status.main.health)
|
||||
const values = Object.values(status.health)
|
||||
|
||||
if (values.some(h => h.result === 'failure')) {
|
||||
return 'failure'
|
||||
@@ -78,7 +82,8 @@ export type PrimaryStatus =
|
||||
| 'restarting'
|
||||
| 'stopped'
|
||||
| 'backingUp'
|
||||
| 'needsConfig'
|
||||
| 'actionRequired'
|
||||
| 'error'
|
||||
|
||||
export type DependencyStatus = 'warning' | 'satisfied'
|
||||
|
||||
@@ -133,11 +138,16 @@ export const PrimaryRendering: Record<PrimaryStatus, StatusRendering> = {
|
||||
color: 'success',
|
||||
showDots: false,
|
||||
},
|
||||
needsConfig: {
|
||||
display: 'Needs Config',
|
||||
actionRequired: {
|
||||
display: 'Action Required',
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { CB } from '@start9labs/start-sdk'
|
||||
import { ISB } from '@start9labs/start-sdk'
|
||||
|
||||
export async function configBuilderToSpec(
|
||||
builder:
|
||||
| CB.Config<Record<string, unknown>, unknown>
|
||||
| CB.Config<Record<string, unknown>, never>,
|
||||
| ISB.InputSpec<Record<string, unknown>, unknown>
|
||||
| ISB.InputSpec<Record<string, unknown>, never>,
|
||||
) {
|
||||
return builder.build({} as any)
|
||||
}
|
||||
|
||||
30
web/projects/ui/src/app/utils/dep-info.ts
Normal file
30
web/projects/ui/src/app/utils/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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,5 +20,5 @@
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user