feat: refactor logs (#2555)

* feat: refactor logs

* chore: comments

* feat: add system logs

* feat: update shared logs
This commit is contained in:
Alex Inkin
2024-02-06 06:26:00 +04:00
committed by GitHub
parent 4410d7f195
commit 9a0ae549f6
33 changed files with 676 additions and 180 deletions

View File

@@ -8,7 +8,6 @@ import { StateService } from 'src/app/services/state.service'
selector: 'success',
templateUrl: 'success.page.html',
styleUrls: ['success.page.scss'],
providers: [DownloadHTMLService],
})
export class SuccessPage {
@ViewChild('canvas', { static: true })

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -168,6 +168,7 @@ tui-hint[data-appearance='onDark'] {
tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] {
border: 0;
backdrop-filter: blur(0.25rem);
box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
// TODO: Replace --tui-elevation-02 when Taiga UI is updated
background: rgb(63 63 63 / 95%);

View File

@@ -43,11 +43,15 @@ import { BreadcrumbsService } from '../../services/breadcrumbs.service'
);
> * {
@include transition(clip-path);
@include transition(all);
position: relative;
margin-left: -1.25rem;
backdrop-filter: blur(1rem);
clip-path: var(--clip-path);
&:active {
backdrop-filter: blur(2rem) brightness(0.75) saturate(0.75);
}
}
}
@@ -60,11 +64,16 @@ import { BreadcrumbsService } from '../../services/breadcrumbs.service'
opacity: 0.5;
.active & {
opacity: 0.25;
opacity: 0.75;
&::before {
// TODO: Theme
background: #363636;
}
}
&::before {
@include transition(clip-path);
@include transition(all);
content: '';
position: absolute;
inset: 0;

View File

@@ -0,0 +1,52 @@
import { Directive, HostListener, inject, Input } from '@angular/core'
import {
convertAnsi,
DownloadHTMLService,
ErrorService,
FetchLogsReq,
FetchLogsRes,
LoadingService,
} from '@start9labs/shared'
import { LogsComponent } from './logs.component'
@Directive({
standalone: true,
selector: 'button[logsDownload]',
})
export class LogsDownloadDirective {
private readonly component = inject(LogsComponent)
private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService)
private readonly downloadHtml = inject(DownloadHTMLService)
@Input({ required: true })
logsDownload!: (params: FetchLogsReq) => Promise<FetchLogsRes>
@HostListener('click')
async download() {
const loader = this.loader.open('Processing 10,000 logs...').subscribe()
try {
const { entries } = await this.logsDownload({
before: true,
limit: 10000,
})
this.downloadHtml.download(
`${this.component.context}-logs.html`,
convertAnsi(entries),
STYLES,
)
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
}
}
}
const STYLES = {
'background-color': '#222428',
color: '#e0e0e0',
'font-family': 'monospace',
}

View File

@@ -0,0 +1,36 @@
import { Directive, inject, Output } from '@angular/core'
import { IntersectionObserveeService } from '@ng-web-apis/intersection-observer'
import { convertAnsi, ErrorService } from '@start9labs/shared'
import { catchError, defer, filter, from, map, of, switchMap, tap } from 'rxjs'
import { LogsComponent } from './logs.component'
@Directive({
standalone: true,
selector: '[logsFetch]',
})
export class LogsFetchDirective {
private readonly observer = inject(IntersectionObserveeService)
private readonly component = inject(LogsComponent)
private readonly errors = inject(ErrorService)
@Output()
readonly logsFetch = defer(() => this.observer).pipe(
filter(([{ isIntersecting }]) => isIntersecting && !this.component.scroll),
switchMap(() =>
from(
this.component.fetchLogs({
cursor: this.component.startCursor,
before: true,
limit: 400,
}),
),
),
tap(res => this.component.setCursor(res['start-cursor'])),
map(({ entries }) => convertAnsi(entries)),
catchError(e => {
this.errors.handleError(e)
return of('')
}),
)
}

View File

