fix: address comments

This commit is contained in:
waterplea
2025-08-09 17:45:31 +07:00
parent 86a24ec067
commit 29ddfad9d7
12 changed files with 169 additions and 79 deletions

View File

@@ -3,7 +3,6 @@ import {
Component, Component,
inject, inject,
input, input,
DOCUMENT,
} from '@angular/core' } from '@angular/core'
import { CopyService, DialogService, i18nPipe } from '@start9labs/shared' import { CopyService, DialogService, i18nPipe } from '@start9labs/shared'
import { TUI_IS_MOBILE } from '@taiga-ui/cdk' import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
@@ -23,16 +22,17 @@ import { InterfaceComponent } from '../interface.component'
selector: 'td[actions]', selector: 'td[actions]',
template: ` template: `
<div class="desktop"> <div class="desktop">
@if (interface.value().type === 'ui') { @if (interface.value()?.type === 'ui') {
<button <a
tuiIconButton tuiIconButton
appearance="flat-grayscale" appearance="flat-grayscale"
iconStart="@tui.external-link" iconStart="@tui.external-link"
[disabled]="disabled()" target="_blank"
(click)="openUI()" rel="noopener noreferrer"
[href]="href()"
> >
{{ 'Open' | i18n }} {{ 'Open' | i18n }}
</button> </a>
} }
<button <button
tuiIconButton tuiIconButton
@@ -65,16 +65,17 @@ import { InterfaceComponent } from '../interface.component'
<button tuiOption new iconStart="@tui.eye" (click)="instructions()"> <button tuiOption new iconStart="@tui.eye" (click)="instructions()">
{{ 'View instructions' | i18n }} {{ 'View instructions' | i18n }}
</button> </button>
@if (interface.value().type === 'ui') { @if (interface.value()?.type === 'ui') {
<button <a
tuiOption tuiOption
new new
iconStart="@tui.external-link" iconStart="@tui.external-link"
[disabled]="disabled()" target="_blank"
(click)="openUI()" rel="noopener noreferrer"
[href]="href()"
> >
{{ 'Open' | i18n }} {{ 'Open' | i18n }}
</button> </a>
} }
<button tuiOption new iconStart="@tui.qr-code" (click)="showQR()"> <button tuiOption new iconStart="@tui.qr-code" (click)="showQR()">
{{ 'Show QR' | i18n }} {{ 'Show QR' | i18n }}
@@ -118,8 +119,6 @@ import { InterfaceComponent } from '../interface.component'
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AddressActionsComponent { export class AddressActionsComponent {
private readonly document = inject(DOCUMENT)
readonly isMobile = inject(TUI_IS_MOBILE) readonly isMobile = inject(TUI_IS_MOBILE)
readonly dialog = inject(DialogService) readonly dialog = inject(DialogService)
readonly copyService = inject(CopyService) readonly copyService = inject(CopyService)
@@ -140,9 +139,5 @@ export class AddressActionsComponent {
.subscribe() .subscribe()
} }
openUI() {
this.document.defaultView?.open(this.href(), '_blank', 'noreferrer')
}
instructions() {} instructions() {}
} }

View File

