f40ef6b7c0
Align Cloudly with the current typedserver, smartconfig, smartstate, and Docker tooling releases so builds and Docker output stay compatible with the upgraded stack.
186 lines
16 KiB
TypeScript
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 accessor 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, domainData: { ...domain.data, 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; }
|
|
}
|