Files
dcrouter/ts_web/elements/domains/ops-view-providers.ts

284 lines
9.0 KiB
TypeScript
Raw Normal View History

import {
DeesElement,
html,
customElement,
type TemplateResult,
css,
state,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../../appstate.js';
import * as interfaces from '../../../dist_ts_interfaces/index.js';
import { viewHostCss } from '../shared/css.js';
declare global {
interface HTMLElementTagNameMap {
'ops-view-providers': OpsViewProviders;
}
}
@customElement('ops-view-providers')
export class OpsViewProviders extends DeesElement {
@state()
accessor domainsState: appstate.IDomainsState = appstate.domainsStatePart.getState()!;
constructor() {
super();
const sub = appstate.domainsStatePart.select().subscribe((newState) => {
this.domainsState = newState;
});
this.rxSubscriptions.push(sub);
}
async connectedCallback() {
await super.connectedCallback();
await appstate.domainsStatePart.dispatchAction(appstate.fetchDomainsAndProvidersAction, null);
}
public static styles = [
cssManager.defaultStyles,
viewHostCss,
css`
.providersContainer {
display: flex;
flex-direction: column;
gap: 24px;
}
.statusBadge {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.statusBadge.ok {
background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
color: ${cssManager.bdTheme('#166534', '#4ade80')};
}
.statusBadge.error {
background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
color: ${cssManager.bdTheme('#991b1b', '#f87171')};
}
.statusBadge.untested {
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
color: ${cssManager.bdTheme('#4b5563', '#9ca3af')};
}
`,
];
public render(): TemplateResult {
const providers = this.domainsState.providers;
return html`
<dees-heading level="3">DNS Providers</dees-heading>
<div class="providersContainer">
<dees-table
.heading1=${'Providers'}
.heading2=${'External DNS provider accounts (Cloudflare, etc.)'}
.data=${providers}
.showColumnFilters=${true}
.displayFunction=${(p: interfaces.data.IDnsProviderPublic) => ({
Name: p.name,
Type: p.type,
Status: this.renderStatusBadge(p.status),
'Last Tested': p.lastTestedAt ? new Date(p.lastTestedAt).toLocaleString() : 'never',
Error: p.lastError || '-',
})}
.dataActions=${[
{
name: 'Add Provider',
iconName: 'lucide:plus',
type: ['header' as const],
actionFunc: async () => {
await this.showCreateDialog();
},
},
{
name: 'Refresh',
iconName: 'lucide:rotateCw',
type: ['header' as const],
actionFunc: async () => {
await appstate.domainsStatePart.dispatchAction(
appstate.fetchDomainsAndProvidersAction,
null,
);
},
},
{
name: 'Test Connection',
iconName: 'lucide:plug',
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
const provider = actionData.item as interfaces.data.IDnsProviderPublic;
await this.testProvider(provider);
},
},
{
name: 'Edit',
iconName: 'lucide:pencil',
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
const provider = actionData.item as interfaces.data.IDnsProviderPublic;
await this.showEditDialog(provider);
},
},
{
name: 'Delete',
iconName: 'lucide:trash2',
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
const provider = actionData.item as interfaces.data.IDnsProviderPublic;
await this.deleteProvider(provider);
},
},
]}
></dees-table>
</div>
`;
}
private renderStatusBadge(status: interfaces.data.TDnsProviderStatus): TemplateResult {
return html`<span class="statusBadge ${status}">${status}</span>`;
}
private async showCreateDialog() {
const { DeesModal } = await import('@design.estate/dees-catalog');
DeesModal.createAndShow({
heading: 'Add DNS Provider',
content: html`
<dees-form>
<dees-input-text .key=${'name'} .label=${'Provider name'} .required=${true}></dees-input-text>
<dees-input-text
.key=${'apiToken'}
.label=${'Cloudflare API token'}
.required=${true}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
{
name: 'Create',
action: async (modalArg: any) => {
const form = modalArg.shadowRoot
?.querySelector('.content')
?.querySelector('dees-form');
if (!form) return;
const data = await form.collectFormData();
await appstate.domainsStatePart.dispatchAction(appstate.createDnsProviderAction, {
name: String(data.name),
type: 'cloudflare',
credentials: { type: 'cloudflare', apiToken: String(data.apiToken) },
});
modalArg.destroy();
},
},
],
});
}
private async showEditDialog(provider: interfaces.data.IDnsProviderPublic) {
const { DeesModal } = await import('@design.estate/dees-catalog');
DeesModal.createAndShow({
heading: `Edit Provider: ${provider.name}`,
content: html`
<dees-form>
<dees-input-text .key=${'name'} .label=${'Provider name'} .value=${provider.name}></dees-input-text>
<dees-input-text
.key=${'apiToken'}
.label=${'New API token (leave blank to keep current)'}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
{
name: 'Save',
action: async (modalArg: any) => {
const form = modalArg.shadowRoot
?.querySelector('.content')
?.querySelector('dees-form');
if (!form) return;
const data = await form.collectFormData();
const apiToken = data.apiToken ? String(data.apiToken) : '';
await appstate.domainsStatePart.dispatchAction(appstate.updateDnsProviderAction, {
id: provider.id,
name: String(data.name),
credentials: apiToken
? { type: 'cloudflare', apiToken }
: undefined,
});
modalArg.destroy();
},
},
],
});
}
private async testProvider(provider: interfaces.data.IDnsProviderPublic) {
const { DeesToast } = await import('@design.estate/dees-catalog');
await appstate.domainsStatePart.dispatchAction(appstate.testDnsProviderAction, {
id: provider.id,
});
const updated = appstate.domainsStatePart
.getState()!
.providers.find((p) => p.id === provider.id);
if (updated?.status === 'ok') {
DeesToast.show({
message: `${provider.name}: connection OK`,
type: 'success',
duration: 3000,
});
} else {
DeesToast.show({
message: `${provider.name}: ${updated?.lastError || 'connection failed'}`,
type: 'error',
duration: 4000,
});
}
}
private async deleteProvider(provider: interfaces.data.IDnsProviderPublic) {
const linkedDomains = this.domainsState.domains.filter((d) => d.providerId === provider.id);
const { DeesModal } = await import('@design.estate/dees-catalog');
const doDelete = async (force: boolean) => {
await appstate.domainsStatePart.dispatchAction(appstate.deleteDnsProviderAction, {
id: provider.id,
force,
});
};
if (linkedDomains.length > 0) {
DeesModal.createAndShow({
heading: `Provider in use`,
content: html`
<p>
Provider <strong>${provider.name}</strong> is referenced by ${linkedDomains.length}
domain(s). Deleting will also remove the imported domain(s) and their cached
records (the records at ${provider.type} are NOT touched).
</p>
`,
menuOptions: [
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
{
name: 'Force Delete',
action: async (modalArg: any) => {
await doDelete(true);
modalArg.destroy();
},
},
],
});
} else {
await doDelete(false);
}
}
}