mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
refactor: gateways page
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
<label tuiInputFiles [(ngModel)]="value">
|
||||
<label tuiInputFiles>
|
||||
<input
|
||||
tuiInputFiles
|
||||
[invalid]="invalid"
|
||||
[accept]="spec.extensions.join(',')"
|
||||
[(ngModel)]="value"
|
||||
(blur)="onFocus(false)"
|
||||
/>
|
||||
<ng-template let-drop>
|
||||
@@ -17,17 +18,25 @@
|
||||
}
|
||||
</div>
|
||||
@if (value) {
|
||||
<tui-tag
|
||||
class="file"
|
||||
size="l"
|
||||
[value]="value.name"
|
||||
[removable]="true"
|
||||
(edited)="value = null"
|
||||
/>
|
||||
<tui-chip>
|
||||
{{ value.name }}
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
size="xs"
|
||||
iconStart="@tui.x"
|
||||
(click.stop)="value = null"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
</tui-chip>
|
||||
} @else {
|
||||
<small>{{ 'Click or drop file here' | i18n }}</small>
|
||||
}
|
||||
</div>
|
||||
<div class="drop" [class.drop_hidden]="!drop">{{ 'Drop file here' | i18n }}</div>
|
||||
<div class="drop" [class.drop_hidden]="!drop">
|
||||
{{ 'Drop file here' | i18n }}
|
||||
</div>
|
||||
</ng-template>
|
||||
</label>
|
||||
|
||||
@@ -40,7 +40,8 @@ small {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
tui-tag {
|
||||
tui-chip {
|
||||
z-index: 1;
|
||||
margin: -0.25rem -0.25rem -0.25rem auto;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { MaskitoDirective } from '@maskito/angular'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { TuiMapperPipe, TuiValueChanges } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiAppearance,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
TuiNumberFormat,
|
||||
} from '@taiga-ui/core'
|
||||
import {
|
||||
TuiChip,
|
||||
TuiElasticContainer,
|
||||
TuiFieldErrorPipe,
|
||||
TuiFiles,
|
||||
@@ -28,11 +30,11 @@ import {
|
||||
TuiInputTimeModule,
|
||||
TuiMultiSelectModule,
|
||||
TuiSelectModule,
|
||||
TuiTagModule,
|
||||
TuiTextareaModule,
|
||||
TuiTextfieldControllerModule,
|
||||
} from '@taiga-ui/legacy'
|
||||
import { ControlDirective } from './control.directive'
|
||||
import { FilterHiddenPipe } from './filter-hidden.pipe'
|
||||
import { FormArrayComponent } from './form-array/form-array.component'
|
||||
import { FormColorComponent } from './form-color/form-color.component'
|
||||
import { FormControlComponent } from './form-control/form-control.component'
|
||||
@@ -49,8 +51,6 @@ import { FormToggleComponent } from './form-toggle/form-toggle.component'
|
||||
import { FormUnionComponent } from './form-union/form-union.component'
|
||||
import { HintPipe } from './hint.pipe'
|
||||
import { MustachePipe } from './mustache.pipe'
|
||||
import { FilterHiddenPipe } from './filter-hidden.pipe'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -66,7 +66,7 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
TuiSwitch,
|
||||
TuiTooltip,
|
||||
...TuiHint,
|
||||
TuiTagModule,
|
||||
TuiChip,
|
||||
TuiButton,
|
||||
...TuiExpand,
|
||||
TuiTextfieldControllerModule,
|
||||
|
||||
@@ -43,7 +43,7 @@ import { GatewayWithID } from './item.component'
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
[textContent]="'view instructions'"
|
||||
[textContent]="'View instructions' | i18n"
|
||||
></a>
|
||||
</p>
|
||||
</hgroup>
|
||||
@@ -62,7 +62,7 @@ import { GatewayWithID } from './item.component'
|
||||
Add
|
||||
</button>
|
||||
</header>
|
||||
<div #table [gateways]="gateways$ | async"></div>
|
||||
<div [gateways]="gateways$ | async"></div>
|
||||
</section>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -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: `
|
||||
<td>{{ proxy().ipInfo.name }}</td>
|
||||
<td>{{ proxy().ipInfo.deviceType || '-' }}</td>
|
||||
<td>
|
||||
<td [style.grid-column]="'span 2'">{{ proxy().ipInfo.name }}</td>
|
||||
<td class="type">{{ proxy().ipInfo.deviceType || '-' }}</td>
|
||||
<td [style.order]="-2">
|
||||
{{ proxy().public ? ('Public' | i18n) : ('Private' | i18n) }}
|
||||
</td>
|
||||
<!-- // @TODO show both LAN IPs? -->
|
||||
<td>{{ proxy().ipInfo.subnets[0] }}</td>
|
||||
<td>{{ proxy().ipInfo.wanIp }}</td>
|
||||
<td class="lan">{{ proxy().ipInfo.subnets[0] }}</td>
|
||||
<td class="wan">{{ proxy().ipInfo.wanIp }}</td>
|
||||
<td>
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.ellipsis"
|
||||
appearance="icon"
|
||||
[tuiDropdown]="content"
|
||||
tuiDropdown
|
||||
size="s"
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.ellipsis-vertical"
|
||||
[tuiAppearanceState]="open ? 'hover' : null"
|
||||
[(tuiDropdownOpen)]="open"
|
||||
[tuiDropdownMaxHeight]="9999"
|
||||
></button>
|
||||
<ng-template #content>
|
||||
<tui-data-list [style.width.rem]="13">
|
||||
>
|
||||
{{ 'More' | i18n }}
|
||||
<tui-data-list size="s" *tuiTextfieldDropdown>
|
||||
<tui-opt-group>
|
||||
<button
|
||||
tuiOption
|
||||
iconStart="@tui.pencil"
|
||||
(click)="onRename.emit(proxy())"
|
||||
>
|
||||
<button tuiOption new iconStart="@tui.pencil" (click)="rename()">
|
||||
{{ 'Rename' | i18n }}
|
||||
</button>
|
||||
@if (proxy().ipInfo.deviceType === 'wireguard') {
|
||||
</tui-opt-group>
|
||||
@if (proxy().ipInfo.deviceType === 'wireguard') {
|
||||
<tui-opt-group>
|
||||
<button
|
||||
tuiOption
|
||||
appearance="negative"
|
||||
iconStart="@tui.trash-2"
|
||||
(click)="onRemove.emit(proxy())"
|
||||
new
|
||||
iconStart="@tui.trash"
|
||||
class="g-negative"
|
||||
(click)="remove()"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
}
|
||||
</tui-opt-group>
|
||||
</tui-opt-group>
|
||||
}
|
||||
</tui-data-list>
|
||||
</ng-template>
|
||||
</button>
|
||||
</td>
|
||||
`,
|
||||
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<GatewayWithID>()
|
||||
|
||||
onRename = output<GatewayWithID>()
|
||||
onRemove = output<GatewayWithID>()
|
||||
|
||||
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<boolean> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
<tr
|
||||
[proxy]="proxy"
|
||||
(onRename)="rename($event)"
|
||||
(onRemove)="remove($event.id)"
|
||||
></tr>
|
||||
<tr [proxy]="proxy"></tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
|
||||
@if (gateways()) {
|
||||
<app-placeholder icon="@tui.door-closed-locked">
|
||||
<!-- @TODO Matt finalize text and add translations -->
|
||||
No gateways
|
||||
</app-placeholder>
|
||||
} @else {
|
||||
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
grid-column: span 6;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiSkeleton, i18nPipe, TableComponent, GatewaysItemComponent],
|
||||
imports: [
|
||||
TuiSkeleton,
|
||||
i18nPipe,
|
||||
TableComponent,
|
||||
GatewaysItemComponent,
|
||||
PlaceholderComponent,
|
||||
],
|
||||
})
|
||||
export class GatewaysTableComponent<T extends GatewayWithID> {
|
||||
readonly gateways = input<readonly T[] | null>(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<boolean> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user