529 lines
21 KiB
TypeScript
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>
|
|||
|
`;
|
|||
|
}
|
|||
|
}
|