From 5741cf084f86338cc8585cc3614ab569fdaf4b5d Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Fri, 6 Aug 2021 09:25:20 -0600 Subject: [PATCH] Subnav (#391) * begin subnav implementation * implement subnav AND angular forms for comparison * unions working-ish, list of enums working * new form approach almost complete * finish new forms approach for action inputs and config * expandable list items and handlebars display * Config animation (#394) * config cammel * config animation Co-authored-by: Drew Ansbacher * improve server settings inputs, still needs work * delete all notifications, styling, and bugs * contracted by default Co-authored-by: Drew Ansbacher Co-authored-by: Drew Ansbacher --- ui/src/app/app.component.html | 2 - ui/src/app/app.component.ts | 1 - ui/src/app/app.module.ts | 8 +- .../config-header.component.html | 6 +- .../form-object/form-label.component.html | 9 + .../form-object/form-object.component.html | 193 ++++++++++++++ .../form-object.component.module.ts | 27 ++ .../form-object/form-object.component.scss | 41 +++ .../form-object/form-object.component.ts | 223 ++++++++++++++++ .../install-wizard.component.html | 4 +- .../install-wizard/prebaked-wizards.ts | 2 +- .../object-config.component.html | 5 +- .../object-config.component.scss | 17 -- .../object-config/object-config.component.ts | 22 +- .../pwa-back.component.module.ts | 2 - .../components/sub-nav/sub-nav.component.html | 18 ++ .../sub-nav/sub-nav.component.module.ts | 16 ++ .../sub-nav/sub-nav.component.scss} | 0 .../components/sub-nav/sub-nav.component.ts | 23 ++ .../app-action-input.module.ts | 9 +- .../app-action-input.page.html | 42 +-- .../app-action-input.page.scss | 9 + .../app-action-input/app-action-input.page.ts | 43 +-- .../app-config-list/app-config-list.page.html | 26 +- .../app-config-list/app-config-list.page.ts | 23 +- .../app-config-object.page.html | 27 +- .../app-config-union.page.html | 17 +- .../app-config-union/app-config-union.page.ts | 4 +- .../app-config-value.page.html | 26 +- .../app-config-value/app-config-value.page.ts | 2 +- .../modals/app-config/app-config.module.ts | 22 ++ .../app-config/app-config.page.html | 93 +++---- .../modals/app-config/app-config.page.scss | 8 + .../app/modals/app-config/app-config.page.ts | 181 +++++++++++++ .../app-restore/app-restore.component.html | 1 + .../app-restore.component.module.ts | 6 +- .../app-restore/app-restore.component.ts | 3 - .../app/modals/enum-list/enum-list.module.ts | 17 ++ .../app/modals/enum-list/enum-list.page.html | 36 +++ .../app/modals/enum-list/enum-list.page.scss | 0 ui/src/app/modals/enum-list/enum-list.page.ts | 56 ++++ ui/src/app/modals/markdown/markdown.module.ts | 2 - ui/src/app/modals/markdown/markdown.page.html | 2 +- ui/src/app/modules/sharing.module.ts | 78 +++--- ui/src/app/modules/subnav.module.ts | 24 ++ .../app-actions-item.component.html | 7 + .../app-actions/app-actions.module.ts | 9 +- .../app-actions/app-actions.page.html | 91 +++---- .../app-actions/app-actions.page.ts | 41 ++- .../app-config/app-config.module.ts | 38 --- .../apps-routes/app-config/app-config.page.ts | 218 ---------------- .../app-instructions.module.ts | 4 - .../app-interfaces-item.component.html | 54 ++++ .../app-interfaces/app-interfaces.module.ts | 9 +- .../app-interfaces/app-interfaces.page.html | 49 ++-- .../app-interfaces/app-interfaces.page.ts | 83 ++++-- .../apps-routes/app-list/app-list.page.html | 2 +- .../apps-routes/app-logs/app-logs.module.ts | 6 +- .../app-manifest/app-manifest.module.ts | 28 -- .../app-manifest/app-manifest.page.html | 67 ----- .../app-manifest/app-manifest.page.scss | 8 - .../app-manifest/app-manifest.page.ts | 69 ----- .../app-metrics/app-metrics.module.ts | 2 - .../app-properties/app-properties.module.ts | 4 - .../apps-routes/app-show/app-show.module.ts | 6 +- .../apps-routes/app-show/app-show.page.html | 6 +- .../apps-routes/app-show/app-show.page.scss | 2 +- .../apps-routes/app-show/app-show.page.ts | 53 ++-- .../pages/apps-routes/apps-routing.module.ts | 12 - ui/src/app/pages/login/login.page.html | 2 +- ui/src/app/pages/login/login.page.ts | 1 + .../app-release-notes.module.ts | 4 - .../marketplace-list.module.ts | 3 - .../marketplace-show.module.ts | 4 - .../marketplace-show.page.html | 4 +- .../notifications/notifications.module.ts | 4 - .../notifications/notifications.page.html | 83 +++--- .../pages/notifications/notifications.page.ts | 23 +- .../app/pages/server-routes/lan/lan.module.ts | 2 - .../security-options.module.ts | 4 - .../security-options.page.html | 16 +- .../security-options/security-options.page.ts | 10 +- .../sessions/sessions.module.ts | 4 - .../sessions/sessions.page.html | 5 +- .../security-routes/sessions/sessions.page.ts | 2 +- .../ssh-keys/ssh-keys.module.ts | 4 - .../ssh-keys/ssh-keys.page.html | 17 +- .../security-routes/ssh-keys/ssh-keys.page.ts | 7 +- .../server-backup/server-backup.module.ts | 6 +- .../server-logs/server-logs.module.ts | 6 +- .../server-metrics/server-metrics.module.ts | 4 +- .../server-metrics/server-metrics.page.ts | 8 +- .../server-show/server-show.module.ts | 2 - .../server-specs/server-specs.module.ts | 2 - .../server-specs/server-specs.page.ts | 11 +- .../wifi/wifi-add/wifi-add.module.ts | 4 +- .../pages/server-routes/wifi/wifi.module.ts | 2 - ui/src/app/pkg-config/config-cursor.ts | 24 +- ui/src/app/pkg-config/config-types.ts | 27 +- ui/src/app/pkg-config/config-utilities.ts | 8 +- ui/src/app/pkg-config/modal-presentable.ts | 30 --- ui/src/app/services/api/api.fixures.ts | 84 +++--- ui/src/app/services/api/api.types.ts | 3 + .../api/embassy/embassy-api.service.ts | 2 + .../api/embassy/embassy-live-api.service.ts | 8 +- .../api/embassy/embassy-mock-api.service.ts | 33 ++- ui/src/app/services/config.service.ts | 3 +- ui/src/app/services/form.service.ts | 244 ++++++++++++++++++ ui/src/app/services/patch-db/data-model.ts | 2 +- .../services/pkg-status-rendering.service.ts | 2 +- ui/src/app/services/server-config.service.ts | 131 +++++++--- ui/src/app/services/startup-alerts.service.ts | 3 +- ui/src/app/services/sub-nav.service.ts | 33 +++ .../tracking-modal-controller.service.ts | 70 ----- ui/src/app/util/misc.util.ts | 3 - ui/src/global.scss | 38 +-- ui/test/config.test.ts | 2 +- 117 files changed, 1967 insertions(+), 1271 deletions(-) create mode 100644 ui/src/app/components/form-object/form-label.component.html create mode 100644 ui/src/app/components/form-object/form-object.component.html create mode 100644 ui/src/app/components/form-object/form-object.component.module.ts create mode 100644 ui/src/app/components/form-object/form-object.component.scss create mode 100644 ui/src/app/components/form-object/form-object.component.ts create mode 100644 ui/src/app/components/sub-nav/sub-nav.component.html create mode 100644 ui/src/app/components/sub-nav/sub-nav.component.module.ts rename ui/src/app/{pages/apps-routes/app-config/app-config.page.scss => components/sub-nav/sub-nav.component.scss} (100%) create mode 100644 ui/src/app/components/sub-nav/sub-nav.component.ts create mode 100644 ui/src/app/modals/app-config/app-config.module.ts rename ui/src/app/{pages/apps-routes => modals}/app-config/app-config.page.html (52%) create mode 100644 ui/src/app/modals/app-config/app-config.page.scss create mode 100644 ui/src/app/modals/app-config/app-config.page.ts create mode 100644 ui/src/app/modals/enum-list/enum-list.module.ts create mode 100644 ui/src/app/modals/enum-list/enum-list.page.html create mode 100644 ui/src/app/modals/enum-list/enum-list.page.scss create mode 100644 ui/src/app/modals/enum-list/enum-list.page.ts create mode 100644 ui/src/app/modules/subnav.module.ts create mode 100644 ui/src/app/pages/apps-routes/app-actions/app-actions-item.component.html delete mode 100644 ui/src/app/pages/apps-routes/app-config/app-config.module.ts delete mode 100644 ui/src/app/pages/apps-routes/app-config/app-config.page.ts create mode 100644 ui/src/app/pages/apps-routes/app-interfaces/app-interfaces-item.component.html delete mode 100644 ui/src/app/pages/apps-routes/app-manifest/app-manifest.module.ts delete mode 100644 ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.html delete mode 100644 ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.scss delete mode 100644 ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.ts delete mode 100644 ui/src/app/pkg-config/modal-presentable.ts create mode 100644 ui/src/app/services/form.service.ts create mode 100644 ui/src/app/services/sub-nav.service.ts delete mode 100644 ui/src/app/services/tracking-modal-controller.service.ts diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index e70261339..aa4aa7fdd 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -70,7 +70,6 @@ - @@ -93,7 +92,6 @@ - diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index 3b00243d5..8dccb050d 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -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() } diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index c9a64052a..10d19e4cd 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -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] }, diff --git a/ui/src/app/components/config-header/config-header.component.html b/ui/src/app/components/config-header/config-header.component.html index 75aa7ec71..9a85e1300 100644 --- a/ui/src/app/components/config-header/config-header.component.html +++ b/ui/src/app/components/config-header/config-header.component.html @@ -7,7 +7,7 @@ {{ error }} - + @@ -19,12 +19,12 @@ - +

Warning!

-

+

\ No newline at end of file diff --git a/ui/src/app/components/form-object/form-label.component.html b/ui/src/app/components/form-object/form-label.component.html new file mode 100644 index 000000000..6090edaf5 --- /dev/null +++ b/ui/src/app/components/form-object/form-label.component.html @@ -0,0 +1,9 @@ + + {{ data.spec.name }} + + (New) + (Edited) + + * + + * \ No newline at end of file diff --git a/ui/src/app/components/form-object/form-object.component.html b/ui/src/app/components/form-object/form-object.component.html new file mode 100644 index 000000000..38ce2139f --- /dev/null +++ b/ui/src/app/components/form-object/form-object.component.html @@ -0,0 +1,193 @@ + +
+ +

{{ unionSpec.tag.name }}

+ + {{ unionSpec.tag.name }} + + + {{ unionSpec.tag['variant-names'][option] }} + + + +
+ + + + +

+ +

+ + + + + + + + + + + {{ spec.units }} + + + + {{ spec.name }} + + + + + {{ spec.name }} + + + {{ spec['value-names'][option] }} + + + +
+ + + + + + + +
+ +
+
+ + + + + + + + + Add + + + +
+
+ + + + + + + + +
+ + + +
+ + + Delete + +
+
+
+ + + + + + + + + +

+ {{ validation.message }} +

+
+
+
+
+
+
+ + + + +

+ +

+ + + +

{{ getEnumListDisplay(formArr.value, spec.spec) }}

+
+ + + +
+
+
+
+

+ {{ spec.name }}: {{ validation.message }} +

+
+
+
+
\ No newline at end of file diff --git a/ui/src/app/components/form-object/form-object.component.module.ts b/ui/src/app/components/form-object/form-object.component.module.ts new file mode 100644 index 000000000..6fec75e08 --- /dev/null +++ b/ui/src/app/components/form-object/form-object.component.module.ts @@ -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 { } diff --git a/ui/src/app/components/form-object/form-object.component.scss b/ui/src/app/components/form-object/form-object.component.scss new file mode 100644 index 000000000..2d1b5e5bd --- /dev/null +++ b/ui/src/app/components/form-object/form-object.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/ui/src/app/components/form-object/form-object.component.ts b/ui/src/app/components/form-object/form-object.component.ts new file mode 100644 index 000000000..c3e0c027a --- /dev/null +++ b/ui/src/app/components/form-object/form-object.component.ts @@ -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() + } +} diff --git a/ui/src/app/components/install-wizard/install-wizard.component.html b/ui/src/app/components/install-wizard/install-wizard.component.html index 48d5ddcd0..579e56689 100644 --- a/ui/src/app/components/install-wizard/install-wizard.component.html +++ b/ui/src/app/components/install-wizard/install-wizard.component.html @@ -39,11 +39,11 @@ {{ cancel.text }} - + {{ cancel.text }} - + diff --git a/ui/src/app/components/install-wizard/prebaked-wizards.ts b/ui/src/app/components/install-wizard/prebaked-wizards.ts index 20d252fb4..6874163de 100644 --- a/ui/src/app/components/install-wizard/prebaked-wizards.ts +++ b/ui/src/app/components/install-wizard/prebaked-wizards.ts @@ -139,7 +139,7 @@ export class WizardBaker { }, }, bottomBar: { - cancel: { afterLoading: { text: 'Cancel' } }, next: 'Update OS', + cancel: { afterLoading: { text: 'Cancel' } }, next: 'Begin Update', }, }, { diff --git a/ui/src/app/components/object-config/object-config.component.html b/ui/src/app/components/object-config/object-config.component.html index ed4a46da3..fb5de98bd 100644 --- a/ui/src/app/components/object-config/object-config.component.html +++ b/ui/src/app/components/object-config/object-config.component.html @@ -1,5 +1,6 @@ -
+ -
\ No newline at end of file + \ No newline at end of file diff --git a/ui/src/app/components/object-config/object-config.component.scss b/ui/src/app/components/object-config/object-config.component.scss index 62a262f47..8dbbe218e 100644 --- a/ui/src/app/components/object-config/object-config.component.scss +++ b/ui/src/app/components/object-config/object-config.component.scss @@ -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; } \ No newline at end of file diff --git a/ui/src/app/components/object-config/object-config.component.ts b/ui/src/app/components/object-config/object-config.component.ts index cbef09341..c101e6385 100644 --- a/ui/src/app/components/object-config/object-config.component.ts +++ b/ui/src/app/components/object-config/object-config.component.ts @@ -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() 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' diff --git a/ui/src/app/components/pwa-back-button/pwa-back.component.module.ts b/ui/src/app/components/pwa-back-button/pwa-back.component.module.ts index 94443f554..889cbcda1 100644 --- a/ui/src/app/components/pwa-back-button/pwa-back.component.module.ts +++ b/ui/src/app/components/pwa-back-button/pwa-back.component.module.ts @@ -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], }) diff --git a/ui/src/app/components/sub-nav/sub-nav.component.html b/ui/src/app/components/sub-nav/sub-nav.component.html new file mode 100644 index 000000000..b870cce84 --- /dev/null +++ b/ui/src/app/components/sub-nav/sub-nav.component.html @@ -0,0 +1,18 @@ + + + + + + + + + + / + {{ segment }} + {{ segment }} + + + + + + \ No newline at end of file diff --git a/ui/src/app/components/sub-nav/sub-nav.component.module.ts b/ui/src/app/components/sub-nav/sub-nav.component.module.ts new file mode 100644 index 000000000..c8b058409 --- /dev/null +++ b/ui/src/app/components/sub-nav/sub-nav.component.module.ts @@ -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 { } diff --git a/ui/src/app/pages/apps-routes/app-config/app-config.page.scss b/ui/src/app/components/sub-nav/sub-nav.component.scss similarity index 100% rename from ui/src/app/pages/apps-routes/app-config/app-config.page.scss rename to ui/src/app/components/sub-nav/sub-nav.component.scss diff --git a/ui/src/app/components/sub-nav/sub-nav.component.ts b/ui/src/app/components/sub-nav/sub-nav.component.ts new file mode 100644 index 000000000..3aec92ea2 --- /dev/null +++ b/ui/src/app/components/sub-nav/sub-nav.component.ts @@ -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] + } +} diff --git a/ui/src/app/modals/app-action-input/app-action-input.module.ts b/ui/src/app/modals/app-action-input/app-action-input.module.ts index 10465c32a..3559304a3 100644 --- a/ui/src/app/modals/app-action-input/app-action-input.module.ts +++ b/ui/src/app/modals/app-action-input/app-action-input.module.ts @@ -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], diff --git a/ui/src/app/modals/app-action-input/app-action-input.page.html b/ui/src/app/modals/app-action-input/app-action-input.page.html index 1626472db..5c7b67628 100644 --- a/ui/src/app/modals/app-action-input/app-action-input.page.html +++ b/ui/src/app/modals/app-action-input/app-action-input.page.html @@ -1,27 +1,29 @@ - - - - - {{ action.name }} - - - Save - - - - - - - - - - - - + +
+ +
+ + + + + + Cancel + + + + + Execute + + + + diff --git a/ui/src/app/modals/app-action-input/app-action-input.page.scss b/ui/src/app/modals/app-action-input/app-action-input.page.scss index e69de29bb..0353411b3 100644 --- a/ui/src/app/modals/app-action-input/app-action-input.page.scss +++ b/ui/src/app/modals/app-action-input/app-action-input.page.scss @@ -0,0 +1,9 @@ +button:disabled, +button[disabled]{ + border: 1px solid #999999; + background-color: #cccccc; + color: #666666; +} +button { + color: var(--ion-color-primary); +} \ No newline at end of file diff --git a/ui/src/app/modals/app-action-input/app-action-input.page.ts b/ui/src/app/modals/app-action-input/app-action-input.page.ts index b268869f4..b73fb3b32 100644 --- a/ui/src/app/modals/app-action-input/app-action-input.page.ts +++ b/ui/src/app/modals/app-action-input/app-action-input.page.ts @@ -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 - 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 { @@ -35,24 +27,15 @@ export class AppActionInputPage { } async save (): Promise { - 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 } } diff --git a/ui/src/app/modals/app-config-list/app-config-list.page.html b/ui/src/app/modals/app-config-list/app-config-list.page.html index a9e23fb2c..176542c04 100644 --- a/ui/src/app/modals/app-config-list/app-config-list.page.html +++ b/ui/src/app/modals/app-config-list/app-config-list.page.html @@ -1,23 +1,11 @@ - - - - - - - - {{ spec.name }} - - - - - - - - - + + + + + @@ -46,7 +34,7 @@
- + @@ -55,7 +43,7 @@ {{ valueString[i] }} - +
diff --git a/ui/src/app/modals/app-config-list/app-config-list.page.ts b/ui/src/app/modals/app-config-list/app-config-list.page.ts index 0a16fb4e1..d7a5ed162 100644 --- a/ui/src/app/modals/app-config-list/app-config-list.page.ts +++ b/ui/src/app/modals/app-config-list/app-config-list.page.ts @@ -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) { diff --git a/ui/src/app/modals/app-config-object/app-config-object.page.html b/ui/src/app/modals/app-config-object/app-config-object.page.html index c1c8e18f5..31204991c 100644 --- a/ui/src/app/modals/app-config-object/app-config-object.page.html +++ b/ui/src/app/modals/app-config-object/app-config-object.page.html @@ -1,27 +1,4 @@ - - - - - - - - {{ spec.name }} - - - Delete - - - - - - - + - - - - - - - + diff --git a/ui/src/app/modals/app-config-union/app-config-union.page.html b/ui/src/app/modals/app-config-union/app-config-union.page.html index 332713beb..abadbccc2 100644 --- a/ui/src/app/modals/app-config-union/app-config-union.page.html +++ b/ui/src/app/modals/app-config-union/app-config-union.page.html @@ -1,15 +1,4 @@ - - - - - - - - {{ spec.name }} - - - - + @@ -24,10 +13,10 @@ {{ spec.tag.name }} - {{ spec.tag.variantNames[option.key] }} + {{ spec.tag['variant-names'][option.key] }} (default) diff --git a/ui/src/app/modals/app-config-union/app-config-union.page.ts b/ui/src/app/modals/app-config-union/app-config-union.page.ts index 4d0f34172..2c386da5f 100644 --- a/ui/src/app/modals/app-config-union/app-config-union.page.ts +++ b/ui/src/app/modals/app-config-union/app-config-union.page.ts @@ -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', } } diff --git a/ui/src/app/modals/app-config-value/app-config-value.page.html b/ui/src/app/modals/app-config-value/app-config-value.page.html index ed74f6133..009084ca8 100644 --- a/ui/src/app/modals/app-config-value/app-config-value.page.html +++ b/ui/src/app/modals/app-config-value/app-config-value.page.html @@ -1,22 +1,4 @@ - - - - - - - - - {{ spec.name }} - - - - Save - - - - - - + @@ -55,15 +37,15 @@ - {{ spec.valueNames[option] }} + {{ spec['value-names'][option] }}
-

- {{ spec.patternDescription }} +

+ {{ spec['pattern-description'] }}

{{ integralDescription }} diff --git a/ui/src/app/modals/app-config-value/app-config-value.page.ts b/ui/src/app/modals/app-config-value/app-config-value.page.ts index 60f8407a9..e5f120d81 100644 --- a/ui/src/app/modals/app-config-value/app-config-value.page.ts +++ b/ui/src/app/modals/app-config-value/app-config-value.page.ts @@ -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 diff --git a/ui/src/app/modals/app-config/app-config.module.ts b/ui/src/app/modals/app-config/app-config.module.ts new file mode 100644 index 000000000..1dd9d1f31 --- /dev/null +++ b/ui/src/app/modals/app-config/app-config.module.ts @@ -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 { } diff --git a/ui/src/app/pages/apps-routes/app-config/app-config.page.html b/ui/src/app/modals/app-config/app-config.page.html similarity index 52% rename from ui/src/app/pages/apps-routes/app-config/app-config.page.html rename to ui/src/app/modals/app-config/app-config.page.html index 97c9d0636..4a1cce58d 100644 --- a/ui/src/app/pages/apps-routes/app-config/app-config.page.html +++ b/ui/src/app/modals/app-config/app-config.page.html @@ -1,40 +1,23 @@ - - - - + + Config + + + Reset Defaults - {{ pkg.manifest.title }} - + - + - - -

{{ error.text }}

-

{{error.moreInfo.buttonText}}

- -

-

- Hide -
- - - - - - - - - + @@ -42,7 +25,7 @@ Initial Config -

To use the default config for {{ pkg.manifest.title }}, click "Save" below.

+

To use the default config for {{ pkg.manifest.title }}, click "Save" above.

@@ -59,7 +42,7 @@

{{ pkg.manifest.title }} config has been modified to satisfy {{ rec.dependentTitle }}. - To accept the changes, click “Save” below. + To accept the changes, click “Save” above.

More Info @@ -67,7 +50,7 @@ hide - +
@@ -75,41 +58,37 @@
- - - -

{{invalid}}

-
-
-

No config options for {{ pkg.manifest.title }} {{ pkg.manifest.version }}.

- - - - - Save - - - - - - - Config Options - - -
+ + +
+ +
+ + + + + + Cancel + + + + + Save + + + + diff --git a/ui/src/app/modals/app-config/app-config.page.scss b/ui/src/app/modals/app-config/app-config.page.scss new file mode 100644 index 000000000..98a2553cd --- /dev/null +++ b/ui/src/app/modals/app-config/app-config.page.scss @@ -0,0 +1,8 @@ +.notifier-item { + margin: 12px; + margin-top: 0px; + border-radius: 12px; + // kills the lines + --border-width: 0; + --inner-border-width: 0; +} \ No newline at end of file diff --git a/ui/src/app/modals/app-config/app-config.page.ts b/ui/src/app/modals/app-config/app-config.page.ts new file mode 100644 index 000000000..a3b24fda7 --- /dev/null +++ b/ui/src/app/modals/app-config/app-config.page.ts @@ -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() + } +} + diff --git a/ui/src/app/modals/app-restore/app-restore.component.html b/ui/src/app/modals/app-restore/app-restore.component.html index 8c84baeef..093ce9aa1 100644 --- a/ui/src/app/modals/app-restore/app-restore.component.html +++ b/ui/src/app/modals/app-restore/app-restore.component.html @@ -10,6 +10,7 @@ + diff --git a/ui/src/app/modals/app-restore/app-restore.component.module.ts b/ui/src/app/modals/app-restore/app-restore.component.module.ts index bb729830f..f5ab8a6d3 100644 --- a/ui/src/app/modals/app-restore/app-restore.component.module.ts +++ b/ui/src/app/modals/app-restore/app-restore.component.module.ts @@ -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, diff --git a/ui/src/app/modals/app-restore/app-restore.component.ts b/ui/src/app/modals/app-restore/app-restore.component.ts index ff3213ef4..36a397dd8 100644 --- a/ui/src/app/modals/app-restore/app-restore.component.ts +++ b/ui/src/app/modals/app-restore/app-restore.component.ts @@ -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 { - console.log('here here here') this.submitting = true // await loader.present() diff --git a/ui/src/app/modals/enum-list/enum-list.module.ts b/ui/src/app/modals/enum-list/enum-list.module.ts new file mode 100644 index 000000000..fa7bfa872 --- /dev/null +++ b/ui/src/app/modals/enum-list/enum-list.module.ts @@ -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 { } \ No newline at end of file diff --git a/ui/src/app/modals/enum-list/enum-list.page.html b/ui/src/app/modals/enum-list/enum-list.page.html new file mode 100644 index 000000000..edd340834 --- /dev/null +++ b/ui/src/app/modals/enum-list/enum-list.page.html @@ -0,0 +1,36 @@ + + + + {{ spec.name }} + + + + {{ selectAll ? 'All' : 'None' }} + + + + + + + + + {{ option.key }} + + + + + + + + + + Cancel + + + + + Done + + + + diff --git a/ui/src/app/modals/enum-list/enum-list.page.scss b/ui/src/app/modals/enum-list/enum-list.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/modals/enum-list/enum-list.page.ts b/ui/src/app/modals/enum-list/enum-list.page.ts new file mode 100644 index 000000000..65f59d3b9 --- /dev/null +++ b/ui/src/app/modals/enum-list/enum-list.page.ts @@ -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] + } +} diff --git a/ui/src/app/modals/markdown/markdown.module.ts b/ui/src/app/modals/markdown/markdown.module.ts index 12d942997..8e3ff6bd1 100644 --- a/ui/src/app/modals/markdown/markdown.module.ts +++ b/ui/src/app/modals/markdown/markdown.module.ts @@ -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], }) diff --git a/ui/src/app/modals/markdown/markdown.page.html b/ui/src/app/modals/markdown/markdown.page.html index d61b820e1..74970c86e 100644 --- a/ui/src/app/modals/markdown/markdown.page.html +++ b/ui/src/app/modals/markdown/markdown.page.html @@ -2,7 +2,7 @@ - + {{ title | titlecase }} diff --git a/ui/src/app/modules/sharing.module.ts b/ui/src/app/modules/sharing.module.ts index ce9067fba..8998b7987 100644 --- a/ui/src/app/modules/sharing.module.ts +++ b/ui/src/app/modules/sharing.module.ts @@ -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 { } \ No newline at end of file diff --git a/ui/src/app/modules/subnav.module.ts b/ui/src/app/modules/subnav.module.ts new file mode 100644 index 000000000..2a97dc466 --- /dev/null +++ b/ui/src/app/modules/subnav.module.ts @@ -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 { } \ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-actions/app-actions-item.component.html b/ui/src/app/pages/apps-routes/app-actions/app-actions-item.component.html new file mode 100644 index 000000000..5292551e6 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-actions/app-actions-item.component.html @@ -0,0 +1,7 @@ + + + +

{{ action.name }}

+

{{ action.description }}

+
+
\ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts b/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts index 8d5cb6e52..7a8a32f54 100644 --- a/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts +++ b/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts @@ -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 { } diff --git a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html index db49895c2..d19da4d21 100644 --- a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html +++ b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html @@ -10,66 +10,37 @@ - + - - - - - - - - - - {{ action.value.name }} - - - {{ action.value.description }} - - - - - - - - - - Restore From Backup - - - All changes since backup will be lost. - - - - - - - - - - Uninstall - - - This will uninstall the service from your Embassy and delete all data permanently. - - - - - + + Standard Actions + + + + - - + + Actions for {{ pkg.manifest.title }} + + + \ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts index 7ac13751c..4de718926 100644 --- a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts +++ b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts @@ -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 { - 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 { + private async executeAction (pkgId: string, actionId: string, input?: object): Promise { 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 +} diff --git a/ui/src/app/pages/apps-routes/app-config/app-config.module.ts b/ui/src/app/pages/apps-routes/app-config/app-config.module.ts deleted file mode 100644 index 38b653d60..000000000 --- a/ui/src/app/pages/apps-routes/app-config/app-config.module.ts +++ /dev/null @@ -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 { } diff --git a/ui/src/app/pages/apps-routes/app-config/app-config.page.ts b/ui/src/app/pages/apps-routes/app-config/app-config.page.ts deleted file mode 100644 index f47146bd8..000000000 --- a/ui/src/app/pages/apps-routes/app-config/app-config.page.ts +++ /dev/null @@ -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() - } -} - diff --git a/ui/src/app/pages/apps-routes/app-instructions/app-instructions.module.ts b/ui/src/app/pages/apps-routes/app-instructions/app-instructions.module.ts index ba27bd38c..0225b2fdc 100644 --- a/ui/src/app/pages/apps-routes/app-instructions/app-instructions.module.ts +++ b/ui/src/app/pages/apps-routes/app-instructions/app-instructions.module.ts @@ -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, diff --git a/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces-item.component.html b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces-item.component.html new file mode 100644 index 000000000..680209901 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces-item.component.html @@ -0,0 +1,54 @@ + + + +

{{ interface.def.name }}

+

{{ interface.def.description }}

+
+
+
+ + + +

Tor Address

+

{{ tor }}

+
+ + + + + + + + +
+ + + +

Tor Address

+

Service does not use a Tor Address

+
+
+ + + + +

LAN Address

+

{{ lan }}

+
+ + + + + + + + +
+ + + +

LAN Address

+

Service does not use a LAN Address

+
+
+
\ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.module.ts b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.module.ts index f619686d3..43d9e4e2f 100644 --- a/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.module.ts +++ b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.module.ts @@ -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 { } diff --git a/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html index 1d82350a7..8c76776ef 100644 --- a/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html +++ b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html @@ -7,37 +7,20 @@
- - - - {{ interface.value.name }} - {{ interface.value.description }} - - Launch - - - - - - - -

Tor Address

-

{{ 'http://' + int['tor-address'] }}

-
- - - -
- - -

LAN Address

-

{{ 'https://' + int['lan-address'] }}

-
- - - -
-
-
-
+ + + + + Web User Interface + + + + + + Other Interfaces +
+ +
+
+
\ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts index 8fc38f34d..503865cd6 100644 --- a/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts +++ b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts @@ -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 { 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 - } } diff --git a/ui/src/app/pages/apps-routes/app-list/app-list.page.html b/ui/src/app/pages/apps-routes/app-list/app-list.page.html index abde30d58..03231d77d 100644 --- a/ui/src/app/pages/apps-routes/app-list/app-list.page.html +++ b/ui/src/app/pages/apps-routes/app-list/app-list.page.html @@ -26,7 +26,7 @@
- +
diff --git a/ui/src/app/pages/apps-routes/app-logs/app-logs.module.ts b/ui/src/app/pages/apps-routes/app-logs/app-logs.module.ts index d815b561a..5bbab85a2 100644 --- a/ui/src/app/pages/apps-routes/app-logs/app-logs.module.ts +++ b/ui/src/app/pages/apps-routes/app-logs/app-logs.module.ts @@ -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], }) diff --git a/ui/src/app/pages/apps-routes/app-manifest/app-manifest.module.ts b/ui/src/app/pages/apps-routes/app-manifest/app-manifest.module.ts deleted file mode 100644 index f794d3178..000000000 --- a/ui/src/app/pages/apps-routes/app-manifest/app-manifest.module.ts +++ /dev/null @@ -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 { } diff --git a/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.html b/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.html deleted file mode 100644 index 7a2313338..000000000 --- a/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - Package Manifest - - - - - Formatted - - - Raw - - - - - - - -
- - Formatted Manifest - - - - - - - - -
- - - - -

