a bunch of UI cleanup around backups as well as other bug fixes and UII improvements

This commit is contained in:
Matt Hill
2026-03-21 16:32:46 -06:00
parent 8b65490d0e
commit bdfa918a33
29 changed files with 446 additions and 311 deletions

View File

@@ -34,15 +34,15 @@ import { StateService } from '../services/state.service'
<tui-icon icon="@tui.circle-check-big" class="g-positive" /> <tui-icon icon="@tui.circle-check-big" class="g-positive" />
{{ 'Setup Complete!' | i18n }} {{ 'Setup Complete!' | i18n }}
</h2> </h2>
@if (!stateService.kiosk) {
<p tuiSubtitle>
{{
'http://start.local was for setup only. It will no longer work.'
| i18n
}}
</p>
}
</hgroup> </hgroup>
@if (!stateService.kiosk) {
<p tuiSubtitle>
{{
'http://start.local was for setup only. It will no longer work.'
| i18n
}}
</p>
}
</header> </header>
@if (!result) { @if (!result) {

View File

@@ -302,15 +302,11 @@ export default {
317: 'Originalpasswort', 317: 'Originalpasswort',
318: 'Originalpasswort eingeben', 318: 'Originalpasswort eingeben',
319: 'Sicherung wird gestartet', 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', 322: 'Letzte Sicherung',
323: 'Ein Ordner auf einem anderen Computer, der mit demselben Netzwerk wie Ihr Start9-Server verbunden ist.', 325: 'Dienste auswählen',
324: 'Ein physisches Laufwerk, das direkt an Ihren Start9-Server angeschlossen ist.', 326: 'Server auswählen',
325: 'Dienste für Sicherung auswählen',
326: 'Serversicherung auswählen',
327: 'Netzwerkordner', 327: 'Netzwerkordner',
328: 'Neuen öffnen', 328: 'Neuen',
329: 'Hostname', 329: 'Hostname',
330: 'Pfad', 330: 'Pfad',
331: 'URL', 331: 'URL',
@@ -355,7 +351,6 @@ export default {
372: 'Passwort erforderlich', 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.', 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', 374: 'Laufwerk wird entschlüsselt',
375: 'Dienste zur Wiederherstellung auswählen',
376: 'Für Sicherung verfügbar', 376: 'Für Sicherung verfügbar',
377: 'StartOS-Sicherungen erkannt', 377: 'StartOS-Sicherungen erkannt',
378: 'Keine 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.', 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', 804: 'Ich habe ein Backup meiner Daten',
805: 'Öffentliche Domain hinzufügen', 805: 'Öffentliche Domain hinzufügen',
806: 'Ergebnis',
} satisfies i18n } satisfies i18n

View File

@@ -301,15 +301,11 @@ export const ENGLISH: Record<string, number> = {
'Original Password': 317, 'Original Password': 317,
'Enter original password': 318, 'Enter original password': 318,
'Beginning backup': 319, '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 '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, 'Select services': 325,
'A physical drive that is plugged directly into your Start9 Server.': 324, 'Select server': 326,
'Select Services to Back Up': 325,
'Select server backup': 326,
'Network Folders': 327, 'Network Folders': 327,
'Open New': 328, 'New': 328,
'Hostname': 329, 'Hostname': 329,
'Path': 330, // as in, a URL path 'Path': 330, // as in, a URL path
'URL': 331, 'URL': 331,
@@ -354,7 +350,6 @@ export const ENGLISH: Record<string, number> = {
'Password required': 372, '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, '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, 'Decrypting drive': 374,
'Select services to restore': 375,
'Available for backup': 376, 'Available for backup': 376,
'StartOS backups detected': 377, 'StartOS backups detected': 377,
'No StartOS backups detected': 378, 'No StartOS backups detected': 378,
@@ -727,4 +722,5 @@ export const ENGLISH: Record<string, number> = {
'This drive uses ext4 and will be automatically converted to btrfs. A backup is strongly recommended before proceeding.': 803, '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, 'I have a backup of my data': 804,
'Add Public Domain': 805, 'Add Public Domain': 805,
'Result': 806,
} }

View File

@@ -302,15 +302,11 @@ export default {
317: 'Contraseña original', 317: 'Contraseña original',
318: 'Ingresa la contraseña original', 318: 'Ingresa la contraseña original',
319: 'Iniciando copia de seguridad', 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', 322: 'Última copia de seguridad',
323: 'Una carpeta en otro ordenador conectado a la misma red que tu servidor Start9.', 325: 'Seleccionar servicios',
324: 'Una unidad física conectada directamente a tu servidor Start9.', 326: 'Seleccionar servidor',
325: 'Seleccionar servicios para respaldar',
326: 'Seleccionar copia de seguridad del servidor',
327: 'Carpetas de red', 327: 'Carpetas de red',
328: 'Abrir nuevo', 328: 'Nuevo',
329: 'Nombre del host', 329: 'Nombre del host',
330: 'Ruta', 330: 'Ruta',
331: 'URL', 331: 'URL',
@@ -355,7 +351,6 @@ export default {
372: 'Se requiere contraseña', 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.', 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', 374: 'Descifrando unidad',
375: 'Seleccionar servicios para restaurar',
376: 'Disponible para copia de seguridad', 376: 'Disponible para copia de seguridad',
377: 'Copias de seguridad de StartOS detectadas', 377: 'Copias de seguridad de StartOS detectadas',
378: 'No se detectaron copias de seguridad de StartOS', 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.', 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', 804: 'Tengo una copia de seguridad de mis datos',
805: 'Agregar dominio público', 805: 'Agregar dominio público',
806: 'Resultado',
} satisfies i18n } satisfies i18n

View File

@@ -302,15 +302,11 @@ export default {
317: 'Mot de passe dorigine', 317: 'Mot de passe dorigine',
318: 'Entrez le mot de passe dorigine', 318: 'Entrez le mot de passe dorigine',
319: 'Démarrage de la sauvegarde', 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 dun appareil sur votre réseau local ou dun disque physique connecté à votre serveur contenant une sauvegarde.',
322: 'Dernière sauvegarde', 322: 'Dernière sauvegarde',
323: 'Un dossier sur un autre ordinateur connecté au même réseau que votre serveur Start9.', 325: 'Sélectionner les services',
324: 'Un disque physique branché directement à votre serveur Start9.', 326: 'Sélectionner le serveur',
325: 'Sélectionner les services à sauvegarder',
326: 'Sélectionner la sauvegarde du serveur',
327: 'Dossiers réseau', 327: 'Dossiers réseau',
328: 'Ouvrir nouveau', 328: 'Nouveau',
329: 'Nom dhôte', 329: 'Nom dhôte',
330: 'Chemin', 330: 'Chemin',
331: 'URL', 331: 'URL',
@@ -355,7 +351,6 @@ export default {
372: 'Mot de passe requis', 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.', 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', 374: 'Déchiffrement du disque',
375: 'Sélectionner les services à restaurer',
376: 'Disponible pour la sauvegarde', 376: 'Disponible pour la sauvegarde',
377: 'Sauvegardes StartOS détectées', 377: 'Sauvegardes StartOS détectées',
378: 'Aucune sauvegarde StartOS détectée', 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.', 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", 804: "J'ai une sauvegarde de mes données",
805: 'Ajouter un domaine public', 805: 'Ajouter un domaine public',
806: 'Résultat',
} satisfies i18n } satisfies i18n

View File

@@ -302,15 +302,11 @@ export default {
317: 'Oryginalne hasło', 317: 'Oryginalne hasło',
318: 'Wprowadź oryginalne hasło', 318: 'Wprowadź oryginalne hasło',
319: 'Rozpoczynanie tworzenia kopii zapasowej', 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', 322: 'Ostatnia kopia zapasowa',
323: 'Folder na innym komputerze, który jest podłączony do tej samej sieci co twój serwer Start9.', 325: 'Wybierz serwisy',
324: 'Fizyczny dysk, który jest podłączony bezpośrednio do Twojego serwera Start9.', 326: 'Wybierz serwer',
325: 'Wybierz serwisy do kopii zapasowej',
326: 'Wybierz kopię zapasową serwera',
327: 'Foldery sieciowe', 327: 'Foldery sieciowe',
328: 'Otwórz nowy', 328: 'Nowy',
329: 'Nazwa hosta', 329: 'Nazwa hosta',
330: 'Ścieżka', 330: 'Ścieżka',
331: 'URL', 331: 'URL',
@@ -355,7 +351,6 @@ export default {
372: 'Wymagane hasło', 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ć.', 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', 374: 'Odszyfrowywanie dysku',
375: 'Wybierz serwisy do przywrócenia',
376: 'Dostępne do kopii zapasowej', 376: 'Dostępne do kopii zapasowej',
377: 'Wykryto kopie zapasowe StartOS', 377: 'Wykryto kopie zapasowe StartOS',
378: 'Nie wykryto kopii zapasowych 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.', 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', 804: 'Mam kopię zapasową moich danych',
805: 'Dodaj domenę publiczną', 805: 'Dodaj domenę publiczną',
806: 'Wynik',
} satisfies i18n } satisfies i18n

View File

@@ -169,7 +169,7 @@ export class DevicesAdd {
}) })
this.dialogs this.dialogs
.open(DEVICES_CONFIG, { data: config, closable: false }) .open(DEVICES_CONFIG, { data: config, closable: false, size: 'm' })
.subscribe() .subscribe()
} }
} catch (e: any) { } catch (e: any) {

View File

@@ -22,7 +22,7 @@ import { QrCodeComponent } from 'ng-qrcode'
</tui-segmented> </tui-segmented>
</aside> </aside>
</header> </header>
@if (segmented?.activeItemIndex) { @if (segmented?.activeItemIndex()) {
<qr-code [value]="config" size="352" /> <qr-code [value]="config" size="352" />
} @else { } @else {
<tui-textfield> <tui-textfield>

View File

@@ -159,7 +159,9 @@ export default class Devices {
try { try {
const data = await this.api.showDeviceConfig({ subnet: subnet.range, ip }) 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) { } catch (e: any) {
console.log(e) console.log(e)
this.errorService.handleError(e) this.errorService.handleError(e)

View File

@@ -7,7 +7,7 @@ import {
} from '@angular/core' } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop' import { toSignal } from '@angular/core/rxjs-interop'
import { i18nKey, i18nPipe } from '@start9labs/shared' 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 { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { map } from 'rxjs' import { map } from 'rxjs'
@@ -17,41 +17,58 @@ import { DataModel } from '../services/patch-db/data-model'
@Component({ @Component({
template: ` template: `
<h3 class="g-title"> <p class="timestamp">{{ data.createdAt | date: 'medium' }}</p>
{{ 'Completed' | i18n }}: {{ data.createdAt | date: 'medium' }} <table class="g-table">
</h3> <thead>
<div tuiCell> <tr>
<div tuiTitle> <th>{{ 'Title' | i18n }}</th>
<strong>{{ 'System data' | i18n }}</strong> <th>{{ 'Result' | i18n }}</th>
<div tuiSubtitle [style.color]="system().color"> </tr>
{{ system().result | i18n }} </thead>
</div> <tbody>
</div> <tr>
<tui-icon [icon]="system().icon" [style.color]="system().color" /> <td>{{ 'System data' | i18n }}</td>
</div> <td [style.color]="system().color">
@if (pkgTitles(); as titles) { <tui-icon [icon]="system().icon" />
@for (pkg of data.content.packages | keyvalue; track $index) { {{ system().result | i18n }}
<div tuiCell> </td>
<div tuiTitle> </tr>
<strong>{{ titles[pkg.key] || pkg.key }}</strong> @if (pkgTitles(); as titles) {
<div tuiSubtitle [style.color]="getColor(pkg.value.error)"> @for (pkg of data.content.packages | keyvalue; track $index) {
{{ <tr>
pkg.value.error <td>{{ titles[pkg.key] || pkg.key }}</td>
? ('Failed' | i18n) + ': ' + pkg.value.error <td [style.color]="getColor(pkg.value.error)">
: ('Succeeded' | i18n) <tui-icon [icon]="getIcon(pkg.value.error)" />
}} {{
</div> pkg.value.error
</div> ? ('Failed' | i18n) + ': ' + pkg.value.error
<tui-icon : ('Succeeded' | i18n)
[icon]="getIcon(pkg.value.error)" }}
[style.color]="getColor(pkg.value.error)" </td>
/> </tr>
</div> }
} }
</tbody>
</table>
`,
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, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, TuiIcon, TuiCell, TuiTitle, i18nPipe], imports: [CommonModule, TuiIcon, i18nPipe],
}) })
export class BackupsReportModal { export class BackupsReportModal {
private readonly i18n = inject(i18nPipe) private readonly i18n = inject(i18nPipe)

View File

@@ -7,7 +7,13 @@ import {
} from '@angular/core' } from '@angular/core'
import { ErrorService, i18nPipe } from '@start9labs/shared' import { ErrorService, i18nPipe } from '@start9labs/shared'
import { ISB, utils } from '@start9labs/start-sdk' 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 { TuiNotificationMiddleService } from '@taiga-ui/kit'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { firstValueFrom } from 'rxjs' import { firstValueFrom } from 'rxjs'
@@ -33,7 +39,18 @@ import { InterfaceAddressItemComponent } from './item.component'
selector: 'section[gatewayGroup]', selector: 'section[gatewayGroup]',
template: ` template: `
<header> <header>
{{ 'Gateway' | i18n }}: {{ gatewayGroup().gatewayName }} @switch (gatewayGroup().deviceType) {
@case ('ethernet') {
<tui-icon icon="@tui.ethernet-port" />
}
@case ('wireless') {
<tui-icon icon="@tui.wifi" />
}
@case ('wireguard') {
<tui-icon icon="@tui.shield" />
}
}
{{ gatewayGroup().gatewayName }}
@if (gatewayGroup().isWireguard) { @if (gatewayGroup().isWireguard) {
<button <button
tuiButton tuiButton
@@ -93,6 +110,11 @@ import { InterfaceAddressItemComponent } from './item.component'
</table> </table>
`, `,
styles: ` styles: `
header tui-icon {
font-size: 1.25rem;
margin-inline-end: 0.375rem;
}
:host ::ng-deep { :host ::ng-deep {
th:first-child { th:first-child {
width: 5rem; width: 5rem;
@@ -104,6 +126,7 @@ import { InterfaceAddressItemComponent } from './item.component'
TuiButton, TuiButton,
TuiDropdown, TuiDropdown,
TuiDataList, TuiDataList,
TuiIcon,
TuiInput, TuiInput,
TableComponent, TableComponent,
PlaceholderComponent, PlaceholderComponent,

View File

@@ -209,7 +209,7 @@ import {
styles: ` styles: `
.plugin-icon { .plugin-icon {
height: 1.25rem; height: 1.25rem;
margin-right: 0.25rem; margin-inline-end: 0.375rem;
border-radius: 100%; border-radius: 100%;
} }

View File

@@ -158,6 +158,7 @@ export class InterfaceService {
return { return {
gatewayId: g.id, gatewayId: g.id,
gatewayName: g.name, gatewayName: g.name,
deviceType: g.ipInfo?.deviceType || 'ethernet',
isWireguard: g.ipInfo?.deviceType === 'wireguard', isWireguard: g.ipInfo?.deviceType === 'wireguard',
addresses, addresses,
} }
@@ -314,6 +315,7 @@ export type GatewayAddress = {
export type GatewayAddressGroup = { export type GatewayAddressGroup = {
gatewayId: string gatewayId: string
gatewayName: string gatewayName: string
deviceType: string
isWireguard: boolean isWireguard: boolean
addresses: GatewayAddress[] addresses: GatewayAddress[]
} }

View File

@@ -120,5 +120,5 @@ export class BackupsBackupModal {
export const BACKUP = new PolymorpheusComponent(BackupsBackupModal) export const BACKUP = new PolymorpheusComponent(BackupsBackupModal)
export const BACKUP_OPTIONS: Partial<TuiDialogOptions<unknown>> = { export const BACKUP_OPTIONS: Partial<TuiDialogOptions<unknown>> = {
label: 'Select Services to Back Up', label: 'Select services',
} }

View File

@@ -235,6 +235,7 @@ export class BackupsHistoryModal {
content: report, content: report,
createdAt: completedAt, createdAt: completedAt,
}, },
size: 'l',
}) })
.subscribe() .subscribe()
} }

View File

@@ -50,16 +50,11 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
<td class="content"> <td class="content">
<tui-line-clamp <tui-line-clamp
style="pointer-events: none" style="pointer-events: none"
[linesLimit]="4" [linesLimit]="3"
[lineHeight]="21" [lineHeight]="21"
[content]="item.message" [content]="item.message"
(overflownChange)="overflow = $event" (overflownChange)="overflow = $event"
/> />
@if (overflow) {
<button tuiLink (click.stop)="onClick(item)">
{{ 'View full' | i18n }}
</button>
}
@if ([1, 2].includes(item.code)) { @if ([1, 2].includes(item.code)) {
<button tuiLink (click.stop)="onClick(item)"> <button tuiLink (click.stop)="onClick(item)">
{{ {{
@@ -68,6 +63,10 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
: ('View details' | i18n) : ('View details' | i18n)
}} }}
</button> </button>
} @else if (overflow) {
<button tuiLink (click.stop)="onClick(item)">
{{ 'View full' | i18n }}
</button>
} }
</td> </td>
} }
@@ -186,12 +185,7 @@ export class NotificationItemComponent {
overflow = false overflow = false
onClick(item: ServerNotification<number>) { onClick(item: ServerNotification<number>) {
if (this.overflow) { this.service.viewModal(item)
this.service.viewModal(item, true) item.seen = true
item.seen = true
} else if ([1, 2].includes(item.code)) {
this.service.viewModal(item)
item.seen = true
}
} }
} }

View File

@@ -9,10 +9,20 @@ import { AuthoritiesTableComponent } from './table.component'
@Component({ @Component({
template: ` template: `
<ng-container *title> <ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left"> <div>
{{ 'Back' | i18n }} <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
</a> {{ 'Back' | i18n }}
{{ 'Certificate Authorities' | i18n }} </a>
{{ 'Certificate Authorities' | i18n }}
<a
tuiIconButton
size="xs"
docsLink
path="/start-os/trust-ca.html"
appearance="icon"
iconStart="@tui.book-open-text"
></a>
</div>
</ng-container> </ng-container>
<section class="g-card"> <section class="g-card">
<header> <header>

View File

@@ -9,13 +9,7 @@ import { toSignal } from '@angular/core/rxjs-interop'
import { ActivatedRoute, RouterLink } from '@angular/router' import { ActivatedRoute, RouterLink } from '@angular/router'
import { DialogService, DocsLinkDirective, i18nPipe } from '@start9labs/shared' import { DialogService, DocsLinkDirective, i18nPipe } from '@start9labs/shared'
import { TuiMapperPipe } from '@taiga-ui/cdk' import { TuiMapperPipe } from '@taiga-ui/cdk'
import { import { TuiButton, TuiLoader, TuiNotification, TuiTitle } from '@taiga-ui/core'
TuiButton,
TuiLink,
TuiLoader,
TuiNotification,
TuiTitle,
} from '@taiga-ui/core'
import { TuiHeader } from '@taiga-ui/layout' import { TuiHeader } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { import {
@@ -35,12 +29,28 @@ import { BACKUP_RESTORE } from './restore.component'
@Component({ @Component({
template: ` template: `
<ng-container *title> <ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left"> <div>
{{ 'Back' | i18n }} <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
</a> {{ 'Back' | i18n }}
{{ </a>
type === 'create' ? ('Create Backup' | i18n) : ('Restore Backup' | i18n) {{
}} type === 'create'
? ('Create Backup' | i18n)
: ('Restore Backup' | i18n)
}}
<a
tuiIconButton
size="xs"
docsLink
[path]="
type === 'create'
? '/start-os/backup-create.html'
: '/start-os/backup-restore.html'
"
appearance="icon"
iconStart="@tui.book-open-text"
></a>
</div>
</ng-container> </ng-container>
<header tuiHeader> <header tuiHeader>
@@ -51,36 +61,19 @@ import { BACKUP_RESTORE } from './restore.component'
? ('Create Backup' | i18n) ? ('Create Backup' | i18n)
: ('Restore Backup' | i18n) : ('Restore Backup' | i18n)
}} }}
<a
tuiIconButton
size="xs"
docsLink
[path]="
type === 'create'
? '/start-os/backup-create.html'
: '/start-os/backup-restore.html'
"
appearance="icon"
iconStart="@tui.book-open-text"
></a>
</h3> </h3>
<p tuiSubtitle>
@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
}}
<a
tuiLink
docsLink
path="/start-os/backup-create.html"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[textContent]="'View instructions' | i18n"
></a>
} @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
}}
<a
tuiLink
docsLink
path="/start-os/backup-restore.html"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[textContent]="'View instructions' | i18n"
></a>
}
</p>
</hgroup> </hgroup>
</header> </header>
@@ -109,30 +102,16 @@ import { BACKUP_RESTORE } from './restore.component'
[style.height.rem]="20" [style.height.rem]="20"
/> />
} @else { } @else {
<section (networkFolders)="onTarget($event)"> <section (networkFolders)="onTarget($event)"></section>
{{ <section (physicalFolders)="onTarget($event)"></section>
'A folder on another computer that is connected to the same network as your Start9 server.'
| i18n
}}
<a
tuiLink
docsLink
path="/start-os/backup-create.html"
fragment="#network-folder"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[textContent]="'View instructions' | i18n"
></a>
</section>
<section (physicalFolders)="onTarget($event)">
{{
'A physical drive that is plugged directly into your Start9 Server.'
| i18n
}}
</section>
} }
} }
`, `,
styles: `
:host-context(tui-root._mobile) [tuiHeader] {
display: none;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
AsyncPipe, AsyncPipe,
@@ -140,7 +119,6 @@ import { BACKUP_RESTORE } from './restore.component'
RouterLink, RouterLink,
TuiButton, TuiButton,
TuiLoader, TuiLoader,
TuiLink,
TuiHeader, TuiHeader,
TuiTitle, TuiTitle,
TuiNotification, TuiNotification,
@@ -184,12 +162,22 @@ export default class SystemBackupComponent implements OnInit {
} }
onTarget(target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>) { onTarget(target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>) {
const component = this.type === 'create' ? BACKUP : BACKUP_RESTORE if (this.type === 'create') {
const label = this.dialog
this.type === 'create' .openComponent(BACKUP, {
? 'Select Services to Back Up' label: 'Select services',
: 'Select server backup' data: target,
size: 'm',
this.dialog.openComponent(component, { label, data: target }).subscribe() })
.subscribe()
} else {
this.dialog
.openComponent(BACKUP_RESTORE, {
label: 'Select server',
data: target,
size: 'l',
})
.subscribe()
}
} }
} }

View File

@@ -7,8 +7,8 @@ import {
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { DialogService, ErrorService, i18nPipe } from '@start9labs/shared' import { DialogService, ErrorService, i18nPipe } from '@start9labs/shared'
import { ISB, T } from '@start9labs/start-sdk' import { ISB, T } from '@start9labs/start-sdk'
import { TuiButton, TuiIcon } from '@taiga-ui/core' import { TuiButton, TuiDataList, TuiDropdown, TuiIcon } from '@taiga-ui/core'
import { TuiNotificationMiddleService, TuiTooltip } from '@taiga-ui/kit' import { TuiNotificationMiddleService } from '@taiga-ui/kit'
import { filter } from 'rxjs' import { filter } from 'rxjs'
import { FormComponent } from 'src/app/routes/portal/components/form.component' import { FormComponent } from 'src/app/routes/portal/components/form.component'
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component' import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
@@ -28,10 +28,14 @@ const ERROR =
template: ` template: `
<header> <header>
{{ 'Network Folders' | i18n }} {{ 'Network Folders' | i18n }}
<tui-icon [tuiTooltip]="cifs" /> <button
<ng-template #cifs><ng-content /></ng-template> tuiButton
<button tuiButton size="xs" iconStart="@tui.plus" (click)="add()"> size="xs"
{{ 'Open New' | i18n }} iconStart="@tui.plus"
[style.margin-inline-start]="'auto'"
(click)="add()"
>
{{ 'New' | i18n }}
</button> </button>
</header> </header>
@@ -59,24 +63,29 @@ const ERROR =
<td class="name">{{ target.entry.path.split('/').pop() }}</td> <td class="name">{{ target.entry.path.split('/').pop() }}</td>
<td>{{ target.entry.hostname }}</td> <td>{{ target.entry.hostname }}</td>
<td>{{ target.entry.path }}</td> <td>{{ target.entry.path }}</td>
<td> <td (click)="$event.stopPropagation()">
<button <button
tuiIconButton tuiIconButton
tuiDropdown
size="s" size="s"
appearance="action-destructive" appearance="flat-grayscale"
iconStart="@tui.trash" iconStart="@tui.ellipsis-vertical"
(click.stop)="forget(target, $index)" [tuiDropdownOpen]="!!opens[$index]"
(tuiDropdownOpenChange)="opens[$index] = $event"
> >
Forget {{ 'More' | i18n }}
</button> <tui-data-list *tuiDropdown>
<button <button tuiOption (click)="edit(target)">
tuiIconButton {{ 'Edit' | i18n }}
appearance="icon" </button>
size="xs" <button
iconStart="@tui.pencil" tuiOption
(click.stop)="edit(target)" class="g-negative"
> (click)="forget(target, $index)"
Edit >
{{ 'Delete' | i18n }}
</button>
</tui-data-list>
</button> </button>
</td> </td>
</tr> </tr>
@@ -106,7 +115,7 @@ const ERROR =
} }
td:first-child { td:first-child {
width: 13rem; width: 16rem;
} }
td:last-child { td:last-child {
@@ -114,10 +123,6 @@ const ERROR =
text-align: right; text-align: right;
} }
[tuiButton] {
margin-inline-start: auto;
}
span { span {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -166,8 +171,9 @@ const ERROR =
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
TuiButton, TuiButton,
TuiDataList,
TuiDropdown,
TuiIcon, TuiIcon,
TuiTooltip,
PlaceholderComponent, PlaceholderComponent,
BackupStatusComponent, BackupStatusComponent,
TableComponent, TableComponent,
@@ -186,6 +192,8 @@ export class BackupNetworkComponent {
readonly service = inject(BackupService) readonly service = inject(BackupService)
readonly networkFolders = output<MappedBackupTarget<CifsBackupTarget>>() readonly networkFolders = output<MappedBackupTarget<CifsBackupTarget>>()
opens: Record<number, boolean> = {}
select(target: MappedBackupTarget<CifsBackupTarget>) { select(target: MappedBackupTarget<CifsBackupTarget>) {
if (!target.entry.mountable) { if (!target.entry.mountable) {
this.dialog.openAlert(ERROR, { label: 'Unable to connect' }).subscribe() this.dialog.openAlert(ERROR, { label: 'Unable to connect' }).subscribe()
@@ -321,7 +329,6 @@ export class BackupNetworkComponent {
), ),
required: true, required: true,
default: null, default: null,
placeholder: 'My Network Folder',
}), }),
password: ISB.Value.text({ password: ISB.Value.text({
name: this.i18n.transform('Password')!, name: this.i18n.transform('Password')!,
@@ -331,7 +338,6 @@ export class BackupNetworkComponent {
required: false, required: false,
default: null, default: null,
masked: true, masked: true,
placeholder: 'My Network Folder',
}), }),
}) })
} }

View File

@@ -5,9 +5,8 @@ import {
output, output,
} from '@angular/core' } from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { ConvertBytesPipe, DialogService, i18nPipe } from '@start9labs/shared' import { DialogService, i18nPipe } from '@start9labs/shared'
import { TuiButton, TuiIcon } from '@taiga-ui/core' import { TuiButton } from '@taiga-ui/core'
import { TuiTooltip } from '@taiga-ui/kit'
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component' import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
import { TableComponent } from 'src/app/routes/portal/components/table.component' import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { DiskBackupTarget } from 'src/app/services/api/api.types' import { DiskBackupTarget } from 'src/app/services/api/api.types'
@@ -19,11 +18,9 @@ import { BackupStatusComponent } from './status.component'
template: ` template: `
<header> <header>
{{ 'Physical Drives' | i18n }} {{ 'Physical Drives' | i18n }}
<tui-icon [tuiTooltip]="drives" />
<ng-template #drives><ng-content /></ng-template>
</header> </header>
<table [appTable]="['Status', 'Name', 'Model', 'Capacity']"> <table [appTable]="['Status', 'Logicalname', 'Name', 'Capacity']">
@for (target of service.drives(); track $index) { @for (target of service.drives(); track $index) {
<tr <tr
tabindex="0" tabindex="0"
@@ -33,14 +30,9 @@ import { BackupStatusComponent } from './status.component'
<td> <td>
<span [backupStatus]="target.hasAnyBackup" [physical]="true"></span> <span [backupStatus]="target.hasAnyBackup" [physical]="true"></span>
</td> </td>
<td class="name"> <td class="name">{{ target.entry.logicalname }}</td>
{{ target.entry.label || target.entry.logicalname }} <td>{{ driveName(target.entry) }}</td>
</td> <td>{{ formatCapacity(target.entry.capacity) }}</td>
<td>
{{ target.entry.vendor || 'Unknown Vendor' }} -
{{ target.entry.model || 'Unknown Model' }}
</td>
<td>{{ target.entry.capacity | convertBytes }}</td>
</tr> </tr>
} @empty { } @empty {
<tr> <tr>
@@ -74,10 +66,6 @@ import { BackupStatusComponent } from './status.component'
} }
} }
td:first-child {
width: 13rem;
}
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
tr { tr {
grid-template-columns: min-content 1fr 4rem; grid-template-columns: min-content 1fr 4rem;
@@ -109,9 +97,6 @@ import { BackupStatusComponent } from './status.component'
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
TuiButton, TuiButton,
TuiIcon,
TuiTooltip,
ConvertBytesPipe,
PlaceholderComponent, PlaceholderComponent,
BackupStatusComponent, BackupStatusComponent,
TableComponent, TableComponent,
@@ -122,9 +107,26 @@ export class BackupPhysicalComponent {
private readonly dialog = inject(DialogService) private readonly dialog = inject(DialogService)
private readonly type = inject(ActivatedRoute).snapshot.data['type'] private readonly type = inject(ActivatedRoute).snapshot.data['type']
private readonly i18n = inject(i18nPipe)
readonly service = inject(BackupService) readonly service = inject(BackupService)
readonly physicalFolders = output<MappedBackupTarget<DiskBackupTarget>>() readonly physicalFolders = output<MappedBackupTarget<DiskBackupTarget>>()
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<DiskBackupTarget>) { select(target: MappedBackupTarget<DiskBackupTarget>) {
if (this.type === 'restore' && !target.hasAnyBackup) { if (this.type === 'restore' && !target.hasAnyBackup) {
this.dialog this.dialog

View File

@@ -22,24 +22,42 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
</span> </span>
<span tuiTitle> <span tuiTitle>
{{ (pkg.value | toManifest).title }} {{ (pkg.value | toManifest).title }}
<span tuiSubtitle> </span>
@if (progress.complete) { <span class="status">
<tui-icon icon="@tui.check" class="g-positive" /> @if (progress.complete) {
{{ 'complete' | i18n }} <tui-icon icon="@tui.check" class="g-positive" />
{{ 'complete' | i18n }}
} @else {
@if ((pkg.key | tuiMapper: toStatus | async) === 'backing-up') {
<tui-loader size="s" />
{{ 'backing up' | i18n }}
} @else { } @else {
@if ((pkg.key | tuiMapper: toStatus | async) === 'backing-up') { <tui-icon icon="@tui.clock" />
<tui-loader size="s" /> {{ 'waiting' | i18n }}
{{ 'backing up' | i18n }}
} @else {
{{ 'waiting' | i18n }}
}
} }
</span> }
</span> </span>
</div> </div>
} }
} }
`, `,
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' }, host: { class: 'g-card' },
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [

View File

@@ -3,12 +3,12 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { import {
DialogService, DialogService,
ErrorService, ErrorService,
i18nPipe,
StartOSDiskInfo, StartOSDiskInfo,
} from '@start9labs/shared' } from '@start9labs/shared'
import { TuiCell, TuiTitle } from '@taiga-ui/core' import { TuiButton } from '@taiga-ui/core'
import { TuiNotificationMiddleService } from '@taiga-ui/kit' import { TuiNotificationMiddleService } from '@taiga-ui/kit'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus' 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 { ApiService } from 'src/app/services/api/embassy-api.service'
import { verifyPassword } from 'src/app/utils/verify-password' import { verifyPassword } from 'src/app/utils/verify-password'
import { BackupContext } from './backup.types' import { BackupContext } from './backup.types'
@@ -16,31 +16,49 @@ import { RECOVER } from './recover.component'
@Component({ @Component({
template: ` template: `
@for (server of target.entry.startOs | keyvalue; track $index) { <table [appTable]="['Hostname', 'StartOS Version', 'Created', null]">
<button @for (server of target.entry.startOs | keyvalue; track $index) {
tuiCell <tr>
class="g-stretch" <td class="name">{{ server.value.hostname }}.local</td>
(click)="onClick(server.key, server.value)" <td>{{ server.value.version }}</td>
> <td>{{ server.value.timestamp | date: 'medium' }}</td>
<span tuiTitle> <td>
<span tuiSubtitle> <button
<b>{{ 'Local Hostname' | i18n }}</b> tuiButton
: {{ server.value.hostname }}.local size="s"
</span> (click)="onClick(server.key, server.value)"
<span tuiSubtitle> >
<b>{{ 'StartOS Version' | i18n }}</b> Select
: {{ server.value.version }} </button>
</span> </td>
<span tuiSubtitle> </tr>
<b>{{ 'Created' | i18n }}</b> }
: {{ server.value.timestamp | date: 'medium' }} </table>
</span> `,
</span> styles: `
</button> 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, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [KeyValuePipe, DatePipe, TuiCell, TuiTitle, i18nPipe], imports: [KeyValuePipe, DatePipe, TuiButton, TableComponent],
}) })
export class BackupRestoreComponent { export class BackupRestoreComponent {
private readonly dialog = inject(DialogService) private readonly dialog = inject(DialogService)
@@ -82,7 +100,7 @@ export class BackupRestoreComponent {
this.context.$implicit.complete() this.context.$implicit.complete()
this.dialog this.dialog
.openComponent(RECOVER, { label: 'Select services to restore', data }) .openComponent(RECOVER, { label: 'Select services', data })
.subscribe() .subscribe()
} finally { } finally {
loader.unsubscribe() loader.unsubscribe()

View File

@@ -13,7 +13,7 @@ import { TuiIcon } from '@taiga-ui/core'
template: ` template: `
@if (type === 'create') { @if (type === 'create') {
<tui-icon <tui-icon
[icon]="physical() ? '@tui.ethernet-port' : '@tui.signal-high'" [icon]="physical() ? '@tui.hard-drive' : '@tui.signal-high'"
class="g-positive" class="g-positive"
/> />
{{ 'Available for backup' | i18n }} {{ 'Available for backup' | i18n }}
@@ -22,7 +22,7 @@ import { TuiIcon } from '@taiga-ui/core'
<tui-icon icon="@tui.save" class="g-positive" /> <tui-icon icon="@tui.save" class="g-positive" />
{{ 'StartOS backups detected' | i18n }} {{ 'StartOS backups detected' | i18n }}
} @else { } @else {
<tui-icon icon="@tui.save-off" class="g-negative" /> <tui-icon icon="@tui.file-x" class="g-negative" />
{{ 'No StartOS backups detected' | i18n }} {{ 'No StartOS backups detected' | i18n }}
} }
} }
@@ -37,6 +37,8 @@ import { TuiIcon } from '@taiga-ui/core'
tui-icon { tui-icon {
font-size: 1rem; font-size: 1rem;
min-width: 1.25rem;
text-align: center;
} }
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {

View File

@@ -27,10 +27,20 @@ const ipv6 =
@Component({ @Component({
template: ` template: `
<ng-container *title> <ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left"> <div>
{{ 'Back' | i18n }} <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
</a> {{ 'Back' | i18n }}
{{ 'DNS Servers' | i18n }} </a>
{{ 'DNS Servers' | i18n }}
<a
tuiIconButton
size="xs"
docsLink
path="/start-os/dns.html"
appearance="icon"
iconStart="@tui.book-open-text"
></a>
</div>
</ng-container> </ng-container>
@if (data(); as d) { @if (data(); as d) {
<form [formGroup]="d.form"> <form [formGroup]="d.form">
@@ -76,6 +86,10 @@ const ipv6 =
max-width: 36rem; max-width: 36rem;
} }
:host-context(tui-root._mobile) [tuiHeader] {
display: none;
}
form header, form header,
form footer { form footer {
margin: 1rem 0; margin: 1rem 0;

View File

@@ -29,10 +29,20 @@ import { GatewaysTableComponent } from './table.component'
@Component({ @Component({
template: ` template: `
<ng-container *title> <ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left"> <div>
{{ 'Back' | i18n }} <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
</a> {{ 'Back' | i18n }}
{{ 'Gateways' | i18n }} </a>
{{ 'Gateways' | i18n }}
<a
tuiIconButton
size="xs"
docsLink
path="/start-os/gateways.html"
appearance="icon"
iconStart="@tui.book-open-text"
></a>
</div>
</ng-container> </ng-container>
<section class="g-card"> <section class="g-card">
@@ -121,6 +131,10 @@ import { GatewaysTableComponent } from './table.component'
} }
`, `,
styles: ` styles: `
:host-context(tui-root._mobile) .g-card > header > [docsLink] {
display: none;
}
.outbound { .outbound {
max-width: 24rem; max-width: 24rem;
margin-top: 2rem; margin-top: 2rem;

View File

@@ -40,10 +40,20 @@ function detectProviderKey(host: string | undefined): string {
@Component({ @Component({
template: ` template: `
<ng-container *title> <ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left"> <div>
{{ 'Back' | i18n }} <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
</a> {{ 'Back' | i18n }}
SMTP </a>
SMTP
<a
tuiIconButton
size="xs"
docsLink
path="/start-os/smtp.html"
appearance="icon"
iconStart="@tui.book-open-text"
></a>
</div>
</ng-container> </ng-container>
@if (form$ | async; as data) { @if (form$ | async; as data) {
<form [formGroup]="data.form"> <form [formGroup]="data.form">
@@ -129,6 +139,10 @@ function detectProviderKey(host: string | undefined): string {
max-width: 36rem; max-width: 36rem;
} }
:host-context(tui-root._mobile) form:first-of-type > [tuiHeader] {
display: none;
}
form header, form header,
form footer { form footer {
margin: 1rem 0; margin: 1rem 0;

View File

@@ -26,15 +26,11 @@ import { SSHTableComponent } from './table.component'
@Component({ @Component({
template: ` template: `
<ng-container *title> <ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left"> <div>
{{ 'Back' | i18n }} <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
</a> {{ 'Back' | i18n }}
SSH </a>
</ng-container> SSH
@let keys = keys$ | async;
<section class="g-card">
<header>
{{ 'SSH Keys' | i18n }}
<a <a
tuiIconButton tuiIconButton
size="xs" size="xs"
@@ -42,9 +38,13 @@ import { SSHTableComponent } from './table.component'
path="/start-os/ssh.html" path="/start-os/ssh.html"
appearance="icon" appearance="icon"
iconStart="@tui.book-open-text" iconStart="@tui.book-open-text"
> ></a>
{{ 'Documentation' | i18n }} </div>
</a> </ng-container>
@let keys = keys$ | async;
<section class="g-card">
<header>
{{ 'SSH Keys' | i18n }}
<button <button
tuiButton tuiButton
size="xs" size="xs"

View File

@@ -52,10 +52,20 @@ import { wifiSpec } from './wifi.const'
@Component({ @Component({
template: ` template: `
<ng-container *title> <ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left"> <div>
{{ 'Back' | i18n }} <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
</a> {{ 'Back' | i18n }}
WiFi </a>
WiFi
<a
tuiIconButton
size="xs"
docsLink
path="/start-os/wifi.html"
appearance="icon"
iconStart="@tui.book-open-text"
></a>
</div>
</ng-container> </ng-container>
<section class="g-card"> <section class="g-card">
<header> <header>
@@ -108,7 +118,7 @@ import { wifiSpec } from './wifi.const'
<div tuiCardLarge="compact" [wifi]="data.available"></div> <div tuiCardLarge="compact" [wifi]="data.available"></div>
} }
<p> <p>
<button tuiButton (click)="other(data)" appearance="flat"> <button tuiButton (click)="other(data)" appearance="grayscale">
+ {{ 'Connect to hidden network' | i18n }} + {{ 'Connect to hidden network' | i18n }}
</button> </button>
</p> </p>
@@ -131,6 +141,10 @@ import { wifiSpec } from './wifi.const'
:host { :host {
max-width: 36rem; max-width: 36rem;
} }
:host-context(tui-root._mobile) header > [docsLink] {
display: none;
}
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [

View File

@@ -36,7 +36,7 @@ export class NotificationService {
getColor(notification: ServerNotification<number>): string { getColor(notification: ServerNotification<number>): string {
switch (notification.level) { switch (notification.level) {
case 'info': case 'info':
return 'var(--tui-status-info)' return 'var(--tui-text-secondary)'
case 'success': case 'success':
return 'var(--tui-status-positive)' return 'var(--tui-status-positive)'
case 'warning': case 'warning':
@@ -62,26 +62,42 @@ export class NotificationService {
} }
} }
viewModal(notification: ServerNotification<number>, full = false) { viewModal(notification: ServerNotification<number>) {
const { data, createdAt, code, title, message } = notification const { data, createdAt, code, title, message } = notification
if (code === 1) { switch (code) {
// Backup Report // Backup report - structured report with per-service results
this.dialogs case 1:
.openComponent(full ? message : REPORT, { this.dialogs
label: 'Backup Report', .openComponent(REPORT, {
data: { content: data, createdAt }, label: 'Backup Report',
}) data: { content: data, createdAt },
.subscribe() size: 'l',
} else { })
// Markdown viewer .subscribe()
this.dialogs break
.openComponent(full ? message : MARKDOWN, {
label: title as i18nKey, // OS update - data contains the full release notes markdown
data: of(data), case 2:
size: 'l', this.dialogs
}) .openComponent(MARKDOWN, {
.subscribe() 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
} }
} }
} }