mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
refactor: domains page
This commit is contained in:
@@ -24,5 +24,5 @@ import { i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
imports: [i18nPipe],
|
||||
})
|
||||
export class TableComponent {
|
||||
readonly appTable = input.required<Array<i18nKey | null>>()
|
||||
readonly appTable = input.required<ReadonlyArray<i18nKey | null>>()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import {
|
||||
TuiButton,
|
||||
TuiDataList,
|
||||
TuiDropdown,
|
||||
TuiTextfield,
|
||||
} from '@taiga-ui/core'
|
||||
import { toAcmeName } from 'src/app/utils/acme'
|
||||
|
||||
import { AcmeService } from './acme.service'
|
||||
|
||||
@Component({
|
||||
selector: 'tr[acme]',
|
||||
template: `
|
||||
<td>{{ toAcmeName(acme().url) }}</td>
|
||||
<td>{{ acme().contact.join(', ') }}</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 size="s" *tuiTextfieldDropdown>
|
||||
<tui-opt-group>
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.pencil"
|
||||
(click)="service.edit(acme())"
|
||||
>
|
||||
{{ 'Edit' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
<tui-opt-group>
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.trash"
|
||||
class="g-negative"
|
||||
(click)="service.remove(acme())"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
</tui-data-list>
|
||||
</button>
|
||||
</td>
|
||||
`,
|
||||
styles: `
|
||||
td:last-child {
|
||||
grid-area: 1 / 2 / 3;
|
||||
align-self: center;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
grid-template-columns: 1fr min-content;
|
||||
|
||||
td:first-child {
|
||||
font: var(--tui-font-text-m);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiButton, i18nPipe, TuiDropdown, TuiDataList, TuiTextfield],
|
||||
})
|
||||
export class DomainsAcmeComponent {
|
||||
protected readonly service = inject(AcmeService)
|
||||
|
||||
readonly acme = input.required<{ url: string; contact: readonly string[] }>()
|
||||
|
||||
open = false
|
||||
|
||||
toAcmeName = toAcmeName
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import {
|
||||
DialogService,
|
||||
ErrorService,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { ISB, utils } from '@start9labs/start-sdk'
|
||||
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 { knownACME } from 'src/app/utils/acme'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AcmeService {
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
private readonly dialog = inject(DialogService)
|
||||
|
||||
async add(providers: { url: string; contact: string[] }[]) {
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Add ACME Provider',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(
|
||||
this.addSpec(providers.map(p => p.url)),
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
text: this.i18n.transform('Save'),
|
||||
handler: async (val: ReturnType<typeof this.addSpec>['_TYPE']) => {
|
||||
const providerUrl =
|
||||
val.provider.selection === 'other'
|
||||
? val.provider.value.url
|
||||
: val.provider.selection
|
||||
|
||||
return this.save(providerUrl, val.contact)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async edit({ url, contact }: { url: string; contact: readonly string[] }) {
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Edit ACME Provider',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(this.editSpec()),
|
||||
buttons: [
|
||||
{
|
||||
text: this.i18n.transform('Save'),
|
||||
handler: async (val: ReturnType<typeof this.editSpec>['_TYPE']) =>
|
||||
this.save(url, val.contact),
|
||||
},
|
||||
],
|
||||
value: { contact },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
remove({ url }: { url: string }) {
|
||||
this.dialog
|
||||
.openConfirm({ label: 'Are you sure?', size: 's' })
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(async () => {
|
||||
const loader = this.loader.open('Removing').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.removeAcme({ provider: url })
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async save(providerUrl: string, contact: readonly string[]) {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.initAcme({
|
||||
provider: new URL(providerUrl).href,
|
||||
contact: contact.map(address => `mailto:${address}`),
|
||||
})
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private addSpec(providers: string[]) {
|
||||
const availableAcme = knownACME.filter(
|
||||
acme => !providers.includes(acme.url),
|
||||
)
|
||||
|
||||
return ISB.InputSpec.of({
|
||||
provider: ISB.Value.union({
|
||||
name: 'Provider',
|
||||
default: (availableAcme[0]?.url as any) || 'other',
|
||||
variants: ISB.Variants.of({
|
||||
...availableAcme.reduce(
|
||||
(obj, curr) => ({
|
||||
...obj,
|
||||
[curr.url]: {
|
||||
name: curr.name,
|
||||
spec: ISB.InputSpec.of({}),
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
other: {
|
||||
name: 'Other',
|
||||
spec: ISB.InputSpec.of({
|
||||
url: ISB.Value.text({
|
||||
name: 'URL',
|
||||
default: null,
|
||||
required: true,
|
||||
inputmode: 'url',
|
||||
patterns: [utils.Patterns.url],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
contact: this.emailListSpec(),
|
||||
})
|
||||
}
|
||||
|
||||
private editSpec() {
|
||||
return ISB.InputSpec.of({
|
||||
contact: this.emailListSpec(),
|
||||
})
|
||||
}
|
||||
|
||||
private emailListSpec() {
|
||||
return ISB.Value.list(
|
||||
ISB.List.text(
|
||||
{
|
||||
name: this.i18n.transform('Contact Emails')!,
|
||||
description: this.i18n.transform(
|
||||
'Needed to obtain a certificate from a Certificate Authority',
|
||||
),
|
||||
minLength: 1,
|
||||
},
|
||||
{
|
||||
inputmode: 'email',
|
||||
patterns: [utils.Patterns.email],
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
DialogService,
|
||||
ErrorService,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { ISB } from '@start9labs/start-sdk'
|
||||
import {
|
||||
TuiButton,
|
||||
TuiDataList,
|
||||
TuiDropdown,
|
||||
TuiTextfield,
|
||||
} from '@taiga-ui/core'
|
||||
import { filter } from 'rxjs'
|
||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
|
||||
@Component({
|
||||
selector: 'tr[domain]',
|
||||
template: `
|
||||
<td>{{ domain().domain }}</td>
|
||||
<td [style.order]="-1">{{ domain().gateway }}</td>
|
||||
<td>{{ domain().acme }}</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 size="s" *tuiTextfieldDropdown>
|
||||
<tui-opt-group>
|
||||
<button tuiOption new iconStart="@tui.pencil" (click)="edit()">
|
||||
{{ 'Edit' | i18n }}
|
||||
</button>
|
||||
<button tuiOption new iconStart="@tui.shield" (click)="showDns()">
|
||||
{{ 'Show DNS' | i18n }}
|
||||
</button>
|
||||
<button tuiOption new iconStart="@tui.shield" (click)="testDns()">
|
||||
{{ 'Test DNS' | 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: `
|
||||
td:last-child {
|
||||
grid-area: 1 / 2 / 4;
|
||||
align-self: center;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
grid-template-columns: 1fr min-content;
|
||||
|
||||
td:first-child {
|
||||
font: var(--tui-font-text-m);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiButton, i18nPipe, TuiDropdown, TuiDataList, TuiTextfield],
|
||||
})
|
||||
export class DomainsDomainComponent {
|
||||
private readonly dialog = inject(DialogService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
|
||||
readonly domain = input.required<any>()
|
||||
|
||||
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 {
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async edit() {
|
||||
const renameSpec = ISB.InputSpec.of({})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Edit',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(renameSpec),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: (value: typeof renameSpec._TYPE) => {},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async showDns() {}
|
||||
|
||||
async testDns() {}
|
||||
}
|
||||
@@ -6,25 +6,15 @@ import {
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import {
|
||||
DocsLinkDirective,
|
||||
ErrorService,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { ISB, utils } from '@start9labs/start-sdk'
|
||||
import { TuiButton, TuiLink, TuiLoader, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiCell, TuiHeader } from '@taiga-ui/layout'
|
||||
import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton, TuiLink, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiHeader } from '@taiga-ui/layout'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs'
|
||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { knownACME, toAcmeName } from 'src/app/utils/acme'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
|
||||
import { AcmeService } from './acme.service'
|
||||
import { DomainsTableComponent } from './table.component'
|
||||
|
||||
@Component({
|
||||
@@ -65,46 +55,13 @@ import { DomainsTableComponent } from './table.component'
|
||||
size="xs"
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="addAcme(value)"
|
||||
(click)="service.add(value)"
|
||||
>
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
}
|
||||
</header>
|
||||
@if (acme(); as value) {
|
||||
@for (provider of value; track $index) {
|
||||
<div tuiCell>
|
||||
<span tuiTitle>
|
||||
<strong>{{ toAcmeName(provider.url) }}</strong>
|
||||
<span tuiSubtitle>
|
||||
{{ 'Contact' | i18n }}: {{ provider.contactString }}
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.pencil"
|
||||
appearance="icon"
|
||||
(click)="editAcme(provider.url, provider.contact)"
|
||||
>
|
||||
{{ 'Edit' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.trash"
|
||||
appearance="icon"
|
||||
(click)="removeAcme(provider.url)"
|
||||
>
|
||||
{{ 'Edit' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
} @empty {
|
||||
<app-placeholder icon="@tui.shield-question">
|
||||
{{ 'No saved providers' | i18n }}
|
||||
</app-placeholder>
|
||||
}
|
||||
} @else {
|
||||
<tui-loader [style.height.rem]="5" />
|
||||
}
|
||||
<domains-table mode="acme" [items]="acme()" />
|
||||
</section>
|
||||
|
||||
<section class="g-card">
|
||||
@@ -113,22 +70,20 @@ import { DomainsTableComponent } from './table.component'
|
||||
<button
|
||||
tuiButton
|
||||
size="xs"
|
||||
[style.margin]="'0 0.5rem 0 auto'"
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="addDomain()"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</header>
|
||||
<div #table [domains]="domains()"></div>
|
||||
<domains-table mode="domains" [items]="domains()" />
|
||||
</section>
|
||||
`,
|
||||
styles: ``,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
TuiButton,
|
||||
TuiLoader,
|
||||
TuiCell,
|
||||
TuiTitle,
|
||||
TuiHeader,
|
||||
TuiLink,
|
||||
@@ -136,179 +91,38 @@ import { DomainsTableComponent } from './table.component'
|
||||
TitleDirective,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
PlaceholderComponent,
|
||||
DomainsTableComponent,
|
||||
],
|
||||
})
|
||||
export default class SystemDomainsComponent {
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
protected readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
protected readonly service = inject(AcmeService)
|
||||
|
||||
acme = toSignal(
|
||||
readonly acme = toSignal(
|
||||
this.patch.watch$('serverInfo', 'network', 'acme').pipe(
|
||||
map(acme =>
|
||||
Object.keys(acme).map(url => {
|
||||
const contact =
|
||||
Object.keys(acme).map(url => ({
|
||||
url,
|
||||
contact:
|
||||
acme[url]?.contact.map(mailto => mailto.replace('mailto:', '')) ||
|
||||
[]
|
||||
return {
|
||||
url,
|
||||
contact,
|
||||
contactString: contact.join(', '),
|
||||
}
|
||||
}),
|
||||
[],
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
domains = signal([])
|
||||
|
||||
toAcmeName = toAcmeName
|
||||
|
||||
async addAcme(
|
||||
providers: {
|
||||
url: string
|
||||
contact: string[]
|
||||
contactString: string
|
||||
}[],
|
||||
) {
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Add ACME Provider',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(
|
||||
this.addAcmeSpec(providers.map(p => p.url)),
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
text: this.i18n.transform('Save'),
|
||||
handler: async (
|
||||
val: ReturnType<typeof this.addAcmeSpec>['_TYPE'],
|
||||
) => {
|
||||
const providerUrl =
|
||||
val.provider.selection === 'other'
|
||||
? val.provider.value.url
|
||||
: val.provider.selection
|
||||
|
||||
return this.saveAcme(providerUrl, val.contact)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
readonly domains = signal([
|
||||
{
|
||||
domain: 'blog.mydomain.com',
|
||||
gateway: 'StartTunnel',
|
||||
acme: 'System',
|
||||
},
|
||||
{
|
||||
domain: 'blog. mydomain.com',
|
||||
gateway: 'StartTunnel',
|
||||
acme: 'System',
|
||||
},
|
||||
])
|
||||
|
||||
async addDomain() {}
|
||||
|
||||
async editAcme(provider: string, contact: string[]) {
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Edit ACME Provider',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(this.editAcmeSpec()),
|
||||
buttons: [
|
||||
{
|
||||
text: this.i18n.transform('Save'),
|
||||
handler: async (
|
||||
val: ReturnType<typeof this.editAcmeSpec>['_TYPE'],
|
||||
) => this.saveAcme(provider, val.contact),
|
||||
},
|
||||
],
|
||||
value: { contact },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async removeAcme(provider: string) {
|
||||
const loader = this.loader.open('Removing').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.removeAcme({ provider })
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async saveAcme(providerUrl: string, contact: string[]) {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.initAcme({
|
||||
provider: new URL(providerUrl).href,
|
||||
contact: contact.map(address => `mailto:${address}`),
|
||||
})
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private addAcmeSpec(providers: string[]) {
|
||||
const availableAcme = knownACME.filter(
|
||||
acme => !providers.includes(acme.url),
|
||||
)
|
||||
|
||||
return ISB.InputSpec.of({
|
||||
provider: ISB.Value.union({
|
||||
name: 'Provider',
|
||||
default: (availableAcme[0]?.url as any) || 'other',
|
||||
variants: ISB.Variants.of({
|
||||
...availableAcme.reduce(
|
||||
(obj, curr) => ({
|
||||
...obj,
|
||||
[curr.url]: {
|
||||
name: curr.name,
|
||||
spec: ISB.InputSpec.of({}),
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
other: {
|
||||
name: 'Other',
|
||||
spec: ISB.InputSpec.of({
|
||||
url: ISB.Value.text({
|
||||
name: 'URL',
|
||||
default: null,
|
||||
required: true,
|
||||
inputmode: 'url',
|
||||
patterns: [utils.Patterns.url],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
contact: this.emailListSpec(),
|
||||
})
|
||||
}
|
||||
|
||||
private editAcmeSpec() {
|
||||
return ISB.InputSpec.of({
|
||||
contact: this.emailListSpec(),
|
||||
})
|
||||
}
|
||||
|
||||
private emailListSpec() {
|
||||
return ISB.Value.list(
|
||||
ISB.List.text(
|
||||
{
|
||||
name: this.i18n.transform('Contact Emails')!,
|
||||
description: this.i18n.transform(
|
||||
'Needed to obtain a certificate from a Certificate Authority',
|
||||
),
|
||||
minLength: 1,
|
||||
},
|
||||
{
|
||||
inputmode: 'email',
|
||||
patterns: [utils.Patterns.email],
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
input,
|
||||
output,
|
||||
} from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import {
|
||||
TuiButton,
|
||||
TuiDataList,
|
||||
TuiDropdown,
|
||||
TuiOptGroup,
|
||||
} from '@taiga-ui/core'
|
||||
|
||||
@Component({
|
||||
selector: 'tr[domain]',
|
||||
template: `
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.ellipsis"
|
||||
appearance="icon"
|
||||
[tuiDropdown]="content"
|
||||
[(tuiDropdownOpen)]="open"
|
||||
[tuiDropdownMaxHeight]="9999"
|
||||
></button>
|
||||
<ng-template #content>
|
||||
<tui-data-list [style.width.rem]="13">
|
||||
<tui-opt-group>
|
||||
<button
|
||||
tuiOption
|
||||
iconStart="@tui.pencil"
|
||||
(click)="onEdit.emit(domain())"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
tuiOption
|
||||
iconStart="@tui.shield"
|
||||
(click)="onShowDns.emit(domain())"
|
||||
>
|
||||
Show DNS
|
||||
</button>
|
||||
<button
|
||||
tuiOption
|
||||
iconStart="@tui.shield"
|
||||
(click)="onTestDns.emit(domain())"
|
||||
>
|
||||
Test DNS
|
||||
</button>
|
||||
<button
|
||||
tuiOption
|
||||
appearance="negative"
|
||||
iconStart="@tui.trash-2"
|
||||
(click)="onRemove.emit(domain())"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
</tui-data-list>
|
||||
</ng-template>
|
||||
</td>
|
||||
`,
|
||||
styles: `
|
||||
td:last-child {
|
||||
grid-area: 3 / span 4;
|
||||
white-space: nowrap;
|
||||
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;
|
||||
|
||||
td {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiButton, i18nPipe, TuiDropdown, TuiDataList, TuiOptGroup],
|
||||
})
|
||||
export class DomainsItemComponent {
|
||||
readonly domain = input.required<any>()
|
||||
|
||||
onEdit = output<any>()
|
||||
onShowDns = output<any>()
|
||||
onTestDns = output<any>()
|
||||
onRemove = output<any>()
|
||||
|
||||
open = false
|
||||
}
|
||||
@@ -1,110 +1,68 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
computed,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
DialogService,
|
||||
ErrorService,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { ISB } from '@start9labs/start-sdk'
|
||||
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 { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { DomainsItemComponent } from './item.component'
|
||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
|
||||
import { DomainsAcmeComponent } from './acme.component'
|
||||
import { DomainsDomainComponent } from './domain.component'
|
||||
|
||||
@Component({
|
||||
selector: '[domains]',
|
||||
selector: 'domains-table',
|
||||
template: `
|
||||
<table [appTable]="['Domain', 'Gateway', 'Default ACME', null]">
|
||||
@for (domain of domains(); track $index) {
|
||||
<tr
|
||||
[domain]="domain"
|
||||
(onEdit)="edit($event)"
|
||||
(onShowDns)="showDns($event)"
|
||||
(onTestDns)="testDns($event)"
|
||||
(onRemove)="remove($event)"
|
||||
></tr>
|
||||
} @empty {
|
||||
@if (domains()) {
|
||||
<app-placeholder icon="@tui.award">
|
||||
{{ 'No domains' | i18n }}
|
||||
</app-placeholder>
|
||||
} @else {
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<table [appTable]="titles()">
|
||||
@for (item of items(); track $index) {
|
||||
@if (mode() === 'domains') {
|
||||
<tr [domain]="item"></tr>
|
||||
} @else if (mode() === 'acme') {
|
||||
<tr [acme]="item"></tr>
|
||||
}
|
||||
} @empty {
|
||||
<tr>
|
||||
<td [attr.colspan]="titles().length">
|
||||
@if (items()) {
|
||||
<app-placeholder icon="@tui.globe">
|
||||
@if (mode() === 'domains') {
|
||||
{{ 'No domains' | i18n }}
|
||||
} @else {
|
||||
{{ 'No saved providers' | i18n }}
|
||||
}
|
||||
</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,
|
||||
DomainsItemComponent,
|
||||
PlaceholderComponent,
|
||||
DomainsDomainComponent,
|
||||
DomainsAcmeComponent,
|
||||
],
|
||||
})
|
||||
export class DomainsTableComponent<T extends any> {
|
||||
readonly domains = input<readonly T[] | null>(null)
|
||||
export class DomainsTableComponent {
|
||||
// @TODO Alex proper types
|
||||
readonly items = input<readonly any[] | null>()
|
||||
readonly mode = input<'domains' | 'acme'>('domains')
|
||||
|
||||
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 titles = computed(() =>
|
||||
this.mode() === 'domains'
|
||||
? (['Domain', 'Gateway', 'Default ACME', null] as const)
|
||||
: (['Provider', 'Contact', null] as const),
|
||||
)
|
||||
|
||||
remove(domain: any) {
|
||||
this.dialog
|
||||
.openConfirm({ label: 'Are you sure?', size: 's' })
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(async () => {
|
||||
const loader = this.loader.open('Deleting').subscribe()
|
||||
|
||||
try {
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async edit(domain: any) {
|
||||
const renameSpec = ISB.InputSpec.of({})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Edit',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(renameSpec),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: (value: typeof renameSpec._TYPE) => {},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async showDns(domain: any) {}
|
||||
|
||||
async testDns(domain: any) {}
|
||||
readonly icon = computed(() =>
|
||||
this.mode() === 'domains' ? '@tui.globe' : '@tui.shield-question',
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,14 +40,14 @@ export const SYSTEM_MENU = [
|
||||
[
|
||||
{
|
||||
icon: '@tui.globe',
|
||||
item: 'Gateways',
|
||||
link: 'gateways',
|
||||
},
|
||||
{
|
||||
icon: '@tui.award',
|
||||
item: 'Domains',
|
||||
link: 'domains',
|
||||
},
|
||||
{
|
||||
icon: '@tui.door-open',
|
||||
item: 'Gateways',
|
||||
link: 'gateways',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user