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:
Matt Hill
2021-10-21 16:49:47 -06:00
committed by Aiden McClelland
parent c6e379bffa
commit 65a4b8ab84
21 changed files with 1691 additions and 1459 deletions

1814
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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>

View File

@@ -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">&nbsp;(New)</ion-text>
<ion-text color="warning" *ngIf="data.isEdited">&nbsp;(Edited)</ion-text>
<ion-text color="success" *ngIf="data.new">&nbsp;(New)</ion-text>
<ion-text color="warning" *ngIf="data.edited">&nbsp;(Edited)</ion-text>
<span *ngIf="(['string', 'number'] | includes : data.spec.type) && !$any(data.spec).nullable">&nbsp;*</span>

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
.help-button {
.slot-start {
display: inline-block;
vertical-align: middle;
}

View File

@@ -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({

View File

@@ -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>

View File

@@ -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()
}
})
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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) => {

View File

@@ -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': {

View File

@@ -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,
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)) {