Add logs window to setup wizard loading screen (#2076)

* add logs window to setup wizard loading screen

* fix type error

* Update frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts

Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com>

---------

Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com>
This commit is contained in:
Matt Hill
2023-05-09 14:58:50 -06:00
committed by Aiden McClelland
parent e53c90f8f0
commit 873f2b2814
9 changed files with 142 additions and 3 deletions

View File

@@ -3,10 +3,11 @@ 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'
@NgModule({
imports: [CommonModule, FormsModule, IonicModule, LoadingPageRoutingModule],
declarations: [LoadingPage, ToMessagePipe],
declarations: [LoadingPage, ToMessagePipe, LogsWindowComponent],
})
export class LoadingPageModule {}

View File

@@ -30,6 +30,10 @@
<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>

View File

@@ -1,3 +1,12 @@
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

@@ -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,74 @@
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,5 +1,7 @@
import * as jose from 'node-jose'
import { DiskListResponse, StartOSDiskInfo } from '@start9labs/shared'
import { DiskListResponse, StartOSDiskInfo, Log } from '@start9labs/shared'
import { Observable } from 'rxjs'
export abstract class ApiService {
pubkey?: jose.JWK.Key
@@ -11,6 +13,8 @@ export abstract class ApiService {
abstract execute(setupInfo: ExecuteReq): Promise<void> // setup.execute
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>
async encrypt(toEncrypt: string): Promise<Encrypted> {
if (!this.pubkey) throw new Error('No pubkey found!')

View File

@@ -5,6 +5,7 @@ import {
encodeBase64,
HttpService,
isRpcError,
Log,
RpcError,
RPCOptions,
} from '@start9labs/shared'
@@ -18,6 +19,8 @@ import {
CompleteRes,
} from './api.service'
import * as jose from 'node-jose'
import { webSocket } from 'rxjs/webSocket'
import { Observable } from 'rxjs'
@Injectable({
providedIn: 'root',
@@ -87,6 +90,14 @@ export class LiveApiService extends ApiService {
})
}
async followLogs(): Promise<string> {
return this.rpcRequest({ method: 'setup.logs.follow', params: {} })
}
openLogsWebsocket$(guid: string): Observable<Log> {
return webSocket(`http://start.local/ws/${guid}`)
}
async complete() {
const res = await this.rpcRequest<CompleteRes>({
method: 'setup.complete',

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'
import { encodeBase64, pauseFor } from '@start9labs/shared'
import { encodeBase64, Log, pauseFor } from '@start9labs/shared'
import {
ApiService,
CifsRecoverySource,
@@ -8,6 +8,7 @@ import {
CompleteRes,
} from './api.service'
import * as jose from 'node-jose'
import { interval, map, Observable } from 'rxjs'
let tries: number
@@ -146,6 +147,20 @@ export class MockApiService extends ApiService {
await pauseFor(1000)
}
async followLogs(): Promise<string> {
await pauseFor(1000)
return 'fake-guid'
}
openLogsWebsocket$(guid: string): Observable<Log> {
return interval(500).pipe(
map(() => ({
timestamp: new Date().toISOString(),
message: 'fake log entry',
})),
)
}
async complete(): Promise<CompleteRes> {
await pauseFor(1000)
return {