From 015131f198b1df2197ce1c906cb365444ef14294 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 15 Aug 2024 08:05:37 -0600 Subject: [PATCH] address comments and more --- web/package-lock.json | 17 -- .../show/screenshots/screenshots.component.ts | 2 +- .../src/app/pages/loading.page.ts | 2 +- .../src/app/services/api.service.ts | 9 +- .../src/app/services/live-api.service.ts | 17 +- .../src/app/services/mock-api.service.ts | 159 ++++++++++-------- .../shared/src/services/setup-logs.service.ts | 7 +- .../shared/src/util/format-progress.ts | 2 +- .../routes/initializing/initializing.page.ts | 2 +- .../components/interfaces/interface.utils.ts | 95 +++-------- .../portal/components/logs/logs.pipe.ts | 19 +-- .../service/routes/interface.component.ts | 28 +-- .../backups/components/status.component.ts | 11 +- .../system/backups/modals/target.component.ts | 13 +- .../routes/system/metrics/metrics.service.ts | 32 +++- .../routes/interfaces/ui.component.ts | 36 ++-- .../ui/src/app/services/api/api.fixures.ts | 41 +++-- .../ui/src/app/services/api/api.types.ts | 104 ++++++------ .../app/services/api/embassy-api.service.ts | 22 +-- .../services/api/embassy-live-api.service.ts | 34 +--- .../services/api/embassy-mock-api.service.ts | 39 +---- .../ui/src/app/services/api/mock-patch.ts | 3 +- .../src/app/services/notification.service.ts | 17 +- .../src/app/services/patch-db/data-model.ts | 1 + 24 files changed, 316 insertions(+), 396 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 804a514d0..8cacf6e07 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -5254,23 +5254,6 @@ "rxjs": ">=6.0.0" } }, - "node_modules/@taiga-ui/experimental": { - "version": "4.0.0-rc.7", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.3" - }, - "peerDependencies": { - "@angular/common": ">=16.0.0", - "@angular/core": ">=16.0.0", - "@taiga-ui/addon-commerce": "^4.0.0-rc.7", - "@taiga-ui/cdk": "^4.0.0-rc.7", - "@taiga-ui/core": "^4.0.0-rc.7", - "@taiga-ui/kit": "^4.0.0-rc.7", - "@taiga-ui/polymorpheus": "^4.6.4", - "rxjs": ">=7.0.0" - } - }, "node_modules/@taiga-ui/i18n": { "version": "4.0.0-rc.7", "license": "Apache-2.0", diff --git a/web/projects/marketplace/src/pages/show/screenshots/screenshots.component.ts b/web/projects/marketplace/src/pages/show/screenshots/screenshots.component.ts index b40c967e6..45de00ae3 100644 --- a/web/projects/marketplace/src/pages/show/screenshots/screenshots.component.ts +++ b/web/projects/marketplace/src/pages/show/screenshots/screenshots.component.ts @@ -14,7 +14,7 @@ import { PolymorpheusContent } from '@taiga-ui/polymorpheus' @Component({ selector: 'marketplace-package-screenshots', template: ` - +
- this.api.openProgressWebsocket$(guid).pipe( + this.api.openWebsocket$(guid).pipe( startWith(progress), catchError((_, watch$) => interval(2000).pipe( diff --git a/web/projects/setup-wizard/src/app/services/api.service.ts b/web/projects/setup-wizard/src/app/services/api.service.ts index 912f2378f..b880ad6e2 100644 --- a/web/projects/setup-wizard/src/app/services/api.service.ts +++ b/web/projects/setup-wizard/src/app/services/api.service.ts @@ -2,9 +2,7 @@ import * as jose from 'node-jose' import { DiskInfo, DiskListResponse, - FollowLogsReq, FollowLogsRes, - Log, PartitionInfo, StartOSDiskInfo, } from '@start9labs/shared' @@ -25,11 +23,8 @@ export abstract class ApiService { abstract execute(setupInfo: T.SetupExecuteParams): Promise // setup.execute abstract complete(): Promise // setup.complete abstract exit(): Promise // setup.exit - abstract followServerLogs(params: FollowLogsReq): Promise // setup.logs.follow - abstract openLogsWebsocket$( - config: WebSocketSubjectConfig, - ): Observable - abstract openProgressWebsocket$(guid: string): Observable + abstract followServerLogs(): Promise // setup.logs.follow + abstract openWebsocket$(guid: string): Observable async encrypt(toEncrypt: string): Promise { if (!this.pubkey) throw new Error('No pubkey found!') diff --git a/web/projects/setup-wizard/src/app/services/live-api.service.ts b/web/projects/setup-wizard/src/app/services/live-api.service.ts index 797a8a176..84cf8c34b 100644 --- a/web/projects/setup-wizard/src/app/services/live-api.service.ts +++ b/web/projects/setup-wizard/src/app/services/live-api.service.ts @@ -3,11 +3,9 @@ import { Inject, Injectable } from '@angular/core' import { DiskListResponse, encodeBase64, - FollowLogsReq, FollowLogsRes, HttpService, isRpcError, - Log, RpcError, RPCOptions, StartOSDiskInfo, @@ -15,7 +13,7 @@ import { import { T } from '@start9labs/start-sdk' import * as jose from 'node-jose' import { Observable } from 'rxjs' -import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket' +import { webSocket } from 'rxjs/webSocket' import { ApiService } from './api.service' @Injectable({ @@ -29,12 +27,13 @@ export class LiveApiService extends ApiService { super() } - openProgressWebsocket$(guid: string): Observable { + openWebsocket$(guid: string): Observable { const { location } = this.document.defaultView! + const protocol = location.protocol === 'http:' ? 'ws' : 'wss' const host = location.host return webSocket({ - url: `ws://${host}/ws/rpc/${guid}`, + url: `${protocol}://${host}/ws/rpc/${guid}`, }) } @@ -99,12 +98,8 @@ export class LiveApiService extends ApiService { }) } - async followServerLogs(params: FollowLogsReq): Promise { - return this.rpcRequest({ method: 'setup.logs.follow', params }) - } - - openLogsWebsocket$({ url }: WebSocketSubjectConfig): Observable { - return webSocket(`http://start.local/ws/${url}`) + async followServerLogs(): Promise { + return this.rpcRequest({ method: 'setup.logs.follow', params: {} }) } async complete(): Promise { diff --git a/web/projects/setup-wizard/src/app/services/mock-api.service.ts b/web/projects/setup-wizard/src/app/services/mock-api.service.ts index ed8435ddf..c71cc8ef4 100644 --- a/web/projects/setup-wizard/src/app/services/mock-api.service.ts +++ b/web/projects/setup-wizard/src/app/services/mock-api.service.ts @@ -2,16 +2,13 @@ import { Injectable } from '@angular/core' import { DiskListResponse, encodeBase64, - FollowLogsReq, FollowLogsRes, - Log, pauseFor, StartOSDiskInfo, } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' import * as jose from 'node-jose' -import { interval, map, Observable, of } from 'rxjs' -import { WebSocketSubjectConfig } from 'rxjs/webSocket' +import { interval, map, Observable } from 'rxjs' import { ApiService } from './api.service' @Injectable({ @@ -47,68 +44,102 @@ export class MockApiService extends ApiService { // websocket - openProgressWebsocket$(guid: string): Observable { - return of(PROGRESS) - // const numPhases = PROGRESS.phases.length + // oldMockProgress$(): Promise { + // const numPhases = PROGRESS.phases.length - // return of(PROGRESS).pipe( - // switchMap(full => - // from(PROGRESS.phases).pipe( - // mergeScan((full, phase, i) => { - // if ( - // !phase.progress || - // typeof phase.progress !== 'object' || - // !phase.progress.total - // ) { - // full.phases[i].progress = true + // return of(PROGRESS).pipe( + // switchMap(full => + // from(PROGRESS.phases).pipe( + // mergeScan((full, phase, i) => { + // if ( + // !phase.progress || + // typeof phase.progress !== 'object' || + // !phase.progress.total + // ) { + // full.phases[i].progress = true - // if ( - // full.overall && - // typeof full.overall === 'object' && - // full.overall.total - // ) { - // const step = full.overall.total / numPhases - // full.overall.done += step - // } + // if ( + // full.overall && + // typeof full.overall === 'object' && + // full.overall.total + // ) { + // const step = full.overall.total / numPhases + // full.overall.done += step + // } - // return of(full).pipe(delay(2000)) - // } else { - // const total = phase.progress.total - // const step = total / 4 - // let done = phase.progress.done + // return of(full).pipe(delay(2000)) + // } else { + // const total = phase.progress.total + // const step = total / 4 + // let done = phase.progress.done - // return interval(1000).pipe( - // takeWhile(() => done < total), - // map(() => { - // done += step + // return interval(1000).pipe( + // takeWhile(() => done < total), + // map(() => { + // done += step - // console.error(done) + // console.error(done) - // if ( - // full.overall && - // typeof full.overall === 'object' && - // full.overall.total - // ) { - // const step = full.overall.total / numPhases / 4 + // if ( + // full.overall && + // typeof full.overall === 'object' && + // full.overall.total + // ) { + // const step = full.overall.total / numPhases / 4 - // full.overall.done += step - // } + // full.overall.done += step + // } - // if (done === total) { - // full.phases[i].progress = true + // if (done === total) { + // full.phases[i].progress = true - // if (i === numPhases - 1) { - // full.overall = true - // } - // } - // return full - // }), - // ) - // } - // }, full), - // ), - // ), - // ) + // if (i === numPhases - 1) { + // full.overall = true + // } + // } + // return full + // }), + // ) + // } + // }, full), + // ), + // ), + // ) + // } + + openWebsocket$(guid: string): Observable { + if (guid === 'logs-guid') { + return interval(500).pipe( + map(() => ({ + timestamp: new Date().toISOString(), + message: 'fake log entry', + bootId: 'boot-id', + })), + ) as Observable + } else if (guid === 'progress-guid') { + // @TODO mock progress + return interval(1000).pipe( + map(() => ({ + overall: true, + phases: [ + { + name: 'Preparing Data', + progress: true, + }, + { + name: 'Transferring Data', + progress: true, + }, + { + name: 'Finalizing Setup', + progress: true, + }, + ], + })), + ) as Observable + } else { + throw new Error('invalid guid type') + } } private statusIndex = 0 @@ -270,24 +301,14 @@ export class MockApiService extends ApiService { } } - async followServerLogs(params: FollowLogsReq): Promise { + async followServerLogs(): Promise { await pauseFor(1000) return { startCursor: 'fakestartcursor', - guid: 'fake-guid', + guid: 'logs-guid', } } - openLogsWebsocket$(config: WebSocketSubjectConfig): Observable { - return interval(500).pipe( - map(() => ({ - timestamp: new Date().toISOString(), - message: 'fake log entry', - bootId: 'boot-id', - })), - ) - } - async complete(): Promise { await pauseFor(1000) return { diff --git a/web/projects/shared/src/services/setup-logs.service.ts b/web/projects/shared/src/services/setup-logs.service.ts index 3d27811ce..a142ef8c0 100644 --- a/web/projects/shared/src/services/setup-logs.service.ts +++ b/web/projects/shared/src/services/setup-logs.service.ts @@ -1,13 +1,12 @@ import { StaticClassProvider } from '@angular/core' import { bufferTime, defer, map, Observable, scan, switchMap } from 'rxjs' -import { WebSocketSubjectConfig } from 'rxjs/webSocket' import { FollowLogsReq, FollowLogsRes, Log } from '../types/api' import { Constructor } from '../types/constructor' import { convertAnsi } from '../util/convert-ansi' interface Api { followServerLogs: (params: FollowLogsReq) => Promise - openLogsWebsocket$: (config: WebSocketSubjectConfig) => Observable + openWebsocket$: (guid: string) => Observable } export function provideSetupLogsService( @@ -22,9 +21,7 @@ export function provideSetupLogsService( export class SetupLogsService extends Observable { private readonly log$ = defer(() => this.api.followServerLogs({})).pipe( - switchMap(({ guid }) => - this.api.openLogsWebsocket$({ url: `/rpc/${guid}` }), - ), + switchMap(({ guid }) => this.api.openWebsocket$(guid)), bufferTime(1000), map(convertAnsi), scan((logs: readonly string[], log) => [...logs, log], []), diff --git a/web/projects/shared/src/util/format-progress.ts b/web/projects/shared/src/util/format-progress.ts index 8007657f8..a625e57f4 100644 --- a/web/projects/shared/src/util/format-progress.ts +++ b/web/projects/shared/src/util/format-progress.ts @@ -1,4 +1,4 @@ -// @TODO Matt this is T.FullProgress but shared does not depend on sdk +// @TODO get types from sdk type Progress = null | boolean | { done: number; total: number | null } type NamedProgress = { name: string; progress: Progress } type FullProgress = { overall: Progress; phases: Array } diff --git a/web/projects/ui/src/app/routes/initializing/initializing.page.ts b/web/projects/ui/src/app/routes/initializing/initializing.page.ts index 338eef83d..5a2a85bfa 100644 --- a/web/projects/ui/src/app/routes/initializing/initializing.page.ts +++ b/web/projects/ui/src/app/routes/initializing/initializing.page.ts @@ -33,7 +33,7 @@ export default class InitializingPage { private readonly state = inject(StateService) readonly progress = toSignal( - defer(() => from(this.api.initGetProgress())).pipe( + defer(() => from(this.api.initFollowProgress())).pipe( switchMap(({ guid, progress }) => this.api .openWebsocket$(guid, {}) diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts index 356c11ac8..e61b37b52 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts @@ -1,4 +1,4 @@ -import { CB, CT, T } from '@start9labs/start-sdk' +import { CB, CT, T, utils } from '@start9labs/start-sdk' import { TuiDialogOptions } from '@taiga-ui/core' import { TuiConfirmData } from '@taiga-ui/kit' import { NetworkInfo } from 'src/app/services/patch-db/data-model' @@ -53,75 +53,44 @@ export type AddressDetails = { url: string } -// @TODO Matt these types have change significantly -export function getAddresses(serviceInterface: any): { - // T.ServiceInterface): { +export function getMultihostAddresses( + serviceInterface: T.ServiceInterface, + host: T.Host, +): { clearnet: AddressDetails[] local: AddressDetails[] tor: AddressDetails[] } { - const host = serviceInterface.hostInfo const addressInfo = serviceInterface.addressInfo - const username = addressInfo.username ? addressInfo.username + '@' : '' - const suffix = addressInfo.suffix || '' - - const hostnames = - host.kind === 'multi' - ? host.hostnames - : host.hostname - ? [host.hostname] - : [] + const hostnamesInfo = host.hostnameInfo[addressInfo.internalPort] const clearnet: AddressDetails[] = [] const local: AddressDetails[] = [] const tor: AddressDetails[] = [] - hostnames.forEach((h: any) => { - let scheme = '' - let port = '' - - if (h.hostname.sslPort) { - port = h.hostname.sslPort === 443 ? '' : `:${h.hostname.sslPort}` - scheme = addressInfo.bindOptions.addSsl?.scheme - ? `${addressInfo.bindOptions.addSsl.scheme}://` - : '' - } - - if (h.hostname.port) { - port = h.hostname.port === 80 ? '' : `:${h.hostname.port}` - scheme = addressInfo.bindOptions.scheme - ? `${addressInfo.bindOptions.scheme}://` - : '' - } - - if (h.kind === 'onion') { - tor.push({ - label: h.hostname.sslPort ? 'HTTPS' : 'HTTP', - url: toHref(scheme, username, h.hostname.value, port, suffix), - }) - } else { - const hostnameKind = h.hostname.kind - - if (hostnameKind === 'domain') { - tor.push({ - url: toHref( - scheme, - username, - `${h.hostname.subdomain}.${h.hostname.domain}`, - port, - suffix, - ), - }) + hostnamesInfo.forEach(hostnameInfo => { + utils.addressHostToUrl(addressInfo, hostnameInfo).forEach(url => { + // Onion + if (hostnameInfo.kind === 'onion') { + tor.push({ url }) + // IP } else { - local.push({ - label: - hostnameKind === 'local' - ? 'Local' - : `${h.networkInterfaceId} (${hostnameKind})`, - url: toHref(scheme, username, h.hostname.value, port, suffix), - }) + // Domain + if (hostnameInfo.hostname.kind === 'domain') { + clearnet.push({ url }) + // Local + } else { + const hostnameKind = hostnameInfo.hostname.kind + local.push({ + label: + hostnameKind === 'local' + ? 'Local' + : `${hostnameInfo.networkInterfaceId} (${hostnameKind})`, + url, + }) + } } - } + }) }) return { @@ -130,13 +99,3 @@ export function getAddresses(serviceInterface: any): { tor, } } - -function toHref( - scheme: string, - username: string, - hostname: string, - port: string, - suffix: string, -): string { - return `${scheme}${username}${hostname}${port}${suffix}` -} diff --git a/web/projects/ui/src/app/routes/portal/components/logs/logs.pipe.ts b/web/projects/ui/src/app/routes/portal/components/logs/logs.pipe.ts index 8fdd43169..94f909b6e 100644 --- a/web/projects/ui/src/app/routes/portal/components/logs/logs.pipe.ts +++ b/web/projects/ui/src/app/routes/portal/components/logs/logs.pipe.ts @@ -1,5 +1,5 @@ import { inject, Pipe, PipeTransform } from '@angular/core' -import { convertAnsi, toLocalIsoString } from '@start9labs/shared' +import { convertAnsi, Log, toLocalIsoString } from '@start9labs/shared' import { bufferTime, catchError, @@ -44,7 +44,13 @@ export class LogsPipe implements PipeTransform { ), defer(() => followLogs(this.options)).pipe( tap(r => this.logs.setCursor(r.startCursor)), - switchMap(r => this.api.openLogsWebsocket$(this.toConfig(r.guid))), + switchMap(r => + this.api.openWebsocket$(r.guid, { + openObserver: { + next: () => this.logs.status$.next('connected'), + }, + }), + ), bufferTime(1000), filter(logs => !!logs.length), map(convertAnsi), @@ -67,15 +73,6 @@ export class LogsPipe implements PipeTransform { private get options() { return this.logs.status$.value === 'connected' ? { limit: 400 } : {} } - - private toConfig(guid: string) { - return { - url: `/rpc/${guid}`, - openObserver: { - next: () => this.logs.status$.next('connected'), - }, - } - } } function getMessage(success: boolean): string { diff --git a/web/projects/ui/src/app/routes/portal/routes/service/routes/interface.component.ts b/web/projects/ui/src/app/routes/portal/routes/service/routes/interface.component.ts index 530aa886c..ad92d4553 100644 --- a/web/projects/ui/src/app/routes/portal/routes/service/routes/interface.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/service/routes/interface.component.ts @@ -3,17 +3,17 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { getPkgId } from '@start9labs/shared' import { PatchDB } from 'patch-db-client' -import { map } from 'rxjs' +import { combineLatest, map } from 'rxjs' import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component' import { DataModel } from 'src/app/services/patch-db/data-model' -import { getAddresses } from '../../../components/interfaces/interface.utils' +import { getMultihostAddresses } from '../../../components/interfaces/interface.utils' @Component({ template: ` `, changeDetection: ChangeDetectionStrategy.OnPush, @@ -22,23 +22,25 @@ import { getAddresses } from '../../../components/interfaces/interface.utils' }) export class ServiceInterfaceRoute { private readonly route = inject(ActivatedRoute) + private readonly patch = inject>(PatchDB) readonly context = { packageId: getPkgId(this.route), interfaceId: this.route.snapshot.paramMap.get('interfaceId') || '', } - readonly interfaceInfo$ = inject>(PatchDB) - .watch$( + readonly interfacesWithAddresses$ = combineLatest([ + this.patch.watch$( 'packageData', this.context.packageId, 'serviceInterfaces', this.context.interfaceId, - ) - .pipe( - map(info => ({ - ...info, - addresses: getAddresses(info), - })), - ) + ), + this.patch.watch$('packageData', this.context.packageId, 'hosts'), + ]).pipe( + map(([iFace, hosts]) => ({ + ...iFace, + addresses: getMultihostAddresses(iFace, hosts[iFace.addressInfo.hostId]), + })), + ) } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/backups/components/status.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/backups/components/status.component.ts index 769ad69d1..541be1833 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/backups/components/status.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/backups/components/status.component.ts @@ -23,6 +23,7 @@ import { BackupType } from '../types/backup-type' export class BackupsStatusComponent { private readonly exver = inject(Exver) + @Input({ required: true }) serverId!: string @Input({ required: true }) type!: BackupType @Input({ required: true }) target!: BackupTarget @@ -61,8 +62,12 @@ export class BackupsStatusComponent { } private get hasBackup(): boolean { - return !!this.target.startOs - // @TODO Matt types changed - // && this.exver.compareExver(this.target.startOs.version, '0.3.0') !== -1 + return ( + this.target.startOs[this.serverId] && + this.exver.compareOsVersion( + this.target.startOs[this.serverId].version, + '0.3.6', + ) !== 'less' + ) } } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/backups/modals/target.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/backups/modals/target.component.ts index 91aae471a..b5cb932b7 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/backups/modals/target.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/backups/modals/target.component.ts @@ -23,6 +23,9 @@ import { BackupsStatusComponent } from '../components/status.component' import { GetDisplayInfoPipe } from '../pipes/get-display-info.pipe' import { BackupType } from '../types/backup-type' import { TARGETS } from './targets.component' +import { getServerInfo } from 'src/app/utils/get-server-info' +import { PatchDB } from 'patch-db-client' +import { DataModel } from 'src/app/services/patch-db/data-model' @Component({ template: ` @@ -40,7 +43,12 @@ import { TARGETS } from './targets.component'
{{ displayInfo.name }} - +
{{ displayInfo.description }}
@@ -69,6 +77,7 @@ export class BackupsTargetModal { private readonly dialogs = inject(TuiDialogService) private readonly errorService = inject(ErrorService) private readonly api = inject(ApiService) + private readonly patch = inject(PatchDB) readonly context = inject>( @@ -81,10 +90,12 @@ export class BackupsTargetModal { ? 'Loading Backup Targets' : 'Loading Backup Sources' + serverId = '' targets: BackupTarget[] = [] async ngOnInit() { try { + this.serverId = (await getServerInfo(this.patch)).id this.targets = (await this.api.getBackupTargets({})).saved } catch (e: any) { this.errorService.handleError(e) diff --git a/web/projects/ui/src/app/routes/portal/routes/system/metrics/metrics.service.ts b/web/projects/ui/src/app/routes/portal/routes/system/metrics/metrics.service.ts index c6780fe3b..6ff818bef 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/metrics/metrics.service.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/metrics/metrics.service.ts @@ -1,18 +1,32 @@ import { inject, Injectable } from '@angular/core' -import { Observable, retry, shareReplay } from 'rxjs' -import { Metrics } from 'src/app/services/api/api.types' +import { + defer, + Observable, + retry, + shareReplay, + startWith, + switchMap, +} from 'rxjs' +import { ServerMetrics } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' @Injectable({ providedIn: 'root', }) -export class MetricsService extends Observable { - // @TODO get real url, 'rpc/{guid}' or something like that - private readonly metrics$ = inject(ApiService) - .openMetricsWebsocket$({ - url: '', - }) - .pipe(retry(), shareReplay(1)) +export class MetricsService extends Observable { + private readonly api = inject(ApiService) + + // @TODO Alex do we need to use defer? I am unsure when this is necessary. + private readonly metrics$ = defer(() => + this.api.followServerMetrics({}), + ).pipe( + switchMap(({ guid, metrics }) => + this.api.openWebsocket$(guid).pipe(startWith(metrics)), + ), + // @TODO Alex how to handle failure and reconnection here? Simple retry() will not work. Seems like we need a general solution for reconnecting websockets: patchDB, logs, metrics, progress, and any future. Reconnection should depend on server state, then we need to get a new guid, then reconnect. Similar to how patchDB websocket currently behaves on disconnect/reconnect. + retry(), + shareReplay(), + ) constructor() { super(subscriber => this.metrics$.subscribe(subscriber)) diff --git a/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/interfaces/ui.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/interfaces/ui.component.ts index 0791ab082..9368421ad 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/interfaces/ui.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/settings/routes/interfaces/ui.component.ts @@ -7,7 +7,7 @@ import { InterfaceComponent, ServiceInterfaceWithAddresses, } from 'src/app/routes/portal/components/interfaces/interface.component' -import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils' +import { getMultihostAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils' import { DataModel } from 'src/app/services/patch-db/data-model' @Component({ @@ -29,7 +29,6 @@ export class StartOsUiComponent { .watch$('serverInfo', 'ui') .pipe( map(hosts => { - // @TODO Matt fix types const serviceInterface: T.ServiceInterface = { id: 'startos-ui', name: 'StartOS UI', @@ -41,31 +40,26 @@ export class StartOsUiComponent { addressInfo: { hostId: '', username: null, + internalPort: 80, + scheme: 'http', + sslScheme: 'https', suffix: '', - bindOptions: { - scheme: 'http', - preferredExternalPort: 80, - addSsl: { - scheme: 'https', - preferredExternalPort: 443, - // @TODO is this alpn correct? - alpn: { specified: ['http/1.1', 'h2'] }, - }, - secure: { - ssl: false, - }, - }, }, - hostInfo: { - id: 'start-os-ui-host', - kind: 'multi', - hostnames: hosts, + } + + // @TODO Aiden confirm this is correct + const host: T.Host = { + kind: 'multi', + bindings: {}, + hostnameInfo: { + 80: hosts, }, - } as any + addresses: [], + } return { ...serviceInterface, - addresses: getAddresses(serviceInterface), + addresses: getMultihostAddresses(serviceInterface, host), } }), ) diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index 05c4770ca..bcc5a1837 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -3,12 +3,7 @@ import { PackageDataEntry, ServerStatusInfo, } from 'src/app/services/patch-db/data-model' -import { - Metrics, - NotificationLevel, - RR, - ServerNotifications, -} from './api.types' +import { RR, ServerMetrics, ServerNotifications } from './api.types' import { BTC_ICON, LND_ICON, PROXY_ICON, REGISTRY_ICON } from './api-icons' import { Log } from '@start9labs/shared' import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' @@ -748,7 +743,7 @@ export module Mock { packageId: null, createdAt: '2019-12-26T14:20:30.872Z', code: 1, - level: NotificationLevel.Success, + level: 'success', title: 'Backup Complete', message: 'StartOS and services have been successfully backed up.', data: { @@ -769,7 +764,7 @@ export module Mock { packageId: null, createdAt: '2019-12-26T14:20:30.872Z', code: 2, - level: NotificationLevel.Warning, + level: 'warning', title: 'SSH Key Added', message: 'A new SSH key was added. If you did not do this, shit is bad.', data: null, @@ -780,7 +775,7 @@ export module Mock { packageId: null, createdAt: '2019-12-26T14:20:30.872Z', code: 3, - level: NotificationLevel.Info, + level: 'info', title: 'SSH Key Removed', message: 'A SSH key was removed.', data: null, @@ -791,7 +786,7 @@ export module Mock { packageId: 'bitcoind', createdAt: '2019-12-26T14:20:30.872Z', code: 4, - level: NotificationLevel.Error, + level: 'error', title: 'Service Crashed', message: new Array(3) .fill( @@ -806,7 +801,7 @@ export module Mock { }, ] - export function getMetrics(): Metrics { + export function getMetrics(): ServerMetrics { return { general: { temperature: { @@ -1020,8 +1015,16 @@ export module Mock { path: '/Desktop/embassy-backups', username: 'TestUser', mountable: false, - // @TODO Matt Provide mock for startOs - startOs: {}, + startOs: { + abcdefgh: { + hostname: 'adjective-noun.local', + version: '0.3.6', + timestamp: new Date().toISOString(), + passwordHash: + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: '', + }, + }, }, { id: 'ftcvewdnkemfksdm', @@ -1054,8 +1057,16 @@ export module Mock { vendor: 'SSK', mountable: true, path: '/HomeFolder/Documents', - // @TODO Matt Provide mock for startOs - startOs: {}, + startOs: { + 'different-server': { + hostname: 'different-server.local', + version: '0.3.6', + timestamp: new Date().toISOString(), + passwordHash: + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: '', + }, + }, }, ], } diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index 70cd7a34e..5efcf2bbf 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -61,7 +61,7 @@ export module RR { // init - export type InitGetProgressRes = { + export type InitFollowProgressRes = { progress: T.FullProgress guid: string } @@ -88,10 +88,10 @@ export module RR { guid: string } - export type GetServerMetricsReq = {} // server.metrics - export type GetServerMetricsRes = { + export type FollowServerMetricsReq = {} // server.metrics.follow + export type FollowServerMetricsRes = { guid: string - metrics: Metrics + metrics: ServerMetrics } export type UpdateServerReq = { registry: string } // server.update @@ -339,9 +339,11 @@ export module RR { export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow export type FollowPackageLogsRes = FollowServerLogsRes - export type GetPackageMetricsReq = { id: string } // package.metrics - // @TODO Matt create package metrics type - export type GetPackageMetricsRes = any + export type FollowPackageMetricsReq = { id: string } // package.metrics.follow + export type FollowPackageMetricsRes = { + guid: string + metrics: AppMetrics + } export type InstallPackageReq = T.InstallParams export type InstallPackageRes = null @@ -415,25 +417,6 @@ export module RR { } // package.proxy.set-outbound export type SetServiceOutboundProxyRes = null - // marketplace - - export type GetMarketplaceInfoReq = { serverId: string } - // @TODO Matt fix type - export type GetMarketplaceInfoRes = any - - export type GetMarketplaceEosReq = { serverId: string } - // @TODO Matt fix type - export type GetMarketplaceEosRes = any - - export type GetMarketplacePackagesReq = { - ids?: { id: string; version: string }[] - // iff !ids - category?: string - query?: string - page?: number - perPage?: number - } - // registry /** these are returned in ASCENDING order. the newest available version will be the LAST in the object */ @@ -443,34 +426,34 @@ export module RR { export type CheckOSUpdateRes = OSUpdate } -export interface OSUpdate { +export type OSUpdate = { version: string headline: string releaseNotes: { [version: string]: string } } -export interface Breakages { +export type Breakages = { [id: string]: TaggedDependencyError } -export interface TaggedDependencyError { +export type TaggedDependencyError = { dependency: string error: DependencyError } -export interface ActionResponse { +export type ActionResponse = { message: string value: string | null copyable: boolean qr: boolean } -interface MetricData { +type MetricData = { value: string unit: string } -export interface Metrics { +export type ServerMetrics = { general: { temperature: MetricData | null } @@ -498,14 +481,28 @@ export interface Metrics { } } -export interface Session { +export type AppMetrics = { + memory: { + percentageUsed: MetricData + used: MetricData + } + cpu: { + percentageUsed: MetricData + } + disk: { + percentageUsed: MetricData + used: MetricData + } +} + +export type Session = { loggedIn: string lastActive: string userAgent: string metadata: SessionMetadata } -export interface SessionMetadata { +export type SessionMetadata = { platforms: PlatformType[] } @@ -565,7 +562,7 @@ export interface CloudBackupTarget extends BaseBackupTarget { provider: 'dropbox' | 'google-drive' } -export interface BackupRun { +export type BackupRun = { id: string startedAt: string completedAt: string @@ -574,7 +571,7 @@ export interface BackupRun { report: BackupReport } -export interface BackupJob { +export type BackupJob = { id: string name: string target: BackupTarget @@ -582,7 +579,7 @@ export interface BackupJob { packageIds: string[] } -export interface BackupInfo { +export type BackupInfo = { version: string timestamp: string packageBackups: { @@ -590,18 +587,18 @@ export interface BackupInfo { } } -export interface PackageBackupInfo { +export type PackageBackupInfo = { title: string version: string osVersion: string timestamp: string } -export interface ServerSpecs { +export type ServerSpecs = { [key: string]: string | number } -export interface SSHKey { +export type SSHKey = { createdAt: string alg: string hostname: string @@ -610,32 +607,25 @@ export interface SSHKey { export type ServerNotifications = ServerNotification[] -export interface ServerNotification { +export type ServerNotification = { id: number packageId: string | null createdAt: string code: T - level: NotificationLevel + level: 'success' | 'info' | 'warning' | 'error' title: string message: string data: NotificationData read: boolean } -export enum NotificationLevel { - Success = 'success', - Info = 'info', - Warning = 'warning', - Error = 'error', -} - export type NotificationData = T extends 0 ? null : T extends 1 ? BackupReport : any -export interface BackupReport { +export type BackupReport = { server: { attempted: boolean error: string | null @@ -647,7 +637,7 @@ export interface BackupReport { } } -export interface AvailableWifi { +export type AvailableWifi = { ssid: string strength: number security: string[] @@ -682,29 +672,29 @@ export type DependencyError = | DependencyErrorHealthChecksFailed | DependencyErrorTransitive -export interface DependencyErrorNotInstalled { +export type DependencyErrorNotInstalled = { type: 'notInstalled' } -export interface DependencyErrorNotRunning { +export type DependencyErrorNotRunning = { type: 'notRunning' } -export interface DependencyErrorIncorrectVersion { +export type DependencyErrorIncorrectVersion = { type: 'incorrectVersion' expected: string // version range received: string // version } -export interface DependencyErrorConfigUnsatisfied { +export type DependencyErrorConfigUnsatisfied = { type: 'configUnsatisfied' } -export interface DependencyErrorHealthChecksFailed { +export type DependencyErrorHealthChecksFailed = { type: 'healthChecksFailed' check: T.HealthCheckResult } -export interface DependencyErrorTransitive { +export type DependencyErrorTransitive = { type: 'transitive' } diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts index e10f60592..45ae8ce10 100644 --- a/web/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts @@ -3,11 +3,10 @@ import { GetPackagesRes, MarketplacePkg, } from '@start9labs/marketplace' -import { Log, RPCOptions } from '@start9labs/shared' +import { RPCOptions } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' import { Observable } from 'rxjs' -import { WebSocketSubjectConfig } from 'rxjs/webSocket' -import { BackupTargetType, Metrics, RR } from './api.types' +import { BackupTargetType, RR } from './api.types' export abstract class ApiService { // http @@ -32,7 +31,7 @@ export abstract class ApiService { abstract openWebsocket$( guid: string, - config: RR.WebsocketConfig, + config?: RR.WebsocketConfig, ): Observable // state @@ -78,20 +77,13 @@ export abstract class ApiService { // init - abstract initGetProgress(): Promise + abstract initFollowProgress(): Promise abstract initFollowLogs( params: RR.FollowServerLogsReq, ): Promise // server - abstract openLogsWebsocket$( - config: WebSocketSubjectConfig, - ): Observable - - abstract openMetricsWebsocket$( - config: WebSocketSubjectConfig, - ): Observable abstract getSystemTime( params: RR.GetSystemTimeReq, @@ -119,9 +111,9 @@ export abstract class ApiService { params: RR.FollowServerLogsReq, ): Promise - abstract getServerMetrics( - params: RR.GetServerMetricsReq, - ): Promise + abstract followServerMetrics( + params: RR.FollowServerMetricsReq, + ): Promise abstract updateServer(url?: string): Promise diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index 4b12c0f38..615d11743 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -3,16 +3,15 @@ import { HttpOptions, HttpService, isRpcError, - Log, Method, RpcError, RPCOptions, } from '@start9labs/shared' import { PATCH_CACHE } from 'src/app/services/patch-db/patch-db-source' import { ApiService } from './embassy-api.service' -import { BackupTargetType, Metrics, RR } from './api.types' +import { BackupTargetType, RR } from './api.types' import { ConfigService } from '../config.service' -import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket' +import { webSocket } from 'rxjs/webSocket' import { Observable, filter, firstValueFrom } from 'rxjs' import { AuthService } from '../auth.service' import { DOCUMENT } from '@angular/common' @@ -93,20 +92,10 @@ export class LiveApiService extends ApiService { } // websocket - // @TODO Matt which of these 2 APIs should we use? - private openWebsocket(config: WebSocketSubjectConfig): Observable { - const { location } = this.document.defaultView! - const protocol = location.protocol === 'http:' ? 'ws' : 'wss' - const host = location.host - - config.url = `${protocol}://${host}/ws${config.url}` - - return webSocket(config) - } openWebsocket$( guid: string, - config: RR.WebsocketConfig, + config: RR.WebsocketConfig = {}, ): Observable { const { location } = this.document.defaultView! const protocol = location.protocol === 'http:' ? 'ws' : 'wss' @@ -210,7 +199,7 @@ export class LiveApiService extends ApiService { // init - async initGetProgress(): Promise { + async initFollowProgress(): Promise { return this.rpcRequest({ method: 'init.subscribe', params: {} }) } @@ -221,15 +210,6 @@ export class LiveApiService extends ApiService { } // server - openLogsWebsocket$(config: WebSocketSubjectConfig): Observable { - return this.openWebsocket(config) - } - - openMetricsWebsocket$( - config: WebSocketSubjectConfig, - ): Observable { - return this.openWebsocket(config) - } async getSystemTime( params: RR.GetSystemTimeReq, @@ -271,9 +251,9 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'net.tor.logs.follow', params }) } - async getServerMetrics( - params: RR.GetServerMetricsReq, - ): Promise { + async followServerMetrics( + params: RR.FollowServerMetricsReq, + ): Promise { return this.rpcRequest({ method: 'server.metrics', params }) } diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 6a7a56606..43c012b8f 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -15,7 +15,7 @@ import { StateInfo, UpdatingState, } from 'src/app/services/patch-db/data-model' -import { BackupTargetType, Metrics, RR } from './api.types' +import { BackupTargetType, RR } from './api.types' import { Mock } from './api.fixures' import { from, @@ -35,7 +35,6 @@ import { GetPackagesRes, MarketplacePkg, } from '@start9labs/marketplace' -import { WebSocketSubjectConfig } from 'rxjs/webSocket' import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md' const PROGRESS: T.FullProgress = { @@ -84,8 +83,8 @@ export class MockApiService extends ApiService { async uploadPackage(guid: string, body: Blob): Promise { await pauseFor(2000) - // @TODO Matt what should this return? - return '' + // @TODO Aiden confirm this is correct + return 'success' } async uploadFile(body: Blob): Promise { @@ -258,7 +257,7 @@ export class MockApiService extends ApiService { // init - async initGetProgress(): Promise { + async initFollowProgress(): Promise { await pauseFor(250) return { progress: PROGRESS, @@ -276,30 +275,6 @@ export class MockApiService extends ApiService { // server - openLogsWebsocket$(config: WebSocketSubjectConfig): Observable { - return interval(50).pipe( - map((_, index) => { - // mock fire open observer - if (index === 0) config.openObserver?.next(new Event('')) - if (index === 100) throw new Error('HAAHHA') - return Mock.ServerLogs[0] - }), - ) - } - - openMetricsWebsocket$( - config: WebSocketSubjectConfig, - ): Observable { - return interval(2000).pipe( - map((_, index) => { - // mock fire open observer - if (index === 0) config.openObserver?.next(new Event('')) - if (index === 4) throw new Error('HAHAHA') - return Mock.getMetrics() - }), - ) - } - async getSystemTime( params: RR.GetSystemTimeReq, ): Promise { @@ -386,9 +361,9 @@ export class MockApiService extends ApiService { return logs } - async getServerMetrics( - params: RR.GetServerMetricsReq, - ): Promise { + async followServerMetrics( + params: RR.FollowServerMetricsReq, + ): Promise { await pauseFor(2000) return { guid: 'iqudh37um-i38u3-34-a51b-jkhd783ein', diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 8a3cf90f5..6759590a2 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -130,8 +130,7 @@ export const mockPatchData: DataModel = { password: '', }, platform: 'x86_64-nonfree', - // @TODO Matt zram needs to be added? - // zram: true, + zram: true, governor: 'performance', passwordHash: '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', diff --git a/web/projects/ui/src/app/services/notification.service.ts b/web/projects/ui/src/app/services/notification.service.ts index a1c421426..d9ce24897 100644 --- a/web/projects/ui/src/app/services/notification.service.ts +++ b/web/projects/ui/src/app/services/notification.service.ts @@ -2,7 +2,6 @@ import { inject, Injectable } from '@angular/core' import { ErrorService } from '@start9labs/shared' import { TuiDialogService } from '@taiga-ui/core' import { - NotificationLevel, ServerNotification, ServerNotifications, } from 'src/app/services/api/api.types' @@ -63,13 +62,13 @@ export class NotificationService { getColor(notification: ServerNotification): string { switch (notification.level) { - case NotificationLevel.Info: + case 'info': return 'var(--tui-status-info)' - case NotificationLevel.Success: + case 'success': return 'var(--tui-status-positive)' - case NotificationLevel.Warning: + case 'warning': return 'var(--tui-status-warning)' - case NotificationLevel.Error: + case 'error': return 'var(--tui-status-negative)' default: return '' @@ -78,12 +77,12 @@ export class NotificationService { getIcon(notification: ServerNotification): string { switch (notification.level) { - case NotificationLevel.Info: + case 'info': return '@tui.info' - case NotificationLevel.Success: + case 'success': return '@tui.circle-check' - case NotificationLevel.Warning: - case NotificationLevel.Error: + case 'warning': + case 'error': return '@tui.circle-alert' default: return '' diff --git a/web/projects/ui/src/app/services/patch-db/data-model.ts b/web/projects/ui/src/app/services/patch-db/data-model.ts index d5a3f66f5..bffd5af36 100644 --- a/web/projects/ui/src/app/services/patch-db/data-model.ts +++ b/web/projects/ui/src/app/services/patch-db/data-model.ts @@ -56,6 +56,7 @@ export type ServerInfo = { platform: string arch: string governor: string | null + zram: boolean } export type NetworkInfo = {