From 495bbecc01666a5006a080934085946817527dbb Mon Sep 17 00:00:00 2001 From: Alex Inkin Date: Fri, 28 Mar 2025 17:02:06 +0400 Subject: [PATCH] feat: refactor logs (#2856) * feat: refactor logs * download root ca, minor transaltions, better interfaces buttons * chore: comments --------- Co-authored-by: Matt Hill --- .../ui/src/app/i18n/dictionaries/english.ts | 31 ++- .../ui/src/app/i18n/dictionaries/spanish.ts | 29 ++- .../interfaces/clearnet.component.ts | 23 +- .../components/interfaces/tor.component.ts | 14 +- .../components/logs/logs.component.scss | 4 +- .../portal/routes/logs/logs.component.ts | 234 +++++++++++++----- .../services/routes/interface.component.ts | 25 +- .../services/routes/outlet.component.ts | 13 +- .../routes/general/general.component.ts | 39 ++- .../routes/interfaces/interfaces.component.ts | 14 +- web/projects/ui/src/styles.scss | 4 + 11 files changed, 315 insertions(+), 115 deletions(-) diff --git a/web/projects/ui/src/app/i18n/dictionaries/english.ts b/web/projects/ui/src/app/i18n/dictionaries/english.ts index e7331678a..0a21bbe44 100644 --- a/web/projects/ui/src/app/i18n/dictionaries/english.ts +++ b/web/projects/ui/src/app/i18n/dictionaries/english.ts @@ -12,7 +12,7 @@ export default { email: 'Email', backup: 'Create Backup', restore: 'Restore Backup', - interfaces: 'User Interface Addresses', + interfaces: 'StartOS UI', acme: 'ACME', wifi: 'WiFi', sessions: 'Active Sessions', @@ -22,16 +22,29 @@ export default { general: { title: 'General Settings', subtitle: 'Manage your overall setup and preferences', - update: 'Software Update', - restart: 'Restart to apply', - check: 'Check for updates', tab: 'Browser Tab Title', language: 'Language', - tor: 'Reset Tor', - daemon: 'Restart the Tor daemon on your server', - disk: 'Disk Repair', - attempt: 'Attempt automatic repair', - repair: 'Repair', + repair: { + title: 'Disk Repair', + subtitle: 'Attempt automatic repair', + button: 'Repair', + }, + ca: { + title: 'Root Certificate Authority', + subtitle: `Download your server's Root CA`, + button: 'Download', + }, + tor: { + title: 'Reset Tor', + subtitle: 'Restart the Tor daemon on your server', + }, + update: { + title: 'Software Update', + button: { + restart: 'Restart to apply', + check: 'Check for updates', + }, + }, sync: { title: 'Clock sync failure', subtitle: diff --git a/web/projects/ui/src/app/i18n/dictionaries/spanish.ts b/web/projects/ui/src/app/i18n/dictionaries/spanish.ts index 82c915987..d84118996 100644 --- a/web/projects/ui/src/app/i18n/dictionaries/spanish.ts +++ b/web/projects/ui/src/app/i18n/dictionaries/spanish.ts @@ -24,16 +24,29 @@ export default { general: { title: 'Configuración General', subtitle: 'Gestiona tu configuración general y preferencias', - update: 'Actualización de Software', - restart: 'Reiniciar para aplicar', - check: 'Buscar actualizaciones', tab: 'Título de la Pestaña del Navegador', language: 'Idioma', - tor: 'Reiniciar Tor', - daemon: 'Reiniciar el daemon de Tor en tu servidor', - disk: 'Reparación de Disco', - attempt: 'Intentar reparación automática', - repair: 'Reparar', + repair: { + title: 'Reparación de Disco', + subtitle: 'Intentar reparación automática', + button: 'Reparar', + }, + ca: { + title: 'Autoridad de Certificación Raíz', + subtitle: 'Descarga la autoridad certificadora raíz de tu servidor', + button: 'Descarga', + }, + tor: { + title: 'Reiniciar Tor', + subtitle: 'Reiniciar el daemon de Tor en tu servidor', + }, + update: { + title: 'Actualización de Software', + button: { + restart: 'Reiniciar para aplicar', + check: 'Buscar actualizaciones', + }, + }, sync: { title: 'Fallo en la sincronización del reloj', subtitle: diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts index 12eba0df4..ce9981dde 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts @@ -61,6 +61,15 @@ type ClearnetForm = { Learn More + @if (clearnet().length) { - } @if (clearnet().length) { @@ -110,8 +111,10 @@ type ClearnetForm = { } @else { - No interfaces available - + No public addresses + } `, diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts index 6687420a6..2f8451ee4 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts @@ -54,6 +54,16 @@ type OnionForm = { Learn More + @if (tor().length) { + + } @if (tor().length) { @@ -85,7 +95,7 @@ type OnionForm = { } @else { No Tor addresses available - + } `, @@ -149,7 +159,7 @@ export class InterfaceTorComponent { async add() { const options: Partial>> = { - label: 'Select Domain/Subdomain', + label: 'New Tor Address', data: { spec: await configBuilderToSpec( ISB.InputSpec.of({ diff --git a/web/projects/ui/src/app/routes/portal/components/logs/logs.component.scss b/web/projects/ui/src/app/routes/portal/components/logs/logs.component.scss index d2aef6bd6..928250f8b 100644 --- a/web/projects/ui/src/app/routes/portal/components/logs/logs.component.scss +++ b/web/projects/ui/src/app/routes/portal/components/logs/logs.component.scss @@ -7,6 +7,9 @@ .scrollbar { flex: 1; + background: var(--tui-background-neutral-1); + border-radius: var(--tui-radius-m); + border: 1rem solid transparent; } .loading-dots { @@ -27,7 +30,6 @@ align-items: center; justify-content: space-between; padding-top: 1rem; - border-top: 1px solid var(--tui-background-neutral-1); } [data-status='reconnecting'] { diff --git a/web/projects/ui/src/app/routes/portal/routes/logs/logs.component.ts b/web/projects/ui/src/app/routes/portal/routes/logs/logs.component.ts index 6f54c95dc..fc5afd5d0 100644 --- a/web/projects/ui/src/app/routes/portal/routes/logs/logs.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/logs/logs.component.ts @@ -1,44 +1,87 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core' -import { FormsModule } from '@angular/forms' -import { TuiSelectModule, TuiTextfieldControllerModule } from '@taiga-ui/legacy' +import { KeyValuePipe } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + inject, + signal, +} from '@angular/core' +import { + TuiAppearance, + TuiButton, + TuiIcon, + TuiLink, + TuiTitle, +} from '@taiga-ui/core' +import { TuiCardMedium } from '@taiga-ui/layout' import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.component' import { RR } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' import { TitleDirective } from 'src/app/services/title.service' +interface Log { + title: string + subtitle: string + icon: string + follow: (params: RR.FollowServerLogsReq) => Promise + fetch: (params: RR.GetServerLogsReq) => Promise +} + @Component({ template: ` - Logs - - {{ subtitle }} - - - @switch (logs) { - @case ('OS Logs') { - + + @if (current(); as key) { + + {{ logs[key].title }} + } @else { + Logs } - @case ('Kernel Logs') { - + + @if (current(); as key) { +
+ + + {{ logs[key].title }} + +

{{ logs[key].subtitle }}

+
+ @for (log of logs | keyvalue; track $index) { + @if (log.key === current()) { + + } } - @case ('Tor Logs') { - + } @else { + @for (log of logs | keyvalue; track $index) { + } } `, @@ -47,51 +90,114 @@ import { TitleDirective } from 'src/app/services/title.service' host: { class: 'g-page' }, styles: [ ` - tui-select { - margin: 1rem 0; + :host { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 1rem; + padding: 1rem; + } + + header { + width: 100%; + padding: 0 1rem; } logs { - height: calc(100% - 5rem); + height: calc(100% - 4rem); + width: 100%; + } + + .close { + position: absolute; + right: 0; + border-radius: 100%; + } + + button::before { + margin: 0 -0.25rem 0 -0.375rem; + --tui-icon-size: 1.5rem; + } + + [tuiCardMedium] { + height: 14rem; + width: 14rem; + cursor: pointer; + box-shadow: + inset 0 0 0 1px var(--tui-background-neutral-1), + var(--tui-shadow-small); + + [tuiSubtitle] { + color: var(--tui-text-secondary); + } + + tui-icon:last-child { + align-self: flex-end; + } + } + + :host-context(tui-root._mobile) { + flex-direction: column; + justify-content: flex-start; + + header { + padding: 0; + } + + .title { + display: none; + } + + logs { + height: calc(100% - 2rem); + } + + [tuiCardMedium] { + width: 100%; + height: auto; + gap: 1rem; + } } `, ], imports: [ - FormsModule, - TuiSelectModule, - TuiTextfieldControllerModule, LogsComponent, TitleDirective, + KeyValuePipe, + TuiTitle, + TuiCardMedium, + TuiIcon, + TuiAppearance, + TuiLink, + TuiButton, ], }) 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' - } + readonly current = signal(null) + readonly logs: Record = { + os: { + title: 'OS Logs', + subtitle: 'Raw, unfiltered operating system logs', + icon: '@tui.square-dashed-bottom-code', + follow: params => this.api.followServerLogs(params), + fetch: params => this.api.getServerLogs(params), + }, + kernel: { + title: 'Kernel Logs', + subtitle: 'Diagnostics for drivers and other kernel processes', + icon: '@tui.square-chevron-right', + follow: params => this.api.followKernelLogs(params), + fetch: params => this.api.getKernelLogs(params), + }, + tor: { + title: 'Tor Logs', + subtitle: 'Diagnostic logs for the Tor daemon on StartOS', + icon: '@tui.globe', + follow: params => this.api.followTorLogs(params), + fetch: params => this.api.getTorLogs(params), + }, } } diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts index 876063fa8..464fc323e 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts @@ -8,7 +8,9 @@ import { import { toSignal } from '@angular/core/rxjs-interop' import { RouterLink } from '@angular/router' import { getPkgId } from '@start9labs/shared' -import { TuiButton } from '@taiga-ui/core' +import { TuiItem } from '@taiga-ui/cdk' +import { TuiButton, TuiLink } from '@taiga-ui/core' +import { TuiBreadcrumbs } from '@taiga-ui/kit' import { PatchDB } from 'patch-db-client' import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component' import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils' @@ -22,6 +24,12 @@ import { TitleDirective } from 'src/app/services/title.service' Back {{ interface()?.name }} + + + Dashboard + + {{ interface()?.name }} + @if (interface(); as serviceInterface) { } `, + styles: ` + :host-context(tui-root._mobile) tui-breadcrumbs { + display: none; + } + `, host: { class: 'g-subpage' }, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [InterfaceComponent, RouterLink, TuiButton, TitleDirective], + imports: [ + InterfaceComponent, + RouterLink, + TuiButton, + TitleDirective, + TuiBreadcrumbs, + TuiItem, + TuiLink, + ], }) export default class ServiceInterfaceRoute { private readonly config = inject(ConfigService) diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/outlet.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/outlet.component.ts index dc38446d6..816b785a0 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/outlet.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/outlet.component.ts @@ -52,6 +52,9 @@ const ICONS = { > {{ item }} + @if (item === 'dashboard') { + + } } @@ -80,7 +83,12 @@ const ICONS = { margin: 0 -0.5rem; } - .active { + a a { + display: none; + } + + .active, + a:has(.active) { color: var(--tui-text-primary); [tuiTitle] { @@ -117,7 +125,8 @@ const ICONS = { background: var(--tui-background-neutral-1); box-shadow: inset 0 -1px var(--tui-background-neutral-1); - &.active { + &.active, + &:has(.active) { background: none; box-shadow: none; } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts index bdc30d25f..f2d4196ea 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts @@ -1,4 +1,4 @@ -import { AsyncPipe } from '@angular/common' +import { AsyncPipe, DOCUMENT } from '@angular/common' import { ChangeDetectionStrategy, Component, @@ -68,7 +68,7 @@ import { SystemWipeComponent } from './wipe.component' - {{ 'system.general.update' | i18n }} + {{ 'system.general.update.title' | i18n }} {{ server.version }} @@ -79,12 +79,12 @@ import { SystemWipeComponent } from './wipe.component' (click)="onUpdate()" > @if (server.statusInfo.updated) { - {{ 'system.general.restart' | i18n }} + {{ 'system.general.update.button.restart' | i18n }} } @else { @if (eos.showUpdate$ | async) { {{ 'ui.update' | i18n }} } @else { - {{ 'system.general.check' | i18n }} + {{ 'system.general.update.button.check' | i18n }} } } @@ -124,14 +124,28 @@ import { SystemWipeComponent } from './wipe.component' /> +
+ + + + {{ 'system.general.ca.title' | i18n }} + + + {{ 'system.general.ca.subtitle' | i18n }} + + + +
- {{ 'system.general.tor' | i18n }} + {{ 'system.general.tor.title' | i18n }} - {{ 'system.general.daemon' | i18n }} + {{ 'system.general.tor.subtitle' | i18n }}
} } + + `, styles: ` :host { @@ -209,6 +225,7 @@ export default class SystemGeneralComponent { SystemWipeComponent, inject(INJECTOR), ) + private readonly document = inject(DOCUMENT) wipe = false count = 0 @@ -268,6 +285,10 @@ export default class SystemGeneralComponent { .subscribe(() => this.resetTor(this.wipe)) } + downloadCA() { + this.document.getElementById('download-ca')?.click() + } + async onRepair() { this.dialogs .open(TUI_CONFIRM, { diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/interfaces/interfaces.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/interfaces/interfaces.component.ts index 31bb6727e..15e411cff 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/interfaces/interfaces.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/interfaces/interfaces.component.ts @@ -14,9 +14,9 @@ import { TitleDirective } from 'src/app/services/title.service' const iface: T.ServiceInterface = { id: '', - name: 'StartOS User Interface', + name: 'StartOS UI', description: - 'The primary user interface for your StartOS server, accessible from any browser.', + 'The web user interface for your StartOS server, accessible from any browser.', type: 'ui' as const, masked: false, addressInfo: { @@ -33,15 +33,12 @@ const iface: T.ServiceInterface = { template: ` Back - Web Addresses + StartOS UI
-

User Interface Addresses

-

- View and manage private and public addresses for accessing your - StartOS UI -

+

{{ iface.name }}

+

{{ iface.description }}

@if (ui(); as ui) { @@ -61,6 +58,7 @@ const iface: T.ServiceInterface = { }) export default class StartOsUiComponent { private readonly config = inject(ConfigService) + iface = iface readonly ui = toSignal( inject>(PatchDB) diff --git a/web/projects/ui/src/styles.scss b/web/projects/ui/src/styles.scss index 62ebb31b9..85511c705 100644 --- a/web/projects/ui/src/styles.scss +++ b/web/projects/ui/src/styles.scss @@ -366,6 +366,10 @@ button.g-action { color: var(--tui-status-info) !important; } +.g-primary { + color: var(--tui-text-primary) !important; +} + .g-secondary { color: var(--tui-text-secondary) !important; }