chore: refactor install and setup wizards (#2561)

* chore: refactor install and setup wizards

* chore: return tui-root
This commit is contained in:
Alex Inkin
2024-02-22 17:58:01 +04:00
committed by GitHub
parent 69d5f521a5
commit 7b41b295b7
109 changed files with 1863 additions and 3538 deletions

View File

@@ -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

View File

@@ -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', {

View File

@@ -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)

View File

@@ -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

View File

@@ -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>

View File

@@ -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 {}

View File

@@ -1,5 +0,0 @@
#container {
padding-bottom: 16px;
font-family: monospace;
white-space: pre-line;
}

View File

@@ -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>&nbsp;&nbsp;${convert.toHtml(entry.message)}`,
)
.join('<br />')
}
}

View File

@@ -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 {}