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

274 lines
9.1 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-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`
<dees-heading level="3">DNS Records</dees-heading>
<div class="dnsContainer">
<div class="domainPicker">
<span>Domain:</span>
<dees-input-dropdown
.options=${domains.map((d) => ({ 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 },
);
}}
></dees-input-dropdown>
</div>
${selectedId
? html`
<dees-table
.heading1=${'DNS Records'}
.heading2=${this.domainHint(selectedId)}
.data=${records}
.showColumnFilters=${true}
.displayFunction=${(r: interfaces.data.IDnsRecord) => ({
Name: r.name,
Type: r.type,
Value: r.value,
TTL: r.ttl,
Source: html`<span class="sourceBadge ${r.source}">${r.source}</span>`,
})}
.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 },
);
},
},
]}
></dees-table>
`
: html`<p style="opacity: 0.7;">Pick a domain above to view its records.</p>`}
</div>
`;
}
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`
<dees-form>
<dees-input-text .key=${'name'} .label=${'Name (FQDN)'} .required=${true}></dees-input-text>
<dees-input-dropdown
.key=${'type'}
.label=${'Type'}
.options=${RECORD_TYPES.map((t) => ({ option: t, key: t }))}
.required=${true}
></dees-input-dropdown>
<dees-input-text
.key=${'value'}
.label=${'Value (for MX use "10 mail.example.com")'}
.required=${true}
></dees-input-text>
<dees-input-text .key=${'ttl'} .label=${'TTL (seconds)'} .value=${'300'}></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();
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`
<dees-form>
<dees-input-text .key=${'name'} .label=${'Name (FQDN)'} .value=${rec.name}></dees-input-text>
<dees-input-text .key=${'value'} .label=${'Value'} .value=${rec.value}></dees-input-text>
<dees-input-text .key=${'ttl'} .label=${'TTL (seconds)'} .value=${String(rec.ttl)}></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();
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();
},
},
],
});
}
}