mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
rework cutofing processing (#716)
* rework cutofing processing * fix default generation bug * dont hard code all dependent ids to 'hello' * fix recommendation display and bug with health cehck not updating * fix key name * fix dependency error updates and retain order on backup * fix health check display
This commit is contained in:
committed by
Aiden McClelland
parent
c6e379bffa
commit
65a4b8ab84
1814
ui/package-lock.json
generated
1814
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,9 +24,8 @@
|
||||
"@start9labs/emver": "0.1.5",
|
||||
"ajv": "^6.12.6",
|
||||
"core-js": "^3.17.2",
|
||||
"fast-json-patch": "^3.1.0",
|
||||
"fuse.js": "^6.4.6",
|
||||
"json-pointer": "^0.6.1",
|
||||
"jsonpointerx": "^1.1.4",
|
||||
"marked": "3.0.2",
|
||||
"mustache": "^4.2.0",
|
||||
"ng-qrcode": "^5.0.0",
|
||||
|
||||
@@ -30,5 +30,8 @@
|
||||
<p *ngIf="control.hasError('listNotUnique')">
|
||||
{{ control.errors.listNotUnique.value }}
|
||||
</p>
|
||||
<p *ngIf="control.hasError('listItemIssue')">
|
||||
{{ control.errors.listItemIssue.value }}
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -1,11 +1,16 @@
|
||||
<ion-button *ngIf="data.spec.description" class="help-button" fill="clear" size="small" (click)="presentAlertDescription()">
|
||||
<ion-button *ngIf="data.spec.description" class="slot-start" fill="clear" size="small" (click)="presentAlertDescription()">
|
||||
<ion-icon name="help-circle-outline"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
<!-- this is a button for css purposes only -->
|
||||
<ion-button *ngIf="data.invalid" class="slot-start" fill="clear" size="small" color="danger">
|
||||
<ion-icon name="warning-outline"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
<span>{{ data.spec.name }}</span>
|
||||
|
||||
<ion-text color="success" *ngIf="data.isNew"> (New)</ion-text>
|
||||
<ion-text color="warning" *ngIf="data.isEdited"> (Edited)</ion-text>
|
||||
<ion-text color="success" *ngIf="data.new"> (New)</ion-text>
|
||||
<ion-text color="warning" *ngIf="data.edited"> (Edited)</ion-text>
|
||||
|
||||
<span *ngIf="(['string', 'number'] | includes : data.spec.type) && !$any(data.spec).nullable"> *</span>
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
<h4 class="input-label">
|
||||
<form-label [data]="{
|
||||
spec: spec,
|
||||
isNew: current && current[entry.key] === undefined,
|
||||
isEdited: entry.value.dirty
|
||||
new: current && current[entry.key] === undefined,
|
||||
edited: entry.value.dirty
|
||||
}"></form-label>
|
||||
</h4>
|
||||
<!-- string or number -->
|
||||
@@ -73,8 +73,8 @@
|
||||
<ion-item-divider>
|
||||
<form-label [data]="{
|
||||
spec: spec,
|
||||
isNew: current && current[entry.key] === undefined,
|
||||
isEdited: entry.value.dirty
|
||||
new: current && current[entry.key] === undefined,
|
||||
edited: entry.value.dirty
|
||||
}"></form-label>
|
||||
</ion-item-divider>
|
||||
<!-- body -->
|
||||
@@ -97,8 +97,8 @@
|
||||
<ion-item-divider>
|
||||
<form-label [data]="{
|
||||
spec: spec,
|
||||
isNew: current && current[entry.key] === undefined,
|
||||
isEdited: entry.value.dirty
|
||||
new: current && current[entry.key] === undefined,
|
||||
edited: entry.value.dirty
|
||||
}"></form-label>
|
||||
<ion-button fill="clear" color="primary" slot="end" (click)="addListItemWrapper(entry.key, spec)">
|
||||
<ion-icon slot="start" name="add"></ion-icon>
|
||||
@@ -117,8 +117,9 @@
|
||||
<ion-item button (click)="toggleExpand(entry.key, i)">
|
||||
<form-label [data]="{
|
||||
spec: $any({ name: objectListInfo[entry.key][i].displayAs || 'Entry ' + (i + 1) }),
|
||||
isNew: false,
|
||||
isEdited: abstractControl.dirty
|
||||
new: false,
|
||||
edited: abstractControl.dirty,
|
||||
invalid: abstractControl.invalid
|
||||
}"></form-label>
|
||||
<ion-icon
|
||||
slot="end"
|
||||
@@ -192,13 +193,13 @@
|
||||
<p class="input-label">
|
||||
<form-label [data]="{
|
||||
spec: spec,
|
||||
isNew: current && current[entry.key] === undefined,
|
||||
isEdited: entry.value.dirty
|
||||
new: current && current[entry.key] === undefined,
|
||||
edited: entry.value.dirty
|
||||
}"></form-label>
|
||||
</p>
|
||||
<!-- list -->
|
||||
<ion-item button detail="false" color="dark" (click)="presentModalEnumList(entry.key, $any(spec), formArr.value)">
|
||||
<ion-label>
|
||||
<ion-label style="white-space: nowrap !important;">
|
||||
<h2>{{ getEnumListDisplay(formArr.value, $any(spec.spec)) }}</h2>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" color="light">
|
||||
@@ -211,7 +212,7 @@
|
||||
*ngIf="formGroup.get(entry.key).errors"
|
||||
[control]="$any(formGroup.get(entry.key))"
|
||||
[spec]="spec"
|
||||
>
|
||||
>
|
||||
</form-error>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.help-button {
|
||||
.slot-start {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Component, Input, Output, SimpleChange, EventEmitter } from '@angular/core'
|
||||
import { AbstractControl, AbstractFormGroupDirective, FormArray, FormGroup } from '@angular/forms'
|
||||
import { AbstractFormGroupDirective, FormArray, FormGroup } from '@angular/forms'
|
||||
import { AlertButton, AlertController, IonicSafeString, ModalController } from '@ionic/angular'
|
||||
import { ConfigSpec, ListValueSpecOf, ListValueSpecString, ValueSpec, ValueSpecBoolean, ValueSpecEnum, ValueSpecList, ValueSpecListOf, ValueSpecNumber, ValueSpecString, ValueSpecUnion } from 'src/app/pkg-config/config-types'
|
||||
import { ConfigSpec, ListValueSpecOf, ValueSpec, ValueSpecBoolean, ValueSpecList, ValueSpecListOf, ValueSpecUnion } from 'src/app/pkg-config/config-types'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
import { Range } from 'src/app/pkg-config/config-utilities'
|
||||
import { EnumListPage } from 'src/app/modals/enum-list/enum-list.page'
|
||||
const Mustache = require('mustache')
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
const Mustache = require('mustache')
|
||||
|
||||
@Component({
|
||||
selector: 'form-object',
|
||||
@@ -76,6 +76,7 @@ export class FormObjectComponent {
|
||||
addListItem (key: string, markDirty = true, val?: string): void {
|
||||
const arr = this.formGroup.get(key) as FormArray
|
||||
if (markDirty) arr.markAsDirty()
|
||||
// @TODO why are these commented out?
|
||||
// const validators = this.formService.getListItemValidators(this.objectSpec[key] as ValueSpecList, key, arr.length)
|
||||
// arr.push(new FormControl(value, validators))
|
||||
const listSpec = this.objectSpec[key] as ValueSpecList
|
||||
@@ -225,8 +226,9 @@ export class FormObjectComponent {
|
||||
|
||||
interface HeaderData {
|
||||
spec: ValueSpec
|
||||
isEdited: boolean
|
||||
isNew: boolean
|
||||
edited: boolean
|
||||
new: boolean
|
||||
invalid?: boolean
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -38,33 +38,23 @@
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="rec && showRec">
|
||||
<ion-item class="rec-item">
|
||||
<ion-label>
|
||||
<h2 style="display: flex; align-items: center;">
|
||||
<ion-icon size="small" style="margin: 4px" slot="start" color="primary" slot="start" name="ellipse"></ion-icon>
|
||||
<ion-thumbnail style="width: 3vh; height: 3vh; margin: 0px 2px 0px 5px;" slot="start">
|
||||
<img [src]="rec.dependentIcon" [alt]="rec.dependentTitle"/>
|
||||
</ion-thumbnail>
|
||||
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: smaller;">{{ rec.dependentTitle }}</ion-text>
|
||||
</h2>
|
||||
<div style="margin: 7px 5px;">
|
||||
<p style="font-size: small; color: var(--ion-color-medium)"> {{ pkg.manifest.title }} config has been modified to satisfy {{ rec.dependentTitle }}.
|
||||
<ion-text color="dark">To accept the changes, click “Save” above.</ion-text>
|
||||
</p>
|
||||
<a style="font-size: small" *ngIf="!openRec" (click)="openRec = true">More Info</a>
|
||||
<ng-container *ngIf="openRec">
|
||||
<p style="margin-top: 10px; color: var(--ion-color-medium); font-size: small" [innerHTML]="rec.description"></p>
|
||||
<a style="font-size: x-small; font-style: italic;" (click)="openRec = false">hide</a>
|
||||
</ng-container>
|
||||
<ion-button style="position: absolute; right: 0; top: 0" color="primary" fill="clear" (click)="dismissRec()">
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
</ng-container>
|
||||
<!-- auto-config -->
|
||||
<ion-item lines="none" *ngIf="dependentInfo" class="rec-item" style="margin-bottom: 48px;">
|
||||
<ion-label>
|
||||
<h2 style="display: flex; align-items: center;">
|
||||
<img style="width: 18px; margin: 4px;" [src]="pkg['static-files'].icon" [alt]="pkg.manifest.title"/>
|
||||
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: 18px;">{{ pkg.manifest.title }}</ion-text>
|
||||
</h2>
|
||||
<p>
|
||||
<ion-text color="dark">
|
||||
{{ pkg.manifest.title }} has been modified to satisfy {{ dependentInfo.title }}.
|
||||
<br />
|
||||
<br />
|
||||
To accept the modifications, click "Save".
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- no config -->
|
||||
<ion-item *ngIf="!hasConfig">
|
||||
@@ -78,7 +68,7 @@
|
||||
<form-object
|
||||
[objectSpec]="configSpec"
|
||||
[formGroup]="configForm"
|
||||
[current]="current"
|
||||
[current]="configForm.value"
|
||||
[showEdited]="true"
|
||||
></form-object>
|
||||
</form>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { AlertController, ModalController, IonContent, LoadingController, IonicSafeString } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { isEmptyObject, isObject, Recommendation } from 'src/app/util/misc.util'
|
||||
import { DependentInfo, isEmptyObject } from 'src/app/util/misc.util'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
@@ -10,6 +10,7 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ErrorToastService, getErrorMessage } from 'src/app/services/error-toast.service'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { convertValuesRecursive, FormService } from 'src/app/services/form.service'
|
||||
import { compare, Operation } from 'fast-json-patch'
|
||||
|
||||
@Component({
|
||||
selector: 'app-config',
|
||||
@@ -19,16 +20,14 @@ import { convertValuesRecursive, FormService } from 'src/app/services/form.servi
|
||||
export class AppConfigPage {
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
@Input() pkgId: string
|
||||
@Input() rec: Recommendation | null = null
|
||||
@Input() dependentInfo?: DependentInfo
|
||||
pkg: PackageDataEntry
|
||||
loadingText: string | undefined
|
||||
configSpec: ConfigSpec
|
||||
configForm: FormGroup
|
||||
current: object
|
||||
original: object
|
||||
hasConfig = false
|
||||
saving = false
|
||||
showRec = true
|
||||
openRec = false
|
||||
loadingError: string | IonicSafeString
|
||||
|
||||
constructor (
|
||||
@@ -49,23 +48,32 @@ export class AppConfigPage {
|
||||
if (!this.hasConfig) return
|
||||
|
||||
try {
|
||||
|
||||
let oldConfig: object
|
||||
let newConfig: object
|
||||
let spec: ConfigSpec
|
||||
if (this.rec) {
|
||||
this.loadingText = `Setting properties to accommodate ${this.rec.dependentTitle}`
|
||||
const { 'old-config': oc, 'new-config': nc, spec: s } = await this.embassyApi.dryConfigureDependency({ 'dependency-id': this.pkgId, 'dependent-id': this.rec.dependentId })
|
||||
let patch: Operation[]
|
||||
if (this.dependentInfo) {
|
||||
this.loadingText = `Setting properties to accommodate ${this.dependentInfo.title}`
|
||||
const { 'old-config': oc, 'new-config': nc, spec: s } = await this.embassyApi.dryConfigureDependency({ 'dependency-id': this.pkgId, 'dependent-id': this.dependentInfo.id })
|
||||
oldConfig = oc
|
||||
newConfig = nc
|
||||
spec = s
|
||||
patch = compare(oldConfig, newConfig)
|
||||
} else {
|
||||
this.loadingText = 'Loading Config'
|
||||
const { config: oc, spec: s } = await this.embassyApi.getPackageConfig({ id: this.pkgId })
|
||||
oldConfig = oc
|
||||
const { config: c, spec: s } = await this.embassyApi.getPackageConfig({ id: this.pkgId })
|
||||
oldConfig = c
|
||||
spec = s
|
||||
}
|
||||
this.setConfig(spec, oldConfig, newConfig)
|
||||
|
||||
this.original = oldConfig
|
||||
this.configSpec = spec
|
||||
this.configForm = this.formService.createForm(spec, newConfig || oldConfig)
|
||||
this.configForm.markAllAsTouched()
|
||||
|
||||
if (patch) {
|
||||
this.markDirty(patch)
|
||||
}
|
||||
} catch (e) {
|
||||
this.loadingError = getErrorMessage(e)
|
||||
} finally {
|
||||
@@ -79,11 +87,8 @@ export class AppConfigPage {
|
||||
|
||||
resetDefaults () {
|
||||
this.configForm = this.formService.createForm(this.configSpec)
|
||||
this.alterConfigRecursive(this.configForm, this.current)
|
||||
}
|
||||
|
||||
dismissRec () {
|
||||
this.showRec = false
|
||||
const patch = compare(this.original, this.configForm.value)
|
||||
this.markDirty(patch)
|
||||
}
|
||||
|
||||
async dismiss () {
|
||||
@@ -143,33 +148,20 @@ export class AppConfigPage {
|
||||
}
|
||||
}
|
||||
|
||||
private setConfig (spec: ConfigSpec, config: object, depConfig?: object) {
|
||||
this.configSpec = spec
|
||||
this.current = config
|
||||
this.configForm = this.formService.createForm(spec, { ...config, ...depConfig })
|
||||
this.configForm.markAllAsTouched()
|
||||
private markDirty (patch: Operation[]) {
|
||||
patch.forEach(op => {
|
||||
const arrPath = op.path.substring(1)
|
||||
.split('/')
|
||||
.map(node => {
|
||||
const num = Number(node)
|
||||
return isNaN(num) ? node : num
|
||||
})
|
||||
|
||||
if (depConfig) {
|
||||
this.alterConfigRecursive(this.configForm, depConfig)
|
||||
}
|
||||
}
|
||||
if (op.op !== 'remove') this.configForm.get(arrPath).markAsDirty()
|
||||
|
||||
private alterConfigRecursive (group: FormGroup, config: object) {
|
||||
Object.keys(config).forEach(key => {
|
||||
const next = group.get(key)
|
||||
if (!next) throw new Error('Dependency config not compatible with service version. Please contact support')
|
||||
const newVal = config[key]
|
||||
// check if val is an object
|
||||
if (isObject(newVal)) {
|
||||
this.alterConfigRecursive(next as FormGroup, newVal)
|
||||
} else {
|
||||
let val1 = group.get(key).value
|
||||
let val2 = config[key]
|
||||
if (Array.isArray(newVal)) {
|
||||
val1 = JSON.stringify(val1)
|
||||
val2 = JSON.stringify(val2)
|
||||
}
|
||||
if (val1 != val2) next.markAsDirty()
|
||||
if (typeof arrPath[arrPath.length - 1] === 'number') {
|
||||
const prevPath = arrPath.slice(0, arrPath.length - 1)
|
||||
this.configForm.get(prevPath).markAsDirty()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { ValueSpecListOf } from '../../pkg-config/config-types'
|
||||
// import { Range } from '../../pkg-config/config-utilities'
|
||||
|
||||
@Component({
|
||||
selector: 'enum-list',
|
||||
@@ -13,12 +12,6 @@ export class EnumListPage {
|
||||
@Input() spec: ValueSpecListOf<'enum'>
|
||||
@Input() current: string[]
|
||||
options: { [option: string]: boolean } = { }
|
||||
|
||||
// min: number | undefined
|
||||
// max: number | undefined
|
||||
// minMessage: string
|
||||
// maxMessage: string
|
||||
|
||||
selectAll = true
|
||||
|
||||
constructor (
|
||||
@@ -26,12 +19,6 @@ export class EnumListPage {
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
// const range = Range.from(this.spec.range)
|
||||
// this.min = range.integralMin()
|
||||
// this.max = range.integralMax()
|
||||
// this.minMessage = `The minimum number of ${this.key} is ${this.min}.`
|
||||
// this.maxMessage = `The maximum number of ${this.key} is ${this.max}.`
|
||||
|
||||
for (let val of this.spec.spec.values) {
|
||||
this.options[val] = this.current.includes(val)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { QRComponent } from 'src/app/components/qr/qr.component'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import * as JsonPointer from 'json-pointer'
|
||||
import { getValueByPointer } from 'fast-json-patch'
|
||||
|
||||
@Component({
|
||||
selector: 'app-properties',
|
||||
@@ -49,7 +49,7 @@ export class AppPropertiesPage {
|
||||
.subscribe(queryParams => {
|
||||
if (queryParams['pointer'] === this.pointer) return
|
||||
this.pointer = queryParams['pointer']
|
||||
this.node = JsonPointer.get(this.properties, this.pointer || '')
|
||||
this.node = getValueByPointer(this.properties, this.pointer || '')
|
||||
}),
|
||||
this.patch.watch$('package-data', this.pkgId, 'installed', 'status', 'main', 'status')
|
||||
.subscribe(status => {
|
||||
@@ -119,7 +119,7 @@ export class AppPropertiesPage {
|
||||
this.loading = true
|
||||
try {
|
||||
this.properties = await this.embassyApi.getPackageProperties({ id: this.pkgId })
|
||||
this.node = JsonPointer.get(this.properties, this.pointer || '')
|
||||
this.node = getValueByPointer(this.properties, this.pointer || '')
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
|
||||
@@ -100,7 +100,6 @@
|
||||
<ion-text [color]="!!dep.errorText ? 'warning' : 'success'">{{ dep.errorText || 'satisfied' }}</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-spinner *ngIf="dep.spinnerColor" slot="end" [color]="dep.spinnerColor" style="height: 3vh; width: 3vh"></ion-spinner>
|
||||
<ion-button *ngIf="dep.actionText" slot="end" fill="clear">
|
||||
{{ dep.actionText }}
|
||||
<ion-icon slot="end" name="arrow-forward"></ion-icon>
|
||||
|
||||
@@ -2,13 +2,13 @@ import { Component, ViewChild } from '@angular/core'
|
||||
import { AlertController, NavController, ModalController, IonContent, LoadingController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ActivatedRoute, NavigationExtras } from '@angular/router'
|
||||
import { exists, isEmptyObject, Recommendation } from 'src/app/util/misc.util'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { DependentInfo, exists, isEmptyObject } from 'src/app/util/misc.util'
|
||||
import { combineLatest, Subscription } from 'rxjs'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { DependencyErrorConfigUnsatisfied, DependencyErrorType, HealthCheckResult, HealthResult, PackageDataEntry, PackageMainStatus, PackageState } from 'src/app/services/patch-db/data-model'
|
||||
import { DependencyError, DependencyErrorType, HealthCheckResult, HealthResult, PackageDataEntry, PackageMainStatus, PackageState } from 'src/app/services/patch-db/data-model'
|
||||
import { DependencyStatus, HealthStatus, PrimaryRendering, PrimaryStatus, renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { ConnectionFailure, ConnectionService } from 'src/app/services/connection.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
@@ -83,34 +83,18 @@ export class AppShowPage {
|
||||
}),
|
||||
|
||||
// 2
|
||||
this.patch.watch$('package-data', this.pkgId, 'installed', 'current-dependencies')
|
||||
combineLatest([
|
||||
this.patch.watch$('package-data', this.pkgId, 'installed', 'current-dependencies'),
|
||||
this.patch.watch$('package-data', this.pkgId, 'installed', 'status', 'dependency-errors'),
|
||||
])
|
||||
.pipe(
|
||||
filter(obj => exists(obj)),
|
||||
filter(([currentDeps, depErrors]) => exists(currentDeps) && exists(depErrors)),
|
||||
)
|
||||
.subscribe(currentDeps => {
|
||||
// remove deleted
|
||||
this.dependencies.forEach((dep, i) => {
|
||||
if (!currentDeps[dep.id]) {
|
||||
dep.sub.unsubscribe()
|
||||
this.dependencies.splice(i, 1)
|
||||
}
|
||||
})
|
||||
|
||||
// subscribe
|
||||
Object.keys(currentDeps)
|
||||
.filter(id => {
|
||||
const inManifest = !!this.pkg.manifest.dependencies[id]
|
||||
const exists = this.dependencies.find(d => d.id === id)
|
||||
return inManifest && !exists
|
||||
})
|
||||
.forEach(id => {
|
||||
const version = this.pkg.manifest.dependencies[id].version
|
||||
const dep = { id, version } as DependencyInfo
|
||||
dep.sub = this.patch.watch$('package-data', id)
|
||||
.subscribe(localDep => {
|
||||
this.setDepValues(dep, localDep)
|
||||
})
|
||||
this.dependencies.push(dep)
|
||||
.subscribe(([currentDeps, depErrors]) => {
|
||||
this.dependencies = Object.keys(currentDeps)
|
||||
.filter(id => !!this.pkg.manifest.dependencies[id])
|
||||
.map(id => {
|
||||
return this.setDepValues(id, depErrors)
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -142,9 +126,6 @@ export class AppShowPage {
|
||||
|
||||
ngOnDestroy () {
|
||||
this.subs.forEach(sub => sub.unsubscribe())
|
||||
this.dependencies.forEach(dep => {
|
||||
dep.sub.unsubscribe()
|
||||
})
|
||||
}
|
||||
|
||||
launchUi (): void {
|
||||
@@ -213,7 +194,7 @@ export class AppShowPage {
|
||||
}
|
||||
}
|
||||
|
||||
async presentModalConfig (props: { pkgId: string, rec?: Recommendation }): Promise<void> {
|
||||
async presentModalConfig (props: { pkgId: string, dependentInfo?: DependentInfo }): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: AppConfigPage,
|
||||
componentProps: props,
|
||||
@@ -221,36 +202,27 @@ export class AppShowPage {
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private setDepValues (dep: DependencyInfo, localDep: PackageDataEntry | undefined): void {
|
||||
private setDepValues (id: string, errors: { [id: string]: DependencyError }): DependencyInfo {
|
||||
let errorText = ''
|
||||
let spinnerColor = ''
|
||||
let actionText = 'View'
|
||||
let action: () => any = () => this.navCtrl.navigateForward(`/services/${dep.id}`)
|
||||
let action: () => any = () => this.navCtrl.navigateForward(`/services/${id}`)
|
||||
|
||||
const error = this.pkg.installed.status['dependency-errors'][dep.id]
|
||||
const error = errors[id]
|
||||
|
||||
if (error) {
|
||||
// health checks failed
|
||||
if ([DependencyErrorType.InterfaceHealthChecksFailed, DependencyErrorType.HealthChecksFailed].includes(error.type)) {
|
||||
errorText = 'Health check failed'
|
||||
// not fully installed (same as !localDep?.installed)
|
||||
// not installed
|
||||
} else if (error.type === DependencyErrorType.NotInstalled) {
|
||||
if (localDep) {
|
||||
errorText = localDep.state // 'Installing' | 'Removing'
|
||||
} else {
|
||||
errorText = 'Not installed'
|
||||
actionText = 'Install'
|
||||
action = () => this.fixDep('install', dep.id)
|
||||
}
|
||||
errorText = 'Not installed'
|
||||
actionText = 'Install'
|
||||
action = () => this.fixDep('install', id)
|
||||
// incorrect version
|
||||
} else if (error.type === DependencyErrorType.IncorrectVersion) {
|
||||
if (localDep) {
|
||||
errorText = localDep.state // 'Updating' | 'Removing'
|
||||
} else {
|
||||
errorText = 'Incorrect version'
|
||||
actionText = 'Update'
|
||||
action = () => this.fixDep('update', dep.id)
|
||||
}
|
||||
errorText = 'Incorrect version'
|
||||
actionText = 'Update'
|
||||
action = () => this.fixDep('update', id)
|
||||
// not running
|
||||
} else if (error.type === DependencyErrorType.NotRunning) {
|
||||
errorText = 'Not running'
|
||||
@@ -259,60 +231,50 @@ export class AppShowPage {
|
||||
} else if (error.type === DependencyErrorType.ConfigUnsatisfied) {
|
||||
errorText = 'Config not satisfied'
|
||||
actionText = 'Auto config'
|
||||
action = () => this.fixDep('configure', dep.id)
|
||||
action = () => this.fixDep('configure', id)
|
||||
} else if (error.type === DependencyErrorType.Transitive) {
|
||||
errorText = 'Dependency has a dependency issue'
|
||||
}
|
||||
|
||||
if (localDep && localDep.state !== PackageState.Installed) {
|
||||
spinnerColor = localDep.state === PackageState.Removing ? 'danger' : 'primary'
|
||||
}
|
||||
}
|
||||
|
||||
const depInfo = this.pkg.installed['dependency-info'][dep.id]
|
||||
const depInfo = this.pkg.installed['dependency-info'][id]
|
||||
|
||||
Object.assign(dep, {
|
||||
return {
|
||||
id,
|
||||
version: this.pkg.manifest.dependencies[id].version,
|
||||
title: depInfo.manifest.title,
|
||||
icon: depInfo.icon,
|
||||
errorText,
|
||||
actionText,
|
||||
spinnerColor,
|
||||
action,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async installDep (depId: string): Promise<void> {
|
||||
const title = this.pkg.installed['dependency-info'][depId].manifest.title
|
||||
const version = this.pkg.manifest.dependencies[depId].version
|
||||
const dependentTitle = this.pkg.manifest.title
|
||||
|
||||
const installRec: Recommendation = {
|
||||
dependentId: this.pkgId,
|
||||
dependentTitle,
|
||||
dependentIcon: this.pkg['static-files'].icon,
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: this.pkgId,
|
||||
title: this.pkg.manifest.title,
|
||||
version,
|
||||
description: `${dependentTitle} requires an install of ${title} satisfying ${version}.`,
|
||||
}
|
||||
const navigationExtras: NavigationExtras = {
|
||||
state: { installRec },
|
||||
state: { dependentInfo },
|
||||
}
|
||||
|
||||
await this.navCtrl.navigateForward(`/marketplace/${depId}`, navigationExtras)
|
||||
}
|
||||
|
||||
private async configureDep (depId: string): Promise<void> {
|
||||
const configRecommendation: Recommendation = {
|
||||
dependentId: this.pkgId,
|
||||
dependentTitle: this.pkg.manifest.title,
|
||||
dependentIcon: this.pkg['static-files'].icon,
|
||||
description: (this.pkg.installed.status['dependency-errors'][depId] as DependencyErrorConfigUnsatisfied).error,
|
||||
}
|
||||
const params = {
|
||||
pkgId: depId,
|
||||
rec: configRecommendation,
|
||||
private async configureDep (dependencyId: string): Promise<void> {
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: this.pkgId,
|
||||
title: this.pkg.manifest.title,
|
||||
}
|
||||
|
||||
await this.presentModalConfig(params)
|
||||
await this.presentModalConfig({
|
||||
pkgId: dependencyId,
|
||||
dependentInfo,
|
||||
})
|
||||
}
|
||||
|
||||
private async presentAlertStart (message: string): Promise<void> {
|
||||
@@ -434,10 +396,8 @@ interface DependencyInfo {
|
||||
icon: string
|
||||
version: string
|
||||
errorText: string
|
||||
spinnerColor: string
|
||||
actionText: string
|
||||
action: () => any
|
||||
sub: Subscription
|
||||
}
|
||||
|
||||
interface Button {
|
||||
|
||||
@@ -72,23 +72,21 @@
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
<!-- recommendation -->
|
||||
<ion-item *ngIf="rec && showRec" class="rec-item">
|
||||
<!-- auto-config -->
|
||||
<ion-item lines="none" *ngIf="dependentInfo" class="rec-item">
|
||||
<ion-label>
|
||||
<h2 style="display: flex; align-items: center;">
|
||||
<ion-thumbnail style="height: 3vh; width: 3vh; margin: 5px" slot="start">
|
||||
<img [src]="rec.dependentIcon" [alt]="rec.dependentTitle"/>
|
||||
</ion-thumbnail>
|
||||
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: smaller;">{{ rec.dependentTitle }}</ion-text>
|
||||
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: 18px;">{{ pkg.manifest.title }}</ion-text>
|
||||
</h2>
|
||||
<div style="margin: 7px 5px;">
|
||||
<p style="color: var(--ion-color-dark); font-size: small">{{ rec.description }}</p>
|
||||
<p *ngIf="pkg.manifest.version | satisfiesEmver: rec.version" class="recommendation-text">{{ pkg.manifest.title }} version {{ pkg.manifest.version | displayEmver }} is compatible.</p>
|
||||
<p *ngIf="!(pkg.manifest.version | satisfiesEmver: rec.version)" class="recommendation-text recommendation-error">{{ pkg.manifest.title }} version {{ pkg.manifest.version | displayEmver }} is NOT compatible.</p>
|
||||
<ion-button style="position: absolute; right: 0; top: 0" fill="clear" (click)="dismissRec()">
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
<p>
|
||||
<ion-text color="dark">
|
||||
{{ dependentInfo.title }} requires an install of {{ pkg.manifest.title }} satisfying {{ dependentInfo.version }}.
|
||||
<br />
|
||||
<br />
|
||||
<span *ngIf="pkg.manifest.version | satisfiesEmver: dependentInfo.version" class="recommendation-text">{{ pkg.manifest.title }} version {{ pkg.manifest.version | displayEmver }} is compatible.</span>
|
||||
<span *ngIf="!(pkg.manifest.version | satisfiesEmver: dependentInfo.version)" class="recommendation-text recommendation-error">{{ pkg.manifest.title }} version {{ pkg.manifest.version | displayEmver }} is NOT compatible.</span>
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { wizardModal } from 'src/app/components/install-wizard/install-wizard.co
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { Emver } from 'src/app/services/emver.service'
|
||||
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
||||
import { pauseFor, Recommendation } from 'src/app/util/misc.util'
|
||||
import { DependentInfo, pauseFor } from 'src/app/util/misc.util'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { PackageDataEntry, PackageState } from 'src/app/services/patch-db/data-model'
|
||||
@@ -27,8 +27,7 @@ export class MarketplaceShowPage {
|
||||
pkg: MarketplacePkg
|
||||
localPkg: PackageDataEntry
|
||||
PackageState = PackageState
|
||||
rec: Recommendation | null = null
|
||||
showRec = true
|
||||
dependentInfo: DependentInfo
|
||||
subs: Subscription[] = []
|
||||
|
||||
constructor (
|
||||
@@ -47,7 +46,7 @@ export class MarketplaceShowPage {
|
||||
|
||||
async ngOnInit () {
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
this.rec = history.state && history.state.installRec as Recommendation
|
||||
this.dependentInfo = history.state && history.state.dependentInfo as DependentInfo
|
||||
|
||||
this.subs = [
|
||||
this.patch.watch$('package-data', this.pkgId)
|
||||
@@ -171,10 +170,6 @@ export class MarketplaceShowPage {
|
||||
this.navCtrl.back()
|
||||
}
|
||||
|
||||
dismissRec () {
|
||||
this.showRec = false
|
||||
}
|
||||
|
||||
private async getPkg (version?: string): Promise<void> {
|
||||
this.loading = true
|
||||
try {
|
||||
|
||||
@@ -84,7 +84,7 @@ export class ServerBackupPage {
|
||||
take(1),
|
||||
)
|
||||
.subscribe(pkgs => {
|
||||
const pkgArr = Object.values(pkgs)
|
||||
const pkgArr = Object.keys(pkgs).sort().map(key => pkgs[key])
|
||||
const activeIndex = pkgArr.findIndex(pkg => pkg.installed.status.main.status === PackageMainStatus.BackingUp)
|
||||
|
||||
this.pkgs = pkgArr.map((pkg, i) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DependencyErrorType, DockerIoFormat, Manifest, PackageDataEntry, PackageMainStatus, PackageState } from 'src/app/services/patch-db/data-model'
|
||||
import { Log, MarketplacePkg, Metric, NotificationLevel, RR, ServerNotifications } from './api.types'
|
||||
import { Operation } from 'fast-json-patch'
|
||||
|
||||
export module Mock {
|
||||
|
||||
@@ -1081,405 +1082,21 @@ export module Mock {
|
||||
},
|
||||
} as any // @TODO why is this necessary?
|
||||
|
||||
export const PackageConfig: RR.GetPackageConfigRes = {
|
||||
// config spec
|
||||
spec: {
|
||||
'testnet': {
|
||||
'name': 'Testnet',
|
||||
'type': 'boolean',
|
||||
'description': 'determines whether your node is running on testnet or mainnet',
|
||||
'warning': 'Chain will have to resync!',
|
||||
'default': true,
|
||||
},
|
||||
'object-list': {
|
||||
'name': 'Object List',
|
||||
'type': 'list',
|
||||
'subtype': 'object',
|
||||
'description': 'This is a list of objects, like users or something',
|
||||
'range': '[0,4]',
|
||||
'default': [
|
||||
{
|
||||
'first-name': 'Admin',
|
||||
'last-name': 'User',
|
||||
'age': 40,
|
||||
},
|
||||
{
|
||||
'first-name': 'Admin2',
|
||||
'last-name': 'User',
|
||||
'age': 40,
|
||||
},
|
||||
],
|
||||
// the outer spec here, at the list level, says that what's inside (the inner spec) pertains to its inner elements.
|
||||
// it just so happens that ValueSpecObject's have the field { spec: ConfigSpec }
|
||||
// see 'union-list' below for a different example.
|
||||
'spec': {
|
||||
'unique-by': 'last-name',
|
||||
'display-as': `I'm {{last-name}}, {{first-name}} {{last-name}}`,
|
||||
'spec': {
|
||||
'first-name': {
|
||||
'name': 'First Name',
|
||||
'type': 'string',
|
||||
'description': 'User first name',
|
||||
'nullable': true,
|
||||
'default': null,
|
||||
'masked': false,
|
||||
'copyable': false,
|
||||
},
|
||||
'last-name': {
|
||||
'name': 'Last Name',
|
||||
'type': 'string',
|
||||
'description': 'User first name',
|
||||
'nullable': true,
|
||||
'default': {
|
||||
'charset': 'a-g,2-9',
|
||||
'len': 12,
|
||||
},
|
||||
'pattern': '^[a-zA-Z]+$',
|
||||
'pattern-description': 'must contain only letters.',
|
||||
'masked': false,
|
||||
'copyable': true,
|
||||
},
|
||||
'age': {
|
||||
'name': 'Age',
|
||||
'type': 'number',
|
||||
'description': 'The age of the user',
|
||||
'nullable': true,
|
||||
'default': null,
|
||||
'integral': false,
|
||||
'warning': 'User must be at least 18.',
|
||||
'range': '[18,*)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'union-list': {
|
||||
'name': 'Union List',
|
||||
'type': 'list',
|
||||
'subtype': 'union',
|
||||
'description': 'This is a sample list of unions',
|
||||
'warning': 'If you change this, things may work.',
|
||||
// a list of union selections. e.g. 'summer', 'winter',...
|
||||
'default': [
|
||||
'summer',
|
||||
],
|
||||
'range': '[0, 2]',
|
||||
'spec': {
|
||||
'tag': {
|
||||
'id': 'preference',
|
||||
'name': 'Preferences',
|
||||
'variant-names': {
|
||||
'summer': 'Summer',
|
||||
'winter': 'Winter',
|
||||
'other': 'Other',
|
||||
},
|
||||
},
|
||||
// this default is used to make a union selection when a new list element is first created
|
||||
'default': 'summer',
|
||||
'variants': {
|
||||
'summer': {
|
||||
'favorite-tree': {
|
||||
'name': 'Favorite Tree',
|
||||
'type': 'string',
|
||||
'nullable': false,
|
||||
'description': 'What is your favorite tree?',
|
||||
'default': 'Maple',
|
||||
'masked': false,
|
||||
'copyable': false,
|
||||
},
|
||||
'favorite-flower': {
|
||||
'name': 'Favorite Flower',
|
||||
'type': 'enum',
|
||||
'description': 'Select your favorite flower',
|
||||
'value-names': {
|
||||
'none': 'Hate Flowers',
|
||||
'red': 'Red',
|
||||
'blue': 'Blue',
|
||||
'purple': 'Purple',
|
||||
},
|
||||
'values': [
|
||||
'none',
|
||||
'red',
|
||||
'blue',
|
||||
'purple',
|
||||
],
|
||||
'default': 'none',
|
||||
},
|
||||
},
|
||||
'winter': {
|
||||
'like-snow': {
|
||||
'name': 'Like Snow?',
|
||||
'type': 'boolean',
|
||||
'description': 'Do you like snow or not?',
|
||||
'default': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
'unique-by': 'preference',
|
||||
},
|
||||
},
|
||||
'random-enum': {
|
||||
'name': 'Random Enum',
|
||||
'type': 'enum',
|
||||
'value-names': {
|
||||
'null': 'Null',
|
||||
'option1': 'One 1',
|
||||
'option2': 'Two 2',
|
||||
'option3': 'Three 3',
|
||||
},
|
||||
'default': 'null',
|
||||
'description': 'This is not even real.',
|
||||
'warning': 'Be careful changing this!',
|
||||
'values': [
|
||||
'null',
|
||||
'option1',
|
||||
'option2',
|
||||
'option3',
|
||||
],
|
||||
},
|
||||
'favorite-number': {
|
||||
'name': 'Favorite Number',
|
||||
'type': 'number',
|
||||
'integral': false,
|
||||
'description': 'Your favorite number of all time',
|
||||
'warning': 'Once you set this number, it can never be changed without severe consequences.',
|
||||
'nullable': true,
|
||||
'default': 7,
|
||||
'range': '(-100,100]',
|
||||
'units': 'BTC',
|
||||
},
|
||||
'unlucky-numbers': {
|
||||
'name': 'Unlucky Numbers',
|
||||
'type': 'list',
|
||||
'subtype': 'number',
|
||||
'description': 'Numbers that you like but are not your top favorite.',
|
||||
'spec': {
|
||||
'integral': false,
|
||||
'range': '[-100,200)',
|
||||
},
|
||||
'range': '[0,10]',
|
||||
'default': [
|
||||
2,
|
||||
3,
|
||||
],
|
||||
},
|
||||
'rpcsettings': {
|
||||
'name': 'RPC Settings',
|
||||
'type': 'object',
|
||||
'unique-by': null,
|
||||
'description': 'rpc username and password',
|
||||
'warning': 'Adding RPC users gives them special permissions on your node.',
|
||||
'spec': {
|
||||
'laws': {
|
||||
'name': 'Laws',
|
||||
'type': 'object',
|
||||
'unique-by': 'law1',
|
||||
'description': 'the law of the realm',
|
||||
'spec': {
|
||||
'law1': {
|
||||
'name': 'First Law',
|
||||
'type': 'string',
|
||||
'description': 'the first law',
|
||||
'nullable': true,
|
||||
'masked': false,
|
||||
'copyable': true,
|
||||
},
|
||||
'law2': {
|
||||
'name': 'Second Law',
|
||||
'type': 'string',
|
||||
'description': 'the second law',
|
||||
'nullable': true,
|
||||
'masked': false,
|
||||
'copyable': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
'rulemakers': {
|
||||
'name': 'Rule Makers',
|
||||
'type': 'list',
|
||||
'subtype': 'object',
|
||||
'description': 'the people who make the rules',
|
||||
'range': '[0,2]',
|
||||
'default': [],
|
||||
'spec': {
|
||||
'unique-by': null,
|
||||
'spec': {
|
||||
'rulemakername': {
|
||||
'name': 'Rulemaker Name',
|
||||
'type': 'string',
|
||||
'description': 'the name of the rule maker',
|
||||
'nullable': false,
|
||||
'default': {
|
||||
'charset': 'a-g,2-9',
|
||||
'len': 12,
|
||||
},
|
||||
'masked': false,
|
||||
'copyable': false,
|
||||
},
|
||||
'rulemakerip': {
|
||||
'name': 'Rulemaker IP',
|
||||
'type': 'string',
|
||||
'description': 'the ip of the rule maker',
|
||||
'nullable': false,
|
||||
'default': '192.168.1.0',
|
||||
'pattern': '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$',
|
||||
'pattern-description': 'may only contain numbers and periods',
|
||||
'masked': false,
|
||||
'copyable': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'rpcuser': {
|
||||
'name': 'RPC Username',
|
||||
'type': 'string',
|
||||
'description': 'rpc username',
|
||||
'nullable': false,
|
||||
'default': 'defaultrpcusername',
|
||||
'pattern': '^[a-zA-Z]+$',
|
||||
'pattern-description': 'must contain only letters.',
|
||||
'masked': false,
|
||||
'copyable': true,
|
||||
},
|
||||
'rpcpass': {
|
||||
'name': 'RPC User Password',
|
||||
'type': 'string',
|
||||
'description': 'rpc password',
|
||||
'nullable': false,
|
||||
'default': {
|
||||
'charset': 'a-z,A-Z,2-9',
|
||||
'len': 20,
|
||||
},
|
||||
'masked': true,
|
||||
'copyable': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
'advanced': {
|
||||
'name': 'Advanced',
|
||||
'type': 'object',
|
||||
'unique-by': null,
|
||||
'description': 'Advanced settings',
|
||||
'spec': {
|
||||
'notifications': {
|
||||
'name': 'Notification Preferences',
|
||||
'type': 'list',
|
||||
'subtype': 'enum',
|
||||
'description': 'how you want to be notified',
|
||||
'range': '[1,3]',
|
||||
'default': [
|
||||
'email',
|
||||
],
|
||||
'spec': {
|
||||
'value-names': {
|
||||
'email': 'EEEEmail',
|
||||
'text': 'Texxxt',
|
||||
'call': 'Ccccall',
|
||||
'push': 'PuuuusH',
|
||||
'webhook': 'WebHooookkeee',
|
||||
},
|
||||
'values': [
|
||||
'email',
|
||||
'text',
|
||||
'call',
|
||||
'push',
|
||||
'webhook',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'bitcoin-node': {
|
||||
'name': 'Bitcoin Node Settings',
|
||||
'type': 'union',
|
||||
'unique-by': null,
|
||||
'description': 'The node settings',
|
||||
'default': 'internal',
|
||||
'warning': 'Careful changing this',
|
||||
'tag': {
|
||||
'id': 'type',
|
||||
'name': 'Type',
|
||||
'variant-names': {
|
||||
'internal': 'Internal',
|
||||
'external': 'External',
|
||||
},
|
||||
},
|
||||
'variants': {
|
||||
'internal': {
|
||||
'lan-address': {
|
||||
'name': 'LAN Address',
|
||||
'type': 'pointer',
|
||||
'subtype': 'app',
|
||||
'target': 'lan-address',
|
||||
'app-id': 'bitcoind',
|
||||
'description': 'the lan address',
|
||||
},
|
||||
},
|
||||
'external': {
|
||||
'public-domain': {
|
||||
'name': 'Public Domain',
|
||||
'type': 'string',
|
||||
'description': 'the public address of the node',
|
||||
'nullable': false,
|
||||
'default': 'bitcoinnode.com',
|
||||
'pattern': '.*',
|
||||
'pattern-description': 'anything',
|
||||
'masked': false,
|
||||
'copyable': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'port': {
|
||||
'name': 'Port',
|
||||
'type': 'number',
|
||||
'integral': true,
|
||||
'description': 'the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444',
|
||||
'nullable': false,
|
||||
'default': 8333,
|
||||
'range': '(0, 9998]',
|
||||
},
|
||||
'favorite-slogan': {
|
||||
'name': 'Favorite Slogan',
|
||||
'type': 'string',
|
||||
'description': 'You most favorite slogan in the whole world, used for paying you.',
|
||||
'nullable': true,
|
||||
'masked': true,
|
||||
'copyable': true,
|
||||
},
|
||||
'rpcallowip': {
|
||||
'name': 'RPC Allowed IPs',
|
||||
'type': 'list',
|
||||
'subtype': 'string',
|
||||
'description': 'external ip addresses that are authorized to access your Bitcoin node',
|
||||
'warning': 'Any IP you allow here will have RPC access to your Bitcoin node.',
|
||||
'range': '[1,10]',
|
||||
'default': [
|
||||
'192.168.1.1',
|
||||
],
|
||||
'spec': {
|
||||
'masked': false,
|
||||
'copyable': false,
|
||||
'pattern': '((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))',
|
||||
'pattern-description': 'must be a valid ipv4, ipv6, or domain name',
|
||||
},
|
||||
},
|
||||
'rpcauth': {
|
||||
'name': 'RPC Auth',
|
||||
'type': 'list',
|
||||
'subtype': 'string',
|
||||
'description': 'api keys that are authorized to access your Bitcoin node.',
|
||||
'range': '[0,*)',
|
||||
'default': [],
|
||||
'spec': {
|
||||
'masked': false,
|
||||
'copyable': false,
|
||||
},
|
||||
},
|
||||
export const ConfigSpec: RR.GetPackageConfigRes['spec'] = {
|
||||
'testnet': {
|
||||
'name': 'Testnet',
|
||||
'type': 'boolean',
|
||||
'description': 'determines whether your node is running on testnet or mainnet',
|
||||
'warning': 'Chain will have to resync!',
|
||||
'default': true,
|
||||
},
|
||||
// actual config
|
||||
config: {
|
||||
testnet: false,
|
||||
'object-list': [
|
||||
'object-list': {
|
||||
'name': 'Object List',
|
||||
'type': 'list',
|
||||
'subtype': 'object',
|
||||
'description': 'This is a list of objects, like users or something',
|
||||
'range': '[0,4]',
|
||||
'default': [
|
||||
{
|
||||
'first-name': 'Admin',
|
||||
'last-name': 'User',
|
||||
@@ -1491,51 +1108,462 @@ export module Mock {
|
||||
'age': 40,
|
||||
},
|
||||
],
|
||||
'union-list': undefined,
|
||||
'random-enum': 'option1',
|
||||
'favorite-number': null,
|
||||
'secondary-numbers': undefined,
|
||||
rpcsettings: {
|
||||
laws: {
|
||||
law1: 'The first law',
|
||||
law2: 'The second law',
|
||||
// the outer spec here, at the list level, says that what's inside (the inner spec) pertains to its inner elements.
|
||||
// it just so happens that ValueSpecObject's have the field { spec: ConfigSpec }
|
||||
// see 'union-list' below for a different example.
|
||||
'spec': {
|
||||
'unique-by': 'last-name',
|
||||
'display-as': `I'm {{last-name}}, {{first-name}} {{last-name}}`,
|
||||
'spec': {
|
||||
'first-name': {
|
||||
'name': 'First Name',
|
||||
'type': 'string',
|
||||
'description': 'User first name',
|
||||
'nullable': true,
|
||||
'default': null,
|
||||
'masked': false,
|
||||
'copyable': false,
|
||||
},
|
||||
'last-name': {
|
||||
'name': 'Last Name',
|
||||
'type': 'string',
|
||||
'description': 'User first name',
|
||||
'nullable': true,
|
||||
'default': {
|
||||
'charset': 'a-g,2-9',
|
||||
'len': 12,
|
||||
},
|
||||
'pattern': '^[a-zA-Z]+$',
|
||||
'pattern-description': 'must contain only letters.',
|
||||
'masked': false,
|
||||
'copyable': true,
|
||||
},
|
||||
'age': {
|
||||
'name': 'Age',
|
||||
'type': 'number',
|
||||
'description': 'The age of the user',
|
||||
'nullable': true,
|
||||
'default': null,
|
||||
'integral': false,
|
||||
'warning': 'User must be at least 18.',
|
||||
'range': '[18,*)',
|
||||
},
|
||||
},
|
||||
rpcpass: null,
|
||||
rpcuser: '123',
|
||||
rulemakers: [],
|
||||
},
|
||||
advanced: {
|
||||
notifications: ['email'],
|
||||
},
|
||||
'union-list': {
|
||||
'name': 'Union List',
|
||||
'type': 'list',
|
||||
'subtype': 'union',
|
||||
'description': 'This is a sample list of unions',
|
||||
'warning': 'If you change this, things may work.',
|
||||
// a list of union selections. e.g. 'summer', 'winter',...
|
||||
'default': [
|
||||
'summer',
|
||||
],
|
||||
'range': '[0, 2]',
|
||||
'spec': {
|
||||
'tag': {
|
||||
'id': 'preference',
|
||||
'name': 'Preferences',
|
||||
'variant-names': {
|
||||
'summer': 'Summer',
|
||||
'winter': 'Winter',
|
||||
'other': 'Other',
|
||||
},
|
||||
},
|
||||
// this default is used to make a union selection when a new list element is first created
|
||||
'default': 'summer',
|
||||
'variants': {
|
||||
'summer': {
|
||||
'favorite-tree': {
|
||||
'name': 'Favorite Tree',
|
||||
'type': 'string',
|
||||
'nullable': false,
|
||||
'description': 'What is your favorite tree?',
|
||||
'default': 'Maple',
|
||||
'masked': false,
|
||||
'copyable': false,
|
||||
},
|
||||
'favorite-flower': {
|
||||
'name': 'Favorite Flower',
|
||||
'type': 'enum',
|
||||
'description': 'Select your favorite flower',
|
||||
'value-names': {
|
||||
'none': 'Hate Flowers',
|
||||
'red': 'Red',
|
||||
'blue': 'Blue',
|
||||
'purple': 'Purple',
|
||||
},
|
||||
'values': [
|
||||
'none',
|
||||
'red',
|
||||
'blue',
|
||||
'purple',
|
||||
],
|
||||
'default': 'none',
|
||||
},
|
||||
},
|
||||
'winter': {
|
||||
'like-snow': {
|
||||
'name': 'Like Snow?',
|
||||
'type': 'boolean',
|
||||
'description': 'Do you like snow or not?',
|
||||
'default': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
'unique-by': 'preference',
|
||||
},
|
||||
},
|
||||
'random-enum': {
|
||||
'name': 'Random Enum',
|
||||
'type': 'enum',
|
||||
'value-names': {
|
||||
'null': 'Null',
|
||||
'option1': 'One 1',
|
||||
'option2': 'Two 2',
|
||||
'option3': 'Three 3',
|
||||
},
|
||||
'default': 'null',
|
||||
'description': 'This is not even real.',
|
||||
'warning': 'Be careful changing this!',
|
||||
'values': [
|
||||
'null',
|
||||
'option1',
|
||||
'option2',
|
||||
'option3',
|
||||
],
|
||||
},
|
||||
'favorite-number': {
|
||||
'name': 'Favorite Number',
|
||||
'type': 'number',
|
||||
'integral': false,
|
||||
'description': 'Your favorite number of all time',
|
||||
'warning': 'Once you set this number, it can never be changed without severe consequences.',
|
||||
'nullable': true,
|
||||
'default': 7,
|
||||
'range': '(-100,100]',
|
||||
'units': 'BTC',
|
||||
},
|
||||
'unlucky-numbers': {
|
||||
'name': 'Unlucky Numbers',
|
||||
'type': 'list',
|
||||
'subtype': 'number',
|
||||
'description': 'Numbers that you like but are not your top favorite.',
|
||||
'spec': {
|
||||
'integral': false,
|
||||
'range': '[-100,200)',
|
||||
},
|
||||
'range': '[0,10]',
|
||||
'default': [
|
||||
2,
|
||||
3,
|
||||
],
|
||||
},
|
||||
'rpcsettings': {
|
||||
'name': 'RPC Settings',
|
||||
'type': 'object',
|
||||
'unique-by': null,
|
||||
'description': 'rpc username and password',
|
||||
'warning': 'Adding RPC users gives them special permissions on your node.',
|
||||
'spec': {
|
||||
'laws': {
|
||||
'name': 'Laws',
|
||||
'type': 'object',
|
||||
'unique-by': 'law1',
|
||||
'description': 'the law of the realm',
|
||||
'spec': {
|
||||
'law1': {
|
||||
'name': 'First Law',
|
||||
'type': 'string',
|
||||
'description': 'the first law',
|
||||
'nullable': true,
|
||||
'masked': false,
|
||||
'copyable': true,
|
||||
},
|
||||
'law2': {
|
||||
'name': 'Second Law',
|
||||
'type': 'string',
|
||||
'description': 'the second law',
|
||||
'nullable': true,
|
||||
'masked': false,
|
||||
'copyable': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
'rulemakers': {
|
||||
'name': 'Rule Makers',
|
||||
'type': 'list',
|
||||
'subtype': 'object',
|
||||
'description': 'the people who make the rules',
|
||||
'range': '[0,2]',
|
||||
'default': [],
|
||||
'spec': {
|
||||
'unique-by': null,
|
||||
'spec': {
|
||||
'rulemakername': {
|
||||
'name': 'Rulemaker Name',
|
||||
'type': 'string',
|
||||
'description': 'the name of the rule maker',
|
||||
'nullable': false,
|
||||
'default': {
|
||||
'charset': 'a-g,2-9',
|
||||
'len': 12,
|
||||
},
|
||||
'masked': false,
|
||||
'copyable': false,
|
||||
},
|
||||
'rulemakerip': {
|
||||
'name': 'Rulemaker IP',
|
||||
'type': 'string',
|
||||
'description': 'the ip of the rule maker',
|
||||
'nullable': false,
|
||||
'default': '192.168.1.0',
|
||||
'pattern': '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$',
|
||||
'pattern-description': 'may only contain numbers and periods',
|
||||
'masked': false,
|
||||
'copyable': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'rpcuser': {
|
||||
'name': 'RPC Username',
|
||||
'type': 'string',
|
||||
'description': 'rpc username',
|
||||
'nullable': false,
|
||||
'default': 'defaultrpcusername',
|
||||
'pattern': '^[a-zA-Z]+$',
|
||||
'pattern-description': 'must contain only letters.',
|
||||
'masked': false,
|
||||
'copyable': true,
|
||||
},
|
||||
'rpcpass': {
|
||||
'name': 'RPC User Password',
|
||||
'type': 'string',
|
||||
'description': 'rpc password',
|
||||
'nullable': false,
|
||||
'default': {
|
||||
'charset': 'a-z,A-Z,2-9',
|
||||
'len': 20,
|
||||
},
|
||||
'masked': true,
|
||||
'copyable': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
'advanced': {
|
||||
'name': 'Advanced',
|
||||
'type': 'object',
|
||||
'unique-by': null,
|
||||
'description': 'Advanced settings',
|
||||
'spec': {
|
||||
'notifications': {
|
||||
'name': 'Notification Preferences',
|
||||
'type': 'list',
|
||||
'subtype': 'enum',
|
||||
'description': 'how you want to be notified',
|
||||
'range': '[1,3]',
|
||||
'default': [
|
||||
'email',
|
||||
],
|
||||
'spec': {
|
||||
'value-names': {
|
||||
'email': 'EEEEmail',
|
||||
'text': 'Texxxt',
|
||||
'call': 'Ccccall',
|
||||
'push': 'PuuuusH',
|
||||
'webhook': 'WebHooookkeee',
|
||||
},
|
||||
'values': [
|
||||
'email',
|
||||
'text',
|
||||
'call',
|
||||
'push',
|
||||
'webhook',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'bitcoin-node': {
|
||||
'name': 'Bitcoin Node Settings',
|
||||
'type': 'union',
|
||||
'unique-by': null,
|
||||
'description': 'The node settings',
|
||||
'default': 'internal',
|
||||
'warning': 'Careful changing this',
|
||||
'tag': {
|
||||
'id': 'type',
|
||||
'name': 'Type',
|
||||
'variant-names': {
|
||||
'internal': 'Internal',
|
||||
'external': 'External',
|
||||
},
|
||||
},
|
||||
'variants': {
|
||||
'internal': {
|
||||
'lan-address': {
|
||||
'name': 'LAN Address',
|
||||
'type': 'pointer',
|
||||
'subtype': 'app',
|
||||
'target': 'lan-address',
|
||||
'app-id': 'bitcoind',
|
||||
'description': 'the lan address',
|
||||
},
|
||||
},
|
||||
'external': {
|
||||
'public-domain': {
|
||||
'name': 'Public Domain',
|
||||
'type': 'string',
|
||||
'description': 'the public address of the node',
|
||||
'nullable': false,
|
||||
'default': 'bitcoinnode.com',
|
||||
'pattern': '.*',
|
||||
'pattern-description': 'anything',
|
||||
'masked': false,
|
||||
'copyable': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'port': {
|
||||
'name': 'Port',
|
||||
'type': 'number',
|
||||
'integral': true,
|
||||
'description': 'the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444',
|
||||
'nullable': false,
|
||||
'default': 8333,
|
||||
'range': '(0, 9998]',
|
||||
},
|
||||
'favorite-slogan': {
|
||||
'name': 'Favorite Slogan',
|
||||
'type': 'string',
|
||||
'description': 'You most favorite slogan in the whole world, used for paying you.',
|
||||
'nullable': true,
|
||||
'masked': true,
|
||||
'copyable': true,
|
||||
},
|
||||
'rpcallowip': {
|
||||
'name': 'RPC Allowed IPs',
|
||||
'type': 'list',
|
||||
'subtype': 'string',
|
||||
'description': 'external ip addresses that are authorized to access your Bitcoin node',
|
||||
'warning': 'Any IP you allow here will have RPC access to your Bitcoin node.',
|
||||
'range': '[1,10]',
|
||||
'default': [
|
||||
'192.168.1.1',
|
||||
],
|
||||
'spec': {
|
||||
'masked': false,
|
||||
'copyable': false,
|
||||
'pattern': '((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))',
|
||||
'pattern-description': 'must be a valid ipv4, ipv6, or domain name',
|
||||
},
|
||||
},
|
||||
'rpcauth': {
|
||||
'name': 'RPC Auth',
|
||||
'type': 'list',
|
||||
'subtype': 'string',
|
||||
'description': 'api keys that are authorized to access your Bitcoin node.',
|
||||
'range': '[0,*)',
|
||||
'default': [],
|
||||
'spec': {
|
||||
'masked': false,
|
||||
'copyable': false,
|
||||
},
|
||||
'bitcoin-node': undefined,
|
||||
port: 5959,
|
||||
rpcallowip: undefined,
|
||||
rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'],
|
||||
},
|
||||
}
|
||||
|
||||
export const mockCupsDependentConfig = {
|
||||
'random-enum': 'option1',
|
||||
export const MockConfig = {
|
||||
testnet: false,
|
||||
'favorite-number': 8,
|
||||
'secondary-numbers': [13, 58, 20],
|
||||
'object-list': [],
|
||||
'union-list': [],
|
||||
'object-list': [
|
||||
{
|
||||
'first-name': 'First',
|
||||
'last-name': 'Last',
|
||||
'age': 30,
|
||||
},
|
||||
{
|
||||
'first-name': 'First2',
|
||||
'last-name': 'Last2',
|
||||
'age': 40,
|
||||
},
|
||||
],
|
||||
'union-list': undefined,
|
||||
'random-enum': 'option1',
|
||||
'favorite-number': null,
|
||||
rpcsettings: {
|
||||
laws: null,
|
||||
laws: {
|
||||
law1: 'The first law',
|
||||
law2: 'The second law',
|
||||
},
|
||||
rpcpass: null,
|
||||
rpcuser: '123',
|
||||
rulemakers: [],
|
||||
},
|
||||
advanced: {
|
||||
notifications: [],
|
||||
notifications: ['email', 'text'],
|
||||
},
|
||||
'bitcoin-Node': { type: 'internal' },
|
||||
'bitcoin-node': undefined,
|
||||
port: 5959,
|
||||
rpcallowip: [],
|
||||
rpcallowip: undefined,
|
||||
rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'],
|
||||
}
|
||||
|
||||
export const MockDependencyConfig = {
|
||||
testnet: true,
|
||||
'object-list': [
|
||||
{
|
||||
'first-name': 'First',
|
||||
'last-name': 'Last',
|
||||
'age': 30,
|
||||
},
|
||||
{
|
||||
'first-name': 'First2',
|
||||
'last-name': 'Last2',
|
||||
'age': 40,
|
||||
},
|
||||
{
|
||||
'first-name': 'First3',
|
||||
'last-name': 'Last3',
|
||||
'age': 60,
|
||||
},
|
||||
],
|
||||
'union-list': undefined,
|
||||
'random-enum': 'option2',
|
||||
'favorite-number': null,
|
||||
rpcsettings: {
|
||||
laws: {
|
||||
law1: 'The first law Amended',
|
||||
law2: 'The second law',
|
||||
},
|
||||
rpcpass: null,
|
||||
rpcuser: '123',
|
||||
rulemakers: [],
|
||||
},
|
||||
advanced: {
|
||||
notifications: ['email', 'text', 'push'],
|
||||
},
|
||||
'bitcoin-node': undefined,
|
||||
port: 20,
|
||||
rpcallowip: undefined,
|
||||
rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'],
|
||||
}
|
||||
|
||||
export const Patch: Operation[] = [
|
||||
{
|
||||
op: 'replace',
|
||||
path: '/testnet',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
op: 'add',
|
||||
path: '/advanced/notifications/1',
|
||||
value: 'text',
|
||||
},
|
||||
]
|
||||
|
||||
export const bitcoind: PackageDataEntry = {
|
||||
state: PackageState.Installed,
|
||||
'static-files': {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { pauseFor } from '../../util/misc.util'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import { PatchOp } from 'patch-db-client'
|
||||
import { DependencyErrorType, InstallProgress, PackageDataEntry, PackageMainStatus, PackageState, ServerStatus } from 'src/app/services/patch-db/data-model'
|
||||
import { RR, WithRevision } from './api.types'
|
||||
import { Log, RR, WithRevision } from './api.types'
|
||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||
import { Mock } from './api.fixures'
|
||||
import { HttpService } from '../http.service'
|
||||
@@ -75,7 +75,7 @@ export class MockApiService extends ApiService {
|
||||
|
||||
async getServerLogs (params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
|
||||
await pauseFor(2000)
|
||||
let entries
|
||||
let entries: Log[]
|
||||
if (Math.random() < .2) {
|
||||
entries = Mock.ServerLogs
|
||||
} else {
|
||||
@@ -390,7 +390,10 @@ export class MockApiService extends ApiService {
|
||||
|
||||
async getPackageConfig (params: RR.GetPackageConfigReq): Promise<RR.GetPackageConfigRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.PackageConfig
|
||||
return {
|
||||
config: Mock.MockConfig,
|
||||
spec: Mock.ConfigSpec,
|
||||
}
|
||||
}
|
||||
|
||||
async drySetPackageConfig (params: RR.DrySetPackageConfigReq): Promise<RR.DrySetPackageConfigRes> {
|
||||
@@ -542,32 +545,9 @@ export class MockApiService extends ApiService {
|
||||
async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise<RR.DryConfigureDependencyRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
'old-config': Mock.PackageConfig.config,
|
||||
spec: Mock.PackageConfig.spec,
|
||||
'new-config': {
|
||||
testnet: true,
|
||||
// objectList: [],
|
||||
// unionList: [],
|
||||
randomEnum: 'option2',
|
||||
favoriteNumber: 9,
|
||||
secondaryNumbers: [2, 3, 5, 6],
|
||||
rpcsettings: {
|
||||
laws: {
|
||||
law1: 'The 1st law',
|
||||
law2: 'The 2nd law',
|
||||
},
|
||||
rpcpass: null,
|
||||
rpcuser: '123',
|
||||
rulemakers: [],
|
||||
},
|
||||
advanced: {
|
||||
notifications: ['call', 'text'],
|
||||
},
|
||||
// bitcoinNode: undefined,
|
||||
port: 22,
|
||||
// rpcallowip: undefined,
|
||||
// rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'],
|
||||
},
|
||||
'old-config': Mock.MockConfig,
|
||||
'new-config': Mock.MockDependencyConfig,
|
||||
spec: Mock.ConfigSpec,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'
|
||||
import { ConfigSpec, isValueSpecListOf, ListValueSpecNumber, ListValueSpecObject, ListValueSpecString, ListValueSpecUnion, UniqueBy, ValueSpec, ValueSpecEnum, ValueSpecList, ValueSpecListOf, ValueSpecNumber, ValueSpecObject, ValueSpecString, ValueSpecUnion } from '../pkg-config/config-types'
|
||||
import { ConfigSpec, isValueSpecListOf, ListValueSpecNumber, ListValueSpecObject, ListValueSpecOf, ListValueSpecString, ListValueSpecUnion, UniqueBy, ValueSpec, ValueSpecEnum, ValueSpecList, ValueSpecNumber, ValueSpecObject, ValueSpecString, ValueSpecUnion } from '../pkg-config/config-types'
|
||||
import { getDefaultString, Range } from '../pkg-config/config-utilities'
|
||||
const Mustache = require('mustache')
|
||||
|
||||
@@ -13,8 +13,8 @@ export class FormService {
|
||||
private readonly formBuilder: FormBuilder,
|
||||
) { }
|
||||
|
||||
createForm (config: ConfigSpec, current: { [key: string]: any } = { }): FormGroup {
|
||||
return this.getFormGroup(config, [], current)
|
||||
createForm (spec: ConfigSpec, current: { [key: string]: any } = { }): FormGroup {
|
||||
return this.getFormGroup(spec, [], current)
|
||||
}
|
||||
|
||||
getUnionObject (spec: ValueSpecUnion | ListValueSpecUnion, selection: string, current?: { [key: string]: any }): FormGroup {
|
||||
@@ -60,12 +60,12 @@ export class FormService {
|
||||
let group = { }
|
||||
Object.entries(config).map(([key, spec]) => {
|
||||
if (spec.type === 'pointer') return
|
||||
group[key] = this.getFormEntry(key, spec, current ? current[key] : { })
|
||||
group[key] = this.getFormEntry(spec, current ? current[key] : undefined)
|
||||
})
|
||||
return this.formBuilder.group(group, { validators } )
|
||||
}
|
||||
|
||||
private getFormEntry (key: string, spec: ValueSpec, currentValue: any): FormGroup | FormArray | FormControl {
|
||||
private getFormEntry (spec: ValueSpec, currentValue?: any): FormGroup | FormArray | FormControl {
|
||||
let validators: ValidatorFn[]
|
||||
let value: any
|
||||
switch (spec.type) {
|
||||
@@ -89,7 +89,7 @@ export class FormService {
|
||||
return this.getFormGroup(spec.spec, [], currentValue)
|
||||
case 'list':
|
||||
validators = this.listValidators(spec)
|
||||
const mapped = (Array.isArray(currentValue) ? currentValue : spec.default as any[]).map((entry: any, index) => {
|
||||
const mapped = (Array.isArray(currentValue) ? currentValue : spec.default as any[]).map(entry => {
|
||||
return this.getListItem(spec, entry)
|
||||
})
|
||||
return this.formBuilder.array(mapped, validators)
|
||||
@@ -139,6 +139,8 @@ export class FormService {
|
||||
|
||||
validators.push(listInRange(spec.range))
|
||||
|
||||
validators.push(listItemIssue())
|
||||
|
||||
if (!isValueSpecListOf(spec, 'enum')) {
|
||||
validators.push(listUnique(spec))
|
||||
}
|
||||
@@ -181,29 +183,40 @@ export function isInteger (): ValidatorFn {
|
||||
}
|
||||
|
||||
export function listInRange (stringRange: string): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
return (control: FormArray): ValidationErrors | null => {
|
||||
try {
|
||||
Range.from(stringRange).checkIncludes(control.value.length)
|
||||
return null
|
||||
} catch (e) {
|
||||
return { numberNotInRange: { value: `List must be ${e.message}` } }
|
||||
return { listNotInRange: { value: `List must be ${e.message}` } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function listItemIssue (): ValidatorFn {
|
||||
return (parentControl: FormArray): ValidationErrors | null => {
|
||||
const problemChild = parentControl.controls.find(c => c.invalid)
|
||||
if (problemChild) {
|
||||
return { listItemIssue: { value: 'Invalid entries' } }
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function listUnique (spec: ValueSpecList): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
for (let idx = 0; idx < control.value.length; idx++) {
|
||||
for (let idx2 = idx + 1; idx2 < control.value.length; idx2++) {
|
||||
if (listItemEquals(spec, control.value[idx], control.value[idx2])) {
|
||||
return (control: FormArray): ValidationErrors | null => {
|
||||
const list = control.value
|
||||
for (let idx = 0; idx < list.length; idx++) {
|
||||
for (let idx2 = idx + 1; idx2 < list.length; idx2++) {
|
||||
if (listItemEquals(spec, list[idx], list[idx2])) {
|
||||
let display1: string
|
||||
let display2: string
|
||||
let uniqueMessage = isObjectOrUnion(spec.spec) ? uniqueByMessageWrapper(spec.spec['unique-by'], spec.spec, control.value[idx]) : ''
|
||||
|
||||
let uniqueMessage = isObjectOrUnion(spec.spec) ? uniqueByMessageWrapper(spec.spec['unique-by'], spec.spec, list[idx]) : ''
|
||||
|
||||
if (isObjectOrUnion(spec.spec) && spec.spec['display-as']) {
|
||||
display1 = `"${(Mustache as any).render(spec.spec['display-as'], control.value[idx])}"`
|
||||
display2 = `"${(Mustache as any).render(spec.spec['display-as'], control.value[idx2])}"`
|
||||
display1 = `"${(Mustache as any).render(spec.spec['display-as'], list[idx])}"`
|
||||
display2 = `"${(Mustache as any).render(spec.spec['display-as'], list[idx2])}"`
|
||||
} else {
|
||||
display1 = `Entry ${idx + 1}`
|
||||
display2 = `Entry ${idx2 + 1}`
|
||||
@@ -367,7 +380,7 @@ function uniqueByMessage (uniqueBy: UniqueBy, configSpec: ConfigSpec, outermost
|
||||
return outermost || subSpecs.filter(ss => ss).length === 1 ? ret : '(' + ret + ')'
|
||||
}
|
||||
|
||||
function isObjectOrUnion (spec: any): spec is ListValueSpecObject | ListValueSpecUnion {
|
||||
function isObjectOrUnion (spec: ListValueSpecOf<any>): spec is ListValueSpecObject | ListValueSpecUnion {
|
||||
// only lists of objects and unions have unique-by
|
||||
return spec['unique-by'] !== undefined
|
||||
}
|
||||
|
||||
@@ -5,11 +5,9 @@ import { DriveInfo, PartitionInfo } from '../services/api/api.types'
|
||||
export type Omit<ObjectType, KeysType extends keyof ObjectType> = Pick<ObjectType, Exclude<keyof ObjectType, KeysType>>
|
||||
export type PromiseRes<T> = { result: 'resolve', value: T } | { result: 'reject', value: Error }
|
||||
|
||||
export type Recommendation = {
|
||||
dependentId: string
|
||||
dependentTitle: string
|
||||
dependentIcon: string,
|
||||
description: string
|
||||
export interface DependentInfo {
|
||||
id: string
|
||||
title: string
|
||||
version?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Ajv from 'ajv'
|
||||
import { JsonPointer } from 'jsonpointerx'
|
||||
import { applyOperation } from 'fast-json-patch'
|
||||
|
||||
const ajv = new Ajv({ jsonPointers: true, allErrors: true, nullable: true })
|
||||
const ajvWithDefaults = new Ajv({ jsonPointers: true, allErrors: true, useDefaults: true, nullable: true, removeAdditional: 'failing' })
|
||||
@@ -88,7 +88,7 @@ function parsePropertiesV2Permissive (properties: PackagePropertiesV2, errorCall
|
||||
for (let err of schemaV2Compiled.errors) {
|
||||
errorCallback(new Error(`/data/${idx}${err.dataPath}: ${err.message}`))
|
||||
if (err.dataPath) {
|
||||
JsonPointer.set(value, err.dataPath, undefined)
|
||||
applyOperation(value, { op: 'replace', path: err.dataPath, value: undefined })
|
||||
}
|
||||
}
|
||||
if (!schemaV2CompiledWithDefaults(value)) {
|
||||
|
||||
Reference in New Issue
Block a user