chore: fix

This commit is contained in:
waterplea
2026-03-16 09:57:46 +04:00
parent b5ac0b5200
commit 7b8bb92d60
15 changed files with 191 additions and 120 deletions

View File

@@ -16,7 +16,7 @@ import { StateService } from '../services/state.service'
<button tuiCell="l" (click)="startFresh()"> <button tuiCell="l" (click)="startFresh()">
<span tuiAvatar="@tui.plus" appearance="positive"></span> <span tuiAvatar="@tui.plus" appearance="positive"></span>
<div tuiTitle> <div tuiTitle>
{{ 'Start Fresh' | i18n }} <b>{{ 'Start Fresh' | i18n }}</b>
<div tuiSubtitle>{{ 'Set up a brand new server' | i18n }}</div> <div tuiSubtitle>{{ 'Set up a brand new server' | i18n }}</div>
</div> </div>
</button> </button>
@@ -24,7 +24,7 @@ import { StateService } from '../services/state.service'
<button tuiCell="l" (click)="restore()"> <button tuiCell="l" (click)="restore()">
<span tuiAvatar="@tui.archive-restore" appearance="warning"></span> <span tuiAvatar="@tui.archive-restore" appearance="warning"></span>
<div tuiTitle> <div tuiTitle>
{{ 'Restore from Backup' | i18n }} <b>{{ 'Restore from Backup' | i18n }}</b>
<div tuiSubtitle> <div tuiSubtitle>
{{ 'Restore StartOS data from an encrypted backup' | i18n }} {{ 'Restore StartOS data from an encrypted backup' | i18n }}
</div> </div>
@@ -34,7 +34,7 @@ import { StateService } from '../services/state.service'
<button tuiCell="l" (click)="transfer()"> <button tuiCell="l" (click)="transfer()">
<span tuiAvatar="@tui.hard-drive-download" appearance="info"></span> <span tuiAvatar="@tui.hard-drive-download" appearance="info"></span>
<div tuiTitle> <div tuiTitle>
{{ 'Transfer' | i18n }} <b>{{ 'Transfer' | i18n }}</b>
<div tuiSubtitle> <div tuiSubtitle>
{{ 'Transfer data from an existing StartOS data drive' | i18n }} {{ 'Transfer data from an existing StartOS data drive' | i18n }}
</div> </div>

View File

