* 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

@@ -70,7 +70,6 @@
<ion-icon name="chevron-up"></ion-icon> <ion-icon name="chevron-up"></ion-icon>
<ion-icon name="chevron-forward"></ion-icon> <!-- needed for detail="true" on ion-item button --> <ion-icon name="chevron-forward"></ion-icon> <!-- needed for detail="true" on ion-item button -->
<ion-icon name="close"></ion-icon> <ion-icon name="close"></ion-icon>
<ion-icon name="close-outline"></ion-icon>
<ion-icon name="code-outline"></ion-icon> <ion-icon name="code-outline"></ion-icon>
<ion-icon name="color-wand-outline"></ion-icon> <ion-icon name="color-wand-outline"></ion-icon>
<ion-icon name="construct-outline"></ion-icon> <ion-icon name="construct-outline"></ion-icon>
@@ -93,7 +92,6 @@
<ion-icon name="medkit-outline"></ion-icon> <ion-icon name="medkit-outline"></ion-icon>
<ion-icon name="newspaper-outline"></ion-icon> <ion-icon name="newspaper-outline"></ion-icon>
<ion-icon name="notifications-outline"></ion-icon> <ion-icon name="notifications-outline"></ion-icon>
<ion-icon name="rocket-outline"></ion-icon>
<ion-icon name="phone-portrait-outline"></ion-icon> <ion-icon name="phone-portrait-outline"></ion-icon>
<ion-icon name="play-circle-outline"></ion-icon> <ion-icon name="play-circle-outline"></ion-icon>
<ion-icon name="power"></ion-icon> <ion-icon name="power"></ion-icon>

View File

@@ -214,7 +214,6 @@ export class AppComponent {
takeWhile(() => auth === AuthState.VERIFIED), takeWhile(() => auth === AuthState.VERIFIED),
) )
.subscribe(version => { .subscribe(version => {
console.log('VERSIONS', this.config.version, version)
if (this.emver.compare(this.config.version, version) !== 0) { if (this.emver.compare(this.config.version, version) !== 0) {
this.presentAlertRefreshNeeded() this.presentAlertRefreshNeeded()
} }

View File

@@ -1,7 +1,7 @@
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core' import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser' import { BrowserModule } from '@angular/platform-browser'
import { RouteReuseStrategy } from '@angular/router' import { RouteReuseStrategy } from '@angular/router'
import { IonicModule, IonicRouteStrategy } from '@ionic/angular' import { IonicModule, IonicRouteStrategy, IonNav } from '@ionic/angular'
import { Drivers } from '@ionic/storage' import { Drivers } from '@ionic/storage'
import { IonicStorageModule } from '@ionic/storage-angular' import { IonicStorageModule } from '@ionic/storage-angular'
import { HttpClientModule } from '@angular/common/http' import { HttpClientModule } from '@angular/common/http'
@@ -19,8 +19,10 @@ import { MarkdownPageModule } from './modals/markdown/markdown.module'
import { PatchDbService } from './services/patch-db/patch-db.service' import { PatchDbService } from './services/patch-db/patch-db.service'
import { LocalStorageBootstrap } from './services/patch-db/local-storage-bootstrap' import { LocalStorageBootstrap } from './services/patch-db/local-storage-bootstrap'
import { SharingModule } from './modules/sharing.module' import { SharingModule } from './modules/sharing.module'
import { APP_CONFIG_COMPONENT_MAPPING } from './services/tracking-modal-controller.service'
import { MarketplaceApiService } from './services/api/marketplace/marketplace-api.service' import { MarketplaceApiService } from './services/api/marketplace/marketplace-api.service'
import { APP_CONFIG_COMPONENT_MAPPING } from './services/sub-nav.service'
import { FormBuilder } from '@angular/forms'
import { FormService } from './services/form.service'
@NgModule({ @NgModule({
declarations: [AppComponent], declarations: [AppComponent],
@@ -42,6 +44,8 @@ import { MarketplaceApiService } from './services/api/marketplace/marketplace-ap
SharingModule, SharingModule,
], ],
providers: [ providers: [
FormBuilder,
IonNav,
Storage, Storage,
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{ provide: ApiService , useFactory: ApiServiceFactory, deps: [ConfigService, HttpService] }, { provide: ApiService , useFactory: ApiServiceFactory, deps: [ConfigService, HttpService] }, { provide: ApiService , useFactory: ApiServiceFactory, deps: [ConfigService, HttpService] }, { provide: ApiService , useFactory: ApiServiceFactory, deps: [ConfigService, HttpService] },

View File

@@ -7,7 +7,7 @@
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text> <ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item-divider *ngIf="spec.description || spec.changeWarning"></ion-item-divider> <ion-item-divider *ngIf="spec.description || spec['change-warning']"></ion-item-divider>
</ng-container> </ng-container>
<!-- description --> <!-- description -->
<ion-item *ngIf="spec.description"> <ion-item *ngIf="spec.description">
@@ -19,12 +19,12 @@
</ion-label> </ion-label>
</ion-item> </ion-item>
<!-- warning --> <!-- warning -->
<ion-item *ngIf="spec.changeWarning"> <ion-item *ngIf="spec['change-warning']">
<ion-label class="ion-text-wrap"> <ion-label class="ion-text-wrap">
<p> <p>
<ion-text color="warning">Warning!</ion-text> <ion-text color="warning">Warning!</ion-text>
</p> </p>
<p [innerHTML]="spec.changeWarning | markdown"></p> <p [innerHTML]="spec['change-warning'] | markdown"></p>
</ion-label> </ion-label>
</ion-item> </ion-item>
</ion-item-group> </ion-item-group>

View File

@@ -0,0 +1,9 @@
<ion-icon *ngIf="data.spec.description" class="help-icon" name="help-circle-outline" (click)="presentAlertDescription()"></ion-icon>
<span>&nbsp;{{ 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>
<span *ngIf="(['string', 'number'] | includes : data.spec.type) && !data.spec.nullable">&nbsp;*</span>
<span *ngIf="data.spec.type === 'list' && Range.from(data.spec.range).min">&nbsp;*</span>

View File

@@ -0,0 +1,193 @@
<ion-item-group [formGroup]="formGroup">
<div *ngFor="let entry of formGroup.controls | keyvalue : asIsOrder">
<ng-container *ngIf="unionSpec && entry.key === unionSpec.tag.id">
<p class="input-label">{{ unionSpec.tag.name }}</p>
<ion-item color="dark">
<ion-label>{{ unionSpec.tag.name }}</ion-label>
<ion-select slot="end" placeholder="Select" [formControlName]="unionSpec.tag.id" [selectedText]="unionSpec.tag['variant-names'][entry.value.value]" (ionChange)="presentAlertChangeWarning(entry.key, unionSpec); updateUnion($event)">
<ion-select-option *ngFor="let option of Object.keys(unionSpec.variants)" [value]="option">
{{ unionSpec.tag['variant-names'][option] }}
</ion-select-option>
</ion-select>
</ion-item>
</ng-container>
<ng-container *ngIf="objectSpec[entry.key] as spec">
<!-- primitive -->
<ng-container *ngIf="['string', 'number', 'boolean', 'enum'] | includes : spec.type">
<!-- label -->
<p class="input-label">
<form-label [data]="{
spec: spec,
isNew: current && current[entry.key] === undefined,
isEdited: entry.value.dirty
}"></form-label>
</p>
<!-- string -->
<ion-item color="dark" *ngIf="spec.type === 'string'">
<ion-input [type]="spec.masked && !unmasked[entry.key] ? 'password' : 'text'" [placeholder]="'Enter ' + spec.name" [formControlName]="entry.key" (ionChange)="presentAlertChangeWarning(entry.key, spec)"></ion-input>
<ion-button *ngIf="spec.masked" fill="clear" color="light" (click)="unmasked[entry.key] = !unmasked[entry.key]">
<ion-icon slot="icon-only" [name]="unmasked[entry.key] ? 'eye-off-outline' : 'eye-outline'" size="small"></ion-icon>
</ion-button>
</ion-item>
<!-- number -->
<ion-item color="dark" *ngIf="spec.type === 'number'">
<ion-input type="tel" [placeholder]="'Enter ' + spec.name" [formControlName]="entry.key" (ionChange)="presentAlertChangeWarning(entry.key, spec)"></ion-input>
<ion-note *ngIf="spec.units" slot="end" color="light" style="font-size: medium;">{{ spec.units }}</ion-note>
</ion-item>
<!-- boolean -->
<ion-item color="dark" *ngIf="spec.type === 'boolean'">
<ion-label>{{ spec.name }}</ion-label>
<ion-toggle style="--background: var(--ion-color-step-600);" slot="end" color="light" [formControlName]="entry.key" (ionChange)="presentAlertChangeWarning(entry.key, spec)"></ion-toggle>
</ion-item>
<!-- enum -->
<ion-item color="dark" *ngIf="spec.type === 'enum'">
<ion-label>{{ spec.name }}</ion-label>
<ion-select slot="end" placeholder="Select" [formControlName]="entry.key" [selectedText]="spec['value-names'][formGroup.get(entry.key).value]" (ionChange)="presentAlertChangeWarning(entry.key, spec)">
<ion-select-option *ngFor="let option of spec.values" [value]="option">
{{ spec['value-names'][option] }}
</ion-select-option>
</ion-select>
</ion-item>
</ng-container>
<!-- object or union -->
<ng-container *ngIf="spec.type === 'object' || spec.type ==='union'">
<!-- label -->
<ion-item-divider>
<form-label [data]="{
spec: spec,
isNew: current && current[entry.key] === undefined,
isEdited: entry.value.dirty
}"></form-label>
</ion-item-divider>
<!-- body -->
<div class="nested-wrapper">
<form-object
[objectSpec]="
spec.type === 'union' ?
spec.variants[entry.value.controls[spec.tag.id].value] :
spec.spec"
[formGroup]="entry.value"
[current]="current ? current[key] : undefined"
[unionSpec]="spec.type === 'union' ? spec : undefined"
></form-object>
</div>
</ng-container>
<!-- list (not enum) -->
<ng-container *ngIf="spec.type === 'list' && spec.subtype !== 'enum'">
<ng-container *ngIf="formGroup.get(entry.key) as formArr" [formArrayName]="entry.key">
<!-- label -->
<ion-item-divider>
<form-label [data]="{
spec: spec,
isNew: current && current[entry.key] === undefined,
isEdited: entry.value.dirty
}"></form-label>
<ion-button fill="clear" color="primary" slot="end" (click)="addListItem(entry.key)">
<ion-icon slot="start" name="add"></ion-icon>
Add
</ion-button>
</ion-item-divider>
<!-- body -->
<div class="nested-wrapper">
<div
*ngFor="let abstractControl of formArr.controls; let i = index;"
class="ion-padding-top"
>
<!-- nested -->
<ng-container *ngIf="spec.subtype === 'object' || spec.subtype === 'union'">
<!-- nested label -->
<ion-item-divider (click)="toggleExpand(entry.key, i)" style="cursor: pointer;">
<form-label [data]="{
spec: {
name: objectListInfo[entry.key][i].displayAs || 'Entry ' + (i + 1)
},
isNew: false,
isEdited: abstractControl.dirty
}"></form-label>
<ion-icon
slot="end"
name="chevron-up"
[ngStyle]="{
'transform': objectListInfo[entry.key][i].expanded ? 'rotate(0deg)' : 'rotate(180deg)',
'transition': 'transform 0.4s ease-out'
}"
></ion-icon>
</ion-item-divider>
<!-- nested body -->
<div
[id]="entry.key"
[ngStyle]="{
'max-height': objectListInfo[entry.key][i].height,
'overflow': 'hidden',
'transition-property': 'max-height',
'transition-duration': '.5s',
'transition-delay': '.05s'
}"
>
<!-- [hidden]="objectListInfo[entry.key][i].expanded ? false : true" -->
<form-object
[objectSpec]="
spec.subtype === 'union' ?
spec.spec.variants[abstractControl.controls[spec.spec.tag.id].value] :
spec.spec.spec"
[formGroup]="abstractControl"
[current]="current && current[entry.key] ? current[entry.key][i] : undefined"
[unionSpec]="spec.subtype === 'union' ? spec.spec : undefined"
></form-object>
<div style="text-align: right; padding-top: 12px;">
<ion-button fill="clear" (click)="presentAlertDelete(entry.key, i)" color="danger">
<ion-icon slot="start" name="close"></ion-icon>
Delete
</ion-button>
</div>
</div>
</ng-container>
<!-- string or number -->
<ion-item-group *ngIf="spec.subtype === 'string' || spec.subtype === 'number'">
<ion-item color="dark">
<ion-input type="spec.spec.masked ? 'password' : 'text'" [placeholder]="'Enter ' + spec.name" [formControlName]="i"></ion-input>
<ion-button slot="end" color="danger" (click)="presentAlertDelete(entry.key, i)">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-item>
<ng-container *ngFor="let validation of formService.validationMessages[entry.key + '/' + i]">
<p style="font-size: small;" *ngIf="abstractControl.hasError(validation.type) && (abstractControl.dirty || abstractControl.touched)">
<ion-text color="danger">{{ validation.message }}</ion-text>
</p>
</ng-container>
</ion-item-group>
</div>
</div>
</ng-container>
</ng-container>
<!-- list (enum) -->
<ng-container *ngIf="spec.type === 'list' && spec.subtype === 'enum'">
<ng-container *ngIf="formGroup.get(entry.key) as formArr" [formArrayName]="entry.key">
<!-- label -->
<p class="input-label">
<form-label [data]="{
spec: spec,
isNew: current && current[entry.key] === undefined,
isEdited: entry.value.dirty
}"></form-label>
</p>
<!-- list -->
<ion-item button detail="false" color="dark" (click)="presentModalEnumList(entry.key, spec, formArr.value)">
<ion-label>
<h2>{{ getEnumListDisplay(formArr.value, spec.spec) }}</h2>
</ion-label>
<ion-button slot="end" fill="clear" color="light">
<ion-icon slot="icon-only" name="chevron-down"></ion-icon>
</ion-button>
</ion-item>
</ng-container>
</ng-container>
<div *ngFor="let validation of formService.validationMessages[entry.key]">
<p class="validation-error" *ngIf="formGroup.get(entry.key).hasError(validation.type)">
<ion-text *ngIf="(formGroup.get(entry.key).dirty || formGroup.get(entry.key).touched)" color="danger">{{ spec.name }}: {{ validation.message }}</ion-text>
</p>
</div>
</ng-container>
</div>
</ion-item-group>

View File

@@ -0,0 +1,27 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormObjectComponent, FormLabelComponent } from './form-object.component'
import { IonicModule } from '@ionic/angular'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { SharingModule } from 'src/app/modules/sharing.module'
import { EnumListPageModule } from 'src/app/modals/enum-list/enum-list.module'
@NgModule({
declarations: [
FormObjectComponent,
FormLabelComponent,
],
imports: [
CommonModule,
IonicModule,
FormsModule,
ReactiveFormsModule,
SharingModule,
EnumListPageModule,
],
exports: [
FormObjectComponent,
FormLabelComponent,
],
})
export class FormObjectComponentModule { }

View File

@@ -0,0 +1,41 @@
.help-icon {
display: inline-block;
vertical-align: middle;
padding-bottom: 2px;
font-size: 18px;
color: var(--ion-color-dark);
cursor: pointer;
}
ion-input {
font-weight: 500;
--placeholder-font-weight: 400;
}
ion-item-divider {
text-transform: unset;
--padding-start: 0;
border-bottom: 1px solid var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13))))
}
.input-label {
// padding-top: 10px;
margin-bottom: 5px;
font-size: medium;
font-weight: bold;
* {
display: inline-block;
vertical-align: middle;
}
}
.nested-wrapper {
padding: 0 0 30px 30px;
// border-bottom: 1px solid var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13))))
}
.validation-error {
p {
font-size: small;
}
}

View File

