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.
This commit is contained in:
		| @@ -11,23 +11,23 @@ import { | ||||
|   html, | ||||
|   state | ||||
| } from '@design.estate/dees-element'; | ||||
| import { CloudlyViewBackups } from './cloudly-view-backups.js'; | ||||
| 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'; | ||||
| import { CloudlyViewOverview } from './cloudly-view-overview.js'; | ||||
| import { CloudlyViewS3 } from './cloudly-view-s3.js'; | ||||
| import { CloudlyViewSecretBundles } from './cloudly-view-secretbundles.js'; | ||||
| import { CloudlyViewSecretGroups } from './cloudly-view-secretgroups.js'; | ||||
| import { CloudlyViewServices } from './cloudly-view-services.js'; | ||||
| import { CloudlyViewExternalRegistries } from './cloudly-view-externalregistries.js'; | ||||
| import { CloudlyViewSettings } from './cloudly-view-settings.js'; | ||||
| import { CloudlyViewTasks } from './cloudly-view-tasks.js'; | ||||
| import { CloudlyViewBackups } from './views/backups/index.js'; | ||||
| import { CloudlyViewClusters } from './views/clusters/index.js'; | ||||
| import { CloudlyViewDbs } from './views/dbs/index.js'; | ||||
| import { CloudlyViewDeployments } from './views/deployments/index.js'; | ||||
| import { CloudlyViewDns } from './views/dns/index.js'; | ||||
| import { CloudlyViewDomains } from './views/domains/index.js'; | ||||
| import { CloudlyViewImages } from './views/images/index.js'; | ||||
| import { CloudlyViewLogs } from './views/logs/index.js'; | ||||
| import { CloudlyViewMails } from './views/mails/index.js'; | ||||
| import { CloudlyViewOverview } from './views/overview/index.js'; | ||||
| import { CloudlyViewS3 } from './views/s3/index.js'; | ||||
| import { CloudlyViewSecretBundles } from './views/secretbundles/index.js'; | ||||
| import { CloudlyViewSecretGroups } from './views/secretgroups/index.js'; | ||||
| import { CloudlyViewServices } from './views/services/index.js'; | ||||
| import { CloudlyViewExternalRegistries } from './views/externalregistries/index.js'; | ||||
| import { CloudlyViewSettings } from './views/settings/index.js'; | ||||
| import { CloudlyViewTasks } from './views/tasks/index.js'; | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|   | ||||
| @@ -1,130 +0,0 @@ | ||||
| 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-backups') | ||||
| export class CloudlyViewBackups extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = { | ||||
|     secretGroups: [], | ||||
|     secretBundles: [], | ||||
|   }; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subecription = appstate.dataState | ||||
|       .select((stateArg) => stateArg) | ||||
|       .subscribe((dataArg) => { | ||||
|         this.data = dataArg; | ||||
|       }); | ||||
|     this.rxSubscriptions.push(subecription); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css` | ||||
|        | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Backups</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'Backups'} | ||||
|         .heading2=${'decoded in client'} | ||||
|         .data=${this.data.backups} | ||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { | ||||
|           return { | ||||
|             id: itemArg.id, | ||||
|             serverAmount: itemArg.data.servers.length, | ||||
|           }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { | ||||
|             name: 'add configBundle', | ||||
|             iconName: 'plus', | ||||
|             type: ['header', 'footer'], | ||||
|             actionFunc: async (dataActionArg) => { | ||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Add ConfigBundle', | ||||
|                 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-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { name: 'create', action: async (modalArg) => {} }, | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'delete', | ||||
|             iconName: 'trash', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Delete ConfigBundle ${actionDataArg.item.id}`, | ||||
|                 content: html` | ||||
|                   <div style="text-align:center"> | ||||
|                     Do you really want to delete the ConfigBundle? | ||||
|                   </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> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'delete', | ||||
|                     action: async (modalArg) => { | ||||
|                       appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { | ||||
|                         configBundleId: actionDataArg.item.id, | ||||
|                       }); | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
| @@ -1,147 +0,0 @@ | ||||
| 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-clusters') | ||||
| export class CloudlyViewClusters extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = {}; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subecription = appstate.dataState | ||||
|       .select((stateArg) => stateArg) | ||||
|       .subscribe((dataArg) => { | ||||
|         this.data = dataArg; | ||||
|       }); | ||||
|     this.rxSubscriptions.push(subecription); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css` | ||||
|        | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Clusters</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'Clusters'} | ||||
|         .heading2=${'decoded in client'} | ||||
|         .data=${this.data.clusters} | ||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { | ||||
|           console.log(itemArg); | ||||
|           return { | ||||
|             id: itemArg.id, | ||||
|             serverAmount: itemArg.data.servers.length, | ||||
|           }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { | ||||
|             name: 'add cluster', | ||||
|             iconName: 'plus', | ||||
|             type: ['header', 'footer'], | ||||
|             actionFunc: async (dataActionArg) => { | ||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Add Cluster', | ||||
|                 content: html` | ||||
|                   <dees-form> | ||||
|                     <dees-input-text | ||||
|                       .key=${'clusterName'} | ||||
|                       .label=${'cluster name'} | ||||
|                       .description=${'a descriptive name for the cluster'} | ||||
|                       .value=${''} | ||||
|                     ></dees-input-text> | ||||
|                     <dees-input-dropdown | ||||
|                       .key=${'setupMode'} | ||||
|                       .label=${'Setup Mode'} | ||||
|                       .description=${'How the cluster infrastructure should be managed'} | ||||
|                       .options=${[ | ||||
|                         {option: 'manual', key: 'manual', description: 'Manual Setup - Add your own servers manually'}, | ||||
|                         {option: 'hetzner', key: 'hetzner', description: 'Hetzner Cloud - Auto-provision servers on Hetzner'}, | ||||
|                         {option: 'aws', key: 'aws', description: 'AWS - Auto-provision on Amazon Web Services (coming soon)', disabled: true}, | ||||
|                         {option: 'digitalocean', key: 'digitalocean', description: 'DigitalOcean - Auto-provision on DigitalOcean (coming soon)', disabled: true} | ||||
|                       ]} | ||||
|                       .selectedOption=${'manual'} | ||||
|                     ></dees-input-dropdown> | ||||
|                   </dees-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'create', | ||||
|                     action: async (modalArg) => { | ||||
|                       const data: { | ||||
|                         clusterName: string; | ||||
|                         setupMode: 'manual' | 'hetzner' | 'aws' | 'digitalocean'; | ||||
|                       } = (await modalArg.shadowRoot | ||||
|                         .querySelector('dees-form') | ||||
|                         .collectFormData()) as any; | ||||
|                       await appstate.dataState.dispatchAction(appstate.addClusterAction, data); | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'delete', | ||||
|             iconName: 'trash', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Delete ConfigBundle ${actionDataArg.item.id}`, | ||||
|                 content: html` | ||||
|                   <div style="text-align:center"> | ||||
|                     Do you really want to delete the ConfigBundle? | ||||
|                   </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> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'delete', | ||||
|                     action: async (modalArg) => { | ||||
|                       appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { | ||||
|                         configBundleId: actionDataArg.item.id, | ||||
|                       }); | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
| @@ -1,130 +0,0 @@ | ||||
| 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-dbs') | ||||
| export class CloudlyViewDbs extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = { | ||||
|     secretGroups: [], | ||||
|     secretBundles: [], | ||||
|   }; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subecription = appstate.dataState | ||||
|       .select((stateArg) => stateArg) | ||||
|       .subscribe((dataArg) => { | ||||
|         this.data = dataArg; | ||||
|       }); | ||||
|     this.rxSubscriptions.push(subecription); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css` | ||||
|        | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>DBs</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'DBs'} | ||||
|         .heading2=${'decoded in client'} | ||||
|         .data=${this.data.dbs} | ||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { | ||||
|           return { | ||||
|             id: itemArg.id, | ||||
|             serverAmount: itemArg.data.servers.length, | ||||
|           }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { | ||||
|             name: 'add configBundle', | ||||
|             iconName: 'plus', | ||||
|             type: ['header', 'footer'], | ||||
|             actionFunc: async (dataActionArg) => { | ||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Add ConfigBundle', | ||||
|                 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-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { name: 'create', action: async (modalArg) => {} }, | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'delete', | ||||
|             iconName: 'trash', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Delete ConfigBundle ${actionDataArg.item.id}`, | ||||
|                 content: html` | ||||
|                   <div style="text-align:center"> | ||||
|                     Do you really want to delete the ConfigBundle? | ||||
|                   </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> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'delete', | ||||
|                     action: async (modalArg) => { | ||||
|                       appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { | ||||
|                         configBundleId: actionDataArg.item.id, | ||||
|                       }); | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
| @@ -1,349 +0,0 @@ | ||||
| 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-deployments') | ||||
| export class CloudlyViewDeployments extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = {}; | ||||
|  | ||||
|   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 { | ||||
|         padding: 2px 8px; | ||||
|         border-radius: 4px; | ||||
|         font-size: 0.85em; | ||||
|         font-weight: 500; | ||||
|       } | ||||
|       .status-running { | ||||
|         background: #4caf50; | ||||
|         color: white; | ||||
|       } | ||||
|       .status-stopped { | ||||
|         background: #f44336; | ||||
|         color: white; | ||||
|       } | ||||
|       .status-paused { | ||||
|         background: #ff9800; | ||||
|         color: white; | ||||
|       } | ||||
|       .status-deploying { | ||||
|         background: #2196f3; | ||||
|         color: white; | ||||
|       } | ||||
|       .health-indicator { | ||||
|         display: inline-flex; | ||||
|         align-items: center; | ||||
|         gap: 4px; | ||||
|         padding: 2px 8px; | ||||
|         border-radius: 4px; | ||||
|         font-size: 0.85em; | ||||
|       } | ||||
|       .health-healthy { | ||||
|         background: #e8f5e9; | ||||
|         color: #2e7d32; | ||||
|       } | ||||
|       .health-unhealthy { | ||||
|         background: #ffebee; | ||||
|         color: #c62828; | ||||
|       } | ||||
|       .health-unknown { | ||||
|         background: #f5f5f5; | ||||
|         color: #666; | ||||
|       } | ||||
|       .resource-usage { | ||||
|         display: flex; | ||||
|         gap: 12px; | ||||
|         font-size: 0.9em; | ||||
|         color: #888; | ||||
|       } | ||||
|       .resource-item { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         gap: 4px; | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   private getServiceName(serviceId: string): string { | ||||
|     const service = this.data.services?.find(s => s.id === serviceId); | ||||
|     return service?.data?.name || serviceId; | ||||
|   } | ||||
|  | ||||
|   private getNodeName(nodeId: string): string { | ||||
|     // This would ideally look up the cluster node name | ||||
|     // For now just return the ID shortened | ||||
|     return nodeId.substring(0, 8); | ||||
|   } | ||||
|  | ||||
|   private getStatusBadgeHtml(status: string): any { | ||||
|     const className = `status-badge status-${status}`; | ||||
|     return html`<span class="${className}">${status}</span>`; | ||||
|   } | ||||
|  | ||||
|   private getHealthIndicatorHtml(health?: string): any { | ||||
|     if (!health) health = 'unknown'; | ||||
|     const className = `health-indicator health-${health}`; | ||||
|     const icon = health === 'healthy' ? '✓' : health === 'unhealthy' ? '✗' : '?'; | ||||
|     return html`<span class="${className}">${icon} ${health}</span>`; | ||||
|   } | ||||
|  | ||||
|   private getResourceUsageHtml(deployment: plugins.interfaces.data.IDeployment): any { | ||||
|     if (!deployment.resourceUsage) { | ||||
|       return html`<span style="color: #aaa;">N/A</span>`; | ||||
|     } | ||||
|     const { cpuUsagePercent, memoryUsedMB } = deployment.resourceUsage; | ||||
|     return html` | ||||
|       <div class="resource-usage"> | ||||
|         <div class="resource-item"> | ||||
|           <lucide-icon name="Cpu" size="14"></lucide-icon> | ||||
|           ${cpuUsagePercent?.toFixed(1) || 0}% | ||||
|         </div> | ||||
|         <div class="resource-item"> | ||||
|           <lucide-icon name="MemoryStick" size="14"></lucide-icon> | ||||
|           ${memoryUsedMB || 0} MB | ||||
|         </div> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Deployments</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'Deployments'} | ||||
|         .heading2=${'Service deployments running on cluster nodes'} | ||||
|         .data=${this.data.deployments || []} | ||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.IDeployment) => { | ||||
|           return { | ||||
|             Service: this.getServiceName(itemArg.serviceId), | ||||
|             Node: this.getNodeName(itemArg.nodeId), | ||||
|             Status: this.getStatusBadgeHtml(itemArg.status), | ||||
|             Health: this.getHealthIndicatorHtml(itemArg.healthStatus), | ||||
|             'Container ID': itemArg.containerId ?  | ||||
|               html`<span style="font-family: monospace; font-size: 0.9em;">${itemArg.containerId.substring(0, 12)}</span>` :  | ||||
|               html`<span style="color: #aaa;">N/A</span>`, | ||||
|             Version: itemArg.version || 'latest', | ||||
|             'Resource Usage': this.getResourceUsageHtml(itemArg), | ||||
|             'Last Updated': itemArg.deployedAt ?  | ||||
|               new Date(itemArg.deployedAt).toLocaleString() :  | ||||
|               'Never', | ||||
|           }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { | ||||
|             name: 'Deploy Service', | ||||
|             iconName: 'plus', | ||||
|             type: ['header', 'footer'], | ||||
|             actionFunc: async (dataActionArg) => { | ||||
|               const availableServices = this.data.services || []; | ||||
|               if (availableServices.length === 0) { | ||||
|                 plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                   heading: 'No Services Available', | ||||
|                   content: html` | ||||
|                     <div style="text-align: center; padding: 24px;"> | ||||
|                       <lucide-icon name="AlertCircle" size="48" style="color: #ff9800; margin-bottom: 16px;"></lucide-icon> | ||||
|                       <div>Please create a service first before creating deployments.</div> | ||||
|                     </div> | ||||
|                   `, | ||||
|                   menuOptions: [ | ||||
|                     { | ||||
|                       name: 'OK', | ||||
|                       action: async (modalArg) => { | ||||
|                         await modalArg.destroy(); | ||||
|                       }, | ||||
|                     }, | ||||
|                   ], | ||||
|                 }); | ||||
|                 return; | ||||
|               } | ||||
|  | ||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Deploy Service', | ||||
|                 content: html` | ||||
|                   <dees-form> | ||||
|                     <dees-input-dropdown  | ||||
|                       .key=${'serviceId'}  | ||||
|                       .label=${'Service'}  | ||||
|                       .options=${availableServices.map(s => ({ key: s.id, value: s.data.name }))} | ||||
|                       .required=${true}> | ||||
|                     </dees-input-dropdown> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'nodeId'}  | ||||
|                       .label=${'Target Node ID'}  | ||||
|                       .required=${true} | ||||
|                       .description=${'Enter the cluster node ID where this service should be deployed'}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'version'}  | ||||
|                       .label=${'Version'}  | ||||
|                       .value=${'latest'} | ||||
|                       .required=${true}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-dropdown  | ||||
|                       .key=${'status'}  | ||||
|                       .label=${'Initial Status'}  | ||||
|                       .options=${['deploying', 'running']} | ||||
|                       .value=${'deploying'} | ||||
|                       .required=${true}> | ||||
|                     </dees-input-dropdown> | ||||
|                   </dees-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'Deploy', | ||||
|                     action: async (modalArg) => { | ||||
|                       const form = modalArg.shadowRoot.querySelector('dees-form') as any; | ||||
|                       const formData = await form.gatherData(); | ||||
|                        | ||||
|                       await appstate.dataState.dispatchAction(appstate.createDeploymentAction, { | ||||
|                         deploymentData: { | ||||
|                           serviceId: formData.serviceId, | ||||
|                           nodeId: formData.nodeId, | ||||
|                           status: formData.status, | ||||
|                           version: formData.version, | ||||
|                           deployedAt: Date.now(), | ||||
|                           usedImageId: 'placeholder', // This would come from the service | ||||
|                           deploymentLog: [], | ||||
|                         }, | ||||
|                       }); | ||||
|                        | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'Cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'Restart', | ||||
|             iconName: 'refresh-cw', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg) => { | ||||
|               const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment; | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Restart Deployment`, | ||||
|                 content: html` | ||||
|                   <div style="text-align:center"> | ||||
|                     Are you sure you want to restart this deployment? | ||||
|                   </div> | ||||
|                   <div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;"> | ||||
|                     <div style="color: #fff; font-weight: bold;"> | ||||
|                       ${this.getServiceName(deployment.serviceId)} | ||||
|                     </div> | ||||
|                     <div style="color: #aaa; font-size: 0.9em; margin-top: 4px;"> | ||||
|                       Node: ${this.getNodeName(deployment.nodeId)} | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'Cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'Restart', | ||||
|                     action: async (modalArg) => { | ||||
|                       // TODO: Implement restart action | ||||
|                       console.log('Restart deployment:', deployment); | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'Stop', | ||||
|             iconName: 'square', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg) => { | ||||
|               const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment; | ||||
|               await appstate.dataState.dispatchAction(appstate.updateDeploymentAction, { | ||||
|                 deploymentId: deployment.id, | ||||
|                 deploymentData: { | ||||
|                   ...deployment, | ||||
|                   status: 'stopped', | ||||
|                 }, | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'Delete', | ||||
|             iconName: 'trash', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg) => { | ||||
|               const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment; | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Delete Deployment`, | ||||
|                 content: html` | ||||
|                   <div style="text-align:center"> | ||||
|                     Are you sure you want to delete this deployment? | ||||
|                   </div> | ||||
|                   <div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;"> | ||||
|                     <div style="color: #fff; font-weight: bold;"> | ||||
|                       ${this.getServiceName(deployment.serviceId)} | ||||
|                     </div> | ||||
|                     <div style="color: #aaa; font-size: 0.9em; margin-top: 4px;"> | ||||
|                       Node: ${this.getNodeName(deployment.nodeId)} | ||||
|                     </div> | ||||
|                     <div style="color: #f44336; margin-top: 8px;"> | ||||
|                       This action cannot be undone. | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'Cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'Delete', | ||||
|                     action: async (modalArg) => { | ||||
|                       await appstate.dataState.dispatchAction(appstate.deleteDeploymentAction, { | ||||
|                         deploymentId: deployment.id, | ||||
|                       }); | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
| @@ -1,429 +0,0 @@ | ||||
| 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-dns') | ||||
| export class CloudlyViewDns extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = { | ||||
|     secretGroups: [], | ||||
|     secretBundles: [], | ||||
|     dnsEntries: [], | ||||
|     domains: [], | ||||
|   }; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subscription = appstate.dataState | ||||
|       .select((stateArg) => stateArg) | ||||
|       .subscribe((dataArg) => { | ||||
|         this.data = dataArg; | ||||
|       }); | ||||
|     this.rxSubscriptions.push(subscription); | ||||
|   } | ||||
|  | ||||
|   async connectedCallback() { | ||||
|     super.connectedCallback(); | ||||
|     // Load all data including domains and DNS entries | ||||
|     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 (dataActionArg) => { | ||||
|               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) => { | ||||
|                       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: '', // Will be set by backend from domain | ||||
|                           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: '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-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) => { | ||||
|                       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: '', // Will be set by backend from domain | ||||
|                           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 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) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'Delete', | ||||
|                     action: async (modalArg) => { | ||||
|                       await appstate.dataState.dispatchAction(appstate.deleteDnsEntryAction, { | ||||
|                         dnsEntryId: dnsEntry.id, | ||||
|                       }); | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
| @@ -1,529 +0,0 @@ | ||||
| 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> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
| @@ -1,436 +0,0 @@ | ||||
| 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-externalregistries') | ||||
| export class CloudlyViewExternalRegistries extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = { | ||||
|     secretGroups: [], | ||||
|     secretBundles: [], | ||||
|     externalRegistries: [], | ||||
|   }; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subscription = appstate.dataState | ||||
|       .select((stateArg) => stateArg) | ||||
|       .subscribe((dataArg) => { | ||||
|         this.data = dataArg; | ||||
|       }); | ||||
|     this.rxSubscriptions.push(subscription); | ||||
|   } | ||||
|  | ||||
|   async connectedCallback() { | ||||
|     super.connectedCallback(); | ||||
|     // Load external registries | ||||
|     await appstate.dataState.dispatchAction(appstate.getAllDataAction, {}); | ||||
|   } | ||||
|  | ||||
|   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-inactive { background: #9E9E9E; } | ||||
|       .status-error { background: #f44336; } | ||||
|       .status-unverified { background: #FF9800; } | ||||
|        | ||||
|       .type-badge { | ||||
|         display: inline-block; | ||||
|         padding: 2px 8px; | ||||
|         border-radius: 4px; | ||||
|         font-size: 0.85em; | ||||
|         font-weight: 500; | ||||
|         color: white; | ||||
|       } | ||||
|       .type-docker { background: #2196F3; } | ||||
|       .type-npm { background: #CB3837; } | ||||
|        | ||||
|       .default-badge { | ||||
|         display: inline-block; | ||||
|         padding: 2px 8px; | ||||
|         border-radius: 4px; | ||||
|         font-size: 0.85em; | ||||
|         font-weight: 500; | ||||
|         background: #673AB7; | ||||
|         color: white; | ||||
|         margin-left: 8px; | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>External Registries</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'External Registries'} | ||||
|         .heading2=${'Configure external Docker and NPM registries'} | ||||
|         .data=${this.data.externalRegistries || []} | ||||
|         .displayFunction=${(registry: plugins.interfaces.data.IExternalRegistry) => { | ||||
|           return { | ||||
|             Name: html`${registry.data.name}${registry.data.isDefault ? html`<span class="default-badge">DEFAULT</span>` : ''}`, | ||||
|             Type: html`<span class="type-badge type-${registry.data.type}">${registry.data.type.toUpperCase()}</span>`, | ||||
|             URL: registry.data.url, | ||||
|             Auth: registry.data.authType === 'none' ? 'Public' : (registry.data.username || 'Token Auth'), | ||||
|             Namespace: registry.data.namespace || '-', | ||||
|             Status: html`<span class="status-badge status-${registry.data.status || 'unverified'}">${(registry.data.status || 'unverified').toUpperCase()}</span>`, | ||||
|             'Last Verified': registry.data.lastVerified ? new Date(registry.data.lastVerified).toLocaleString() : 'Never', | ||||
|           }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { | ||||
|             name: 'Add Registry', | ||||
|             iconName: 'plus', | ||||
|             type: ['header', 'footer'], | ||||
|             actionFunc: async (dataActionArg) => { | ||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Add External Registry', | ||||
|                 content: html` | ||||
|                   <dees-form> | ||||
|                     <dees-input-dropdown  | ||||
|                       .key=${'type'}  | ||||
|                       .label=${'Registry Type'}  | ||||
|                       .options=${[ | ||||
|                         {key: 'docker', option: 'Docker'}, | ||||
|                         {key: 'npm', option: 'NPM'} | ||||
|                       ]} | ||||
|                       .value=${'docker'} | ||||
|                       .required=${true}> | ||||
|                     </dees-input-dropdown> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'name'}  | ||||
|                       .label=${'Registry Name'}  | ||||
|                       .placeholder=${'My Docker Hub'} | ||||
|                       .required=${true}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'url'}  | ||||
|                       .label=${'Registry URL'}  | ||||
|                       .placeholder=${'https://index.docker.io/v2/ or registry.gitlab.com'} | ||||
|                       .required=${true}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'username'}  | ||||
|                       .label=${'Username (only needed for basic auth)'}  | ||||
|                       .placeholder=${'username or leave empty for token auth'}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'password'}  | ||||
|                       .label=${'Password / Token (NPM _authToken, Docker access token, etc.)'}  | ||||
|                       .placeholder=${'Token or password'} | ||||
|                       .isPasswordBool=${true}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'namespace'}  | ||||
|                       .label=${'Namespace/Organization (optional)'}  | ||||
|                       .placeholder=${'myorg'}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'description'}  | ||||
|                       .label=${'Description (optional)'}  | ||||
|                       .placeholder=${'Production Docker registry'}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-dropdown  | ||||
|                       .key=${'authType'}  | ||||
|                       .label=${'Authentication Type'}  | ||||
|                       .options=${[ | ||||
|                         {key: 'none', option: 'No Authentication (Public Registry)'}, | ||||
|                         {key: 'basic', option: 'Basic Auth (Username + Password)'}, | ||||
|                         {key: 'token', option: 'Token Only (NPM, GitHub, GitLab tokens)'}, | ||||
|                         {key: 'oauth2', option: 'OAuth2 (Advanced)'} | ||||
|                       ]} | ||||
|                       .value=${'none'}> | ||||
|                     </dees-input-dropdown> | ||||
|                     <dees-input-checkbox  | ||||
|                       .key=${'isDefault'}  | ||||
|                       .label=${'Set as default registry for this type'}  | ||||
|                       .value=${false}> | ||||
|                     </dees-input-checkbox> | ||||
|                     <dees-input-checkbox  | ||||
|                       .key=${'insecure'}  | ||||
|                       .label=${'Allow insecure connections (HTTP/self-signed certs)'}  | ||||
|                       .value=${false}> | ||||
|                     </dees-input-checkbox> | ||||
|                   </dees-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'Create Registry', | ||||
|                     action: async (modalArg) => { | ||||
|                       const form = modalArg.shadowRoot.querySelector('dees-form') as any; | ||||
|                       const formData = await form.gatherData(); | ||||
|                        | ||||
|                       await appstate.dataState.dispatchAction(appstate.createExternalRegistryAction, { | ||||
|                         registryData: { | ||||
|                           type: formData.type, | ||||
|                           name: formData.name, | ||||
|                           url: formData.url, | ||||
|                           username: formData.username, | ||||
|                           password: formData.password, | ||||
|                           namespace: formData.namespace || undefined, | ||||
|                           description: formData.description || undefined, | ||||
|                           authType: formData.authType, | ||||
|                           isDefault: formData.isDefault, | ||||
|                           insecure: formData.insecure, | ||||
|                         }, | ||||
|                       }); | ||||
|                        | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'Cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'Edit', | ||||
|             iconName: 'edit', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg) => { | ||||
|               const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry; | ||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Edit Registry: ${registry.data.name}`, | ||||
|                 content: html` | ||||
|                   <dees-form> | ||||
|                     <dees-input-dropdown  | ||||
|                       .key=${'type'}  | ||||
|                       .label=${'Registry Type'}  | ||||
|                       .options=${[ | ||||
|                         {key: 'docker', option: 'Docker'}, | ||||
|                         {key: 'npm', option: 'NPM'} | ||||
|                       ]} | ||||
|                       .value=${registry.data.type} | ||||
|                       .required=${true}> | ||||
|                     </dees-input-dropdown> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'name'}  | ||||
|                       .label=${'Registry Name'}  | ||||
|                       .value=${registry.data.name} | ||||
|                       .required=${true}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'url'}  | ||||
|                       .label=${'Registry URL'}  | ||||
|                       .value=${registry.data.url} | ||||
|                       .required=${true}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'username'}  | ||||
|                       .label=${'Username (only needed for basic auth)'}  | ||||
|                       .value=${registry.data.username || ''} | ||||
|                       .placeholder=${'Leave empty for token auth'}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'password'}  | ||||
|                       .label=${'Password / Token (leave empty to keep current)'}  | ||||
|                       .placeholder=${'New token or password'} | ||||
|                       .isPasswordBool=${true}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'namespace'}  | ||||
|                       .label=${'Namespace/Organization (optional)'}  | ||||
|                       .value=${registry.data.namespace || ''}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'description'}  | ||||
|                       .label=${'Description (optional)'}  | ||||
|                       .value=${registry.data.description || ''}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-dropdown  | ||||
|                       .key=${'authType'}  | ||||
|                       .label=${'Authentication Type'}  | ||||
|                       .options=${[ | ||||
|                         {key: 'none', option: 'No Authentication (Public Registry)'}, | ||||
|                         {key: 'basic', option: 'Basic Auth (Username + Password)'}, | ||||
|                         {key: 'token', option: 'Token Only (NPM, GitHub, GitLab tokens)'}, | ||||
|                         {key: 'oauth2', option: 'OAuth2 (Advanced)'} | ||||
|                       ]} | ||||
|                       .value=${registry.data.authType || 'none'}> | ||||
|                     </dees-input-dropdown> | ||||
|                     <dees-input-checkbox  | ||||
|                       .key=${'isDefault'}  | ||||
|                       .label=${'Set as default registry for this type'}  | ||||
|                       .value=${registry.data.isDefault || false}> | ||||
|                     </dees-input-checkbox> | ||||
|                     <dees-input-checkbox  | ||||
|                       .key=${'insecure'}  | ||||
|                       .label=${'Allow insecure connections (HTTP/self-signed certs)'}  | ||||
|                       .value=${registry.data.insecure || false}> | ||||
|                     </dees-input-checkbox> | ||||
|                   </dees-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'Update Registry', | ||||
|                     action: async (modalArg) => { | ||||
|                       const form = modalArg.shadowRoot.querySelector('dees-form') as any; | ||||
|                       const formData = await form.gatherData(); | ||||
|                        | ||||
|                       const updateData: any = { | ||||
|                         type: formData.type, | ||||
|                         name: formData.name, | ||||
|                         url: formData.url, | ||||
|                         username: formData.username, | ||||
|                         namespace: formData.namespace || undefined, | ||||
|                         description: formData.description || undefined, | ||||
|                         authType: formData.authType, | ||||
|                         isDefault: formData.isDefault, | ||||
|                         insecure: formData.insecure, | ||||
|                       }; | ||||
|                        | ||||
|                       // Only include password if it was changed | ||||
|                       if (formData.password) { | ||||
|                         updateData.password = formData.password; | ||||
|                       } else { | ||||
|                         updateData.password = registry.data.password; | ||||
|                       } | ||||
|                        | ||||
|                       await appstate.dataState.dispatchAction(appstate.updateExternalRegistryAction, { | ||||
|                         registryId: registry.id, | ||||
|                         registryData: updateData, | ||||
|                       }); | ||||
|                        | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'Cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'Test Connection', | ||||
|             iconName: 'check-circle', | ||||
|             type: ['contextmenu'], | ||||
|             actionFunc: async (actionDataArg) => { | ||||
|               const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry; | ||||
|                | ||||
|               // Show loading modal | ||||
|               const loadingModal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Testing Registry Connection', | ||||
|                 content: html` | ||||
|                   <div style="text-align: center; padding: 20px;"> | ||||
|                     <dees-spinner></dees-spinner> | ||||
|                     <p style="margin-top: 20px;">Testing connection to ${registry.data.name}...</p> | ||||
|                   </div> | ||||
|                 `, | ||||
|                 menuOptions: [], | ||||
|               }); | ||||
|                | ||||
|               // Test the connection | ||||
|               await appstate.dataState.dispatchAction(appstate.verifyExternalRegistryAction, { | ||||
|                 registryId: registry.id, | ||||
|               }); | ||||
|                | ||||
|               // Close loading modal | ||||
|               await loadingModal.destroy(); | ||||
|                | ||||
|               // Get updated registry | ||||
|               const updatedRegistry = this.data.externalRegistries?.find(r => r.id === registry.id); | ||||
|                | ||||
|               // Show result modal | ||||
|               const resultModal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Connection Test Result', | ||||
|                 content: html` | ||||
|                   <div style="text-align: center; padding: 20px;"> | ||||
|                     ${updatedRegistry?.data.status === 'active' ? html` | ||||
|                       <div style="color: #4CAF50; font-size: 48px;">✓</div> | ||||
|                       <p style="margin-top: 20px; color: #4CAF50;">Connection successful!</p> | ||||
|                     ` : html` | ||||
|                       <div style="color: #f44336; font-size: 48px;">✗</div> | ||||
|                       <p style="margin-top: 20px; color: #f44336;">Connection failed!</p> | ||||
|                       ${updatedRegistry?.data.lastError ? html` | ||||
|                         <p style="margin-top: 10px; font-size: 0.9em; color: #999;"> | ||||
|                           Error: ${updatedRegistry.data.lastError} | ||||
|                         </p> | ||||
|                       ` : ''} | ||||
|                     `} | ||||
|                   </div> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'OK', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'Delete', | ||||
|             iconName: 'trash', | ||||
|             type: ['contextmenu'], | ||||
|             actionFunc: async (actionDataArg) => { | ||||
|               const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry; | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Delete Registry: ${registry.data.name}`, | ||||
|                 content: html` | ||||
|                   <div style="text-align:center"> | ||||
|                     <p>Do you really want to delete this external registry?</p> | ||||
|                     <p style="color: #999; font-size: 0.9em; margin-top: 10px;"> | ||||
|                       This will remove all stored credentials and configuration. | ||||
|                     </p> | ||||
|                   </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;" | ||||
|                   > | ||||
|                     ${registry.data.name} (${registry.data.url}) | ||||
|                   </div> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'Cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'Delete', | ||||
|                     action: async (modalArg) => { | ||||
|                       await appstate.dataState.dispatchAction(appstate.deleteExternalRegistryAction, { | ||||
|                         registryId: registry.id, | ||||
|                       }); | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
| @@ -1,292 +0,0 @@ | ||||
| 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-images') | ||||
| export class CloudlyViewImages extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = {}; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     appstate.dataState | ||||
|       .select((stateArg) => stateArg) | ||||
|       .subscribe((dataArg) => { | ||||
|         this.data = dataArg; | ||||
|       }); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css` | ||||
|        | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Images</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         heading1="Images" | ||||
|         heading2="an image is needed for running a service" | ||||
|         .data=${this.data.images} | ||||
|         .displayFunction=${(image: plugins.interfaces.data.IImage) => { | ||||
|           return { | ||||
|             id: image.id, | ||||
|             name: image.data.name, | ||||
|             description: image.data.description, | ||||
|             versions: image.data.versions.length, | ||||
|           }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { | ||||
|             name: 'create Image', | ||||
|             type: ['header', 'footer'], | ||||
|             iconName: 'plus', | ||||
|             actionFunc: async () => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'create new Image', | ||||
|                 content: html` | ||||
|                   <dees-form> | ||||
|                     <dees-input-text | ||||
|                       .label=${'name'} | ||||
|                       .key=${'data.name'} | ||||
|                       .value=${''} | ||||
|                     ></dees-input-text> | ||||
|                     <dees-input-text | ||||
|                       .label=${'description'} | ||||
|                       .key=${'data.description'} | ||||
|                       .value=${''} | ||||
|                     ></dees-input-text> | ||||
|                   </dees-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'save', | ||||
|                     action: async (modalArg) => { | ||||
|                       const deesForm = modalArg.shadowRoot.querySelector('dees-form'); | ||||
|                       const formData = await deesForm.collectFormData(); | ||||
|                       console.log(`Prepare saving of data:`); | ||||
|                       console.log(formData); | ||||
|                       await appstate.dataState.dispatchAction(appstate.createImageAction, { | ||||
|                         imageName: formData['data.name'] as string, | ||||
|                         description: formData['data.description'] as string, | ||||
|                       }); | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'edit', | ||||
|             type: ['contextmenu', 'inRow', 'doubleClick'], | ||||
|             iconName: 'penToSquare', | ||||
|             actionFunc: async ( | ||||
|               dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup> | ||||
|             ) => { | ||||
|               const environmentsArray: Array< | ||||
|                 plugins.interfaces.data.ISecretGroup['data']['environments'][any] & { | ||||
|                   environment: string; | ||||
|                 } | ||||
|               > = []; | ||||
|               for (const environmentName of Object.keys(dataArg.item.data.environments)) { | ||||
|                 environmentsArray.push({ | ||||
|                   environment: environmentName, | ||||
|                   ...dataArg.item.data.environments[environmentName], | ||||
|                 }); | ||||
|               } | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Edit Secret', | ||||
|                 content: html` | ||||
|                   <dees-form> | ||||
|                     <dees-input-text | ||||
|                       .key=${'id'} | ||||
|                       .disabled=${true} | ||||
|                       .label=${'ID'} | ||||
|                       .value=${dataArg.item.id} | ||||
|                     ></dees-input-text> | ||||
|                     <dees-input-text | ||||
|                       .key=${'data.name'} | ||||
|                       .disabled=${false} | ||||
|                       .label=${'name'} | ||||
|                       .value=${dataArg.item.data.name} | ||||
|                     ></dees-input-text> | ||||
|                     <dees-input-text | ||||
|                       .key=${'data.description'} | ||||
|                       .disabled=${false} | ||||
|                       .label=${'description'} | ||||
|                       .value=${dataArg.item.data.description} | ||||
|                     ></dees-input-text> | ||||
|                     <dees-input-text | ||||
|                       .key=${'data.key'} | ||||
|                       .disabled=${false} | ||||
|                       .label=${'key'} | ||||
|                       .value=${dataArg.item.data.key} | ||||
|                     ></dees-input-text> | ||||
|                     <dees-table | ||||
|                       .key=${'environments'} | ||||
|                       .heading1=${'Environments'} | ||||
|                       .heading2=${'double-click to edit values'} | ||||
|                       .data=${environmentsArray.map((itemArg) => { | ||||
|                         return { | ||||
|                           environment: itemArg.environment, | ||||
|                           value: itemArg.value, | ||||
|                         }; | ||||
|                       })} | ||||
|                       .editableFields=${['environment', 'value']} | ||||
|                       .dataActions=${[ | ||||
|                         { | ||||
|                           name: 'delete', | ||||
|                           iconName: 'trash', | ||||
|                           type: ['inRow'], | ||||
|                           actionFunc: async (actionDataArg) => { | ||||
|                             actionDataArg.table.data.splice( | ||||
|                               actionDataArg.table.data.indexOf(actionDataArg.item), | ||||
|                               1 | ||||
|                             ); | ||||
|                           }, | ||||
|                         }, | ||||
|                       ] as plugins.deesCatalog.ITableAction[]} | ||||
|                     ></dees-table> | ||||
|                   </dees-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'Cancel', | ||||
|                     iconName: null, | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'Save', | ||||
|                     iconName: null, | ||||
|                     action: async (modalArg) => { | ||||
|                       const data = await modalArg.shadowRoot | ||||
|                         .querySelector('dees-form') | ||||
|                         .collectFormData(); | ||||
|                       console.log(data); | ||||
|                       const updatedSecretGroup: plugins.interfaces.data.ISecretGroup = { | ||||
|                         id: dataArg.item.id, | ||||
|                         data: { | ||||
|                           name: data['data.name'] as string, | ||||
|                           description: data['data.description'] as string, | ||||
|                           key: data['data.key'] as string, | ||||
|                           environments: {}, | ||||
|                           tags: dataArg.item.data.tags, | ||||
|                         }, | ||||
|                       }; | ||||
|                       const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] = | ||||
|                         {}; | ||||
|                       for (const itemArg of data['environments'] as any[]) { | ||||
|                       } | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'history', | ||||
|             iconName: 'clockRotateLeft', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async ( | ||||
|               dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup> | ||||
|             ) => { | ||||
|               const historyArray: Array<{ | ||||
|                 environment: string; | ||||
|                 value: string; | ||||
|               }> = []; | ||||
|               for (const environment of Object.keys(dataArg.item.data.environments)) { | ||||
|                 for (const historyItem of dataArg.item.data.environments[environment].history) { | ||||
|                   historyArray.push({ | ||||
|                     environment, | ||||
|                     value: historyItem.value, | ||||
|                   }); | ||||
|                 } | ||||
|               } | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `history for ${dataArg.item.data.key}`, | ||||
|                 content: html` | ||||
|                   <dees-table | ||||
|                     .data=${historyArray} | ||||
|                     .dataActions=${[ | ||||
|                       { | ||||
|                         name: 'delete', | ||||
|                         iconName: 'trash', | ||||
|                         type: ['contextmenu', 'inRow'], | ||||
|                         actionFunc: async ( | ||||
|                           itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]> | ||||
|                         ) => { | ||||
|                           console.log('delete', itemArg); | ||||
|                         }, | ||||
|                       }, | ||||
|                     ] as plugins.deesCatalog.ITableAction[]} | ||||
|                   ></dees-table> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'close', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'delete', | ||||
|             iconName: 'trash', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async ( | ||||
|               itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.IImage> | ||||
|             ) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Delete Image "${itemArg.item.data.name}"`, | ||||
|                 content: html` | ||||
|                   <div style="text-align:center">Do you really want to delete the image?</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;" | ||||
|                   > | ||||
|                     ${itemArg.item.id} | ||||
|                   </div> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'delete', | ||||
|                     action: async (modalArg) => { | ||||
|                       console.log(`Delete ${itemArg.item.id}`); | ||||
|                       await appstate.dataState.dispatchAction(appstate.deleteImageAction, { | ||||
|                         imageId: itemArg.item.id, | ||||
|                       }); | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
| @@ -1,130 +0,0 @@ | ||||
| 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-logs') | ||||
| export class CloudlyViewLogs extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = { | ||||
|     secretGroups: [], | ||||
|     secretBundles: [], | ||||
|   }; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subecription = appstate.dataState | ||||
|       .select((stateArg) => stateArg) | ||||
|       .subscribe((dataArg) => { | ||||
|         this.data = dataArg; | ||||
|       }); | ||||
|     this.rxSubscriptions.push(subecription); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css` | ||||
|        | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Logs</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'Logs'} | ||||
|         .heading2=${'decoded in client'} | ||||
|         .data=${this.data.deployments} | ||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { | ||||
|           return { | ||||
|             id: itemArg.id, | ||||
|             serverAmount: itemArg.data.servers.length, | ||||
|           }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { | ||||
|             name: 'add configBundle', | ||||
|             iconName: 'plus', | ||||
|             type: ['header', 'footer'], | ||||
|             actionFunc: async (dataActionArg) => { | ||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Add ConfigBundle', | ||||
|                 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-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { name: 'create', action: async (modalArg) => {} }, | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'delete', | ||||
|             iconName: 'trash', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Delete ConfigBundle ${actionDataArg.item.id}`, | ||||
|                 content: html` | ||||
|                   <div style="text-align:center"> | ||||
|                     Do you really want to delete the ConfigBundle? | ||||
|                   </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> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'delete', | ||||
|                     action: async (modalArg) => { | ||||
|                       appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { | ||||
|                         configBundleId: actionDataArg.item.id, | ||||
|                       }); | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
| @@ -1,128 +0,0 @@ | ||||
| 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-mails') | ||||
| export class CloudlyViewMails extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = { | ||||
|     secretGroups: [], | ||||
|     secretBundles: [], | ||||
|   }; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subecription = appstate.dataState | ||||
|       .select((stateArg) => stateArg) | ||||
|       .subscribe((dataArg) => { | ||||
|         this.data = dataArg; | ||||
|       }); | ||||
|     this.rxSubscriptions.push(subecription); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css`` | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Mails</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'Mails'} | ||||
|         .heading2=${'decoded in client'} | ||||
|         .data=${this.data.deployments} | ||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { | ||||
|           return { | ||||
|             id: itemArg.id, | ||||
|             serverAmount: itemArg.data.servers.length, | ||||
|           }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { | ||||
|             name: 'add configBundle', | ||||
|             iconName: 'plus', | ||||
|             type: ['header', 'footer'], | ||||
|             actionFunc: async (dataActionArg) => { | ||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Add ConfigBundle', | ||||
|                 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-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { name: 'create', action: async (modalArg) => {} }, | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'delete', | ||||
|             iconName: 'trash', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Delete ConfigBundle ${actionDataArg.item.id}`, | ||||
|                 content: html` | ||||
|                   <div style="text-align:center"> | ||||
|                     Do you really want to delete the ConfigBundle? | ||||
|                   </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> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'delete', | ||||
|                     action: async (modalArg) => { | ||||
|                       appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { | ||||
|                         configBundleId: actionDataArg.item.id, | ||||
|                       }); | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
| @@ -1,156 +0,0 @@ | ||||
| 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-overview') | ||||
| export class CloudlyViewOverview extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = { | ||||
|     secretGroups: [], | ||||
|     secretBundles: [], | ||||
|   }; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subecription = appstate.dataState | ||||
|       .select((stateArg) => stateArg) | ||||
|       .subscribe((dataArg) => { | ||||
|         this.data = dataArg; | ||||
|       }); | ||||
|     this.rxSubscriptions.push(subecription); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css` | ||||
|       dees-statsgrid { | ||||
|         margin-top: 24px; | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     // Calculate total nodes across all clusters | ||||
|     const totalNodes = this.data.clusters?.reduce((sum, cluster) =>  | ||||
|       sum + (cluster.data.nodes?.length || 0), 0) || 0; | ||||
|  | ||||
|     // Create tiles for the stats grid | ||||
|     const statsTiles = [ | ||||
|       { | ||||
|         id: 'clusters', | ||||
|         title: 'Total Clusters', | ||||
|         value: this.data.clusters?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Network', | ||||
|         description: 'Active clusters' | ||||
|       }, | ||||
|       { | ||||
|         id: 'nodes', | ||||
|         title: 'Total Nodes', | ||||
|         value: totalNodes, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Server', | ||||
|         description: 'Connected nodes' | ||||
|       }, | ||||
|       { | ||||
|         id: 'services', | ||||
|         title: 'Services', | ||||
|         value: this.data.services?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Layers', | ||||
|         description: 'Deployed services' | ||||
|       }, | ||||
|       { | ||||
|         id: 'deployments', | ||||
|         title: 'Deployments', | ||||
|         value: this.data.deployments?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Rocket', | ||||
|         description: 'Active deployments' | ||||
|       }, | ||||
|       { | ||||
|         id: 'secretGroups', | ||||
|         title: 'Secret Groups', | ||||
|         value: this.data.secretGroups?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:ShieldCheck', | ||||
|         description: 'Configured secret groups' | ||||
|       }, | ||||
|       { | ||||
|         id: 'secretBundles', | ||||
|         title: 'Secret Bundles', | ||||
|         value: this.data.secretBundles?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:LockKeyhole', | ||||
|         description: 'Available secret bundles' | ||||
|       }, | ||||
|       { | ||||
|         id: 'images', | ||||
|         title: 'Images', | ||||
|         value: this.data.images?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Image', | ||||
|         description: 'Container images' | ||||
|       }, | ||||
|       { | ||||
|         id: 'dns', | ||||
|         title: 'DNS Entries', | ||||
|         value: this.data.dnsEntries?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Globe', | ||||
|         description: 'Managed DNS records' | ||||
|       }, | ||||
|       { | ||||
|         id: 'databases', | ||||
|         title: 'Databases', | ||||
|         value: this.data.dbs?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Database', | ||||
|         description: 'Database instances' | ||||
|       }, | ||||
|       { | ||||
|         id: 'backups', | ||||
|         title: 'Backups', | ||||
|         value: this.data.backups?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Save', | ||||
|         description: 'Available backups' | ||||
|       }, | ||||
|       { | ||||
|         id: 'mails', | ||||
|         title: 'Mail Domains', | ||||
|         value: this.data.mails?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Mail', | ||||
|         description: 'Mail configurations' | ||||
|       }, | ||||
|       { | ||||
|         id: 's3', | ||||
|         title: 'S3 Buckets', | ||||
|         value: this.data.s3?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Cloud', | ||||
|         description: 'Storage buckets' | ||||
|       } | ||||
|     ]; | ||||
|  | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Overview</cloudly-sectionheading> | ||||
|       <dees-statsgrid | ||||
|         .tiles=${statsTiles} | ||||
|         .minTileWidth=${250} | ||||
|         .gap=${16} | ||||
|       ></dees-statsgrid> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
| @@ -1,130 +0,0 @@ | ||||
| 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-s3') | ||||
| export class CloudlyViewS3 extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = { | ||||
|     secretGroups: [], | ||||
|     secretBundles: [], | ||||
|   }; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subecription = appstate.dataState | ||||
|       .select((stateArg) => stateArg) | ||||
|       .subscribe((dataArg) => { | ||||
|         this.data = dataArg; | ||||
|       }); | ||||
|     this.rxSubscriptions.push(subecription); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css` | ||||
|        | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>S3</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'S3'} | ||||
|         .heading2=${'decoded in client'} | ||||
|         .data=${this.data.s3} | ||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { | ||||
|           return { | ||||
|             id: itemArg.id, | ||||
|             serverAmount: itemArg.data.servers.length, | ||||
|           }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { | ||||
|             name: 'add configBundle', | ||||
|             iconName: 'plus', | ||||
|             type: ['header', 'footer'], | ||||
|             actionFunc: async (dataActionArg) => { | ||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Add ConfigBundle', | ||||
|                 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-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { name: 'create', action: async (modalArg) => {} }, | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'delete', | ||||
|             iconName: 'trash', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Delete ConfigBundle ${actionDataArg.item.id}`, | ||||
|                 content: html` | ||||
|                   <div style="text-align:center"> | ||||
|                     Do you really want to delete the ConfigBundle? | ||||
|                   </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> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'delete', | ||||
|                     action: async (modalArg) => { | ||||
|                       appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { | ||||
|                         configBundleId: actionDataArg.item.id, | ||||
|                       }); | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
| @@ -1,167 +0,0 @@ | ||||
| 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-secretbundles') | ||||
| export class CloudlyViewSecretBundles extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = {}; | ||||
|  | ||||
|   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` | ||||
|        | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>SecretBundles</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'SecretBundles'} | ||||
|         .heading2=${'decoded in client'} | ||||
|         .data=${this.data.secretBundles || []} | ||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ISecretBundle) => { | ||||
|           return { | ||||
|             name: itemArg.data.name, | ||||
|             secretGroups: (() => { | ||||
|               const secretGroupIds = itemArg.data.includedSecretGroupIds; | ||||
|               let secretGroupNames: string[] = []; | ||||
|               for (const secretGroupId of secretGroupIds) { | ||||
|                 const secretGroup = this.data.secretGroups.find( | ||||
|                   (secretGroupArg) => secretGroupArg.id === secretGroupId | ||||
|                 ); | ||||
|                 if (secretGroup) { | ||||
|                   secretGroupNames.push(secretGroup.data.name); | ||||
|                 } | ||||
|               } | ||||
|               return secretGroupNames.join(', '); | ||||
|             })(), | ||||
|             tags: html`<dees-chips | ||||
|               .selectionMode=${'none'} | ||||
|               .selectableChips=${itemArg.data.includedTags} | ||||
|             ></dees-chips>`, | ||||
|           }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { | ||||
|             name: 'add SecretBundle', | ||||
|             iconName: 'plus', | ||||
|             type: ['header', 'footer'], | ||||
|             actionFunc: async (dataActionArg) => { | ||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Add SecretBundle', | ||||
|                 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-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { name: 'create', action: async (modalArg) => {} }, | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'delete', | ||||
|             iconName: 'trash', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Delete ConfigBundle ${actionDataArg.item.id}`, | ||||
|                 content: html` | ||||
|                   <div style="text-align:center"> | ||||
|                     Do you really want to delete the ConfigBundle? | ||||
|                   </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> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'delete', | ||||
|                     action: async (modalArg) => { | ||||
|                       appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { | ||||
|                         configBundleId: actionDataArg.item.id, | ||||
|                       }); | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'edit', | ||||
|             iconName: 'penToSquare', | ||||
|             type: ['doubleClick', 'contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg) => { | ||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Edit SecretBundle', | ||||
|                 content: html` | ||||
|                   <dees-form> | ||||
|                     <dees-input-text .label=${'purpose'}></dees-input-text> | ||||
|                   </dees-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { name: 'save', action: async (modalArg) => {} }, | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
| @@ -1,362 +0,0 @@ | ||||
| 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-secretsgroups') | ||||
| export class CloudlyViewSecretGroups extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = {}; | ||||
|  | ||||
|   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` | ||||
|        | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>SecretGroups</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         heading1="SecretGroups" | ||||
|         heading2="decoded in client" | ||||
|         .data=${this.data.secretGroups || []} | ||||
|         .displayFunction=${(secretGroup: plugins.interfaces.data.ISecretGroup) => { | ||||
|           return { | ||||
|             name: secretGroup.data.name, | ||||
|             priority: secretGroup.data.priority, | ||||
|             tags: html`<dees-chips | ||||
|               .selectionMode=${'none'} | ||||
|               .selectableChips=${secretGroup.data.tags} | ||||
|             ></dees-chips>`, | ||||
|             key: secretGroup.data.key, | ||||
|             history: (() => { | ||||
|               const allHistory = []; | ||||
|               for (const environment in secretGroup.data.environments) { | ||||
|                 allHistory.push(...secretGroup.data.environments[environment].history); | ||||
|               } | ||||
|               return allHistory.length; | ||||
|             })(), | ||||
|           }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { | ||||
|             name: 'add SecretGroup', | ||||
|             type: ['header', 'footer'], | ||||
|             iconName: 'plus', | ||||
|             actionFunc: async () => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'create new SecretGroup', | ||||
|                 content: html` | ||||
|                   <dees-form> | ||||
|                     <dees-input-text | ||||
|                       .label=${'name'} | ||||
|                       .key=${'data.name'} | ||||
|                       .value=${''} | ||||
|                     ></dees-input-text> | ||||
|                     <dees-input-text | ||||
|                       .label=${'description'} | ||||
|                       .key=${'data.description'} | ||||
|                       .value=${''} | ||||
|                     ></dees-input-text> | ||||
|                     <dees-input-text | ||||
|                       .label=${'Secret Key (data.key)'} | ||||
|                       .key=${'data.key'} | ||||
|                       .value=${''} | ||||
|                     ></dees-input-text> | ||||
|                     <dees-table | ||||
|                       .heading1=${'Environments'} | ||||
|                       .heading2=${'keys need to be unique'} | ||||
|                       key="environments" | ||||
|                       .data=${[ | ||||
|                         { | ||||
|                           environment: 'production', | ||||
|                           value: '', | ||||
|                         }, | ||||
|                         { | ||||
|                           environment: 'staging', | ||||
|                           value: '', | ||||
|                         }, | ||||
|                       ]} | ||||
|                       .dataActions=${[ | ||||
|                         { | ||||
|                           name: 'add environment', | ||||
|                           iconName: 'plus', | ||||
|                           type: ['footer'], | ||||
|                           actionFunc: async (dataArg) => { | ||||
|                             dataArg.table.data.push({ | ||||
|                               environment: 'new environment', | ||||
|                               value: '', | ||||
|                             }); | ||||
|                             dataArg.table.requestUpdate('data'); | ||||
|                           }, | ||||
|                         }, | ||||
|                         { | ||||
|                           name: 'delete environment', | ||||
|                           iconName: 'trash', | ||||
|                           type: ['inRow'], | ||||
|                           actionFunc: async (dataArg) => { | ||||
|                             dataArg.table.data.splice(dataArg.table.data.indexOf(dataArg.item), 1); | ||||
|                             dataArg.table.requestUpdate('data'); | ||||
|                           }, | ||||
|                         }, | ||||
|                       ] as plugins.deesCatalog.ITableAction[]} | ||||
|                       .editableFields=${['environment', 'value']} | ||||
|                     ></dees-table> | ||||
|                   </dees-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'save', | ||||
|                     action: async (modalArg) => { | ||||
|                       const deesForm = modalArg.shadowRoot.querySelector('dees-form'); | ||||
|                       const formData = await deesForm.collectFormData(); | ||||
|                       console.log(`Prepare saving of data:`); | ||||
|                       console.log(formData); | ||||
|                       const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] = | ||||
|                         {}; | ||||
|                       for (const itemArg of formData['environments'] as any[]) { | ||||
|                         environments[itemArg.environment] = { | ||||
|                           value: itemArg.value, | ||||
|                           history: [], | ||||
|                           lastUpdated: Date.now(), | ||||
|                         }; | ||||
|                       } | ||||
|                       await appstate.dataState.dispatchAction(appstate.createSecretGroupAction, { | ||||
|                         id: null, | ||||
|                         data: { | ||||
|                           name: formData['data.name'] as string, | ||||
|                           description: formData['data.description'] as string, | ||||
|                           key: formData['data.key'] as string, | ||||
|                           environments, | ||||
|                           tags: [], | ||||
|                         }, | ||||
|                       }); | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'edit', | ||||
|             type: ['contextmenu', 'inRow', 'doubleClick'], | ||||
|             iconName: 'penToSquare', | ||||
|             actionFunc: async ( | ||||
|               dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup> | ||||
|             ) => { | ||||
|               const environmentsArray: Array< | ||||
|                 plugins.interfaces.data.ISecretGroup['data']['environments'][any] & { | ||||
|                   environment: string; | ||||
|                 } | ||||
|               > = []; | ||||
|               for (const environmentName of Object.keys(dataArg.item.data.environments)) { | ||||
|                 environmentsArray.push({ | ||||
|                   environment: environmentName, | ||||
|                   ...dataArg.item.data.environments[environmentName], | ||||
|                 }); | ||||
|               } | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Edit Secret', | ||||
|                 content: html` | ||||
|                   <dees-form> | ||||
|                     <dees-input-text | ||||
|                       .key=${'id'} | ||||
|                       .disabled=${true} | ||||
|                       .label=${'ID'} | ||||
|                       .value=${dataArg.item.id} | ||||
|                     ></dees-input-text> | ||||
|                     <dees-input-text | ||||
|                       .key=${'data.name'} | ||||
|                       .disabled=${false} | ||||
|                       .label=${'name'} | ||||
|                       .value=${dataArg.item.data.name} | ||||
|                     ></dees-input-text> | ||||
|                     <dees-input-text | ||||
|                       .key=${'data.description'} | ||||
|                       .disabled=${false} | ||||
|                       .label=${'description'} | ||||
|                       .value=${dataArg.item.data.description} | ||||
|                     ></dees-input-text> | ||||
|                     <dees-input-text | ||||
|                       .key=${'data.key'} | ||||
|                       .disabled=${false} | ||||
|                       .label=${'key'} | ||||
|                       .value=${dataArg.item.data.key} | ||||
|                     ></dees-input-text> | ||||
|                     <dees-table | ||||
|                       .key=${'environments'} | ||||
|                       .heading1=${'Environments'} | ||||
|                       .heading2=${'double-click to edit values'} | ||||
|                       .data=${environmentsArray.map((itemArg) => { | ||||
|                         return { | ||||
|                           environment: itemArg.environment, | ||||
|                           value: itemArg.value, | ||||
|                         }; | ||||
|                       })} | ||||
|                       .editableFields=${['environment', 'value']} | ||||
|                       .dataActions=${[ | ||||
|                         { | ||||
|                           name: 'delete', | ||||
|                           iconName: 'trash', | ||||
|                           type: ['inRow'], | ||||
|                           actionFunc: async (actionDataArg) => { | ||||
|                             actionDataArg.table.data.splice( | ||||
|                               actionDataArg.table.data.indexOf(actionDataArg.item), | ||||
|                               1 | ||||
|                             ); | ||||
|                           }, | ||||
|                         }, | ||||
|                       ] as plugins.deesCatalog.ITableAction[]} | ||||
|                     ></dees-table> | ||||
|                   </dees-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'Cancel', | ||||
|                     iconName: null, | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'Save', | ||||
|                     iconName: null, | ||||
|                     action: async (modalArg) => { | ||||
|                       const data = await modalArg.shadowRoot | ||||
|                         .querySelector('dees-form') | ||||
|                         .collectFormData(); | ||||
|                       console.log(data); | ||||
|                       const updatedSecretGroup: plugins.interfaces.data.ISecretGroup = { | ||||
|                         id: dataArg.item.id, | ||||
|                         data: { | ||||
|                           name: data['data.name'] as string, | ||||
|                           description: data['data.description'] as string, | ||||
|                           key: data['data.key'] as string, | ||||
|                           environments: {}, | ||||
|                           tags: dataArg.item.data.tags, | ||||
|                         }, | ||||
|                       }; | ||||
|                       const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] = | ||||
|                         {}; | ||||
|                       for (const itemArg of data['environments'] as any[]) { | ||||
|                       } | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'history', | ||||
|             iconName: 'clockRotateLeft', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async ( | ||||
|               dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup> | ||||
|             ) => { | ||||
|               const historyArray: Array<{ | ||||
|                 environment: string; | ||||
|                 value: string; | ||||
|               }> = []; | ||||
|               for (const environment of Object.keys(dataArg.item.data.environments)) { | ||||
|                 for (const historyItem of dataArg.item.data.environments[environment].history) { | ||||
|                   historyArray.push({ | ||||
|                     environment, | ||||
|                     value: historyItem.value, | ||||
|                   }); | ||||
|                 } | ||||
|               } | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `history for ${dataArg.item.data.key}`, | ||||
|                 content: html` | ||||
|                   <dees-table | ||||
|                     .data=${historyArray} | ||||
|                     .dataActions=${[ | ||||
|                       { | ||||
|                         name: 'delete', | ||||
|                         iconName: 'trash', | ||||
|                         type: ['contextmenu', 'inRow'], | ||||
|                         actionFunc: async ( | ||||
|                           itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]> | ||||
|                         ) => { | ||||
|                           console.log('delete', itemArg); | ||||
|                         }, | ||||
|                       }, | ||||
|                     ] as plugins.deesCatalog.ITableAction[]} | ||||
|                   ></dees-table> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'close', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'delete', | ||||
|             iconName: 'trash', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async ( | ||||
|               itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup> | ||||
|             ) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Delete ${itemArg.item.data.key}`, | ||||
|                 content: html` | ||||
|                   <div style="text-align:center">Do you really want to delete the secret?</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;" | ||||
|                   > | ||||
|                     ${itemArg.item.data.key} | ||||
|                   </div> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'delete', | ||||
|                     action: async (modalArg) => { | ||||
|                       console.log(`Delete ${itemArg.item.id}`); | ||||
|                       await appstate.dataState.dispatchAction(appstate.deleteSecretGroupAction, { | ||||
|                         secretGroupId: itemArg.item.id, | ||||
|                       }); | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
| @@ -1,461 +0,0 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as shared from '../elements/shared/index.js'; | ||||
|  | ||||
| import { | ||||
|   DeesElement, | ||||
|   customElement, | ||||
|   html, | ||||
|   state, | ||||
|   css, | ||||
|   cssManager, | ||||
|   property, | ||||
| } from '@design.estate/dees-element'; | ||||
|  | ||||
| import * as appstate from '../appstate.js'; | ||||
|  | ||||
| @customElement('cloudly-view-settings') | ||||
| export class CloudlyViewSettings extends DeesElement { | ||||
|   @state() | ||||
|   private settings: plugins.interfaces.data.ICloudlySettingsMasked = {}; | ||||
|  | ||||
|   @state() | ||||
|   private isLoading = false; | ||||
|  | ||||
|   @state() | ||||
|   private testResults: {[key: string]: {success: boolean; message: string}} = {}; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     this.loadSettings(); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css` | ||||
|       .settings-container { | ||||
|         padding: 24px 0; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         gap: 16px; | ||||
|       } | ||||
|  | ||||
|       .provider-icon { | ||||
|         margin-right: 8px; | ||||
|         font-size: 20px; | ||||
|       } | ||||
|  | ||||
|       .test-status { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         gap: 12px; | ||||
|         margin-bottom: 16px; | ||||
|       } | ||||
|  | ||||
|       .test-status dees-button { | ||||
|         margin-left: auto; | ||||
|       } | ||||
|  | ||||
|       .loading-container { | ||||
|         display: flex; | ||||
|         justify-content: center; | ||||
|         padding: 48px; | ||||
|       } | ||||
|  | ||||
|       .actions-container { | ||||
|         display: flex; | ||||
|         justify-content: center; | ||||
|         margin-top: 24px; | ||||
|       } | ||||
|  | ||||
|       dees-panel { | ||||
|         margin-bottom: 16px; | ||||
|       } | ||||
|  | ||||
|       .form-grid { | ||||
|         display: grid; | ||||
|         grid-template-columns: 1fr 1fr; | ||||
|         gap: 16px; | ||||
|       } | ||||
|  | ||||
|       .form-grid.single { | ||||
|         grid-template-columns: 1fr; | ||||
|       } | ||||
|  | ||||
|       @media (max-width: 768px) { | ||||
|         .form-grid { | ||||
|           grid-template-columns: 1fr; | ||||
|         } | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   private async loadSettings() { | ||||
|     this.isLoading = true; | ||||
|     try { | ||||
|       // Use shared API client | ||||
|       const response = await appstate.apiClient.settings.getSettings(); | ||||
|       this.settings = response.settings; | ||||
|     } catch (error) { | ||||
|       console.error('Failed to load settings:', error); | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ | ||||
|         message: `Failed to load settings: ${error.message}`, | ||||
|         type: 'error', | ||||
|       }); | ||||
|     } finally { | ||||
|       this.isLoading = false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async saveSettings(formData: any) { | ||||
|     console.log('saveSettings called with formData:', formData); | ||||
|     this.isLoading = true; | ||||
|     try { | ||||
|       const updates: Partial<plugins.interfaces.data.ICloudlySettings> = {}; | ||||
|        | ||||
|       // Process form data | ||||
|       for (const [key, value] of Object.entries(formData)) { | ||||
|         console.log(`Processing ${key}:`, value); | ||||
|         if (value !== undefined && value !== '****' && !value?.toString().endsWith('****')) { | ||||
|           // Only update if value changed (not masked) | ||||
|           updates[key as keyof plugins.interfaces.data.ICloudlySettings] = value as string; | ||||
|         } | ||||
|       } | ||||
|       console.log('Updates to send:', updates); | ||||
|  | ||||
|       const response = await appstate.apiClient.settings.updateSettings(updates); | ||||
|  | ||||
|       if (response.success) { | ||||
|         plugins.deesCatalog.DeesToast.createAndShow({ | ||||
|           message: 'Settings saved successfully', | ||||
|           type: 'success', | ||||
|         }); | ||||
|         await this.loadSettings(); // Reload to get masked values | ||||
|       } else { | ||||
|         throw new Error(response.message); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       console.error('Failed to save settings:', error); | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ | ||||
|         message: `Failed to save settings: ${error.message}`, | ||||
|         type: 'error', | ||||
|       }); | ||||
|     } finally { | ||||
|       this.isLoading = false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async testConnection(provider: string) { | ||||
|     this.isLoading = true; | ||||
|     try { | ||||
|       const response = await appstate.apiClient.settings.testProviderConnection(provider); | ||||
|  | ||||
|       this.testResults = { | ||||
|         ...this.testResults, | ||||
|         [provider]: { | ||||
|           success: response.connectionValid, | ||||
|           message: response.message | ||||
|         } | ||||
|       }; | ||||
|  | ||||
|       // Show toast notification | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ | ||||
|         message: response.message, | ||||
|         type: response.connectionValid ? 'success' : 'error', | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       this.testResults = { | ||||
|         ...this.testResults, | ||||
|         [provider]: { | ||||
|           success: false, | ||||
|           message: `Test failed: ${error.message}` | ||||
|         } | ||||
|       }; | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ | ||||
|         message: `Connection test failed: ${error.message}`, | ||||
|         type: 'error', | ||||
|       }); | ||||
|     } finally { | ||||
|       this.isLoading = false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private renderProviderStatus(provider: string) { | ||||
|     const result = this.testResults[provider]; | ||||
|     if (!result) return ''; | ||||
|      | ||||
|     return html` | ||||
|       <dees-badge  | ||||
|         .type=${result.success ? 'success' : 'error'} | ||||
|         .text=${result.success ? 'Connected' : 'Failed'} | ||||
|       ></dees-badge> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   public render() { | ||||
|     if (this.isLoading && Object.keys(this.settings).length === 0) { | ||||
|       return html` | ||||
|         <div class="loading-container"> | ||||
|           <dees-spinner></dees-spinner> | ||||
|         </div> | ||||
|       `; | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Settings</cloudly-sectionheading> | ||||
|       <div class="settings-container"> | ||||
|         <dees-form @formData=${(e: CustomEvent) => { | ||||
|           console.log('formData event received:', e); | ||||
|           console.log('Event detail:', e.detail); | ||||
|           console.log('Event detail.data:', e.detail.data); | ||||
|           this.saveSettings(e.detail.data); | ||||
|         }}> | ||||
|              | ||||
|             <!-- Hetzner Cloud --> | ||||
|             <dees-panel  | ||||
|               .title=${'Hetzner Cloud'} | ||||
|               .subtitle=${'Configure Hetzner Cloud API access'} | ||||
|               .variant=${'outline'} | ||||
|             > | ||||
|               <div class="test-status"> | ||||
|                 ${this.renderProviderStatus('hetzner')} | ||||
|                 <dees-button | ||||
|                   .text=${'Test Connection'} | ||||
|                   .type=${'secondary'} | ||||
|                   @click=${(e: Event) => { | ||||
|                     e.preventDefault(); | ||||
|                     e.stopPropagation(); | ||||
|                     this.testConnection('hetzner'); | ||||
|                   }} | ||||
|                 ></dees-button> | ||||
|               </div> | ||||
|               <div class="form-grid single"> | ||||
|                 <dees-input-text | ||||
|                   .key=${'hetznerToken'} | ||||
|                   .label=${'API Token'} | ||||
|                   .value=${this.settings.hetznerToken || ''} | ||||
|                   .isPasswordBool=${true} | ||||
|                   .description=${'Your Hetzner Cloud API token for managing infrastructure'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|               </div> | ||||
|             </dees-panel> | ||||
|  | ||||
|             <!-- Cloudflare --> | ||||
|             <dees-panel  | ||||
|               .title=${'Cloudflare'} | ||||
|               .subtitle=${'Configure Cloudflare API access'} | ||||
|               .variant=${'outline'} | ||||
|             > | ||||
|               <div class="test-status"> | ||||
|                 ${this.renderProviderStatus('cloudflare')} | ||||
|                 <dees-button | ||||
|                   .text=${'Test Connection'} | ||||
|                   .type=${'secondary'} | ||||
|                   @click=${(e: Event) => { | ||||
|                     e.preventDefault(); | ||||
|                     e.stopPropagation(); | ||||
|                     this.testConnection('cloudflare'); | ||||
|                   }} | ||||
|                 ></dees-button> | ||||
|               </div> | ||||
|               <div class="form-grid single"> | ||||
|                 <dees-input-text | ||||
|                   .key=${'cloudflareToken'} | ||||
|                   .label=${'API Token'} | ||||
|                   .value=${this.settings.cloudflareToken || ''} | ||||
|                   .isPasswordBool=${true} | ||||
|                   .description=${'Cloudflare API token with DNS and Zone permissions'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|               </div> | ||||
|             </dees-panel> | ||||
|  | ||||
|             <!-- AWS --> | ||||
|             <dees-panel  | ||||
|               .title=${'Amazon Web Services'} | ||||
|               .subtitle=${'Configure AWS credentials'} | ||||
|               .variant=${'outline'} | ||||
|             > | ||||
|               <div class="test-status"> | ||||
|                 ${this.renderProviderStatus('aws')} | ||||
|                 <dees-button | ||||
|                   .text=${'Test Connection'} | ||||
|                   .type=${'secondary'} | ||||
|                   @click=${(e: Event) => { | ||||
|                     e.preventDefault(); | ||||
|                     e.stopPropagation(); | ||||
|                     this.testConnection('aws'); | ||||
|                   }} | ||||
|                 ></dees-button> | ||||
|               </div> | ||||
|               <div class="form-grid"> | ||||
|                 <dees-input-text | ||||
|                   .key=${'awsAccessKey'} | ||||
|                   .label=${'Access Key ID'} | ||||
|                   .value=${this.settings.awsAccessKey || ''} | ||||
|                   .isPasswordBool=${true} | ||||
|                   .description=${'AWS IAM access key identifier'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|                 <dees-input-text | ||||
|                   .key=${'awsSecretKey'} | ||||
|                   .label=${'Secret Access Key'} | ||||
|                   .value=${this.settings.awsSecretKey || ''} | ||||
|                   .isPasswordBool=${true} | ||||
|                   .description=${'AWS IAM secret access key'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|               </div> | ||||
|               <div class="form-grid single"> | ||||
|                 <dees-input-dropdown | ||||
|                   .key=${'awsRegion'} | ||||
|                   .label=${'Default Region'} | ||||
|                   .selectedOption=${this.settings.awsRegion || 'us-east-1'} | ||||
|                   .options=${[ | ||||
|                     { key: 'us-east-1', option: 'US East (N. Virginia)', payload: null }, | ||||
|                     { key: 'us-west-2', option: 'US West (Oregon)', payload: null }, | ||||
|                     { key: 'eu-west-1', option: 'EU (Ireland)', payload: null }, | ||||
|                     { key: 'eu-central-1', option: 'EU (Frankfurt)', payload: null }, | ||||
|                     { key: 'ap-southeast-1', option: 'Asia Pacific (Singapore)', payload: null }, | ||||
|                     { key: 'ap-northeast-1', option: 'Asia Pacific (Tokyo)', payload: null }, | ||||
|                   ]} | ||||
|                   .description=${'Default AWS region for resource provisioning'} | ||||
|                 ></dees-input-dropdown> | ||||
|               </div> | ||||
|             </dees-panel> | ||||
|  | ||||
|             <!-- DigitalOcean --> | ||||
|             <dees-panel  | ||||
|               .title=${'DigitalOcean'} | ||||
|               .subtitle=${'Configure DigitalOcean API access'} | ||||
|               .variant=${'outline'} | ||||
|             > | ||||
|               <div class="test-status"> | ||||
|                 ${this.renderProviderStatus('digitalocean')} | ||||
|                 <dees-button | ||||
|                   .text=${'Test Connection'} | ||||
|                   .type=${'secondary'} | ||||
|                   @click=${(e: Event) => { | ||||
|                     e.preventDefault(); | ||||
|                     e.stopPropagation(); | ||||
|                     this.testConnection('digitalocean'); | ||||
|                   }} | ||||
|                 ></dees-button> | ||||
|               </div> | ||||
|               <div class="form-grid single"> | ||||
|                 <dees-input-text | ||||
|                   .key=${'digitalOceanToken'} | ||||
|                   .label=${'Personal Access Token'} | ||||
|                   .value=${this.settings.digitalOceanToken || ''} | ||||
|                   .isPasswordBool=${true} | ||||
|                   .description=${'DigitalOcean personal access token with read/write scope'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|               </div> | ||||
|             </dees-panel> | ||||
|  | ||||
|             <!-- Azure --> | ||||
|             <dees-panel  | ||||
|               .title=${'Microsoft Azure'} | ||||
|               .subtitle=${'Configure Azure service principal'} | ||||
|               .variant=${'outline'} | ||||
|             > | ||||
|               <div class="test-status"> | ||||
|                 ${this.renderProviderStatus('azure')} | ||||
|                 <dees-button | ||||
|                   .text=${'Test Connection'} | ||||
|                   .type=${'secondary'} | ||||
|                   @click=${(e: Event) => { | ||||
|                     e.preventDefault(); | ||||
|                     e.stopPropagation(); | ||||
|                     this.testConnection('azure'); | ||||
|                   }} | ||||
|                 ></dees-button> | ||||
|               </div> | ||||
|               <div class="form-grid"> | ||||
|                 <dees-input-text | ||||
|                   .key=${'azureClientId'} | ||||
|                   .label=${'Application (Client) ID'} | ||||
|                   .value=${this.settings.azureClientId || ''} | ||||
|                   .isPasswordBool=${true} | ||||
|                   .description=${'Azure AD application client ID'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|                 <dees-input-text | ||||
|                   .key=${'azureClientSecret'} | ||||
|                   .label=${'Client Secret'} | ||||
|                   .value=${this.settings.azureClientSecret || ''} | ||||
|                   .isPasswordBool=${true} | ||||
|                   .description=${'Azure AD application client secret'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|               </div> | ||||
|               <div class="form-grid"> | ||||
|                 <dees-input-text | ||||
|                   .key=${'azureTenantId'} | ||||
|                   .label=${'Directory (Tenant) ID'} | ||||
|                   .value=${this.settings.azureTenantId || ''} | ||||
|                   .description=${'Azure AD tenant identifier'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|                 <dees-input-text | ||||
|                   .key=${'azureSubscriptionId'} | ||||
|                   .label=${'Subscription ID'} | ||||
|                   .value=${this.settings.azureSubscriptionId || ''} | ||||
|                   .description=${'Azure subscription for resource management'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|               </div> | ||||
|             </dees-panel> | ||||
|  | ||||
|             <!-- Google Cloud --> | ||||
|             <dees-panel  | ||||
|               .title=${'Google Cloud Platform'} | ||||
|               .subtitle=${'Configure GCP service account'} | ||||
|               .variant=${'outline'} | ||||
|             > | ||||
|               <div class="test-status"> | ||||
|                 ${this.renderProviderStatus('google')} | ||||
|                 <dees-button | ||||
|                   .text=${'Test Connection'} | ||||
|                   .type=${'secondary'} | ||||
|                   @click=${(e: Event) => { | ||||
|                     e.preventDefault(); | ||||
|                     e.stopPropagation(); | ||||
|                     this.testConnection('google'); | ||||
|                   }} | ||||
|                 ></dees-button> | ||||
|               </div> | ||||
|               <div class="form-grid single"> | ||||
|                 <dees-input-textarea | ||||
|                   .key=${'googleCloudKeyJson'} | ||||
|                   .label=${'Service Account Key (JSON)'} | ||||
|                   .value=${this.settings.googleCloudKeyJson || ''} | ||||
|                   .isPasswordBool=${true} | ||||
|                   .description=${'Complete JSON key file for service account authentication'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-textarea> | ||||
|               </div> | ||||
|               <div class="form-grid single"> | ||||
|                 <dees-input-text | ||||
|                   .key=${'googleCloudProjectId'} | ||||
|                   .label=${'Project ID'} | ||||
|                   .value=${this.settings.googleCloudProjectId || ''} | ||||
|                   .description=${'Google Cloud project identifier'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|               </div> | ||||
|             </dees-panel> | ||||
|  | ||||
|             <div class="actions-container"> | ||||
|               <dees-form-submit  | ||||
|                 .text=${'Save All Settings'} | ||||
|                 .disabled=${this.isLoading} | ||||
|               ></dees-form-submit> | ||||
|             </div> | ||||
|           </dees-form> | ||||
|         </div> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
| @@ -1,914 +0,0 @@ | ||||
| import * as shared from '../elements/shared/index.js'; | ||||
| import * as plugins from '../plugins.js'; | ||||
|  | ||||
| import { | ||||
|   DeesElement, | ||||
|   customElement, | ||||
|   html, | ||||
|   state, | ||||
|   css, | ||||
|   cssManager, | ||||
|   property, | ||||
| } from '@design.estate/dees-element'; | ||||
|  | ||||
| import * as appstate from '../appstate.js'; | ||||
|  | ||||
| @customElement('cloudly-view-tasks') | ||||
| export class CloudlyViewTasks extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = {}; | ||||
|  | ||||
|   @state() | ||||
|   private selectedExecution: plugins.interfaces.data.ITaskExecution | null = null; | ||||
|  | ||||
|   @state() | ||||
|   private loading = false; | ||||
|  | ||||
|   @state() | ||||
|   private filterStatus: string = 'all'; | ||||
|  | ||||
|   @state() | ||||
|   private searchQuery: string = ''; | ||||
|  | ||||
|   @state() | ||||
|   private categoryFilter: string = 'all'; | ||||
|  | ||||
|   @state() | ||||
|   private autoRefresh: boolean = true; | ||||
|  | ||||
|   private _refreshHandle: any = null; | ||||
|   @state() | ||||
|   private canceling: Record<string, boolean> = {}; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subscription = appstate.dataState | ||||
|       .select((stateArg) => stateArg) | ||||
|       .subscribe((dataArg) => { | ||||
|         this.data = dataArg; | ||||
|       }); | ||||
|     this.rxSubscriptions.push(subscription); | ||||
|      | ||||
|     // Load initial data (non-blocking) | ||||
|     this.loadInitialData(); | ||||
|  | ||||
|     // Start periodic refresh (lightweight; executions only by default) | ||||
|     this.startAutoRefresh(); | ||||
|   } | ||||
|  | ||||
|   private async loadInitialData() { | ||||
|     try { | ||||
|       await appstate.dataState.dispatchAction(appstate.taskActions.getTasks, {}); | ||||
|       await appstate.dataState.dispatchAction(appstate.taskActions.getTaskExecutions, {}); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to load initial task data:', error); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private startAutoRefresh() { | ||||
|     this.stopAutoRefresh(); | ||||
|     if (!this.autoRefresh) return; | ||||
|     this._refreshHandle = setInterval(async () => { | ||||
|       try { | ||||
|         await this.loadExecutionsWithFilter(); | ||||
|       } catch (err) { | ||||
|         // ignore transient errors during refresh | ||||
|       } | ||||
|     }, 5000); | ||||
|   } | ||||
|  | ||||
|   private stopAutoRefresh() { | ||||
|     if (this._refreshHandle) { | ||||
|       clearInterval(this._refreshHandle); | ||||
|       this._refreshHandle = null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public async disconnectedCallback(): Promise<void> { | ||||
|     await (super.disconnectedCallback?.()); | ||||
|     this.stopAutoRefresh(); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css` | ||||
|       .toolbar { | ||||
|         display: flex; | ||||
|         gap: 12px; | ||||
|         align-items: center; | ||||
|         margin: 4px 0 16px 0; | ||||
|         flex-wrap: wrap; | ||||
|       } | ||||
|  | ||||
|       .toolbar .spacer { | ||||
|         flex: 1 1 auto; | ||||
|       } | ||||
|  | ||||
|       .search-input { | ||||
|         background: #111; | ||||
|         color: #ddd; | ||||
|         border: 1px solid #333; | ||||
|         border-radius: 6px; | ||||
|         padding: 8px 10px; | ||||
|         min-width: 220px; | ||||
|       } | ||||
|  | ||||
|       .chipbar { | ||||
|         display: flex; | ||||
|         gap: 8px; | ||||
|         flex-wrap: wrap; | ||||
|       } | ||||
|  | ||||
|       .chip { | ||||
|         padding: 6px 10px; | ||||
|         background: #2a2a2a; | ||||
|         color: #bbb; | ||||
|         border: 1px solid #444; | ||||
|         border-radius: 16px; | ||||
|         cursor: pointer; | ||||
|         transition: all 0.2s; | ||||
|         user-select: none; | ||||
|       } | ||||
|  | ||||
|       .chip.active { | ||||
|         background: #2196f3; | ||||
|         border-color: #2196f3; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .task-grid { | ||||
|         display: grid; | ||||
|         grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); | ||||
|         gap: 16px; | ||||
|         margin-bottom: 32px; | ||||
|       } | ||||
|  | ||||
|       .task-card { | ||||
|         background: #131313; | ||||
|         border: 1px solid #2a2a2a; | ||||
|         border-radius: 10px; | ||||
|         padding: 16px; | ||||
|         transition: border-color 0.2s, background 0.2s; | ||||
|       } | ||||
|  | ||||
|       .task-card:hover { border-color: #3a3a3a; } | ||||
|  | ||||
|       .card-header { | ||||
|         display: flex; | ||||
|         justify-content: space-between; | ||||
|         align-items: center; | ||||
|         gap: 12px; | ||||
|         margin-bottom: 12px; | ||||
|       } | ||||
|  | ||||
|       .header-left { display: flex; align-items: center; gap: 12px; min-width: 0; } | ||||
|       .header-right { display: flex; align-items: center; gap: 8px; } | ||||
|       .task-icon { color: #cfcfcf; } | ||||
|       .task-name { font-size: 1.05em; font-weight: 650; color: #fff; letter-spacing: 0.1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | ||||
|       .task-subtitle { color: #8c8c8c; font-size: 0.9em; } | ||||
|  | ||||
|       .task-description { | ||||
|         color: #b5b5b5; | ||||
|         font-size: 0.95em; | ||||
|         margin-bottom: 12px; | ||||
|         display: -webkit-box; | ||||
|         -webkit-line-clamp: 2; | ||||
|         -webkit-box-orient: vertical; | ||||
|         overflow: hidden; | ||||
|       } | ||||
|  | ||||
|       .task-meta { | ||||
|         display: flex; | ||||
|         gap: 8px; | ||||
|         flex-wrap: wrap; | ||||
|       } | ||||
|  | ||||
|       .category-badge, .status-badge { | ||||
|         padding: 2px 8px; | ||||
|         border-radius: 4px; | ||||
|         font-size: 0.85em; | ||||
|         font-weight: 500; | ||||
|       } | ||||
|  | ||||
|       .category-maintenance { | ||||
|         background: #ff9800; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .category-deployment { | ||||
|         background: #2196f3; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .category-backup { | ||||
|         background: #4caf50; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .category-monitoring { | ||||
|         background: #9c27b0; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .category-cleanup { | ||||
|         background: #795548; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .category-system { | ||||
|         background: #607d8b; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .category-security { | ||||
|         background: #f44336; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .status-running { | ||||
|         background: #2196f3; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .status-completed { | ||||
|         background: #4caf50; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .status-failed { | ||||
|         background: #f44336; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .status-cancelled { | ||||
|         background: #ff9800; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .trigger-button { | ||||
|         padding: 6px 12px; | ||||
|         background: #2196f3; | ||||
|         color: white; | ||||
|         border: none; | ||||
|         border-radius: 6px; | ||||
|         cursor: pointer; | ||||
|         font-size: 0.9em; | ||||
|         transition: background 0.2s; | ||||
|       } | ||||
|  | ||||
|       .trigger-button:hover { | ||||
|         background: #1976d2; | ||||
|       } | ||||
|  | ||||
|       .trigger-button:disabled { | ||||
|         background: #666; | ||||
|         cursor: not-allowed; | ||||
|       } | ||||
|  | ||||
|       .schedule-info { | ||||
|         color: #666; | ||||
|         font-size: 0.85em; | ||||
|         margin-top: 8px; | ||||
|       } | ||||
|  | ||||
|       .last-run { | ||||
|         color: #888; | ||||
|         font-size: 0.85em; | ||||
|         margin-top: 4px; | ||||
|       } | ||||
|  | ||||
|       .execution-logs { | ||||
|         background: #0a0a0a; | ||||
|         border: 1px solid #333; | ||||
|         border-radius: 6px; | ||||
|         padding: 16px; | ||||
|         margin-top: 16px; | ||||
|         max-height: 400px; | ||||
|         overflow-y: auto; | ||||
|       } | ||||
|  | ||||
|       .log-entry { | ||||
|         font-family: monospace; | ||||
|         font-size: 0.9em; | ||||
|         margin-bottom: 8px; | ||||
|         padding: 4px 8px; | ||||
|         border-radius: 4px; | ||||
|       } | ||||
|  | ||||
|       .log-info { | ||||
|         color: #2196f3; | ||||
|       } | ||||
|  | ||||
|       .log-warning { | ||||
|         color: #ff9800; | ||||
|         background: rgba(255, 152, 0, 0.1); | ||||
|       } | ||||
|  | ||||
|       .log-error { | ||||
|         color: #f44336; | ||||
|         background: rgba(244, 67, 54, 0.1); | ||||
|       } | ||||
|  | ||||
|       .log-success { | ||||
|         color: #4caf50; | ||||
|         background: rgba(76, 175, 80, 0.1); | ||||
|       } | ||||
|  | ||||
|       .filter-bar { | ||||
|         display: flex; | ||||
|         gap: 8px; | ||||
|         margin-bottom: 16px; | ||||
|       } | ||||
|  | ||||
|       .filter-button { | ||||
|         padding: 6px 12px; | ||||
|         background: #333; | ||||
|         color: #ccc; | ||||
|         border: 1px solid #555; | ||||
|         border-radius: 4px; | ||||
|         cursor: pointer; | ||||
|         transition: all 0.2s; | ||||
|       } | ||||
|  | ||||
|       .filter-button.active { | ||||
|         background: #2196f3; | ||||
|         color: white; | ||||
|         border-color: #2196f3; | ||||
|       } | ||||
|  | ||||
|       .filter-button:hover:not(.active) { | ||||
|         background: #444; | ||||
|       } | ||||
|  | ||||
|       .metrics { | ||||
|         display: flex; | ||||
|         gap: 16px; | ||||
|         margin-top: 12px; | ||||
|         padding-top: 12px; | ||||
|         border-top: 1px solid #333; | ||||
|       } | ||||
|  | ||||
|       .metric { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|       } | ||||
|  | ||||
|       .metric-label { | ||||
|         color: #666; | ||||
|         font-size: 0.85em; | ||||
|       } | ||||
|  | ||||
|       .metric-value { | ||||
|         color: #fff; | ||||
|         font-size: 1.1em; | ||||
|         font-weight: 600; | ||||
|       } | ||||
|  | ||||
|       .card-actions, .task-header .right { | ||||
|         display: flex; | ||||
|         gap: 8px; | ||||
|         align-items: center; | ||||
|       } | ||||
|  | ||||
|       .metrics-grid { | ||||
|         display: grid; | ||||
|         grid-template-columns: repeat(3, minmax(0, 1fr)); | ||||
|         gap: 10px; | ||||
|         margin-top: 8px; | ||||
|       } | ||||
|  | ||||
|       .metric-item { | ||||
|         background: #0f0f0f; | ||||
|         border: 1px solid #2c2c2c; | ||||
|         border-radius: 8px; | ||||
|         padding: 10px 12px; | ||||
|       } | ||||
|  | ||||
|       .metric-item .label { | ||||
|         color: #8d8d8d; | ||||
|         font-size: 0.8em; | ||||
|       } | ||||
|  | ||||
|       .metric-item .value { | ||||
|         color: #eaeaea; | ||||
|         font-weight: 600; | ||||
|         margin-top: 4px; | ||||
|       } | ||||
|  | ||||
|       .lastline { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         gap: 8px; | ||||
|         color: #a0a0a0; | ||||
|         font-size: 0.9em; | ||||
|         margin-top: 10px; | ||||
|       } | ||||
|  | ||||
|       .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } | ||||
|       .dot.info { background: #2196f3; } | ||||
|       .dot.success { background: #4caf50; } | ||||
|       .dot.warning { background: #ff9800; } | ||||
|       .dot.error { background: #f44336; } | ||||
|  | ||||
|       .card-footer { | ||||
|         display: flex; | ||||
|         gap: 12px; | ||||
|         margin-top: 12px; | ||||
|       } | ||||
|  | ||||
|       .link-button { | ||||
|         background: transparent; | ||||
|         border: none; | ||||
|         color: #8ab4ff; | ||||
|         cursor: pointer; | ||||
|         padding: 0; | ||||
|         font-size: 0.95em; | ||||
|       } | ||||
|       .link-button:hover { text-decoration: underline; } | ||||
|  | ||||
|       .secondary-button { | ||||
|         padding: 6px 12px; | ||||
|         background: #2b2b2b; | ||||
|         color: #ccc; | ||||
|         border: 1px solid #444; | ||||
|         border-radius: 6px; | ||||
|         cursor: pointer; | ||||
|         font-size: 0.9em; | ||||
|         transition: background 0.2s, border-color 0.2s; | ||||
|       } | ||||
|  | ||||
|       .secondary-button:hover { | ||||
|         background: #363636; | ||||
|         border-color: #555; | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|  | ||||
|   private async triggerTask(taskName: string) { | ||||
|     try { | ||||
|       const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|         heading: `Run Task: ${taskName}`, | ||||
|         content: html` | ||||
|           <div> | ||||
|             <p>Do you want to trigger this task now?</p> | ||||
|           </div> | ||||
|         `, | ||||
|         menuOptions: [ | ||||
|           { | ||||
|             name: 'Run now', | ||||
|             action: async (modalArg: any) => { | ||||
|               await appstate.dataState.dispatchAction( | ||||
|                 appstate.taskActions.triggerTask, { taskName } | ||||
|               ); | ||||
|               plugins.deesCatalog.DeesToast.createAndShow({ | ||||
|                 message: `Task ${taskName} triggered`, | ||||
|                 type: 'success', | ||||
|               }); | ||||
|               await modalArg.destroy(); | ||||
|               // Refresh executions to reflect the new run quickly | ||||
|               await this.loadExecutionsWithFilter(); | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             name: 'Cancel', | ||||
|             action: async (modalArg: any) => modalArg.destroy() | ||||
|           } | ||||
|         ] | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to trigger task:', error); | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ | ||||
|         message: `Failed to trigger: ${error.message}`, | ||||
|         type: 'error', | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async cancelTaskFor(taskName: string) { | ||||
|     try { | ||||
|       const executions = (this.data.taskExecutions || []) | ||||
|         .filter(e => e.data.taskName === taskName && e.data.status === 'running') | ||||
|         .sort((a, b) => (b.data.startedAt || 0) - (a.data.startedAt || 0)); | ||||
|       const running = executions[0]; | ||||
|       if (!running) return; | ||||
|  | ||||
|       this.canceling = { ...this.canceling, [running.id]: true }; | ||||
|       try { | ||||
|         await appstate.dataState.dispatchAction( | ||||
|           appstate.taskActions.cancelTask, { executionId: running.id } | ||||
|         ); | ||||
|         plugins.deesCatalog.DeesToast.createAndShow({ | ||||
|           message: `Cancelled ${taskName}`, | ||||
|           type: 'success', | ||||
|         }); | ||||
|       } finally { | ||||
|         this.canceling = { ...this.canceling, [running.id]: false }; | ||||
|         await this.loadExecutionsWithFilter(); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       console.error('Failed to cancel task:', err); | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ | ||||
|         message: `Cancel failed: ${err.message}`, | ||||
|         type: 'error', | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async loadExecutionsWithFilter() { | ||||
|     try { | ||||
|       const filter: any = {}; | ||||
|       if (this.filterStatus !== 'all') { | ||||
|         filter.status = this.filterStatus; | ||||
|       } | ||||
|        | ||||
|       await appstate.dataState.dispatchAction( | ||||
|         appstate.taskActions.getTaskExecutions, { filter } | ||||
|       ); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to load executions:', error); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private formatDate(timestamp: number): string { | ||||
|     return new Date(timestamp).toLocaleString(); | ||||
|   } | ||||
|  | ||||
|   private formatDuration(ms: number): string { | ||||
|     if (ms < 1000) return `${ms}ms`; | ||||
|     if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; | ||||
|     if (ms < 3600000) return `${(ms / 60000).toFixed(1)}m`; | ||||
|     return `${(ms / 3600000).toFixed(1)}h`; | ||||
|   } | ||||
|  | ||||
|   private formatRelativeTime(ts?: number): string { | ||||
|     if (!ts) return '-'; | ||||
|     const diff = Date.now() - ts; | ||||
|     const abs = Math.abs(diff); | ||||
|     if (abs < 60_000) return `${Math.round(abs / 1000)}s ago`; | ||||
|     if (abs < 3_600_000) return `${Math.round(abs / 60_000)}m ago`; | ||||
|     if (abs < 86_400_000) return `${Math.round(abs / 3_600_000)}h ago`; | ||||
|     return `${Math.round(abs / 86_400_000)}d ago`; | ||||
|   } | ||||
|  | ||||
|   private getCategoryIcon(category: string): string { | ||||
|     switch (category) { | ||||
|       case 'maintenance': | ||||
|         return 'lucide:Wrench'; | ||||
|       case 'deployment': | ||||
|         return 'lucide:Rocket'; | ||||
|       case 'backup': | ||||
|         return 'lucide:Archive'; | ||||
|       case 'monitoring': | ||||
|         return 'lucide:Activity'; | ||||
|       case 'cleanup': | ||||
|         return 'lucide:Trash2'; | ||||
|       case 'system': | ||||
|         return 'lucide:Settings'; | ||||
|       case 'security': | ||||
|         return 'lucide:Shield'; | ||||
|       default: | ||||
|         return 'lucide:Play'; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private getCategoryHue(category: string): number { | ||||
|     switch (category) { | ||||
|       case 'maintenance': return 28;    // orange | ||||
|       case 'deployment': return 208;    // blue | ||||
|       case 'backup': return 122;        // green | ||||
|       case 'monitoring': return 280;    // purple | ||||
|       case 'cleanup': return 20;        // brownish | ||||
|       case 'system': return 200;        // steel | ||||
|       case 'security': return 0;        // red | ||||
|       default: return 210;              // default blue | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private getStatusColor(status?: string): string { | ||||
|     switch (status) { | ||||
|       case 'running': return '#2196f3'; | ||||
|       case 'completed': return '#4caf50'; | ||||
|       case 'failed': return '#f44336'; | ||||
|       case 'cancelled': return '#ff9800'; | ||||
|       default: return '#3a3a3a'; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private formatCronFriendly(cron?: string): string { | ||||
|     if (!cron) return ''; | ||||
|     const parts = cron.trim().split(/\s+/); | ||||
|     if (parts.length !== 5) return cron; // fallback | ||||
|     const [min, hour, dom, mon, dow] = parts; | ||||
|     if (min === '*/1' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'every minute'; | ||||
|     if (min.startsWith('*/') && hour === '*' && dom === '*' && mon === '*' && dow === '*') return `every ${min.replace('*/','')} min`; | ||||
|     if (min === '0' && hour.startsWith('*/') && dom === '*' && mon === '*' && dow === '*') return `every ${hour.replace('*/','')} hours`; | ||||
|     if (min === '0' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'hourly'; | ||||
|     if (min === '0' && hour === '0' && dom === '*' && mon === '*' && dow === '*') return 'daily'; | ||||
|     if (min === '0' && hour === '0' && dom === '1' && mon === '*' && dow === '*') return 'monthly'; | ||||
|     return cron; | ||||
|   } | ||||
|  | ||||
|   private renderTaskCard(task: any) { | ||||
|     const executions = this.data.taskExecutions || []; | ||||
|     const lastExecution = executions | ||||
|       .filter(e => e.data.taskName === task.name) | ||||
|       .sort((a, b) => (b.data.startedAt || 0) - (a.data.startedAt || 0))[0]; | ||||
|      | ||||
|     const isRunning = lastExecution?.data.status === 'running'; | ||||
|     const executionsForTask = executions.filter(e => e.data.taskName === task.name); | ||||
|     const now = Date.now(); | ||||
|     const last24hCount = executionsForTask.filter(e => (e.data.startedAt || 0) > now - 86_400_000).length; | ||||
|     const completed = executionsForTask.filter(e => e.data.status === 'completed'); | ||||
|     const successRate = executionsForTask.length ? Math.round((completed.length * 100) / executionsForTask.length) : 0; | ||||
|     const avgDuration = completed.length ? Math.round(completed.reduce((acc, e) => acc + (e.data.duration || 0), 0) / completed.length) : undefined; | ||||
|     const lastLog = lastExecution?.data.logs && lastExecution.data.logs.length > 0 ? lastExecution.data.logs[lastExecution.data.logs.length - 1] : null; | ||||
|      | ||||
|     const status = lastExecution?.data.status as ('running'|'completed'|'failed'|'cancelled'|undefined); | ||||
|     const hue = this.getCategoryHue(task.category); | ||||
|     const subtitle = [ | ||||
|       task.category, | ||||
|       task.schedule ? `⏱ ${this.formatCronFriendly(task.schedule)}` : null, | ||||
|       isRunning | ||||
|         ? (lastExecution?.data.startedAt ? `Started ${this.formatRelativeTime(lastExecution.data.startedAt)}` : 'Running') | ||||
|         : (task.lastRun ? `Last ${this.formatRelativeTime(task.lastRun)}` : 'Never run') | ||||
|     ].filter(Boolean).join(' • '); | ||||
|  | ||||
|     return html` | ||||
|       <div class="task-card"> | ||||
|         <div class="card-header"> | ||||
|           <div class="header-left"> | ||||
|             <dees-icon class="task-icon" .iconName=${this.getCategoryIcon(task.category)}></dees-icon> | ||||
|             <div> | ||||
|               <div class="task-name" title=${task.name}>${task.name}</div> | ||||
|               <div class="task-subtitle" title=${task.schedule || ''}>${subtitle}</div> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="header-right"> | ||||
|             ${lastExecution ? html`<span class="status-badge status-${lastExecution.data.status}">${lastExecution.data.status}</span>` : html`<span class="status-badge" style="background:#2e2e2e;color:#ddd;border:1px solid #3a3a3a;">idle</span>`} | ||||
|             ${isRunning ? html` | ||||
|               <dees-spinner style="--size: 18px"></dees-spinner> | ||||
|               <dees-button | ||||
|                 .text=${this.canceling[lastExecution!.id] ? 'Cancelling…' : 'Cancel'} | ||||
|                 .type=${'secondary'} | ||||
|                 .disabled=${!!this.canceling[lastExecution!.id]} | ||||
|                 @click=${() => this.cancelTaskFor(task.name)} | ||||
|               ></dees-button> | ||||
|             ` : html` | ||||
|               <dees-button .text=${'Run'} .type=${'primary'} .disabled=${!task.enabled} @click=${() => this.triggerTask(task.name)}></dees-button> | ||||
|             `} | ||||
|           </div> | ||||
|         </div> | ||||
|          | ||||
|         <div class="task-description" title=${task.description || ''}>${task.description}</div> | ||||
|          | ||||
|         ${lastExecution ? html` | ||||
|           <div class="metrics-grid"> | ||||
|             <div class="metric-item"> | ||||
|               <div class="label">Last Status</div> | ||||
|               <div class="value"> | ||||
|                 <span class="status-badge status-${lastExecution.data.status}">${lastExecution.data.status}</span> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="metric-item"> | ||||
|               <div class="label">Avg Duration</div> | ||||
|               <div class="value">${avgDuration ? this.formatDuration(avgDuration) : '-'}</div> | ||||
|             </div> | ||||
|             <div class="metric-item"> | ||||
|               <div class="label">24h Runs · Success</div> | ||||
|               <div class="value">${last24hCount} · ${successRate}%</div> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="lastline"> | ||||
|             ${lastLog ? html`<span class="dot ${lastLog.severity}"></span> ${lastLog.message}` : 'No recent logs'} | ||||
|           </div> | ||||
|           <div class="card-footer"> | ||||
|             <button class="link-button" @click=${() => this.openExecutionDetails(lastExecution)}>Details</button> | ||||
|             ${lastExecution.data.logs?.length ? html`<button class="link-button" @click=${() => this.openLogsModal(lastExecution)}>Logs</button>` : ''} | ||||
|           </div> | ||||
|         ` : html` | ||||
|           <div class="metrics-grid"> | ||||
|             <div class="metric-item"> | ||||
|               <div class="label">Last Status</div> | ||||
|               <div class="value">—</div> | ||||
|             </div> | ||||
|             <div class="metric-item"> | ||||
|               <div class="label">Avg Duration</div> | ||||
|               <div class="value">—</div> | ||||
|             </div> | ||||
|             <div class="metric-item"> | ||||
|               <div class="label">24h Runs · Success</div> | ||||
|               <div class="value">0 · 0%</div> | ||||
|             </div> | ||||
|           </div> | ||||
|         `} | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private async openExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) { | ||||
|     this.selectedExecution = execution; | ||||
|     // Scroll into view of the details section | ||||
|     requestAnimationFrame(() => { | ||||
|       this.shadowRoot?.querySelector('cloudly-sectionheading + .execution-details')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private async openLogsModal(execution: plugins.interfaces.data.ITaskExecution) { | ||||
|     await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|       heading: `Logs: ${execution.data.taskName}`, | ||||
|       content: html` | ||||
|         <div class="execution-logs"> | ||||
|           ${(execution.data.logs || []).map(log => html` | ||||
|             <div class="log-entry log-${log.severity}"> | ||||
|               <span>${this.formatDate(log.timestamp)}</span> - ${log.message} | ||||
|             </div> | ||||
|           `)} | ||||
|         </div> | ||||
|       `, | ||||
|       menuOptions: [ | ||||
|         { | ||||
|           name: 'Copy All', | ||||
|           action: async (modalArg: any) => { | ||||
|             try { | ||||
|               await navigator.clipboard.writeText((execution.data.logs || []) | ||||
|                 .map(l => `${new Date(l.timestamp).toISOString()} [${l.severity}] ${l.message}`).join('\n')); | ||||
|               plugins.deesCatalog.DeesToast.createAndShow({ message: 'Logs copied', type: 'success' }); | ||||
|             } catch (e) { | ||||
|               plugins.deesCatalog.DeesToast.createAndShow({ message: 'Copy failed', type: 'error' }); | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         { name: 'Close', action: async (modalArg: any) => modalArg.destroy() } | ||||
|       ] | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private renderExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) { | ||||
|     return html` | ||||
|       <div class="execution-details"> | ||||
|         <h3>Execution Details: ${execution.data.taskName}</h3> | ||||
|          | ||||
|         <div class="metrics"> | ||||
|           <div class="metric"> | ||||
|             <span class="metric-label">Started</span> | ||||
|             <span class="metric-value">${this.formatDate(execution.data.startedAt)}</span> | ||||
|           </div> | ||||
|           ${execution.data.completedAt ? html` | ||||
|             <div class="metric"> | ||||
|               <span class="metric-label">Completed</span> | ||||
|               <span class="metric-value">${this.formatDate(execution.data.completedAt)}</span> | ||||
|             </div> | ||||
|           ` : ''} | ||||
|           ${execution.data.duration ? html` | ||||
|             <div class="metric"> | ||||
|               <span class="metric-label">Duration</span> | ||||
|               <span class="metric-value">${this.formatDuration(execution.data.duration)}</span> | ||||
|             </div> | ||||
|           ` : ''} | ||||
|           <div class="metric"> | ||||
|             <span class="metric-label">Triggered By</span> | ||||
|             <span class="metric-value">${execution.data.triggeredBy}</span> | ||||
|           </div> | ||||
|         </div> | ||||
|          | ||||
|         ${execution.data.logs && execution.data.logs.length > 0 ? html` | ||||
|           <h4>Logs</h4> | ||||
|           <div class="execution-logs"> | ||||
|             ${execution.data.logs.map(log => html` | ||||
|               <div class="log-entry log-${log.severity}"> | ||||
|                 <span>${this.formatDate(log.timestamp)}</span> -  | ||||
|                 ${log.message} | ||||
|               </div> | ||||
|             `)} | ||||
|           </div> | ||||
|         ` : ''} | ||||
|          | ||||
|         ${execution.data.metrics ? html` | ||||
|           <h4>Metrics</h4> | ||||
|           <div class="metrics"> | ||||
|             ${Object.entries(execution.data.metrics).map(([key, value]) => html` | ||||
|               <div class="metric"> | ||||
|                 <span class="metric-label">${key}</span> | ||||
|                 <span class="metric-value">${typeof value === 'object' ? JSON.stringify(value) : value}</span> | ||||
|               </div> | ||||
|             `)} | ||||
|           </div> | ||||
|         ` : ''} | ||||
|          | ||||
|         ${execution.data.error ? html` | ||||
|           <h4>Error</h4> | ||||
|           <div class="execution-logs"> | ||||
|             <div class="log-entry log-error"> | ||||
|               ${execution.data.error.message} | ||||
|               ${execution.data.error.stack ? html`<pre>${execution.data.error.stack}</pre>` : ''} | ||||
|             </div> | ||||
|           </div> | ||||
|         ` : ''} | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   public render() { | ||||
|     const tasks = (this.data.tasks || []) as any[]; | ||||
|     const categories = Array.from(new Set(tasks.map(t => t.category))).sort(); | ||||
|     const filteredTasks = tasks | ||||
|       .filter(t => this.categoryFilter === 'all' || t.category === this.categoryFilter) | ||||
|       .filter(t => !this.searchQuery || t.name.toLowerCase().includes(this.searchQuery.toLowerCase()) || (t.description || '').toLowerCase().includes(this.searchQuery.toLowerCase())); | ||||
|  | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Tasks</cloudly-sectionheading> | ||||
|  | ||||
|       <dees-panel | ||||
|         .title=${'Task Library'} | ||||
|         .subtitle=${'Run maintenance, monitoring and system tasks'} | ||||
|         .variant=${'outline'} | ||||
|       > | ||||
|         <div class="toolbar"> | ||||
|           <div class="chipbar"> | ||||
|             <div class="chip ${this.categoryFilter === 'all' ? 'active' : ''}" | ||||
|               @click=${() => { this.categoryFilter = 'all'; }}> | ||||
|               All | ||||
|             </div> | ||||
|             ${categories.map(cat => html` | ||||
|               <div class="chip ${this.categoryFilter === cat ? 'active' : ''}" | ||||
|                 @click=${() => { this.categoryFilter = cat; }}> | ||||
|                 ${cat} | ||||
|               </div> | ||||
|             `)} | ||||
|           </div> | ||||
|           <div class="spacer"></div> | ||||
|           <input class="search-input" placeholder="Search tasks" .value=${this.searchQuery} | ||||
|             @input=${(e: any) => { this.searchQuery = e.target.value; }} /> | ||||
|           <button class="secondary-button" @click=${async () => { | ||||
|             await this.loadExecutionsWithFilter(); | ||||
|           }}>Refresh</button> | ||||
|           <button class="secondary-button" @click=${() => { | ||||
|             this.autoRefresh = !this.autoRefresh; | ||||
|             this.autoRefresh ? this.startAutoRefresh() : this.stopAutoRefresh(); | ||||
|           }}> | ||||
|             ${this.autoRefresh ? 'Auto-Refresh: On' : 'Auto-Refresh: Off'} | ||||
|           </button> | ||||
|         </div> | ||||
|  | ||||
|         <div class="task-grid"> | ||||
|           ${filteredTasks.map(task => this.renderTaskCard(task))} | ||||
|         </div> | ||||
|       </dees-panel> | ||||
|  | ||||
|       <cloudly-sectionheading>Execution History</cloudly-sectionheading> | ||||
|  | ||||
|       <dees-panel | ||||
|         .title=${'Recent Executions'} | ||||
|         .subtitle=${'History of task runs and their outcomes'} | ||||
|         .variant=${'outline'} | ||||
|       > | ||||
|         <dees-table | ||||
|           .heading1=${'Task Executions'} | ||||
|           .heading2=${'History of task runs and their outcomes'} | ||||
|           .data=${this.data.taskExecutions || []} | ||||
|           .displayFunction=${(itemArg: plugins.interfaces.data.ITaskExecution) => { | ||||
|             return { | ||||
|               Task: itemArg.data.taskName, | ||||
|               Status: html`<span class="status-badge status-${itemArg.data.status}">${itemArg.data.status}</span>`, | ||||
|               'Started At': this.formatDate(itemArg.data.startedAt), | ||||
|               Duration: itemArg.data.duration ? this.formatDuration(itemArg.data.duration) : '-', | ||||
|               'Triggered By': itemArg.data.triggeredBy, | ||||
|               Logs: itemArg.data.logs?.length || 0, | ||||
|             }; | ||||
|           }} | ||||
|           .actionFunction=${async (itemArg: plugins.interfaces.data.ITaskExecution) => { | ||||
|             const actions: any[] = [ | ||||
|               { | ||||
|                 name: 'View Details', | ||||
|                 iconName: 'lucide:Eye', | ||||
|                 type: ['inRow'], | ||||
|                 actionFunc: async () => { | ||||
|                   this.selectedExecution = itemArg; | ||||
|                 }, | ||||
|               } | ||||
|             ]; | ||||
|             if (itemArg.data.status === 'running') { | ||||
|               actions.push({ | ||||
|                 name: 'Cancel', | ||||
|                 iconName: 'lucide:SquareX', | ||||
|                 type: ['inRow'], | ||||
|                 actionFunc: async () => { | ||||
|                   await appstate.dataState.dispatchAction(appstate.taskActions.cancelTask, { executionId: itemArg.id }); | ||||
|                   await this.loadExecutionsWithFilter(); | ||||
|                 }, | ||||
|               }); | ||||
|             } | ||||
|             return actions; | ||||
|           }} | ||||
|         ></dees-table> | ||||
|       </dees-panel> | ||||
|  | ||||
|       ${this.selectedExecution ? html` | ||||
|         <cloudly-sectionheading>Execution Details</cloudly-sectionheading> | ||||
|         ${this.renderExecutionDetails(this.selectedExecution)} | ||||
|       ` : ''} | ||||
|     `; | ||||
|   } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| export * from './shared/index.js'; | ||||
| export * from './cloudly-dashboard.js'; | ||||
| export * from './cloudly-view-secretgroups.js'; | ||||
| export * from './cloudly-view-secretbundles.js'; | ||||
| export * from './views/secretgroups/index.js'; | ||||
| export * from './views/secretbundles/index.js'; | ||||
|   | ||||
							
								
								
									
										52
									
								
								ts_web/elements/views/backups/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								ts_web/elements/views/backups/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| 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-backups') | ||||
| export class CloudlyViewBackups extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; }); | ||||
|     this.rxSubscriptions.push(subecription); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Backups</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'Backups'} | ||||
|         .heading2=${'decoded in client'} | ||||
|         .data=${this.data.backups} | ||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { return { id: itemArg.id, serverAmount: itemArg.data.servers.length }; }} | ||||
|         .dataActions=${[ | ||||
|           { name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => { | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', 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-form> | ||||
|               `, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|           { name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html` | ||||
|                 <div style="text-align:center">Do you really want to delete the ConfigBundle?</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> | ||||
|               `, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { interface HTMLElementTagNameMap { 'cloudly-view-backups': CloudlyViewBackups; } } | ||||
|  | ||||
							
								
								
									
										111
									
								
								ts_web/elements/views/clusters/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								ts_web/elements/views/clusters/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | ||||
| 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-clusters') | ||||
| export class CloudlyViewClusters extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = {} as any; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subecription = appstate.dataState | ||||
|       .select((stateArg) => stateArg) | ||||
|       .subscribe((dataArg) => { | ||||
|         this.data = dataArg; | ||||
|       }); | ||||
|     this.rxSubscriptions.push(subecription); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css``, | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Clusters</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'Clusters'} | ||||
|         .heading2=${'decoded in client'} | ||||
|         .data=${this.data.clusters} | ||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { | ||||
|           return { | ||||
|             id: itemArg.id, | ||||
|             serverAmount: itemArg.data.servers.length, | ||||
|           }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { | ||||
|             name: 'add cluster', | ||||
|             iconName: 'plus', | ||||
|             type: ['header', 'footer'], | ||||
|             actionFunc: async () => { | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Add Cluster', | ||||
|                 content: html` | ||||
|                   <dees-form> | ||||
|                     <dees-input-text .key=${'clusterName'} .label=${'cluster name'} .description=${'a descriptive name for the cluster'} .value=${''}></dees-input-text> | ||||
|                     <dees-input-dropdown .key=${'setupMode'} .label=${'Setup Mode'} .description=${'How the cluster infrastructure should be managed'} | ||||
|                       .options=${[ | ||||
|                         {option: 'manual', key: 'manual', description: 'Manual Setup - Add your own servers manually'}, | ||||
|                         {option: 'hetzner', key: 'hetzner', description: 'Hetzner Cloud - Auto-provision servers on Hetzner'}, | ||||
|                         {option: 'aws', key: 'aws', description: 'AWS - Auto-provision on Amazon Web Services (coming soon)', disabled: true}, | ||||
|                         {option: 'digitalocean', key: 'digitalocean', description: 'DigitalOcean - Auto-provision on DigitalOcean (coming soon)', disabled: true} | ||||
|                       ]} | ||||
|                       .selectedOption=${'manual'}> | ||||
|                     </dees-input-dropdown> | ||||
|                   </dees-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { name: 'create', action: async (modalArg: any) => { | ||||
|                       const data = (await modalArg.shadowRoot.querySelector('dees-form').collectFormData()) as any; | ||||
|                       await appstate.dataState.dispatchAction(appstate.addClusterAction, data); | ||||
|                       await modalArg.destroy(); | ||||
|                     }}, | ||||
|                   { name: 'cancel', action: async (modalArg: any) => modalArg.destroy() }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'delete', | ||||
|             iconName: 'trash', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg: any) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Delete ConfigBundle ${actionDataArg.item.id}`, | ||||
|                 content: html` | ||||
|                   <div style="text-align:center">Do you really want to delete the ConfigBundle?</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> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, | ||||
|                   { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     'cloudly-view-clusters': CloudlyViewClusters; | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										52
									
								
								ts_web/elements/views/dbs/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								ts_web/elements/views/dbs/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| 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-dbs') | ||||
| export class CloudlyViewDbs extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; }); | ||||
|     this.rxSubscriptions.push(subecription); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>DBs</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'DBs'} | ||||
|         .heading2=${'decoded in client'} | ||||
|         .data=${this.data.dbs} | ||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { return { id: itemArg.id, serverAmount: itemArg.data.servers.length }; }} | ||||
|         .dataActions=${[ | ||||
|           { name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => { | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', 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-form> | ||||
|               `, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|           { name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html` | ||||
|                 <div style="text-align:center">Do you really want to delete the ConfigBundle?</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> | ||||
|               `, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { interface HTMLElementTagNameMap { 'cloudly-view-dbs': CloudlyViewDbs; } } | ||||
|  | ||||
							
								
								
									
										222
									
								
								ts_web/elements/views/deployments/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								ts_web/elements/views/deployments/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,222 @@ | ||||
| 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-deployments') | ||||
| export class CloudlyViewDeployments extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = {} 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 { padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; } | ||||
|       .status-running { background: #4caf50; color: white; } | ||||
|       .status-stopped { background: #f44336; color: white; } | ||||
|       .status-paused { background: #ff9800; color: white; } | ||||
|       .status-deploying { background: #2196f3; color: white; } | ||||
|       .health-indicator { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; } | ||||
|       .health-healthy { background: #e8f5e9; color: #2e7d32; } | ||||
|       .health-unhealthy { background: #ffebee; color: #c62828; } | ||||
|       .health-unknown { background: #f5f5f5; color: #666; } | ||||
|       .resource-usage { display: flex; gap: 12px; font-size: 0.9em; color: #888; } | ||||
|       .resource-item { display: flex; align-items: center; gap: 4px; } | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   private getServiceName(serviceId: string): string { | ||||
|     const service = this.data.services?.find(s => s.id === serviceId); | ||||
|     return service?.data?.name || serviceId; | ||||
|   } | ||||
|  | ||||
|   private getNodeName(nodeId: string): string { | ||||
|     return nodeId.substring(0, 8); | ||||
|   } | ||||
|  | ||||
|   private getStatusBadgeHtml(status: string): any { | ||||
|     const className = `status-badge status-${status}`; | ||||
|     return html`<span class="${className}">${status}</span>`; | ||||
|   } | ||||
|  | ||||
|   private getHealthIndicatorHtml(health?: string): any { | ||||
|     if (!health) health = 'unknown'; | ||||
|     const className = `health-indicator health-${health}`; | ||||
|     const icon = health === 'healthy' ? '✓' : health === 'unhealthy' ? '✗' : '?'; | ||||
|     return html`<span class="${className}">${icon} ${health}</span>`; | ||||
|   } | ||||
|  | ||||
|   private getResourceUsageHtml(deployment: plugins.interfaces.data.IDeployment): any { | ||||
|     if (!deployment.resourceUsage) { | ||||
|       return html`<span style="color: #aaa;">N/A</span>`; | ||||
|     } | ||||
|     const { cpuUsagePercent, memoryUsedMB } = deployment.resourceUsage; | ||||
|     return html` | ||||
|       <div class="resource-usage"> | ||||
|         <div class="resource-item"> | ||||
|           <lucide-icon name="Cpu" size="14"></lucide-icon> | ||||
|           ${cpuUsagePercent?.toFixed(1) || 0}% | ||||
|         </div> | ||||
|         <div class="resource-item"> | ||||
|           <lucide-icon name="MemoryStick" size="14"></lucide-icon> | ||||
|           ${memoryUsedMB || 0} MB | ||||
|         </div> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Deployments</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'Deployments'} | ||||
|         .heading2=${'Service deployments running on cluster nodes'} | ||||
|         .data=${this.data.deployments || []} | ||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.IDeployment) => { | ||||
|           return { | ||||
|             Service: this.getServiceName(itemArg.serviceId), | ||||
|             Node: this.getNodeName(itemArg.nodeId), | ||||
|             Status: this.getStatusBadgeHtml(itemArg.status), | ||||
|             Health: this.getHealthIndicatorHtml(itemArg.healthStatus), | ||||
|             'Container ID': itemArg.containerId ? html`<span style="font-family: monospace; font-size: 0.9em;">${itemArg.containerId.substring(0, 12)}</span>` : html`<span style="color: #aaa;">N/A</span>`, | ||||
|             Version: itemArg.version || 'latest', | ||||
|             'Resource Usage': this.getResourceUsageHtml(itemArg), | ||||
|             'Last Updated': itemArg.deployedAt ? new Date(itemArg.deployedAt).toLocaleString() : 'Never', | ||||
|           }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { | ||||
|             name: 'Deploy Service', | ||||
|             iconName: 'plus', | ||||
|             type: ['header', 'footer'], | ||||
|             actionFunc: async () => { | ||||
|               const availableServices = this.data.services || []; | ||||
|               if (availableServices.length === 0) { | ||||
|                 plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                   heading: 'No Services Available', | ||||
|                   content: html`<div style="text-align: center; padding: 24px;"><lucide-icon name="AlertCircle" size="48" style="color: #ff9800; margin-bottom: 16px;"></lucide-icon><div>Please create a service first before creating deployments.</div></div>`, | ||||
|                   menuOptions: [ { name: 'OK', action: async (modalArg: any) => { await modalArg.destroy(); } } ], | ||||
|                 }); | ||||
|                 return; | ||||
|               } | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Deploy Service', | ||||
|                 content: html` | ||||
|                   <dees-form> | ||||
|                     <dees-input-dropdown .key=${'serviceId'} .label=${'Service'} .options=${availableServices.map(s => ({ key: s.id, value: s.data.name }))} .required=${true}></dees-input-dropdown> | ||||
|                     <dees-input-text .key=${'nodeId'} .label=${'Target Node ID'} .required=${true} .description=${'Enter the cluster node ID where this service should be deployed'}></dees-input-text> | ||||
|                     <dees-input-text .key=${'version'} .label=${'Version'} .value=${'latest'} .required=${true}></dees-input-text> | ||||
|                     <dees-input-dropdown .key=${'status'} .label=${'Initial Status'} .options=${['deploying', 'running']} .value=${'deploying'} .required=${true}></dees-input-dropdown> | ||||
|                   </dees-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { name: 'Deploy', action: async (modalArg: any) => { | ||||
|                       const form = modalArg.shadowRoot.querySelector('dees-form') as any; | ||||
|                       const formData = await form.gatherData(); | ||||
|                       await appstate.dataState.dispatchAction(appstate.createDeploymentAction, { | ||||
|                         deploymentData: { | ||||
|                           serviceId: formData.serviceId, | ||||
|                           nodeId: formData.nodeId, | ||||
|                           status: formData.status, | ||||
|                           version: formData.version, | ||||
|                           deployedAt: Date.now(), | ||||
|                           usedImageId: 'placeholder', | ||||
|                           deploymentLog: [], | ||||
|                         }, | ||||
|                       }); | ||||
|                       await modalArg.destroy(); | ||||
|                     }}, | ||||
|                   { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'Restart', | ||||
|             iconName: 'refresh-cw', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg: any) => { | ||||
|               const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment; | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Restart Deployment`, | ||||
|                 content: html` | ||||
|                   <div style="text-align:center">Are you sure you want to restart this deployment?</div> | ||||
|                   <div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;"> | ||||
|                     <div style="color: #fff; font-weight: bold;">${this.getServiceName(deployment.serviceId)}</div> | ||||
|                     <div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">Node: ${this.getNodeName(deployment.nodeId)}</div> | ||||
|                   </div> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, | ||||
|                   { name: 'Restart', action: async (modalArg: any) => { console.log('Restart deployment:', deployment); await modalArg.destroy(); } }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'Stop', | ||||
|             iconName: 'square', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg: any) => { | ||||
|               const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment; | ||||
|               await appstate.dataState.dispatchAction(appstate.updateDeploymentAction, { | ||||
|                 deploymentId: deployment.id, | ||||
|                 deploymentData: { ...deployment, status: 'stopped' }, | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'Delete', | ||||
|             iconName: 'trash', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg: any) => { | ||||
|               const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment; | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Delete Deployment`, | ||||
|                 content: html` | ||||
|                   <div style="text-align:center">Are you sure you want to delete this deployment?</div> | ||||
|                   <div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;"> | ||||
|                     <div style="color: #fff; font-weight: bold;">${this.getServiceName(deployment.serviceId)}</div> | ||||
|                     <div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">Node: ${this.getNodeName(deployment.nodeId)}</div> | ||||
|                     <div style="color: #f44336; margin-top: 8px;">This action cannot be undone.</div> | ||||
|                   </div> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, | ||||
|                   { name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteDeploymentAction, { deploymentId: deployment.id, }); await modalArg.destroy(); } }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     'cloudly-view-deployments': CloudlyViewDeployments; | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										143
									
								
								ts_web/elements/views/dns/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								ts_web/elements/views/dns/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | ||||
| 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; } } | ||||
|  | ||||
							
								
								
									
										176
									
								
								ts_web/elements/views/domains/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								ts_web/elements/views/domains/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,176 @@ | ||||
| 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; } | ||||
|       .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 () => { | ||||
|               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(); } }, ], | ||||
|               }); | ||||
|             } }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { 'cloudly-view-domains': CloudlyViewDomains; } | ||||
| } | ||||
|  | ||||
							
								
								
									
										118
									
								
								ts_web/elements/views/externalregistries/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								ts_web/elements/views/externalregistries/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| 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-externalregistries') | ||||
| export class CloudlyViewExternalRegistries extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = { secretGroups: [], secretBundles: [], externalRegistries: [] } 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` | ||||
|       .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-inactive { background: #9E9E9E; } | ||||
|       .status-error { background: #f44336; } | ||||
|       .status-unverified { background: #FF9800; } | ||||
|       .type-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; color: white; } | ||||
|       .type-docker { background: #2196F3; } | ||||
|       .type-npm { background: #CB3837; } | ||||
|       .default-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; background: #673AB7; color: white; margin-left: 8px; } | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>External Registries</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'External Registries'} | ||||
|         .heading2=${'Configure external Docker and NPM registries'} | ||||
|         .data=${this.data.externalRegistries || []} | ||||
|         .displayFunction=${(registry: plugins.interfaces.data.IExternalRegistry) => { | ||||
|           return { | ||||
|             Name: html`${registry.data.name}${registry.data.isDefault ? html`<span class="default-badge">DEFAULT</span>` : ''}`, | ||||
|             Type: html`<span class="type-badge type-${registry.data.type}">${registry.data.type.toUpperCase()}</span>`, | ||||
|             URL: registry.data.url, | ||||
|             Auth: registry.data.authType === 'none' ? 'Public' : (registry.data.username || 'Token Auth'), | ||||
|             Namespace: registry.data.namespace || '-', | ||||
|             Status: html`<span class="status-badge status-${registry.data.status || 'unverified'}">${(registry.data.status || 'unverified').toUpperCase()}</span>`, | ||||
|             'Last Verified': registry.data.lastVerified ? new Date(registry.data.lastVerified).toLocaleString() : 'Never', | ||||
|           }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { name: 'Add Registry', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => { | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add External Registry', content: html` | ||||
|                 <dees-form> | ||||
|                   <dees-input-dropdown .key=${'type'} .label=${'Registry Type'} .options=${[{key: 'docker', option: 'Docker'}, {key: 'npm', option: 'NPM'}]} .value=${'docker'} .required=${true}></dees-input-dropdown> | ||||
|                   <dees-input-text .key=${'name'} .label=${'Registry Name'} .placeholder=${'My Docker Hub'} .required=${true}></dees-input-text> | ||||
|                   <dees-input-text .key=${'url'} .label=${'Registry URL'} .placeholder=${'https://index.docker.io/v2/ or registry.gitlab.com'} .required=${true}></dees-input-text> | ||||
|                   <dees-input-text .key=${'username'} .label=${'Username (only needed for basic auth)'} .placeholder=${'username or leave empty for token auth'}></dees-input-text> | ||||
|                   <dees-input-text .key=${'password'} .label=${'Password / Token (NPM _authToken, Docker access token, etc.)'} .placeholder=${'Token or password'} .isPasswordBool=${true}></dees-input-text> | ||||
|                   <dees-input-text .key=${'namespace'} .label=${'Namespace/Organization (optional)'} .placeholder=${'myorg'}></dees-input-text> | ||||
|                   <dees-input-text .key=${'description'} .label=${'Description (optional)'} .placeholder=${'Production Docker registry'}></dees-input-text> | ||||
|                   <dees-input-dropdown .key=${'authType'} .label=${'Authentication Type'} .options=${[{key: 'none', option: 'No Authentication (Public Registry)'}, {key: 'basic', option: 'Basic Auth (Username + Password)'}, {key: 'token', option: 'Token Only (NPM, GitHub, GitLab tokens)'}, {key: 'oauth2', option: 'OAuth2 (Advanced)'}]} .value=${'none'}></dees-input-dropdown> | ||||
|                   <dees-input-checkbox .key=${'isDefault'} .label=${'Set as default registry for this type'} .value=${false}></dees-input-checkbox> | ||||
|                   <dees-input-checkbox .key=${'insecure'} .label=${'Allow insecure connections (HTTP/self-signed certs)'} .value=${false}></dees-input-checkbox> | ||||
|                 </dees-form> | ||||
|               `, menuOptions: [ { name: 'Create Registry', action: async (modalArg: any) => { const form = modalArg.shadowRoot.querySelector('dees-form') as any; const formData = await form.gatherData(); await appstate.dataState.dispatchAction(appstate.createExternalRegistryAction, { registryData: { type: formData.type, name: formData.name, url: formData.url, username: formData.username, password: formData.password, namespace: formData.namespace || undefined, description: formData.description || undefined, authType: formData.authType, isDefault: formData.isDefault, insecure: formData.insecure, }, }); await modalArg.destroy(); } }, { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|           { name: 'Edit', iconName: 'edit', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { | ||||
|               const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry; | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ heading: `Edit Registry: ${registry.data.name}`, content: html` | ||||
|                 <dees-form> | ||||
|                   <dees-input-dropdown .key=${'type'} .label=${'Registry Type'} .options=${[{key: 'docker', option: 'Docker'}, {key: 'npm', option: 'NPM'}]} .value=${registry.data.type} .required=${true}></dees-input-dropdown> | ||||
|                   <dees-input-text .key=${'name'} .label=${'Registry Name'} .value=${registry.data.name} .required=${true}></dees-input-text> | ||||
|                   <dees-input-text .key=${'url'} .label=${'Registry URL'} .value=${registry.data.url} .required=${true}></dees-input-text> | ||||
|                   <dees-input-text .key=${'username'} .label=${'Username (only needed for basic auth)'} .value=${registry.data.username || ''} .placeholder=${'Leave empty for token auth'}></dees-input-text> | ||||
|                   <dees-input-text .key=${'password'} .label=${'Password / Token (leave empty to keep current)'} .placeholder=${'New token or password'} .isPasswordBool=${true}></dees-input-text> | ||||
|                   <dees-input-text .key=${'namespace'} .label=${'Namespace/Organization (optional)'} .value=${registry.data.namespace || ''}></dees-input-text> | ||||
|                   <dees-input-text .key=${'description'} .label=${'Description (optional)'} .value=${registry.data.description || ''}></dees-input-text> | ||||
|                   <dees-input-dropdown .key=${'authType'} .label=${'Authentication Type'} .options=${[{key: 'none', option: 'No Authentication (Public Registry)'}, {key: 'basic', option: 'Basic Auth (Username + Password)'}, {key: 'token', option: 'Token Only (NPM, GitHub, GitLab tokens)'}, {key: 'oauth2', option: 'OAuth2 (Advanced)'}]} .value=${registry.data.authType || 'none'}></dees-input-dropdown> | ||||
|                   <dees-input-checkbox .key=${'isDefault'} .label=${'Set as default registry for this type'} .value=${registry.data.isDefault || false}></dees-input-checkbox> | ||||
|                   <dees-input-checkbox .key=${'insecure'} .label=${'Allow insecure connections (HTTP/self-signed certs)'} .value=${registry.data.insecure || false}></dees-input-checkbox> | ||||
|                 </dees-form> | ||||
|               `, menuOptions: [ { name: 'Update Registry', action: async (modalArg: any) => { const form = modalArg.shadowRoot.querySelector('dees-form') as any; const formData = await form.gatherData(); const updateData: any = { type: formData.type, name: formData.name, url: formData.url, username: formData.username, namespace: formData.namespace || undefined, description: formData.description || undefined, authType: formData.authType, isDefault: formData.isDefault, insecure: formData.insecure, }; if (formData.password) { updateData.password = formData.password; } await appstate.dataState.dispatchAction(appstate.updateExternalRegistryAction, { registryId: registry.id, updates: updateData, }); await modalArg.destroy(); } }, { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|           { name: 'Test Connection', iconName: 'check-circle', type: ['contextmenu'], actionFunc: async (actionDataArg: any) => { | ||||
|               const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry; | ||||
|               const loadingModal = await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Testing Registry Connection', content: html`<div style="text-align: center; padding: 20px;"><dees-spinner></dees-spinner><p style="margin-top: 20px;">Testing connection to ${registry.data.name}...</p></div>`, menuOptions: [] }); | ||||
|               await appstate.dataState.dispatchAction(appstate.verifyExternalRegistryAction, { registryId: registry.id, }); | ||||
|               await loadingModal.destroy(); | ||||
|               const updatedRegistry = this.data.externalRegistries?.find(r => r.id === registry.id); | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Connection Test Result', content: html`<div style="text-align: center; padding: 20px;">${updatedRegistry?.data.status === 'active' ? html`<div style="color: #4CAF50; font-size: 48px;">✓</div><p style="margin-top: 20px; color: #4CAF50;">Connection successful!</p>` : html`<div style="color: #f44336; font-size: 48px;">✗</div><p style="margin-top: 20px; color: #f44336;">Connection failed!</p>${updatedRegistry?.data.lastError ? html`<p style="margin-top: 10px; font-size: 0.9em; color: #999;">Error: ${updatedRegistry.data.lastError}</p>` : ''}`}</div>`, menuOptions: [ { name: 'OK', action: async (modalArg: any) => { await modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|           { name: 'Delete', iconName: 'trash', type: ['contextmenu'], actionFunc: async (actionDataArg: any) => { | ||||
|               const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry; | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete Registry: ${registry.data.name}`, content: html`<div style="text-align:center"><p>Do you really want to delete this external registry?</p><p style="color: #999; font-size: 0.9em; margin-top: 10px;">This will remove all stored credentials and configuration.</p></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;">${registry.data.name} (${registry.data.url})</div>`, menuOptions: [ { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteExternalRegistryAction, { registryId: registry.id, }); await modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { interface HTMLElementTagNameMap { 'cloudly-view-externalregistries': CloudlyViewExternalRegistries; } } | ||||
|  | ||||
							
								
								
									
										142
									
								
								ts_web/elements/views/images/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								ts_web/elements/views/images/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | ||||
| 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-images') | ||||
| export class CloudlyViewImages extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = {} as any; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     appstate.dataState | ||||
|       .select((stateArg) => stateArg) | ||||
|       .subscribe((dataArg) => { | ||||
|         this.data = dataArg; | ||||
|       }); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css``, | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Images</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         heading1="Images" | ||||
|         heading2="an image is needed for running a service" | ||||
|         .data=${this.data.images} | ||||
|         .displayFunction=${(image: plugins.interfaces.data.IImage) => { | ||||
|           return { id: image.id, name: image.data.name, description: image.data.description, versions: image.data.versions.length }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { | ||||
|             name: 'create Image', | ||||
|             type: ['header', 'footer'], | ||||
|             iconName: 'plus', | ||||
|             actionFunc: async () => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'create new Image', | ||||
|                 content: html` | ||||
|                   <dees-form> | ||||
|                     <dees-input-text .label=${'name'} .key=${'data.name'} .value=${''}></dees-input-text> | ||||
|                     <dees-input-text .label=${'description'} .key=${'data.description'} .value=${''}></dees-input-text> | ||||
|                   </dees-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, | ||||
|                   { name: 'save', action: async (modalArg: any) => { | ||||
|                       const deesForm = modalArg.shadowRoot.querySelector('dees-form'); | ||||
|                       const formData = await deesForm.collectFormData(); | ||||
|                       await appstate.dataState.dispatchAction(appstate.createImageAction, { imageName: formData['data.name'] as string, description: formData['data.description'] as string }); | ||||
|                       await modalArg.destroy(); | ||||
|                     } }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'edit', | ||||
|             type: ['contextmenu', 'inRow', 'doubleClick'], | ||||
|             iconName: 'penToSquare', | ||||
|             actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => { | ||||
|               const environmentsArray: Array<plugins.interfaces.data.ISecretGroup['data']['environments'][any] & { environment: string; }> = []; | ||||
|               for (const environmentName of Object.keys(dataArg.item.data.environments)) { | ||||
|                 environmentsArray.push({ environment: environmentName, ...dataArg.item.data.environments[environmentName] }); | ||||
|               } | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Edit Secret', | ||||
|                 content: html` | ||||
|                   <dees-form> | ||||
|                     <dees-input-text .key=${'id'} .disabled=${true} .label=${'ID'} .value=${dataArg.item.id}></dees-input-text> | ||||
|                     <dees-input-text .key=${'data.name'} .disabled=${false} .label=${'name'} .value=${dataArg.item.data.name}></dees-input-text> | ||||
|                     <dees-input-text .key=${'data.description'} .disabled=${false} .label=${'description'} .value=${dataArg.item.data.description}></dees-input-text> | ||||
|                     <dees-input-text .key=${'data.key'} .disabled=${false} .label=${'key'} .value=${dataArg.item.data.key}></dees-input-text> | ||||
|                     <dees-table .key=${'environments'} .heading1=${'Environments'} .heading2=${'double-click to edit values'} | ||||
|                       .data=${environmentsArray.map((itemArg) => ({ environment: itemArg.environment, value: itemArg.value }))} | ||||
|                       .editableFields=${['environment', 'value']} | ||||
|                       .dataActions=${[{ name: 'delete', iconName: 'trash', type: ['inRow'], actionFunc: async (actionDataArg: any) => { actionDataArg.table.data.splice(actionDataArg.table.data.indexOf(actionDataArg.item), 1); } }] as plugins.deesCatalog.ITableAction[]}> | ||||
|                     </dees-table> | ||||
|                   </dees-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { name: 'Cancel', iconName: null, action: async (modalArg: any) => { await modalArg.destroy(); } }, | ||||
|                   { name: 'Save', iconName: null, action: async (modalArg: any) => { const data = await modalArg.shadowRoot.querySelector('dees-form').collectFormData(); console.log(data); } }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'history', | ||||
|             iconName: 'clockRotateLeft', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => { | ||||
|               const historyArray: Array<{ environment: string; value: string; }> = []; | ||||
|               for (const environment of Object.keys(dataArg.item.data.environments)) { | ||||
|                 for (const historyItem of dataArg.item.data.environments[environment].history) { | ||||
|                   historyArray.push({ environment, value: historyItem.value }); | ||||
|                 } | ||||
|               } | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `history for ${dataArg.item.data.key}`, | ||||
|                 content: html`<dees-table .data=${historyArray} .dataActions=${[{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]>) => { console.log('delete', itemArg); }, }] as plugins.deesCatalog.ITableAction[]}></dees-table>`, | ||||
|                 menuOptions: [ { name: 'close', action: async (modalArg: any) => { await modalArg.destroy(); } } ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: 'delete', | ||||
|             iconName: 'trash', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.IImage>) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Delete Image "${itemArg.item.data.name}"`, | ||||
|                 content: html` | ||||
|                   <div style="text-align:center">Do you really want to delete the image?</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;">${itemArg.item.id}</div> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
|                   { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, | ||||
|                   { name: 'delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteImageAction, { imageId: itemArg.item.id, }); await modalArg.destroy(); } }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
|           }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     'cloudly-view-images': CloudlyViewImages; | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										61
									
								
								ts_web/elements/views/logs/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								ts_web/elements/views/logs/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| 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-logs') | ||||
| export class CloudlyViewLogs extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; }); | ||||
|     this.rxSubscriptions.push(subecription); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Logs</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'Logs'} | ||||
|         .heading2=${'decoded in client'} | ||||
|         .data=${this.data.deployments} | ||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { | ||||
|           return { id: itemArg.id, serverAmount: itemArg.data.servers.length }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => { | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', 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-form> | ||||
|               `, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|           { name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html` | ||||
|                 <div style="text-align:center">Do you really want to delete the ConfigBundle?</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> | ||||
|               `, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { interface HTMLElementTagNameMap { 'cloudly-view-logs': CloudlyViewLogs; } } | ||||
|  | ||||
							
								
								
									
										61
									
								
								ts_web/elements/views/mails/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								ts_web/elements/views/mails/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| 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-mails') | ||||
| export class CloudlyViewMails extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; }); | ||||
|     this.rxSubscriptions.push(subecription); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Mails</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'Mails'} | ||||
|         .heading2=${'decoded in client'} | ||||
|         .data=${this.data.deployments} | ||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { | ||||
|           return { id: itemArg.id, serverAmount: itemArg.data.servers.length }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => { | ||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', 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-form> | ||||
|               `, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|           { name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html` | ||||
|                 <div style=\"text-align:center\">Do you really want to delete the ConfigBundle?</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> | ||||
|               `, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { interface HTMLElementTagNameMap { 'cloudly-view-mails': CloudlyViewMails; } } | ||||
|  | ||||
							
								
								
									
										71
									
								
								ts_web/elements/views/overview/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								ts_web/elements/views/overview/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| 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-overview') | ||||
| export class CloudlyViewOverview extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = { | ||||
|     secretGroups: [], | ||||
|     secretBundles: [], | ||||
|   }; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subecription = appstate.dataState | ||||
|       .select((stateArg) => stateArg) | ||||
|       .subscribe((dataArg) => { | ||||
|         this.data = dataArg; | ||||
|       }); | ||||
|     this.rxSubscriptions.push(subecription); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css` | ||||
|       dees-statsgrid { margin-top: 24px; } | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     const totalNodes = this.data.clusters?.reduce((sum, cluster) =>  | ||||
|       sum + (cluster.data.nodes?.length || 0), 0) || 0; | ||||
|  | ||||
|     const statsTiles = [ | ||||
|       { id: 'clusters', title: 'Total Clusters', value: this.data.clusters?.length || 0, type: 'number' as const, iconName: 'lucide:Network', description: 'Active clusters' }, | ||||
|       { id: 'nodes', title: 'Total Nodes', value: totalNodes, type: 'number' as const, iconName: 'lucide:Server', description: 'Connected nodes' }, | ||||
|       { id: 'services', title: 'Services', value: this.data.services?.length || 0, type: 'number' as const, iconName: 'lucide:Layers', description: 'Deployed services' }, | ||||
|       { id: 'deployments', title: 'Deployments', value: this.data.deployments?.length || 0, type: 'number' as const, iconName: 'lucide:Rocket', description: 'Active deployments' }, | ||||
|       { id: 'secretGroups', title: 'Secret Groups', value: this.data.secretGroups?.length || 0, type: 'number' as const, iconName: 'lucide:ShieldCheck', description: 'Configured secret groups' }, | ||||
|       { id: 'secretBundles', title: 'Secret Bundles', value: this.data.secretBundles?.length || 0, type: 'number' as const, iconName: 'lucide:LockKeyhole', description: 'Available secret bundles' }, | ||||
|       { id: 'images', title: 'Images', value: this.data.images?.length || 0, type: 'number' as const, iconName: 'lucide:Image', description: 'Container images' }, | ||||
|       { id: 'dns', title: 'DNS Entries', value: this.data.dnsEntries?.length || 0, type: 'number' as const, iconName: 'lucide:Globe', description: 'Managed DNS records' }, | ||||
|       { id: 'databases', title: 'Databases', value: this.data.dbs?.length || 0, type: 'number' as const, iconName: 'lucide:Database', description: 'Database instances' }, | ||||
|       { id: 'backups', title: 'Backups', value: this.data.backups?.length || 0, type: 'number' as const, iconName: 'lucide:Save', description: 'Available backups' }, | ||||
|       { id: 'mails', title: 'Mail Domains', value: this.data.mails?.length || 0, type: 'number' as const, iconName: 'lucide:Mail', description: 'Mail configurations' }, | ||||
|       { id: 's3', title: 'S3 Buckets', value: this.data.s3?.length || 0, type: 'number' as const, iconName: 'lucide:Cloud', description: 'Storage buckets' }, | ||||
|     ]; | ||||
|  | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Overview</cloudly-sectionheading> | ||||
|       <dees-statsgrid .tiles=${statsTiles} .minTileWidth=${250} .gap=${16}></dees-statsgrid> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     'cloudly-view-overview': CloudlyViewOverview; | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										52
									
								
								ts_web/elements/views/s3/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								ts_web/elements/views/s3/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| 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-s3') | ||||
| export class CloudlyViewS3 extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; }); | ||||
|     this.rxSubscriptions.push(subecription); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>S3</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'S3'} | ||||
|         .heading2=${'decoded in client'} | ||||
|         .data=${this.data.s3} | ||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { return { id: itemArg.id, serverAmount: itemArg.data.servers.length }; }} | ||||
|         .dataActions=${[ | ||||
|           { name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => { | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', 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-form> | ||||
|               `, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|           { name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html` | ||||
|                 <div style="text-align:center">Do you really want to delete the ConfigBundle?</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> | ||||
|               `, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { interface HTMLElementTagNameMap { 'cloudly-view-s3': CloudlyViewS3; } } | ||||
|  | ||||
							
								
								
									
										76
									
								
								ts_web/elements/views/secretbundles/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								ts_web/elements/views/secretbundles/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| 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-secretbundles') | ||||
| export class CloudlyViewSecretBundles extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = {} 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`` ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>SecretBundles</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         .heading1=${'SecretBundles'} | ||||
|         .heading2=${'decoded in client'} | ||||
|         .data=${this.data.secretBundles || []} | ||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ISecretBundle) => { | ||||
|           return { | ||||
|             name: itemArg.data.name, | ||||
|             secretGroups: (() => { | ||||
|               const secretGroupIds = itemArg.data.includedSecretGroupIds; | ||||
|               let secretGroupNames: string[] = []; | ||||
|               for (const secretGroupId of secretGroupIds) { | ||||
|                 const secretGroup = this.data.secretGroups.find((secretGroupArg: any) => secretGroupArg.id === secretGroupId); | ||||
|                 if (secretGroup) { secretGroupNames.push(secretGroup.data.name); } | ||||
|               } | ||||
|               return secretGroupNames.join(', '); | ||||
|             })(), | ||||
|             tags: html`<dees-chips .selectionMode=${'none'} .selectableChips=${itemArg.data.includedTags}></dees-chips>`, | ||||
|           }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { name: 'add SecretBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => { | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add SecretBundle', 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-form> | ||||
|               `, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|           { name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html` | ||||
|                 <div style="text-align:center">Do you really want to delete the ConfigBundle?</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> | ||||
|               `, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|           { name: 'edit', iconName: 'penToSquare', type: ['doubleClick', 'contextmenu', 'inRow'], actionFunc: async () => { | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Edit SecretBundle', content: html`<dees-form><dees-input-text .label=${'purpose'}></dees-input-text></dees-form>`, menuOptions: [ { name: 'save', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { interface HTMLElementTagNameMap { 'cloudly-view-secretbundles': CloudlyViewSecretBundles; } } | ||||
|  | ||||
							
								
								
									
										77
									
								
								ts_web/elements/views/secretgroups/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								ts_web/elements/views/secretgroups/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| 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-secretsgroups') | ||||
| export class CloudlyViewSecretGroups extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = {} 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`` ]; | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>SecretGroups</cloudly-sectionheading> | ||||
|       <dees-table | ||||
|         heading1="SecretGroups" | ||||
|         heading2="decoded in client" | ||||
|         .data=${this.data.secretGroups || []} | ||||
|         .displayFunction=${(secretGroup: plugins.interfaces.data.ISecretGroup) => { | ||||
|           return { | ||||
|             name: secretGroup.data.name, | ||||
|             priority: secretGroup.data.priority, | ||||
|             tags: html`<dees-chips .selectionMode=${'none'} .selectableChips=${secretGroup.data.tags}></dees-chips>`, | ||||
|             key: secretGroup.data.key, | ||||
|             history: (() => { const allHistory = []; for (const environment in secretGroup.data.environments) { allHistory.push(...secretGroup.data.environments[environment].history); } return allHistory.length; })(), | ||||
|           }; | ||||
|         }} | ||||
|         .dataActions=${[ | ||||
|           { name: 'add SecretGroup', type: ['header', 'footer'], iconName: 'plus', actionFunc: async () => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ heading: 'create new SecretGroup', content: html` | ||||
|                 <dees-form> | ||||
|                   <dees-input-text .label=${'name'} .key=${'data.name'} .value=${''}></dees-input-text> | ||||
|                   <dees-input-text .label=${'description'} .key=${'data.description'} .value=${''}></dees-input-text> | ||||
|                   <dees-input-text .label=${'Secret Key (data.key)'} .key=${'data.key'} .value=${''}></dees-input-text> | ||||
|                   <dees-table heading1=${'Environments'} heading2=${'keys need to be unique'} key="environments" .data=${[{ environment: 'production', value: '' }, { environment: 'staging', value: '' }]} .dataActions=${[{ name: 'add environment', iconName: 'plus', type: ['footer'], actionFunc: async (dataArg: any) => { dataArg.table.data.push({ environment: 'new environment', value: '' }); dataArg.table.requestUpdate('data'); } }, { name: 'delete environment', iconName: 'trash', type: ['inRow'], actionFunc: async (dataArg: any) => { dataArg.table.data.splice(dataArg.table.data.indexOf(dataArg.item), 1); dataArg.table.requestUpdate('data'); } }] as plugins.deesCatalog.ITableAction[]} .editableFields=${['environment', 'value']}> | ||||
|                   </dees-table> | ||||
|                 </dees-form> | ||||
|               `, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'save', action: async (modalArg: any) => { const deesForm = modalArg.shadowRoot.querySelector('dees-form'); const formData = await deesForm.collectFormData(); const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] = {}; for (const itemArg of formData['environments'] as any[]) { environments[itemArg.environment] = { value: itemArg.value, history: [], lastUpdated: Date.now(), }; } await appstate.dataState.dispatchAction(appstate.createSecretGroupAction, { id: null, data: { name: formData['data.name'] as string, description: formData['data.description'] as string, key: formData['data.key'] as string, environments, tags: [], }, }); await modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|           { name: 'edit', type: ['contextmenu', 'inRow', 'doubleClick'], iconName: 'penToSquare', actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => { | ||||
|               const environmentsArray: Array<plugins.interfaces.data.ISecretGroup['data']['environments'][any] & { environment: string; }> = []; | ||||
|               for (const environmentName of Object.keys(dataArg.item.data.environments)) { environmentsArray.push({ environment: environmentName, ...dataArg.item.data.environments[environmentName], }); } | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Edit Secret', content: html` | ||||
|                 <dees-form> | ||||
|                   <dees-input-text .key=${'id'} .disabled=${true} .label=${'ID'} .value=${dataArg.item.id}></dees-input-text> | ||||
|                   <dees-input-text .key=${'data.name'} .disabled=${false} .label=${'name'} .value=${dataArg.item.data.name}></dees-input-text> | ||||
|                   <dees-input-text .key=${'data.description'} .disabled=${false} .label=${'description'} .value=${dataArg.item.data.description}></dees-input-text> | ||||
|                   <dees-input-text .key=${'data.key'} .disabled=${false} .label=${'key'} .value=${dataArg.item.data.key}></dees-input-text> | ||||
|                   <dees-table .key=${'environments'} .heading1=${'Environments'} .heading2=${'double-click to edit values'} .data=${environmentsArray.map((itemArg) => ({ environment: itemArg.environment, value: itemArg.value, }))} .editableFields=${['environment', 'value']} .dataActions=${[{ name: 'delete', iconName: 'trash', type: ['inRow'], actionFunc: async (actionDataArg: any) => { actionDataArg.table.data.splice(actionDataArg.table.data.indexOf(actionDataArg.item), 1); } }] as plugins.deesCatalog.ITableAction[]}> | ||||
|                   </dees-table> | ||||
|                 </dees-form> | ||||
|               `, menuOptions: [ { name: 'Cancel', iconName: null, action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'Save', iconName: null, action: async (modalArg: any) => { const data = await modalArg.shadowRoot.querySelector('dees-form').collectFormData(); console.log(data); } } ] }); | ||||
|             } }, | ||||
|           { name: 'history', iconName: 'clockRotateLeft', type: ['contextmenu', 'inRow'], actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => { | ||||
|               const historyArray: Array<{ environment: string; value: string; }> = []; for (const environment of Object.keys(dataArg.item.data.environments)) { for (const historyItem of dataArg.item.data.environments[environment].history) { historyArray.push({ environment, value: historyItem.value, }); } } | ||||
|               await plugins.deesCatalog.DeesModal.createAndShow({ heading: `history for ${dataArg.item.data.key}`, content: html`<dees-table .data=${historyArray} .dataActions=${[{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]>) => { console.log('delete', itemArg); }, }] as plugins.deesCatalog.ITableAction[]}></dees-table>`, menuOptions: [ { name: 'close', action: async (modalArg: any) => { await modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|           { name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => { | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ${itemArg.item.data.key}`, content: html`<div style="text-align:center">Do you really want to delete the secret?</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;">${itemArg.item.data.key}</div>`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteSecretGroupAction, { secretGroupId: itemArg.item.id, }); await modalArg.destroy(); } } ] }); | ||||
|             } }, | ||||
|         ] as plugins.deesCatalog.ITableAction[]} | ||||
|       ></dees-table> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { interface HTMLElementTagNameMap { 'cloudly-view-secretsgroups': CloudlyViewSecretGroups; } } | ||||
|  | ||||
| @@ -1,5 +1,5 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as shared from '../elements/shared/index.js'; | ||||
| import * as plugins from '../../../plugins.js'; | ||||
| import * as shared from '../../shared/index.js'; | ||||
| 
 | ||||
| import { | ||||
|   DeesElement, | ||||
| @@ -10,12 +10,12 @@ import { | ||||
|   cssManager, | ||||
| } from '@design.estate/dees-element'; | ||||
| 
 | ||||
| import * as appstate from '../appstate.js'; | ||||
| import * as appstate from '../../../appstate.js'; | ||||
| 
 | ||||
| @customElement('cloudly-view-services') | ||||
| export class CloudlyViewServices extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = {}; | ||||
|   private data: appstate.IDataState = {} as any; | ||||
| 
 | ||||
|   constructor() { | ||||
|     super(); | ||||
| @@ -31,45 +31,20 @@ export class CloudlyViewServices extends DeesElement { | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css` | ||||
|       .category-badge { | ||||
|         padding: 2px 8px; | ||||
|         border-radius: 4px; | ||||
|         font-size: 0.9em; | ||||
|         font-weight: 500; | ||||
|       } | ||||
|       .category-base { | ||||
|         background: #2196f3; | ||||
|         color: white; | ||||
|       } | ||||
|       .category-distributed { | ||||
|         background: #9c27b0; | ||||
|         color: white; | ||||
|       } | ||||
|       .category-workload { | ||||
|         background: #4caf50; | ||||
|         color: white; | ||||
|       } | ||||
|       .strategy-badge { | ||||
|         padding: 2px 8px; | ||||
|         border-radius: 4px; | ||||
|         font-size: 0.85em; | ||||
|         background: #444; | ||||
|         color: #ccc; | ||||
|         margin-left: 4px; | ||||
|       } | ||||
|       .category-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.9em; font-weight: 500; } | ||||
|       .category-base { background: #2196f3; color: white; } | ||||
|       .category-distributed { background: #9c27b0; color: white; } | ||||
|       .category-workload { background: #4caf50; color: white; } | ||||
|       .strategy-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.85em; background: #444; color: #ccc; margin-left: 4px; } | ||||
|     `,
 | ||||
|   ]; | ||||
| 
 | ||||
|   private getCategoryIcon(category: string): string { | ||||
|     switch (category) { | ||||
|       case 'base': | ||||
|         return 'lucide:ServerCog'; | ||||
|       case 'distributed': | ||||
|         return 'lucide:Network'; | ||||
|       case 'workload': | ||||
|         return 'lucide:Container'; | ||||
|       default: | ||||
|         return 'lucide:Box'; | ||||
|       case 'base': return 'lucide:ServerCog'; | ||||
|       case 'distributed': return 'lucide:Network'; | ||||
|       case 'workload': return 'lucide:Container'; | ||||
|       default: return 'lucide:Box'; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @@ -110,70 +85,28 @@ export class CloudlyViewServices extends DeesElement { | ||||
|             name: 'Add Service', | ||||
|             iconName: 'plus', | ||||
|             type: ['header', 'footer'], | ||||
|             actionFunc: async (dataActionArg) => { | ||||
|             actionFunc: async () => { | ||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: 'Add Service', | ||||
|                 content: html` | ||||
|                   <dees-form> | ||||
|                     <dees-input-text .key=${'name'} .label=${'Service Name'} .required=${true}></dees-input-text> | ||||
|                     <dees-input-text .key=${'description'} .label=${'Description'} .required=${true}></dees-input-text> | ||||
|                     <dees-input-dropdown  | ||||
|                       .key=${'serviceCategory'}  | ||||
|                       .label=${'Service Category'}  | ||||
|                       .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=${[{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=${'1'} | ||||
|                       .type=${'number'}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-checkbox  | ||||
|                       .key=${'antiAffinity'}  | ||||
|                       .label=${'Enable Anti-Affinity'} | ||||
|                       .value=${false}> | ||||
|                     </dees-input-checkbox> | ||||
|                     <dees-input-dropdown .key=${'serviceCategory'} .label=${'Service Category'} .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=${[{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=${'1'} .type=${'number'}></dees-input-text> | ||||
|                     <dees-input-checkbox .key=${'antiAffinity'} .label=${'Enable Anti-Affinity'} .value=${false}></dees-input-checkbox> | ||||
|                     <dees-input-text .key=${'imageId'} .label=${'Image ID'} .required=${true}></dees-input-text> | ||||
|                     <dees-input-text .key=${'imageVersion'} .label=${'Image Version'} .value=${'latest'} .required=${true}></dees-input-text> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'scaleFactor'}  | ||||
|                       .label=${'Scale Factor'}  | ||||
|                       .value=${'1'} | ||||
|                       .type=${'number'} | ||||
|                       .required=${true}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-dropdown  | ||||
|                       .key=${'balancingStrategy'}  | ||||
|                       .label=${'Balancing Strategy'}  | ||||
|                       .options=${[{key: 'round-robin', option: 'Round Robin'}, {key: 'least-connections', option: 'Least Connections'}]} | ||||
|                       .value=${'round-robin'} | ||||
|                       .required=${true}> | ||||
|                     </dees-input-dropdown> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'webPort'}  | ||||
|                       .label=${'Web Port'}  | ||||
|                       .value=${'80'} | ||||
|                       .type=${'number'} | ||||
|                       .required=${true}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-text .key=${'scaleFactor'} .label=${'Scale Factor'} .value=${'1'} .type=${'number'} .required=${true}></dees-input-text> | ||||
|                     <dees-input-dropdown .key=${'balancingStrategy'} .label=${'Balancing Strategy'} .options=${[{key: 'round-robin', option: 'Round Robin'}, {key: 'least-connections', option: 'Least Connections'}]} .value=${'round-robin'} .required=${true}></dees-input-dropdown> | ||||
|                     <dees-input-text .key=${'webPort'} .label=${'Web Port'} .value=${'80'} .type=${'number'} .required=${true}></dees-input-text> | ||||
|                   </dees-form> | ||||
|                 `,
 | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'Create Service', | ||||
|                     action: async (modalArg) => { | ||||
|                   { name: 'Create Service', action: async (modalArg: any) => { | ||||
|                       const form = modalArg.shadowRoot.querySelector('dees-form') as any; | ||||
|                       const formData = await form.gatherData(); | ||||
|                        | ||||
|                       await appstate.dataState.dispatchAction(appstate.createServiceAction, { | ||||
|                         serviceData: { | ||||
|                           name: formData.name, | ||||
| @@ -186,24 +119,15 @@ export class CloudlyViewServices extends DeesElement { | ||||
|                           imageVersion: formData.imageVersion, | ||||
|                           scaleFactor: parseInt(formData.scaleFactor), | ||||
|                           balancingStrategy: formData.balancingStrategy, | ||||
|                           ports: { | ||||
|                             web: parseInt(formData.webPort), | ||||
|                           }, | ||||
|                           ports: { web: parseInt(formData.webPort) }, | ||||
|                           environment: {}, | ||||
|                           domains: [], | ||||
|                           deploymentIds: [], | ||||
|                         }, | ||||
|                       }); | ||||
|                        | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'Cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                     }}, | ||||
|                   { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
| @@ -212,7 +136,7 @@ export class CloudlyViewServices extends DeesElement { | ||||
|             name: 'Edit', | ||||
|             iconName: 'edit', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg) => { | ||||
|             actionFunc: async (actionDataArg: any) => { | ||||
|               const service = actionDataArg.item as plugins.interfaces.data.IService; | ||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Edit Service: ${service.data.name}`, | ||||
| @@ -220,55 +144,19 @@ export class CloudlyViewServices extends DeesElement { | ||||
|                   <dees-form> | ||||
|                     <dees-input-text .key=${'name'} .label=${'Service Name'} .value=${service.data.name} .required=${true}></dees-input-text> | ||||
|                     <dees-input-text .key=${'description'} .label=${'Description'} .value=${service.data.description} .required=${true}></dees-input-text> | ||||
|                     <dees-input-dropdown  | ||||
|                       .key=${'serviceCategory'}  | ||||
|                       .label=${'Service Category'}  | ||||
|                       .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=${[{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> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'maxReplicas'}  | ||||
|                       .label=${'Max Replicas (for distributed services)'}  | ||||
|                       .value=${service.data.maxReplicas || ''} | ||||
|                       .type=${'number'}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-checkbox  | ||||
|                       .key=${'antiAffinity'}  | ||||
|                       .label=${'Enable Anti-Affinity'} | ||||
|                       .value=${service.data.antiAffinity || false}> | ||||
|                     </dees-input-checkbox> | ||||
|                     <dees-input-dropdown .key=${'serviceCategory'} .label=${'Service Category'} .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=${[{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> | ||||
|                     <dees-input-text .key=${'maxReplicas'} .label=${'Max Replicas (for distributed services)'} .value=${service.data.maxReplicas || ''} .type=${'number'}></dees-input-text> | ||||
|                     <dees-input-checkbox .key=${'antiAffinity'} .label=${'Enable Anti-Affinity'} .value=${service.data.antiAffinity || false}></dees-input-checkbox> | ||||
|                     <dees-input-text .key=${'imageVersion'} .label=${'Image Version'} .value=${service.data.imageVersion} .required=${true}></dees-input-text> | ||||
|                     <dees-input-text  | ||||
|                       .key=${'scaleFactor'}  | ||||
|                       .label=${'Scale Factor'}  | ||||
|                       .value=${service.data.scaleFactor} | ||||
|                       .type=${'number'} | ||||
|                       .required=${true}> | ||||
|                     </dees-input-text> | ||||
|                     <dees-input-dropdown  | ||||
|                       .key=${'balancingStrategy'}  | ||||
|                       .label=${'Balancing Strategy'}  | ||||
|                       .options=${[{key: 'round-robin', option: 'Round Robin'}, {key: 'least-connections', option: 'Least Connections'}]} | ||||
|                       .value=${service.data.balancingStrategy} | ||||
|                       .required=${true}> | ||||
|                     </dees-input-dropdown> | ||||
|                     <dees-input-text .key=${'scaleFactor'} .label=${'Scale Factor'} .value=${service.data.scaleFactor} .type=${'number'} .required=${true}></dees-input-text> | ||||
|                     <dees-input-dropdown .key=${'balancingStrategy'} .label=${'Balancing Strategy'} .options=${[{key: 'round-robin', option: 'Round Robin'}, {key: 'least-connections', option: 'Least Connections'}]} .value=${service.data.balancingStrategy} .required=${true}></dees-input-dropdown> | ||||
|                   </dees-form> | ||||
|                 `,
 | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'Update Service', | ||||
|                     action: async (modalArg) => { | ||||
|                   { name: 'Update Service', action: async (modalArg: any) => { | ||||
|                       const form = modalArg.shadowRoot.querySelector('dees-form') as any; | ||||
|                       const formData = await form.gatherData(); | ||||
|                        | ||||
|                       await appstate.dataState.dispatchAction(appstate.updateServiceAction, { | ||||
|                         serviceId: service.id, | ||||
|                         serviceData: { | ||||
| @@ -284,16 +172,9 @@ export class CloudlyViewServices extends DeesElement { | ||||
|                           balancingStrategy: formData.balancingStrategy, | ||||
|                         }, | ||||
|                       }); | ||||
|                        | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'Cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                     }}, | ||||
|                   { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
| @@ -302,9 +183,8 @@ export class CloudlyViewServices extends DeesElement { | ||||
|             name: 'Deploy', | ||||
|             iconName: 'rocket', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg) => { | ||||
|             actionFunc: async (actionDataArg: any) => { | ||||
|               const service = actionDataArg.item as plugins.interfaces.data.IService; | ||||
|               // TODO: Implement deployment action
 | ||||
|               console.log('Deploy service:', service); | ||||
|             }, | ||||
|           }, | ||||
| @@ -312,38 +192,21 @@ export class CloudlyViewServices extends DeesElement { | ||||
|             name: 'Delete', | ||||
|             iconName: 'trash', | ||||
|             type: ['contextmenu', 'inRow'], | ||||
|             actionFunc: async (actionDataArg) => { | ||||
|             actionFunc: async (actionDataArg: any) => { | ||||
|               const service = actionDataArg.item as plugins.interfaces.data.IService; | ||||
|               plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|                 heading: `Delete Service: ${service.data.name}`, | ||||
|                 content: html` | ||||
|                   <div style="text-align:center"> | ||||
|                     Are you sure you want to delete this service? | ||||
|                   </div> | ||||
|                   <div style="text-align:center">Are you sure you want to delete this service?</div> | ||||
|                   <div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;"> | ||||
|                     <div style="color: #fff; font-weight: bold;">${service.data.name}</div> | ||||
|                     <div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">${service.data.description}</div> | ||||
|                     <div style="color: #f44336; margin-top: 8px;"> | ||||
|                       This will also delete ${service.data.deploymentIds?.length || 0} deployment(s) | ||||
|                     </div> | ||||
|                     <div style="color: #f44336; margin-top: 8px;">This will also delete ${service.data.deploymentIds?.length || 0} deployment(s)</div> | ||||
|                   </div> | ||||
|                 `,
 | ||||
|                 menuOptions: [ | ||||
|                   { | ||||
|                     name: 'Cancel', | ||||
|                     action: async (modalArg) => { | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     name: 'Delete', | ||||
|                     action: async (modalArg) => { | ||||
|                       await appstate.dataState.dispatchAction(appstate.deleteServiceAction, { | ||||
|                         serviceId: service.id, | ||||
|                       }); | ||||
|                       await modalArg.destroy(); | ||||
|                     }, | ||||
|                   }, | ||||
|                   { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() }, | ||||
|                   { name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteServiceAction, { serviceId: service.id }); await modalArg.destroy(); } }, | ||||
|                 ], | ||||
|               }); | ||||
|             }, | ||||
| @@ -352,4 +215,11 @@ export class CloudlyViewServices extends DeesElement { | ||||
|       ></dees-table> | ||||
|     `;
 | ||||
|   } | ||||
| } | ||||
| } | ||||
| 
 | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     'cloudly-view-services': CloudlyViewServices; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										206
									
								
								ts_web/elements/views/settings/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								ts_web/elements/views/settings/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,206 @@ | ||||
| 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-settings') | ||||
| export class CloudlyViewSettings extends DeesElement { | ||||
|   @state() | ||||
|   private settings: plugins.interfaces.data.ICloudlySettingsMasked = {} as any; | ||||
|  | ||||
|   @state() | ||||
|   private isLoading = false; | ||||
|  | ||||
|   @state() | ||||
|   private testResults: {[key: string]: {success: boolean; message: string}} = {}; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     this.loadSettings(); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css` | ||||
|       .settings-container { padding: 24px 0; display: flex; flex-direction: column; gap: 16px; } | ||||
|       .provider-icon { margin-right: 8px; font-size: 20px; } | ||||
|       .test-status { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; } | ||||
|       .test-status dees-button { margin-left: auto; } | ||||
|       .loading-container { display: flex; justify-content: center; padding: 48px; } | ||||
|       .actions-container { display: flex; justify-content: center; margin-top: 24px; } | ||||
|       dees-panel { margin-bottom: 16px; } | ||||
|       .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } | ||||
|       .form-grid.single { grid-template-columns: 1fr; } | ||||
|       @media (max-width: 768px) { .form-grid { grid-template-columns: 1fr; } } | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   private async loadSettings() { | ||||
|     this.isLoading = true; | ||||
|     try { | ||||
|       const response = await appstate.apiClient.settings.getSettings(); | ||||
|       this.settings = response.settings; | ||||
|     } catch (error: any) { | ||||
|       console.error('Failed to load settings:', error); | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ message: `Failed to load settings: ${error.message}`, type: 'error' }); | ||||
|     } finally { | ||||
|       this.isLoading = false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async saveSettings(formData: any) { | ||||
|     this.isLoading = true; | ||||
|     try { | ||||
|       const updates: Partial<plugins.interfaces.data.ICloudlySettings> = {}; | ||||
|       for (const [key, value] of Object.entries(formData)) { | ||||
|         if (value !== undefined && value !== '****' && !value?.toString().endsWith('****')) { | ||||
|           updates[key as keyof plugins.interfaces.data.ICloudlySettings] = value as string; | ||||
|         } | ||||
|       } | ||||
|       const response = await appstate.apiClient.settings.updateSettings(updates); | ||||
|       if (response.success) { | ||||
|         plugins.deesCatalog.DeesToast.createAndShow({ message: 'Settings saved successfully', type: 'success' }); | ||||
|         await this.loadSettings(); | ||||
|       } else { | ||||
|         throw new Error(response.message); | ||||
|       } | ||||
|     } catch (error: any) { | ||||
|       console.error('Failed to save settings:', error); | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ message: `Failed to save settings: ${error.message}`, type: 'error' }); | ||||
|     } finally { | ||||
|       this.isLoading = false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async testConnection(provider: string) { | ||||
|     this.isLoading = true; | ||||
|     try { | ||||
|       const response = await appstate.apiClient.settings.testProviderConnection(provider); | ||||
|       this.testResults = { ...this.testResults, [provider]: { success: response.connectionValid, message: response.message } }; | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ message: response.message, type: response.connectionValid ? 'success' : 'error' }); | ||||
|     } catch (error: any) { | ||||
|       this.testResults = { ...this.testResults, [provider]: { success: false, message: `Test failed: ${error.message}` } }; | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ message: `Connection test failed: ${error.message}`, type: 'error' }); | ||||
|     } finally { | ||||
|       this.isLoading = false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private renderProviderStatus(provider: string) { | ||||
|     const result = this.testResults[provider]; | ||||
|     if (!result) return '' as any; | ||||
|     return html`<dees-badge .type=${result.success ? 'success' : 'error'} .text=${result.success ? 'Connected' : 'Failed'}></dees-badge>`; | ||||
|   } | ||||
|  | ||||
|   public render() { | ||||
|     if (this.isLoading && Object.keys(this.settings).length === 0) { | ||||
|       return html`<div class="loading-container"><dees-spinner></dees-spinner></div>`; | ||||
|     } | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Settings</cloudly-sectionheading> | ||||
|       <div class="settings-container"> | ||||
|         <dees-form @formData=${(e: CustomEvent) => { this.saveSettings((e.detail as any).data); }}> | ||||
|           <dees-panel .title=${'Hetzner Cloud'} .subtitle=${'Configure Hetzner Cloud API access'} .variant=${'outline'}> | ||||
|             <div class="test-status"> | ||||
|               ${this.renderProviderStatus('hetzner')} | ||||
|               <dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('hetzner'); }}></dees-button> | ||||
|             </div> | ||||
|             <div class="form-grid single"> | ||||
|               <dees-input-text .key=${'hetznerToken'} .label=${'API Token'} .value=${this.settings.hetznerToken || ''} .isPasswordBool=${true} .description=${'Your Hetzner Cloud API token for managing infrastructure'} .required=${false}></dees-input-text> | ||||
|             </div> | ||||
|           </dees-panel> | ||||
|  | ||||
|           <dees-panel .title=${'Cloudflare'} .subtitle=${'Configure Cloudflare API access'} .variant=${'outline'}> | ||||
|             <div class="test-status"> | ||||
|               ${this.renderProviderStatus('cloudflare')} | ||||
|               <dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('cloudflare'); }}></dees-button> | ||||
|             </div> | ||||
|             <div class="form-grid single"> | ||||
|               <dees-input-text .key=${'cloudflareToken'} .label=${'API Token'} .value=${this.settings.cloudflareToken || ''} .isPasswordBool=${true} .description=${'Cloudflare API token with DNS and Zone permissions'} .required=${false}></dees-input-text> | ||||
|             </div> | ||||
|           </dees-panel> | ||||
|  | ||||
|           <dees-panel .title=${'Amazon Web Services'} .subtitle=${'Configure AWS credentials'} .variant=${'outline'}> | ||||
|             <div class="test-status"> | ||||
|               ${this.renderProviderStatus('aws')} | ||||
|               <dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('aws'); }}></dees-button> | ||||
|             </div> | ||||
|             <div class="form-grid"> | ||||
|               <dees-input-text .key=${'awsAccessKey'} .label=${'Access Key ID'} .value=${this.settings.awsAccessKey || ''} .isPasswordBool=${true} .description=${'AWS IAM access key identifier'} .required=${false}></dees-input-text> | ||||
|               <dees-input-text .key=${'awsSecretKey'} .label=${'Secret Access Key'} .value=${this.settings.awsSecretKey || ''} .isPasswordBool=${true} .description=${'AWS IAM secret access key'} .required=${false}></dees-input-text> | ||||
|             </div> | ||||
|             <div class="form-grid single"> | ||||
|               <dees-input-dropdown .key=${'awsRegion'} .label=${'Default Region'} .selectedOption=${this.settings.awsRegion || 'us-east-1'} .options=${[ | ||||
|                 { key: 'us-east-1', option: 'US East (N. Virginia)', payload: null }, | ||||
|                 { key: 'us-west-2', option: 'US West (Oregon)', payload: null }, | ||||
|                 { key: 'eu-west-1', option: 'EU (Ireland)', payload: null }, | ||||
|                 { key: 'eu-central-1', option: 'EU (Frankfurt)', payload: null }, | ||||
|                 { key: 'ap-southeast-1', option: 'Asia Pacific (Singapore)', payload: null }, | ||||
|                 { key: 'ap-northeast-1', option: 'Asia Pacific (Tokyo)', payload: null }, | ||||
|               ]} .description=${'Default AWS region for resource provisioning'}></dees-input-dropdown> | ||||
|             </div> | ||||
|           </dees-panel> | ||||
|  | ||||
|           <dees-panel .title=${'DigitalOcean'} .subtitle=${'Configure DigitalOcean API access'} .variant=${'outline'}> | ||||
|             <div class="test-status"> | ||||
|               ${this.renderProviderStatus('digitalocean')} | ||||
|               <dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('digitalocean'); }}></dees-button> | ||||
|             </div> | ||||
|             <div class="form-grid single"> | ||||
|               <dees-input-text .key=${'digitalOceanToken'} .label=${'Personal Access Token'} .value=${this.settings.digitalOceanToken || ''} .isPasswordBool=${true} .description=${'DigitalOcean personal access token with read/write scope'} .required=${false}></dees-input-text> | ||||
|             </div> | ||||
|           </dees-panel> | ||||
|  | ||||
|           <dees-panel .title=${'Microsoft Azure'} .subtitle=${'Configure Azure service principal'} .variant=${'outline'}> | ||||
|             <div class="test-status"> | ||||
|               ${this.renderProviderStatus('azure')} | ||||
|               <dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('azure'); }}></dees-button> | ||||
|             </div> | ||||
|             <div class="form-grid"> | ||||
|               <dees-input-text .key=${'azureClientId'} .label=${'Application (Client) ID'} .value=${this.settings.azureClientId || ''} .isPasswordBool=${true} .description=${'Azure AD application client ID'} .required=${false}></dees-input-text> | ||||
|               <dees-input-text .key=${'azureClientSecret'} .label=${'Client Secret'} .value=${this.settings.azureClientSecret || ''} .isPasswordBool=${true} .description=${'Azure AD application client secret'} .required=${false}></dees-input-text> | ||||
|             </div> | ||||
|             <div class="form-grid"> | ||||
|               <dees-input-text .key=${'azureTenantId'} .label=${'Directory (Tenant) ID'} .value=${this.settings.azureTenantId || ''} .description=${'Azure AD tenant identifier'} .required=${false}></dees-input-text> | ||||
|               <dees-input-text .key=${'azureSubscriptionId'} .label=${'Subscription ID'} .value=${this.settings.azureSubscriptionId || ''} .description=${'Azure subscription for resource management'} .required=${false}></dees-input-text> | ||||
|             </div> | ||||
|           </dees-panel> | ||||
|  | ||||
|           <dees-panel .title=${'Google Cloud Platform'} .subtitle=${'Configure GCP service account'} .variant=${'outline'}> | ||||
|             <div class="test-status"> | ||||
|               ${this.renderProviderStatus('google')} | ||||
|               <dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('google'); }}></dees-button> | ||||
|             </div> | ||||
|             <div class="form-grid single"> | ||||
|               <dees-input-textarea .key=${'googleCloudKeyJson'} .label=${'Service Account Key (JSON)'} .value=${this.settings.googleCloudKeyJson || ''} .isPasswordBool=${true} .description=${'Complete JSON key file for service account authentication'} .required=${false}></dees-input-textarea> | ||||
|             </div> | ||||
|             <div class="form-grid single"> | ||||
|               <dees-input-text .key=${'googleCloudProjectId'} .label=${'Project ID'} .value=${this.settings.googleCloudProjectId || ''} .description=${'Google Cloud project identifier'} .required=${false}></dees-input-text> | ||||
|             </div> | ||||
|           </dees-panel> | ||||
|  | ||||
|           <div class="actions-container"> | ||||
|             <dees-form-submit .text=${'Save All Settings'} .disabled=${this.isLoading}></dees-form-submit> | ||||
|           </div> | ||||
|         </dees-form> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     'cloudly-view-settings': CloudlyViewSettings; | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										308
									
								
								ts_web/elements/views/tasks/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										308
									
								
								ts_web/elements/views/tasks/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,308 @@ | ||||
| import * as shared from '../../shared/index.js'; | ||||
| import * as plugins from '../../../plugins.js'; | ||||
|  | ||||
| import { | ||||
|   DeesElement, | ||||
|   customElement, | ||||
|   html, | ||||
|   state, | ||||
|   css, | ||||
|   cssManager, | ||||
| } from '@design.estate/dees-element'; | ||||
|  | ||||
| import * as appstate from '../../../appstate.js'; | ||||
| import './parts/cloudly-task-panel.js'; | ||||
| import './parts/cloudly-execution-details.js'; | ||||
| import { formatCronFriendly, formatDate, formatDuration } from './utils.js'; | ||||
|  | ||||
| @customElement('cloudly-view-tasks') | ||||
| export class CloudlyViewTasks extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = {} as any; | ||||
|  | ||||
|   @state() | ||||
|   private selectedExecution: plugins.interfaces.data.ITaskExecution | null = null; | ||||
|  | ||||
|   @state() | ||||
|   private loading = false; | ||||
|  | ||||
|   @state() | ||||
|   private filterStatus: string = 'all'; | ||||
|  | ||||
|   @state() | ||||
|   private searchQuery: string = ''; | ||||
|  | ||||
|   @state() | ||||
|   private categoryFilter: string = 'all'; | ||||
|  | ||||
|   @state() | ||||
|   private autoRefresh: boolean = true; | ||||
|  | ||||
|   private _refreshHandle: any = null; | ||||
|   @state() | ||||
|   private canceling: Record<string, boolean> = {}; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subscription = appstate.dataState | ||||
|       .select((stateArg) => stateArg) | ||||
|       .subscribe((dataArg) => { | ||||
|         this.data = dataArg; | ||||
|       }); | ||||
|     this.rxSubscriptions.push(subscription); | ||||
|      | ||||
|     // Load initial data (non-blocking) | ||||
|     this.loadInitialData(); | ||||
|  | ||||
|     // Start periodic refresh (lightweight; executions only by default) | ||||
|     this.startAutoRefresh(); | ||||
|   } | ||||
|  | ||||
|   private async loadInitialData() { | ||||
|     try { | ||||
|       await appstate.dataState.dispatchAction(appstate.taskActions.getTasks, {}); | ||||
|       await appstate.dataState.dispatchAction(appstate.taskActions.getTaskExecutions, {}); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to load initial task data:', error); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private startAutoRefresh() { | ||||
|     this.stopAutoRefresh(); | ||||
|     if (!this.autoRefresh) return; | ||||
|     this._refreshHandle = setInterval(async () => { | ||||
|       try { | ||||
|         await this.loadExecutionsWithFilter(); | ||||
|       } catch (err) { | ||||
|         // ignore transient errors during refresh | ||||
|       } | ||||
|     }, 5000); | ||||
|   } | ||||
|  | ||||
|   private stopAutoRefresh() { | ||||
|     if (this._refreshHandle) { | ||||
|       clearInterval(this._refreshHandle); | ||||
|       this._refreshHandle = null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public async disconnectedCallback(): Promise<void> { | ||||
|     await (super.disconnectedCallback?.()); | ||||
|     this.stopAutoRefresh(); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css` | ||||
|       .toolbar { display: flex; gap: 12px; align-items: center; margin: 4px 0 16px 0; flex-wrap: wrap; } | ||||
|       .toolbar .spacer { flex: 1 1 auto; } | ||||
|       .search-input { background: #111; color: #ddd; border: 1px solid #333; border-radius: 6px; padding: 8px 10px; min-width: 220px; } | ||||
|       .chipbar { display: flex; gap: 8px; flex-wrap: wrap; } | ||||
|       .chip { padding: 6px 10px; background: #2a2a2a; color: #bbb; border: 1px solid #444; border-radius: 16px; cursor: pointer; transition: all 0.2s; user-select: none; } | ||||
|       .chip.active { background: #2196f3; border-color: #2196f3; color: white; } | ||||
|  | ||||
|       .task-list { display: flex; flex-direction: column; gap: 16px; margin-bottom: 32px; } | ||||
|  | ||||
|       .secondary-button { padding: 6px 12px; background: #2b2b2b; color: #ccc; border: 1px solid #444; border-radius: 6px; cursor: pointer; font-size: 0.9em; transition: background 0.2s, border-color 0.2s; } | ||||
|       .secondary-button:hover { background: #363636; border-color: #555; } | ||||
|  | ||||
|       /* Shared badge styles used within the table content */ | ||||
|       .status-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; } | ||||
|       .status-running { background: #2196f3; color: white; } | ||||
|       .status-completed { background: #4caf50; color: white; } | ||||
|       .status-failed { background: #f44336; color: white; } | ||||
|       .status-cancelled { background: #ff9800; color: white; } | ||||
|  | ||||
|       .execution-logs { background: #0a0a0a; border: 1px solid #333; border-radius: 6px; padding: 16px; margin-top: 16px; max-height: 400px; overflow-y: auto; } | ||||
|       .log-entry { font-family: monospace; font-size: 0.9em; margin-bottom: 8px; padding: 4px 8px; border-radius: 4px; } | ||||
|       .log-info { color: #2196f3; } | ||||
|       .log-warning { color: #ff9800; background: rgba(255, 152, 0, 0.1); } | ||||
|       .log-error { color: #f44336; background: rgba(244, 67, 54, 0.1); } | ||||
|       .log-success { color: #4caf50; background: rgba(76, 175, 80, 0.1); } | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   private async triggerTask(taskName: string) { | ||||
|     try { | ||||
|       const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|         heading: `Run Task: ${taskName}`, | ||||
|         content: html`<div><p>Do you want to trigger this task now?</p></div>`, | ||||
|         menuOptions: [ | ||||
|           { | ||||
|             name: 'Run now', | ||||
|             action: async (modalArg: any) => { | ||||
|               await appstate.dataState.dispatchAction(appstate.taskActions.triggerTask, { taskName }); | ||||
|               plugins.deesCatalog.DeesToast.createAndShow({ message: `Task ${taskName} triggered`, type: 'success' }); | ||||
|               await modalArg.destroy(); | ||||
|               await this.loadExecutionsWithFilter(); | ||||
|             } | ||||
|           }, | ||||
|           { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() } | ||||
|         ] | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to trigger task:', error); | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ message: `Failed to trigger: ${error.message}`, type: 'error' }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async cancelTaskFor(taskName: string) { | ||||
|     try { | ||||
|       const executions = (this.data.taskExecutions || []) | ||||
|         .filter((e: any) => e.data.taskName === taskName && e.data.status === 'running') | ||||
|         .sort((a: any, b: any) => (b.data.startedAt || 0) - (a.data.startedAt || 0)); | ||||
|       const running = executions[0]; | ||||
|       if (!running) return; | ||||
|  | ||||
|       this.canceling = { ...this.canceling, [running.id]: true }; | ||||
|       try { | ||||
|         await appstate.dataState.dispatchAction(appstate.taskActions.cancelTask, { executionId: running.id }); | ||||
|         plugins.deesCatalog.DeesToast.createAndShow({ message: `Cancelled ${taskName}`, type: 'success' }); | ||||
|       } finally { | ||||
|         this.canceling = { ...this.canceling, [running.id]: false }; | ||||
|         await this.loadExecutionsWithFilter(); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       console.error('Failed to cancel task:', err); | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ message: `Cancel failed: ${err.message}`, type: 'error' }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async loadExecutionsWithFilter() { | ||||
|     try { | ||||
|       const filter: any = {}; | ||||
|       if (this.filterStatus !== 'all') { | ||||
|         filter.status = this.filterStatus; | ||||
|       } | ||||
|       await appstate.dataState.dispatchAction(appstate.taskActions.getTaskExecutions, { filter }); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to load executions:', error); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async openExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) { | ||||
|     this.selectedExecution = execution; | ||||
|     requestAnimationFrame(() => { | ||||
|       this.shadowRoot?.querySelector('cloudly-sectionheading + cloudly-execution-details')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private async openLogsModal(execution: plugins.interfaces.data.ITaskExecution) { | ||||
|     await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|       heading: `Logs: ${execution.data.taskName}`, | ||||
|       content: html` | ||||
|         <div class="execution-logs"> | ||||
|           ${(execution.data.logs || []).map((log: any) => html` | ||||
|             <div class="log-entry log-${log.severity}"><span>${formatDate(log.timestamp)}</span> - ${log.message}</div> | ||||
|           `)} | ||||
|         </div> | ||||
|       `, | ||||
|       menuOptions: [ | ||||
|         { | ||||
|           name: 'Copy All', | ||||
|           action: async (modalArg: any) => { | ||||
|             try { | ||||
|               await navigator.clipboard.writeText((execution.data.logs || []) | ||||
|                 .map((l: any) => `${new Date(l.timestamp).toISOString()} [${l.severity}] ${l.message}`).join('\n')); | ||||
|               plugins.deesCatalog.DeesToast.createAndShow({ message: 'Logs copied', type: 'success' }); | ||||
|             } catch (e) { | ||||
|               plugins.deesCatalog.DeesToast.createAndShow({ message: 'Copy failed', type: 'error' }); | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         { name: 'Close', action: async (modalArg: any) => modalArg.destroy() } | ||||
|       ] | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   public render() { | ||||
|     const tasks = (this.data.tasks || []) as any[]; | ||||
|     const categories = Array.from(new Set(tasks.map(t => t.category))).sort(); | ||||
|     const filteredTasks = tasks | ||||
|       .filter(t => this.categoryFilter === 'all' || t.category === this.categoryFilter) | ||||
|       .filter(t => !this.searchQuery || t.name.toLowerCase().includes(this.searchQuery.toLowerCase()) || (t.description || '').toLowerCase().includes(this.searchQuery.toLowerCase())); | ||||
|  | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Tasks</cloudly-sectionheading> | ||||
|  | ||||
|       <dees-panel .title=${'Task Library'} .subtitle=${'Run maintenance, monitoring and system tasks'} .variant=${'outline'}> | ||||
|         <div class="toolbar"> | ||||
|           <div class="chipbar"> | ||||
|             <div class="chip ${this.categoryFilter === 'all' ? 'active' : ''}" | ||||
|               @click=${() => { this.categoryFilter = 'all'; }}> | ||||
|               All | ||||
|             </div> | ||||
|             ${categories.map(cat => html` | ||||
|               <div class="chip ${this.categoryFilter === cat ? 'active' : ''}" | ||||
|                 @click=${() => { this.categoryFilter = cat; }}> | ||||
|                 ${cat} | ||||
|               </div> | ||||
|             `)} | ||||
|           </div> | ||||
|           <div class="spacer"></div> | ||||
|           <input class="search-input" placeholder="Search tasks" .value=${this.searchQuery} | ||||
|             @input=${(e: any) => { this.searchQuery = e.target.value; }} /> | ||||
|           <button class="secondary-button" @click=${async () => { await this.loadExecutionsWithFilter(); }}>Refresh</button> | ||||
|           <button class="secondary-button" @click=${() => { this.autoRefresh = !this.autoRefresh; this.autoRefresh ? this.startAutoRefresh() : this.stopAutoRefresh(); }}> | ||||
|             ${this.autoRefresh ? 'Auto-Refresh: On' : 'Auto-Refresh: Off'} | ||||
|           </button> | ||||
|         </div> | ||||
|  | ||||
|         <div class="task-list"> | ||||
|           ${filteredTasks.map(task => html` | ||||
|             <cloudly-task-panel | ||||
|               .task=${task} | ||||
|               .executions=${this.data.taskExecutions || []} | ||||
|               .canceling=${this.canceling} | ||||
|               .onRun=${(name: string) => this.triggerTask(name)} | ||||
|               .onCancel=${(name: string) => this.cancelTaskFor(name)} | ||||
|               .onOpenDetails=${(exec: any) => this.openExecutionDetails(exec)} | ||||
|               .onOpenLogs=${(exec: any) => this.openLogsModal(exec)} | ||||
|             ></cloudly-task-panel> | ||||
|           `)} | ||||
|         </div> | ||||
|       </dees-panel> | ||||
|  | ||||
|       <cloudly-sectionheading>Execution History</cloudly-sectionheading> | ||||
|  | ||||
|       <dees-panel .title=${'Recent Executions'} .subtitle=${'History of task runs and their outcomes'} .variant=${'outline'}> | ||||
|         <dees-table | ||||
|           .heading1=${'Task Executions'} | ||||
|           .heading2=${'History of task runs and their outcomes'} | ||||
|           .data=${this.data.taskExecutions || []} | ||||
|           .displayFunction=${(itemArg: plugins.interfaces.data.ITaskExecution) => { | ||||
|             return { | ||||
|               Task: itemArg.data.taskName, | ||||
|               Status: html`<span class="status-badge status-${itemArg.data.status}">${itemArg.data.status}</span>`, | ||||
|               'Started At': formatDate(itemArg.data.startedAt), | ||||
|               Duration: itemArg.data.duration ? formatDuration(itemArg.data.duration) : '-', | ||||
|               'Triggered By': itemArg.data.triggeredBy, | ||||
|               Logs: itemArg.data.logs?.length || 0, | ||||
|             } as any; | ||||
|           }} | ||||
|           .actionFunction=${async (itemArg: plugins.interfaces.data.ITaskExecution) => { | ||||
|             const actions: any[] = [ | ||||
|               { name: 'View Details', iconName: 'lucide:Eye', type: ['inRow'], actionFunc: async () => { this.selectedExecution = itemArg; } } | ||||
|             ]; | ||||
|             if (itemArg.data.status === 'running') { | ||||
|               actions.push({ name: 'Cancel', iconName: 'lucide:SquareX', type: ['inRow'], actionFunc: async () => { await appstate.dataState.dispatchAction(appstate.taskActions.cancelTask, { executionId: itemArg.id }); await this.loadExecutionsWithFilter(); } }); | ||||
|             } | ||||
|             return actions; | ||||
|           }} | ||||
|         ></dees-table> | ||||
|       </dees-panel> | ||||
|  | ||||
|       ${this.selectedExecution ? html` | ||||
|         <cloudly-sectionheading>Execution Details</cloudly-sectionheading> | ||||
|         <cloudly-execution-details .execution=${this.selectedExecution}></cloudly-execution-details> | ||||
|       ` : ''} | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     'cloudly-view-tasks': CloudlyViewTasks; | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,93 @@ | ||||
| import { DeesElement, customElement, html, css, cssManager, property } from '@design.estate/dees-element'; | ||||
| import { formatDate, formatDuration } from '../utils.js'; | ||||
|  | ||||
| @customElement('cloudly-execution-details') | ||||
| export class CloudlyExecutionDetails extends DeesElement { | ||||
|   @property({ type: Object }) execution: any; | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     css` | ||||
|       .execution-details h3, .execution-details h4 { margin: 8px 0; } | ||||
|       .metrics { display: flex; gap: 16px; margin-top: 12px; padding-top: 12px; border-top: 1px solid #333; } | ||||
|       .metric { display: flex; flex-direction: column; } | ||||
|       .metric-label { color: #666; font-size: 0.85em; } | ||||
|       .metric-value { color: #fff; font-size: 1.1em; font-weight: 600; } | ||||
|       .execution-logs { background: #0a0a0a; border: 1px solid #333; border-radius: 6px; padding: 16px; margin-top: 16px; max-height: 400px; overflow-y: auto; } | ||||
|       .log-entry { font-family: monospace; font-size: 0.9em; margin-bottom: 8px; padding: 4px 8px; border-radius: 4px; } | ||||
|       .log-info { color: #2196f3; } | ||||
|       .log-warning { color: #ff9800; background: rgba(255, 152, 0, 0.1); } | ||||
|       .log-error { color: #f44336; background: rgba(244, 67, 54, 0.1); } | ||||
|       .log-success { color: #4caf50; background: rgba(76, 175, 80, 0.1); } | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     const execution = this.execution; | ||||
|     if (!execution) return html``; | ||||
|     return html` | ||||
|       <div class="execution-details"> | ||||
|         <h3>Execution Details: ${execution.data.taskName}</h3> | ||||
|         <div class="metrics"> | ||||
|           <div class="metric"> | ||||
|             <span class="metric-label">Started</span> | ||||
|             <span class="metric-value">${formatDate(execution.data.startedAt)}</span> | ||||
|           </div> | ||||
|           ${execution.data.completedAt ? html` | ||||
|             <div class="metric"> | ||||
|               <span class="metric-label">Completed</span> | ||||
|               <span class="metric-value">${formatDate(execution.data.completedAt)}</span> | ||||
|             </div> | ||||
|           ` : ''} | ||||
|           ${execution.data.duration ? html` | ||||
|             <div class="metric"> | ||||
|               <span class="metric-label">Duration</span> | ||||
|               <span class="metric-value">${formatDuration(execution.data.duration)}</span> | ||||
|             </div> | ||||
|           ` : ''} | ||||
|           <div class="metric"> | ||||
|             <span class="metric-label">Triggered By</span> | ||||
|             <span class="metric-value">${execution.data.triggeredBy}</span> | ||||
|           </div> | ||||
|         </div> | ||||
|         ${execution.data.logs && execution.data.logs.length > 0 ? html` | ||||
|           <h4>Logs</h4> | ||||
|           <div class="execution-logs"> | ||||
|             ${execution.data.logs.map((log: any) => html` | ||||
|               <div class="log-entry log-${log.severity}"> | ||||
|                 <span>${formatDate(log.timestamp)}</span> - ${log.message} | ||||
|               </div> | ||||
|             `)} | ||||
|           </div> | ||||
|         ` : ''} | ||||
|         ${execution.data.metrics ? html` | ||||
|           <h4>Metrics</h4> | ||||
|           <div class="metrics"> | ||||
|             ${Object.entries(execution.data.metrics).map(([key, value]) => html` | ||||
|               <div class="metric"> | ||||
|                 <span class="metric-label">${key}</span> | ||||
|                 <span class="metric-value">${typeof value === 'object' ? JSON.stringify(value) : value}</span> | ||||
|               </div> | ||||
|             `)} | ||||
|           </div> | ||||
|         ` : ''} | ||||
|         ${execution.data.error ? html` | ||||
|           <h4>Error</h4> | ||||
|           <div class="execution-logs"> | ||||
|             <div class="log-entry log-error"> | ||||
|               ${execution.data.error.message} | ||||
|               ${execution.data.error.stack ? html`<pre>${execution.data.error.stack}</pre>` : ''} | ||||
|             </div> | ||||
|           </div> | ||||
|         ` : ''} | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     'cloudly-execution-details': CloudlyExecutionDetails; | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										206
									
								
								ts_web/elements/views/tasks/parts/cloudly-task-panel.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								ts_web/elements/views/tasks/parts/cloudly-task-panel.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,206 @@ | ||||
| import { DeesElement, customElement, html, css, cssManager, property } from '@design.estate/dees-element'; | ||||
| import { formatCronFriendly, formatDuration, formatRelativeTime, getCategoryHue, getCategoryIcon } from '../utils.js'; | ||||
|  | ||||
| @customElement('cloudly-task-panel') | ||||
| export class CloudlyTaskPanel extends DeesElement { | ||||
|   @property({ type: Object }) task: any; | ||||
|   @property({ type: Array }) executions: any[] = []; | ||||
|   @property({ type: Object }) canceling: Record<string, boolean> = {}; | ||||
|  | ||||
|   // Callbacks provided by parent view | ||||
|   @property({ attribute: false }) onRun?: (taskName: string) => void; | ||||
|   @property({ attribute: false }) onCancel?: (taskName: string) => void; | ||||
|   @property({ attribute: false }) onOpenDetails?: (execution: any) => void; | ||||
|   @property({ attribute: false }) onOpenLogs?: (execution: any) => void; | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     css` | ||||
|       .task-panel { | ||||
|         background: #131313; | ||||
|         border: 1px solid #2a2a2a; | ||||
|         border-radius: 10px; | ||||
|         padding: 16px; | ||||
|         transition: border-color 0.2s, background 0.2s; | ||||
|       } | ||||
|       .task-panel:hover { border-color: #3a3a3a; } | ||||
|  | ||||
|       .panel-header { | ||||
|         display: flex; | ||||
|         justify-content: space-between; | ||||
|         align-items: center; | ||||
|         gap: 12px; | ||||
|         margin-bottom: 12px; | ||||
|       } | ||||
|       .header-left { display: flex; align-items: center; gap: 12px; min-width: 0; } | ||||
|       .header-right { display: flex; align-items: center; gap: 8px; } | ||||
|       .task-icon { color: #cfcfcf; font-size: 28px; } | ||||
|       .task-name { font-size: 1.05em; font-weight: 650; color: #fff; letter-spacing: 0.1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | ||||
|       .task-subtitle { color: #8c8c8c; font-size: 0.9em; } | ||||
|  | ||||
|       .task-description { | ||||
|         color: #b5b5b5; | ||||
|         font-size: 0.95em; | ||||
|         margin-bottom: 12px; | ||||
|         display: -webkit-box; | ||||
|         -webkit-line-clamp: 2; | ||||
|         -webkit-box-orient: vertical; | ||||
|         overflow: hidden; | ||||
|       } | ||||
|  | ||||
|       .metrics-grid { | ||||
|         display: grid; | ||||
|         grid-template-columns: repeat(3, minmax(0, 1fr)); | ||||
|         gap: 10px; | ||||
|         margin-top: 8px; | ||||
|         width: 100%; | ||||
|         max-width: 760px; | ||||
|       } | ||||
|       .metric-item { | ||||
|         background: #0f0f0f; | ||||
|         border: 1px solid #2c2c2c; | ||||
|         border-radius: 8px; | ||||
|         padding: 10px 12px; | ||||
|       } | ||||
|       .metric-item .label { color: #8d8d8d; font-size: 0.8em; } | ||||
|       .metric-item .value { color: #eaeaea; font-weight: 600; margin-top: 4px; } | ||||
|  | ||||
|       .lastline { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         gap: 8px; | ||||
|         color: #a0a0a0; | ||||
|         font-size: 0.9em; | ||||
|         margin-top: 10px; | ||||
|       } | ||||
|       .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } | ||||
|       .dot.info { background: #2196f3; } | ||||
|       .dot.success { background: #4caf50; } | ||||
|       .dot.warning { background: #ff9800; } | ||||
|       .dot.error { background: #f44336; } | ||||
|  | ||||
|       .panel-footer { display: flex; gap: 12px; margin-top: 12px; } | ||||
|  | ||||
|       .link-button { background: transparent; border: none; color: #8ab4ff; cursor: pointer; padding: 0; font-size: 0.95em; } | ||||
|       .link-button:hover { text-decoration: underline; } | ||||
|  | ||||
|       .status-badge { | ||||
|         padding: 2px 8px; | ||||
|         border-radius: 4px; | ||||
|         font-size: 0.85em; | ||||
|         font-weight: 500; | ||||
|       } | ||||
|       .status-running { background: #2196f3; color: white; } | ||||
|       .status-completed { background: #4caf50; color: white; } | ||||
|       .status-failed { background: #f44336; color: white; } | ||||
|       .status-cancelled { background: #ff9800; color: white; } | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   private computeData() { | ||||
|     const task = this.task || {}; | ||||
|     const executions = this.executions || []; | ||||
|     const lastExecution = executions | ||||
|       .filter((e: any) => e.data.taskName === task.name) | ||||
|       .sort((a: any, b: any) => (b.data.startedAt || 0) - (a.data.startedAt || 0))[0]; | ||||
|     const isRunning = lastExecution?.data.status === 'running'; | ||||
|     const executionsForTask = executions.filter((e: any) => e.data.taskName === task.name); | ||||
|     const now = Date.now(); | ||||
|     const last24hCount = executionsForTask.filter((e: any) => (e.data.startedAt || 0) > now - 86_400_000).length; | ||||
|     const completed = executionsForTask.filter((e: any) => e.data.status === 'completed'); | ||||
|     const successRate = executionsForTask.length ? Math.round((completed.length * 100) / executionsForTask.length) : 0; | ||||
|     const avgDuration = completed.length ? Math.round(completed.reduce((acc: number, e: any) => acc + (e.data.duration || 0), 0) / completed.length) : undefined; | ||||
|     const lastLog = lastExecution?.data.logs && lastExecution.data.logs.length > 0 ? lastExecution.data.logs[lastExecution.data.logs.length - 1] : null; | ||||
|     const subtitle = [ | ||||
|       task.category, | ||||
|       task.schedule ? `⏱ ${formatCronFriendly(task.schedule)}` : null, | ||||
|       isRunning | ||||
|         ? (lastExecution?.data.startedAt ? `Started ${formatRelativeTime(lastExecution.data.startedAt)}` : 'Running') | ||||
|         : (task.lastRun ? `Last ${formatRelativeTime(task.lastRun)}` : 'Never run') | ||||
|     ].filter(Boolean).join(' • '); | ||||
|     return { lastExecution, isRunning, last24hCount, successRate, avgDuration, lastLog, subtitle }; | ||||
|   } | ||||
|  | ||||
|   public render() { | ||||
|     const task = this.task; | ||||
|     const { lastExecution, isRunning, last24hCount, successRate, avgDuration, lastLog, subtitle } = this.computeData(); | ||||
|  | ||||
|     return html` | ||||
|       <div class="task-panel"> | ||||
|         <div class="panel-header"> | ||||
|           <div class="header-left"> | ||||
|             <dees-icon class="task-icon" .icon=${getCategoryIcon(task.category)}></dees-icon> | ||||
|             <div> | ||||
|               <div class="task-name" title=${task.name}>${task.name}</div> | ||||
|               <div class="task-subtitle" title=${task.schedule || ''}>${subtitle}</div> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="header-right"> | ||||
|             ${lastExecution ? html`<span class="status-badge status-${lastExecution.data.status}">${lastExecution.data.status}</span>` : html`<span class="status-badge" style="background:#2e2e2e;color:#ddd;border:1px solid #3a3a3a;">idle</span>`} | ||||
|             ${isRunning ? html` | ||||
|               <dees-spinner style="--size: 18px"></dees-spinner> | ||||
|               <dees-button | ||||
|                 .text=${this.canceling[lastExecution!.id] ? 'Cancelling…' : 'Cancel'} | ||||
|                 .type=${'secondary'} | ||||
|                 .disabled=${!!this.canceling[lastExecution!.id]} | ||||
|                 @click=${() => this.onCancel?.(task.name)} | ||||
|               ></dees-button> | ||||
|             ` : html` | ||||
|               <dees-button .text=${'Run'} .type=${'primary'} .disabled=${!task.enabled} @click=${() => this.onRun?.(task.name)}></dees-button> | ||||
|             `} | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="task-description" title=${task.description || ''}>${task.description}</div> | ||||
|  | ||||
|         ${lastExecution ? html` | ||||
|           <div class="metrics-grid"> | ||||
|             <div class="metric-item"> | ||||
|               <div class="label">Last Status</div> | ||||
|               <div class="value"> | ||||
|                 <span class="status-badge status-${lastExecution.data.status}">${lastExecution.data.status}</span> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="metric-item"> | ||||
|               <div class="label">Avg Duration</div> | ||||
|               <div class="value">${avgDuration ? formatDuration(avgDuration) : '-'}</div> | ||||
|             </div> | ||||
|             <div class="metric-item"> | ||||
|               <div class="label">24h Runs · Success</div> | ||||
|               <div class="value">${last24hCount} · ${successRate}%</div> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="lastline"> | ||||
|             ${lastLog ? html`<span class="dot ${lastLog.severity}"></span> ${lastLog.message}` : 'No recent logs'} | ||||
|           </div> | ||||
|           <div class="panel-footer"> | ||||
|             <button class="link-button" @click=${() => this.onOpenDetails?.(lastExecution)}>Details</button> | ||||
|             ${lastExecution.data.logs?.length ? html`<button class="link-button" @click=${() => this.onOpenLogs?.(lastExecution)}>Logs</button>` : ''} | ||||
|           </div> | ||||
|         ` : html` | ||||
|           <div class="metrics-grid"> | ||||
|             <div class="metric-item"> | ||||
|               <div class="label">Last Status</div> | ||||
|               <div class="value">—</div> | ||||
|             </div> | ||||
|             <div class="metric-item"> | ||||
|               <div class="label">Avg Duration</div> | ||||
|               <div class="value">—</div> | ||||
|             </div> | ||||
|             <div class="metric-item"> | ||||
|               <div class="label">24h Runs · Success</div> | ||||
|               <div class="value">0 · 0%</div> | ||||
|             </div> | ||||
|           </div> | ||||
|         `} | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     'cloudly-task-panel': CloudlyTaskPanel; | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										68
									
								
								ts_web/elements/views/tasks/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								ts_web/elements/views/tasks/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| export function formatDate(timestamp: number): string { | ||||
|   return new Date(timestamp).toLocaleString(); | ||||
| } | ||||
|  | ||||
| export function formatDuration(ms: number): string { | ||||
|   if (ms < 1000) return `${ms}ms`; | ||||
|   if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; | ||||
|   if (ms < 3600000) return `${(ms / 60000).toFixed(1)}m`; | ||||
|   return `${(ms / 3600000).toFixed(1)}h`; | ||||
| } | ||||
|  | ||||
| export function formatRelativeTime(ts?: number): string { | ||||
|   if (!ts) return '-'; | ||||
|   const diff = Date.now() - ts; | ||||
|   const abs = Math.abs(diff); | ||||
|   if (abs < 60_000) return `${Math.round(abs / 1000)}s ago`; | ||||
|   if (abs < 3_600_000) return `${Math.round(abs / 60_000)}m ago`; | ||||
|   if (abs < 86_400_000) return `${Math.round(abs / 3_600_000)}h ago`; | ||||
|   return `${Math.round(abs / 86_400_000)}d ago`; | ||||
| } | ||||
|  | ||||
| export function getCategoryIcon(category: string): string { | ||||
|   switch (category) { | ||||
|     case 'maintenance': | ||||
|       return 'lucide:Wrench'; | ||||
|     case 'deployment': | ||||
|       return 'lucide:Rocket'; | ||||
|     case 'backup': | ||||
|       return 'lucide:Archive'; | ||||
|     case 'monitoring': | ||||
|       return 'lucide:Activity'; | ||||
|     case 'cleanup': | ||||
|       return 'lucide:Trash2'; | ||||
|     case 'system': | ||||
|       return 'lucide:Settings'; | ||||
|     case 'security': | ||||
|       return 'lucide:Shield'; | ||||
|     default: | ||||
|       return 'lucide:Play'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function getCategoryHue(category: string): number { | ||||
|   switch (category) { | ||||
|     case 'maintenance': return 28;    // orange | ||||
|     case 'deployment': return 208;    // blue | ||||
|     case 'backup': return 122;        // green | ||||
|     case 'monitoring': return 280;    // purple | ||||
|     case 'cleanup': return 20;        // brownish | ||||
|     case 'system': return 200;        // steel | ||||
|     case 'security': return 0;        // red | ||||
|     default: return 210;              // default blue | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function formatCronFriendly(cron?: string): string { | ||||
|   if (!cron) return ''; | ||||
|   const parts = cron.trim().split(/\s+/); | ||||
|   if (parts.length !== 5) return cron; // fallback | ||||
|   const [min, hour, dom, mon, dow] = parts; | ||||
|   if (min === '*/1' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'every minute'; | ||||
|   if (min.startsWith('*/') && hour === '*' && dom === '*' && mon === '*' && dow === '*') return `every ${min.replace('*/','')} min`; | ||||
|   if (min === '0' && hour.startsWith('*/') && dom === '*' && mon === '*' && dow === '*') return `every ${hour.replace('*/','')} hours`; | ||||
|   if (min === '0' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'hourly'; | ||||
|   if (min === '0' && hour === '0' && dom === '*' && mon === '*' && dow === '*') return 'daily'; | ||||
|   if (min === '0' && hour === '0' && dom === '1' && mon === '*' && dow === '*') return 'monthly'; | ||||
|   return cron; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user