mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
Feat/logs revamp (#2075)
auto reconnect to logs websocket and hopefully fix scrolling issues
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [defaultHref]="defaultBack"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ title }}</ion-title>
|
||||
<ion-title>{{ pageTitle }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
>
|
||||
<ion-infinite-scroll
|
||||
id="scroller"
|
||||
[disabled]="!needInfinite"
|
||||
[disabled]="infiniteStatus !== 1"
|
||||
position="top"
|
||||
threshold="1000"
|
||||
(ionInfinite)="doInfinite($event)"
|
||||
@@ -33,10 +33,18 @@
|
||||
|
||||
<ng-container *ngIf="!loading">
|
||||
<div id="bottom-div"></div>
|
||||
<div *ngIf="websocketFail" class="ion-text-center ion-padding">
|
||||
<ion-text color="warning"> Websocket failed.... </ion-text>
|
||||
</div>
|
||||
|
||||
<p
|
||||
*ngIf="websocketStatus === 'reconnecting'"
|
||||
class="ion-text-center loading-dots"
|
||||
>
|
||||
<ion-text color="success">Reconnecting</ion-text>
|
||||
</p>
|
||||
<p
|
||||
*ngIf="websocketStatus === 'disconnected'"
|
||||
class="ion-text-center loading-dots"
|
||||
>
|
||||
<ion-text color="warning">Waiting for network connectivity</ion-text>
|
||||
</p>
|
||||
<div
|
||||
[ngStyle]="{
|
||||
position: 'fixed',
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { IonContent, LoadingController } from '@ionic/angular'
|
||||
import { bufferTime, takeUntil, tap } from 'rxjs'
|
||||
import {
|
||||
bufferTime,
|
||||
catchError,
|
||||
filter,
|
||||
finalize,
|
||||
from,
|
||||
Observable,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from 'rxjs'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import {
|
||||
LogsRes,
|
||||
@@ -13,6 +23,7 @@ import {
|
||||
} from '@start9labs/shared'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
|
||||
var Convert = require('ansi-to-html')
|
||||
var convert = new Convert({
|
||||
@@ -40,15 +51,20 @@ export class LogsComponent {
|
||||
@Input() fetchLogs!: (params: ServerLogsReq) => Promise<LogsRes>
|
||||
@Input() context!: string
|
||||
@Input() defaultBack!: string
|
||||
@Input() title!: string
|
||||
@Input() pageTitle!: string
|
||||
|
||||
loading = true
|
||||
needInfinite = false
|
||||
infiniteStatus: 0 | 1 | 2 = 0
|
||||
startCursor?: string
|
||||
isOnBottom = true
|
||||
autoScroll = true
|
||||
websocketFail = false
|
||||
websocketStatus:
|
||||
| 'connecting'
|
||||
| 'connected'
|
||||
| 'reconnecting'
|
||||
| 'disconnected' = 'connecting'
|
||||
limit = 400
|
||||
count = 0
|
||||
|
||||
constructor(
|
||||
private readonly errToast: ErrorToastService,
|
||||
@@ -56,50 +72,20 @@ export class LogsComponent {
|
||||
private readonly api: ApiService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly downloadHtml: DownloadHTMLService,
|
||||
private readonly connectionService: ConnectionService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
const { 'start-cursor': startCursor, guid } = await this.followLogs({
|
||||
limit: this.limit,
|
||||
})
|
||||
|
||||
this.startCursor = startCursor
|
||||
|
||||
const config: WebSocketSubjectConfig<Log> = {
|
||||
url: `/rpc/${guid}`,
|
||||
openObserver: {
|
||||
next: () => {
|
||||
this.websocketFail = false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
let totalLogs = 0
|
||||
|
||||
this.api
|
||||
.openLogsWebsocket$(config)
|
||||
.pipe(
|
||||
tap(_ => {
|
||||
totalLogs++
|
||||
if (totalLogs === this.limit) this.needInfinite = true
|
||||
}),
|
||||
bufferTime(500),
|
||||
tap(msgs => {
|
||||
this.loading = false
|
||||
this.processRes({ entries: msgs })
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe({
|
||||
error: () => {
|
||||
this.websocketFail = true
|
||||
if (this.isOnBottom) this.scrollToBottom()
|
||||
},
|
||||
})
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
}
|
||||
from(this.followLogs({ limit: this.limit }))
|
||||
.pipe(
|
||||
switchMap(({ 'start-cursor': startCursor, guid }) => {
|
||||
this.startCursor = startCursor
|
||||
return this.connect$(guid)
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
finalize(() => console.log('CLOSING')),
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
async doInfinite(e: any): Promise<void> {
|
||||
@@ -119,7 +105,7 @@ export class LogsComponent {
|
||||
}
|
||||
|
||||
handleScroll(e: any) {
|
||||
if (e.detail.deltaY < 0) this.autoScroll = false
|
||||
if (e.detail.deltaY < -50) this.autoScroll = false
|
||||
}
|
||||
|
||||
handleScrollEnd() {
|
||||
@@ -130,7 +116,7 @@ export class LogsComponent {
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
this.content?.scrollToBottom(250)
|
||||
this.content?.scrollToBottom(200)
|
||||
}
|
||||
|
||||
async download() {
|
||||
@@ -160,6 +146,65 @@ export class LogsComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private reconnect$(): Observable<Log[]> {
|
||||
return from(this.followLogs({})).pipe(
|
||||
tap(_ => this.recordConnectionChange()),
|
||||
switchMap(({ guid }) => this.connect$(guid, true)),
|
||||
)
|
||||
}
|
||||
|
||||
private connect$(guid: string, reconnect = false) {
|
||||
const config: WebSocketSubjectConfig<Log> = {
|
||||
url: `/rpc/${guid}`,
|
||||
openObserver: {
|
||||
next: () => {
|
||||
this.websocketStatus = 'connected'
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return this.api.openLogsWebsocket$(config).pipe(
|
||||
tap(_ => this.count++),
|
||||
bufferTime(1000),
|
||||
tap(msgs => {
|
||||
this.loading = false
|
||||
this.processRes({ entries: msgs })
|
||||
if (this.infiniteStatus === 0 && this.count >= this.limit)
|
||||
this.infiniteStatus = 1
|
||||
}),
|
||||
catchError(() => {
|
||||
this.recordConnectionChange(false)
|
||||
return this.connectionService.connected$.pipe(
|
||||
tap(
|
||||
connected =>
|
||||
(this.websocketStatus = connected
|
||||
? 'reconnecting'
|
||||
: 'disconnected'),
|
||||
),
|
||||
filter(Boolean),
|
||||
switchMap(() => this.reconnect$()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private recordConnectionChange(success = true) {
|
||||
const container = document.getElementById('container')
|
||||
const elem = document.getElementById('template')?.cloneNode()
|
||||
if (!(elem instanceof HTMLElement)) return
|
||||
elem.innerHTML = `<div style="padding: ${
|
||||
success ? '36px 0' : '36px 0 0 0'
|
||||
}; color: ${success ? '#2fdf75' : '#ff4961'}; text-align: center;">${
|
||||
success ? 'Reconnected' : 'Disconnected'
|
||||
} at ${toLocalIsoString(new Date())}</div>`
|
||||
container?.append(elem)
|
||||
if (this.isOnBottom) {
|
||||
setTimeout(() => {
|
||||
this.scrollToBottom()
|
||||
}, 25)
|
||||
}
|
||||
}
|
||||
|
||||
private processRes(res: LogsRes) {
|
||||
const { entries, 'start-cursor': startCursor } = res
|
||||
|
||||
@@ -180,7 +225,7 @@ export class LogsComponent {
|
||||
container?.prepend(newLogs)
|
||||
const afterContainerHeight = container?.scrollHeight || 0
|
||||
|
||||
// scroll down
|
||||
// maintain scroll height
|
||||
setTimeout(() => {
|
||||
this.content?.scrollToPoint(
|
||||
0,
|
||||
@@ -189,12 +234,11 @@ export class LogsComponent {
|
||||
}, 25)
|
||||
|
||||
if (entries.length < this.limit) {
|
||||
this.needInfinite = false
|
||||
this.infiniteStatus = 2
|
||||
}
|
||||
} else {
|
||||
container?.append(newLogs)
|
||||
if (this.autoScroll) {
|
||||
// scroll to bottom
|
||||
setTimeout(() => {
|
||||
this.scrollToBottom()
|
||||
}, 25)
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
[followLogs]="followLogs()"
|
||||
[defaultBack]="'/services/' + pkgId"
|
||||
[context]="pkgId"
|
||||
title="Service Logs"
|
||||
pageTitle="Service Logs"
|
||||
class="ion-page"
|
||||
></logs>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
[fetchLogs]="fetchLogs()"
|
||||
[followLogs]="followLogs()"
|
||||
context="kernel"
|
||||
defaultBack="embassy"
|
||||
title="Kernel Logs"
|
||||
defaultBack="system"
|
||||
pageTitle="Kernel Logs"
|
||||
class="ion-page"
|
||||
></logs>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
[fetchLogs]="fetchLogs()"
|
||||
[followLogs]="followLogs()"
|
||||
context="eos"
|
||||
defaultBack="embassy"
|
||||
title="OS Logs"
|
||||
defaultBack="system"
|
||||
pageTitle="OS Logs"
|
||||
class="ion-page"
|
||||
></logs>
|
||||
|
||||
@@ -171,6 +171,7 @@ export class MockApiService extends ApiService {
|
||||
map((_, index) => {
|
||||
// mock fire open observer
|
||||
if (index === 0) config.openObserver?.next(new Event(''))
|
||||
if (index === 100) throw new Error('HAAHHA')
|
||||
return Mock.ServerLogs[0]
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -295,24 +295,6 @@ h2 {
|
||||
line-height: unset;
|
||||
}
|
||||
|
||||
@keyframes ellipsis-dot {
|
||||
25% {
|
||||
content: '';
|
||||
}
|
||||
|
||||
50% {
|
||||
content: '.';
|
||||
}
|
||||
|
||||
75% {
|
||||
content: '..';
|
||||
}
|
||||
|
||||
100% {
|
||||
content: '...';
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flickerAnimation {
|
||||
0% {
|
||||
opacity: 1;
|
||||
|
||||
Reference in New Issue
Block a user