@@ -0,0 +1,223 @@
import { Component, Input, SimpleChange } from '@angular/core'
import { FormArray, FormGroup } from '@angular/forms'
import { AlertController, ModalController } from '@ionic/angular'
import { ConfigSpec, ListValueSpecOf, ValueSpec, 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'
import * as handlebars from 'handlebars'
import { pauseFor } from 'src/app/util/misc.util'
@Component({
selector: 'form-object',
templateUrl: './form-object.component.html',
styleUrls: ['./form-object.component.scss'],
})
export class FormObjectComponent {
@Input() objectSpec: ConfigSpec
@Input() formGroup: FormGroup
@Input() unionSpec: ValueSpecUnion
@Input() current: { [key: string]: any }
@Input() showEdited: boolean = false
warningAck: { [key: string]: boolean } = { }
unmasked: { [key: string]: boolean } = { }
// @TODO for when we want to expand/collapse normal objects/union in addition to list ones
// objectExpanded: { [key: string]: boolean } = { }
objectListInfo: { [key: string]: { expanded: boolean, height: string, displayAs: string }[] } = { }
Object = Object
constructor (
private readonly alertCtrl: AlertController,
private readonly modalCtrl: ModalController,
private readonly formService: FormService,
) { }
ngOnChanges (changes: { [propName: string]: SimpleChange }) {
// @TODO figure out why changes are being triggered so often. If too heavy, switch to ngOnInit and figure out another way to manually reset defaults is executed. Needed because otherwise ObjectListInfo won't be accurate.
// if ( changes['current'] && changes['current'].previousValue != changes['current'].currentValue ) {
// console.log('CURRENT')
// }
// if ( changes['formGroup'] && changes['formGroup'].previousValue != changes['formGroup'].currentValue ) {
// console.log('FORM GROUP')
// }
// if ( changes['objectSpec'] && changes['objectSpec'].previousValue != changes['objectSpec'].currentValue ) {
// console.log('OBJECT SPEC')
// }
// Lists are automatically expanded, but their members are not
Object.keys(this.objectSpec).forEach(key => {
const spec = this.objectSpec[key]
if (spec.type === 'list' && ['object', 'union'].includes(spec.subtype)) {
this.objectListInfo[key] = [];
(this.formGroup.get(key).value as any[]).forEach((obj, index) => {
const displayAs = (spec.spec as ListValueSpecOf<'object'>)['display-as']
this.objectListInfo[key][index] = {
expanded: false,
height: '0px',
displayAs: displayAs ? handlebars.compile(displayAs)(obj) : '',
}
})
}
})
}
getEnumListDisplay (arr: string[], spec: ListValueSpecOf<'enum'>): string {
return arr.map((v: string) => spec['value-names'][v]).join(', ')
}
updateUnion (e: any): void {
Object.keys(this.formGroup.controls).forEach(control => {
if (control === 'type') return
this.formGroup.removeControl(control)
})
const unionGroup = this.formService.getUnionObject(this.unionSpec as ValueSpecUnion, e.detail.value)
Object.keys(unionGroup.controls).forEach(control => {
if (control === 'type') return
this.formGroup.addControl(control, unionGroup.controls[control])
})
}
addListItem (key: string, markDirty = true, val?: string): void {
const arr = this.formGroup.get(key) as FormArray
if (markDirty) arr.markAsDirty()
// 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
const newItem = this.formService.getListItem(key, arr.length, listSpec, val)
newItem.markAllAsTouched()
arr.insert(0, newItem)
if (['object', 'union'].includes(listSpec.subtype)) {
const displayAs = (listSpec.spec as ListValueSpecOf<'object'>)['display-as']
this.objectListInfo[key].push({
height: '0px',
expanded: true,
displayAs: displayAs ? handlebars.compile(displayAs)(newItem.value) : '',
})
}
pauseFor(200).then(() => {
const index = this.objectListInfo[key].length - 1
this.objectListInfo[key][index].height = this.getDocSize(key)
})
}
toggleExpand (key: string, i: number) {
this.objectListInfo[key][i].expanded = !this.objectListInfo[key][i].expanded
this.objectListInfo[key][i].height = this.objectListInfo[key][i].expanded ? this.getDocSize(key) : '0px'
}
async presentModalEnumList (key: string, spec: ValueSpecListOf<'enum'>, current: string[]) {
const modal = await this.modalCtrl.create({
componentProps: {
key,
spec,
current,
},
component: EnumListPage,
})
modal.onWillDismiss().then(res => {
const data = res.data
if (!data) return
this.updateEnumList(key, current, data)
})
await modal.present()
}
async presentAlertChangeWarning (key: string, spec: ValueSpec) {
if (!spec['change-warning'] || this.warningAck[key]) return
this.warningAck[key] = true
const alert = await this.alertCtrl.create({
header: 'Warning',
subHeader: `Editing ${spec.name} has consequences:`,
message: spec['change-warning'],
buttons: ['Ok'],
})
await alert.present()
}
async presentAlertDelete (key: string, index: number) {
const alert = await this.alertCtrl.create({
backdropDismiss: false,
header: 'Confirm',
message: 'Are you sure you want to delete this entry?',
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Delete',
handler: () => {
this.deleteListItem(key, index)
},
},
],
})
await alert.present()
}
private deleteListItem (key: string, index: number, markDirty = true): void {
this.objectListInfo[key][index].height = '0px'
const arr = this.formGroup.get(key) as FormArray
if (markDirty) arr.markAsDirty()
pauseFor(500).then(() => {
this.objectListInfo[key].splice(index, 1)
arr.removeAt(index)
})
}
private updateEnumList (key: string, current: string[], updated: string[]) {
this.formGroup.get(key).markAsDirty()
let deleted = current.filter(x => !updated.includes(x))
deleted.forEach((_, index) => this.deleteListItem(key, index, false))
let added = updated.filter(x => !current.includes(x))
added.forEach(val => this.addListItem(key, false, val))
}
getDocSize (selected: string) {
const element = document.getElementById(selected)
return `${element.scrollHeight}px`
}
asIsOrder () {
return 0
}
}
interface HeaderData {
spec: ValueSpec
isEdited: boolean
isNew: boolean
}
@Component({
selector: 'form-label',
templateUrl: './form-label.component.html',
styleUrls: ['./form-object.component.scss'],
})
export class FormLabelComponent {
Range = Range
@Input() data: HeaderData
constructor (
private readonly alertCtrl: AlertController,
) { }
async presentAlertDescription () {
const { name, description } = this.data.spec
const alert = await this.alertCtrl.create({
header: name,
message: description,
buttons: ['Ok'],
})
await alert.present()
}
}

View File

@@ -39,11 +39,11 @@
<!-- cancel button if loading/not loading --> <!-- cancel button if loading/not loading -->
<ion-button slot="start" *ngIf="(currentSlide.loading$ | async) && currentBottomBar.cancel.whileLoading as cancel" (click)="transitions.cancel()" class="toolbar-button" fill="outline"> <ion-button slot="start" *ngIf="(currentSlide.loading$ | async) && currentBottomBar.cancel.whileLoading as cancel" (click)="transitions.cancel()" class="toolbar-button" fill="outline">
<ion-text *ngIf="cancel.text" [class.smaller-text]="cancel.text > 16">{{ cancel.text }}</ion-text> <ion-text *ngIf="cancel.text" [class.smaller-text]="cancel.text > 16">{{ cancel.text }}</ion-text>
<ion-icon *ngIf="!cancel.text" name="close-outline"></ion-icon> <ion-icon *ngIf="!cancel.text" name="close"></ion-icon>
</ion-button> </ion-button>
<ion-button slot="start" *ngIf="!(currentSlide.loading$ | async) && currentBottomBar.cancel.afterLoading as cancel" (click)="transitions.cancel()" class="toolbar-button" fill="outline"> <ion-button slot="start" *ngIf="!(currentSlide.loading$ | async) && currentBottomBar.cancel.afterLoading as cancel" (click)="transitions.cancel()" class="toolbar-button" fill="outline">
<ion-text *ngIf="cancel.text" [class.smaller-text]="cancel.text > 16">{{ cancel.text }}</ion-text> <ion-text *ngIf="cancel.text" [class.smaller-text]="cancel.text > 16">{{ cancel.text }}</ion-text>
<ion-icon *ngIf="!cancel.text" name="close-outline"></ion-icon> <ion-icon *ngIf="!cancel.text" name="close"></ion-icon>
</ion-button> </ion-button>
<!-- next/finish buttons --> <!-- next/finish buttons -->

View File

@@ -139,7 +139,7 @@ export class WizardBaker {
}, },
}, },
bottomBar: { bottomBar: {
cancel: { afterLoading: { text: 'Cancel' } }, next: 'Update OS', cancel: { afterLoading: { text: 'Cancel' } }, next: 'Begin Update',
}, },
}, },
{ {

View File

@@ -1,5 +1,6 @@
<div *ngFor="let keyval of (spec.type === 'object' ? spec.spec : spec.variants[value[spec.tag.id]]) | keyvalue: asIsOrder"> <ion-item-group>
<object-config-item <object-config-item
*ngFor="let keyval of (spec.type === 'object' ? spec.spec : spec.variants[value[spec.tag.id]]) | keyvalue: asIsOrder"
[key]="keyval.key" [key]="keyval.key"
[spec]="keyval.value" [spec]="keyval.value"
[value]="value[keyval.key]" [value]="value[keyval.key]"
@@ -7,4 +8,4 @@
(onClick)="handleClick(keyval.key)" (onClick)="handleClick(keyval.key)"
[class.add-margin]="keyval.key === 'advanced'" [class.add-margin]="keyval.key === 'advanced'"
></object-config-item> ></object-config-item>
</div> </ion-item-group>

View File

@@ -10,28 +10,11 @@
font-style: italic; font-style: italic;
} }
.status-icon{
// width: 2%;
margin-right: 12px;
}
.bright {
color: white !important;
}
.bold { .bold {
font-weight: bold; font-weight: bold;
} }
.invalid {
color: var(--ion-color-danger) !important;
}
.organizer { .organizer {
display: flex; display: flex;
align-items: center; align-items: center;
}
.name {
text-decoration: underline;
} }

View File

@@ -1,17 +1,17 @@
import { Component, EventEmitter, Input, Output } from '@angular/core' import { Component, EventEmitter, Input, Output } from '@angular/core'
import { Annotation, Annotations } from '../../pkg-config/config-utilities' import { Annotation, Annotations } from '../../pkg-config/config-utilities'
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
import { ConfigCursor } from 'src/app/pkg-config/config-cursor' import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
import { ModalPresentable } from 'src/app/pkg-config/modal-presentable'
import { ValueSpecOf, ValueSpec } from 'src/app/pkg-config/config-types' import { ValueSpecOf, ValueSpec } from 'src/app/pkg-config/config-types'
import { MaskPipe } from 'src/app/pipes/mask.pipe' import { MaskPipe } from 'src/app/pipes/mask.pipe'
import { IonNav } from '@ionic/angular'
import { SubNavService } from 'src/app/services/sub-nav.service'
@Component({ @Component({
selector: 'object-config', selector: 'object-config',
templateUrl: './object-config.component.html', templateUrl: './object-config.component.html',
styleUrls: ['./object-config.component.scss'], styleUrls: ['./object-config.component.scss'],
}) })
export class ObjectConfigComponent extends ModalPresentable { export class ObjectConfigComponent {
@Input() cursor: ConfigCursor<'object' | 'union'> @Input() cursor: ConfigCursor<'object' | 'union'>
@Output() onEdit = new EventEmitter<boolean>() @Output() onEdit = new EventEmitter<boolean>()
spec: ValueSpecOf<'object' | 'union'> spec: ValueSpecOf<'object' | 'union'>
@@ -19,10 +19,9 @@ export class ObjectConfigComponent extends ModalPresentable {
annotations: Annotations<'object' | 'union'> annotations: Annotations<'object' | 'union'>
constructor ( constructor (
trackingModalCtrl: TrackingModalController, private readonly subNav: SubNavService,
) { private readonly nav: IonNav,
super(trackingModalCtrl) ) { }
}
ngOnInit () { ngOnInit () {
this.spec = this.cursor.spec() this.spec = this.cursor.spec()
@@ -33,11 +32,7 @@ export class ObjectConfigComponent extends ModalPresentable {
async handleClick (key: string) { async handleClick (key: string) {
const nextCursor = this.cursor.seekNext(key) const nextCursor = this.cursor.seekNext(key)
nextCursor.createFirstEntryForList() nextCursor.createFirstEntryForList()
this.subNav.push(key, nextCursor, this.nav)
await this.presentModal(nextCursor, () => {
this.onEdit.emit(true)
this.annotations = this.cursor.getAnnotations()
})
} }
asIsOrder () { asIsOrder () {
@@ -50,7 +45,6 @@ export class ObjectConfigComponent extends ModalPresentable {
templateUrl: './object-config-item.component.html', templateUrl: './object-config-item.component.html',
styleUrls: ['./object-config.component.scss'], styleUrls: ['./object-config.component.scss'],
}) })
export class ObjectConfigItemComponent { export class ObjectConfigItemComponent {
@Input() key: string @Input() key: string
@Input() spec: ValueSpec @Input() spec: ValueSpec
@@ -84,7 +78,7 @@ export class ObjectConfigItemComponent {
} }
break break
case 'enum': case 'enum':
this.displayValue = this.spec.valueNames[this.value] this.displayValue = this.spec['value-names'][this.value]
break break
case 'pointer': case 'pointer':
this.displayValue = 'System Defined' this.displayValue = 'System Defined'

View File

@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'
import { PwaBackComponent } from './pwa-back.component' import { PwaBackComponent } from './pwa-back.component'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router' import { RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -13,7 +12,6 @@ import { SharingModule } from 'src/app/modules/sharing.module'
CommonModule, CommonModule,
IonicModule, IonicModule,
RouterModule.forChild([]), RouterModule.forChild([]),
SharingModule,
], ],
exports: [PwaBackComponent], exports: [PwaBackComponent],
}) })

View File

@@ -0,0 +1,18 @@
<ion-header *ngIf="subNav.path.length as length">
<ion-toolbar class="subheader" color="dark">
<ion-buttons slot="start" *ngIf="length > 1">
<ion-button color="light" (click)="subNav.pop(nav)">
<ion-icon slot="icon-only" name="arrow-back-outline"></ion-icon>
</ion-button>
</ion-buttons>
<ion-title>
<span *ngFor="let segment of subNav.path; let i = index">
/
<span *ngIf="i === length -1"> {{ segment }}</span>
<a *ngIf="i !== length -1" style="cursor: pointer;" (click)="subNav.popTo(i, nav)"> {{ segment }}</a>
</span>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-nav [root]="rootPage" [rootParams]="rootParams"></ion-nav>

View File

@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { SubNavComponent } from './sub-nav.component'
import { IonicModule } from '@ionic/angular'
@NgModule({
declarations: [
SubNavComponent,
],
imports: [
CommonModule,
IonicModule,
],
exports: [SubNavComponent],
})
export class SubNavComponentModule { }

View File

@@ -0,0 +1,23 @@
import { Component, Input, ViewChild } from '@angular/core'
import { IonNav } from '@ionic/angular'
import { SubNavService } from 'src/app/services/sub-nav.service'
@Component({
selector: 'sub-nav',
templateUrl: './sub-nav.component.html',
styleUrls: ['./sub-nav.component.scss'],
})
export class SubNavComponent {
@Input() path: string
@Input() rootPage: any
@Input() rootParams: { [key: string]: any }
@ViewChild(IonNav) nav: IonNav
constructor (
public readonly subNav: SubNavService,
) { }
ngOnInit () {
this.subNav.path = [this.path]
}
}

View File

