Files
cloudly/ts_web/elements/views/domains/index.ts

186 lines
16 KiB
TypeScript

import * as plugins from '../../../plugins.js';
import * as shared from '../../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: [] } as any;
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; }
.activation-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; }
.activation-available { background: #2b2b2b; color: #bbb; border: 1px solid #444; }
.activation-activated { background: #4CAF50; color: #fff; }
.activation-ignored { background: #9E9E9E; color: #fff; }
.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 getActivationBadge(state?: 'available'|'activated'|'ignored') { const s = state || 'available'; return html`<span class="activation-badge activation-${s}">${s.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),
Activation: this.getActivationBadge((itemArg.data as any).activationState),
'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: 'Sync from Cloudflare', iconName: 'cloud', type: ['header'], actionFunc: async () => { await appstate.dataState.dispatchAction(appstate.taskActions.triggerTask, { taskName: 'cloudflare-domain-sync' } as any); await appstate.dataState.dispatchAction(appstate.getAllDataAction, null); plugins.deesCatalog.DeesToast.createAndShow({ message: 'Triggered Cloudflare sync', type: 'success' }); } },
{ name: 'Add Domain', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
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: any) => {
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: any) => modalArg.destroy() },
],
});
} },
{ name: 'Edit', iconName: 'edit', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
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-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}></dees-input-checkbox>
<dees-input-text .key=${'tags'} .label=${'Tags (comma separated)'} .value=${(domain.data.tags || []).join(', ')}></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'Save Changes', action: async (modalArg: any) => {
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
const formData = await form.gatherData();
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, updates: { name: formData.name, description: formData.description || undefined, status: formData.status, autoRenew: formData.autoRenew, dnssecEnabled: formData.dnssecEnabled, tags }, });
await modalArg.destroy();
}},
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
} },
{ name: 'Verify Ownership', iconName: 'check-circle', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
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: any) => { 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: any) => modalArg.destroy() },
],
});
} },
{ name: 'View DNS Records', iconName: 'list', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const domain = actionDataArg.item as plugins.interfaces.data.IDomain; console.log('View DNS records for domain:', domain.data.name); } },
{ name: 'Delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
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: any) => { await modalArg.destroy(); } }, { name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteDomainAction, { domainId: domain.id, }); await modalArg.destroy(); } }, ],
});
} },
{ name: 'Activate', iconName: 'check', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const domain = actionDataArg.item as plugins.interfaces.data.IDomain; await appstate.dataState.dispatchAction(appstate.updateDomainAction, { domainId: domain.id, domainData: { ...(domain.data as any), activationState: 'activated' } as any }); } },
{ name: 'Deactivate', iconName: 'slash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const domain = actionDataArg.item as plugins.interfaces.data.IDomain; await appstate.dataState.dispatchAction(appstate.updateDomainAction, { domainId: domain.id, domainData: { ...(domain.data as any), activationState: 'available' } as any }); } },
{ name: 'Ignore', iconName: 'ban', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const domain = actionDataArg.item as plugins.interfaces.data.IDomain; await appstate.dataState.dispatchAction(appstate.updateDomainAction, { domainId: domain.id, domainData: { ...(domain.data as any), activationState: 'ignored' } as any }); } },
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}
declare global {
interface HTMLElementTagNameMap { 'cloudly-view-domains': CloudlyViewDomains; }
}