@@ -1,9 +1,10 @@
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 } from '@taiga-ui/core'
import { TuiAccordion } from '@taiga-ui/experimental' import { TuiAccordion } from '@taiga-ui/experimental'
import { TuiSkeleton } from '@taiga-ui/kit'
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component' import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
import { TableComponent } from 'src/app/routes/portal/components/table.component' import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { MappedServiceInterface } from '../interface.service' import { MappedServiceInterface } from '../interface.service'
import { InterfaceAddressItemComponent } from './item.component' import { InterfaceAddressItemComponent } from './item.component'
@@ -12,41 +13,79 @@ import { InterfaceAddressItemComponent } from './item.component'
template: ` template: `
<header>{{ 'Addresses' | i18n }}</header> <header>{{ 'Addresses' | i18n }}</header>
<table [appTable]="[null, 'Type', 'Gateway', 'URL', null]"> <table [appTable]="[null, 'Type', 'Gateway', 'URL', null]">
@for ( @for (address of addresses()?.common; track $index) {
address of addresses().common.concat(addresses().uncommon);
track $index
) {
<tr [address]="address" [isRunning]="isRunning()"></tr> <tr [address]="address" [isRunning]="isRunning()"></tr>
} @empty { } @empty {
<tr> @if (addresses()) {
<td colspan="5"> <tr>
<app-placeholder icon="@tui.list-x"> <td colspan="5">
{{ 'No addresses' | i18n }} <app-placeholder icon="@tui.list-x">
</app-placeholder> {{ 'No addresses' | i18n }}
</td> </app-placeholder>
</tr> </td>
</tr>
} @else {
@for (_ of [0, 1]; track $index) {
<tr>
<td colspan="5">
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
</td>
</tr>
}
}
} }
<!-- @if (addresses().uncommon.length) { </table>
<tui-accordion> @if (addresses()?.uncommon?.length) {
<button tuiAccordion>{{ 'Uncommon' | i18n }}</button> <tui-accordion>
<tui-expand> <tui-expand>
@for (address of addresses().uncommon; track $index) { <hr />
<table class="g-table">
@for (address of addresses()?.uncommon; track $index) {
<tr [address]="address" [isRunning]="isRunning()"></tr> <tr [address]="address" [isRunning]="isRunning()"></tr>
} }
</tui-expand> </table>
</tui-accordion> </tui-expand>
} --> <button
</table> appearance="secondary-grayscale"
iconEnd=""
[(tuiAccordion)]="uncommon"
>
@if (uncommon) {
Hide uncommon
} @else {
Show uncommon
}
</button>
</tui-accordion>
}
`, `,
styles: ` styles: `
tui-accordion {
border-radius: 0;
}
[tuiAccordion], [tuiAccordion],
tui-expand { tui-expand {
box-shadow: none; box-shadow: none;
padding: 0; padding: 0;
background: none !important; }
&::after { [tuiAccordion] {
margin-inline-end: 0.25rem; justify-content: center;
height: 3rem;
border-radius: 0 0 var(--tui-radius-m) var(--tui-radius-m) !important;
}
hr {
margin: 0;
height: 0.25rem;
border-radius: 1rem;
}
:host-context(tui-root._mobile) {
[tuiAccordion] {
margin: 0.5rem 0;
border-radius: var(--tui-radius-m) !important;
} }
} }
`, `,
@@ -56,12 +95,16 @@ import { InterfaceAddressItemComponent } from './item.component'
PlaceholderComponent, PlaceholderComponent,
i18nPipe, i18nPipe,
InterfaceAddressItemComponent, InterfaceAddressItemComponent,
TuiButton,
TuiAccordion, TuiAccordion,
TuiSkeleton,
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class InterfaceAddressesComponent { export class InterfaceAddressesComponent {
readonly addresses = input.required<MappedServiceInterface['addresses']>() readonly addresses = input.required<
MappedServiceInterface['addresses'] | undefined
>()
readonly isRunning = input.required<boolean>() readonly isRunning = input.required<boolean>()
uncommon = false
} }

View File