@@ -2,16 +2,17 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { AppActionInputPage } from './app-action-input.page' import { AppActionInputPage } from './app-action-input.page'
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { ConfigHeaderComponentModule } from 'src/app/components/config-header/config-header.component.module' import { FormObjectComponentModule } from 'src/app/components/form-object/form-object.component.module'
@NgModule({ @NgModule({
declarations: [AppActionInputPage], declarations: [AppActionInputPage],
imports: [ imports: [
CommonModule, CommonModule,
IonicModule, IonicModule,
ObjectConfigComponentModule, FormsModule,
ConfigHeaderComponentModule, ReactiveFormsModule,
FormObjectComponentModule,
], ],
entryComponents: [AppActionInputPage], entryComponents: [AppActionInputPage],
exports: [AppActionInputPage], exports: [AppActionInputPage],

View File

@@ -1,27 +1,29 @@
<ion-header> <ion-header>
<ion-toolbar> <ion-toolbar>
<ion-buttons slot="start">
<ion-button (click)="dismiss()" color="light">
<ion-icon slot="icon-only" name="close-outline"></ion-icon>
</ion-button>
</ion-buttons>
<ion-title>{{ action.name }}</ion-title> <ion-title>{{ action.name }}</ion-title>
<ion-buttons slot="end">
<ion-button [disabled]="error" (click)="save()">
Save
</ion-button>
</ion-buttons>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content class="ion-padding">
<form [formGroup]="actionForm" (ngSubmit)="save()" novalidate>
<config-header [spec]="spec" [error]="error"></config-header> <form-object
[objectSpec]="action['input-spec']"
<!-- object --> [formGroup]="actionForm"
<ion-item-group> ></form-object>
<ion-item-divider></ion-item-divider> </form>
<object-config [cursor]="cursor" (onEdit)="handleObjectEdit()"></object-config>
</ion-item-group>
</ion-content> </ion-content>
<ion-footer>
<ion-toolbar>
<ion-buttons slot="start" class="ion-padding-start">
<ion-button fill="outline" (click)="dismiss()">
Cancel
</ion-button>
</ion-buttons>
<ion-buttons slot="end" class="ion-padding-end">
<ion-button fill="outline" color="primary" (click)="save()">
Execute
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-footer>

View File

@@ -0,0 +1,9 @@
button:disabled,
button[disabled]{
border: 1px solid #999999;
background-color: #cccccc;
color: #666666;
}
button {
color: var(--ion-color-primary);
}

View File

@@ -1,9 +1,8 @@
import { Component, Input } from '@angular/core' import { Component, Input } from '@angular/core'
import { LoadingController, ModalController } from '@ionic/angular' import { FormGroup } from '@angular/forms'
import { ConfigCursor } from 'src/app/pkg-config/config-cursor' import { ModalController } from '@ionic/angular'
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { Action } from 'src/app/services/patch-db/data-model' import { Action } from 'src/app/services/patch-db/data-model'
import { FormService } from 'src/app/services/form.service'
@Component({ @Component({
selector: 'app-action-input', selector: 'app-action-input',
@@ -12,22 +11,15 @@ import { Action } from 'src/app/services/patch-db/data-model'
}) })
export class AppActionInputPage { export class AppActionInputPage {
@Input() action: Action @Input() action: Action
@Input() cursor: ConfigCursor<'object'> actionForm: FormGroup
@Input() execute: () => Promise<void>
spec: ValueSpecObject
value: object
error: string
constructor ( constructor (
private readonly modalCtrl: ModalController, private readonly modalCtrl: ModalController,
private readonly errToast: ErrorToastService, private readonly formService: FormService,
private readonly loadingCtrl: LoadingController,
) { } ) { }
ngOnInit () { ngOnInit () {
this.spec = this.cursor.spec() this.actionForm = this.formService.createForm(this.action['input-spec'])
this.value = this.cursor.config()
this.error = this.cursor.checkInvalid()
} }
async dismiss (): Promise<void> { async dismiss (): Promise<void> {
@@ -35,24 +27,15 @@ export class AppActionInputPage {
} }
async save (): Promise<void> { async save (): Promise<void> {
const loader = await this.loadingCtrl.create({ if (this.actionForm.invalid) {
spinner: 'lines', this.actionForm.markAllAsTouched()
message: 'Executing action', document.getElementsByClassName('validation-error')[0].parentElement.parentElement.scrollIntoView({ behavior: 'smooth' })
cssClass: 'loader-ontop-of-all', return
})
await loader.present()
try {
await this.execute()
this.modalCtrl.dismiss()
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
} }
this.modalCtrl.dismiss(this.actionForm.value)
} }
handleObjectEdit (): void { asIsOrder () {
this.error = this.cursor.checkInvalid() return 0
} }
} }

View File

@@ -1,23 +1,11 @@
<ion-header> <ion-content class="subheader-padding">
<ion-toolbar>
<ion-buttons slot="start">
<ion-button (click)="dismiss()">
<ion-icon name="arrow-back"></ion-icon>
</ion-button>
</ion-buttons>
<ion-title>{{ spec.name }}</ion-title>
<ion-buttons *ngIf="spec.subtype !== 'enum'" slot="end">
<ion-button (click)="presentModalValueEdit()">
<ion-icon name="add" color="primary"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<config-header [spec]="spec" [error]="error"></config-header> <config-header [spec]="spec" [error]="error"></config-header>
<ion-button *ngIf="spec.subtype !== 'enum'" (click)="createOrEdit()">
<ion-icon name="add" color="primary"></ion-icon>
</ion-button>
<!-- enum list --> <!-- enum list -->
<ion-item-group *ngIf="spec.subtype === 'enum'"> <ion-item-group *ngIf="spec.subtype === 'enum'">
<ion-item-divider class="borderless"></ion-item-divider> <ion-item-divider class="borderless"></ion-item-divider>
@@ -46,7 +34,7 @@
</ion-item-divider> </ion-item-divider>
<div *ngFor="let v of value; index as i;"> <div *ngFor="let v of value; index as i;">
<ion-item button detail="false" (click)="presentModalValueEdit(i)"> <ion-item button detail="false" (click)="createOrEdit(i)">
<ion-icon size="small" slot="start" *ngIf="!annotations.members[i] || (annotations.members[i] | annotationStatus: 'NoChange')" style="margin-right: 15px; color: rgba(0,0,0,0); background: radial-gradient(#2a4e8970, #2a4e8970 35%, transparent 35%, transparent);" name="ellipse"></ion-icon> <ion-icon size="small" slot="start" *ngIf="!annotations.members[i] || (annotations.members[i] | annotationStatus: 'NoChange')" style="margin-right: 15px; color: rgba(0,0,0,0); background: radial-gradient(#2a4e8970, #2a4e8970 35%, transparent 35%, transparent);" name="ellipse"></ion-icon>
<ion-icon size="small" slot="start" *ngIf="!annotations.members[i] || (annotations.members[i] | annotationStatus: 'Added')" style="margin-right: 15px; color: rgba(0,0,0,0); background: radial-gradient(#2a4e8970, #2a4e8970 35%, transparent 35%, transparent);" name="ellipse"></ion-icon> <ion-icon size="small" slot="start" *ngIf="!annotations.members[i] || (annotations.members[i] | annotationStatus: 'Added')" style="margin-right: 15px; color: rgba(0,0,0,0); background: radial-gradient(#2a4e8970, #2a4e8970 35%, transparent 35%, transparent);" name="ellipse"></ion-icon>
<ion-icon size="small" slot="start" *ngIf="annotations.members[i] && (annotations.members[i] | annotationStatus: 'Edited')" style="margin-right: 15px" color="primary" name="ellipse"></ion-icon> <ion-icon size="small" slot="start" *ngIf="annotations.members[i] && (annotations.members[i] | annotationStatus: 'Edited')" style="margin-right: 15px" color="primary" name="ellipse"></ion-icon>
@@ -55,7 +43,7 @@
<ion-label>{{ valueString[i] }}</ion-label> <ion-label>{{ valueString[i] }}</ion-label>
<ion-button slot="end" fill="clear" (click)="presentAlertDelete(i, $event)"> <ion-button slot="end" fill="clear" (click)="presentAlertDelete(i, $event)">
<ion-icon slot="icon-only" name="close-outline"></ion-icon> <ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button> </ion-button>
</ion-item> </ion-item>
</div> </div>

View File

@@ -1,17 +1,16 @@
import { Component, Input } from '@angular/core' import { Component, Input } from '@angular/core'
import { AlertController } from '@ionic/angular' import { AlertController, IonNav } from '@ionic/angular'
import { Annotations, Range } from '../../pkg-config/config-utilities' import { Annotations, Range } from '../../pkg-config/config-utilities'
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
import { ConfigCursor } from 'src/app/pkg-config/config-cursor' import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
import { ValueSpecList, isValueSpecListOf } from 'src/app/pkg-config/config-types' import { ValueSpecList, isValueSpecListOf } from 'src/app/pkg-config/config-types'
import { ModalPresentable } from 'src/app/pkg-config/modal-presentable' import { SubNavService } from 'src/app/services/sub-nav.service'
@Component({ @Component({
selector: 'app-config-list', selector: 'app-config-list',
templateUrl: './app-config-list.page.html', templateUrl: './app-config-list.page.html',
styleUrls: ['./app-config-list.page.scss'], styleUrls: ['./app-config-list.page.scss'],
}) })
export class AppConfigListPage extends ModalPresentable { export class AppConfigListPage {
@Input() cursor: ConfigCursor<'list'> @Input() cursor: ConfigCursor<'list'>
spec: ValueSpecList spec: ValueSpecList
@@ -22,7 +21,6 @@ export class AppConfigListPage extends ModalPresentable {
// enum only // enum only
options: { value: string, checked: boolean }[] = [] options: { value: string, checked: boolean }[] = []
selectAll = true selectAll = true
//
min: number | undefined min: number | undefined
max: number | undefined max: number | undefined
@@ -34,10 +32,9 @@ export class AppConfigListPage extends ModalPresentable {
constructor ( constructor (
private readonly alertCtrl: AlertController, private readonly alertCtrl: AlertController,
trackingModalCtrl: TrackingModalController, private readonly subNav: SubNavService,
) { private readonly nav: IonNav,
super(trackingModalCtrl) ) { }
}
ngOnInit () { ngOnInit () {
this.spec = this.cursor.spec() this.spec = this.cursor.spec()
@@ -59,10 +56,6 @@ export class AppConfigListPage extends ModalPresentable {
this.updateCaches() this.updateCaches()
} }
async dismiss () {
return this.dismissModal(this.value)
}
// enum only // enum only
toggleSelectAll () { toggleSelectAll () {
if (!isValueSpecListOf(this.spec, 'enum')) { throw new Error('unreachable') } if (!isValueSpecListOf(this.spec, 'enum')) { throw new Error('unreachable') }
@@ -98,10 +91,10 @@ export class AppConfigListPage extends ModalPresentable {
this.updateCaches() this.updateCaches()
} }
async presentModalValueEdit (index?: number) { async createOrEdit (index?: number) {
const nextCursor = this.cursor.seekNext(index === undefined ? this.value.length : index) const nextCursor = this.cursor.seekNext(index === undefined ? this.value.length : index)
nextCursor.createFirstEntryForList() nextCursor.createFirstEntryForList()
return this.presentModal(nextCursor, () => this.updateCaches()) this.subNav.push(String(index), nextCursor, this.nav)
} }
async presentAlertDelete (key: number, e: Event) { async presentAlertDelete (key: number, e: Event) {

View File

@@ -1,27 +1,4 @@
<ion-header> <ion-content class="subheader-padding">
<ion-toolbar>
<ion-buttons slot="start">
<ion-button (click)="dismiss()">
<ion-icon name="arrow-back"></ion-icon>
</ion-button>
</ion-buttons>
<ion-title>{{ spec.name }}</ion-title>
<ion-buttons *ngIf="spec.nullable" slot="end">
<ion-button color="danger" (click)="presentAlertDestroy()">
Delete
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<config-header [spec]="spec" [error]="error"></config-header> <config-header [spec]="spec" [error]="error"></config-header>
<object-config [cursor]="cursor" (onEdit)="handleObjectEdit()" (onClick)="updatePath($event)"></object-config>
<!-- object -->
<ion-item-group>
<ion-item-divider></ion-item-divider>
<object-config [cursor]="cursor" (onEdit)="handleObjectEdit()"></object-config>
</ion-item-group>
</ion-content> </ion-content>

View File

@@ -1,15 +1,4 @@
<ion-header> <ion-content class="subheader-padding">
<ion-toolbar>
<ion-buttons slot="start">
<ion-button (click)="dismiss()">
<ion-icon name="arrow-back"></ion-icon>
</ion-button>
</ion-buttons>
<ion-title>{{ spec.name }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<config-header [spec]="spec" [error]="error"></config-header> <config-header [spec]="spec" [error]="error"></config-header>
@@ -24,10 +13,10 @@
</ion-icon> </ion-icon>
<ion-label>{{ spec.tag.name }}</ion-label> <ion-label>{{ spec.tag.name }}</ion-label>
<ion-select slot="end" [interfaceOptions]="setSelectOptions()" placeholder="Select One" <ion-select slot="end" [interfaceOptions]="setSelectOptions()" placeholder="Select One"
[(ngModel)]="value[spec.tag.id]" [selectedText]="spec.tag.variantNames[value[spec.tag.id]]" [(ngModel)]="value[spec.tag.id]" [selectedText]="spec.tag['variant-names'][value[spec.tag.id]]"
(ngModelChange)="handleUnionChange()"> (ngModelChange)="handleUnionChange()">
<ion-select-option *ngFor="let option of spec.variants | keyvalue: asIsOrder" [value]="option.key"> <ion-select-option *ngFor="let option of spec.variants | keyvalue: asIsOrder" [value]="option.key">
{{ spec.tag.variantNames[option.key] }} {{ spec.tag['variant-names'][option.key] }}
<span *ngIf="option.key === spec.default"> (default)</span> <span *ngIf="option.key === spec.default"> (default)</span>
</ion-select-option> </ion-select-option>
</ion-select> </ion-select>

View File

@@ -46,8 +46,8 @@ export class AppConfigUnionPage {
setSelectOptions () { setSelectOptions () {
return { return {
header: this.spec.tag.name, header: this.spec.tag.name,
subHeader: this.spec.changeWarning ? 'Warning!' : undefined, subHeader: this.spec['change-warning'] ? 'Warning!' : undefined,
message: this.spec.changeWarning ? `${this.spec.changeWarning}` : undefined, message: this.spec['change-warning'] ? `${this.spec['change-warning']}` : undefined,
cssClass: 'select-change-warning', cssClass: 'select-change-warning',
} }
} }

View File

@@ -1,22 +1,4 @@
<ion-header> <ion-content class="subheader-padding">
<ion-toolbar>
<ion-buttons slot="start">
<ion-button (click)="dismiss()">
<ion-icon name="arrow-back"></ion-icon>
</ion-button>
</ion-buttons>
<ion-title>
{{ spec.name }}
</ion-title>
<ion-buttons *ngIf="!!saveFn" slot="end">
<ion-button [disabled]="!!error" (click)="save()">
Save
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<config-header [spec]="spec" [error]="error"></config-header> <config-header [spec]="spec" [error]="error"></config-header>
@@ -55,15 +37,15 @@
<ion-list *ngIf="spec.type === 'enum'"> <ion-list *ngIf="spec.type === 'enum'">
<ion-radio-group [(ngModel)]="value"> <ion-radio-group [(ngModel)]="value">
<ion-item *ngFor="let option of spec.values"> <ion-item *ngFor="let option of spec.values">
<ion-label>{{ spec.valueNames[option] }}</ion-label> <ion-label>{{ spec['value-names'][option] }}</ion-label>
<ion-radio slot="start" [value]="option"></ion-radio> <ion-radio slot="start" [value]="option"></ion-radio>
</ion-item> </ion-item>
</ion-radio-group> </ion-radio-group>
</ion-list> </ion-list>
<!-- metadata --> <!-- metadata -->
<div class="ion-padding-start"> <div class="ion-padding-start">
<p *ngIf="spec.type === 'string' && spec.patternDescription"> <p *ngIf="spec.type === 'string' && spec['pattern-description']">
{{ spec.patternDescription }} {{ spec['pattern-description'] }}
</p> </p>
<p *ngIf="spec.type === 'number' && spec.integral"> <p *ngIf="spec.type === 'number' && spec.integral">
{{ integralDescription }} {{ integralDescription }}

View File

@@ -127,7 +127,7 @@ export class AppConfigValuePage {
} }
// test pattern if string // test pattern if string
if (this.spec.type === 'string' && this.value) { if (this.spec.type === 'string' && this.value) {
const { pattern, patternDescription } = this.spec const { pattern, 'pattern-description' : patternDescription } = this.spec
if (pattern && !RegExp(pattern).test(this.value as string)) { if (pattern && !RegExp(pattern).test(this.value as string)) {
this.error = patternDescription || `Must match ${pattern}` this.error = patternDescription || `Must match ${pattern}`
return false return false

View File

@@ -0,0 +1,22 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { IonicModule } from '@ionic/angular'
import { AppConfigPage } from './app-config.page'
import { SharingModule } from 'src/app/modules/sharing.module'
import { FormObjectComponentModule } from 'src/app/components/form-object/form-object.component.module'
@NgModule({
declarations: [AppConfigPage],
imports: [
CommonModule,
FormsModule,
IonicModule,
SharingModule,
FormObjectComponentModule,
ReactiveFormsModule,
],
entryComponents: [AppConfigPage],
exports: [AppConfigPage],
})
export class AppConfigPageModule { }

View File

@@ -1,40 +1,23 @@
<ion-header> <ion-header>
<ion-toolbar> <ion-toolbar *ngIf="patch.data['package-data'][pkgId] as pkg">
<ion-buttons slot="start"> <ion-title>Config</ion-title>
<ion-button (click)="cancel()"> <ion-buttons slot="end" class="ion-padding-end">
<ion-icon name="arrow-back"></ion-icon> <ion-button fill="clear" [disabled]="loadingText" (click)="resetDefaults()">
Reset Defaults
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>
<ion-title>{{ pkg.manifest.title }}</ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-padding-top"> <ion-content class="ion-padding">
<!-- loading --> <!-- loading -->
<text-spinner *ngIf="loadingText; else loaded" [text]="loadingText"></text-spinner> <text-spinner *ngIf="loadingText; else loaded" [text]="loadingText"></text-spinner>
<!-- not loading --> <!-- not loading -->
<ng-template #loaded> <ng-template #loaded>
<ion-item *ngIf="error" class="notifier-item">
<ion-label style="margin: 7px 5px;" class="ion-text-wrap">
<p style="color: var(--ion-color-danger)">{{ error.text }}</p>
<p><a style="color: var(--ion-color-danger); text-decoration: underline; font-weight: bold;" *ngIf="error.moreInfo && !openErrorMoreInfo" (click)="openErrorMoreInfo = true">{{error.moreInfo.buttonText}}</a></p>
<ng-container *ngIf="openErrorMoreInfo"> <ng-container *ngIf="patch.data['package-data'][pkgId] as pkg">
<p style="margin-top: 10px; color: var(--ion-color-medium);" [innerHTML]="error.moreInfo.title"></p>
<p style="margin-top: 10px; color: var(--ion-color-medium); font-size: small" [innerHTML]="error.moreInfo.description"></p>
<a style="font-size: x-small; font-style: italic;" (click)="openErrorMoreInfo = false">Hide</a>
</ng-container>
</ion-label>
<ion-button style="position: absolute; right: 0; top: 0" *ngIf="pkg" color="danger" fill="clear" (click)="dismissError()">
<ion-icon name="close-outline"></ion-icon>
</ion-button>
</ion-item>
<ng-container *ngIf="pkg">
<!-- @TODO make sure this is how to determine if pkg is in needs_config -->
<ng-container *ngIf="pkg.manifest.config && !pkg.installed.status.configured && !edited"> <ng-container *ngIf="pkg.manifest.config && !pkg.installed.status.configured && !edited">
<ion-item class="notifier-item"> <ion-item class="notifier-item">
<ion-label class="ion-text-wrap"> <ion-label class="ion-text-wrap">
@@ -42,7 +25,7 @@
<ion-icon size="small" style="margin-right: 5px" slot="start" color="dark" slot="start" name="alert-circle-outline"></ion-icon> <ion-icon size="small" style="margin-right: 5px" slot="start" color="dark" slot="start" name="alert-circle-outline"></ion-icon>
<ion-text style="font-size: smaller;">Initial Config</ion-text> <ion-text style="font-size: smaller;">Initial Config</ion-text>
</h2> </h2>
<p style="font-size: small">To use the default config for {{ pkg.manifest.title }}, click "Save" below.</p> <p style="font-size: small">To use the default config for {{ pkg.manifest.title }}, click "Save" above.</p>
</ion-label> </ion-label>
</ion-item> </ion-item>
</ng-container> </ng-container>
@@ -59,7 +42,7 @@
</h2> </h2>
<div style="margin: 7px 5px;"> <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 }}. <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” below.</ion-text> <ion-text color="dark">To accept the changes, click “Save” above.</ion-text>
</p> </p>
<a style="font-size: small" *ngIf="!openRec" (click)="openRec = true">More Info</a> <a style="font-size: small" *ngIf="!openRec" (click)="openRec = true">More Info</a>
<ng-container *ngIf="openRec"> <ng-container *ngIf="openRec">
@@ -67,7 +50,7 @@
<a style="font-size: x-small; font-style: italic;" (click)="openRec = false">hide</a> <a style="font-size: x-small; font-style: italic;" (click)="openRec = false">hide</a>
</ng-container> </ng-container>
<ion-button style="position: absolute; right: 0; top: 0" color="primary" fill="clear" (click)="dismissRec()"> <ion-button style="position: absolute; right: 0; top: 0" color="primary" fill="clear" (click)="dismissRec()">
<ion-icon name="close-outline"></ion-icon> <ion-icon name="close"></ion-icon>
</ion-button> </ion-button>
</div> </div>
</ion-label> </ion-label>
@@ -75,41 +58,37 @@
<ion-item-divider></ion-item-divider> <ion-item-divider></ion-item-divider>
</ng-container> </ng-container>
<ion-item *ngIf="invalid" class="notifier-item">
<ion-icon size="small" slot="start" color="danger" name="warning-outline"></ion-icon>
<ion-label class="ion-text-wrap">
<p style="color: var(--ion-color-danger)">{{invalid}}</p>
</ion-label>
</ion-item>
<!-- no config --> <!-- no config -->
<ion-item *ngIf="!hasConfig"> <ion-item *ngIf="!hasConfig">
<ion-label class="ion-text-wrap"> <ion-label class="ion-text-wrap">
<p>No config options for {{ pkg.manifest.title }} {{ pkg.manifest.version }}.</p> <p>No config options for {{ pkg.manifest.title }} {{ pkg.manifest.version }}.</p>
</ion-label> </ion-label>
</ion-item> </ion-item>
<!-- save button, always show -->
<ion-button
[disabled]="invalid || (!edited && !added && !pkg.installed.status.configured )"
fill="outline"
expand="block"
style="margin: 10px"
color="primary"
(click)="save(pkg)"
>
<ion-text color="primary" style="font-weight: bold">
Save
</ion-text>
</ion-button>
<!-- has config -->
<ng-container *ngIf="hasConfig">
<ion-item-group class="ion-text-wrap ion-padding-bottom">
<ion-item-divider>Config Options</ion-item-divider>
<object-config [cursor]="rootCursor" (onEdit)="handleObjectEdit()"></object-config>
</ion-item-group>
</ng-container>
</ng-container> </ng-container>
<!-- has config -->
<form [formGroup]="configForm" (ngSubmit)="save()" novalidate>
<form-object
[objectSpec]="configSpec"
[formGroup]="configForm"
[current]="current"
showEdited
></form-object>
</form>
</ng-template> </ng-template>
</ion-content> </ion-content>
<ion-footer>
<ion-toolbar *ngIf="patch.data['package-data'][pkgId] as pkg">
<ion-buttons slot="start" class="ion-padding-start">
<ion-button fill="outline" (click)="dismiss()">
Cancel
</ion-button>
</ion-buttons>
<ion-buttons slot="end" class="ion-padding-end">
<ion-button fill="outline" color="primary" [disabled]="loadingText" (click)="save(pkg)">
Save
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-footer>

View File

@@ -0,0 +1,8 @@
.notifier-item {
margin: 12px;
margin-top: 0px;
border-radius: 12px;
// kills the lines
--border-width: 0;
--inner-border-width: 0;
}

View File

@@ -0,0 +1,181 @@
import { Component, Input, ViewChild } from '@angular/core'
import { AlertController, ModalController, IonContent, LoadingController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
import { isEmptyObject, Recommendation } 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'
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 { FormService } from 'src/app/services/form.service'
@Component({
selector: 'app-config',
templateUrl: './app-config.page.html',
styleUrls: ['./app-config.page.scss'],
})
export class AppConfigPage {
@Input() pkgId: string
loadingText: string | undefined
configSpec: ConfigSpec
configForm: FormGroup
current: object
hasConfig = false
rec: Recommendation | null = null
showRec = true
openRec = false
@ViewChild(IonContent) content: IonContent
constructor (
private readonly wizardBaker: WizardBaker,
private readonly embassyApi: ApiService,
private readonly errToast: ErrorToastService,
private readonly loadingCtrl: LoadingController,
private readonly alertCtrl: AlertController,
private readonly modalCtrl: ModalController,
private readonly formService: FormService,
public readonly patch: PatchDbService,
) { }
async ngOnInit () {
const rec = history.state?.configRecommendation as Recommendation
try {
this.loadingText = 'Loading Config'
const { spec, config } = await this.embassyApi.getPackageConfig({ id: this.pkgId })
let depConfig: object
if (rec) {
this.loadingText = `Setting properties to accommodate ${rec.dependentTitle}...`
depConfig = await this.embassyApi.dryConfigureDependency({ 'dependency-id': this.pkgId, 'dependent-id': rec.dependentId })
}
this.setConfig(spec, config, depConfig)
} catch (e) {
this.errToast.present(e)
} finally {
this.loadingText = undefined
}
}
ngAfterViewInit () {
this.content.scrollToPoint(undefined, 1)
}
setConfig (spec: ConfigSpec, config: object, depConfig?: object) {
this.configSpec = spec
this.current = config
this.hasConfig = !isEmptyObject(config)
this.configForm = this.formService.createForm(spec, { ...config, ...depConfig })
this.configForm.markAllAsTouched()
if (depConfig) {
this.markDirtyRecursive(this.configForm, depConfig)
}
}
markDirtyRecursive (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 (newVal && typeof newVal === 'object' && !Array.isArray(newVal)) {
this.markDirtyRecursive(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()
}
})
}
resetDefaults () {
this.configForm = this.formService.createForm(this.configSpec)
this.markDirtyRecursive(this.configForm, this.current)
}
dismissRec () {
this.showRec = false
}
async dismiss () {
if (this.configForm.dirty) {
await this.presentAlertUnsaved()
} else {
this.modalCtrl.dismiss()
}
}
async save (pkg: PackageDataEntry) {
if (this.configForm.invalid) {
document.getElementsByClassName('validation-error')[0].parentElement.parentElement.scrollIntoView({ behavior: 'smooth' })
return
}
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: `Saving config...`,
cssClass: 'loader',
})
await loader.present()
const config = this.configForm.value
try {
const breakages = await this.embassyApi.drySetPackageConfig({
id: pkg.manifest.id,
config,
})
if (!isEmptyObject(breakages.length)) {
const { cancelled } = await wizardModal(
this.modalCtrl,
this.wizardBaker.configure({
pkg,
breakages,
}),
)
if (cancelled) return
}
await this.embassyApi.setPackageConfig({
id: pkg.manifest.id,
config,
})
this.modalCtrl.dismiss()
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
private async presentAlertUnsaved () {
const alert = await this.alertCtrl.create({
header: 'Unsaved Changes',
message: 'You have unsaved changes. Are you sure you want to leave?',
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: `Leave`,
handler: () => {
this.modalCtrl.dismiss()
},
},
],
})
await alert.present()
}
}

View File

@@ -10,6 +10,7 @@
</ion-title> </ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-padding-top"> <ion-content class="ion-padding-top">
<text-spinner *ngIf="loading" text="Loading Drives"></text-spinner> <text-spinner *ngIf="loading" text="Loading Drives"></text-spinner>

View File

@@ -2,19 +2,15 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { AppRestoreComponent } from './app-restore.component' import { AppRestoreComponent } from './app-restore.component'
import { PwaBackComponentModule } from '../../components/pwa-back-button/pwa-back.component.module'
import { BackupConfirmationComponentModule } from '../backup-confirmation/backup-confirmation.component.module' import { BackupConfirmationComponentModule } from '../backup-confirmation/backup-confirmation.component.module'
import { SharingModule } from '../../modules/sharing.module' import { SharingModule } from '../../modules/sharing.module'
import { TextSpinnerComponentModule } from '../../components/text-spinner/text-spinner.component.module'
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
IonicModule, IonicModule,
SharingModule,
BackupConfirmationComponentModule, BackupConfirmationComponentModule,
PwaBackComponentModule, SharingModule,
TextSpinnerComponentModule,
], ],
declarations: [ declarations: [
AppRestoreComponent, AppRestoreComponent,

View File

@@ -31,7 +31,6 @@ export class AppRestoreComponent {
) { } ) { }
ngOnInit () { ngOnInit () {
console.log('initing')
this.getExternalDisks() this.getExternalDisks()
} }
@@ -51,7 +50,6 @@ export class AppRestoreComponent {
} catch (e) { } catch (e) {
this.errToast.present(e) this.errToast.present(e)
} finally { } finally {
console.log('loading false')
this.loading = false this.loading = false
} }
} }
@@ -80,7 +78,6 @@ export class AppRestoreComponent {
} }
private async restore (logicalname: string, password: string): Promise<void> { private async restore (logicalname: string, password: string): Promise<void> {
console.log('here here here')
this.submitting = true this.submitting = true
// await loader.present() // await loader.present()

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { EnumListPage } from './enum-list.page'
import { FormsModule } from '@angular/forms'
@NgModule({
declarations: [EnumListPage],
imports: [
CommonModule,
IonicModule,
FormsModule,
],
entryComponents: [EnumListPage],
exports: [EnumListPage],
})
export class EnumListPageModule { }

View File

@@ -0,0 +1,36 @@
<ion-header>
<ion-toolbar>
<ion-title>
{{ spec.name }}
</ion-title>
<ion-buttons slot="end">
<ion-button slot="end" fill="clear" color="primary" (click)="toggleSelectAll()">
{{ selectAll ? 'All' : 'None' }}
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item-group>
<ion-item *ngFor="let option of options | keyvalue : asIsOrder">
<ion-label>{{ option.key }}</ion-label>
<ion-checkbox slot="end" [(ngModel)]="option.value" (click)="toggleSelected(option.key)"></ion-checkbox>
</ion-item>
</ion-item-group>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-buttons slot="start" class="ion-padding-start">
<ion-button fill="outline" (click)="dismiss()">
Cancel
</ion-button>
</ion-buttons>
<ion-buttons slot="end" class="ion-padding-end">
<ion-button fill="outline" color="primary" (click)="save()">
Done
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-footer>

View File

@@ -0,0 +1,56 @@
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',
templateUrl: './enum-list.page.html',
styleUrls: ['./enum-list.page.scss'],
})
export class EnumListPage {
@Input() key: string
@Input() spec: ValueSpecListOf<'enum'>
@Input() current: string[]
options: { [option: string]: boolean } = { }
// min: number | undefined
// max: number | undefined
// minMessage: string
// maxMessage: string
selectAll = true
constructor (
private readonly modalCtrl: ModalController,
) { }
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)
}
}
dismiss () {
this.modalCtrl.dismiss()
}
save () {
this.modalCtrl.dismiss(Object.keys(this.options).filter(key => this.options[key]))
}
toggleSelectAll () {
Object.keys(this.options).forEach(k => this.options[k] = this.selectAll)
this.selectAll = !this.selectAll
}
async toggleSelected (key: string) {
this.options[key] = !this.options[key]
}
}

View File

@@ -3,14 +3,12 @@ import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { MarkdownPage } from './markdown.page' import { MarkdownPage } from './markdown.page'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
IonicModule, IonicModule,
SharingModule, SharingModule,
TextSpinnerComponentModule,
], ],
declarations: [MarkdownPage], declarations: [MarkdownPage],
}) })