{{ prop.key }}

-
-
-
- - - - -

{{ prop.key }}

-

{{ prop.value }}

-
-
-
-
-
- - - - - {{ prop }} - - - -
- -
-

-  
-
- diff --git a/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.scss b/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.scss deleted file mode 100644 index 3d34b4d98..000000000 --- a/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.scss +++ /dev/null @@ -1,8 +0,0 @@ -.raw { - background-color: var(--ion-color-light); - pre { - margin: 0; - padding: 12px; - white-space: pre-wrap; - } -} \ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.ts b/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.ts deleted file mode 100644 index 10fdc0a36..000000000 --- a/ui/src/app/pages/apps-routes/app-manifest/app-manifest.page.ts +++ /dev/null @@ -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 { - this.pointer = `${this.pointer || ''}/${key}` - this.setNode() - } - - asIsOrder (a: any, b: any) { - return 0 - } -} diff --git a/ui/src/app/pages/apps-routes/app-metrics/app-metrics.module.ts b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.module.ts index a4afbc9d1..1e6346782 100644 --- a/ui/src/app/pages/apps-routes/app-metrics/app-metrics.module.ts +++ b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.module.ts @@ -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, ], diff --git a/ui/src/app/pages/apps-routes/app-properties/app-properties.module.ts b/ui/src/app/pages/apps-routes/app-properties/app-properties.module.ts index ccf65fd53..db64af1b9 100644 --- a/ui/src/app/pages/apps-routes/app-properties/app-properties.module.ts +++ b/ui/src/app/pages/apps-routes/app-properties/app-properties.module.ts @@ -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], }) diff --git a/ui/src/app/pages/apps-routes/app-show/app-show.module.ts b/ui/src/app/pages/apps-routes/app-show/app-show.module.ts index fdc51942e..8e8afa59f 100644 --- a/ui/src/app/pages/apps-routes/app-show/app-show.module.ts +++ b/ui/src/app/pages/apps-routes/app-show/app-show.module.ts @@ -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], }) diff --git a/ui/src/app/pages/apps-routes/app-show/app-show.page.html b/ui/src/app/pages/apps-routes/app-show/app-show.page.html index 43a6b06a5..02bfbe7ef 100644 --- a/ui/src/app/pages/apps-routes/app-show/app-show.page.html +++ b/ui/src/app/pages/apps-routes/app-show/app-show.page.html @@ -27,8 +27,8 @@ - Web - + + Open UI Configure @@ -53,7 +53,7 @@ - +