@@ -13,7 +13,7 @@ import { AddressActionsComponent } from './actions.component'
selector: 'tr[address]', selector: 'tr[address]',
template: ` template: `
@if (address(); as address) { @if (address(); as address) {
<td> <td [style.width.rem]="3">
<button <button
tuiIconButton tuiIconButton
appearance="flat-grayscale" appearance="flat-grayscale"
@@ -23,14 +23,25 @@ import { AddressActionsComponent } from './actions.component'
{{ 'View instructions' | i18n }} {{ 'View instructions' | i18n }}
</button> </button>
</td> </td>
<td>{{ address.type }}</td> <td [style.width.rem]="6">{{ address.type }}</td>
<td [style.order]="-1">{{ address.gatewayName || '-' }}</td> <td [style.width.rem]="10" [style.order]="-1">
{{ address.gatewayName || '-' }}
</td>
<td>{{ address.url }}</td> <td>{{ address.url }}</td>
<td actions [disabled]="!isRunning()" [href]="address.url"></td> <td
actions
[disabled]="!isRunning()"
[href]="address.url"
[style.width.rem]="7"
></td>
} }
`, `,
styles: ` styles: `
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
td {
width: auto !important;
}
td:first-child { td:first-child {
display: none; display: none;
} }

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core' import { ChangeDetectionStrategy, Component, input } from '@angular/core'
import { DocsLinkDirective, i18nPipe } from '@start9labs/shared' import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
import { TuiButton } from '@taiga-ui/core' import { TuiButton } from '@taiga-ui/core'
import { TuiSkeleton } from '@taiga-ui/kit'
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component' import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
import { TableComponent } from 'src/app/routes/portal/components/table.component' import { TableComponent } from 'src/app/routes/portal/components/table.component'
@@ -34,13 +35,23 @@ import { ClearnetDomain } from './interface.service'
@for (domain of clearnetDomains(); track $index) { @for (domain of clearnetDomains(); track $index) {
<tr [domain]="domain"></tr> <tr [domain]="domain"></tr>
} @empty { } @empty {
<tr> @if (clearnetDomains()) {
<td colspan="4"> <tr>
<app-placeholder icon="@tui.globe"> <td colspan="4">
{{ 'No clearnet domains' | i18n }} <app-placeholder icon="@tui.globe">
</app-placeholder> {{ 'No clearnet domains' | i18n }}
</td> </app-placeholder>
</tr> </td>
</tr>
} @else {
@for (_ of [0, 1]; track $index) {
<tr>
<td colspan="4">
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
</td>
</tr>
}
}
} }
</table> </table>
`, `,
@@ -57,11 +68,14 @@ import { ClearnetDomain } from './interface.service'
i18nPipe, i18nPipe,
DocsLinkDirective, DocsLinkDirective,
DomainComponent, DomainComponent,
TuiSkeleton,
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class InterfaceClearnetDomainsComponent { export class InterfaceClearnetDomainsComponent {
readonly clearnetDomains = input.required<readonly ClearnetDomain[]>() readonly clearnetDomains = input.required<
readonly ClearnetDomain[] | undefined
>()
open = false open = false

View File

@@ -143,7 +143,7 @@ export class DomainComponent {
await this.api.pkgRemoveDomain({ await this.api.pkgRemoveDomain({
...params, ...params,
package: this.interface.packageId(), package: this.interface.packageId(),
host: this.interface.value().addressInfo.hostId, host: this.interface.value()?.addressInfo.hostId || '',
}) })
} else { } else {
await this.api.osUiRemoveDomain(params) await this.api.osUiRemoveDomain(params)

View File

@@ -1,7 +1,7 @@
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 { TuiTitle } from '@taiga-ui/core'
import { TuiSwitch } from '@taiga-ui/kit' import { TuiSkeleton, 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 { TuiCell } from '@taiga-ui/layout'
@@ -24,14 +24,28 @@ import { InterfaceGateway } from './interface.service'
[disabled]="isOs() && !gateway.public" [disabled]="isOs() && !gateway.public"
/> />
</label> </label>
} @empty {
@for (_ of [0, 1]; track $index) {
<label tuiCell="s">
<span tuiTitle [tuiSkeleton]="true">{{ 'Loading' | i18n }}</span>
</label>
}
} }
`, `,
host: { class: 'g-card' }, host: { class: 'g-card' },
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, FormsModule, TuiSwitch, i18nPipe, TuiCell, TuiTitle], imports: [
CommonModule,
FormsModule,
TuiSwitch,
i18nPipe,
TuiCell,
TuiTitle,
TuiSkeleton,
],
}) })
export class InterfaceGatewaysComponent { export class InterfaceGatewaysComponent {
readonly gateways = input.required<InterfaceGateway[]>() readonly gateways = input.required<InterfaceGateway[] | undefined>()
readonly isOs = input.required<boolean>() readonly isOs = input.required<boolean>()
async onToggle(gateway: InterfaceGateway) {} async onToggle(gateway: InterfaceGateway) {}

View File

@@ -11,12 +11,15 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component'
template: ` template: `
<!-- @TODO Alex / Matt translation in all nested components --> <!-- @TODO Alex / Matt translation in all nested components -->
<div [style.display]="'grid'"> <div [style.display]="'grid'">
<section [gateways]="value().gateways" [isOs]="value().isOs"></section> <section
<section [torDomains]="value().torDomains"></section> [gateways]="value()?.gateways"
<section [clearnetDomains]="value().clearnetDomains"></section> [isOs]="!!value()?.isOs"
></section>
<section [torDomains]="value()?.torDomains"></section>
<section [clearnetDomains]="value()?.clearnetDomains"></section>
</div> </div>
<hr [style.width.rem]="10" /> <hr [style.width.rem]="10" />
<section [addresses]="value().addresses" [isRunning]="true"></section> <section [addresses]="value()?.addresses" [isRunning]="true"></section>
`, `,
styles: ` styles: `
:host { :host {
@@ -29,6 +32,12 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component'
div { div {
gap: inherit; gap: inherit;
} }
::ng-deep [tuiSkeleton] {
width: 100%;
height: 1rem;
border-radius: var(--tui-radius-s);
}
} }
:host-context(tui-root._mobile) section { :host-context(tui-root._mobile) section {
@@ -46,6 +55,6 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component'
}) })
export class InterfaceComponent { export class InterfaceComponent {
readonly packageId = input('') readonly packageId = input('')
readonly value = input.required<MappedServiceInterface>() readonly value = input.required<MappedServiceInterface | undefined>()
readonly isRunning = input.required<boolean>() readonly isRunning = input.required<boolean>()
} }

