This commit is contained in:
Matt Hill
2025-08-07 08:18:47 -06:00
12 changed files with 397 additions and 318 deletions

View File

@@ -12,16 +12,17 @@ import {
tuiButtonOptionsProvider, tuiButtonOptionsProvider,
TuiDataList, TuiDataList,
TuiDropdown, TuiDropdown,
TuiTextfield,
} from '@taiga-ui/core' } from '@taiga-ui/core'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { QRModal } from 'src/app/routes/portal/modals/qr.component' import { QRModal } from 'src/app/routes/portal/modals/qr.component'
import { InterfaceComponent } from '../interface.component' import { InterfaceComponent } from '../interface.component'
@Component({ @Component({
selector: 'td[actions]', selector: 'td[actions]',
template: ` template: `
<div class="desktop"> <div class="desktop">
<ng-content />
@if (interface.value().type === 'ui') { @if (interface.value().type === 'ui') {
<button <button
tuiIconButton tuiIconButton
@@ -52,46 +53,48 @@ import { InterfaceComponent } from '../interface.component'
</div> </div>
<div class="mobile"> <div class="mobile">
<button <button
tuiDropdown
tuiIconButton tuiIconButton
appearance="flat-grayscale"
iconStart="@tui.ellipsis-vertical" iconStart="@tui.ellipsis-vertical"
tuiDropdownOpen [tuiAppearanceState]="open ? 'hover' : null"
[tuiDropdown]="dropdown" [(tuiDropdownOpen)]="open"
> >
{{ 'Actions' | i18n }} {{ 'Actions' | i18n }}
<ng-template #dropdown let-close> <tui-data-list *tuiTextfieldDropdown="let close">
<tui-data-list> <button tuiOption new iconStart="@tui.eye" (click)="instructions()">
<tui-opt-group> {{ 'View instructions' | i18n }}
@if (interface.value().type === 'ui') { </button>
<button @if (interface.value().type === 'ui') {
tuiOption <button
iconStart="@tui.external-link" tuiOption
[disabled]="disabled()" new
(click)="openUI()" iconStart="@tui.external-link"
> [disabled]="disabled()"
{{ 'Open' | i18n }} (click)="openUI()"
</button> >
} {{ 'Open' | i18n }}
<button tuiOption iconStart="@tui.qr-code" (click)="showQR()"> </button>
{{ 'Show QR' | i18n }} }
</button> <button tuiOption new iconStart="@tui.qr-code" (click)="showQR()">
<button {{ 'Show QR' | i18n }}
tuiOption </button>
iconStart="@tui.copy" <button
(click)="copyService.copy(href()); close()" tuiOption
> new
{{ 'Copy URL' | i18n }} iconStart="@tui.copy"
</button> (click)="copyService.copy(href()); close()"
</tui-opt-group> >
<tui-opt-group><ng-content select="[tuiOption]" /></tui-opt-group> {{ 'Copy URL' | i18n }}
</tui-data-list> </button>
</ng-template> </tui-data-list>
</button> </button>
</div> </div>
`, `,
styles: ` styles: `
:host { :host {
text-align: right; text-align: right;
grid-area: 1 / 2 / 3 / 3; grid-area: 1 / 2 / 4 / 3;
place-content: center; place-content: center;
white-space: nowrap; white-space: nowrap;
} }
@@ -110,7 +113,7 @@ import { InterfaceComponent } from '../interface.component'
} }
} }
`, `,
imports: [TuiButton, TuiDropdown, TuiDataList, i18nPipe], imports: [TuiButton, TuiDropdown, TuiDataList, i18nPipe, TuiTextfield],
providers: [tuiButtonOptionsProvider({ appearance: 'icon' })], providers: [tuiButtonOptionsProvider({ appearance: 'icon' })],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
@@ -125,6 +128,8 @@ export class AddressActionsComponent {
readonly href = input.required<string>() readonly href = input.required<string>()
readonly disabled = input.required<boolean>() readonly disabled = input.required<boolean>()
open = false
showQR() { showQR() {
this.dialog this.dialog
.openComponent(new PolymorpheusComponent(QRModal), { .openComponent(new PolymorpheusComponent(QRModal), {
@@ -138,4 +143,6 @@ export class AddressActionsComponent {
openUI() { openUI() {
this.document.defaultView?.open(this.href(), '_blank', 'noreferrer') this.document.defaultView?.open(this.href(), '_blank', 'noreferrer')
} }
instructions() {}
} }

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core' import { ChangeDetectionStrategy, Component, input } from '@angular/core'
import { i18nPipe } from '@start9labs/shared' import { i18nPipe } from '@start9labs/shared'
import { TuiButton, TuiDataList, TuiDropdown } from '@taiga-ui/core' import { TuiButton } from '@taiga-ui/core'
import { TuiAccordion } from '@taiga-ui/experimental'
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 { MappedServiceInterface } from '../interface.utils' import { MappedServiceInterface } from '../interface.utils'
@@ -10,70 +11,95 @@ import { AddressActionsComponent } from './actions.component'
selector: 'section[addresses]', selector: 'section[addresses]',
template: ` template: `
<header>{{ 'Addresses' | i18n }}</header> <header>{{ 'Addresses' | i18n }}</header>
@if (addresses().common.length) { <table [appTable]="[null, 'Type', 'Gateway', 'URL', null]">
<section class="g-card"> @for (address of addresses().common; track $index) {
<header>{{ 'Common' | i18n }}</header> <tr>
<table [appTable]="[null, 'Type', 'Gateway', 'URL', null]"> <td>
@for (address of addresses().common; track $index) { <button
<tr> tuiIconButton
<td> appearance="flat-grayscale"
<button iconStart="@tui.eye"
tuiIconButton (click)="instructions()"
appearance="flat-grayscale" >
iconStart="@tui.eye" {{ 'View instructions' | i18n }}
(click)="instructions()" </button>
> </td>
{{ 'View instructions' | i18n }} <td>{{ address.type }}</td>
</button> <td [style.order]="-1">{{ address.gateway }}</td>
</td> <td>{{ address.url }}</td>
<td>{{ address.type }}</td> <td actions [disabled]="!isRunning()" [href]="address.url"></td>
<td>{{ address.gateway }}</td> </tr>
<td>{{ address.url }}</td> } @empty {
<td actions [disabled]="!isRunning()" [href]="address.url"></td> <tr>
</tr> <td colspan="5">
} <app-placeholder icon="@tui.app-window">
</table> {{ 'No addresses' | i18n }}
</section> </app-placeholder>
} @else { </td>
<app-placeholder icon="@tui.app-window"> </tr>
{{ 'No addresses' | i18n }} }
</app-placeholder> </table>
}
@if (addresses().uncommon.length) { @if (addresses().uncommon.length) {
<section class="g-card"> <tui-accordion>
<header>{{ 'Uncommon' | i18n }}</header> <button tuiAccordion>{{ 'Uncommon' | i18n }}</button>
<table [appTable]="[null, 'Type', 'Gateway', 'URL', null]"> <tui-expand>
@for (address of addresses().uncommon; track $index) { <table [appTable]="[null, 'Type', 'Gateway', 'URL', null]">
<tr> @for (address of addresses().uncommon; track $index) {
<td> <tr>
<button <td>
tuiIconButton <button
appearance="flat-grayscale" tuiIconButton
iconStart="@tui.eye" appearance="flat-grayscale"
(click)="instructions()" iconStart="@tui.eye"
> (click)="instructions()"
{{ 'View instructions' | i18n }} >
</button> {{ 'View instructions' | i18n }}
</td> </button>
<td>{{ address.type }}</td> </td>
<td>{{ address.gateway }}</td> <td>{{ address.type }}</td>
<td>{{ address.url }}</td> <td [style.order]="-1">{{ address.gateway }}</td>
<td actions [disabled]="!isRunning()" [href]="address.url"></td> <td>{{ address.url }}</td>
</tr> <td actions [disabled]="!isRunning()" [href]="address.url"></td>
} </tr>
</table> }
</section> </table>
</tui-expand>
</tui-accordion>
} }
`, `,
styles: `
[tuiAccordion],
tui-expand {
box-shadow: none;
padding: 0;
background: none !important;
&::after {
margin-inline-end: 0.25rem;
}
}
:host-context(tui-root._mobile) {
td:first-child {
display: none;
}
td:nth-child(2) {
font: var(--tui-font-text-m);
font-weight: bold;
color: var(--tui-text-primary);
}
}
`,
host: { class: 'g-card' },
imports: [ imports: [
TableComponent, TableComponent,
PlaceholderComponent, PlaceholderComponent,
i18nPipe, i18nPipe,
TuiDropdown,
TuiDataList,
AddressActionsComponent, AddressActionsComponent,
TuiButton, TuiButton,
TuiAccordion,
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })

View File

@@ -1,35 +1,10 @@
import { import { ChangeDetectionStrategy, Component, input } from '@angular/core'
ChangeDetectionStrategy, import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
Component, import { TuiButton } from '@taiga-ui/core'
inject,
input,
} from '@angular/core'
import {
DialogService,
DocsLinkDirective,
ErrorService,
i18nPipe,
LoadingService,
} from '@start9labs/shared'
import { ISB, utils } from '@start9labs/start-sdk'
import {
TuiAppearance,
TuiButton,
TuiDataList,
TuiDropdown,
TuiLink,
} from '@taiga-ui/core'
import { filter } from 'rxjs'
import {
FormComponent,
FormContext,
} from 'src/app/routes/portal/components/form.component'
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
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 { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service' import { DomainComponent } from './domain.component'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { ClearnetDomain } from './interface.utils' import { ClearnetDomain } from './interface.utils'
@Component({ @Component({
@@ -38,13 +13,14 @@ import { ClearnetDomain } from './interface.utils'
<header> <header>
{{ 'Clearnet Domains' | i18n }} {{ 'Clearnet Domains' | i18n }}
<a <a
tuiLink tuiIconButton
docsLink docsLink
path="/user-manual/connecting-remotely/clearnet.html" path="/user-manual/connecting-remotely/clearnet.html"
appearance="action-grayscale" appearance="icon"
iconEnd="@tui.external-link" iconStart="@tui.external-link"
[pseudo]="true" >
></a> {{ 'Documentation' | i18n }}
</a>
<button <button
tuiButton tuiButton
iconStart="@tui.plus" iconStart="@tui.plus"
@@ -54,133 +30,40 @@ import { ClearnetDomain } from './interface.utils'
{{ 'Add' | i18n }} {{ 'Add' | i18n }}
</button> </button>
</header> </header>
@if (clearnetDomains().length) { <table [appTable]="['Domain', 'Certificate Authority', 'Type', null]">
<table [appTable]="['Domain', 'Certificate Authority', 'Type', null]"> @for (domain of clearnetDomains(); track $index) {
@for (domain of clearnetDomains(); track $index) { <tr [domain]="domain"></tr>
<tr> } @empty {
<td>{{ domain.fqdn }}</td> <tr>
<td>{{ domain.authority }}</td> <td colspan="4">
<td>{{ domain.public ? 'public' : 'private' }}</td> <app-placeholder icon="@tui.app-window">
<td> {{ 'No clearnet domains' | i18n }}
<button </app-placeholder>
tuiIconButton </td>
tuiDropdown </tr>
size="s" }
appearance="flat-grayscale" </table>
iconStart="@tui.ellipsis-vertical" `,
[tuiAppearanceState]="open ? 'hover' : null" styles: `
[(tuiDropdownOpen)]="open" :host {
> grid-column: span 3;
{{ 'More' | i18n }}
<tui-data-list size="s" *tuiTextfieldDropdown>
<tui-opt-group>
<button
tuiOption
new
[iconStart]="
domain.public ? '@tui.globe-lock' : '@tui.globe'
"
(click)="togglePrivate(domain)"
>
{{
domain.public
? ('Make private' | i18n)
: ('Make public' | i18n)
}}
</button>
<button
tuiOption
new
iconStart="@tui.award"
(click)="changeCa(domain)"
>
{{ 'Change CA' | i18n }}
</button>
</tui-opt-group>
<tui-opt-group>
<button
tuiOption
new
iconStart="@tui.trash"
class="g-negative"
(click)="remove(domain.fqdn)"
>
{{ 'Delete' | i18n }}
</button>
</tui-opt-group>
</tui-data-list>
</button>
</td>
</tr>
}
</table>
} @else {
<app-placeholder icon="@tui.app-window">
{{ 'No clearnet domains' | i18n }}
</app-placeholder>
} }
`, `,
host: { class: 'g-card' },
imports: [ imports: [
TuiButton, TuiButton,
TuiLink,
TuiAppearance,
TableComponent, TableComponent,
PlaceholderComponent, PlaceholderComponent,
i18nPipe, i18nPipe,
DocsLinkDirective, DocsLinkDirective,
TuiDropdown, DomainComponent,
TuiDataList,
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class InterfaceClearnetDomainsComponent { export class InterfaceClearnetDomainsComponent {
private readonly dialog = inject(DialogService)
private readonly formDialog = inject(FormDialogService)
private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService)
private readonly api = inject(ApiService)
private readonly interface = inject(InterfaceComponent)
private readonly i18n = inject(i18nPipe)
readonly clearnetDomains = input.required<readonly ClearnetDomain[]>() readonly clearnetDomains = input.required<readonly ClearnetDomain[]>()
open = false open = false
// @TODO add, toggle, and change CA call same idempotent endpoint, either pkgAddDomain or osUiAddDomain add() {}
async add() {
// @TODO baseDomain (select), subdomain (optional)(text), certificateAuthority (select), public/private (select)
}
async togglePrivate(domain: ClearnetDomain) {}
async changeCa(domain: ClearnetDomain) {}
async remove(fqdn: string) {
this.dialog
.openConfirm({ label: 'Are you sure?', size: 's' })
.pipe(filter(Boolean))
.subscribe(async () => {
const loader = this.loader.open('Removing').subscribe()
const params = { fqdn }
try {
if (this.interface.packageId()) {
await this.api.pkgRemoveDomain({
...params,
package: this.interface.packageId(),
host: this.interface.value().addressInfo.hostId,
})
} else {
await this.api.osUiRemoveDomain(params)
}
return true
} catch (e: any) {
this.errorService.handleError(e)
return false
} finally {
loader.unsubscribe()
}
})
}
} }

View File

@@ -0,0 +1,161 @@
import {
ChangeDetectionStrategy,
Component,
inject,
input,
} from '@angular/core'
import {
DialogService,
ErrorService,
i18nPipe,
LoadingService,
} from '@start9labs/shared'
import {
TuiButton,
TuiDataList,
TuiDropdown,
TuiTextfield,
} from '@taiga-ui/core'
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.utils'
@Component({
selector: 'tr[domain]',
template: `
<td>{{ domain().fqdn }}</td>
<td>{{ domain().authority || '-' }}</td>
<td>
@if (domain().public) {
<tui-badge size="s" appearance="primary-success">
{{ 'Public' | i18n }}
</tui-badge>
} @else {
<tui-badge size="s" appearance="primary-destructive">
{{ 'Private' | i18n }}
</tui-badge>
}
</td>
<td>
<button
tuiIconButton
tuiDropdown
size="s"
appearance="flat-grayscale"
iconStart="@tui.ellipsis-vertical"
[tuiAppearanceState]="open ? 'hover' : null"
[(tuiDropdownOpen)]="open"
>
{{ '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>
</button>
</td>
`,
styles: `
:host {
grid-template-columns: min-content 1fr min-content;
}
td:nth-child(2) {
order: -1;
grid-column: span 2;
}
td:last-child {
grid-area: 1 / 3 / 3;
align-self: center;
text-align: right;
}
:host-context(tui-root._mobile) {
tui-badge {
vertical-align: bottom;
margin-inline-start: 0.25rem;
}
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
TuiButton,
TuiDataList,
TuiDropdown,
i18nPipe,
TuiTextfield,
TuiBadge,
],
})
export class DomainComponent {
private readonly dialog = inject(DialogService)
private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService)
private readonly api = inject(ApiService)
private readonly interface = inject(InterfaceComponent)
readonly domain = input.required<ClearnetDomain>()
open = false
toggle() {}
edit() {}
remove() {
this.dialog
.openConfirm({ label: 'Are you sure?', size: 's' })
.pipe(filter(Boolean))
.subscribe(async () => {
const loader = this.loader.open('Removing').subscribe()
const params = { fqdn: this.domain().fqdn }
try {
if (this.interface.packageId()) {
await this.api.pkgRemoveDomain({
...params,
package: this.interface.packageId(),
host: this.interface.value().addressInfo.hostId,
})
} else {
await this.api.osUiRemoveDomain(params)
}
return true
} catch (e: any) {
this.errorService.handleError(e)
return false
} finally {
loader.unsubscribe()
}
})
}
}

View File

@@ -1,32 +1,45 @@
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, input } from '@angular/core' import { ChangeDetectionStrategy, Component, input } from '@angular/core'
import { TuiTitle } from '@taiga-ui/core'
import { TuiSwitch } from '@taiga-ui/kit' import { TuiSwitch } from '@taiga-ui/kit'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { i18nPipe } from '@start9labs/shared' import { i18nPipe } from '@start9labs/shared'
import { TuiCell } from '@taiga-ui/layout'
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
@Component({ @Component({
selector: 'section[gateways]', selector: 'section[gateways]',
template: ` template: `
<header>{{ 'Gateways' | i18n }}</header> <header>{{ 'Gateways' | i18n }}</header>
<ul> @for (gateway of gateways(); track $index) {
@for (gateway of gateways(); track $index) { <label tuiCell="s">
<li> <span tuiTitle>{{ gateway.name }}</span>
{{ gateway.name }} <input
<input type="checkbox"
type="checkbox" tuiSwitch
tuiSwitch size="s"
[style.margin-inline-start]="'auto'" [showIcons]="false"
[showIcons]="false" [ngModel]="gateway.enabled"
[ngModel]="gateway.enabled" (ngModelChange)="onToggle(gateway)"
(ngModelChange)="onToggle(gateway)" />
/> </label>
</li> } @empty {
} <app-placeholder icon="@tui.door-closed-locked">
<ul></ul> No gateways
</ul> </app-placeholder>
}
`, `,
host: { class: 'g-card' },
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, FormsModule, TuiSwitch, i18nPipe], imports: [
CommonModule,
FormsModule,
TuiSwitch,
i18nPipe,
TuiCell,
TuiTitle,
PlaceholderComponent,
],
}) })
export class InterfaceGatewaysComponent { export class InterfaceGatewaysComponent {
readonly gateways = input.required<any>() readonly gateways = input.required<any>()

View File

@@ -9,17 +9,14 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component'
@Component({ @Component({
selector: 'service-interface', selector: 'service-interface',
template: ` template: `
<section class="g-card" [gateways]="value().gateways"></section> <!-- @TODO Alex / Matt translation in all nested components -->
<section class="g-card" [torDomains]="value().torDomains"></section> <div [style.display]="'grid'">
<section <section [gateways]="value().gateways"></section>
class="g-card" <section [torDomains]="value().torDomains"></section>
[clearnetDomains]="value().clearnetDomains" <section [clearnetDomains]="value().clearnetDomains"></section>
></section> </div>
<section <hr [style.width.rem]="10" />
class="g-card" <section [addresses]="value().addresses" [isRunning]="true"></section>
[addresses]="value().addresses"
[isRunning]="true"
></section>
`, `,
styles: ` styles: `
:host { :host {
@@ -29,10 +26,14 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component'
color: var(--tui-text-secondary); color: var(--tui-text-secondary);
font: var(--tui-font-text-l); font: var(--tui-font-text-l);
::ng-deep td { div {
overflow-wrap: anywhere; gap: inherit;
} }
} }
:host-context(tui-root._mobile) section {
grid-column: span 1;
}
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
providers: [tuiButtonOptionsProvider({ size: 'xs' })], providers: [tuiButtonOptionsProvider({ size: 'xs' })],

View File

@@ -12,19 +12,20 @@ import {
LoadingService, LoadingService,
} from '@start9labs/shared' } from '@start9labs/shared'
import { ISB, utils } from '@start9labs/start-sdk' import { ISB, utils } from '@start9labs/start-sdk'
import { TuiAppearance, TuiButton, TuiLink } from '@taiga-ui/core' import { TuiButton, TuiTitle } from '@taiga-ui/core'
import { TuiCell } from '@taiga-ui/layout'
import { filter } from 'rxjs' import { filter } from 'rxjs'
import { import {
FormComponent, FormComponent,
FormContext, FormContext,
} from 'src/app/routes/portal/components/form.component' } from 'src/app/routes/portal/components/form.component'
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
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 { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service' import { FormDialogService } from 'src/app/services/form-dialog.service'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { InterfaceComponent } from './interface.component'
type OnionForm = { type OnionForm = {
key: string key: string
} }
@@ -33,16 +34,16 @@ type OnionForm = {
selector: 'section[torDomains]', selector: 'section[torDomains]',
template: ` template: `
<header> <header>
<!-- @TODO translation -->
Tor Domains Tor Domains
<a <a
tuiLink tuiIconButton
docsLink docsLink
path="/user-manual/connecting-remotely/tor.html" path="/user-manual/connecting-remotely/tor.html"
appearance="action-grayscale" appearance="icon"
iconEnd="@tui.external-link" iconStart="@tui.external-link"
[pseudo]="true" >
></a> {{ 'Documentation' | i18n }}
</a>
<button <button
tuiButton tuiButton
iconStart="@tui.plus" iconStart="@tui.plus"
@@ -52,35 +53,34 @@ type OnionForm = {
{{ 'Add' | i18n }} {{ 'Add' | i18n }}
</button> </button>
</header> </header>
@if (torDomains().length) { @for (domain of torDomains(); track $index) {
<table [appTable]="['Domain', null]"> <div tuiCell="s">
@for (domain of torDomains(); track $index) { <span tuiTitle>{{ domain }}</span>
<tr> <button
<td>{{ domain }}</td> tuiIconButton
<td> iconStart="@tui.trash"
<button appearance="action-destructive"
tuiIconButton (click)="remove(domain)"
iconStart="@tui.trash" >
appearance="action-destructive" {{ 'Delete' | i18n }}
(click)="remove(domain)" </button>
> </div>
{{ 'Delete' | i18n }} } @empty {
</button> <app-placeholder icon="@tui.target">
</td>
</tr>
}
</table>
} @else {
<app-placeholder icon="@tui.app-window">
{{ 'No Tor domains' | i18n }} {{ 'No Tor domains' | i18n }}
</app-placeholder> </app-placeholder>
} }
`, `,
styles: `
:host {
grid-column: span 2;
}
`,
host: { class: 'g-card' },
imports: [ imports: [
TuiCell,
TuiTitle,
TuiButton, TuiButton,
TuiLink,
TuiAppearance,
TableComponent,
PlaceholderComponent, PlaceholderComponent,
i18nPipe, i18nPipe,
DocsLinkDirective, DocsLinkDirective,
@@ -98,13 +98,13 @@ export class InterfaceTorDomainsComponent {
readonly torDomains = input.required<readonly string[]>() readonly torDomains = input.required<readonly string[]>()
async remove(domain: string) { async remove(onion: string) {
this.dialog this.dialog
.openConfirm({ label: 'Are you sure?', size: 's' }) .openConfirm({ label: 'Are you sure?', size: 's' })
.pipe(filter(Boolean)) .pipe(filter(Boolean))
.subscribe(async () => { .subscribe(async () => {
const loader = this.loader.open('Removing').subscribe() const loader = this.loader.open('Removing').subscribe()
const params = { onion: domain } const params = { onion }
try { try {
if (this.interface.packageId()) { if (this.interface.packageId()) {

View File

@@ -31,7 +31,7 @@ import { Authority, AuthorityService } from './authority.service'
[(tuiDropdownOpen)]="open" [(tuiDropdownOpen)]="open"
> >
{{ 'More' | i18n }} {{ 'More' | i18n }}
<tui-data-list size="s" *tuiTextfieldDropdown> <tui-data-list *tuiTextfieldDropdown>
@if (authority.url) { @if (authority.url) {
<tui-opt-group> <tui-opt-group>
<button <button
@@ -82,11 +82,6 @@ import { Authority, AuthorityService } from './authority.service'
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
grid-template-columns: 1fr min-content; grid-template-columns: 1fr min-content;
td:first-child {
font: var(--tui-font-text-m);
font-weight: bold;
}
.hidden { .hidden {
display: none; display: none;
} }

View File

@@ -30,7 +30,7 @@ import { DomainService, MappedDomain } from './domain.service'
[(tuiDropdownOpen)]="open" [(tuiDropdownOpen)]="open"
> >
{{ 'More' | i18n }} {{ 'More' | i18n }}
<tui-data-list size="s" *tuiTextfieldDropdown> <tui-data-list *tuiTextfieldDropdown>
<tui-opt-group> <tui-opt-group>
<button <button
tuiOption tuiOption
@@ -74,11 +74,6 @@ import { DomainService, MappedDomain } from './domain.service'
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
grid-template-columns: 1fr min-content; grid-template-columns: 1fr min-content;
td:first-child {
font: var(--tui-font-text-m);
font-weight: bold;
}
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -1,5 +1,6 @@
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { RouterLink } from '@angular/router'
import { import {
DocsLinkDirective, DocsLinkDirective,
ErrorService, ErrorService,
@@ -55,11 +56,12 @@ import { GatewayPlus } from './item.component'
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
CommonModule, CommonModule,
RouterLink,
TuiButton, TuiButton,
TuiLink,
GatewaysTableComponent, GatewaysTableComponent,
TitleDirective, TitleDirective,
i18nPipe, i18nPipe,
TuiLink,
DocsLinkDirective, DocsLinkDirective,
], ],
}) })

View File

@@ -59,7 +59,7 @@ export type GatewayPlus = T.NetworkInterfaceInfo & {
[(tuiDropdownOpen)]="open" [(tuiDropdownOpen)]="open"
> >
{{ 'More' | i18n }} {{ 'More' | i18n }}
<tui-data-list size="s" *tuiTextfieldDropdown> <tui-data-list *tuiTextfieldDropdown>
<tui-opt-group> <tui-opt-group>
<button tuiOption new iconStart="@tui.pencil" (click)="rename()"> <button tuiOption new iconStart="@tui.pencil" (click)="rename()">
{{ 'Rename' | i18n }} {{ 'Rename' | i18n }}
@@ -93,11 +93,6 @@ export type GatewayPlus = T.NetworkInterfaceInfo & {
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
grid-template-columns: min-content 1fr min-content; grid-template-columns: min-content 1fr min-content;
td:first-child {
font: var(--tui-font-text-m);
font-weight: bold;
}
.type { .type {
order: -1; order: -1;

View File

@@ -225,6 +225,7 @@ hr {
padding: 0; padding: 0;
&:first-child { &:first-child {
font: var(--tui-font-text-m);
font-weight: bold; font-weight: bold;
color: var(--tui-text-primary); color: var(--tui-text-primary);
} }