@@ -0,0 +1,66 @@
<tui-scrollbar class="scrollbar">
<section
class="top"
waIntersectionObserver
(waIntersectionObservee)="onLoading($event[0].isIntersecting)"
(logsFetch)="onPrevious($event)"
>
@if (loading) {
<tui-loader textContent="Loading older logs" />
}
</section>
<section #el childList (waMutationObserver)="restoreScroll(el)">
@for (log of previous; track log) {
<pre [innerHTML]="log | dompurify"></pre>
}
</section>
@if (followLogs | logs | async; as logs) {
<section childList (waMutationObserver)="scrollToBottom()">
@for (log of logs; track log) {
<pre [innerHTML]="log | dompurify"></pre>
}
@if ((status$ | async) !== 'connected') {
<p class="loading-dots" [attr.data-status]="status$.value">
{{
status$.value === 'reconnecting'
? 'Reconnecting'
: 'Waiting for network connectivity'
}}
</p>
}
</section>
} @else {
<tui-loader textContent="Loading logs" [style.margin-top.rem]="5" />
}
<section
#bottom
class="bottom"
waIntersectionObserver
(waIntersectionObservee)="
setScroll($event[$event.length - 1].isIntersecting)
"
></section>
</tui-scrollbar>
<footer class="footer">
<button
tuiButton
appearance="flat"
iconLeft="tuiIconArrowDownCircle"
(click)="setScroll(true); scrollToBottom()"
>
Scroll to bottom
</button>
<button
tuiButton
appearance="flat"
iconLeft="tuiIconDownload"
[logsDownload]="fetchLogs"
>
Download
</button>
</footer>

View File

@@ -0,0 +1,43 @@
:host {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.scrollbar {
flex: 1;
}
.loading-dots {
text-align: center;
}
.top {
height: 10rem;
margin-bottom: -5rem;
}
.bottom {
height: 3rem;
}
.footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 1rem;
border-top: 1px solid var(--tui-clear);
}
[data-status='reconnecting'] {
color: var(--tui-success-fill);
}
[data-status='disconnected'] {
color: var(--tui-warning-fill);
}
pre {
overflow: visible;
}

View File

@@ -0,0 +1,104 @@
import { CommonModule } from '@angular/common'
import { Component, ElementRef, Input, ViewChild } from '@angular/core'
import {
INTERSECTION_ROOT,
IntersectionObserverModule,
} from '@ng-web-apis/intersection-observer'
import { MutationObserverModule } from '@ng-web-apis/mutation-observer'
import { FetchLogsReq, FetchLogsRes } from '@start9labs/shared'
import {
TuiLoaderModule,
TuiScrollbarComponent,
TuiScrollbarModule,
} from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
import { RR } from 'src/app/services/api/api.types'
import { LogsDownloadDirective } from './logs-download.directive'
import { LogsFetchDirective } from './logs-fetch.directive'
import { LogsPipe } from './logs.pipe'
import { BehaviorSubject } from 'rxjs'
@Component({
standalone: true,
selector: 'logs',
templateUrl: './logs.component.html',
styleUrls: ['./logs.component.scss'],
imports: [
CommonModule,
IntersectionObserverModule,
MutationObserverModule,
NgDompurifyModule,
TuiButtonModule,
TuiLoaderModule,
TuiScrollbarModule,
LogsDownloadDirective,
LogsFetchDirective,
LogsPipe,
],
providers: [
{
provide: INTERSECTION_ROOT,
useExisting: ElementRef,
},
],
})
export class LogsComponent {
@ViewChild('bottom')
private readonly bottom?: ElementRef<HTMLElement>
@ViewChild(TuiScrollbarComponent, { read: ElementRef })
private readonly scrollbar?: ElementRef<HTMLElement>
@Input({ required: true }) followLogs!: (
params: RR.FollowServerLogsReq,
) => Promise<RR.FollowServerLogsRes>
@Input({ required: true }) fetchLogs!: (
params: FetchLogsReq,
) => Promise<FetchLogsRes>
@Input({ required: true }) context!: string
scrollTop = 0
startCursor?: string
scroll = true
loading = false
previous: readonly string[] = []
readonly status$ = new BehaviorSubject<
'connected' | 'disconnected' | 'reconnecting'
>('connected')
onLoading(loading: boolean) {
this.loading = loading && !this.scroll
}
onPrevious(previous: string) {
this.onLoading(false)
this.scrollTop = this.scrollbar?.nativeElement.scrollTop || 0
this.previous = [previous, ...this.previous]
}
setCursor(startCursor = this.startCursor) {
this.startCursor = startCursor
}
setScroll(scroll: boolean) {
this.scroll = scroll
}
restoreScroll({ firstElementChild }: HTMLElement) {
this.scrollbar?.nativeElement.scrollTo(
this.scrollbar?.nativeElement.scrollLeft || 0,
this.scrollTop + (firstElementChild?.clientHeight || 0),
)
}
scrollToBottom() {
if (this.scroll)
this.bottom?.nativeElement.scrollIntoView({
behavior: 'smooth',
})
}
}

