mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
0.3.0 refactor
ui: adds overlay layer to patch-db-client ui: getting towards mocks ui: cleans up factory init ui: nice type hack ui: live api for patch ui: api service source + http starts up ui: api source + http ui: rework patchdb config, pass stashTimeout into patchDbModel wires in temp patching into api service ui: example of wiring patchdbmodel into page begin integration remove unnecessary method linting first data rendering rework app initialization http source working for ssh delete call temp patches working entire Embassy tab complete not in kansas anymore ripping, saving progress progress for API request response types and endoint defs Update data-model.ts shambles, but in a good way progress big progress progress installed list working big progress progress progress begin marketplace redesign Update api-types.ts Update api-types.ts marketplace improvements cosmetic dependencies and recommendations begin nym auth approach install wizard restore flow and donations
This commit is contained in:
committed by
Aiden McClelland
parent
fd685ae32c
commit
594d93eb3b
484
ui/src/app/pkg-config/config-cursor.ts
Normal file
484
ui/src/app/pkg-config/config-cursor.ts
Normal file
@@ -0,0 +1,484 @@
|
||||
import { ValueSpec, ConfigSpec, UniqueBy, ValueSpecOf, ValueType, ValueSpecObject, ValueSpecUnion } from './config-types'
|
||||
import { Annotations, getDefaultObject, getDefaultUnion, listInnerSpec, mapConfigSpec, Range } from './config-utilities'
|
||||
import * as pointer from 'json-pointer'
|
||||
import * as handlebars from 'handlebars'
|
||||
|
||||
export class ConfigCursor<T extends ValueType> {
|
||||
private cachedSpec?: ValueSpecOf<T>
|
||||
|
||||
constructor (
|
||||
private readonly rootSpec: ConfigSpec,
|
||||
private readonly rootOldConfig: object,
|
||||
private readonly rootMappedConfig: object = null,
|
||||
private readonly rootConfig: object = null,
|
||||
private readonly ptr: string = '',
|
||||
) {
|
||||
if (!this.rootOldConfig) {
|
||||
this.rootOldConfig = getDefaultObject(this.rootSpec)
|
||||
}
|
||||
if (!this.rootMappedConfig) {
|
||||
this.rootMappedConfig = JSON.parse(JSON.stringify(this.rootOldConfig))
|
||||
mapConfigSpec(this.rootSpec, this.rootMappedConfig)
|
||||
}
|
||||
if (!this.rootConfig) {
|
||||
this.rootConfig = JSON.parse(JSON.stringify(this.rootMappedConfig))
|
||||
}
|
||||
}
|
||||
|
||||
seek<S extends ValueType> (ptr: string): ConfigCursor<S> {
|
||||
return new ConfigCursor(
|
||||
this.rootSpec,
|
||||
this.rootOldConfig,
|
||||
this.rootMappedConfig,
|
||||
this.rootConfig,
|
||||
pointer.compile(
|
||||
pointer.parse(this.ptr)
|
||||
.concat(pointer.parse(ptr)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
seekNext<S extends ValueType> (key: string | number): ConfigCursor<S> {
|
||||
return this.seek(pointer.compile([`${key}`]))
|
||||
}
|
||||
|
||||
unseek<S extends ValueType> (levels?: number): ConfigCursor<S> {
|
||||
let ptr: string
|
||||
if (levels === undefined) {
|
||||
ptr = ''
|
||||
} else {
|
||||
// TODO, delete or make use of, it isn't being used so far
|
||||
// This is not being used so far
|
||||
let ptr_arr = pointer.parse(this.ptr)
|
||||
for (let i = 0; i < levels; i++) {
|
||||
ptr_arr.pop()
|
||||
}
|
||||
ptr = pointer.compile(ptr_arr)
|
||||
}
|
||||
return new ConfigCursor(
|
||||
this.rootSpec,
|
||||
this.rootOldConfig,
|
||||
this.rootMappedConfig,
|
||||
this.rootConfig,
|
||||
ptr,
|
||||
)
|
||||
}
|
||||
|
||||
key (): string {
|
||||
return pointer.parse(this.ptr).pop()
|
||||
}
|
||||
|
||||
oldConfig (): any {
|
||||
if (pointer.has(this.rootOldConfig, this.ptr)) {
|
||||
return pointer.get(this.rootOldConfig, this.ptr)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
mappedConfig (): any {
|
||||
if (pointer.has(this.rootMappedConfig, this.ptr)) {
|
||||
return pointer.get(this.rootMappedConfig, this.ptr)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
toString (): string {
|
||||
const spec: ValueSpec = this.spec()
|
||||
const config = this.config()
|
||||
switch (spec.type) {
|
||||
case 'string':
|
||||
return config
|
||||
case 'number':
|
||||
return `${config}${spec.units ? ' ' + spec.units : ''}`
|
||||
case 'object':
|
||||
return spec.displayAs ? handlebars.compile(spec.displayAs)(config) : ''
|
||||
case 'union':
|
||||
return spec.displayAs ? handlebars.compile(spec.displayAs)(config) : config[spec.tag.id]
|
||||
case 'pointer':
|
||||
return 'System Defined'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// if (config : T) then (spec : ValueSpecOf<T>)
|
||||
config (): any {
|
||||
if (pointer.has(this.rootConfig, this.ptr)) {
|
||||
return pointer.get(this.rootConfig, this.ptr)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
// if (config : T) then (spec : ValueSpecOf<T>)
|
||||
spec (): ValueSpecOf<T> {
|
||||
if (this.cachedSpec) return this.cachedSpec
|
||||
const parsed = pointer.parse(this.ptr)
|
||||
|
||||
// We elevate the rootSpec (ConfigSpec) to a dummy ValueSpecObject
|
||||
let ret: ValueSpec = {
|
||||
type: 'object',
|
||||
spec: this.rootSpec,
|
||||
nullable: false,
|
||||
nullByDefault: false,
|
||||
name: 'Config',
|
||||
displayAs: 'Config',
|
||||
uniqueBy: null,
|
||||
}
|
||||
let ptr = []
|
||||
for (let seg of parsed) {
|
||||
switch (ret.type) {
|
||||
case 'object':
|
||||
ret = ret.spec[seg]
|
||||
break
|
||||
case 'union':
|
||||
if (seg === ret.tag.id) {
|
||||
ret = {
|
||||
type: 'enum',
|
||||
default: ret.default,
|
||||
values: Object.keys(ret.variants),
|
||||
name: ret.tag.name,
|
||||
description: ret.tag.description,
|
||||
valueNames: ret.tag.variantNames,
|
||||
}
|
||||
} else {
|
||||
const cfg = this.unseek().seek(pointer.compile(ptr))
|
||||
ret = ret.variants[cfg.config()[ret.tag.id]][seg]
|
||||
}
|
||||
break
|
||||
case 'list':
|
||||
//in essence, for a list we replace the list typed ValueSpecOf with it's internal ListValueSpec, a ValueSpecOf<T> where config @ ptr is of type T[].
|
||||
// we also append default values to it.
|
||||
// note also that jsonKey is not used. jsonKey in this case is an index of an array, like 0, 1, etc.
|
||||
// this implies that every index of a list has an identical inner spec
|
||||
ret = listInnerSpec(ret)
|
||||
break
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
if (ret === undefined) break
|
||||
ptr.push(seg)
|
||||
}
|
||||
this.cachedSpec = ret as ValueSpecOf<T>
|
||||
return this.cachedSpec
|
||||
}
|
||||
|
||||
checkInvalid (): string | null { // null if valid
|
||||
const spec: ValueSpec = this.spec()
|
||||
const cfg = this.config()
|
||||
switch (spec.type) {
|
||||
case 'string':
|
||||
if (!cfg) {
|
||||
return spec.nullable ? null : `${spec.name} is missing.`
|
||||
} else if (typeof cfg === 'string') {
|
||||
if (!spec.pattern || new RegExp(spec.pattern).test(cfg)) {
|
||||
return null
|
||||
} else {
|
||||
return spec.patternDescription
|
||||
}
|
||||
} else {
|
||||
throw new TypeError(`${this.ptr}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
|
||||
}
|
||||
case 'number':
|
||||
if (!cfg) {
|
||||
return spec.nullable ? null : `${spec.name} is missing.`
|
||||
} else if (typeof cfg === 'number') {
|
||||
if (spec.integral && cfg != Math.trunc(cfg)) {
|
||||
return `${spec.name} must be an integer.`
|
||||
}
|
||||
try {
|
||||
Range.from(spec.range).checkIncludes(cfg)
|
||||
return null
|
||||
} catch (e) {
|
||||
return e.message
|
||||
}
|
||||
} else {
|
||||
throw new TypeError(`${this.ptr}: expected number, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
|
||||
}
|
||||
case 'boolean':
|
||||
if (typeof cfg === 'boolean') {
|
||||
return null
|
||||
} else {
|
||||
throw new TypeError(`${this.ptr}: expected boolean, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
|
||||
}
|
||||
case 'enum':
|
||||
if (typeof cfg === 'string') {
|
||||
spec.valuesSet = spec.valuesSet || new Set(spec.values)
|
||||
return spec.valuesSet.has(cfg) ? null : `${cfg} is not a valid selection.`
|
||||
} else {
|
||||
throw new TypeError(`${this.ptr}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
|
||||
}
|
||||
case 'list':
|
||||
if (Array.isArray(cfg)) {
|
||||
const range = Range.from(spec.range)
|
||||
const min = range.integralMin()
|
||||
const max = range.integralMax()
|
||||
const length = cfg.length
|
||||
if (min && length < min) {
|
||||
return spec.subtype === 'enum' ? 'Not enough options selected.' : 'List is too short.'
|
||||
}
|
||||
if (max && length > max) {
|
||||
return spec.subtype === 'enum' ? 'Too many options selected.' : 'List is too long.'
|
||||
}
|
||||
for (let idx = 0; idx < cfg.length; idx++) {
|
||||
let cursor = this.seekNext(idx)
|
||||
const invalid = cursor.checkInvalid()
|
||||
if (invalid) {
|
||||
return `Item #${idx + 1} is invalid. ${invalid}.`
|
||||
}
|
||||
if (spec.subtype === 'enum') continue
|
||||
for (let idx2 = idx + 1; idx2 < cfg.length; idx2++) {
|
||||
if (cursor.equals(this.seekNext(idx2))) {
|
||||
return `Item #${idx + 1} is not unique.` + ('uniqueBy' in cursor.spec()) ? `${
|
||||
displayUniqueBy(
|
||||
(cursor.spec() as ValueSpecObject | ValueSpecUnion).uniqueBy,
|
||||
(cursor.spec() as ValueSpecObject | ValueSpecUnion),
|
||||
cursor.config(),
|
||||
)
|
||||
} must be unique.` : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
} else {
|
||||
throw new TypeError(`${this.ptr}: expected array, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
|
||||
}
|
||||
case 'object':
|
||||
if (!cfg) {
|
||||
return spec.nullable ? null : `${spec.name} is missing.`
|
||||
} else if (typeof cfg === 'object' && !Array.isArray(cfg)) {
|
||||
for (let idx in spec.spec) {
|
||||
if (this.seekNext(idx).checkInvalid()) {
|
||||
return `${spec.spec[idx].name} is invalid.`
|
||||
}
|
||||
}
|
||||
return null
|
||||
} else {
|
||||
throw new TypeError(`${this.ptr}: expected object, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
|
||||
}
|
||||
case 'pointer':
|
||||
return null
|
||||
case 'union':
|
||||
if (typeof cfg === 'object' && !Array.isArray(cfg)) {
|
||||
if (typeof cfg[spec.tag.id] === 'string') {
|
||||
for (let idx in spec.variants[cfg[spec.tag.id]]) {
|
||||
if (this.seekNext(idx).checkInvalid()) {
|
||||
return `${spec.variants[cfg[spec.tag.id]][idx].name} is invalid.`
|
||||
}
|
||||
}
|
||||
return null
|
||||
} else {
|
||||
throw new TypeError(`${this.ptr}/${spec.tag.id}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
|
||||
}
|
||||
} else {
|
||||
throw new TypeError(`${this.ptr}: expected object, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isNew (): boolean {
|
||||
const oldCfg = this.oldConfig()
|
||||
const mappedCfg = this.mappedConfig()
|
||||
if (mappedCfg && oldCfg && typeof mappedCfg === 'object' && typeof oldCfg === 'object') {
|
||||
for (let key in mappedCfg) {
|
||||
if (this.seekNext(key).isNew()) return true
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
return mappedCfg !== oldCfg
|
||||
}
|
||||
}
|
||||
|
||||
isEdited (): boolean {
|
||||
const cfg = this.config()
|
||||
const mappedCfg = this.mappedConfig()
|
||||
if (cfg && mappedCfg && typeof cfg === 'object' && typeof mappedCfg === 'object') {
|
||||
const spec = this.spec()
|
||||
if (spec === undefined) return true
|
||||
let allKeys: Set<string>
|
||||
if (spec.type === 'union') {
|
||||
let unionSpec = spec as ValueSpecOf<'union'>
|
||||
const labelForSelection = unionSpec.tag.id
|
||||
allKeys = new Set([labelForSelection, ...Object.keys(unionSpec.variants[cfg[labelForSelection]])])
|
||||
} else {
|
||||
allKeys = new Set([...Object.keys(cfg), ...Object.keys(mappedCfg)])
|
||||
}
|
||||
|
||||
for (let key of allKeys) {
|
||||
if (this.seekNext(key).isEdited()) return true
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
return cfg !== mappedCfg
|
||||
}
|
||||
}
|
||||
|
||||
equals (cursor: ConfigCursor<T>): boolean {
|
||||
const lhs = this.config()
|
||||
const rhs = cursor.config()
|
||||
const spec: ValueSpec = this.spec()
|
||||
|
||||
switch (spec.type) {
|
||||
case 'string':
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
case 'enum':
|
||||
return lhs === rhs
|
||||
case 'object':
|
||||
case 'union':
|
||||
return isEqual(spec.uniqueBy, this as ConfigCursor<'object' | 'union'>, cursor as ConfigCursor<'object' | 'union'>)
|
||||
case 'list':
|
||||
if (lhs.length !== rhs.length) {
|
||||
return false
|
||||
}
|
||||
for (let idx = 0; idx < lhs.length; idx++) {
|
||||
if (!this.seekNext(`${idx}`).equals(cursor.seekNext(`${idx}`))) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
getAnnotations (): Annotations<T> {
|
||||
const spec: ValueSpec = this.spec()
|
||||
switch (spec.type) {
|
||||
case 'object': {
|
||||
const ret: Annotations<'object'> = {
|
||||
self: {
|
||||
invalid: this.checkInvalid(),
|
||||
edited: this.isEdited(),
|
||||
added: this.isNew(),
|
||||
},
|
||||
members: { },
|
||||
}
|
||||
for (let key in spec.spec) {
|
||||
let annotation: any = this.seekNext(key).getAnnotations()
|
||||
if ('self' in annotation) {
|
||||
annotation = annotation.self
|
||||
}
|
||||
ret.members[key] = annotation
|
||||
}
|
||||
return ret as Annotations<T>
|
||||
}
|
||||
case 'union': {
|
||||
const ret: Annotations<'union'> = {
|
||||
self: {
|
||||
invalid: this.checkInvalid(),
|
||||
edited: this.isEdited(),
|
||||
added: this.isNew(),
|
||||
},
|
||||
members: {
|
||||
[spec.tag.id]: this.seekNext<'enum'>(spec.tag.id).getAnnotations(),
|
||||
},
|
||||
}
|
||||
for (let key in spec.variants[this.config()[spec.tag.id]]) {
|
||||
let annotation: any = this.seekNext(key).getAnnotations()
|
||||
if ('self' in annotation) {
|
||||
annotation = annotation.self
|
||||
}
|
||||
ret.members[key] = annotation
|
||||
}
|
||||
return ret as Annotations<T>
|
||||
}
|
||||
case 'list': {
|
||||
const ret: Annotations<'list'> = {
|
||||
self: {
|
||||
invalid: this.checkInvalid(),
|
||||
edited: this.isEdited(),
|
||||
added: this.isNew(),
|
||||
},
|
||||
members: [],
|
||||
}
|
||||
for (let key in this.config()) {
|
||||
let annotation: any = this.seekNext(key).getAnnotations()
|
||||
if ('self' in annotation) {
|
||||
annotation = annotation.self
|
||||
}
|
||||
ret.members[key] = annotation
|
||||
}
|
||||
return ret as Annotations<T>
|
||||
}
|
||||
default:
|
||||
return {
|
||||
invalid: this.checkInvalid(),
|
||||
edited: this.isEdited(),
|
||||
added: this.isNew(),
|
||||
} as Annotations<T>
|
||||
}
|
||||
}
|
||||
|
||||
async createFirstEntryForList () {
|
||||
const spec: ValueSpec = this.spec()
|
||||
|
||||
if (spec.type === 'object' && !this.config()) {
|
||||
pointer.set(this.rootConfig, this.ptr, getDefaultObject(spec.spec))
|
||||
}
|
||||
|
||||
if (spec.type === 'union' && !this.config()) {
|
||||
pointer.set(this.rootConfig, this.ptr, getDefaultUnion(spec))
|
||||
}
|
||||
}
|
||||
|
||||
injectModalData (res: { data?: any }): void {
|
||||
if (res.data !== undefined) {
|
||||
pointer.set(this.rootConfig, this.ptr, res.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isEqual (uniqueBy: UniqueBy, lhs: ConfigCursor<'object'>, rhs: ConfigCursor<'object'>): boolean {
|
||||
if (uniqueBy === null) {
|
||||
return false
|
||||
} else if (typeof uniqueBy === 'string') {
|
||||
return lhs.seekNext(uniqueBy).equals(rhs.seekNext(uniqueBy))
|
||||
} else if ('any' in uniqueBy) {
|
||||
for (let subSpec of uniqueBy.any) {
|
||||
if (isEqual(subSpec, lhs, rhs)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else if ('all' in uniqueBy) {
|
||||
for (let subSpec of uniqueBy.all) {
|
||||
if (!isEqual(subSpec, lhs, rhs)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export function displayUniqueBy (uniqueBy: UniqueBy, spec: ValueSpecObject | ValueSpecUnion, value: object): string {
|
||||
if (typeof uniqueBy === 'string') {
|
||||
if (spec.type === 'object') {
|
||||
return spec.spec[uniqueBy].name
|
||||
} else if (spec.type === 'union') {
|
||||
if (uniqueBy === spec.tag.id) {
|
||||
return spec.tag.name
|
||||
} else {
|
||||
return spec.variants[value[spec.tag.id]][uniqueBy].name
|
||||
}
|
||||
}
|
||||
} else if ('any' in uniqueBy) {
|
||||
return uniqueBy.any.map(uq => {
|
||||
if (typeof uq === 'object' && 'all' in uq) {
|
||||
return `(${displayUniqueBy(uq, spec, value)})`
|
||||
} else {
|
||||
return displayUniqueBy(uq, spec, value)
|
||||
}
|
||||
}).join(' and ')
|
||||
} else if ('all' in uniqueBy) {
|
||||
return uniqueBy.all.map(uq => {
|
||||
if (typeof uq === 'object' && 'any' in uq) {
|
||||
return `(${displayUniqueBy(uq, spec, value)})`
|
||||
} else {
|
||||
return displayUniqueBy(uq, spec, value)
|
||||
}
|
||||
}).join(' or ')
|
||||
}
|
||||
}
|
||||
134
ui/src/app/pkg-config/config-types.ts
Normal file
134
ui/src/app/pkg-config/config-types.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
export interface ConfigSpec { [key: string]: ValueSpec }
|
||||
|
||||
export type ValueType = 'string' | 'number' | 'boolean' | 'enum' | 'list' | 'object' | 'pointer' | 'union'
|
||||
export type ValueSpec = ValueSpecOf<ValueType>
|
||||
|
||||
// core spec types. These types provide the metadata for performing validations
|
||||
export type ValueSpecOf<T extends ValueType> =
|
||||
T extends 'string' ? ValueSpecString :
|
||||
T extends 'number' ? ValueSpecNumber :
|
||||
T extends 'boolean' ? ValueSpecBoolean :
|
||||
T extends 'enum' ? ValueSpecEnum :
|
||||
T extends 'list' ? ValueSpecList :
|
||||
T extends 'object' ? ValueSpecObject :
|
||||
T extends 'pointer' ? ValueSpecPointer :
|
||||
T extends 'union' ? ValueSpecUnion :
|
||||
never
|
||||
|
||||
export interface ValueSpecString extends ListValueSpecString, WithStandalone {
|
||||
type: 'string'
|
||||
default?: DefaultString
|
||||
nullable: boolean
|
||||
masked: boolean
|
||||
copyable: boolean
|
||||
}
|
||||
|
||||
export interface ValueSpecNumber extends ListValueSpecNumber, WithStandalone {
|
||||
type: 'number'
|
||||
nullable: boolean
|
||||
default?: number
|
||||
}
|
||||
|
||||
export interface ValueSpecEnum extends ListValueSpecEnum, WithStandalone {
|
||||
type: 'enum'
|
||||
default: string
|
||||
}
|
||||
|
||||
export interface ValueSpecBoolean extends WithStandalone {
|
||||
type: 'boolean'
|
||||
default: boolean
|
||||
}
|
||||
|
||||
export interface ValueSpecUnion extends ListValueSpecUnion, WithStandalone {
|
||||
type: 'union'
|
||||
}
|
||||
|
||||
export interface ValueSpecPointer extends WithStandalone {
|
||||
type: 'pointer'
|
||||
subtype: 'app' | 'system'
|
||||
target: 'lan-address' | 'tor-address' | 'config'
|
||||
'app-id': string
|
||||
}
|
||||
|
||||
export interface ValueSpecObject extends ListValueSpecObject, WithStandalone {
|
||||
type: 'object'
|
||||
nullable: boolean
|
||||
nullByDefault: boolean
|
||||
}
|
||||
|
||||
export interface WithStandalone {
|
||||
name: string
|
||||
description?: string
|
||||
changeWarning?: string
|
||||
}
|
||||
|
||||
// no lists of booleans, lists, pointers
|
||||
export type ListValueSpecType = 'string' | 'number' | 'enum' | 'object' | 'union'
|
||||
|
||||
// represents a spec for the values of a list
|
||||
export type ListValueSpecOf<T extends ListValueSpecType> =
|
||||
T extends 'string' ? ListValueSpecString :
|
||||
T extends 'number' ? ListValueSpecNumber :
|
||||
T extends 'enum' ? ListValueSpecEnum :
|
||||
T extends 'object' ? ListValueSpecObject :
|
||||
T extends 'union' ? ListValueSpecUnion :
|
||||
never
|
||||
|
||||
// represents a spec for a list
|
||||
export type ValueSpecList = ValueSpecListOf<ListValueSpecType>
|
||||
export interface ValueSpecListOf<T extends ListValueSpecType> extends WithStandalone {
|
||||
type: 'list'
|
||||
subtype: T
|
||||
spec: ListValueSpecOf<T>
|
||||
range: string // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules
|
||||
default: string[] | number[] | DefaultString[] | object[]
|
||||
}
|
||||
|
||||
// sometimes the type checker needs just a little bit of help
|
||||
export function isValueSpecListOf<S extends ListValueSpecType> (t: ValueSpecList, s: S): t is ValueSpecListOf<S> {
|
||||
return t.subtype === s
|
||||
}
|
||||
|
||||
export interface ListValueSpecString {
|
||||
pattern?: string
|
||||
patternDescription?: string
|
||||
}
|
||||
|
||||
export interface ListValueSpecNumber {
|
||||
range: string
|
||||
integral: boolean
|
||||
units?: string
|
||||
}
|
||||
|
||||
export interface ListValueSpecEnum {
|
||||
values: string[]
|
||||
valuesSet?: Set<string>
|
||||
valueNames: { [value: string]: string }
|
||||
}
|
||||
|
||||
export interface ListValueSpecObject {
|
||||
spec: ConfigSpec //this is a mapped type of the config object at this level, replacing the object's values with specs on those values
|
||||
uniqueBy: UniqueBy //indicates whether duplicates can be permitted in the list
|
||||
displayAs?: string //this should be a handlebars template which can make use of the entire config which corresponds to 'spec'
|
||||
}
|
||||
|
||||
export type UniqueBy = null | string | { any: UniqueBy[] } | { all: UniqueBy[] }
|
||||
|
||||
export interface ListValueSpecUnion {
|
||||
tag: UnionTagSpec
|
||||
variants: { [key: string]: ConfigSpec }
|
||||
displayAs?: string //this may be a handlebars template which can conditionally (on tag.id) make use of each union's entries, or if left blank will display as tag.id
|
||||
uniqueBy: UniqueBy
|
||||
default: string //this should be the variantName which one prefers a user to start with by default when creating a new union instance in a list
|
||||
}
|
||||
|
||||
export interface UnionTagSpec {
|
||||
id: string //The name of the field containing one of the union variants
|
||||
name: string
|
||||
description?: string
|
||||
variantNames: { //the name of each variant
|
||||
[variant: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
export type DefaultString = string | { charset: string, len: number }
|
||||
413
ui/src/app/pkg-config/config-utilities.ts
Normal file
413
ui/src/app/pkg-config/config-utilities.ts
Normal file
@@ -0,0 +1,413 @@
|
||||
import {
|
||||
ValueSpec, ValueSpecList, DefaultString, ValueSpecUnion, ConfigSpec,
|
||||
ValueSpecObject, ValueSpecString, ValueSpecEnum, ValueSpecNumber,
|
||||
ValueSpecBoolean, ValueSpecPointer, ValueSpecOf, ListValueSpecType
|
||||
} from './config-types'
|
||||
|
||||
export interface Annotation {
|
||||
invalid: string | null
|
||||
edited: boolean
|
||||
added: boolean
|
||||
}
|
||||
|
||||
export type Annotations<T extends string> =
|
||||
T extends 'object' | 'union' ? { self: Annotation, members: { [key: string]: Annotation } } :
|
||||
T extends 'list' ? { self: Annotation, members: Annotation[] } :
|
||||
Annotation
|
||||
|
||||
export class Range {
|
||||
min?: number
|
||||
max?: number
|
||||
minInclusive: boolean
|
||||
maxInclusive: boolean
|
||||
|
||||
|
||||
static from (s: string): Range {
|
||||
const r = new Range()
|
||||
r.minInclusive = s.startsWith('[')
|
||||
r.maxInclusive = s.endsWith(']')
|
||||
const [minStr, maxStr] = s.split(',').map(a => a.trim())
|
||||
r.min = minStr === '(*' ? undefined : Number(minStr.slice(1))
|
||||
r.max = maxStr === '*)' ? undefined : Number(maxStr.slice(0, -1))
|
||||
return r
|
||||
}
|
||||
|
||||
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.hasMax() && ((!this.maxInclusive && this.max == n || (this.max < n)))) {
|
||||
throw new Error(`Value must be ${this.maxMessage()}.`)
|
||||
}
|
||||
}
|
||||
|
||||
hasMin (): boolean {
|
||||
return this.min !== undefined
|
||||
}
|
||||
|
||||
hasMax (): boolean {
|
||||
return this.max !== undefined
|
||||
}
|
||||
|
||||
minMessage (): string {
|
||||
return `greater than${this.minInclusive ? ' or equal to' : ''} ${this.min}`
|
||||
}
|
||||
|
||||
maxMessage (): string {
|
||||
return `less than${this.maxInclusive ? ' or equal to' : ''} ${this.max}`
|
||||
}
|
||||
|
||||
description (): string {
|
||||
let message = 'Value can be any number.'
|
||||
|
||||
if (this.hasMin() || this.hasMax()) {
|
||||
message = 'Value must be'
|
||||
}
|
||||
|
||||
if (this.hasMin() && this.hasMax()) {
|
||||
message = `${message} ${this.minMessage()} AND ${this.maxMessage()}.`
|
||||
} else if (this.hasMin() && !this.hasMax()) {
|
||||
message = `${message} ${this.minMessage()}.`
|
||||
} else if (!this.hasMin() && this.hasMax()) {
|
||||
message = `${message} ${this.maxMessage()}.`
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
integralMin (): number | undefined {
|
||||
if (this.min) {
|
||||
const ceil = Math.ceil(this.min)
|
||||
if (this.minInclusive) {
|
||||
return ceil
|
||||
} else {
|
||||
if (ceil === this.min) {
|
||||
return ceil + 1
|
||||
} else {
|
||||
return ceil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
integralMax (): number | undefined {
|
||||
if (this.max) {
|
||||
const floor = Math.floor(this.max)
|
||||
if (this.maxInclusive) {
|
||||
return floor
|
||||
} else {
|
||||
if (floor === this.max) {
|
||||
return floor - 1
|
||||
} else {
|
||||
return floor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// converts a ValueSpecList, i.e. a spec for a list, to its inner ListValueSpec, i.e., a spec for the values within the list.
|
||||
// We then augment it with the defalt values (e.g. nullable: false) to make a
|
||||
export function listInnerSpec (listSpec: ValueSpecList): ValueSpecOf<ListValueSpecType> {
|
||||
return {
|
||||
type: listSpec.subtype,
|
||||
nullable: false,
|
||||
name: listSpec.name,
|
||||
description: listSpec.description,
|
||||
changeWarning: listSpec.changeWarning,
|
||||
...listSpec.spec as any, //listSpec.spec is a ListValueSpecOf listSpec.subtype
|
||||
}
|
||||
}
|
||||
|
||||
export function mapSpecToConfigValue (spec: ValueSpec, value: any): any {
|
||||
if (value === undefined) return undefined
|
||||
switch (spec.type) {
|
||||
case 'string': return mapStringSpec(value)
|
||||
case 'number': return mapNumberSpec(value)
|
||||
case 'boolean': return mapBooleanSpec(spec, value)
|
||||
case 'enum': return mapEnumSpec(spec, value)
|
||||
case 'list': return mapListSpec(spec, value)
|
||||
case 'object': return mapObjectSpec(spec, value)
|
||||
case 'union': return mapUnionSpec(spec, value)
|
||||
case 'pointer': return value
|
||||
}
|
||||
}
|
||||
|
||||
export function mapConfigSpec (configSpec: ConfigSpec, value: any): object {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
Object.entries(configSpec).map(([key, val]) => {
|
||||
value[key] = mapSpecToConfigValue(val, value[key])
|
||||
if (value[key] === undefined) {
|
||||
value[key] = getDefaultConfigValue(val)
|
||||
}
|
||||
})
|
||||
return value
|
||||
} else {
|
||||
return getDefaultObject(configSpec)
|
||||
}
|
||||
}
|
||||
|
||||
export function mapObjectSpec (spec: ValueSpecObject, value: any): object {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
return mapConfigSpec(spec.spec, value)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function mapUnionSpec (spec: ValueSpecUnion, value: any): object {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
const variant = mapEnumSpec({
|
||||
...spec.tag,
|
||||
type: 'enum',
|
||||
default: spec.default,
|
||||
values: Object.keys(spec.variants),
|
||||
valueNames: spec.tag.variantNames,
|
||||
}, value[spec.tag.id])
|
||||
value = mapConfigSpec(spec.variants[variant], value)
|
||||
value[spec.tag.id] = variant
|
||||
return value
|
||||
} else {
|
||||
return getDefaultUnion(spec)
|
||||
}
|
||||
}
|
||||
|
||||
export function mapStringSpec (value: any): string {
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function mapNumberSpec (value: any): number {
|
||||
if (typeof value === 'number') {
|
||||
return value
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function mapEnumSpec (spec: ValueSpecEnum, value: any): string {
|
||||
if (typeof value === 'string' && spec.values.includes(value)) {
|
||||
return value
|
||||
} else {
|
||||
return spec.default
|
||||
}
|
||||
}
|
||||
|
||||
export function mapListSpec (spec: ValueSpecList, value: any): string[] | number[] | object[] {
|
||||
if (Array.isArray(value)) {
|
||||
const innerSpec = listInnerSpec(spec)
|
||||
return value.map(item => mapSpecToConfigValue(innerSpec, item))
|
||||
} else {
|
||||
return getDefaultList(spec)
|
||||
}
|
||||
}
|
||||
|
||||
export function mapBooleanSpec (spec: ValueSpecBoolean, value: any): boolean {
|
||||
if (typeof value === 'boolean') {
|
||||
return value
|
||||
} else {
|
||||
return spec.default
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultConfigValue (spec: ValueSpec): string | number | object | string[] | number[] | object[] | boolean | null {
|
||||
switch (spec.type) {
|
||||
case 'object':
|
||||
return spec.nullByDefault ? null : getDefaultObject(spec.spec)
|
||||
case 'union':
|
||||
return getDefaultUnion(spec)
|
||||
case 'string':
|
||||
return spec.default ? getDefaultString(spec.default) : null
|
||||
case 'number':
|
||||
return spec.default || null
|
||||
case 'list':
|
||||
return getDefaultList(spec)
|
||||
case 'enum':
|
||||
case 'boolean':
|
||||
return spec.default
|
||||
case 'pointer':
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultObject (spec: ConfigSpec): object {
|
||||
const obj = { }
|
||||
Object.entries(spec).map(([key, val]) => {
|
||||
obj[key] = getDefaultConfigValue(val)
|
||||
})
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
export function getDefaultList (spec: ValueSpecList): string[] | number[] | object[] {
|
||||
if (spec.subtype === 'object') {
|
||||
const l = (spec.default as any[])
|
||||
const range = Range.from(spec.range)
|
||||
while (l.length < range.integralMin()) {
|
||||
l.push(getDefaultConfigValue(listInnerSpec(spec)))
|
||||
}
|
||||
return l as string[] | number[] | object[]
|
||||
} else {
|
||||
const l = (spec.default as any[]).map(d => getDefaultConfigValue({ ...listInnerSpec(spec), default: d }))
|
||||
return l as string[] | number[] | object[]
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultUnion (spec: ValueSpecUnion): object {
|
||||
return { [spec.tag.id]: spec.default, ...getDefaultObject(spec.variants[spec.default]) }
|
||||
}
|
||||
|
||||
export function getDefaultMapTagKey (defaultSpec: DefaultString = '', value: object): string {
|
||||
const keySrc = getDefaultString(defaultSpec)
|
||||
|
||||
const keys = Object.keys(value)
|
||||
|
||||
let key = keySrc
|
||||
let idx = 1
|
||||
while (keys.includes(key)) {
|
||||
key = `${keySrc}-${idx++}`
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
export function getDefaultString (defaultSpec: DefaultString): string {
|
||||
if (typeof defaultSpec === 'string') {
|
||||
return defaultSpec
|
||||
} else {
|
||||
let s = ''
|
||||
for (let i = 0; i < defaultSpec.len; i++) {
|
||||
s = s + getRandomCharInSet(defaultSpec.charset)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultDescription (spec: ValueSpec): string {
|
||||
let toReturn: string | undefined
|
||||
switch (spec.type) {
|
||||
case 'string':
|
||||
if (typeof spec.default === 'string') {
|
||||
toReturn = spec.default
|
||||
} else if (typeof spec.default === 'object') {
|
||||
toReturn = 'random'
|
||||
}
|
||||
break
|
||||
case 'number':
|
||||
if (typeof spec.default === 'number') {
|
||||
toReturn = String(spec.default)
|
||||
}
|
||||
break
|
||||
case 'boolean':
|
||||
toReturn = spec.default === true ? 'True' : 'False'
|
||||
break
|
||||
case 'enum':
|
||||
toReturn = spec.valueNames[spec.default]
|
||||
break
|
||||
}
|
||||
|
||||
return toReturn || ''
|
||||
}
|
||||
|
||||
// a,g,h,A-Z,,,,-
|
||||
export function getRandomCharInSet (charset: string): string {
|
||||
const set = stringToCharSet(charset)
|
||||
let charIdx = Math.floor(Math.random() * set.len)
|
||||
for (let range of set.ranges) {
|
||||
if (range.len > charIdx) {
|
||||
return String.fromCharCode(range.start.charCodeAt(0) + charIdx)
|
||||
}
|
||||
charIdx -= range.len
|
||||
}
|
||||
throw new Error('unreachable')
|
||||
}
|
||||
|
||||
function stringToCharSet (charset: string): CharSet {
|
||||
let set: CharSet = { ranges: [], len: 0 }
|
||||
let start: string | null = null
|
||||
let end: string | null = null
|
||||
let in_range = false
|
||||
for (let char of charset) {
|
||||
switch (char) {
|
||||
case ',':
|
||||
if (start !== null && end !== null) {
|
||||
if (start!.charCodeAt(0) > end!.charCodeAt(0)) {
|
||||
throw new Error('start > end of charset')
|
||||
}
|
||||
const len = end.charCodeAt(0) - start.charCodeAt(0) + 1
|
||||
set.ranges.push({
|
||||
start,
|
||||
end,
|
||||
len,
|
||||
})
|
||||
set.len += len
|
||||
start = null
|
||||
end = null
|
||||
in_range = false
|
||||
} else if (start !== null && !in_range) {
|
||||
set.len += 1
|
||||
set.ranges.push({ start, end: start, len: 1 })
|
||||
start = null
|
||||
} else if (start !== null && in_range) {
|
||||
end = ','
|
||||
} else if (start === null && end === null && !in_range) {
|
||||
start = ','
|
||||
} else {
|
||||
throw new Error('unexpected ","')
|
||||
}
|
||||
break
|
||||
case '-':
|
||||
if (start === null) {
|
||||
start = '-'
|
||||
} else if (!in_range) {
|
||||
in_range = true
|
||||
} else if (in_range && end === null) {
|
||||
end = '-'
|
||||
} else {
|
||||
throw new Error('unexpected "-"')
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (start === null) {
|
||||
start = char
|
||||
} else if (in_range && end === null) {
|
||||
end = char
|
||||
} else {
|
||||
throw new Error(`unexpected "${char}"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (start !== null && end !== null) {
|
||||
if (start!.charCodeAt(0) > end!.charCodeAt(0)) {
|
||||
throw new Error('start > end of charset')
|
||||
}
|
||||
const len = end.charCodeAt(0) - start.charCodeAt(0) + 1
|
||||
set.ranges.push({
|
||||
start,
|
||||
end,
|
||||
len,
|
||||
})
|
||||
set.len += len
|
||||
} else if (start !== null) {
|
||||
set.len += 1
|
||||
set.ranges.push({
|
||||
start,
|
||||
end: start,
|
||||
len: 1,
|
||||
})
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
interface CharSet {
|
||||
ranges: {
|
||||
start: string
|
||||
end: string
|
||||
len: number
|
||||
}[]
|
||||
len: number
|
||||
}
|
||||
27
ui/src/app/pkg-config/modal-presentable.ts
Normal file
27
ui/src/app/pkg-config/modal-presentable.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ConfigCursor } from './config-cursor'
|
||||
import { TrackingModalController } from '../services/tracking-modal-controller.service'
|
||||
|
||||
export class ModalPresentable {
|
||||
constructor (private readonly trackingModalCtrl: TrackingModalController) { }
|
||||
|
||||
async presentModal (cursor: ConfigCursor<any>, callback: () => any) {
|
||||
const modal = await this.trackingModalCtrl.createConfigModal({
|
||||
backdropDismiss: false,
|
||||
presentingElement: await this.trackingModalCtrl.getTop(),
|
||||
componentProps: {
|
||||
cursor,
|
||||
},
|
||||
}, cursor.spec().type)
|
||||
|
||||
modal.onWillDismiss().then(res => {
|
||||
cursor.injectModalData(res)
|
||||
callback()
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
dismissModal (a: any) {
|
||||
return this.trackingModalCtrl.dismiss(a)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user