Files
cloudly/ts_web/elements/cloudly-view-domains.ts
Juergen Kunz 766191899c feat(dns): Implement DNS management functionality
- Added DnsManager and DnsEntry classes to handle DNS entries.
- Introduced new interfaces for DNS entry requests and data structures.
- Updated Cloudly class to include DnsManager instance.
- Enhanced app state to manage DNS entries and actions for creating, updating, and deleting DNS records.
- Created UI components for DNS management, including forms for adding and editing DNS entries.
- Updated overview and services views to reflect DNS entries.
- Added validation and formatting methods for DNS entries.
2025-09-09 15:08:28 +00:00

529 lines
21 KiB
TypeScript

import * as plugins from '../plugins.js';
import * as shared from '../elements/shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-domains')
export class CloudlyViewDomains extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
domains: [],
dnsEntries: [],
};
constructor() {
super();
const subscription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subscription);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.85em;
font-weight: 500;
color: white;
}
.status-active { background: #4CAF50; }
.status-pending { background: #FF9800; }
.status-expired { background: #f44336; }
.status-suspended { background: #9E9E9E; }
.status-transferred { background: #607D8B; }
.verification-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.85em;
font-weight: 500;
}
.verification-verified { background: #4CAF50; color: white; }
.verification-pending { background: #FF9800; color: white; }
.verification-failed { background: #f44336; color: white; }
.verification-not_required { background: #E0E0E0; color: #333; }
.ssl-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.8em;
}
.ssl-active { color: #4CAF50; }
.ssl-pending { color: #FF9800; }
.ssl-expired { color: #f44336; }
.ssl-none { color: #9E9E9E; }
.nameserver-list {
font-size: 0.85em;
color: #666;
}
.expiry-warning {
color: #FF9800;
font-weight: 500;
}
.expiry-critical {
color: #f44336;
font-weight: bold;
}
`,
];
private getStatusBadge(status: string) {
return html`<span class="status-badge status-${status}">${status.toUpperCase()}</span>`;
}
private getVerificationBadge(status: string) {
const displayText = status === 'not_required' ? 'Not Required' : status.replace('_', ' ').toUpperCase();
return html`<span class="verification-badge verification-${status}">${displayText}</span>`;
}
private getSslBadge(sslStatus?: string) {
if (!sslStatus) return html`<span class="ssl-badge ssl-none">—</span>`;
const icon = sslStatus === 'active' ? '🔒' : sslStatus === 'expired' ? '⚠️' : '🔓';
return html`<span class="ssl-badge ssl-${sslStatus}">${icon} ${sslStatus.toUpperCase()}</span>`;
}
private formatDate(timestamp?: number) {
if (!timestamp) return '—';
const date = new Date(timestamp);
return date.toLocaleDateString();
}
private getDaysUntilExpiry(expiresAt?: number) {
if (!expiresAt) return null;
const days = Math.floor((expiresAt - Date.now()) / (1000 * 60 * 60 * 24));
return days;
}
private getExpiryDisplay(expiresAt?: number) {
const days = this.getDaysUntilExpiry(expiresAt);
if (days === null) return '—';
if (days < 0) {
return html`<span class="expiry-critical">Expired ${Math.abs(days)} days ago</span>`;
} else if (days <= 30) {
return html`<span class="expiry-warning">Expires in ${days} days</span>`;
} else {
return `${days} days`;
}
}
public render() {
return html`
<cloudly-sectionheading>Domain Management</cloudly-sectionheading>
<dees-table
.heading1=${'Domains'}
.heading2=${'Manage your domains and DNS zones'}
.data=${this.data.domains || []}
.displayFunction=${(itemArg: plugins.interfaces.data.IDomain) => {
const dnsCount = this.data.dnsEntries?.filter(dns => dns.data.zone === itemArg.data.name).length || 0;
return {
Domain: html`
<div>
<div style="font-weight: 500;">${itemArg.data.name}</div>
${itemArg.data.description ? html`<div style="font-size: 0.85em; color: #666; margin-top: 2px;">${itemArg.data.description}</div>` : ''}
</div>
`,
Status: this.getStatusBadge(itemArg.data.status),
Verification: this.getVerificationBadge(itemArg.data.verificationStatus),
SSL: this.getSslBadge(itemArg.data.sslStatus),
'DNS Records': dnsCount,
Registrar: itemArg.data.registrar?.name || '—',
Expires: this.getExpiryDisplay(itemArg.data.expiresAt),
'Auto-Renew': itemArg.data.autoRenew ? '✓' : '✗',
Nameservers: html`<div class="nameserver-list">${itemArg.data.nameservers?.join(', ') || '—'}</div>`,
};
}}
.dataActions=${[
{
name: 'Add Domain',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add Domain',
content: html`
<dees-form>
<dees-input-text
.key=${'name'}
.label=${'Domain Name'}
.placeholder=${'example.com'}
.required=${true}>
</dees-input-text>
<dees-input-text
.key=${'description'}
.label=${'Description'}
.placeholder=${'Main company domain'}>
</dees-input-text>
<dees-input-dropdown
.key=${'status'}
.label=${'Status'}
.options=${[
{key: 'active', option: 'Active'},
{key: 'pending', option: 'Pending'},
{key: 'expired', option: 'Expired'},
{key: 'suspended', option: 'Suspended'},
]}
.value=${'pending'}
.required=${true}>
</dees-input-dropdown>
<dees-input-text
.key=${'nameservers'}
.label=${'Nameservers (comma separated)'}
.placeholder=${'ns1.example.com, ns2.example.com'}>
</dees-input-text>
<dees-input-text
.key=${'registrarName'}
.label=${'Registrar Name'}
.placeholder=${'GoDaddy, Namecheap, etc.'}>
</dees-input-text>
<dees-input-text
.key=${'registrarUrl'}
.label=${'Registrar URL'}
.placeholder=${'https://registrar.com'}>
</dees-input-text>
<dees-input-text
.key=${'expiresAt'}
.label=${'Expiration Date'}
.type=${'date'}>
</dees-input-text>
<dees-input-checkbox
.key=${'autoRenew'}
.label=${'Auto-Renew Enabled'}
.value=${true}>
</dees-input-checkbox>
<dees-input-checkbox
.key=${'dnssecEnabled'}
.label=${'DNSSEC Enabled'}
.value=${false}>
</dees-input-checkbox>
<dees-input-checkbox
.key=${'isPrimary'}
.label=${'Primary Domain'}
.value=${false}>
</dees-input-checkbox>
<dees-input-text
.key=${'tags'}
.label=${'Tags (comma separated)'}
.placeholder=${'production, critical'}>
</dees-input-text>
</dees-form>
`,
menuOptions: [
{
name: 'Create Domain',
action: async (modalArg) => {
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
const formData = await form.gatherData();
const nameservers = formData.nameservers
? formData.nameservers.split(',').map((ns: string) => ns.trim()).filter((ns: string) => ns)
: [];
const tags = formData.tags
? formData.tags.split(',').map((tag: string) => tag.trim()).filter((tag: string) => tag)
: [];
await appstate.dataState.dispatchAction(appstate.createDomainAction, {
domainData: {
name: formData.name,
description: formData.description || undefined,
status: formData.status,
verificationStatus: 'pending',
nameservers,
registrar: formData.registrarName ? {
name: formData.registrarName,
url: formData.registrarUrl || undefined,
} : undefined,
expiresAt: formData.expiresAt ? new Date(formData.expiresAt).getTime() : undefined,
autoRenew: formData.autoRenew,
dnssecEnabled: formData.dnssecEnabled,
isPrimary: formData.isPrimary,
tags: tags.length > 0 ? tags : undefined,
},
});
await modalArg.destroy();
},
},
{
name: 'Cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'Edit',
iconName: 'edit',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: `Edit Domain: ${domain.data.name}`,
content: html`
<dees-form>
<dees-input-text
.key=${'name'}
.label=${'Domain Name'}
.value=${domain.data.name}
.required=${true}>
</dees-input-text>
<dees-input-text
.key=${'description'}
.label=${'Description'}
.value=${domain.data.description || ''}>
</dees-input-text>
<dees-input-dropdown
.key=${'status'}
.label=${'Status'}
.options=${[
{key: 'active', option: 'Active'},
{key: 'pending', option: 'Pending'},
{key: 'expired', option: 'Expired'},
{key: 'suspended', option: 'Suspended'},
{key: 'transferred', option: 'Transferred'},
]}
.value=${domain.data.status}
.required=${true}>
</dees-input-dropdown>
<dees-input-text
.key=${'nameservers'}
.label=${'Nameservers (comma separated)'}
.value=${domain.data.nameservers?.join(', ') || ''}>
</dees-input-text>
<dees-input-text
.key=${'registrarName'}
.label=${'Registrar Name'}
.value=${domain.data.registrar?.name || ''}>
</dees-input-text>
<dees-input-text
.key=${'registrarUrl'}
.label=${'Registrar URL'}
.value=${domain.data.registrar?.url || ''}>
</dees-input-text>
<dees-input-text
.key=${'expiresAt'}
.label=${'Expiration Date'}
.type=${'date'}
.value=${domain.data.expiresAt ? new Date(domain.data.expiresAt).toISOString().split('T')[0] : ''}>
</dees-input-text>
<dees-input-checkbox
.key=${'autoRenew'}
.label=${'Auto-Renew Enabled'}
.value=${domain.data.autoRenew}>
</dees-input-checkbox>
<dees-input-checkbox
.key=${'dnssecEnabled'}
.label=${'DNSSEC Enabled'}
.value=${domain.data.dnssecEnabled || false}>
</dees-input-checkbox>
<dees-input-checkbox
.key=${'isPrimary'}
.label=${'Primary Domain'}
.value=${domain.data.isPrimary || false}>
</dees-input-checkbox>
<dees-input-text
.key=${'tags'}
.label=${'Tags (comma separated)'}
.value=${domain.data.tags?.join(', ') || ''}>
</dees-input-text>
</dees-form>
`,
menuOptions: [
{
name: 'Update Domain',
action: async (modalArg) => {
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
const formData = await form.gatherData();
const nameservers = formData.nameservers
? formData.nameservers.split(',').map((ns: string) => ns.trim()).filter((ns: string) => ns)
: [];
const tags = formData.tags
? formData.tags.split(',').map((tag: string) => tag.trim()).filter((tag: string) => tag)
: [];
await appstate.dataState.dispatchAction(appstate.updateDomainAction, {
domainId: domain.id,
domainData: {
...domain.data,
name: formData.name,
description: formData.description || undefined,
status: formData.status,
nameservers,
registrar: formData.registrarName ? {
name: formData.registrarName,
url: formData.registrarUrl || undefined,
} : undefined,
expiresAt: formData.expiresAt ? new Date(formData.expiresAt).getTime() : undefined,
autoRenew: formData.autoRenew,
dnssecEnabled: formData.dnssecEnabled,
isPrimary: formData.isPrimary,
tags: tags.length > 0 ? tags : undefined,
},
});
await modalArg.destroy();
},
},
{
name: 'Cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'Verify',
iconName: 'check-circle',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: `Verify Domain: ${domain.data.name}`,
content: html`
<div style="text-align:center; padding: 20px;">
<p>Choose a verification method for <strong>${domain.data.name}</strong></p>
<dees-form>
<dees-input-dropdown
.key=${'method'}
.label=${'Verification Method'}
.options=${[
{key: 'dns', option: 'DNS TXT Record'},
{key: 'http', option: 'HTTP File Upload'},
{key: 'email', option: 'Email Verification'},
{key: 'manual', option: 'Manual Verification'},
]}
.value=${'dns'}
.required=${true}>
</dees-input-dropdown>
</dees-form>
${domain.data.verificationToken ? html`
<div style="margin-top: 20px; padding: 15px; background: #333; border-radius: 8px;">
<div style="color: #aaa; font-size: 0.9em;">Verification Token:</div>
<code style="color: #4CAF50; word-break: break-all;">${domain.data.verificationToken}</code>
</div>
` : ''}
</div>
`,
menuOptions: [
{
name: 'Start Verification',
action: async (modalArg) => {
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
const formData = await form.gatherData();
await appstate.dataState.dispatchAction(appstate.verifyDomainAction, {
domainId: domain.id,
verificationMethod: formData.method,
});
await modalArg.destroy();
},
},
{
name: 'Cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'View DNS Records',
iconName: 'list',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
// Navigate to DNS view with filter for this domain
// TODO: Implement navigation with filter
console.log('View DNS records for domain:', domain.data.name);
},
},
{
name: 'Delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
const dnsCount = this.data.dnsEntries?.filter(dns => dns.data.zone === domain.data.name).length || 0;
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete Domain`,
content: html`
<div style="text-align:center">
Are you sure you want to delete this domain?
</div>
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
<div style="color: #fff; font-weight: bold; font-size: 1.1em;">
${domain.data.name}
</div>
${domain.data.description ? html`
<div style="color: #aaa; margin-top: 4px;">
${domain.data.description}
</div>
` : ''}
${dnsCount > 0 ? html`
<div style="color: #f44336; margin-top: 12px; padding: 8px; background: #1a1a1a; border-radius: 4px;">
⚠️ This domain has ${dnsCount} DNS record${dnsCount > 1 ? 's' : ''} that will also be deleted
</div>
` : ''}
</div>
`,
menuOptions: [
{
name: 'Cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'Delete',
action: async (modalArg) => {
await appstate.dataState.dispatchAction(appstate.deleteDomainAction, {
domainId: domain.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}