diff --git a/frontend/projects/setup-wizard/src/app/app.component.ts b/frontend/projects/setup-wizard/src/app/app.component.ts index e4bf41f5c..b821e089d 100644 --- a/frontend/projects/setup-wizard/src/app/app.component.ts +++ b/frontend/projects/setup-wizard/src/app/app.component.ts @@ -17,7 +17,7 @@ export class AppComponent { async ngOnInit() { try { - const inProgress = await this.apiService.getStatus() + const inProgress = await this.apiService.getSetupStatus() let route = '/home' if (inProgress) { diff --git a/frontend/projects/setup-wizard/src/app/app.module.ts b/frontend/projects/setup-wizard/src/app/app.module.ts index f2ba59012..0f48d072d 100644 --- a/frontend/projects/setup-wizard/src/app/app.module.ts +++ b/frontend/projects/setup-wizard/src/app/app.module.ts @@ -18,7 +18,12 @@ import { HomePageModule } from './pages/home/home.module' import { LoadingPageModule } from './pages/loading/loading.module' import { RecoverPageModule } from './pages/recover/recover.module' import { TransferPageModule } from './pages/transfer/transfer.module' -import { RELATIVE_URL, WorkspaceConfig } from '@start9labs/shared' +import { + provideSetupLogsService, + provideSetupService, + RELATIVE_URL, + WorkspaceConfig, +} from '@start9labs/shared' const { useMocks, @@ -43,6 +48,8 @@ const { TuiRootModule, ], providers: [ + provideSetupService(ApiService), + provideSetupLogsService(ApiService), { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, { provide: ApiService, diff --git a/frontend/projects/setup-wizard/src/app/pages/loading/loading-routing.module.ts b/frontend/projects/setup-wizard/src/app/pages/loading/loading-routing.module.ts deleted file mode 100644 index d2b8628c4..000000000 --- a/frontend/projects/setup-wizard/src/app/pages/loading/loading-routing.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NgModule } from '@angular/core' -import { RouterModule, Routes } from '@angular/router' -import { LoadingPage } from './loading.page' - -const routes: Routes = [ - { - path: '', - component: LoadingPage, - }, -] - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class LoadingPageRoutingModule { } diff --git a/frontend/projects/setup-wizard/src/app/pages/loading/loading.module.ts b/frontend/projects/setup-wizard/src/app/pages/loading/loading.module.ts index e0b42e6b8..9c7ae1bc9 100644 --- a/frontend/projects/setup-wizard/src/app/pages/loading/loading.module.ts +++ b/frontend/projects/setup-wizard/src/app/pages/loading/loading.module.ts @@ -1,13 +1,17 @@ import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { FormsModule } from '@angular/forms' -import { LoadingPage, ToMessagePipe } from './loading.page' -import { LogsWindowComponent } from './logs-window/logs-window.component' -import { LoadingPageRoutingModule } from './loading-routing.module' +import { RouterModule, Routes } from '@angular/router' +import { LoadingModule } from '@start9labs/shared' +import { LoadingPage } from './loading.page' + +const routes: Routes = [ + { + path: '', + component: LoadingPage, + }, +] @NgModule({ - imports: [CommonModule, FormsModule, IonicModule, LoadingPageRoutingModule], - declarations: [LoadingPage, ToMessagePipe, LogsWindowComponent], + imports: [LoadingModule, RouterModule.forChild(routes)], + declarations: [LoadingPage], }) export class LoadingPageModule {} diff --git a/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.html b/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.html index 8fffbf343..559705a7f 100644 --- a/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.html +++ b/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.html @@ -1,40 +1,5 @@ - - - - - - - Initializing StartOS - - - Progress: {{ (decimal * 100).toFixed(0)}}% - - - - - - - {{ progress.decimal | toMessage }} - - - - - - - - - - + diff --git a/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.ts b/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.ts index fd09c84d1..f88c23e86 100644 --- a/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.ts @@ -1,51 +1,13 @@ import { Component } from '@angular/core' import { NavController } from '@ionic/angular' import { StateService } from 'src/app/services/state.service' -import { Pipe, PipeTransform } from '@angular/core' @Component({ - selector: 'app-loading', templateUrl: 'loading.page.html', - styleUrls: ['loading.page.scss'], }) export class LoadingPage { - readonly progress$ = this.stateService.dataProgress$ - constructor( - private readonly stateService: StateService, - private readonly navCtrl: NavController, + readonly stateService: StateService, + readonly navCtrl: NavController, ) {} - - ngOnInit() { - this.stateService.pollDataTransferProgress() - const progSub = this.stateService.dataCompletionSubject$.subscribe( - async complete => { - if (complete) { - progSub.unsubscribe() - await this.navCtrl.navigateForward(`/success`) - } - }, - ) - } -} - -@Pipe({ - name: 'toMessage', -}) -export class ToMessagePipe implements PipeTransform { - constructor(private readonly stateService: StateService) {} - - transform(progress: number | null): string { - if (['fresh', 'attach'].includes(this.stateService.setupType || '')) { - return 'Setting up your server' - } - - if (!progress) { - return 'Preparing data. This can take a while' - } else if (progress < 1) { - return 'Copying data' - } else { - return 'Finalizing' - } - } } diff --git a/frontend/projects/setup-wizard/src/app/services/api/api.service.ts b/frontend/projects/setup-wizard/src/app/services/api/api.service.ts index cac52e3c9..0b94cd39d 100644 --- a/frontend/projects/setup-wizard/src/app/services/api/api.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/api/api.service.ts @@ -1,11 +1,17 @@ import * as jose from 'node-jose' -import { DiskListResponse, StartOSDiskInfo, Log } from '@start9labs/shared' +import { + DiskListResponse, + StartOSDiskInfo, + Log, + SetupStatus, +} from '@start9labs/shared' import { Observable } from 'rxjs' +import { WebSocketSubjectConfig } from 'rxjs/webSocket' export abstract class ApiService { pubkey?: jose.JWK.Key - abstract getStatus(): Promise // setup.status + abstract getSetupStatus(): Promise // setup.status abstract getPubKey(): Promise // setup.get-pubkey abstract getDrives(): Promise // setup.disk.list abstract verifyCifs(cifs: CifsRecoverySource): Promise // setup.cifs.verify @@ -14,7 +20,9 @@ export abstract class ApiService { abstract complete(): Promise // setup.complete abstract exit(): Promise // setup.exit abstract followLogs(): Promise // setup.logs.follow - abstract openLogsWebsocket$(guid: string): Observable + abstract openLogsWebsocket$( + config: WebSocketSubjectConfig, + ): Observable async encrypt(toEncrypt: string): Promise { if (!this.pubkey) throw new Error('No pubkey found!') @@ -31,12 +39,6 @@ type Encrypted = { encrypted: string } -export type StatusRes = { - 'bytes-transferred': number - 'total-bytes': number | null - complete: boolean -} | null - export type AttachReq = { guid: string 'embassy-password': Encrypted 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 c218a3a0b..808258015 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 @@ -8,18 +8,18 @@ import { Log, RpcError, RPCOptions, + SetupStatus, } from '@start9labs/shared' import { ApiService, CifsRecoverySource, DiskRecoverySource, - StatusRes, AttachReq, ExecuteReq, CompleteRes, } from './api.service' import * as jose from 'node-jose' -import { webSocket } from 'rxjs/webSocket' +import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket' import { Observable } from 'rxjs' @Injectable({ @@ -30,8 +30,8 @@ export class LiveApiService extends ApiService { super() } - async getStatus() { - return this.rpcRequest({ + async getSetupStatus() { + return this.rpcRequest({ method: 'setup.status', params: {}, }) @@ -94,8 +94,8 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'setup.logs.follow', params: {} }) } - openLogsWebsocket$(guid: string): Observable { - return webSocket(`http://start.local/ws/${guid}`) + openLogsWebsocket$({ url }: WebSocketSubjectConfig): Observable { + return webSocket(`http://start.local/ws/${url}`) } async complete() { diff --git a/frontend/projects/setup-wizard/src/app/services/api/mock-api.service.ts b/frontend/projects/setup-wizard/src/app/services/api/mock-api.service.ts index 301b686c3..c42ef4200 100644 --- a/frontend/projects/setup-wizard/src/app/services/api/mock-api.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/api/mock-api.service.ts @@ -1,5 +1,10 @@ import { Injectable } from '@angular/core' -import { encodeBase64, Log, pauseFor } from '@start9labs/shared' +import { + encodeBase64, + getSetupStatusMock, + Log, + pauseFor, +} from '@start9labs/shared' import { ApiService, CifsRecoverySource, @@ -9,32 +14,14 @@ import { } from './api.service' import * as jose from 'node-jose' import { interval, map, Observable } from 'rxjs' - -let tries: number +import { WebSocketSubjectConfig } from 'rxjs/webSocket' @Injectable({ providedIn: 'root', }) export class MockApiService extends ApiService { - async getStatus() { - const restoreOrMigrate = true - const total = 4 - - await pauseFor(1000) - - if (tries === undefined) { - tries = 0 - return null - } - - tries++ - const progress = tries - 1 - - return { - 'bytes-transferred': restoreOrMigrate ? progress : 0, - 'total-bytes': restoreOrMigrate ? total : null, - complete: progress === total, - } + async getSetupStatus() { + return getSetupStatusMock() } async getPubKey() { @@ -152,7 +139,7 @@ export class MockApiService extends ApiService { return 'fake-guid' } - openLogsWebsocket$(guid: string): Observable { + openLogsWebsocket$(config: WebSocketSubjectConfig): Observable { return interval(500).pipe( map(() => ({ timestamp: new Date().toISOString(), 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 4379ee13d..e70478559 100644 --- a/frontend/projects/setup-wizard/src/app/services/state.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/state.service.ts @@ -1,7 +1,5 @@ import { Injectable } from '@angular/core' -import { BehaviorSubject } from 'rxjs' import { ApiService, RecoverySource } from './api/api.service' -import { pauseFor, ErrorToastService } from '@start9labs/shared' @Injectable({ providedIn: 'root', @@ -12,47 +10,7 @@ export class StateService { recoverySource?: RecoverySource recoveryPassword?: string - dataTransferProgress?: { - bytesTransferred: number - totalBytes: number | null - complete: boolean - } - dataProgress$ = new BehaviorSubject(0) - dataCompletionSubject$ = new BehaviorSubject(false) - - constructor( - private readonly api: ApiService, - private readonly errorToastService: ErrorToastService, - ) {} - - async pollDataTransferProgress() { - await pauseFor(500) - - if (this.dataTransferProgress?.complete) { - this.dataCompletionSubject$.next(true) - return - } - - try { - const progress = await this.api.getStatus() - if (!progress) return - - this.dataTransferProgress = { - bytesTransferred: progress['bytes-transferred'], - totalBytes: progress['total-bytes'], - complete: progress.complete, - } - if (this.dataTransferProgress.totalBytes) { - this.dataProgress$.next( - this.dataTransferProgress.bytesTransferred / - this.dataTransferProgress.totalBytes, - ) - } - } catch (e: any) { - this.errorToastService.present(e) - } - setTimeout(() => this.pollDataTransferProgress(), 0) // prevent call stack from growing - } + constructor(private readonly api: ApiService) {} async importDrive(guid: string, password: string): Promise { await this.api.attach({ diff --git a/frontend/projects/shared/package.json b/frontend/projects/shared/package.json index 1fb22944d..a2bbec95f 100644 --- a/frontend/projects/shared/package.json +++ b/frontend/projects/shared/package.json @@ -9,7 +9,8 @@ "@ng-web-apis/mutation-observer": ">=2.0.0", "@ng-web-apis/resize-observer": ">=2.0.0", "@start9labs/emver": "^0.1.5", - "@taiga-ui/cdk": ">=3.0.0" + "@taiga-ui/cdk": ">=3.0.0", + "ansi-to-html": "^0.7.2" }, "exports": { "./assets/": "./assets/" diff --git a/frontend/projects/shared/src/components/loading/loading.component.html b/frontend/projects/shared/src/components/loading/loading.component.html new file mode 100644 index 000000000..b58ee2f74 --- /dev/null +++ b/frontend/projects/shared/src/components/loading/loading.component.html @@ -0,0 +1,34 @@ + + + + + + + Initializing StartOS + + + Progress: {{ (progress * 100).toFixed(0) }}% + + + + + + + {{ getMessage(progress) }} + + + + + + + + + + diff --git a/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.scss b/frontend/projects/shared/src/components/loading/loading.component.scss similarity index 67% rename from frontend/projects/setup-wizard/src/app/pages/loading/loading.page.scss rename to frontend/projects/shared/src/components/loading/loading.component.scss index c3e40e419..f21705ce5 100644 --- a/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.scss +++ b/frontend/projects/shared/src/components/loading/loading.component.scss @@ -2,6 +2,12 @@ ion-card-title { font-size: 42px; } +.progress { + max-width: 700px; + padding-bottom: 20px; + margin: auto auto 40px; +} + .logs-container { margin-top: 24px; height: 280px; @@ -9,4 +15,4 @@ ion-card-title { overflow: hidden; border-radius: 31px; margin-inline: 10px; -} \ No newline at end of file +} diff --git a/frontend/projects/shared/src/components/loading/loading.component.ts b/frontend/projects/shared/src/components/loading/loading.component.ts new file mode 100644 index 000000000..3207aebb7 --- /dev/null +++ b/frontend/projects/shared/src/components/loading/loading.component.ts @@ -0,0 +1,35 @@ +import { Component, inject, Input, Output } from '@angular/core' +import { delay, filter } from 'rxjs' +import { SetupService } from '../../services/setup.service' + +@Component({ + selector: 'app-loading', + templateUrl: 'loading.component.html', + styleUrls: ['loading.component.scss'], +}) +export class LoadingComponent { + readonly progress$ = inject(SetupService) + + @Input() + setupType?: 'fresh' | 'restore' | 'attach' | 'transfer' + + @Output() + readonly finished = this.progress$.pipe( + filter(progress => progress === 1), + delay(500), + ) + + getMessage(progress: number | null): string { + if (['fresh', 'attach'].includes(this.setupType || '')) { + return 'Setting up your server' + } + + if (!progress) { + return 'Preparing data. This can take a while' + } else if (progress < 1) { + return 'Copying data' + } else { + return 'Finalizing' + } + } +} diff --git a/frontend/projects/shared/src/components/loading/loading.module.ts b/frontend/projects/shared/src/components/loading/loading.module.ts new file mode 100644 index 000000000..1ffcd7e36 --- /dev/null +++ b/frontend/projects/shared/src/components/loading/loading.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { TuiLetModule } from '@taiga-ui/cdk' + +import { LogsWindowComponent } from './logs-window/logs-window.component' +import { LoadingComponent } from './loading.component' + +@NgModule({ + imports: [CommonModule, IonicModule, TuiLetModule], + declarations: [LoadingComponent, LogsWindowComponent], + exports: [LoadingComponent], +}) +export class LoadingModule {} diff --git a/frontend/projects/setup-wizard/src/app/pages/loading/logs-window/logs-window.component.html b/frontend/projects/shared/src/components/loading/logs-window/logs-window.component.html similarity index 100% rename from frontend/projects/setup-wizard/src/app/pages/loading/logs-window/logs-window.component.html rename to frontend/projects/shared/src/components/loading/logs-window/logs-window.component.html diff --git a/frontend/projects/setup-wizard/src/app/pages/loading/logs-window/logs-window.component.scss b/frontend/projects/shared/src/components/loading/logs-window/logs-window.component.scss similarity index 100% rename from frontend/projects/setup-wizard/src/app/pages/loading/logs-window/logs-window.component.scss rename to frontend/projects/shared/src/components/loading/logs-window/logs-window.component.scss diff --git a/frontend/projects/setup-wizard/src/app/pages/loading/logs-window/logs-window.component.ts b/frontend/projects/shared/src/components/loading/logs-window/logs-window.component.ts similarity index 57% rename from frontend/projects/setup-wizard/src/app/pages/loading/logs-window/logs-window.component.ts rename to frontend/projects/shared/src/components/loading/logs-window/logs-window.component.ts index 8f0bda1bb..4378be4af 100644 --- a/frontend/projects/setup-wizard/src/app/pages/loading/logs-window/logs-window.component.ts +++ b/frontend/projects/shared/src/components/loading/logs-window/logs-window.component.ts @@ -1,9 +1,10 @@ import { Component, ViewChild } from '@angular/core' import { IonContent } from '@ionic/angular' -import { from, map, switchMap, takeUntil } from 'rxjs' -import { ApiService } from 'src/app/services/api/api.service' -import { Log, toLocalIsoString } from '@start9labs/shared' +import { map, takeUntil } from 'rxjs' import { TuiDestroyService } from '@taiga-ui/cdk' +import { SetupLogsService } from '../../../services/setup-logs.service' +import { Log } from '../../../types/api' +import { toLocalIsoString } from '../../../util/to-local-iso-string' var Convert = require('ansi-to-html') var convert = new Convert({ @@ -23,36 +24,29 @@ export class LogsWindowComponent { autoScroll = true constructor( - private readonly api: ApiService, + private readonly logs: SetupLogsService, private readonly destroy$: TuiDestroyService, ) {} ngOnInit() { - from(this.api.followLogs()) + this.logs .pipe( - switchMap(guid => - this.api.openLogsWebsocket$(guid).pipe( - map(log => { - const container = document.getElementById('container') - const newLogs = document.getElementById('template')?.cloneNode() - - if (!(newLogs instanceof HTMLElement)) return - - newLogs.innerHTML = this.convertToAnsi(log) - - container?.append(newLogs) - - if (this.autoScroll) { - setTimeout(() => { - this.content?.scrollToBottom(250) - }, 0) - } - }), - ), - ), + map(log => this.convertToAnsi(log)), takeUntil(this.destroy$), ) - .subscribe() + .subscribe(innerHTML => { + const container = document.getElementById('container') + const newLogs = document.getElementById('template')?.cloneNode() + + if (!(newLogs instanceof HTMLElement)) return + + newLogs.innerHTML = innerHTML + container?.append(newLogs) + + if (this.autoScroll) { + setTimeout(() => this.content?.scrollToBottom(250)) + } + }) } handleScroll(e: any) { diff --git a/frontend/projects/shared/src/mocks/get-setup-status.ts b/frontend/projects/shared/src/mocks/get-setup-status.ts new file mode 100644 index 000000000..205ebd3ee --- /dev/null +++ b/frontend/projects/shared/src/mocks/get-setup-status.ts @@ -0,0 +1,25 @@ +import { SetupStatus } from '../types/api' +import { pauseFor } from '../util/misc.util' + +let tries: number | undefined + +export async function getSetupStatusMock(): Promise { + const restoreOrMigrate = true + const total = 4 + + await pauseFor(1000) + + if (tries === undefined) { + tries = 0 + return null + } + + tries++ + const progress = tries - 1 + + return { + 'bytes-transferred': restoreOrMigrate ? progress : 0, + 'total-bytes': restoreOrMigrate ? total : null, + complete: progress === total, + } +} diff --git a/frontend/projects/shared/src/public-api.ts b/frontend/projects/shared/src/public-api.ts index c7399173f..da0632cc1 100644 --- a/frontend/projects/shared/src/public-api.ts +++ b/frontend/projects/shared/src/public-api.ts @@ -9,6 +9,9 @@ export * from './components/alert/alert.component' export * from './components/alert/alert.module' export * from './components/alert/alert-button.directive' export * from './components/alert/alert-input.directive' +export * from './components/loading/logs-window/logs-window.component' +export * from './components/loading/loading.module' +export * from './components/loading/loading.component' export * from './components/markdown/markdown.component' export * from './components/markdown/markdown.component.module' export * from './components/text-spinner/text-spinner.component' @@ -25,6 +28,8 @@ export * from './directives/responsive-col/responsive-col-viewport.directive' export * from './directives/safe-links/safe-links.directive' export * from './directives/safe-links/safe-links.module' +export * from './mocks/get-setup-status' + export * from './pipes/emver/emver.module' export * from './pipes/emver/emver.pipe' export * from './pipes/guid/guid.module' @@ -43,6 +48,8 @@ export * from './services/emver.service' export * from './services/error.service' export * from './services/error-toast.service' export * from './services/http.service' +export * from './services/setup.service' +export * from './services/setup-logs.service' export * from './themes/dark-theme/dark-theme.component' export * from './themes/dark-theme/dark-theme.module' @@ -50,6 +57,7 @@ export * from './themes/light-theme/light-theme.component' export * from './themes/light-theme/light-theme.module' export * from './types/api' +export * from './types/constructor' export * from './types/http.types' export * from './types/rpc.types' export * from './types/url' diff --git a/frontend/projects/shared/src/services/setup-logs.service.ts b/frontend/projects/shared/src/services/setup-logs.service.ts new file mode 100644 index 000000000..1c3182e29 --- /dev/null +++ b/frontend/projects/shared/src/services/setup-logs.service.ts @@ -0,0 +1,30 @@ +import { StaticClassProvider } from '@angular/core' +import { defer, Observable, switchMap } from 'rxjs' +import { WebSocketSubjectConfig } from 'rxjs/webSocket' +import { Log } from '../types/api' +import { Constructor } from '../types/constructor' + +interface Api { + followLogs: () => Promise + openLogsWebsocket$: (config: WebSocketSubjectConfig) => Observable +} + +export function provideSetupLogsService( + api: Constructor, +): StaticClassProvider { + return { + provide: SetupLogsService, + deps: [api], + useClass: SetupLogsService, + } +} + +export class SetupLogsService extends Observable { + private readonly log$ = defer(() => this.api.followLogs()).pipe( + switchMap(url => this.api.openLogsWebsocket$({ url })), + ) + + constructor(private readonly api: Api) { + super(subscriber => this.log$.subscribe(subscriber)) + } +} diff --git a/frontend/projects/shared/src/services/setup.service.ts b/frontend/projects/shared/src/services/setup.service.ts new file mode 100644 index 000000000..f05007869 --- /dev/null +++ b/frontend/projects/shared/src/services/setup.service.ts @@ -0,0 +1,59 @@ +import { inject, StaticClassProvider, Type } from '@angular/core' +import { + catchError, + EMPTY, + exhaustMap, + filter, + from, + interval, + map, + Observable, + shareReplay, + takeWhile, +} from 'rxjs' +import { SetupStatus } from '../types/api' +import { ErrorToastService } from './error-toast.service' +import { Constructor } from '../types/constructor' + +export function provideSetupService( + api: Constructor[0]>, +): StaticClassProvider { + return { + provide: SetupService, + deps: [api], + useClass: SetupService, + } +} + +export class SetupService extends Observable { + private readonly errorToastService = inject(ErrorToastService) + private readonly progress$ = interval(500).pipe( + exhaustMap(() => + from(this.api.getSetupStatus()).pipe( + catchError(e => { + this.errorToastService.present(e) + + return EMPTY + }), + ), + ), + filter(Boolean), + map(progress => { + if (progress.complete) { + return 1 + } + + return progress['total-bytes'] + ? progress['bytes-transferred'] / progress['total-bytes'] + : 0 + }), + takeWhile(value => value !== 1, true), + shareReplay(1), + ) + + constructor( + private readonly api: { getSetupStatus: () => Promise }, + ) { + super(subscriber => this.progress$.subscribe(subscriber)) + } +} diff --git a/frontend/projects/shared/src/types/api.ts b/frontend/projects/shared/src/types/api.ts index 1473dc16a..419e99a07 100644 --- a/frontend/projects/shared/src/types/api.ts +++ b/frontend/projects/shared/src/types/api.ts @@ -41,3 +41,9 @@ export type StartOSDiskInfo = { 'password-hash': string | null 'wrapped-key': string | null } + +export interface SetupStatus { + 'bytes-transferred': number + 'total-bytes': number | null + complete: boolean +} diff --git a/frontend/projects/shared/src/types/constructor.ts b/frontend/projects/shared/src/types/constructor.ts new file mode 100644 index 000000000..ebc945938 --- /dev/null +++ b/frontend/projects/shared/src/types/constructor.ts @@ -0,0 +1 @@ +export type Constructor = abstract new (...args: any[]) => T diff --git a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts index 362eb9c3a..474de4095 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts @@ -2,7 +2,7 @@ import { BehaviorSubject, Observable } from 'rxjs' import { Update } from 'patch-db-client' import { RR, Encrypted, BackupTargetType, Metrics } from './api.types' import { DataModel } from 'src/app/services/patch-db/data-model' -import { Log } from '@start9labs/shared' +import { Log, SetupStatus } from '@start9labs/shared' import { WebSocketSubjectConfig } from 'rxjs/webSocket' import type { JWK } from 'node-jose' @@ -293,4 +293,6 @@ export abstract class ApiService { abstract sideloadPackage( params: RR.SideloadPackageReq, ): Promise + + abstract getSetupStatus(): Promise } diff --git a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts index 61c96cfa4..2194ac0e3 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -8,6 +8,7 @@ import { Method, RpcError, RPCOptions, + SetupStatus, } from '@start9labs/shared' import { ApiService } from './embassy-api.service' import { BackupTargetType, Metrics, RR } from './api.types' @@ -31,7 +32,9 @@ export class LiveApiService extends ApiService { private readonly patch: PatchDB, ) { super() - ; (window as any).rpcClient = this + + // @ts-ignore + this.document.defaultView.rpcClient = this } // for getting static files: ex icons, instructions, licenses @@ -122,6 +125,10 @@ export class LiveApiService extends ApiService { return this.openWebsocket(config) } + async followLogs(): Promise { + return this.rpcRequest({ method: 'setup.logs.follow', params: {} }) + } + openLogsWebsocket$(config: WebSocketSubjectConfig): Observable { return this.openWebsocket(config) } @@ -498,6 +505,13 @@ export class LiveApiService extends ApiService { }) } + async getSetupStatus() { + return this.rpcRequest({ + method: 'setup.status', + params: {}, + }) + } + private openWebsocket(config: WebSocketSubjectConfig): Observable { const { location } = this.document.defaultView! const protocol = location.protocol === 'http:' ? 'ws' : 'wss' 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 70ff824bc..0ab330818 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 @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core' -import { Log, pauseFor } from '@start9labs/shared' +import { pauseFor, Log, getSetupStatusMock } from '@start9labs/shared' import { ApiService } from './embassy-api.service' import { Operation, @@ -999,6 +999,10 @@ export class MockApiService extends ApiService { return '4120e092-05ab-4de2-9fbd-c3f1f4b1df9e' // no significance, randomly generated } + async getSetupStatus() { + return getSetupStatusMock() + } + private async updateProgress(id: string): Promise { const progress = { ...PROGRESS } const phases = [
{{ progress.decimal | toMessage }}
{{ getMessage(progress) }}