View File

@@ -2,7 +2,7 @@
<ion-toolbar> <ion-toolbar>
<ion-buttons slot="start"> <ion-buttons slot="start">
<ion-button (click)="dismiss()"> <ion-button (click)="dismiss()">
<ion-icon slot="icon-only" name="close-outline"></ion-icon> <ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>
<ion-title>{{ title | titlecase }}</ion-title> <ion-title>{{ title | titlecase }}</ion-title>

View File

@@ -10,42 +10,50 @@ import { HasUiPipe, LaunchablePipe } from '../pipes/ui.pipe'
import { EmptyPipe } from '../pipes/empty.pipe' import { EmptyPipe } from '../pipes/empty.pipe'
import { NotificationColorPipe } from '../pipes/notification-color.pipe' import { NotificationColorPipe } from '../pipes/notification-color.pipe'
import { InstallState } from '../pipes/install-state.pipe' import { InstallState } from '../pipes/install-state.pipe'
import { TextSpinnerComponentModule } from '../components/text-spinner/text-spinner.component.module'
import { PwaBackComponentModule } from '../components/pwa-back-button/pwa-back.component.module'
@NgModule({ @NgModule({
declarations: [ declarations: [
EmverComparesPipe, EmverComparesPipe,
EmverSatisfiesPipe, EmverSatisfiesPipe,
TypeofPipe, TypeofPipe,
IncludesPipe, IncludesPipe,
InstallState, InstallState,
MarkdownPipe, MarkdownPipe,
AnnotationStatusPipe, AnnotationStatusPipe,
TruncateCenterPipe, TruncateCenterPipe,
TruncateEndPipe, TruncateEndPipe,
MaskPipe, MaskPipe,
EmverDisplayPipe, EmverDisplayPipe,
HasUiPipe, HasUiPipe,
LaunchablePipe, LaunchablePipe,
EmptyPipe, EmptyPipe,
NotificationColorPipe, NotificationColorPipe,
], ],
imports: [], imports: [
exports: [ TextSpinnerComponentModule,
EmverComparesPipe, PwaBackComponentModule,
EmverSatisfiesPipe, ],
TypeofPipe, exports: [
IncludesPipe, EmverComparesPipe,
MarkdownPipe, EmverSatisfiesPipe,
AnnotationStatusPipe, TypeofPipe,
TruncateEndPipe, IncludesPipe,
TruncateCenterPipe, MarkdownPipe,
MaskPipe, AnnotationStatusPipe,
EmverDisplayPipe, TruncateEndPipe,
HasUiPipe, TruncateCenterPipe,
InstallState, MaskPipe,
LaunchablePipe, EmverDisplayPipe,
EmptyPipe, HasUiPipe,
NotificationColorPipe, InstallState,
], LaunchablePipe,
EmptyPipe,
NotificationColorPipe,
// components
TextSpinnerComponentModule,
PwaBackComponentModule,
],
}) })
export class SharingModule { } export class SharingModule { }

View File

@@ -0,0 +1,24 @@
import { NgModule } from '@angular/core'
import { AppConfigListPageModule } from 'src/app/modals/app-config-list/app-config-list.module'
import { AppConfigObjectPageModule } from 'src/app/modals/app-config-object/app-config-object.module'
import { AppConfigUnionPageModule } from 'src/app/modals/app-config-union/app-config-union.module'
import { AppConfigValuePageModule } from 'src/app/modals/app-config-value/app-config-value.module'
import { SubNavComponentModule } from '../components/sub-nav/sub-nav.component.module'
@NgModule({
imports: [
AppConfigListPageModule,
AppConfigObjectPageModule,
AppConfigUnionPageModule,
AppConfigValuePageModule,
SubNavComponentModule,
],
exports: [
AppConfigListPageModule,
AppConfigObjectPageModule,
AppConfigUnionPageModule,
AppConfigValuePageModule,
SubNavComponentModule,
],
})
export class SubNavModule { }

View File

@@ -0,0 +1,7 @@
<ion-item button>
<ion-icon slot="start" [name]="action.icon"></ion-icon>
<ion-label class="ion-text-wrap">
<h1>{{ action.name }}</h1>
<h2>{{ action.description }}</h2>
</ion-label>
</ion-item>

View File

@@ -2,8 +2,7 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { AppActionsPage } from './app-actions.page' import { AppActionsPage, AppActionsItemComponent } from './app-actions.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { QRComponentModule } from 'src/app/components/qr/qr.component.module' import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
import { AppActionInputPageModule } from 'src/app/modals/app-action-input/app-action-input.module' import { AppActionInputPageModule } from 'src/app/modals/app-action-input/app-action-input.module'
@@ -21,12 +20,14 @@ const routes: Routes = [
CommonModule, CommonModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule,
QRComponentModule, QRComponentModule,
SharingModule, SharingModule,
AppActionInputPageModule, AppActionInputPageModule,
AppRestoreComponentModule, AppRestoreComponentModule,
], ],
declarations: [AppActionsPage], declarations: [
AppActionsPage,
AppActionsItemComponent,
],
}) })
export class AppActionsPageModule { } export class AppActionsPageModule { }

View File

@@ -10,66 +10,37 @@
<ion-content class="ion-padding-top"> <ion-content class="ion-padding-top">
<ng-container *ngIf="patch.data['package-data'][pkgId] as pkg"> <ion-item-group *ngIf="patch.data['package-data'][pkgId] as pkg">
<ion-grid class="ion-text-center" style="margin: 0 6px;"> <!-- ** standard actions ** -->
<ion-row> <ion-item-divider>Standard Actions</ion-item-divider>
<ion-col *ngFor="let action of pkg.manifest.actions | keyvalue: asIsOrder" size="6"> <app-actions-item
<ion-card button style="cursor: pointer !important; height: 88%;" color="light" (click)="handleAction(pkg, action)"> [action]="{
<ion-card-header> name: 'Restore From Backup',
<ion-card-subtitle> description: 'All changes since backup will be lost.',
<ion-icon size="large" *ngIf="!(action.value['allowed-statuses'] | includes: pkg.installed.status.main.status); else goodIcon" color="danger" name="close-outline"></ion-icon> icon: 'color-wand-outline'
<ion-icon size="large" #goAhead name="play-circle-outline"></ion-icon> }"
</ion-card-subtitle> (click)="restore()">
<ion-card-title>{{ action.value.name }}</ion-card-title> </app-actions-item>
</ion-card-header> <app-actions-item
<ion-card-content> [action]="{
{{ action.value.description }} name: 'Uninstall',
</ion-card-content> description: 'This will uninstall the service from your Embassy and delete all data permanently.',
</ion-card> icon: 'trash-outline'
</ion-col> }"
<ion-col size="6"> (click)="uninstall(pkg.manifest)">
<ion-card button style="cursor: pointer !important; height: 88%;" color="light" (click)="restore()"> </app-actions-item>
<ion-card-header>
<ion-card-subtitle>
<ion-icon size="large" name="color-wand-outline"></ion-icon>
</ion-card-subtitle>
<ion-card-title>Restore From Backup</ion-card-title>
</ion-card-header>
<ion-card-content>
All changes since backup will be lost.
</ion-card-content>
</ion-card>
</ion-col>
<ion-col size="6">
<ion-card button style="cursor: pointer !important; height: 88%;" color="light" (click)="uninstall(pkg.manifest)">
<ion-card-header>
<ion-card-subtitle>
<ion-icon size="large" name="trash-outline"></ion-icon>
</ion-card-subtitle>
<ion-card-title>Uninstall</ion-card-title>
</ion-card-header>
<ion-card-content>
This will uninstall the service from your Embassy and delete all data permanently.
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
<!-- <ion-item-group> <!-- ** specific actions ** -->
<ion-item button *ngFor="let action of pkg.manifest.actions | keyvalue: asIsOrder" (click)="handleAction(pkg, action)" > <ion-item-divider>Actions for {{ pkg.manifest.title }}</ion-item-divider>
<ion-label class="ion-text-wrap"> <app-actions-item
<h2><ion-text color="primary">{{ action.value.name }}</ion-text><ion-icon *ngIf="!(action.value['allowed-statuses'] | includes: pkg.installed.status.main.status)" color="danger" name="close-outline"></ion-icon></h2> *ngFor="let action of pkg.manifest.actions | keyvalue: asIsOrder"
<p><ion-text color="dark">{{ action.value.description }}</ion-text></p> [action]="{
</ion-label> name: action.value.name,
</ion-item> description: action.value.description,
<ion-item button (click)="uninstall(pkg.manifest)" > icon: 'play-circle-outline'
<ion-label class="ion-text-wrap"> }"
<h2><ion-text color="primary">Uninstall</ion-text></h2> (click)="handleAction(pkg, action)">
<p><ion-text color="dark">This will uninstall the service from your Embassy and delete all data permanently.</ion-text></p> </app-actions-item>
</ion-label> </ion-item-group>
</ion-item>
</ion-item-group> -->
</ng-container>
</ion-content> </ion-content>

