mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
feat: refactor logs (#2555)
* feat: refactor logs * chore: comments * feat: add system logs * feat: update shared logs
This commit is contained in:
@@ -25,9 +25,7 @@
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
|
||||
<div class="logs-container">
|
||||
<logs-window></logs-window>
|
||||
</div>
|
||||
<logs-window class="logs" />
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
@@ -8,11 +8,15 @@ ion-card-title {
|
||||
margin: auto auto 40px;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
margin-top: 24px;
|
||||
height: 280px;
|
||||
.logs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 18rem;
|
||||
padding: 1rem;
|
||||
margin: 1.5rem 0.75rem;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
border-radius: 31px;
|
||||
margin-inline: 10px;
|
||||
border-radius: 2rem;
|
||||
// TODO: Theme
|
||||
background: #181818;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ 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 { LogsWindowComponent } from './logs-window.component'
|
||||
import { InitializingComponent } from './initializing.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule, TuiLetModule],
|
||||
declarations: [InitializingComponent, LogsWindowComponent],
|
||||
imports: [CommonModule, IonicModule, TuiLetModule, LogsWindowComponent],
|
||||
declarations: [InitializingComponent],
|
||||
exports: [InitializingComponent],
|
||||
})
|
||||
export class InitializingModule {}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import { Component, ElementRef, inject } from '@angular/core'
|
||||
import {
|
||||
IntersectionObserverModule,
|
||||
INTERSECTION_ROOT,
|
||||
} from '@ng-web-apis/intersection-observer'
|
||||
import { MutationObserverModule } from '@ng-web-apis/mutation-observer'
|
||||
import { TuiScrollbarModule } from '@taiga-ui/core'
|
||||
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
|
||||
import { SetupLogsService } from '../../services/setup-logs.service'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'logs-window',
|
||||
template: `
|
||||
<tui-scrollbar childList subtree (waMutationObserver)="scrollTo(bottom)">
|
||||
@for (log of logs$ | async; track log) {
|
||||
<pre [innerHTML]="log | dompurify"></pre>
|
||||
}
|
||||
<section
|
||||
#bottom
|
||||
waIntersectionObserver
|
||||
[style.padding.rem]="1"
|
||||
(waIntersectionObservee)="onBottom($event)"
|
||||
></section>
|
||||
</tui-scrollbar>
|
||||
`,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
MutationObserverModule,
|
||||
IntersectionObserverModule,
|
||||
NgDompurifyModule,
|
||||
TuiScrollbarModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: INTERSECTION_ROOT,
|
||||
useExisting: ElementRef,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class LogsWindowComponent {
|
||||
readonly logs$ = inject(SetupLogsService)
|
||||
scroll = true
|
||||
|
||||
scrollTo(bottom: HTMLElement) {
|
||||
if (this.scroll) bottom.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
|
||||
onBottom([{ isIntersecting }]: readonly IntersectionObserverEntry[]) {
|
||||
this.scroll = isIntersecting
|
||||
}
|
||||
}
|
||||
@@ -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,67 +0,0 @@
|
||||
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'
|
||||
import Convert from 'ansi-to-html'
|
||||
const 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> ${convert.toHtml(log.message)}<br />`
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
export * from './classes/http-error'
|
||||
export * from './classes/rpc-error'
|
||||
|
||||
export * from './components/initializing/logs-window/logs-window.component'
|
||||
export * from './components/initializing/logs-window.component'
|
||||
export * from './components/initializing/initializing.module'
|
||||
export * from './components/initializing/initializing.component'
|
||||
export * from './components/loading/loading.component'
|
||||
@@ -63,6 +63,7 @@ export * from './tokens/relative-url'
|
||||
export * from './tokens/theme'
|
||||
|
||||
export * from './util/base-64'
|
||||
export * from './util/convert-ansi'
|
||||
export * from './util/copy-to-clipboard'
|
||||
export * from './util/get-new-entries'
|
||||
export * from './util/get-pkg-id'
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
|
||||
@Injectable()
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DownloadHTMLService {
|
||||
constructor(@Inject(DOCUMENT) private readonly document: Document) {}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { StaticClassProvider } from '@angular/core'
|
||||
import { defer, Observable, switchMap } from 'rxjs'
|
||||
import { bufferTime, defer, map, Observable, scan, switchMap } from 'rxjs'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import { FollowLogsReq, FollowLogsRes, Log } from '../types/api'
|
||||
import { Constructor } from '../types/constructor'
|
||||
import { convertAnsi } from '../util/convert-ansi'
|
||||
|
||||
interface Api {
|
||||
followServerLogs: (params: FollowLogsReq) => Promise<FollowLogsRes>
|
||||
@@ -19,11 +20,14 @@ export function provideSetupLogsService(
|
||||
}
|
||||
}
|
||||
|
||||
export class SetupLogsService extends Observable<Log> {
|
||||
export class SetupLogsService extends Observable<readonly string[]> {
|
||||
private readonly log$ = defer(() => this.api.followServerLogs({})).pipe(
|
||||
switchMap(({ guid }) =>
|
||||
this.api.openLogsWebsocket$({ url: `/rpc/${guid}` }),
|
||||
),
|
||||
bufferTime(1000),
|
||||
map(convertAnsi),
|
||||
scan((logs: readonly string[], log) => [...logs, log], []),
|
||||
)
|
||||
|
||||
constructor(private readonly api: Api) {
|
||||
|
||||
20
web/projects/shared/src/util/convert-ansi.ts
Normal file
20
web/projects/shared/src/util/convert-ansi.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Log } from '../types/api'
|
||||
import { toLocalIsoString } from './to-local-iso-string'
|
||||
|
||||
const Convert = require('ansi-to-html')
|
||||
const CONVERT = new Convert({
|
||||
bg: 'transparent',
|
||||
colors: { 4: 'Cyan' },
|
||||
escapeXML: true,
|
||||
})
|
||||
|
||||
export function convertAnsi(entries: readonly Log[]): string {
|
||||
return entries
|
||||
.map(
|
||||
({ timestamp, message }) =>
|
||||
`<b style="color: #FFF">${toLocalIsoString(
|
||||
new Date(timestamp),
|
||||
)}</b> ${CONVERT.toHtml(message)}`,
|
||||
)
|
||||
.join('<br />')
|
||||
}
|
||||
Reference in New Issue
Block a user