squashing the bugs

This commit is contained in:
Matt Hill
2021-10-05 11:46:49 -06:00
committed by Aiden McClelland
parent 7756841b1e
commit d47dd28384
19 changed files with 122 additions and 175 deletions

View File

@@ -20,10 +20,8 @@
{{ longMessage }}
</div>
<div style="margin: 25px 0px;">
<div style="border-width: 0px 0px 1px 0px; font-size: unset; text-align: left; font-weight: bold; margin-left: 13px; border-style: solid; border-color: var(--ion-color-light-tint);"
*ngIf="hasDependentViolation"
>
<ion-text color="warning">Will Stop</ion-text>
<div *ngIf="hasDependentViolation" style="border-width: 0px 0px 1px 0px; font-size: unset; text-align: left; font-weight: bold; margin-left: 13px; border-style: solid; border-color: var(--ion-color-light-tint);">
<ion-text color="warning">Affected Services</ion-text>
</div>
<ion-item
style="--ion-item-background: margin-top: 5px"

View File

@@ -49,7 +49,7 @@ export class DependentsComponent implements OnInit, Loadable {
complete: () => {
this.hasDependentViolation = this.dependentBreakages && !isEmptyObject(this.dependentBreakages)
if (this.hasDependentViolation) {
this.longMessage = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title} will cause the following services to STOP running. Starting them again will require additional actions.`
this.longMessage = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title} will prohibit the following services from functioning properly and will cause them to stop if they are currently running.`
this.color$.next('warning')
} else if (this.params.skipConfirmationDialogue) {
this.transitions.next()

View File

@@ -202,7 +202,7 @@ export class WizardBaker {
action,
verb: 'uninstalling',
title,
fetchBreakages: () => this.embassyApi.dryRemovePackage({ id }).then(breakages => breakages),
fetchBreakages: () => this.embassyApi.dryUninstallPackage({ id }).then(breakages => breakages),
},
},
bottomBar: { cancel: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, next: 'Uninstall' },
@@ -214,7 +214,7 @@ export class WizardBaker {
action,
verb: 'uninstalling',
title,
executeAction: () => this.embassyApi.removePackage({ id }),
executeAction: () => this.embassyApi.uninstallPackage({ id }),
},
},
bottomBar: { finish: 'Dismiss', cancel: { whileLoading: { } } },

View File

