mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
Subnav (#391)
* 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:
committed by
Aiden McClelland
parent
a43ff976a2
commit
5741cf084f
@@ -70,7 +70,6 @@
|
||||
<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="close"></ion-icon>
|
||||
<ion-icon name="close-outline"></ion-icon>
|
||||
<ion-icon name="code-outline"></ion-icon>
|
||||
<ion-icon name="color-wand-outline"></ion-icon>
|
||||
<ion-icon name="construct-outline"></ion-icon>
|
||||
@@ -93,7 +92,6 @@
|
||||
<ion-icon name="medkit-outline"></ion-icon>
|
||||
<ion-icon name="newspaper-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="play-circle-outline"></ion-icon>
|
||||
<ion-icon name="power"></ion-icon>
|
||||
|
||||
@@ -214,7 +214,6 @@ export class AppComponent {
|
||||
takeWhile(() => auth === AuthState.VERIFIED),
|
||||
)
|
||||
.subscribe(version => {
|
||||
console.log('VERSIONS', this.config.version, version)
|
||||
if (this.emver.compare(this.config.version, version) !== 0) {
|
||||
this.presentAlertRefreshNeeded()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { RouteReuseStrategy } from '@angular/router'
|
||||
import { IonicModule, IonicRouteStrategy } from '@ionic/angular'
|
||||
import { IonicModule, IonicRouteStrategy, IonNav } from '@ionic/angular'
|
||||
import { Drivers } from '@ionic/storage'
|
||||
import { IonicStorageModule } from '@ionic/storage-angular'
|
||||
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 { LocalStorageBootstrap } from './services/patch-db/local-storage-bootstrap'
|
||||
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 { APP_CONFIG_COMPONENT_MAPPING } from './services/sub-nav.service'
|
||||
import { FormBuilder } from '@angular/forms'
|
||||
import { FormService } from './services/form.service'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
@@ -42,6 +44,8 @@ import { MarketplaceApiService } from './services/api/marketplace/marketplace-ap
|
||||
SharingModule,
|
||||
],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
IonNav,
|
||||
Storage,
|
||||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
|
||||
{ provide: ApiService , useFactory: ApiServiceFactory, deps: [ConfigService, HttpService] }, { provide: ApiService , useFactory: ApiServiceFactory, deps: [ConfigService, HttpService] },
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-label>
|
||||
</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>
|
||||
<!-- description -->
|
||||
<ion-item *ngIf="spec.description">
|
||||
@@ -19,12 +19,12 @@
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- warning -->
|
||||
<ion-item *ngIf="spec.changeWarning">
|
||||
<ion-item *ngIf="spec['change-warning']">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p>
|
||||
<ion-text color="warning">Warning!</ion-text>
|
||||
</p>
|
||||
<p [innerHTML]="spec.changeWarning | markdown"></p>
|
||||
<p [innerHTML]="spec['change-warning'] | markdown"></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
@@ -0,0 +1,9 @@
|
||||
<ion-icon *ngIf="data.spec.description" class="help-icon" name="help-circle-outline" (click)="presentAlertDescription()"></ion-icon>
|
||||
<span> {{ data.spec.name }}</span>
|
||||
|
||||
<ion-text color="success" *ngIf="data.isNew"> (New)</ion-text>
|
||||
<ion-text color="warning" *ngIf="data.isEdited"> (Edited)</ion-text>
|
||||
|
||||
<span *ngIf="(['string', 'number'] | includes : data.spec.type) && !data.spec.nullable"> *</span>
|
||||
|
||||
<span *ngIf="data.spec.type === 'list' && Range.from(data.spec.range).min"> *</span>
|
||||
193
ui/src/app/components/form-object/form-object.component.html
Normal file
193
ui/src/app/components/form-object/form-object.component.html
Normal 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>
|
||||
@@ -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 { }
|
||||
41
ui/src/app/components/form-object/form-object.component.scss
Normal file
41
ui/src/app/components/form-object/form-object.component.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
223
ui/src/app/components/form-object/form-object.component.ts
Normal file
223
ui/src/app/components/form-object/form-object.component.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -39,11 +39,11 @@
|
||||
<!-- 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-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 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-icon *ngIf="!cancel.text" name="close-outline"></ion-icon>
|
||||
<ion-icon *ngIf="!cancel.text" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
<!-- next/finish buttons -->
|
||||
|
||||
@@ -139,7 +139,7 @@ export class WizardBaker {
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: { afterLoading: { text: 'Cancel' } }, next: 'Update OS',
|
||||
cancel: { afterLoading: { text: 'Cancel' } }, next: 'Begin Update',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
*ngFor="let keyval of (spec.type === 'object' ? spec.spec : spec.variants[value[spec.tag.id]]) | keyvalue: asIsOrder"
|
||||
[key]="keyval.key"
|
||||
[spec]="keyval.value"
|
||||
[value]="value[keyval.key]"
|
||||
@@ -7,4 +8,4 @@
|
||||
(onClick)="handleClick(keyval.key)"
|
||||
[class.add-margin]="keyval.key === 'advanced'"
|
||||
></object-config-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
@@ -10,28 +10,11 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.status-icon{
|
||||
// width: 2%;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.bright {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
color: var(--ion-color-danger) !important;
|
||||
}
|
||||
|
||||
.organizer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
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 { ModalPresentable } from 'src/app/pkg-config/modal-presentable'
|
||||
import { ValueSpecOf, ValueSpec } from 'src/app/pkg-config/config-types'
|
||||
import { MaskPipe } from 'src/app/pipes/mask.pipe'
|
||||
import { IonNav } from '@ionic/angular'
|
||||
import { SubNavService } from 'src/app/services/sub-nav.service'
|
||||
|
||||
@Component({
|
||||
selector: 'object-config',
|
||||
templateUrl: './object-config.component.html',
|
||||
styleUrls: ['./object-config.component.scss'],
|
||||
})
|
||||
export class ObjectConfigComponent extends ModalPresentable {
|
||||
export class ObjectConfigComponent {
|
||||
@Input() cursor: ConfigCursor<'object' | 'union'>
|
||||
@Output() onEdit = new EventEmitter<boolean>()
|
||||
spec: ValueSpecOf<'object' | 'union'>
|
||||
@@ -19,10 +19,9 @@ export class ObjectConfigComponent extends ModalPresentable {
|
||||
annotations: Annotations<'object' | 'union'>
|
||||
|
||||
constructor (
|
||||
trackingModalCtrl: TrackingModalController,
|
||||
) {
|
||||
super(trackingModalCtrl)
|
||||
}
|
||||
private readonly subNav: SubNavService,
|
||||
private readonly nav: IonNav,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.spec = this.cursor.spec()
|
||||
@@ -33,11 +32,7 @@ export class ObjectConfigComponent extends ModalPresentable {
|
||||
async handleClick (key: string) {
|
||||
const nextCursor = this.cursor.seekNext(key)
|
||||
nextCursor.createFirstEntryForList()
|
||||
|
||||
await this.presentModal(nextCursor, () => {
|
||||
this.onEdit.emit(true)
|
||||
this.annotations = this.cursor.getAnnotations()
|
||||
})
|
||||
this.subNav.push(key, nextCursor, this.nav)
|
||||
}
|
||||
|
||||
asIsOrder () {
|
||||
@@ -50,7 +45,6 @@ export class ObjectConfigComponent extends ModalPresentable {
|
||||
templateUrl: './object-config-item.component.html',
|
||||
styleUrls: ['./object-config.component.scss'],
|
||||
})
|
||||
|
||||
export class ObjectConfigItemComponent {
|
||||
@Input() key: string
|
||||
@Input() spec: ValueSpec
|
||||
@@ -84,7 +78,7 @@ export class ObjectConfigItemComponent {
|
||||
}
|
||||
break
|
||||
case 'enum':
|
||||
this.displayValue = this.spec.valueNames[this.value]
|
||||
this.displayValue = this.spec['value-names'][this.value]
|
||||
break
|
||||
case 'pointer':
|
||||
this.displayValue = 'System Defined'
|
||||
|
||||
@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'
|
||||
import { PwaBackComponent } from './pwa-back.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -13,7 +12,6 @@ import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild([]),
|
||||
SharingModule,
|
||||
],
|
||||
exports: [PwaBackComponent],
|
||||
})
|
||||
|
||||
18
ui/src/app/components/sub-nav/sub-nav.component.html
Normal file
18
ui/src/app/components/sub-nav/sub-nav.component.html
Normal 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>
|
||||
16
ui/src/app/components/sub-nav/sub-nav.component.module.ts
Normal file
16
ui/src/app/components/sub-nav/sub-nav.component.module.ts
Normal 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 { }
|
||||
23
ui/src/app/components/sub-nav/sub-nav.component.ts
Normal file
23
ui/src/app/components/sub-nav/sub-nav.component.ts
Normal 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]
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,17 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppActionInputPage } from './app-action-input.page'
|
||||
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
|
||||
import { ConfigHeaderComponentModule } from 'src/app/components/config-header/config-header.component.module'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { FormObjectComponentModule } from 'src/app/components/form-object/form-object.component.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppActionInputPage],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
ObjectConfigComponentModule,
|
||||
ConfigHeaderComponentModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
FormObjectComponentModule,
|
||||
],
|
||||
entryComponents: [AppActionInputPage],
|
||||
exports: [AppActionInputPage],
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
<ion-header>
|
||||
<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-buttons 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>
|
||||
|
||||
<!-- object -->
|
||||
<ion-item-group>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<object-config [cursor]="cursor" (onEdit)="handleObjectEdit()"></object-config>
|
||||
</ion-item-group>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<form [formGroup]="actionForm" (ngSubmit)="save()" novalidate>
|
||||
<form-object
|
||||
[objectSpec]="action['input-spec']"
|
||||
[formGroup]="actionForm"
|
||||
></form-object>
|
||||
</form>
|
||||
</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>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
button:disabled,
|
||||
button[disabled]{
|
||||
border: 1px solid #999999;
|
||||
background-color: #cccccc;
|
||||
color: #666666;
|
||||
}
|
||||
button {
|
||||
color: var(--ion-color-primary);
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { LoadingController, ModalController } from '@ionic/angular'
|
||||
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { Action } from 'src/app/services/patch-db/data-model'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-action-input',
|
||||
@@ -12,22 +11,15 @@ import { Action } from 'src/app/services/patch-db/data-model'
|
||||
})
|
||||
export class AppActionInputPage {
|
||||
@Input() action: Action
|
||||
@Input() cursor: ConfigCursor<'object'>
|
||||
@Input() execute: () => Promise<void>
|
||||
spec: ValueSpecObject
|
||||
value: object
|
||||
error: string
|
||||
actionForm: FormGroup
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly formService: FormService,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.spec = this.cursor.spec()
|
||||
this.value = this.cursor.config()
|
||||
this.error = this.cursor.checkInvalid()
|
||||
this.actionForm = this.formService.createForm(this.action['input-spec'])
|
||||
}
|
||||
|
||||
async dismiss (): Promise<void> {
|
||||
@@ -35,24 +27,15 @@ export class AppActionInputPage {
|
||||
}
|
||||
|
||||
async save (): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Executing action',
|
||||
cssClass: 'loader-ontop-of-all',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.execute()
|
||||
this.modalCtrl.dismiss()
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
if (this.actionForm.invalid) {
|
||||
this.actionForm.markAllAsTouched()
|
||||
document.getElementsByClassName('validation-error')[0].parentElement.parentElement.scrollIntoView({ behavior: 'smooth' })
|
||||
return
|
||||
}
|
||||
this.modalCtrl.dismiss(this.actionForm.value)
|
||||
}
|
||||
|
||||
handleObjectEdit (): void {
|
||||
this.error = this.cursor.checkInvalid()
|
||||
asIsOrder () {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
<ion-header>
|
||||
<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>
|
||||
<ion-content class="subheader-padding">
|
||||
|
||||
<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 -->
|
||||
<ion-item-group *ngIf="spec.subtype === 'enum'">
|
||||
<ion-item-divider class="borderless"></ion-item-divider>
|
||||
@@ -46,7 +34,7 @@
|
||||
</ion-item-divider>
|
||||
|
||||
<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: '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>
|
||||
@@ -55,7 +43,7 @@
|
||||
<ion-label>{{ valueString[i] }}</ion-label>
|
||||
|
||||
<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-item>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
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 { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
|
||||
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||
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({
|
||||
selector: 'app-config-list',
|
||||
templateUrl: './app-config-list.page.html',
|
||||
styleUrls: ['./app-config-list.page.scss'],
|
||||
})
|
||||
export class AppConfigListPage extends ModalPresentable {
|
||||
export class AppConfigListPage {
|
||||
@Input() cursor: ConfigCursor<'list'>
|
||||
|
||||
spec: ValueSpecList
|
||||
@@ -22,7 +21,6 @@ export class AppConfigListPage extends ModalPresentable {
|
||||
// enum only
|
||||
options: { value: string, checked: boolean }[] = []
|
||||
selectAll = true
|
||||
//
|
||||
|
||||
min: number | undefined
|
||||
max: number | undefined
|
||||
@@ -34,10 +32,9 @@ export class AppConfigListPage extends ModalPresentable {
|
||||
|
||||
constructor (
|
||||
private readonly alertCtrl: AlertController,
|
||||
trackingModalCtrl: TrackingModalController,
|
||||
) {
|
||||
super(trackingModalCtrl)
|
||||
}
|
||||
private readonly subNav: SubNavService,
|
||||
private readonly nav: IonNav,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.spec = this.cursor.spec()
|
||||
@@ -59,10 +56,6 @@ export class AppConfigListPage extends ModalPresentable {
|
||||
this.updateCaches()
|
||||
}
|
||||
|
||||
async dismiss () {
|
||||
return this.dismissModal(this.value)
|
||||
}
|
||||
|
||||
// enum only
|
||||
toggleSelectAll () {
|
||||
if (!isValueSpecListOf(this.spec, 'enum')) { throw new Error('unreachable') }
|
||||
@@ -98,10 +91,10 @@ export class AppConfigListPage extends ModalPresentable {
|
||||
this.updateCaches()
|
||||
}
|
||||
|
||||
async presentModalValueEdit (index?: number) {
|
||||
async createOrEdit (index?: number) {
|
||||
const nextCursor = this.cursor.seekNext(index === undefined ? this.value.length : index)
|
||||
nextCursor.createFirstEntryForList()
|
||||
return this.presentModal(nextCursor, () => this.updateCaches())
|
||||
this.subNav.push(String(index), nextCursor, this.nav)
|
||||
}
|
||||
|
||||
async presentAlertDelete (key: number, e: Event) {
|
||||
|
||||
@@ -1,27 +1,4 @@
|
||||
<ion-header>
|
||||
<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>
|
||||
|
||||
<ion-content class="subheader-padding">
|
||||
<config-header [spec]="spec" [error]="error"></config-header>
|
||||
|
||||
<!-- object -->
|
||||
<ion-item-group>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<object-config [cursor]="cursor" (onEdit)="handleObjectEdit()"></object-config>
|
||||
</ion-item-group>
|
||||
|
||||
<object-config [cursor]="cursor" (onEdit)="handleObjectEdit()" (onClick)="updatePath($event)"></object-config>
|
||||
</ion-content>
|
||||
|
||||
@@ -1,15 +1,4 @@
|
||||
<ion-header>
|
||||
<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>
|
||||
<ion-content class="subheader-padding">
|
||||
|
||||
<config-header [spec]="spec" [error]="error"></config-header>
|
||||
|
||||
@@ -24,10 +13,10 @@
|
||||
</ion-icon>
|
||||
<ion-label>{{ spec.tag.name }}</ion-label>
|
||||
<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()">
|
||||
<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>
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
|
||||
@@ -46,8 +46,8 @@ export class AppConfigUnionPage {
|
||||
setSelectOptions () {
|
||||
return {
|
||||
header: this.spec.tag.name,
|
||||
subHeader: this.spec.changeWarning ? 'Warning!' : undefined,
|
||||
message: this.spec.changeWarning ? `${this.spec.changeWarning}` : undefined,
|
||||
subHeader: this.spec['change-warning'] ? 'Warning!' : undefined,
|
||||
message: this.spec['change-warning'] ? `${this.spec['change-warning']}` : undefined,
|
||||
cssClass: 'select-change-warning',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,4 @@
|
||||
<ion-header>
|
||||
<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>
|
||||
<ion-content class="subheader-padding">
|
||||
|
||||
<config-header [spec]="spec" [error]="error"></config-header>
|
||||
|
||||
@@ -55,15 +37,15 @@
|
||||
<ion-list *ngIf="spec.type === 'enum'">
|
||||
<ion-radio-group [(ngModel)]="value">
|
||||
<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-item>
|
||||
</ion-radio-group>
|
||||
</ion-list>
|
||||
<!-- metadata -->
|
||||
<div class="ion-padding-start">
|
||||
<p *ngIf="spec.type === 'string' && spec.patternDescription">
|
||||
{{ spec.patternDescription }}
|
||||
<p *ngIf="spec.type === 'string' && spec['pattern-description']">
|
||||
{{ spec['pattern-description'] }}
|
||||
</p>
|
||||
<p *ngIf="spec.type === 'number' && spec.integral">
|
||||
{{ integralDescription }}
|
||||
|
||||
@@ -127,7 +127,7 @@ export class AppConfigValuePage {
|
||||
}
|
||||
// test pattern if string
|
||||
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)) {
|
||||
this.error = patternDescription || `Must match ${pattern}`
|
||||
return false
|
||||
|
||||
22
ui/src/app/modals/app-config/app-config.module.ts
Normal file
22
ui/src/app/modals/app-config/app-config.module.ts
Normal 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 { }
|
||||
@@ -1,40 +1,23 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="cancel()">
|
||||
<ion-icon name="arrow-back"></ion-icon>
|
||||
<ion-toolbar *ngIf="patch.data['package-data'][pkgId] as pkg">
|
||||
<ion-title>Config</ion-title>
|
||||
<ion-buttons slot="end" class="ion-padding-end">
|
||||
<ion-button fill="clear" [disabled]="loadingText" (click)="resetDefaults()">
|
||||
Reset Defaults
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ pkg.manifest.title }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-content class="ion-padding">
|
||||
|
||||
<!-- loading -->
|
||||
<text-spinner *ngIf="loadingText; else loaded" [text]="loadingText"></text-spinner>
|
||||
|
||||
<!-- not loading -->
|
||||
<!-- not loading -->
|
||||
<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">
|
||||
<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="patch.data['package-data'][pkgId] as pkg">
|
||||
<ng-container *ngIf="pkg.manifest.config && !pkg.installed.status.configured && !edited">
|
||||
<ion-item class="notifier-item">
|
||||
<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-text style="font-size: smaller;">Initial Config</ion-text>
|
||||
</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-item>
|
||||
</ng-container>
|
||||
@@ -59,7 +42,7 @@
|
||||
</h2>
|
||||
<div style="margin: 7px 5px;">
|
||||
<p style="font-size: small; color: var(--ion-color-medium)"> {{ pkg.manifest.title }} config has been modified to satisfy {{ rec.dependentTitle }}.
|
||||
<ion-text color="dark">To accept the changes, click “Save” below.</ion-text>
|
||||
<ion-text color="dark">To accept the changes, click “Save” above.</ion-text>
|
||||
</p>
|
||||
<a style="font-size: small" *ngIf="!openRec" (click)="openRec = true">More Info</a>
|
||||
<ng-container *ngIf="openRec">
|
||||
@@ -67,7 +50,7 @@
|
||||
<a style="font-size: x-small; font-style: italic;" (click)="openRec = false">hide</a>
|
||||
</ng-container>
|
||||
<ion-button style="position: absolute; right: 0; top: 0" color="primary" fill="clear" (click)="dismissRec()">
|
||||
<ion-icon name="close-outline"></ion-icon>
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-label>
|
||||
@@ -75,41 +58,37 @@
|
||||
<ion-item-divider></ion-item-divider>
|
||||
</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 -->
|
||||
<ion-item *ngIf="!hasConfig">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p>No config options for {{ pkg.manifest.title }} {{ pkg.manifest.version }}.</p>
|
||||
</ion-label>
|
||||
</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>
|
||||
|
||||
<!-- has config -->
|
||||
<form [formGroup]="configForm" (ngSubmit)="save()" novalidate>
|
||||
<form-object
|
||||
[objectSpec]="configSpec"
|
||||
[formGroup]="configForm"
|
||||
[current]="current"
|
||||
showEdited
|
||||
></form-object>
|
||||
</form>
|
||||
</ng-template>
|
||||
</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>
|
||||
8
ui/src/app/modals/app-config/app-config.page.scss
Normal file
8
ui/src/app/modals/app-config/app-config.page.scss
Normal 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;
|
||||
}
|
||||
181
ui/src/app/modals/app-config/app-config.page.ts
Normal file
181
ui/src/app/modals/app-config/app-config.page.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
|
||||
<text-spinner *ngIf="loading" text="Loading Drives"></text-spinner>
|
||||
|
||||
@@ -2,19 +2,15 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
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 { SharingModule } from '../../modules/sharing.module'
|
||||
import { TextSpinnerComponentModule } from '../../components/text-spinner/text-spinner.component.module'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
SharingModule,
|
||||
BackupConfirmationComponentModule,
|
||||
PwaBackComponentModule,
|
||||
TextSpinnerComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [
|
||||
AppRestoreComponent,
|
||||
|
||||
@@ -31,7 +31,6 @@ export class AppRestoreComponent {
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
console.log('initing')
|
||||
this.getExternalDisks()
|
||||
}
|
||||
|
||||
@@ -51,7 +50,6 @@ export class AppRestoreComponent {
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
console.log('loading false')
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
@@ -80,7 +78,6 @@ export class AppRestoreComponent {
|
||||
}
|
||||
|
||||
private async restore (logicalname: string, password: string): Promise<void> {
|
||||
console.log('here here here')
|
||||
this.submitting = true
|
||||
// await loader.present()
|
||||
|
||||
|
||||
17
ui/src/app/modals/enum-list/enum-list.module.ts
Normal file
17
ui/src/app/modals/enum-list/enum-list.module.ts
Normal 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 { }
|
||||
36
ui/src/app/modals/enum-list/enum-list.page.html
Normal file
36
ui/src/app/modals/enum-list/enum-list.page.html
Normal 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>
|
||||
0
ui/src/app/modals/enum-list/enum-list.page.scss
Normal file
0
ui/src/app/modals/enum-list/enum-list.page.scss
Normal file
56
ui/src/app/modals/enum-list/enum-list.page.ts
Normal file
56
ui/src/app/modals/enum-list/enum-list.page.ts
Normal 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]
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,12 @@ import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { MarkdownPage } from './markdown.page'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
SharingModule,
|
||||
TextSpinnerComponentModule,
|
||||
],
|
||||
declarations: [MarkdownPage],
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<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-buttons>
|
||||
<ion-title>{{ title | titlecase }}</ion-title>
|
||||
|
||||
@@ -10,42 +10,50 @@ import { HasUiPipe, LaunchablePipe } from '../pipes/ui.pipe'
|
||||
import { EmptyPipe } from '../pipes/empty.pipe'
|
||||
import { NotificationColorPipe } from '../pipes/notification-color.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({
|
||||
declarations: [
|
||||
EmverComparesPipe,
|
||||
EmverSatisfiesPipe,
|
||||
TypeofPipe,
|
||||
IncludesPipe,
|
||||
InstallState,
|
||||
MarkdownPipe,
|
||||
AnnotationStatusPipe,
|
||||
TruncateCenterPipe,
|
||||
TruncateEndPipe,
|
||||
MaskPipe,
|
||||
EmverDisplayPipe,
|
||||
HasUiPipe,
|
||||
LaunchablePipe,
|
||||
EmptyPipe,
|
||||
NotificationColorPipe,
|
||||
],
|
||||
imports: [],
|
||||
exports: [
|
||||
EmverComparesPipe,
|
||||
EmverSatisfiesPipe,
|
||||
TypeofPipe,
|
||||
IncludesPipe,
|
||||
MarkdownPipe,
|
||||
AnnotationStatusPipe,
|
||||
TruncateEndPipe,
|
||||
TruncateCenterPipe,
|
||||
MaskPipe,
|
||||
EmverDisplayPipe,
|
||||
HasUiPipe,
|
||||
InstallState,
|
||||
LaunchablePipe,
|
||||
EmptyPipe,
|
||||
NotificationColorPipe,
|
||||
],
|
||||
declarations: [
|
||||
EmverComparesPipe,
|
||||
EmverSatisfiesPipe,
|
||||
TypeofPipe,
|
||||
IncludesPipe,
|
||||
InstallState,
|
||||
MarkdownPipe,
|
||||
AnnotationStatusPipe,
|
||||
TruncateCenterPipe,
|
||||
TruncateEndPipe,
|
||||
MaskPipe,
|
||||
EmverDisplayPipe,
|
||||
HasUiPipe,
|
||||
LaunchablePipe,
|
||||
EmptyPipe,
|
||||
NotificationColorPipe,
|
||||
],
|
||||
imports: [
|
||||
TextSpinnerComponentModule,
|
||||
PwaBackComponentModule,
|
||||
],
|
||||
exports: [
|
||||
EmverComparesPipe,
|
||||
EmverSatisfiesPipe,
|
||||
TypeofPipe,
|
||||
IncludesPipe,
|
||||
MarkdownPipe,
|
||||
AnnotationStatusPipe,
|
||||
TruncateEndPipe,
|
||||
TruncateCenterPipe,
|
||||
MaskPipe,
|
||||
EmverDisplayPipe,
|
||||
HasUiPipe,
|
||||
InstallState,
|
||||
LaunchablePipe,
|
||||
EmptyPipe,
|
||||
NotificationColorPipe,
|
||||
// components
|
||||
TextSpinnerComponentModule,
|
||||
PwaBackComponentModule,
|
||||
],
|
||||
})
|
||||
export class SharingModule { }
|
||||
24
ui/src/app/modules/subnav.module.ts
Normal file
24
ui/src/app/modules/subnav.module.ts
Normal 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 { }
|
||||
@@ -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>
|
||||
@@ -2,8 +2,7 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppActionsPage } from './app-actions.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { AppActionsPage, AppActionsItemComponent } from './app-actions.page'
|
||||
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { AppActionInputPageModule } from 'src/app/modals/app-action-input/app-action-input.module'
|
||||
@@ -21,12 +20,14 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
QRComponentModule,
|
||||
SharingModule,
|
||||
AppActionInputPageModule,
|
||||
AppRestoreComponentModule,
|
||||
],
|
||||
declarations: [AppActionsPage],
|
||||
declarations: [
|
||||
AppActionsPage,
|
||||
AppActionsItemComponent,
|
||||
],
|
||||
})
|
||||
export class AppActionsPageModule { }
|
||||
|
||||
@@ -10,66 +10,37 @@
|
||||
|
||||
|
||||
<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;">
|
||||
<ion-row>
|
||||
<ion-col *ngFor="let action of pkg.manifest.actions | keyvalue: asIsOrder" size="6">
|
||||
<ion-card button style="cursor: pointer !important; height: 88%;" color="light" (click)="handleAction(pkg, action)">
|
||||
<ion-card-header>
|
||||
<ion-card-subtitle>
|
||||
<ion-icon size="large" *ngIf="!(action.value['allowed-statuses'] | includes: pkg.installed.status.main.status); else goodIcon" color="danger" name="close-outline"></ion-icon>
|
||||
<ion-icon size="large" #goAhead name="play-circle-outline"></ion-icon>
|
||||
</ion-card-subtitle>
|
||||
<ion-card-title>{{ action.value.name }}</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
{{ action.value.description }}
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
<ion-col size="6">
|
||||
<ion-card button style="cursor: pointer !important; height: 88%;" color="light" (click)="restore()">
|
||||
<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>
|
||||
<!-- ** standard actions ** -->
|
||||
<ion-item-divider>Standard Actions</ion-item-divider>
|
||||
<app-actions-item
|
||||
[action]="{
|
||||
name: 'Restore From Backup',
|
||||
description: 'All changes since backup will be lost.',
|
||||
icon: 'color-wand-outline'
|
||||
}"
|
||||
(click)="restore()">
|
||||
</app-actions-item>
|
||||
<app-actions-item
|
||||
[action]="{
|
||||
name: 'Uninstall',
|
||||
description: 'This will uninstall the service from your Embassy and delete all data permanently.',
|
||||
icon: 'trash-outline'
|
||||
}"
|
||||
(click)="uninstall(pkg.manifest)">
|
||||
</app-actions-item>
|
||||
|
||||
<!-- <ion-item-group>
|
||||
<ion-item button *ngFor="let action of pkg.manifest.actions | keyvalue: asIsOrder" (click)="handleAction(pkg, action)" >
|
||||
<ion-label class="ion-text-wrap">
|
||||
<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>
|
||||
<p><ion-text color="dark">{{ action.value.description }}</ion-text></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="uninstall(pkg.manifest)" >
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2><ion-text color="primary">Uninstall</ion-text></h2>
|
||||
<p><ion-text color="dark">This will uninstall the service from your Embassy and delete all data permanently.</ion-text></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group> -->
|
||||
</ng-container>
|
||||
<!-- ** specific actions ** -->
|
||||
<ion-item-divider>Actions for {{ pkg.manifest.title }}</ion-item-divider>
|
||||
<app-actions-item
|
||||
*ngFor="let action of pkg.manifest.actions | keyvalue: asIsOrder"
|
||||
[action]="{
|
||||
name: action.value.name,
|
||||
description: action.value.description,
|
||||
icon: 'play-circle-outline'
|
||||
}"
|
||||
(click)="handleAction(pkg, action)">
|
||||
</app-actions-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
|
||||
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 { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
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 { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
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 }) {
|
||||
if ((action.value['allowed-statuses'] as PackageMainStatus[]).includes(pkg.installed.status.main.status)) {
|
||||
const inputSpec = action.value['input-spec']
|
||||
if (inputSpec) {
|
||||
if (action.value['input-spec']) {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: AppActionInputPage,
|
||||
componentProps: {
|
||||
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()
|
||||
} else {
|
||||
const alert = await this.alertCtrl.create({
|
||||
@@ -105,7 +105,7 @@ export class AppActionsPage {
|
||||
}
|
||||
|
||||
async restore (): Promise<void> {
|
||||
const m = await this.modalCtrl.create({
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
pkgId: this.pkgId,
|
||||
},
|
||||
@@ -113,12 +113,12 @@ export class AppActionsPage {
|
||||
backdropDismiss: false,
|
||||
})
|
||||
|
||||
m.onWillDismiss().then(res => {
|
||||
modal.onWillDismiss().then(res => {
|
||||
const data = res.data
|
||||
if (data.error) this.errToast.present(data.error)
|
||||
})
|
||||
|
||||
return await m.present()
|
||||
return await modal.present()
|
||||
}
|
||||
|
||||
async uninstall (manifest: Manifest) {
|
||||
@@ -137,7 +137,7 @@ export class AppActionsPage {
|
||||
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({
|
||||
spinner: 'lines',
|
||||
message: 'Executing action...',
|
||||
@@ -146,7 +146,11 @@ export class AppActionsPage {
|
||||
await loader.present()
|
||||
|
||||
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({
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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 { }
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
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 { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -19,9 +17,7 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
TextSpinnerComponentModule,
|
||||
],
|
||||
declarations: [
|
||||
AppInstructionsPage,
|
||||
|
||||
@@ -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>
|
||||
@@ -2,8 +2,7 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppInterfacesPage } from './app-interfaces.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { AppInterfacesItemComponent, AppInterfacesPage } from './app-interfaces.page'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -18,9 +17,11 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [AppInterfacesPage],
|
||||
declarations: [
|
||||
AppInterfacesPage,
|
||||
AppInterfacesItemComponent,
|
||||
],
|
||||
})
|
||||
export class AppInterfacesPageModule { }
|
||||
|
||||
@@ -7,37 +7,20 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content *ngIf="patch.data['package-data'][pkgId] as pkg">
|
||||
<ion-card *ngFor="let interface of pkg.manifest.interfaces | keyvalue: asIsOrder">
|
||||
<ion-card-header>
|
||||
<ion-card-title>{{ interface.value.name }}</ion-card-title>
|
||||
<ion-card-subtitle>{{ interface.value.description }}</ion-card-subtitle>
|
||||
<ion-button style="margin-top: 12px;" *ngIf="interface.value.ui" [disabled]="!(pkg | isLaunchable)" fill="outline" color="dark" expand="block" (click)="launch(pkg)">
|
||||
Launch
|
||||
<ion-icon slot="end" name="rocket-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<ng-container *ngIf="pkg.installed['interface-info'].addresses[interface.key] as int">
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>Tor Address</h2>
|
||||
<p>{{ 'http://' + int['tor-address'] }}</p>
|
||||
</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 class="ion-padding-top">
|
||||
<ion-item-group>
|
||||
<!-- iff ui -->
|
||||
<ng-container *ngIf="ui">
|
||||
<ion-item-divider>Web User Interface</ion-item-divider>
|
||||
<app-interfaces-item [interface]="ui"></app-interfaces-item>
|
||||
</ng-container>
|
||||
|
||||
<!-- other interface -->
|
||||
<ng-container *ngIf="other.length">
|
||||
<ion-item-divider>Other Interfaces</ion-item-divider>
|
||||
<div *ngFor="let interface of other" style="margin-bottom: 30px;">
|
||||
<app-interfaces-item [interface]="interface"></app-interfaces-item>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
@@ -1,38 +1,87 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { IonContent, ToastController } from '@ionic/angular'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { InstalledPackageDataEntry, PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { InterfaceDef, InterfaceInfo } from 'src/app/services/patch-db/data-model'
|
||||
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'
|
||||
|
||||
interface LocalInterface {
|
||||
def: InterfaceDef
|
||||
addresses: InterfaceInfo['addresses'][string]
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-Interfaces',
|
||||
templateUrl: './app-Interfaces.page.html',
|
||||
styleUrls: ['./app-Interfaces.page.scss'],
|
||||
selector: 'app-interfaces',
|
||||
templateUrl: './app-interfaces.page.html',
|
||||
styleUrls: ['./app-interfaces.page.scss'],
|
||||
})
|
||||
export class AppInterfacesPage {
|
||||
pkg: PackageDataEntry
|
||||
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
pkgId: string
|
||||
ui: LocalInterface | null
|
||||
other: LocalInterface[]
|
||||
|
||||
constructor (
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly config: ConfigService,
|
||||
public readonly patch: PatchDbService,
|
||||
) { }
|
||||
|
||||
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 () {
|
||||
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> {
|
||||
let message = ''
|
||||
await copyToClipboard(address || '')
|
||||
@@ -45,12 +94,4 @@ export class AppInterfacesPage {
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
launch (pkg: PackageDataEntry): void {
|
||||
window.open(this.config.launchableURL(pkg), '_blank')
|
||||
}
|
||||
|
||||
asIsOrder () {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<ion-card class="installed-card" [routerLink]="['/services', pkg.value.entry.manifest.id]">
|
||||
<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)">
|
||||
<ion-icon name="rocket-outline"></ion-icon>
|
||||
<ion-icon name="open-outline"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppLogsPage } from './app-logs.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -18,8 +17,7 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
TextSpinnerComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [AppLogsPage],
|
||||
})
|
||||
|
||||
@@ -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 { }
|
||||
@@ -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>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.raw {
|
||||
background-color: var(--ion-color-light);
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
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 { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
|
||||
|
||||
@@ -19,7 +18,6 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
SkeletonListComponentModule,
|
||||
],
|
||||
|
||||
@@ -3,10 +3,8 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
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 { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -20,10 +18,8 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
QRComponentModule,
|
||||
SharingModule,
|
||||
TextSpinnerComponentModule,
|
||||
],
|
||||
declarations: [AppPropertiesPage],
|
||||
})
|
||||
|
||||
@@ -5,8 +5,8 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { AppShowPage } from './app-show.page'
|
||||
import { StatusComponentModule } from 'src/app/components/status/status.component.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 { AppConfigPageModule } from 'src/app/modals/app-config/app-config.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -19,11 +19,11 @@ const routes: Routes = [
|
||||
imports: [
|
||||
CommonModule,
|
||||
StatusComponentModule,
|
||||
SharingModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
InstallWizardComponentModule,
|
||||
AppConfigPageModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [AppShowPage],
|
||||
})
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
<status size="x-large" weight="500" [rendering]="rendering"></status>
|
||||
</ion-label>
|
||||
<ion-button slot="end" class="action-button" *ngIf="pkg.state === PackageState.Installed && (pkg | hasUi)" [disabled]="!(pkg | isLaunchable)" (click)="launchUiTab()">
|
||||
Web
|
||||
<ion-icon slot="end" name="rocket-outline"></ion-icon>
|
||||
<ion-icon slot="start" name="open-outline"></ion-icon>
|
||||
Open UI
|
||||
</ion-button>
|
||||
<ion-button slot="end" class="action-button" *ngIf="rendering.feStatus === FeStatus.NeedsConfig" [routerLink]="['config']">
|
||||
Configure
|
||||
@@ -53,7 +53,7 @@
|
||||
<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-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-label>
|
||||
<p>{{ health.key }}</p>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.action-button {
|
||||
margin: 10px;
|
||||
min-height: 36px;
|
||||
min-width: 72px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.icon-spinner {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, ViewChild } from '@angular/core'
|
||||
import { AlertController, NavController, ModalController, IonContent, LoadingController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
|
||||
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 { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
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 { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { AppConfigPage } from 'src/app/modals/app-config/app-config.page'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show',
|
||||
@@ -31,7 +32,6 @@ export class AppShowPage {
|
||||
Math = Math
|
||||
mainStatus: MainStatus
|
||||
|
||||
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
subs: Subscription[] = []
|
||||
|
||||
@@ -64,21 +64,20 @@ export class AppShowPage {
|
||||
this.patch.watch$('package-data', this.pkgId, 'installed', 'status', 'main')
|
||||
.subscribe(main => {
|
||||
this.mainStatus = main
|
||||
console.log(this.mainStatus)
|
||||
}),
|
||||
]
|
||||
this.setButtons()
|
||||
}
|
||||
|
||||
// ngAfterViewInit () {
|
||||
// this.content.scrollToPoint(undefined, 1)
|
||||
// }
|
||||
ngAfterViewInit () {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.subs.forEach(sub => sub.unsubscribe())
|
||||
}
|
||||
|
||||
launchUiTab (): void {
|
||||
launchUi (): void {
|
||||
window.open(this.config.launchableURL(this.pkg), '_blank')
|
||||
}
|
||||
|
||||
@@ -94,8 +93,6 @@ export class AppShowPage {
|
||||
try {
|
||||
const breakages = await this.embassyApi.dryStopPackage({ id })
|
||||
|
||||
console.log('BREAKAGES', breakages)
|
||||
|
||||
if (!isEmptyObject(breakages)) {
|
||||
const { cancelled } = await wizardModal(
|
||||
this.modalCtrl,
|
||||
@@ -108,7 +105,7 @@ export class AppShowPage {
|
||||
)
|
||||
if (cancelled) return
|
||||
}
|
||||
return this.embassyApi.stopPackage({ id }).then(chill)
|
||||
await this.embassyApi.stopPackage({ id })
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
@@ -156,8 +153,14 @@ export class AppShowPage {
|
||||
}
|
||||
}
|
||||
|
||||
asIsOrder () {
|
||||
return 0
|
||||
async presentModalConfig (): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: AppConfigPage,
|
||||
componentProps: {
|
||||
pkgId: this.pkgId,
|
||||
},
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private async installDep (depId: string): Promise<void> {
|
||||
@@ -234,8 +237,9 @@ export class AppShowPage {
|
||||
}
|
||||
}
|
||||
|
||||
setButtons (): void {
|
||||
private setButtons (): void {
|
||||
this.buttons = [
|
||||
// instructions
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['instructions'], { relativeTo: this.route }),
|
||||
title: 'Instructions',
|
||||
@@ -243,13 +247,15 @@ export class AppShowPage {
|
||||
color: 'danger',
|
||||
disabled: [],
|
||||
},
|
||||
// config
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['config'], { relativeTo: this.route }),
|
||||
title: 'Settings',
|
||||
action: async () => this.presentModalConfig(),
|
||||
title: 'Config',
|
||||
icon: 'construct-outline',
|
||||
color: 'danger',
|
||||
disabled: [FEStatus.Installing, FEStatus.Updating, FEStatus.Removing, FEStatus.BackingUp, FEStatus.Restoring],
|
||||
},
|
||||
// properties
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['properties'], { relativeTo: this.route }),
|
||||
title: 'Properties',
|
||||
@@ -257,6 +263,7 @@ export class AppShowPage {
|
||||
color: 'danger',
|
||||
disabled: [],
|
||||
},
|
||||
// interfaces
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['interfaces'], { relativeTo: this.route }),
|
||||
title: 'Interfaces',
|
||||
@@ -264,6 +271,7 @@ export class AppShowPage {
|
||||
color: 'danger',
|
||||
disabled: [],
|
||||
},
|
||||
// actions
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }),
|
||||
title: 'Actions',
|
||||
@@ -271,6 +279,7 @@ export class AppShowPage {
|
||||
color: 'danger',
|
||||
disabled: [],
|
||||
},
|
||||
// metrics
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }),
|
||||
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.
|
||||
disabled: [FEStatus.Installing, FEStatus.Updating, FEStatus.Removing, FEStatus.BackingUp, FEStatus.Restoring],
|
||||
},
|
||||
// logs
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }),
|
||||
title: 'Logs',
|
||||
@@ -286,22 +296,19 @@ export class AppShowPage {
|
||||
color: 'danger',
|
||||
disabled: [],
|
||||
},
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['manifest'], { relativeTo: this.route }),
|
||||
title: 'Package Details',
|
||||
icon: 'finger-print-outline',
|
||||
color: 'danger',
|
||||
disabled: [],
|
||||
},
|
||||
{
|
||||
action: () => this.donate(),
|
||||
title: 'Donate',
|
||||
title: `Donate to ${this.pkg.manifest.title}`,
|
||||
icon: 'logo-bitcoin',
|
||||
color: 'danger',
|
||||
disabled: [],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
asIsOrder () {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
interface Button {
|
||||
|
||||
@@ -19,14 +19,6 @@ const routes: Routes = [
|
||||
path: ':pkgId/actions',
|
||||
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',
|
||||
loadChildren: () => import('./app-instructions/app-instructions.module').then(m => m.AppInstructionsPageModule),
|
||||
@@ -39,10 +31,6 @@ const routes: Routes = [
|
||||
path: ':pkgId/logs',
|
||||
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',
|
||||
loadChildren: () => import('./app-metrics/app-metrics.module').then(m => m.AppMetricsPageModule),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<ion-row class="ion-align-items-center" style="height: 100%;">
|
||||
<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;" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export class LoginPage {
|
||||
this.loader = await this.loadingCtrl.create({
|
||||
message: 'Logging in',
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await this.loader.present()
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
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 { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -19,9 +17,7 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
TextSpinnerComponentModule,
|
||||
],
|
||||
declarations: [AppReleaseNotes],
|
||||
})
|
||||
|
||||
@@ -6,8 +6,6 @@ import { MarketplaceListPage } from './marketplace-list.page'
|
||||
import { SharingModule } from '../../../modules/sharing.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 { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -24,7 +22,6 @@ const routes: Routes = [
|
||||
StatusComponentModule,
|
||||
SharingModule,
|
||||
BadgeMenuComponentModule,
|
||||
TextSpinnerComponentModule,
|
||||
],
|
||||
declarations: [MarketplaceListPage],
|
||||
})
|
||||
|
||||
@@ -4,10 +4,8 @@ import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { MarketplaceShowPage } from './marketplace-show.page'
|
||||
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 { 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 = [
|
||||
{
|
||||
@@ -21,10 +19,8 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
StatusComponentModule,
|
||||
TextSpinnerComponentModule,
|
||||
RouterModule.forChild(routes),
|
||||
SharingModule,
|
||||
PwaBackComponentModule,
|
||||
InstallWizardComponentModule,
|
||||
],
|
||||
declarations: [MarketplaceShowPage],
|
||||
|
||||
@@ -81,8 +81,8 @@
|
||||
<p style="color: var(--ion-color-dark); font-size: small">{{ rec.description }}</p>
|
||||
<p *ngIf="pkg.manifest.version | satisfiesEmver: rec.version" class="recommendation-text">{{ pkg.manifest.title }} version {{ pkg.manifest.version | displayEmver }} is compatible.</p>
|
||||
<p *ngIf="!(pkg.manifest.version | satisfiesEmver: rec.version)" class="recommendation-text recommendation-error">{{ pkg.manifest.title }} version {{ pkg.manifest.version | displayEmver }} is NOT compatible.</p>
|
||||
<ion-button style="position: absolute; right: 0; top: 0" color="primary" fill="clear" (click)="dismissRec()">
|
||||
<ion-icon name="close-outline"></ion-icon>
|
||||
<ion-button style="position: absolute; right: 0; top: 0" fill="clear" (click)="dismissRec()">
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-label>
|
||||
|
||||
@@ -3,10 +3,8 @@ import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
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 { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -20,10 +18,8 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
SharingModule,
|
||||
TextSpinnerComponentModule,
|
||||
],
|
||||
declarations: [NotificationsPage],
|
||||
})
|
||||
|
||||
@@ -17,41 +17,52 @@
|
||||
|
||||
<text-spinner *ngIf="loading" text="Loading Notifications"></text-spinner>
|
||||
|
||||
<ion-item-group *ngIf="!notifications.length && !loading">
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
Notifications about Embassy and services will appear here.
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<ion-item-group style="margin-bottom: 16px;">
|
||||
<ion-item *ngFor="let not of notifications; let i = index">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>
|
||||
<ion-text [color]="not | notificationColor"><b>{{ not.title }}</b></ion-text>
|
||||
</h2>
|
||||
<h2 class="notification-message">
|
||||
{{ not.message }}
|
||||
<a *ngIf="not.code === 1" style="text-decoration: none;" (click)="viewBackupReport(not)">
|
||||
View Report
|
||||
</a>
|
||||
</h2>
|
||||
<p>
|
||||
{{ not['created-at'] | date: 'short' }}
|
||||
<a *ngIf="not['package-id'] as pkgId" style="text-decoration: none;" [routerLink]="['/services', not['package-id']]">
|
||||
- {{ not['package-id'] }}
|
||||
</a>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="remove(not.id, i)">
|
||||
<ion-icon slot="icon-only" name="close-outline"></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>
|
||||
<!-- no notifications -->
|
||||
<ng-container *ngIf="!loading">
|
||||
<ion-item-group *ngIf="!notifications.length">
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
Notifications about Embassy and services will appear here.
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- has notifications -->
|
||||
<ng-container *ngIf="notifications.length">
|
||||
<ion-item-group style="margin-bottom: 16px;">
|
||||
<ion-item-divider>
|
||||
<ion-button slot="end" fill="clear" (click)="deleteAll()">
|
||||
Delete All
|
||||
</ion-button>
|
||||
</ion-item-divider>
|
||||
<ion-item *ngFor="let not of notifications; let i = index">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>
|
||||
<ion-text [color]="not | notificationColor"><b>{{ not.title }}</b></ion-text>
|
||||
</h2>
|
||||
<h2 class="notification-message">
|
||||
{{ not.message }}
|
||||
<a *ngIf="not.code === 1" style="text-decoration: none;" (click)="viewBackupReport(not)">
|
||||
View Report
|
||||
</a>
|
||||
</h2>
|
||||
<p>
|
||||
{{ not['created-at'] | date: 'short' }}
|
||||
<a *ngIf="not['package-id'] as pkgId" style="text-decoration: none;" [routerLink]="['/services', not['package-id']]">
|
||||
- {{ not['package-id'] }}
|
||||
</a>
|
||||
</p>
|
||||
</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>
|
||||
@@ -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({
|
||||
spinner: 'lines',
|
||||
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>) {
|
||||
const data = notification.data
|
||||
|
||||
@@ -99,9 +117,6 @@ export class NotificationsPage {
|
||||
if (embassyFailed || packagesFailed) {
|
||||
buttons.push({
|
||||
text: 'Retry',
|
||||
handler: () => {
|
||||
console.log('retry backup')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
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'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -18,7 +17,6 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [LANPage],
|
||||
|
||||
@@ -3,8 +3,6 @@ import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { SecurityOptionsPage } from './security-options.page'
|
||||
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'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -18,9 +16,7 @@ const routes: Routes = [
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
ObjectConfigComponentModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [
|
||||
|
||||
@@ -11,23 +11,23 @@
|
||||
|
||||
<ion-item-group>
|
||||
<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-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-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-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 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-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 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-note slot="end">{{ patch.data['server-info']['package-marketplace'] }}</ion-note>
|
||||
<ion-note slot="end">{{ server['package-marketplace'] }}</ion-note>
|
||||
</ion-item> -->
|
||||
|
||||
<ion-item-divider>Security</ion-item-divider>
|
||||
|
||||
@@ -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 { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { IonContent } from '@ionic/angular'
|
||||
|
||||
@Component({
|
||||
selector: 'security-options',
|
||||
@@ -9,14 +10,15 @@ import { ConfigService } from 'src/app/services/config.service'
|
||||
styleUrls: ['./security-options.page.scss'],
|
||||
})
|
||||
export class SecurityOptionsPage {
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
|
||||
constructor (
|
||||
private readonly serverConfigService: ServerConfigService,
|
||||
public readonly serverConfig: ServerConfigService,
|
||||
public readonly config: ConfigService,
|
||||
public readonly patch: PatchDbService,
|
||||
) { }
|
||||
|
||||
async presentModalValueEdit (key: string, current?: any): Promise<void> {
|
||||
await this.serverConfigService.presentModalValueEdit(key, current)
|
||||
ngAfterViewInit () {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
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 { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -19,9 +17,7 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
TextSpinnerComponentModule,
|
||||
],
|
||||
declarations: [SessionsPage],
|
||||
})
|
||||
|
||||
@@ -31,8 +31,9 @@
|
||||
<h2>Last Active: {{ session.value['last-active'] | date : 'medium' }}</h2>
|
||||
<p>{{ session.value['user-agent'] }}</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="presentAlertKill(session.key)">
|
||||
<ion-icon slot="icon-only" name="close-outline"></ion-icon>
|
||||
<ion-button slot="end" fill="clear" color="danger" (click)="presentAlertKill(session.key)">
|
||||
<ion-icon slot="start" name="close"></ion-icon>
|
||||
Kill
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component } from '@angular/core'
|
||||
import { AlertController, getPlatforms, LoadingController } from '@ionic/angular'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.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({
|
||||
selector: 'sessions',
|
||||
|
||||
@@ -3,9 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
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 { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -19,9 +17,7 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
TextSpinnerComponentModule,
|
||||
],
|
||||
declarations: [SSHKeysPage],
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</ion-buttons>
|
||||
<ion-title>SSH Keys</ion-title>
|
||||
<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-button>
|
||||
</ion-buttons>
|
||||
@@ -15,14 +15,25 @@
|
||||
<ion-content class="ion-padding-top">
|
||||
<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 *ngFor="let ssh of sshKeys | keyvalue : asIsOrder">
|
||||
<ion-label class="ion-text-wrap">
|
||||
{{ ssh.value.alg }} {{ ssh.key }} {{ ssh.value.hostname }}
|
||||
</ion-label>
|
||||
<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-item>
|
||||
</ion-item-group>
|
||||
|
||||
@@ -15,13 +15,14 @@ export class SSHKeysPage {
|
||||
loading = true
|
||||
sshKeys: SSHKeys
|
||||
subs: Subscription[] = []
|
||||
readonly docsUrl = 'https://docs.start9.com/user-manual/general/developer-options/ssh-setup.html'
|
||||
|
||||
constructor (
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly serverConfigService: ServerConfigService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly sshService: SSHService,
|
||||
public readonly serverConfig: ServerConfigService,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
@@ -41,10 +42,6 @@ export class SSHKeysPage {
|
||||
this.subs.forEach(sub => sub.unsubscribe())
|
||||
}
|
||||
|
||||
async presentModalAdd () {
|
||||
await this.serverConfigService.presentModalValueEdit('ssh')
|
||||
}
|
||||
|
||||
async presentAlertDelete (hash: string) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: false,
|
||||
|
||||
@@ -4,8 +4,7 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { ServerBackupPage } from './server-backup.page'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
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 { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -20,8 +19,7 @@ const routes: Routes = [
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
BackupConfirmationComponentModule,
|
||||
PwaBackComponentModule,
|
||||
TextSpinnerComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [
|
||||
ServerBackupPage,
|
||||
|
||||
@@ -3,8 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { ServerLogsPage } from './server-logs.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -18,8 +17,7 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
TextSpinnerComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [ServerLogsPage],
|
||||
})
|
||||
|
||||
@@ -3,8 +3,8 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
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 { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -18,8 +18,8 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SkeletonListComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [ServerMetricsPage],
|
||||
})
|
||||
|
||||
@@ -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 { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
@@ -13,6 +14,7 @@ export class ServerMetricsPage {
|
||||
loading = true
|
||||
going = false
|
||||
metrics: Metrics = { }
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
|
||||
constructor (
|
||||
private readonly errToast: ErrorToastService,
|
||||
@@ -23,6 +25,10 @@ export class ServerMetricsPage {
|
||||
this.startDaemon()
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.stopDaemon()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ServerShowPage } from './server-show.page'
|
||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
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'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -24,7 +23,6 @@ const routes: Routes = [
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
SharingModule,
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
],
|
||||
declarations: [ServerShowPage],
|
||||
|
||||
@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
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'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -18,7 +17,6 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [ServerSpecsPage],
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ToastController } from '@ionic/angular'
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { IonContent, ToastController } from '@ionic/angular'
|
||||
import { copyToClipboard } from 'src/app/util/web.util'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { Subscription } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'server-specs',
|
||||
@@ -10,13 +9,17 @@ import { Subscription } from 'rxjs'
|
||||
styleUrls: ['./server-specs.page.scss'],
|
||||
})
|
||||
export class ServerSpecsPage {
|
||||
subs: Subscription[] = []
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
|
||||
constructor (
|
||||
private readonly toastCtrl: ToastController,
|
||||
public readonly patch: PatchDbService,
|
||||
) { }
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
|
||||
async copy (address: string) {
|
||||
let message = ''
|
||||
await copyToClipboard(address || '')
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
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 = [
|
||||
{
|
||||
@@ -19,7 +19,7 @@ const routes: Routes = [
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [WifiAddPage],
|
||||
})
|
||||
|
||||
@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
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'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -22,7 +21,6 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [WifiListPage],
|
||||
|
||||
@@ -93,9 +93,9 @@ export class ConfigCursor<T extends ValueType> {
|
||||
case 'number':
|
||||
return `${config}${spec.units ? ' ' + spec.units : ''}`
|
||||
case 'object':
|
||||
return spec.displayAs ? handlebars.compile(spec.displayAs)(config) : ''
|
||||
return spec['display-as'] ? handlebars.compile(spec['display-as'])(config) : ''
|
||||
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':
|
||||
return 'System Defined'
|
||||
default:
|
||||
@@ -121,11 +121,9 @@ export class ConfigCursor<T extends ValueType> {
|
||||
let ret: ValueSpec = {
|
||||
type: 'object',
|
||||
spec: this.rootSpec,
|
||||
nullable: false,
|
||||
nullByDefault: false,
|
||||
name: 'Config',
|
||||
displayAs: 'Config',
|
||||
uniqueBy: null,
|
||||
'display-as': 'Config',
|
||||
'unique-by': null,
|
||||
}
|
||||
let ptr = []
|
||||
for (let seg of parsed) {
|
||||
@@ -141,7 +139,7 @@ export class ConfigCursor<T extends ValueType> {
|
||||
values: Object.keys(ret.variants),
|
||||
name: ret.tag.name,
|
||||
description: ret.tag.description,
|
||||
valueNames: ret.tag.variantNames,
|
||||
'value-names': ret.tag['variant-names'],
|
||||
}
|
||||
} else {
|
||||
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)) {
|
||||
return null
|
||||
} else {
|
||||
return spec.patternDescription
|
||||
return spec['pattern-description']
|
||||
}
|
||||
} else {
|
||||
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':
|
||||
if (typeof cfg === 'string') {
|
||||
spec.valuesSet = spec.valuesSet || new Set(spec.values)
|
||||
return spec.valuesSet.has(cfg) ? null : `${cfg} is not a valid selection.`
|
||||
spec['values-set'] = spec['values-set'] || new Set(spec.values)
|
||||
return spec['values-set'].has(cfg) ? null : `${cfg} is not a valid selection.`
|
||||
} else {
|
||||
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))) {
|
||||
return `Item #${idx + 1} is not unique.` + ('uniqueBy' in cursor.spec()) ? `${
|
||||
displayUniqueBy(
|
||||
(cursor.spec() as ValueSpecObject | ValueSpecUnion).uniqueBy,
|
||||
(cursor.spec() as ValueSpecObject | ValueSpecUnion)['unique-by'],
|
||||
(cursor.spec() as ValueSpecObject | ValueSpecUnion),
|
||||
cursor.config(),
|
||||
)
|
||||
@@ -247,7 +245,7 @@ export class ConfigCursor<T extends ValueType> {
|
||||
}
|
||||
case 'object':
|
||||
if (!cfg) {
|
||||
return spec.nullable ? null : `${spec.name} is missing.`
|
||||
return `${spec.name} is missing.`
|
||||
} else if (typeof cfg === 'object' && !Array.isArray(cfg)) {
|
||||
for (let idx in spec.spec) {
|
||||
if (this.seekNext(idx).checkInvalid()) {
|
||||
@@ -328,7 +326,7 @@ export class ConfigCursor<T extends ValueType> {
|
||||
return lhs === rhs
|
||||
case 'object':
|
||||
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':
|
||||
if (lhs.length !== rhs.length) {
|
||||
return false
|
||||
|
||||
@@ -52,14 +52,12 @@ export interface ValueSpecPointer extends WithStandalone {
|
||||
|
||||
export interface ValueSpecObject extends ListValueSpecObject, WithStandalone {
|
||||
type: 'object'
|
||||
nullable: boolean
|
||||
nullByDefault: boolean
|
||||
}
|
||||
|
||||
export interface WithStandalone {
|
||||
name: string
|
||||
description?: string
|
||||
changeWarning?: string
|
||||
'change-warning'?: string
|
||||
}
|
||||
|
||||
// no lists of booleans, lists, pointers
|
||||
@@ -90,8 +88,9 @@ export function isValueSpecListOf<S extends ListValueSpecType> (t: ValueSpecList
|
||||
}
|
||||
|
||||
export interface ListValueSpecString {
|
||||
// @TODO add masked?
|
||||
pattern?: string
|
||||
patternDescription?: string
|
||||
'pattern-description'?: string
|
||||
}
|
||||
|
||||
export interface ListValueSpecNumber {
|
||||
@@ -102,14 +101,14 @@ export interface ListValueSpecNumber {
|
||||
|
||||
export interface ListValueSpecEnum {
|
||||
values: string[]
|
||||
valuesSet?: Set<string>
|
||||
valueNames: { [value: string]: string }
|
||||
'values-set'?: Set<string>
|
||||
'value-names': { [value: string]: string }
|
||||
}
|
||||
|
||||
export interface ListValueSpecObject {
|
||||
spec: ConfigSpec //this is a mapped type of the config object at this level, replacing the object's values with specs on those values
|
||||
uniqueBy: UniqueBy //indicates whether duplicates can be permitted in the list
|
||||
displayAs?: string //this should be a handlebars template which can make use of the entire config which corresponds to 'spec'
|
||||
spec: ConfigSpec // this is a mapped type of the config object at this level, replacing the object's values with specs on those values
|
||||
'unique-by': UniqueBy // indicates whether duplicates can be permitted in the list
|
||||
'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[] }
|
||||
@@ -117,16 +116,16 @@ export type UniqueBy = null | string | { any: UniqueBy[] } | { all: UniqueBy[] }
|
||||
export interface ListValueSpecUnion {
|
||||
tag: UnionTagSpec
|
||||
variants: { [key: string]: ConfigSpec }
|
||||
displayAs?: string //this may be a handlebars template which can conditionally (on tag.id) make use of each union's entries, or if left blank will display as tag.id
|
||||
uniqueBy: UniqueBy
|
||||
default: string //this should be the variantName which one prefers a user to start with by default when creating a new union instance in a list
|
||||
'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
|
||||
'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
|
||||
}
|
||||
|
||||
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
|
||||
description?: string
|
||||
variantNames: { //the name of each variant
|
||||
'variant-names': { // the name of each variant
|
||||
[variant: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ export function listInnerSpec (listSpec: ValueSpecList): ValueSpecOf<ListValueSp
|
||||
nullable: false,
|
||||
name: listSpec.name,
|
||||
description: listSpec.description,
|
||||
changeWarning: listSpec.changeWarning,
|
||||
changeWarning: listSpec['change-warning'],
|
||||
...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',
|
||||
default: spec.default,
|
||||
values: Object.keys(spec.variants),
|
||||
valueNames: spec.tag.variantNames,
|
||||
'value-names': spec.tag['variant-names'],
|
||||
}, value[spec.tag.id])
|
||||
value = mapConfigSpec(spec.variants[variant], value)
|
||||
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 {
|
||||
switch (spec.type) {
|
||||
case 'object':
|
||||
return spec.nullByDefault ? null : getDefaultObject(spec.spec)
|
||||
return getDefaultObject(spec.spec)
|
||||
case 'union':
|
||||
return getDefaultUnion(spec)
|
||||
case 'string':
|
||||
@@ -306,7 +306,7 @@ export function getDefaultDescription (spec: ValueSpec): string {
|
||||
toReturn = spec.default === true ? 'True' : 'False'
|
||||
break
|
||||
case 'enum':
|
||||
toReturn = spec.valueNames[spec.default]
|
||||
toReturn = spec['value-names'][spec.default]
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user