* begin subnav implementation

* implement subnav AND angular forms for comparison

* unions working-ish, list of enums working

* new form approach almost complete

* finish new forms approach for action inputs and config

* expandable list items and handlebars display

* Config animation (#394)

* config cammel

* config animation

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

* improve server settings inputs, still needs work

* delete all notifications, styling, and bugs

* contracted by default

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

View File

@@ -70,7 +70,6 @@
<ion-icon name="chevron-up"></ion-icon>
<ion-icon name="chevron-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>

View File

@@ -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()
}

View File

@@ -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] },

View File

@@ -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>

View File

@@ -0,0 +1,9 @@
<ion-icon *ngIf="data.spec.description" class="help-icon" name="help-circle-outline" (click)="presentAlertDescription()"></ion-icon>
<span>&nbsp;{{ data.spec.name }}</span>
<ion-text color="success" *ngIf="data.isNew">&nbsp;(New)</ion-text>
<ion-text color="warning" *ngIf="data.isEdited">&nbsp;(Edited)</ion-text>
<span *ngIf="(['string', 'number'] | includes : data.spec.type) && !data.spec.nullable">&nbsp;*</span>
<span *ngIf="data.spec.type === 'list' && Range.from(data.spec.range).min">&nbsp;*</span>

View File

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

View File

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

View File

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

View File

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

View File

@@ -39,11 +39,11 @@
<!-- cancel button if loading/not loading -->
<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 -->

View File

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

View File

@@ -1,5 +1,6 @@
<div *ngFor="let keyval of (spec.type === 'object' ? spec.spec : spec.variants[value[spec.tag.id]]) | keyvalue: asIsOrder">
<ion-item-group>
<object-config-item
*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>

View File

@@ -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;
}

View File

@@ -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'

View File

@@ -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],
})

View File

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

View File

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

View File

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

View File

@@ -2,16 +2,17 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { 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],

View File

@@ -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>

View File

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

View File

@@ -1,9 +1,8 @@
import { Component, Input } from '@angular/core'
import { 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
}
}

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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',
}
}

View File

@@ -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 }}

View File

@@ -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

View File

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

View File

@@ -1,40 +1,23 @@
<ion-header>
<ion-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>

View File

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

View File

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

View File

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

View File

@@ -2,19 +2,15 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { 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,

View File

@@ -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()

View File

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

View File

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

View File

@@ -0,0 +1,56 @@
import { Component, Input } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { ValueSpecListOf } from '../../pkg-config/config-types'
// import { Range } from '../../pkg-config/config-utilities'
@Component({
selector: 'enum-list',
templateUrl: './enum-list.page.html',
styleUrls: ['./enum-list.page.scss'],
})
export class EnumListPage {
@Input() key: string
@Input() spec: ValueSpecListOf<'enum'>
@Input() current: string[]
options: { [option: string]: boolean } = { }
// min: number | undefined
// max: number | undefined
// minMessage: string
// maxMessage: string
selectAll = true
constructor (
private readonly modalCtrl: ModalController,
) { }
ngOnInit () {
// const range = Range.from(this.spec.range)
// this.min = range.integralMin()
// this.max = range.integralMax()
// this.minMessage = `The minimum number of ${this.key} is ${this.min}.`
// this.maxMessage = `The maximum number of ${this.key} is ${this.max}.`
for (let val of this.spec.spec.values) {
this.options[val] = this.current.includes(val)
}
}
dismiss () {
this.modalCtrl.dismiss()
}
save () {
this.modalCtrl.dismiss(Object.keys(this.options).filter(key => this.options[key]))
}
toggleSelectAll () {
Object.keys(this.options).forEach(k => this.options[k] = this.selectAll)
this.selectAll = !this.selectAll
}
async toggleSelected (key: string) {
this.options[key] = !this.options[key]
}
}

View File

@@ -3,14 +3,12 @@ import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { 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],
})

View File

@@ -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>

View File

@@ -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 { }

View File

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

View File

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

View File

@@ -2,8 +2,7 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { 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 { }

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
import { Component, ViewChild } from '@angular/core'
import { Component, Input, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { 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
}

View File

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

View File

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

View File

@@ -3,9 +3,7 @@ import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router'
import { 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,

View File

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

View File

@@ -2,8 +2,7 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { 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 { }

View File

@@ -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>

View File

@@ -1,38 +1,87 @@
import { Component, ViewChild } from '@angular/core'
import { Component, Input, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { 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
}
}

View File

@@ -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>

View File

@@ -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],
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router'
import { 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,
],

View File

@@ -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],
})

View File

@@ -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],
})

View File

@@ -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>

View File

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

View File

@@ -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 {

View File

@@ -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),

View File

@@ -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>

View File

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

View File

@@ -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],
})

View File

@@ -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],
})

View File

@@ -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],

View File

@@ -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>

View File

@@ -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],
})

View File

@@ -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>

View File

@@ -57,7 +57,7 @@ export class NotificationsPage {
}
}
async remove (id: string, index: number): Promise<void> {
async delete (id: string, index: number): Promise<void> {
const loader = await this.loadingCtrl.create({
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')
},
})
}

View File

@@ -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],

View File

@@ -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: [

View File

@@ -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>

View File

@@ -1,7 +1,8 @@
import { Component } from '@angular/core'
import { Component, ViewChild } from '@angular/core'
import { ServerConfigService } from 'src/app/services/server-config.service'
import { 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)
}
}

View File

@@ -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],
})

View File

@@ -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>

View File

@@ -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',

View File

@@ -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],
})

View File

@@ -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>

View File

@@ -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,

View File

@@ -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,

View File

@@ -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],
})

View File

@@ -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],
})

View File

@@ -1,4 +1,5 @@
import { Component } from '@angular/core'
import { Component, ViewChild } from '@angular/core'
import { IonContent } from '@ionic/angular'
import { Metrics } from 'src/app/services/api/api.types'
import { 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()
}

View File

@@ -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],

View File

@@ -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],

View File

@@ -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 || '')

View File

@@ -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],
})

View File

@@ -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],

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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