From 86dbf26253c8b1be542c9ba196f8632ada84577e Mon Sep 17 00:00:00 2001
From: waterplea
Date: Tue, 5 Aug 2025 17:39:48 +0700
Subject: [PATCH] refactor: gateways page
---
.../form/form-file/form-file.component.html | 27 ++-
.../form/form-file/form-file.component.scss | 3 +-
.../portal/components/form/form.module.ts | 8 +-
.../routes/gateways/gateways.component.ts | 4 +-
.../system/routes/gateways/item.component.ts | 184 +++++++++++++-----
.../system/routes/gateways/table.component.ts | 113 ++---------
6 files changed, 186 insertions(+), 153 deletions(-)
diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.html
index 9ea93323f..e8b3cd99b 100644
--- a/web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.html
+++ b/web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.html
@@ -1,8 +1,9 @@
-
@@ -62,7 +62,7 @@ import { GatewayWithID } from './item.component'
Add
-
+
`,
changeDetection: ChangeDetectionStrategy.OnPush,
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/item.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/item.component.ts
index efa883d65..ab07129d3 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/item.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/item.component.ts
@@ -1,17 +1,28 @@
import {
ChangeDetectionStrategy,
Component,
+ inject,
input,
- output,
} from '@angular/core'
-import { i18nPipe } from '@start9labs/shared'
-import { T } from '@start9labs/start-sdk'
+import {
+ DialogService,
+ ErrorService,
+ i18nPipe,
+ LoadingService,
+} from '@start9labs/shared'
+import { ISB, T } from '@start9labs/start-sdk'
import {
TuiButton,
TuiDataList,
TuiDropdown,
TuiOptGroup,
+ TuiTextfield,
} from '@taiga-ui/core'
+import { filter } from 'rxjs'
+import { FormComponent } from 'src/app/routes/portal/components/form.component'
+import { ApiService } from 'src/app/services/api/embassy-api.service'
+import { FormDialogService } from 'src/app/services/form-dialog.service'
+import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
export type GatewayWithID = T.NetworkInterfaceInfo & {
id: string
@@ -21,79 +32,164 @@ export type GatewayWithID = T.NetworkInterfaceInfo & {
@Component({
selector: 'tr[proxy]',
template: `
- {{ proxy().ipInfo.name }} |
- {{ proxy().ipInfo.deviceType || '-' }} |
-
+ | {{ proxy().ipInfo.name }} |
+ {{ proxy().ipInfo.deviceType || '-' }} |
+
{{ proxy().public ? ('Public' | i18n) : ('Private' | i18n) }}
|
- {{ proxy().ipInfo.subnets[0] }} |
- {{ proxy().ipInfo.wanIp }} |
+ {{ proxy().ipInfo.subnets[0] }} |
+ {{ proxy().ipInfo.wanIp }} |
-
-
+ >
+ {{ 'More' | i18n }}
+
-
+ @if (proxy().ipInfo.deviceType === 'wireguard') {
+
{{ 'Delete' | i18n }}
- }
-
+
+ }
-
+
|
`,
styles: `
td:last-child {
- grid-area: 3 / span 4;
- white-space: nowrap;
+ grid-area: 1 / 3 / 5;
+ align-self: center;
text-align: right;
- flex-direction: row-reverse;
- justify-content: flex-end;
- gap: 0.5rem;
}
:host-context(tui-root._mobile) {
- display: grid;
- grid-template-columns: repeat(3, min-content) 1fr;
- align-items: center;
- padding: 1rem 0.5rem;
- gap: 0.5rem;
+ grid-template-columns: min-content 1fr min-content;
- td {
- display: flex;
- padding: 0;
+ td:first-child {
+ font: var(--tui-font-text-m);
+ font-weight: bold;
+ }
+
+ .type {
+ order: -1;
+
+ &::before {
+ content: '\\00A0(';
+ }
+
+ &::after {
+ content: ')';
+ }
+ }
+
+ .lan,
+ .wan {
+ grid-column: span 2;
+
+ &::before {
+ content: 'LAN IPs: ';
+ color: var(--tui-text-primary);
+ }
+ }
+
+ .wan::before {
+ content: 'WAN IP: ';
}
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
- imports: [TuiButton, i18nPipe, TuiDropdown, TuiDataList, TuiOptGroup],
+ imports: [
+ TuiButton,
+ TuiDropdown,
+ TuiDataList,
+ TuiOptGroup,
+ TuiTextfield,
+ i18nPipe,
+ ],
})
export class GatewaysItemComponent {
+ private readonly dialog = inject(DialogService)
+ private readonly loader = inject(LoadingService)
+ private readonly errorService = inject(ErrorService)
+ private readonly api = inject(ApiService)
+ private readonly formDialog = inject(FormDialogService)
+
readonly proxy = input.required()
- onRename = output()
- onRemove = output()
-
open = false
+
+ remove() {
+ this.dialog
+ .openConfirm({ label: 'Are you sure?', size: 's' })
+ .pipe(filter(Boolean))
+ .subscribe(async () => {
+ const loader = this.loader.open('Deleting').subscribe()
+
+ try {
+ await this.api.removeTunnel({ id: this.proxy().id })
+ } catch (e: any) {
+ this.errorService.handleError(e)
+ } finally {
+ loader.unsubscribe()
+ }
+ })
+ }
+
+ async rename() {
+ const { ipInfo, id } = this.proxy()
+ const renameSpec = ISB.InputSpec.of({
+ label: ISB.Value.text({
+ name: 'Label',
+ required: true,
+ default: ipInfo?.name || null,
+ }),
+ })
+
+ this.formDialog.open(FormComponent, {
+ label: 'Rename',
+ data: {
+ spec: await configBuilderToSpec(renameSpec),
+ buttons: [
+ {
+ text: 'Save',
+ handler: (value: typeof renameSpec._TYPE) =>
+ this.update(id, value.label),
+ },
+ ],
+ },
+ })
+ }
+
+ private async update(id: string, name: string): Promise {
+ const loader = this.loader.open('Saving').subscribe()
+
+ try {
+ await this.api.updateTunnel({ id, name })
+ return true
+ } catch (e: any) {
+ this.errorService.handleError(e)
+ return false
+ } finally {
+ loader.unsubscribe()
+ }
+ }
}
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/table.component.ts
index c91062657..87a0c19ec 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/table.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/table.component.ts
@@ -1,25 +1,9 @@
-import {
- ChangeDetectionStrategy,
- Component,
- inject,
- input,
-} from '@angular/core'
-import {
- DialogService,
- ErrorService,
- i18nPipe,
- LoadingService,
-} from '@start9labs/shared'
-import { ISB } from '@start9labs/start-sdk'
+import { ChangeDetectionStrategy, Component, input } from '@angular/core'
+import { i18nPipe } from '@start9labs/shared'
import { TuiSkeleton } from '@taiga-ui/kit'
-import { filter } from 'rxjs'
-import { FormComponent } from 'src/app/routes/portal/components/form.component'
-import { ApiService } from 'src/app/services/api/embassy-api.service'
-import { FormDialogService } from 'src/app/services/form-dialog.service'
-import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
+import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
-import { GatewayWithID } from './item.component'
-import { GatewaysItemComponent } from './item.component'
+import { GatewaysItemComponent, GatewayWithID } from './item.component'
@Component({
selector: '[gateways]',
@@ -35,89 +19,32 @@ import { GatewaysItemComponent } from './item.component'
]"
>
@for (proxy of gateways(); track $index) {
-
+
} @empty {
|
- {{ 'Loading' | i18n }}
+ @if (gateways()) {
+
+
+ No gateways
+
+ } @else {
+ {{ 'Loading' | i18n }}
+ }
|
}
`,
- styles: `
- :host {
- grid-column: span 6;
- }
- `,
changeDetection: ChangeDetectionStrategy.OnPush,
- imports: [TuiSkeleton, i18nPipe, TableComponent, GatewaysItemComponent],
+ imports: [
+ TuiSkeleton,
+ i18nPipe,
+ TableComponent,
+ GatewaysItemComponent,
+ PlaceholderComponent,
+ ],
})
export class GatewaysTableComponent {
readonly gateways = input(null)
-
- private readonly dialog = inject(DialogService)
- private readonly loader = inject(LoadingService)
- private readonly errorService = inject(ErrorService)
- private readonly api = inject(ApiService)
- private readonly formDialog = inject(FormDialogService)
-
- remove(id: string) {
- this.dialog
- .openConfirm({ label: 'Are you sure?', size: 's' })
- .pipe(filter(Boolean))
- .subscribe(async () => {
- const loader = this.loader.open('Deleting').subscribe()
-
- try {
- await this.api.removeTunnel({ id })
- } catch (e: any) {
- this.errorService.handleError(e)
- } finally {
- loader.unsubscribe()
- }
- })
- }
-
- async rename(gateway: GatewayWithID) {
- const renameSpec = ISB.InputSpec.of({
- label: ISB.Value.text({
- name: 'Label',
- required: true,
- default: gateway.ipInfo?.name || null,
- }),
- })
-
- this.formDialog.open(FormComponent, {
- label: 'Rename',
- data: {
- spec: await configBuilderToSpec(renameSpec),
- buttons: [
- {
- text: 'Save',
- handler: (value: typeof renameSpec._TYPE) =>
- this.update(gateway.id, value.label),
- },
- ],
- },
- })
- }
-
- private async update(id: string, label: string): Promise {
- const loader = this.loader.open('Saving').subscribe()
-
- try {
- await this.api.updateTunnel({ id, name: label })
- return true
- } catch (e: any) {
- this.errorService.handleError(e)
- return false
- } finally {
- loader.unsubscribe()
- }
- }
}