View File

@@ -1,4 +1,4 @@
import { Component, ViewChild } from '@angular/core' import { Component, Input, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
import { AlertController, IonContent, LoadingController, ModalController, NavController } from '@ionic/angular' import { AlertController, IonContent, LoadingController, ModalController, NavController } from '@ionic/angular'
@@ -7,7 +7,6 @@ import { Action, Manifest, PackageDataEntry, PackageMainStatus } from 'src/app/s
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
import { AppActionInputPage } from 'src/app/modals/app-action-input/app-action-input.page' import { AppActionInputPage } from 'src/app/modals/app-action-input/app-action-input.page'
import { ErrorToastService } from 'src/app/services/error-toast.service' import { ErrorToastService } from 'src/app/services/error-toast.service'
import { AppRestoreComponent } from 'src/app/modals/app-restore/app-restore.component' import { AppRestoreComponent } from 'src/app/modals/app-restore/app-restore.component'
@@ -49,16 +48,17 @@ export class AppActionsPage {
async handleAction (pkg: PackageDataEntry, action: { key: string, value: Action }) { async handleAction (pkg: PackageDataEntry, action: { key: string, value: Action }) {
if ((action.value['allowed-statuses'] as PackageMainStatus[]).includes(pkg.installed.status.main.status)) { if ((action.value['allowed-statuses'] as PackageMainStatus[]).includes(pkg.installed.status.main.status)) {
const inputSpec = action.value['input-spec'] if (action.value['input-spec']) {
if (inputSpec) {
const modal = await this.modalCtrl.create({ const modal = await this.modalCtrl.create({
component: AppActionInputPage, component: AppActionInputPage,
componentProps: { componentProps: {
action: action.value, action: action.value,
cursor: new ConfigCursor(inputSpec, { }),
execute: () => this.executeAction(pkg.manifest.id, action.key),
}, },
}) })
modal.onWillDismiss().then(({ data }) => {
if (!data) return
this.executeAction(pkg.manifest.id, action.key, data)
})
await modal.present() await modal.present()
} else { } else {
const alert = await this.alertCtrl.create({ const alert = await this.alertCtrl.create({
@@ -105,7 +105,7 @@ export class AppActionsPage {
} }
async restore (): Promise<void> { async restore (): Promise<void> {
const m = await this.modalCtrl.create({ const modal = await this.modalCtrl.create({
componentProps: { componentProps: {
pkgId: this.pkgId, pkgId: this.pkgId,
}, },
@@ -113,12 +113,12 @@ export class AppActionsPage {
backdropDismiss: false, backdropDismiss: false,
}) })
m.onWillDismiss().then(res => { modal.onWillDismiss().then(res => {
const data = res.data const data = res.data
if (data.error) this.errToast.present(data.error) if (data.error) this.errToast.present(data.error)
}) })
return await m.present() return await modal.present()
} }
async uninstall (manifest: Manifest) { async uninstall (manifest: Manifest) {
@@ -137,7 +137,7 @@ export class AppActionsPage {
return this.navCtrl.navigateRoot('/services') return this.navCtrl.navigateRoot('/services')
} }
private async executeAction (pkgId: string, actionId: string): Promise<void> { private async executeAction (pkgId: string, actionId: string, input?: object): Promise<void> {
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
spinner: 'lines', spinner: 'lines',
message: 'Executing action...', message: 'Executing action...',
@@ -146,7 +146,11 @@ export class AppActionsPage {
await loader.present() await loader.present()
try { try {
const res = await this.embassyApi.executePackageAction({ id: pkgId, 'action-id': actionId }) const res = await this.embassyApi.executePackageAction({
id: pkgId,
'action-id': actionId,
input,
})
const successAlert = await this.alertCtrl.create({ const successAlert = await this.alertCtrl.create({
header: 'Execution Complete', header: 'Execution Complete',
@@ -161,3 +165,18 @@ export class AppActionsPage {
} }
} }
} }
interface LocalAction {
name: string
description: string
icon: string
}
@Component({
selector: 'app-actions-item',
templateUrl: './app-actions-item.component.html',
styleUrls: ['./app-actions.page.scss'],
})
export class AppActionsItemComponent {
@Input() action: LocalAction
}

View File

@@ -1,38 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { AppConfigPage } from './app-config.page'
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
import { AppConfigListPageModule } from 'src/app/modals/app-config-list/app-config-list.module'
import { AppConfigObjectPageModule } from 'src/app/modals/app-config-object/app-config-object.module'
import { AppConfigUnionPageModule } from 'src/app/modals/app-config-union/app-config-union.module'
import { AppConfigValuePageModule } from 'src/app/modals/app-config-value/app-config-value.module'
import { SharingModule } from 'src/app/modules/sharing.module'
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
const routes: Routes = [
{
path: '',
component: AppConfigPage,
},
]
@NgModule({
imports: [
ObjectConfigComponentModule,
AppConfigListPageModule,
AppConfigObjectPageModule,
AppConfigUnionPageModule,
AppConfigValuePageModule,
TextSpinnerComponentModule,
SharingModule,
CommonModule,
FormsModule,
IonicModule,
RouterModule.forChild(routes),
],
declarations: [AppConfigPage],
})
export class AppConfigPageModule { }

View File

@@ -1,218 +0,0 @@
import { Component, ViewChild } from '@angular/core'
import { NavController, AlertController, ModalController, IonContent, LoadingController } from '@ionic/angular'
import { ActivatedRoute } from '@angular/router'
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
import { isEmptyObject, Recommendation } from 'src/app/util/misc.util'
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
import { from, fromEvent, of, Subscription } from 'rxjs'
import { catchError, concatMap, map, take, tap } from 'rxjs/operators'
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'
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
import { PackageDataEntry, PackageState } 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'
@Component({
selector: 'app-config',
templateUrl: './app-config.page.html',
styleUrls: ['./app-config.page.scss'],
})
export class AppConfigPage {
error: { text: string, moreInfo?:
{ title: string, description: string, buttonText: string }
}
loadingText: string | undefined
pkg: PackageDataEntry
hasConfig = false
mocalShowing = false
packageState = PackageState
rec: Recommendation | null = null
showRec = true
openRec = false
invalid: string
edited: boolean
added: boolean
rootCursor: ConfigCursor<'object'>
spec: ConfigSpec
config: object
@ViewChild(IonContent) content: IonContent
subs: Subscription[] = []
constructor (
private readonly navCtrl: NavController,
private readonly route: ActivatedRoute,
private readonly wizardBaker: WizardBaker,
private readonly embassyApi: ApiService,
private readonly errToast: ErrorToastService,
private readonly loadingCtrl: LoadingController,
private readonly alertCtrl: AlertController,
private readonly modalController: ModalController,
private readonly trackingModalCtrl: TrackingModalController,
private readonly patch: PatchDbService,
) { }
async ngOnInit () {
const pkgId = this.route.snapshot.paramMap.get('pkgId') as string
this.subs = [
this.route.params.pipe(take(1)).subscribe(params => {
if (params.edit) {
window.history.back()
}
}),
fromEvent(window, 'popstate').subscribe(() => {
this.mocalShowing = false
this.trackingModalCtrl.dismissAll()
}),
this.trackingModalCtrl.onCreateAny$().subscribe(() => {
if (!this.mocalShowing) {
window.history.pushState(null, null, window.location.href + '/edit')
this.mocalShowing = true
}
}),
this.trackingModalCtrl.onDismissAny$().subscribe(() => {
if (!this.trackingModalCtrl.anyModals && this.mocalShowing === true) {
this.navCtrl.back()
}
}),
this.patch.watch$('package-data', pkgId)
.pipe(
tap(pkg => this.pkg = pkg),
tap(() => this.loadingText = 'Loading config...'),
concatMap(() => this.embassyApi.getPackageConfig({ id: pkgId })),
concatMap(({ spec, config }) => {
const rec = history.state && history.state.configRecommendation as Recommendation
if (rec) {
this.loadingText = `Setting properties to accommodate ${rec.dependentTitle}...`
return from(this.embassyApi.dryConfigureDependency({ 'dependency-id': pkgId, 'dependent-id': rec.dependentId }))
.pipe(
map(res => ({
spec,
config,
dependencyConfig: res,
})),
tap(() => this.rec = rec),
catchError(e => {
this.error = { text: `Could not set properties to accommodate ${rec.dependentTitle}: ${e.message}`, moreInfo: {
title: `${rec.dependentTitle} requires the following:`,
description: rec.description,
buttonText: 'Configure Manually',
} }
return of({ spec, config, dependencyConfig: null })
}),
)
} else {
return of({ spec, config, dependencyConfig: null })
}
}),
map(({ spec, config, dependencyConfig }) => this.setConfig(spec, config, dependencyConfig)),
tap(() => this.loadingText = undefined),
take(1),
).subscribe({
error: e => {
console.error(e.message)
this.error = { text: e.message }
},
}),
]
}
ngAfterViewInit () {
this.content.scrollToPoint(undefined, 1)
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}
setConfig (spec: ConfigSpec, config: object, dependencyConfig?: object) {
this.rootCursor = dependencyConfig ? new ConfigCursor(spec, config, null, dependencyConfig) : new ConfigCursor(spec, config)
this.spec = this.rootCursor.spec().spec
this.config = this.rootCursor.config()
this.handleObjectEdit()
this.hasConfig = !isEmptyObject(this.spec)
}
dismissRec () {
this.showRec = false
}
dismissError () {
this.error = undefined
}
async cancel () {
if (this.edited) {
await this.presentAlertUnsaved()
} else {
this.navCtrl.back()
}
}
async save (pkg: PackageDataEntry) {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: `Saving config...`,
cssClass: 'loader',
})
await loader.present()
try {
const breakages = await this.embassyApi.drySetPackageConfig({ id: pkg.manifest.id, config: this.config })
if (!isEmptyObject(breakages.length)) {
const { cancelled } = await wizardModal(
this.modalController,
this.wizardBaker.configure({
pkg,
breakages,
}),
)
if (cancelled) return
}
await this.embassyApi.setPackageConfig({ id: pkg.manifest.id, config: this.config })
this.navCtrl.back()
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
handleObjectEdit () {
this.edited = this.rootCursor.isEdited()
this.added = this.rootCursor.isNew()
this.invalid = this.rootCursor.checkInvalid()
}
private async presentAlertUnsaved () {
const alert = await this.alertCtrl.create({
backdropDismiss: false,
header: 'Unsaved Changes',
message: 'You have unsaved changes. Are you sure you want to leave?',
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: `Leave`,
handler: () => {
this.navCtrl.back()
},
},
],
})
await alert.present()
}
}

View File

@@ -3,9 +3,7 @@ import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { AppInstructionsPage } from './app-instructions.page' import { AppInstructionsPage } from './app-instructions.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -19,9 +17,7 @@ const routes: Routes = [
CommonModule, CommonModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule,
SharingModule, SharingModule,
TextSpinnerComponentModule,
], ],
declarations: [ declarations: [
AppInstructionsPage, AppInstructionsPage,

View File

@@ -0,0 +1,54 @@
<ion-item>
<ion-icon slot="start" [name]="interface.def.ui ? 'desktop-outline' : 'terminal-outline'"></ion-icon>
<ion-label class="ion-text-wrap">
<h1>{{ interface.def.name }}</h1>
<h2>{{ interface.def.description }}</h2>
</ion-label>
</ion-item>
<div style="padding-left: 54px;">
<!-- has tor -->
<ion-item *ngIf="interface.addresses['tor-address'] as tor">
<ion-label class="ion-text-wrap">
<h2>Tor Address</h2>
<p>{{ tor }}</p>
</ion-label>
<ion-buttons slot="end">
<ion-button *ngIf="interface.def.ui" fill="clear" (click)="launch(tor)">
<ion-icon size="small" slot="icon-only" name="open-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" (click)="copy(tor)">
<ion-icon size="small" slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-item>
<!-- no tor -->
<ion-item *ngIf="!interface.addresses['tor-address']">
<ion-label class="ion-text-wrap">
<h2>Tor Address</h2>
<p>Service does not use a Tor Address</p>
</ion-label>
</ion-item>
<!-- lan -->
<ion-item *ngIf="interface.addresses['lan-address'] as lan">
<ion-label class="ion-text-wrap">
<h2>LAN Address</h2>
<p>{{ lan }}</p>
</ion-label>
<ion-buttons slot="end">
<ion-button *ngIf="interface.def.ui" fill="clear" (click)="launch(lan)">
<ion-icon size="small" slot="icon-only" name="open-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" (click)="copy(lan)">
<ion-icon size="small" slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-item>
<!-- no lan -->
<ion-item *ngIf="!interface.addresses['lan-address']">
<ion-label class="ion-text-wrap">
<h2>LAN Address</h2>
<p>Service does not use a LAN Address</p>
</ion-label>
</ion-item>
</div>

View File

@@ -2,8 +2,7 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { AppInterfacesPage } from './app-interfaces.page' import { AppInterfacesItemComponent, AppInterfacesPage } from './app-interfaces.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
const routes: Routes = [ const routes: Routes = [
@@ -18,9 +17,11 @@ const routes: Routes = [
CommonModule, CommonModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule,
SharingModule, SharingModule,
], ],
declarations: [AppInterfacesPage], declarations: [
AppInterfacesPage,
AppInterfacesItemComponent,
],
}) })
export class AppInterfacesPageModule { } export class AppInterfacesPageModule { }

View File

@@ -7,37 +7,20 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content *ngIf="patch.data['package-data'][pkgId] as pkg"> <ion-content class="ion-padding-top">
<ion-card *ngFor="let interface of pkg.manifest.interfaces | keyvalue: asIsOrder"> <ion-item-group>
<ion-card-header> <!-- iff ui -->
<ion-card-title>{{ interface.value.name }}</ion-card-title> <ng-container *ngIf="ui">
<ion-card-subtitle>{{ interface.value.description }}</ion-card-subtitle> <ion-item-divider>Web User Interface</ion-item-divider>
<ion-button style="margin-top: 12px;" *ngIf="interface.value.ui" [disabled]="!(pkg | isLaunchable)" fill="outline" color="dark" expand="block" (click)="launch(pkg)"> <app-interfaces-item [interface]="ui"></app-interfaces-item>
Launch </ng-container>
<ion-icon slot="end" name="rocket-outline"></ion-icon>
</ion-button> <!-- other interface -->
</ion-card-header> <ng-container *ngIf="other.length">
<ion-card-content> <ion-item-divider>Other Interfaces</ion-item-divider>
<ng-container *ngIf="pkg.installed['interface-info'].addresses[interface.key] as int"> <div *ngFor="let interface of other" style="margin-bottom: 30px;">
<ion-item> <app-interfaces-item [interface]="interface"></app-interfaces-item>
<ion-label class="ion-text-wrap"> </div>
<h2>Tor Address</h2> </ng-container>
<p>{{ 'http://' + int['tor-address'] }}</p> </ion-item-group>
</ion-label>
<ion-button slot="end" fill="clear" (click)="copy('http://' + int['tor-address'])">
<ion-icon size="small" slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
<ion-item>
<ion-label class="ion-text-wrap">
<h2>LAN Address</h2>
<p>{{ 'https://' + int['lan-address'] }}</p>
</ion-label>
<ion-button slot="end" fill="clear" (click)="copy('https://' + int['lan-address'])">
<ion-icon size="small" slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
</ng-container>
</ion-card-content>
</ion-card>
</ion-content> </ion-content>

View File

@@ -1,38 +1,87 @@
import { Component, ViewChild } from '@angular/core' import { Component, Input, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { IonContent, ToastController } from '@ionic/angular' import { IonContent, ToastController } from '@ionic/angular'
import { Subscription } from 'rxjs' import { InterfaceDef, InterfaceInfo } from 'src/app/services/patch-db/data-model'
import { InstalledPackageDataEntry, PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { ConfigService } from 'src/app/services/config.service'
import { copyToClipboard } from 'src/app/util/web.util' import { copyToClipboard } from 'src/app/util/web.util'
interface LocalInterface {
def: InterfaceDef
addresses: InterfaceInfo['addresses'][string]
}
@Component({ @Component({
selector: 'app-Interfaces', selector: 'app-interfaces',
templateUrl: './app-Interfaces.page.html', templateUrl: './app-interfaces.page.html',
styleUrls: ['./app-Interfaces.page.scss'], styleUrls: ['./app-interfaces.page.scss'],
}) })
export class AppInterfacesPage { export class AppInterfacesPage {
pkg: PackageDataEntry
@ViewChild(IonContent) content: IonContent @ViewChild(IonContent) content: IonContent
pkgId: string ui: LocalInterface | null
other: LocalInterface[]
constructor ( constructor (
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
private readonly toastCtrl: ToastController,
private readonly config: ConfigService,
public readonly patch: PatchDbService, public readonly patch: PatchDbService,
) { } ) { }
ngOnInit () { ngOnInit () {
this.pkgId = this.route.snapshot.paramMap.get('pkgId') const pkgId = this.route.snapshot.paramMap.get('pkgId')
const pkg = this.patch.data['package-data'][pkgId]
const interfaces = pkg.manifest.interfaces
const addressesMap = pkg.installed['interface-info'].addresses
const ui = interfaces['ui']
if (ui) {
const uiAddresses = addressesMap['ui']
this.ui = {
def: ui,
addresses: {
'lan-address': uiAddresses['lan-address'] ? 'https://' + uiAddresses['lan-address'] : null,
'tor-address': uiAddresses['tor-address'] ? 'http://' + uiAddresses['tor-address'] : null,
},
}
}
this.other = Object.keys(interfaces)
.filter(key => key !== 'ui')
.map(key => {
const addresses = addressesMap[key]
return {
def: interfaces[key],
addresses: {
'lan-address': addresses['lan-address'] ? 'https://' + addresses['lan-address'] : null,
'tor-address': addresses['tor-address'] ? 'http://' + addresses['tor-address'] : null,
},
}
})
} }
ngAfterViewInit () { ngAfterViewInit () {
this.content.scrollToPoint(undefined, 1) this.content.scrollToPoint(undefined, 1)
} }
asIsOrder () {
return 0
}
}
@Component({
selector: 'app-interfaces-item',
templateUrl: './app-interfaces-item.component.html',
styleUrls: ['./app-interfaces.page.scss'],
})
export class AppInterfacesItemComponent {
@Input() interface: LocalInterface
constructor (
private readonly toastCtrl: ToastController,
) { }
launch (url: string): void {
window.open(url, '_blank')
}
async copy (address: string): Promise<void> { async copy (address: string): Promise<void> {
let message = '' let message = ''
await copyToClipboard(address || '') await copyToClipboard(address || '')
@@ -45,12 +94,4 @@ export class AppInterfacesPage {
}) })
await toast.present() await toast.present()
} }
launch (pkg: PackageDataEntry): void {
window.open(this.config.launchableURL(pkg), '_blank')
}
asIsOrder () {
return 0
}
} }

View File

@@ -26,7 +26,7 @@
<ion-card class="installed-card" [routerLink]="['/services', pkg.value.entry.manifest.id]"> <ion-card class="installed-card" [routerLink]="['/services', pkg.value.entry.manifest.id]">
<div class="launch-container" *ngIf="pkg.value.entry | hasUi"> <div class="launch-container" *ngIf="pkg.value.entry | hasUi">
<div class="launch-button-triangle" (click)="launchUi(pkg.value.entry, $event)" [class.launch-disabled]="!(pkg.value.entry | isLaunchable)"> <div class="launch-button-triangle" (click)="launchUi(pkg.value.entry, $event)" [class.launch-disabled]="!(pkg.value.entry | isLaunchable)">
<ion-icon name="rocket-outline"></ion-icon> <ion-icon name="open-outline"></ion-icon>
</div> </div>
</div> </div>

View File

@@ -3,8 +3,7 @@ import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { AppLogsPage } from './app-logs.page' import { AppLogsPage } from './app-logs.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' import { SharingModule } from 'src/app/modules/sharing.module'
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -18,8 +17,7 @@ const routes: Routes = [
CommonModule, CommonModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule, SharingModule,
TextSpinnerComponentModule,
], ],
declarations: [AppLogsPage], declarations: [AppLogsPage],
}) })

View File

@@ -1,28 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { AppManifestPage } from './app-manifest.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { SharingModule } from 'src/app/modules/sharing.module'
import { FormsModule } from '@angular/forms'
const routes: Routes = [
{
path: '',
component: AppManifestPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
FormsModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
SharingModule,
],
declarations: [AppManifestPage],
})
export class AppManifestPageModule { }

View File

@@ -1,67 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Package Manifest</ion-title>
</ion-toolbar>
<ion-toolbar>
<ion-segment [(ngModel)]="segmentValue">
<ion-segment-button value="formatted">
<ion-label>Formatted</ion-label>
</ion-segment-button>
<ion-segment-button value="raw">
<ion-label>Raw</ion-label>
</ion-segment-button>
</ion-segment>
</ion-toolbar>
</ion-header>
<ion-content *ngIf="pkg" class="ion-padding">
<div *ngIf="segmentValue === 'formatted'" style="background-color: var(--ion-color-light);">
<ion-toolbar>
<ion-title>Formatted Manifest</ion-title>
<ion-buttons slot="start" *ngIf="!!pointer">
<ion-button (click)="handleFormattedBack()">
<ion-icon slot="icon-only" name="arrow-back-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
<!-- node is object -->
<ion-item-group *ngIf="(node | typeof) === 'object'">
<div *ngFor="let prop of node | keyvalue : asIsOrder">
<!-- object/array -->
<ng-container *ngIf="['object', 'array'] | includes : (prop.value | typeof); else notObj">
<ion-item button detail="true" *ngIf="!(prop.value | empty)" (click)="goToNested(prop.key)">
<ion-label class="ion-text-wrap">
<h2>{{ prop.key }}</h2>
</ion-label>
</ion-item>
</ng-container>
<!-- not object/array -->
<ng-template #notObj>
<ion-item *ngIf="prop.value">
<ion-label class="ion-text-wrap">
<h2>{{ prop.key }}</h2>
<p>{{ prop.value }}</p>
</ion-label>
</ion-item>
</ng-template>
</div>
</ion-item-group>
<!-- node is array -->
<ion-item-group *ngIf="(node | typeof) === 'array'">
<ion-item *ngFor="let prop of node">
<ion-label class="ion-text-wrap">
{{ prop }}
</ion-label>
</ion-item>
</ion-item-group>
</div>
<div *ngIf="segmentValue === 'raw'" class="raw">
<pre [innerHTML]="pkg.manifest | json"></pre>
</div>
</ion-content>

View File

@@ -1,8 +0,0 @@
.raw {
background-color: var(--ion-color-light);
pre {
margin: 0;
padding: 12px;
white-space: pre-wrap;
}
}

View File

@@ -1,69 +0,0 @@
import { Component, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { Subscription } from 'rxjs'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import * as JsonPointer from 'json-pointer'
import { IonContent } from '@ionic/angular'
@Component({
selector: 'app-manifest',
templateUrl: './app-manifest.page.html',
styleUrls: ['./app-manifest.page.scss'],
})
export class AppManifestPage {
pkg: PackageDataEntry
pointer: string
node: object
segmentValue: 'formatted' | 'raw' = 'formatted'
@ViewChild(IonContent) content: IonContent
subs: Subscription[] = []
constructor (
private readonly route: ActivatedRoute,
private readonly patch: PatchDbService,
) { }
ngOnInit () {
const pkgId = this.route.snapshot.paramMap.get('pkgId')
this.subs = [
this.patch.watch$('package-data', pkgId)
.subscribe(pkg => {
this.pkg = pkg
this.setNode()
}),
]
this.setNode()
}
ngAfterViewInit () {
this.content.scrollToPoint(undefined, 1)
}
ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe())
}
handleFormattedBack () {
const arr = this.pointer.split('/')
arr.pop()
this.pointer = arr.join('/')
this.setNode()
}
private setNode () {
this.node = JsonPointer.get(this.pkg.manifest, this.pointer || '')
}
async goToNested (key: string): Promise<any> {
this.pointer = `${this.pointer || ''}/${key}`
this.setNode()
}
asIsOrder (a: any, b: any) {
return 0
}
}

View File

@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { AppMetricsPage } from './app-metrics.page' import { AppMetricsPage } from './app-metrics.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module' import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
@@ -19,7 +18,6 @@ const routes: Routes = [
CommonModule, CommonModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule,
SharingModule, SharingModule,
SkeletonListComponentModule, SkeletonListComponentModule,
], ],

View File

@@ -3,10 +3,8 @@ import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { AppPropertiesPage } from './app-properties.page' import { AppPropertiesPage } from './app-properties.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { QRComponentModule } from 'src/app/components/qr/qr.component.module' import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -20,10 +18,8 @@ const routes: Routes = [
CommonModule, CommonModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule,
QRComponentModule, QRComponentModule,
SharingModule, SharingModule,
TextSpinnerComponentModule,
], ],
declarations: [AppPropertiesPage], declarations: [AppPropertiesPage],
}) })