{{ health.key }}

diff --git a/ui/src/app/pages/apps-routes/app-show/app-show.page.scss b/ui/src/app/pages/apps-routes/app-show/app-show.page.scss index 504da9ea1..7ec7d334e 100644 --- a/ui/src/app/pages/apps-routes/app-show/app-show.page.scss +++ b/ui/src/app/pages/apps-routes/app-show/app-show.page.scss @@ -5,7 +5,7 @@ .action-button { margin: 10px; min-height: 36px; - min-width: 72px; + min-width: 120px; } .icon-spinner { diff --git a/ui/src/app/pages/apps-routes/app-show/app-show.page.ts b/ui/src/app/pages/apps-routes/app-show/app-show.page.ts index 0b1c6e51b..e9eb61a55 100644 --- a/ui/src/app/pages/apps-routes/app-show/app-show.page.ts +++ b/ui/src/app/pages/apps-routes/app-show/app-show.page.ts @@ -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 { + const modal = await this.modalCtrl.create({ + component: AppConfigPage, + componentProps: { + pkgId: this.pkgId, + }, + }) + await modal.present() } private async installDep (depId: string): Promise { @@ -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 { diff --git a/ui/src/app/pages/apps-routes/apps-routing.module.ts b/ui/src/app/pages/apps-routes/apps-routing.module.ts index 7816170c1..4884c0f1f 100644 --- a/ui/src/app/pages/apps-routes/apps-routing.module.ts +++ b/ui/src/app/pages/apps-routes/apps-routing.module.ts @@ -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), diff --git a/ui/src/app/pages/login/login.page.html b/ui/src/app/pages/login/login.page.html index ef6a33ed2..aa907c2ad 100644 --- a/ui/src/app/pages/login/login.page.html +++ b/ui/src/app/pages/login/login.page.html @@ -3,7 +3,7 @@ -
+
diff --git a/ui/src/app/pages/login/login.page.ts b/ui/src/app/pages/login/login.page.ts index dd40cbd80..b41641456 100644 --- a/ui/src/app/pages/login/login.page.ts +++ b/ui/src/app/pages/login/login.page.ts @@ -35,6 +35,7 @@ export class LoginPage { this.loader = await this.loadingCtrl.create({ message: 'Logging in', spinner: 'lines', + cssClass: 'loader', }) await this.loader.present() diff --git a/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.module.ts b/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.module.ts index 8d4c06a60..ac54f1a3c 100644 --- a/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.module.ts +++ b/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.module.ts @@ -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], }) diff --git a/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts b/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts index 0e061a424..7a0db298c 100644 --- a/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts +++ b/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts @@ -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], }) diff --git a/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts b/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts index 7023a22e8..3cf576e98 100644 --- a/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts +++ b/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts @@ -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], diff --git a/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html b/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html index bdd67a882..810b96bb3 100644 --- a/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html +++ b/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html @@ -81,8 +81,8 @@

