feat(web): Add deployments API typings and web UI improvements: services & deployments management with CRUD and actions
This commit is contained in:
		| @@ -1,5 +1,14 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 2025-09-08 - 5.3.0 - feat(web) | ||||||
|  | Add deployments API typings and web UI improvements: services & deployments management with CRUD and actions | ||||||
|  |  | ||||||
|  | - Add deployment request interfaces (ts_interfaces/requests/deployment.ts) to define typed API for create/read/update/delete/scale/restart operations. | ||||||
|  | - Extend web app state (ts_web/appstate.ts) to include typed services and deployments, and add actions for create/update/delete of services and deployments. | ||||||
|  | - Enhance web views (ts_web/elements/*): CloudlyViewServices and CloudlyViewDeployments now include richer display, styling, and UI actions (create, edit, deploy, restart, stop, delete). | ||||||
|  | - Fix subscription variable naming in several web components (subecription -> subscription) and improve table display functions to handle missing data safely. | ||||||
|  | - Add .claude/settings.local.json (tooling/permissions) used for local development/test tooling. | ||||||
|  |  | ||||||
| ## 2025-09-07 - 5.2.0 - feat(settings) | ## 2025-09-07 - 5.2.0 - feat(settings) | ||||||
| Add runtime settings management, node & baremetal managers, and settings UI | Add runtime settings management, node & baremetal managers, and settings UI | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,6 @@ | |||||||
|  */ |  */ | ||||||
| export const commitinfo = { | export const commitinfo = { | ||||||
|   name: '@serve.zone/cloudly', |   name: '@serve.zone/cloudly', | ||||||
|   version: '5.2.0', |   version: '5.3.0', | ||||||
|   description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.' |   description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.' | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										141
									
								
								ts_interfaces/requests/deployment.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								ts_interfaces/requests/deployment.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | |||||||
|  | import * as plugins from '../plugins.js'; | ||||||
|  | import type { IDeployment } from '../data/deployment.js'; | ||||||
|  | import type { IIdentity } from '../data/user.js'; | ||||||
|  |  | ||||||
|  | export interface IReq_Any_Cloudly_GetDeploymentById  | ||||||
|  | extends plugins.typedrequestInterfaces.implementsTR< | ||||||
|  |   plugins.typedrequestInterfaces.ITypedRequest, | ||||||
|  |   IReq_Any_Cloudly_GetDeploymentById | ||||||
|  | > { | ||||||
|  |   method: 'getDeploymentById'; | ||||||
|  |   request: { | ||||||
|  |     identity: IIdentity; | ||||||
|  |     deploymentId: string; | ||||||
|  |   }; | ||||||
|  |   response: { | ||||||
|  |     deployment: IDeployment; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface IReq_Any_Cloudly_GetDeployments | ||||||
|  | extends plugins.typedrequestInterfaces.implementsTR< | ||||||
|  |   plugins.typedrequestInterfaces.ITypedRequest, | ||||||
|  |   IReq_Any_Cloudly_GetDeployments | ||||||
|  | > { | ||||||
|  |   method: 'getDeployments'; | ||||||
|  |   request: { | ||||||
|  |     identity: IIdentity; | ||||||
|  |   }; | ||||||
|  |   response: { | ||||||
|  |     deployments: IDeployment[]; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface IReq_Any_Cloudly_GetDeploymentsByService | ||||||
|  | extends plugins.typedrequestInterfaces.implementsTR< | ||||||
|  |   plugins.typedrequestInterfaces.ITypedRequest, | ||||||
|  |   IReq_Any_Cloudly_GetDeploymentsByService | ||||||
|  | > { | ||||||
|  |   method: 'getDeploymentsByService'; | ||||||
|  |   request: { | ||||||
|  |     identity: IIdentity; | ||||||
|  |     serviceId: string; | ||||||
|  |   }; | ||||||
|  |   response: { | ||||||
|  |     deployments: IDeployment[]; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface IReq_Any_Cloudly_GetDeploymentsByNode | ||||||
|  | extends plugins.typedrequestInterfaces.implementsTR< | ||||||
|  |   plugins.typedrequestInterfaces.ITypedRequest, | ||||||
|  |   IReq_Any_Cloudly_GetDeploymentsByNode | ||||||
|  | > { | ||||||
|  |   method: 'getDeploymentsByNode'; | ||||||
|  |   request: { | ||||||
|  |     identity: IIdentity; | ||||||
|  |     nodeId: string; | ||||||
|  |   }; | ||||||
|  |   response: { | ||||||
|  |     deployments: IDeployment[]; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface IReq_Any_Cloudly_CreateDeployment | ||||||
|  | extends plugins.typedrequestInterfaces.implementsTR< | ||||||
|  |   plugins.typedrequestInterfaces.ITypedRequest, | ||||||
|  |   IReq_Any_Cloudly_CreateDeployment | ||||||
|  | > { | ||||||
|  |   method: 'createDeployment'; | ||||||
|  |   request: { | ||||||
|  |     identity: IIdentity; | ||||||
|  |     deploymentData: Partial<IDeployment>; | ||||||
|  |   }; | ||||||
|  |   response: { | ||||||
|  |     deployment: IDeployment; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface IReq_Any_Cloudly_UpdateDeployment | ||||||
|  | extends plugins.typedrequestInterfaces.implementsTR< | ||||||
|  |   plugins.typedrequestInterfaces.ITypedRequest, | ||||||
|  |   IReq_Any_Cloudly_UpdateDeployment | ||||||
|  | > { | ||||||
|  |   method: 'updateDeployment'; | ||||||
|  |   request: { | ||||||
|  |     identity: IIdentity; | ||||||
|  |     deploymentId: string; | ||||||
|  |     deploymentData: Partial<IDeployment>; | ||||||
|  |   }; | ||||||
|  |   response: { | ||||||
|  |     deployment: IDeployment; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface IReq_Any_Cloudly_DeleteDeploymentById | ||||||
|  | extends plugins.typedrequestInterfaces.implementsTR< | ||||||
|  |   plugins.typedrequestInterfaces.ITypedRequest, | ||||||
|  |   IReq_Any_Cloudly_DeleteDeploymentById | ||||||
|  | > { | ||||||
|  |   method: 'deleteDeploymentById'; | ||||||
|  |   request: { | ||||||
|  |     identity: IIdentity; | ||||||
|  |     deploymentId: string; | ||||||
|  |   }; | ||||||
|  |   response: { | ||||||
|  |     success: boolean; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface IReq_Any_Cloudly_RestartDeployment | ||||||
|  | extends plugins.typedrequestInterfaces.implementsTR< | ||||||
|  |   plugins.typedrequestInterfaces.ITypedRequest, | ||||||
|  |   IReq_Any_Cloudly_RestartDeployment | ||||||
|  | > { | ||||||
|  |   method: 'restartDeployment'; | ||||||
|  |   request: { | ||||||
|  |     identity: IIdentity; | ||||||
|  |     deploymentId: string; | ||||||
|  |   }; | ||||||
|  |   response: { | ||||||
|  |     success: boolean; | ||||||
|  |     deployment: IDeployment; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface IReq_Any_Cloudly_ScaleDeployment | ||||||
|  | extends plugins.typedrequestInterfaces.implementsTR< | ||||||
|  |   plugins.typedrequestInterfaces.ITypedRequest, | ||||||
|  |   IReq_Any_Cloudly_ScaleDeployment | ||||||
|  | > { | ||||||
|  |   method: 'scaleDeployment'; | ||||||
|  |   request: { | ||||||
|  |     identity: IIdentity; | ||||||
|  |     deploymentId: string; | ||||||
|  |     replicas: number; | ||||||
|  |   }; | ||||||
|  |   response: { | ||||||
|  |     success: boolean; | ||||||
|  |     deployment: IDeployment; | ||||||
|  |   }; | ||||||
|  | } | ||||||
| @@ -3,6 +3,6 @@ | |||||||
|  */ |  */ | ||||||
| export const commitinfo = { | export const commitinfo = { | ||||||
|   name: '@serve.zone/cloudly', |   name: '@serve.zone/cloudly', | ||||||
|   version: '5.2.0', |   version: '5.3.0', | ||||||
|   description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.' |   description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.' | ||||||
| } | } | ||||||
|   | |||||||
| @@ -48,8 +48,8 @@ export interface IDataState { | |||||||
|   secretBundles?: plugins.interfaces.data.ISecretBundle[]; |   secretBundles?: plugins.interfaces.data.ISecretBundle[]; | ||||||
|   clusters?: plugins.interfaces.data.ICluster[]; |   clusters?: plugins.interfaces.data.ICluster[]; | ||||||
|   images?: any[]; |   images?: any[]; | ||||||
|   services?: any[]; |   services?: plugins.interfaces.data.IService[]; | ||||||
|   deployments?: any[]; |   deployments?: plugins.interfaces.data.IDeployment[]; | ||||||
|   dns?: any[]; |   dns?: any[]; | ||||||
|   mails?: any[]; |   mails?: any[]; | ||||||
|   logs?: any[]; |   logs?: any[]; | ||||||
| @@ -136,9 +136,90 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => { | |||||||
|     clusters: responseClusters.clusters, |     clusters: responseClusters.clusters, | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // Services | ||||||
|  |   const trGetServices = | ||||||
|  |     new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.service.IRequest_Any_Cloudly_GetServices>( | ||||||
|  |       '/typedrequest', | ||||||
|  |       'getServices' | ||||||
|  |     ); | ||||||
|  |   const responseServices = await trGetServices.fire({ | ||||||
|  |     identity: loginStatePart.getState().identity, | ||||||
|  |   }); | ||||||
|  |   currentState = { | ||||||
|  |     ...currentState, | ||||||
|  |     services: responseServices.services, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   // Deployments | ||||||
|  |   const trGetDeployments = | ||||||
|  |     new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.deployment.IReq_Any_Cloudly_GetDeployments>( | ||||||
|  |       '/typedrequest', | ||||||
|  |       'getDeployments' | ||||||
|  |     ); | ||||||
|  |   const responseDeployments = await trGetDeployments.fire({ | ||||||
|  |     identity: loginStatePart.getState().identity, | ||||||
|  |   }); | ||||||
|  |   currentState = { | ||||||
|  |     ...currentState, | ||||||
|  |     deployments: responseDeployments.deployments, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   return currentState; |   return currentState; | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | // Service Actions | ||||||
|  | export const createServiceAction = dataState.createAction( | ||||||
|  |   async (statePartArg, payloadArg: { serviceData: plugins.interfaces.data.IService['data'] }) => { | ||||||
|  |     let currentState = statePartArg.getState(); | ||||||
|  |     const trCreateService = | ||||||
|  |       new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.service.IRequest_Any_Cloudly_CreateService>( | ||||||
|  |         '/typedrequest', | ||||||
|  |         'createService' | ||||||
|  |       ); | ||||||
|  |     const response = await trCreateService.fire({ | ||||||
|  |       identity: loginStatePart.getState().identity, | ||||||
|  |       serviceData: payloadArg.serviceData, | ||||||
|  |     }); | ||||||
|  |     currentState = await dataState.dispatchAction(getAllDataAction, null); | ||||||
|  |     return currentState; | ||||||
|  |   } | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | export const updateServiceAction = dataState.createAction( | ||||||
|  |   async (statePartArg, payloadArg: { serviceId: string; serviceData: plugins.interfaces.data.IService['data'] }) => { | ||||||
|  |     let currentState = statePartArg.getState(); | ||||||
|  |     const trUpdateService = | ||||||
|  |       new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.service.IRequest_Any_Cloudly_UpdateService>( | ||||||
|  |         '/typedrequest', | ||||||
|  |         'updateService' | ||||||
|  |       ); | ||||||
|  |     const response = await trUpdateService.fire({ | ||||||
|  |       identity: loginStatePart.getState().identity, | ||||||
|  |       serviceId: payloadArg.serviceId, | ||||||
|  |       serviceData: payloadArg.serviceData, | ||||||
|  |     }); | ||||||
|  |     currentState = await dataState.dispatchAction(getAllDataAction, null); | ||||||
|  |     return currentState; | ||||||
|  |   } | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | export const deleteServiceAction = dataState.createAction( | ||||||
|  |   async (statePartArg, payloadArg: { serviceId: string }) => { | ||||||
|  |     let currentState = statePartArg.getState(); | ||||||
|  |     const trDeleteService = | ||||||
|  |       new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.service.IRequest_Any_Cloudly_DeleteServiceById>( | ||||||
|  |         '/typedrequest', | ||||||
|  |         'deleteServiceById' | ||||||
|  |       ); | ||||||
|  |     const response = await trDeleteService.fire({ | ||||||
|  |       identity: loginStatePart.getState().identity, | ||||||
|  |       serviceId: payloadArg.serviceId, | ||||||
|  |     }); | ||||||
|  |     currentState = await dataState.dispatchAction(getAllDataAction, null); | ||||||
|  |     return currentState; | ||||||
|  |   } | ||||||
|  | ); | ||||||
|  |  | ||||||
| // SecretGroup Actions | // SecretGroup Actions | ||||||
| export const createSecretGroupAction = dataState.createAction( | export const createSecretGroupAction = dataState.createAction( | ||||||
|   async (statePartArg, payloadArg: plugins.interfaces.data.ISecretGroup) => { |   async (statePartArg, payloadArg: plugins.interfaces.data.ISecretGroup) => { | ||||||
| @@ -239,6 +320,59 @@ export const deleteImageAction = dataState.createAction( | |||||||
|   } |   } | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | // Deployment Actions | ||||||
|  | export const createDeploymentAction = dataState.createAction( | ||||||
|  |   async (statePartArg, payloadArg: { deploymentData: Partial<plugins.interfaces.data.IDeployment> }) => { | ||||||
|  |     let currentState = statePartArg.getState(); | ||||||
|  |     const trCreateDeployment = | ||||||
|  |       new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.deployment.IReq_Any_Cloudly_CreateDeployment>( | ||||||
|  |         '/typedrequest', | ||||||
|  |         'createDeployment' | ||||||
|  |       ); | ||||||
|  |     const response = await trCreateDeployment.fire({ | ||||||
|  |       identity: loginStatePart.getState().identity, | ||||||
|  |       deploymentData: payloadArg.deploymentData, | ||||||
|  |     }); | ||||||
|  |     currentState = await dataState.dispatchAction(getAllDataAction, null); | ||||||
|  |     return currentState; | ||||||
|  |   } | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | export const updateDeploymentAction = dataState.createAction( | ||||||
|  |   async (statePartArg, payloadArg: { deploymentId: string; deploymentData: Partial<plugins.interfaces.data.IDeployment> }) => { | ||||||
|  |     let currentState = statePartArg.getState(); | ||||||
|  |     const trUpdateDeployment = | ||||||
|  |       new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.deployment.IReq_Any_Cloudly_UpdateDeployment>( | ||||||
|  |         '/typedrequest', | ||||||
|  |         'updateDeployment' | ||||||
|  |       ); | ||||||
|  |     const response = await trUpdateDeployment.fire({ | ||||||
|  |       identity: loginStatePart.getState().identity, | ||||||
|  |       deploymentId: payloadArg.deploymentId, | ||||||
|  |       deploymentData: payloadArg.deploymentData, | ||||||
|  |     }); | ||||||
|  |     currentState = await dataState.dispatchAction(getAllDataAction, null); | ||||||
|  |     return currentState; | ||||||
|  |   } | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | export const deleteDeploymentAction = dataState.createAction( | ||||||
|  |   async (statePartArg, payloadArg: { deploymentId: string }) => { | ||||||
|  |     let currentState = statePartArg.getState(); | ||||||
|  |     const trDeleteDeployment = | ||||||
|  |       new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.deployment.IReq_Any_Cloudly_DeleteDeploymentById>( | ||||||
|  |         '/typedrequest', | ||||||
|  |         'deleteDeploymentById' | ||||||
|  |       ); | ||||||
|  |     const response = await trDeleteDeployment.fire({ | ||||||
|  |       identity: loginStatePart.getState().identity, | ||||||
|  |       deploymentId: payloadArg.deploymentId, | ||||||
|  |     }); | ||||||
|  |     currentState = await dataState.dispatchAction(getAllDataAction, null); | ||||||
|  |     return currentState; | ||||||
|  |   } | ||||||
|  | ); | ||||||
|  |  | ||||||
| // cluster | // cluster | ||||||
| export const addClusterAction = dataState.createAction( | export const addClusterAction = dataState.createAction( | ||||||
|   async ( |   async ( | ||||||
|   | |||||||
| @@ -22,62 +22,222 @@ export class CloudlyViewDeployments extends DeesElement { | |||||||
|  |  | ||||||
|   constructor() { |   constructor() { | ||||||
|     super(); |     super(); | ||||||
|     const subecription = appstate.dataState |     const subscription = appstate.dataState | ||||||
|       .select((stateArg) => stateArg) |       .select((stateArg) => stateArg) | ||||||
|       .subscribe((dataArg) => { |       .subscribe((dataArg) => { | ||||||
|         this.data = dataArg; |         this.data = dataArg; | ||||||
|       }); |       }); | ||||||
|     this.rxSubscriptions.push(subecription); |     this.rxSubscriptions.push(subscription); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public static styles = [ |   public static styles = [ | ||||||
|     cssManager.defaultStyles, |     cssManager.defaultStyles, | ||||||
|     shared.viewHostCss, |     shared.viewHostCss, | ||||||
|     css` |     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() { |   public render() { | ||||||
|     return html` |     return html` | ||||||
|       <cloudly-sectionheading>Deployments</cloudly-sectionheading> |       <cloudly-sectionheading>Deployments</cloudly-sectionheading> | ||||||
|       <dees-table |       <dees-table | ||||||
|         .heading1=${'Deployments'} |         .heading1=${'Deployments'} | ||||||
|         .heading2=${'decoded in client'} |         .heading2=${'Service deployments running on cluster nodes'} | ||||||
|         .data=${this.data.deployments} |         .data=${this.data.deployments || []} | ||||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { |         .displayFunction=${(itemArg: plugins.interfaces.data.IDeployment) => { | ||||||
|           return { |           return { | ||||||
|             id: itemArg.id, |             Service: this.getServiceName(itemArg.serviceId), | ||||||
|             serverAmount: itemArg.data.servers.length, |             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=${[ |         .dataActions=${[ | ||||||
|           { |           { | ||||||
|             name: 'add configBundle', |             name: 'Deploy Service', | ||||||
|             iconName: 'plus', |             iconName: 'plus', | ||||||
|             type: ['header', 'footer'], |             type: ['header', 'footer'], | ||||||
|             actionFunc: async (dataActionArg) => { |             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({ |               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||||
|                 heading: 'Add ConfigBundle', |                 heading: 'Deploy Service', | ||||||
|                 content: html` |                 content: html` | ||||||
|                   <dees-form> |                   <dees-form> | ||||||
|                     <dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text> |                     <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  |                     <dees-input-text  | ||||||
|                       .key=${'data.secretGroupIds'} |                       .key=${'nodeId'}  | ||||||
|                       .label=${'secretGroupIds'} |                       .label=${'Target Node ID'}  | ||||||
|                       .value=${''} |                       .required=${true} | ||||||
|                     ></dees-input-text> |                       .description=${'Enter the cluster node ID where this service should be deployed'}> | ||||||
|  |                     </dees-input-text> | ||||||
|                     <dees-input-text  |                     <dees-input-text  | ||||||
|                       .key=${'data.includedTags'} |                       .key=${'version'}  | ||||||
|                       .label=${'includedTags'} |                       .label=${'Version'}  | ||||||
|                       .value=${''} |                       .value=${'latest'} | ||||||
|                     ></dees-input-text> |                       .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> |                   </dees-form> | ||||||
|                 `, |                 `, | ||||||
|                 menuOptions: [ |                 menuOptions: [ | ||||||
|                   { name: 'create', action: async (modalArg) => {} }, |  | ||||||
|                   { |                   { | ||||||
|                     name: 'cancel', |                     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) => { |                     action: async (modalArg) => { | ||||||
|                       modalArg.destroy(); |                       modalArg.destroy(); | ||||||
|                     }, |                     }, | ||||||
| @@ -87,34 +247,96 @@ export class CloudlyViewDeployments extends DeesElement { | |||||||
|             }, |             }, | ||||||
|           }, |           }, | ||||||
|           { |           { | ||||||
|             name: 'delete', |             name: 'Restart', | ||||||
|             iconName: 'trash', |             iconName: 'refresh-cw', | ||||||
|             type: ['contextmenu', 'inRow'], |             type: ['contextmenu', 'inRow'], | ||||||
|             actionFunc: async (actionDataArg) => { |             actionFunc: async (actionDataArg) => { | ||||||
|  |               const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment; | ||||||
|               plugins.deesCatalog.DeesModal.createAndShow({ |               plugins.deesCatalog.DeesModal.createAndShow({ | ||||||
|                 heading: `Delete ConfigBundle ${actionDataArg.item.id}`, |                 heading: `Restart Deployment`, | ||||||
|                 content: html` |                 content: html` | ||||||
|                   <div style="text-align:center"> |                   <div style="text-align:center"> | ||||||
|                     Do you really want to delete the ConfigBundle? |                     Are you sure you want to restart this deployment? | ||||||
|                   </div> |                   </div> | ||||||
|                   <div |                   <div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;"> | ||||||
|                     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;" |                     <div style="color: #fff; font-weight: bold;"> | ||||||
|                   > |                       ${this.getServiceName(deployment.serviceId)} | ||||||
|                     ${actionDataArg.item.id} |                     </div> | ||||||
|  |                     <div style="color: #aaa; font-size: 0.9em; margin-top: 4px;"> | ||||||
|  |                       Node: ${this.getNodeName(deployment.nodeId)} | ||||||
|  |                     </div> | ||||||
|                   </div> |                   </div> | ||||||
|                 `, |                 `, | ||||||
|                 menuOptions: [ |                 menuOptions: [ | ||||||
|                   { |                   { | ||||||
|                     name: 'cancel', |                     name: 'Cancel', | ||||||
|                     action: async (modalArg) => { |                     action: async (modalArg) => { | ||||||
|                       await modalArg.destroy(); |                       await modalArg.destroy(); | ||||||
|                     }, |                     }, | ||||||
|                   }, |                   }, | ||||||
|                   { |                   { | ||||||
|                     name: 'delete', |                     name: 'Restart', | ||||||
|                     action: async (modalArg) => { |                     action: async (modalArg) => { | ||||||
|                       appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { |                       // TODO: Implement restart action | ||||||
|                         configBundleId: actionDataArg.item.id, |                       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(); |                       await modalArg.destroy(); | ||||||
|                     }, |                     }, | ||||||
|   | |||||||
| @@ -22,62 +22,187 @@ export class CloudlyViewServices extends DeesElement { | |||||||
|  |  | ||||||
|   constructor() { |   constructor() { | ||||||
|     super(); |     super(); | ||||||
|     const subecription = appstate.dataState |     const subscription = appstate.dataState | ||||||
|       .select((stateArg) => stateArg) |       .select((stateArg) => stateArg) | ||||||
|       .subscribe((dataArg) => { |       .subscribe((dataArg) => { | ||||||
|         this.data = dataArg; |         this.data = dataArg; | ||||||
|       }); |       }); | ||||||
|     this.rxSubscriptions.push(subecription); |     this.rxSubscriptions.push(subscription); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public static styles = [ |   public static styles = [ | ||||||
|     cssManager.defaultStyles, |     cssManager.defaultStyles, | ||||||
|     shared.viewHostCss, |     shared.viewHostCss, | ||||||
|     css` |     css` | ||||||
|        |       .category-badge { | ||||||
|  |         padding: 2px 8px; | ||||||
|  |         border-radius: 4px; | ||||||
|  |         font-size: 0.9em; | ||||||
|  |         font-weight: 500; | ||||||
|  |       } | ||||||
|  |       .category-base { | ||||||
|  |         background: #2196f3; | ||||||
|  |         color: white; | ||||||
|  |       } | ||||||
|  |       .category-distributed { | ||||||
|  |         background: #9c27b0; | ||||||
|  |         color: white; | ||||||
|  |       } | ||||||
|  |       .category-workload { | ||||||
|  |         background: #4caf50; | ||||||
|  |         color: white; | ||||||
|  |       } | ||||||
|  |       .strategy-badge { | ||||||
|  |         padding: 2px 8px; | ||||||
|  |         border-radius: 4px; | ||||||
|  |         font-size: 0.85em; | ||||||
|  |         background: #444; | ||||||
|  |         color: #ccc; | ||||||
|  |         margin-left: 4px; | ||||||
|  |       } | ||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|  |   private getCategoryIcon(category: string): string { | ||||||
|  |     switch (category) { | ||||||
|  |       case 'base': | ||||||
|  |         return 'lucide:ServerCog'; | ||||||
|  |       case 'distributed': | ||||||
|  |         return 'lucide:Network'; | ||||||
|  |       case 'workload': | ||||||
|  |         return 'lucide:Container'; | ||||||
|  |       default: | ||||||
|  |         return 'lucide:Box'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private getCategoryBadgeHtml(category: string): any { | ||||||
|  |     const className = `category-badge category-${category}`; | ||||||
|  |     return html`<span class="${className}">${category}</span>`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private getStrategyBadgeHtml(strategy: string): any { | ||||||
|  |     return html`<span class="strategy-badge">${strategy}</span>`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public render() { |   public render() { | ||||||
|     return html` |     return html` | ||||||
|       <cloudly-sectionheading>Services</cloudly-sectionheading> |       <cloudly-sectionheading>Services</cloudly-sectionheading> | ||||||
|       <dees-table |       <dees-table | ||||||
|         .heading1=${'Services'} |         .heading1=${'Services'} | ||||||
|         .heading2=${'decoded in client'} |         .heading2=${'Service configuration and deployment management'} | ||||||
|         .data=${this.data.services} |         .data=${this.data.services || []} | ||||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { |         .displayFunction=${(itemArg: plugins.interfaces.data.IService) => { | ||||||
|           return { |           return { | ||||||
|             id: itemArg.id, |             Name: itemArg.data.name, | ||||||
|             serverAmount: itemArg.data.servers.length, |             Description: itemArg.data.description, | ||||||
|  |             Category: this.getCategoryBadgeHtml(itemArg.data.serviceCategory || 'workload'), | ||||||
|  |             'Deployment Strategy': html` | ||||||
|  |               ${this.getStrategyBadgeHtml(itemArg.data.deploymentStrategy || 'custom')} | ||||||
|  |               ${itemArg.data.maxReplicas ? html`<span style="color: #888; margin-left: 8px;">Max: ${itemArg.data.maxReplicas}</span>` : ''} | ||||||
|  |               ${itemArg.data.antiAffinity ? html`<span style="color: #f44336; margin-left: 8px;">⚡ Anti-affinity</span>` : ''} | ||||||
|  |             `, | ||||||
|  |             'Image': `${itemArg.data.imageId}:${itemArg.data.imageVersion}`, | ||||||
|  |             'Scale Factor': itemArg.data.scaleFactor, | ||||||
|  |             'Balancing': itemArg.data.balancingStrategy, | ||||||
|  |             'Deployments': itemArg.data.deploymentIds?.length || 0, | ||||||
|           }; |           }; | ||||||
|         }} |         }} | ||||||
|         .dataActions=${[ |         .dataActions=${[ | ||||||
|           { |           { | ||||||
|             name: 'add configBundle', |             name: 'Add Service', | ||||||
|             iconName: 'plus', |             iconName: 'plus', | ||||||
|             type: ['header', 'footer'], |             type: ['header', 'footer'], | ||||||
|             actionFunc: async (dataActionArg) => { |             actionFunc: async (dataActionArg) => { | ||||||
|               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ |               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||||
|                 heading: 'Add ConfigBundle', |                 heading: 'Add Service', | ||||||
|                 content: html` |                 content: html` | ||||||
|                   <dees-form> |                   <dees-form> | ||||||
|                     <dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></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-dropdown  | ||||||
|  |                       .key=${'serviceCategory'}  | ||||||
|  |                       .label=${'Service Category'}  | ||||||
|  |                       .options=${['base', 'distributed', 'workload']} | ||||||
|  |                       .value=${'workload'} | ||||||
|  |                       .required=${true}> | ||||||
|  |                     </dees-input-dropdown> | ||||||
|  |                     <dees-input-dropdown  | ||||||
|  |                       .key=${'deploymentStrategy'}  | ||||||
|  |                       .label=${'Deployment Strategy'}  | ||||||
|  |                       .options=${['all-nodes', 'limited-replicas', 'custom']} | ||||||
|  |                       .value=${'custom'} | ||||||
|  |                       .required=${true}> | ||||||
|  |                     </dees-input-dropdown> | ||||||
|                     <dees-input-text  |                     <dees-input-text  | ||||||
|                       .key=${'data.secretGroupIds'} |                       .key=${'maxReplicas'}  | ||||||
|                       .label=${'secretGroupIds'} |                       .label=${'Max Replicas (for distributed services)'}  | ||||||
|                       .value=${''} |                       .value=${'3'} | ||||||
|                     ></dees-input-text> |                       .type=${'number'}> | ||||||
|  |                     </dees-input-text> | ||||||
|  |                     <dees-input-checkbox  | ||||||
|  |                       .key=${'antiAffinity'}  | ||||||
|  |                       .label=${'Enable Anti-Affinity'} | ||||||
|  |                       .value=${false}> | ||||||
|  |                     </dees-input-checkbox> | ||||||
|  |                     <dees-input-text .key=${'imageId'} .label=${'Image ID'} .required=${true}></dees-input-text> | ||||||
|  |                     <dees-input-text .key=${'imageVersion'} .label=${'Image Version'} .value=${'latest'} .required=${true}></dees-input-text> | ||||||
|                     <dees-input-text  |                     <dees-input-text  | ||||||
|                       .key=${'data.includedTags'} |                       .key=${'scaleFactor'}  | ||||||
|                       .label=${'includedTags'} |                       .label=${'Scale Factor'}  | ||||||
|                       .value=${''} |                       .value=${'1'} | ||||||
|                     ></dees-input-text> |                       .type=${'number'} | ||||||
|  |                       .required=${true}> | ||||||
|  |                     </dees-input-text> | ||||||
|  |                     <dees-input-dropdown  | ||||||
|  |                       .key=${'balancingStrategy'}  | ||||||
|  |                       .label=${'Balancing Strategy'}  | ||||||
|  |                       .options=${['round-robin', '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', action: async (modalArg) => {} }, |  | ||||||
|                   { |                   { | ||||||
|                     name: 'cancel', |                     name: 'Create Service', | ||||||
|  |                     action: async (modalArg) => { | ||||||
|  |                       const form = modalArg.shadowRoot.querySelector('dees-form') as any; | ||||||
|  |                       const formData = await form.gatherData(); | ||||||
|  |                        | ||||||
|  |                       await appstate.dataState.dispatchAction(appstate.createServiceAction, { | ||||||
|  |                         serviceData: { | ||||||
|  |                           name: formData.name, | ||||||
|  |                           description: formData.description, | ||||||
|  |                           serviceCategory: formData.serviceCategory, | ||||||
|  |                           deploymentStrategy: formData.deploymentStrategy, | ||||||
|  |                           maxReplicas: formData.maxReplicas ? parseInt(formData.maxReplicas) : undefined, | ||||||
|  |                           antiAffinity: formData.antiAffinity, | ||||||
|  |                           imageId: formData.imageId, | ||||||
|  |                           imageVersion: formData.imageVersion, | ||||||
|  |                           scaleFactor: parseInt(formData.scaleFactor), | ||||||
|  |                           balancingStrategy: formData.balancingStrategy, | ||||||
|  |                           ports: { | ||||||
|  |                             web: parseInt(formData.webPort), | ||||||
|  |                           }, | ||||||
|  |                           environment: {}, | ||||||
|  |                           domains: [], | ||||||
|  |                           deploymentIds: [], | ||||||
|  |                         }, | ||||||
|  |                       }); | ||||||
|  |                        | ||||||
|  |                       await modalArg.destroy(); | ||||||
|  |                     }, | ||||||
|  |                   }, | ||||||
|  |                   { | ||||||
|  |                     name: 'Cancel', | ||||||
|                     action: async (modalArg) => { |                     action: async (modalArg) => { | ||||||
|                       modalArg.destroy(); |                       modalArg.destroy(); | ||||||
|                     }, |                     }, | ||||||
| @@ -87,34 +212,137 @@ export class CloudlyViewServices extends DeesElement { | |||||||
|             }, |             }, | ||||||
|           }, |           }, | ||||||
|           { |           { | ||||||
|             name: 'delete', |             name: 'Edit', | ||||||
|  |             iconName: 'edit', | ||||||
|  |             type: ['contextmenu', 'inRow'], | ||||||
|  |             actionFunc: async (actionDataArg) => { | ||||||
|  |               const service = actionDataArg.item as plugins.interfaces.data.IService; | ||||||
|  |               const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||||
|  |                 heading: `Edit Service: ${service.data.name}`, | ||||||
|  |                 content: html` | ||||||
|  |                   <dees-form> | ||||||
|  |                     <dees-input-text .key=${'name'} .label=${'Service Name'} .value=${service.data.name} .required=${true}></dees-input-text> | ||||||
|  |                     <dees-input-text .key=${'description'} .label=${'Description'} .value=${service.data.description} .required=${true}></dees-input-text> | ||||||
|  |                     <dees-input-dropdown  | ||||||
|  |                       .key=${'serviceCategory'}  | ||||||
|  |                       .label=${'Service Category'}  | ||||||
|  |                       .options=${['base', 'distributed', 'workload']} | ||||||
|  |                       .value=${service.data.serviceCategory || 'workload'} | ||||||
|  |                       .required=${true}> | ||||||
|  |                     </dees-input-dropdown> | ||||||
|  |                     <dees-input-dropdown  | ||||||
|  |                       .key=${'deploymentStrategy'}  | ||||||
|  |                       .label=${'Deployment Strategy'}  | ||||||
|  |                       .options=${['all-nodes', 'limited-replicas', 'custom']} | ||||||
|  |                       .value=${service.data.deploymentStrategy || 'custom'} | ||||||
|  |                       .required=${true}> | ||||||
|  |                     </dees-input-dropdown> | ||||||
|  |                     <dees-input-text  | ||||||
|  |                       .key=${'maxReplicas'}  | ||||||
|  |                       .label=${'Max Replicas (for distributed services)'}  | ||||||
|  |                       .value=${service.data.maxReplicas || ''} | ||||||
|  |                       .type=${'number'}> | ||||||
|  |                     </dees-input-text> | ||||||
|  |                     <dees-input-checkbox  | ||||||
|  |                       .key=${'antiAffinity'}  | ||||||
|  |                       .label=${'Enable Anti-Affinity'} | ||||||
|  |                       .value=${service.data.antiAffinity || false}> | ||||||
|  |                     </dees-input-checkbox> | ||||||
|  |                     <dees-input-text .key=${'imageVersion'} .label=${'Image Version'} .value=${service.data.imageVersion} .required=${true}></dees-input-text> | ||||||
|  |                     <dees-input-text  | ||||||
|  |                       .key=${'scaleFactor'}  | ||||||
|  |                       .label=${'Scale Factor'}  | ||||||
|  |                       .value=${service.data.scaleFactor} | ||||||
|  |                       .type=${'number'} | ||||||
|  |                       .required=${true}> | ||||||
|  |                     </dees-input-text> | ||||||
|  |                     <dees-input-dropdown  | ||||||
|  |                       .key=${'balancingStrategy'}  | ||||||
|  |                       .label=${'Balancing Strategy'}  | ||||||
|  |                       .options=${['round-robin', 'least-connections']} | ||||||
|  |                       .value=${service.data.balancingStrategy} | ||||||
|  |                       .required=${true}> | ||||||
|  |                     </dees-input-dropdown> | ||||||
|  |                   </dees-form> | ||||||
|  |                 `, | ||||||
|  |                 menuOptions: [ | ||||||
|  |                   { | ||||||
|  |                     name: 'Update Service', | ||||||
|  |                     action: async (modalArg) => { | ||||||
|  |                       const form = modalArg.shadowRoot.querySelector('dees-form') as any; | ||||||
|  |                       const formData = await form.gatherData(); | ||||||
|  |                        | ||||||
|  |                       await appstate.dataState.dispatchAction(appstate.updateServiceAction, { | ||||||
|  |                         serviceId: service.id, | ||||||
|  |                         serviceData: { | ||||||
|  |                           ...service.data, | ||||||
|  |                           name: formData.name, | ||||||
|  |                           description: formData.description, | ||||||
|  |                           serviceCategory: formData.serviceCategory, | ||||||
|  |                           deploymentStrategy: formData.deploymentStrategy, | ||||||
|  |                           maxReplicas: formData.maxReplicas ? parseInt(formData.maxReplicas) : undefined, | ||||||
|  |                           antiAffinity: formData.antiAffinity, | ||||||
|  |                           imageVersion: formData.imageVersion, | ||||||
|  |                           scaleFactor: parseInt(formData.scaleFactor), | ||||||
|  |                           balancingStrategy: formData.balancingStrategy, | ||||||
|  |                         }, | ||||||
|  |                       }); | ||||||
|  |                        | ||||||
|  |                       await modalArg.destroy(); | ||||||
|  |                     }, | ||||||
|  |                   }, | ||||||
|  |                   { | ||||||
|  |                     name: 'Cancel', | ||||||
|  |                     action: async (modalArg) => { | ||||||
|  |                       modalArg.destroy(); | ||||||
|  |                     }, | ||||||
|  |                   }, | ||||||
|  |                 ], | ||||||
|  |               }); | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             name: 'Deploy', | ||||||
|  |             iconName: 'rocket', | ||||||
|  |             type: ['contextmenu', 'inRow'], | ||||||
|  |             actionFunc: async (actionDataArg) => { | ||||||
|  |               const service = actionDataArg.item as plugins.interfaces.data.IService; | ||||||
|  |               // TODO: Implement deployment action | ||||||
|  |               console.log('Deploy service:', service); | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             name: 'Delete', | ||||||
|             iconName: 'trash', |             iconName: 'trash', | ||||||
|             type: ['contextmenu', 'inRow'], |             type: ['contextmenu', 'inRow'], | ||||||
|             actionFunc: async (actionDataArg) => { |             actionFunc: async (actionDataArg) => { | ||||||
|  |               const service = actionDataArg.item as plugins.interfaces.data.IService; | ||||||
|               plugins.deesCatalog.DeesModal.createAndShow({ |               plugins.deesCatalog.DeesModal.createAndShow({ | ||||||
|                 heading: `Delete ConfigBundle ${actionDataArg.item.id}`, |                 heading: `Delete Service: ${service.data.name}`, | ||||||
|                 content: html` |                 content: html` | ||||||
|                   <div style="text-align:center"> |                   <div style="text-align:center"> | ||||||
|                     Do you really want to delete the ConfigBundle? |                     Are you sure you want to delete this service? | ||||||
|                   </div> |                   </div> | ||||||
|                   <div |                   <div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;"> | ||||||
|                     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;" |                     <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> | ||||||
|                     ${actionDataArg.item.id} |                     <div style="color: #f44336; margin-top: 8px;"> | ||||||
|  |                       This will also delete ${service.data.deploymentIds?.length || 0} deployment(s) | ||||||
|  |                     </div> | ||||||
|                   </div> |                   </div> | ||||||
|                 `, |                 `, | ||||||
|                 menuOptions: [ |                 menuOptions: [ | ||||||
|                   { |                   { | ||||||
|                     name: 'cancel', |                     name: 'Cancel', | ||||||
|                     action: async (modalArg) => { |                     action: async (modalArg) => { | ||||||
|                       await modalArg.destroy(); |                       await modalArg.destroy(); | ||||||
|                     }, |                     }, | ||||||
|                   }, |                   }, | ||||||
|                   { |                   { | ||||||
|                     name: 'delete', |                     name: 'Delete', | ||||||
|                     action: async (modalArg) => { |                     action: async (modalArg) => { | ||||||
|                       appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { |                       await appstate.dataState.dispatchAction(appstate.deleteServiceAction, { | ||||||
|                         configBundleId: actionDataArg.item.id, |                         serviceId: service.id, | ||||||
|                       }); |                       }); | ||||||
|                       await modalArg.destroy(); |                       await modalArg.destroy(); | ||||||
|                     }, |                     }, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user