mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
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:
committed by
Aiden McClelland
parent
e53c90f8f0
commit
873f2b2814
@@ -3,10 +3,11 @@ import { CommonModule } from '@angular/common'
|
|||||||
import { IonicModule } from '@ionic/angular'
|
import { IonicModule } from '@ionic/angular'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { LoadingPage, ToMessagePipe } from './loading.page'
|
import { LoadingPage, ToMessagePipe } from './loading.page'
|
||||||
|
import { LogsWindowComponent } from './logs-window/logs-window.component'
|
||||||
import { LoadingPageRoutingModule } from './loading-routing.module'
|
import { LoadingPageRoutingModule } from './loading-routing.module'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule, FormsModule, IonicModule, LoadingPageRoutingModule],
|
imports: [CommonModule, FormsModule, IonicModule, LoadingPageRoutingModule],
|
||||||
declarations: [LoadingPage, ToMessagePipe],
|
declarations: [LoadingPage, ToMessagePipe, LogsWindowComponent],
|
||||||
})
|
})
|
||||||
export class LoadingPageModule {}
|
export class LoadingPageModule {}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@
|
|||||||
<p>{{ progress.decimal | toMessage }}</p>
|
<p>{{ progress.decimal | toMessage }}</p>
|
||||||
</ion-card-content>
|
</ion-card-content>
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
||||||
|
<div class="logs-container">
|
||||||
|
<logs-window></logs-window>
|
||||||
|
</div>
|
||||||
</ion-col>
|
</ion-col>
|
||||||
</ion-row>
|
</ion-row>
|
||||||
</ion-grid>
|
</ion-grid>
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
ion-card-title {
|
ion-card-title {
|
||||||
font-size: 42px;
|
font-size: 42px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logs-container {
|
||||||
|
margin-top: 24px;
|
||||||
|
height: 280px;
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 31px;
|
||||||
|
margin-inline: 10px;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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> ${convert.toHtml(log.message)}<br />`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import * as jose from 'node-jose'
|
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 {
|
export abstract class ApiService {
|
||||||
pubkey?: jose.JWK.Key
|
pubkey?: jose.JWK.Key
|
||||||
|
|
||||||
@@ -11,6 +13,8 @@ export abstract class ApiService {
|
|||||||
abstract execute(setupInfo: ExecuteReq): Promise<void> // setup.execute
|
abstract execute(setupInfo: ExecuteReq): Promise<void> // setup.execute
|
||||||
abstract complete(): Promise<CompleteRes> // setup.complete
|
abstract complete(): Promise<CompleteRes> // setup.complete
|
||||||
abstract exit(): Promise<void> // setup.exit
|
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> {
|
async encrypt(toEncrypt: string): Promise<Encrypted> {
|
||||||
if (!this.pubkey) throw new Error('No pubkey found!')
|
if (!this.pubkey) throw new Error('No pubkey found!')
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
encodeBase64,
|
encodeBase64,
|
||||||
HttpService,
|
HttpService,
|
||||||
isRpcError,
|
isRpcError,
|
||||||
|
Log,
|
||||||
RpcError,
|
RpcError,
|
||||||
RPCOptions,
|
RPCOptions,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
@@ -18,6 +19,8 @@ import {
|
|||||||
CompleteRes,
|
CompleteRes,
|
||||||
} from './api.service'
|
} from './api.service'
|
||||||
import * as jose from 'node-jose'
|
import * as jose from 'node-jose'
|
||||||
|
import { webSocket } from 'rxjs/webSocket'
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
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() {
|
async complete() {
|
||||||
const res = await this.rpcRequest<CompleteRes>({
|
const res = await this.rpcRequest<CompleteRes>({
|
||||||
method: 'setup.complete',
|
method: 'setup.complete',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { encodeBase64, pauseFor } from '@start9labs/shared'
|
import { encodeBase64, Log, pauseFor } from '@start9labs/shared'
|
||||||
import {
|
import {
|
||||||
ApiService,
|
ApiService,
|
||||||
CifsRecoverySource,
|
CifsRecoverySource,
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
CompleteRes,
|
CompleteRes,
|
||||||
} from './api.service'
|
} from './api.service'
|
||||||
import * as jose from 'node-jose'
|
import * as jose from 'node-jose'
|
||||||
|
import { interval, map, Observable } from 'rxjs'
|
||||||
|
|
||||||
let tries: number
|
let tries: number
|
||||||
|
|
||||||
@@ -146,6 +147,20 @@ export class MockApiService extends ApiService {
|
|||||||
await pauseFor(1000)
|
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> {
|
async complete(): Promise<CompleteRes> {
|
||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user