View File

@@ -5,8 +5,8 @@ import { IonicModule } from '@ionic/angular'
import { AppShowPage } from './app-show.page' import { AppShowPage } from './app-show.page'
import { StatusComponentModule } from 'src/app/components/status/status.component.module' import { StatusComponentModule } from 'src/app/components/status/status.component.module'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module' import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
import { AppConfigPageModule } from 'src/app/modals/app-config/app-config.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -19,11 +19,11 @@ const routes: Routes = [
imports: [ imports: [
CommonModule, CommonModule,
StatusComponentModule, StatusComponentModule,
SharingModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule,
InstallWizardComponentModule, InstallWizardComponentModule,
AppConfigPageModule,
SharingModule,
], ],
declarations: [AppShowPage], declarations: [AppShowPage],
}) })

View File

@@ -27,8 +27,8 @@
<status size="x-large" weight="500" [rendering]="rendering"></status> <status size="x-large" weight="500" [rendering]="rendering"></status>
</ion-label> </ion-label>
<ion-button slot="end" class="action-button" *ngIf="pkg.state === PackageState.Installed && (pkg | hasUi)" [disabled]="!(pkg | isLaunchable)" (click)="launchUiTab()"> <ion-button slot="end" class="action-button" *ngIf="pkg.state === PackageState.Installed && (pkg | hasUi)" [disabled]="!(pkg | isLaunchable)" (click)="launchUiTab()">
Web <ion-icon slot="start" name="open-outline"></ion-icon>
<ion-icon slot="end" name="rocket-outline"></ion-icon> Open UI
</ion-button> </ion-button>
<ion-button slot="end" class="action-button" *ngIf="rendering.feStatus === FeStatus.NeedsConfig" [routerLink]="['config']"> <ion-button slot="end" class="action-button" *ngIf="rendering.feStatus === FeStatus.NeedsConfig" [routerLink]="['config']">
Configure Configure
@@ -53,7 +53,7 @@
<ion-item *ngFor="let health of mainStatus.health | keyvalue : asIsOrder"> <ion-item *ngFor="let health of mainStatus.health | keyvalue : asIsOrder">
<ion-spinner class="icon-spinner" color="warning" slot="start" *ngIf="['starting', 'loading'] | includes : health.value.result"></ion-spinner> <ion-spinner class="icon-spinner" color="warning" slot="start" *ngIf="['starting', 'loading'] | includes : health.value.result"></ion-spinner>
<ion-icon slot="start" *ngIf="health.value.result === 'success'" name="checkmark-outline" color="success"></ion-icon> <ion-icon slot="start" *ngIf="health.value.result === 'success'" name="checkmark-outline" color="success"></ion-icon>
<ion-icon slot="start" *ngIf="health.value.result === 'failure'" name="close-outline" color="danger"></ion-icon> <ion-icon slot="start" *ngIf="health.value.result === 'failure'" name="close" color="danger"></ion-icon>
<ion-icon slot="start" *ngIf="health.value.result === 'disabled'" name="remove-outline" color="dark"></ion-icon> <ion-icon slot="start" *ngIf="health.value.result === 'disabled'" name="remove-outline" color="dark"></ion-icon>
<ion-label> <ion-label>
<p>{{ health.key }}</p> <p>{{ health.key }}</p>

View File

@@ -5,7 +5,7 @@
.action-button { .action-button {
margin: 10px; margin: 10px;
min-height: 36px; min-height: 36px;
min-width: 72px; min-width: 120px;
} }
.icon-spinner { .icon-spinner {

View File

@@ -2,7 +2,7 @@ import { Component, ViewChild } from '@angular/core'
import { AlertController, NavController, ModalController, IonContent, LoadingController } from '@ionic/angular' import { AlertController, NavController, ModalController, IonContent, LoadingController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
import { ActivatedRoute, NavigationExtras } from '@angular/router' import { ActivatedRoute, NavigationExtras } from '@angular/router'
import { chill, isEmptyObject, Recommendation } from 'src/app/util/misc.util' import { isEmptyObject, Recommendation } from 'src/app/util/misc.util'
import { combineLatest, Subscription } from 'rxjs' import { combineLatest, Subscription } from 'rxjs'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
@@ -12,6 +12,7 @@ import { DependencyErrorConfigUnsatisfied, DependencyErrorNotInstalled, Dependen
import { FEStatus, PkgStatusRendering, renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' import { FEStatus, PkgStatusRendering, renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
import { ConnectionService } from 'src/app/services/connection.service' import { ConnectionService } from 'src/app/services/connection.service'
import { ErrorToastService } from 'src/app/services/error-toast.service' import { ErrorToastService } from 'src/app/services/error-toast.service'
import { AppConfigPage } from 'src/app/modals/app-config/app-config.page'
@Component({ @Component({
selector: 'app-show', selector: 'app-show',
@@ -31,7 +32,6 @@ export class AppShowPage {
Math = Math Math = Math
mainStatus: MainStatus mainStatus: MainStatus
@ViewChild(IonContent) content: IonContent @ViewChild(IonContent) content: IonContent
subs: Subscription[] = [] subs: Subscription[] = []
@@ -64,21 +64,20 @@ export class AppShowPage {
this.patch.watch$('package-data', this.pkgId, 'installed', 'status', 'main') this.patch.watch$('package-data', this.pkgId, 'installed', 'status', 'main')
.subscribe(main => { .subscribe(main => {
this.mainStatus = main this.mainStatus = main
console.log(this.mainStatus)
}), }),
] ]
this.setButtons() this.setButtons()
} }
// ngAfterViewInit () { ngAfterViewInit () {
// this.content.scrollToPoint(undefined, 1) this.content.scrollToPoint(undefined, 1)
// } }
ngOnDestroy () { ngOnDestroy () {
this.subs.forEach(sub => sub.unsubscribe()) this.subs.forEach(sub => sub.unsubscribe())
} }
launchUiTab (): void { launchUi (): void {
window.open(this.config.launchableURL(this.pkg), '_blank') window.open(this.config.launchableURL(this.pkg), '_blank')
} }
@@ -94,8 +93,6 @@ export class AppShowPage {
try { try {
const breakages = await this.embassyApi.dryStopPackage({ id }) const breakages = await this.embassyApi.dryStopPackage({ id })
console.log('BREAKAGES', breakages)
if (!isEmptyObject(breakages)) { if (!isEmptyObject(breakages)) {
const { cancelled } = await wizardModal( const { cancelled } = await wizardModal(
this.modalCtrl, this.modalCtrl,
@@ -108,7 +105,7 @@ export class AppShowPage {
) )
if (cancelled) return if (cancelled) return
} }
return this.embassyApi.stopPackage({ id }).then(chill) await this.embassyApi.stopPackage({ id })
} catch (e) { } catch (e) {
this.errToast.present(e) this.errToast.present(e)
} finally { } finally {
@@ -156,8 +153,14 @@ export class AppShowPage {
} }
} }
asIsOrder () { async presentModalConfig (): Promise<void> {
return 0 const modal = await this.modalCtrl.create({
component: AppConfigPage,
componentProps: {
pkgId: this.pkgId,
},
})
await modal.present()
} }
private async installDep (depId: string): Promise<void> { private async installDep (depId: string): Promise<void> {
@@ -234,8 +237,9 @@ export class AppShowPage {
} }
} }
setButtons (): void { private setButtons (): void {
this.buttons = [ this.buttons = [
// instructions
{ {
action: () => this.navCtrl.navigateForward(['instructions'], { relativeTo: this.route }), action: () => this.navCtrl.navigateForward(['instructions'], { relativeTo: this.route }),
title: 'Instructions', title: 'Instructions',
@@ -243,13 +247,15 @@ export class AppShowPage {
color: 'danger', color: 'danger',
disabled: [], disabled: [],
}, },
// config
{ {
action: () => this.navCtrl.navigateForward(['config'], { relativeTo: this.route }), action: async () => this.presentModalConfig(),
title: 'Settings', title: 'Config',
icon: 'construct-outline', icon: 'construct-outline',
color: 'danger', color: 'danger',
disabled: [FEStatus.Installing, FEStatus.Updating, FEStatus.Removing, FEStatus.BackingUp, FEStatus.Restoring], disabled: [FEStatus.Installing, FEStatus.Updating, FEStatus.Removing, FEStatus.BackingUp, FEStatus.Restoring],
}, },
// properties
{ {
action: () => this.navCtrl.navigateForward(['properties'], { relativeTo: this.route }), action: () => this.navCtrl.navigateForward(['properties'], { relativeTo: this.route }),
title: 'Properties', title: 'Properties',
@@ -257,6 +263,7 @@ export class AppShowPage {
color: 'danger', color: 'danger',
disabled: [], disabled: [],
}, },
// interfaces
{ {
action: () => this.navCtrl.navigateForward(['interfaces'], { relativeTo: this.route }), action: () => this.navCtrl.navigateForward(['interfaces'], { relativeTo: this.route }),
title: 'Interfaces', title: 'Interfaces',
@@ -264,6 +271,7 @@ export class AppShowPage {
color: 'danger', color: 'danger',
disabled: [], disabled: [],
}, },
// actions
{ {
action: () => this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }), action: () => this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }),
title: 'Actions', title: 'Actions',
@@ -271,6 +279,7 @@ export class AppShowPage {
color: 'danger', color: 'danger',
disabled: [], disabled: [],
}, },
// metrics
{ {
action: () => this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }), action: () => this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }),
title: 'Monitor', title: 'Monitor',
@@ -279,6 +288,7 @@ export class AppShowPage {
// @TODO make the disabled check better. Don't want to list every status here. Monitor should be disabled except is pkg is running. // @TODO make the disabled check better. Don't want to list every status here. Monitor should be disabled except is pkg is running.
disabled: [FEStatus.Installing, FEStatus.Updating, FEStatus.Removing, FEStatus.BackingUp, FEStatus.Restoring], disabled: [FEStatus.Installing, FEStatus.Updating, FEStatus.Removing, FEStatus.BackingUp, FEStatus.Restoring],
}, },
// logs
{ {
action: () => this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }), action: () => this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }),
title: 'Logs', title: 'Logs',
@@ -286,22 +296,19 @@ export class AppShowPage {
color: 'danger', color: 'danger',
disabled: [], disabled: [],
}, },
{
action: () => this.navCtrl.navigateForward(['manifest'], { relativeTo: this.route }),
title: 'Package Details',
icon: 'finger-print-outline',
color: 'danger',
disabled: [],
},
{ {
action: () => this.donate(), action: () => this.donate(),
title: 'Donate', title: `Donate to ${this.pkg.manifest.title}`,
icon: 'logo-bitcoin', icon: 'logo-bitcoin',
color: 'danger', color: 'danger',
disabled: [], disabled: [],
}, },
] ]
} }
asIsOrder () {
return 0
}
} }
interface Button { interface Button {

View File

@@ -19,14 +19,6 @@ const routes: Routes = [
path: ':pkgId/actions', path: ':pkgId/actions',
loadChildren: () => import('./app-actions/app-actions.module').then(m => m.AppActionsPageModule), loadChildren: () => import('./app-actions/app-actions.module').then(m => m.AppActionsPageModule),
}, },
{
path: ':pkgId/config',
loadChildren: () => import('./app-config/app-config.module').then(m => m.AppConfigPageModule),
},
{
path: ':pkgId/config/:edit',
loadChildren: () => import('./app-config/app-config.module').then(m => m.AppConfigPageModule),
},
{ {
path: ':pkgId/instructions', path: ':pkgId/instructions',
loadChildren: () => import('./app-instructions/app-instructions.module').then(m => m.AppInstructionsPageModule), loadChildren: () => import('./app-instructions/app-instructions.module').then(m => m.AppInstructionsPageModule),
@@ -39,10 +31,6 @@ const routes: Routes = [
path: ':pkgId/logs', path: ':pkgId/logs',
loadChildren: () => import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule), loadChildren: () => import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule),
}, },
{
path: ':pkgId/manifest',
loadChildren: () => import('./app-manifest/app-manifest.module').then(m => m.AppManifestPageModule),
},
{ {
path: ':pkgId/metrics', path: ':pkgId/metrics',
loadChildren: () => import('./app-metrics/app-metrics.module').then(m => m.AppMetricsPageModule), loadChildren: () => import('./app-metrics/app-metrics.module').then(m => m.AppMetricsPageModule),

View File

@@ -3,7 +3,7 @@
<ion-row class="ion-align-items-center" style="height: 100%;"> <ion-row class="ion-align-items-center" style="height: 100%;">
<ion-col class="ion-text-center"> <ion-col class="ion-text-center">
<div style="padding-bottom: 32px;"> <div style="padding-bottom: 16px;">
<img src="assets/img/logo.png" style="max-width: 240px;" /> <img src="assets/img/logo.png" style="max-width: 240px;" />
</div> </div>

View File

@@ -35,6 +35,7 @@ export class LoginPage {
this.loader = await this.loadingCtrl.create({ this.loader = await this.loadingCtrl.create({
message: 'Logging in', message: 'Logging in',
spinner: 'lines', spinner: 'lines',
cssClass: 'loader',
}) })
await this.loader.present() await this.loader.present()

View File

@@ -3,9 +3,7 @@ import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { AppReleaseNotes } from './app-release-notes.page' import { AppReleaseNotes } from './app-release-notes.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -19,9 +17,7 @@ const routes: Routes = [
CommonModule, CommonModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule,
SharingModule, SharingModule,
TextSpinnerComponentModule,
], ],
declarations: [AppReleaseNotes], declarations: [AppReleaseNotes],
}) })

