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.
This commit is contained in:
@@ -16,6 +16,7 @@ import { CloudlyViewClusters } from './cloudly-view-clusters.js';
|
||||
import { CloudlyViewDbs } from './cloudly-view-dbs.js';
|
||||
import { CloudlyViewDeployments } from './cloudly-view-deployments.js';
|
||||
import { CloudlyViewDns } from './cloudly-view-dns.js';
|
||||
import { CloudlyViewDomains } from './cloudly-view-domains.js';
|
||||
import { CloudlyViewImages } from './cloudly-view-images.js';
|
||||
import { CloudlyViewLogs } from './cloudly-view-logs.js';
|
||||
import { CloudlyViewMails } from './cloudly-view-mails.js';
|
||||
@@ -125,6 +126,11 @@ export class CloudlyDashboard extends DeesElement {
|
||||
iconName: 'lucide:Rocket',
|
||||
element: CloudlyViewDeployments,
|
||||
},
|
||||
{
|
||||
name: 'Domains',
|
||||
iconName: 'lucide:Globe2',
|
||||
element: CloudlyViewDomains,
|
||||
},
|
||||
{
|
||||
name: 'DNS',
|
||||
iconName: 'lucide:Globe',
|
||||
|
@@ -18,65 +18,191 @@ export class CloudlyViewDns extends DeesElement {
|
||||
private data: appstate.IDataState = {
|
||||
secretGroups: [],
|
||||
secretBundles: [],
|
||||
dnsEntries: [],
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subecription = appstate.dataState
|
||||
const subscription = appstate.dataState
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((dataArg) => {
|
||||
this.data = dataArg;
|
||||
});
|
||||
this.rxSubscriptions.push(subecription);
|
||||
this.rxSubscriptions.push(subscription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
css`
|
||||
.dns-type-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
.type-A { background: #4CAF50; }
|
||||
.type-AAAA { background: #45a049; }
|
||||
.type-CNAME { background: #2196F3; }
|
||||
.type-MX { background: #FF9800; }
|
||||
.type-TXT { background: #9C27B0; }
|
||||
.type-NS { background: #795548; }
|
||||
.type-SOA { background: #607D8B; }
|
||||
.type-SRV { background: #E91E63; }
|
||||
.type-CAA { background: #00BCD4; }
|
||||
.type-PTR { background: #673AB7; }
|
||||
|
||||
.status-active {
|
||||
color: #4CAF50;
|
||||
}
|
||||
.status-inactive {
|
||||
color: #f44336;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
private getRecordTypeBadge(type: string) {
|
||||
return html`<span class="dns-type-badge type-${type}">${type}</span>`;
|
||||
}
|
||||
|
||||
private getStatusBadge(active: boolean) {
|
||||
return html`<span class="${active ? 'status-active' : 'status-inactive'}">
|
||||
${active ? '✓ Active' : '✗ Inactive'}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>DNS</cloudly-sectionheading>
|
||||
<cloudly-sectionheading>DNS Management</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'DNS'}
|
||||
.heading2=${'decoded in client'}
|
||||
.data=${this.data.deployments}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
||||
.heading1=${'DNS Entries'}
|
||||
.heading2=${'Manage DNS records for your domains'}
|
||||
.data=${this.data.dnsEntries || []}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.IDnsEntry) => {
|
||||
return {
|
||||
id: itemArg.id,
|
||||
serverAmount: itemArg.data.servers.length,
|
||||
Type: this.getRecordTypeBadge(itemArg.data.type),
|
||||
Name: itemArg.data.name === '@' ? '<root>' : itemArg.data.name,
|
||||
Value: itemArg.data.value,
|
||||
TTL: `${itemArg.data.ttl}s`,
|
||||
Priority: itemArg.data.priority || '-',
|
||||
Zone: itemArg.data.zone,
|
||||
Status: this.getStatusBadge(itemArg.data.active),
|
||||
Description: itemArg.data.description || '-',
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'add configBundle',
|
||||
name: 'Add DNS Entry',
|
||||
iconName: 'plus',
|
||||
type: ['header', 'footer'],
|
||||
actionFunc: async (dataActionArg) => {
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Add ConfigBundle',
|
||||
heading: 'Add DNS Entry',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.secretGroupIds'}
|
||||
.label=${'secretGroupIds'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.includedTags'}
|
||||
.label=${'includedTags'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
<dees-input-dropdown
|
||||
.key=${'type'}
|
||||
.label=${'Record Type'}
|
||||
.options=${[
|
||||
{key: 'A', option: 'A - IPv4 Address'},
|
||||
{key: 'AAAA', option: 'AAAA - IPv6 Address'},
|
||||
{key: 'CNAME', option: 'CNAME - Canonical Name'},
|
||||
{key: 'MX', option: 'MX - Mail Exchange'},
|
||||
{key: 'TXT', option: 'TXT - Text Record'},
|
||||
{key: 'NS', option: 'NS - Name Server'},
|
||||
{key: 'SOA', option: 'SOA - Start of Authority'},
|
||||
{key: 'SRV', option: 'SRV - Service'},
|
||||
{key: 'CAA', option: 'CAA - Certification Authority'},
|
||||
{key: 'PTR', option: 'PTR - Pointer'},
|
||||
]}
|
||||
.value=${'A'}
|
||||
.required=${true}>
|
||||
</dees-input-dropdown>
|
||||
<dees-input-text
|
||||
.key=${'zone'}
|
||||
.label=${'Zone (Domain)'}
|
||||
.placeholder=${'example.com'}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'name'}
|
||||
.label=${'Name'}
|
||||
.placeholder=${'@ for root, www, mail, etc.'}
|
||||
.value=${'@'}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'value'}
|
||||
.label=${'Value'}
|
||||
.placeholder=${'IP address, domain, or text value'}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'ttl'}
|
||||
.label=${'TTL (seconds)'}
|
||||
.value=${'3600'}
|
||||
.type=${'number'}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'priority'}
|
||||
.label=${'Priority (MX/SRV only)'}
|
||||
.type=${'number'}
|
||||
.placeholder=${'10'}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'weight'}
|
||||
.label=${'Weight (SRV only)'}
|
||||
.type=${'number'}
|
||||
.placeholder=${'0'}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'port'}
|
||||
.label=${'Port (SRV only)'}
|
||||
.type=${'number'}
|
||||
.placeholder=${'443'}>
|
||||
</dees-input-text>
|
||||
<dees-input-checkbox
|
||||
.key=${'active'}
|
||||
.label=${'Active'}
|
||||
.value=${true}>
|
||||
</dees-input-checkbox>
|
||||
<dees-input-text
|
||||
.key=${'description'}
|
||||
.label=${'Description (optional)'}
|
||||
.placeholder=${'What is this record for?'}>
|
||||
</dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'create', action: async (modalArg) => {} },
|
||||
{
|
||||
name: 'cancel',
|
||||
name: 'Create DNS Entry',
|
||||
action: async (modalArg) => {
|
||||
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||
const formData = await form.gatherData();
|
||||
|
||||
await appstate.dataState.dispatchAction(appstate.createDnsEntryAction, {
|
||||
dnsEntryData: {
|
||||
type: formData.type,
|
||||
zone: formData.zone,
|
||||
name: formData.name || '@',
|
||||
value: formData.value,
|
||||
ttl: parseInt(formData.ttl) || 3600,
|
||||
priority: formData.priority ? parseInt(formData.priority) : undefined,
|
||||
weight: formData.weight ? parseInt(formData.weight) : undefined,
|
||||
port: formData.port ? parseInt(formData.port) : undefined,
|
||||
active: formData.active,
|
||||
description: formData.description || undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
@@ -86,34 +212,192 @@ export class CloudlyViewDns extends DeesElement {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
name: 'Edit',
|
||||
iconName: 'edit',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry;
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Edit DNS Entry`,
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-dropdown
|
||||
.key=${'type'}
|
||||
.label=${'Record Type'}
|
||||
.options=${[
|
||||
{key: 'A', option: 'A - IPv4 Address'},
|
||||
{key: 'AAAA', option: 'AAAA - IPv6 Address'},
|
||||
{key: 'CNAME', option: 'CNAME - Canonical Name'},
|
||||
{key: 'MX', option: 'MX - Mail Exchange'},
|
||||
{key: 'TXT', option: 'TXT - Text Record'},
|
||||
{key: 'NS', option: 'NS - Name Server'},
|
||||
{key: 'SOA', option: 'SOA - Start of Authority'},
|
||||
{key: 'SRV', option: 'SRV - Service'},
|
||||
{key: 'CAA', option: 'CAA - Certification Authority'},
|
||||
{key: 'PTR', option: 'PTR - Pointer'},
|
||||
]}
|
||||
.value=${dnsEntry.data.type}
|
||||
.required=${true}>
|
||||
</dees-input-dropdown>
|
||||
<dees-input-text
|
||||
.key=${'zone'}
|
||||
.label=${'Zone (Domain)'}
|
||||
.value=${dnsEntry.data.zone}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'name'}
|
||||
.label=${'Name'}
|
||||
.value=${dnsEntry.data.name}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'value'}
|
||||
.label=${'Value'}
|
||||
.value=${dnsEntry.data.value}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'ttl'}
|
||||
.label=${'TTL (seconds)'}
|
||||
.value=${dnsEntry.data.ttl}
|
||||
.type=${'number'}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'priority'}
|
||||
.label=${'Priority (MX/SRV only)'}
|
||||
.value=${dnsEntry.data.priority || ''}
|
||||
.type=${'number'}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'weight'}
|
||||
.label=${'Weight (SRV only)'}
|
||||
.value=${dnsEntry.data.weight || ''}
|
||||
.type=${'number'}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'port'}
|
||||
.label=${'Port (SRV only)'}
|
||||
.value=${dnsEntry.data.port || ''}
|
||||
.type=${'number'}>
|
||||
</dees-input-text>
|
||||
<dees-input-checkbox
|
||||
.key=${'active'}
|
||||
.label=${'Active'}
|
||||
.value=${dnsEntry.data.active}>
|
||||
</dees-input-checkbox>
|
||||
<dees-input-text
|
||||
.key=${'description'}
|
||||
.label=${'Description (optional)'}
|
||||
.value=${dnsEntry.data.description || ''}>
|
||||
</dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Update DNS Entry',
|
||||
action: async (modalArg) => {
|
||||
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||
const formData = await form.gatherData();
|
||||
|
||||
await appstate.dataState.dispatchAction(appstate.updateDnsEntryAction, {
|
||||
dnsEntryId: dnsEntry.id,
|
||||
dnsEntryData: {
|
||||
...dnsEntry.data,
|
||||
type: formData.type,
|
||||
zone: formData.zone,
|
||||
name: formData.name || '@',
|
||||
value: formData.value,
|
||||
ttl: parseInt(formData.ttl) || 3600,
|
||||
priority: formData.priority ? parseInt(formData.priority) : undefined,
|
||||
weight: formData.weight ? parseInt(formData.weight) : undefined,
|
||||
port: formData.port ? parseInt(formData.port) : undefined,
|
||||
active: formData.active,
|
||||
description: formData.description || undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Duplicate',
|
||||
iconName: 'copy',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry;
|
||||
await appstate.dataState.dispatchAction(appstate.createDnsEntryAction, {
|
||||
dnsEntryData: {
|
||||
...dnsEntry.data,
|
||||
description: `Copy of ${dnsEntry.data.description || dnsEntry.data.name}`,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Toggle Active',
|
||||
iconName: 'power',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry;
|
||||
await appstate.dataState.dispatchAction(appstate.updateDnsEntryAction, {
|
||||
dnsEntryId: dnsEntry.id,
|
||||
dnsEntryData: {
|
||||
...dnsEntry.data,
|
||||
active: !dnsEntry.data.active,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry;
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
||||
heading: `Delete DNS Entry`,
|
||||
content: html`
|
||||
<div style="text-align:center">
|
||||
Do you really want to delete the ConfigBundle?
|
||||
Are you sure you want to delete this DNS entry?
|
||||
</div>
|
||||
<div
|
||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
||||
>
|
||||
${actionDataArg.item.id}
|
||||
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
|
||||
<div style="color: #fff; font-weight: bold;">
|
||||
${dnsEntry.data.type} - ${dnsEntry.data.name}.${dnsEntry.data.zone}
|
||||
</div>
|
||||
<div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">
|
||||
${dnsEntry.data.value}
|
||||
</div>
|
||||
${dnsEntry.data.description ? html`
|
||||
<div style="color: #888; font-size: 0.85em; margin-top: 8px;">
|
||||
${dnsEntry.data.description}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'cancel',
|
||||
name: 'Cancel',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
name: 'Delete',
|
||||
action: async (modalArg) => {
|
||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
||||
configBundleId: actionDataArg.item.id,
|
||||
await appstate.dataState.dispatchAction(appstate.deleteDnsEntryAction, {
|
||||
dnsEntryId: dnsEntry.id,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
@@ -126,4 +410,4 @@ export class CloudlyViewDns extends DeesElement {
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
529
ts_web/elements/cloudly-view-domains.ts
Normal file
529
ts_web/elements/cloudly-view-domains.ts
Normal file
@@ -0,0 +1,529 @@
|
||||
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>
|
||||
`;
|
||||
}
|
||||
}
|
@@ -104,11 +104,11 @@ export class CloudlyViewOverview extends DeesElement {
|
||||
},
|
||||
{
|
||||
id: 'dns',
|
||||
title: 'DNS Zones',
|
||||
value: this.data.dns?.length || 0,
|
||||
title: 'DNS Entries',
|
||||
value: this.data.dnsEntries?.length || 0,
|
||||
type: 'number' as const,
|
||||
iconName: 'lucide:Globe',
|
||||
description: 'Managed DNS zones'
|
||||
description: 'Managed DNS records'
|
||||
},
|
||||
{
|
||||
id: 'databases',
|
||||
|
@@ -120,21 +120,21 @@ export class CloudlyViewServices extends DeesElement {
|
||||
<dees-input-dropdown
|
||||
.key=${'serviceCategory'}
|
||||
.label=${'Service Category'}
|
||||
.options=${['base', 'distributed', 'workload']}
|
||||
.options=${[{key: 'base', option: 'Base'}, {key: 'distributed', option: 'Distributed'}, {key: 'workload', option: 'Workload'}]}
|
||||
.value=${'workload'}
|
||||
.required=${true}>
|
||||
</dees-input-dropdown>
|
||||
<dees-input-dropdown
|
||||
.key=${'deploymentStrategy'}
|
||||
.label=${'Deployment Strategy'}
|
||||
.options=${['all-nodes', 'limited-replicas', 'custom']}
|
||||
.options=${[{key: 'all-nodes', option: 'All Nodes'}, {key: 'limited-replicas', option: 'Limited Replicas'}, {key: 'custom', option: 'Custom'}]}
|
||||
.value=${'custom'}
|
||||
.required=${true}>
|
||||
</dees-input-dropdown>
|
||||
<dees-input-text
|
||||
.key=${'maxReplicas'}
|
||||
.label=${'Max Replicas (for distributed services)'}
|
||||
.value=${'3'}
|
||||
.value=${'1'}
|
||||
.type=${'number'}>
|
||||
</dees-input-text>
|
||||
<dees-input-checkbox
|
||||
@@ -154,7 +154,7 @@ export class CloudlyViewServices extends DeesElement {
|
||||
<dees-input-dropdown
|
||||
.key=${'balancingStrategy'}
|
||||
.label=${'Balancing Strategy'}
|
||||
.options=${['round-robin', 'least-connections']}
|
||||
.options=${[{key: 'round-robin', option: 'Round Robin'}, {key: 'least-connections', option: 'Least Connections'}]}
|
||||
.value=${'round-robin'}
|
||||
.required=${true}>
|
||||
</dees-input-dropdown>
|
||||
@@ -223,14 +223,14 @@ export class CloudlyViewServices extends DeesElement {
|
||||
<dees-input-dropdown
|
||||
.key=${'serviceCategory'}
|
||||
.label=${'Service Category'}
|
||||
.options=${['base', 'distributed', 'workload']}
|
||||
.options=${[{key: 'base', option: 'Base'}, {key: 'distributed', option: 'Distributed'}, {key: 'workload', option: 'Workload'}]}
|
||||
.value=${service.data.serviceCategory || 'workload'}
|
||||
.required=${true}>
|
||||
</dees-input-dropdown>
|
||||
<dees-input-dropdown
|
||||
.key=${'deploymentStrategy'}
|
||||
.label=${'Deployment Strategy'}
|
||||
.options=${['all-nodes', 'limited-replicas', 'custom']}
|
||||
.options=${[{key: 'all-nodes', option: 'All Nodes'}, {key: 'limited-replicas', option: 'Limited Replicas'}, {key: 'custom', option: 'Custom'}]}
|
||||
.value=${service.data.deploymentStrategy || 'custom'}
|
||||
.required=${true}>
|
||||
</dees-input-dropdown>
|
||||
@@ -256,7 +256,7 @@ export class CloudlyViewServices extends DeesElement {
|
||||
<dees-input-dropdown
|
||||
.key=${'balancingStrategy'}
|
||||
.label=${'Balancing Strategy'}
|
||||
.options=${['round-robin', 'least-connections']}
|
||||
.options=${[{key: 'round-robin', option: 'Round Robin'}, {key: 'least-connections', option: 'Least Connections'}]}
|
||||
.value=${service.data.balancingStrategy}
|
||||
.required=${true}>
|
||||
</dees-input-dropdown>
|
||||
|
Reference in New Issue
Block a user