View File

@@ -0,0 +1,87 @@
import { inject, Pipe, PipeTransform } from '@angular/core'
import { convertAnsi, toLocalIsoString } from '@start9labs/shared'
import {
bufferTime,
catchError,
defer,
filter,
ignoreElements,
map,
merge,
Observable,
repeat,
scan,
skipWhile,
startWith,
switchMap,
take,
tap,
} from 'rxjs'
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'
import { LogsComponent } from './logs.component'
@Pipe({
name: 'logs',
standalone: true,
})
export class LogsPipe implements PipeTransform {
private readonly api = inject(ApiService)
private readonly logs = inject(LogsComponent)
private readonly connection = inject(ConnectionService)
transform(
followLogs: (
params: RR.FollowServerLogsReq,
) => Promise<RR.FollowServerLogsRes>,
): Observable<readonly string[]> {
return merge(
this.logs.status$.pipe(
skipWhile(value => value === 'connected'),
filter(value => value === 'connected'),
map(() => getMessage(true)),
),
defer(() => followLogs(this.options)).pipe(
tap(r => this.logs.setCursor(r['start-cursor'])),
switchMap(r => this.api.openLogsWebsocket$(this.toConfig(r.guid))),
bufferTime(1000),
filter(logs => !!logs.length),
map(convertAnsi),
),
).pipe(
catchError(() =>
this.connection.connected$.pipe(
tap(v => this.logs.status$.next(v ? 'reconnecting' : 'disconnected')),
filter(Boolean),
take(1),
ignoreElements(),
startWith(getMessage(false)),
),
),
repeat(),
scan((logs: string[], log) => [...logs, log], []),
)
}
private get options() {
return this.logs.status$.value === 'connected' ? { limit: 400 } : {}
}
private toConfig(guid: string) {
return {
url: `/rpc/${guid}`,
openObserver: {
next: () => this.logs.status$.next('connected'),
},
}
}
}
function getMessage(success: boolean): string {
return `<p style="color: ${
success ? 'var(--tui-success-fill)' : 'var(--tui-error-fill)'
}; text-align: center;">${
success ? 'Reconnected' : 'Disconnected'
} at ${toLocalIsoString(new Date())}</p>`
}

View File

