mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
chore: refactor install and setup wizards (#2561)
* chore: refactor install and setup wizards * chore: return tui-root
This commit is contained in:
@@ -56,14 +56,11 @@
|
||||
</ion-menu>
|
||||
|
||||
<ion-router-outlet
|
||||
[responsiveColViewport]="viewport"
|
||||
id="main-content"
|
||||
class="container"
|
||||
[class.container_offline]="offline$ | async"
|
||||
>
|
||||
<ion-content
|
||||
#viewport="viewport"
|
||||
responsiveColViewport
|
||||
class="ion-padding with-widgets"
|
||||
style="pointer-events: none; opacity: 0"
|
||||
></ion-content>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -10,7 +10,6 @@ import {
|
||||
LightThemeModule,
|
||||
LoadingModule,
|
||||
MarkdownModule,
|
||||
ResponsiveColViewportDirective,
|
||||
SharedPipesModule,
|
||||
} from '@start9labs/shared'
|
||||
import {
|
||||
@@ -57,7 +56,6 @@ import { RoutingModule } from './routing.module'
|
||||
TuiAlertModule,
|
||||
TuiModeModule,
|
||||
TuiThemeNightModule,
|
||||
ResponsiveColViewportDirective,
|
||||
DarkThemeModule,
|
||||
LightThemeModule,
|
||||
ServiceWorkerModule.register('ngsw-worker.js', {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import {
|
||||
InitializingModule,
|
||||
InitializingComponent,
|
||||
provideSetupLogsService,
|
||||
provideSetupService,
|
||||
} from '@start9labs/shared'
|
||||
@@ -20,7 +20,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
provideSetupService(ApiService),
|
||||
provideSetupLogsService(ApiService),
|
||||
],
|
||||
imports: [InitializingModule],
|
||||
imports: [InitializingComponent],
|
||||
})
|
||||
export class LoadingPage {
|
||||
readonly navCtrl = inject(NavController)
|
||||
|
||||
@@ -45,10 +45,8 @@ import { TuiDialogContext } from '@taiga-ui/core'
|
||||
})
|
||||
export class HeaderSnekComponent implements AfterViewInit, OnDestroy {
|
||||
private readonly document = inject(DOCUMENT)
|
||||
private readonly dialog = inject(POLYMORPHEUS_CONTEXT) as TuiDialogContext<
|
||||
number,
|
||||
number
|
||||
>
|
||||
private readonly dialog =
|
||||
inject<TuiDialogContext<number, number>>(POLYMORPHEUS_CONTEXT)
|
||||
|
||||
highScore: number = this.dialog.data
|
||||
score = 0
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [defaultHref]="defaultBack"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ pageTitle }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content
|
||||
[scrollEvents]="true"
|
||||
(ionScroll)="handleScroll($event)"
|
||||
(ionScrollEnd)="handleScrollEnd()"
|
||||
class="ion-padding with-widgets"
|
||||
>
|
||||
<ion-infinite-scroll
|
||||
id="scroller"
|
||||
[disabled]="infiniteStatus !== 1"
|
||||
position="top"
|
||||
threshold="1000"
|
||||
(ionInfinite)="doInfinite($event)"
|
||||
>
|
||||
<ion-infinite-scroll-content
|
||||
loadingSpinner="lines"
|
||||
></ion-infinite-scroll-content>
|
||||
</ion-infinite-scroll>
|
||||
|
||||
<text-spinner *ngIf="loading" text="Loading Logs"></text-spinner>
|
||||
|
||||
<div id="container">
|
||||
<div id="template"></div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!loading">
|
||||
<div id="bottom-div"></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',
|
||||
bottom: '96px',
|
||||
right: isOnBottom ? '-52px' : '30px',
|
||||
'background-color': 'var(--ion-color-medium)',
|
||||
'border-radius': '100%',
|
||||
transition: 'right 0.25s ease-out'
|
||||
}"
|
||||
>
|
||||
<ion-button
|
||||
style="
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
--padding-start: 0px;
|
||||
--padding-end: 0px;
|
||||
--border-radius: 100%;
|
||||
"
|
||||
color="dark"
|
||||
(click)="scrollToBottom(); autoScroll = true"
|
||||
strong
|
||||
>
|
||||
<ion-icon name="chevron-down"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer class="with-widgets">
|
||||
<ion-toolbar>
|
||||
<div class="inline ion-padding-start">
|
||||
<ion-checkbox [(ngModel)]="autoScroll" color="dark"></ion-checkbox>
|
||||
<p class="ion-padding-start">Autoscroll</p>
|
||||
</div>
|
||||
<ion-button
|
||||
*ngIf="!loading"
|
||||
slot="end"
|
||||
class="ion-padding-end"
|
||||
fill="clear"
|
||||
strong
|
||||
(click)="download()"
|
||||
>
|
||||
Download
|
||||
<ion-icon slot="end" name="download-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
@@ -1,13 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { LogsComponent } from './logs.component'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { TextSpinnerComponentModule } from '@start9labs/shared'
|
||||
|
||||
@NgModule({
|
||||
declarations: [LogsComponent],
|
||||
imports: [CommonModule, IonicModule, TextSpinnerComponentModule, FormsModule],
|
||||
exports: [LogsComponent],
|
||||
})
|
||||
export class LogsComponentModule {}
|
||||
@@ -1,5 +0,0 @@
|
||||
#container {
|
||||
padding-bottom: 16px;
|
||||
font-family: monospace;
|
||||
white-space: pre-line;
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { IonContent } from '@ionic/angular'
|
||||
import {
|
||||
bufferTime,
|
||||
catchError,
|
||||
filter,
|
||||
finalize,
|
||||
from,
|
||||
Observable,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from 'rxjs'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import {
|
||||
FetchLogsReq,
|
||||
FetchLogsRes,
|
||||
toLocalIsoString,
|
||||
Log,
|
||||
DownloadHTMLService,
|
||||
LoadingService,
|
||||
ErrorService,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiDestroyService } from '@taiga-ui/cdk'
|
||||
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({
|
||||
newline: true,
|
||||
bg: 'transparent',
|
||||
colors: {
|
||||
4: 'Cyan',
|
||||
},
|
||||
escapeXML: true,
|
||||
})
|
||||
|
||||
@Component({
|
||||
selector: 'logs',
|
||||
templateUrl: './logs.component.html',
|
||||
styleUrls: ['./logs.component.scss'],
|
||||
providers: [TuiDestroyService],
|
||||
})
|
||||
export class LogsComponent {
|
||||
@ViewChild(IonContent)
|
||||
private content?: IonContent
|
||||
|
||||
@Input({ required: true }) followLogs!: (
|
||||
params: RR.FollowServerLogsReq,
|
||||
) => Promise<RR.FollowServerLogsRes>
|
||||
@Input({ required: true }) fetchLogs!: (
|
||||
params: FetchLogsReq,
|
||||
) => Promise<FetchLogsRes>
|
||||
@Input({ required: true }) context!: string
|
||||
@Input() defaultBack = ''
|
||||
@Input() pageTitle = ''
|
||||
|
||||
loading = true
|
||||
infiniteStatus: 0 | 1 | 2 = 0
|
||||
startCursor?: string
|
||||
isOnBottom = true
|
||||
autoScroll = true
|
||||
websocketStatus:
|
||||
| 'connecting'
|
||||
| 'connected'
|
||||
| 'reconnecting'
|
||||
| 'disconnected' = 'connecting'
|
||||
limit = 400
|
||||
count = 0
|
||||
|
||||
constructor(
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly destroy$: TuiDestroyService,
|
||||
private readonly api: ApiService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly downloadHtml: DownloadHTMLService,
|
||||
private readonly connectionService: ConnectionService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
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> {
|
||||
try {
|
||||
const res = await this.fetchLogs({
|
||||
cursor: this.startCursor,
|
||||
before: true,
|
||||
limit: this.limit,
|
||||
})
|
||||
|
||||
this.processRes(res)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
e.target.complete()
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll(e: any) {
|
||||
if (e.detail.deltaY < -50) this.autoScroll = false
|
||||
}
|
||||
|
||||
handleScrollEnd() {
|
||||
const bottomDiv = document.getElementById('bottom-div')
|
||||
this.isOnBottom =
|
||||
!!bottomDiv &&
|
||||
bottomDiv.getBoundingClientRect().top - 420 < window.innerHeight
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
this.content?.scrollToBottom(200)
|
||||
}
|
||||
|
||||
async download() {
|
||||
const loader = this.loader.open('Processing 10,000 logs...').subscribe()
|
||||
|
||||
try {
|
||||
const { entries } = await this.fetchLogs({
|
||||
before: true,
|
||||
limit: 10000,
|
||||
})
|
||||
|
||||
const styles = {
|
||||
'background-color': '#222428',
|
||||
color: '#e0e0e0',
|
||||
'font-family': 'monospace',
|
||||
}
|
||||
const html = this.convertToAnsi(entries)
|
||||
|
||||
this.downloadHtml.download(`${this.context}-logs.html`, html, styles)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
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: FetchLogsRes) {
|
||||
const { entries, 'start-cursor': startCursor } = res
|
||||
|
||||
if (!entries.length) return
|
||||
|
||||
const container = document.getElementById('container')
|
||||
const newLogs = document.getElementById('template')?.cloneNode()
|
||||
|
||||
if (!(newLogs instanceof HTMLElement)) return
|
||||
|
||||
newLogs.innerHTML = this.convertToAnsi(entries)
|
||||
|
||||
// if response contains a startCursor, it means we are scrolling backwards
|
||||
if (startCursor) {
|
||||
this.startCursor = startCursor
|
||||
|
||||
const beforeContainerHeight = container?.scrollHeight || 0
|
||||
container?.prepend(newLogs)
|
||||
const afterContainerHeight = container?.scrollHeight || 0
|
||||
|
||||
// maintain scroll height
|
||||
setTimeout(() => {
|
||||
this.content?.scrollToPoint(
|
||||
0,
|
||||
afterContainerHeight - beforeContainerHeight,
|
||||
)
|
||||
}, 25)
|
||||
|
||||
if (entries.length < this.limit) {
|
||||
this.infiniteStatus = 2
|
||||
}
|
||||
} else {
|
||||
container?.append(newLogs)
|
||||
if (this.autoScroll) {
|
||||
setTimeout(() => {
|
||||
this.scrollToBottom()
|
||||
}, 25)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private convertToAnsi(entries: Log[]) {
|
||||
return entries
|
||||
.map(
|
||||
entry =>
|
||||
`<span style="color: #FFF; font-weight: bold;">${toLocalIsoString(
|
||||
new Date(entry.timestamp),
|
||||
)}</span> ${convert.toHtml(entry.message)}`,
|
||||
)
|
||||
.join('<br />')
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,13 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { ResponsiveColDirective } from '@start9labs/shared'
|
||||
import { AnyLinkComponent } from './any-link/any-link.component'
|
||||
import { WidgetListComponent } from './widget-list.component'
|
||||
import { WidgetCardComponent } from './widget-card/widget-card.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [WidgetListComponent, WidgetCardComponent, AnyLinkComponent],
|
||||
imports: [CommonModule, IonicModule, RouterModule, ResponsiveColDirective],
|
||||
imports: [CommonModule, IonicModule, RouterModule],
|
||||
exports: [WidgetListComponent],
|
||||
})
|
||||
export class WidgetListComponentModule {}
|
||||
|
||||
Reference in New Issue
Block a user