mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
refactor: extract loading status to shared library (#2282)
* refactor: extract loading status to shared library * chore: remove inline style
This commit is contained in:
committed by
Aiden McClelland
parent
52c0bb5302
commit
3804a46f3b
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 { }
|
||||
@@ -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 {}
|
||||
|
||||
@@ -1,40 +1,5 @@
|
||||
<ion-content>
|
||||
<ion-grid>
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col class="ion-text-center">
|
||||
<ion-card
|
||||
*ngIf="{ decimal: progress$ | async } as progress"
|
||||
color="dark"
|
||||
>
|
||||
<ion-card-header>
|
||||
<ion-card-title>Initializing StartOS</ion-card-title>
|
||||
<div class="center-wrapper">
|
||||
<ion-card-subtitle *ngIf="progress.decimal as decimal">
|
||||
Progress: {{ (decimal * 100).toFixed(0)}}%
|
||||
</ion-card-subtitle>
|
||||
</div>
|
||||
</ion-card-header>
|
||||
|
||||
<ion-card-content class="ion-margin">
|
||||
<ion-progress-bar
|
||||
color="tertiary"
|
||||
style="
|
||||
max-width: 700px;
|
||||
margin: auto;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 40px;
|
||||
"
|
||||
[type]="progress.decimal && progress.decimal < 1 ? 'determinate' : 'indeterminate'"
|
||||
[value]="progress.decimal || 0"
|
||||
></ion-progress-bar>
|
||||
<p>{{ progress.decimal | toMessage }}</p>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
|
||||
<div class="logs-container">
|
||||
<logs-window></logs-window>
|
||||
</div>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-content>
|
||||
<app-loading
|
||||
class="ion-page"
|
||||
[setupType]="stateService.setupType"
|
||||
(finished)="navCtrl.navigateForward('/success')"
|
||||
></app-loading>
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<StatusRes> // setup.status
|
||||
abstract getSetupStatus(): Promise<SetupStatus | null> // setup.status
|
||||
abstract getPubKey(): Promise<void> // setup.get-pubkey
|
||||
abstract getDrives(): Promise<DiskListResponse> // setup.disk.list
|
||||
abstract verifyCifs(cifs: CifsRecoverySource): Promise<StartOSDiskInfo> // setup.cifs.verify
|
||||
@@ -14,7 +20,9 @@ export abstract class ApiService {
|
||||
abstract complete(): Promise<CompleteRes> // setup.complete
|
||||
abstract exit(): Promise<void> // setup.exit
|
||||
abstract followLogs(): Promise<string> // setup.logs.follow
|
||||
abstract openLogsWebsocket$(guid: string): Observable<Log>
|
||||
abstract openLogsWebsocket$(
|
||||
config: WebSocketSubjectConfig<Log>,
|
||||
): Observable<Log>
|
||||
|
||||
async encrypt(toEncrypt: string): Promise<Encrypted> {
|
||||
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
|
||||
|
||||
@@ -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<StatusRes>({
|
||||
async getSetupStatus() {
|
||||
return this.rpcRequest<SetupStatus | null>({
|
||||
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<Log> {
|
||||
return webSocket(`http://start.local/ws/${guid}`)
|
||||
openLogsWebsocket$({ url }: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||
return webSocket(`http://start.local/ws/${url}`)
|
||||
}
|
||||
|
||||
async complete() {
|
||||
|
||||
@@ -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<Log> {
|
||||
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||
return interval(500).pipe(
|
||||
map(() => ({
|
||||
timestamp: new Date().toISOString(),
|
||||
|
||||
@@ -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<number>(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<void> {
|
||||
await this.api.attach({
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<ion-content>
|
||||
<ion-grid>
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col class="ion-text-center">
|
||||
<ion-card *tuiLet="progress$ | async as progress" color="dark">
|
||||
<ion-card-header>
|
||||
<ion-card-title>Initializing StartOS</ion-card-title>
|
||||
<div class="center-wrapper">
|
||||
<ion-card-subtitle *ngIf="progress">
|
||||
Progress: {{ (progress * 100).toFixed(0) }}%
|
||||
</ion-card-subtitle>
|
||||
</div>
|
||||
</ion-card-header>
|
||||
|
||||
<ion-card-content class="ion-margin">
|
||||
<ion-progress-bar
|
||||
color="tertiary"
|
||||
class="progress"
|
||||
[type]="
|
||||
progress && progress < 1 ? 'determinate' : 'indeterminate'
|
||||
"
|
||||
[value]="progress || 0"
|
||||
></ion-progress-bar>
|
||||
<p>{{ getMessage(progress) }}</p>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
|
||||
<div class="logs-container">
|
||||
<logs-window></logs-window>
|
||||
</div>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-content>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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) {
|
||||
25
frontend/projects/shared/src/mocks/get-setup-status.ts
Normal file
25
frontend/projects/shared/src/mocks/get-setup-status.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { SetupStatus } from '../types/api'
|
||||
import { pauseFor } from '../util/misc.util'
|
||||
|
||||
let tries: number | undefined
|
||||
|
||||
export async function getSetupStatusMock(): Promise<SetupStatus | null> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
30
frontend/projects/shared/src/services/setup-logs.service.ts
Normal file
30
frontend/projects/shared/src/services/setup-logs.service.ts
Normal file
@@ -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<string>
|
||||
openLogsWebsocket$: (config: WebSocketSubjectConfig<Log>) => Observable<Log>
|
||||
}
|
||||
|
||||
export function provideSetupLogsService(
|
||||
api: Constructor<Api>,
|
||||
): StaticClassProvider {
|
||||
return {
|
||||
provide: SetupLogsService,
|
||||
deps: [api],
|
||||
useClass: SetupLogsService,
|
||||
}
|
||||
}
|
||||
|
||||
export class SetupLogsService extends Observable<Log> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
59
frontend/projects/shared/src/services/setup.service.ts
Normal file
59
frontend/projects/shared/src/services/setup.service.ts
Normal file
@@ -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<ConstructorParameters<typeof SetupService>[0]>,
|
||||
): StaticClassProvider {
|
||||
return {
|
||||
provide: SetupService,
|
||||
deps: [api],
|
||||
useClass: SetupService,
|
||||
}
|
||||
}
|
||||
|
||||
export class SetupService extends Observable<number> {
|
||||
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<SetupStatus | null> },
|
||||
) {
|
||||
super(subscriber => this.progress$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
1
frontend/projects/shared/src/types/constructor.ts
Normal file
1
frontend/projects/shared/src/types/constructor.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type Constructor<T> = abstract new (...args: any[]) => T
|
||||
@@ -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<RR.SideloadPacakgeRes>
|
||||
|
||||
abstract getSetupStatus(): Promise<SetupStatus | null>
|
||||
}
|
||||
|
||||
@@ -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<DataModel>,
|
||||
) {
|
||||
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<string> {
|
||||
return this.rpcRequest({ method: 'setup.logs.follow', params: {} })
|
||||
}
|
||||
|
||||
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||
return this.openWebsocket(config)
|
||||
}
|
||||
@@ -498,6 +505,13 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async getSetupStatus() {
|
||||
return this.rpcRequest<SetupStatus | null>({
|
||||
method: 'setup.status',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
private openWebsocket<T>(config: WebSocketSubjectConfig<T>): Observable<T> {
|
||||
const { location } = this.document.defaultView!
|
||||
const protocol = location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
|
||||
@@ -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<void> {
|
||||
const progress = { ...PROGRESS }
|
||||
const phases = [
|
||||
|
||||
Reference in New Issue
Block a user