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:
		| @@ -50,7 +50,8 @@ export interface IDataState { | ||||
|   images?: any[]; | ||||
|   services?: plugins.interfaces.data.IService[]; | ||||
|   deployments?: plugins.interfaces.data.IDeployment[]; | ||||
|   dns?: any[]; | ||||
|   domains?: plugins.interfaces.data.IDomain[]; | ||||
|   dnsEntries?: plugins.interfaces.data.IDnsEntry[]; | ||||
|   mails?: any[]; | ||||
|   logs?: any[]; | ||||
|   s3?: any[]; | ||||
| @@ -66,7 +67,8 @@ export const dataState = await appstate.getStatePart<IDataState>( | ||||
|     images: [], | ||||
|     services: [], | ||||
|     deployments: [], | ||||
|     dns: [], | ||||
|     domains: [], | ||||
|     dnsEntries: [], | ||||
|     mails: [], | ||||
|     logs: [], | ||||
|     s3: [], | ||||
| @@ -180,6 +182,50 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => { | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   // Domains | ||||
|   const trGetDomains = | ||||
|     new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.domain.IRequest_Any_Cloudly_GetDomains>( | ||||
|       '/typedrequest', | ||||
|       'getDomains' | ||||
|     ); | ||||
|   try { | ||||
|     const responseDomains = await trGetDomains.fire({ | ||||
|       identity: loginStatePart.getState().identity, | ||||
|     }); | ||||
|     currentState = { | ||||
|       ...currentState, | ||||
|       domains: responseDomains?.domains || [], | ||||
|     }; | ||||
|   } catch (error) { | ||||
|     console.error('Failed to fetch domains:', error); | ||||
|     currentState = { | ||||
|       ...currentState, | ||||
|       domains: [], | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   // DNS Entries | ||||
|   const trGetDnsEntries = | ||||
|     new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.dns.IRequest_Any_Cloudly_GetDnsEntries>( | ||||
|       '/typedrequest', | ||||
|       'getDnsEntries' | ||||
|     ); | ||||
|   try { | ||||
|     const responseDnsEntries = await trGetDnsEntries.fire({ | ||||
|       identity: loginStatePart.getState().identity, | ||||
|     }); | ||||
|     currentState = { | ||||
|       ...currentState, | ||||
|       dnsEntries: responseDnsEntries?.dnsEntries || [], | ||||
|     }; | ||||
|   } catch (error) { | ||||
|     console.error('Failed to fetch DNS entries:', error); | ||||
|     currentState = { | ||||
|       ...currentState, | ||||
|       dnsEntries: [], | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   return currentState; | ||||
| }); | ||||
|  | ||||
| @@ -389,6 +435,130 @@ export const deleteDeploymentAction = dataState.createAction( | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // DNS Actions | ||||
| export const createDnsEntryAction = dataState.createAction( | ||||
|   async (statePartArg, payloadArg: { dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }) => { | ||||
|     let currentState = statePartArg.getState(); | ||||
|     const trCreateDnsEntry = | ||||
|       new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.dns.IRequest_Any_Cloudly_CreateDnsEntry>( | ||||
|         '/typedrequest', | ||||
|         'createDnsEntry' | ||||
|       ); | ||||
|     const response = await trCreateDnsEntry.fire({ | ||||
|       identity: loginStatePart.getState().identity, | ||||
|       dnsEntryData: payloadArg.dnsEntryData, | ||||
|     }); | ||||
|     currentState = await dataState.dispatchAction(getAllDataAction, null); | ||||
|     return currentState; | ||||
|   } | ||||
| ); | ||||
|  | ||||
| export const updateDnsEntryAction = dataState.createAction( | ||||
|   async (statePartArg, payloadArg: { dnsEntryId: string; dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }) => { | ||||
|     let currentState = statePartArg.getState(); | ||||
|     const trUpdateDnsEntry = | ||||
|       new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.dns.IRequest_Any_Cloudly_UpdateDnsEntry>( | ||||
|         '/typedrequest', | ||||
|         'updateDnsEntry' | ||||
|       ); | ||||
|     const response = await trUpdateDnsEntry.fire({ | ||||
|       identity: loginStatePart.getState().identity, | ||||
|       dnsEntryId: payloadArg.dnsEntryId, | ||||
|       dnsEntryData: payloadArg.dnsEntryData, | ||||
|     }); | ||||
|     currentState = await dataState.dispatchAction(getAllDataAction, null); | ||||
|     return currentState; | ||||
|   } | ||||
| ); | ||||
|  | ||||
| export const deleteDnsEntryAction = dataState.createAction( | ||||
|   async (statePartArg, payloadArg: { dnsEntryId: string }) => { | ||||
|     let currentState = statePartArg.getState(); | ||||
|     const trDeleteDnsEntry = | ||||
|       new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.dns.IRequest_Any_Cloudly_DeleteDnsEntry>( | ||||
|         '/typedrequest', | ||||
|         'deleteDnsEntry' | ||||
|       ); | ||||
|     const response = await trDeleteDnsEntry.fire({ | ||||
|       identity: loginStatePart.getState().identity, | ||||
|       dnsEntryId: payloadArg.dnsEntryId, | ||||
|     }); | ||||
|     currentState = await dataState.dispatchAction(getAllDataAction, null); | ||||
|     return currentState; | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // Domain Actions | ||||
| export const createDomainAction = dataState.createAction( | ||||
|   async (statePartArg, payloadArg: { domainData: plugins.interfaces.data.IDomain['data'] }) => { | ||||
|     let currentState = statePartArg.getState(); | ||||
|     const trCreateDomain = | ||||
|       new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.domain.IRequest_Any_Cloudly_CreateDomain>( | ||||
|         '/typedrequest', | ||||
|         'createDomain' | ||||
|       ); | ||||
|     const response = await trCreateDomain.fire({ | ||||
|       identity: loginStatePart.getState().identity, | ||||
|       domainData: payloadArg.domainData, | ||||
|     }); | ||||
|     currentState = await dataState.dispatchAction(getAllDataAction, null); | ||||
|     return currentState; | ||||
|   } | ||||
| ); | ||||
|  | ||||
| export const updateDomainAction = dataState.createAction( | ||||
|   async (statePartArg, payloadArg: { domainId: string; domainData: plugins.interfaces.data.IDomain['data'] }) => { | ||||
|     let currentState = statePartArg.getState(); | ||||
|     const trUpdateDomain = | ||||
|       new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.domain.IRequest_Any_Cloudly_UpdateDomain>( | ||||
|         '/typedrequest', | ||||
|         'updateDomain' | ||||
|       ); | ||||
|     const response = await trUpdateDomain.fire({ | ||||
|       identity: loginStatePart.getState().identity, | ||||
|       domainId: payloadArg.domainId, | ||||
|       domainData: payloadArg.domainData, | ||||
|     }); | ||||
|     currentState = await dataState.dispatchAction(getAllDataAction, null); | ||||
|     return currentState; | ||||
|   } | ||||
| ); | ||||
|  | ||||
| export const deleteDomainAction = dataState.createAction( | ||||
|   async (statePartArg, payloadArg: { domainId: string }) => { | ||||
|     let currentState = statePartArg.getState(); | ||||
|     const trDeleteDomain = | ||||
|       new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.domain.IRequest_Any_Cloudly_DeleteDomain>( | ||||
|         '/typedrequest', | ||||
|         'deleteDomain' | ||||
|       ); | ||||
|     const response = await trDeleteDomain.fire({ | ||||
|       identity: loginStatePart.getState().identity, | ||||
|       domainId: payloadArg.domainId, | ||||
|     }); | ||||
|     currentState = await dataState.dispatchAction(getAllDataAction, null); | ||||
|     return currentState; | ||||
|   } | ||||
| ); | ||||
|  | ||||
| export const verifyDomainAction = dataState.createAction( | ||||
|   async (statePartArg, payloadArg: { domainId: string; verificationMethod?: 'dns' | 'http' | 'email' | 'manual' }) => { | ||||
|     let currentState = statePartArg.getState(); | ||||
|     const trVerifyDomain = | ||||
|       new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.domain.IRequest_Any_Cloudly_VerifyDomain>( | ||||
|         '/typedrequest', | ||||
|         'verifyDomain' | ||||
|       ); | ||||
|     const response = await trVerifyDomain.fire({ | ||||
|       identity: loginStatePart.getState().identity, | ||||
|       domainId: payloadArg.domainId, | ||||
|       verificationMethod: payloadArg.verificationMethod, | ||||
|     }); | ||||
|     currentState = await dataState.dispatchAction(getAllDataAction, null); | ||||
|     return currentState; | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // cluster | ||||
| export const addClusterAction = dataState.createAction( | ||||
|   async ( | ||||
|   | ||||
| @@ -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