mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
forms for adding domain, rework things based on new ideas
This commit is contained in:
@@ -90,20 +90,15 @@ export default {
|
||||
88: 'Aktionen',
|
||||
89: 'nicht empfohlen',
|
||||
90: 'Root-CA ist vertrauenswürdig!',
|
||||
93: 'Öffentlich machen',
|
||||
94: 'Privat machen',
|
||||
95: 'Keine öffentlichen Adressen',
|
||||
96: 'Domain hinzufügen',
|
||||
97: 'Wird entfernt',
|
||||
98: 'Wird öffentlich gemacht',
|
||||
99: 'Wird privat gemacht',
|
||||
100: 'Nicht gespeicherte Änderungen',
|
||||
101: 'Sie haben nicht gespeicherte Änderungen. Möchten Sie die Seite wirklich verlassen?',
|
||||
102: 'Verlassen',
|
||||
103: 'Sind Sie sicher?',
|
||||
104: 'Domain auswählen',
|
||||
108: 'Öffentlich',
|
||||
109: 'Privat',
|
||||
109: 'privat',
|
||||
111: 'Keine Onion-Domains',
|
||||
112: 'Neue Onion-Domain',
|
||||
113: 'Privater Schlüssel (optional)',
|
||||
@@ -282,7 +277,7 @@ export default {
|
||||
292: 'Upload wird gestartet',
|
||||
293: 'Erneut versuchen',
|
||||
294: '.s9pk-Paketdatei hochladen',
|
||||
295: 'Warnung: Der Upload über Tor ist langsam. Wechseln Sie für bessere Leistung ins lokale Netzwerk.',
|
||||
295: 'Warnung: Der Upload über Tor ist langsam.',
|
||||
296: 'Hochladen',
|
||||
297: 'Version 1 s9pk erkannt. Dieses Format ist veraltet. Falls nötig, kann ein V1 s9pk über start-cli installiert werden.',
|
||||
298: 'Ungültige Paketdatei',
|
||||
@@ -522,7 +517,7 @@ export default {
|
||||
544: 'Domain bearbeiten',
|
||||
545: 'Keine Domains',
|
||||
546: 'Anbieter',
|
||||
547: 'DNS verwalten',
|
||||
547: '',
|
||||
548: '',
|
||||
549: '',
|
||||
550: '',
|
||||
|
||||
@@ -89,20 +89,15 @@ export const ENGLISH = {
|
||||
'Actions': 88, // as in, actions available to the user
|
||||
'not recommended': 89,
|
||||
'Root CA Trusted!': 90,
|
||||
'Make public': 93,
|
||||
'Make private': 94,
|
||||
'No public addresses': 95,
|
||||
'Add domain': 96,
|
||||
'Removing': 97,
|
||||
'Making public': 98,
|
||||
'Making private': 99,
|
||||
'Unsaved changes': 100,
|
||||
'You have unsaved changes. Are you sure you want to leave?': 101,
|
||||
'Leave': 102,
|
||||
'Are you sure?': 103,
|
||||
'Select domain': 104,
|
||||
'Public': 108,
|
||||
'Private': 109,
|
||||
'public': 108,
|
||||
'private': 109,
|
||||
'No Tor domains': 111,
|
||||
'New Tor domain': 112,
|
||||
'Private Key (optional)': 113,
|
||||
@@ -281,7 +276,7 @@ export const ENGLISH = {
|
||||
'Starting upload': 292,
|
||||
'Try again': 293,
|
||||
'Upload .s9pk package file': 294,
|
||||
'Warning: package upload will be slow over Tor. Switch to local for a better experience.': 295,
|
||||
'Warning: package upload will be slow over Tor.': 295,
|
||||
'Upload': 296,
|
||||
'Version 1 s9pk detected. This package format is deprecated. You can sideload a V1 s9pk via start-cli if necessary.': 297,
|
||||
'Invalid package file': 298,
|
||||
@@ -521,7 +516,7 @@ export const ENGLISH = {
|
||||
'Edit domain': 544,
|
||||
'No domains': 545,
|
||||
'Provider': 546,
|
||||
'Manage DNS': 547,
|
||||
'View DNS': 547,
|
||||
'Clearnet Domains': 548,
|
||||
'No clearnet domains': 549,
|
||||
'Addresses': 550,
|
||||
|
||||
@@ -90,20 +90,15 @@ export default {
|
||||
88: 'Acciones',
|
||||
89: 'no recomendado',
|
||||
90: '¡CA raíz confiable!',
|
||||
93: 'Hacer público',
|
||||
94: 'Hacer privado',
|
||||
95: 'Sin direcciones públicas',
|
||||
96: 'Agregar dominio',
|
||||
97: 'Eliminando',
|
||||
98: 'Haciendo público',
|
||||
99: 'Haciendo privado',
|
||||
100: 'Cambios no guardados',
|
||||
101: 'Tienes cambios no guardados. ¿Estás seguro de que deseas salir?',
|
||||
102: 'Salir',
|
||||
103: '¿Estás seguro?',
|
||||
104: 'Seleccionar dominio',
|
||||
108: 'Público',
|
||||
109: 'Privado',
|
||||
108: 'público',
|
||||
109: 'privado',
|
||||
111: 'Sin dominios onion',
|
||||
112: 'Nueva dominio onion',
|
||||
113: 'Clave privada (opcional)',
|
||||
@@ -282,7 +277,7 @@ export default {
|
||||
292: 'Iniciando carga',
|
||||
293: 'Intentar de nuevo',
|
||||
294: 'Subir archivo de paquete .s9pk',
|
||||
295: 'Advertencia: la carga del paquete será lenta a través de Tor. Cambia a conexión local para una mejor experiencia.',
|
||||
295: 'Advertencia: la carga del paquete será lenta a través de Tor.',
|
||||
296: 'Subir',
|
||||
297: 'Se detectó un paquete s9pk de versión 1. Este formato está obsoleto. Puedes instalarlo manualmente con start-cli si es necesario.',
|
||||
298: 'Archivo de paquete inválido',
|
||||
@@ -522,7 +517,7 @@ export default {
|
||||
544: 'Editar dominio',
|
||||
545: 'Sin dominios',
|
||||
546: 'Proveedor',
|
||||
547: 'Administrar DNS',
|
||||
547: '',
|
||||
548: '',
|
||||
549: '',
|
||||
550: '',
|
||||
|
||||
@@ -90,20 +90,15 @@ export default {
|
||||
88: 'Actions',
|
||||
89: 'non recommandé',
|
||||
90: 'Certificat racine approuvé !',
|
||||
93: 'Rendre public',
|
||||
94: 'Rendre privé',
|
||||
95: 'Aucune adresse publique',
|
||||
96: 'Ajouter un domaine',
|
||||
97: 'Suppression',
|
||||
98: 'Mise en public',
|
||||
99: 'Mise en privé',
|
||||
100: 'Modifications non enregistrées',
|
||||
101: 'Vous avez des modifications non enregistrées. Voulez-vous vraiment quitter ?',
|
||||
102: 'Quitter',
|
||||
103: 'Êtes-vous sûr ?',
|
||||
104: 'Sélectionner un domaine',
|
||||
108: 'Public',
|
||||
109: 'Privé',
|
||||
108: 'public',
|
||||
109: 'privé',
|
||||
111: 'Aucune domaine onion',
|
||||
112: 'Nouvelle domaine onion',
|
||||
113: 'Clé privée (optionnel)',
|
||||
@@ -282,7 +277,7 @@ export default {
|
||||
292: 'Début du téléversement',
|
||||
293: 'Réessayer',
|
||||
294: 'Téléverser un fichier .s9pk',
|
||||
295: 'Attention : le téléversement du paquet sera lent via Tor. Passez en local pour une meilleure expérience.',
|
||||
295: 'Attention : le téléversement du paquet sera lent via Tor.',
|
||||
296: 'Téléverser',
|
||||
297: 'Version 1 de s9pk détectée. Ce format de paquet est obsolète. Vous pouvez installer manuellement un s9pk V1 via start-cli si nécessaire.',
|
||||
298: 'Fichier paquet invalide',
|
||||
@@ -522,7 +517,7 @@ export default {
|
||||
544: 'Modifier le domaine',
|
||||
545: 'Aucun domaine',
|
||||
546: 'Fournisseur',
|
||||
547: 'Gérer le DNS',
|
||||
547: '',
|
||||
548: '',
|
||||
549: '',
|
||||
550: '',
|
||||
|
||||
@@ -90,20 +90,15 @@ export default {
|
||||
88: 'Akcje',
|
||||
89: 'niezalecane',
|
||||
90: 'Główny certyfikat CA zaufany!',
|
||||
93: 'Upublicznij',
|
||||
94: 'Ukryj',
|
||||
95: 'Brak publicznych adresów',
|
||||
96: 'Dodaj domenę',
|
||||
97: 'Usuwanie',
|
||||
98: 'Upublicznianie',
|
||||
99: 'Ukrywanie',
|
||||
100: 'Niezapisane zmiany',
|
||||
101: 'Masz niezapisane zmiany. Czy na pewno chcesz opuścić tę stronę?',
|
||||
102: 'Opuść',
|
||||
103: 'Czy jesteś pewien?',
|
||||
104: 'Wybierz domenę',
|
||||
108: 'Publiczny',
|
||||
109: 'Prywatny',
|
||||
108: 'publiczny',
|
||||
109: 'prywatny',
|
||||
111: 'Brak domeny onion',
|
||||
112: 'Nowy domenę onion',
|
||||
113: 'Klucz prywatny (opcjonalnie)',
|
||||
@@ -282,7 +277,7 @@ export default {
|
||||
292: 'Rozpoczynanie przesyłania',
|
||||
293: 'Spróbuj ponownie',
|
||||
294: 'Prześlij plik pakietu .s9pk',
|
||||
295: 'Uwaga: przesyłanie pakietu przez Tor będzie powolne. Przełącz się na sieć lokalną, aby uzyskać lepszą wydajność.',
|
||||
295: 'Uwaga: przesyłanie pakietu przez Tor będzie powolne.',
|
||||
296: 'Prześlij',
|
||||
297: 'Wykryto pakiet s9pk w wersji 1. Ten format pakietu jest przestarzały. Możesz zainstalować pakiet s9pk V1 przez start-cli, jeśli to konieczne.',
|
||||
298: 'Nieprawidłowy plik pakietu',
|
||||
@@ -522,7 +517,7 @@ export default {
|
||||
544: 'Edytuj domenę',
|
||||
545: 'Brak domen',
|
||||
546: 'Dostawca',
|
||||
547: 'Zarządzaj DNS',
|
||||
547: '',
|
||||
548: '',
|
||||
549: '',
|
||||
550: '',
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { toAuthorityName } from 'src/app/utils/acme'
|
||||
|
||||
@Pipe({
|
||||
name: 'authorityName',
|
||||
})
|
||||
export class AuthorityNamePipe implements PipeTransform {
|
||||
transform(value: string | null = null): string {
|
||||
return toAuthorityName(value)
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,7 @@ import {
|
||||
} from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { QRModal } from 'src/app/routes/portal/modals/qr.component'
|
||||
|
||||
import { InterfaceComponent } from '../interface.component'
|
||||
import { InterfaceService } from '../interface.service'
|
||||
|
||||
@Component({
|
||||
selector: 'td[actions]',
|
||||
|
||||
@@ -12,7 +12,7 @@ import { InterfaceAddressItemComponent } from './item.component'
|
||||
selector: 'section[addresses]',
|
||||
template: `
|
||||
<header>{{ 'Addresses' | i18n }}</header>
|
||||
<table [appTable]="[null, 'Type', 'Gateway', 'URL', null]">
|
||||
<table [appTable]="[null, 'Type', 'Access', 'Gateway', 'URL', null]">
|
||||
@for (address of addresses()?.common; track $index) {
|
||||
<tr [address]="address" [isRunning]="isRunning()"></tr>
|
||||
} @empty {
|
||||
@@ -27,7 +27,7 @@ import { InterfaceAddressItemComponent } from './item.component'
|
||||
} @else {
|
||||
@for (_ of [0, 1]; track $index) {
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<td colspan="6">
|
||||
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { DisplayAddress } from '../interface.service'
|
||||
import { AddressActionsComponent } from './actions.component'
|
||||
import { TuiBadge } from '@taiga-ui/kit'
|
||||
|
||||
@Component({
|
||||
selector: 'tr[address]',
|
||||
@@ -17,13 +18,26 @@ import { AddressActionsComponent } from './actions.component'
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.eye"
|
||||
iconStart="@tui.info"
|
||||
(click)="viewDetails(address.bullets)"
|
||||
>
|
||||
{{ 'Address details' | i18n }}
|
||||
</button>
|
||||
</td>
|
||||
<td [style.width.rem]="6">{{ address.type }}</td>
|
||||
<td [style.width.rem]="5">
|
||||
@if (address.access === 'public') {
|
||||
<tui-badge size="s" appearance="primary-success">
|
||||
{{ 'public' | i18n }}
|
||||
</tui-badge>
|
||||
} @else if (address.access === 'private') {
|
||||
<tui-badge size="s" appearance="primary-destructive">
|
||||
{{ 'private' | i18n }}
|
||||
</tui-badge>
|
||||
} @else {
|
||||
-
|
||||
}
|
||||
</td>
|
||||
<td [style.width.rem]="10" [style.order]="-1">
|
||||
{{ address.gatewayName || '-' }}
|
||||
</td>
|
||||
@@ -54,7 +68,7 @@ import { AddressActionsComponent } from './actions.component'
|
||||
}
|
||||
}
|
||||
`,
|
||||
imports: [i18nPipe, AddressActionsComponent, TuiButton],
|
||||
imports: [i18nPipe, AddressActionsComponent, TuiButton, TuiBadge],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceAddressItemComponent {
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { TuiSkeleton } from '@taiga-ui/kit'
|
||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
|
||||
import { DomainComponent } from './domain.component'
|
||||
import { ClearnetDomain } from './interface.service'
|
||||
|
||||
@Component({
|
||||
selector: 'section[clearnetDomains]',
|
||||
template: `
|
||||
<header>
|
||||
{{ 'Clearnet Domains' | i18n }}
|
||||
<a
|
||||
tuiIconButton
|
||||
docsLink
|
||||
path="/user-manual/connecting-remotely/clearnet.html"
|
||||
appearance="icon"
|
||||
iconStart="@tui.external-link"
|
||||
>
|
||||
{{ 'Documentation' | i18n }}
|
||||
</a>
|
||||
<button
|
||||
tuiButton
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="add()"
|
||||
>
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
</header>
|
||||
<table [appTable]="['Domain', 'Certificate Authority', 'Type', null]">
|
||||
@for (domain of clearnetDomains(); track $index) {
|
||||
<tr [domain]="domain"></tr>
|
||||
} @empty {
|
||||
@if (clearnetDomains()) {
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<app-placeholder icon="@tui.globe">
|
||||
{{ 'No clearnet domains' | i18n }}
|
||||
</app-placeholder>
|
||||
</td>
|
||||
</tr>
|
||||
} @else {
|
||||
@for (_ of [0, 1]; track $index) {
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
}
|
||||
</table>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
grid-column: span 3;
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
imports: [
|
||||
TuiButton,
|
||||
TableComponent,
|
||||
PlaceholderComponent,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
DomainComponent,
|
||||
TuiSkeleton,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceClearnetDomainsComponent {
|
||||
readonly clearnetDomains = input.required<
|
||||
readonly ClearnetDomain[] | undefined
|
||||
>()
|
||||
|
||||
open = false
|
||||
|
||||
add() {}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
DocsLinkDirective,
|
||||
ErrorService,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { TuiSkeleton } from '@taiga-ui/kit'
|
||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { InterfaceClearnetDomainsItemComponent } from './item.component'
|
||||
import { ClearnetDomain } from '../interface.service'
|
||||
import { ISB, utils } from '@start9labs/start-sdk'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { FormComponent } from '../../form.component'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { map } from 'rxjs'
|
||||
import { toAuthorityName } from 'src/app/utils/acme'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { InterfaceComponent } from '../interface.component'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
@Component({
|
||||
selector: 'section[clearnetDomains]',
|
||||
template: `
|
||||
<header>
|
||||
{{ 'Clearnet Domains' | i18n }}
|
||||
<a
|
||||
tuiIconButton
|
||||
docsLink
|
||||
path="/user-manual/connecting-remotely/clearnet.html"
|
||||
appearance="icon"
|
||||
iconStart="@tui.external-link"
|
||||
>
|
||||
{{ 'Documentation' | i18n }}
|
||||
</a>
|
||||
<button
|
||||
tuiButton
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="add()"
|
||||
>
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
</header>
|
||||
<table [appTable]="['Domain', 'Certificate Authority', 'Type', null]">
|
||||
@for (domain of clearnetDomains(); track $index) {
|
||||
<tr [domain]="domain"></tr>
|
||||
} @empty {
|
||||
@if (clearnetDomains()) {
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<app-placeholder icon="@tui.globe">
|
||||
{{ 'No clearnet domains' | i18n }}
|
||||
</app-placeholder>
|
||||
</td>
|
||||
</tr>
|
||||
} @else {
|
||||
@for (_ of [0, 1]; track $index) {
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
}
|
||||
</table>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
grid-column: span 3;
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
imports: [
|
||||
TuiButton,
|
||||
TableComponent,
|
||||
PlaceholderComponent,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
InterfaceClearnetDomainsItemComponent,
|
||||
TuiSkeleton,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceClearnetDomainsComponent {
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly interface = inject(InterfaceComponent)
|
||||
|
||||
readonly clearnetDomains = input.required<
|
||||
readonly ClearnetDomain[] | undefined
|
||||
>()
|
||||
|
||||
private readonly domains = toSignal(
|
||||
this.patch.watch$('serverInfo', 'network', 'domains'),
|
||||
)
|
||||
|
||||
private readonly acme = toSignal(
|
||||
this.patch.watch$('serverInfo', 'network', 'acme').pipe(
|
||||
map(acme =>
|
||||
Object.keys(acme).reduce<Record<string, string>>(
|
||||
(obj, url) => ({
|
||||
...obj,
|
||||
[url]: toAuthorityName(url),
|
||||
}),
|
||||
{ local: toAuthorityName(null) },
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
async add() {
|
||||
const addSpec = ISB.InputSpec.of({
|
||||
type: ISB.Value.union({
|
||||
name: 'Type',
|
||||
default: 'public',
|
||||
description:
|
||||
'- **Public**: the domain can be accessed by anyone with an Internet connection.\n- **Private**: the domain can only be accessed by people connected to the same Local Area Network (LAN) as the server, either physically or via VPN.',
|
||||
variants: ISB.Variants.of({
|
||||
public: {
|
||||
name: 'Public',
|
||||
spec: ISB.InputSpec.of({
|
||||
domain: ISB.Value.select({
|
||||
name: 'Domain',
|
||||
default: '',
|
||||
values: Object.keys(this.domains() || {}).reduce<
|
||||
Record<string, string>
|
||||
>(
|
||||
(obj, domain) => ({
|
||||
...obj,
|
||||
[domain]: domain,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
}),
|
||||
subdomain: ISB.Value.text({
|
||||
name: 'Subdomain',
|
||||
description: 'Optionally enter a subdomain',
|
||||
required: false,
|
||||
default: null,
|
||||
patterns: [], // @TODO subdomain pattern
|
||||
}),
|
||||
...this.acmeSpec(true),
|
||||
}),
|
||||
},
|
||||
private: {
|
||||
name: 'Private',
|
||||
spec: ISB.InputSpec.of({
|
||||
fqdn: ISB.Value.text({
|
||||
name: 'Domain',
|
||||
description:
|
||||
'Enter a fully qualified domain name. For example, if you control domain.com, you could enter domain.com or subdomain.domain.com or another.subdomain.domain.com.',
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
}),
|
||||
...this.acmeSpec(false),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Add domain',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(addSpec),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async (input: typeof addSpec._TYPE) => {
|
||||
const loader = this.loader.open('Removing').subscribe()
|
||||
const type = input.type.selection
|
||||
const params = {
|
||||
private: type === 'private',
|
||||
fqdn:
|
||||
type === 'public'
|
||||
? `${input.type.value.subdomain}.${input.type.value.domain}`
|
||||
: input.type.value.fqdn,
|
||||
acme:
|
||||
input.type.value.authority === 'local'
|
||||
? null
|
||||
: input.type.value.authority,
|
||||
}
|
||||
try {
|
||||
if (this.interface.packageId()) {
|
||||
await this.api.pkgAddDomain({
|
||||
...params,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.value()?.addressInfo.hostId || '',
|
||||
})
|
||||
} else {
|
||||
await this.api.osUiAddDomain(params)
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private acmeSpec(isPublic: boolean) {
|
||||
return {
|
||||
authority: ISB.Value.select({
|
||||
name: 'Certificate Authority',
|
||||
description:
|
||||
'Select the Certificate Authority that will issue the SSL/TLS certificate for this domain',
|
||||
values: this.acme()!,
|
||||
default: isPublic ? '' : 'local',
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,66 +19,33 @@ import {
|
||||
import { TuiBadge } from '@taiga-ui/kit'
|
||||
import { filter } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { InterfaceComponent } from './interface.component'
|
||||
import { ClearnetDomain } from './interface.service'
|
||||
import { InterfaceComponent } from '../interface.component'
|
||||
import { ClearnetDomain } from '../interface.service'
|
||||
|
||||
@Component({
|
||||
selector: 'tr[domain]',
|
||||
template: `
|
||||
<td>{{ domain().fqdn }}</td>
|
||||
<td>{{ domain().authority || '-' }}</td>
|
||||
<td>{{ domain().authority }}</td>
|
||||
<td>
|
||||
@if (domain().public) {
|
||||
<tui-badge size="s" appearance="primary-success">
|
||||
{{ 'Public' | i18n }}
|
||||
{{ 'public' | i18n }}
|
||||
</tui-badge>
|
||||
} @else {
|
||||
<tui-badge size="s" appearance="primary-destructive">
|
||||
{{ 'Private' | i18n }}
|
||||
{{ 'private' | i18n }}
|
||||
</tui-badge>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
tuiIconButton
|
||||
tuiDropdown
|
||||
size="s"
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.ellipsis-vertical"
|
||||
[tuiAppearanceState]="open ? 'hover' : null"
|
||||
[(tuiDropdownOpen)]="open"
|
||||
iconStart="@tui.trash"
|
||||
appearance="action-destructive"
|
||||
(click)="remove()"
|
||||
>
|
||||
{{ 'More' | i18n }}
|
||||
<tui-data-list *tuiTextfieldDropdown>
|
||||
<tui-opt-group>
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
[iconStart]="domain().public ? '@tui.eye-off' : '@tui.eye'"
|
||||
(click)="toggle()"
|
||||
>
|
||||
@if (domain().public) {
|
||||
{{ 'Make private' | i18n }}
|
||||
} @else {
|
||||
{{ 'Make public' | i18n }}
|
||||
}
|
||||
</button>
|
||||
<button tuiOption new iconStart="@tui.pencil" (click)="edit()">
|
||||
{{ 'Edit' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
<tui-opt-group>
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.trash"
|
||||
class="g-negative"
|
||||
(click)="remove()"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
</tui-data-list>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
</td>
|
||||
`,
|
||||
@@ -115,7 +82,7 @@ import { ClearnetDomain } from './interface.service'
|
||||
TuiBadge,
|
||||
],
|
||||
})
|
||||
export class DomainComponent {
|
||||
export class InterfaceClearnetDomainsItemComponent {
|
||||
private readonly dialog = inject(DialogService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
@@ -124,12 +91,6 @@ export class DomainComponent {
|
||||
|
||||
readonly domain = input.required<ClearnetDomain>()
|
||||
|
||||
open = false
|
||||
|
||||
toggle() {}
|
||||
|
||||
edit() {}
|
||||
|
||||
remove() {
|
||||
this.dialog
|
||||
.openConfirm({ label: 'Are you sure?', size: 's' })
|
||||
@@ -3,7 +3,7 @@ import { tuiButtonOptionsProvider } from '@taiga-ui/core'
|
||||
import { MappedServiceInterface } from './interface.service'
|
||||
import { InterfaceGatewaysComponent } from './gateways.component'
|
||||
import { InterfaceTorDomainsComponent } from './tor-domains.component'
|
||||
import { InterfaceClearnetDomainsComponent } from './clearnet-domains.component'
|
||||
import { InterfaceClearnetDomainsComponent } from './clearnet-domains/clearnet-domains.component'
|
||||
import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { T, utils } from '@start9labs/start-sdk'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { toAuthorityName } from 'src/app/utils/acme'
|
||||
import { GatewayPlus } from 'src/app/services/gateway.service'
|
||||
import { DialogService, i18nKey } from '@start9labs/shared'
|
||||
import { i18nKey } from '@start9labs/shared'
|
||||
|
||||
type AddressWithInfo = {
|
||||
url: URL
|
||||
@@ -134,7 +134,9 @@ function toDisplayAddress(
|
||||
]
|
||||
// Tor (HTTP)
|
||||
} else {
|
||||
bullets.unshift('Ideal for anonymous, remote connectivity')
|
||||
bullets.unshift(
|
||||
'Ideal for anonymous, censorship-resistant hosting and remote access',
|
||||
)
|
||||
type = `${type} (HTTP)`
|
||||
}
|
||||
// ** Not Tor **
|
||||
@@ -143,7 +145,7 @@ function toDisplayAddress(
|
||||
const gateway = gateways.find(g => g.id === info.gatewayId)!
|
||||
gatewayName = gateway.ipInfo.name
|
||||
|
||||
const gatewayIpv4 = gateway.ipInfo.subnets[0]
|
||||
const gatewayIpv4 = gateway.ipv4[0]
|
||||
const isWireguard = gateway.ipInfo.deviceType === 'wireguard'
|
||||
|
||||
const localIdeal = 'Ideal for local access'
|
||||
@@ -158,7 +160,7 @@ function toDisplayAddress(
|
||||
access = 'private'
|
||||
bullets = [
|
||||
localIdeal,
|
||||
'Not recommended for VPN access. VPNs do not support ".local" domains without extra configuration',
|
||||
'Not recommended for VPN access. VPNs do not support ".local" domains without advanced configuration',
|
||||
lanRequired,
|
||||
rootCaRequired,
|
||||
]
|
||||
@@ -174,7 +176,7 @@ function toDisplayAddress(
|
||||
]
|
||||
if (!gateway.public) {
|
||||
bullets.push(
|
||||
`Requires creating a port forwarding rule in gateway "${gatewayName}": ${port} -> ${info.hostname.value}:${port}`,
|
||||
`Requires port forwarding in gateway "${gatewayName}": ${port} -> ${info.hostname.value}:${port}`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -203,8 +205,8 @@ function toDisplayAddress(
|
||||
if (info.public) {
|
||||
access = 'public'
|
||||
bullets = [
|
||||
`Requires creating DNS records for "${domains[info.hostname.value]?.root}", as shown in System -> Domains`,
|
||||
`Requires creating a port forwarding rule in gateway "${gatewayName}": ${port} -> ${info.hostname.value}:${port === 443 ? 5443 : port}`,
|
||||
`Requires DNS record(s) for ${domains[info.hostname.value]?.root}, as shown in System -> Domains`,
|
||||
`Requires port forwarding in gateway "${gatewayName}": ${port} -> ${info.hostname.value}:${port === 443 ? 5443 : port}`,
|
||||
]
|
||||
if (domain.acme) {
|
||||
bullets.unshift('Ideal for public access via the Internet')
|
||||
@@ -218,7 +220,7 @@ function toDisplayAddress(
|
||||
} else {
|
||||
access = 'private'
|
||||
const ipPortBad = 'when using IP addresses and ports is undesirable'
|
||||
const customDnsRequired = `Requires creating custom DNS records for ${info.hostname.value} that resolve to ${gatewayIpv4}`
|
||||
const customDnsRequired = `Requires DNS record for ${info.hostname.value} that resolve to ${gatewayIpv4}`
|
||||
if (isWireguard) {
|
||||
bullets = [
|
||||
`${vpnAccess} StartTunnel (or similar) ${ipPortBad}`,
|
||||
@@ -463,18 +465,9 @@ export type InterfaceGateway = GatewayPlus & {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
// export type InterfaceGateway = {
|
||||
// id: string
|
||||
// name: string
|
||||
// enabled: boolean
|
||||
// public: boolean
|
||||
// type: T.NetworkInterfaceType
|
||||
// lanIpv4: string | null
|
||||
// }
|
||||
|
||||
export type ClearnetDomain = {
|
||||
fqdn: string
|
||||
authority: string | null
|
||||
authority: string
|
||||
public: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -57,10 +57,7 @@ import { MarketplacePkgSideload, validateS9pk } from './sideload.utils'
|
||||
<p>{{ 'Upload .s9pk package file' | i18n }}</p>
|
||||
@if (isTor) {
|
||||
<p class="g-warning">
|
||||
{{
|
||||
'Warning: package upload will be slow over Tor. Switch to local for a better experience.'
|
||||
| i18n
|
||||
}}
|
||||
{{ 'Warning: package upload will be slow over Tor.' | i18n }}
|
||||
</p>
|
||||
}
|
||||
<button tuiButton>{{ 'Upload' | i18n }}</button>
|
||||
|
||||
@@ -1,55 +1,57 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
DialogService,
|
||||
ErrorService,
|
||||
i18nKey,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { MappedDomain } from './domain.service'
|
||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { parse } from 'tldts'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
@Component({
|
||||
selector: 'dns',
|
||||
template: `
|
||||
<section class="g-card">
|
||||
<header>{{ $any('Using IP') | i18n }}</header>
|
||||
@let wanIp = context.data.gateway.ipInfo?.wanIp || ('Error' | i18n);
|
||||
|
||||
@let subdomain = context.data.subdomain;
|
||||
@let wanIp = context.data.gateway.ipInfo?.wanIp || ('Error' | i18n);
|
||||
<table [appTable]="[$any('Record'), $any('Host'), 'Value', 'Purpose']">
|
||||
<tr>
|
||||
<td>A</td>
|
||||
<td>{{ subdomain() || '@' }}</td>
|
||||
<td>{{ wanIp }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>A</td>
|
||||
<td>{{ subdomain() ? '*.' + subdomain() : '*' }}</td>
|
||||
<td>{{ wanIp }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
|
||||
<table [appTable]="['Type', $any('Host'), 'Value', 'Purpose']">
|
||||
<tr>
|
||||
<td>A</td>
|
||||
<td>{{ subdomain || '@' }}</td>
|
||||
<td>{{ wanIp }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>A</td>
|
||||
<td>{{ subdomain ? '*.' + subdomain : '*' }}</td>
|
||||
<td>{{ wanIp }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
@if (context.data.gateway.ipInfo?.deviceType !== 'wireguard') {
|
||||
<section class="g-card">
|
||||
<header>{{ $any('Using Dynamic DNS') | i18n }}</header>
|
||||
<table [appTable]="['Type', $any('Host'), 'Value', 'Purpose']">
|
||||
<tr>
|
||||
<td>ALIAS</td>
|
||||
<td>{{ subdomain || '@' }}</td>
|
||||
<td>[Dynamic DNS Address]</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ALIAS</td>
|
||||
<td>{{ subdomain ? '*.' + subdomain : '*' }}</td>
|
||||
<td>[Dynamic DNS Address]</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</section>
|
||||
}
|
||||
<!-- <tr>
|
||||
<td>ALIAS</td>
|
||||
<td>{{ subdomain() || '@' }}</td>
|
||||
<td>[DDNS Address]</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ALIAS</td>
|
||||
<td>{{ subdomain() ? '*.' + subdomain() : '*' }}</td>
|
||||
<td>[DDNS Address]</td>
|
||||
<td></td>
|
||||
</tr> -->
|
||||
</table>
|
||||
|
||||
<footer class="g-buttons">
|
||||
<button tuiButton size="l" (click)="testDns()">
|
||||
@@ -69,9 +71,12 @@ export class DnsComponent {
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly dialog = inject(DialogService)
|
||||
|
||||
readonly context = injectContext<TuiDialogContext<void, MappedDomain>>()
|
||||
|
||||
readonly subdomain = computed(() => parse(this.context.data.fqdn).subdomain)
|
||||
|
||||
async testDns() {
|
||||
const loader = this.loader.open().subscribe()
|
||||
|
||||
@@ -88,6 +93,18 @@ export class DnsComponent {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
description(subdomain: boolean) {
|
||||
const message = subdomain
|
||||
? `This DNS record routes ${this.context.data.fqdn} (no subdomain) to your server.`
|
||||
: `This DNS record routes subdomains of ${this.context.data.fqdn} to your server.`
|
||||
|
||||
this.dialog
|
||||
.openAlert(message as i18nKey, {
|
||||
label: 'Purpose' as i18nKey,
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
export const DNS = new PolymorpheusComponent(DnsComponent)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { inject, Injectable } from '@angular/core'
|
||||
import {
|
||||
DialogService,
|
||||
ErrorService,
|
||||
i18nKey,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
@@ -14,7 +15,6 @@ import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { parse } from 'tldts'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { DNS } from './dns.component'
|
||||
|
||||
@@ -22,7 +22,6 @@ import { DNS } from './dns.component'
|
||||
|
||||
export type MappedDomain = {
|
||||
fqdn: string
|
||||
subdomain: string | null
|
||||
gateway: {
|
||||
id: string
|
||||
name: string | null
|
||||
@@ -30,6 +29,13 @@ export type MappedDomain = {
|
||||
}
|
||||
}
|
||||
|
||||
type GatewayWithId = T.NetworkInterfaceInfo & {
|
||||
id: string
|
||||
ipInfo: T.IpInfo & {
|
||||
wanIp: string
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DomainService {
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
@@ -43,18 +49,13 @@ export class DomainService {
|
||||
readonly data = toSignal(
|
||||
this.patch.watch$('serverInfo', 'network').pipe(
|
||||
map(({ gateways, domains }) => ({
|
||||
gateways: Object.entries(gateways).reduce<Record<string, string>>(
|
||||
(obj, [id, n]) => ({
|
||||
...obj,
|
||||
[id]: n.ipInfo?.name || '',
|
||||
}),
|
||||
{},
|
||||
),
|
||||
gateways: Object.entries(gateways)
|
||||
.filter(([_, g]) => g.ipInfo && g.ipInfo.wanIp)
|
||||
.map(([id, g]) => ({ id, ...g })) as GatewayWithId[],
|
||||
domains: Object.entries(domains).map(
|
||||
([fqdn, { gateway }]) =>
|
||||
({
|
||||
fqdn,
|
||||
subdomain: parse(fqdn).subdomain,
|
||||
gateway: {
|
||||
id: gateway,
|
||||
ipInfo: gateways[gateway]?.ipInfo || null,
|
||||
@@ -70,7 +71,7 @@ export class DomainService {
|
||||
fqdn: ISB.Value.text({
|
||||
name: 'Domain',
|
||||
description:
|
||||
'Enter a domain/subdomain. For example, if you control domain.com, you could enter domain.com or subdomain.domain.com or another.subdomain.domain.com. In any case, the domain you enter and all possible subdomains of the domain will be available for assignment in StartOS',
|
||||
'Enter a fully qualified domain name. For example, if you control domain.com, you could enter domain.com or subdomain.domain.com or another.subdomain.domain.com. In any case, the domain you enter and all possible subdomains of the domain will be available for assignment in StartOS',
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
@@ -141,7 +142,7 @@ export class DomainService {
|
||||
|
||||
showDns(domain: MappedDomain) {
|
||||
this.dialog
|
||||
.openComponent(DNS, { label: 'Manage DNS', data: domain })
|
||||
.openComponent(DNS, { label: 'DNS Records' as i18nKey, data: domain })
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
@@ -160,13 +161,24 @@ export class DomainService {
|
||||
}
|
||||
|
||||
private gatewaysSpec() {
|
||||
const gateways = this.data()?.gateways || []
|
||||
|
||||
return {
|
||||
gateway: ISB.Value.select({
|
||||
gateway: ISB.Value.dynamicSelect(() => ({
|
||||
name: 'Gateway',
|
||||
description: 'Select which gateway to use for this domain.',
|
||||
values: this.data()!.gateways,
|
||||
description: 'Select a gateway to use for this domain.',
|
||||
values: gateways.reduce<Record<string, string>>(
|
||||
(obj, gateway) => ({
|
||||
...obj,
|
||||
[gateway.id]: gateway.ipInfo!.name,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
default: '',
|
||||
}),
|
||||
disabled: gateways
|
||||
.filter(g => g.ipInfo.wanIp.split('.').at(-1) === '100')
|
||||
.map(g => g.id),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,11 @@ import { DomainsTableComponent } from './table.component'
|
||||
<domains-table />
|
||||
</section>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
max-width: 50rem;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
TuiButton,
|
||||
|
||||
@@ -18,7 +18,7 @@ import { DomainService, MappedDomain } from './domain.service'
|
||||
template: `
|
||||
@if (domain(); as domain) {
|
||||
<td>{{ domain.fqdn }}</td>
|
||||
<td [style.order]="-1">{{ domain.gateway.ipInfo?.name || '-' }}</td>
|
||||
<td>{{ domain.gateway.ipInfo?.name || '-' }}</td>
|
||||
<td>
|
||||
<button
|
||||
tuiIconButton
|
||||
@@ -35,18 +35,18 @@ import { DomainService, MappedDomain } from './domain.service'
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.pencil"
|
||||
(click)="domainService.edit(domain)"
|
||||
iconStart="@tui.eye"
|
||||
(click)="domainService.showDns(domain)"
|
||||
>
|
||||
{{ 'Edit' | i18n }}
|
||||
{{ 'View DNS' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.eye"
|
||||
(click)="domainService.showDns(domain)"
|
||||
iconStart="@tui.pencil"
|
||||
(click)="domainService.edit(domain)"
|
||||
>
|
||||
{{ 'Manage DNS' | i18n }}
|
||||
{{ 'Edit' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
<tui-opt-group>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { DomainService } from './domain.service'
|
||||
<tr [domain]="domain"></tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td [attr.colspan]="4">
|
||||
<td [attr.colspan]="3">
|
||||
@if (domainService.data()?.domains) {
|
||||
<app-placeholder icon="@tui.globe">
|
||||
{{ 'No domains' | i18n }}
|
||||
|
||||
@@ -30,9 +30,14 @@ import { GatewayPlus } from 'src/app/services/gateway.service'
|
||||
template: `
|
||||
@if (gateway(); as gateway) {
|
||||
<td [style.grid-column]="'span 2'">{{ gateway.ipInfo.name }}</td>
|
||||
<td class="type">{{ gateway.ipInfo.deviceType || '-' }}</td>
|
||||
<td [style.order]="-2">
|
||||
{{ gateway.public ? ('Public' | i18n) : ('Private' | i18n) }}
|
||||
<td class="type">
|
||||
@if (gateway.ipInfo.deviceType; as type) {
|
||||
{{ type }} ({{
|
||||
gateway.public ? ('public' | i18n) : ('private' | i18n)
|
||||
}})
|
||||
} @else {
|
||||
-
|
||||
}
|
||||
</td>
|
||||
<td class="lan">{{ gateway.ipv4.join(', ') }}</td>
|
||||
<td
|
||||
@@ -89,15 +94,8 @@ import { GatewayPlus } from 'src/app/services/gateway.service'
|
||||
grid-template-columns: min-content 1fr min-content;
|
||||
|
||||
.type {
|
||||
grid-column: span 2;
|
||||
order: -1;
|
||||
|
||||
&::before {
|
||||
content: '\\00A0(';
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: ')';
|
||||
}
|
||||
}
|
||||
|
||||
.lan,
|
||||
|
||||
@@ -8,16 +8,7 @@ import { GatewayService } from 'src/app/services/gateway.service'
|
||||
@Component({
|
||||
selector: 'gateways-table',
|
||||
template: `
|
||||
<table
|
||||
[appTable]="[
|
||||
'Name',
|
||||
'Type',
|
||||
'Access',
|
||||
$any('LAN IP'),
|
||||
$any('WAN IP'),
|
||||
null,
|
||||
]"
|
||||
>
|
||||
<table [appTable]="['Name', 'Type', $any('LAN IP'), $any('WAN IP'), null]">
|
||||
@for (gateway of gatewayService.gateways(); track $index) {
|
||||
<tr [gateway]="gateway"></tr>
|
||||
} @empty {
|
||||
|
||||
@@ -70,36 +70,34 @@ import UpdatesComponent from './updates.component'
|
||||
<td class="desktop">{{ item().gitHash }}</td>
|
||||
<td class="desktop">{{ item().s9pk.publishedAt | date }}</td>
|
||||
<td>
|
||||
<div>
|
||||
<button
|
||||
tuiIconButton
|
||||
size="m"
|
||||
appearance="icon"
|
||||
[tuiChevron]="expanded()"
|
||||
>
|
||||
{{ 'Show more' | i18n }}
|
||||
</button>
|
||||
@if (local().stateInfo.state === 'updating') {
|
||||
<tui-progress-circle
|
||||
size="xs"
|
||||
[max]="100"
|
||||
[value]="
|
||||
(local().stateInfo.installingInfo?.progress?.overall
|
||||
| installingProgress) || 0
|
||||
"
|
||||
/>
|
||||
} @else {
|
||||
<button
|
||||
tuiIconButton
|
||||
size="m"
|
||||
appearance="icon"
|
||||
[tuiChevron]="expanded()"
|
||||
tuiButton
|
||||
size="s"
|
||||
[loading]="!ready()"
|
||||
[appearance]="error() ? 'destructive' : 'primary'"
|
||||
(click.stop)="onClick()"
|
||||
>
|
||||
{{ 'Show more' | i18n }}
|
||||
{{ error() ? ('Retry' | i18n) : ('Update' | i18n) }}
|
||||
</button>
|
||||
@if (local().stateInfo.state === 'updating') {
|
||||
<tui-progress-circle
|
||||
size="xs"
|
||||
[max]="100"
|
||||
[value]="
|
||||
(local().stateInfo.installingInfo?.progress?.overall
|
||||
| installingProgress) || 0
|
||||
"
|
||||
/>
|
||||
} @else {
|
||||
<button
|
||||
tuiButton
|
||||
size="s"
|
||||
[loading]="!ready()"
|
||||
[appearance]="error() ? 'destructive' : 'primary'"
|
||||
(click.stop)="onClick()"
|
||||
>
|
||||
{{ error() ? ('Retry' | i18n) : ('Update' | i18n) }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
Reference in New Issue
Block a user