@@ -4,6 +4,10 @@ export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
icon: 'tuiIconSave',
title: 'Backups',
},
'/portal/system/logs': {
icon: 'tuiIconFileText',
title: 'Logs',
},
'/portal/system/marketplace': {
icon: 'tuiIconShoppingCart',
title: 'Marketplace',

View File

@@ -1,29 +1,16 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { getPkgId } from '@start9labs/shared'
import { LogsComponentModule } from 'src/app/common/logs/logs.component.module'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { RR } from 'src/app/services/api/api.types'
import { LogsComponent } from 'src/app/apps/portal/components/logs/logs.component'
@Component({
template: '<logs [fetchLogs]="fetch" [followLogs]="follow" [context]="id" />',
styles: [
`
logs {
display: block;
height: calc(100% - 9rem);
min-height: 20rem;
margin-bottom: 5rem;
::ng-deep ion-header {
display: none;
}
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [LogsComponentModule],
styles: [':host { height: 100%}'],
imports: [LogsComponent],
})
export class ServiceLogsRoute {
private readonly api = inject(ApiService)

View File

@@ -1,45 +1,19 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ActivatedRoute, Router, RouterModule } from '@angular/router'
import { TuiIconModule } from '@taiga-ui/experimental'
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router'
import { PatchDB } from 'patch-db-client'
import { distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { toRouterLink } from '../../../utils/to-router-link'
@Component({
template: `
<a
*ngIf="service$ | async as service"
routerLinkActive="_current"
[routerLinkActiveOptions]="{ exact: true }"
[routerLink]="getLink(service.manifest.id)"
>
<tui-icon icon="tuiIconChevronLeft" />
{{ service.manifest.title }}
</a>
<ng-container *ngIf="service$ | async" />
<router-outlet />
`,
styles: [
`
a {
display: inline-flex;
align-items: center;
gap: 0.5rem;
margin: 1rem 0;
font-size: 1rem;
color: var(--tui-text-01);
}
._current {
display: none;
}
`,
],
host: { class: 'g-page' },
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, RouterModule, TuiIconModule],
imports: [CommonModule, RouterOutlet],
})
export class ServiceOutletComponent {
private readonly patch = inject(PatchDB<DataModel>)
@@ -58,8 +32,4 @@ export class ServiceOutletComponent {
}
}),
)
getLink(id: string): string {
return toRouterLink(id)
}
}

View File

@@ -40,7 +40,7 @@ import { JOBS } from './modals/jobs.component'
BackupsUpcomingComponent,
],
})
export class BackupsComponent {
export default class BackupsComponent {
private readonly dialogs = inject(TuiDialogService)
readonly options = [

View File

@@ -0,0 +1,95 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { TuiTextfieldControllerModule } from '@taiga-ui/core'
import { TuiSelectModule } from '@taiga-ui/kit'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { RR } from 'src/app/services/api/api.types'
import { LogsComponent } from '../../../components/logs/logs.component'
@Component({
template: `
<div class="g-plaque"></div>
<tui-select
tuiTextfieldAppearance="unstyled"
tuiTextfieldSize="m"
[(ngModel)]="logs"
>
{{ subtitle }}
<select tuiSelect [items]="items"></select>
</tui-select>
@switch (logs) {
@case ('OS Logs') {
<logs
context="OS Logs"
[followLogs]="followOS"
[fetchLogs]="fetchOS"
></logs>
}
@case ('Kernel Logs') {
<logs
context="Kernel Logs"
[followLogs]="followKernel"
[fetchLogs]="fetchKernel"
></logs>
}
@case ('Tor Logs') {
<logs
context="Tor Logs"
[followLogs]="followTor"
[fetchLogs]="fetchTor"
></logs>
}
}
`,
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'g-edged' },
styles: [
`
tui-select {
margin: -0.5rem 0 1rem;
}
logs {
height: calc(100% - 4rem);
}
`,
],
imports: [
FormsModule,
TuiSelectModule,
TuiTextfieldControllerModule,
LogsComponent,
],
})
export default class SystemLogsComponent {
private readonly api = inject(ApiService)
readonly items = ['OS Logs', 'Kernel Logs', 'Tor Logs']
logs = 'OS Logs'
readonly followOS = async (params: RR.FollowServerLogsReq) =>
this.api.followServerLogs(params)
readonly fetchOS = async (params: RR.GetServerLogsReq) =>
this.api.getServerLogs(params)
readonly followKernel = async (params: RR.FollowServerLogsReq) =>
this.api.followKernelLogs(params)
readonly fetchKernel = async (params: RR.GetServerLogsReq) =>
this.api.getKernelLogs(params)
readonly followTor = async (params: RR.FollowServerLogsReq) =>
this.api.followTorLogs(params)
readonly fetchTor = async (params: RR.GetServerLogsReq) =>
this.api.getTorLogs(params)
get subtitle(): string {
switch (this.logs) {
case 'OS Logs':
return 'Raw, unfiltered operating system logs'
case 'Kernel Logs':
return 'Diagnostic log stream for device drivers and other kernel processes'
default:
return 'Diagnostic log stream for the Tor daemon on StartOS'
}
}
}

View File

@@ -63,7 +63,7 @@ import { NotificationsTableComponent } from './table.component'
TuiLetModule,
],
})
export class NotificationsComponent {
export default class NotificationsComponent {
readonly service = inject(NotificationService)
readonly api = inject(ApiService)
readonly errorService = inject(ErrorService)

View File

@@ -1,8 +1,6 @@
import { Routes } from '@angular/router'
import { SettingsComponent } from './settings.component'
export const SETTINGS_ROUTES: Routes = [
export default [
{
path: '',
component: SettingsComponent,

View File

@@ -93,7 +93,7 @@ import { SideloadPackageComponent } from './package.component'
SideloadPackageComponent,
],
})
export class SideloadComponent {
export default class SideloadComponent {
readonly refresh$ = new Subject<void>()
readonly isTor = inject(ConfigService).isTor()

View File

@@ -5,4 +5,4 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SnekComponent {}
export default class SnekComponent {}

View File

@@ -7,10 +7,15 @@ const ROUTES: Routes = [
{
title: systemTabResolver,
path: 'backups',
loadComponent: () =>
import('./backups/backups.component').then(m => m.BackupsComponent),
loadComponent: () => import('./backups/backups.component'),
data: toNavigationItem('/portal/system/backups'),
},
{
title: systemTabResolver,
path: 'logs',
loadComponent: () => import('./logs/logs.component'),
data: toNavigationItem('/portal/system/logs'),
},
{
title: systemTabResolver,
path: 'marketplace',
@@ -20,38 +25,31 @@ const ROUTES: Routes = [
{
title: systemTabResolver,
path: 'settings',
loadChildren: () =>
import('./settings/settings.routes').then(m => m.SETTINGS_ROUTES),
loadChildren: () => import('./settings/settings.routes'),
data: toNavigationItem('/portal/system/settings'),
},
{
title: systemTabResolver,
path: 'notifications',
loadComponent: () =>
import('./notifications/notifications.component').then(
m => m.NotificationsComponent,
),
loadComponent: () => import('./notifications/notifications.component'),
data: toNavigationItem('/portal/system/notifications'),
},
{
title: systemTabResolver,
path: 'sideload',
loadComponent: () =>
import('./sideload/sideload.component').then(m => m.SideloadComponent),
loadComponent: () => import('./sideload/sideload.component'),
data: toNavigationItem('/portal/system/sideload'),
},
{
title: systemTabResolver,
path: 'updates',
loadComponent: () =>
import('./updates/updates.component').then(m => m.UpdatesComponent),
loadComponent: () => import('./updates/updates.component'),
data: toNavigationItem('/portal/system/updates'),
},
{
title: systemTabResolver,
path: 'snek',
loadComponent: () =>
import('./snek/snek.component').then(m => m.SnekComponent),
loadComponent: () => import('./snek/snek.component'),
data: toNavigationItem('/portal/system/snek'),
},
]

View File

@@ -34,7 +34,7 @@ import { SkeletonListComponent } from '../../../components/skeleton-list.compone
</p>
<updates-item
*ngFor="
let pkg of data.mp[host.url]?.packages | filterUpdates : data.local;
let pkg of data.mp[host.url]?.packages | filterUpdates: data.local;
else: loading;
empty: blank
"
@@ -61,7 +61,7 @@ import { SkeletonListComponent } from '../../../components/skeleton-list.compone
SkeletonListComponent,
],
})
export class UpdatesComponent {
export default class UpdatesComponent {
private readonly marketplace = inject(
AbstractMarketplaceService,
) as MarketplaceService

View File

@@ -40,7 +40,7 @@ var convert = new Convert({
selector: 'logs',
templateUrl: './logs.component.html',
styleUrls: ['./logs.component.scss'],
providers: [TuiDestroyService, DownloadHTMLService],
providers: [TuiDestroyService],
})
export class LogsComponent {
@ViewChild(IonContent)

View File

@@ -380,6 +380,55 @@ ul {
background: #373a3f;
}
.g-edged {
position: relative;
display: block;
height: calc(100% - 1rem);
box-sizing: border-box;
overflow: auto;
isolation: isolate;
padding: 2rem 3rem;
margin: 0 2.75rem 1rem;
backdrop-filter: blur(2rem) brightness(0.75) saturate(0.75);
clip-path: var(--clip-path);
--clip-path: polygon(
1.75rem 0%,
calc(100% - 3.35rem) 0%,
100% 5rem,
100% calc(100% - 4rem),
calc(100% - 3rem) 100%,
3rem 100%,
0% calc(100% - 2rem),
0% 3rem
);
.g-plaque {
@include transition(opacity);
position: absolute;
inset: 0;
z-index: -1;
filter: url(#round-corners);
opacity: 0.75;
// TODO: Theme
box-shadow: inset 0 0 2rem white;
&::before {
@include transition(clip-path);
content: '';
position: absolute;
inset: 0;
clip-path: var(--clip-path);
// TODO: Theme
background: #333;
box-shadow:
inset 0 1px rgba(255, 255, 255, 0.2),
inset -1px 0 rgba(255, 255, 255, 0.1),
inset 1px 0 rgba(255, 255, 255, 0.1);
}
}
}
.g-table {
width: 100%;
min-width: 40rem;
@@ -491,3 +540,7 @@ svg:not(:root) {
.externalLink {
color: var(--ion-color-secondary);
}
.ion-page {
overflow: clip;
}