diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index 9b2c6bf2a..d4f8690a8 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -97,26 +97,29 @@ export class AppComponent { if (auth === AuthState.VERIFIED) { this.patch.start() + this.showMenu = true + // if on the login screen, route to dashboard + if (this.router.url.startsWith('/login')) { + this.router.navigate([''], { replaceUrl: true }) + } + + this.subscriptions = this.subscriptions.concat([ + // start the connection monitor + ...this.connectionService.start(), + // watch connection to display connectivity issues + this.watchConnection(), + // // watch router to highlight selected menu item + this.watchRouter(), + // // watch status to display/hide maintenance page + ]) + this.patch.watch$() .pipe( filter(data => !isEmptyObject(data as object)), take(1), ) .subscribe(_ => { - this.showMenu = true - // if on the login screen, route to dashboard - if (this.router.url.startsWith('/login')) { - this.router.navigate([''], { replaceUrl: true }) - } - - this.subscriptions = [ - // start the connection monitor - ...this.connectionService.start(), - // watch connection to display connectivity issues - this.watchConnection(), - // // watch router to highlight selected menu item - this.watchRouter(), - // // watch status to display/hide maintenance page + this.subscriptions = this.subscriptions.concat([ this.watchStatus(), // // watch version to refresh browser window this.watchVersion(), @@ -124,7 +127,7 @@ export class AppComponent { this.watchNotifications(), // // run startup alerts this.startupAlertsService.runChecks(), - ] + ]) }) // UNVERIFIED } else if (auth === AuthState.UNVERIFIED) { @@ -230,6 +233,10 @@ export class AppComponent { message = 'Embassy not found on Local Area Network.' link = 'https://docs.start9.com/support/FAQ/setup-faq.html#lan-failure' break + case ConnectionFailure.Unknown: + message = 'Unknown connection error. Please refresh the page.' + link = 'https://docs.start9.com/support/FAQ/setup-faq.html#unknown-failure' + break } await this.presentToastOffline(message, link) } diff --git a/ui/src/app/components/form-object/form-object.component.html b/ui/src/app/components/form-object/form-object.component.html index de772b0e5..5b95bcd0c 100644 --- a/ui/src/app/components/form-object/form-object.component.html +++ b/ui/src/app/components/form-object/form-object.component.html @@ -25,14 +25,14 @@ - + - + {{ spec.units }} @@ -43,7 +43,7 @@ {{ spec.name }} - + {{ spec['value-names'][option] }} @@ -68,7 +68,7 @@ spec.variants[entry.value.controls[spec.tag.id].value] : spec.spec" [formGroup]="entry.value" - [current]="current ? current[key] : undefined" + [current]="current ? current[entry.key] : undefined" [unionSpec]="spec.type === 'union' ? spec : undefined" > diff --git a/ui/src/app/components/install-wizard/complete/complete.component.ts b/ui/src/app/components/install-wizard/complete/complete.component.ts index ea239117f..48e106304 100644 --- a/ui/src/app/components/install-wizard/complete/complete.component.ts +++ b/ui/src/app/components/install-wizard/complete/complete.component.ts @@ -20,10 +20,10 @@ export class CompleteComponent implements OnInit, Loadable { } @Input() transitions: { - cancel: () => void - next: () => void - final: () => void - error: (e: Error) => void + cancel: () => any + next: () => any + final: () => any + error: (e: Error) => any } loading$ = new BehaviorSubject(false) diff --git a/ui/src/app/components/install-wizard/dependents/dependents.component.html b/ui/src/app/components/install-wizard/dependents/dependents.component.html index 6ca972529..d0b4ee574 100644 --- a/ui/src/app/components/install-wizard/dependents/dependents.component.html +++ b/ui/src/app/components/install-wizard/dependents/dependents.component.html @@ -30,10 +30,10 @@ *ngFor="let dep of dependentBreakages | keyvalue" > - + -
{{ dep.value.title }}
+
{{ patch.data['package-data'][dep.key].manifest.title }}
diff --git a/ui/src/app/components/install-wizard/dependents/dependents.component.ts b/ui/src/app/components/install-wizard/dependents/dependents.component.ts index bfe30d990..29aaf5e41 100644 --- a/ui/src/app/components/install-wizard/dependents/dependents.component.ts +++ b/ui/src/app/components/install-wizard/dependents/dependents.component.ts @@ -2,6 +2,7 @@ import { Component, Input, OnInit } from '@angular/core' import { BehaviorSubject, from, Subject } from 'rxjs' import { takeUntil, tap } from 'rxjs/operators' import { Breakages } from 'src/app/services/api/api.types' +import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { capitalizeFirstLetter, isEmptyObject } from 'src/app/util/misc.util' import { Loadable, markAsLoadingDuring$ } from '../loadable' import { WizardAction } from '../wizard-types' @@ -20,10 +21,10 @@ export class DependentsComponent implements OnInit, Loadable { skipConfirmationDialogue?: boolean } @Input() transitions: { - cancel: () => void - next: () => void - final: () => void - error: (e: Error) => void + cancel: () => any + next: () => any + final: () => any + error: (e: Error) => any } dependentBreakages: Breakages @@ -33,7 +34,9 @@ export class DependentsComponent implements OnInit, Loadable { loading$ = new BehaviorSubject(false) cancel$ = new Subject() - constructor () { } + constructor ( + public readonly patch: PatchDbService, + ) { } ngOnInit () { } load () { 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 e34ec4a2d..d7c27e34c 100644 --- a/ui/src/app/components/install-wizard/install-wizard.component.html +++ b/ui/src/app/components/install-wizard/install-wizard.component.html @@ -38,11 +38,11 @@ - {{ cancel.text }} + {{ cancel.text }} - {{ cancel.text }} + {{ cancel.text }} diff --git a/ui/src/app/components/logs/logs.page.html b/ui/src/app/components/logs/logs.page.html index db594a434..2add93473 100644 --- a/ui/src/app/components/logs/logs.page.html +++ b/ui/src/app/components/logs/logs.page.html @@ -1,6 +1,6 @@ {{ disconnected ? 'Unknown' : rendering.display }} - +

diff --git a/ui/src/app/modals/app-config/app-config.page.html b/ui/src/app/modals/app-config/app-config.page.html index 8dd8a7e0f..f59c2a427 100644 --- a/ui/src/app/modals/app-config/app-config.page.html +++ b/ui/src/app/modals/app-config/app-config.page.html @@ -64,17 +64,17 @@

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

- - -
- -
+ +
+ +
+
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 6a07a4ac1..d5fd86b25 100644 --- a/ui/src/app/modals/app-restore/app-restore.component.ts +++ b/ui/src/app/modals/app-restore/app-restore.component.ts @@ -15,6 +15,7 @@ export class AppRestoreComponent { @Input() pkgId: string disks: DiskInfo loading = true + submitting = false allPartitionsMounted: boolean modal: HTMLIonModalElement @@ -73,10 +74,17 @@ export class AppRestoreComponent { } private async restore (logicalname: string, password: string): Promise { - await this.embassyApi.restorePackage({ - id: this.pkgId, - logicalname, - password, - }) + this.submitting = true + try { + await this.embassyApi.restorePackage({ + id: this.pkgId, + logicalname, + password, + }) + } catch (e) { + this.errToast.present(e) + } finally { + this.submitting = false + } } } diff --git a/ui/src/app/modals/backup-confirmation/backup-confirmation.component.ts b/ui/src/app/modals/backup-confirmation/backup-confirmation.component.ts index 7fc35e536..56edf0e6f 100644 --- a/ui/src/app/modals/backup-confirmation/backup-confirmation.component.ts +++ b/ui/src/app/modals/backup-confirmation/backup-confirmation.component.ts @@ -1,7 +1,6 @@ import { Component, Input } from '@angular/core' import { IonicSafeString, LoadingController, ModalController } from '@ionic/angular' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService, getErrorMessage } from 'src/app/services/error-toast.service' +import { getErrorMessage } from 'src/app/services/error-toast.service' @Component({ selector: 'backup-confirmation', diff --git a/ui/src/app/modals/generic-form/generic-form.page.html b/ui/src/app/modals/generic-form/generic-form.page.html index 6acf39778..a51380520 100644 --- a/ui/src/app/modals/generic-form/generic-form.page.html +++ b/ui/src/app/modals/generic-form/generic-form.page.html @@ -10,18 +10,19 @@ -
+ +
- - - + + + {{ button.text }} diff --git a/ui/src/app/modals/generic-form/generic-form.page.ts b/ui/src/app/modals/generic-form/generic-form.page.ts index 898ab1bbf..dc38d028a 100644 --- a/ui/src/app/modals/generic-form/generic-form.page.ts +++ b/ui/src/app/modals/generic-form/generic-form.page.ts @@ -7,6 +7,7 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types' export interface ActionButton { text: string handler: (value: any) => Promise + isSubmit?: boolean } @Component({ @@ -18,6 +19,7 @@ export class GenericFormPage { @Input() title: string @Input() spec: ConfigSpec @Input() buttons: ActionButton[] + submitBtn: ActionButton formGroup: FormGroup constructor ( @@ -27,20 +29,25 @@ export class GenericFormPage { ngOnInit () { this.formGroup = this.formService.createForm(this.spec) + this.submitBtn = this.buttons.find(btn => btn.isSubmit) || { + text: '', + handler: () => Promise.resolve(true), + } + console.log(this.submitBtn) } async dismiss (): Promise { this.modalCtrl.dismiss() } - async handleClick (button: ActionButton): Promise { + async handleClick (handler: ActionButton['handler']): Promise { if (this.formGroup.invalid) { this.formGroup.markAllAsTouched() document.getElementsByClassName('validation-error')[0].parentElement.parentElement.scrollIntoView({ behavior: 'smooth' }) return } - const success = await button.handler(this.formGroup.value) + const success = await handler(this.formGroup.value) if (success !== false) this.modalCtrl.dismiss() } } 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 e05637b12..3bceba08f 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 @@ -61,6 +61,7 @@ export class AppActionsPage { handler: (value: any) => { return this.executeAction(pkg.manifest.id, action.key, value) }, + isSubmit: true, }, ], }, diff --git a/ui/src/app/pages/apps-routes/app-list/app-list.page.ts b/ui/src/app/pages/apps-routes/app-list/app-list.page.ts index 50c7c1473..56c79ba9a 100644 --- a/ui/src/app/pages/apps-routes/app-list/app-list.page.ts +++ b/ui/src/app/pages/apps-routes/app-list/app-list.page.ts @@ -6,6 +6,7 @@ import { PackageDataEntry, PackageState } from 'src/app/services/patch-db/data-m import { Subscription } from 'rxjs' import { PkgStatusRendering, renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' import { filter } from 'rxjs/operators' +import { isEmptyObject } from 'src/app/util/misc.util' @Component({ selector: 'app-list', @@ -38,7 +39,11 @@ export class AppListPage { this.patch.watch$('package-data') .pipe( filter(obj => { - return Object.keys(obj).length !== Object.keys(this.pkgs).length + return obj && + ( + isEmptyObject(obj) || + Object.keys(obj).length !== Object.keys(this.pkgs).length + ) }), ) .subscribe(pkgs => { diff --git a/ui/src/app/pages/login/login.page.html b/ui/src/app/pages/login/login.page.html index aa907c2ad..385704263 100644 --- a/ui/src/app/pages/login/login.page.html +++ b/ui/src/app/pages/login/login.page.html @@ -1,6 +1,6 @@ - +
diff --git a/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html b/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html index 27af02dc8..44c7259fc 100644 --- a/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html +++ b/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html @@ -7,16 +7,20 @@ -

Embassy Marketplace

+

Embassy Marketplace

- - - - - - - - + + + + + + + + + + + + diff --git a/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.scss b/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.scss index d1ba64ed8..cd586765d 100644 --- a/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.scss +++ b/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.scss @@ -21,6 +21,7 @@ .selected { font-weight: bold; + font-size: 17px; } .dim { diff --git a/ui/src/app/pages/server-routes/wifi/wifi.page.ts b/ui/src/app/pages/server-routes/wifi/wifi.page.ts index a6f790ac7..14be6b664 100644 --- a/ui/src/app/pages/server-routes/wifi/wifi.page.ts +++ b/ui/src/app/pages/server-routes/wifi/wifi.page.ts @@ -85,7 +85,7 @@ export class WifiPage { spec: wifiSpec.spec, buttons: [ { - text: 'Save', + text: 'Save for Later', handler: async (value: { ssid: string, password: string }) => { await this.save(value.ssid, value.password) }, diff --git a/ui/src/app/services/connection.service.ts b/ui/src/app/services/connection.service.ts index ad041f9b1..1669f83e1 100644 --- a/ui/src/app/services/connection.service.ts +++ b/ui/src/app/services/connection.service.ts @@ -55,18 +55,23 @@ export class ConnectionService { } else { // diagnosing this.connectionFailure$.next(ConnectionFailure.Diagnosing) - const torSuccess = await this.testAddrs(addrs.tor) - if (torSuccess) { - // TOR SUCCESS, EMBASSY IS PROBLEM - this.connectionFailure$.next(ConnectionFailure.Embassy) + + if (!addrs) { + this.connectionFailure$.next(ConnectionFailure.Unknown) } else { - const clearnetSuccess = await this.testAddrs(addrs.clearnet) - if (clearnetSuccess) { - // CLEARNET SUCCESS, TOR IS PROBLEM - this.connectionFailure$.next(ConnectionFailure.Tor) + const torSuccess = await this.testAddrs(addrs.tor) + if (torSuccess) { + // TOR SUCCESS, EMBASSY IS PROBLEM + this.connectionFailure$.next(ConnectionFailure.Embassy) } else { - // INTERNET IS PROBLEM - this.connectionFailure$.next(ConnectionFailure.Internet) + const clearnetSuccess = await this.testAddrs(addrs.clearnet) + if (clearnetSuccess) { + // CLEARNET SUCCESS, TOR IS PROBLEM + this.connectionFailure$.next(ConnectionFailure.Tor) + } else { + // INTERNET IS PROBLEM + this.connectionFailure$.next(ConnectionFailure.Internet) + } } } } @@ -101,4 +106,5 @@ export enum ConnectionFailure { Tor = 'tor', Lan = 'lan', Internet = 'internet', + Unknown = 'unknown', } diff --git a/ui/src/app/services/patch-db/patch-db.service.ts b/ui/src/app/services/patch-db/patch-db.service.ts index 6fb2efb3a..3c558ad76 100644 --- a/ui/src/app/services/patch-db/patch-db.service.ts +++ b/ui/src/app/services/patch-db/patch-db.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable, InjectionToken } from '@angular/core' import { Bootstrapper, PatchDB, Source, Store } from 'patch-db-client' -import { BehaviorSubject, combineLatest, Observable, of, Subscription } from 'rxjs' -import { catchError, debounceTime, delay, filter, finalize, map, take, tap, timeout } from 'rxjs/operators' +import { BehaviorSubject, Observable, of, Subscription } from 'rxjs' +import { catchError, debounceTime, finalize, map, tap } from 'rxjs/operators' import { pauseFor } from 'src/app/util/misc.util' import { ApiService } from '../api/embassy-api.service' import { DataModel } from './data-model' @@ -20,7 +20,7 @@ export enum PatchConnection { providedIn: 'root', }) export class PatchDbService { - patchConnection$ = new BehaviorSubject(PatchConnection.Initializing) + private patchConnection$ = new BehaviorSubject(PatchConnection.Initializing) private patchDb: PatchDB private patchSub: Subscription data: DataModel @@ -40,50 +40,37 @@ export class PatchDbService { } start (): void { + console.log(this.patchSub ? 'restarting patch-db' : 'starting patch-db') + // make sure everything is stopped before initializing if (this.patchSub) { - console.log('Retrying') this.patchSub.unsubscribe() this.patchSub = undefined } - try { - const connectedSub$ = this.patchDb.connectionMade$() - .pipe( - tap(() => { - this.patchConnection$.next(PatchConnection.Connected) - }), - timeout(30000), - take(1), - ) - const updateSub$ = this.patchDb.sync$() - .pipe( - debounceTime(500), - tap(cache => { - this.bootstrapper.update(cache) - }), - ) - - this.patchSub = combineLatest([connectedSub$, updateSub$]) - .subscribe({ - error: async e => { - console.error('patch-db-sync sub ERROR', e) - this.patchConnection$.next(PatchConnection.Disconnected) - console.log('Erroring out') - await pauseFor(4000) - this.start() - }, - complete: () => { - console.warn('patch-db-sync sub COMPLETE') - }, - }) - } catch (e) { - console.error('Failed to initialize PatchDB', e) - } + this.patchSub = this.patchDb.sync$() + .pipe( + debounceTime(400), + tap(cache => { + this.patchConnection$.next(PatchConnection.Connected) + this.bootstrapper.update(cache) + }), + ) + .subscribe({ + error: async e => { + console.error('patch-db SYNC ERROR', e) + this.patchConnection$.next(PatchConnection.Disconnected) + await pauseFor(4000) + this.start() + }, + complete: () => { + console.warn('patch-db SYNC COMPLETE') + }, + }) } stop (): void { - console.log('STOPPING PATCH DB') + console.log('stopping patch-db') this.patchConnection$.next(PatchConnection.Initializing) this.patchDb.store.reset() if (this.patchSub) { @@ -109,7 +96,7 @@ export class PatchDbService { .pipe( tap(data => console.log('NEW VALUE', data, ...args)), catchError(e => { - console.error('Error watching Patch DB', e) + console.error('Error watching patch-db', e) return of(e.message) }), finalize(() => console.log('UNSUBSCRIBING', ...args)),