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, |   html, | ||||||
|   state |   state | ||||||
| } from '@design.estate/dees-element'; | } from '@design.estate/dees-element'; | ||||||
| import { CloudlyViewBackups } from './cloudly-view-backups.js'; | import { CloudlyViewBackups } from './views/backups/index.js'; | ||||||
| import { CloudlyViewClusters } from './cloudly-view-clusters.js'; | import { CloudlyViewClusters } from './views/clusters/index.js'; | ||||||
| import { CloudlyViewDbs } from './cloudly-view-dbs.js'; | import { CloudlyViewDbs } from './views/dbs/index.js'; | ||||||
| import { CloudlyViewDeployments } from './cloudly-view-deployments.js'; | import { CloudlyViewDeployments } from './views/deployments/index.js'; | ||||||
| import { CloudlyViewDns } from './cloudly-view-dns.js'; | import { CloudlyViewDns } from './views/dns/index.js'; | ||||||
| import { CloudlyViewDomains } from './cloudly-view-domains.js'; | import { CloudlyViewDomains } from './views/domains/index.js'; | ||||||
| import { CloudlyViewImages } from './cloudly-view-images.js'; | import { CloudlyViewImages } from './views/images/index.js'; | ||||||
| import { CloudlyViewLogs } from './cloudly-view-logs.js'; | import { CloudlyViewLogs } from './views/logs/index.js'; | ||||||
| import { CloudlyViewMails } from './cloudly-view-mails.js'; | import { CloudlyViewMails } from './views/mails/index.js'; | ||||||
| import { CloudlyViewOverview } from './cloudly-view-overview.js'; | import { CloudlyViewOverview } from './views/overview/index.js'; | ||||||
| import { CloudlyViewS3 } from './cloudly-view-s3.js'; | import { CloudlyViewS3 } from './views/s3/index.js'; | ||||||
| import { CloudlyViewSecretBundles } from './cloudly-view-secretbundles.js'; | import { CloudlyViewSecretBundles } from './views/secretbundles/index.js'; | ||||||
| import { CloudlyViewSecretGroups } from './cloudly-view-secretgroups.js'; | import { CloudlyViewSecretGroups } from './views/secretgroups/index.js'; | ||||||
| import { CloudlyViewServices } from './cloudly-view-services.js'; | import { CloudlyViewServices } from './views/services/index.js'; | ||||||
| import { CloudlyViewExternalRegistries } from './cloudly-view-externalregistries.js'; | import { CloudlyViewExternalRegistries } from './views/externalregistries/index.js'; | ||||||
| import { CloudlyViewSettings } from './cloudly-view-settings.js'; | import { CloudlyViewSettings } from './views/settings/index.js'; | ||||||
| import { CloudlyViewTasks } from './cloudly-view-tasks.js'; | import { CloudlyViewTasks } from './views/tasks/index.js'; | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   interface HTMLElementTagNameMap { |   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 './shared/index.js'; | ||||||
| export * from './cloudly-dashboard.js'; | export * from './cloudly-dashboard.js'; | ||||||
| export * from './cloudly-view-secretgroups.js'; | export * from './views/secretgroups/index.js'; | ||||||
| export * from './cloudly-view-secretbundles.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 plugins from '../../../plugins.js'; | ||||||
| import * as shared from '../elements/shared/index.js'; | import * as shared from '../../shared/index.js'; | ||||||
| 
 | 
 | ||||||
| import { | import { | ||||||
|   DeesElement, |   DeesElement, | ||||||
| @@ -10,12 +10,12 @@ import { | |||||||
|   cssManager, |   cssManager, | ||||||
| } from '@design.estate/dees-element'; | } from '@design.estate/dees-element'; | ||||||
| 
 | 
 | ||||||
| import * as appstate from '../appstate.js'; | import * as appstate from '../../../appstate.js'; | ||||||
| 
 | 
 | ||||||
| @customElement('cloudly-view-services') | @customElement('cloudly-view-services') | ||||||
| export class CloudlyViewServices extends DeesElement { | export class CloudlyViewServices extends DeesElement { | ||||||
|   @state() |   @state() | ||||||
|   private data: appstate.IDataState = {}; |   private data: appstate.IDataState = {} as any; | ||||||
| 
 | 
 | ||||||
|   constructor() { |   constructor() { | ||||||
|     super(); |     super(); | ||||||
| @@ -31,45 +31,20 @@ export class CloudlyViewServices extends DeesElement { | |||||||
|     cssManager.defaultStyles, |     cssManager.defaultStyles, | ||||||
|     shared.viewHostCss, |     shared.viewHostCss, | ||||||
|     css` |     css` | ||||||
|       .category-badge { |       .category-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.9em; font-weight: 500; } | ||||||
|         padding: 2px 8px; |       .category-base { background: #2196f3; color: white; } | ||||||
|         border-radius: 4px; |       .category-distributed { background: #9c27b0; color: white; } | ||||||
|         font-size: 0.9em; |       .category-workload { background: #4caf50; color: white; } | ||||||
|         font-weight: 500; |       .strategy-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.85em; background: #444; color: #ccc; margin-left: 4px; } | ||||||
|       } |  | ||||||
|       .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 { |   private getCategoryIcon(category: string): string { | ||||||
|     switch (category) { |     switch (category) { | ||||||
|       case 'base': |       case 'base': return 'lucide:ServerCog'; | ||||||
|         return 'lucide:ServerCog'; |       case 'distributed': return 'lucide:Network'; | ||||||
|       case 'distributed': |       case 'workload': return 'lucide:Container'; | ||||||
|         return 'lucide:Network'; |       default: return 'lucide:Box'; | ||||||
|       case 'workload': |  | ||||||
|         return 'lucide:Container'; |  | ||||||
|       default: |  | ||||||
|         return 'lucide:Box'; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @@ -110,70 +85,28 @@ export class CloudlyViewServices extends DeesElement { | |||||||
|             name: 'Add Service', |             name: 'Add Service', | ||||||
|             iconName: 'plus', |             iconName: 'plus', | ||||||
|             type: ['header', 'footer'], |             type: ['header', 'footer'], | ||||||
|             actionFunc: async (dataActionArg) => { |             actionFunc: async () => { | ||||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ |               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||||
|                 heading: 'Add Service', |                 heading: 'Add Service', | ||||||
|                 content: html` |                 content: html` | ||||||
|                   <dees-form> |                   <dees-form> | ||||||
|                     <dees-input-text .key=${'name'} .label=${'Service Name'} .required=${true}></dees-input-text> |                     <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-text .key=${'description'} .label=${'Description'} .required=${true}></dees-input-text> | ||||||
|                     <dees-input-dropdown  |                     <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> | ||||||
|                       .key=${'serviceCategory'}  |                     <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> | ||||||
|                       .label=${'Service Category'}  |                     <dees-input-text .key=${'maxReplicas'} .label=${'Max Replicas (for distributed services)'} .value=${'1'} .type=${'number'}></dees-input-text> | ||||||
|                       .options=${[{key: 'base', option: 'Base'}, {key: 'distributed', option: 'Distributed'}, {key: 'workload', option: 'Workload'}]} |                     <dees-input-checkbox .key=${'antiAffinity'} .label=${'Enable Anti-Affinity'} .value=${false}></dees-input-checkbox> | ||||||
|                       .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=${'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=${'imageVersion'} .label=${'Image Version'} .value=${'latest'} .required=${true}></dees-input-text> | ||||||
|                     <dees-input-text  |                     <dees-input-text .key=${'scaleFactor'} .label=${'Scale Factor'} .value=${'1'} .type=${'number'} .required=${true}></dees-input-text> | ||||||
|                       .key=${'scaleFactor'}  |                     <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> | ||||||
|                       .label=${'Scale Factor'}  |                     <dees-input-text .key=${'webPort'} .label=${'Web Port'} .value=${'80'} .type=${'number'} .required=${true}></dees-input-text> | ||||||
|                       .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> |                   </dees-form> | ||||||
|                 `,
 |                 `,
 | ||||||
|                 menuOptions: [ |                 menuOptions: [ | ||||||
|                   { |                   { name: 'Create Service', action: async (modalArg: any) => { | ||||||
|                     name: 'Create Service', |  | ||||||
|                     action: async (modalArg) => { |  | ||||||
|                       const form = modalArg.shadowRoot.querySelector('dees-form') as any; |                       const form = modalArg.shadowRoot.querySelector('dees-form') as any; | ||||||
|                       const formData = await form.gatherData(); |                       const formData = await form.gatherData(); | ||||||
|                        |  | ||||||
|                       await appstate.dataState.dispatchAction(appstate.createServiceAction, { |                       await appstate.dataState.dispatchAction(appstate.createServiceAction, { | ||||||
|                         serviceData: { |                         serviceData: { | ||||||
|                           name: formData.name, |                           name: formData.name, | ||||||
| @@ -186,24 +119,15 @@ export class CloudlyViewServices extends DeesElement { | |||||||
|                           imageVersion: formData.imageVersion, |                           imageVersion: formData.imageVersion, | ||||||
|                           scaleFactor: parseInt(formData.scaleFactor), |                           scaleFactor: parseInt(formData.scaleFactor), | ||||||
|                           balancingStrategy: formData.balancingStrategy, |                           balancingStrategy: formData.balancingStrategy, | ||||||
|                           ports: { |                           ports: { web: parseInt(formData.webPort) }, | ||||||
|                             web: parseInt(formData.webPort), |  | ||||||
|                           }, |  | ||||||
|                           environment: {}, |                           environment: {}, | ||||||
|                           domains: [], |                           domains: [], | ||||||
|                           deploymentIds: [], |                           deploymentIds: [], | ||||||
|                         }, |                         }, | ||||||
|                       }); |                       }); | ||||||
|                        |  | ||||||
|                       await modalArg.destroy(); |                       await modalArg.destroy(); | ||||||
|                     }, |                     }}, | ||||||
|                   }, |                   { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() }, | ||||||
|                   { |  | ||||||
|                     name: 'Cancel', |  | ||||||
|                     action: async (modalArg) => { |  | ||||||
|                       modalArg.destroy(); |  | ||||||
|                     }, |  | ||||||
|                   }, |  | ||||||
|                 ], |                 ], | ||||||
|               }); |               }); | ||||||
|             }, |             }, | ||||||
| @@ -212,7 +136,7 @@ export class CloudlyViewServices extends DeesElement { | |||||||
|             name: 'Edit', |             name: 'Edit', | ||||||
|             iconName: 'edit', |             iconName: 'edit', | ||||||
|             type: ['contextmenu', 'inRow'], |             type: ['contextmenu', 'inRow'], | ||||||
|             actionFunc: async (actionDataArg) => { |             actionFunc: async (actionDataArg: any) => { | ||||||
|               const service = actionDataArg.item as plugins.interfaces.data.IService; |               const service = actionDataArg.item as plugins.interfaces.data.IService; | ||||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ |               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||||
|                 heading: `Edit Service: ${service.data.name}`, |                 heading: `Edit Service: ${service.data.name}`, | ||||||
| @@ -220,55 +144,19 @@ export class CloudlyViewServices extends DeesElement { | |||||||
|                   <dees-form> |                   <dees-form> | ||||||
|                     <dees-input-text .key=${'name'} .label=${'Service Name'} .value=${service.data.name} .required=${true}></dees-input-text> |                     <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-text .key=${'description'} .label=${'Description'} .value=${service.data.description} .required=${true}></dees-input-text> | ||||||
|                     <dees-input-dropdown  |                     <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> | ||||||
|                       .key=${'serviceCategory'}  |                     <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> | ||||||
|                       .label=${'Service Category'}  |                     <dees-input-text .key=${'maxReplicas'} .label=${'Max Replicas (for distributed services)'} .value=${service.data.maxReplicas || ''} .type=${'number'}></dees-input-text> | ||||||
|                       .options=${[{key: 'base', option: 'Base'}, {key: 'distributed', option: 'Distributed'}, {key: 'workload', option: 'Workload'}]} |                     <dees-input-checkbox .key=${'antiAffinity'} .label=${'Enable Anti-Affinity'} .value=${service.data.antiAffinity || false}></dees-input-checkbox> | ||||||
|                       .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=${'imageVersion'} .label=${'Image Version'} .value=${service.data.imageVersion} .required=${true}></dees-input-text> | ||||||
|                     <dees-input-text  |                     <dees-input-text .key=${'scaleFactor'} .label=${'Scale Factor'} .value=${service.data.scaleFactor} .type=${'number'} .required=${true}></dees-input-text> | ||||||
|                       .key=${'scaleFactor'}  |                     <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> | ||||||
|                       .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> |                   </dees-form> | ||||||
|                 `,
 |                 `,
 | ||||||
|                 menuOptions: [ |                 menuOptions: [ | ||||||
|                   { |                   { name: 'Update Service', action: async (modalArg: any) => { | ||||||
|                     name: 'Update Service', |  | ||||||
|                     action: async (modalArg) => { |  | ||||||
|                       const form = modalArg.shadowRoot.querySelector('dees-form') as any; |                       const form = modalArg.shadowRoot.querySelector('dees-form') as any; | ||||||
|                       const formData = await form.gatherData(); |                       const formData = await form.gatherData(); | ||||||
|                        |  | ||||||
|                       await appstate.dataState.dispatchAction(appstate.updateServiceAction, { |                       await appstate.dataState.dispatchAction(appstate.updateServiceAction, { | ||||||
|                         serviceId: service.id, |                         serviceId: service.id, | ||||||
|                         serviceData: { |                         serviceData: { | ||||||
| @@ -284,16 +172,9 @@ export class CloudlyViewServices extends DeesElement { | |||||||
|                           balancingStrategy: formData.balancingStrategy, |                           balancingStrategy: formData.balancingStrategy, | ||||||
|                         }, |                         }, | ||||||
|                       }); |                       }); | ||||||
|                        |  | ||||||
|                       await modalArg.destroy(); |                       await modalArg.destroy(); | ||||||
|                     }, |                     }}, | ||||||
|                   }, |                   { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() }, | ||||||
|                   { |  | ||||||
|                     name: 'Cancel', |  | ||||||
|                     action: async (modalArg) => { |  | ||||||
|                       modalArg.destroy(); |  | ||||||
|                     }, |  | ||||||
|                   }, |  | ||||||
|                 ], |                 ], | ||||||
|               }); |               }); | ||||||
|             }, |             }, | ||||||
| @@ -302,9 +183,8 @@ export class CloudlyViewServices extends DeesElement { | |||||||
|             name: 'Deploy', |             name: 'Deploy', | ||||||
|             iconName: 'rocket', |             iconName: 'rocket', | ||||||
|             type: ['contextmenu', 'inRow'], |             type: ['contextmenu', 'inRow'], | ||||||
|             actionFunc: async (actionDataArg) => { |             actionFunc: async (actionDataArg: any) => { | ||||||
|               const service = actionDataArg.item as plugins.interfaces.data.IService; |               const service = actionDataArg.item as plugins.interfaces.data.IService; | ||||||
|               // TODO: Implement deployment action
 |  | ||||||
|               console.log('Deploy service:', service); |               console.log('Deploy service:', service); | ||||||
|             }, |             }, | ||||||
|           }, |           }, | ||||||
| @@ -312,38 +192,21 @@ export class CloudlyViewServices extends DeesElement { | |||||||
|             name: 'Delete', |             name: 'Delete', | ||||||
|             iconName: 'trash', |             iconName: 'trash', | ||||||
|             type: ['contextmenu', 'inRow'], |             type: ['contextmenu', 'inRow'], | ||||||
|             actionFunc: async (actionDataArg) => { |             actionFunc: async (actionDataArg: any) => { | ||||||
|               const service = actionDataArg.item as plugins.interfaces.data.IService; |               const service = actionDataArg.item as plugins.interfaces.data.IService; | ||||||
|               plugins.deesCatalog.DeesModal.createAndShow({ |               plugins.deesCatalog.DeesModal.createAndShow({ | ||||||
|                 heading: `Delete Service: ${service.data.name}`, |                 heading: `Delete Service: ${service.data.name}`, | ||||||
|                 content: html` |                 content: html` | ||||||
|                   <div style="text-align:center"> |                   <div style="text-align:center">Are you sure you want to delete this service?</div> | ||||||
|                     Are you sure you want to delete this service? |  | ||||||
|                   </div> |  | ||||||
|                   <div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;"> |                   <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: #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: #aaa; font-size: 0.9em; margin-top: 4px;">${service.data.description}</div> | ||||||
|                     <div style="color: #f44336; margin-top: 8px;"> |                     <div style="color: #f44336; margin-top: 8px;">This will also delete ${service.data.deploymentIds?.length || 0} deployment(s)</div> | ||||||
|                       This will also delete ${service.data.deploymentIds?.length || 0} deployment(s) |  | ||||||
|                     </div> |  | ||||||
|                   </div> |                   </div> | ||||||
|                 `,
 |                 `,
 | ||||||
|                 menuOptions: [ |                 menuOptions: [ | ||||||
|                   { |                   { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() }, | ||||||
|                     name: 'Cancel', |                   { name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteServiceAction, { serviceId: service.id }); await modalArg.destroy(); } }, | ||||||
|                     action: async (modalArg) => { |  | ||||||
|                       await modalArg.destroy(); |  | ||||||
|                     }, |  | ||||||
|                   }, |  | ||||||
|                   { |  | ||||||
|                     name: 'Delete', |  | ||||||
|                     action: async (modalArg) => { |  | ||||||
|                       await appstate.dataState.dispatchAction(appstate.deleteServiceAction, { |  | ||||||
|                         serviceId: service.id, |  | ||||||
|                       }); |  | ||||||
|                       await modalArg.destroy(); |  | ||||||
|                     }, |  | ||||||
|                   }, |  | ||||||
|                 ], |                 ], | ||||||
|               }); |               }); | ||||||
|             }, |             }, | ||||||
| @@ -352,4 +215,11 @@ export class CloudlyViewServices extends DeesElement { | |||||||
|       ></dees-table> |       ></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