From bdfa918a33a79ff5e12b6beca04e9e1bd853fae7 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Sat, 21 Mar 2026 16:32:46 -0600 Subject: [PATCH] a bunch of UI cleanup around backups as well as other bug fixes and UII improvements --- .../src/app/pages/success.page.ts | 16 +-- .../shared/src/i18n/dictionaries/de.ts | 12 +- .../shared/src/i18n/dictionaries/en.ts | 12 +- .../shared/src/i18n/dictionaries/es.ts | 12 +- .../shared/src/i18n/dictionaries/fr.ts | 12 +- .../shared/src/i18n/dictionaries/pl.ts | 12 +- .../src/app/routes/home/routes/devices/add.ts | 2 +- .../app/routes/home/routes/devices/config.ts | 2 +- .../app/routes/home/routes/devices/index.ts | 4 +- .../app/components/backup-report.component.ts | 83 ++++++----- .../addresses/addresses.component.ts | 27 +++- .../interfaces/addresses/plugin.component.ts | 2 +- .../interfaces/interface.service.ts | 2 + .../routes/backups/modals/backup.component.ts | 2 +- .../backups/modals/history.component.ts | 1 + .../routes/notifications/item.component.ts | 20 +-- .../authorities/authorities.component.ts | 18 ++- .../routes/backups/backups.component.ts | 130 ++++++++---------- .../routes/backups/network.component.ts | 62 +++++---- .../routes/backups/physical.component.ts | 44 +++--- .../routes/backups/progress.component.ts | 40 ++++-- .../routes/backups/restore.component.ts | 68 +++++---- .../system/routes/backups/status.component.ts | 6 +- .../routes/system/routes/dns/dns.component.ts | 22 ++- .../routes/gateways/gateways.component.ts | 22 ++- .../system/routes/smtp/smtp.component.ts | 22 ++- .../routes/system/routes/ssh/ssh.component.ts | 24 ++-- .../system/routes/wifi/wifi.component.ts | 24 +++- .../src/app/services/notification.service.ts | 54 +++++--- 29 files changed, 446 insertions(+), 311 deletions(-) diff --git a/web/projects/setup-wizard/src/app/pages/success.page.ts b/web/projects/setup-wizard/src/app/pages/success.page.ts index f21d5f4ba..8fc7884bc 100644 --- a/web/projects/setup-wizard/src/app/pages/success.page.ts +++ b/web/projects/setup-wizard/src/app/pages/success.page.ts @@ -34,15 +34,15 @@ import { StateService } from '../services/state.service' {{ 'Setup Complete!' | i18n }} + @if (!stateService.kiosk) { +

+ {{ + 'http://start.local was for setup only. It will no longer work.' + | i18n + }} +

+ } - @if (!stateService.kiosk) { -

- {{ - 'http://start.local was for setup only. It will no longer work.' - | i18n - }} -

- } @if (!result) { diff --git a/web/projects/shared/src/i18n/dictionaries/de.ts b/web/projects/shared/src/i18n/dictionaries/de.ts index 3215c61d6..8e93725b9 100644 --- a/web/projects/shared/src/i18n/dictionaries/de.ts +++ b/web/projects/shared/src/i18n/dictionaries/de.ts @@ -302,15 +302,11 @@ export default { 317: 'Originalpasswort', 318: 'Originalpasswort eingeben', 319: 'Sicherung wird gestartet', - 320: 'Sichern Sie StartOS und Dienstdaten, indem Sie sich mit einem Gerät im lokalen Netzwerk oder einem physischen Laufwerk verbinden, das an Ihren Server angeschlossen ist.', - 321: 'Stellen Sie StartOS und Dienstdaten von einem Gerät im lokalen Netzwerk oder einem physischen Laufwerk mit vorhandener Sicherung wieder her.', 322: 'Letzte Sicherung', - 323: 'Ein Ordner auf einem anderen Computer, der mit demselben Netzwerk wie Ihr Start9-Server verbunden ist.', - 324: 'Ein physisches Laufwerk, das direkt an Ihren Start9-Server angeschlossen ist.', - 325: 'Dienste für Sicherung auswählen', - 326: 'Serversicherung auswählen', + 325: 'Dienste auswählen', + 326: 'Server auswählen', 327: 'Netzwerkordner', - 328: 'Neuen öffnen', + 328: 'Neuen', 329: 'Hostname', 330: 'Pfad', 331: 'URL', @@ -355,7 +351,6 @@ export default { 372: 'Passwort erforderlich', 373: 'Geben Sie das Master-Passwort ein, das zur Verschlüsselung dieser Sicherung verwendet wurde. Im nächsten Schritt wählen Sie die Dienste aus, die wiederhergestellt werden sollen.', 374: 'Laufwerk wird entschlüsselt', - 375: 'Dienste zur Wiederherstellung auswählen', 376: 'Für Sicherung verfügbar', 377: 'StartOS-Sicherungen erkannt', 378: 'Keine StartOS-Sicherungen erkannt', @@ -726,4 +721,5 @@ export default { 803: 'Dieses Laufwerk verwendet ext4 und wird automatisch in btrfs konvertiert. Ein Backup wird dringend empfohlen, bevor Sie fortfahren.', 804: 'Ich habe ein Backup meiner Daten', 805: 'Öffentliche Domain hinzufügen', + 806: 'Ergebnis', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/en.ts b/web/projects/shared/src/i18n/dictionaries/en.ts index 957f839df..ed961ee84 100644 --- a/web/projects/shared/src/i18n/dictionaries/en.ts +++ b/web/projects/shared/src/i18n/dictionaries/en.ts @@ -301,15 +301,11 @@ export const ENGLISH: Record = { 'Original Password': 317, 'Enter original password': 318, 'Beginning backup': 319, - 'Back up StartOS and service data by connecting to a device on your local network or a physical drive connected to your server.': 320, - 'Restore StartOS and service data from a device on your local network or a physical drive connected to your server that contains an existing backup.': 321, 'Last Backup': 322, // as in, the last time the server was backed up - 'A folder on another computer that is connected to the same network as your Start9 server.': 323, - 'A physical drive that is plugged directly into your Start9 Server.': 324, - 'Select Services to Back Up': 325, - 'Select server backup': 326, + 'Select services': 325, + 'Select server': 326, 'Network Folders': 327, - 'Open New': 328, + 'New': 328, 'Hostname': 329, 'Path': 330, // as in, a URL path 'URL': 331, @@ -354,7 +350,6 @@ export const ENGLISH: Record = { 'Password required': 372, 'Enter the master password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.': 373, 'Decrypting drive': 374, - 'Select services to restore': 375, 'Available for backup': 376, 'StartOS backups detected': 377, 'No StartOS backups detected': 378, @@ -727,4 +722,5 @@ export const ENGLISH: Record = { 'This drive uses ext4 and will be automatically converted to btrfs. A backup is strongly recommended before proceeding.': 803, 'I have a backup of my data': 804, 'Add Public Domain': 805, + 'Result': 806, } diff --git a/web/projects/shared/src/i18n/dictionaries/es.ts b/web/projects/shared/src/i18n/dictionaries/es.ts index 1382c1be5..01edc0200 100644 --- a/web/projects/shared/src/i18n/dictionaries/es.ts +++ b/web/projects/shared/src/i18n/dictionaries/es.ts @@ -302,15 +302,11 @@ export default { 317: 'Contraseña original', 318: 'Ingresa la contraseña original', 319: 'Iniciando copia de seguridad', - 320: 'Haz una copia de seguridad de StartOS y los datos de los servicios conectándote a un dispositivo en tu red local o a una unidad física conectada a tu servidor.', - 321: 'Restaura StartOS y los datos de los servicios desde un dispositivo en tu red local o una unidad física conectada a tu servidor que contenga una copia de seguridad existente.', 322: 'Última copia de seguridad', - 323: 'Una carpeta en otro ordenador conectado a la misma red que tu servidor Start9.', - 324: 'Una unidad física conectada directamente a tu servidor Start9.', - 325: 'Seleccionar servicios para respaldar', - 326: 'Seleccionar copia de seguridad del servidor', + 325: 'Seleccionar servicios', + 326: 'Seleccionar servidor', 327: 'Carpetas de red', - 328: 'Abrir nuevo', + 328: 'Nuevo', 329: 'Nombre del host', 330: 'Ruta', 331: 'URL', @@ -355,7 +351,6 @@ export default { 372: 'Se requiere contraseña', 373: 'Ingresa la contraseña maestra que se usó para cifrar esta copia de seguridad. En la siguiente pantalla, podrás seleccionar los servicios individuales que deseas restaurar.', 374: 'Descifrando unidad', - 375: 'Seleccionar servicios para restaurar', 376: 'Disponible para copia de seguridad', 377: 'Copias de seguridad de StartOS detectadas', 378: 'No se detectaron copias de seguridad de StartOS', @@ -726,4 +721,5 @@ export default { 803: 'Esta unidad usa ext4 y se convertirá automáticamente a btrfs. Se recomienda encarecidamente hacer una copia de seguridad antes de continuar.', 804: 'Tengo una copia de seguridad de mis datos', 805: 'Agregar dominio público', + 806: 'Resultado', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/fr.ts b/web/projects/shared/src/i18n/dictionaries/fr.ts index 7fac66a9d..aa9460f85 100644 --- a/web/projects/shared/src/i18n/dictionaries/fr.ts +++ b/web/projects/shared/src/i18n/dictionaries/fr.ts @@ -302,15 +302,11 @@ export default { 317: 'Mot de passe d’origine', 318: 'Entrez le mot de passe d’origine', 319: 'Démarrage de la sauvegarde', - 320: 'Sauvegardez StartOS et les données des services en connectant un appareil sur votre réseau local ou un disque physique connecté à votre serveur.', - 321: 'Restaurez StartOS et les données des services à partir d’un appareil sur votre réseau local ou d’un disque physique connecté à votre serveur contenant une sauvegarde.', 322: 'Dernière sauvegarde', - 323: 'Un dossier sur un autre ordinateur connecté au même réseau que votre serveur Start9.', - 324: 'Un disque physique branché directement à votre serveur Start9.', - 325: 'Sélectionner les services à sauvegarder', - 326: 'Sélectionner la sauvegarde du serveur', + 325: 'Sélectionner les services', + 326: 'Sélectionner le serveur', 327: 'Dossiers réseau', - 328: 'Ouvrir nouveau', + 328: 'Nouveau', 329: 'Nom d’hôte', 330: 'Chemin', 331: 'URL', @@ -355,7 +351,6 @@ export default { 372: 'Mot de passe requis', 373: 'Entrez le mot de passe principal utilisé pour chiffrer cette sauvegarde. Vous pourrez choisir les services à restaurer à l’écran suivant.', 374: 'Déchiffrement du disque', - 375: 'Sélectionner les services à restaurer', 376: 'Disponible pour la sauvegarde', 377: 'Sauvegardes StartOS détectées', 378: 'Aucune sauvegarde StartOS détectée', @@ -726,4 +721,5 @@ export default { 803: 'Ce disque utilise ext4 et sera automatiquement converti en btrfs. Il est fortement recommandé de faire une sauvegarde avant de continuer.', 804: "J'ai une sauvegarde de mes données", 805: 'Ajouter un domaine public', + 806: 'Résultat', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/pl.ts b/web/projects/shared/src/i18n/dictionaries/pl.ts index 2c412f701..d8ffd4153 100644 --- a/web/projects/shared/src/i18n/dictionaries/pl.ts +++ b/web/projects/shared/src/i18n/dictionaries/pl.ts @@ -302,15 +302,11 @@ export default { 317: 'Oryginalne hasło', 318: 'Wprowadź oryginalne hasło', 319: 'Rozpoczynanie tworzenia kopii zapasowej', - 320: 'Utwórz kopię zapasową StartOS i danych serwisów, łącząc się z urządzeniem w sieci lokalnej lub z fizycznym dyskiem podłączonym do Twojego serwera.', - 321: 'Przywróć StartOS i dane serwisów z urządzenia w sieci lokalnej lub z fizycznego dysku podłączonego do Twojego serwera, który zawiera istniejącą kopię zapasową.', 322: 'Ostatnia kopia zapasowa', - 323: 'Folder na innym komputerze, który jest podłączony do tej samej sieci co twój serwer Start9.', - 324: 'Fizyczny dysk, który jest podłączony bezpośrednio do Twojego serwera Start9.', - 325: 'Wybierz serwisy do kopii zapasowej', - 326: 'Wybierz kopię zapasową serwera', + 325: 'Wybierz serwisy', + 326: 'Wybierz serwer', 327: 'Foldery sieciowe', - 328: 'Otwórz nowy', + 328: 'Nowy', 329: 'Nazwa hosta', 330: 'Ścieżka', 331: 'URL', @@ -355,7 +351,6 @@ export default { 372: 'Wymagane hasło', 373: 'Wprowadź hasło główne, które zostało użyte do zaszyfrowania tej kopii zapasowej. Na następnym ekranie wybierzesz poszczególne serwisy, które chcesz przywrócić.', 374: 'Odszyfrowywanie dysku', - 375: 'Wybierz serwisy do przywrócenia', 376: 'Dostępne do kopii zapasowej', 377: 'Wykryto kopie zapasowe StartOS', 378: 'Nie wykryto kopii zapasowych StartOS', @@ -726,4 +721,5 @@ export default { 803: 'Ten dysk używa ext4 i zostanie automatycznie skonwertowany na btrfs. Zdecydowanie zaleca się wykonanie kopii zapasowej przed kontynuowaniem.', 804: 'Mam kopię zapasową moich danych', 805: 'Dodaj domenę publiczną', + 806: 'Wynik', } satisfies i18n diff --git a/web/projects/start-tunnel/src/app/routes/home/routes/devices/add.ts b/web/projects/start-tunnel/src/app/routes/home/routes/devices/add.ts index 790ea6f9c..fe10d0b82 100644 --- a/web/projects/start-tunnel/src/app/routes/home/routes/devices/add.ts +++ b/web/projects/start-tunnel/src/app/routes/home/routes/devices/add.ts @@ -169,7 +169,7 @@ export class DevicesAdd { }) this.dialogs - .open(DEVICES_CONFIG, { data: config, closable: false }) + .open(DEVICES_CONFIG, { data: config, closable: false, size: 'm' }) .subscribe() } } catch (e: any) { diff --git a/web/projects/start-tunnel/src/app/routes/home/routes/devices/config.ts b/web/projects/start-tunnel/src/app/routes/home/routes/devices/config.ts index 3e6587875..4f495039e 100644 --- a/web/projects/start-tunnel/src/app/routes/home/routes/devices/config.ts +++ b/web/projects/start-tunnel/src/app/routes/home/routes/devices/config.ts @@ -22,7 +22,7 @@ import { QrCodeComponent } from 'ng-qrcode' - @if (segmented?.activeItemIndex) { + @if (segmented?.activeItemIndex()) { } @else { diff --git a/web/projects/start-tunnel/src/app/routes/home/routes/devices/index.ts b/web/projects/start-tunnel/src/app/routes/home/routes/devices/index.ts index 6867d00f7..89dca6941 100644 --- a/web/projects/start-tunnel/src/app/routes/home/routes/devices/index.ts +++ b/web/projects/start-tunnel/src/app/routes/home/routes/devices/index.ts @@ -159,7 +159,9 @@ export default class Devices { try { const data = await this.api.showDeviceConfig({ subnet: subnet.range, ip }) - this.dialogs.open(DEVICES_CONFIG, { data, closable: false }).subscribe() + this.dialogs + .open(DEVICES_CONFIG, { data, closable: false, size: 'm' }) + .subscribe() } catch (e: any) { console.log(e) this.errorService.handleError(e) diff --git a/web/projects/ui/src/app/components/backup-report.component.ts b/web/projects/ui/src/app/components/backup-report.component.ts index 628c08b0a..d2a70f36f 100644 --- a/web/projects/ui/src/app/components/backup-report.component.ts +++ b/web/projects/ui/src/app/components/backup-report.component.ts @@ -7,7 +7,7 @@ import { } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' import { i18nKey, i18nPipe } from '@start9labs/shared' -import { TuiDialogContext, TuiIcon, TuiTitle, TuiCell } from '@taiga-ui/core' +import { TuiDialogContext, TuiIcon } from '@taiga-ui/core' import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PatchDB } from 'patch-db-client' import { map } from 'rxjs' @@ -17,41 +17,58 @@ import { DataModel } from '../services/patch-db/data-model' @Component({ template: ` -

- {{ 'Completed' | i18n }}: {{ data.createdAt | date: 'medium' }} -

-
-
- {{ 'System data' | i18n }} -
- {{ system().result | i18n }} -
-
- -
- @if (pkgTitles(); as titles) { - @for (pkg of data.content.packages | keyvalue; track $index) { -
-
- {{ titles[pkg.key] || pkg.key }} -
- {{ - pkg.value.error - ? ('Failed' | i18n) + ': ' + pkg.value.error - : ('Succeeded' | i18n) - }} -
-
- -
- } +

{{ data.createdAt | date: 'medium' }}

+ + + + + + + + + + + + + @if (pkgTitles(); as titles) { + @for (pkg of data.content.packages | keyvalue; track $index) { + + + + + } + } + +
{{ 'Title' | i18n }}{{ 'Result' | i18n }}
{{ 'System data' | i18n }} + + {{ system().result | i18n }} +
{{ titles[pkg.key] || pkg.key }} + + {{ + pkg.value.error + ? ('Failed' | i18n) + ': ' + pkg.value.error + : ('Succeeded' | i18n) + }} +
+ `, + styles: ` + .timestamp { + color: var(--tui-text-secondary); + margin: 0 0 1rem; + } + + td:first-child { + white-space: nowrap; + } + + tui-icon { + font-size: 1rem; + vertical-align: sub; + margin-inline-end: 0.25rem; } `, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [CommonModule, TuiIcon, TuiCell, TuiTitle, i18nPipe], + imports: [CommonModule, TuiIcon, i18nPipe], }) export class BackupsReportModal { private readonly i18n = inject(i18nPipe) diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/addresses.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/addresses.component.ts index 3a8688556..0cc9e7708 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/addresses.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/addresses.component.ts @@ -7,7 +7,13 @@ import { } from '@angular/core' import { ErrorService, i18nPipe } from '@start9labs/shared' import { ISB, utils } from '@start9labs/start-sdk' -import { TuiButton, TuiDataList, TuiDropdown, TuiInput } from '@taiga-ui/core' +import { + TuiButton, + TuiDataList, + TuiDropdown, + TuiIcon, + TuiInput, +} from '@taiga-ui/core' import { TuiNotificationMiddleService } from '@taiga-ui/kit' import { PatchDB } from 'patch-db-client' import { firstValueFrom } from 'rxjs' @@ -33,7 +39,18 @@ import { InterfaceAddressItemComponent } from './item.component' selector: 'section[gatewayGroup]', template: `
- {{ 'Gateway' | i18n }}: {{ gatewayGroup().gatewayName }} + @switch (gatewayGroup().deviceType) { + @case ('ethernet') { + + } + @case ('wireless') { + + } + @case ('wireguard') { + + } + } + {{ gatewayGroup().gatewayName }} @if (gatewayGroup().isWireguard) { - } @if ([1, 2].includes(item.code)) { + } @else if (overflow) { + } } @@ -186,12 +185,7 @@ export class NotificationItemComponent { overflow = false onClick(item: ServerNotification) { - if (this.overflow) { - this.service.viewModal(item, true) - item.seen = true - } else if ([1, 2].includes(item.code)) { - this.service.viewModal(item) - item.seen = true - } + this.service.viewModal(item) + item.seen = true } } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/authorities/authorities.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/authorities/authorities.component.ts index 590da78ae..d5d6f1bf3 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/authorities/authorities.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/authorities/authorities.component.ts @@ -9,10 +9,20 @@ import { AuthoritiesTableComponent } from './table.component' @Component({ template: ` - - {{ 'Back' | i18n }} - - {{ 'Certificate Authorities' | i18n }} +
+ + {{ 'Back' | i18n }} + + {{ 'Certificate Authorities' | i18n }} + +
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backups.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backups.component.ts index 94e2aeb89..0609ce444 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backups.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backups.component.ts @@ -9,13 +9,7 @@ import { toSignal } from '@angular/core/rxjs-interop' import { ActivatedRoute, RouterLink } from '@angular/router' import { DialogService, DocsLinkDirective, i18nPipe } from '@start9labs/shared' import { TuiMapperPipe } from '@taiga-ui/cdk' -import { - TuiButton, - TuiLink, - TuiLoader, - TuiNotification, - TuiTitle, -} from '@taiga-ui/core' +import { TuiButton, TuiLoader, TuiNotification, TuiTitle } from '@taiga-ui/core' import { TuiHeader } from '@taiga-ui/layout' import { PatchDB } from 'patch-db-client' import { @@ -35,12 +29,28 @@ import { BACKUP_RESTORE } from './restore.component' @Component({ template: ` - - {{ 'Back' | i18n }} - - {{ - type === 'create' ? ('Create Backup' | i18n) : ('Restore Backup' | i18n) - }} +
+ + {{ 'Back' | i18n }} + + {{ + type === 'create' + ? ('Create Backup' | i18n) + : ('Restore Backup' | i18n) + }} + +
@@ -51,36 +61,19 @@ import { BACKUP_RESTORE } from './restore.component' ? ('Create Backup' | i18n) : ('Restore Backup' | i18n) }} + -

- @if (type === 'create') { - {{ - 'Back up StartOS and service data by connecting to a device on your local network or a physical drive connected to your server.' - | i18n - }} - - } @else { - {{ - 'Restore StartOS and service data from a device on your local network or a physical drive connected to your server that contains an existing backup.' - | i18n - }} - - } -

@@ -109,30 +102,16 @@ import { BACKUP_RESTORE } from './restore.component' [style.height.rem]="20" /> } @else { -
- {{ - 'A folder on another computer that is connected to the same network as your Start9 server.' - | i18n - }} - -
-
- {{ - 'A physical drive that is plugged directly into your Start9 Server.' - | i18n - }} -
+
+
} } `, + styles: ` + :host-context(tui-root._mobile) [tuiHeader] { + display: none; + } + `, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ AsyncPipe, @@ -140,7 +119,6 @@ import { BACKUP_RESTORE } from './restore.component' RouterLink, TuiButton, TuiLoader, - TuiLink, TuiHeader, TuiTitle, TuiNotification, @@ -184,12 +162,22 @@ export default class SystemBackupComponent implements OnInit { } onTarget(target: MappedBackupTarget) { - const component = this.type === 'create' ? BACKUP : BACKUP_RESTORE - const label = - this.type === 'create' - ? 'Select Services to Back Up' - : 'Select server backup' - - this.dialog.openComponent(component, { label, data: target }).subscribe() + if (this.type === 'create') { + this.dialog + .openComponent(BACKUP, { + label: 'Select services', + data: target, + size: 'm', + }) + .subscribe() + } else { + this.dialog + .openComponent(BACKUP_RESTORE, { + label: 'Select server', + data: target, + size: 'l', + }) + .subscribe() + } } } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/network.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/network.component.ts index b40b5196d..29f99a1b4 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/network.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/network.component.ts @@ -7,8 +7,8 @@ import { import { ActivatedRoute } from '@angular/router' import { DialogService, ErrorService, i18nPipe } from '@start9labs/shared' import { ISB, T } from '@start9labs/start-sdk' -import { TuiButton, TuiIcon } from '@taiga-ui/core' -import { TuiNotificationMiddleService, TuiTooltip } from '@taiga-ui/kit' +import { TuiButton, TuiDataList, TuiDropdown, TuiIcon } from '@taiga-ui/core' +import { TuiNotificationMiddleService } from '@taiga-ui/kit' import { filter } from 'rxjs' import { FormComponent } from 'src/app/routes/portal/components/form.component' import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component' @@ -28,10 +28,14 @@ const ERROR = template: `
{{ 'Network Folders' | i18n }} - - -
@@ -59,24 +63,29 @@ const ERROR = {{ target.entry.path.split('/').pop() }} {{ target.entry.hostname }} {{ target.entry.path }} - + - + + @@ -106,7 +115,7 @@ const ERROR = } td:first-child { - width: 13rem; + width: 16rem; } td:last-child { @@ -114,10 +123,6 @@ const ERROR = text-align: right; } - [tuiButton] { - margin-inline-start: auto; - } - span { display: flex; align-items: center; @@ -166,8 +171,9 @@ const ERROR = changeDetection: ChangeDetectionStrategy.OnPush, imports: [ TuiButton, + TuiDataList, + TuiDropdown, TuiIcon, - TuiTooltip, PlaceholderComponent, BackupStatusComponent, TableComponent, @@ -186,6 +192,8 @@ export class BackupNetworkComponent { readonly service = inject(BackupService) readonly networkFolders = output>() + opens: Record = {} + select(target: MappedBackupTarget) { if (!target.entry.mountable) { this.dialog.openAlert(ERROR, { label: 'Unable to connect' }).subscribe() @@ -321,7 +329,6 @@ export class BackupNetworkComponent { ), required: true, default: null, - placeholder: 'My Network Folder', }), password: ISB.Value.text({ name: this.i18n.transform('Password')!, @@ -331,7 +338,6 @@ export class BackupNetworkComponent { required: false, default: null, masked: true, - placeholder: 'My Network Folder', }), }) } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/physical.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/physical.component.ts index c2abb021e..ded119f6f6 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/physical.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/physical.component.ts @@ -5,9 +5,8 @@ import { output, } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { ConvertBytesPipe, DialogService, i18nPipe } from '@start9labs/shared' -import { TuiButton, TuiIcon } from '@taiga-ui/core' -import { TuiTooltip } from '@taiga-ui/kit' +import { DialogService, i18nPipe } from '@start9labs/shared' +import { TuiButton } from '@taiga-ui/core' import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component' import { TableComponent } from 'src/app/routes/portal/components/table.component' import { DiskBackupTarget } from 'src/app/services/api/api.types' @@ -19,11 +18,9 @@ import { BackupStatusComponent } from './status.component' template: `
{{ 'Physical Drives' | i18n }} - -
- +
@for (target of service.drives(); track $index) { - - - + + + } @empty { @@ -74,10 +66,6 @@ import { BackupStatusComponent } from './status.component' } } - td:first-child { - width: 13rem; - } - :host-context(tui-root._mobile) { tr { grid-template-columns: min-content 1fr 4rem; @@ -109,9 +97,6 @@ import { BackupStatusComponent } from './status.component' changeDetection: ChangeDetectionStrategy.OnPush, imports: [ TuiButton, - TuiIcon, - TuiTooltip, - ConvertBytesPipe, PlaceholderComponent, BackupStatusComponent, TableComponent, @@ -122,9 +107,26 @@ export class BackupPhysicalComponent { private readonly dialog = inject(DialogService) private readonly type = inject(ActivatedRoute).snapshot.data['type'] + private readonly i18n = inject(i18nPipe) + readonly service = inject(BackupService) readonly physicalFolders = output>() + driveName(entry: DiskBackupTarget): string { + return ( + [entry.vendor, entry.model].filter(Boolean).join(' ') || + this.i18n.transform('Unknown Drive') + ) + } + + formatCapacity(bytes: number): string { + const gb = bytes / 1e9 + if (gb >= 1000) { + return `${(gb / 1000).toFixed(1)} TB` + } + return `${gb.toFixed(0)} GB` + } + select(target: MappedBackupTarget) { if (this.type === 'restore' && !target.hasAnyBackup) { this.dialog diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/progress.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/progress.component.ts index 7a597e7f6..365db87e9 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/progress.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/progress.component.ts @@ -22,24 +22,42 @@ import { DataModel } from 'src/app/services/patch-db/data-model' {{ (pkg.value | toManifest).title }} - - @if (progress.complete) { - - {{ 'complete' | i18n }} + + + @if (progress.complete) { + + {{ 'complete' | i18n }} + } @else { + @if ((pkg.key | tuiMapper: toStatus | async) === 'backing-up') { + + {{ 'backing up' | i18n }} } @else { - @if ((pkg.key | tuiMapper: toStatus | async) === 'backing-up') { - - {{ 'backing up' | i18n }} - } @else { - {{ 'waiting' | i18n }} - } + + {{ 'waiting' | i18n }} } - + } } } `, + styles: ` + :host { + max-width: 36rem; + } + + .status { + display: flex; + align-items: center; + gap: 0.25rem; + margin-inline-start: auto; + white-space: nowrap; + } + + .status tui-icon { + font-size: 1rem; + } + `, host: { class: 'g-card' }, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/restore.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/restore.component.ts index d84099b1c..7cc9f2d58 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/restore.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/restore.component.ts @@ -3,12 +3,12 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { DialogService, ErrorService, - i18nPipe, StartOSDiskInfo, } from '@start9labs/shared' -import { TuiCell, TuiTitle } from '@taiga-ui/core' +import { TuiButton } from '@taiga-ui/core' import { TuiNotificationMiddleService } from '@taiga-ui/kit' import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus' +import { TableComponent } from 'src/app/routes/portal/components/table.component' import { ApiService } from 'src/app/services/api/embassy-api.service' import { verifyPassword } from 'src/app/utils/verify-password' import { BackupContext } from './backup.types' @@ -16,31 +16,49 @@ import { RECOVER } from './recover.component' @Component({ template: ` - @for (server of target.entry.startOs | keyvalue; track $index) { - +
- {{ target.entry.label || target.entry.logicalname }} - - {{ target.entry.vendor || 'Unknown Vendor' }} - - {{ target.entry.model || 'Unknown Model' }} - {{ target.entry.capacity | convertBytes }}{{ target.entry.logicalname }}{{ driveName(target.entry) }}{{ formatCapacity(target.entry.capacity) }}
+ @for (server of target.entry.startOs | keyvalue; track $index) { + + + + + + + } +
{{ server.value.hostname }}.local{{ server.value.version }}{{ server.value.timestamp | date: 'medium' }} + +
+ `, + styles: ` + td:last-child { + text-align: right; + } + + :host-context(tui-root._mobile) { + tr { + grid-template-columns: 1fr auto; + } + + .name { + color: var(--tui-text-primary); + font: var(--tui-typography-body-m); + font-weight: bold; + } + + td:last-child { + grid-area: 1 / 2 / 4 / 2; + align-self: center; + } } `, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [KeyValuePipe, DatePipe, TuiCell, TuiTitle, i18nPipe], + imports: [KeyValuePipe, DatePipe, TuiButton, TableComponent], }) export class BackupRestoreComponent { private readonly dialog = inject(DialogService) @@ -82,7 +100,7 @@ export class BackupRestoreComponent { this.context.$implicit.complete() this.dialog - .openComponent(RECOVER, { label: 'Select services to restore', data }) + .openComponent(RECOVER, { label: 'Select services', data }) .subscribe() } finally { loader.unsubscribe() diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/status.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/status.component.ts index 20e26faf8..de84f1885 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/status.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/status.component.ts @@ -13,7 +13,7 @@ import { TuiIcon } from '@taiga-ui/core' template: ` @if (type === 'create') { {{ 'Available for backup' | i18n }} @@ -22,7 +22,7 @@ import { TuiIcon } from '@taiga-ui/core' {{ 'StartOS backups detected' | i18n }} } @else { - + {{ 'No StartOS backups detected' | i18n }} } } @@ -37,6 +37,8 @@ import { TuiIcon } from '@taiga-ui/core' tui-icon { font-size: 1rem; + min-width: 1.25rem; + text-align: center; } :host-context(tui-root._mobile) { diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/dns/dns.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/dns/dns.component.ts index f70b13348..679b8d063 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/dns/dns.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/dns/dns.component.ts @@ -27,10 +27,20 @@ const ipv6 = @Component({ template: ` - - {{ 'Back' | i18n }} - - {{ 'DNS Servers' | i18n }} +
+ + {{ 'Back' | i18n }} + + {{ 'DNS Servers' | i18n }} + +
@if (data(); as d) {
@@ -76,6 +86,10 @@ const ipv6 = max-width: 36rem; } + :host-context(tui-root._mobile) [tuiHeader] { + display: none; + } + form header, form footer { margin: 1rem 0; diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/gateways.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/gateways.component.ts index 0b9776e2e..08666d2cc 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/gateways.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/gateways.component.ts @@ -29,10 +29,20 @@ import { GatewaysTableComponent } from './table.component' @Component({ template: ` - - {{ 'Back' | i18n }} - - {{ 'Gateways' | i18n }} +
+ + {{ 'Back' | i18n }} + + {{ 'Gateways' | i18n }} + +
@@ -121,6 +131,10 @@ import { GatewaysTableComponent } from './table.component' } `, styles: ` + :host-context(tui-root._mobile) .g-card > header > [docsLink] { + display: none; + } + .outbound { max-width: 24rem; margin-top: 2rem; diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/smtp/smtp.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/smtp/smtp.component.ts index 6ee478ba5..aa4dabe01 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/smtp/smtp.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/smtp/smtp.component.ts @@ -40,10 +40,20 @@ function detectProviderKey(host: string | undefined): string { @Component({ template: ` - - {{ 'Back' | i18n }} - - SMTP + @if (form$ | async; as data) { @@ -129,6 +139,10 @@ function detectProviderKey(host: string | undefined): string { max-width: 36rem; } + :host-context(tui-root._mobile) form:first-of-type > [tuiHeader] { + display: none; + } + form header, form footer { margin: 1rem 0; diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/ssh/ssh.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/ssh/ssh.component.ts index e25e9f86f..9612690a1 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/ssh/ssh.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/ssh/ssh.component.ts @@ -26,15 +26,11 @@ import { SSHTableComponent } from './table.component' @Component({ template: ` - - {{ 'Back' | i18n }} - - SSH - - @let keys = keys$ | async; -
-
- {{ 'SSH Keys' | i18n }} + + + @let keys = keys$ | async; +
+
+ {{ 'SSH Keys' | i18n }}

@@ -131,6 +141,10 @@ import { wifiSpec } from './wifi.const' :host { max-width: 36rem; } + + :host-context(tui-root._mobile) header > [docsLink] { + display: none; + } `, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ diff --git a/web/projects/ui/src/app/services/notification.service.ts b/web/projects/ui/src/app/services/notification.service.ts index e6721e9b3..5dedae10f 100644 --- a/web/projects/ui/src/app/services/notification.service.ts +++ b/web/projects/ui/src/app/services/notification.service.ts @@ -36,7 +36,7 @@ export class NotificationService { getColor(notification: ServerNotification): string { switch (notification.level) { case 'info': - return 'var(--tui-status-info)' + return 'var(--tui-text-secondary)' case 'success': return 'var(--tui-status-positive)' case 'warning': @@ -62,26 +62,42 @@ export class NotificationService { } } - viewModal(notification: ServerNotification, full = false) { + viewModal(notification: ServerNotification) { const { data, createdAt, code, title, message } = notification - if (code === 1) { - // Backup Report - this.dialogs - .openComponent(full ? message : REPORT, { - label: 'Backup Report', - data: { content: data, createdAt }, - }) - .subscribe() - } else { - // Markdown viewer - this.dialogs - .openComponent(full ? message : MARKDOWN, { - label: title as i18nKey, - data: of(data), - size: 'l', - }) - .subscribe() + switch (code) { + // Backup report - structured report with per-service results + case 1: + this.dialogs + .openComponent(REPORT, { + label: 'Backup Report', + data: { content: data, createdAt }, + size: 'l', + }) + .subscribe() + break + + // OS update - data contains the full release notes markdown + case 2: + this.dialogs + .openComponent(MARKDOWN, { + label: title as i18nKey, + data: of(data), + size: 'l', + }) + .subscribe() + break + + // General notification - show the message text + default: + this.dialogs + .openComponent(MARKDOWN, { + label: title as i18nKey, + data: of(message), + size: 'l', + }) + .subscribe() + break } } }