{{ rec.description }}

{{ pkg.manifest.title }} version {{ pkg.manifest.version | displayEmver }} is compatible.

{{ pkg.manifest.title }} version {{ pkg.manifest.version | displayEmver }} is NOT compatible.

- - + +
diff --git a/ui/src/app/pages/notifications/notifications.module.ts b/ui/src/app/pages/notifications/notifications.module.ts index 0aaf655cf..63195890e 100644 --- a/ui/src/app/pages/notifications/notifications.module.ts +++ b/ui/src/app/pages/notifications/notifications.module.ts @@ -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], }) diff --git a/ui/src/app/pages/notifications/notifications.page.html b/ui/src/app/pages/notifications/notifications.page.html index ae6351eed..207f24740 100644 --- a/ui/src/app/pages/notifications/notifications.page.html +++ b/ui/src/app/pages/notifications/notifications.page.html @@ -17,41 +17,52 @@ - - - - Notifications about Embassy and services will appear here. - - - - - - - -

- {{ not.title }} -

-

- {{ not.message }} - - View Report - -

-

- {{ not['created-at'] | date: 'short' }} - - - {{ not['package-id'] }} - -

-
- - - -
-
- - - - + + + + + + Notifications about Embassy and services will appear here. + + + + + + + + + + Delete All + + + + +

+ {{ not.title }} +

+

+ {{ not.message }} + + View Report + +

+

+ {{ not['created-at'] | date: 'short' }} + + - {{ not['package-id'] }} + +

