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

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

View File

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

View File

@@ -0,0 +1,18 @@
ion-card-title {
font-size: 42px;
}
.progress {
max-width: 700px;
padding-bottom: 20px;
margin: auto auto 40px;
}
.logs-container {
margin-top: 24px;
height: 280px;
text-align: left;
overflow: hidden;
border-radius: 31px;
margin-inline: 10px;
}

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
<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

@@ -0,0 +1,10 @@
// 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

@@ -0,0 +1,68 @@
import { Component, ViewChild } from '@angular/core'
import { IonContent } from '@ionic/angular'
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({
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 logs: SetupLogsService,
private readonly destroy$: TuiDestroyService,
) {}
ngOnInit() {
this.logs
.pipe(
map(log => this.convertToAnsi(log)),
takeUntil(this.destroy$),
)
.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) {
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

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

View File

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

View 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))
}
}

View 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))
}
}

View File

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

View File

@@ -0,0 +1 @@
export type Constructor<T> = abstract new (...args: any[]) => T