@@ -29,20 +29,20 @@ import { StateService } from '../services/state.service'
<canvas matrix></canvas> <canvas matrix></canvas>
<section tuiCardLarge> <section tuiCardLarge>
<header tuiHeader> <header tuiHeader>
<h2 tuiTitle> <hgroup tuiTitle>
<span class="inline-title"> <h2 tuiCell="m">
<tui-icon icon="@tui.circle-check-big" class="g-positive" /> <tui-icon icon="@tui.circle-check-big" class="g-positive" />
{{ 'Setup Complete!' | i18n }} {{ 'Setup Complete!' | i18n }}
</span> </h2>
@if (!stateService.kiosk) { </hgroup>
<span tuiSubtitle> @if (!stateService.kiosk) {
{{ <p tuiSubtitle>
'http://start.local was for setup only. It will no longer work.' {{
| i18n 'http://start.local was for setup only. It will no longer work.'
}} | i18n
</span> }}
} </p>
</h2> }
</header> </header>
@if (!result) { @if (!result) {
@@ -52,15 +52,15 @@ import { StateService } from '../services/state.service'
@if (!stateService.kiosk) { @if (!stateService.kiosk) {
<button tuiCell="l" (click)="download()"> <button tuiCell="l" (click)="download()">
<span tuiAvatar="@tui.download" appearance="secondary"></span> <span tuiAvatar="@tui.download" appearance="secondary"></span>
<div tuiTitle> <span tuiTitle>
{{ 'Download Address Info' | i18n }} <b>{{ 'Download Address Info' | i18n }}</b>
<div tuiSubtitle> <span tuiSubtitle>
{{ {{
"Contains your server's permanent local address and Root CA" "Contains your server's permanent local address and Root CA"
| i18n | i18n
}} }}
</div> </span>
</div> </span>
@if (downloaded) { @if (downloaded) {
<tui-icon icon="@tui.circle-check" class="g-positive" /> <tui-icon icon="@tui.circle-check" class="g-positive" />
} }
@@ -76,15 +76,15 @@ import { StateService } from '../services/state.service'
(click)="removeMedia()" (click)="removeMedia()"
> >
<span tuiAvatar="@tui.usb" appearance="secondary"></span> <span tuiAvatar="@tui.usb" appearance="secondary"></span>
<div tuiTitle> <span tuiTitle>
{{ 'Remove Installation Media' | i18n }} <b>{{ 'Remove Installation Media' | i18n }}</b>
<div tuiSubtitle> <span tuiSubtitle>
{{ {{
'Remove USB stick or other installation media from your server' 'Remove USB stick or other installation media from your server'
| i18n | i18n
}} }}
</div> </span>
</div> </span>
@if (usbRemoved) { @if (usbRemoved) {
<tui-icon icon="@tui.circle-check" class="g-positive" /> <tui-icon icon="@tui.circle-check" class="g-positive" />
} }
@@ -99,15 +99,15 @@ import { StateService } from '../services/state.service'
(click)="acknowledgeMok()" (click)="acknowledgeMok()"
> >
<span tuiAvatar="@tui.shield-check" appearance="secondary"></span> <span tuiAvatar="@tui.shield-check" appearance="secondary"></span>
<div tuiTitle> <span tuiTitle>
{{ 'Secure Boot Enrollment' | i18n }} <b>{{ 'Secure Boot Enrollment' | i18n }}</b>
<div tuiSubtitle> <span tuiSubtitle>
{{ {{
'Prepare for Secure Boot key enrollment on the next reboot' 'Prepare for Secure Boot key enrollment on the next reboot'
| i18n | i18n
}} }}
</div> </span>
</div> </span>
@if (mokAcknowledged) { @if (mokAcknowledged) {
<tui-icon icon="@tui.circle-check" class="g-positive" /> <tui-icon icon="@tui.circle-check" class="g-positive" />
} }
@@ -126,9 +126,9 @@ import { StateService } from '../services/state.service'
(click)="reboot()" (click)="reboot()"
> >
<span tuiAvatar="@tui.rotate-cw" appearance="secondary"></span> <span tuiAvatar="@tui.rotate-cw" appearance="secondary"></span>
<div tuiTitle> <span tuiTitle>
{{ 'Restart Server' | i18n }} <b>{{ 'Restart Server' | i18n }}</b>
<div tuiSubtitle> <span tuiSubtitle>
@if (rebooting) { @if (rebooting) {
{{ 'Waiting for server to come back online' | i18n }} {{ 'Waiting for server to come back online' | i18n }}
} @else if (rebooted) { } @else if (rebooted) {
@@ -136,8 +136,8 @@ import { StateService } from '../services/state.service'
} @else { } @else {
{{ 'Restart your server to complete setup' | i18n }} {{ 'Restart your server to complete setup' | i18n }}
} }
</div> </span>
</div> </span>
@if (rebooting) { @if (rebooting) {
<tui-loader /> <tui-loader />
} @else if (rebooted) { } @else if (rebooted) {
@@ -147,12 +147,12 @@ import { StateService } from '../services/state.service'
} @else if (stateService.kiosk) { } @else if (stateService.kiosk) {
<button tuiCell="l" (click)="exitKiosk()"> <button tuiCell="l" (click)="exitKiosk()">
<span tuiAvatar="@tui.log-in" appearance="secondary"></span> <span tuiAvatar="@tui.log-in" appearance="secondary"></span>
<div tuiTitle> <span tuiTitle>
{{ 'Continue to Login' | i18n }} <b>{{ 'Continue to Login' | i18n }}</b>
<div tuiSubtitle> <span tuiSubtitle>
{{ 'Proceed to the StartOS login screen' | i18n }} {{ 'Proceed to the StartOS login screen' | i18n }}
</div> </span>
</div> </span>
</button> </button>
} }
@@ -165,10 +165,10 @@ import { StateService } from '../services/state.service'
(click)="openLocalAddress()" (click)="openLocalAddress()"
> >
<span tuiAvatar="@tui.external-link" appearance="secondary"></span> <span tuiAvatar="@tui.external-link" appearance="secondary"></span>
<div tuiTitle> <span tuiTitle>
{{ 'Open Local Address' | i18n }} <b>{{ 'Open Local Address' | i18n }}</b>
<div tuiSubtitle>{{ lanAddress }}</div> <span tuiSubtitle>{{ lanAddress }}</span>
</div> </span>
</button> </button>
<app-documentation hidden [lanAddress]="lanAddress" /> <app-documentation hidden [lanAddress]="lanAddress" />
@@ -177,12 +177,6 @@ import { StateService } from '../services/state.service'
</section> </section>
`, `,
styles: ` styles: `
.inline-title {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
[tuiCell].disabled { [tuiCell].disabled {
opacity: var(--tui-disabled-opacity); opacity: var(--tui-disabled-opacity);
pointer-events: none; pointer-events: none;

View File

@@ -67,10 +67,6 @@ button:disabled {
} }
} }
[tuiCell]:not([tuiOption]):not(:last-of-type) {
box-shadow: 0 calc(0.5rem + 1px) 0 -0.5rem var(--tui-border-normal);
}
// TODO: Remove in Taiga v5.0 // TODO: Remove in Taiga v5.0
[tuiButton] { [tuiButton] {
min-block-size: var(--t-size); min-block-size: var(--t-size);

View File

@@ -37,7 +37,7 @@ export const appConfig: ApplicationConfig = {
provideRouter(routes, withRouterConfig({ onSameUrlNavigation: 'reload' })), provideRouter(routes, withRouterConfig({ onSameUrlNavigation: 'reload' })),
provideTaiga({ mode: 'dark' }), provideTaiga({ mode: 'dark' }),
tuiDropdownOptionsProvider({ appearance: 'start-9' }), tuiDropdownOptionsProvider({ appearance: 'start-9' }),
tuiDialogOptionsProvider({ appearance: 'start-9 taiga' }), tuiDialogOptionsProvider({ appearance: 'start-9 taiga', size: 's' }),
{ {
provide: PatchDB, provide: PatchDB,
deps: [PatchDbSource, PATCH_CACHE], deps: [PatchDbSource, PATCH_CACHE],

View File

@@ -0,0 +1,35 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
import { TuiIcon } from '@taiga-ui/core'
@Component({
selector: 'app-placeholder',
template: `
@if (icon(); as icon) {
<tui-icon [icon]="icon" />
}
<ng-content />
`,
styles: `
:host {
display: flex;
flex: 1;
flex-direction: column;
gap: 0.5rem;
align-items: center;
justify-content: center;
text-align: center;
padding: 1rem;
font: var(--tui-typography-body-l);
color: var(--tui-text-tertiary);
tui-icon {
font-size: 2.5rem;
}
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiIcon],
})
export class PlaceholderComponent {
readonly icon = input<string>()
}

View File

@@ -168,7 +168,9 @@ export class DevicesAdd {
ip, ip,
}) })
this.dialogs.open(DEVICES_CONFIG, { data: config }).subscribe() this.dialogs
.open(DEVICES_CONFIG, { data: config, closable: false })
.subscribe()
} }
} catch (e: any) { } catch (e: any) {
console.error(e) console.error(e)

View File

@@ -3,24 +3,28 @@ import {
Component, Component,
computed, computed,
inject, inject,
Signal,
} from '@angular/core' } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop' import { toSignal } from '@angular/core/rxjs-interop'
import { ErrorService } from '@start9labs/shared' import { ErrorService } from '@start9labs/shared'
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile' import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
import { TuiButton, TuiDataList, TuiDropdown } from '@taiga-ui/core' import { TuiButton, TuiDataList, TuiDropdown } from '@taiga-ui/core'
import { TUI_CONFIRM, TuiNotificationMiddleService } from '@taiga-ui/kit' import {
TUI_CONFIRM,
TuiNotificationMiddleService,
TuiSkeleton,
} from '@taiga-ui/kit'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { filter, map } from 'rxjs' import { filter, map } from 'rxjs'
import { PlaceholderComponent } from 'src/app/routes/home/components/placeholder'
import { ApiService } from 'src/app/services/api/api.service' import { ApiService } from 'src/app/services/api/api.service'
import { TunnelData } from 'src/app/services/patch-db/data-model' import { TunnelData } from 'src/app/services/patch-db/data-model'
import { DEVICES_ADD } from './add' import { DEVICES_ADD } from './add'
import { DEVICES_CONFIG } from './config' import { DEVICES_CONFIG } from './config'
import { MappedDevice, MappedSubnet } from './utils' import { MappedDevice } from './utils'
@Component({ @Component({
template: ` template: `
<table class="g-table"> <table class="g-table" [tuiSkeleton]="!devices()">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
@@ -49,7 +53,11 @@ import { MappedDevice, MappedSubnet } from './utils'
iconStart="@tui.ellipsis-vertical" iconStart="@tui.ellipsis-vertical"
> >
Actions Actions
<tui-data-list *tuiDropdown size="s"> <tui-data-list
*tuiDropdown="let close"
size="s"
(click)="close()"
>
<button <button
tuiOption tuiOption
iconStart="@tui.pencil" iconStart="@tui.pencil"
@@ -76,13 +84,23 @@ import { MappedDevice, MappedSubnet } from './utils'
</td> </td>
</tr> </tr>
} @empty { } @empty {
<div class="placeholder">No devices</div> <tr>
<td colspan="4">
<app-placeholder icon="@tui.laptop">No devices</app-placeholder>
</td>
</tr>
} }
</tbody> </tbody>
</table> </table>
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiButton, TuiDropdown, TuiDataList], imports: [
TuiButton,
TuiDropdown,
TuiDataList,
PlaceholderComponent,
TuiSkeleton,
],
}) })
export default class Devices { export default class Devices {
private readonly dialogs = inject(TuiResponsiveDialogService) private readonly dialogs = inject(TuiResponsiveDialogService)
@@ -90,7 +108,7 @@ export default class Devices {
private readonly loading = inject(TuiNotificationMiddleService) private readonly loading = inject(TuiNotificationMiddleService)
private readonly errorService = inject(ErrorService) private readonly errorService = inject(ErrorService)
protected readonly subnets: Signal<readonly MappedSubnet[]> = toSignal( protected readonly subnets = toSignal(
inject<PatchDB<TunnelData>>(PatchDB) inject<PatchDB<TunnelData>>(PatchDB)
.watch$('wg', 'subnets') .watch$('wg', 'subnets')
.pipe( .pipe(
@@ -102,11 +120,11 @@ export default class Devices {
})), })),
), ),
), ),
{ initialValue: [] }, { initialValue: null },
) )
protected readonly devices = computed(() => protected readonly devices = computed(() =>
this.subnets().flatMap(subnet => this.subnets()?.flatMap(subnet =>
Object.entries(subnet.clients).map(([ip, { name }]) => ({ Object.entries(subnet.clients).map(([ip, { name }]) => ({
subnet: { subnet: {
name: subnet.name, name: subnet.name,
@@ -141,7 +159,7 @@ export default class Devices {
try { try {
const data = await this.api.showDeviceConfig({ subnet: subnet.range, ip }) const data = await this.api.showDeviceConfig({ subnet: subnet.range, ip })
this.dialogs.open(DEVICES_CONFIG, { data }).subscribe() this.dialogs.open(DEVICES_CONFIG, { data, closable: false }).subscribe()
} catch (e: any) { } catch (e: any) {
console.log(e) console.log(e)
this.errorService.handleError(e) this.errorService.handleError(e)

View File

@@ -15,6 +15,7 @@ import {
TuiCheckbox, TuiCheckbox,
TuiDialogContext, TuiDialogContext,
TuiError, TuiError,
TuiInput,
TuiNumberFormat, TuiNumberFormat,
} from '@taiga-ui/core' } from '@taiga-ui/core'
import { import {
@@ -35,7 +36,7 @@ import { MappedDevice, PortForwardsData } from './utils'
<form tuiForm [formGroup]="form"> <form tuiForm [formGroup]="form">
<tui-textfield> <tui-textfield>
<label tuiLabel>Label</label> <label tuiLabel>Label</label>
<input tuiTextfield formControlName="label" /> <input tuiInput formControlName="label" />
</tui-textfield> </tui-textfield>
<tui-error formControlName="label" /> <tui-error formControlName="label" />
<tui-textfield tuiChevron> <tui-textfield tuiChevron>
@@ -129,6 +130,7 @@ import { MappedDevice, PortForwardsData } from './utils'
TuiCheckbox, TuiCheckbox,
TuiValueChanges, TuiValueChanges,
TuiElasticContainer, TuiElasticContainer,
TuiInput,
], ],
}) })
export class PortForwardsAdd { export class PortForwardsAdd {

View File

@@ -6,12 +6,7 @@ import {
} from '@angular/forms' } from '@angular/forms'
import { ErrorService } from '@start9labs/shared' import { ErrorService } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
import { import { TuiButton, TuiDialogContext, TuiError, TuiInput } from '@taiga-ui/core'
TuiButton,
TuiDialogContext,
TuiError,
TuiTextfield,
} from '@taiga-ui/core'
import { TuiNotificationMiddleService } from '@taiga-ui/kit' import { TuiNotificationMiddleService } from '@taiga-ui/kit'
import { TuiForm } from '@taiga-ui/layout' import { TuiForm } from '@taiga-ui/layout'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
@@ -27,7 +22,7 @@ export interface EditLabelData {
<form tuiForm [formGroup]="form"> <form tuiForm [formGroup]="form">
<tui-textfield> <tui-textfield>
<label tuiLabel>Label</label> <label tuiLabel>Label</label>
<input tuiTextfield formControlName="label" /> <input tuiInput formControlName="label" />
</tui-textfield> </tui-textfield>
<tui-error formControlName="label" /> <tui-error formControlName="label" />
<footer> <footer>
@@ -38,7 +33,7 @@ export interface EditLabelData {
</form> </form>
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ReactiveFormsModule, TuiButton, TuiError, TuiTextfield, TuiForm], imports: [ReactiveFormsModule, TuiButton, TuiError, TuiInput, TuiForm],
}) })
export class PortForwardsEditLabel { export class PortForwardsEditLabel {
private readonly api = inject(ApiService) private readonly api = inject(ApiService)

View File

@@ -21,10 +21,12 @@ import {
import { import {
TUI_CONFIRM, TUI_CONFIRM,
TuiNotificationMiddleService, TuiNotificationMiddleService,
TuiSkeleton,
TuiSwitch, TuiSwitch,
} from '@taiga-ui/kit' } from '@taiga-ui/kit'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { filter, map } from 'rxjs' import { filter, map } from 'rxjs'
import { PlaceholderComponent } from 'src/app/routes/home/components/placeholder'
import { PORT_FORWARDS_ADD } from 'src/app/routes/home/routes/port-forwards/add' import { PORT_FORWARDS_ADD } from 'src/app/routes/home/routes/port-forwards/add'
import { PORT_FORWARDS_EDIT_LABEL } from 'src/app/routes/home/routes/port-forwards/edit-label' import { PORT_FORWARDS_EDIT_LABEL } from 'src/app/routes/home/routes/port-forwards/edit-label'
import { ApiService } from 'src/app/services/api/api.service' import { ApiService } from 'src/app/services/api/api.service'
@@ -34,7 +36,7 @@ import { MappedDevice, MappedForward } from './utils'
@Component({ @Component({
template: ` template: `
<table class="g-table"> <table class="g-table" [tuiSkeleton]="!portForwards()">
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
@@ -84,11 +86,14 @@ import { MappedDevice, MappedForward } from './utils'
iconStart="@tui.ellipsis-vertical" iconStart="@tui.ellipsis-vertical"
> >
Actions Actions
<tui-data-list *tuiDropdown size="s"> <tui-data-list
*tuiDropdown="let close"
size="s"
(click)="close()"
>
<button <button
tuiOption tuiOption
iconStart="@tui.pencil" iconStart="@tui.pencil"
new
(click)="onEditLabel(forward)" (click)="onEditLabel(forward)"
> >
{{ forward.label ? 'Rename' : 'Add label' }} {{ forward.label ? 'Rename' : 'Add label' }}
@@ -96,7 +101,6 @@ import { MappedDevice, MappedForward } from './utils'
<button <button
tuiOption tuiOption
iconStart="@tui.trash" iconStart="@tui.trash"
new
(click)="onDelete(forward)" (click)="onDelete(forward)"
> >
Delete Delete
@@ -106,7 +110,13 @@ import { MappedDevice, MappedForward } from './utils'
</td> </td>
</tr> </tr>
} @empty { } @empty {
<div class="placeholder">No port forwards</div> <tr>
<td colspan="7">
<app-placeholder icon="@tui.globe">
No port forwards
</app-placeholder>
</td>
</tr>
} }
</tbody> </tbody>
</table> </table>
@@ -120,6 +130,8 @@ import { MappedDevice, MappedForward } from './utils'
TuiLoader, TuiLoader,
TuiSwitch, TuiSwitch,
TuiTextfield, TuiTextfield,
PlaceholderComponent,
TuiSkeleton,
], ],
}) })
export default class PortForwards { export default class PortForwards {
@@ -128,8 +140,6 @@ export default class PortForwards {
private readonly loading = inject(TuiNotificationMiddleService) private readonly loading = inject(TuiNotificationMiddleService)
private readonly patch = inject<PatchDB<TunnelData>>(PatchDB) private readonly patch = inject<PatchDB<TunnelData>>(PatchDB)
private readonly errorService = inject(ErrorService) private readonly errorService = inject(ErrorService)
private readonly portForwards = toSignal(this.patch.watch$('portForwards'))
private readonly ips = toSignal( private readonly ips = toSignal(
this.patch.watch$('gateways').pipe( this.patch.watch$('gateways').pipe(
map(g => map(g =>
@@ -157,6 +167,7 @@ export default class PortForwards {
{ initialValue: [] }, { initialValue: [] },
) )
protected readonly portForwards = toSignal(this.patch.watch$('portForwards'))
protected readonly forwards = computed(() => protected readonly forwards = computed(() =>
Object.entries(this.portForwards() || {}).map(([source, entry]) => { Object.entries(this.portForwards() || {}).map(([source, entry]) => {
const sourceSplit = source.split(':') const sourceSplit = source.split(':')

View File

@@ -3,9 +3,14 @@ import { toSignal } from '@angular/core/rxjs-interop'
import { utils } from '@start9labs/start-sdk' import { utils } from '@start9labs/start-sdk'
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile' import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
import { TuiButton, TuiDataList, TuiDropdown } from '@taiga-ui/core' import { TuiButton, TuiDataList, TuiDropdown } from '@taiga-ui/core'
import { TUI_CONFIRM, TuiNotificationMiddleService } from '@taiga-ui/kit' import {
TUI_CONFIRM,
TuiNotificationMiddleService,
TuiSkeleton,
} from '@taiga-ui/kit'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { filter, map } from 'rxjs' import { filter, map } from 'rxjs'
import { PlaceholderComponent } from 'src/app/routes/home/components/placeholder'
import { ApiService } from 'src/app/services/api/api.service' import { ApiService } from 'src/app/services/api/api.service'
import { TunnelData } from 'src/app/services/patch-db/data-model' import { TunnelData } from 'src/app/services/patch-db/data-model'
@@ -13,7 +18,7 @@ import { SUBNETS_ADD } from './add'
@Component({ @Component({
template: ` template: `
<table class="g-table"> <table class="g-table" [tuiSkeleton]="!subnets()">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
@@ -40,7 +45,11 @@ import { SUBNETS_ADD } from './add'
iconStart="@tui.ellipsis-vertical" iconStart="@tui.ellipsis-vertical"
> >
Actions Actions
<tui-data-list *tuiDropdown size="s"> <tui-data-list
*tuiDropdown="let close"
size="s"
(click)="close()"
>
<button <button
tuiOption tuiOption
iconStart="@tui.pencil" iconStart="@tui.pencil"
@@ -60,20 +69,30 @@ import { SUBNETS_ADD } from './add'
</td> </td>
</tr> </tr>
} @empty { } @empty {
<div class="placeholder">No subnets</div> <tr>
<td colspan="3">
<app-placeholder icon="@tui.network">No subnets</app-placeholder>
</td>
</tr>
} }
</tbody> </tbody>
</table> </table>
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiButton, TuiDropdown, TuiDataList], imports: [
TuiButton,
TuiDropdown,
TuiDataList,
PlaceholderComponent,
TuiSkeleton,
],
}) })
export default class Subnets { export default class Subnets {
private readonly dialogs = inject(TuiResponsiveDialogService) private readonly dialogs = inject(TuiResponsiveDialogService)
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
private readonly loading = inject(TuiNotificationMiddleService) private readonly loading = inject(TuiNotificationMiddleService)
protected readonly subnets = toSignal<MappedSubnet[], []>( protected readonly subnets = toSignal(
inject<PatchDB<TunnelData>>(PatchDB) inject<PatchDB<TunnelData>>(PatchDB)
.watch$('wg', 'subnets') .watch$('wg', 'subnets')
.pipe( .pipe(
@@ -85,7 +104,7 @@ export default class Subnets {
})), })),
), ),
), ),
{ initialValue: [] }, { initialValue: null },
) )
protected onAdd(): void { protected onAdd(): void {
@@ -111,7 +130,7 @@ export default class Subnets {
.open(TUI_CONFIRM, { label: 'Are you sure?' }) .open(TUI_CONFIRM, { label: 'Are you sure?' })
.pipe(filter(Boolean)) .pipe(filter(Boolean))
.subscribe(async () => { .subscribe(async () => {
const subnet = this.subnets()[index]?.range || '' const subnet = this.subnets()?.[index]?.range || ''
const loader = this.loading.open('').subscribe() const loader = this.loading.open('').subscribe()
try { try {
@@ -125,13 +144,13 @@ export default class Subnets {
} }
private getNext(): string { private getNext(): string {
const current = this.subnets().map(s => utils.IpNet.parse(s.range)) const current = this.subnets()?.map(s => utils.IpNet.parse(s.range))
const suggestion = utils.IpNet.parse('10.59.0.1/24') const suggestion = utils.IpNet.parse('10.59.0.1/24')
for (let i = 0; i < 256; i++) { for (let i = 0; i < 256; i++) {
suggestion.octets[2] = Math.floor(Math.random() * 256) suggestion.octets[2] = Math.floor(Math.random() * 256)
if ( if (
!current.some( !current?.some(
s => s.contains(suggestion), // inverse check unnecessary since we don't allow subnets smaller than /24 s => s.contains(suggestion), // inverse check unnecessary since we don't allow subnets smaller than /24
) )
) { ) {

View File

@@ -1,9 +1,8 @@
import { Component, computed, inject, Injectable, signal } from '@angular/core' import { computed, inject, Injectable, signal } from '@angular/core'
import { toObservable } from '@angular/core/rxjs-interop' import { toObservable } from '@angular/core/rxjs-interop'
import { ErrorService } from '@start9labs/shared' import { ErrorService } from '@start9labs/shared'
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile' import { T } from '@start9labs/start-sdk'
import { TuiLoader } from '@taiga-ui/core' import { TuiNotificationMiddleService } from '@taiga-ui/kit'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { import {
catchError, catchError,
EMPTY, EMPTY,
@@ -14,25 +13,16 @@ import {
switchMap, switchMap,
takeWhile, takeWhile,
} from 'rxjs' } from 'rxjs'
import { T } from '@start9labs/start-sdk'
import { ApiService } from './api/api.service' import { ApiService } from './api/api.service'
import { AuthService } from './auth.service' import { AuthService } from './auth.service'
@Component({
template: '<tui-loader size="xl" [textContent]="text" />',
imports: [TuiLoader],
})
class UpdatingDialog {
protected readonly text = 'StartTunnel is updating...'
}
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class UpdateService { export class UpdateService {
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
private readonly auth = inject(AuthService) private readonly auth = inject(AuthService)
private readonly dialogs = inject(TuiResponsiveDialogService) private readonly loading = inject(TuiNotificationMiddleService)
private readonly errorService = inject(ErrorService) private readonly errorService = inject(ErrorService)
readonly result = signal<T.Tunnel.TunnelUpdateResult | null>(null) readonly result = signal<T.Tunnel.TunnelUpdateResult | null>(null)
@@ -106,11 +96,8 @@ export class UpdateService {
private showUpdatingDialog(): void { private showUpdatingDialog(): void {
if (this.updatingDialog) return if (this.updatingDialog) return
this.updatingDialog = this.dialogs this.updatingDialog = this.loading
.open(new PolymorpheusComponent(UpdatingDialog), { .open('StartTunnel is updating...')
closable: false,
dismissible: false,
})
.subscribe({ complete: () => (this.updatingDialog = null) }) .subscribe({ complete: () => (this.updatingDialog = null) })
} }

View File

@@ -93,12 +93,6 @@ tui-notification-middle {
} }
} }
.placeholder {
padding: 1rem;
font: var(--tui-font-text-l);
color: var(--tui-text-tertiary);
}
qr-code { qr-code {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -107,3 +101,21 @@ qr-code {
tui-data-list { tui-data-list {
--tui-text-action: var(--tui-text-primary); --tui-text-action: var(--tui-text-primary);
} }
[tuiTheme='dark'] tui-notification-middle[style] {
&.tui-enter,
&.tui-leave {
--tui-scale: 0;
animation-name: tuiScale;
}
&::before {
background: var(--tui-background-neutral-1);
backdrop-filter: blur(1rem);
box-shadow: inset 0 1px 1px var(--tui-background-neutral-2);
}
tui-loader svg {
stroke: white;
}
}

View File

@@ -98,6 +98,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
padding-top: 1rem; padding-top: 1rem;
border-radius: 0; border-radius: 0;
cursor: pointer; cursor: pointer;
overflow: hidden;
box-shadow: 0 -1px rgba(255, 255, 255, 0.1); box-shadow: 0 -1px rgba(255, 255, 255, 0.1);
} }

View File

@@ -171,8 +171,7 @@ ul {
.g-table { .g-table {
width: stretch; width: stretch;
border: 1px solid var(--tui-background-neutral-1); border: 1px solid var(--tui-background-neutral-1);
border-spacing: 0; border-collapse: collapse !important;
border-collapse: collapse;
border-radius: var(--tui-radius-s); border-radius: var(--tui-radius-s);
overflow: hidden; overflow: hidden;
box-shadow: inset 0 0 0 1px var(--tui-background-neutral-1); box-shadow: inset 0 0 0 1px var(--tui-background-neutral-1);