mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +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",
|
"@start9labs/emver": "0.1.5",
|
||||||
"ajv": "^6.12.6",
|
"ajv": "^6.12.6",
|
||||||
"core-js": "^3.17.2",
|
"core-js": "^3.17.2",
|
||||||
|
"fast-json-patch": "^3.1.0",
|
||||||
"fuse.js": "^6.4.6",
|
"fuse.js": "^6.4.6",
|
||||||
"json-pointer": "^0.6.1",
|
|
||||||
"jsonpointerx": "^1.1.4",
|
|
||||||
"marked": "3.0.2",
|
"marked": "3.0.2",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"ng-qrcode": "^5.0.0",
|
"ng-qrcode": "^5.0.0",
|
||||||
|
|||||||
@@ -30,5 +30,8 @@
|
|||||||
<p *ngIf="control.hasError('listNotUnique')">
|
<p *ngIf="control.hasError('listNotUnique')">
|
||||||
{{ control.errors.listNotUnique.value }}
|
{{ control.errors.listNotUnique.value }}
|
||||||
</p>
|
</p>
|
||||||
|
<p *ngIf="control.hasError('listItemIssue')">
|
||||||
|
{{ control.errors.listItemIssue.value }}
|
||||||
|
</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</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-icon name="help-circle-outline"></ion-icon>
|
||||||
</ion-button>
|
</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>
|
<span>{{ data.spec.name }}</span>
|
||||||
|
|
||||||
<ion-text color="success" *ngIf="data.isNew"> (New)</ion-text>
|
<ion-text color="success" *ngIf="data.new"> (New)</ion-text>
|
||||||
<ion-text color="warning" *ngIf="data.isEdited"> (Edited)</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>
|
<span *ngIf="(['string', 'number'] | includes : data.spec.type) && !$any(data.spec).nullable"> *</span>
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,8 @@
|
|||||||
<h4 class="input-label">
|
<h4 class="input-label">
|
||||||
<form-label [data]="{
|
<form-label [data]="{
|
||||||
spec: spec,
|
spec: spec,
|
||||||
isNew: current && current[entry.key] === undefined,
|
new: current && current[entry.key] === undefined,
|
||||||
isEdited: entry.value.dirty
|
edited: entry.value.dirty
|
||||||
}"></form-label>
|
}"></form-label>
|
||||||
</h4>
|
</h4>
|
||||||
<!-- string or number -->
|
<!-- string or number -->
|
||||||
@@ -73,8 +73,8 @@
|
|||||||
<ion-item-divider>
|
<ion-item-divider>
|
||||||
<form-label [data]="{
|
<form-label [data]="{
|
||||||
spec: spec,
|
spec: spec,
|
||||||
isNew: current && current[entry.key] === undefined,
|
new: current && current[entry.key] === undefined,
|
||||||
isEdited: entry.value.dirty
|
edited: entry.value.dirty
|
||||||
}"></form-label>
|
}"></form-label>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<!-- body -->
|
<!-- body -->
|
||||||
@@ -97,8 +97,8 @@
|
|||||||
<ion-item-divider>
|
<ion-item-divider>
|
||||||
<form-label [data]="{
|
<form-label [data]="{
|
||||||
spec: spec,
|
spec: spec,
|
||||||
isNew: current && current[entry.key] === undefined,
|
new: current && current[entry.key] === undefined,
|
||||||
isEdited: entry.value.dirty
|
edited: entry.value.dirty
|
||||||
}"></form-label>
|
}"></form-label>
|
||||||
<ion-button fill="clear" color="primary" slot="end" (click)="addListItemWrapper(entry.key, spec)">
|
<ion-button fill="clear" color="primary" slot="end" (click)="addListItemWrapper(entry.key, spec)">
|
||||||
<ion-icon slot="start" name="add"></ion-icon>
|
<ion-icon slot="start" name="add"></ion-icon>
|
||||||
@@ -117,8 +117,9 @@
|
|||||||
<ion-item button (click)="toggleExpand(entry.key, i)">
|
<ion-item button (click)="toggleExpand(entry.key, i)">
|
||||||
<form-label [data]="{
|
<form-label [data]="{
|
||||||
spec: $any({ name: objectListInfo[entry.key][i].displayAs || 'Entry ' + (i + 1) }),
|
spec: $any({ name: objectListInfo[entry.key][i].displayAs || 'Entry ' + (i + 1) }),
|
||||||
isNew: false,
|
new: false,
|
||||||
isEdited: abstractControl.dirty
|
edited: abstractControl.dirty,
|
||||||
|
invalid: abstractControl.invalid
|
||||||
}"></form-label>
|
}"></form-label>
|
||||||
<ion-icon
|
<ion-icon
|
||||||
slot="end"
|
slot="end"
|
||||||
@@ -192,13 +193,13 @@
|
|||||||
<p class="input-label">
|
<p class="input-label">
|
||||||
<form-label [data]="{
|
<form-label [data]="{
|
||||||
spec: spec,
|
spec: spec,
|
||||||
isNew: current && current[entry.key] === undefined,
|
new: current && current[entry.key] === undefined,
|
||||||
isEdited: entry.value.dirty
|
edited: entry.value.dirty
|
||||||
}"></form-label>
|
}"></form-label>
|
||||||
</p>
|
</p>
|
||||||
<!-- list -->
|
<!-- list -->
|
||||||
<ion-item button detail="false" color="dark" (click)="presentModalEnumList(entry.key, $any(spec), formArr.value)">
|
<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>
|
<h2>{{ getEnumListDisplay(formArr.value, $any(spec.spec)) }}</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-button slot="end" fill="clear" color="light">
|
<ion-button slot="end" fill="clear" color="light">
|
||||||
@@ -211,7 +212,7 @@
|
|||||||
*ngIf="formGroup.get(entry.key).errors"
|
*ngIf="formGroup.get(entry.key).errors"
|
||||||
[control]="$any(formGroup.get(entry.key))"
|
[control]="$any(formGroup.get(entry.key))"
|
||||||
[spec]="spec"
|
[spec]="spec"
|
||||||
>
|
>
|
||||||
</form-error>
|
</form-error>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.help-button {
|
.slot-start {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Component, Input, Output, SimpleChange, EventEmitter } from '@angular/core'
|
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 { 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 { FormService } from 'src/app/services/form.service'
|
||||||
import { Range } from 'src/app/pkg-config/config-utilities'
|
import { Range } from 'src/app/pkg-config/config-utilities'
|
||||||
import { EnumListPage } from 'src/app/modals/enum-list/enum-list.page'
|
import { EnumListPage } from 'src/app/modals/enum-list/enum-list.page'
|
||||||
const Mustache = require('mustache')
|
|
||||||
import { pauseFor } from 'src/app/util/misc.util'
|
import { pauseFor } from 'src/app/util/misc.util'
|
||||||
|
const Mustache = require('mustache')
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'form-object',
|
selector: 'form-object',
|
||||||
@@ -76,6 +76,7 @@ export class FormObjectComponent {
|
|||||||
addListItem (key: string, markDirty = true, val?: string): void {
|
addListItem (key: string, markDirty = true, val?: string): void {
|
||||||
const arr = this.formGroup.get(key) as FormArray
|
const arr = this.formGroup.get(key) as FormArray
|
||||||
if (markDirty) arr.markAsDirty()
|
if (markDirty) arr.markAsDirty()
|
||||||
|
// @TODO why are these commented out?
|
||||||
// const validators = this.formService.getListItemValidators(this.objectSpec[key] as ValueSpecList, key, arr.length)
|
// const validators = this.formService.getListItemValidators(this.objectSpec[key] as ValueSpecList, key, arr.length)
|
||||||
// arr.push(new FormControl(value, validators))
|
// arr.push(new FormControl(value, validators))
|
||||||
const listSpec = this.objectSpec[key] as ValueSpecList
|
const listSpec = this.objectSpec[key] as ValueSpecList
|
||||||
@@ -225,8 +226,9 @@ export class FormObjectComponent {
|
|||||||
|
|
||||||
interface HeaderData {
|
interface HeaderData {
|
||||||
spec: ValueSpec
|
spec: ValueSpec
|
||||||
isEdited: boolean
|
edited: boolean
|
||||||
isNew: boolean
|
new: boolean
|
||||||
|
invalid?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -38,33 +38,23 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ng-container *ngIf="rec && showRec">
|
<!-- auto-config -->
|
||||||
<ion-item class="rec-item">
|
<ion-item lines="none" *ngIf="dependentInfo" class="rec-item" style="margin-bottom: 48px;">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2 style="display: flex; align-items: center;">
|
<h2 style="display: flex; align-items: center;">
|
||||||
<ion-icon size="small" style="margin: 4px" slot="start" color="primary" slot="start" name="ellipse"></ion-icon>
|
<img style="width: 18px; margin: 4px;" [src]="pkg['static-files'].icon" [alt]="pkg.manifest.title"/>
|
||||||
<ion-thumbnail style="width: 3vh; height: 3vh; margin: 0px 2px 0px 5px;" slot="start">
|
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: 18px;">{{ pkg.manifest.title }}</ion-text>
|
||||||
<img [src]="rec.dependentIcon" [alt]="rec.dependentTitle"/>
|
</h2>
|
||||||
</ion-thumbnail>
|
<p>
|
||||||
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: smaller;">{{ rec.dependentTitle }}</ion-text>
|
<ion-text color="dark">
|
||||||
</h2>
|
{{ pkg.manifest.title }} has been modified to satisfy {{ dependentInfo.title }}.
|
||||||
<div style="margin: 7px 5px;">
|
<br />
|
||||||
<p style="font-size: small; color: var(--ion-color-medium)"> {{ pkg.manifest.title }} config has been modified to satisfy {{ rec.dependentTitle }}.
|
<br />
|
||||||
<ion-text color="dark">To accept the changes, click “Save” above.</ion-text>
|
To accept the modifications, click "Save".
|
||||||
</p>
|
</ion-text>
|
||||||
<a style="font-size: small" *ngIf="!openRec" (click)="openRec = true">More Info</a>
|
</p>
|
||||||
<ng-container *ngIf="openRec">
|
</ion-label>
|
||||||
<p style="margin-top: 10px; color: var(--ion-color-medium); font-size: small" [innerHTML]="rec.description"></p>
|
</ion-item>
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- no config -->
|
<!-- no config -->
|
||||||
<ion-item *ngIf="!hasConfig">
|
<ion-item *ngIf="!hasConfig">
|
||||||
@@ -78,7 +68,7 @@
|
|||||||
<form-object
|
<form-object
|
||||||
[objectSpec]="configSpec"
|
[objectSpec]="configSpec"
|
||||||
[formGroup]="configForm"
|
[formGroup]="configForm"
|
||||||
[current]="current"
|
[current]="configForm.value"
|
||||||
[showEdited]="true"
|
[showEdited]="true"
|
||||||
></form-object>
|
></form-object>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, Input, ViewChild } from '@angular/core'
|
import { Component, Input, ViewChild } from '@angular/core'
|
||||||
import { AlertController, ModalController, IonContent, LoadingController, IonicSafeString } from '@ionic/angular'
|
import { AlertController, ModalController, IonContent, LoadingController, IonicSafeString } from '@ionic/angular'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
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 { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
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 { ErrorToastService, getErrorMessage } from 'src/app/services/error-toast.service'
|
||||||
import { FormGroup } from '@angular/forms'
|
import { FormGroup } from '@angular/forms'
|
||||||
import { convertValuesRecursive, FormService } from 'src/app/services/form.service'
|
import { convertValuesRecursive, FormService } from 'src/app/services/form.service'
|
||||||
|
import { compare, Operation } from 'fast-json-patch'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-config',
|
selector: 'app-config',
|
||||||
@@ -19,16 +20,14 @@ import { convertValuesRecursive, FormService } from 'src/app/services/form.servi
|
|||||||
export class AppConfigPage {
|
export class AppConfigPage {
|
||||||
@ViewChild(IonContent) content: IonContent
|
@ViewChild(IonContent) content: IonContent
|
||||||
@Input() pkgId: string
|
@Input() pkgId: string
|
||||||
@Input() rec: Recommendation | null = null
|
@Input() dependentInfo?: DependentInfo
|
||||||
pkg: PackageDataEntry
|
pkg: PackageDataEntry
|
||||||
loadingText: string | undefined
|
loadingText: string | undefined
|
||||||
configSpec: ConfigSpec
|
configSpec: ConfigSpec
|
||||||
configForm: FormGroup
|
configForm: FormGroup
|
||||||
current: object
|
original: object
|
||||||
hasConfig = false
|
hasConfig = false
|
||||||
saving = false
|
saving = false
|
||||||
showRec = true
|
|
||||||
openRec = false
|
|
||||||
loadingError: string | IonicSafeString
|
loadingError: string | IonicSafeString
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
@@ -49,23 +48,32 @@ export class AppConfigPage {
|
|||||||
if (!this.hasConfig) return
|
if (!this.hasConfig) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
let oldConfig: object
|
let oldConfig: object
|
||||||
let newConfig: object
|
let newConfig: object
|
||||||
let spec: ConfigSpec
|
let spec: ConfigSpec
|
||||||
if (this.rec) {
|
let patch: Operation[]
|
||||||
this.loadingText = `Setting properties to accommodate ${this.rec.dependentTitle}`
|
if (this.dependentInfo) {
|
||||||
const { 'old-config': oc, 'new-config': nc, spec: s } = await this.embassyApi.dryConfigureDependency({ 'dependency-id': this.pkgId, 'dependent-id': this.rec.dependentId })
|
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
|
oldConfig = oc
|
||||||
newConfig = nc
|
newConfig = nc
|
||||||
spec = s
|
spec = s
|
||||||
|
patch = compare(oldConfig, newConfig)
|
||||||
} else {
|
} else {
|
||||||
this.loadingText = 'Loading Config'
|
this.loadingText = 'Loading Config'
|
||||||
const { config: oc, spec: s } = await this.embassyApi.getPackageConfig({ id: this.pkgId })
|
const { config: c, spec: s } = await this.embassyApi.getPackageConfig({ id: this.pkgId })
|
||||||
oldConfig = oc
|
oldConfig = c
|
||||||
spec = s
|
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) {
|
} catch (e) {
|
||||||
this.loadingError = getErrorMessage(e)
|
this.loadingError = getErrorMessage(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -79,11 +87,8 @@ export class AppConfigPage {
|
|||||||
|
|
||||||
resetDefaults () {
|
resetDefaults () {
|
||||||
this.configForm = this.formService.createForm(this.configSpec)
|
this.configForm = this.formService.createForm(this.configSpec)
|
||||||
this.alterConfigRecursive(this.configForm, this.current)
|
const patch = compare(this.original, this.configForm.value)
|
||||||
}
|
this.markDirty(patch)
|
||||||
|
|
||||||
dismissRec () {
|
|
||||||
this.showRec = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async dismiss () {
|
async dismiss () {
|
||||||
@@ -143,33 +148,20 @@ export class AppConfigPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setConfig (spec: ConfigSpec, config: object, depConfig?: object) {
|
private markDirty (patch: Operation[]) {
|
||||||
this.configSpec = spec
|
patch.forEach(op => {
|
||||||
this.current = config
|
const arrPath = op.path.substring(1)
|
||||||
this.configForm = this.formService.createForm(spec, { ...config, ...depConfig })
|
.split('/')
|
||||||
this.configForm.markAllAsTouched()
|
.map(node => {
|
||||||
|
const num = Number(node)
|
||||||
|
return isNaN(num) ? node : num
|
||||||
|
})
|
||||||
|
|
||||||
if (depConfig) {
|
if (op.op !== 'remove') this.configForm.get(arrPath).markAsDirty()
|
||||||
this.alterConfigRecursive(this.configForm, depConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private alterConfigRecursive (group: FormGroup, config: object) {
|
if (typeof arrPath[arrPath.length - 1] === 'number') {
|
||||||
Object.keys(config).forEach(key => {
|
const prevPath = arrPath.slice(0, arrPath.length - 1)
|
||||||
const next = group.get(key)
|
this.configForm.get(prevPath).markAsDirty()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { ModalController } from '@ionic/angular'
|
import { ModalController } from '@ionic/angular'
|
||||||
import { ValueSpecListOf } from '../../pkg-config/config-types'
|
import { ValueSpecListOf } from '../../pkg-config/config-types'
|
||||||
// import { Range } from '../../pkg-config/config-utilities'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'enum-list',
|
selector: 'enum-list',
|
||||||
@@ -13,12 +12,6 @@ export class EnumListPage {
|
|||||||
@Input() spec: ValueSpecListOf<'enum'>
|
@Input() spec: ValueSpecListOf<'enum'>
|
||||||
@Input() current: string[]
|
@Input() current: string[]
|
||||||
options: { [option: string]: boolean } = { }
|
options: { [option: string]: boolean } = { }
|
||||||
|
|
||||||
// min: number | undefined
|
|
||||||
// max: number | undefined
|
|
||||||
// minMessage: string
|
|
||||||
// maxMessage: string
|
|
||||||
|
|
||||||
selectAll = true
|
selectAll = true
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
@@ -26,12 +19,6 @@ export class EnumListPage {
|
|||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit () {
|
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) {
|
for (let val of this.spec.spec.values) {
|
||||||
this.options[val] = this.current.includes(val)
|
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 { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
|
import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
|
||||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||||
import * as JsonPointer from 'json-pointer'
|
import { getValueByPointer } from 'fast-json-patch'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-properties',
|
selector: 'app-properties',
|
||||||
@@ -49,7 +49,7 @@ export class AppPropertiesPage {
|
|||||||
.subscribe(queryParams => {
|
.subscribe(queryParams => {
|
||||||
if (queryParams['pointer'] === this.pointer) return
|
if (queryParams['pointer'] === this.pointer) return
|
||||||
this.pointer = queryParams['pointer']
|
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')
|
this.patch.watch$('package-data', this.pkgId, 'installed', 'status', 'main', 'status')
|
||||||
.subscribe(status => {
|
.subscribe(status => {
|
||||||
@@ -119,7 +119,7 @@ export class AppPropertiesPage {
|
|||||||
this.loading = true
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
this.properties = await this.embassyApi.getPackageProperties({ id: this.pkgId })
|
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) {
|
} catch (e) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -100,7 +100,6 @@
|
|||||||
<ion-text [color]="!!dep.errorText ? 'warning' : 'success'">{{ dep.errorText || 'satisfied' }}</ion-text>
|
<ion-text [color]="!!dep.errorText ? 'warning' : 'success'">{{ dep.errorText || 'satisfied' }}</ion-text>
|
||||||
</p>
|
</p>
|
||||||
</ion-label>
|
</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">
|
<ion-button *ngIf="dep.actionText" slot="end" fill="clear">
|
||||||
{{ dep.actionText }}
|
{{ dep.actionText }}
|
||||||
<ion-icon slot="end" name="arrow-forward"></ion-icon>
|
<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 { AlertController, NavController, ModalController, IonContent, LoadingController } from '@ionic/angular'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { ActivatedRoute, NavigationExtras } from '@angular/router'
|
import { ActivatedRoute, NavigationExtras } from '@angular/router'
|
||||||
import { exists, isEmptyObject, Recommendation } from 'src/app/util/misc.util'
|
import { DependentInfo, exists, isEmptyObject } from 'src/app/util/misc.util'
|
||||||
import { Subscription } from 'rxjs'
|
import { combineLatest, Subscription } from 'rxjs'
|
||||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.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 { DependencyStatus, HealthStatus, PrimaryRendering, PrimaryStatus, renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||||
import { ConnectionFailure, ConnectionService } from 'src/app/services/connection.service'
|
import { ConnectionFailure, ConnectionService } from 'src/app/services/connection.service'
|
||||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||||
@@ -83,34 +83,18 @@ export class AppShowPage {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
// 2
|
// 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(
|
.pipe(
|
||||||
filter(obj => exists(obj)),
|
filter(([currentDeps, depErrors]) => exists(currentDeps) && exists(depErrors)),
|
||||||
)
|
)
|
||||||
.subscribe(currentDeps => {
|
.subscribe(([currentDeps, depErrors]) => {
|
||||||
// remove deleted
|
this.dependencies = Object.keys(currentDeps)
|
||||||
this.dependencies.forEach((dep, i) => {
|
.filter(id => !!this.pkg.manifest.dependencies[id])
|
||||||
if (!currentDeps[dep.id]) {
|
.map(id => {
|
||||||
dep.sub.unsubscribe()
|
return this.setDepValues(id, depErrors)
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -142,9 +126,6 @@ export class AppShowPage {
|
|||||||
|
|
||||||
ngOnDestroy () {
|
ngOnDestroy () {
|
||||||
this.subs.forEach(sub => sub.unsubscribe())
|
this.subs.forEach(sub => sub.unsubscribe())
|
||||||
this.dependencies.forEach(dep => {
|
|
||||||
dep.sub.unsubscribe()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
launchUi (): void {
|
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({
|
const modal = await this.modalCtrl.create({
|
||||||
component: AppConfigPage,
|
component: AppConfigPage,
|
||||||
componentProps: props,
|
componentProps: props,
|
||||||
@@ -221,36 +202,27 @@ export class AppShowPage {
|
|||||||
await modal.present()
|
await modal.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
private setDepValues (dep: DependencyInfo, localDep: PackageDataEntry | undefined): void {
|
private setDepValues (id: string, errors: { [id: string]: DependencyError }): DependencyInfo {
|
||||||
let errorText = ''
|
let errorText = ''
|
||||||
let spinnerColor = ''
|
|
||||||
let actionText = 'View'
|
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) {
|
if (error) {
|
||||||
// health checks failed
|
// health checks failed
|
||||||
if ([DependencyErrorType.InterfaceHealthChecksFailed, DependencyErrorType.HealthChecksFailed].includes(error.type)) {
|
if ([DependencyErrorType.InterfaceHealthChecksFailed, DependencyErrorType.HealthChecksFailed].includes(error.type)) {
|
||||||
errorText = 'Health check failed'
|
errorText = 'Health check failed'
|
||||||
// not fully installed (same as !localDep?.installed)
|
// not installed
|
||||||
} else if (error.type === DependencyErrorType.NotInstalled) {
|
} else if (error.type === DependencyErrorType.NotInstalled) {
|
||||||
if (localDep) {
|
errorText = 'Not installed'
|
||||||
errorText = localDep.state // 'Installing' | 'Removing'
|
actionText = 'Install'
|
||||||
} else {
|
action = () => this.fixDep('install', id)
|
||||||
errorText = 'Not installed'
|
|
||||||
actionText = 'Install'
|
|
||||||
action = () => this.fixDep('install', dep.id)
|
|
||||||
}
|
|
||||||
// incorrect version
|
// incorrect version
|
||||||
} else if (error.type === DependencyErrorType.IncorrectVersion) {
|
} else if (error.type === DependencyErrorType.IncorrectVersion) {
|
||||||
if (localDep) {
|
errorText = 'Incorrect version'
|
||||||
errorText = localDep.state // 'Updating' | 'Removing'
|
actionText = 'Update'
|
||||||
} else {
|
action = () => this.fixDep('update', id)
|
||||||
errorText = 'Incorrect version'
|
|
||||||
actionText = 'Update'
|
|
||||||
action = () => this.fixDep('update', dep.id)
|
|
||||||
}
|
|
||||||
// not running
|
// not running
|
||||||
} else if (error.type === DependencyErrorType.NotRunning) {
|
} else if (error.type === DependencyErrorType.NotRunning) {
|
||||||
errorText = 'Not running'
|
errorText = 'Not running'
|
||||||
@@ -259,60 +231,50 @@ export class AppShowPage {
|
|||||||
} else if (error.type === DependencyErrorType.ConfigUnsatisfied) {
|
} else if (error.type === DependencyErrorType.ConfigUnsatisfied) {
|
||||||
errorText = 'Config not satisfied'
|
errorText = 'Config not satisfied'
|
||||||
actionText = 'Auto config'
|
actionText = 'Auto config'
|
||||||
action = () => this.fixDep('configure', dep.id)
|
action = () => this.fixDep('configure', id)
|
||||||
} else if (error.type === DependencyErrorType.Transitive) {
|
} else if (error.type === DependencyErrorType.Transitive) {
|
||||||
errorText = 'Dependency has a dependency issue'
|
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,
|
title: depInfo.manifest.title,
|
||||||
icon: depInfo.icon,
|
icon: depInfo.icon,
|
||||||
errorText,
|
errorText,
|
||||||
actionText,
|
actionText,
|
||||||
spinnerColor,
|
|
||||||
action,
|
action,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async installDep (depId: string): Promise<void> {
|
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 version = this.pkg.manifest.dependencies[depId].version
|
||||||
const dependentTitle = this.pkg.manifest.title
|
|
||||||
|
|
||||||
const installRec: Recommendation = {
|
const dependentInfo: DependentInfo = {
|
||||||
dependentId: this.pkgId,
|
id: this.pkgId,
|
||||||
dependentTitle,
|
title: this.pkg.manifest.title,
|
||||||
dependentIcon: this.pkg['static-files'].icon,
|
|
||||||
version,
|
version,
|
||||||
description: `${dependentTitle} requires an install of ${title} satisfying ${version}.`,
|
|
||||||
}
|
}
|
||||||
const navigationExtras: NavigationExtras = {
|
const navigationExtras: NavigationExtras = {
|
||||||
state: { installRec },
|
state: { dependentInfo },
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.navCtrl.navigateForward(`/marketplace/${depId}`, navigationExtras)
|
await this.navCtrl.navigateForward(`/marketplace/${depId}`, navigationExtras)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async configureDep (depId: string): Promise<void> {
|
private async configureDep (dependencyId: string): Promise<void> {
|
||||||
const configRecommendation: Recommendation = {
|
const dependentInfo: DependentInfo = {
|
||||||
dependentId: this.pkgId,
|
id: this.pkgId,
|
||||||
dependentTitle: this.pkg.manifest.title,
|
title: 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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.presentModalConfig(params)
|
await this.presentModalConfig({
|
||||||
|
pkgId: dependencyId,
|
||||||
|
dependentInfo,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async presentAlertStart (message: string): Promise<void> {
|
private async presentAlertStart (message: string): Promise<void> {
|
||||||
@@ -434,10 +396,8 @@ interface DependencyInfo {
|
|||||||
icon: string
|
icon: string
|
||||||
version: string
|
version: string
|
||||||
errorText: string
|
errorText: string
|
||||||
spinnerColor: string
|
|
||||||
actionText: string
|
actionText: string
|
||||||
action: () => any
|
action: () => any
|
||||||
sub: Subscription
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Button {
|
interface Button {
|
||||||
|
|||||||
@@ -72,23 +72,21 @@
|
|||||||
</ion-row>
|
</ion-row>
|
||||||
</ion-grid>
|
</ion-grid>
|
||||||
|
|
||||||
<!-- recommendation -->
|
<!-- auto-config -->
|
||||||
<ion-item *ngIf="rec && showRec" class="rec-item">
|
<ion-item lines="none" *ngIf="dependentInfo" class="rec-item">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2 style="display: flex; align-items: center;">
|
<h2 style="display: flex; align-items: center;">
|
||||||
<ion-thumbnail style="height: 3vh; width: 3vh; margin: 5px" slot="start">
|
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: 18px;">{{ pkg.manifest.title }}</ion-text>
|
||||||
<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>
|
</h2>
|
||||||
<div style="margin: 7px 5px;">
|
<p>
|
||||||
<p style="color: var(--ion-color-dark); font-size: small">{{ rec.description }}</p>
|
<ion-text color="dark">
|
||||||
<p *ngIf="pkg.manifest.version | satisfiesEmver: rec.version" class="recommendation-text">{{ pkg.manifest.title }} version {{ pkg.manifest.version | displayEmver }} is compatible.</p>
|
{{ dependentInfo.title }} requires an install of {{ pkg.manifest.title }} satisfying {{ dependentInfo.version }}.
|
||||||
<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>
|
<br />
|
||||||
<ion-button style="position: absolute; right: 0; top: 0" fill="clear" (click)="dismissRec()">
|
<br />
|
||||||
<ion-icon name="close"></ion-icon>
|
<span *ngIf="pkg.manifest.version | satisfiesEmver: dependentInfo.version" class="recommendation-text">{{ pkg.manifest.title }} version {{ pkg.manifest.version | displayEmver }} is compatible.</span>
|
||||||
</ion-button>
|
<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>
|
||||||
</div>
|
</ion-text>
|
||||||
|
</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</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 { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||||
import { Emver } from 'src/app/services/emver.service'
|
import { Emver } from 'src/app/services/emver.service'
|
||||||
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
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 { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||||
import { PackageDataEntry, PackageState } from 'src/app/services/patch-db/data-model'
|
import { PackageDataEntry, PackageState } from 'src/app/services/patch-db/data-model'
|
||||||
@@ -27,8 +27,7 @@ export class MarketplaceShowPage {
|
|||||||
pkg: MarketplacePkg
|
pkg: MarketplacePkg
|
||||||
localPkg: PackageDataEntry
|
localPkg: PackageDataEntry
|
||||||
PackageState = PackageState
|
PackageState = PackageState
|
||||||
rec: Recommendation | null = null
|
dependentInfo: DependentInfo
|
||||||
showRec = true
|
|
||||||
subs: Subscription[] = []
|
subs: Subscription[] = []
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
@@ -47,7 +46,7 @@ export class MarketplaceShowPage {
|
|||||||
|
|
||||||
async ngOnInit () {
|
async ngOnInit () {
|
||||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
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.subs = [
|
||||||
this.patch.watch$('package-data', this.pkgId)
|
this.patch.watch$('package-data', this.pkgId)
|
||||||
@@ -171,10 +170,6 @@ export class MarketplaceShowPage {
|
|||||||
this.navCtrl.back()
|
this.navCtrl.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissRec () {
|
|
||||||
this.showRec = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getPkg (version?: string): Promise<void> {
|
private async getPkg (version?: string): Promise<void> {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export class ServerBackupPage {
|
|||||||
take(1),
|
take(1),
|
||||||
)
|
)
|
||||||
.subscribe(pkgs => {
|
.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)
|
const activeIndex = pkgArr.findIndex(pkg => pkg.installed.status.main.status === PackageMainStatus.BackingUp)
|
||||||
|
|
||||||
this.pkgs = pkgArr.map((pkg, i) => {
|
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 { 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 { Log, MarketplacePkg, Metric, NotificationLevel, RR, ServerNotifications } from './api.types'
|
||||||
|
import { Operation } from 'fast-json-patch'
|
||||||
|
|
||||||
export module Mock {
|
export module Mock {
|
||||||
|
|
||||||
@@ -1081,405 +1082,21 @@ export module Mock {
|
|||||||
},
|
},
|
||||||
} as any // @TODO why is this necessary?
|
} as any // @TODO why is this necessary?
|
||||||
|
|
||||||
export const PackageConfig: RR.GetPackageConfigRes = {
|
export const ConfigSpec: RR.GetPackageConfigRes['spec'] = {
|
||||||
// config spec
|
'testnet': {
|
||||||
spec: {
|
'name': 'Testnet',
|
||||||
'testnet': {
|
'type': 'boolean',
|
||||||
'name': 'Testnet',
|
'description': 'determines whether your node is running on testnet or mainnet',
|
||||||
'type': 'boolean',
|
'warning': 'Chain will have to resync!',
|
||||||
'description': 'determines whether your node is running on testnet or mainnet',
|
'default': true,
|
||||||
'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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
// actual config
|
'object-list': {
|
||||||
config: {
|
'name': 'Object List',
|
||||||
testnet: false,
|
'type': 'list',
|
||||||
'object-list': [
|
'subtype': 'object',
|
||||||
|
'description': 'This is a list of objects, like users or something',
|
||||||
|
'range': '[0,4]',
|
||||||
|
'default': [
|
||||||
{
|
{
|
||||||
'first-name': 'Admin',
|
'first-name': 'Admin',
|
||||||
'last-name': 'User',
|
'last-name': 'User',
|
||||||
@@ -1491,51 +1108,462 @@ export module Mock {
|
|||||||
'age': 40,
|
'age': 40,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'union-list': undefined,
|
// the outer spec here, at the list level, says that what's inside (the inner spec) pertains to its inner elements.
|
||||||
'random-enum': 'option1',
|
// it just so happens that ValueSpecObject's have the field { spec: ConfigSpec }
|
||||||
'favorite-number': null,
|
// see 'union-list' below for a different example.
|
||||||
'secondary-numbers': undefined,
|
'spec': {
|
||||||
rpcsettings: {
|
'unique-by': 'last-name',
|
||||||
laws: {
|
'display-as': `I'm {{last-name}}, {{first-name}} {{last-name}}`,
|
||||||
law1: 'The first law',
|
'spec': {
|
||||||
law2: 'The second law',
|
'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 = {
|
export const MockConfig = {
|
||||||
'random-enum': 'option1',
|
|
||||||
testnet: false,
|
testnet: false,
|
||||||
'favorite-number': 8,
|
'object-list': [
|
||||||
'secondary-numbers': [13, 58, 20],
|
{
|
||||||
'object-list': [],
|
'first-name': 'First',
|
||||||
'union-list': [],
|
'last-name': 'Last',
|
||||||
|
'age': 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'first-name': 'First2',
|
||||||
|
'last-name': 'Last2',
|
||||||
|
'age': 40,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'union-list': undefined,
|
||||||
|
'random-enum': 'option1',
|
||||||
|
'favorite-number': null,
|
||||||
rpcsettings: {
|
rpcsettings: {
|
||||||
laws: null,
|
laws: {
|
||||||
|
law1: 'The first law',
|
||||||
|
law2: 'The second law',
|
||||||
|
},
|
||||||
rpcpass: null,
|
rpcpass: null,
|
||||||
rpcuser: '123',
|
rpcuser: '123',
|
||||||
rulemakers: [],
|
rulemakers: [],
|
||||||
},
|
},
|
||||||
advanced: {
|
advanced: {
|
||||||
notifications: [],
|
notifications: ['email', 'text'],
|
||||||
},
|
},
|
||||||
'bitcoin-Node': { type: 'internal' },
|
'bitcoin-node': undefined,
|
||||||
port: 5959,
|
port: 5959,
|
||||||
rpcallowip: [],
|
rpcallowip: undefined,
|
||||||
rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'],
|
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 = {
|
export const bitcoind: PackageDataEntry = {
|
||||||
state: PackageState.Installed,
|
state: PackageState.Installed,
|
||||||
'static-files': {
|
'static-files': {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { pauseFor } from '../../util/misc.util'
|
|||||||
import { ApiService } from './embassy-api.service'
|
import { ApiService } from './embassy-api.service'
|
||||||
import { PatchOp } from 'patch-db-client'
|
import { PatchOp } from 'patch-db-client'
|
||||||
import { DependencyErrorType, InstallProgress, PackageDataEntry, PackageMainStatus, PackageState, ServerStatus } from 'src/app/services/patch-db/data-model'
|
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 { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||||
import { Mock } from './api.fixures'
|
import { Mock } from './api.fixures'
|
||||||
import { HttpService } from '../http.service'
|
import { HttpService } from '../http.service'
|
||||||
@@ -75,7 +75,7 @@ export class MockApiService extends ApiService {
|
|||||||
|
|
||||||
async getServerLogs (params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
|
async getServerLogs (params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
let entries
|
let entries: Log[]
|
||||||
if (Math.random() < .2) {
|
if (Math.random() < .2) {
|
||||||
entries = Mock.ServerLogs
|
entries = Mock.ServerLogs
|
||||||
} else {
|
} else {
|
||||||
@@ -390,7 +390,10 @@ export class MockApiService extends ApiService {
|
|||||||
|
|
||||||
async getPackageConfig (params: RR.GetPackageConfigReq): Promise<RR.GetPackageConfigRes> {
|
async getPackageConfig (params: RR.GetPackageConfigReq): Promise<RR.GetPackageConfigRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
return Mock.PackageConfig
|
return {
|
||||||
|
config: Mock.MockConfig,
|
||||||
|
spec: Mock.ConfigSpec,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async drySetPackageConfig (params: RR.DrySetPackageConfigReq): Promise<RR.DrySetPackageConfigRes> {
|
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> {
|
async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise<RR.DryConfigureDependencyRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
return {
|
return {
|
||||||
'old-config': Mock.PackageConfig.config,
|
'old-config': Mock.MockConfig,
|
||||||
spec: Mock.PackageConfig.spec,
|
'new-config': Mock.MockDependencyConfig,
|
||||||
'new-config': {
|
spec: Mock.ConfigSpec,
|
||||||
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'],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'
|
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'
|
import { getDefaultString, Range } from '../pkg-config/config-utilities'
|
||||||
const Mustache = require('mustache')
|
const Mustache = require('mustache')
|
||||||
|
|
||||||
@@ -13,8 +13,8 @@ export class FormService {
|
|||||||
private readonly formBuilder: FormBuilder,
|
private readonly formBuilder: FormBuilder,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
createForm (config: ConfigSpec, current: { [key: string]: any } = { }): FormGroup {
|
createForm (spec: ConfigSpec, current: { [key: string]: any } = { }): FormGroup {
|
||||||
return this.getFormGroup(config, [], current)
|
return this.getFormGroup(spec, [], current)
|
||||||
}
|
}
|
||||||
|
|
||||||
getUnionObject (spec: ValueSpecUnion | ListValueSpecUnion, selection: string, current?: { [key: string]: any }): FormGroup {
|
getUnionObject (spec: ValueSpecUnion | ListValueSpecUnion, selection: string, current?: { [key: string]: any }): FormGroup {
|
||||||
@@ -60,12 +60,12 @@ export class FormService {
|
|||||||
let group = { }
|
let group = { }
|
||||||
Object.entries(config).map(([key, spec]) => {
|
Object.entries(config).map(([key, spec]) => {
|
||||||
if (spec.type === 'pointer') return
|
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 } )
|
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 validators: ValidatorFn[]
|
||||||
let value: any
|
let value: any
|
||||||
switch (spec.type) {
|
switch (spec.type) {
|
||||||
@@ -89,7 +89,7 @@ export class FormService {
|
|||||||
return this.getFormGroup(spec.spec, [], currentValue)
|
return this.getFormGroup(spec.spec, [], currentValue)
|
||||||
case 'list':
|
case 'list':
|
||||||
validators = this.listValidators(spec)
|
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.getListItem(spec, entry)
|
||||||
})
|
})
|
||||||
return this.formBuilder.array(mapped, validators)
|
return this.formBuilder.array(mapped, validators)
|
||||||
@@ -139,6 +139,8 @@ export class FormService {
|
|||||||
|
|
||||||
validators.push(listInRange(spec.range))
|
validators.push(listInRange(spec.range))
|
||||||
|
|
||||||
|
validators.push(listItemIssue())
|
||||||
|
|
||||||
if (!isValueSpecListOf(spec, 'enum')) {
|
if (!isValueSpecListOf(spec, 'enum')) {
|
||||||
validators.push(listUnique(spec))
|
validators.push(listUnique(spec))
|
||||||
}
|
}
|
||||||
@@ -181,29 +183,40 @@ export function isInteger (): ValidatorFn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function listInRange (stringRange: string): ValidatorFn {
|
export function listInRange (stringRange: string): ValidatorFn {
|
||||||
return (control: AbstractControl): ValidationErrors | null => {
|
return (control: FormArray): ValidationErrors | null => {
|
||||||
try {
|
try {
|
||||||
Range.from(stringRange).checkIncludes(control.value.length)
|
Range.from(stringRange).checkIncludes(control.value.length)
|
||||||
return null
|
return null
|
||||||
} catch (e) {
|
} 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 {
|
export function listUnique (spec: ValueSpecList): ValidatorFn {
|
||||||
return (control: AbstractControl): ValidationErrors | null => {
|
return (control: FormArray): ValidationErrors | null => {
|
||||||
for (let idx = 0; idx < control.value.length; idx++) {
|
const list = control.value
|
||||||
for (let idx2 = idx + 1; idx2 < control.value.length; idx2++) {
|
for (let idx = 0; idx < list.length; idx++) {
|
||||||
if (listItemEquals(spec, control.value[idx], control.value[idx2])) {
|
for (let idx2 = idx + 1; idx2 < list.length; idx2++) {
|
||||||
|
if (listItemEquals(spec, list[idx], list[idx2])) {
|
||||||
let display1: string
|
let display1: string
|
||||||
let display2: 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']) {
|
if (isObjectOrUnion(spec.spec) && spec.spec['display-as']) {
|
||||||
display1 = `"${(Mustache as any).render(spec.spec['display-as'], control.value[idx])}"`
|
display1 = `"${(Mustache as any).render(spec.spec['display-as'], list[idx])}"`
|
||||||
display2 = `"${(Mustache as any).render(spec.spec['display-as'], control.value[idx2])}"`
|
display2 = `"${(Mustache as any).render(spec.spec['display-as'], list[idx2])}"`
|
||||||
} else {
|
} else {
|
||||||
display1 = `Entry ${idx + 1}`
|
display1 = `Entry ${idx + 1}`
|
||||||
display2 = `Entry ${idx2 + 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 + ')'
|
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
|
// only lists of objects and unions have unique-by
|
||||||
return spec['unique-by'] !== undefined
|
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 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 PromiseRes<T> = { result: 'resolve', value: T } | { result: 'reject', value: Error }
|
||||||
|
|
||||||
export type Recommendation = {
|
export interface DependentInfo {
|
||||||
dependentId: string
|
id: string
|
||||||
dependentTitle: string
|
title: string
|
||||||
dependentIcon: string,
|
|
||||||
description: string
|
|
||||||
version?: string
|
version?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as Ajv from 'ajv'
|
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 ajv = new Ajv({ jsonPointers: true, allErrors: true, nullable: true })
|
||||||
const ajvWithDefaults = new Ajv({ jsonPointers: true, allErrors: true, useDefaults: true, nullable: true, removeAdditional: 'failing' })
|
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) {
|
for (let err of schemaV2Compiled.errors) {
|
||||||
errorCallback(new Error(`/data/${idx}${err.dataPath}: ${err.message}`))
|
errorCallback(new Error(`/data/${idx}${err.dataPath}: ${err.message}`))
|
||||||
if (err.dataPath) {
|
if (err.dataPath) {
|
||||||
JsonPointer.set(value, err.dataPath, undefined)
|
applyOperation(value, { op: 'replace', path: err.dataPath, value: undefined })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!schemaV2CompiledWithDefaults(value)) {
|
if (!schemaV2CompiledWithDefaults(value)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user