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-dns': OpsViewDns; } } const RECORD_TYPES: interfaces.data.TDnsRecordType[] = [ 'A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'CAA', ]; @customElement('ops-view-dns') export class OpsViewDns 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); // If a domain is already selected (e.g. via "View Records" navigation), refresh its records const selected = this.domainsState.selectedDomainId; if (selected) { await appstate.domainsStatePart.dispatchAction(appstate.fetchDnsRecordsForDomainAction, { domainId: selected, }); } } public static styles = [ cssManager.defaultStyles, viewHostCss, css` .dnsContainer { display: flex; flex-direction: column; gap: 24px; } .domainPicker { display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: ${cssManager.bdTheme('#f9fafb', '#111827')}; border-radius: 8px; } .sourceBadge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; } .sourceBadge.local { background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')}; color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')}; } .sourceBadge.synced { background: ${cssManager.bdTheme('#fef3c7', '#451a03')}; color: ${cssManager.bdTheme('#92400e', '#fde047')}; } `, ]; public render(): TemplateResult { const domains = this.domainsState.domains; const selectedId = this.domainsState.selectedDomainId; const records = this.domainsState.records; return html` DNS Records
Domain: ({ option: d.name, key: d.id }))} .selectedOption=${selectedId ? { option: domains.find((d) => d.id === selectedId)?.name || '', key: selectedId } : undefined} @selectedOption=${async (e: CustomEvent) => { const id = (e.detail as any)?.key; if (!id) return; await appstate.domainsStatePart.dispatchAction( appstate.fetchDnsRecordsForDomainAction, { domainId: id }, ); }} >
${selectedId ? html` ({ Name: r.name, Type: r.type, Value: r.value, TTL: r.ttl, Source: html`${r.source}`, })} .dataActions=${[ { name: 'Add Record', iconName: 'lucide:plus', type: ['header' as const], actionFunc: async () => { await this.showCreateRecordDialog(selectedId); }, }, { name: 'Refresh', iconName: 'lucide:rotateCw', type: ['header' as const], actionFunc: async () => { await appstate.domainsStatePart.dispatchAction( appstate.fetchDnsRecordsForDomainAction, { domainId: selectedId }, ); }, }, { name: 'Edit', iconName: 'lucide:pencil', type: ['inRow', 'contextmenu'] as any, actionFunc: async (actionData: any) => { const rec = actionData.item as interfaces.data.IDnsRecord; await this.showEditRecordDialog(rec); }, }, { name: 'Delete', iconName: 'lucide:trash2', type: ['inRow', 'contextmenu'] as any, actionFunc: async (actionData: any) => { const rec = actionData.item as interfaces.data.IDnsRecord; await appstate.domainsStatePart.dispatchAction( appstate.deleteDnsRecordAction, { id: rec.id, domainId: rec.domainId }, ); }, }, ]} > ` : html`

Pick a domain above to view its records.

`}
`; } private domainHint(domainId: string): string { const domain = this.domainsState.domains.find((d) => d.id === domainId); if (!domain) return ''; if (domain.source === 'dcrouter') { return 'Records are served by dcrouter (authoritative).'; } return 'Records are stored at the provider — changes here are pushed via the provider API.'; } private async showCreateRecordDialog(domainId: string) { const { DeesModal } = await import('@design.estate/dees-catalog'); DeesModal.createAndShow({ heading: 'Add DNS Record', content: html` ({ option: t, key: t }))} .required=${true} > `, 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(); const type = (data.type?.key ?? data.type) as interfaces.data.TDnsRecordType; await appstate.domainsStatePart.dispatchAction(appstate.createDnsRecordAction, { domainId, name: String(data.name), type, value: String(data.value), ttl: parseInt(String(data.ttl || '300'), 10), }); modalArg.destroy(); }, }, ], }); } private async showEditRecordDialog(rec: interfaces.data.IDnsRecord) { const { DeesModal } = await import('@design.estate/dees-catalog'); DeesModal.createAndShow({ heading: `Edit ${rec.type} ${rec.name}`, content: html` `, 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(); await appstate.domainsStatePart.dispatchAction(appstate.updateDnsRecordAction, { id: rec.id, domainId: rec.domainId, name: String(data.name), value: String(data.value), ttl: parseInt(String(data.ttl || '300'), 10), }); modalArg.destroy(); }, }, ], }); } }