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'; import './dns-provider-form.js'; import type { DnsProviderForm } from './dns-provider-form.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` DNS Providers
({ Name: p.name, Type: this.providerTypeLabel(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); }, }, ]} >
`; } private renderStatusBadge(status: interfaces.data.TDnsProviderStatus): TemplateResult { return html`${status}`; } private providerTypeLabel(type: interfaces.data.TDnsProviderType): string { return interfaces.data.getDnsProviderTypeDescriptor(type)?.displayName ?? type; } private async showCreateDialog() { const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog'); const formEl = document.createElement('dns-provider-form') as DnsProviderForm; DeesModal.createAndShow({ heading: 'Add DNS Provider', content: html`${formEl}`, menuOptions: [ { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() }, { name: 'Create', action: async (modalArg: any) => { const data = await formEl.collectData(); if (!data) return; if (!data.name) { DeesToast.show({ message: 'Name is required', type: 'warning', duration: 2500 }); return; } if (!data.credentialsTouched) { DeesToast.show({ message: 'Fill in the provider credentials', type: 'warning', duration: 2500, }); return; } await appstate.domainsStatePart.dispatchAction(appstate.createDnsProviderAction, { name: data.name, type: data.type, credentials: data.credentials, }); modalArg.destroy(); }, }, ], }); } private async showEditDialog(provider: interfaces.data.IDnsProviderPublic) { const { DeesModal } = await import('@design.estate/dees-catalog'); const formEl = document.createElement('dns-provider-form') as DnsProviderForm; formEl.providerName = provider.name; formEl.selectedType = provider.type; formEl.lockType = true; formEl.credentialsHint = 'Leave credential fields blank to keep the current values. Fill them to rotate.'; DeesModal.createAndShow({ heading: `Edit Provider: ${provider.name}`, content: html`${formEl}`, menuOptions: [ { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() }, { name: 'Save', action: async (modalArg: any) => { const data = await formEl.collectData(); if (!data) return; await appstate.domainsStatePart.dispatchAction(appstate.updateDnsProviderAction, { id: provider.id, name: data.name || provider.name, // Only send credentials if the user actually entered something — // otherwise we keep the current secret untouched. credentials: data.credentialsTouched ? data.credentials : 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`

Provider ${provider.name} 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).

`, 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); } } }