Files
cloudly/ts_web/elements/views/dns/index.ts
Juergen Kunz bb313fd9dc feat: Add settings view for cloud provider configurations
- Implemented CloudlyViewSettings component for managing cloud provider settings including Hetzner, Cloudflare, AWS, DigitalOcean, Azure, and Google Cloud.
- Added functionality to load, save, and test connections for each provider.
- Enhanced UI with loading states and success/error notifications.

feat: Create tasks view with execution history

- Developed CloudlyViewTasks component to display and manage tasks and their executions.
- Integrated auto-refresh functionality for task executions.
- Added filtering and searching capabilities for tasks.

feat: Implement execution details and task panel components

- Created CloudlyExecutionDetails component to show detailed information about task executions including logs and metrics.
- Developed CloudlyTaskPanel component to display individual tasks with execution status and actions to run or cancel tasks.

feat: Utility functions for formatting and categorization

- Added utility functions for formatting dates, durations, and cron expressions.
- Implemented functions to retrieve category icons and hues for task categorization.
2025-09-14 17:28:21 +00:00

144 lines
11 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-dns')
export class CloudlyViewDns extends DeesElement {
@state()
private data: appstate.IDataState = { secretGroups: [], secretBundles: [], dnsEntries: [], domains: [] } as any;
constructor() {
super();
const subscription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
this.rxSubscriptions.push(subscription);
}
async connectedCallback() {
super.connectedCallback();
await appstate.dataState.dispatchAction(appstate.getAllDataAction, {});
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
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 Management</cloudly-sectionheading>
<dees-table
.heading1=${'DNS Entries'}
.heading2=${'Manage DNS records for your domains'}
.data=${this.data.dnsEntries || []}
.displayFunction=${(itemArg: plugins.interfaces.data.IDnsEntry) => {
return {
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 DNS Entry', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add 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=${'A'} .required=${true}></dees-input-dropdown>
<dees-input-dropdown .key=${'domainId'} .label=${'Domain'} .options=${this.data.domains?.map(domain => ({ key: domain.id, option: domain.data.name })) || []} .required=${true}></dees-input-dropdown>
<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 DNS Entry', action: async (modalArg: any) => {
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
const formData = await form.gatherData();
await appstate.dataState.dispatchAction(appstate.createDnsEntryAction, { dnsEntryData: { type: formData.type, domainId: formData.domainId, 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: any) => modalArg.destroy() },
],
});
} },
{ name: 'Edit', iconName: 'edit', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
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-dropdown .key=${'domainId'} .label=${'Domain'} .options=${this.data.domains?.map(domain => ({ key: domain.id, option: domain.data.name })) || []} .value=${dnsEntry.data.domainId || ''} .required=${true}></dees-input-dropdown>
<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: any) => {
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, domainId: formData.domainId, 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: any) => modalArg.destroy() },
],
});
} },
{ name: 'Duplicate', iconName: 'copy', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { 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: any) => { 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: any) => { const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry; plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete DNS Entry`, content: html`<div style="text-align:center">Are you sure you want to delete this DNS entry?</div><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', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteDnsEntryAction, { dnsEntryId: dnsEntry.id, }); await modalArg.destroy(); } }, ], }); } },
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}
declare global { interface HTMLElementTagNameMap { 'cloudly-view-dns': CloudlyViewDns; } }