From 0390954a85ad24b0a54f39d339977a41b8ec8fab Mon Sep 17 00:00:00 2001 From: waterplea Date: Thu, 26 May 2022 18:20:31 +0300 Subject: [PATCH] feat: enable `strictNullChecks` feat: enable `noImplicitAny` chore: remove sync data access fix loading package data for affected dependencies chore: properly get alt marketplace data update patchdb client to allow for emit on undefined values --- .../src/app/pages/logs/logs.page.ts | 66 +++++---- .../src/app/services/api/mock-api.service.ts | 3 - .../src/app/services/http.service.ts | 4 +- .../release-notes/release-notes.component.ts | 3 +- .../show/additional/additional.component.ts | 2 +- .../src/pipes/filter-packages.pipe.ts | 2 +- .../src/services/marketplace.service.ts | 5 +- .../src/app/modals/password/password.page.ts | 34 +++-- .../prod-key-modal/prod-key-modal.page.ts | 12 +- .../src/app/pages/recover/recover.page.ts | 22 +-- .../src/app/pages/success/success.page.ts | 16 ++- .../src/app/services/api/http.service.ts | 16 ++- .../src/app/services/api/live-api.service.ts | 4 +- .../src/app/services/state.service.ts | 2 +- frontend/projects/shared/package.json | 1 + .../projects/shared/src/classes/rpc-error.ts | 4 +- .../shared/src/pipes/emver/emver.pipe.ts | 8 +- .../unit-conversion/unit-conversion.pipe.ts | 9 +- frontend/projects/shared/src/public-api.ts | 1 + .../shared/src/services/emver.service.ts | 2 +- .../src/services/error-toast.service.ts | 2 +- .../projects/shared/src/util/get-pkg-id.ts | 11 ++ .../projects/shared/src/util/misc.util.ts | 2 +- frontend/projects/shared/src/util/unused.ts | 45 +----- .../ui/src/app/app/footer/footer.component.ts | 2 +- .../app/global/services/offline.service.ts | 2 + .../global/services/patch-monitor.service.ts | 2 - .../global/services/unread-toast.service.ts | 2 +- .../global/services/update-toast.service.ts | 2 +- .../ui/src/app/app/menu/menu.component.html | 6 +- .../form-object/form-error.component.html | 14 +- .../form-object/form-object.component.html | 22 ++- .../form-object/form-object.component.ts | 32 +++-- .../dependents/dependents.component.html | 9 +- .../dependents/dependents.component.ts | 3 + .../install-wizard/prebaked-wizards.ts | 4 +- .../ui/src/app/components/logs/logs.page.ts | 28 ++-- .../app/components/status/status.component.ts | 2 +- .../modals/app-config/app-config.page.html | 2 +- .../app/modals/app-config/app-config.page.ts | 17 +-- .../modals/generic-form/generic-form.page.ts | 2 +- .../generic-input/generic-input.component.ts | 2 +- .../ui/src/app/modals/snake/snake.page.ts | 31 +++-- .../app-actions/app-actions.page.ts | 15 +- .../app-interfaces/app-interfaces.page.ts | 14 +- .../app-list-pkg/app-list-pkg.component.ts | 4 +- .../app-list-reorder.component.html | 26 ++-- .../apps-routes/app-logs/app-logs.page.ts | 19 +-- .../app-metrics/app-metrics.page.ts | 5 +- .../app-properties/app-properties.page.ts | 8 +- .../apps-routes/app-show/app-show.page.html | 6 +- .../apps-routes/app-show/app-show.page.ts | 3 +- .../app-show-progress.component.ts | 2 +- .../app-show-status.component.html | 4 +- .../app-show-status.component.ts | 9 +- .../app-show/pipes/to-buttons.pipe.ts | 2 +- .../app-show/pipes/to-dependencies.pipe.ts | 8 +- .../dev-config/dev-config.page.ts | 11 +- .../dev-instructions/dev-instructions.page.ts | 10 +- .../dev-manifest/dev-manifest.page.ts | 5 +- .../developer-list/developer-list.page.ts | 4 - .../developer-menu/developer-menu.page.html | 2 +- .../developer-menu/developer-menu.page.ts | 18 +-- .../marketplace-list.page.html | 4 +- .../marketplace-show-controls.component.ts | 4 +- .../marketplace-show.page.html | 4 +- .../marketplace-show/marketplace-show.page.ts | 14 +- .../install-progress.pipe.ts | 4 +- .../release-notes/release-notes.page.ts | 3 +- .../pages/notifications/notifications.page.ts | 14 +- .../app/pages/server-routes/lan/lan.page.html | 2 +- .../app/pages/server-routes/lan/lan.page.ts | 9 +- .../marketplaces/marketplaces.page.ts | 79 +++++++---- .../restore/restore.component.ts | 3 +- .../server-backup/server-backup.page.ts | 29 ++-- .../server-logs/server-logs.page.ts | 22 ++- .../server-metrics/server-metrics.page.ts | 2 +- .../server-show/server-show.page.html | 2 +- .../app/pages/server-routes/wifi/wifi.page.ts | 2 +- .../ui/src/app/pkg-config/config-utilities.ts | 6 +- .../ui/src/app/services/api/api.fixures.ts | 7 - .../ui/src/app/services/api/api.types.ts | 2 +- .../services/api/embassy-mock-api.service.ts | 11 +- .../ui/src/app/services/api/mock-patch.ts | 10 +- .../ui/src/app/services/config.service.ts | 26 ++-- .../ui/src/app/services/form.service.ts | 67 +++++---- .../ui/src/app/services/http.service.ts | 27 ++-- .../src/app/services/marketplace.service.ts | 131 ++++++++++++------ .../src/app/services/patch-db/data-model.ts | 26 ++-- .../app/services/patch-db/patch-db.factory.ts | 4 +- .../services/pkg-status-rendering.service.ts | 11 +- .../src/app/services/server-config.service.ts | 7 +- .../src/app/services/ui-launcher.service.ts | 2 +- .../ui/src/app/util/get-project-id.ts | 11 ++ .../src/app/util/package-loading-progress.ts | 5 +- .../ui/src/app/util/parse-data-model.ts | 8 +- .../ui/src/app/util/properties.util.ts | 9 +- frontend/tsconfig.json | 5 +- patch-db | 2 +- 99 files changed, 674 insertions(+), 535 deletions(-) create mode 100644 frontend/projects/shared/src/util/get-pkg-id.ts create mode 100644 frontend/projects/ui/src/app/util/get-project-id.ts diff --git a/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts b/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts index ff7f08738..49687c48d 100644 --- a/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts +++ b/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts @@ -23,40 +23,47 @@ export class LogsPage { scrollToBottomButton = false isOnBottom = true - constructor ( - private readonly api: ApiService, - ) { } + constructor(private readonly api: ApiService) {} - ngOnInit () { + ngOnInit() { this.getLogs() } - async getLogs () { + async getLogs() { try { // get logs const logs = await this.fetch() - if (!logs.length) return + + if (!logs?.length) return const container = document.getElementById('container') - const beforeContainerHeight = container.scrollHeight - const newLogs = document.getElementById('template').cloneNode(true) as HTMLElement - newLogs.innerHTML = logs.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`).join('\n') + (logs.length ? '\n' : '') + const beforeContainerHeight = container?.scrollHeight || 0 + const newLogs = document.getElementById('template')?.cloneNode(true) - container.prepend(newLogs) - const afterContainerHeight = container.scrollHeight + if (!(newLogs instanceof HTMLElement)) return + + newLogs.innerHTML = + logs + .map(l => `${l.timestamp} ${convert.toHtml(l.message)}`) + .join('\n') + (logs.length ? '\n' : '') + container?.prepend(newLogs) + + const afterContainerHeight = container?.scrollHeight || 0 // scroll down scrollBy(0, afterContainerHeight - beforeContainerHeight) - this.content.scrollToPoint(0, afterContainerHeight - beforeContainerHeight) + this.content.scrollToPoint( + 0, + afterContainerHeight - beforeContainerHeight, + ) if (logs.length < this.limit) { this.needInfinite = false } - - } catch (e) { } + } catch (e) {} } - async fetch (isBefore: boolean = true) { + async fetch(isBefore: boolean = true) { try { const cursor = isBefore ? this.startCursor : this.endCursor @@ -81,33 +88,40 @@ export class LogsPage { } } - async loadMore () { + async loadMore() { try { this.loadingMore = true const logs = await this.fetch(false) - if (!logs.length) return this.loadingMore = false + + if (!logs?.length) return (this.loadingMore = false) const container = document.getElementById('container') - const newLogs = document.getElementById('template').cloneNode(true) as HTMLElement - newLogs.innerHTML = logs.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`).join('\n') + (logs.length ? '\n' : '') - container.append(newLogs) + const newLogs = document.getElementById('template')?.cloneNode(true) + + if (!(newLogs instanceof HTMLElement)) return + + newLogs.innerHTML = + logs + .map(l => `${l.timestamp} ${convert.toHtml(l.message)}`) + .join('\n') + (logs.length ? '\n' : '') + container?.append(newLogs) this.loadingMore = false this.scrollEvent() - } catch (e) { } + } catch (e) {} } - scrollEvent () { + scrollEvent() { const buttonDiv = document.getElementById('button-div') - this.isOnBottom = buttonDiv.getBoundingClientRect().top < window.innerHeight + this.isOnBottom = + !!buttonDiv && buttonDiv.getBoundingClientRect().top < window.innerHeight } - scrollToBottom () { + scrollToBottom() { this.content.scrollToBottom(500) } - async loadData (e: any): Promise { + async loadData(e: any): Promise { await this.getLogs() e.target.complete() } } - diff --git a/frontend/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts b/frontend/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts index c1a772450..85bb2f6d4 100644 --- a/frontend/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts +++ b/frontend/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts @@ -25,17 +25,14 @@ export class MockApiService extends ApiService { async restart(): Promise { await pauseFor(1000) - return null } async forgetDrive(): Promise { await pauseFor(1000) - return null } async repairDisk(): Promise { await pauseFor(1000) - return null } async getLogs(params: GetLogsReq): Promise { 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 f5cf2958e..3a57d0844 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,5 @@ import { Injectable } from '@angular/core' -import { HttpClient, HttpErrorResponse } from '@angular/common/http' +import { HttpClient } from '@angular/common/http' import { HttpError, RpcError } from '@start9labs/shared' @Injectable({ @@ -12,6 +12,8 @@ export class HttpService { const res = await this.httpRequest>(options) if (isRpcError(res)) throw new RpcError(res.error) if (isRpcSuccess(res)) return res.result + + throw new Error('Unknown RPC response') } async httpRequest(body: RPCOptions): Promise { diff --git a/frontend/projects/marketplace/src/pages/release-notes/release-notes.component.ts b/frontend/projects/marketplace/src/pages/release-notes/release-notes.component.ts index a1dd976e5..d841461bd 100644 --- a/frontend/projects/marketplace/src/pages/release-notes/release-notes.component.ts +++ b/frontend/projects/marketplace/src/pages/release-notes/release-notes.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core' import { ActivatedRoute } from '@angular/router' +import { getPkgId } from '@start9labs/shared' import { AbstractMarketplaceService } from '../../services/marketplace.service' @Component({ @@ -9,7 +10,7 @@ import { AbstractMarketplaceService } from '../../services/marketplace.service' changeDetection: ChangeDetectionStrategy.OnPush, }) export class ReleaseNotesComponent { - private readonly pkgId = this.route.snapshot.paramMap.get('pkgId') + private readonly pkgId = getPkgId(this.route) private selected: string | null = null diff --git a/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts b/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts index e71a4fe9e..d0a624da3 100644 --- a/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts +++ b/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts @@ -34,7 +34,7 @@ export class AdditionalComponent { const alert = await this.alertCtrl.create({ header: 'Versions', inputs: this.pkg.versions - .sort((a, b) => -1 * this.emver.compare(a, b)) + .sort((a, b) => -1 * (this.emver.compare(a, b) || 0)) .map(v => ({ name: v, // for CSS type: 'radio', diff --git a/frontend/projects/marketplace/src/pipes/filter-packages.pipe.ts b/frontend/projects/marketplace/src/pipes/filter-packages.pipe.ts index 74b282956..464c33c71 100644 --- a/frontend/projects/marketplace/src/pipes/filter-packages.pipe.ts +++ b/frontend/projects/marketplace/src/pipes/filter-packages.pipe.ts @@ -1,5 +1,5 @@ import { NgModule, Pipe, PipeTransform } from '@angular/core' -import Fuse from 'fuse.js/dist/fuse.min.js' +import Fuse from 'fuse.js' import { MarketplacePkg } from '../types/marketplace-pkg' import { MarketplaceManifest } from '../types/marketplace-manifest' diff --git a/frontend/projects/marketplace/src/services/marketplace.service.ts b/frontend/projects/marketplace/src/services/marketplace.service.ts index 7d40d1bcf..3db2baabc 100644 --- a/frontend/projects/marketplace/src/services/marketplace.service.ts +++ b/frontend/projects/marketplace/src/services/marketplace.service.ts @@ -15,5 +15,8 @@ export abstract class AbstractMarketplaceService { abstract getPackageMarkdown(type: string, pkgId: string): Observable - abstract getPackage(id: string, version: string): Observable + abstract getPackage( + id: string, + version: string, + ): Observable } diff --git a/frontend/projects/setup-wizard/src/app/modals/password/password.page.ts b/frontend/projects/setup-wizard/src/app/modals/password/password.page.ts index b95238092..1016ceb5a 100644 --- a/frontend/projects/setup-wizard/src/app/modals/password/password.page.ts +++ b/frontend/projects/setup-wizard/src/app/modals/password/password.page.ts @@ -1,6 +1,10 @@ import { Component, Input, ViewChild } from '@angular/core' import { IonInput, ModalController } from '@ionic/angular' -import { DiskInfo, CifsBackupTarget, DiskBackupTarget } from 'src/app/services/api/api.service' +import { + DiskInfo, + CifsBackupTarget, + DiskBackupTarget, +} from 'src/app/services/api/api.service' import * as argon2 from '@start9labs/argon2' @Component({ @@ -21,26 +25,27 @@ export class PasswordPage { passwordVer = '' unmasked2 = false - constructor ( - private modalController: ModalController, - ) { } + constructor(private modalController: ModalController) {} - ngAfterViewInit () { + ngAfterViewInit() { setTimeout(() => this.elem.setFocus(), 400) } - async verifyPw () { - if (!this.target || !this.target['embassy-os']) this.pwError = 'No recovery target' // unreachable + async verifyPw() { + if (!this.target || !this.target['embassy-os']) + this.pwError = 'No recovery target' // unreachable try { - argon2.verify(this.target['embassy-os']['password-hash'], this.password) + const passwordHash = this.target['embassy-os']?.['password-hash'] || '' + + argon2.verify(passwordHash, this.password) this.modalController.dismiss({ password: this.password }, 'success') } catch (e) { this.pwError = 'Incorrect password provided' } } - async submitPw () { + async submitPw() { this.validate() if (this.password !== this.passwordVer) { this.verError = '*passwords do not match' @@ -50,8 +55,8 @@ export class PasswordPage { this.modalController.dismiss({ password: this.password }, 'success') } - validate () { - if (!!this.target) return this.pwError = '' + validate() { + if (!!this.target) return (this.pwError = '') if (this.passwordVer) { this.checkVer() @@ -64,11 +69,12 @@ export class PasswordPage { } } - checkVer () { - this.verError = this.password !== this.passwordVer ? 'Passwords do not match' : '' + checkVer() { + this.verError = + this.password !== this.passwordVer ? 'Passwords do not match' : '' } - cancel () { + cancel() { this.modalController.dismiss() } } diff --git a/frontend/projects/setup-wizard/src/app/modals/prod-key-modal/prod-key-modal.page.ts b/frontend/projects/setup-wizard/src/app/modals/prod-key-modal/prod-key-modal.page.ts index 773dca484..548576b6d 100644 --- a/frontend/projects/setup-wizard/src/app/modals/prod-key-modal/prod-key-modal.page.ts +++ b/frontend/projects/setup-wizard/src/app/modals/prod-key-modal/prod-key-modal.page.ts @@ -16,19 +16,19 @@ export class ProdKeyModal { productKey = '' unmasked = false - constructor ( + constructor( private readonly modalController: ModalController, private readonly apiService: ApiService, private readonly loadingCtrl: LoadingController, private readonly httpService: HttpService, - ) { } + ) {} - ngAfterViewInit () { + ngAfterViewInit() { setTimeout(() => this.elem.setFocus(), 400) } - async verifyProductKey () { - if (!this.productKey) return + async verifyProductKey() { + if (!this.productKey || !this.target.logicalname) return const loader = await this.loadingCtrl.create({ message: 'Verifying Product Key', @@ -48,7 +48,7 @@ export class ProdKeyModal { } } - cancel () { + cancel() { this.modalController.dismiss() } } 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 d1c543f30..9b2ba6563 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 @@ -68,8 +68,8 @@ export class RecoverPage { 'embassy-os': p['embassy-os'], } this.mappedDrives.push({ - hasValidBackup: p['embassy-os']?.full, - is02x: drive['embassy-os']?.version.startsWith('0.2'), + hasValidBackup: !!p['embassy-os']?.full, + is02x: !!drive['embassy-os']?.version.startsWith('0.2'), drive, }) }) @@ -111,7 +111,8 @@ export class RecoverPage { { text: 'Use Drive', handler: async () => { - await this.importDrive(importableDrive.guid) + if (importableDrive.guid) + await this.importDrive(importableDrive.guid) }, }, ], @@ -148,11 +149,14 @@ export class RecoverPage { } async select(target: DiskBackupTarget) { - const is02x = target['embassy-os'].version.startsWith('0.2') + const is02x = target['embassy-os']?.version.startsWith('0.2') + const { logicalname } = target + + if (!logicalname) return if (this.stateService.hasProductKey) { if (is02x) { - this.selectRecoverySource(target.logicalname) + this.selectRecoverySource(logicalname) } else { const modal = await this.modalController.create({ component: PasswordPage, @@ -160,8 +164,8 @@ export class RecoverPage { cssClass: 'alertlike-modal', }) modal.onDidDismiss().then(res => { - if (res.data && res.data.password) { - this.selectRecoverySource(target.logicalname, res.data.password) + if (res.data?.password) { + this.selectRecoverySource(logicalname, res.data.password) } }) await modal.present() @@ -188,8 +192,8 @@ export class RecoverPage { cssClass: 'alertlike-modal', }) modal.onDidDismiss().then(res => { - if (res.data && res.data.productKey) { - this.selectRecoverySource(target.logicalname) + if (res.data?.productKey) { + this.selectRecoverySource(logicalname) } }) await modal.present() 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 d87497dc8..5af128478 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 @@ -24,7 +24,7 @@ export class SuccessPage { await this.stateService.completeEmbassy() document .getElementById('install-cert') - .setAttribute( + ?.setAttribute( 'href', 'data:application/x-x509-ca-cert;base64,' + encodeURIComponent(this.stateService.cert), @@ -56,20 +56,24 @@ export class SuccessPage { } installCert() { - document.getElementById('install-cert').click() + document.getElementById('install-cert')?.click() } download() { - document.getElementById('tor-addr').innerHTML = this.stateService.torAddress - document.getElementById('lan-addr').innerHTML = this.stateService.lanAddress + const torAddress = document.getElementById('tor-addr') + const lanAddress = document.getElementById('lan-addr') + + if (torAddress) torAddress.innerHTML = this.stateService.torAddress + if (lanAddress) lanAddress.innerHTML = this.stateService.lanAddress + document .getElementById('cert') - .setAttribute( + ?.setAttribute( 'href', 'data:application/x-x509-ca-cert;base64,' + encodeURIComponent(this.stateService.cert), ) - let html = document.getElementById('downloadable').innerHTML + let html = document.getElementById('downloadable')?.innerHTML || '' const filename = 'embassy-info.html' const elem = document.createElement('a') 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 907251752..1ac4505be 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 @@ -15,7 +15,7 @@ import { HttpError, RpcError } from '@start9labs/shared' }) export class HttpService { fullUrl: string - productKey: string + productKey?: string constructor(private readonly http: HttpClient) { const port = window.location.port @@ -43,6 +43,8 @@ export class HttpService { } if (isRpcSuccess(res)) return res.result + + throw new Error('Unknown RPC response') } async encryptedHttpRequest(httpOpts: { @@ -53,7 +55,7 @@ export class HttpService { const url = urlIsRelative ? this.fullUrl + httpOpts.url : httpOpts.url const encryptedBody = await AES_CTR.encryptPbkdf2( - this.productKey, + this.productKey || '', encodeUtf8(JSON.stringify(httpOpts.body)), ) const options = { @@ -74,7 +76,7 @@ export class HttpService { .toPromise() .then(res => AES_CTR.decryptPbkdf2( - this.productKey, + this.productKey || '', (res as any).body as ArrayBuffer, ), ) @@ -206,7 +208,7 @@ type AES_CTR = { secretKey: string, messageBuffer: Uint8Array, ) => Promise - decryptPbkdf2: (secretKey, arr: ArrayBuffer) => Promise + decryptPbkdf2: (secretKey: string, arr: ArrayBuffer) => Promise } export const AES_CTR: AES_CTR = { @@ -243,8 +245,10 @@ export const AES_CTR: AES_CTR = { export const encode16 = (buffer: Uint8Array) => buffer.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '') -export const decode16 = hexString => - new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16))) +export const decode16 = (hexString: string) => + new Uint8Array( + hexString.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || [], + ) export function encodeUtf8(str: string): Uint8Array { const encoder = new TextEncoder() diff --git a/frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts b/frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts index 54d52af92..4e83f6f0d 100644 --- a/frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts @@ -43,7 +43,7 @@ export class LiveApiService extends ApiService { ) } - async set02XDrive(logicalname) { + async set02XDrive(logicalname: string) { return this.http.rpcRequest( { method: 'setup.recovery.v2.set', @@ -124,7 +124,7 @@ export class LiveApiService extends ApiService { } function isCifsSource( - source: CifsRecoverySource | DiskRecoverySource | undefined, + source: CifsRecoverySource | DiskRecoverySource | null, ): source is CifsRecoverySource { return !!(source as CifsRecoverySource)?.hostname } 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 0c32275ff..7379443fe 100644 --- a/frontend/projects/setup-wizard/src/app/services/state.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/state.service.ts @@ -18,7 +18,7 @@ export class StateService { embassyLoaded = false recoverySource: CifsRecoverySource | DiskRecoverySource - recoveryPassword: string + recoveryPassword?: string dataTransferProgress: { bytesTransferred: number diff --git a/frontend/projects/shared/package.json b/frontend/projects/shared/package.json index 2154e301d..c6b7b5c36 100644 --- a/frontend/projects/shared/package.json +++ b/frontend/projects/shared/package.json @@ -4,6 +4,7 @@ "peerDependencies": { "@angular/common": "^13.2.0", "@angular/core": "^13.2.0", + "@angular/router": "^13.2.0", "@ionic/angular": "^6.0.3", "@start9labs/emver": "^0.1.5" }, diff --git a/frontend/projects/shared/src/classes/rpc-error.ts b/frontend/projects/shared/src/classes/rpc-error.ts index 9b224bf49..2c4c48450 100644 --- a/frontend/projects/shared/src/classes/rpc-error.ts +++ b/frontend/projects/shared/src/classes/rpc-error.ts @@ -12,7 +12,7 @@ export class RpcError { return `${this.error.message}\n\n${this.error.data}` } - return this.error.data.details + return this.error.data?.details ? `${this.error.message}\n\n${this.error.data.details}` : this.error.message } @@ -20,6 +20,6 @@ export class RpcError { private getRevision(): T | null { return typeof this.error.data === 'string' ? null - : this.error.data.revision || null + : this.error.data?.revision || null } } diff --git a/frontend/projects/shared/src/pipes/emver/emver.pipe.ts b/frontend/projects/shared/src/pipes/emver/emver.pipe.ts index d16302736..4b82ff80f 100644 --- a/frontend/projects/shared/src/pipes/emver/emver.pipe.ts +++ b/frontend/projects/shared/src/pipes/emver/emver.pipe.ts @@ -7,8 +7,12 @@ import { Emver } from '../../services/emver.service' export class EmverSatisfiesPipe implements PipeTransform { constructor(private readonly emver: Emver) {} - transform(versionUnderTest: string, range: string): boolean { - return this.emver.satisfies(versionUnderTest, range) + transform(versionUnderTest?: string, range?: string): boolean { + return ( + !!versionUnderTest && + !!range && + this.emver.satisfies(versionUnderTest, range) + ) } } diff --git a/frontend/projects/shared/src/pipes/unit-conversion/unit-conversion.pipe.ts b/frontend/projects/shared/src/pipes/unit-conversion/unit-conversion.pipe.ts index fb40958e8..266e7fe9a 100644 --- a/frontend/projects/shared/src/pipes/unit-conversion/unit-conversion.pipe.ts +++ b/frontend/projects/shared/src/pipes/unit-conversion/unit-conversion.pipe.ts @@ -19,18 +19,17 @@ export class ConvertBytesPipe implements PipeTransform { name: 'durationToSeconds', }) export class DurationToSecondsPipe implements PipeTransform { - transform(duration: string | null): number { + transform(duration?: string | null): number { if (!duration) return 0 - const splitUnit = duration.match(/^([0-9]*(\.[0-9]+)?)(ns|µs|ms|s|m|d)$/) - const unit = splitUnit[3] - const num = splitUnit[1] + const [, num, , unit] = + duration.match(/^([0-9]*(\.[0-9]+)?)(ns|µs|ms|s|m|d)$/) || [] return Number(num) * unitsToSeconds[unit] } } const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] -const unitsToSeconds = { +const unitsToSeconds: Record = { ns: 1e-9, µs: 1e-6, ms: 0.001, diff --git a/frontend/projects/shared/src/public-api.ts b/frontend/projects/shared/src/public-api.ts index f8a18c239..ccc43978e 100644 --- a/frontend/projects/shared/src/public-api.ts +++ b/frontend/projects/shared/src/public-api.ts @@ -34,5 +34,6 @@ export * from './types/rpc-error-details' export * from './types/url' export * from './types/workspace-config' +export * from './util/get-pkg-id' export * from './util/misc.util' export * from './util/unused' diff --git a/frontend/projects/shared/src/services/emver.service.ts b/frontend/projects/shared/src/services/emver.service.ts index c8a0f308d..8dda885ac 100644 --- a/frontend/projects/shared/src/services/emver.service.ts +++ b/frontend/projects/shared/src/services/emver.service.ts @@ -7,7 +7,7 @@ import * as emver from '@start9labs/emver' export class Emver { constructor() {} - compare(lhs: string, rhs: string): number { + compare(lhs: string, rhs: string): number | null { if (!lhs || !rhs) return null return emver.compare(lhs, rhs) } diff --git a/frontend/projects/shared/src/services/error-toast.service.ts b/frontend/projects/shared/src/services/error-toast.service.ts index e960713ac..5df0ca34e 100644 --- a/frontend/projects/shared/src/services/error-toast.service.ts +++ b/frontend/projects/shared/src/services/error-toast.service.ts @@ -5,7 +5,7 @@ import { IonicSafeString, ToastController } from '@ionic/angular' providedIn: 'root', }) export class ErrorToastService { - private toast: HTMLIonToastElement + private toast?: HTMLIonToastElement constructor(private readonly toastCtrl: ToastController) {} diff --git a/frontend/projects/shared/src/util/get-pkg-id.ts b/frontend/projects/shared/src/util/get-pkg-id.ts new file mode 100644 index 000000000..f9fdf2cea --- /dev/null +++ b/frontend/projects/shared/src/util/get-pkg-id.ts @@ -0,0 +1,11 @@ +import { ActivatedRoute } from '@angular/router' + +export function getPkgId({ snapshot }: ActivatedRoute): string { + const pkgId = snapshot.paramMap.get('pkgId') + + if (!pkgId) { + throw new Error('pkgId is missing from route params') + } + + return pkgId +} diff --git a/frontend/projects/shared/src/util/misc.util.ts b/frontend/projects/shared/src/util/misc.util.ts index 68d1446a3..85dc6231f 100644 --- a/frontend/projects/shared/src/util/misc.util.ts +++ b/frontend/projects/shared/src/util/misc.util.ts @@ -28,7 +28,7 @@ export function debounce(delay: number = 300): MethodDecorator { const original = descriptor.value - descriptor.value = function (...args) { + descriptor.value = function (this: any, ...args: any[]) { clearTimeout(this[timeoutKey]) this[timeoutKey] = setTimeout(() => original.apply(this, args), delay) } diff --git a/frontend/projects/shared/src/util/unused.ts b/frontend/projects/shared/src/util/unused.ts index 2326a7fbb..31e916d08 100644 --- a/frontend/projects/shared/src/util/unused.ts +++ b/frontend/projects/shared/src/util/unused.ts @@ -33,7 +33,7 @@ export function traceThrowDesc(description: string, t: T | undefined): T { export function inMs( count: number, unit: 'days' | 'hours' | 'minutes' | 'seconds', -) { +): number { switch (unit) { case 'seconds': return count * 1000 @@ -63,31 +63,6 @@ export function toObject(t: T[], map: (t0: T) => string): Record { }, {} as Record) } -export function deepCloneUnknown(value: T): T { - if (typeof value !== 'object' || value === null) { - return value - } - if (Array.isArray(value)) { - return deepCloneArray(value) - } - return deepCloneObject(value) -} - -export function deepCloneObject(source: T) { - const result = {} - Object.keys(source).forEach(key => { - const value = source[key] - result[key] = deepCloneUnknown(value) - }, {}) - return result as T -} - -export function deepCloneArray(collection: any) { - return collection.map(value => { - return deepCloneUnknown(value) - }) -} - export function partitionArray( ts: T[], condition: (t: T) => boolean, @@ -110,21 +85,3 @@ export function update( ): Record { return { ...t, ...u } } - -export function uniqueBy( - ts: T[], - uniqueBy: (t: T) => string, - prioritize: (t1: T, t2: T) => T, -) { - return Object.values( - ts.reduce((acc, next) => { - const previousValue = acc[uniqueBy(next)] - if (previousValue) { - acc[uniqueBy(next)] = prioritize(acc[uniqueBy(next)], previousValue) - } else { - acc[uniqueBy(next)] = previousValue - } - return acc - }, {}), - ) -} diff --git a/frontend/projects/ui/src/app/app/footer/footer.component.ts b/frontend/projects/ui/src/app/app/footer/footer.component.ts index 7ac3a581b..2856aebb0 100644 --- a/frontend/projects/ui/src/app/app/footer/footer.component.ts +++ b/frontend/projects/ui/src/app/app/footer/footer.component.ts @@ -30,7 +30,7 @@ export class FooterComponent { getProgress({ downloaded, size, - }: ServerInfo['status-info']['update-progress']): number { + }: NonNullable): number { return Math.round((100 * (downloaded || 1)) / (size || 1)) } } 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 index 570e0857c..60eded578 100644 --- a/frontend/projects/ui/src/app/app/global/services/offline.service.ts +++ b/frontend/projects/ui/src/app/app/global/services/offline.service.ts @@ -83,6 +83,8 @@ function getMessage(failure: ConnectionFailure): OfflineMessage { message: 'Embassy not found on Local Area Network.', link: 'https://start9.com/latest/support/common-issues', } + default: + return { message: '' } } } 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 index b7ae6c1ae..efa33a800 100644 --- 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 @@ -5,7 +5,6 @@ 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({ @@ -27,7 +26,6 @@ export class PatchMonitorService extends Observable { ) constructor( - private readonly connectionService: ConnectionService, private readonly authService: AuthService, private readonly patch: PatchDbService, private readonly storage: Storage, 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 index 070a2584d..27ec741b6 100644 --- 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 @@ -51,7 +51,7 @@ export class UnreadToastService extends Observable { await this.unreadToast?.dismiss() this.unreadToast = await this.toastCtrl.create(TOAST) - this.unreadToast.buttons.push({ + this.unreadToast.buttons?.push({ side: 'end', text: 'View', handler: () => { 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 index 073aba2bc..d4f262a82 100644 --- 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 @@ -49,7 +49,7 @@ export class UpdateToastService extends Observable { await this.updateToast?.dismiss() this.updateToast = await this.toastCtrl.create(TOAST) - this.updateToast.buttons.push({ + this.updateToast.buttons?.push({ side: 'end', text: 'Restart', handler: () => { diff --git a/frontend/projects/ui/src/app/app/menu/menu.component.html b/frontend/projects/ui/src/app/app/menu/menu.component.html index a68a3c4e5..c3579153d 100644 --- a/frontend/projects/ui/src/app/app/menu/menu.component.html +++ b/frontend/projects/ui/src/app/app/menu/menu.component.html @@ -26,13 +26,15 @@ {{ page.title }} diff --git a/frontend/projects/ui/src/app/components/form-object/form-error.component.html b/frontend/projects/ui/src/app/components/form-object/form-error.component.html index 98fee9ce1..7980b910c 100644 --- a/frontend/projects/ui/src/app/components/form-object/form-error.component.html +++ b/frontend/projects/ui/src/app/components/form-object/form-error.component.html @@ -1,12 +1,10 @@
-

- {{ spec.name }} is required -

+

{{ spec.name }} is required

- {{ spec['pattern-description'] }} + {{ $any(spec)['pattern-description'] }}

@@ -15,7 +13,7 @@ {{ spec.name }} must be an integer

- {{ control.errors['numberNotInRange'].value }} + {{ control.errors?.['numberNotInRange']?.value }}

{{ spec.name }} must be a number @@ -25,13 +23,13 @@

- {{ control.errors['listNotInRange'].value }} + {{ control.errors?.['listNotInRange']?.value }}

- {{ control.errors['listNotUnique'].value }} + {{ control.errors?.['listNotUnique']?.value }}

- {{ control.errors['listItemIssue'].value }} + {{ control.errors?.['listItemIssue']?.value }}

diff --git a/frontend/projects/ui/src/app/components/form-object/form-object.component.html b/frontend/projects/ui/src/app/components/form-object/form-object.component.html index 8af1e4025..78df5cd74 100644 --- a/frontend/projects/ui/src/app/components/form-object/form-object.component.html +++ b/frontend/projects/ui/src/app/components/form-object/form-object.component.html @@ -51,7 +51,7 @@ @@ -129,7 +129,7 @@ @@ -208,7 +208,7 @@ @@ -281,16 +281,12 @@ : $any(spec.spec).spec " [formGroup]="abstractControl" - [current]=" - current && current[entry.key] - ? current[entry.key][i] - : undefined - " + [current]="current?.[entry.key]?.[i]" [unionSpec]=" spec.subtype === 'union' ? $any(spec.spec) : undefined " (onInputChange)=" - updateLabel(entry.key, i, spec.spec['display-as']) + updateLabel(entry.key, i, $any(spec.spec)['display-as']) " (onExpand)="resize(entry.key, i)" > @@ -350,7 +346,7 @@ @@ -372,7 +368,7 @@ 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 7f51f5295..c81f006c0 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 @@ -34,8 +34,8 @@ const Mustache = require('mustache') export class FormObjectComponent { @Input() objectSpec: ConfigSpec @Input() formGroup: FormGroup - @Input() unionSpec: ValueSpecUnion - @Input() current: { [key: string]: any } + @Input() unionSpec?: ValueSpecUnion + @Input() current?: { [key: string]: any } @Input() showEdited: boolean = false @Output() onInputChange = new EventEmitter() @Output() onExpand = new EventEmitter() @@ -61,7 +61,7 @@ export class FormObjectComponent { if (spec.type === 'list' && ['object', 'union'].includes(spec.subtype)) { this.objectListDisplay[key] = [] - this.formGroup.get(key).value.forEach((obj, index) => { + this.formGroup.get(key)?.value.forEach((obj: any, index: number) => { const displayAs = (spec.spec as ListValueSpecOf<'object'>)[ 'display-as' ] @@ -87,7 +87,7 @@ export class FormObjectComponent { } updateUnion(e: any): void { - const primary = this.unionSpec.tag.id + const primary = this.unionSpec?.tag.id Object.keys(this.formGroup.controls).forEach(control => { if (control === primary) return @@ -104,7 +104,7 @@ export class FormObjectComponent { this.formGroup.addControl(control, unionGroup.controls[control]) }) - Object.entries(this.unionSpec.variants[e.detail.value]).forEach( + Object.entries(this.unionSpec?.variants[e.detail.value] || {}).forEach( ([key, value]) => { if (['object', 'union'].includes(value.type)) { this.objectDisplay[key] = { @@ -138,6 +138,9 @@ export class FormObjectComponent { if (markDirty) arr.markAsDirty() const listSpec = this.objectSpec[key] as ValueSpecList const newItem = this.formService.getListItem(listSpec, val) + + if (!newItem) return + newItem.markAllAsTouched() arr.insert(0, newItem) if (['object', 'union'].includes(listSpec.subtype)) { @@ -177,13 +180,14 @@ export class FormObjectComponent { updateLabel(key: string, i: number, displayAs: string) { this.objectListDisplay[key][i].displayAs = displayAs - ? Mustache.render(displayAs, this.formGroup.get(key).value[i]) + ? Mustache.render(displayAs, this.formGroup.get(key)?.value[i]) : '' } - getWarningText(text: string): IonicSafeString { - if (text) - return new IonicSafeString(`${text}`) + getWarningText(text: string = ''): IonicSafeString | string { + return text + ? new IonicSafeString(`${text}`) + : '' } handleInputChange() { @@ -192,8 +196,8 @@ export class FormObjectComponent { handleBooleanChange(key: string, spec: ValueSpecBoolean) { if (spec.warning) { - const current = this.formGroup.get(key).value - const cancelFn = () => this.formGroup.get(key).setValue(!current) + const current = this.formGroup.get(key)?.value + const cancelFn = () => this.formGroup.get(key)?.setValue(!current) this.presentAlertChangeWarning(key, spec, undefined, cancelFn) } } @@ -307,7 +311,7 @@ export class FormObjectComponent { } private updateEnumList(key: string, current: string[], updated: string[]) { - this.formGroup.get(key).markAsDirty() + this.formGroup.get(key)?.markAsDirty() for (let i = current.length - 1; i >= 0; i--) { if (!updated.includes(current[i])) { @@ -322,9 +326,9 @@ export class FormObjectComponent { }) } - private getDocSize(key: string, index = 0) { + private getDocSize(key: string, index = 0): string { const element = document.getElementById(this.getElementId(key, index)) - return `${element.scrollHeight}px` + return `${element?.scrollHeight}px` } getElementId(key: string, index = 0): string { 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 55ef77ee2..afeca4d7c 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 @@ -16,7 +16,7 @@ {{ dependentViolation }} -
+
Affected Services
@@ -26,13 +26,10 @@ *ngFor="let dep of dependentBreakages | keyvalue" > - + -
{{ patch.data['package-data'][dep.key].manifest.title }}
+
{{ pkgs[dep.key].manifest.title }}
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 ae51eb86e..af31bc3a3 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 @@ -35,6 +35,8 @@ export class DependentsComponent { loading$ = new BehaviorSubject(false) cancel$ = new Subject() + readonly pkgs$ = this.patch.watch$('package-data') + constructor(public readonly patch: PatchDbService) {} load() { @@ -45,6 +47,7 @@ export class DependentsComponent { ) .subscribe({ complete: () => { + console.log('DEP BREAKS, ', this.dependentBreakages) if ( this.dependentBreakages && !isEmptyObject(this.dependentBreakages) diff --git a/frontend/projects/ui/src/app/components/install-wizard/prebaked-wizards.ts b/frontend/projects/ui/src/app/components/install-wizard/prebaked-wizards.ts index 835a2b169..332b2ec21 100644 --- a/frontend/projects/ui/src/app/components/install-wizard/prebaked-wizards.ts +++ b/frontend/projects/ui/src/app/components/install-wizard/prebaked-wizards.ts @@ -33,7 +33,7 @@ export class WizardBaker { const action = 'update' const toolbar: TopbarParams = { action, title, version } - const slideDefinitions: SlideDefinition[] = [ + const slideDefinitions: Array = [ installAlert ? { slide: { @@ -170,7 +170,7 @@ export class WizardBaker { const action = 'downgrade' const toolbar: TopbarParams = { action, title, version } - const slideDefinitions: SlideDefinition[] = [ + const slideDefinitions: Array = [ installAlert ? { slide: { 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 85d3b2f06..3634a1ac3 100644 --- a/frontend/projects/ui/src/app/components/logs/logs.page.ts +++ b/frontend/projects/ui/src/app/components/logs/logs.page.ts @@ -66,19 +66,20 @@ export class LogsPage { try { // get logs const logs = await this.fetch() - if (!logs.length) return + if (!logs?.length) return const container = document.getElementById('container') - const beforeContainerHeight = container.scrollHeight - const newLogs = document - .getElementById('template') - .cloneNode(true) as HTMLElement + const beforeContainerHeight = container?.scrollHeight || 0 + const newLogs = document.getElementById('template')?.cloneNode(true) + + if (!(newLogs instanceof HTMLElement)) return + newLogs.innerHTML = logs .map(l => `${l.timestamp} ${convert.toHtml(l.message)}`) .join('\n') + (logs.length ? '\n' : '') - container.prepend(newLogs) - const afterContainerHeight = container.scrollHeight + container?.prepend(newLogs) + const afterContainerHeight = container?.scrollHeight || 0 // scroll down scrollBy(0, afterContainerHeight - beforeContainerHeight) @@ -97,17 +98,18 @@ export class LogsPage { try { this.loadingMore = true const logs = await this.fetch(false) - if (!logs.length) return (this.loadingMore = false) + if (!logs?.length) return (this.loadingMore = false) const container = document.getElementById('container') - const newLogs = document - .getElementById('template') - .cloneNode(true) as HTMLElement + const newLogs = document.getElementById('template')?.cloneNode(true) + + if (!(newLogs instanceof HTMLElement)) return + newLogs.innerHTML = logs .map(l => `${l.timestamp} ${convert.toHtml(l.message)}`) .join('\n') + (logs.length ? '\n' : '') - container.append(newLogs) + container?.append(newLogs) this.loadingMore = false this.scrollEvent() } catch (e) {} @@ -116,7 +118,7 @@ export class LogsPage { scrollEvent() { const buttonDiv = document.getElementById('button-div') this.isOnBottom = - buttonDiv && buttonDiv.getBoundingClientRect().top < window.innerHeight + !!buttonDiv && buttonDiv.getBoundingClientRect().top < window.innerHeight } scrollToBottom() { diff --git a/frontend/projects/ui/src/app/components/status/status.component.ts b/frontend/projects/ui/src/app/components/status/status.component.ts index bff9823ad..bed2cd3bf 100644 --- a/frontend/projects/ui/src/app/components/status/status.component.ts +++ b/frontend/projects/ui/src/app/components/status/status.component.ts @@ -20,5 +20,5 @@ export class StatusComponent { @Input() weight?: string = 'normal' @Input() disconnected?: boolean = false @Input() installProgress?: number - @Input() sigtermTimeout?: string + @Input() sigtermTimeout?: string | null = null } 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 ce8cbba5d..35dbd29a2 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 @@ -33,7 +33,7 @@ { this.pkg = pkg @@ -62,13 +61,14 @@ export class AppActionsPage { } async handleAction(action: { key: string; value: Action }) { - const status = this.pkg.installed.status + const status = this.pkg.installed?.status if ( + status && (action.value['allowed-statuses'] as PackageMainStatus[]).includes( status.main.status, ) ) { - if (!isEmptyObject(action.value['input-spec'])) { + if (!isEmptyObject(action.value['input-spec'] || {})) { const modal = await this.modalCtrl.create({ component: GenericFormPage, componentProps: { @@ -112,7 +112,7 @@ export class AppActionsPage { const statuses = [...action.value['allowed-statuses']] const last = statuses.pop() let statusesStr = statuses.join(', ') - let error = null + let error = '' if (statuses.length) { if (statuses.length > 1) { // oxford comma @@ -144,7 +144,7 @@ export class AppActionsPage { id, title, version, - uninstallAlert: alerts.uninstall, + uninstallAlert: alerts.uninstall || undefined, }), ) @@ -177,6 +177,7 @@ export class AppActionsPage { }) setTimeout(() => successModal.present(), 400) + return true } catch (e: any) { this.errToast.present(e) return false diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts index 1b9fe3183..0ebe2a175 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts @@ -1,6 +1,7 @@ import { Component, Input, ViewChild } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { IonContent, ToastController } from '@ionic/angular' +import { getPkgId } from '@start9labs/shared' import { getUiInterfaceKey } from 'src/app/services/config.service' import { InstalledPackageDataEntry, @@ -23,7 +24,7 @@ export class AppInterfacesPage { @ViewChild(IonContent) content: IonContent ui: LocalInterface | null other: LocalInterface[] = [] - pkgId: string + readonly pkgId = getPkgId(this.route) constructor( private readonly route: ActivatedRoute, @@ -31,11 +32,12 @@ export class AppInterfacesPage { ) {} ngOnInit() { - this.pkgId = this.route.snapshot.paramMap.get('pkgId') const pkg = this.patch.getData()['package-data'][this.pkgId] const interfaces = pkg.manifest.interfaces const uiKey = getUiInterfaceKey(interfaces) + if (!pkg?.installed) return + const addressesMap = pkg.installed['interface-addresses'] if (uiKey) { @@ -45,10 +47,10 @@ export class AppInterfacesPage { addresses: { 'lan-address': uiAddresses['lan-address'] ? 'https://' + uiAddresses['lan-address'] - : null, + : '', 'tor-address': uiAddresses['tor-address'] ? 'http://' + uiAddresses['tor-address'] - : null, + : '', }, } } @@ -62,10 +64,10 @@ export class AppInterfacesPage { addresses: { 'lan-address': addresses['lan-address'] ? 'https://' + addresses['lan-address'] - : null, + : '', 'tor-address': addresses['tor-address'] ? 'http://' + addresses['tor-address'] - : null, + : '', }, } }) diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts index 5fc139f5b..b34df2ba1 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts @@ -21,7 +21,9 @@ export class AppListPkgComponent { constructor(private readonly launcherService: UiLauncherService) {} get status(): PackageMainStatus { - return this.pkg.entry.installed?.status.main.status + return ( + this.pkg.entry.installed?.status.main.status || PackageMainStatus.Stopped + ) } get manifest(): Manifest { diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list-reorder/app-list-reorder.component.html b/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list-reorder/app-list-reorder.component.html index c6651c10c..09521fcdb 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list-reorder/app-list-reorder.component.html +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list-reorder/app-list-reorder.component.html @@ -1,12 +1,17 @@ - {{ reordering ? "Reorder" : "Installed Services" }} - + {{ reordering ? 'Reorder' : 'Installed Services' }} + - {{ reordering ? "Done" : "Reorder" }} + {{ reordering ? 'Done' : 'Reorder' }} @@ -14,11 +19,15 @@ - + @@ -27,7 +36,7 @@

{{ pkg.entry.manifest.title }}

{{ pkg.entry.manifest.version | displayEmver }}

diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-logs/app-logs.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-logs/app-logs.page.ts index 15883fdeb..06fb4e0ec 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-logs/app-logs.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-logs/app-logs.page.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core' import { ActivatedRoute } from '@angular/router' +import { getPkgId } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/embassy-api.service' @Component({ @@ -8,22 +9,22 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' styleUrls: ['./app-logs.page.scss'], }) export class AppLogsPage { - pkgId: string + readonly pkgId = getPkgId(this.route) loading = true needInfinite = true before: string - constructor ( + constructor( private readonly route: ActivatedRoute, private readonly embassyApi: ApiService, - ) { } + ) {} - ngOnInit () { - this.pkgId = this.route.snapshot.paramMap.get('pkgId') - } - - fetchFetchLogs () { - return async (params: { before_flag?: boolean, limit?: number, cursor?: string }) => { + fetchFetchLogs() { + return async (params: { + before_flag?: boolean + limit?: number + cursor?: string + }) => { return this.embassyApi.getPackageLogs({ id: this.pkgId, before_flag: params.before_flag, 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 e7e58fd88..a2154ea09 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 @@ -5,7 +5,7 @@ import { Subscription } from 'rxjs' import { Metric } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' import { MainStatus } from 'src/app/services/patch-db/data-model' -import { pauseFor, ErrorToastService } from '@start9labs/shared' +import { pauseFor, ErrorToastService, getPkgId } from '@start9labs/shared' @Component({ selector: 'app-metrics', @@ -14,7 +14,7 @@ import { pauseFor, ErrorToastService } from '@start9labs/shared' }) export class AppMetricsPage { loading = true - pkgId: string + readonly pkgId = getPkgId(this.route) mainStatus: MainStatus going = false metrics: Metric @@ -29,7 +29,6 @@ export class AppMetricsPage { ) {} ngOnInit() { - this.pkgId = this.route.snapshot.paramMap.get('pkgId') this.startDaemon() } 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 0be553ef0..40ae74044 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 @@ -15,7 +15,7 @@ import { PackageProperties } from 'src/app/util/properties.util' import { QRComponent } from 'src/app/components/qr/qr.component' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PackageMainStatus } from 'src/app/services/patch-db/data-model' -import { ErrorToastService } from '@start9labs/shared' +import { ErrorToastService, getPkgId } from '@start9labs/shared' import { getValueByPointer } from 'fast-json-patch' @Component({ @@ -25,7 +25,7 @@ import { getValueByPointer } from 'fast-json-patch' }) export class AppPropertiesPage { loading = true - pkgId: string + readonly pkgId = getPkgId(this.route) pointer: string properties: PackageProperties node: PackageProperties @@ -55,8 +55,6 @@ export class AppPropertiesPage { } async ngOnInit() { - this.pkgId = this.route.snapshot.paramMap.get('pkgId') - await this.getProperties() this.subs = [ @@ -100,7 +98,7 @@ export class AppPropertiesPage { const alert = await this.alertCtrl.create({ header: property.key, - message: property.value.description, + message: property.value.description || undefined, }) await alert.present() } diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html index 8301419f3..1501e5de9 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html @@ -6,7 +6,7 @@ @@ -16,7 +16,7 @@ - + { diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.ts index cb173f467..63974f821 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.ts @@ -31,6 +31,6 @@ export class AppShowProgressComponent { } getColor(action: keyof InstallProgress): string { - return this.pkg['install-progress'][action] ? 'success' : 'secondary' + return this.pkg['install-progress']?.[action] ? 'success' : 'secondary' } } diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html index e690693df..3f0c3bf42 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html @@ -37,7 +37,7 @@ { const { id, title, version } = this.pkg.manifest const hasDependents = !!Object.keys( - this.pkg.installed['current-dependents'], + this.pkg.installed?.['current-dependents'] || {}, ).filter(depId => depId !== id).length if (!hasDependents) { diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts index bc6af0b3d..6080fd7a0 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts @@ -134,7 +134,7 @@ export class ToButtonsPipe implements PipeTransform { private async donate({ manifest }: PackageDataEntry): Promise { const url = manifest['donation-url'] if (url) { - this.document.defaultView.open(url, '_blank', 'noreferrer') + this.document.defaultView?.open(url, '_blank', 'noreferrer') } else { const alert = await this.alertCtrl.create({ header: 'Not Accepting Donations', diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-dependencies.pipe.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-dependencies.pipe.ts index 16d7cad27..9f9fcfe12 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-dependencies.pipe.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-dependencies.pipe.ts @@ -62,7 +62,7 @@ export class ToDependenciesPipe implements PipeTransform { private setDepValues( pkg: PackageDataEntry, id: string, - errors: { [id: string]: DependencyError }, + errors: { [id: string]: DependencyError | null }, ): DependencyInfo { let errorText = '' let actionText = 'View' @@ -105,13 +105,13 @@ export class ToDependenciesPipe implements PipeTransform { errorText = `${errorText}. ${pkg.manifest.title} will not work as expected.` } - const depInfo = pkg.installed['dependency-info'][id] + const depInfo = pkg.installed?.['dependency-info'][id] return { id, version: pkg.manifest.dependencies[id].version, - title: depInfo.manifest?.title || id, - icon: depInfo.icon, + title: depInfo?.manifest?.title || id, + icon: depInfo?.icon || '', errorText, actionText, action, 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 77a742c80..3feb5627b 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 @@ -1,12 +1,13 @@ import { Component } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { ModalController } from '@ionic/angular' +import { debounce, exists, ErrorToastService } from '@start9labs/shared' import * as yaml from 'js-yaml' -import { take } from 'rxjs/operators' +import { filter, take } from 'rxjs/operators' import { ApiService } from 'src/app/services/api/embassy-api.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' +import { getProjectId } from 'src/app/util/get-project-id' import { GenericFormPage } from '../../../modals/generic-form/generic-form.page' -import { debounce, ErrorToastService } from '@start9labs/shared' @Component({ selector: 'dev-config', @@ -14,7 +15,7 @@ import { debounce, ErrorToastService } from '@start9labs/shared' styleUrls: ['dev-config.page.scss'], }) export class DevConfigPage { - projectId: string + readonly projectId = getProjectId(this.route) editorOptions = { theme: 'vs-dark', language: 'yaml' } code: string = '' saving: boolean = false @@ -28,11 +29,9 @@ export class DevConfigPage { ) {} ngOnInit() { - this.projectId = this.route.snapshot.paramMap.get('projectId') - this.patchDb .watch$('ui', 'dev', this.projectId, 'config') - .pipe(take(1)) + .pipe(filter(exists), take(1)) .subscribe(config => { this.code = config }) 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 abc38be38..ad650e1a0 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 @@ -1,14 +1,16 @@ import { Component } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { ModalController } from '@ionic/angular' -import { take } from 'rxjs/operators' +import { filter, take } from 'rxjs/operators' import { ApiService } from 'src/app/services/api/embassy-api.service' import { debounce, + exists, ErrorToastService, MarkdownComponent, } from '@start9labs/shared' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' +import { getProjectId } from 'src/app/util/get-project-id' @Component({ selector: 'dev-instructions', @@ -16,7 +18,7 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' styleUrls: ['dev-instructions.page.scss'], }) export class DevInstructionsPage { - projectId: string + readonly projectId = getProjectId(this.route) editorOptions = { theme: 'vs-dark', language: 'markdown' } code: string = '' saving: boolean = false @@ -30,11 +32,9 @@ export class DevInstructionsPage { ) {} ngOnInit() { - this.projectId = this.route.snapshot.paramMap.get('projectId') - this.patchDb .watch$('ui', 'dev', this.projectId, 'instructions') - .pipe(take(1)) + .pipe(filter(exists), take(1)) .subscribe(config => { this.code = config }) diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.ts index 9d0b04d1c..cb76f263f 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.ts @@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router' import * as yaml from 'js-yaml' import { take } from 'rxjs/operators' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' +import { getProjectId } from 'src/app/util/get-project-id' @Component({ selector: 'dev-manifest', @@ -10,7 +11,7 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' styleUrls: ['dev-manifest.page.scss'], }) export class DevManifestPage { - projectId: string + readonly projectId = getProjectId(this.route) editorOptions = { theme: 'vs-dark', language: 'yaml', readOnly: true } manifest: string = '' @@ -20,8 +21,6 @@ export class DevManifestPage { ) {} ngOnInit() { - this.projectId = this.route.snapshot.paramMap.get('projectId') - this.patchDb .watch$('ui', 'dev', this.projectId) .pipe(take(1)) 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 eebcb50b7..092b84b75 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 @@ -231,9 +231,7 @@ const SAMPLE_CONFIG: ConfigSpec = { masked: false, copyable: false, // optional - warning: null, description: 'Example description for required string input.', - default: null, placeholder: 'Enter string value', pattern: '^[a-zA-Z0-9! _]+$', 'pattern-description': 'Must be alphanumeric (may contain underscore).', @@ -248,14 +246,12 @@ const SAMPLE_CONFIG: ConfigSpec = { warning: 'Example warning to display when changing this number value.', units: 'ms', description: 'Example description for optional number input.', - default: null, placeholder: 'Enter number value', }, 'sample-boolean': { type: 'boolean', name: 'Example Boolean Toggle', // optional - warning: null, description: 'Example description for boolean toggle', default: true, }, diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html index dfbe35ad1..f27a3deb8 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html @@ -3,7 +3,7 @@ - {{ patchDb.data.ui.dev[projectId].name}} + {{ name }} View Manifest 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 00b307671..780526d81 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 @@ -5,10 +5,10 @@ import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' import { BasicInfo, getBasicInfoSpec } from './form-info' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService } from '@start9labs/shared' +import { ErrorToastService, DestroyService } from '@start9labs/shared' import { takeUntil } from 'rxjs/operators' import { DevProjectData } from 'src/app/services/patch-db/data-model' -import { DestroyService } from '../../../../../../shared/src/services/destroy.service' +import { getProjectId } from 'src/app/util/get-project-id' import * as yaml from 'js-yaml' @Component({ @@ -18,7 +18,7 @@ import * as yaml from 'js-yaml' providers: [DestroyService], }) export class DeveloperMenuPage { - projectId: string + readonly projectId = getProjectId(this.route) projectData: DevProjectData constructor( @@ -28,12 +28,14 @@ export class DeveloperMenuPage { private readonly api: ApiService, private readonly errToast: ErrorToastService, private readonly destroy$: DestroyService, - public readonly patchDb: PatchDbService, + private readonly patchDb: PatchDbService, ) {} - ngOnInit() { - this.projectId = this.route.snapshot.paramMap.get('projectId') + get name(): string { + return this.patchDb.data.ui?.dev?.[this.projectId]?.name || '' + } + ngOnInit() { this.patchDb .watch$('ui', 'dev', this.projectId) .pipe(takeUntil(this.destroy$)) @@ -51,14 +53,14 @@ export class DeveloperMenuPage { buttons: [ { text: 'Save', - handler: basicInfo => { + handler: (basicInfo: any) => { basicInfo.description = { short: basicInfo.short, long: basicInfo.long, } delete basicInfo.short delete basicInfo.long - this.saveBasicInfo(basicInfo as BasicInfo) + this.saveBasicInfo(basicInfo) }, isSubmit: true, }, diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html index d56e85af5..a85b6d591 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html @@ -9,10 +9,10 @@ diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts index 4d5bb543b..2599ddb9f 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts @@ -25,7 +25,7 @@ export class MarketplaceShowControlsComponent { pkg: MarketplacePkg @Input() - localPkg: PackageDataEntry + localPkg: PackageDataEntry | null = null readonly PackageState = PackageState @@ -77,7 +77,7 @@ export class MarketplaceShowControlsComponent { title, version, serviceRequirements: dependencies, - installAlert: alerts.install, + installAlert: alerts.install || undefined, } const { cancelled } = await wizardModal( diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html index 3244e1d15..77fdc975c 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html @@ -5,9 +5,9 @@ ('*') @@ -28,12 +28,16 @@ export class MarketplaceShowPage { shareReplay({ bufferSize: 1, refCount: true }), ) - readonly pkg$: Observable = this.loadVersion$.pipe( + readonly pkg$: Observable = this.loadVersion$.pipe( switchMap(version => this.marketplaceService.getPackage(this.pkgId, version), ), // TODO: Better fallback - catchError(e => this.errToast.present(e) && of({} as MarketplacePkg)), + catchError(e => { + this.errToast.present(e) + + return of({} as MarketplacePkg) + }), ) constructor( diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-status/install-progress.pipe.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-status/install-progress.pipe.ts index 2c20f7a1f..1d731bf93 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-status/install-progress.pipe.ts +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-status/install-progress.pipe.ts @@ -6,8 +6,8 @@ import { packageLoadingProgress } from 'src/app/util/package-loading-progress' name: 'installProgress', }) export class InstallProgressPipe implements PipeTransform { - transform(loadData: InstallProgress): string { - const { totalProgress } = packageLoadingProgress(loadData) + transform(loadData?: InstallProgress): string { + const totalProgress = packageLoadingProgress(loadData)?.totalProgress || 0 return totalProgress < 99 ? totalProgress + '%' : 'finalizing' } diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.page.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.page.ts index de0487c24..f8397fe32 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.page.ts +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/release-notes/release-notes.page.ts @@ -1,12 +1,13 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' import { ActivatedRoute } from '@angular/router' +import { getPkgId } from '@start9labs/shared' @Component({ templateUrl: './release-notes.page.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ReleaseNotesPage { - readonly href = `/marketplace/${this.route.snapshot.paramMap.get('pkgId')}` + readonly href = `/marketplace/${getPkgId(this.route)}` constructor(private readonly route: ActivatedRoute) {} } 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 0a36ffc36..620a5479c 100644 --- a/frontend/projects/ui/src/app/pages/notifications/notifications.page.ts +++ b/frontend/projects/ui/src/app/pages/notifications/notifications.page.ts @@ -23,7 +23,7 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' export class NotificationsPage { loading = true notifications: ServerNotifications = [] - beforeCursor: number + beforeCursor?: number needInfinite = false fromToast = false readonly perPage = 40 @@ -51,19 +51,23 @@ export class NotificationsPage { } async getNotifications(): Promise { - let notifications: ServerNotifications = [] try { - notifications = await this.embassyApi.getNotifications({ + const notifications = await this.embassyApi.getNotifications({ before: this.beforeCursor, limit: this.perPage, }) + + if (!notifications) return [] + this.beforeCursor = notifications[notifications.length - 1]?.id this.needInfinite = notifications.length >= this.perPage + + return notifications } catch (e: any) { this.errToast.present(e) - } finally { - return notifications } + + return [] } async delete(id: number, index: number): Promise { diff --git a/frontend/projects/ui/src/app/pages/server-routes/lan/lan.page.html b/frontend/projects/ui/src/app/pages/server-routes/lan/lan.page.html index 229f1e925..d2d2ca87e 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/lan/lan.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/lan/lan.page.html @@ -33,7 +33,7 @@ >instructions. - +
For security reasons, you must setup LAN over a diff --git a/frontend/projects/ui/src/app/pages/server-routes/lan/lan.page.ts b/frontend/projects/ui/src/app/pages/server-routes/lan/lan.page.ts index 5943d1ad6..a91ecf933 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/lan/lan.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/lan/lan.page.ts @@ -8,8 +8,7 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' styleUrls: ['./lan.page.scss'], }) export class LANPage { - downloadIsDisabled: boolean - + readonly downloadIsDisabled = !this.config.isTor() readonly server$ = this.patch.watch$('server-info') constructor( @@ -17,11 +16,7 @@ export class LANPage { private readonly patch: PatchDbService, ) {} - ngOnInit() { - this.downloadIsDisabled = !this.config.isTor() - } - installCert(): void { - document.getElementById('install-cert').click() + document.getElementById('install-cert')?.click() } } 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 7202a3238..8a13eaffe 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 @@ -15,7 +15,19 @@ import { v4 } from 'uuid' import { UIMarketplaceData } from '../../../services/patch-db/data-model' import { ConfigService } from '../../../services/config.service' import { MarketplaceService } from 'src/app/services/marketplace.service' -import { finalize, first } from 'rxjs/operators' +import { + distinctUntilChanged, + finalize, + first, + map, + startWith, +} from 'rxjs/operators' + +type Marketplaces = { + id: string | undefined + name: string + url: string +}[] @Component({ selector: 'marketplaces', @@ -24,7 +36,7 @@ import { finalize, first } from 'rxjs/operators' }) export class MarketplacesPage { selectedId: string | undefined - marketplaces: { id: string | undefined; name: string; url: string }[] = [] + marketplaces: Marketplaces = [] constructor( private readonly api: ApiService, @@ -39,27 +51,33 @@ export class MarketplacesPage { ) {} ngOnInit() { - this.patch.watch$('ui', 'marketplace').subscribe(mp => { - const marketplaces = [ - { - id: undefined, - name: this.config.marketplace.name, - url: this.config.marketplace.url, - }, - ] - if (mp) { - this.selectedId = mp['selected-id'] - const alts = Object.entries(mp['known-hosts']).map(([k, v]) => { - return { - id: k, - name: v.name, - url: v.url, - } - }) - marketplaces.push.apply(marketplaces, alts) - } - this.marketplaces = marketplaces - }) + this.patch + .watch$('ui') + .pipe( + map(ui => ui.marketplace), + distinctUntilChanged(), + ) + .subscribe(mp => { + let marketplaces: Marketplaces = [ + { + id: undefined, + name: this.config.marketplace.name, + url: this.config.marketplace.url, + }, + ] + if (mp) { + this.selectedId = mp['selected-id'] || undefined + const alts = Object.entries(mp['known-hosts']).map(([k, v]) => { + return { + id: k, + name: v.name, + url: v.url, + } + }) + marketplaces = marketplaces.concat(alts) + } + this.marketplaces = marketplaces + }) } async presentModalAdd() { @@ -91,9 +109,10 @@ export class MarketplacesPage { await modal.present() } - async presentAction(id: string) { + async presentAction(id: string = '') { // no need to view actions if is selected marketplace - if (id === this.patch.getData().ui.marketplace?.['selected-id']) return + if (!id || id === this.patch.getData().ui.marketplace?.['selected-id']) + return const buttons: ActionSheetButton[] = [ { @@ -200,7 +219,10 @@ export class MarketplacesPage { ? (JSON.parse( JSON.stringify(this.patch.getData().ui.marketplace), ) as UIMarketplaceData) - : { 'selected-id': undefined, 'known-hosts': {} } + : { + 'selected-id': undefined, + 'known-hosts': {} as Record, + } // no-op on duplicates const currentUrls = this.marketplaces.map(mp => mp.url) @@ -242,7 +264,10 @@ export class MarketplacesPage { ? (JSON.parse( JSON.stringify(this.patch.getData().ui.marketplace), ) as UIMarketplaceData) - : { 'selected-id': undefined, 'known-hosts': {} } + : { + 'selected-id': undefined, + 'known-hosts': {} as Record, + } // no-op on duplicates const currentUrls = this.marketplaces.map(mp => mp.url) diff --git a/frontend/projects/ui/src/app/pages/server-routes/restore/restore.component.ts b/frontend/projects/ui/src/app/pages/server-routes/restore/restore.component.ts index f9456d385..5279a9477 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/restore/restore.component.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/restore/restore.component.ts @@ -43,7 +43,8 @@ export class RestorePage { useMask: true, buttonText: 'Next', submitFn: async (password: string) => { - argon2.verify(target.entry['embassy-os']['password-hash'], password) + const passwordHash = target.entry['embassy-os']?.['password-hash'] || '' + argon2.verify(passwordHash, password) await this.restoreFromBackup(target, password) }, } diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts index b9921b095..6e0c4cffe 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts @@ -55,7 +55,7 @@ export class ServerBackupPage { } else { if (this.backingUp) { this.backingUp = false - this.pkgs.forEach(pkg => pkg.sub.unsubscribe()) + this.pkgs.forEach(pkg => pkg.sub?.unsubscribe()) this.navCtrl.navigateRoot('/embassy') } } @@ -65,7 +65,7 @@ export class ServerBackupPage { ngOnDestroy() { this.subs.forEach(sub => sub.unsubscribe()) - this.pkgs.forEach(pkg => pkg.sub.unsubscribe()) + this.pkgs.forEach(pkg => pkg.sub?.unsubscribe()) } async presentModalPassword( @@ -98,7 +98,10 @@ export class ServerBackupPage { // existing backup } else { try { - argon2.verify(target.entry['embassy-os']['password-hash'], password) + const passwordHash = + target.entry['embassy-os']?.['password-hash'] || '' + + argon2.verify(passwordHash, password) } catch { setTimeout( () => this.presentModalOldPassword(target, password), @@ -133,7 +136,9 @@ export class ServerBackupPage { useMask: true, buttonText: 'Create Backup', submitFn: async (oldPassword: string) => { - argon2.verify(target.entry['embassy-os']['password-hash'], oldPassword) + const passwordHash = target.entry['embassy-os']?.['password-hash'] || '' + + argon2.verify(passwordHash, oldPassword) await this.createBackup(target.id, password, oldPassword) }, } @@ -182,15 +187,11 @@ export class ServerBackupPage { pkg.installed?.status.main.status === PackageMainStatus.BackingUp, ) - this.pkgs = pkgArr.map((pkg, i) => { - const pkgInfo = { - entry: pkg, - active: i === activeIndex, - complete: i < activeIndex, - sub: null, - } - return pkgInfo - }) + this.pkgs = pkgArr.map((pkg, i) => ({ + entry: pkg, + active: i === activeIndex, + complete: i < activeIndex, + })) // subscribe to pkg this.pkgs.forEach(pkg => { @@ -220,5 +221,5 @@ interface PkgInfo { entry: PackageDataEntry active: boolean complete: boolean - sub: Subscription + sub?: Subscription } diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-logs/server-logs.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-logs/server-logs.page.ts index ec0865164..3b52fab5e 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-logs/server-logs.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-logs/server-logs.page.ts @@ -12,11 +12,11 @@ export class ServerLogsPage { needInfinite = true before: string - constructor ( + constructor( private readonly embassyApi: ApiService, ) { } - fetchFetchLogs () { + fetchFetchLogs() { return async (params: { before_flag?: boolean, limit?: number, cursor?: string }) => { return this.embassyApi.getServerLogs({ before_flag: params.before_flag, @@ -25,4 +25,22 @@ export class ServerLogsPage { }) } } + + async copy(): Promise { + const logs = document + .getElementById('template') + ?.cloneNode(true) as HTMLElement + const formatted = '```' + logs.innerHTML + '```' + const success = await copyToClipboard(formatted) + const message = success + ? 'Copied to clipboard!' + : 'Failed to copy to clipboard.' + + const toast = await this.toastCtrl.create({ + header: message, + position: 'bottom', + duration: 1000, + }) + await toast.present() + } } 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 69413132d..2e308d087 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 @@ -30,7 +30,7 @@ export class ServerMetricsPage { }) const height = headersCount * 54 + rowsCount * 50 + 24 // extra 24 for room at the bottom const elem = document.getElementById('metricSection') - elem.style.height = `${height}px` + if (elem) elem.style.height = `${height}px` this.startDaemon() this.loading = false } 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 fde31e3b3..7ea2d6767 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,7 +1,7 @@ - {{ (ui$ | async).name || "Embassy-" + (server$ | async).id }} + {{ (ui$ | async)?.name || "Embassy-" + (server$ | async)?.id }} Loading 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 fe8e02a3a..784f2e9b8 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 @@ -42,7 +42,7 @@ export class WifiPage { await this.getWifi() } - async getWifi(timeout?: number): Promise { + async getWifi(timeout: number = 0): Promise { this.loading = true try { this.wifi = await this.api.getWifi({}, timeout) diff --git a/frontend/projects/ui/src/app/pkg-config/config-utilities.ts b/frontend/projects/ui/src/app/pkg-config/config-utilities.ts index 672625f9d..6173e8e0c 100644 --- a/frontend/projects/ui/src/app/pkg-config/config-utilities.ts +++ b/frontend/projects/ui/src/app/pkg-config/config-utilities.ts @@ -18,7 +18,7 @@ export class Range { checkIncludes(n: number) { if ( - this.hasMin() !== undefined && + this.hasMin() && (this.min > n || (!this.minInclusive && this.min == n)) ) { throw new Error(this.minMessage()) @@ -31,11 +31,11 @@ export class Range { } } - hasMin(): boolean { + hasMin(): this is Range & { min: number } { return this.min !== undefined } - hasMax(): boolean { + hasMax(): this is Range & { max: number } { return this.max !== undefined } diff --git a/frontend/projects/ui/src/app/services/api/api.fixures.ts b/frontend/projects/ui/src/app/services/api/api.fixures.ts index 673379b9a..b3e995cdc 100644 --- a/frontend/projects/ui/src/app/services/api/api.fixures.ts +++ b/frontend/projects/ui/src/app/services/api/api.fixures.ts @@ -177,8 +177,6 @@ export module Mock { nullable: true, masked: false, copyable: false, - pattern: null, - 'pattern-description': null, warning: 'You may loose all your money by providing your name.', }, notifications: { @@ -213,7 +211,6 @@ export module Mock { name: 'Top Speed', description: 'The fastest you can possibly run.', nullable: false, - default: null, range: '[-1000, 1000]', integral: false, units: 'm/s', @@ -248,7 +245,6 @@ export module Mock { name: { type: 'string', name: 'Name', - description: null, nullable: false, masked: false, copyable: false, @@ -258,7 +254,6 @@ export module Mock { email: { type: 'string', name: 'Email', - description: null, nullable: false, masked: false, copyable: true, @@ -1187,7 +1182,6 @@ export module Mock { type: 'string', description: 'User first name', nullable: true, - default: null, masked: false, copyable: false, }, @@ -1210,7 +1204,6 @@ export module Mock { type: 'number', description: 'The age of the user', nullable: true, - default: null, integral: false, warning: 'User must be at least 18.', range: '[18,*)', diff --git a/frontend/projects/ui/src/app/services/api/api.types.ts b/frontend/projects/ui/src/app/services/api/api.types.ts index 284325d2c..6e3f568ae 100644 --- a/frontend/projects/ui/src/app/services/api/api.types.ts +++ b/frontend/projects/ui/src/app/services/api/api.types.ts @@ -265,7 +265,7 @@ export module RR { } export type WithExpire = { 'expire-id'?: string } & T -export type WithRevision = { response: T; revision?: Revision } +export type WithRevision = { response: T | null; revision?: Revision } export interface MarketplaceEOS { version: string 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 1940af73c..0f740b535 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 @@ -99,7 +99,7 @@ export class MockApiService extends ApiService { async killSessions(params: RR.KillSessionsReq): Promise { await pauseFor(2000) - return null + return { response: null } } // server @@ -749,13 +749,14 @@ export class MockApiService extends ApiService { { progress: 'downloaded', completion: 'download-complete' }, { progress: 'validated', completion: 'validation-complete' }, { progress: 'unpacked', completion: 'unpack-complete' }, - ] + ] as const for (let phase of phases) { let i = progress[phase.progress] - while (i < progress.size) { + const size = progress?.size || 0 + while (i < size) { await pauseFor(250) - i = Math.min(i + 5, progress.size) + i = Math.min(i + 5, size) progress[phase.progress] = i if (i === progress.size) { @@ -858,7 +859,7 @@ export class MockApiService extends ApiService { private async withRevision( patch: Operation[], - response: T = null, + response: T | null = null, ): Promise> { if (!this.sequence) { const { sequence } = await this.bootstrapper.init() diff --git a/frontend/projects/ui/src/app/services/api/mock-patch.ts b/frontend/projects/ui/src/app/services/api/mock-patch.ts index 51a21fb00..f2f44dda5 100644 --- a/frontend/projects/ui/src/app/services/api/mock-patch.ts +++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts @@ -11,12 +11,9 @@ import { export const mockPatchData: DataModel = { ui: { name: `Matt's Embassy`, - 'auto-check-updates': undefined, + 'auto-check-updates': false, 'pkg-order': [], 'ack-welcome': '1.0.0', - marketplace: undefined, - dev: undefined, - gaming: undefined, }, 'server-info': { id: 'abcdefgh', @@ -212,8 +209,6 @@ export const mockPatchData: DataModel = { nullable: true, masked: false, copyable: false, - pattern: null, - 'pattern-description': null, warning: 'You may loose all your money by providing your name.', }, notifications: { @@ -248,7 +243,6 @@ export const mockPatchData: DataModel = { name: 'Top Speed', description: 'The fastest you can possibly run.', nullable: false, - default: null, range: '[-1000, 1000]', integral: false, units: 'm/s', @@ -283,7 +277,6 @@ export const mockPatchData: DataModel = { name: { type: 'string', name: 'Name', - description: null, nullable: false, masked: false, copyable: false, @@ -293,7 +286,6 @@ export const mockPatchData: DataModel = { email: { type: 'string', name: 'Email', - description: null, nullable: false, masked: false, copyable: true, diff --git a/frontend/projects/ui/src/app/services/config.service.ts b/frontend/projects/ui/src/app/services/config.service.ts index 55ba88269..c7de8d843 100644 --- a/frontend/projects/ui/src/app/services/config.service.ts +++ b/frontend/projects/ui/src/app/services/config.service.ts @@ -28,7 +28,7 @@ export class ConfigService { api = api marketplace = marketplace skipStartupAlerts = useMocks && mocks.skipStartupAlerts - isConsulate = window['platform'] === 'ios' + isConsulate = (window as any)['platform'] === 'ios' supportsWebSockets = !!window.WebSocket || this.isConsulate isTor(): boolean { @@ -76,14 +76,20 @@ export function hasLanUi(interfaces: Record): boolean { return !!int?.['lan-config'] } -export function torUiAddress(pkg: PackageDataEntry): string { - const key = getUiInterfaceKey(pkg.manifest.interfaces) - return pkg.installed['interface-addresses'][key]['tor-address'] +export function torUiAddress({ + manifest, + installed, +}: PackageDataEntry): string { + const key = getUiInterfaceKey(manifest.interfaces) + return installed ? installed['interface-addresses'][key]['tor-address'] : '' } -export function lanUiAddress(pkg: PackageDataEntry): string { - const key = getUiInterfaceKey(pkg.manifest.interfaces) - return pkg.installed['interface-addresses'][key]['lan-address'] +export function lanUiAddress({ + manifest, + installed, +}: PackageDataEntry): string { + const key = getUiInterfaceKey(manifest.interfaces) + return installed ? installed['interface-addresses'][key]['lan-address'] : '' } export function hasUi(interfaces: Record): boolean { @@ -103,11 +109,11 @@ export function removePort(str: string): string { export function getUiInterfaceKey( interfaces: Record, ): string { - return Object.keys(interfaces).find(key => interfaces[key].ui) + return Object.keys(interfaces).find(key => interfaces[key].ui) || '' } export function getUiInterfaceValue( interfaces: Record, -): InterfaceDef { - return Object.values(interfaces).find(i => i.ui) +): InterfaceDef | null { + return Object.values(interfaces).find(i => i.ui) || null } diff --git a/frontend/projects/ui/src/app/services/form.service.ts b/frontend/projects/ui/src/app/services/form.service.ts index b9265aaf3..b5f3affaa 100644 --- a/frontend/projects/ui/src/app/services/form.service.ts +++ b/frontend/projects/ui/src/app/services/form.service.ts @@ -86,7 +86,7 @@ export class FormService { validators: ValidatorFn[] = [], current: { [key: string]: any } = {}, ): FormGroup { - let group = {} + let group: Record = {} Object.entries(config).map(([key, spec]) => { if (spec.type === 'pointer') return group[key] = this.getFormEntry(spec, current ? current[key] : undefined) @@ -137,6 +137,8 @@ export class FormService { case 'enum': value = currentValue === undefined ? spec.default : currentValue return this.formBuilder.control(value) + default: + return this.formBuilder.control(null) } } } @@ -298,25 +300,20 @@ export function listUnique(spec: ValueSpecList): ValidatorFn { } function listItemEquals(spec: ValueSpecList, val1: any, val2: any): boolean { + // TODO: fix types switch (spec.subtype) { case 'string': case 'number': case 'enum': return val1 == val2 case 'object': - return listObjEquals( - spec.spec['unique-by'], - spec.spec as ListValueSpecObject, - val1, - val2, - ) + const obj: ListValueSpecObject = spec.spec as any + + return listObjEquals(obj['unique-by'], obj, val1, val2) case 'union': - return unionEquals( - spec.spec['unique-by'], - spec.spec as ListValueSpecUnion, - val1, - val2, - ) + const union: ListValueSpecUnion = spec.spec as any + + return unionEquals(union['unique-by'], union, val1, val2) default: return false } @@ -330,9 +327,21 @@ function itemEquals(spec: ValueSpec, val1: any, val2: any): boolean { case 'enum': return val1 == val2 case 'object': - return objEquals(spec['unique-by'], spec as ValueSpecObject, val1, val2) + // TODO: 'unique-by' does not exist on ValueSpecObject, fix types + return objEquals( + (spec as any)['unique-by'], + spec as ValueSpecObject, + val1, + val2, + ) case 'union': - return unionEquals(spec['unique-by'], spec as ValueSpecUnion, val1, val2) + // TODO: 'unique-by' does not exist on ValueSpecUnion, fix types + return unionEquals( + (spec as any)['unique-by'], + spec as ValueSpecUnion, + val1, + val2, + ) case 'list': if (val1.length !== val2.length) { return false @@ -373,6 +382,7 @@ function listObjEquals( } return true } + return false } function objEquals( @@ -384,7 +394,8 @@ function objEquals( if (uniqueBy === null) { return false } else if (typeof uniqueBy === 'string') { - return itemEquals(spec[uniqueBy], val1[uniqueBy], val2[uniqueBy]) + // TODO: fix types + return itemEquals((spec as any)[uniqueBy], val1[uniqueBy], val2[uniqueBy]) } else if ('any' in uniqueBy) { for (let subSpec of uniqueBy.any) { if (objEquals(subSpec, spec, val1, val2)) { @@ -400,6 +411,7 @@ function objEquals( } return true } + return false } function unionEquals( @@ -433,12 +445,13 @@ function unionEquals( } return true } + return false } function uniqueByMessageWrapper( uniqueBy: UniqueBy, spec: ListValueSpecObject | ListValueSpecUnion, - obj: object, + obj: Record, ) { let configSpec: ConfigSpec if (isUnion(spec)) { @@ -460,9 +473,9 @@ function uniqueByMessage( outermost = true, ): string { let joinFunc - const subSpecs = [] + const subSpecs: string[] = [] if (uniqueBy === null) { - return null + return '' } else if (typeof uniqueBy === 'string') { return configSpec[uniqueBy] ? configSpec[uniqueBy].name : uniqueBy } else if ('any' in uniqueBy) { @@ -476,7 +489,7 @@ function uniqueByMessage( subSpecs.push(uniqueByMessage(subSpec, configSpec, false)) } } - const ret = subSpecs.filter(ss => ss).join(joinFunc) + const ret = subSpecs.filter(Boolean).join(joinFunc) return outermost || subSpecs.filter(ss => ss).length === 1 ? ret : '(' + ret + ')' @@ -486,7 +499,7 @@ function isObjectOrUnion( spec: ListValueSpecOf, ): spec is ListValueSpecObject | ListValueSpecUnion { // only lists of objects and unions have unique-by - return spec['unique-by'] !== undefined + return 'unique-by' in spec } function isUnion(spec: any): spec is ListValueSpecUnion { @@ -499,18 +512,20 @@ export function convertValuesRecursive( group: FormGroup, ) { Object.entries(configSpec).forEach(([key, valueSpec]) => { + const control = group.get(key) + + if (!control) return + if (valueSpec.type === 'number') { - const control = group.get(key) control.setValue(control.value ? Number(control.value) : null) } else if (valueSpec.type === 'string') { - const control = group.get(key) if (!control.value) control.setValue(null) } else if (valueSpec.type === 'object') { convertValuesRecursive(valueSpec.spec, group.get(key) as FormGroup) } else if (valueSpec.type === 'union') { - const control = group.get(key) as FormGroup - const spec = valueSpec.variants[control.controls[valueSpec.tag.id].value] - convertValuesRecursive(spec, control) + const formGr = group.get(key) as FormGroup + const spec = valueSpec.variants[formGr.controls[valueSpec.tag.id].value] + convertValuesRecursive(spec, formGr) } else if (valueSpec.type === 'list') { const formArr = group.get(key) as FormArray const { controls } = formArr diff --git a/frontend/projects/ui/src/app/services/http.service.ts b/frontend/projects/ui/src/app/services/http.service.ts index f10cb8e8b..20eff39f7 100644 --- a/frontend/projects/ui/src/app/services/http.service.ts +++ b/frontend/projects/ui/src/app/services/http.service.ts @@ -1,10 +1,5 @@ import { Injectable } from '@angular/core' -import { - HttpClient, - HttpErrorResponse, - HttpHeaders, - HttpParams, -} from '@angular/common/http' +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' import { Observable, from, interval, race } from 'rxjs' import { map, take } from 'rxjs/operators' import { ConfigService } from './config.service' @@ -27,6 +22,7 @@ export class HttpService { this.fullUrl = `${window.location.protocol}//${window.location.hostname}:${port}` } + // @ts-ignore TODO: fix typing async rpcRequest(rpcOpts: RPCOptions): Promise { const { url, version } = this.config.api rpcOpts.params = rpcOpts.params || {} @@ -53,12 +49,15 @@ export class HttpService { const urlIsRelative = httpOpts.url.startsWith('/') const url = urlIsRelative ? this.fullUrl + httpOpts.url : httpOpts.url + const { params } = httpOpts - Object.keys(httpOpts.params || {}).forEach(key => { - if (httpOpts.params[key] === undefined) { - delete httpOpts.params[key] - } - }) + if (hasParams(params)) { + Object.keys(params).forEach(key => { + if (params[key] === undefined) { + delete params[key] + } + }) + } const options = { responseType: httpOpts.responseType || 'json', @@ -181,6 +180,12 @@ export interface HttpOptions { timeout?: number } +function hasParams( + params?: HttpOptions['params'], +): params is Record { + return !!params +} + function withTimeout(req: Observable, timeout: number): Observable { return race( from(req.toPromise()), // this guarantees it only emits on completion, intermediary emissions are suppressed. diff --git a/frontend/projects/ui/src/app/services/marketplace.service.ts b/frontend/projects/ui/src/app/services/marketplace.service.ts index 938f82866..67d08f885 100644 --- a/frontend/projects/ui/src/app/services/marketplace.service.ts +++ b/frontend/projects/ui/src/app/services/marketplace.service.ts @@ -5,16 +5,20 @@ import { MarketplacePkg, AbstractMarketplaceService, Marketplace, - MarketplaceData, } from '@start9labs/marketplace' import { defer, from, Observable, of } from 'rxjs' import { RR } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ConfigService } from 'src/app/services/config.service' -import { ServerInfo } from 'src/app/services/patch-db/data-model' +import { + ServerInfo, + UIMarketplaceData, +} from 'src/app/services/patch-db/data-model' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { catchError, + distinctUntilChanged, + filter, map, shareReplay, startWith, @@ -27,35 +31,56 @@ import { export class MarketplaceService extends AbstractMarketplaceService { private readonly notes = new Map>() - 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 altMarketplaceData$: Observable< + UIMarketplaceData | undefined + > = this.patch.watch$('ui').pipe( + map(ui => ui.marketplace), + distinctUntilChanged(), + shareReplay({ bufferSize: 1, refCount: true }), + ) - private readonly data$: Observable = this.init$.pipe( + private readonly marketplace$ = this.altMarketplaceData$.pipe( + map(data => this.toMarketplace(data)), + ) + + private readonly serverInfo$: Observable = this.patch + .watch$('server-info') + .pipe(take(1), shareReplay({ bufferSize: 1, refCount: true })) + + private readonly categories$: Observable = this.marketplace$.pipe( switchMap(({ url }) => - from(this.getMarketplaceData({ 'server-id': this.serverInfo.id }, url)), - ), - shareReplay(), - ) - - private readonly pkg$: Observable = this.init$.pipe( - take(1), - switchMap(({ url, name }) => - from(this.getMarketplacePkgs({ page: 1, 'per-page': 100 }, url)).pipe( - tap(() => this.onPackages(name)), + this.serverInfo$.pipe( + switchMap(({ id }) => + from(this.getMarketplaceData({ 'server-id': id }, url)), + ), ), ), - shareReplay(), - catchError(e => this.errToast.present(e) && of([])), + map(({ categories }) => categories), ) + private readonly pkg$: Observable = + this.altMarketplaceData$.pipe( + switchMap(data => + this.serverInfo$.pipe( + switchMap(info => + from( + this.getMarketplacePkgs( + { page: 1, 'per-page': 100 }, + this.toMarketplace(data).url, + info['eos-version-compat'], + ), + ).pipe(tap(() => this.onPackages(data))), + ), + ), + ), + catchError(e => { + this.errToast.present(e) + + return of([]) + }), + shareReplay({ bufferSize: 1, refCount: true }), + ) + constructor( private readonly api: ApiService, private readonly patch: PatchDbService, @@ -68,22 +93,29 @@ export class MarketplaceService extends AbstractMarketplaceService { } getMarketplace(): Observable { - return this.init$ + return this.marketplace$ } getCategories(): Observable { - return this.data$.pipe(map(({ categories }) => categories)) + return this.categories$ } getPackages(): Observable { return this.pkg$ } - getPackage(id: string, version: string): Observable { + getPackage(id: string, version: string): Observable { const params = { ids: [{ id, version }] } - const fallback$ = this.init$.pipe( - take(1), - switchMap(({ url }) => from(this.getMarketplacePkgs(params, url))), + const fallback$ = this.marketplace$.pipe( + switchMap(({ url }) => + this.serverInfo$.pipe( + switchMap(info => + from( + this.getMarketplacePkgs(params, url, info['eos-version-compat']), + ), + ), + ), + ), map(pkgs => this.findPackage(pkgs, id, version)), startWith(null), ) @@ -91,23 +123,29 @@ export class MarketplaceService extends AbstractMarketplaceService { return this.getPackages().pipe( map(pkgs => this.findPackage(pkgs, id, version)), switchMap(pkg => (pkg ? of(pkg) : fallback$)), - tap(pkg => { + filter((pkg): pkg is MarketplacePkg | null => { if (pkg === undefined) { throw new Error(`No results for ${id}${version ? ' ' + version : ''}`) } + + return true }), ) } getReleaseNotes(id: string): Observable> { if (this.notes.has(id)) { - return of(this.notes.get(id)) + return of(this.notes.get(id) || {}) } - return this.init$.pipe( + return this.marketplace$.pipe( switchMap(({ url }) => this.loadReleaseNotes(id, url)), tap(response => this.notes.set(id, response)), - catchError(e => this.errToast.present(e) && of({})), + catchError(e => { + this.errToast.present(e) + + return of({}) + }), ) } @@ -173,22 +211,19 @@ export class MarketplaceService extends AbstractMarketplaceService { async getMarketplacePkgs( params: Omit, url: string, + eosVersionCompat: string, ): Promise { if (params.query) delete params.category if (params.ids) params.ids = JSON.stringify(params.ids) const qp: RR.GetMarketplacePackagesReq = { ...params, - 'eos-version-compat': this.serverInfo['eos-version-compat'], + 'eos-version-compat': eosVersionCompat, } return this.api.marketplaceProxy('/package/v0/index', qp, url) } - private get serverInfo(): ServerInfo { - return this.patch.getData()['server-info'] - } - private loadReleaseNotes( id: string, url: string, @@ -202,15 +237,15 @@ export class MarketplaceService extends AbstractMarketplaceService { ) } - private onPackages(name: string) { - const { marketplace } = this.patch.getData().ui + private onPackages(data?: UIMarketplaceData) { + const { name } = this.toMarketplace(data) - if (!marketplace?.['selected-id']) { + if (!data?.['selected-id']) { return } - const selectedId = marketplace['selected-id'] - const knownHosts = marketplace['known-hosts'] + const selectedId = data['selected-id'] + const knownHosts = data['known-hosts'] if (knownHosts[selectedId].name !== name) { this.api.setDbValue({ @@ -220,6 +255,12 @@ export class MarketplaceService extends AbstractMarketplaceService { } } + private toMarketplace(marketplace?: UIMarketplaceData): Marketplace { + return marketplace?.['selected-id'] + ? marketplace['known-hosts'][marketplace['selected-id']] + : this.config.marketplace + } + private findPackage( pkgs: readonly MarketplacePkg[], id: string, diff --git a/frontend/projects/ui/src/app/services/patch-db/data-model.ts b/frontend/projects/ui/src/app/services/patch-db/data-model.ts index 41a38da12..98f06f7ca 100644 --- a/frontend/projects/ui/src/app/services/patch-db/data-model.ts +++ b/frontend/projects/ui/src/app/services/patch-db/data-model.ts @@ -17,15 +17,13 @@ export interface UIData { 'auto-check-updates': boolean 'pkg-order': string[] 'ack-welcome': string // EOS version - marketplace: UIMarketplaceData - dev: DevData - gaming: - | { - snake: { - 'high-score': number - } - } - | undefined + marketplace?: UIMarketplaceData + dev?: DevData + gaming?: { + snake: { + 'high-score': number + } + } } export interface UIMarketplaceData { @@ -113,7 +111,7 @@ export interface CurrentDependencyInfo { 'health-checks': string[] // array of health check IDs } -export interface Manifest extends MarketplaceManifest { +export interface Manifest extends MarketplaceManifest { main: ActionImpl 'health-checks': Record< string, @@ -124,7 +122,7 @@ export interface Manifest extends MarketplaceManifest { 'min-os-version': string interfaces: Record backup: BackupActions - migrations: Migrations + migrations: Migrations | null actions: Record permissions: any // @TODO 0.3.1 } @@ -155,8 +153,8 @@ export enum DockerIoFormat { } export interface ConfigActions { - get: ActionImpl - set: ActionImpl + get: ActionImpl | null + set: ActionImpl | null } export type Volume = VolumeData @@ -229,7 +227,7 @@ export interface Action { warning: string | null implementation: ActionImpl 'allowed-statuses': (PackageMainStatus.Stopped | PackageMainStatus.Running)[] - 'input-spec': ConfigSpec + 'input-spec': ConfigSpec | null } export interface Status { 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 0a3709208..bbc832a8e 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 @@ -36,8 +36,8 @@ export function realSourceFactory( { defaultView }: Document, ): Source[] { const { patchDb } = config - const { host } = defaultView.location - const protocol = defaultView.location.protocol === 'http:' ? 'ws' : 'wss' + const host = defaultView?.location.host + const protocol = defaultView?.location.protocol === 'http:' ? 'ws' : 'wss' return [ new WebsocketSource(`${protocol}://${host}/ws/db`), 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 9a233dd65..adf14d229 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 @@ -17,7 +17,7 @@ export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus { let dependency: DependencyStatus | null = null let health: HealthStatus | null = null - if (pkg.state === PackageState.Installed) { + if (pkg.state === PackageState.Installed && pkg.installed) { primary = getPrimaryStatus(pkg.installed.status) dependency = getDependencyStatus(pkg) health = getHealthStatus(pkg.installed.status) @@ -36,9 +36,10 @@ function getPrimaryStatus(status: Status): PrimaryStatus { } } -function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus { +function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus | null { const installed = pkg.installed - if (isEmptyObject(installed['current-dependencies'])) return null + if (!installed || isEmptyObject(installed['current-dependencies'])) + return null const depErrors = installed.status['dependency-errors'] const depIds = Object.keys(depErrors).filter(key => !!depErrors[key]) @@ -46,9 +47,9 @@ function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus { return depIds.length ? DependencyStatus.Warning : DependencyStatus.Satisfied } -function getHealthStatus(status: Status): HealthStatus { +function getHealthStatus(status: Status): HealthStatus | null { if (status.main.status !== PackageMainStatus.Running || !status.main.health) { - return + return null } const values = Object.values(status.main.health) 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 d9a6032fc..8d1fc4f5f 100644 --- a/frontend/projects/ui/src/app/services/server-config.service.ts +++ b/frontend/projects/ui/src/app/services/server-config.service.ts @@ -16,7 +16,10 @@ export class ServerConfigService { private readonly embassyApi: ApiService, ) {} - async presentAlert(key: string, current?: any): Promise { + async presentAlert( + key: string, + current?: any, + ): Promise { const spec = serverConfig[key] let inputs: AlertInput[] @@ -66,7 +69,7 @@ export class ServerConfigService { ] break default: - return + return null } const alert = await this.alertCtrl.create({ diff --git a/frontend/projects/ui/src/app/services/ui-launcher.service.ts b/frontend/projects/ui/src/app/services/ui-launcher.service.ts index 267fb7b52..d6cb16cf3 100644 --- a/frontend/projects/ui/src/app/services/ui-launcher.service.ts +++ b/frontend/projects/ui/src/app/services/ui-launcher.service.ts @@ -13,7 +13,7 @@ export class UiLauncherService { ) {} launch(pkg: PackageDataEntry): void { - this.document.defaultView.open( + this.document.defaultView?.open( this.config.launchableURL(pkg), '_blank', 'noreferrer', diff --git a/frontend/projects/ui/src/app/util/get-project-id.ts b/frontend/projects/ui/src/app/util/get-project-id.ts new file mode 100644 index 000000000..85d4e52f4 --- /dev/null +++ b/frontend/projects/ui/src/app/util/get-project-id.ts @@ -0,0 +1,11 @@ +import { ActivatedRoute } from '@angular/router' + +export function getProjectId({ snapshot }: ActivatedRoute): string { + const projectId = snapshot.paramMap.get('projectId') + + if (!projectId) { + throw new Error('projectId is missing from route params') + } + + return projectId +} diff --git a/frontend/projects/ui/src/app/util/package-loading-progress.ts b/frontend/projects/ui/src/app/util/package-loading-progress.ts index 566103c0f..4eb0353e2 100644 --- a/frontend/projects/ui/src/app/util/package-loading-progress.ts +++ b/frontend/projects/ui/src/app/util/package-loading-progress.ts @@ -3,9 +3,9 @@ import { InstallProgress } from 'src/app/types/install-progress' import { ProgressData } from 'src/app/types/progress-data' export function packageLoadingProgress( - loadData: InstallProgress, + loadData?: InstallProgress, ): ProgressData | null { - if (isEmptyObject(loadData)) { + if (!loadData || isEmptyObject(loadData)) { return null } @@ -20,6 +20,7 @@ export function packageLoadingProgress( } = loadData // only permit 100% when "complete" == true + size = size || 0 downloaded = downloadComplete ? size : Math.max(downloaded - 1, 0) validated = validationComplete ? size : Math.max(validated - 1, 0) unpacked = unpackComplete ? size : Math.max(unpacked - 1, 0) diff --git a/frontend/projects/ui/src/app/util/parse-data-model.ts b/frontend/projects/ui/src/app/util/parse-data-model.ts index e1d6aef24..21fdc91ba 100644 --- a/frontend/projects/ui/src/app/util/parse-data-model.ts +++ b/frontend/projects/ui/src/app/util/parse-data-model.ts @@ -5,12 +5,12 @@ import { } from 'src/app/services/patch-db/data-model' export function parseDataModel(data: DataModel): ParsedData { - const all = JSON.parse(JSON.stringify(data['package-data'])) as { - [id: string]: PackageDataEntry - } + const all: Record = JSON.parse( + JSON.stringify(data['package-data']), + ) const order = [...(data.ui['pkg-order'] || [])] - const pkgs = [] + const pkgs: PackageDataEntry[] = [] const recoveredPkgs = Object.entries(data['recovered-packages']) .filter(([id, _]) => !all[id]) .map(([id, val]) => ({ diff --git a/frontend/projects/ui/src/app/util/properties.util.ts b/frontend/projects/ui/src/app/util/properties.util.ts index 5fcad320e..8d8b15619 100644 --- a/frontend/projects/ui/src/app/util/properties.util.ts +++ b/frontend/projects/ui/src/app/util/properties.util.ts @@ -29,7 +29,7 @@ const matchPropertiesV1 = shape( qr: boolean, }, ['description', 'copyable', 'qr'], - { description: null as null, copyable: false, qr: false } as const, + { copyable: false, qr: false } as const, ) type PropertiesV1 = typeof matchPropertiesV1._TYPE @@ -49,7 +49,6 @@ const matchPackagePropertyString = shape( }, ['description', 'copyable', 'qr', 'masked'], { - description: null as null, copyable: false, qr: false, masked: false, @@ -100,16 +99,16 @@ export function parsePropertiesPermissive( name, value: { value: String(value), - description: null, copyable: false, qr: false, masked: false, }, })) .reduce((acc, { name, value }) => { - acc[name] = value + // TODO: Fix type + acc[name] = value as any return acc - }, {}) + }, {} as PackageProperties) } switch (properties.version) { case 1: diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 0355e72f3..e3b08335e 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -8,13 +8,14 @@ "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, "alwaysStrict": true, - // "strictNullChecks": true, + "strictNullChecks": true, "strictBindCallApply": true, "strictFunctionTypes": true, // "strictPropertyInitialization": true, - // "noImplicitAny": true, + "noImplicitAny": true, "noImplicitThis": true, "useUnknownInCatchVariables": true, diff --git a/patch-db b/patch-db index 8e7c893d4..a3aa82184 160000 --- a/patch-db +++ b/patch-db @@ -1 +1 @@ -Subproject commit 8e7c893d48f03bb1a38b826b680ea77d0e109825 +Subproject commit a3aa821847e6fd9fc1be573ecda9cb3c5f722f97