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 { appRouter } from '../../router.js'; declare global { interface HTMLElementTagNameMap { 'ops-view-domains': OpsViewDomains; } } @customElement('ops-view-domains') export class OpsViewDomains 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` .domainsContainer { display: flex; flex-direction: column; gap: 24px; } .sourceBadge { display: inline-flex; align-items: center; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; } .sourceBadge.manual { background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')}; color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')}; } .sourceBadge.provider { background: ${cssManager.bdTheme('#fef3c7', '#451a03')}; color: ${cssManager.bdTheme('#92400e', '#fde047')}; } `, ]; public render(): TemplateResult { const domains = this.domainsState.domains; const providersById = new Map(this.domainsState.providers.map((p) => [p.id, p])); return html` Domains
({ Name: d.name, Source: this.renderSourceBadge(d, providersById), Authoritative: d.authoritative ? 'yes' : 'no', Nameservers: d.nameservers?.join(', ') || '-', 'Last Synced': d.lastSyncedAt ? new Date(d.lastSyncedAt).toLocaleString() : '-', })} .dataActions=${[ { name: 'Add Manual Domain', iconName: 'lucide:plus', type: ['header' as const], actionFunc: async () => { await this.showCreateManualDialog(); }, }, { name: 'Import from Provider', iconName: 'lucide:download', type: ['header' as const], actionFunc: async () => { await this.showImportDialog(); }, }, { name: 'Refresh', iconName: 'lucide:rotateCw', type: ['header' as const], actionFunc: async () => { await appstate.domainsStatePart.dispatchAction( appstate.fetchDomainsAndProvidersAction, null, ); }, }, { name: 'View Records', iconName: 'lucide:list', type: ['inRow', 'contextmenu'] as any, actionFunc: async (actionData: any) => { const domain = actionData.item as interfaces.data.IDomain; await appstate.domainsStatePart.dispatchAction( appstate.fetchDnsRecordsForDomainAction, { domainId: domain.id }, ); appRouter.navigateToView('domains', 'dns'); }, }, { name: 'Sync Now', iconName: 'lucide:rotateCw', type: ['inRow', 'contextmenu'] as any, actionFunc: async (actionData: any) => { const domain = actionData.item as interfaces.data.IDomain; if (domain.source !== 'provider') { const { DeesToast } = await import('@design.estate/dees-catalog'); DeesToast.show({ message: 'Sync only applies to provider-managed domains', type: 'warning', duration: 3000, }); return; } await appstate.domainsStatePart.dispatchAction(appstate.syncDomainAction, { id: domain.id, }); }, }, { name: 'Delete', iconName: 'lucide:trash2', type: ['inRow', 'contextmenu'] as any, actionFunc: async (actionData: any) => { const domain = actionData.item as interfaces.data.IDomain; await this.deleteDomain(domain); }, }, ]} >
`; } private renderSourceBadge( d: interfaces.data.IDomain, providersById: Map, ): TemplateResult { if (d.source === 'manual') { return html`Manual`; } const provider = d.providerId ? providersById.get(d.providerId) : undefined; return html`${provider?.name || 'Provider'}`; } private async showCreateManualDialog() { const { DeesModal } = await import('@design.estate/dees-catalog'); DeesModal.createAndShow({ heading: 'Add Manual Domain', content: html`

dcrouter will become the authoritative DNS server for this domain. You'll need to delegate the domain's nameservers to dcrouter to make this effective.

`, 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.createManualDomainAction, { name: String(data.name), description: data.description ? String(data.description) : undefined, }); modalArg.destroy(); }, }, ], }); } private async showImportDialog() { const providers = this.domainsState.providers; if (providers.length === 0) { const { DeesToast } = await import('@design.estate/dees-catalog'); DeesToast.show({ message: 'Add a DNS provider first (Domains > Providers)', type: 'warning', duration: 3500, }); return; } const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog'); DeesModal.createAndShow({ heading: 'Import Domains from Provider', content: html` ({ option: p.name, key: p.id }))} .required=${true} >

Tip: use "List Provider Domains" to see what's available before typing.

`, menuOptions: [ { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() }, { name: 'List Provider Domains', action: async (_modalArg: any) => { const form = _modalArg.shadowRoot ?.querySelector('.content') ?.querySelector('dees-form'); if (!form) return; const data = await form.collectFormData(); const providerKey = data.providerId?.key ?? data.providerId; if (!providerKey) { DeesToast.show({ message: 'Pick a provider first', type: 'warning', duration: 2500 }); return; } const result = await appstate.fetchProviderDomains(String(providerKey)); if (!result.success) { DeesToast.show({ message: result.message || 'Failed to fetch domains', type: 'error', duration: 4000, }); return; } const list = (result.domains ?? []).map((d) => d.name).join(', '); DeesToast.show({ message: `Provider has: ${list || '(none)'}`, type: 'info', duration: 8000, }); }, }, { name: 'Import', action: async (modalArg: any) => { const form = modalArg.shadowRoot ?.querySelector('.content') ?.querySelector('dees-form'); if (!form) return; const data = await form.collectFormData(); const providerKey = data.providerId?.key ?? data.providerId; if (!providerKey) { DeesToast.show({ message: 'Pick a provider', type: 'warning', duration: 2500 }); return; } const names = String(data.domainNames || '') .split(',') .map((s) => s.trim()) .filter(Boolean); if (names.length === 0) { DeesToast.show({ message: 'Enter at least one FQDN', type: 'warning', duration: 2500 }); return; } await appstate.domainsStatePart.dispatchAction( appstate.importDomainsFromProviderAction, { providerId: String(providerKey), domainNames: names }, ); modalArg.destroy(); }, }, ], }); } private async deleteDomain(domain: interfaces.data.IDomain) { const { DeesModal } = await import('@design.estate/dees-catalog'); DeesModal.createAndShow({ heading: `Delete domain ${domain.name}?`, content: html`

${domain.source === 'provider' ? 'This removes the domain and its cached records from dcrouter only. The zone at the provider is NOT touched.' : 'This removes the domain and all of its DNS records from dcrouter. dcrouter will no longer answer queries for this domain after the next restart.'}

`, menuOptions: [ { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() }, { name: 'Delete', action: async (modalArg: any) => { await appstate.domainsStatePart.dispatchAction(appstate.deleteDomainAction, { id: domain.id, }); modalArg.destroy(); }, }, ], }); } }