* begin subnav implementation

* implement subnav AND angular forms for comparison

* unions working-ish, list of enums working

* new form approach almost complete

* finish new forms approach for action inputs and config

* expandable list items and handlebars display

* Config animation (#394)

* config cammel

* config animation

Co-authored-by: Drew Ansbacher <drew.ansbacher@spiredigital.com>

* improve server settings inputs, still needs work

* delete all notifications, styling, and bugs

* contracted by default

Co-authored-by: Drew Ansbacher <drew.ansbacher@gmail.com>
Co-authored-by: Drew Ansbacher <drew.ansbacher@spiredigital.com>
This commit is contained in:
Matt Hill
2021-08-06 09:25:20 -06:00
committed by Aiden McClelland
parent a43ff976a2
commit 5741cf084f
117 changed files with 1967 additions and 1271 deletions

View File

@@ -880,8 +880,8 @@ export module Mock {
'name': 'Testnet',
'type': 'boolean',
'description': 'determines whether your node is running on testnet or mainnet',
'changeWarning': 'Chain will have to resync!',
'default': false,
'change-warning': 'Chain will have to resync!',
'default': true,
},
'objectList': {
'name': 'Object List',
@@ -905,8 +905,8 @@ export module Mock {
// it just so happens that ValueSpecObject's have the field { spec: ConfigSpec }
// see 'unionList' below for a different example.
'spec': {
'uniqueBy': 'lastName',
'displayAs': `I'm {{lastName}}, {{firstName}} {{lastName}}`,
'unique-by': 'lastName',
'display-as': `I'm {{lastName}}, {{firstName}} {{lastName}}`,
'spec': {
'firstName': {
'name': 'First Name',
@@ -927,7 +927,7 @@ export module Mock {
'len': 12,
},
'pattern': '^[a-zA-Z]+$',
'patternDescription': 'must contain only letters.',
'pattern-description': 'must contain only letters.',
'masked': false,
'copyable': true,
},
@@ -938,7 +938,7 @@ export module Mock {
'nullable': true,
'default': null,
'integral': false,
'changeWarning': 'User must be at least 18.',
'change-warning': 'User must be at least 18.',
'range': '[18,*)',
},
},
@@ -949,7 +949,7 @@ export module Mock {
'type': 'list',
'subtype': 'union',
'description': 'This is a sample list of unions',
'changeWarning': 'If you change this, things may work.',
'change-warning': 'If you change this, things may work.',
// a list of union selections. e.g. 'summer', 'winter',...
'default': [
'summer',
@@ -959,7 +959,7 @@ export module Mock {
'tag': {
'id': 'preference',
'name': 'Preferences',
'variantNames': {
'variant-names': {
'summer': 'Summer',
'winter': 'Winter',
'other': 'Other',
@@ -982,7 +982,7 @@ export module Mock {
'name': 'Favorite Flower',
'type': 'enum',
'description': 'Select your favorite flower',
'valueNames': {
'value-names': {
'none': 'Hate Flowers',
'red': 'Red',
'blue': 'Blue',
@@ -1006,13 +1006,13 @@ export module Mock {
},
},
},
'uniqueBy': 'preference',
'unique-by': 'preference',
},
},
'randomEnum': {
'name': 'Random Enum',
'type': 'enum',
'valueNames': {
'value-names': {
'null': 'Null',
'option1': 'One 1',
'option2': 'Two 2',
@@ -1020,7 +1020,7 @@ export module Mock {
},
'default': 'null',
'description': 'This is not even real.',
'changeWarning': 'Be careful chnaging this!',
'change-warning': 'Be careful chnaging this!',
'values': [
'null',
'option1',
@@ -1033,7 +1033,7 @@ export module Mock {
'type': 'number',
'integral': false,
'description': 'Your favorite number of all time',
'changeWarning': 'Once you set this number, it can never be changed without severe consequences.',
'change-warning': 'Once you set this number, it can never be changed without severe consequences.',
'nullable': false,
'default': 7,
'range': '(-100,100]',
@@ -1057,19 +1057,15 @@ export module Mock {
'rpcsettings': {
'name': 'RPC Settings',
'type': 'object',
'uniqueBy': null,
'unique-by': null,
'description': 'rpc username and password',
'changeWarning': 'Adding RPC users gives them special permissions on your node.',
'nullable': false,
'nullByDefault': false,
'change-warning': 'Adding RPC users gives them special permissions on your node.',
'spec': {
'laws': {
'name': 'Laws',
'type': 'object',
'uniqueBy': 'law1',
'unique-by': 'law1',
'description': 'the law of the realm',
'nullable': true,
'nullByDefault': true,
'spec': {
'law1': {
'name': 'First Law',
@@ -1097,7 +1093,7 @@ export module Mock {
'range': '[0,2]',
'default': [],
'spec': {
'uniqueBy': null,
'unique-by': null,
'spec': {
'rulemakername': {
'name': 'Rulemaker Name',
@@ -1118,7 +1114,7 @@ export module Mock {
'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])$',
'patternDescription': 'may only contain numbers and periods',
'pattern-description': 'may only contain numbers and periods',
'masked': false,
'copyable': true,
},
@@ -1132,7 +1128,7 @@ export module Mock {
'nullable': false,
'default': 'defaultrpcusername',
'pattern': '^[a-zA-Z]+$',
'patternDescription': 'must contain only letters.',
'pattern-description': 'must contain only letters.',
'masked': false,
'copyable': true,
},
@@ -1153,10 +1149,8 @@ export module Mock {
'advanced': {
'name': 'Advanced',
'type': 'object',
'uniqueBy': null,
'unique-by': null,
'description': 'Advanced settings',
'nullable': false,
'nullByDefault': false,
'spec': {
'notifications': {
'name': 'Notification Preferences',
@@ -1168,7 +1162,7 @@ export module Mock {
'email',
],
'spec': {
'valueNames': {
'value-names': {
'email': 'EEEEmail',
'text': 'Texxxt',
'call': 'Ccccall',
@@ -1189,14 +1183,14 @@ export module Mock {
'bitcoinNode': {
'name': 'Bitcoin Node Settings',
'type': 'union',
'uniqueBy': null,
'unique-by': null,
'description': 'The node settings',
'default': 'internal',
'changeWarning': 'Careful changing this',
'change-warning': 'Careful changing this',
'tag': {
'id': 'type',
'name': 'Type',
'variantNames': {
'variant-names': {
'internal': 'Internal',
'external': 'External',
},
@@ -1220,7 +1214,7 @@ export module Mock {
'nullable': false,
'default': 'bitcoinnode.com',
'pattern': '.*',
'patternDescription': 'anything',
'pattern-description': 'anything',
'masked': false,
'copyable': true,
},
@@ -1249,14 +1243,14 @@ export module Mock {
'type': 'list',
'subtype': 'string',
'description': 'external ip addresses that are authorized to access your Bitcoin node',
'changeWarning': 'Any IP you allow here will have RPC access to your Bitcoin node.',
'change-warning': 'Any IP you allow here will have RPC access to your Bitcoin node.',
'range': '[1,10]',
'default': [
'192.168.1.1',
],
'spec': {
'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]$))',
'patternDescription': 'must be a valid ipv4, ipv6, or domain name',
'pattern-description': 'must be a valid ipv4, ipv6, or domain name',
},
},
'rpcauth': {
@@ -1271,24 +1265,37 @@ export module Mock {
},
// actual config
config: {
testnet: undefined,
objectList: undefined,
testnet: false,
objectList: [
{
'firstName': 'Admin',
'lastName': 'User',
'age': 40,
},
{
'firstName': 'Admin2',
'lastName': 'User',
'age': 40,
},
],
unionList: undefined,
randomEnum: 'option1',
favoriteNumber: 8,
secondaryNumbers: undefined,
rpcsettings: {
laws: null,
laws: {
law1: 'The first law',
law2: 'The second law',
},
rpcpass: null,
rpcuser: '123',
rulemakers: [],
},
advanced: {
notifications: ['call'],
notifications: ['email'],
},
bitcoinNode: undefined,
port: 5959,
maxconnections: null,
rpcallowip: undefined,
rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'],
},
@@ -1312,7 +1319,6 @@ export module Mock {
},
bitcoinNode: { type: 'internal' },
port: 5959,
maxconnections: null,
rpcallowip: [],
rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'],
}

View File

@@ -78,6 +78,9 @@ export module RR {
export type DeleteNotificationReq = { id: string } // notification.delete
export type DeleteNotificationRes = null
export type DeleteAllNotificationsReq = { } // notification.delete.all
export type DeleteAllNotificationsRes = null
// wifi
export type AddWifiReq = { // wifi.add

View File

@@ -86,6 +86,8 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
abstract deleteNotification (params: RR.DeleteNotificationReq): Promise<RR.DeleteNotificationRes>
abstract deleteAllNotifications (params: RR.DeleteAllNotificationsReq): Promise<RR.DeleteAllNotificationsRes>
// wifi
abstract addWifi (params: RR.AddWifiReq): Promise<RR.AddWifiRes>

View File

@@ -102,11 +102,15 @@ export class LiveApiService extends ApiService {
// notification
async getNotificationsRaw (params: RR.GetNotificationsReq): Promise<RR.GetNotificationsRes> {
return this.http.rpcRequest({ method: 'notifications.list', params })
return this.http.rpcRequest({ method: 'notification.list', params })
}
async deleteNotification (params: RR.DeleteNotificationReq): Promise<RR.DeleteNotificationRes> {
return this.http.rpcRequest({ method: 'notifications.delete', params })
return this.http.rpcRequest({ method: 'notification.delete', params })
}
async deleteAllNotifications (params: RR.DeleteAllNotificationsReq): Promise<RR.DeleteAllNotificationsRes> {
return this.http.rpcRequest({ method: 'notification.delete.all', params })
}
// wifi

View File

@@ -35,6 +35,7 @@ export class MockApiService extends ApiService {
}
async setDbValueRaw (params: RR.SetDBValueReq): Promise<RR.SetDBValueRes> {
await pauseFor(2000)
return this.http.rpcRequest<WithRevision<null>>({ method: 'db.put.ui', params })
}
@@ -202,6 +203,11 @@ export class MockApiService extends ApiService {
return null
}
async deleteAllNotifications (params: RR.DeleteAllNotificationsReq): Promise<RR.DeleteAllNotificationsRes> {
await pauseFor(2000)
return null
}
// wifi
async addWifi (params: RR.AddWifiReq): Promise<RR.AddWifiRes> {
@@ -484,7 +490,30 @@ export class MockApiService extends ApiService {
async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise<RR.DryConfigureDependencyRes> {
await pauseFor(2000)
return { }
return {
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'],
}
}
private async updateProgress (id: string, initialProgress: InstallProgress) {
@@ -495,9 +524,7 @@ export class MockApiService extends ApiService {
]
for (let phase of phases) {
let i = initialProgress[phase.progress]
console.log('Initial i', i)
while (i < initialProgress.size) {
console.log(i)
await pauseFor(1000)
i = Math.min(i + 5, initialProgress.size)
initialProgress[phase.progress] = i

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'
import { InterfaceDef, Manifest, PackageDataEntry, PackageMainStatus, PackageState } from './patch-db/data-model'
import { InterfaceDef, PackageDataEntry, PackageMainStatus, PackageState } from './patch-db/data-model'
const { start9Marketplace, patchDb, api, mocks } = require('../../../config.json') as UiConfig
@@ -67,7 +67,6 @@ export class ConfigService {
}
launchableURL (pkg: PackageDataEntry): string {
console.log('PKGPKGPKG', pkg)
return this.isTor() ? `http://${torUiAddress(pkg)}` : `https://${lanUiAddress(pkg)}`
}
}

View File

@@ -0,0 +1,244 @@
import { Injectable } from '@angular/core'
import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'
import { ConfigSpec, isValueSpecListOf, ListValueSpecNumber, ListValueSpecString, ListValueSpecUnion, ValueSpec, ValueSpecEnum, ValueSpecList, ValueSpecNumber, ValueSpecObject, ValueSpecString, ValueSpecUnion } from '../pkg-config/config-types'
import { getDefaultString, Range } from '../pkg-config/config-utilities'
@Injectable({
providedIn: 'root',
})
export class FormService {
validationMessages: { [key: string]: { type: string, message: string }[] } = { }
constructor (
private readonly formBuilder: FormBuilder,
) { }
createForm (config: ConfigSpec, current: { [key: string]: any } = { }): FormGroup {
return this.getFormGroup(config, [], current)
}
getListItemValidators (spec: ValueSpecList, key: string, index: number) {
const listKey = `${key}/${index}`
this.validationMessages[listKey] = []
if (isValueSpecListOf(spec, 'string')) {
return this.stringValidators(listKey, spec.spec)
} else if (isValueSpecListOf(spec, 'number')) {
return this.numberValidators(listKey, spec.spec)
}
}
getUnionObject (spec: ValueSpecUnion | ListValueSpecUnion, selection: string, current?: { [key: string]: any }): FormGroup {
const { variants, tag } = spec
const { name, description, 'change-warning' : changeWarning } = isFullUnion(spec) ? spec : { ...spec.tag, 'change-warning': undefined }
const enumSpec: ValueSpecEnum = {
type: 'enum',
name,
description,
'change-warning': changeWarning,
default: selection,
values: Object.keys(variants),
'value-names': tag['variant-names'],
}
return this.getFormGroup({ [spec.tag.id]: enumSpec, ...spec.variants[selection] }, [], current)
}
getFormGroup (config: ConfigSpec, validators: ValidatorFn[] = [], current: { [key: string]: any } = { }): FormGroup {
let group = { }
Object.entries(config).map(([key, spec]) => {
if (spec.type === 'pointer') return
group[key] = this.getFormEntry(key, spec, current ? current[key] : { })
})
return this.formBuilder.group(group, { validators } )
}
getListItem (key: string, index: number, spec: ValueSpecList, entry: any) {
const listItemValidators = this.getListItemValidators(spec, key, index)
if (isValueSpecListOf(spec, 'string')) {
return this.formBuilder.control(entry, listItemValidators)
} else if (isValueSpecListOf(spec, 'number')) {
return this.formBuilder.control(entry, listItemValidators)
} else if (isValueSpecListOf(spec, 'enum')) {
return this.formBuilder.control(entry)
} else if (isValueSpecListOf(spec, 'object')) {
return this.getFormGroup(spec.spec.spec, listItemValidators, entry)
} else if (isValueSpecListOf(spec, 'union')) {
return this.getUnionObject(spec.spec, spec.spec.default, entry)
}
}
private getFormEntry (key: string, spec: ValueSpec, currentValue: any): FormGroup | FormArray | FormControl {
this.validationMessages[key] = []
let validators: ValidatorFn[]
let value: any
switch (spec.type) {
case 'string':
validators = this.stringValidators(key, spec)
if (currentValue !== undefined) {
value = currentValue
} else {
value = spec.default ? getDefaultString(spec.default) : null
}
return this.formBuilder.control(value, validators)
case 'number':
validators = this.numberValidators(key, spec)
if (currentValue !== undefined) {
value = currentValue
} else {
value = spec.default || null
}
return this.formBuilder.control(value, validators)
case 'object':
return this.getFormGroup(spec.spec, [], currentValue)
case 'list':
validators = this.listValidators(key, spec)
const mapped = (Array.isArray(currentValue) ? currentValue : spec.default as any[]).map((entry: any, index) => {
return this.getListItem(key, index, spec, entry)
})
return this.formBuilder.array(mapped, validators)
case 'union':
return this.getUnionObject(spec, currentValue?.[spec.tag.id] || spec.default, currentValue)
case 'boolean':
case 'enum':
value = currentValue === undefined ? spec.default : currentValue
return this.formBuilder.control(value)
}
}
private stringValidators (key: string, spec: ValueSpecString | ListValueSpecString): ValidatorFn[] {
const validators: ValidatorFn[] = []
if (!(spec as ValueSpecString).nullable) {
validators.push(Validators.required)
this.validationMessages[key].push({
type: 'required',
message: 'Cannot be blank.',
})
}
if (spec.pattern) {
validators.push(Validators.pattern(spec.pattern))
this.validationMessages[key].push({
type: 'pattern',
message: spec['pattern-description'],
})
}
return validators
}
private numberValidators (key: string, spec: ValueSpecNumber | ListValueSpecNumber): ValidatorFn[] {
const validators: ValidatorFn[] = []
if (!(spec as ValueSpecNumber).nullable) {
validators.push(Validators.required)
this.validationMessages[key].push({
type: 'required',
message: 'Cannot be blank.',
})
}
if (spec.integral) {
validators.push(isInteger())
this.validationMessages[key].push({
type: 'numberNotInteger',
message: 'Number must be an integer.',
})
}
validators.push(numberInRange(spec.range))
this.validationMessages[key].push({
type: 'numberNotInRange',
message: 'Number not in range.',
})
return validators
}
private listValidators (key: string, spec: ValueSpecList): ValidatorFn[] {
const validators: ValidatorFn[] = []
validators.push(listInRange(spec.range))
this.validationMessages[key].push({
type: 'listNotInRange',
message: 'List not in range.',
})
if (!isValueSpecListOf(spec, 'enum')) {
validators.push(listUnique(spec))
this.validationMessages[key].push({
type: 'listNotUnique',
message: 'List contains duplicate entries.',
})
}
return validators
}
}
function isFullUnion (spec: ValueSpecUnion | ListValueSpecUnion): spec is ValueSpecUnion {
return !!(spec as ValueSpecUnion).name
}
export function numberInRange (stringRange: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
try {
Range.from(stringRange).checkIncludes(control.value)
return null
} catch (e) {
return { numberNotInRange: { value: control.value } }
}
}
}
export function isInteger (): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
return control.value == Math.trunc(control.value) ?
null :
{ numberNotInteger: { value: control.value } }
}
}
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: control.value } }
} else {
return null
}
}
}
export function listUnique (spec: ValueSpec): 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 (equals(spec, control.value[idx], control.value[idx2])) {
return { listNotUnique: { value: control.value } }
} else {
return null
}
}
}
}
}
export function equals (spec: ValueSpec, val1: any, val2: any): boolean {
switch (spec.type) {
case 'string':
case 'number':
case 'boolean':
case 'enum':
return val1 === val2
case 'object':
case 'union':
// @TODO how to check this
return false
default:
return false
}
}

View File

@@ -348,7 +348,7 @@ export interface DependencyEntry {
optional: string | null
recommended: boolean
description: string | null
config: ConfigRuleEntryWithSuggestions[]
config: ConfigRuleEntryWithSuggestions[] // @TODO when do we use this?
interfaces: any[] // @TODO placeholder
}

View File

@@ -20,7 +20,7 @@ function handleInstalledState (status: Status): PkgStatusRendering {
switch (status.main.status) {
case PackageMainStatus.Stopping: return { display: 'Stopping', color: 'dark', showDots: true, feStatus: FEStatus.Stopping }
case PackageMainStatus.Stopped: return { display: 'Stopped', color: 'medium', showDots: false, feStatus: FEStatus.Stopped }
case PackageMainStatus.Stopped: return { display: 'Stopped', color: 'dark', showDots: false, feStatus: FEStatus.Stopped }
case PackageMainStatus.BackingUp: return { display: 'Backing Up', color: 'warning', showDots: true, feStatus: FEStatus.BackingUp }
case PackageMainStatus.Restoring: return { display: 'Restoring', color: 'primary', showDots: true, feStatus: FEStatus.Restoring }
case PackageMainStatus.Running: return handleRunningState(status.main)

View File

@@ -1,10 +1,12 @@
import { Injectable } from '@angular/core'
import { AppConfigValuePage } from '../modals/app-config-value/app-config-value.page'
import { AlertInput, AlertButton } from '@ionic/core'
// import { AppConfigValuePage } from '../modals/app-config-value/app-config-value.page'
import { ApiService } from './api/embassy/embassy-api.service'
import { ConfigSpec } from '../pkg-config/config-types'
import { ConfigCursor } from '../pkg-config/config-cursor'
import { SSHService } from '../pages/server-routes/security-routes/ssh-keys/ssh.service'
import { TrackingModalController } from './tracking-modal-controller.service'
import { AlertController, LoadingController } from '@ionic/angular'
import { ErrorToastService } from './error-toast.service'
// import { ModalController } from '@ionic/angular'
@Injectable({
providedIn: 'root',
@@ -12,41 +14,110 @@ import { TrackingModalController } from './tracking-modal-controller.service'
export class ServerConfigService {
constructor (
private readonly trackingModalCtrl: TrackingModalController,
// private readonly modalCtrl: ModalController,
private readonly loadingCtrl: LoadingController,
private readonly errToast: ErrorToastService,
private readonly alertCtrl: AlertController,
private readonly embassyApi: ApiService,
private readonly sshService: SSHService,
) { }
async presentModalValueEdit (key: string, current?: string) {
const cursor = new ConfigCursor(serverConfig, { [key]: current }).seekNext(key)
async presentAlert (key: string, current?: any): Promise<void> {
const spec = serverConfig[key]
const modal = await this.trackingModalCtrl.create({
backdropDismiss: false,
component: AppConfigValuePage,
presentingElement: await this.trackingModalCtrl.getTop(),
componentProps: {
cursor,
saveFn: this.saveFns[key],
let inputs: AlertInput[]
let buttons: AlertButton[] = [
{
text: 'Cancel',
role: 'cancel',
},
})
{
text: 'Save',
handler: async (data: any) => {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Saving...',
cssClass: 'loader',
})
loader.present()
await modal.present()
try {
await this.saveFns[key](data)
} catch (e) {
this.errToast.present(e.message)
} finally {
loader.dismiss()
}
},
},
]
switch (spec.type) {
case 'boolean':
inputs = [
{
name: 'enabled',
type: 'radio',
label: 'Enabled',
value: true,
checked: current,
},
{
name: 'disabled',
type: 'radio',
label: 'Disabled',
value: false,
checked: !current,
},
]
break
case 'string':
inputs = [
{
name: key,
type: 'textarea',
placeholder: 'Enter SSH public key',
value: current,
},
]
break
}
const alert = await this.alertCtrl.create({
header: spec.name,
message: spec.description,
inputs,
buttons,
})
await alert.present()
}
// async presentModalForm (key: string, current?: string) {
// const modal = await this.modalCtrl.create({
// component: AppConfigValuePage,
// componentProps: {
// cursor,
// saveFn: this.saveFns[key],
// },
// })
// await modal.present()
// }
saveFns: { [key: string]: (val: any) => Promise<any> } = {
autoCheckUpdates: async (enabled: boolean) => {
'auto-check-updates': async (enabled: boolean) => {
console.log('SAVING auto check', enabled)
return this.embassyApi.setDbValue({ pointer: '/auto-check-updates', value: enabled })
},
ssh: async (pubkey: string) => {
return this.sshService.add(pubkey)
},
eosMarketplace: async (enabled: boolean) => {
'eos-marketplace': async (enabled: boolean) => {
return this.embassyApi.setEosMarketplace(enabled)
},
// packageMarketplace: async (url: string) => {
// 'package-marketplace': async (url: string) => {
// return this.embassyApi.setPackageMarketplace({ url })
// },
shareStats: async (enabled: boolean) => {
'share-stats': async (enabled: boolean) => {
return this.embassyApi.setShareStats({ value: enabled })
},
// password: async (password: string) => {
@@ -55,8 +126,8 @@ export class ServerConfigService {
}
}
const serverConfig: ConfigSpec = {
autoCheckUpdates: {
export const serverConfig: ConfigSpec = {
'auto-check-updates': {
type: 'boolean',
name: 'Auto Check for Updates',
description: 'On launch, EmbassyOS will automatically check for updates of itself and your installed services. Updating still requires user approval and action. No updates will ever be performed automatically.',
@@ -65,33 +136,33 @@ const serverConfig: ConfigSpec = {
ssh: {
type: 'string',
name: 'SSH Key',
description: 'Add SSH keys to your Embassy to gain root access from the command line.',
description: 'Enter an SSH public key to authorize root access from the command line.',
nullable: false,
// @TODO regex for SSH Key
// pattern: '',
patternDescription: 'Must be a valid SSH key',
'pattern-description': 'Must be a valid SSH key',
masked: false,
copyable: false,
},
eosMarketplace: {
'eos-marketplace': {
type: 'boolean',
name: 'Tor Only Marketplace',
description: `Use Start9's Tor (instead of clearnet) Marketplace.`,
changeWarning: 'This will result in higher latency and slower download times.',
'change-warning': 'This will result in higher latency and slower download times.',
default: false,
},
// packageMarketplace: {
// 'package-marketplace': {
// type: 'string',
// name: 'Package Marketplace',
// description: `Use for alternative embassy marketplace. Leave empty to use start9's marketplace.`,
// nullable: true,
// // @TODO regex for URL
// // pattern: '',
// patternDescription: 'Must be a valid URL.',
// 'pattern-description': 'Must be a valid URL.',
// masked: false,
// copyable: false,
// },
shareStats: {
'share-stats': {
type: 'boolean',
name: 'Share Anonymous Statistics',
description: 'Start9 uses this information to identify bugs quickly and improve EmbassyOS. The information is 100% anonymous and transmitted over Tor.',
@@ -104,8 +175,8 @@ const serverConfig: ConfigSpec = {
// nullable: false,
// // @TODO regex for 12 chars
// // pattern: '',
// patternDescription: 'Must contain at least 12 characters.',
// changeWarning: 'If you forget your master password, there is absolutely no way to recover your data. This can result in loss of money! Keep in mind, old backups will still be encrypted by the password used to encrypt them.',
// 'pattern-description': 'Must contain at least 12 characters.',
// 'change-warning': 'If you forget your master password, there is absolutely no way to recover your data. This can result in loss of money! Keep in mind, old backups will still be encrypted by the password used to encrypt them.',
// masked: false,
// copyable: false,
// },

View File

@@ -9,7 +9,7 @@ import { ConfigService } from './config.service'
import { Emver } from './emver.service'
import { MarketplaceService } from '../pages/marketplace-routes/marketplace.service'
import { MarketplaceApiService } from './api/marketplace/marketplace-api.service'
import { DataModel, PackageDataEntry } from './patch-db/data-model'
import { DataModel } from './patch-db/data-model'
import { PatchDbService } from './patch-db/patch-db.service'
import { filter, take } from 'rxjs/operators'
import { isEmptyObject } from '../util/misc.util'
@@ -78,7 +78,6 @@ export class StartupAlertsService {
let checkRes: any
try {
checkRes = await c.check()
console.log('CHECK RES', checkRes)
} catch (e) {
console.error(`Exception in ${c.name} check:`, e)
return true

View File

@@ -0,0 +1,33 @@
import { Inject, Injectable, InjectionToken } from '@angular/core'
import { IonNav } from '@ionic/angular'
import { AppConfigComponentMapping } from '../modals/app-config-injectable'
import { ConfigCursor } from '../pkg-config/config-cursor'
export const APP_CONFIG_COMPONENT_MAPPING = new InjectionToken<string>('APP_CONFIG_COMPONENTS')
@Injectable({
providedIn: 'root',
})
export class SubNavService {
path: string[]
constructor (
@Inject(APP_CONFIG_COMPONENT_MAPPING) private readonly appConfigComponentMapping: AppConfigComponentMapping,
) { }
async push (key: string, cursor: ConfigCursor<any>, nav: IonNav) {
const component = this.appConfigComponentMapping[cursor.spec().type]
this.path.push(key)
nav.push(component, { cursor }, { mode: 'ios' })
}
async pop (nav: IonNav) {
this.path.pop()
nav.pop({ mode: 'ios' })
}
async popTo (index: number, nav: IonNav) {
this.path = this.path.slice(0, index + 1)
nav.popTo(index, { mode: 'ios' })
}
}

View File

@@ -1,70 +0,0 @@
import { Inject, Injectable, InjectionToken } from '@angular/core'
import { Observable, Subject } from 'rxjs'
import { ModalController } from '@ionic/angular'
import { ModalOptions } from '@ionic/core'
import { ValueSpec } from '../pkg-config/config-types'
import { AppConfigComponentMapping } from '../modals/app-config-injectable'
export const APP_CONFIG_COMPONENT_MAPPING = new InjectionToken<string>('APP_CONFIG_COMPONENTS')
@Injectable({
providedIn: 'root',
})
export class TrackingModalController {
private modals: { [modalId: string] : HTMLIonModalElement} = { }
private readonly $onDismiss$ = new Subject<string>()
private readonly $onCreate$ = new Subject<string>()
constructor (
private readonly modalCtrl: ModalController,
@Inject(APP_CONFIG_COMPONENT_MAPPING) private readonly appConfigComponentMapping: AppConfigComponentMapping,
) { }
async createConfigModal (o: Omit<ModalOptions, 'component'>, type: ValueSpec['type']) {
const component = this.appConfigComponentMapping[type]
return this.create({ ...o, component })
}
async create (a: ModalOptions): Promise<HTMLIonModalElement> {
const modal = await this.modalCtrl.create(a)
this.modals[modal.id] = modal
this.$onCreate$.next(modal.id)
modal.onWillDismiss().then(() => {
delete this.modals[modal.id]
this.$onDismiss$.next(modal.id)
})
return modal
}
dismissAll (): Promise<boolean[]> {
return Promise.all(
Object.values(this.modals).map(m => m.dismiss()),
)
}
dismiss (val?: any): Promise<boolean> {
return this.modalCtrl.dismiss(val)
}
onCreateAny$ (): Observable<string> {
return this.$onCreate$.asObservable()
}
onDismissAny$ (): Observable<string> {
return this.$onDismiss$.asObservable()
}
async getTop (): Promise<HTMLIonModalElement> {
return this.modalCtrl.getTop()
}
get anyModals (): boolean {
return Object.keys(this.modals).length !== 0
}
get modalCount (): number {
return Object.keys(this.modals).length
}
}