diff --git a/frontend/package.json b/frontend/package.json index c40f28eb3..1bbd9b0e6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,7 @@ "check:diagnostic-ui": "tsc --project projects/diagnostic-ui/tsconfig.json --noEmit --skipLibCheck", "check:setup-wizard": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck", "check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck", - "build:deps": "cd ../patch-db/client && npm install && npm run build", + "build:deps": "rm -rf .angular/cache && cd ../patch-db/client && npm install && npm run build", "build:diagnostic-ui": "ng run diagnostic-ui:build", "build:setup-wizard": "ng run setup-wizard:build", "build:ui": "ng run ui:build && tsc projects/ui/postprocess.ts && node projects/ui/postprocess.js && git log | head -n1 > dist/ui/git-hash.txt", diff --git a/frontend/projects/diagnostic-ui/src/app/services/http.service.ts b/frontend/projects/diagnostic-ui/src/app/services/http.service.ts index 5bcfafe94..f5cf2958e 100644 --- a/frontend/projects/diagnostic-ui/src/app/services/http.service.ts +++ b/frontend/projects/diagnostic-ui/src/app/services/http.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core' import { HttpClient, HttpErrorResponse } from '@angular/common/http' +import { HttpError, RpcError } from '@start9labs/shared' @Injectable({ providedIn: 'root', @@ -25,31 +26,6 @@ export class HttpService { } } -function RpcError(e: RPCError['error']): void { - const { code, message, data } = e - - this.code = code - - if (typeof data === 'string') { - this.message = `${message}\n\n${data}` - } else { - if (data.details) { - this.message = `${message}\n\n${data.details}` - } else { - this.message = message - } - } -} - -function HttpError(e: HttpErrorResponse): void { - const { status, statusText } = e - - this.code = status - this.message = statusText - this.details = null - this.revision = null -} - function isRpcError( arg: { error: Error } | { result: Result }, ): arg is { error: Error } { @@ -102,7 +78,3 @@ export interface RPCError extends RPCBase { } export type RPCResponse = RPCSuccess | RPCError - -type HttpError = HttpErrorResponse & { - error: { code: string; message: string } -} diff --git a/frontend/projects/setup-wizard/src/app/app.component.ts b/frontend/projects/setup-wizard/src/app/app.component.ts index e5fbebf49..9914d0c41 100644 --- a/frontend/projects/setup-wizard/src/app/app.component.ts +++ b/frontend/projects/setup-wizard/src/app/app.component.ts @@ -29,7 +29,7 @@ export class AppComponent { this.stateService.isMigrating = false await this.navCtrl.navigateForward(`/recover`) } - } catch (e) { + } catch (e: any) { this.errorToastService.present(e) } } diff --git a/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts b/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts index ee77a1f9a..f7a13496c 100644 --- a/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts @@ -73,7 +73,7 @@ export class EmbassyPage { }) await alert.present() } - } catch (e) { + } catch (e: any) { this.errorToastService.present(e) } finally { this.loading = false @@ -141,7 +141,7 @@ export class EmbassyPage { } else { await this.navCtrl.navigateForward(`/success`) } - } catch (e) { + } catch (e: any) { this.errorToastService.present({ message: `${e.message}\n\nRestart Embassy to try again.`, }) diff --git a/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts b/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts index 6906e46c6..d1c543f30 100644 --- a/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts @@ -119,7 +119,7 @@ export class RecoverPage { await alert.present() this.hasShownGuidAlert = true } - } catch (e) { + } catch (e: any) { this.errorToastService.present(e) } finally { this.loading = false @@ -205,7 +205,7 @@ export class RecoverPage { try { await this.stateService.importDrive(guid) await this.navCtrl.navigateForward(`/success`) - } catch (e) { + } catch (e: any) { this.errorToastService.present(e) } finally { loader.dismiss() diff --git a/frontend/projects/setup-wizard/src/app/pages/success/success.page.ts b/frontend/projects/setup-wizard/src/app/pages/success/success.page.ts index ad087b2d4..d87497dc8 100644 --- a/frontend/projects/setup-wizard/src/app/pages/success/success.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/success/success.page.ts @@ -30,7 +30,7 @@ export class SuccessPage { encodeURIComponent(this.stateService.cert), ) this.download() - } catch (e) { + } catch (e: any) { await this.errCtrl.present(e) } } diff --git a/frontend/projects/setup-wizard/src/app/services/api/http.service.ts b/frontend/projects/setup-wizard/src/app/services/api/http.service.ts index ee8e94bc2..907251752 100644 --- a/frontend/projects/setup-wizard/src/app/services/api/http.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/api/http.service.ts @@ -8,6 +8,7 @@ import { import { Observable } from 'rxjs' import * as aesjs from 'aes-js' import * as pbkdf2 from 'pbkdf2' +import { HttpError, RpcError } from '@start9labs/shared' @Injectable({ providedIn: 'root', @@ -80,7 +81,7 @@ export class HttpService { .then(res => JSON.parse(res)) .catch(e => { if (!e.status && !e.statusText) { - throw new EncryptionError(e) + throw new EncryptionError() } else { throw new HttpError(e) } @@ -120,34 +121,10 @@ export class HttpService { } } -function RpcError(e: RPCError['error']): void { - const { code, message, data } = e - - this.code = code - - if (typeof data === 'string') { - this.message = `${message}\n\n${data}` - } else { - if (data.details) { - this.message = `${message}\n\n${data.details}` - } else { - this.message = message - } - } -} - -function HttpError(e: HttpErrorResponse): void { - const { status, statusText } = e - - this.code = status - this.message = statusText - this.details = null -} - -function EncryptionError(e: HttpErrorResponse): void { - this.code = null - this.message = 'Invalid Key' - this.details = null +class EncryptionError { + readonly code = null + readonly message = 'Invalid Key' + readonly details = null } function isRpcError( @@ -205,10 +182,6 @@ export interface RPCError extends RPCBase { export type RPCResponse = RPCSuccess | RPCError -type HttpError = HttpErrorResponse & { - error: { code: string; message: string } -} - export interface HttpOptions { method: Method url: string diff --git a/frontend/projects/setup-wizard/src/app/services/state.service.ts b/frontend/projects/setup-wizard/src/app/services/state.service.ts index 9d6aa9cad..0c32275ff 100644 --- a/frontend/projects/setup-wizard/src/app/services/state.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/state.service.ts @@ -49,7 +49,7 @@ export class StateService { let progress try { progress = await this.apiService.getRecoveryStatus() - } catch (e) { + } catch (e: any) { this.errorToastService.present({ message: `${e.message}\n\nRestart Embassy to try again.`, }) diff --git a/frontend/projects/shared/src/classes/http-error.ts b/frontend/projects/shared/src/classes/http-error.ts new file mode 100644 index 000000000..06414abcd --- /dev/null +++ b/frontend/projects/shared/src/classes/http-error.ts @@ -0,0 +1,10 @@ +import { HttpErrorResponse } from '@angular/common/http' + +export class HttpError { + readonly code = this.error.status + readonly message = this.error.statusText + readonly details = null + readonly revision = null + + constructor(private readonly error: HttpErrorResponse) {} +} diff --git a/frontend/projects/shared/src/classes/rpc-error.ts b/frontend/projects/shared/src/classes/rpc-error.ts new file mode 100644 index 000000000..9b224bf49 --- /dev/null +++ b/frontend/projects/shared/src/classes/rpc-error.ts @@ -0,0 +1,25 @@ +import { RpcErrorDetails } from '../types/rpc-error-details' + +export class RpcError { + readonly code = this.error.code + readonly message = this.getMessage() + readonly revision = this.getRevision() + + constructor(private readonly error: RpcErrorDetails) {} + + private getMessage(): string { + if (typeof this.error.data === 'string') { + return `${this.error.message}\n\n${this.error.data}` + } + + return this.error.data.details + ? `${this.error.message}\n\n${this.error.data.details}` + : this.error.message + } + + private getRevision(): T | null { + return typeof this.error.data === 'string' + ? null + : this.error.data.revision || null + } +} diff --git a/frontend/projects/shared/src/public-api.ts b/frontend/projects/shared/src/public-api.ts index 46cc94034..f8a18c239 100644 --- a/frontend/projects/shared/src/public-api.ts +++ b/frontend/projects/shared/src/public-api.ts @@ -2,6 +2,9 @@ * Public API Surface of @start9labs/shared */ +export * from './classes/http-error' +export * from './classes/rpc-error' + export * from './components/markdown/markdown.component' export * from './components/markdown/markdown.module' export * from './components/text-spinner/text-spinner.component.module' @@ -27,6 +30,7 @@ export * from './services/destroy.service' export * from './services/emver.service' export * from './services/error-toast.service' +export * from './types/rpc-error-details' export * from './types/url' export * from './types/workspace-config' diff --git a/frontend/projects/shared/src/types/rpc-error-details.ts b/frontend/projects/shared/src/types/rpc-error-details.ts new file mode 100644 index 000000000..5f1b0e973 --- /dev/null +++ b/frontend/projects/shared/src/types/rpc-error-details.ts @@ -0,0 +1,10 @@ +export interface RpcErrorDetails { + code: number + message: string + data?: + | { + details: string + revision?: T | null + } + | string +} diff --git a/frontend/projects/shared/src/util/misc.util.ts b/frontend/projects/shared/src/util/misc.util.ts index 44a0f0ca4..68d1446a3 100644 --- a/frontend/projects/shared/src/util/misc.util.ts +++ b/frontend/projects/shared/src/util/misc.util.ts @@ -14,14 +14,14 @@ export function capitalizeFirstLetter(string: string): string { return string.charAt(0).toUpperCase() + string.slice(1) } -export const exists = (t: any) => { +export function exists(t: T | undefined): t is T { return t !== undefined } export function debounce(delay: number = 300): MethodDecorator { return function ( target: any, - propertyKey: string, + propertyKey: string | symbol, descriptor: PropertyDescriptor, ) { const timeoutKey = Symbol() diff --git a/frontend/projects/ui/src/app/app.component.html b/frontend/projects/ui/src/app/app.component.html index 3a6653a57..ed5cc0cf8 100644 --- a/frontend/projects/ui/src/app/app.component.html +++ b/frontend/projects/ui/src/app/app.component.html @@ -1,8 +1,8 @@ - + diff --git a/frontend/projects/ui/src/app/app.component.ts b/frontend/projects/ui/src/app/app.component.ts index 6de8b5721..7458ca9f5 100644 --- a/frontend/projects/ui/src/app/app.component.ts +++ b/frontend/projects/ui/src/app/app.component.ts @@ -1,40 +1,6 @@ -import { Component, HostListener, NgZone } from '@angular/core' -import { Router } from '@angular/router' -import { - AlertController, - IonicSafeString, - LoadingController, - ModalController, - ToastController, -} from '@ionic/angular' -import { ToastButton } from '@ionic/core' -import { Storage } from '@ionic/storage-angular' -import { - debounce, - isEmptyObject, - Emver, - ErrorToastService, -} from '@start9labs/shared' -import { Subscription } from 'rxjs' -import { - debounceTime, - distinctUntilChanged, - filter, - take, -} from 'rxjs/operators' -import { AuthService, AuthState } from './services/auth.service' -import { ApiService } from './services/api/embassy-api.service' +import { Component } from '@angular/core' +import { AuthService } from './services/auth.service' import { SplitPaneTracker } from './services/split-pane.service' -import { PatchDbService } from './services/patch-db/patch-db.service' -import { - ConnectionFailure, - ConnectionService, -} from './services/connection.service' -import { ConfigService } from './services/config.service' -import { UIData } from 'src/app/services/patch-db/data-model' -import { LocalStorageService } from './services/local-storage.service' -import { EOSService } from './services/eos.service' -import { OSWelcomePage } from './modals/os-welcome/os-welcome.page' @Component({ selector: 'app-root', @@ -42,333 +8,12 @@ import { OSWelcomePage } from './modals/os-welcome/os-welcome.page' styleUrls: ['app.component.scss'], }) export class AppComponent { - showMenu = false - offlineToast: HTMLIonToastElement - updateToast: HTMLIonToastElement - notificationToast: HTMLIonToastElement - subscriptions: Subscription[] = [] - constructor( - private readonly storage: Storage, - private readonly authService: AuthService, - private readonly router: Router, - private readonly embassyApi: ApiService, - private readonly alertCtrl: AlertController, - private readonly loadingCtrl: LoadingController, - private readonly emver: Emver, - private readonly connectionService: ConnectionService, - private readonly modalCtrl: ModalController, - private readonly toastCtrl: ToastController, - private readonly errToast: ErrorToastService, - private readonly config: ConfigService, - private readonly zone: NgZone, + readonly authService: AuthService, private readonly splitPane: SplitPaneTracker, - private readonly patch: PatchDbService, - private readonly localStorageService: LocalStorageService, - private readonly eosService: EOSService, - ) { - this.init() - } - - @HostListener('document:keydown.enter', ['$event']) - @debounce() - handleKeyboardEvent() { - const elems = document.getElementsByClassName('enter-click') - const elem = elems[elems.length - 1] as HTMLButtonElement - - if (elem && !elem.classList.contains('no-click') && !elem.disabled) { - elem.click() - } - } + ) {} splitPaneVisible({ detail }: any) { this.splitPane.sidebarOpen$.next(detail.visible) } - - async init() { - await this.storage.create() - await this.authService.init() - await this.localStorageService.init() - - this.router.initialNavigation() - - // watch auth - this.authService.watch$().subscribe(async auth => { - // VERIFIED - if (auth === AuthState.VERIFIED) { - await 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(), - ]) - - this.patch - .watch$() - .pipe( - filter(obj => !isEmptyObject(obj)), - take(1), - ) - .subscribe(data => { - // check for updates to EOS - this.checkForEosUpdate(data.ui) - // show eos welcome message - this.showEosWelcome(data.ui['ack-welcome']) - - this.subscriptions = this.subscriptions.concat([ - // watch status to present toast for updated state - this.watchStatus(), - // watch version to refresh browser window - this.watchVersion(), - // watch unread notification count to display toast - this.watchNotifications(), - ]) - }) - // UNVERIFIED - } else if (auth === AuthState.UNVERIFIED) { - this.subscriptions.forEach(sub => sub.unsubscribe()) - this.subscriptions = [] - this.showMenu = false - this.patch.stop() - this.storage.clear() - if (this.errToast) this.errToast.dismiss() - if (this.updateToast) this.updateToast.dismiss() - if (this.notificationToast) this.notificationToast.dismiss() - if (this.offlineToast) this.offlineToast.dismiss() - this.zone.run(() => { - this.router.navigate(['/login'], { replaceUrl: true }) - }) - } - }) - } - - private checkForEosUpdate(ui: UIData): void { - if (ui['auto-check-updates'] !== false) { - this.eosService.getEOS() - } - } - - private async showEosWelcome(ackVersion: string): Promise { - if (!this.config.skipStartupAlerts && ackVersion !== this.config.version) { - const modal = await this.modalCtrl.create({ - component: OSWelcomePage, - presentingElement: await this.modalCtrl.getTop(), - backdropDismiss: false, - componentProps: { - version: this.config.version, - }, - }) - modal.onWillDismiss().then(() => { - this.embassyApi - .setDbValue({ pointer: '/ack-welcome', value: this.config.version }) - .catch() - }) - modal.present() - } - } - - private watchConnection(): Subscription { - return this.connectionService - .watchFailure$() - .pipe(distinctUntilChanged(), debounceTime(500)) - .subscribe(async connectionFailure => { - if (connectionFailure === ConnectionFailure.None) { - if (this.offlineToast) { - await this.offlineToast.dismiss() - this.offlineToast = undefined - } - } else { - let message: string | IonicSafeString - let link: string - switch (connectionFailure) { - case ConnectionFailure.Network: - message = 'Phone or computer has no network connection.' - break - case ConnectionFailure.Tor: - message = 'Browser unable to connect over Tor.' - link = 'https://start9.com/latest/support/common-issues' - break - case ConnectionFailure.Lan: - message = 'Embassy not found on Local Area Network.' - link = 'https://start9.com/latest/support/common-issues' - break - } - await this.presentToastOffline(message, link) - } - }) - } - - private watchStatus(): Subscription { - return this.patch - .watch$('server-info', 'status-info', 'updated') - .subscribe(isUpdated => { - if (isUpdated && !this.updateToast) { - this.presentToastUpdated() - } - }) - } - - private watchVersion(): Subscription { - return this.patch.watch$('server-info', 'version').subscribe(version => { - if (this.emver.compare(this.config.version, version) !== 0) { - this.presentAlertRefreshNeeded() - } - }) - } - - private watchNotifications(): Subscription { - let previous: number - return this.patch - .watch$('server-info', 'unread-notification-count') - .subscribe(count => { - if (previous !== undefined && count > previous) - this.presentToastNotifications() - previous = count - }) - } - - private async presentAlertRefreshNeeded() { - const alert = await this.alertCtrl.create({ - backdropDismiss: true, - header: 'Refresh Needed', - message: - 'Your user interface is cached and out of date. Hard refresh the page to get the latest UI.', - buttons: [ - { - text: 'Refresh Page', - cssClass: 'enter-click', - handler: () => { - location.reload() - }, - }, - ], - }) - await alert.present() - } - - private async presentToastUpdated() { - if (this.updateToast) return - - this.updateToast = await this.toastCtrl.create({ - header: 'EOS download complete!', - message: - 'Restart your Embassy for these updates to take effect. It can take several minutes to come back online.', - position: 'bottom', - duration: 0, - cssClass: 'success-toast', - buttons: [ - { - side: 'start', - icon: 'close', - handler: () => { - return true - }, - }, - { - side: 'end', - text: 'Restart', - handler: () => { - this.restart() - }, - }, - ], - }) - await this.updateToast.present() - } - - private async presentToastNotifications() { - if (this.notificationToast) return - - this.notificationToast = await this.toastCtrl.create({ - header: 'Embassy', - message: `New notifications`, - position: 'bottom', - duration: 4000, - buttons: [ - { - side: 'start', - icon: 'close', - handler: () => { - return true - }, - }, - { - side: 'end', - text: 'View', - handler: () => { - this.router.navigate(['/notifications'], { - queryParams: { toast: true }, - }) - }, - }, - ], - }) - await this.notificationToast.present() - } - - private async presentToastOffline( - message: string | IonicSafeString, - link?: string, - ) { - if (this.offlineToast) { - this.offlineToast.message = message - return - } - - let buttons: ToastButton[] = [ - { - side: 'start', - icon: 'close', - handler: () => { - return true - }, - }, - ] - - if (link) { - buttons.push({ - side: 'end', - text: 'View solutions', - handler: () => { - window.open(link, '_blank', 'noreferrer') - return false - }, - }) - } - - this.offlineToast = await this.toastCtrl.create({ - header: 'Unable to Connect', - cssClass: 'warning-toast', - message, - position: 'bottom', - duration: 0, - buttons, - }) - await this.offlineToast.present() - } - - private async restart(): Promise { - const loader = await this.loadingCtrl.create({ - spinner: 'lines', - message: 'Restarting...', - cssClass: 'loader', - }) - await loader.present() - - try { - await this.embassyApi.restartServer({}) - } catch (e) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - } } diff --git a/frontend/projects/ui/src/app/app.module.ts b/frontend/projects/ui/src/app/app.module.ts index d6d3e0eb3..36911efbd 100644 --- a/frontend/projects/ui/src/app/app.module.ts +++ b/frontend/projects/ui/src/app/app.module.ts @@ -1,36 +1,23 @@ -import { NgModule, ErrorHandler } from '@angular/core' -import { BrowserAnimationsModule } from '@angular/platform-browser/animations' -import { RouteReuseStrategy } from '@angular/router' -import { IonicModule, IonicRouteStrategy, IonNav } from '@ionic/angular' -import { Drivers } from '@ionic/storage' -import { IonicStorageModule, Storage } from '@ionic/storage-angular' import { HttpClientModule } from '@angular/common/http' +import { NgModule } from '@angular/core' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { IonicModule } from '@ionic/angular' +import { Drivers } from '@ionic/storage' +import { IonicStorageModule } from '@ionic/storage-angular' +import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor' +import { MarkdownModule, SharedPipesModule } from '@start9labs/shared' + import { AppComponent } from './app.component' import { AppRoutingModule } from './app-routing.module' -import { ApiService } from './services/api/embassy-api.service' -import { PatchDbServiceFactory } from './services/patch-db/patch-db.factory' -import { ConfigService } from './services/config.service' import { OSWelcomePageModule } from './modals/os-welcome/os-welcome.module' -import { PatchDbService } from './services/patch-db/patch-db.service' -import { LocalStorageBootstrap } from './services/patch-db/local-storage-bootstrap' -import { FormBuilder } from '@angular/forms' import { GenericInputComponentModule } from './modals/generic-input/generic-input.component.module' -import { AuthService } from './services/auth.service' -import { GlobalErrorHandler } from './services/global-error-handler.service' -import { MockApiService } from './services/api/embassy-mock-api.service' -import { LiveApiService } from './services/api/embassy-live-api.service' -import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor' -import { - MarkdownModule, - SharedPipesModule, - WorkspaceConfig, -} from '@start9labs/shared' import { MarketplaceModule } from './marketplace.module' import { PreloaderModule } from './app/preloader/preloader.module' import { FooterModule } from './app/footer/footer.module' import { MenuModule } from './app/menu/menu.module' - -const { useMocks } = require('../../../../config.json') as WorkspaceConfig +import { EnterModule } from './app/enter/enter.module' +import { APP_PROVIDERS } from './app.providers' +import { GlobalModule } from './app/global/global.module' @NgModule({ declarations: [AppComponent], @@ -51,40 +38,16 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig MenuModule, PreloaderModule, FooterModule, + EnterModule, OSWelcomePageModule, MarkdownModule, GenericInputComponentModule, MonacoEditorModule, SharedPipesModule, MarketplaceModule, + GlobalModule, ], - providers: [ - FormBuilder, - IonNav, - { - provide: RouteReuseStrategy, - useClass: IonicRouteStrategy, - }, - { - provide: ApiService, - useClass: useMocks ? MockApiService : LiveApiService, - }, - { - provide: PatchDbService, - useFactory: PatchDbServiceFactory, - deps: [ - ConfigService, - ApiService, - LocalStorageBootstrap, - AuthService, - Storage, - ], - }, - { - provide: ErrorHandler, - useClass: GlobalErrorHandler, - }, - ], + providers: APP_PROVIDERS, bootstrap: [AppComponent], }) export class AppModule {} diff --git a/frontend/projects/ui/src/app/app.providers.ts b/frontend/projects/ui/src/app/app.providers.ts new file mode 100644 index 000000000..038e09f69 --- /dev/null +++ b/frontend/projects/ui/src/app/app.providers.ts @@ -0,0 +1,65 @@ +import { DOCUMENT } from '@angular/common' +import { APP_INITIALIZER, ErrorHandler, Provider } from '@angular/core' +import { FormBuilder } from '@angular/forms' +import { Router, RouteReuseStrategy } from '@angular/router' +import { IonicRouteStrategy, IonNav } from '@ionic/angular' +import { Storage } from '@ionic/storage-angular' +import { WorkspaceConfig } from '@start9labs/shared' + +import { ApiService } from './services/api/embassy-api.service' +import { MockApiService } from './services/api/embassy-mock-api.service' +import { LiveApiService } from './services/api/embassy-live-api.service' +import { + PATCH_SOURCE, + mockSourceFactory, + realSourceFactory, +} from './services/patch-db/patch-db.factory' +import { ConfigService } from './services/config.service' +import { GlobalErrorHandler } from './services/global-error-handler.service' +import { AuthService } from './services/auth.service' +import { LocalStorageService } from './services/local-storage.service' + +const { useMocks } = require('../../../../config.json') as WorkspaceConfig + +export const APP_PROVIDERS: Provider[] = [ + FormBuilder, + IonNav, + { + provide: RouteReuseStrategy, + useClass: IonicRouteStrategy, + }, + { + provide: ApiService, + useClass: useMocks ? MockApiService : LiveApiService, + }, + { + provide: PATCH_SOURCE, + deps: [ApiService, ConfigService, DOCUMENT], + useFactory: useMocks ? mockSourceFactory : realSourceFactory, + }, + { + provide: ErrorHandler, + useClass: GlobalErrorHandler, + }, + { + provide: APP_INITIALIZER, + deps: [Storage, AuthService, LocalStorageService, Router], + useFactory: appInitializer, + multi: true, + }, +] + +export function appInitializer( + storage: Storage, + auth: AuthService, + localStorage: LocalStorageService, + router: Router, +): () => Promise { + return async () => { + await storage.create() + await auth.init() + await localStorage.init() + + router.initialNavigation() + } +} diff --git a/frontend/projects/ui/src/app/app/enter/enter.directive.ts b/frontend/projects/ui/src/app/app/enter/enter.directive.ts new file mode 100644 index 000000000..2a93ad93b --- /dev/null +++ b/frontend/projects/ui/src/app/app/enter/enter.directive.ts @@ -0,0 +1,21 @@ +import { Directive, HostListener, Inject } from '@angular/core' +import { DOCUMENT } from '@angular/common' +import { debounce } from '@start9labs/shared' + +@Directive({ + selector: '[appEnter]', +}) +export class EnterDirective { + constructor(@Inject(DOCUMENT) private readonly document: Document) {} + + @HostListener('document:keydown.enter') + @debounce() + handleKeyboardEvent() { + const elems = this.document.querySelectorAll('.enter-click') + const elem = elems[elems.length - 1] as HTMLButtonElement + + if (elem && !elem.classList.contains('no-click') && !elem.disabled) { + elem.click() + } + } +} diff --git a/frontend/projects/ui/src/app/app/enter/enter.module.ts b/frontend/projects/ui/src/app/app/enter/enter.module.ts new file mode 100644 index 000000000..776f2eb38 --- /dev/null +++ b/frontend/projects/ui/src/app/app/enter/enter.module.ts @@ -0,0 +1,9 @@ +import { NgModule } from '@angular/core' + +import { EnterDirective } from './enter.directive' + +@NgModule({ + declarations: [EnterDirective], + exports: [EnterDirective], +}) +export class EnterModule {} diff --git a/frontend/projects/ui/src/app/app/global/global.module.ts b/frontend/projects/ui/src/app/app/global/global.module.ts new file mode 100644 index 000000000..27800f21f --- /dev/null +++ b/frontend/projects/ui/src/app/app/global/global.module.ts @@ -0,0 +1,64 @@ +import { + ClassProvider, + ExistingProvider, + Inject, + InjectionToken, + NgModule, + OnDestroy, + Type, +} from '@angular/core' +import { merge, Observable } from 'rxjs' +import { OfflineService } from './services/offline.service' +import { LogoutService } from './services/logout.service' +import { PatchMonitorService } from './services/patch-monitor.service' +import { PatchDataService } from './services/patch-data.service' +import { ConnectionMonitorService } from './services/connection-monitor.service' +import { UnreadToastService } from './services/unread-toast.service' +import { RefreshToastService } from './services/refresh-toast.service' +import { UpdateToastService } from './services/update-toast.service' + +const GLOBAL_SERVICE = new InjectionToken[]>( + 'A multi token of global Observable services', +) + +@NgModule({ + providers: [ + [ + ConnectionMonitorService, + LogoutService, + OfflineService, + RefreshToastService, + UnreadToastService, + UpdateToastService, + ].map(useClass), + [PatchDataService, PatchMonitorService].map(useExisting), + ], +}) +export class GlobalModule implements OnDestroy { + readonly subscription = merge(...this.services).subscribe() + + constructor( + @Inject(GLOBAL_SERVICE) + private readonly services: readonly Observable[], + ) {} + + ngOnDestroy() { + this.subscription.unsubscribe() + } +} + +function useClass(useClass: Type): ClassProvider { + return { + provide: GLOBAL_SERVICE, + multi: true, + useClass, + } +} + +function useExisting(useExisting: Type): ExistingProvider { + return { + provide: GLOBAL_SERVICE, + multi: true, + useExisting, + } +} diff --git a/frontend/projects/ui/src/app/app/global/services/connection-monitor.service.ts b/frontend/projects/ui/src/app/app/global/services/connection-monitor.service.ts new file mode 100644 index 000000000..94e739100 --- /dev/null +++ b/frontend/projects/ui/src/app/app/global/services/connection-monitor.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core' +import { EMPTY, Observable } from 'rxjs' +import { switchMap } from 'rxjs/operators' + +import { PatchMonitorService } from './patch-monitor.service' +import { ConnectionService } from 'src/app/services/connection.service' + +// Start connection monitor upon PatchDb start +@Injectable() +export class ConnectionMonitorService extends Observable { + private readonly stream$ = this.patchMonitor.pipe( + switchMap(started => (started ? this.connectionService.start() : EMPTY)), + ) + + constructor( + private readonly patchMonitor: PatchMonitorService, + private readonly connectionService: ConnectionService, + ) { + super(subscriber => this.stream$.subscribe(subscriber)) + } +} diff --git a/frontend/projects/ui/src/app/app/global/services/logout.service.ts b/frontend/projects/ui/src/app/app/global/services/logout.service.ts new file mode 100644 index 000000000..18cd4d3d7 --- /dev/null +++ b/frontend/projects/ui/src/app/app/global/services/logout.service.ts @@ -0,0 +1,27 @@ +import { Injectable, NgZone } from '@angular/core' +import { Router } from '@angular/router' +import { filter, tap } from 'rxjs/operators' +import { Observable } from 'rxjs' + +import { AuthService } from 'src/app/services/auth.service' + +// Redirect to login page upon broken authorization +@Injectable() +export class LogoutService extends Observable { + private readonly stream$ = this.authService.isVerified$.pipe( + filter(verified => !verified), + tap(() => { + this.zone.run(() => { + this.router.navigate(['/login'], { replaceUrl: true }) + }) + }), + ) + + constructor( + private readonly authService: AuthService, + private readonly zone: NgZone, + private readonly router: Router, + ) { + super(subscriber => this.stream$.subscribe(subscriber)) + } +} diff --git a/frontend/projects/ui/src/app/app/global/services/offline.service.ts b/frontend/projects/ui/src/app/app/global/services/offline.service.ts new file mode 100644 index 000000000..570e0857c --- /dev/null +++ b/frontend/projects/ui/src/app/app/global/services/offline.service.ts @@ -0,0 +1,115 @@ +import { Injectable } from '@angular/core' +import { IonicSafeString, ToastController, ToastOptions } from '@ionic/angular' +import { ToastButton } from '@ionic/core' +import { EMPTY, from, Observable } from 'rxjs' +import { + debounceTime, + distinctUntilChanged, + filter, + map, + switchMap, + tap, +} from 'rxjs/operators' + +import { AuthService } from 'src/app/services/auth.service' +import { + ConnectionFailure, + ConnectionService, +} from 'src/app/services/connection.service' + +// Watch for connection status +@Injectable() +export class OfflineService extends Observable { + private current?: HTMLIonToastElement + + private readonly connection$ = this.connectionService + .watchFailure$() + .pipe(distinctUntilChanged(), debounceTime(500)) + + private readonly stream$ = this.authService.isVerified$.pipe( + // Close on logout + tap(() => this.current?.dismiss()), + switchMap(verified => (verified ? this.connection$ : EMPTY)), + // Close on change to connection state + tap(() => this.current?.dismiss()), + filter(connection => connection !== ConnectionFailure.None), + map(getMessage), + switchMap(({ message, link }) => + this.getToast().pipe( + tap(toast => { + this.current = toast + + toast.message = message + toast.buttons = getButtons(link) + toast.present() + }), + ), + ), + ) + + constructor( + private readonly authService: AuthService, + private readonly connectionService: ConnectionService, + private readonly toastCtrl: ToastController, + ) { + super(subscriber => this.stream$.subscribe(subscriber)) + } + + private getToast(): Observable { + return from(this.toastCtrl.create(TOAST)) + } +} + +const TOAST: ToastOptions = { + header: 'Unable to Connect', + cssClass: 'warning-toast', + message: '', + position: 'bottom', + duration: 0, + buttons: [], +} + +function getMessage(failure: ConnectionFailure): OfflineMessage { + switch (failure) { + case ConnectionFailure.Network: + return { message: 'Phone or computer has no network connection.' } + case ConnectionFailure.Tor: + return { + message: 'Browser unable to connect over Tor.', + link: 'https://start9.com/latest/support/common-issues', + } + case ConnectionFailure.Lan: + return { + message: 'Embassy not found on Local Area Network.', + link: 'https://start9.com/latest/support/common-issues', + } + } +} + +function getButtons(link?: string): ToastButton[] { + const buttons: ToastButton[] = [ + { + side: 'start', + icon: 'close', + handler: () => true, + }, + ] + + if (link) { + buttons.push({ + side: 'end', + text: 'View solutions', + handler: () => { + window.open(link, '_blank', 'noreferrer') + return false + }, + }) + } + + return buttons +} + +interface OfflineMessage { + readonly message: string | IonicSafeString + readonly link?: string +} diff --git a/frontend/projects/ui/src/app/app/global/services/patch-data.service.ts b/frontend/projects/ui/src/app/app/global/services/patch-data.service.ts new file mode 100644 index 000000000..9bf624479 --- /dev/null +++ b/frontend/projects/ui/src/app/app/global/services/patch-data.service.ts @@ -0,0 +1,80 @@ +import { Injectable } from '@angular/core' +import { ModalController } from '@ionic/angular' +import { Storage } from '@ionic/storage-angular' +import { Observable, of } from 'rxjs' +import { filter, share, switchMap, take, tap } from 'rxjs/operators' +import { isEmptyObject } from '@start9labs/shared' + +import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' +import { AuthService } from 'src/app/services/auth.service' +import { DataModel, UIData } from 'src/app/services/patch-db/data-model' +import { EOSService } from 'src/app/services/eos.service' +import { OSWelcomePage } from 'src/app/modals/os-welcome/os-welcome.page' +import { ConfigService } from 'src/app/services/config.service' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { PatchMonitorService } from './patch-monitor.service' + +// Get data from PatchDb after is starts and act upon it +@Injectable({ + providedIn: 'root', +}) +export class PatchDataService extends Observable { + private readonly stream$ = this.patchMonitor.pipe( + switchMap(started => + started + ? this.patch.watch$().pipe( + filter(obj => !isEmptyObject(obj)), + take(1), + tap(({ ui }) => { + // check for updates to EOS + this.checkForEosUpdate(ui) + // show eos welcome message + this.showEosWelcome(ui['ack-welcome']) + }), + ) + : of(null), + ), + share(), + ) + + constructor( + private readonly patchMonitor: PatchMonitorService, + private readonly authService: AuthService, + private readonly patch: PatchDbService, + private readonly storage: Storage, + private readonly eosService: EOSService, + private readonly config: ConfigService, + private readonly modalCtrl: ModalController, + private readonly embassyApi: ApiService, + ) { + super(subscriber => this.stream$.subscribe(subscriber)) + } + + private checkForEosUpdate(ui: UIData): void { + if (ui['auto-check-updates'] !== false) { + this.eosService.getEOS() + } + } + + private async showEosWelcome(ackVersion: string): Promise { + if (this.config.skipStartupAlerts || ackVersion === this.config.version) { + return + } + + const modal = await this.modalCtrl.create({ + component: OSWelcomePage, + presentingElement: await this.modalCtrl.getTop(), + backdropDismiss: false, + componentProps: { + version: this.config.version, + }, + }) + modal.onWillDismiss().then(() => { + this.embassyApi + .setDbValue({ pointer: '/ack-welcome', value: this.config.version }) + .catch() + }) + + await modal.present() + } +} diff --git a/frontend/projects/ui/src/app/app/global/services/patch-monitor.service.ts b/frontend/projects/ui/src/app/app/global/services/patch-monitor.service.ts new file mode 100644 index 000000000..b7ae6c1ae --- /dev/null +++ b/frontend/projects/ui/src/app/app/global/services/patch-monitor.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core' +import { Storage } from '@ionic/storage-angular' +import { from, Observable, of } from 'rxjs' +import { mapTo, share, switchMap } from 'rxjs/operators' + +import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' +import { AuthService } from 'src/app/services/auth.service' +import { ConnectionService } from 'src/app/services/connection.service' + +// Start and stop PatchDb upon verification +@Injectable({ + providedIn: 'root', +}) +export class PatchMonitorService extends Observable { + private readonly stream$ = this.authService.isVerified$.pipe( + switchMap(verified => { + if (verified) { + return from(this.patch.start()).pipe(mapTo(true)) + } + + this.patch.stop() + this.storage.clear() + + return of(false) + }), + share(), + ) + + constructor( + private readonly connectionService: ConnectionService, + private readonly authService: AuthService, + private readonly patch: PatchDbService, + private readonly storage: Storage, + ) { + super(subscriber => this.stream$.subscribe(subscriber)) + } +} diff --git a/frontend/projects/ui/src/app/app/global/services/refresh-toast.service.ts b/frontend/projects/ui/src/app/app/global/services/refresh-toast.service.ts new file mode 100644 index 000000000..db871687d --- /dev/null +++ b/frontend/projects/ui/src/app/app/global/services/refresh-toast.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core' +import { AlertController, AlertOptions } from '@ionic/angular' +import { EMPTY, from, Observable } from 'rxjs' +import { filter, switchMap } from 'rxjs/operators' +import { Emver } from '@start9labs/shared' + +import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' +import { ConfigService } from 'src/app/services/config.service' +import { PatchDataService } from './patch-data.service' + +// Watch version to refresh browser window +@Injectable() +export class RefreshToastService extends Observable { + private readonly stream$ = this.patchData.pipe( + switchMap(data => + data ? this.patch.watch$('server-info', 'version') : EMPTY, + ), + filter(version => !!this.emver.compare(this.config.version, version)), + switchMap(() => this.getAlert()), + switchMap(alert => alert.present()), + ) + + constructor( + private readonly patchData: PatchDataService, + private readonly patch: PatchDbService, + private readonly emver: Emver, + private readonly config: ConfigService, + private readonly alertCtrl: AlertController, + ) { + super(subscriber => this.stream$.subscribe(subscriber)) + } + + private getAlert(): Observable { + return from(this.alertCtrl.create(ALERT)) + } +} + +const ALERT: AlertOptions = { + backdropDismiss: true, + header: 'Refresh Needed', + message: + 'Your user interface is cached and out of date. Hard refresh the page to get the latest UI.', + buttons: [ + { + text: 'Refresh Page', + cssClass: 'enter-click', + handler: () => { + location.reload() + }, + }, + ], +} diff --git a/frontend/projects/ui/src/app/app/global/services/unread-toast.service.ts b/frontend/projects/ui/src/app/app/global/services/unread-toast.service.ts new file mode 100644 index 000000000..070a2584d --- /dev/null +++ b/frontend/projects/ui/src/app/app/global/services/unread-toast.service.ts @@ -0,0 +1,80 @@ +import { Injectable } from '@angular/core' +import { Router } from '@angular/router' +import { + LoadingController, + ToastController, + ToastOptions, +} from '@ionic/angular' +import { EMPTY, merge, Observable } from 'rxjs' +import { filter, pairwise, switchMap, tap } from 'rxjs/operators' +import { ErrorToastService } from '@start9labs/shared' + +import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' +import { ConfigService } from 'src/app/services/config.service' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { PatchDataService } from './patch-data.service' + +// Watch unread notification count to display toast +@Injectable() +export class UnreadToastService extends Observable { + private unreadToast: HTMLIonToastElement + + private readonly stream$ = this.patchData.pipe( + switchMap(data => { + if (data) { + return this.patch.watch$('server-info', 'unread-notification-count') + } + + this.unreadToast?.dismiss() + + return EMPTY + }), + pairwise(), + filter(([prev, cur]) => cur > prev), + tap(() => { + this.showToast() + }), + ) + + constructor( + private readonly router: Router, + private readonly patchData: PatchDataService, + private readonly patch: PatchDbService, + private readonly config: ConfigService, + private readonly embassyApi: ApiService, + private readonly toastCtrl: ToastController, + ) { + super(subscriber => this.stream$.subscribe(subscriber)) + } + + private async showToast() { + await this.unreadToast?.dismiss() + + this.unreadToast = await this.toastCtrl.create(TOAST) + this.unreadToast.buttons.push({ + side: 'end', + text: 'View', + handler: () => { + this.router.navigate(['/notifications'], { + queryParams: { toast: true }, + }) + }, + }) + + await this.unreadToast.present() + } +} + +const TOAST: ToastOptions = { + header: 'Embassy', + message: `New notifications`, + position: 'bottom', + duration: 4000, + buttons: [ + { + side: 'start', + icon: 'close', + handler: () => true, + }, + ], +} diff --git a/frontend/projects/ui/src/app/app/global/services/update-toast.service.ts b/frontend/projects/ui/src/app/app/global/services/update-toast.service.ts new file mode 100644 index 000000000..8000e3331 --- /dev/null +++ b/frontend/projects/ui/src/app/app/global/services/update-toast.service.ts @@ -0,0 +1,98 @@ +import { Injectable } from '@angular/core' +import { + LoadingController, + LoadingOptions, + ToastController, + ToastOptions, +} from '@ionic/angular' +import { EMPTY, Observable } from 'rxjs' +import { distinctUntilChanged, filter, switchMap } from 'rxjs/operators' +import { ErrorToastService } from '@start9labs/shared' + +import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { PatchDataService } from './patch-data.service' + +// Watch status to present toast for updated state +@Injectable() +export class UpdateToastService extends Observable { + private updateToast: HTMLIonToastElement + + private readonly stream$ = this.patchData.pipe( + switchMap(data => { + if (data) { + return this.patch.watch$('server-info', 'status-info', 'updated') + } + + this.errToast.dismiss() + this.updateToast?.dismiss() + + return EMPTY + }), + distinctUntilChanged(), + filter(Boolean), + switchMap(() => this.showToast()), + ) + + constructor( + private readonly patchData: PatchDataService, + private readonly patch: PatchDbService, + private readonly embassyApi: ApiService, + private readonly toastCtrl: ToastController, + private readonly errToast: ErrorToastService, + private readonly loadingCtrl: LoadingController, + ) { + super(subscriber => this.stream$.subscribe(subscriber)) + } + + private async showToast() { + await this.updateToast?.dismiss() + + this.updateToast = await this.toastCtrl.create(TOAST) + this.updateToast.buttons.push({ + side: 'end', + text: 'Restart', + handler: () => { + this.restart() + }, + }) + + await this.updateToast.present() + } + + private async restart(): Promise { + const loader = await this.loadingCtrl.create(LOADER) + + await loader.present() + + try { + await this.embassyApi.restartServer({}) + } catch (e: any) { + await this.errToast.present(e) + } finally { + await loader.dismiss() + } + } +} + +const LOADER: LoadingOptions = { + spinner: 'lines', + message: 'Restarting...', + cssClass: 'loader', +} + +const TOAST: ToastOptions = { + header: 'EOS download complete!', + message: + 'Restart your Embassy for these updates to take effect. It can take several minutes to come back online.', + position: 'bottom', + duration: 0, + cssClass: 'success-toast', + buttons: [ + { + side: 'start', + icon: 'close', + handler: () => true, + }, + ], +} diff --git a/frontend/projects/ui/src/app/app/snek/snek.directive.ts b/frontend/projects/ui/src/app/app/snek/snek.directive.ts index a60185c5c..e7454c6bd 100644 --- a/frontend/projects/ui/src/app/app/snek/snek.directive.ts +++ b/frontend/projects/ui/src/app/app/snek/snek.directive.ts @@ -44,7 +44,7 @@ export class SnekDirective { pointer: '/gaming', value: { snake: { 'high-score': data.highScore } }, }) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { this.loadingCtrl.dismiss() diff --git a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts index bfdd17293..5539af4e3 100644 --- a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts +++ b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts @@ -157,7 +157,7 @@ export class BackupDrivesComponent { entry, }) return true - } catch (e) { + } catch (e: any) { this.errToast.present(e) return false } finally { @@ -209,9 +209,8 @@ export class BackupDrivesComponent { try { const res = await this.embassyApi.updateBackupTarget(value) - const entry = Object.values(res)[0] - this.backupService.cifs[index].entry = entry - } catch (e) { + this.backupService.cifs[index].entry = Object.values(res)[0] + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() @@ -229,7 +228,7 @@ export class BackupDrivesComponent { try { await this.embassyApi.removeBackupTarget({ id }) this.backupService.cifs.splice(index, 1) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() diff --git a/frontend/projects/ui/src/app/components/backup-drives/backup.service.ts b/frontend/projects/ui/src/app/components/backup-drives/backup.service.ts index ddeac1a06..47a46070e 100644 --- a/frontend/projects/ui/src/app/components/backup-drives/backup.service.ts +++ b/frontend/projects/ui/src/app/components/backup-drives/backup.service.ts @@ -48,7 +48,7 @@ export class BackupService { entry: drive as DiskBackupTarget, } }) - } catch (e) { + } catch (e: any) { this.loadingError = getErrorMessage(e) } finally { this.loading = false diff --git a/frontend/projects/ui/src/app/components/form-object/form-object.component.ts b/frontend/projects/ui/src/app/components/form-object/form-object.component.ts index 04c6a9f0c..67d6353e4 100644 --- a/frontend/projects/ui/src/app/components/form-object/form-object.component.ts +++ b/frontend/projects/ui/src/app/components/form-object/form-object.component.ts @@ -212,8 +212,7 @@ export class FormObjectComponent { component: EnumListPage, }) - modal.onWillDismiss().then((res: { data: string[] }) => { - const data = res.data + modal.onWillDismiss().then(({ data }) => { if (!data) return this.updateEnumList(key, current, data) }) diff --git a/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.html b/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.html index c5f0dfbef..55ef77ee2 100644 --- a/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.html +++ b/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.html @@ -1,23 +1,23 @@
- Checking for installed services which depend on {{ params.title }}... + + Checking for installed services which depend on + {{ params.title }}... +
-
- -
- - WARNING - +
+
+ WARNING
{{ dependentViolation }}
-
-
+
+
Affected Services
@@ -25,8 +25,11 @@ style="--ion-item-background: margin-top: 5px" *ngFor="let dep of dependentBreakages | keyvalue" > - - + +
{{ patch.data['package-data'][dep.key].manifest.title }}
@@ -34,4 +37,4 @@
-
\ No newline at end of file +
diff --git a/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.scss b/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.scss new file mode 100644 index 000000000..051ba8121 --- /dev/null +++ b/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.scss @@ -0,0 +1,35 @@ +.wrapper { + margin-top: 25px; +} + +.warning { + margin: 15px; + display: flex; + justify-content: center; + align-items: center; +} + +.label { + font-size: xx-large; + font-weight: bold; +} + +.items { + margin: 25px 0; +} + +.affected { + border-width: 0 0 1px 0; + font-size: unset; + text-align: left; + font-weight: bold; + margin-left: 13px; + border-style: solid; + border-color: var(--ion-color-light-tint); +} + +.thumbnail { + position: relative; + height: 4vh; + width: 4vh; +} diff --git a/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.ts b/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.ts index 0c781ac5d..ae51eb86e 100644 --- a/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.ts +++ b/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.ts @@ -10,7 +10,10 @@ import { WizardAction } from '../wizard-types' @Component({ selector: 'dependents', templateUrl: './dependents.component.html', - styleUrls: ['../install-wizard.component.scss'], + styleUrls: [ + './dependents.component.scss', + '../install-wizard.component.scss', + ], }) export class DependentsComponent { @Input() params: { diff --git a/frontend/projects/ui/src/app/components/logs/logs.page.ts b/frontend/projects/ui/src/app/components/logs/logs.page.ts index 91e9df532..ac9364697 100644 --- a/frontend/projects/ui/src/app/components/logs/logs.page.ts +++ b/frontend/projects/ui/src/app/components/logs/logs.page.ts @@ -54,7 +54,7 @@ export class LogsPage { this.loading = false return logsRes.entries - } catch (e) { + } catch (e: any) { this.errToast.present(e) } } diff --git a/frontend/projects/ui/src/app/guards/auth.guard.ts b/frontend/projects/ui/src/app/guards/auth.guard.ts index 576664aa9..9606f82f7 100644 --- a/frontend/projects/ui/src/app/guards/auth.guard.ts +++ b/frontend/projects/ui/src/app/guards/auth.guard.ts @@ -1,38 +1,29 @@ import { Injectable } from '@angular/core' -import { CanActivate, Router, CanActivateChild } from '@angular/router' -import { tap } from 'rxjs/operators' -import { AuthState, AuthService } from '../services/auth.service' +import { CanActivate, Router, CanActivateChild, UrlTree } from '@angular/router' +import { map } from 'rxjs/operators' +import { AuthService } from '../services/auth.service' +import { Observable } from 'rxjs' @Injectable({ providedIn: 'root', }) export class AuthGuard implements CanActivate, CanActivateChild { - authState: AuthState - - constructor ( + constructor( private readonly authService: AuthService, private readonly router: Router, - ) { - this.authService.watch$() - .pipe( - tap(auth => this.authState = auth), - ).subscribe() - } + ) {} - canActivate (): boolean { + canActivate(): Observable { return this.runAuthCheck() } - canActivateChild (): boolean { + canActivateChild(): Observable { return this.runAuthCheck() } - private runAuthCheck (): boolean { - if (this.authState === AuthState.VERIFIED) { - return true - } else { - this.router.navigate(['/login'], { replaceUrl: true }) - return false - } + private runAuthCheck(): Observable { + return this.authService.isVerified$.pipe( + map(verified => verified || this.router.parseUrl('/login')), + ) } } diff --git a/frontend/projects/ui/src/app/guards/unauth.guard.ts b/frontend/projects/ui/src/app/guards/unauth.guard.ts index 18552a830..3951173e9 100644 --- a/frontend/projects/ui/src/app/guards/unauth.guard.ts +++ b/frontend/projects/ui/src/app/guards/unauth.guard.ts @@ -1,31 +1,21 @@ import { Injectable } from '@angular/core' -import { CanActivate, Router } from '@angular/router' -import { tap } from 'rxjs/operators' -import { AuthService, AuthState } from '../services/auth.service' +import { CanActivate, Router, UrlTree } from '@angular/router' +import { map } from 'rxjs/operators' +import { AuthService } from '../services/auth.service' +import { Observable } from 'rxjs' @Injectable({ providedIn: 'root', }) export class UnauthGuard implements CanActivate { - authState: AuthState - - constructor ( + constructor( private readonly authService: AuthService, private readonly router: Router, - ) { - this.authService.watch$() - .pipe( - tap(auth => this.authState = auth), - ).subscribe() - } + ) {} - canActivate (): boolean { - if (this.authState === AuthState.VERIFIED) { - this.router.navigateByUrl('') - return false - } else { - return true - } + canActivate(): Observable { + return this.authService.isVerified$.pipe( + map(verified => !verified || this.router.parseUrl('')), + ) } } - diff --git a/frontend/projects/ui/src/app/modals/app-config/app-config.page.html b/frontend/projects/ui/src/app/modals/app-config/app-config.page.html index cd8969ddc..ce8cbba5d 100644 --- a/frontend/projects/ui/src/app/modals/app-config/app-config.page.html +++ b/frontend/projects/ui/src/app/modals/app-config/app-config.page.html @@ -6,7 +6,11 @@ Config - + Reset Defaults @@ -16,38 +20,52 @@ - - + - - + - - {{ loadingError }} - + {{ loadingError }} - + - To use the default config for {{ pkg.manifest.title }}, click "Save" below. + To use the default config for {{ pkg.manifest.title }}, click + "Save" below. - + - + -

- - {{ pkg.manifest.title }} +

+ + {{ pkg.manifest.title }}

- The following modifications have been made to {{ pkg.manifest.title }} to satisfy {{ dependentInfo.title }}: + The following modifications have been made to {{ + pkg.manifest.title }} to satisfy {{ dependentInfo.title }}:

@@ -56,14 +74,17 @@

- + -

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

+

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

- +
-
+
- - + + Save - + Close diff --git a/frontend/projects/ui/src/app/modals/app-config/app-config.page.ts b/frontend/projects/ui/src/app/modals/app-config/app-config.page.ts index c2d1e92ba..654b5fef1 100644 --- a/frontend/projects/ui/src/app/modals/app-config/app-config.page.ts +++ b/frontend/projects/ui/src/app/modals/app-config/app-config.page.ts @@ -36,7 +36,7 @@ export class AppConfigPage { @Input() pkgId: string @Input() dependentInfo?: DependentInfo diff: string[] // only if dependent info - pkg: PackageDataEntry + pkg?: PackageDataEntry loadingText: string | undefined configSpec: ConfigSpec configForm: FormGroup @@ -58,7 +58,7 @@ export class AppConfigPage { async ngOnInit() { this.pkg = this.patch.getData()['package-data'][this.pkgId] - this.hasConfig = !!this.pkg.manifest.config + this.hasConfig = !!this.pkg?.manifest.config if (!this.hasConfig) return @@ -102,7 +102,7 @@ export class AppConfigPage { this.diff = this.getDiff(patch) this.markDirty(patch) } - } catch (e) { + } catch (e: any) { this.loadingError = getErrorMessage(e) } finally { this.loadingText = undefined @@ -133,7 +133,7 @@ export class AppConfigPage { if (this.configForm.invalid) { document .getElementsByClassName('validation-error')[0] - .parentElement.parentElement.scrollIntoView({ behavior: 'smooth' }) + ?.parentElement.parentElement.scrollIntoView({ behavior: 'smooth' }) return } @@ -170,7 +170,7 @@ export class AppConfigPage { config, }) this.modalCtrl.dismiss() - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { this.saving = false diff --git a/frontend/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts b/frontend/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts index 61265b017..841e6c510 100644 --- a/frontend/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts +++ b/frontend/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts @@ -82,7 +82,7 @@ export class AppRecoverSelectPage { password: this.password, }) this.modalCtrl.dismiss(undefined, 'success') - } catch (e) { + } catch (e: any) { this.error = getErrorMessage(e) } finally { loader.dismiss() diff --git a/frontend/projects/ui/src/app/modals/generic-form/generic-form.page.ts b/frontend/projects/ui/src/app/modals/generic-form/generic-form.page.ts index 47db0dc9f..321d066ca 100644 --- a/frontend/projects/ui/src/app/modals/generic-form/generic-form.page.ts +++ b/frontend/projects/ui/src/app/modals/generic-form/generic-form.page.ts @@ -50,7 +50,7 @@ export class GenericFormPage { this.formGroup.markAllAsTouched() document .getElementsByClassName('validation-error')[0] - .parentElement.parentElement.scrollIntoView({ behavior: 'smooth' }) + ?.parentElement.parentElement.scrollIntoView({ behavior: 'smooth' }) return } diff --git a/frontend/projects/ui/src/app/modals/generic-input/generic-input.component.ts b/frontend/projects/ui/src/app/modals/generic-input/generic-input.component.ts index aaa4b3109..ef4deb296 100644 --- a/frontend/projects/ui/src/app/modals/generic-input/generic-input.component.ts +++ b/frontend/projects/ui/src/app/modals/generic-input/generic-input.component.ts @@ -52,7 +52,7 @@ export class GenericInputComponent { try { await this.options.submitFn(value) this.modalCtrl.dismiss(undefined, 'success') - } catch (e) { + } catch (e: any) { this.error = getErrorMessage(e) } } diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts index be9458d6b..b0f7311b0 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts @@ -178,7 +178,7 @@ export class AppActionsPage { }) setTimeout(() => successModal.present(), 400) - } catch (e) { + } catch (e: any) { this.errToast.present(e) return false } finally { diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list-empty/app-list-empty.component.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list-empty/app-list-empty.component.ts index c17ce99e0..3c32e4abb 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list-empty/app-list-empty.component.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list-empty/app-list-empty.component.ts @@ -1,9 +1,9 @@ -import { ChangeDetectionStrategy, Component } from "@angular/core"; +import { ChangeDetectionStrategy, Component } from '@angular/core' @Component({ - selector: "app-list-empty", - templateUrl: "app-list-empty.component.html", - styleUrls: ["app-list-empty.component.scss"], + selector: 'app-list-empty', + templateUrl: 'app-list-empty.component.html', + styleUrls: ['app-list-empty.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppListEmptyComponent {} diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.ts index 2803d77af..906c66e52 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.ts @@ -18,6 +18,7 @@ export class AppListPage { recoveredPkgs: readonly RecoveredInfo[] = [] order: readonly string[] = [] reordering = false + loading = true constructor( private readonly api: ApiService, @@ -26,7 +27,9 @@ export class AppListPage { ) {} get empty(): boolean { - return !this.pkgs.length && isEmptyObject(this.recoveredPkgs) + return ( + !this.loading && !this.pkgs.length && isEmptyObject(this.recoveredPkgs) + ) } ngOnInit() { @@ -40,6 +43,7 @@ export class AppListPage { this.pkgs = pkgs this.recoveredPkgs = recoveredPkgs this.order = order + this.loading = false // set order in UI DB if there were unknown packages if (order.length < pkgs.length) { diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts index 1abaf0ad6..6169c8709 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts @@ -12,10 +12,12 @@ export class PackageInfoPipe implements PipeTransform { constructor(private readonly patch: PatchDbService) {} transform(pkg: PackageDataEntry): Observable { - return this.patch.watch$('package-data', pkg.manifest.id).pipe( - filter(v => !!v), - map(getPackageInfo), - startWith(getPackageInfo(pkg)), - ) + return this.patch + .watch$('package-data', pkg.manifest.id) + .pipe( + filter(Boolean), + startWith(pkg), + map(getPackageInfo), + ) } } diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts index 41e9e7c09..e7e58fd88 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts @@ -57,7 +57,7 @@ export class AppMetricsPage { async getMetrics(): Promise { try { this.metrics = await this.embassyApi.getPkgMetrics({ id: this.pkgId }) - } catch (e) { + } catch (e: any) { this.errToast.present(e) this.stopDaemon() } finally { diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts index 8b6e91ca6..0be553ef0 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts @@ -149,7 +149,7 @@ export class AppPropertiesPage { id: this.pkgId, }) this.node = getValueByPointer(this.properties, this.pointer || '') - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { this.loading = false diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts index 75a4d7ffb..5c78f0209 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts @@ -122,7 +122,7 @@ export class AppShowStatusComponent { try { await this.embassyApi.stopPackage({ id }) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() @@ -149,7 +149,7 @@ export class AppShowStatusComponent { try { await this.embassyApi.startPackage({ id: this.pkg.manifest.id }) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts index a9e122067..77a742c80 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts @@ -42,7 +42,7 @@ export class DevConfigPage { let doc: any try { doc = yaml.load(this.code) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } @@ -73,7 +73,7 @@ export class DevConfigPage { pointer: `/dev/${this.projectId}/config`, value: this.code, }) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { this.saving = false diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts index aedc88709..abc38be38 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts @@ -60,7 +60,7 @@ export class DevInstructionsPage { pointer: `/dev/${this.projectId}/instructions`, value: this.code, }) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { this.saving = false diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts index 636fcbb5e..b7f929c84 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts @@ -159,7 +159,7 @@ export class DeveloperListPage { } else { await this.api.setDbValue({ pointer: `/dev`, value: { [id]: def } }) } - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() @@ -197,7 +197,7 @@ export class DeveloperListPage { try { await this.api.setDbValue({ pointer: `/dev/${id}/name`, value: newName }) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() @@ -216,7 +216,7 @@ export class DeveloperListPage { const devDataToSave: DevData = JSON.parse(JSON.stringify(this.devData)) delete devDataToSave[id] await this.api.setDbValue({ pointer: `/dev`, value: devDataToSave }) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts index 16888c82d..48e92c3f5 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts @@ -81,7 +81,7 @@ export class DeveloperMenuPage { pointer: `/dev/${this.projectId}/basic-info`, value: basicInfo, }) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() diff --git a/frontend/projects/ui/src/app/pages/login/login.page.ts b/frontend/projects/ui/src/app/pages/login/login.page.ts index 84e9d872b..696d14556 100644 --- a/frontend/projects/ui/src/app/pages/login/login.page.ts +++ b/frontend/projects/ui/src/app/pages/login/login.page.ts @@ -1,8 +1,8 @@ import { Component } from '@angular/core' import { LoadingController, getPlatforms } from '@ionic/angular' -import { Subscription } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' import { AuthService } from 'src/app/services/auth.service' +import { Router } from '@angular/router' @Component({ selector: 'login', @@ -13,30 +13,24 @@ export class LoginPage { password = '' unmasked = false error = '' - loader: HTMLIonLoadingElement - patchConnectionSub: Subscription + loader?: HTMLIonLoadingElement - constructor ( + constructor( + private readonly router: Router, private readonly authService: AuthService, private readonly loadingCtrl: LoadingController, private readonly api: ApiService, - ) { } + ) {} - ngOnDestroy () { - if (this.loader) { - this.loader.dismiss() - this.loader = undefined - } - if (this.patchConnectionSub) { - this.patchConnectionSub.unsubscribe() - } + ngOnDestroy() { + this.loader?.dismiss() } - toggleMask () { + toggleMask() { this.unmasked = !this.unmasked } - async submit () { + async submit() { this.error = '' this.loader = await this.loadingCtrl.create({ @@ -53,9 +47,11 @@ export class LoginPage { metadata: { platforms: getPlatforms() }, }) - this.authService.setVerified() this.password = '' - } catch (e) { + this.authService + .setVerified() + .then(() => this.router.navigate([''], { replaceUrl: true })) + } catch (e: any) { this.error = e.code === 34 ? 'Invalid Password' : e.message } finally { this.loader.dismiss() diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts index 0ef493279..b37f0d294 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core' import { Observable } from 'rxjs' -import { filter, first, map, startWith, switchMapTo, tap } from 'rxjs/operators' +import { filter, first, map, startWith, switchMapTo } from 'rxjs/operators' import { exists, isEmptyObject } from '@start9labs/shared' import { AbstractMarketplaceService, @@ -9,7 +9,6 @@ import { import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' -import { spreadProgress } from '../utils/spread-progress' @Component({ selector: 'marketplace-list', @@ -20,7 +19,6 @@ export class MarketplaceListPage { .watch$('package-data') .pipe( filter(data => exists(data) && !isEmptyObject(data)), - tap(pkgs => Object.values(pkgs).forEach(spreadProgress)), startWith({}), ) diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts index 2df7ac541..a6a9d23ab 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts @@ -7,11 +7,9 @@ import { } from '@start9labs/marketplace' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' -import { BehaviorSubject, defer, Observable, of } from 'rxjs' +import { BehaviorSubject, Observable, of } from 'rxjs' import { catchError, filter, shareReplay, switchMap, tap } from 'rxjs/operators' -import { spreadProgress } from '../utils/spread-progress' - @Component({ selector: 'marketplace-show', templateUrl: './marketplace-show.page.html', @@ -23,13 +21,12 @@ export class MarketplaceShowPage { readonly loadVersion$ = new BehaviorSubject('*') - readonly localPkg$ = defer(() => - this.patch.watch$('package-data', this.pkgId), - ).pipe( - filter(Boolean), - tap(spreadProgress), - shareReplay({ bufferSize: 1, refCount: true }), - ) + readonly localPkg$ = this.patch + .watch$('package-data', this.pkgId) + .pipe( + filter(Boolean), + shareReplay({ bufferSize: 1, refCount: true }), + ) readonly pkg$: Observable = this.loadVersion$.pipe( switchMap(version => diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/utils/spread-progress.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/utils/spread-progress.ts deleted file mode 100644 index 3c31eb869..000000000 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/utils/spread-progress.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PackageDataEntry } from 'src/app/services/patch-db/data-model' - -export function spreadProgress(pkg: PackageDataEntry) { - pkg['install-progress'] = { ...pkg['install-progress'] } -} diff --git a/frontend/projects/ui/src/app/pages/notifications/notifications.page.html b/frontend/projects/ui/src/app/pages/notifications/notifications.page.html index ac8492fc4..184fc82b7 100644 --- a/frontend/projects/ui/src/app/pages/notifications/notifications.page.html +++ b/frontend/projects/ui/src/app/pages/notifications/notifications.page.html @@ -75,8 +75,8 @@

- {{ patch.data['package-data'][not['package-id']] ? + + {{ patch.data['package-data'][not['package-id']] ? patch.data['package-data'][not['package-id']].manifest.title : not['package-id'] }} - diff --git a/frontend/projects/ui/src/app/pages/notifications/notifications.page.ts b/frontend/projects/ui/src/app/pages/notifications/notifications.page.ts index e7697a827..0fc8de7f2 100644 --- a/frontend/projects/ui/src/app/pages/notifications/notifications.page.ts +++ b/frontend/projects/ui/src/app/pages/notifications/notifications.page.ts @@ -59,7 +59,7 @@ export class NotificationsPage { }) this.beforeCursor = notifications[notifications.length - 1]?.id this.needInfinite = notifications.length >= this.perPage - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { return notifications @@ -78,7 +78,7 @@ export class NotificationsPage { await this.embassyApi.deleteNotification({ id }) this.notifications.splice(index, 1) this.beforeCursor = this.notifications[this.notifications.length - 1]?.id - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() @@ -169,7 +169,7 @@ export class NotificationsPage { }) this.notifications = [] this.beforeCursor = undefined - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() diff --git a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts index 6799663e2..f67bb7f45 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts @@ -147,7 +147,7 @@ export class MarketplacesPage { { 'server-id': this.patch.getData()['server-info'].id }, url, ) - } catch (e) { + } catch (e: any) { this.errToast.present(e) loader.dismiss() return @@ -158,7 +158,7 @@ export class MarketplacesPage { try { marketplace['selected-id'] = id await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace }) - } catch (e) { + } catch (e: any) { this.errToast.present(e) loader.dismiss() } @@ -190,7 +190,7 @@ export class MarketplacesPage { try { delete marketplace['known-hosts'][id] await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace }) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() @@ -223,7 +223,7 @@ export class MarketplacesPage { url, ) marketplace['known-hosts'][id] = { name, url } - } catch (e) { + } catch (e: any) { this.errToast.present(e) loader.dismiss() return @@ -233,7 +233,7 @@ export class MarketplacesPage { try { await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace }) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() @@ -266,7 +266,7 @@ export class MarketplacesPage { ) marketplace['known-hosts'][id] = { name, url } marketplace['selected-id'] = id - } catch (e) { + } catch (e: any) { this.errToast.present(e) loader.dismiss() return @@ -276,7 +276,7 @@ export class MarketplacesPage { try { await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace }) - } catch (e) { + } catch (e: any) { this.errToast.present(e) loader.dismiss() return diff --git a/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.html b/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.html index 8900117c2..a7ccdc317 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.html @@ -7,24 +7,23 @@ - - + + General - + Device Name - {{ patch.data.ui.name || defaultName }} + {{ ui.name || 'Embassy-' + server.id }} Marketplace Auto Check for Updates - {{ patch.data.ui['auto-check-updates'] !== false ? 'Enabled' : - 'Disabled' }} + + {{ ui['auto-check-updates'] !== false ? 'Enabled' : 'Disabled' }} + diff --git a/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.ts b/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.ts index 51a53510d..894a9e048 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/preferences/preferences.page.ts @@ -21,39 +21,37 @@ import { LocalStorageService } from '../../../services/local-storage.service' }) export class PreferencesPage { @ViewChild(IonContent) content: IonContent - defaultName: string clicks = 0 - constructor ( + readonly ui$ = this.patch.watch$('ui') + readonly server$ = this.patch.watch$('server-info') + + constructor( private readonly loadingCtrl: LoadingController, private readonly modalCtrl: ModalController, private readonly api: ApiService, private readonly toastCtrl: ToastController, private readonly localStorageService: LocalStorageService, - public readonly serverConfig: ServerConfigService, - public readonly patch: PatchDbService, - ) { } + private readonly patch: PatchDbService, + readonly serverConfig: ServerConfigService, + ) {} - ngOnInit () { - this.defaultName = `Embassy-${this.patch.getData()['server-info'].id}` - } - - ngAfterViewInit () { + ngAfterViewInit() { this.content.scrollToPoint(undefined, 1) } - async presentModalName (): Promise { + async presentModalName(placeholder: string): Promise { const options: GenericInputOptions = { title: 'Edit Device Name', message: 'This is for your reference only.', label: 'Device Name', useMask: false, - placeholder: this.defaultName, + placeholder, nullable: true, initialValue: this.patch.getData().ui.name, buttonText: 'Save', submitFn: (value: string) => - this.setDbValue('name', value || this.defaultName), + this.setDbValue('name', value || placeholder), } const modal = await this.modalCtrl.create({ @@ -66,7 +64,7 @@ export class PreferencesPage { await modal.present() } - private async setDbValue (key: string, value: string): Promise { + private async setDbValue(key: string, value: string): Promise { const loader = await this.loadingCtrl.create({ spinner: 'lines', message: 'Saving...', @@ -81,7 +79,7 @@ export class PreferencesPage { } } - async addClick () { + async addClick() { this.clicks++ if (this.clicks >= 5) { this.clicks = 0 diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts index 22013b4f3..69413132d 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts @@ -55,7 +55,7 @@ export class ServerMetricsPage { private async getMetrics(): Promise { try { this.metrics = await this.embassyApi.getServerMetrics({}) - } catch (e) { + } catch (e: any) { this.errToast.present(e) this.stopDaemon() } diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html index db5f35281..fde31e3b3 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html @@ -1,12 +1,11 @@ - Loading - {{ patch.data.ui.name || "Embassy-" + patch.data['server-info'].id - }} + + {{ (ui$ | async).name || "Embassy-" + (server$ | async).id }} + + + Loading + @@ -22,18 +21,19 @@ - +
- {{ cat.key }} + + {{ cat.key }} + {{ cat.key }} + {{ cat.key }} +

- + - Last Backup: {{ patch.data['server-info']['last-backup'] ? - (patch.data['server-info']['last-backup'] | date: 'short') : - 'never' }} + Last Backup: {{ server['last-backup'] ? + (server['last-backup'] | date: 'short') : 'never' }} diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts index 2ff3ea691..04de06212 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -15,7 +15,6 @@ import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' import { exists, isEmptyObject, ErrorToastService } from '@start9labs/shared' import { EOSService } from 'src/app/services/eos.service' -import { ServerStatus } from 'src/app/services/patch-db/data-model' import { LocalStorageService } from 'src/app/services/local-storage.service' @Component({ @@ -24,10 +23,12 @@ import { LocalStorageService } from 'src/app/services/local-storage.service' styleUrls: ['server-show.page.scss'], }) export class ServerShowPage { - ServerStatus = ServerStatus hasRecoveredPackage: boolean clicks = 0 + readonly server$ = this.patch.watch$('server-info') + readonly ui$ = this.patch.watch$('ui') + constructor( private readonly alertCtrl: AlertController, private readonly modalCtrl: ModalController, @@ -164,7 +165,7 @@ export class ServerShowPage { this.embassyApi.repairDisk({}).then(_ => { this.restart() }) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } }, @@ -185,7 +186,7 @@ export class ServerShowPage { try { await this.embassyApi.restartServer({}) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() @@ -202,7 +203,7 @@ export class ServerShowPage { try { await this.embassyApi.shutdownServer({}) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() @@ -219,7 +220,7 @@ export class ServerShowPage { try { await this.embassyApi.systemRebuild({}) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() @@ -239,7 +240,7 @@ export class ServerShowPage { if (updateAvailable) { this.updateEos() } - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() @@ -387,7 +388,8 @@ export class ServerShowPage { }, { title: 'Kernel Logs', - description: 'Diagnostic log stream for device drivers and other kernel processes', + description: + 'Diagnostic log stream for device drivers and other kernel processes', icon: 'receipt-outline', action: () => this.navCtrl.navigateForward(['kernel-logs'], { diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html b/frontend/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html index 3ff2e1b1f..3129d7864 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html @@ -8,10 +8,9 @@ - Basic - +

EmbassyOS Version

@@ -47,5 +46,4 @@
- - \ No newline at end of file + diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts index f41b01660..31dc327fc 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts @@ -12,20 +12,23 @@ import { ConfigService } from 'src/app/services/config.service' export class ServerSpecsPage { @ViewChild(IonContent) content: IonContent - constructor ( - private readonly toastCtrl: ToastController, - public readonly patch: PatchDbService, - public readonly config: ConfigService, - ) { } + readonly server$ = this.patch.watch$('server-info') - ngAfterViewInit () { + constructor( + private readonly toastCtrl: ToastController, + private readonly patch: PatchDbService, + public readonly config: ConfigService, + ) {} + + ngAfterViewInit() { this.content.scrollToPoint(undefined, 1) } - async copy (address: string) { + async copy(address: string) { let message = '' - await copyToClipboard(address || '') - .then(success => { message = success ? 'copied to clipboard!' : 'failed to copy'}) + await copyToClipboard(address || '').then(success => { + message = success ? 'copied to clipboard!' : 'failed to copy' + }) const toast = await this.toastCtrl.create({ header: message, @@ -35,7 +38,7 @@ export class ServerSpecsPage { await toast.present() } - asIsOrder (a: any, b: any) { + asIsOrder(a: any, b: any) { return 0 } } diff --git a/frontend/projects/ui/src/app/pages/server-routes/sessions/sessions.page.ts b/frontend/projects/ui/src/app/pages/server-routes/sessions/sessions.page.ts index f8365a6e1..4c489ecad 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/sessions/sessions.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/sessions/sessions.page.ts @@ -23,7 +23,7 @@ export class SessionsPage { async ngOnInit() { try { this.sessionInfo = await this.embassyApi.getSessions({}) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { this.loading = false @@ -62,7 +62,7 @@ export class SessionsPage { try { await this.embassyApi.killSessions({ ids: [id] }) delete this.sessionInfo.sessions[id] - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() diff --git a/frontend/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts b/frontend/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts index a4a77bb86..76a43dcf9 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts @@ -37,7 +37,7 @@ export class SSHKeysPage { async getKeys(): Promise { try { this.sshKeys = await this.embassyApi.getSshKeys({}) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { this.loading = false @@ -111,7 +111,7 @@ export class SSHKeysPage { const entry = this.sshKeys[i] await this.embassyApi.deleteSshKey({ fingerprint: entry.fingerprint }) this.sshKeys.splice(i, 1) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() diff --git a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts index 571e2476c..477b5e317 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts @@ -49,7 +49,7 @@ export class WifiPage { if (!this.wifi.country) { await this.presentAlertCountry() } - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { this.loading = false @@ -179,7 +179,7 @@ export class WifiPage { await this.api.setWifiCountry({ country }) await this.getWifi() this.wifi.country = country - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() @@ -271,7 +271,7 @@ export class WifiPage { try { await this.api.connectWifi({ ssid }) await this.confirmWifi(ssid) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() @@ -290,7 +290,7 @@ export class WifiPage { await this.api.deleteWifi({ ssid }) await this.getWifi() delete this.wifi.ssids[ssid] - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() @@ -313,7 +313,7 @@ export class WifiPage { connect: false, }) await this.getWifi() - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() @@ -337,7 +337,7 @@ export class WifiPage { }) await this.confirmWifi(ssid, true) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() diff --git a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts index d93adbbec..d6c475ae2 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts @@ -266,7 +266,7 @@ export abstract class ApiService implements Source, Http { private syncResponse< T, F extends (...args: any[]) => Promise<{ response: T; revision?: Revision }>, - >(f: F, temp?: Operation): (...args: Parameters) => Promise { + >(f: F, temp?: Operation): (...args: Parameters) => Promise { return (...a) => { // let expireId = undefined // if (temp) { diff --git a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 7dab2033d..1940af73c 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -19,9 +19,23 @@ import { BehaviorSubject } from 'rxjs' import { LocalStorageBootstrap } from '../patch-db/local-storage-bootstrap' import { mockPatchData } from './mock-patch' +const PROGRESS: InstallProgress = { + size: 120, + downloaded: 0, + 'download-complete': false, + validated: 0, + 'validation-complete': false, + unpacked: 0, + 'unpack-complete': false, +} + @Injectable() export class MockApiService extends ApiService { - readonly mockPatch$ = new BehaviorSubject>(undefined) + readonly mockPatch$ = new BehaviorSubject>({ + id: 1, + value: mockPatchData, + expireId: null, + }) private readonly revertTime = 4000 sequence: number @@ -391,8 +405,13 @@ export class MockApiService extends ApiService { await pauseFor(8000) - appPatch[0].value = PackageMainStatus.Stopped - this.updateMock(appPatch) + const newPatch = [ + { + ...appPatch[0], + value: PackageMainStatus.Stopped, + }, + ] + this.updateMock(newPatch) } await pauseFor(1000) @@ -454,32 +473,21 @@ export class MockApiService extends ApiService { params: RR.InstallPackageReq, ): Promise { await pauseFor(2000) - const initialProgress: InstallProgress = { - size: 120, - downloaded: 0, - 'download-complete': false, - validated: 0, - 'validation-complete': false, - unpacked: 0, - 'unpack-complete': false, - } setTimeout(async () => { - this.updateProgress(params.id, initialProgress) + this.updateProgress(params.id) }, 1000) - const pkg: PackageDataEntry = { - ...Mock.LocalPkgs[params.id], - state: PackageState.Installing, - 'install-progress': initialProgress, - installed: undefined, - } - - const patch = [ + const patch: Operation[] = [ { op: PatchOp.ADD, path: `/package-data/${params.id}`, - value: pkg, + value: { + ...Mock.LocalPkgs[params.id], + state: PackageState.Installing, + 'install-progress': { ...PROGRESS }, + installed: undefined, + }, }, ] return this.withRevision(patch) @@ -527,32 +535,20 @@ export class MockApiService extends ApiService { params: RR.RestorePackagesReq, ): Promise { await pauseFor(2000) - const patch: Operation[] = params.ids.map(id => { - const initialProgress: InstallProgress = { - size: 120, - downloaded: 120, - 'download-complete': true, - validated: 0, - 'validation-complete': false, - unpacked: 0, - 'unpack-complete': false, - } - - const pkg: PackageDataEntry = { - ...Mock.LocalPkgs[id], - state: PackageState.Restoring, - 'install-progress': initialProgress, - installed: undefined, - } - + const patch: Operation[] = params.ids.map(id => { setTimeout(async () => { - this.updateProgress(id, initialProgress) + this.updateProgress(id) }, 2000) return { op: PatchOp.ADD, path: `/package-data/${id}`, - value: pkg, + value: { + ...Mock.LocalPkgs[id], + state: PackageState.Restoring, + 'install-progress': { ...PROGRESS }, + installed: undefined, + }, } }) @@ -703,11 +699,11 @@ export class MockApiService extends ApiService { await pauseFor(2000) setTimeout(async () => { - const patch2 = [ + const patch2: RemoveOperation[] = [ { op: PatchOp.REMOVE, path: `/package-data/${params.id}`, - } as RemoveOperation, + }, ] this.updateMock(patch2) }, this.revertTime) @@ -727,11 +723,11 @@ export class MockApiService extends ApiService { params: RR.DeleteRecoveredPackageReq, ): Promise { await pauseFor(2000) - const patch = [ + const patch: RemoveOperation[] = [ { op: PatchOp.REMOVE, path: `/recovered-packages/${params.id}`, - } as RemoveOperation, + }, ] return this.withRevision(patch) } @@ -747,30 +743,30 @@ export class MockApiService extends ApiService { } } - private async updateProgress( - id: string, - initialProgress: InstallProgress, - ): Promise { + private async updateProgress(id: string): Promise { + const progress = { ...PROGRESS } const phases = [ { progress: 'downloaded', completion: 'download-complete' }, { progress: 'validated', completion: 'validation-complete' }, { progress: 'unpacked', completion: 'unpack-complete' }, ] + for (let phase of phases) { - let i = initialProgress[phase.progress] - while (i < initialProgress.size) { + let i = progress[phase.progress] + while (i < progress.size) { await pauseFor(250) - i = Math.min(i + 5, initialProgress.size) - initialProgress[phase.progress] = i - if (i === initialProgress.size) { - initialProgress[phase.completion] = true + i = Math.min(i + 5, progress.size) + progress[phase.progress] = i + + if (i === progress.size) { + progress[phase.completion] = true } const patch = [ { op: PatchOp.REPLACE, path: `/package-data/${id}/install-progress`, - value: initialProgress, + value: { ...progress }, }, ] this.updateMock(patch) @@ -778,7 +774,7 @@ export class MockApiService extends ApiService { } setTimeout(() => { - const patch2: any = [ + const patch2: Operation[] = [ { op: PatchOp.REPLACE, path: `/package-data/${id}`, @@ -822,7 +818,7 @@ export class MockApiService extends ApiService { this.updateMock(patch2) setTimeout(async () => { - const patch3: Operation[] = [ + const patch3: Operation[] = [ { op: PatchOp.REPLACE, path: '/server-info/status', @@ -847,7 +843,7 @@ export class MockApiService extends ApiService { }, 1000) } - private async updateMock(patch: Operation[]): Promise { + private async updateMock(patch: Operation[]): Promise { if (!this.sequence) { const { sequence } = await this.bootstrapper.init() this.sequence = sequence @@ -861,7 +857,7 @@ export class MockApiService extends ApiService { } private async withRevision( - patch: Operation[], + patch: Operation[], response: T = null, ): Promise> { if (!this.sequence) { diff --git a/frontend/projects/ui/src/app/services/auth.service.ts b/frontend/projects/ui/src/app/services/auth.service.ts index 84a5cf308..4e09627ca 100644 --- a/frontend/projects/ui/src/app/services/auth.service.ts +++ b/frontend/projects/ui/src/app/services/auth.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core' -import { BehaviorSubject, Observable } from 'rxjs' -import { distinctUntilChanged } from 'rxjs/operators' +import { Observable, ReplaySubject } from 'rxjs' +import { distinctUntilChanged, map } from 'rxjs/operators' import { Storage } from '@ionic/storage-angular' export enum AuthState { @@ -12,27 +12,29 @@ export enum AuthState { }) export class AuthService { private readonly LOGGED_IN_KEY = 'loggedInKey' - private readonly authState$: BehaviorSubject = new BehaviorSubject(undefined) + private readonly authState$ = new ReplaySubject(1) - constructor ( - private readonly storage: Storage, - ) { } + readonly isVerified$ = this.watch$().pipe( + map(state => state === AuthState.VERIFIED), + ) - async init (): Promise { + constructor(private readonly storage: Storage) {} + + async init(): Promise { const loggedIn = await this.storage.get(this.LOGGED_IN_KEY) - this.authState$.next( loggedIn ? AuthState.VERIFIED : AuthState.UNVERIFIED) + this.authState$.next(loggedIn ? AuthState.VERIFIED : AuthState.UNVERIFIED) } - watch$ (): Observable { + watch$(): Observable { return this.authState$.pipe(distinctUntilChanged()) } - async setVerified (): Promise { + async setVerified(): Promise { await this.storage.set(this.LOGGED_IN_KEY, true) this.authState$.next(AuthState.VERIFIED) } - async setUnverified (): Promise { + async setUnverified(): Promise { this.authState$.next(AuthState.UNVERIFIED) } } diff --git a/frontend/projects/ui/src/app/services/connection.service.ts b/frontend/projects/ui/src/app/services/connection.service.ts index 821de96d4..879fc76e0 100644 --- a/frontend/projects/ui/src/app/services/connection.service.ts +++ b/frontend/projects/ui/src/app/services/connection.service.ts @@ -4,21 +4,34 @@ import { combineLatest, fromEvent, merge, + Observable, + Subject, Subscription, } from 'rxjs' import { PatchConnection, PatchDbService } from './patch-db/patch-db.service' -import { distinctUntilChanged } from 'rxjs/operators' +import { + distinctUntilChanged, + map, + mapTo, + startWith, + tap, +} from 'rxjs/operators' import { ConfigService } from './config.service' @Injectable({ providedIn: 'root', }) export class ConnectionService { - private readonly networkState$ = new BehaviorSubject(true) - private readonly connectionFailure$ = new BehaviorSubject( - ConnectionFailure.None, + private readonly networkState$ = merge( + fromEvent(window, 'online').pipe(mapTo(true)), + fromEvent(window, 'offline').pipe(mapTo(false)), + ).pipe( + startWith(null), + map(() => navigator.onLine), ) + private readonly connectionFailure$ = new Subject() + constructor( private readonly configService: ConfigService, private readonly patch: PatchDbService, @@ -28,15 +41,8 @@ export class ConnectionService { return this.connectionFailure$.asObservable() } - start(): Subscription[] { - const sub1 = merge( - fromEvent(window, 'online'), - fromEvent(window, 'offline'), - ).subscribe(event => { - this.networkState$.next(event.type === 'online') - }) - - const sub2 = combineLatest([ + start(): Observable { + return combineLatest([ // 1 this.networkState$.pipe(distinctUntilChanged()), // 2 @@ -45,20 +51,21 @@ export class ConnectionService { this.patch .watch$('server-info', 'status-info', 'update-progress') .pipe(distinctUntilChanged()), - ]).subscribe(async ([network, patchConnection, progress]) => { - if (!network) { - this.connectionFailure$.next(ConnectionFailure.Network) - } else if (patchConnection !== PatchConnection.Disconnected) { - this.connectionFailure$.next(ConnectionFailure.None) - } else if (!!progress && progress.downloaded === progress.size) { - this.connectionFailure$.next(ConnectionFailure.None) - } else if (!this.configService.isTor()) { - this.connectionFailure$.next(ConnectionFailure.Lan) - } else { - this.connectionFailure$.next(ConnectionFailure.Tor) - } - }) - return [sub1, sub2] + ]).pipe( + tap(([network, patchConnection, progress]) => { + if (!network) { + this.connectionFailure$.next(ConnectionFailure.Network) + } else if (patchConnection !== PatchConnection.Disconnected) { + this.connectionFailure$.next(ConnectionFailure.None) + } else if (!!progress && progress.downloaded === progress.size) { + this.connectionFailure$.next(ConnectionFailure.None) + } else if (!this.configService.isTor()) { + this.connectionFailure$.next(ConnectionFailure.Lan) + } else { + this.connectionFailure$.next(ConnectionFailure.Tor) + } + }), + ) } } diff --git a/frontend/projects/ui/src/app/services/form.service.ts b/frontend/projects/ui/src/app/services/form.service.ts index 77819ea15..b9265aaf3 100644 --- a/frontend/projects/ui/src/app/services/form.service.ts +++ b/frontend/projects/ui/src/app/services/form.service.ts @@ -1,11 +1,9 @@ import { Injectable } from '@angular/core' import { - AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, - ValidationErrors, ValidatorFn, Validators, } from '@angular/forms' @@ -69,7 +67,7 @@ export class FormService { } getListItem(spec: ValueSpecList, entry: any) { - const listItemValidators = this.getListItemValidators(spec) + const listItemValidators = getListItemValidators(spec) if (isValueSpecListOf(spec, 'string')) { return this.formBuilder.control(entry, listItemValidators) } else if (isValueSpecListOf(spec, 'number')) { @@ -83,14 +81,6 @@ export class FormService { } } - private getListItemValidators(spec: ValueSpecList) { - if (isValueSpecListOf(spec, 'string')) { - return this.stringValidators(spec.spec) - } else if (isValueSpecListOf(spec, 'number')) { - return this.numberValidators(spec.spec) - } - } - private getFormGroup( config: ConfigSpec, validators: ValidatorFn[] = [], @@ -112,7 +102,7 @@ export class FormService { let value: any switch (spec.type) { case 'string': - validators = this.stringValidators(spec) + validators = stringValidators(spec) if (currentValue !== undefined) { value = currentValue } else { @@ -120,7 +110,7 @@ export class FormService { } return this.formBuilder.control(value, validators) case 'number': - validators = this.numberValidators(spec) + validators = numberValidators(spec) if (currentValue !== undefined) { value = currentValue } else { @@ -130,7 +120,7 @@ export class FormService { case 'object': return this.getFormGroup(spec.spec, [], currentValue) case 'list': - validators = this.listValidators(spec) + validators = listValidators(spec) const mapped = ( Array.isArray(currentValue) ? currentValue : (spec.default as any[]) ).map(entry => { @@ -149,56 +139,64 @@ export class FormService { return this.formBuilder.control(value) } } +} - private stringValidators( - spec: ValueSpecString | ListValueSpecString, - ): ValidatorFn[] { - const validators: ValidatorFn[] = [] +function getListItemValidators(spec: ValueSpecList) { + if (isValueSpecListOf(spec, 'string')) { + return stringValidators(spec.spec) + } else if (isValueSpecListOf(spec, 'number')) { + return numberValidators(spec.spec) + } +} - if (!(spec as ValueSpecString).nullable) { - validators.push(Validators.required) - } +function stringValidators( + spec: ValueSpecString | ListValueSpecString, +): ValidatorFn[] { + const validators: ValidatorFn[] = [] - if (spec.pattern) { - validators.push(Validators.pattern(spec.pattern)) - } - - return validators + if (!(spec as ValueSpecString).nullable) { + validators.push(Validators.required) } - private numberValidators( - spec: ValueSpecNumber | ListValueSpecNumber, - ): ValidatorFn[] { - const validators: ValidatorFn[] = [] - - validators.push(isNumber()) - - if (!(spec as ValueSpecNumber).nullable) { - validators.push(Validators.required) - } - - if (spec.integral) { - validators.push(isInteger()) - } - - validators.push(numberInRange(spec.range)) - - return validators + if (spec.pattern) { + validators.push(Validators.pattern(spec.pattern)) } - private listValidators(spec: ValueSpecList): ValidatorFn[] { - const validators: ValidatorFn[] = [] + return validators +} - validators.push(listInRange(spec.range)) +function numberValidators( + spec: ValueSpecNumber | ListValueSpecNumber, +): ValidatorFn[] { + const validators: ValidatorFn[] = [] - validators.push(listItemIssue()) + validators.push(isNumber()) - if (!isValueSpecListOf(spec, 'enum')) { - validators.push(listUnique(spec)) - } - - return validators + if (!(spec as ValueSpecNumber).nullable) { + validators.push(Validators.required) } + + if (spec.integral) { + validators.push(isInteger()) + } + + validators.push(numberInRange(spec.range)) + + return validators +} + +function listValidators(spec: ValueSpecList): ValidatorFn[] { + const validators: ValidatorFn[] = [] + + validators.push(listInRange(spec.range)) + + validators.push(listItemIssue()) + + if (!isValueSpecListOf(spec, 'enum')) { + validators.push(listUnique(spec)) + } + + return validators } function isFullUnion( @@ -208,48 +206,47 @@ function isFullUnion( } export function numberInRange(stringRange: string): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { + return control => { const value = control.value if (!value) return null try { Range.from(stringRange).checkIncludes(value) return null - } catch (e) { + } catch (e: any) { return { numberNotInRange: { value: `Number must be ${e.message}` } } } } } export function isNumber(): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - return !control.value || control.value == Number(control.value) + return control => + !control.value || control.value == Number(control.value) ? null : { notNumber: { value: control.value } } - } } export function isInteger(): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - return !control.value || control.value == Math.trunc(control.value) + return control => + !control.value || control.value == Math.trunc(control.value) ? null : { numberNotInteger: { value: control.value } } - } } export function listInRange(stringRange: string): ValidatorFn { - return (control: FormArray): ValidationErrors | null => { + return control => { try { Range.from(stringRange).checkIncludes(control.value.length) return null - } catch (e) { + } catch (e: any) { return { listNotInRange: { value: `List must be ${e.message}` } } } } } export function listItemIssue(): ValidatorFn { - return (parentControl: FormArray): ValidationErrors | null => { - const problemChild = parentControl.controls.find(c => c.invalid) + return parentControl => { + const { controls } = parentControl as FormArray + const problemChild = controls.find(c => c.invalid) if (problemChild) { return { listItemIssue: { value: 'Invalid entries' } } } else { @@ -259,7 +256,7 @@ export function listItemIssue(): ValidatorFn { } export function listUnique(spec: ValueSpecList): ValidatorFn { - return (control: FormArray): ValidationErrors | null => { + return control => { const list = control.value for (let idx = 0; idx < list.length; idx++) { for (let idx2 = idx + 1; idx2 < list.length; idx2++) { @@ -516,25 +513,29 @@ export function convertValuesRecursive( convertValuesRecursive(spec, control) } else if (valueSpec.type === 'list') { const formArr = group.get(key) as FormArray + const { controls } = formArr + if (valueSpec.subtype === 'number') { - formArr.controls.forEach(control => { + controls.forEach(control => { control.setValue(control.value ? Number(control.value) : null) }) } else if (valueSpec.subtype === 'string') { - formArr.controls.forEach(control => { + controls.forEach(control => { if (!control.value) control.setValue(null) }) } else if (valueSpec.subtype === 'object') { - formArr.controls.forEach((formGroup: FormGroup) => { + controls.forEach(formGroup => { const objectSpec = valueSpec.spec as ListValueSpecObject - convertValuesRecursive(objectSpec.spec, formGroup) + convertValuesRecursive(objectSpec.spec, formGroup as FormGroup) }) } else if (valueSpec.subtype === 'union') { - formArr.controls.forEach((formGroup: FormGroup) => { + controls.forEach(formGroup => { const unionSpec = valueSpec.spec as ListValueSpecUnion const spec = - unionSpec.variants[formGroup.controls[unionSpec.tag.id].value] - convertValuesRecursive(spec, formGroup) + unionSpec.variants[ + (formGroup as FormGroup).controls[unionSpec.tag.id].value + ] + convertValuesRecursive(spec, formGroup as FormGroup) }) } } diff --git a/frontend/projects/ui/src/app/services/http.service.ts b/frontend/projects/ui/src/app/services/http.service.ts index 89d3721bf..f10cb8e8b 100644 --- a/frontend/projects/ui/src/app/services/http.service.ts +++ b/frontend/projects/ui/src/app/services/http.service.ts @@ -10,6 +10,7 @@ import { map, take } from 'rxjs/operators' import { ConfigService } from './config.service' import { Revision } from 'patch-db-client' import { AuthService } from './auth.service' +import { HttpError, RpcError } from '@start9labs/shared' @Injectable({ providedIn: 'root', @@ -98,33 +99,6 @@ export class HttpService { } } -function RpcError(e: RPCError['error']): void { - const { code, message, data } = e - - this.code = code - - if (typeof data === 'string') { - this.message = `${message}\n\n${data}` - this.revision = null - } else { - if (data.details) { - this.message = `${message}\n\n${data.details}` - } else { - this.message = message - } - this.revision = data.revision - } -} - -function HttpError(e: HttpErrorResponse): void { - const { status, statusText } = e - - this.code = status - this.message = statusText - this.details = null - this.revision = null -} - function isRpcError( arg: { error: Error } | { result: Result }, ): arg is { error: Error } { @@ -188,10 +162,6 @@ export interface RPCError extends RPCBase { export type RPCResponse = RPCSuccess | RPCError -type HttpError = HttpErrorResponse & { - error: { code: string; message: string } -} - export interface HttpOptions { method: Method url: string diff --git a/frontend/projects/ui/src/app/services/local-storage.service.ts b/frontend/projects/ui/src/app/services/local-storage.service.ts index 0b728ead4..4b73ff083 100644 --- a/frontend/projects/ui/src/app/services/local-storage.service.ts +++ b/frontend/projects/ui/src/app/services/local-storage.service.ts @@ -8,8 +8,9 @@ const SHOW_DISK_REPAIR = 'SHOW_DISK_REPAIR' providedIn: 'root', }) export class LocalStorageService { - showDevTools$: BehaviorSubject = new BehaviorSubject(false) - showDiskRepair$: BehaviorSubject = new BehaviorSubject(false) + readonly showDevTools$ = new BehaviorSubject(false) + readonly showDiskRepair$ = new BehaviorSubject(false) + constructor(private readonly storage: Storage) {} async init() { diff --git a/frontend/projects/ui/src/app/services/marketplace.service.ts b/frontend/projects/ui/src/app/services/marketplace.service.ts index 43225273e..01cbed35f 100644 --- a/frontend/projects/ui/src/app/services/marketplace.service.ts +++ b/frontend/projects/ui/src/app/services/marketplace.service.ts @@ -27,16 +27,16 @@ import { export class MarketplaceService extends AbstractMarketplaceService { private readonly notes = new Map>() - private readonly init$: Observable = defer(() => - this.patch.watch$('ui', 'marketplace'), - ).pipe( - map(marketplace => - marketplace?.['selected-id'] - ? marketplace['known-hosts'][marketplace['selected-id']] - : this.config.marketplace, - ), - shareReplay(), - ) + private readonly init$: Observable = this.patch + .watch$('ui', 'marketplace') + .pipe( + map(marketplace => + marketplace?.['selected-id'] + ? marketplace['known-hosts'][marketplace['selected-id']] + : this.config.marketplace, + ), + shareReplay(), + ) private readonly data$: Observable = this.init$.pipe( switchMap(({ url }) => diff --git a/frontend/projects/ui/src/app/services/patch-db/patch-db.factory.ts b/frontend/projects/ui/src/app/services/patch-db/patch-db.factory.ts index 0e0d326c3..0a3709208 100644 --- a/frontend/projects/ui/src/app/services/patch-db/patch-db.factory.ts +++ b/frontend/projects/ui/src/app/services/patch-db/patch-db.factory.ts @@ -1,54 +1,46 @@ -import { MockSource, PollSource, WebsocketSource } from 'patch-db-client' -import { ConfigService } from 'src/app/services/config.service' -import { LocalStorageBootstrap } from './local-storage-bootstrap' -import { PatchDbService } from './patch-db.service' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { AuthService } from '../auth.service' -import { MockApiService } from '../api/embassy-mock-api.service' -import { filter } from 'rxjs/operators' +import { inject, InjectionToken } from '@angular/core' import { exists } from '@start9labs/shared' -import { DataModel } from 'src/app/services/patch-db/data-model' -import { Storage } from '@ionic/storage-angular' +import { filter } from 'rxjs/operators' +import { + Bootstrapper, + MockSource, + PollSource, + Source, + WebsocketSource, +} from 'patch-db-client' -export function PatchDbServiceFactory( - config: ConfigService, - embassyApi: ApiService, - bootstrapper: LocalStorageBootstrap, - auth: AuthService, - storage: Storage, -): PatchDbService { - const { - useMocks, - patchDb: { poll }, - } = config +import { ConfigService } from '../config.service' +import { LocalStorageBootstrap } from './local-storage-bootstrap' +import { ApiService } from '../api/embassy-api.service' +import { MockApiService } from '../api/embassy-mock-api.service' +import { DataModel } from './data-model' - if (useMocks) { - const source = new MockSource( - (embassyApi as MockApiService).mockPatch$.pipe(filter(exists)), - ) - return new PatchDbService( - source, - source, - embassyApi, - bootstrapper, - auth, - storage, - ) - } else { - const protocol = window.location.protocol === 'http:' ? 'ws' : 'wss' - const host = window.location.host - const wsSource = new WebsocketSource( - `${protocol}://${host}/ws/db`, - ) - const pollSource = new PollSource({ ...poll }, embassyApi) +export const PATCH_SOURCE = new InjectionToken[]>( + '[wsSources, pollSources]', +) +export const BOOTSTRAPPER = new InjectionToken>('', { + factory: () => inject(LocalStorageBootstrap), +}) - return new PatchDbService( - wsSource, - pollSource, - embassyApi, - bootstrapper, - auth, - storage, - ) - } +export function mockSourceFactory({ + mockPatch$, +}: MockApiService): Source[] { + return Array(2).fill( + new MockSource(mockPatch$.pipe(filter(exists))), + ) +} + +export function realSourceFactory( + embassyApi: ApiService, + config: ConfigService, + { defaultView }: Document, +): Source[] { + const { patchDb } = config + const { host } = defaultView.location + const protocol = defaultView.location.protocol === 'http:' ? 'ws' : 'wss' + + return [ + new WebsocketSource(`${protocol}://${host}/ws/db`), + new PollSource({ ...patchDb.poll }, embassyApi), + ] } diff --git a/frontend/projects/ui/src/app/services/patch-db/patch-db.service.ts b/frontend/projects/ui/src/app/services/patch-db/patch-db.service.ts index 7ece20432..15fe98463 100644 --- a/frontend/projects/ui/src/app/services/patch-db/patch-db.service.ts +++ b/frontend/projects/ui/src/app/services/patch-db/patch-db.service.ts @@ -1,13 +1,19 @@ -import { Inject, Injectable, InjectionToken } from '@angular/core' +import { Inject, Injectable } from '@angular/core' import { Storage } from '@ionic/storage-angular' import { Bootstrapper, PatchDB, Source, Store } from 'patch-db-client' -import { BehaviorSubject, Observable, of, Subscription } from 'rxjs' +import { + BehaviorSubject, + Observable, + of, + ReplaySubject, + Subscription, +} from 'rxjs' import { catchError, debounceTime, + filter, finalize, mergeMap, - skip, switchMap, take, tap, @@ -17,13 +23,7 @@ import { isEmptyObject, pauseFor } from '@start9labs/shared' import { DataModel } from './data-model' import { ApiService } from '../api/embassy-api.service' import { AuthService } from '../auth.service' -import { patch } from '@start9labs/emver' - -export const PATCH_HTTP = new InjectionToken>('') -export const PATCH_SOURCE = new InjectionToken>('') -export const BOOTSTRAPPER = new InjectionToken>('') -export const AUTH = new InjectionToken('') -export const STORAGE = new InjectionToken('') +import { BOOTSTRAPPER, PATCH_SOURCE } from './patch-db.factory' export enum PatchConnection { Initializing = 'initializing', @@ -36,13 +36,13 @@ export enum PatchConnection { }) export class PatchDbService { private readonly WS_SUCCESS = 'wsSuccess' - private patchConnection$ = new BehaviorSubject(PatchConnection.Initializing) + private patchConnection$ = new ReplaySubject(1) private wsSuccess$ = new BehaviorSubject(false) private polling$ = new BehaviorSubject(false) private patchDb: PatchDB private subs: Subscription[] = [] private sources$: BehaviorSubject[]> = new BehaviorSubject([ - this.wsSource, + this.sources[0], ]) data: DataModel @@ -61,18 +61,18 @@ export class PatchDbService { } constructor( - @Inject(PATCH_SOURCE) private readonly wsSource: Source, - @Inject(PATCH_SOURCE) private readonly pollSource: Source, - @Inject(PATCH_HTTP) private readonly http: ApiService, + // [wsSources, pollSources] + @Inject(PATCH_SOURCE) private readonly sources: Source[], @Inject(BOOTSTRAPPER) private readonly bootstrapper: Bootstrapper, - @Inject(AUTH) private readonly auth: AuthService, - @Inject(STORAGE) private readonly storage: Storage, + private readonly http: ApiService, + private readonly auth: AuthService, + private readonly storage: Storage, ) {} async init(): Promise { const cache = await this.bootstrapper.init() - this.sources$.next([this.wsSource, this.http]) + this.sources$.next([this.sources[0], this.http]) this.patchDb = new PatchDB(this.sources$, this.http, cache) @@ -94,13 +94,13 @@ export class PatchDbService { console.log('patchDB: POLLING FAILED', e) this.patchConnection$.next(PatchConnection.Disconnected) await pauseFor(2000) - this.sources$.next([this.pollSource, this.http]) + this.sources$.next([this.sources[1], this.http]) return } console.log('patchDB: WEBSOCKET FAILED', e) this.polling$.next(true) - this.sources$.next([this.pollSource, this.http]) + this.sources$.next([this.sources[1], this.http]) }), ) .subscribe({ @@ -152,7 +152,7 @@ export class PatchDbService { console.log('patchDB: SWITCHING BACK TO WEBSOCKETS') this.patchConnection$.next(PatchConnection.Initializing) this.polling$.next(false) - this.sources$.next([this.wsSource, this.http]) + this.sources$.next([this.sources[0], this.http]) } }), ) @@ -180,19 +180,14 @@ export class PatchDbService { // prettier-ignore watch$: Store['watch$'] = (...args: (string | number)[]): Observable => { - // TODO: refactor with a better solution to race condition const argsString = '/' + args.join('/') - const source$ = - this.patchDb?.store.watch$(...(args as [])) || - this.patchConnection$.pipe( - skip(1), - take(1), - switchMap(() => this.patchDb.store.watch$(...(args as []))), - ) console.log('patchDB: WATCHING ', argsString) - return source$.pipe( + return this.patchConnection$.pipe( + filter(status => status === PatchConnection.Connected), + take(1), + switchMap(() => this.patchDb.store.watch$(...(args as []))), tap(data => console.log('patchDB: NEW VALUE', argsString, data)), catchError(e => { console.error('patchDB: WATCH ERROR', e) diff --git a/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts b/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts index fcb5f6f8a..9a233dd65 100644 --- a/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts +++ b/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts @@ -47,18 +47,25 @@ function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus { } function getHealthStatus(status: Status): HealthStatus { - if (status.main.status === PackageMainStatus.Running) { - const values = Object.values(status.main.health) - if (values.some(h => h.result === 'failure')) { - return HealthStatus.Failure - } else if (values.some(h => h.result === 'starting')) { - return HealthStatus.Starting - } else if (values.some(h => h.result === 'loading')) { - return HealthStatus.Loading - } else { - return HealthStatus.Healthy - } + if (status.main.status !== PackageMainStatus.Running || !status.main.health) { + return } + + const values = Object.values(status.main.health) + + if (values.some(h => h.result === 'failure')) { + return HealthStatus.Failure + } + + if (values.some(h => h.result === 'starting')) { + return HealthStatus.Starting + } + + if (values.some(h => h.result === 'loading')) { + return HealthStatus.Loading + } + + return HealthStatus.Healthy } export interface StatusRendering { diff --git a/frontend/projects/ui/src/app/services/server-config.service.ts b/frontend/projects/ui/src/app/services/server-config.service.ts index 9793b5f76..788c9ca12 100644 --- a/frontend/projects/ui/src/app/services/server-config.service.ts +++ b/frontend/projects/ui/src/app/services/server-config.service.ts @@ -37,7 +37,7 @@ export class ServerConfigService { try { await this.saveFns[key](data) - } catch (e) { + } catch (e: any) { this.errToast.present(e) } finally { loader.dismiss() diff --git a/frontend/projects/ui/src/app/services/split-pane.service.ts b/frontend/projects/ui/src/app/services/split-pane.service.ts index 51b1325cd..520495442 100644 --- a/frontend/projects/ui/src/app/services/split-pane.service.ts +++ b/frontend/projects/ui/src/app/services/split-pane.service.ts @@ -5,5 +5,5 @@ import { Injectable } from '@angular/core' providedIn: 'root', }) export class SplitPaneTracker { - sidebarOpen$: BehaviorSubject = new BehaviorSubject(false) -} \ No newline at end of file + readonly sidebarOpen$ = new BehaviorSubject(false) +} diff --git a/frontend/projects/ui/src/app/types/install-progress.ts b/frontend/projects/ui/src/app/types/install-progress.ts index 432d63de9..58b1f6286 100644 --- a/frontend/projects/ui/src/app/types/install-progress.ts +++ b/frontend/projects/ui/src/app/types/install-progress.ts @@ -1,9 +1,9 @@ export interface InstallProgress { - size: number | null - downloaded: number - 'download-complete': boolean - validated: number - 'validation-complete': boolean - unpacked: number - 'unpack-complete': boolean + readonly size: number | null + readonly downloaded: number + readonly 'download-complete': boolean + readonly validated: number + readonly 'validation-complete': boolean + readonly unpacked: number + readonly 'unpack-complete': boolean } diff --git a/frontend/projects/ui/src/app/util/rxjs.util.ts b/frontend/projects/ui/src/app/util/rxjs.util.ts index 020144d5a..449e62d87 100644 --- a/frontend/projects/ui/src/app/util/rxjs.util.ts +++ b/frontend/projects/ui/src/app/util/rxjs.util.ts @@ -1,31 +1,54 @@ -import { Observable, from, interval, race, OperatorFunction, Observer, combineLatest } from 'rxjs' -import { take, map, concatMap } from 'rxjs/operators' +import { + Observable, + from, + interval, + race, + OperatorFunction, + Observer, + combineLatest, +} from 'rxjs' +import { take, map } from 'rxjs/operators' -export function fromAsync$ (async: (s: S) => Promise, s: S): Observable -export function fromAsync$ (async: () => Promise): Observable -export function fromAsync$ (async: (s: S) => Promise, s?: S): Observable { +export function fromAsync$( + async: (s: S) => Promise, + s: S, +): Observable +export function fromAsync$(async: () => Promise): Observable +export function fromAsync$( + async: (s: S) => Promise, + s?: S, +): Observable { return from(async(s as S)) } -export function fromAsyncP (async: () => Promise): Promise -export function fromAsyncP (async: (s: S) => Promise, s?: S): Promise { +export function fromAsyncP(async: () => Promise): Promise +export function fromAsyncP( + async: (s: S) => Promise, + s?: S, +): Promise { return async(s as S) } // emits + completes after ms -export function emitAfter$ (ms: number): Observable { +export function emitAfter$(ms: number): Observable { return interval(ms).pipe(take(1)) } // throws unless source observable emits withing timeout -export function throwIn (timeout: number): OperatorFunction { - return o => race( - o, - emitAfter$(timeout).pipe(map(() => { throw new Error('timeout') } ))) +export function throwIn(timeout: number): OperatorFunction { + return o => + race( + o, + emitAfter$(timeout).pipe( + map(() => { + throw new Error('timeout') + }), + ), + ) } // o.pipe(squash) : Observable regardless of o emission type. -export const squash = map(() => { }) +export const squash = map(() => {}) /* The main purpose of fromSync$ is to normalize error handling during a sequence @@ -59,10 +82,10 @@ export const squash = map(() => { }) } ``` */ -export function fromSync$ (sync: (s: S) => T, s: S): Observable -export function fromSync$ (sync: () => T): Observable -export function fromSync$ (sync: (s: S) => T, s?: S): Observable { - return new Observable( (subscriber: Observer) => { +export function fromSync$(sync: (s: S) => T, s: S): Observable +export function fromSync$(sync: () => T): Observable +export function fromSync$(sync: (s: S) => T, s?: S): Observable { + return new Observable((subscriber: Observer) => { try { subscriber.next(sync(s as S)) subscriber.complete() @@ -71,24 +94,3 @@ export function fromSync$ (sync: (s: S) => T, s?: S): Observable { } }) } - -/* - this function concats the current values (e.g in behavior subjects) or next values (in traditional observables) to a collection of values in a pipe. - e.g. if t: Observable, and o1: Observable o2: Observable then t.pipe(concatObservableValues([o1, o2])): Observable<[T1, O1, O2]> and emits iff t emits. - Note that the standard combineLatest([t, o1, o2]) is also of type Observable<[T, O2, O2]>, but this observable triggers when any of t, o1, o2 emits. -*/ -export function concatObservableValues (observables: [Observable]): OperatorFunction -export function concatObservableValues (observables: [Observable]): OperatorFunction<[T], [T, O]> -export function concatObservableValues (observables: [Observable]): OperatorFunction<[T1, T2], [T1, T2, O]> -export function concatObservableValues (observables: [Observable, Observable]): OperatorFunction<[T], [T, O1, O2]> -export function concatObservableValues (observables: [Observable, Observable]): OperatorFunction<[T1, T2], [T1, T2, O1, O2]> -export function concatObservableValues (observables: Observable[]): OperatorFunction { - return o => o.pipe(concatMap(args => combineLatest(observables).pipe( - map(obs => { - if (!(args instanceof Array)) return [args, ...obs] - return [...args, ...obs] - }), - take(1), - ), - )) -} \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index aac9e859b..0355e72f3 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -8,6 +8,16 @@ "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noFallthroughCasesInSwitch": true, + + "alwaysStrict": true, + // "strictNullChecks": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + // "strictPropertyInitialization": true, + // "noImplicitAny": true, + "noImplicitThis": true, + "useUnknownInCatchVariables": true, + "sourceMap": true, "declaration": false, "downlevelIteration": true, diff --git a/patch-db b/patch-db index 35973d7ae..6f6c26acd 160000 --- a/patch-db +++ b/patch-db @@ -1 +1 @@ -Subproject commit 35973d7aef054842faa13c82f357252563108949 +Subproject commit 6f6c26acd440bc6dfb007aa8f33b9a572756c8fb