View File

@@ -6,8 +6,6 @@ import { MarketplaceListPage } from './marketplace-list.page'
import { SharingModule } from '../../../modules/sharing.module' import { SharingModule } from '../../../modules/sharing.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { StatusComponentModule } from 'src/app/components/status/status.component.module' import { StatusComponentModule } from 'src/app/components/status/status.component.module'
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -24,7 +22,6 @@ const routes: Routes = [
StatusComponentModule, StatusComponentModule,
SharingModule, SharingModule,
BadgeMenuComponentModule, BadgeMenuComponentModule,
TextSpinnerComponentModule,
], ],
declarations: [MarketplaceListPage], declarations: [MarketplaceListPage],
}) })

View File

@@ -4,10 +4,8 @@ import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { MarketplaceShowPage } from './marketplace-show.page' import { MarketplaceShowPage } from './marketplace-show.page'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { StatusComponentModule } from 'src/app/components/status/status.component.module' import { StatusComponentModule } from 'src/app/components/status/status.component.module'
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module' import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -21,10 +19,8 @@ const routes: Routes = [
CommonModule, CommonModule,
IonicModule, IonicModule,
StatusComponentModule, StatusComponentModule,
TextSpinnerComponentModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
SharingModule, SharingModule,
PwaBackComponentModule,
InstallWizardComponentModule, InstallWizardComponentModule,
], ],
declarations: [MarketplaceShowPage], declarations: [MarketplaceShowPage],

View File

@@ -81,8 +81,8 @@
<p style="color: var(--ion-color-dark); font-size: small">{{ rec.description }}</p> <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">{{ 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> <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" color="primary" fill="clear" (click)="dismissRec()"> <ion-button style="position: absolute; right: 0; top: 0" fill="clear" (click)="dismissRec()">
<ion-icon name="close-outline"></ion-icon> <ion-icon name="close"></ion-icon>
</ion-button> </ion-button>
</div> </div>
</ion-label> </ion-label>

View File

@@ -3,10 +3,8 @@ import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router' import { RouterModule, Routes } from '@angular/router'
import { NotificationsPage } from './notifications.page' import { NotificationsPage } from './notifications.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -20,10 +18,8 @@ const routes: Routes = [
CommonModule, CommonModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule,
BadgeMenuComponentModule, BadgeMenuComponentModule,
SharingModule, SharingModule,
TextSpinnerComponentModule,
], ],
declarations: [NotificationsPage], declarations: [NotificationsPage],
}) })

View File

@@ -17,41 +17,52 @@
<text-spinner *ngIf="loading" text="Loading Notifications"></text-spinner> <text-spinner *ngIf="loading" text="Loading Notifications"></text-spinner>
<ion-item-group *ngIf="!notifications.length && !loading"> <!-- no notifications -->
<ion-item> <ng-container *ngIf="!loading">
<ion-label class="ion-text-wrap"> <ion-item-group *ngIf="!notifications.length">
Notifications about Embassy and services will appear here. <ion-item>
</ion-label> <ion-label class="ion-text-wrap">
</ion-item> Notifications about Embassy and services will appear here.
</ion-item-group> </ion-label>
</ion-item>
<ion-item-group style="margin-bottom: 16px;"> </ion-item-group>
<ion-item *ngFor="let not of notifications; let i = index">
<ion-label class="ion-text-wrap"> <!-- has notifications -->
<h2> <ng-container *ngIf="notifications.length">
<ion-text [color]="not | notificationColor"><b>{{ not.title }}</b></ion-text> <ion-item-group style="margin-bottom: 16px;">
</h2> <ion-item-divider>
<h2 class="notification-message"> <ion-button slot="end" fill="clear" (click)="deleteAll()">
{{ not.message }} Delete All
<a *ngIf="not.code === 1" style="text-decoration: none;" (click)="viewBackupReport(not)"> </ion-button>
View Report </ion-item-divider>
</a> <ion-item *ngFor="let not of notifications; let i = index">
</h2> <ion-label class="ion-text-wrap">
<p> <h2>
{{ not['created-at'] | date: 'short' }} <ion-text [color]="not | notificationColor"><b>{{ not.title }}</b></ion-text>
<a *ngIf="not['package-id'] as pkgId" style="text-decoration: none;" [routerLink]="['/services', not['package-id']]"> </h2>
- {{ not['package-id'] }} <h2 class="notification-message">
</a> {{ not.message }}
</p> <a *ngIf="not.code === 1" style="text-decoration: none;" (click)="viewBackupReport(not)">
</ion-label> View Report
<ion-button slot="end" fill="clear" (click)="remove(not.id, i)"> </a>
<ion-icon slot="icon-only" name="close-outline"></ion-icon> </h2>
</ion-button> <p>
</ion-item> {{ not['created-at'] | date: 'short' }}
</ion-item-group> <a *ngIf="not['package-id'] as pkgId" style="text-decoration: none;" [routerLink]="['/services', not['package-id']]">
- {{ not['package-id'] }}
<ion-infinite-scroll [disabled]="!needInfinite" (ionInfinite)="doInfinite($event)"> </a>
<ion-infinite-scroll-content loadingSpinner="lines"></ion-infinite-scroll-content> </p>
</ion-infinite-scroll> </ion-label>
<ion-button slot="end" fill="clear" (click)="delete(not.id, i)">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-item>
</ion-item-group>
<ion-infinite-scroll [disabled]="!needInfinite" (ionInfinite)="doInfinite($event)">
<ion-infinite-scroll-content loadingSpinner="lines"></ion-infinite-scroll-content>
</ion-infinite-scroll>
</ng-container>
</ng-container>
</ion-content> </ion-content>

View File

@@ -57,7 +57,7 @@ export class NotificationsPage {
} }
} }
async remove (id: string, index: number): Promise<void> { async delete (id: string, index: number): Promise<void> {
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
spinner: 'lines', spinner: 'lines',
message: 'Deleting...', message: 'Deleting...',
@@ -75,6 +75,24 @@ export class NotificationsPage {
} }
} }
async deleteAll (): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Deleting...',
cssClass: 'loader',
})
await loader.present()
try {
await this.embassyApi.deleteAllNotifications({ })
this.notifications = []
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
async viewBackupReport (notification: ServerNotification<1>) { async viewBackupReport (notification: ServerNotification<1>) {
const data = notification.data const data = notification.data
@@ -99,9 +117,6 @@ export class NotificationsPage {
if (embassyFailed || packagesFailed) { if (embassyFailed || packagesFailed) {
buttons.push({ buttons.push({
text: 'Retry', text: 'Retry',
handler: () => {
console.log('retry backup')
},
}) })
} }

View File

@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { LANPage } from './lan.page' import { LANPage } from './lan.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
const routes: Routes = [ const routes: Routes = [
@@ -18,7 +17,6 @@ const routes: Routes = [
CommonModule, CommonModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule,
SharingModule, SharingModule,
], ],
declarations: [LANPage], declarations: [LANPage],

View File

@@ -3,8 +3,6 @@ import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { SecurityOptionsPage } from './security-options.page' import { SecurityOptionsPage } from './security-options.page'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
const routes: Routes = [ const routes: Routes = [
@@ -18,9 +16,7 @@ const routes: Routes = [
imports: [ imports: [
CommonModule, CommonModule,
IonicModule, IonicModule,
ObjectConfigComponentModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule,
SharingModule, SharingModule,
], ],
declarations: [ declarations: [

View File

@@ -11,23 +11,23 @@
<ion-item-group> <ion-item-group>
<ion-item-divider>General</ion-item-divider> <ion-item-divider>General</ion-item-divider>
<ion-item button (click)="presentModalValueEdit('shareStats', patch.data['server-info']['share-stats'])"> <ion-item button (click)="serverConfig.presentAlert('share-stats', server['share-stats'])">
<ion-label>Share Anonymous Statistics</ion-label> <ion-label>Share Anonymous Statistics</ion-label>
<ion-note slot="end">{{ patch.data['server-info']['share-stats'] }}</ion-note> <ion-note slot="end">{{ server['share-stats'] ? 'Enabled' : 'Disabled' }}</ion-note>
</ion-item> </ion-item>
<ion-item-divider>Marketplace</ion-item-divider> <ion-item-divider>Marketplace</ion-item-divider>
<ion-item button (click)="presentModalValueEdit('autoCheckUpdates', patch.data.ui['auto-check-updates'])"> <ion-item button (click)="serverConfig.presentAlert('auto-check-updates', patch.data.ui['auto-check-updates'])">
<ion-label>Auto Check for Updates</ion-label> <ion-label>Auto Check for Updates</ion-label>
<ion-note slot="end">{{ patch.data.ui['auto-check-updates'] }}</ion-note> <ion-note slot="end">{{ patch.data.ui['auto-check-updates'] ? 'Enabled' : 'Disabled' }}</ion-note>
</ion-item> </ion-item>
<ion-item button (click)="presentModalValueEdit('eosMarketplace', patch.data['server-info']['eos-marketplace'] === config.start9Marketplace.tor)"> <ion-item button (click)="serverConfig.presentAlert('eos-marketplace', server['eos-marketplace'] === config.start9Marketplace.tor)">
<ion-label>Tor Only Marketplace</ion-label> <ion-label>Tor Only Marketplace</ion-label>
<ion-note slot="end">{{ patch.data['server-info']['eos-marketplace'] === config.start9Marketplace.tor }}</ion-note> <ion-note slot="end">{{ server['eos-marketplace'] === config.start9Marketplace.tor ? 'Enabled' : 'Disabled' }}</ion-note>
</ion-item> </ion-item>
<!-- <ion-item button (click)="presentModalValueEdit('packageMarketplace', patch.data['server-info']['package-marketplace'])"> <!-- <ion-item button (click)="presentModalValueEdit('packageMarketplace', server['package-marketplace'])">
<ion-label>Package Marketplace</ion-label> <ion-label>Package Marketplace</ion-label>
<ion-note slot="end">{{ patch.data['server-info']['package-marketplace'] }}</ion-note> <ion-note slot="end">{{ server['package-marketplace'] }}</ion-note>
</ion-item> --> </ion-item> -->
<ion-item-divider>Security</ion-item-divider> <ion-item-divider>Security</ion-item-divider>

View File

@@ -1,7 +1,8 @@
import { Component } from '@angular/core' import { Component, ViewChild } from '@angular/core'
import { ServerConfigService } from 'src/app/services/server-config.service' import { ServerConfigService } from 'src/app/services/server-config.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { IonContent } from '@ionic/angular'
@Component({ @Component({
selector: 'security-options', selector: 'security-options',
@@ -9,14 +10,15 @@ import { ConfigService } from 'src/app/services/config.service'
styleUrls: ['./security-options.page.scss'], styleUrls: ['./security-options.page.scss'],
}) })
export class SecurityOptionsPage { export class SecurityOptionsPage {
@ViewChild(IonContent) content: IonContent
constructor ( constructor (
private readonly serverConfigService: ServerConfigService, public readonly serverConfig: ServerConfigService,
public readonly config: ConfigService, public readonly config: ConfigService,
public readonly patch: PatchDbService, public readonly patch: PatchDbService,
) { } ) { }
async presentModalValueEdit (key: string, current?: any): Promise<void> { ngAfterViewInit () {
await this.serverConfigService.presentModalValueEdit(key, current) this.content.scrollToPoint(undefined, 1)
} }
} }

View File

@@ -3,9 +3,7 @@ import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router' import { RouterModule, Routes } from '@angular/router'
import { SessionsPage } from './sessions.page' import { SessionsPage } from './sessions.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -19,9 +17,7 @@ const routes: Routes = [
CommonModule, CommonModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule,
SharingModule, SharingModule,
TextSpinnerComponentModule,
], ],
declarations: [SessionsPage], declarations: [SessionsPage],
}) })

View File

@@ -31,8 +31,9 @@
<h2>Last Active: {{ session.value['last-active'] | date : 'medium' }}</h2> <h2>Last Active: {{ session.value['last-active'] | date : 'medium' }}</h2>
<p>{{ session.value['user-agent'] }}</p> <p>{{ session.value['user-agent'] }}</p>
</ion-label> </ion-label>
<ion-button slot="end" fill="clear" (click)="presentAlertKill(session.key)"> <ion-button slot="end" fill="clear" color="danger" (click)="presentAlertKill(session.key)">
<ion-icon slot="icon-only" name="close-outline"></ion-icon> <ion-icon slot="start" name="close"></ion-icon>
Kill
</ion-button> </ion-button>
</ion-item> </ion-item>
</div> </div>

View File

@@ -2,7 +2,7 @@ import { Component } from '@angular/core'
import { AlertController, getPlatforms, LoadingController } from '@ionic/angular' import { AlertController, getPlatforms, LoadingController } from '@ionic/angular'
import { ErrorToastService } from 'src/app/services/error-toast.service' import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
import { PlatformType, RR, SessionMetadata } from 'src/app/services/api/api.types' import { PlatformType, RR } from 'src/app/services/api/api.types'
@Component({ @Component({
selector: 'sessions', selector: 'sessions',

View File

@@ -3,9 +3,7 @@ import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router' import { RouterModule, Routes } from '@angular/router'
import { SSHKeysPage } from './ssh-keys.page' import { SSHKeysPage } from './ssh-keys.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -19,9 +17,7 @@ const routes: Routes = [
CommonModule, CommonModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule,
SharingModule, SharingModule,
TextSpinnerComponentModule,
], ],
declarations: [SSHKeysPage], declarations: [SSHKeysPage],
}) })

View File

@@ -5,7 +5,7 @@
</ion-buttons> </ion-buttons>
<ion-title>SSH Keys</ion-title> <ion-title>SSH Keys</ion-title>
<ion-buttons slot="end"> <ion-buttons slot="end">
<ion-button (click)="presentModalAdd()"> <ion-button (click)="serverConfig.presentAlert('ssh')">
<ion-icon slot="icon-only" name="add-outline"></ion-icon> <ion-icon slot="icon-only" name="add-outline"></ion-icon>
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>
@@ -15,14 +15,25 @@
<ion-content class="ion-padding-top"> <ion-content class="ion-padding-top">
<text-spinner *ngIf="loading" text="Loading Keys"></text-spinner> <text-spinner *ngIf="loading" text="Loading Keys"></text-spinner>
<ion-item-group> <ion-item-group *ngIf="!loading">
<!-- about -->
<ion-item>
<ion-label class="ion-text-wrap">
<p class="ion-padding-bottom">About</p>
<h2>Adding an SSH key to your Embassy can be useful for advanced usage from the command line, as well as for debugging purposes.</h2>
</ion-label>
</ion-item>
<ion-item [href]="docsUrl" target="_blank" detail="false">
<ion-icon slot="start" name="list-outline"></ion-icon>
<ion-label>View Instructions</ion-label>
</ion-item>
<ion-item-divider>Saved Keys</ion-item-divider> <ion-item-divider>Saved Keys</ion-item-divider>
<ion-item *ngFor="let ssh of sshKeys | keyvalue : asIsOrder"> <ion-item *ngFor="let ssh of sshKeys | keyvalue : asIsOrder">
<ion-label class="ion-text-wrap"> <ion-label class="ion-text-wrap">
{{ ssh.value.alg }} {{ ssh.key }} {{ ssh.value.hostname }} {{ ssh.value.alg }} {{ ssh.key }} {{ ssh.value.hostname }}
</ion-label> </ion-label>
<ion-button slot="end" fill="clear" (click)="presentAlertDelete(ssh.key)"> <ion-button slot="end" fill="clear" (click)="presentAlertDelete(ssh.key)">
<ion-icon slot="icon-only" name="close-outline"></ion-icon> <ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button> </ion-button>
</ion-item> </ion-item>
</ion-item-group> </ion-item-group>

View File

@@ -15,13 +15,14 @@ export class SSHKeysPage {
loading = true loading = true
sshKeys: SSHKeys sshKeys: SSHKeys
subs: Subscription[] = [] subs: Subscription[] = []
readonly docsUrl = 'https://docs.start9.com/user-manual/general/developer-options/ssh-setup.html'
constructor ( constructor (
private readonly loadingCtrl: LoadingController, private readonly loadingCtrl: LoadingController,
private readonly errToast: ErrorToastService, private readonly errToast: ErrorToastService,
private readonly serverConfigService: ServerConfigService,
private readonly alertCtrl: AlertController, private readonly alertCtrl: AlertController,
private readonly sshService: SSHService, private readonly sshService: SSHService,
public readonly serverConfig: ServerConfigService,
) { } ) { }
async ngOnInit () { async ngOnInit () {
@@ -41,10 +42,6 @@ export class SSHKeysPage {
this.subs.forEach(sub => sub.unsubscribe()) this.subs.forEach(sub => sub.unsubscribe())
} }
async presentModalAdd () {
await this.serverConfigService.presentModalValueEdit('ssh')
}
async presentAlertDelete (hash: string) { async presentAlertDelete (hash: string) {
const alert = await this.alertCtrl.create({ const alert = await this.alertCtrl.create({
backdropDismiss: false, backdropDismiss: false,

View File

@@ -4,8 +4,7 @@ import { IonicModule } from '@ionic/angular'
import { ServerBackupPage } from './server-backup.page' import { ServerBackupPage } from './server-backup.page'
import { RouterModule, Routes } from '@angular/router' import { RouterModule, Routes } from '@angular/router'
import { BackupConfirmationComponentModule } from 'src/app/modals/backup-confirmation/backup-confirmation.component.module' import { BackupConfirmationComponentModule } from 'src/app/modals/backup-confirmation/backup-confirmation.component.module'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' import { SharingModule } from 'src/app/modules/sharing.module'
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -20,8 +19,7 @@ const routes: Routes = [
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
BackupConfirmationComponentModule, BackupConfirmationComponentModule,
PwaBackComponentModule, SharingModule,
TextSpinnerComponentModule,
], ],
declarations: [ declarations: [
ServerBackupPage, ServerBackupPage,

View File

@@ -3,8 +3,7 @@ import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { ServerLogsPage } from './server-logs.page' import { ServerLogsPage } from './server-logs.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' import { SharingModule } from 'src/app/modules/sharing.module'
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -18,8 +17,7 @@ const routes: Routes = [
CommonModule, CommonModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule, SharingModule,
TextSpinnerComponentModule,
], ],
declarations: [ServerLogsPage], declarations: [ServerLogsPage],
}) })

View File

@@ -3,8 +3,8 @@ import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { ServerMetricsPage } from './server-metrics.page' import { ServerMetricsPage } from './server-metrics.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module' import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
import { SharingModule } from 'src/app/modules/sharing.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -18,8 +18,8 @@ const routes: Routes = [
CommonModule, CommonModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule,
SkeletonListComponentModule, SkeletonListComponentModule,
SharingModule,
], ],
declarations: [ServerMetricsPage], declarations: [ServerMetricsPage],
}) })

