mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +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,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;
|
||||
}
|
||||
@@ -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 +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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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> ${convert.toHtml(log.message)}<br />`
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user