View File

@@ -8,7 +8,7 @@ export class MaskPipe implements PipeTransform {
private readonly interface = inject(InterfaceComponent) private readonly interface = inject(InterfaceComponent)
transform(value: string): string { transform(value: string): string {
return this.interface.value().masked return this.interface.value()?.masked
? '•'.repeat(Math.min(32, value.length)) ? '•'.repeat(Math.min(32, value.length))
: value : value
} }

View File

@@ -13,6 +13,7 @@ import {
} from '@start9labs/shared' } from '@start9labs/shared'
import { ISB, utils } from '@start9labs/start-sdk' import { ISB, utils } from '@start9labs/start-sdk'
import { TuiButton, TuiTitle } from '@taiga-ui/core' import { TuiButton, TuiTitle } from '@taiga-ui/core'
import { TuiSkeleton } from '@taiga-ui/kit'
import { TuiCell } from '@taiga-ui/layout' import { TuiCell } from '@taiga-ui/layout'
import { filter } from 'rxjs' import { filter } from 'rxjs'
import { import {
@@ -66,9 +67,17 @@ type OnionForm = {
</button> </button>
</div> </div>
} @empty { } @empty {
<app-placeholder icon="@tui.target"> @if (torDomains()) {
{{ 'No Tor domains' | i18n }} <app-placeholder icon="@tui.target">
</app-placeholder> {{ 'No Tor domains' | i18n }}
</app-placeholder>
} @else {
@for (_ of [0, 1]; track $index) {
<label tuiCell="s">
<span tuiTitle [tuiSkeleton]="true">{{ 'Loading' | i18n }}</span>
</label>
}
}
} }
`, `,
styles: ` styles: `
@@ -84,6 +93,7 @@ type OnionForm = {
PlaceholderComponent, PlaceholderComponent,
i18nPipe, i18nPipe,
DocsLinkDirective, DocsLinkDirective,
TuiSkeleton,
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
@@ -96,7 +106,7 @@ export class InterfaceTorDomainsComponent {
private readonly interface = inject(InterfaceComponent) private readonly interface = inject(InterfaceComponent)
private readonly i18n = inject(i18nPipe) private readonly i18n = inject(i18nPipe)
readonly torDomains = input.required<readonly string[]>() readonly torDomains = input.required<readonly string[] | undefined>()
async remove(onion: string) { async remove(onion: string) {
this.dialog this.dialog
@@ -111,7 +121,7 @@ export class InterfaceTorDomainsComponent {
await this.api.pkgRemoveOnion({ await this.api.pkgRemoveOnion({
...params, ...params,
package: this.interface.packageId(), package: this.interface.packageId(),
host: this.interface.value().addressInfo.hostId, host: this.interface.value()?.addressInfo.hostId || '',
}) })
} else { } else {
await this.api.serverRemoveOnion(params) await this.api.serverRemoveOnion(params)
@@ -166,7 +176,7 @@ export class InterfaceTorDomainsComponent {
await this.api.pkgAddOnion({ await this.api.pkgAddOnion({
onion, onion,
package: this.interface.packageId(), package: this.interface.packageId(),
host: this.interface.value().addressInfo.hostId, host: this.interface.value()?.addressInfo.hostId || '',
}) })
} else { } else {
await this.api.serverAddOnion({ onion }) await this.api.serverAddOnion({ onion })

View File

@@ -37,9 +37,7 @@ import { TitleDirective } from 'src/app/services/title.service'
<p tuiSubtitle>{{ iface.description }}</p> <p tuiSubtitle>{{ iface.description }}</p>
</hgroup> </hgroup>
</header> </header>
@if (ui(); as ui) { <service-interface [value]="ui()" [isRunning]="true" />
<service-interface [value]="ui" [isRunning]="true" />
}
`, `,
host: { class: 'g-subpage' }, host: { class: 'g-subpage' },
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -101,10 +101,6 @@ import { map } from 'rxjs'
} }
} }
hr {
background: var(--tui-border-normal);
}
::ng-deep hgroup h3 { ::ng-deep hgroup h3 {
display: none; display: none;
} }

View File

@@ -19,7 +19,7 @@ ul {
hr { hr {
height: 1px; height: 1px;
background: var(--tui-background-neutral-1); background: var(--tui-border-normal);
border: none; border: none;
} }