View File

@@ -1,4 +1,5 @@
import { Component } from '@angular/core' import { Component, ViewChild } from '@angular/core'
import { IonContent } from '@ionic/angular'
import { Metrics } from 'src/app/services/api/api.types' import { Metrics } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
import { ErrorToastService } from 'src/app/services/error-toast.service' import { ErrorToastService } from 'src/app/services/error-toast.service'
@@ -13,6 +14,7 @@ export class ServerMetricsPage {
loading = true loading = true
going = false going = false
metrics: Metrics = { } metrics: Metrics = { }
@ViewChild(IonContent) content: IonContent
constructor ( constructor (
private readonly errToast: ErrorToastService, private readonly errToast: ErrorToastService,
@@ -23,6 +25,10 @@ export class ServerMetricsPage {
this.startDaemon() this.startDaemon()
} }
ngAfterViewInit () {
this.content.scrollToPoint(undefined, 1)
}
ngOnDestroy () { ngOnDestroy () {
this.stopDaemon() this.stopDaemon()
} }

View File

@@ -6,7 +6,6 @@ import { ServerShowPage } from './server-show.page'
import { StatusComponentModule } from 'src/app/components/status/status.component.module' import { StatusComponentModule } from 'src/app/components/status/status.component.module'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
const routes: Routes = [ const routes: Routes = [
@@ -24,7 +23,6 @@ const routes: Routes = [
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
SharingModule, SharingModule,
PwaBackComponentModule,
BadgeMenuComponentModule, BadgeMenuComponentModule,
], ],
declarations: [ServerShowPage], declarations: [ServerShowPage],

View File

@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { ServerSpecsPage } from './server-specs.page' import { ServerSpecsPage } from './server-specs.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
const routes: Routes = [ const routes: Routes = [
@@ -18,7 +17,6 @@ const routes: Routes = [
CommonModule, CommonModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule,
SharingModule, SharingModule,
], ],
declarations: [ServerSpecsPage], declarations: [ServerSpecsPage],

View File

@@ -1,8 +1,7 @@
import { Component } from '@angular/core' import { Component, ViewChild } from '@angular/core'
import { ToastController } from '@ionic/angular' import { IonContent, ToastController } from '@ionic/angular'
import { copyToClipboard } from 'src/app/util/web.util' import { copyToClipboard } from 'src/app/util/web.util'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { Subscription } from 'rxjs'
@Component({ @Component({
selector: 'server-specs', selector: 'server-specs',
@@ -10,13 +9,17 @@ import { Subscription } from 'rxjs'
styleUrls: ['./server-specs.page.scss'], styleUrls: ['./server-specs.page.scss'],
}) })
export class ServerSpecsPage { export class ServerSpecsPage {
subs: Subscription[] = [] @ViewChild(IonContent) content: IonContent
constructor ( constructor (
private readonly toastCtrl: ToastController, private readonly toastCtrl: ToastController,
public readonly patch: PatchDbService, public readonly patch: PatchDbService,
) { } ) { }
ngAfterViewInit () {
this.content.scrollToPoint(undefined, 1)
}
async copy (address: string) { async copy (address: string) {
let message = '' let message = ''
await copyToClipboard(address || '') await copyToClipboard(address || '')

View File

@@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router' import { RouterModule, Routes } from '@angular/router'
import { WifiAddPage } from './wifi-add.page' import { WifiAddPage } from './wifi-add.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' import { SharingModule } from 'src/app/modules/sharing.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -19,7 +19,7 @@ const routes: Routes = [
FormsModule, FormsModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule, SharingModule,
], ],
declarations: [WifiAddPage], declarations: [WifiAddPage],
}) })

View File

@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router' import { RouterModule, Routes } from '@angular/router'
import { WifiListPage } from './wifi.page' import { WifiListPage } from './wifi.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
const routes: Routes = [ const routes: Routes = [
@@ -22,7 +21,6 @@ const routes: Routes = [
CommonModule, CommonModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
PwaBackComponentModule,
SharingModule, SharingModule,
], ],
declarations: [WifiListPage], declarations: [WifiListPage],

View File

@@ -93,9 +93,9 @@ export class ConfigCursor<T extends ValueType> {
case 'number': case 'number':
return `${config}${spec.units ? ' ' + spec.units : ''}` return `${config}${spec.units ? ' ' + spec.units : ''}`
case 'object': case 'object':
return spec.displayAs ? handlebars.compile(spec.displayAs)(config) : '' return spec['display-as'] ? handlebars.compile(spec['display-as'])(config) : ''
case 'union': case 'union':
return spec.displayAs ? handlebars.compile(spec.displayAs)(config) : config[spec.tag.id] return spec['display-as'] ? handlebars.compile(spec['display-as'])(config) : config[spec.tag.id]
case 'pointer': case 'pointer':
return 'System Defined' return 'System Defined'
default: default:
@@ -121,11 +121,9 @@ export class ConfigCursor<T extends ValueType> {
let ret: ValueSpec = { let ret: ValueSpec = {
type: 'object', type: 'object',
spec: this.rootSpec, spec: this.rootSpec,
nullable: false,
nullByDefault: false,
name: 'Config', name: 'Config',
displayAs: 'Config', 'display-as': 'Config',
uniqueBy: null, 'unique-by': null,
} }
let ptr = [] let ptr = []
for (let seg of parsed) { for (let seg of parsed) {
@@ -141,7 +139,7 @@ export class ConfigCursor<T extends ValueType> {
values: Object.keys(ret.variants), values: Object.keys(ret.variants),
name: ret.tag.name, name: ret.tag.name,
description: ret.tag.description, description: ret.tag.description,
valueNames: ret.tag.variantNames, 'value-names': ret.tag['variant-names'],
} }
} else { } else {
const cfg = this.unseek().seek(pointer.compile(ptr)) const cfg = this.unseek().seek(pointer.compile(ptr))
@@ -176,7 +174,7 @@ export class ConfigCursor<T extends ValueType> {
if (!spec.pattern || new RegExp(spec.pattern).test(cfg)) { if (!spec.pattern || new RegExp(spec.pattern).test(cfg)) {
return null return null
} else { } else {
return spec.patternDescription return spec['pattern-description']
} }
} else { } else {
throw new TypeError(`${this.ptr}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`) throw new TypeError(`${this.ptr}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
@@ -205,8 +203,8 @@ export class ConfigCursor<T extends ValueType> {
} }
case 'enum': case 'enum':
if (typeof cfg === 'string') { if (typeof cfg === 'string') {
spec.valuesSet = spec.valuesSet || new Set(spec.values) spec['values-set'] = spec['values-set'] || new Set(spec.values)
return spec.valuesSet.has(cfg) ? null : `${cfg} is not a valid selection.` return spec['values-set'].has(cfg) ? null : `${cfg} is not a valid selection.`
} else { } else {
throw new TypeError(`${this.ptr}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`) throw new TypeError(`${this.ptr}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
} }
@@ -233,7 +231,7 @@ export class ConfigCursor<T extends ValueType> {
if (cursor.equals(this.seekNext(idx2))) { if (cursor.equals(this.seekNext(idx2))) {
return `Item #${idx + 1} is not unique.` + ('uniqueBy' in cursor.spec()) ? `${ return `Item #${idx + 1} is not unique.` + ('uniqueBy' in cursor.spec()) ? `${
displayUniqueBy( displayUniqueBy(
(cursor.spec() as ValueSpecObject | ValueSpecUnion).uniqueBy, (cursor.spec() as ValueSpecObject | ValueSpecUnion)['unique-by'],
(cursor.spec() as ValueSpecObject | ValueSpecUnion), (cursor.spec() as ValueSpecObject | ValueSpecUnion),
cursor.config(), cursor.config(),
) )
@@ -247,7 +245,7 @@ export class ConfigCursor<T extends ValueType> {
} }
case 'object': case 'object':
if (!cfg) { if (!cfg) {
return spec.nullable ? null : `${spec.name} is missing.` return `${spec.name} is missing.`
} else if (typeof cfg === 'object' && !Array.isArray(cfg)) { } else if (typeof cfg === 'object' && !Array.isArray(cfg)) {
for (let idx in spec.spec) { for (let idx in spec.spec) {
if (this.seekNext(idx).checkInvalid()) { if (this.seekNext(idx).checkInvalid()) {
@@ -328,7 +326,7 @@ export class ConfigCursor<T extends ValueType> {
return lhs === rhs return lhs === rhs
case 'object': case 'object':
case 'union': case 'union':
return isEqual(spec.uniqueBy, this as ConfigCursor<'object' | 'union'>, cursor as ConfigCursor<'object' | 'union'>) return isEqual(spec['unique-by'], this as ConfigCursor<'object' | 'union'>, cursor as ConfigCursor<'object' | 'union'>)
case 'list': case 'list':
if (lhs.length !== rhs.length) { if (lhs.length !== rhs.length) {
return false return false

View File

@@ -52,14 +52,12 @@ export interface ValueSpecPointer extends WithStandalone {
export interface ValueSpecObject extends ListValueSpecObject, WithStandalone { export interface ValueSpecObject extends ListValueSpecObject, WithStandalone {
type: 'object' type: 'object'
nullable: boolean
nullByDefault: boolean
} }
export interface WithStandalone { export interface WithStandalone {
name: string name: string
description?: string description?: string
changeWarning?: string 'change-warning'?: string
} }
// no lists of booleans, lists, pointers // no lists of booleans, lists, pointers
@@ -90,8 +88,9 @@ export function isValueSpecListOf<S extends ListValueSpecType> (t: ValueSpecList
} }
export interface ListValueSpecString { export interface ListValueSpecString {
// @TODO add masked?
pattern?: string pattern?: string
patternDescription?: string 'pattern-description'?: string
} }
export interface ListValueSpecNumber { export interface ListValueSpecNumber {
@@ -102,14 +101,14 @@ export interface ListValueSpecNumber {
export interface ListValueSpecEnum { export interface ListValueSpecEnum {
values: string[] values: string[]
valuesSet?: Set<string> 'values-set'?: Set<string>
valueNames: { [value: string]: string } 'value-names': { [value: string]: string }
} }
export interface ListValueSpecObject { 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 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 'unique-by': 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' 'display-as'?: 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 type UniqueBy = null | string | { any: UniqueBy[] } | { all: UniqueBy[] }
@@ -117,16 +116,16 @@ export type UniqueBy = null | string | { any: UniqueBy[] } | { all: UniqueBy[] }
export interface ListValueSpecUnion { export interface ListValueSpecUnion {
tag: UnionTagSpec tag: UnionTagSpec
variants: { [key: string]: ConfigSpec } 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 'display-as'?: 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 'unique-by': 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 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 { export interface UnionTagSpec {
id: string //The name of the field containing one of the union variants id: string // The name of the field containing one of the union variants
name: string name: string
description?: string description?: string
variantNames: { //the name of each variant 'variant-names': { // the name of each variant
[variant: string]: string [variant: string]: string
} }
} }

View File

@@ -114,7 +114,7 @@ export function listInnerSpec (listSpec: ValueSpecList): ValueSpecOf<ListValueSp
nullable: false, nullable: false,
name: listSpec.name, name: listSpec.name,
description: listSpec.description, description: listSpec.description,
changeWarning: listSpec.changeWarning, changeWarning: listSpec['change-warning'],
...listSpec.spec as any, //listSpec.spec is a ListValueSpecOf listSpec.subtype ...listSpec.spec as any, //listSpec.spec is a ListValueSpecOf listSpec.subtype
} }
} }
@@ -162,7 +162,7 @@ export function mapUnionSpec (spec: ValueSpecUnion, value: any): object {
type: 'enum', type: 'enum',
default: spec.default, default: spec.default,
values: Object.keys(spec.variants), values: Object.keys(spec.variants),
valueNames: spec.tag.variantNames, 'value-names': spec.tag['variant-names'],
}, value[spec.tag.id]) }, value[spec.tag.id])
value = mapConfigSpec(spec.variants[variant], value) value = mapConfigSpec(spec.variants[variant], value)
value[spec.tag.id] = variant value[spec.tag.id] = variant
@@ -216,7 +216,7 @@ export function mapBooleanSpec (spec: ValueSpecBoolean, value: any): boolean {
export function getDefaultConfigValue (spec: ValueSpec): string | number | object | string[] | number[] | object[] | boolean | null { export function getDefaultConfigValue (spec: ValueSpec): string | number | object | string[] | number[] | object[] | boolean | null {
switch (spec.type) { switch (spec.type) {
case 'object': case 'object':
return spec.nullByDefault ? null : getDefaultObject(spec.spec) return getDefaultObject(spec.spec)
case 'union': case 'union':
return getDefaultUnion(spec) return getDefaultUnion(spec)
case 'string': case 'string':
@@ -306,7 +306,7 @@ export function getDefaultDescription (spec: ValueSpec): string {
toReturn = spec.default === true ? 'True' : 'False' toReturn = spec.default === true ? 'True' : 'False'
break break
case 'enum': case 'enum':
toReturn = spec.valueNames[spec.default] toReturn = spec['value-names'][spec.default]
break break
} }

Some files were not shown because too many files have changed in this diff Show More