@@ -9,7 +9,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { FormGroup } from '@angular/forms'
import { convertToNumberRecursive, FormService } from 'src/app/services/form.service'
import { convertValuesRecursive, FormService } from 'src/app/services/form.service'
@Component({
selector: 'app-config',
@@ -87,9 +87,7 @@ export class AppConfigPage {
}
async save () {
convertToNumberRecursive(this.configSpec, this.configForm)
console.log('SAVING', this.configForm.value)
convertValuesRecursive(this.configSpec, this.configForm)
if (this.configForm.invalid) {
document.getElementsByClassName('validation-error')[0].parentElement.parentElement.scrollIntoView({ behavior: 'smooth' })

View File

@@ -1,7 +1,7 @@
import { Component, Input } from '@angular/core'
import { FormGroup } from '@angular/forms'
import { ModalController } from '@ionic/angular'
import { convertToNumberRecursive, FormService } from 'src/app/services/form.service'
import { convertValuesRecursive, FormService } from 'src/app/services/form.service'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
export interface ActionButton {
@@ -40,7 +40,7 @@ export class GenericFormPage {
}
async handleClick (handler: ActionButton['handler']): Promise<void> {
convertToNumberRecursive(this.spec, this.formGroup)
convertValuesRecursive(this.spec, this.formGroup)
if (this.formGroup.invalid) {
this.formGroup.markAllAsTouched()

View File

@@ -10,7 +10,7 @@
</ion-item>
<form (ngSubmit)="submit()">
<div style="margin-left: 16px;">
<div style="margin: 0 0 24px 16px;">
<p class="input-label">{{ label }}</p>
<ion-item lines="none" color="dark">
<ion-input [type]="useMask && !unmasked ? 'password' : 'text'" [(ngModel)]="value" name="value" [placeholder]="placeholder" (ionChange)="error = ''"></ion-input>
@@ -19,9 +19,7 @@
</ion-button>
</ion-item>
<!-- error -->
<p>
<ion-text [color]="error ? 'danger' : 'medium'">{{ error || 'placeholder' }}</ion-text>
</p>
<p *ngIf="error"><ion-text color="danger">{{ error }}</ion-text></p>
</div>
<div class="ion-text-right">

View File

@@ -1,6 +1,5 @@
import { Component, Input } from '@angular/core'
import { IonicSafeString, LoadingController, ModalController } from '@ionic/angular'
import { getErrorMessage } from 'src/app/services/error-toast.service'
import { ModalController } from '@ionic/angular'
@Component({
selector: 'generic-input',
@@ -18,11 +17,10 @@ export class GenericInputComponent {
@Input() value = ''
@Input() submitFn: (value: string) => Promise<any>
unmasked = false
error: string | IonicSafeString
error: string
constructor (
private readonly modalCtrl: ModalController,
private readonly loadingCtrl: LoadingController,
) { }
toggleMask () {
@@ -34,19 +32,8 @@ export class GenericInputComponent {
}
async submit () {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
cssClass: 'loader',
})
loader.present()
try {
await this.submitFn(this.value)
this.modalCtrl.dismiss(undefined, 'success')
} catch (e) {
this.error = getErrorMessage(e)
} finally {
loader.dismiss()
}
// @TODO validate input?
await this.submitFn(this.value)
this.modalCtrl.dismiss(undefined, 'success')
}
}

View File

@@ -14,7 +14,7 @@
<!-- not loading -->
<div *ngIf="!loading">
<div *ngIf="pkgs | empty; else list" class="ion-text-center ion-padding">
<div *ngIf="empty; else list" class="ion-text-center ion-padding">
<div style="display: flex; flex-direction: column; justify-content: center; height: 40vh">
<h2>Welcome to <ion-text color="danger" style="font-family: 'Montserrat';">Embassy</ion-text></h2>
<p class="ion-text-wrap">Get started by installing your first service.</p>

View File

@@ -21,11 +21,12 @@ export class AppListPage {
connectionFailure: boolean
pkgs: { [id: string]: PkgInfo } = { }
loading = true
empty = false
constructor (
private readonly config: ConfigService,
private readonly connectionService: ConnectionService,
private readonly installPackageService: PackageLoadingService,
private readonly pkgLoading: PackageLoadingService,
public readonly patch: PatchDbService,
) { }
@@ -34,11 +35,7 @@ export class AppListPage {
this.patch.watch$('package-data')
.pipe(
filter(obj => {
return obj &&
(
isEmptyObject(obj) ||
Object.keys(obj).length !== Object.keys(this.pkgs).length
)
return obj && Object.keys(obj).length !== Object.keys(this.pkgs).length
}),
)
.subscribe(pkgs => {
@@ -46,6 +43,8 @@ export class AppListPage {
const ids = Object.keys(pkgs)
this.empty = !ids.length
Object.keys(this.pkgs).forEach(id => {
if (!ids.includes(id)) {
this.pkgs[id].sub.unsubscribe()
@@ -59,7 +58,7 @@ export class AppListPage {
this.pkgs[id] = {
entry: pkgs[id],
primaryRendering: PrimaryRendering[renderPkgStatus(pkgs[id]).primary],
installProgress: !isEmptyObject(pkgs[id]['install-progress']) ? this.installPackageService.transform(pkgs[id]['install-progress']) : undefined,
installProgress: !isEmptyObject(pkgs[id]['install-progress']) ? this.pkgLoading.transform(pkgs[id]['install-progress']) : undefined,
error: false,
sub: null,
}
@@ -69,7 +68,7 @@ export class AppListPage {
const statuses = renderPkgStatus(pkg)
const primaryRendering = PrimaryRendering[statuses.primary]
this.pkgs[id].entry = pkg
this.pkgs[id].installProgress = !isEmptyObject(pkg['install-progress']) ? this.installPackageService.transform(pkg['install-progress']) : undefined
this.pkgs[id].installProgress = !isEmptyObject(pkg['install-progress']) ? this.pkgLoading.transform(pkg['install-progress']) : undefined
this.pkgs[id].primaryRendering = primaryRendering
this.pkgs[id].error = statuses.health === HealthStatus.Failure || [DependencyStatus.Issue, DependencyStatus.Critical].includes(statuses.dependency)
})

View File

@@ -13,8 +13,7 @@ import { DependencyStatus, HealthStatus, PrimaryRendering, PrimaryStatus, render
import { ConnectionFailure, ConnectionService } from 'src/app/services/connection.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { AppConfigPage } from 'src/app/modals/app-config/app-config.page'
import { PackageLoadingService } from 'src/app/services/package-loading.service'
import { ProgressData } from 'src/app/pipes/install-state.pipe'
import { PackageLoadingService, ProgressData } from 'src/app/services/package-loading.service'
@Component({
selector: 'app-show',

View File

@@ -47,7 +47,10 @@ export class ServerMetricsPage {
async getMetrics (): Promise<void> {
try {
this.metrics = await this.embassyApi.getServerMetrics({ })
const metrics = await this.embassyApi.getServerMetrics({ })
Object.entries(metrics).forEach(([key, val]) => {
this.metrics[key] = val
})
} catch (e) {
this.errToast.present(e)
this.stopDaemon()

View File

@@ -1,4 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core'
import { PackageLoadingService, ProgressData } from '../services/package-loading.service'
import { InstallProgress } from '../services/patch-db/data-model'
@Pipe({
@@ -6,37 +7,11 @@ import { InstallProgress } from '../services/patch-db/data-model'
})
export class InstallState implements PipeTransform {
constructor (
private readonly pkgLoading: PackageLoadingService,
) { }
transform (loadData: InstallProgress): ProgressData {
let { downloaded, validated, unpacked, size, 'download-complete': downloadComplete, 'validation-complete': validationComplete, 'unpack-complete': unpackComplete } = loadData
downloaded = downloadComplete ? size : downloaded
validated = validationComplete ? size : validated
unpacked = unpackComplete ? size : unpacked
const downloadWeight = 1
const validateWeight = .2
const unpackWeight = .7
const numerator = Math.floor(
downloadWeight * downloaded +
validateWeight * validated +
unpackWeight * unpacked)
const denominator = Math.floor(loadData.size * (downloadWeight + validateWeight + unpackWeight))
return {
totalProgress: Math.round(100 * numerator / denominator),
downloadProgress: Math.round(100 * downloaded / size),
validateProgress: Math.round(100 * validated / size),
unpackProgress: Math.round(100 * unpacked / size),
isComplete: downloadComplete && validationComplete && unpackComplete,
}
return this.pkgLoading.transform(loadData)
}
}
export interface ProgressData {
totalProgress: number
downloadProgress: number
validateProgress: number
unpackProgress: number
isComplete: boolean
}

View File

@@ -6,7 +6,6 @@ export class Range {
minInclusive: boolean
maxInclusive: boolean
static from (s: string): Range {
const r = new Range()
r.minInclusive = s.startsWith('[')
@@ -18,11 +17,23 @@ export class Range {
}
checkIncludes (n: number) {
if (this.hasMin() !== undefined && ((!this.minInclusive && this.min == n || (this.min > n)))) {
throw new Error(`Value must be ${this.minMessage()}.`)
if (
this.hasMin() !== undefined &&
(
(this.min > n) ||
(!this.minInclusive && this.min == n)
)
) {
throw new Error(this.minMessage())
}
if (this.hasMax() && ((!this.maxInclusive && this.max == n || (this.max < n)))) {
throw new Error(`Value must be ${this.maxMessage()}.`)
if (
this.hasMax() &&
(
(this.max < n) ||
(!this.maxInclusive && this.max == n)
)
) {
throw new Error(this.maxMessage())
}
}

View File

@@ -979,7 +979,7 @@ export module Mock {
'warning': 'Chain will have to resync!',
'default': true,
},
'objectList': {
'object-list': {
'name': 'Object List',
'type': 'list',
'subtype': 'object',
@@ -987,24 +987,24 @@ export module Mock {
'range': '[0,4]',
'default': [
{
'firstName': 'Admin',
'lastName': 'User',
'first-name': 'Admin',
'last-name': 'User',
'age': 40,
},
{
'firstName': 'Admin2',
'lastName': 'User',
'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 'unionList' below for a different example.
// see 'union-list' below for a different example.
'spec': {
'unique-by': 'lastName',
'display-as': `I'm {{lastName}}, {{firstName}} {{lastName}}`,
'unique-by': 'last-name',
'display-as': `I'm {{last-name}}, {{first-name}} {{last-name}}`,
'spec': {
'firstName': {
'first-name': {
'name': 'First Name',
'type': 'string',
'description': 'User first name',
@@ -1013,7 +1013,7 @@ export module Mock {
'masked': false,
'copyable': false,
},
'lastName': {
'last-name': {
'name': 'Last Name',
'type': 'string',
'description': 'User first name',
@@ -1040,7 +1040,7 @@ export module Mock {
},
},
},
'unionList': {
'union-list': {
'name': 'Union List',
'type': 'list',
'subtype': 'union',
@@ -1105,7 +1105,7 @@ export module Mock {
'unique-by': 'preference',
},
},
'randomEnum': {
'random-enum': {
'name': 'Random Enum',
'type': 'enum',
'value-names': {
@@ -1124,18 +1124,18 @@ export module Mock {
'option3',
],
},
'favoriteNumber': {
'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': false,
'nullable': true,
'default': 7,
'range': '(-100,100]',
'units': 'BTC',
},
'unluckyNumbers': {
'unlucky-numbers': {
'name': 'Unlucky Numbers',
'type': 'list',
'subtype': 'number',
@@ -1276,7 +1276,7 @@ export module Mock {
},
},
},
'bitcoinNode': {
'bitcoin-node': {
'name': 'Bitcoin Node Settings',
'type': 'union',
'unique-by': null,
@@ -1324,9 +1324,9 @@ export module Mock {
'description': 'the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444',
'nullable': false,
'default': 8333,
'range': '[0, 9999]',
'range': '(0, 9998]',
},
'favoriteSlogan': {
'favorite-slogan': {
'name': 'Favorite Slogan',
'type': 'string',
'description': 'You most favorite slogan in the whole world, used for paying you.',
@@ -1367,22 +1367,22 @@ export module Mock {
// actual config
config: {
testnet: false,
objectList: [
'object-list': [
{
'firstName': 'Admin',
'lastName': 'User',
'first-name': 'Admin',
'last-name': 'User',
'age': 40,
},
{
'firstName': 'Admin2',
'lastName': 'User',
'first-name': 'Admin2',
'last-name': 'User',
'age': 40,
},
],
unionList: undefined,
randomEnum: 'option1',
favoriteNumber: 8,
secondaryNumbers: undefined,
'union-list': undefined,
'random-enum': 'option1',
'favorite-number': null,
'secondary-numbers': undefined,
rpcsettings: {
laws: {
law1: 'The first law',
@@ -1395,7 +1395,7 @@ export module Mock {
advanced: {
notifications: ['email'],
},
bitcoinNode: undefined,
'bitcoin-node': undefined,
port: 5959,
rpcallowip: undefined,
rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'],
@@ -1403,12 +1403,12 @@ export module Mock {
}
export const mockCupsDependentConfig = {
randomEnum: 'option1',
'random-enum': 'option1',
testnet: false,
favoriteNumber: 8,
secondaryNumbers: [13, 58, 20],
objectList: [],
unionList: [],
'favorite-number': 8,
'secondary-numbers': [13, 58, 20],
'object-list': [],
'union-list': [],
rpcsettings: {
laws: null,
rpcpass: null,
@@ -1418,7 +1418,7 @@ export module Mock {
advanced: {
notifications: [],
},
bitcoinNode: { type: 'internal' },
'bitcoin-Node': { type: 'internal' },
port: 5959,
rpcallowip: [],
rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'],

View File

@@ -175,11 +175,11 @@ export module RR {
export type StopPackageReq = WithExpire<{ id: string }> // package.stop
export type StopPackageRes = WithRevision<null>
export type DryRemovePackageReq = RemovePackageReq // package.remove.dry
export type DryRemovePackageRes = Breakages
export type DryUninstallPackageReq = UninstallPackageReq // package.uninstall.dry
export type DryUninstallPackageRes = Breakages
export type RemovePackageReq = WithExpire<{ id: string }> // package.remove
export type RemovePackageRes = WithRevision<null>
export type UninstallPackageReq = WithExpire<{ id: string }> // package.uninstall
export type UninstallPackageRes = WithRevision<null>
export type DryConfigureDependencyReq = { 'dependency-id': string, 'dependent-id': string } // package.dependency.configure.dry
export type DryConfigureDependencyRes = object

View File

@@ -171,11 +171,11 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
() => this.stopPackageRaw(params),
)()
abstract dryRemovePackage (params: RR.DryRemovePackageReq): Promise<RR.DryRemovePackageRes>
abstract dryUninstallPackage (params: RR.DryUninstallPackageReq): Promise<RR.DryUninstallPackageRes>
protected abstract removePackageRaw (params: RR.RemovePackageReq): Promise<RR.RemovePackageRes>
removePackage = (params: RR.RemovePackageReq) => this.syncResponse(
() => this.removePackageRaw(params),
protected abstract uninstallPackageRaw (params: RR.UninstallPackageReq): Promise<RR.UninstallPackageRes>
uninstallPackage = (params: RR.UninstallPackageReq) => this.syncResponse(
() => this.uninstallPackageRaw(params),
)()
abstract dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise<RR.DryConfigureDependencyRes>

View File

@@ -260,12 +260,12 @@ export class LiveApiService extends ApiService {
return this.http.rpcRequest({ method: 'package.stop', params })
}
async dryRemovePackage (params: RR.DryRemovePackageReq): Promise <RR.DryRemovePackageRes> {
return this.http.rpcRequest({ method: 'package.remove.dry', params })
async dryUninstallPackage (params: RR.DryUninstallPackageReq): Promise <RR.DryUninstallPackageRes> {
return this.http.rpcRequest({ method: 'package.uninstall.dry', params })
}
async removePackageRaw (params: RR.RemovePackageReq): Promise <RR.RemovePackageRes> {
return this.http.rpcRequest({ method: 'package.remove', params })
async uninstallPackageRaw (params: RR.UninstallPackageReq): Promise <RR.UninstallPackageRes> {
return this.http.rpcRequest({ method: 'package.uninstall', params })
}
async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise <RR.DryConfigureDependencyRes> {

View File

@@ -452,12 +452,12 @@ export class MockApiService extends ApiService {
return res
}
async dryRemovePackage (params: RR.DryRemovePackageReq): Promise<RR.DryRemovePackageRes> {
async dryUninstallPackage (params: RR.DryUninstallPackageReq): Promise<RR.DryUninstallPackageRes> {
await pauseFor(2000)
return { }
}
async removePackageRaw (params: RR.RemovePackageReq): Promise<RR.RemovePackageRes> {
async uninstallPackageRaw (params: RR.UninstallPackageReq): Promise<RR.UninstallPackageRes> {
await pauseFor(2000)
const patch = [
{

View File

@@ -153,12 +153,13 @@ function isFullUnion (spec: ValueSpecUnion | ListValueSpecUnion): spec is ValueS
export function numberInRange (stringRange: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (!control.value) return null
const value = control.value
if (!value) return null
try {
Range.from(stringRange).checkIncludes(control.value)
Range.from(stringRange).checkIncludes(value)
return null
} catch (e) {
return { numberNotInRange: { value: getRangeMessage(stringRange) } }
return { numberNotInRange: { value: `Number must be ${e.message}` } }
}
}
}
@@ -181,30 +182,15 @@ export function isInteger (): ValidatorFn {
export function listInRange (stringRange: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const range = Range.from(stringRange)
const min = range.integralMin()
const max = range.integralMax()
const length = control.value.length
if ((min && length < min) || (max && length > max)) {
return { listNotInRange: { value: getRangeMessage(stringRange, true) } }
} else {
try {
Range.from(stringRange).checkIncludes(control.value.length)
return null
} catch (e) {
return { numberNotInRange: { value: `List must be ${e.message}` } }
}
}
}
function getRangeMessage (stringRange: string, isList = false): string {
const range = Range.from(stringRange)
const min = range.integralMin()
const max = range.integralMax()
const messageStart = isList ? 'List length must be ' : 'Must be '
const minMessage = !!min ? `greater than ${min}` : ''
const and = !!min && !!max ? ' and ' : ''
const maxMessage = !!max ? `less than ${max}` : ''
return `${messageStart} ${minMessage}${and}${maxMessage}`
}
export function listUnique (spec: ValueSpecList): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
for (let idx = 0; idx < control.value.length; idx++) {
@@ -391,48 +377,41 @@ function isUnion (spec: any): spec is ListValueSpecUnion {
return !!spec.tag
}
const sampleUniqueBy: UniqueBy = {
all: [
'last name',
{ any: [
'favorite color',
null,
] },
{ any: [
'favorite color',
'first name',
null,
] },
],
}
export function convertToNumberRecursive (configSpec: ConfigSpec, group: FormGroup) {
export function convertValuesRecursive (configSpec: ConfigSpec, group: FormGroup) {
Object.entries(configSpec).forEach(([key, valueSpec]) => {
if (valueSpec.type === 'number') {
const control = group.get(key)
control.setValue(Number(control.value))
control.setValue(control.value ? Number(control.value) : null)
} else if (valueSpec.type === 'string') {
const control = group.get(key)
if (!control.value) control.setValue(null)
} else if (valueSpec.type === 'object') {
convertToNumberRecursive(valueSpec.spec, group.get(key) as FormGroup)
convertValuesRecursive(valueSpec.spec, group.get(key) as FormGroup)
} else if (valueSpec.type === 'union') {
const control = group.get(key) as FormGroup
const spec = valueSpec.variants[control.controls[valueSpec.tag.id].value]
convertToNumberRecursive(spec, control)
convertValuesRecursive(spec, control)
} else if (valueSpec.type === 'list') {
const formArr = group.get(key) as FormArray
if (valueSpec.subtype === 'number') {
formArr.controls.forEach(control => {
control.setValue(Number(control.value))
control.setValue(control.value ? Number(control.value) : null)
})
} else if (valueSpec.subtype === 'string') {
formArr.controls.forEach(control => {
if (!control.value) control.setValue(null)
control.setValue(control.value ? Number(control.value) : null)
})
} else if (valueSpec.subtype === 'object') {
formArr.controls.forEach((formGroup: FormGroup) => {
const objectSpec = valueSpec.spec as ListValueSpecObject
convertToNumberRecursive(objectSpec.spec, formGroup)
convertValuesRecursive(objectSpec.spec, formGroup)
})
} else if (valueSpec.subtype === 'union') {
formArr.controls.forEach((formGroup: FormGroup) => {
const unionSpec = valueSpec.spec as ListValueSpecUnion
const spec = unionSpec.variants[formGroup.controls[unionSpec.tag.id].value]
convertToNumberRecursive(spec, formGroup)
convertValuesRecursive(spec, formGroup)
})
}
}