+
+ + + +
+
+ + + + +
+
\ No newline at end of file diff --git a/ui/src/app/pages/notifications/notifications.page.ts b/ui/src/app/pages/notifications/notifications.page.ts index 9457f84a2..52132c920 100644 --- a/ui/src/app/pages/notifications/notifications.page.ts +++ b/ui/src/app/pages/notifications/notifications.page.ts @@ -57,7 +57,7 @@ export class NotificationsPage { } } - async remove (id: string, index: number): Promise { + async delete (id: string, index: number): Promise { const loader = await this.loadingCtrl.create({ spinner: 'lines', message: 'Deleting...', @@ -75,6 +75,24 @@ export class NotificationsPage { } } + async deleteAll (): Promise { + 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') - }, }) } diff --git a/ui/src/app/pages/server-routes/lan/lan.module.ts b/ui/src/app/pages/server-routes/lan/lan.module.ts index 744334479..538f55d14 100644 --- a/ui/src/app/pages/server-routes/lan/lan.module.ts +++ b/ui/src/app/pages/server-routes/lan/lan.module.ts @@ -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], diff --git a/ui/src/app/pages/server-routes/security-routes/security-options/security-options.module.ts b/ui/src/app/pages/server-routes/security-routes/security-options/security-options.module.ts index a1b32b4a8..412442d43 100644 --- a/ui/src/app/pages/server-routes/security-routes/security-options/security-options.module.ts +++ b/ui/src/app/pages/server-routes/security-routes/security-options/security-options.module.ts @@ -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: [ diff --git a/ui/src/app/pages/server-routes/security-routes/security-options/security-options.page.html b/ui/src/app/pages/server-routes/security-routes/security-options/security-options.page.html index 287159381..cf33c828d 100644 --- a/ui/src/app/pages/server-routes/security-routes/security-options/security-options.page.html +++ b/ui/src/app/pages/server-routes/security-routes/security-options/security-options.page.html @@ -11,23 +11,23 @@ General - + Share Anonymous Statistics - {{ patch.data['server-info']['share-stats'] }} + {{ server['share-stats'] ? 'Enabled' : 'Disabled' }} Marketplace - + Auto Check for Updates - {{ patch.data.ui['auto-check-updates'] }} + {{ patch.data.ui['auto-check-updates'] ? 'Enabled' : 'Disabled' }} - + Tor Only Marketplace - {{ patch.data['server-info']['eos-marketplace'] === config.start9Marketplace.tor }} + {{ server['eos-marketplace'] === config.start9Marketplace.tor ? 'Enabled' : 'Disabled' }} - Security diff --git a/ui/src/app/pages/server-routes/security-routes/security-options/security-options.page.ts b/ui/src/app/pages/server-routes/security-routes/security-options/security-options.page.ts index 69acb16cd..9dfe91290 100644 --- a/ui/src/app/pages/server-routes/security-routes/security-options/security-options.page.ts +++ b/ui/src/app/pages/server-routes/security-routes/security-options/security-options.page.ts @@ -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 { - await this.serverConfigService.presentModalValueEdit(key, current) + ngAfterViewInit () { + this.content.scrollToPoint(undefined, 1) } } diff --git a/ui/src/app/pages/server-routes/security-routes/sessions/sessions.module.ts b/ui/src/app/pages/server-routes/security-routes/sessions/sessions.module.ts index a8006122d..15ef586e4 100644 --- a/ui/src/app/pages/server-routes/security-routes/sessions/sessions.module.ts +++ b/ui/src/app/pages/server-routes/security-routes/sessions/sessions.module.ts @@ -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], }) diff --git a/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.html b/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.html index 331171c7a..4503c8346 100644 --- a/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.html +++ b/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.html @@ -31,8 +31,9 @@

Last Active: {{ session.value['last-active'] | date : 'medium' }}

{{ session.value['user-agent'] }}

- - + + + Kill
diff --git a/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.ts b/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.ts index bfba3f89b..cd7344313 100644 --- a/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.ts +++ b/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.ts @@ -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', diff --git a/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.module.ts b/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.module.ts index 814e08fdd..5e3bd9b91 100644 --- a/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.module.ts +++ b/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.module.ts @@ -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], }) diff --git a/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.page.html b/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.page.html index 0572593d8..adcf10f4b 100644 --- a/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.page.html +++ b/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.page.html @@ -5,7 +5,7 @@ SSH Keys - + @@ -15,14 +15,25 @@ - + + + + +

About

+

Adding an SSH key to your Embassy can be useful for advanced usage from the command line, as well as for debugging purposes.

+
+
+ + + View Instructions + Saved Keys {{ ssh.value.alg }} {{ ssh.key }} {{ ssh.value.hostname }} - +
diff --git a/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.page.ts b/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.page.ts index 7e2beeb3f..9eca53c36 100644 --- a/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.page.ts +++ b/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.page.ts @@ -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, diff --git a/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts b/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts index 7a411305d..d16b52955 100644 --- a/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts +++ b/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts @@ -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, diff --git a/ui/src/app/pages/server-routes/server-logs/server-logs.module.ts b/ui/src/app/pages/server-routes/server-logs/server-logs.module.ts index e1a7b0490..25408d076 100644 --- a/ui/src/app/pages/server-routes/server-logs/server-logs.module.ts +++ b/ui/src/app/pages/server-routes/server-logs/server-logs.module.ts @@ -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], }) diff --git a/ui/src/app/pages/server-routes/server-metrics/server-metrics.module.ts b/ui/src/app/pages/server-routes/server-metrics/server-metrics.module.ts index 151a11da9..ba6defbd9 100644 --- a/ui/src/app/pages/server-routes/server-metrics/server-metrics.module.ts +++ b/ui/src/app/pages/server-routes/server-metrics/server-metrics.module.ts @@ -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], }) diff --git a/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts index 8761d3e7f..b413326ed 100644 --- a/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts +++ b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts @@ -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() } diff --git a/ui/src/app/pages/server-routes/server-show/server-show.module.ts b/ui/src/app/pages/server-routes/server-show/server-show.module.ts index 79cc2be2b..8c41afc98 100644 --- a/ui/src/app/pages/server-routes/server-show/server-show.module.ts +++ b/ui/src/app/pages/server-routes/server-show/server-show.module.ts @@ -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], diff --git a/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts b/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts index cc8ab1182..a6b28c60c 100644 --- a/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts +++ b/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts @@ -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], diff --git a/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts b/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts index 9aa535b81..d7acd3020 100644 --- a/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts +++ b/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts @@ -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 || '') diff --git a/ui/src/app/pages/server-routes/wifi/wifi-add/wifi-add.module.ts b/ui/src/app/pages/server-routes/wifi/wifi-add/wifi-add.module.ts index 61c0bded8..a574af4d2 100644 --- a/ui/src/app/pages/server-routes/wifi/wifi-add/wifi-add.module.ts +++ b/ui/src/app/pages/server-routes/wifi/wifi-add/wifi-add.module.ts @@ -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], }) diff --git a/ui/src/app/pages/server-routes/wifi/wifi.module.ts b/ui/src/app/pages/server-routes/wifi/wifi.module.ts index dbadcf1f4..5f19caa7e 100644 --- a/ui/src/app/pages/server-routes/wifi/wifi.module.ts +++ b/ui/src/app/pages/server-routes/wifi/wifi.module.ts @@ -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], diff --git a/ui/src/app/pkg-config/config-cursor.ts b/ui/src/app/pkg-config/config-cursor.ts index 6a6e5a29d..9ebe3d446 100644 --- a/ui/src/app/pkg-config/config-cursor.ts +++ b/ui/src/app/pkg-config/config-cursor.ts @@ -93,9 +93,9 @@ export class ConfigCursor { 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 { 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 { 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 { 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 { } 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 { 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 { } 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 { 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 diff --git a/ui/src/app/pkg-config/config-types.ts b/ui/src/app/pkg-config/config-types.ts index e14b25a0c..d1ff7505b 100644 --- a/ui/src/app/pkg-config/config-types.ts +++ b/ui/src/app/pkg-config/config-types.ts @@ -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 (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 - valueNames: { [value: string]: string } + 'values-set'?: Set + '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 } } diff --git a/ui/src/app/pkg-config/config-utilities.ts b/ui/src/app/pkg-config/config-utilities.ts index 4f67ed843..653ec6020 100644 --- a/ui/src/app/pkg-config/config-utilities.ts +++ b/ui/src/app/pkg-config/config-utilities.ts @@ -114,7 +114,7 @@ export function listInnerSpec (listSpec: ValueSpecList): ValueSpecOf, callback: () => any) { - const modal = await this.trackingModalCtrl.createConfigModal({ - backdropDismiss: false, - presentingElement: await this.trackingModalCtrl.getTop(), - componentProps: { - cursor, - }, - }, cursor.spec().type) - - modal.onWillDismiss().then(res => { - cursor.injectModalData(res) - callback() - }) - - await modal.present() - } - - dismissModal (a: any) { - return this.trackingModalCtrl.dismiss(a) - } -} diff --git a/ui/src/app/services/api/api.fixures.ts b/ui/src/app/services/api/api.fixures.ts index ff8987501..95db82fe3 100644 --- a/ui/src/app/services/api/api.fixures.ts +++ b/ui/src/app/services/api/api.fixures.ts @@ -880,8 +880,8 @@ export module Mock { 'name': 'Testnet', 'type': 'boolean', 'description': 'determines whether your node is running on testnet or mainnet', - 'changeWarning': 'Chain will have to resync!', - 'default': false, + 'change-warning': 'Chain will have to resync!', + 'default': true, }, 'objectList': { 'name': 'Object List', @@ -905,8 +905,8 @@ export module Mock { // it just so happens that ValueSpecObject's have the field { spec: ConfigSpec } // see 'unionList' below for a different example. 'spec': { - 'uniqueBy': 'lastName', - 'displayAs': `I'm {{lastName}}, {{firstName}} {{lastName}}`, + 'unique-by': 'lastName', + 'display-as': `I'm {{lastName}}, {{firstName}} {{lastName}}`, 'spec': { 'firstName': { 'name': 'First Name', @@ -927,7 +927,7 @@ export module Mock { 'len': 12, }, 'pattern': '^[a-zA-Z]+$', - 'patternDescription': 'must contain only letters.', + 'pattern-description': 'must contain only letters.', 'masked': false, 'copyable': true, }, @@ -938,7 +938,7 @@ export module Mock { 'nullable': true, 'default': null, 'integral': false, - 'changeWarning': 'User must be at least 18.', + 'change-warning': 'User must be at least 18.', 'range': '[18,*)', }, }, @@ -949,7 +949,7 @@ export module Mock { 'type': 'list', 'subtype': 'union', 'description': 'This is a sample list of unions', - 'changeWarning': 'If you change this, things may work.', + 'change-warning': 'If you change this, things may work.', // a list of union selections. e.g. 'summer', 'winter',... 'default': [ 'summer', @@ -959,7 +959,7 @@ export module Mock { 'tag': { 'id': 'preference', 'name': 'Preferences', - 'variantNames': { + 'variant-names': { 'summer': 'Summer', 'winter': 'Winter', 'other': 'Other', @@ -982,7 +982,7 @@ export module Mock { 'name': 'Favorite Flower', 'type': 'enum', 'description': 'Select your favorite flower', - 'valueNames': { + 'value-names': { 'none': 'Hate Flowers', 'red': 'Red', 'blue': 'Blue', @@ -1006,13 +1006,13 @@ export module Mock { }, }, }, - 'uniqueBy': 'preference', + 'unique-by': 'preference', }, }, 'randomEnum': { 'name': 'Random Enum', 'type': 'enum', - 'valueNames': { + 'value-names': { 'null': 'Null', 'option1': 'One 1', 'option2': 'Two 2', @@ -1020,7 +1020,7 @@ export module Mock { }, 'default': 'null', 'description': 'This is not even real.', - 'changeWarning': 'Be careful chnaging this!', + 'change-warning': 'Be careful chnaging this!', 'values': [ 'null', 'option1', @@ -1033,7 +1033,7 @@ export module Mock { 'type': 'number', 'integral': false, 'description': 'Your favorite number of all time', - 'changeWarning': 'Once you set this number, it can never be changed without severe consequences.', + 'change-warning': 'Once you set this number, it can never be changed without severe consequences.', 'nullable': false, 'default': 7, 'range': '(-100,100]', @@ -1057,19 +1057,15 @@ export module Mock { 'rpcsettings': { 'name': 'RPC Settings', 'type': 'object', - 'uniqueBy': null, + 'unique-by': null, 'description': 'rpc username and password', - 'changeWarning': 'Adding RPC users gives them special permissions on your node.', - 'nullable': false, - 'nullByDefault': false, + 'change-warning': 'Adding RPC users gives them special permissions on your node.', 'spec': { 'laws': { 'name': 'Laws', 'type': 'object', - 'uniqueBy': 'law1', + 'unique-by': 'law1', 'description': 'the law of the realm', - 'nullable': true, - 'nullByDefault': true, 'spec': { 'law1': { 'name': 'First Law', @@ -1097,7 +1093,7 @@ export module Mock { 'range': '[0,2]', 'default': [], 'spec': { - 'uniqueBy': null, + 'unique-by': null, 'spec': { 'rulemakername': { 'name': 'Rulemaker Name', @@ -1118,7 +1114,7 @@ export module Mock { 'nullable': false, 'default': '192.168.1.0', 'pattern': '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$', - 'patternDescription': 'may only contain numbers and periods', + 'pattern-description': 'may only contain numbers and periods', 'masked': false, 'copyable': true, }, @@ -1132,7 +1128,7 @@ export module Mock { 'nullable': false, 'default': 'defaultrpcusername', 'pattern': '^[a-zA-Z]+$', - 'patternDescription': 'must contain only letters.', + 'pattern-description': 'must contain only letters.', 'masked': false, 'copyable': true, }, @@ -1153,10 +1149,8 @@ export module Mock { 'advanced': { 'name': 'Advanced', 'type': 'object', - 'uniqueBy': null, + 'unique-by': null, 'description': 'Advanced settings', - 'nullable': false, - 'nullByDefault': false, 'spec': { 'notifications': { 'name': 'Notification Preferences', @@ -1168,7 +1162,7 @@ export module Mock { 'email', ], 'spec': { - 'valueNames': { + 'value-names': { 'email': 'EEEEmail', 'text': 'Texxxt', 'call': 'Ccccall', @@ -1189,14 +1183,14 @@ export module Mock { 'bitcoinNode': { 'name': 'Bitcoin Node Settings', 'type': 'union', - 'uniqueBy': null, + 'unique-by': null, 'description': 'The node settings', 'default': 'internal', - 'changeWarning': 'Careful changing this', + 'change-warning': 'Careful changing this', 'tag': { 'id': 'type', 'name': 'Type', - 'variantNames': { + 'variant-names': { 'internal': 'Internal', 'external': 'External', }, @@ -1220,7 +1214,7 @@ export module Mock { 'nullable': false, 'default': 'bitcoinnode.com', 'pattern': '.*', - 'patternDescription': 'anything', + 'pattern-description': 'anything', 'masked': false, 'copyable': true, }, @@ -1249,14 +1243,14 @@ export module Mock { 'type': 'list', 'subtype': 'string', 'description': 'external ip addresses that are authorized to access your Bitcoin node', - 'changeWarning': 'Any IP you allow here will have RPC access to your Bitcoin node.', + 'change-warning': 'Any IP you allow here will have RPC access to your Bitcoin node.', 'range': '[1,10]', 'default': [ '192.168.1.1', ], 'spec': { 'pattern': '((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))', - 'patternDescription': 'must be a valid ipv4, ipv6, or domain name', + 'pattern-description': 'must be a valid ipv4, ipv6, or domain name', }, }, 'rpcauth': { @@ -1271,24 +1265,37 @@ export module Mock { }, // actual config config: { - testnet: undefined, - objectList: undefined, + testnet: false, + objectList: [ + { + 'firstName': 'Admin', + 'lastName': 'User', + 'age': 40, + }, + { + 'firstName': 'Admin2', + 'lastName': 'User', + 'age': 40, + }, + ], unionList: undefined, randomEnum: 'option1', favoriteNumber: 8, secondaryNumbers: undefined, rpcsettings: { - laws: null, + laws: { + law1: 'The first law', + law2: 'The second law', + }, rpcpass: null, rpcuser: '123', rulemakers: [], }, advanced: { - notifications: ['call'], + notifications: ['email'], }, bitcoinNode: undefined, port: 5959, - maxconnections: null, rpcallowip: undefined, rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'], }, @@ -1312,7 +1319,6 @@ export module Mock { }, bitcoinNode: { type: 'internal' }, port: 5959, - maxconnections: null, rpcallowip: [], rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'], } diff --git a/ui/src/app/services/api/api.types.ts b/ui/src/app/services/api/api.types.ts index 6e25cd8d5..179fabdcb 100644 --- a/ui/src/app/services/api/api.types.ts +++ b/ui/src/app/services/api/api.types.ts @@ -78,6 +78,9 @@ export module RR { export type DeleteNotificationReq = { id: string } // notification.delete export type DeleteNotificationRes = null + export type DeleteAllNotificationsReq = { } // notification.delete.all + export type DeleteAllNotificationsRes = null + // wifi export type AddWifiReq = { // wifi.add diff --git a/ui/src/app/services/api/embassy/embassy-api.service.ts b/ui/src/app/services/api/embassy/embassy-api.service.ts index fcb27b42a..6cc69620e 100644 --- a/ui/src/app/services/api/embassy/embassy-api.service.ts +++ b/ui/src/app/services/api/embassy/embassy-api.service.ts @@ -86,6 +86,8 @@ export abstract class ApiService implements Source, Http { abstract deleteNotification (params: RR.DeleteNotificationReq): Promise + abstract deleteAllNotifications (params: RR.DeleteAllNotificationsReq): Promise + // wifi abstract addWifi (params: RR.AddWifiReq): Promise diff --git a/ui/src/app/services/api/embassy/embassy-live-api.service.ts b/ui/src/app/services/api/embassy/embassy-live-api.service.ts index 1c395798e..9e1260d6a 100644 --- a/ui/src/app/services/api/embassy/embassy-live-api.service.ts +++ b/ui/src/app/services/api/embassy/embassy-live-api.service.ts @@ -102,11 +102,15 @@ export class LiveApiService extends ApiService { // notification async getNotificationsRaw (params: RR.GetNotificationsReq): Promise { - return this.http.rpcRequest({ method: 'notifications.list', params }) + return this.http.rpcRequest({ method: 'notification.list', params }) } async deleteNotification (params: RR.DeleteNotificationReq): Promise { - return this.http.rpcRequest({ method: 'notifications.delete', params }) + return this.http.rpcRequest({ method: 'notification.delete', params }) + } + + async deleteAllNotifications (params: RR.DeleteAllNotificationsReq): Promise { + return this.http.rpcRequest({ method: 'notification.delete.all', params }) } // wifi diff --git a/ui/src/app/services/api/embassy/embassy-mock-api.service.ts b/ui/src/app/services/api/embassy/embassy-mock-api.service.ts index 97aebdc9d..24b6c57cb 100644 --- a/ui/src/app/services/api/embassy/embassy-mock-api.service.ts +++ b/ui/src/app/services/api/embassy/embassy-mock-api.service.ts @@ -35,6 +35,7 @@ export class MockApiService extends ApiService { } async setDbValueRaw (params: RR.SetDBValueReq): Promise { + await pauseFor(2000) return this.http.rpcRequest>({ method: 'db.put.ui', params }) } @@ -202,6 +203,11 @@ export class MockApiService extends ApiService { return null } + async deleteAllNotifications (params: RR.DeleteAllNotificationsReq): Promise { + await pauseFor(2000) + return null + } + // wifi async addWifi (params: RR.AddWifiReq): Promise { @@ -484,7 +490,30 @@ export class MockApiService extends ApiService { async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise { await pauseFor(2000) - return { } + return { + testnet: true, + // objectList: [], + // unionList: [], + randomEnum: 'option2', + favoriteNumber: 9, + secondaryNumbers: [2, 3, 5, 6], + rpcsettings: { + laws: { + law1: 'The 1st law', + law2: 'The 2nd law', + }, + rpcpass: null, + rpcuser: '123', + rulemakers: [], + }, + advanced: { + notifications: ['call', 'text'], + }, + // bitcoinNode: undefined, + port: 22, + // rpcallowip: undefined, + // rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'], + } } private async updateProgress (id: string, initialProgress: InstallProgress) { @@ -495,9 +524,7 @@ export class MockApiService extends ApiService { ] for (let phase of phases) { let i = initialProgress[phase.progress] - console.log('Initial i', i) while (i < initialProgress.size) { - console.log(i) await pauseFor(1000) i = Math.min(i + 5, initialProgress.size) initialProgress[phase.progress] = i diff --git a/ui/src/app/services/config.service.ts b/ui/src/app/services/config.service.ts index adb429747..c461878b6 100644 --- a/ui/src/app/services/config.service.ts +++ b/ui/src/app/services/config.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core' -import { InterfaceDef, Manifest, PackageDataEntry, PackageMainStatus, PackageState } from './patch-db/data-model' +import { InterfaceDef, PackageDataEntry, PackageMainStatus, PackageState } from './patch-db/data-model' const { start9Marketplace, patchDb, api, mocks } = require('../../../config.json') as UiConfig @@ -67,7 +67,6 @@ export class ConfigService { } launchableURL (pkg: PackageDataEntry): string { - console.log('PKGPKGPKG', pkg) return this.isTor() ? `http://${torUiAddress(pkg)}` : `https://${lanUiAddress(pkg)}` } } diff --git a/ui/src/app/services/form.service.ts b/ui/src/app/services/form.service.ts new file mode 100644 index 000000000..03c261c46 --- /dev/null +++ b/ui/src/app/services/form.service.ts @@ -0,0 +1,244 @@ +import { Injectable } from '@angular/core' +import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms' +import { ConfigSpec, isValueSpecListOf, ListValueSpecNumber, ListValueSpecString, ListValueSpecUnion, ValueSpec, ValueSpecEnum, ValueSpecList, ValueSpecNumber, ValueSpecObject, ValueSpecString, ValueSpecUnion } from '../pkg-config/config-types' +import { getDefaultString, Range } from '../pkg-config/config-utilities' + +@Injectable({ + providedIn: 'root', +}) +export class FormService { + validationMessages: { [key: string]: { type: string, message: string }[] } = { } + + constructor ( + private readonly formBuilder: FormBuilder, + ) { } + + createForm (config: ConfigSpec, current: { [key: string]: any } = { }): FormGroup { + return this.getFormGroup(config, [], current) + } + + getListItemValidators (spec: ValueSpecList, key: string, index: number) { + const listKey = `${key}/${index}` + this.validationMessages[listKey] = [] + if (isValueSpecListOf(spec, 'string')) { + return this.stringValidators(listKey, spec.spec) + } else if (isValueSpecListOf(spec, 'number')) { + return this.numberValidators(listKey, spec.spec) + } + } + + getUnionObject (spec: ValueSpecUnion | ListValueSpecUnion, selection: string, current?: { [key: string]: any }): FormGroup { + const { variants, tag } = spec + const { name, description, 'change-warning' : changeWarning } = isFullUnion(spec) ? spec : { ...spec.tag, 'change-warning': undefined } + + const enumSpec: ValueSpecEnum = { + type: 'enum', + name, + description, + 'change-warning': changeWarning, + default: selection, + values: Object.keys(variants), + 'value-names': tag['variant-names'], + } + return this.getFormGroup({ [spec.tag.id]: enumSpec, ...spec.variants[selection] }, [], current) + } + + getFormGroup (config: ConfigSpec, validators: ValidatorFn[] = [], current: { [key: string]: any } = { }): FormGroup { + let group = { } + Object.entries(config).map(([key, spec]) => { + if (spec.type === 'pointer') return + group[key] = this.getFormEntry(key, spec, current ? current[key] : { }) + }) + return this.formBuilder.group(group, { validators } ) + } + + getListItem (key: string, index: number, spec: ValueSpecList, entry: any) { + const listItemValidators = this.getListItemValidators(spec, key, index) + if (isValueSpecListOf(spec, 'string')) { + return this.formBuilder.control(entry, listItemValidators) + } else if (isValueSpecListOf(spec, 'number')) { + return this.formBuilder.control(entry, listItemValidators) + } else if (isValueSpecListOf(spec, 'enum')) { + return this.formBuilder.control(entry) + } else if (isValueSpecListOf(spec, 'object')) { + return this.getFormGroup(spec.spec.spec, listItemValidators, entry) + } else if (isValueSpecListOf(spec, 'union')) { + return this.getUnionObject(spec.spec, spec.spec.default, entry) + } + } + + private getFormEntry (key: string, spec: ValueSpec, currentValue: any): FormGroup | FormArray | FormControl { + this.validationMessages[key] = [] + let validators: ValidatorFn[] + let value: any + switch (spec.type) { + case 'string': + validators = this.stringValidators(key, spec) + if (currentValue !== undefined) { + value = currentValue + } else { + value = spec.default ? getDefaultString(spec.default) : null + } + return this.formBuilder.control(value, validators) + case 'number': + validators = this.numberValidators(key, spec) + if (currentValue !== undefined) { + value = currentValue + } else { + value = spec.default || null + } + return this.formBuilder.control(value, validators) + case 'object': + return this.getFormGroup(spec.spec, [], currentValue) + case 'list': + validators = this.listValidators(key, spec) + const mapped = (Array.isArray(currentValue) ? currentValue : spec.default as any[]).map((entry: any, index) => { + return this.getListItem(key, index, spec, entry) + }) + return this.formBuilder.array(mapped, validators) + case 'union': + return this.getUnionObject(spec, currentValue?.[spec.tag.id] || spec.default, currentValue) + case 'boolean': + case 'enum': + value = currentValue === undefined ? spec.default : currentValue + return this.formBuilder.control(value) + } + } + + private stringValidators (key: string, spec: ValueSpecString | ListValueSpecString): ValidatorFn[] { + const validators: ValidatorFn[] = [] + + if (!(spec as ValueSpecString).nullable) { + validators.push(Validators.required) + this.validationMessages[key].push({ + type: 'required', + message: 'Cannot be blank.', + }) + } + + if (spec.pattern) { + validators.push(Validators.pattern(spec.pattern)) + this.validationMessages[key].push({ + type: 'pattern', + message: spec['pattern-description'], + }) + } + + return validators + } + + private numberValidators (key: string, spec: ValueSpecNumber | ListValueSpecNumber): ValidatorFn[] { + const validators: ValidatorFn[] = [] + + if (!(spec as ValueSpecNumber).nullable) { + validators.push(Validators.required) + this.validationMessages[key].push({ + type: 'required', + message: 'Cannot be blank.', + }) + } + + if (spec.integral) { + validators.push(isInteger()) + this.validationMessages[key].push({ + type: 'numberNotInteger', + message: 'Number must be an integer.', + }) + } + + validators.push(numberInRange(spec.range)) + this.validationMessages[key].push({ + type: 'numberNotInRange', + message: 'Number not in range.', + }) + + return validators + } + + private listValidators (key: string, spec: ValueSpecList): ValidatorFn[] { + const validators: ValidatorFn[] = [] + + validators.push(listInRange(spec.range)) + this.validationMessages[key].push({ + type: 'listNotInRange', + message: 'List not in range.', + }) + + if (!isValueSpecListOf(spec, 'enum')) { + validators.push(listUnique(spec)) + this.validationMessages[key].push({ + type: 'listNotUnique', + message: 'List contains duplicate entries.', + }) + } + + return validators + } +} + +function isFullUnion (spec: ValueSpecUnion | ListValueSpecUnion): spec is ValueSpecUnion { + return !!(spec as ValueSpecUnion).name +} + +export function numberInRange (stringRange: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + try { + Range.from(stringRange).checkIncludes(control.value) + return null + } catch (e) { + return { numberNotInRange: { value: control.value } } + } + } +} + +export function isInteger (): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + return control.value == Math.trunc(control.value) ? + null : + { numberNotInteger: { value: control.value } } + } +} + +export function listInRange (stringRange: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const range = Range.from(stringRange) + const min = range.integralMin() + const max = range.integralMax() + const length = control.value.length + if ((min && length < min) || (max && length > max)) { + return { listNotInRange: { value: control.value } } + } else { + return null + } + } +} + +export function listUnique (spec: ValueSpec): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + for (let idx = 0; idx < control.value.length; idx++) { + for (let idx2 = idx + 1; idx2 < control.value.length; idx2++) { + if (equals(spec, control.value[idx], control.value[idx2])) { + return { listNotUnique: { value: control.value } } + } else { + return null + } + } + } + } +} + +export function equals (spec: ValueSpec, val1: any, val2: any): boolean { + switch (spec.type) { + case 'string': + case 'number': + case 'boolean': + case 'enum': + return val1 === val2 + case 'object': + case 'union': + // @TODO how to check this + return false + default: + return false + } +} diff --git a/ui/src/app/services/patch-db/data-model.ts b/ui/src/app/services/patch-db/data-model.ts index 05f66bbe2..4acea4018 100644 --- a/ui/src/app/services/patch-db/data-model.ts +++ b/ui/src/app/services/patch-db/data-model.ts @@ -348,7 +348,7 @@ export interface DependencyEntry { optional: string | null recommended: boolean description: string | null - config: ConfigRuleEntryWithSuggestions[] + config: ConfigRuleEntryWithSuggestions[] // @TODO when do we use this? interfaces: any[] // @TODO placeholder } diff --git a/ui/src/app/services/pkg-status-rendering.service.ts b/ui/src/app/services/pkg-status-rendering.service.ts index 4fba04e29..d5ffaf1da 100644 --- a/ui/src/app/services/pkg-status-rendering.service.ts +++ b/ui/src/app/services/pkg-status-rendering.service.ts @@ -20,7 +20,7 @@ function handleInstalledState (status: Status): PkgStatusRendering { switch (status.main.status) { case PackageMainStatus.Stopping: return { display: 'Stopping', color: 'dark', showDots: true, feStatus: FEStatus.Stopping } - case PackageMainStatus.Stopped: return { display: 'Stopped', color: 'medium', showDots: false, feStatus: FEStatus.Stopped } + case PackageMainStatus.Stopped: return { display: 'Stopped', color: 'dark', showDots: false, feStatus: FEStatus.Stopped } case PackageMainStatus.BackingUp: return { display: 'Backing Up', color: 'warning', showDots: true, feStatus: FEStatus.BackingUp } case PackageMainStatus.Restoring: return { display: 'Restoring', color: 'primary', showDots: true, feStatus: FEStatus.Restoring } case PackageMainStatus.Running: return handleRunningState(status.main) diff --git a/ui/src/app/services/server-config.service.ts b/ui/src/app/services/server-config.service.ts index e9f624899..b3adb14a4 100644 --- a/ui/src/app/services/server-config.service.ts +++ b/ui/src/app/services/server-config.service.ts @@ -1,10 +1,12 @@ import { Injectable } from '@angular/core' -import { AppConfigValuePage } from '../modals/app-config-value/app-config-value.page' +import { AlertInput, AlertButton } from '@ionic/core' +// import { AppConfigValuePage } from '../modals/app-config-value/app-config-value.page' import { ApiService } from './api/embassy/embassy-api.service' import { ConfigSpec } from '../pkg-config/config-types' -import { ConfigCursor } from '../pkg-config/config-cursor' import { SSHService } from '../pages/server-routes/security-routes/ssh-keys/ssh.service' -import { TrackingModalController } from './tracking-modal-controller.service' +import { AlertController, LoadingController } from '@ionic/angular' +import { ErrorToastService } from './error-toast.service' +// import { ModalController } from '@ionic/angular' @Injectable({ providedIn: 'root', @@ -12,41 +14,110 @@ import { TrackingModalController } from './tracking-modal-controller.service' export class ServerConfigService { constructor ( - private readonly trackingModalCtrl: TrackingModalController, + // private readonly modalCtrl: ModalController, + private readonly loadingCtrl: LoadingController, + private readonly errToast: ErrorToastService, + private readonly alertCtrl: AlertController, private readonly embassyApi: ApiService, private readonly sshService: SSHService, ) { } - async presentModalValueEdit (key: string, current?: string) { - const cursor = new ConfigCursor(serverConfig, { [key]: current }).seekNext(key) + async presentAlert (key: string, current?: any): Promise { + const spec = serverConfig[key] - const modal = await this.trackingModalCtrl.create({ - backdropDismiss: false, - component: AppConfigValuePage, - presentingElement: await this.trackingModalCtrl.getTop(), - componentProps: { - cursor, - saveFn: this.saveFns[key], + let inputs: AlertInput[] + let buttons: AlertButton[] = [ + { + text: 'Cancel', + role: 'cancel', }, - }) + { + text: 'Save', + handler: async (data: any) => { + const loader = await this.loadingCtrl.create({ + spinner: 'lines', + message: 'Saving...', + cssClass: 'loader', + }) + loader.present() - await modal.present() + try { + await this.saveFns[key](data) + } catch (e) { + this.errToast.present(e.message) + } finally { + loader.dismiss() + } + }, + }, + ] + + switch (spec.type) { + case 'boolean': + inputs = [ + { + name: 'enabled', + type: 'radio', + label: 'Enabled', + value: true, + checked: current, + }, + { + name: 'disabled', + type: 'radio', + label: 'Disabled', + value: false, + checked: !current, + }, + ] + break + case 'string': + inputs = [ + { + name: key, + type: 'textarea', + placeholder: 'Enter SSH public key', + value: current, + }, + ] + break + } + + const alert = await this.alertCtrl.create({ + header: spec.name, + message: spec.description, + inputs, + buttons, + }) + await alert.present() } + // async presentModalForm (key: string, current?: string) { + // const modal = await this.modalCtrl.create({ + // component: AppConfigValuePage, + // componentProps: { + // cursor, + // saveFn: this.saveFns[key], + // }, + // }) + // await modal.present() + // } + saveFns: { [key: string]: (val: any) => Promise } = { - autoCheckUpdates: async (enabled: boolean) => { + 'auto-check-updates': async (enabled: boolean) => { + console.log('SAVING auto check', enabled) return this.embassyApi.setDbValue({ pointer: '/auto-check-updates', value: enabled }) }, ssh: async (pubkey: string) => { return this.sshService.add(pubkey) }, - eosMarketplace: async (enabled: boolean) => { + 'eos-marketplace': async (enabled: boolean) => { return this.embassyApi.setEosMarketplace(enabled) }, - // packageMarketplace: async (url: string) => { + // 'package-marketplace': async (url: string) => { // return this.embassyApi.setPackageMarketplace({ url }) // }, - shareStats: async (enabled: boolean) => { + 'share-stats': async (enabled: boolean) => { return this.embassyApi.setShareStats({ value: enabled }) }, // password: async (password: string) => { @@ -55,8 +126,8 @@ export class ServerConfigService { } } -const serverConfig: ConfigSpec = { - autoCheckUpdates: { +export const serverConfig: ConfigSpec = { + 'auto-check-updates': { type: 'boolean', name: 'Auto Check for Updates', description: 'On launch, EmbassyOS will automatically check for updates of itself and your installed services. Updating still requires user approval and action. No updates will ever be performed automatically.', @@ -65,33 +136,33 @@ const serverConfig: ConfigSpec = { ssh: { type: 'string', name: 'SSH Key', - description: 'Add SSH keys to your Embassy to gain root access from the command line.', + description: 'Enter an SSH public key to authorize root access from the command line.', nullable: false, // @TODO regex for SSH Key // pattern: '', - patternDescription: 'Must be a valid SSH key', + 'pattern-description': 'Must be a valid SSH key', masked: false, copyable: false, }, - eosMarketplace: { + 'eos-marketplace': { type: 'boolean', name: 'Tor Only Marketplace', description: `Use Start9's Tor (instead of clearnet) Marketplace.`, - changeWarning: 'This will result in higher latency and slower download times.', + 'change-warning': 'This will result in higher latency and slower download times.', default: false, }, - // packageMarketplace: { + // 'package-marketplace': { // type: 'string', // name: 'Package Marketplace', // description: `Use for alternative embassy marketplace. Leave empty to use start9's marketplace.`, // nullable: true, // // @TODO regex for URL // // pattern: '', - // patternDescription: 'Must be a valid URL.', + // 'pattern-description': 'Must be a valid URL.', // masked: false, // copyable: false, // }, - shareStats: { + 'share-stats': { type: 'boolean', name: 'Share Anonymous Statistics', description: 'Start9 uses this information to identify bugs quickly and improve EmbassyOS. The information is 100% anonymous and transmitted over Tor.', @@ -104,8 +175,8 @@ const serverConfig: ConfigSpec = { // nullable: false, // // @TODO regex for 12 chars // // pattern: '', - // patternDescription: 'Must contain at least 12 characters.', - // changeWarning: 'If you forget your master password, there is absolutely no way to recover your data. This can result in loss of money! Keep in mind, old backups will still be encrypted by the password used to encrypt them.', + // 'pattern-description': 'Must contain at least 12 characters.', + // 'change-warning': 'If you forget your master password, there is absolutely no way to recover your data. This can result in loss of money! Keep in mind, old backups will still be encrypted by the password used to encrypt them.', // masked: false, // copyable: false, // }, diff --git a/ui/src/app/services/startup-alerts.service.ts b/ui/src/app/services/startup-alerts.service.ts index d42cd1027..f505dad4c 100644 --- a/ui/src/app/services/startup-alerts.service.ts +++ b/ui/src/app/services/startup-alerts.service.ts @@ -9,7 +9,7 @@ import { ConfigService } from './config.service' import { Emver } from './emver.service' import { MarketplaceService } from '../pages/marketplace-routes/marketplace.service' import { MarketplaceApiService } from './api/marketplace/marketplace-api.service' -import { DataModel, PackageDataEntry } from './patch-db/data-model' +import { DataModel } from './patch-db/data-model' import { PatchDbService } from './patch-db/patch-db.service' import { filter, take } from 'rxjs/operators' import { isEmptyObject } from '../util/misc.util' @@ -78,7 +78,6 @@ export class StartupAlertsService { let checkRes: any try { checkRes = await c.check() - console.log('CHECK RES', checkRes) } catch (e) { console.error(`Exception in ${c.name} check:`, e) return true diff --git a/ui/src/app/services/sub-nav.service.ts b/ui/src/app/services/sub-nav.service.ts new file mode 100644 index 000000000..c50811d83 --- /dev/null +++ b/ui/src/app/services/sub-nav.service.ts @@ -0,0 +1,33 @@ +import { Inject, Injectable, InjectionToken } from '@angular/core' +import { IonNav } from '@ionic/angular' +import { AppConfigComponentMapping } from '../modals/app-config-injectable' +import { ConfigCursor } from '../pkg-config/config-cursor' + +export const APP_CONFIG_COMPONENT_MAPPING = new InjectionToken('APP_CONFIG_COMPONENTS') + +@Injectable({ + providedIn: 'root', +}) +export class SubNavService { + path: string[] + + constructor ( + @Inject(APP_CONFIG_COMPONENT_MAPPING) private readonly appConfigComponentMapping: AppConfigComponentMapping, + ) { } + + async push (key: string, cursor: ConfigCursor, nav: IonNav) { + const component = this.appConfigComponentMapping[cursor.spec().type] + this.path.push(key) + nav.push(component, { cursor }, { mode: 'ios' }) + } + + async pop (nav: IonNav) { + this.path.pop() + nav.pop({ mode: 'ios' }) + } + + async popTo (index: number, nav: IonNav) { + this.path = this.path.slice(0, index + 1) + nav.popTo(index, { mode: 'ios' }) + } +} \ No newline at end of file diff --git a/ui/src/app/services/tracking-modal-controller.service.ts b/ui/src/app/services/tracking-modal-controller.service.ts deleted file mode 100644 index a17225905..000000000 --- a/ui/src/app/services/tracking-modal-controller.service.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Inject, Injectable, InjectionToken } from '@angular/core' -import { Observable, Subject } from 'rxjs' -import { ModalController } from '@ionic/angular' -import { ModalOptions } from '@ionic/core' -import { ValueSpec } from '../pkg-config/config-types' -import { AppConfigComponentMapping } from '../modals/app-config-injectable' - -export const APP_CONFIG_COMPONENT_MAPPING = new InjectionToken('APP_CONFIG_COMPONENTS') - -@Injectable({ - providedIn: 'root', -}) -export class TrackingModalController { - private modals: { [modalId: string] : HTMLIonModalElement} = { } - - private readonly $onDismiss$ = new Subject() - private readonly $onCreate$ = new Subject() - - constructor ( - private readonly modalCtrl: ModalController, - @Inject(APP_CONFIG_COMPONENT_MAPPING) private readonly appConfigComponentMapping: AppConfigComponentMapping, - ) { } - - async createConfigModal (o: Omit, type: ValueSpec['type']) { - const component = this.appConfigComponentMapping[type] - return this.create({ ...o, component }) - } - - async create (a: ModalOptions): Promise { - const modal = await this.modalCtrl.create(a) - this.modals[modal.id] = modal - this.$onCreate$.next(modal.id) - - modal.onWillDismiss().then(() => { - delete this.modals[modal.id] - this.$onDismiss$.next(modal.id) - }) - return modal - } - - dismissAll (): Promise { - return Promise.all( - Object.values(this.modals).map(m => m.dismiss()), - ) - } - - dismiss (val?: any): Promise { - return this.modalCtrl.dismiss(val) - } - - onCreateAny$ (): Observable { - return this.$onCreate$.asObservable() - } - - onDismissAny$ (): Observable { - return this.$onDismiss$.asObservable() - } - - async getTop (): Promise { - return this.modalCtrl.getTop() - } - - get anyModals (): boolean { - return Object.keys(this.modals).length !== 0 - } - - get modalCount (): number { - return Object.keys(this.modals).length - } -} diff --git a/ui/src/app/util/misc.util.ts b/ui/src/app/util/misc.util.ts index e2ee6912f..8ff4e12a6 100644 --- a/ui/src/app/util/misc.util.ts +++ b/ui/src/app/util/misc.util.ts @@ -149,9 +149,6 @@ export function partitionArray (ts: T[], condition: (t: T) => boolean): [T[], return [yes, no] } -export const chill = () => { } -export const chillAsync = async () => { } - export function uniqueBy (ts: T[], uniqueBy: (t: T) => string, prioritize: (t1: T, t2: T) => T) { return Object.values(ts.reduce((acc, next) => { const previousValue = acc[uniqueBy(next)] diff --git a/ui/src/global.scss b/ui/src/global.scss index 4225ea8d5..b03348501 100644 --- a/ui/src/global.scss +++ b/ui/src/global.scss @@ -25,6 +25,15 @@ @import "~@ionic/angular/css/text-transformation.css"; @import "~@ionic/angular/css/flex-utils.css"; +$subheader-height: 48px; + +.subheader-padding { + --padding-top: #{$subheader-height} + 10px; +} + +.subheader { + --min-height: #{$subheader-height}; +} .select-change-warning .alert-sub-title { color: var(--ion-color-warning) @@ -151,6 +160,12 @@ ion-button { white-space: nowrap; } +.modal-wrapper.sc-ion-modal-md { + border-radius: 6px; + border: 2px solid rgba(255,255,255,.03); + box-shadow: 0 0 70px 70px black; +} + .modal-wrapper { position: absolute; height: 90% !important; @@ -160,17 +175,11 @@ ion-button { display: block; } -.modal-wrapper.sc-ion-modal-md { - border-radius: 6px; - border: 2px solid rgba(255,255,255,.03); - box-shadow: 0 0 70px 70px black; -} - @media (min-width:1000px) { .modal-wrapper { position: absolute; - height: 70% !important; - top: 15%; + height: 80% !important; + top: 10%; width: 60% !important; left: 20%; display: block; @@ -226,11 +235,11 @@ ion-slides { ion-item-divider { text-transform: uppercase; - margin-top: 24px; + --padding-top: 24px; font-weight: 600; --color: var(--ion-color-dark); - border: none; font-size: medium; + border-bottom: none; } ion-item { @@ -247,15 +256,6 @@ ion-loading { padding-bottom: 3px; } -.notifier-item { - margin: 12px; - margin-top: 0px; - border-radius: 12px; - // kills the lines - --border-width: 0; - --inner-border-width: 0; -} - .rec-item { margin: 20px; border-style: solid; diff --git a/ui/test/config.test.ts b/ui/test/config.test.ts index 6b2389aa4..ed65cdda7 100644 --- a/ui/test/config.test.ts +++ b/ui/test/config.test.ts @@ -206,7 +206,7 @@ function test () { tag: { id: 'variant', name: 'Variant', - variantNames: { + 'variant-names': { 'variant-a': 'Variant A', 'variant-b': 'Variant B', },