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" />
{{ 'Setup Complete!' | i18n }}
</h2>
@if (!stateService.kiosk) {
<p tuiSubtitle>
{{
'http://start.local was for setup only. It will no longer work.'
| i18n
}}
</p>
}
</hgroup>
@if (!stateService.kiosk) {
<p tuiSubtitle>
{{
'http://start.local was for setup only. It will no longer work.'
| i18n
}}
</p>
}
</header>
@if (!result) {

View File

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

View File

@@ -301,15 +301,11 @@ export const ENGLISH: Record<string, number> = {
'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<string, number> = {
'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<string, number> = {
'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,
}

View File

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

View File

@@ -302,15 +302,11 @@ export default {
317: 'Mot de passe dorigine',
318: 'Entrez le mot de passe dorigine',
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',
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 dhô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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: `
<h3 class="g-title">
{{ 'Completed' | i18n }}: {{ data.createdAt | date: 'medium' }}
</h3>
<div tuiCell>
<div tuiTitle>
<strong>{{ 'System data' | i18n }}</strong>
<div tuiSubtitle [style.color]="system().color">
{{ system().result | i18n }}
</div>
</div>
<tui-icon [icon]="system().icon" [style.color]="system().color" />
</div>
@if (pkgTitles(); as titles) {
@for (pkg of data.content.packages | keyvalue; track $index) {
<div tuiCell>
<div tuiTitle>
<strong>{{ titles[pkg.key] || pkg.key }}</strong>
<div tuiSubtitle [style.color]="getColor(pkg.value.error)">
{{
pkg.value.error
? ('Failed' | i18n) + ': ' + pkg.value.error
: ('Succeeded' | i18n)
}}
</div>
</div>
<tui-icon
[icon]="getIcon(pkg.value.error)"
[style.color]="getColor(pkg.value.error)"
/>
</div>
}
<p class="timestamp">{{ data.createdAt | date: 'medium' }}</p>
<table class="g-table">
<thead>
<tr>
<th>{{ 'Title' | i18n }}</th>
<th>{{ 'Result' | i18n }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ 'System data' | i18n }}</td>
<td [style.color]="system().color">
<tui-icon [icon]="system().icon" />
{{ system().result | i18n }}
</td>
</tr>
@if (pkgTitles(); as titles) {
@for (pkg of data.content.packages | keyvalue; track $index) {
<tr>
<td>{{ titles[pkg.key] || pkg.key }}</td>
<td [style.color]="getColor(pkg.value.error)">
<tui-icon [icon]="getIcon(pkg.value.error)" />
{{
pkg.value.error
? ('Failed' | i18n) + ': ' + pkg.value.error
: ('Succeeded' | i18n)
}}
</td>
</tr>
}
}
</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,
imports: [CommonModule, TuiIcon, TuiCell, TuiTitle, i18nPipe],
imports: [CommonModule, TuiIcon, i18nPipe],
})
export class BackupsReportModal {
private readonly i18n = inject(i18nPipe)

View File

@@ -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: `
<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) {
<button
tuiButton
@@ -93,6 +110,11 @@ import { InterfaceAddressItemComponent } from './item.component'
</table>
`,
styles: `
header tui-icon {
font-size: 1.25rem;
margin-inline-end: 0.375rem;
}
:host ::ng-deep {
th:first-child {
width: 5rem;
@@ -104,6 +126,7 @@ import { InterfaceAddressItemComponent } from './item.component'
TuiButton,
TuiDropdown,
TuiDataList,
TuiIcon,
TuiInput,
TableComponent,
PlaceholderComponent,

View File

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

View File

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

View File

@@ -120,5 +120,5 @@ export class BackupsBackupModal {
export const BACKUP = new PolymorpheusComponent(BackupsBackupModal)
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,
createdAt: completedAt,
},
size: 'l',
})
.subscribe()
}

View File

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

View File

@@ -9,10 +9,20 @@ import { AuthoritiesTableComponent } from './table.component'
@Component({
template: `
<ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
{{ 'Back' | i18n }}
</a>
{{ 'Certificate Authorities' | i18n }}
<div>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
{{ 'Back' | 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>
<section class="g-card">
<header>

View File

@@ -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: `
<ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
{{ 'Back' | i18n }}
</a>
{{
type === 'create' ? ('Create Backup' | i18n) : ('Restore Backup' | i18n)
}}
<div>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
{{ 'Back' | i18n }}
</a>
{{
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>
<header tuiHeader>
@@ -51,36 +61,19 @@ import { BACKUP_RESTORE } from './restore.component'
? ('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>
</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>
</header>
@@ -109,30 +102,16 @@ import { BACKUP_RESTORE } from './restore.component'
[style.height.rem]="20"
/>
} @else {
<section (networkFolders)="onTarget($event)">
{{
'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>
<section (networkFolders)="onTarget($event)"></section>
<section (physicalFolders)="onTarget($event)"></section>
}
}
`,
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<CifsBackupTarget | DiskBackupTarget>) {
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()
}
}
}

View File

@@ -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: `
<header>
{{ 'Network Folders' | i18n }}
<tui-icon [tuiTooltip]="cifs" />
<ng-template #cifs><ng-content /></ng-template>
<button tuiButton size="xs" iconStart="@tui.plus" (click)="add()">
{{ 'Open New' | i18n }}
<button
tuiButton
size="xs"
iconStart="@tui.plus"
[style.margin-inline-start]="'auto'"
(click)="add()"
>
{{ 'New' | i18n }}
</button>
</header>
@@ -59,24 +63,29 @@ const ERROR =
<td class="name">{{ target.entry.path.split('/').pop() }}</td>
<td>{{ target.entry.hostname }}</td>
<td>{{ target.entry.path }}</td>
<td>
<td (click)="$event.stopPropagation()">
<button
tuiIconButton
tuiDropdown
size="s"
appearance="action-destructive"
iconStart="@tui.trash"
(click.stop)="forget(target, $index)"
appearance="flat-grayscale"
iconStart="@tui.ellipsis-vertical"
[tuiDropdownOpen]="!!opens[$index]"
(tuiDropdownOpenChange)="opens[$index] = $event"
>
Forget
</button>
<button
tuiIconButton
appearance="icon"
size="xs"
iconStart="@tui.pencil"
(click.stop)="edit(target)"
>
Edit
{{ 'More' | i18n }}
<tui-data-list *tuiDropdown>
<button tuiOption (click)="edit(target)">
{{ 'Edit' | i18n }}
</button>
<button
tuiOption
class="g-negative"
(click)="forget(target, $index)"
>
{{ 'Delete' | i18n }}
</button>
</tui-data-list>
</button>
</td>
</tr>
@@ -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<MappedBackupTarget<CifsBackupTarget>>()
opens: Record<number, boolean> = {}
select(target: MappedBackupTarget<CifsBackupTarget>) {
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',
}),
})
}

View File

@@ -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: `
<header>
{{ 'Physical Drives' | i18n }}
<tui-icon [tuiTooltip]="drives" />
<ng-template #drives><ng-content /></ng-template>
</header>
<table [appTable]="['Status', 'Name', 'Model', 'Capacity']">
<table [appTable]="['Status', 'Logicalname', 'Name', 'Capacity']">
@for (target of service.drives(); track $index) {
<tr
tabindex="0"
@@ -33,14 +30,9 @@ import { BackupStatusComponent } from './status.component'
<td>
<span [backupStatus]="target.hasAnyBackup" [physical]="true"></span>
</td>
<td class="name">
{{ target.entry.label || target.entry.logicalname }}
</td>
<td>
{{ target.entry.vendor || 'Unknown Vendor' }} -
{{ target.entry.model || 'Unknown Model' }}
</td>
<td>{{ target.entry.capacity | convertBytes }}</td>
<td class="name">{{ target.entry.logicalname }}</td>
<td>{{ driveName(target.entry) }}</td>
<td>{{ formatCapacity(target.entry.capacity) }}</td>
</tr>
} @empty {
<tr>
@@ -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<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>) {
if (this.type === 'restore' && !target.hasAnyBackup) {
this.dialog

View File

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

View File

@@ -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) {
<button
tuiCell
class="g-stretch"
(click)="onClick(server.key, server.value)"
>
<span tuiTitle>
<span tuiSubtitle>
<b>{{ 'Local Hostname' | i18n }}</b>
: {{ server.value.hostname }}.local
</span>
<span tuiSubtitle>
<b>{{ 'StartOS Version' | i18n }}</b>
: {{ server.value.version }}
</span>
<span tuiSubtitle>
<b>{{ 'Created' | i18n }}</b>
: {{ server.value.timestamp | date: 'medium' }}
</span>
</span>
</button>
<table [appTable]="['Hostname', 'StartOS Version', 'Created', null]">
@for (server of target.entry.startOs | keyvalue; track $index) {
<tr>
<td class="name">{{ server.value.hostname }}.local</td>
<td>{{ server.value.version }}</td>
<td>{{ server.value.timestamp | date: 'medium' }}</td>
<td>
<button
tuiButton
size="s"
(click)="onClick(server.key, server.value)"
>
Select
</button>
</td>
</tr>
}
</table>
`,
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()

View File

@@ -13,7 +13,7 @@ import { TuiIcon } from '@taiga-ui/core'
template: `
@if (type === 'create') {
<tui-icon
[icon]="physical() ? '@tui.ethernet-port' : '@tui.signal-high'"
[icon]="physical() ? '@tui.hard-drive' : '@tui.signal-high'"
class="g-positive"
/>
{{ 'Available for backup' | i18n }}
@@ -22,7 +22,7 @@ import { TuiIcon } from '@taiga-ui/core'
<tui-icon icon="@tui.save" class="g-positive" />
{{ 'StartOS backups detected' | i18n }}
} @else {
<tui-icon icon="@tui.save-off" class="g-negative" />
<tui-icon icon="@tui.file-x" class="g-negative" />
{{ '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) {

View File

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

View File

@@ -29,10 +29,20 @@ import { GatewaysTableComponent } from './table.component'
@Component({
template: `
<ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
{{ 'Back' | i18n }}
</a>
{{ 'Gateways' | i18n }}
<div>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
{{ 'Back' | 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>
<section class="g-card">
@@ -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;

View File

@@ -40,10 +40,20 @@ function detectProviderKey(host: string | undefined): string {
@Component({
template: `
<ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
{{ 'Back' | i18n }}
</a>
SMTP
<div>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
{{ 'Back' | i18n }}
</a>
SMTP
<a
tuiIconButton
size="xs"
docsLink
path="/start-os/smtp.html"
appearance="icon"
iconStart="@tui.book-open-text"
></a>
</div>
</ng-container>
@if (form$ | async; as data) {
<form [formGroup]="data.form">
@@ -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;

View File

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

View File

@@ -52,10 +52,20 @@ import { wifiSpec } from './wifi.const'
@Component({
template: `
<ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
{{ 'Back' | i18n }}
</a>
WiFi
<div>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
{{ 'Back' | i18n }}
</a>
WiFi
<a
tuiIconButton
size="xs"
docsLink
path="/start-os/wifi.html"
appearance="icon"
iconStart="@tui.book-open-text"
></a>
</div>
</ng-container>
<section class="g-card">
<header>
@@ -108,7 +118,7 @@ import { wifiSpec } from './wifi.const'
<div tuiCardLarge="compact" [wifi]="data.available"></div>
}
<p>
<button tuiButton (click)="other(data)" appearance="flat">
<button tuiButton (click)="other(data)" appearance="grayscale">
+ {{ 'Connect to hidden network' | i18n }}
</button>
</p>
@@ -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: [

View File

@@ -36,7 +36,7 @@ export class NotificationService {
getColor(notification: ServerNotification<number>): 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<number>, full = false) {
viewModal(notification: ServerNotification<number>) {
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
}
}
}