refactor: extract loading status to shared library (#2282)

* refactor: extract loading status to shared library

* chore: remove inline style
This commit is contained in:
Alex Inkin
2023-05-26 00:04:47 +04:00
committed by Aiden McClelland
parent 52c0bb5302
commit 3804a46f3b
27 changed files with 320 additions and 218 deletions

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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 { }

View File

@@ -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 {}

View File

@@ -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>

View File

@@ -1,12 +0,0 @@
ion-card-title {
font-size: 42px;
}
.logs-container {
margin-top: 24px;
height: 280px;
text-align: left;
overflow: hidden;
border-radius: 31px;
margin-inline: 10px;
}

View File

@@ -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'
}
}
}

View File

@@ -1,11 +0,0 @@
<ion-content
[scrollEvents]="true"
(ionScroll)="handleScroll($event)"
(ionScrollEnd)="handleScrollEnd()"
class="ion-padding"
color="light"
>
<div id="container"></div>
</ion-content>
<div id="template"></div>

View File

@@ -1,10 +0,0 @@
// Hide scrollbar for Chrome, Safari and Opera
ion-content::part(scroll)::-webkit-scrollbar {
display: none;
}
// Hide scrollbar for IE, Edge and Firefox
ion-content::part(scroll) {
-ms-overflow-style: none; // IE and Edge
scrollbar-width: none; // Firefox
}

View File

@@ -1,74 +0,0 @@
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 { TuiDestroyService } from '@taiga-ui/cdk'
var Convert = require('ansi-to-html')
var convert = new Convert({
bg: 'transparent',
})
@Component({
selector: 'logs-window',
templateUrl: 'logs-window.component.html',
styleUrls: ['logs-window.component.scss'],
providers: [TuiDestroyService],
})
export class LogsWindowComponent {
@ViewChild(IonContent)
private content?: IonContent
autoScroll = true
constructor(
private readonly api: ApiService,
private readonly destroy$: TuiDestroyService,
) {}
ngOnInit() {
from(this.api.followLogs())
.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)
}
}),
),
),
takeUntil(this.destroy$),
)
.subscribe()
}
handleScroll(e: any) {
if (e.detail.deltaY < 0) this.autoScroll = false
}
async handleScrollEnd() {
const elem = await this.content?.getScrollElement()
if (elem && elem.scrollHeight - elem.scrollTop - elem.clientHeight < 64) {
this.autoScroll = true
}
}
private convertToAnsi(log: Log) {
return `<span style="color: #FFF; font-weight: bold;">${toLocalIsoString(
new Date(log.timestamp),
)}</span>&nbsp;&nbsp;${convert.toHtml(log.message)}<br />`
}
}

View File

@@ -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

View File

@@ -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() {

View File

@@ -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(),

View File

@@ -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({