From e19639c9bebaf85ed26ce6895a626a2626bc49b9 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Mon, 8 Sep 2025 06:46:14 +0000 Subject: [PATCH] feat(web): Add deployments API typings and web UI improvements: services & deployments management with CRUD and actions --- changelog.md | 9 + ts/00_commitinfo_data.ts | 2 +- ts_interfaces/requests/deployment.ts | 141 +++++++++ ts_web/00_commitinfo_data.ts | 2 +- ts_web/appstate.ts | 138 ++++++++- ts_web/elements/cloudly-view-deployments.ts | 294 ++++++++++++++++--- ts_web/elements/cloudly-view-services.ts | 298 +++++++++++++++++--- 7 files changed, 809 insertions(+), 75 deletions(-) create mode 100644 ts_interfaces/requests/deployment.ts diff --git a/changelog.md b/changelog.md index 6ef5f2a..bb05811 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # 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) Add runtime settings management, node & baremetal managers, and settings UI diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index bcc152b..972aa4a 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { 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.' } diff --git a/ts_interfaces/requests/deployment.ts b/ts_interfaces/requests/deployment.ts new file mode 100644 index 0000000..f8cce68 --- /dev/null +++ b/ts_interfaces/requests/deployment.ts @@ -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; + }; + 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; + }; + 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; + }; +} \ No newline at end of file diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index bcc152b..972aa4a 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { 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.' } diff --git a/ts_web/appstate.ts b/ts_web/appstate.ts index 7f78734..0dad3ab 100644 --- a/ts_web/appstate.ts +++ b/ts_web/appstate.ts @@ -48,8 +48,8 @@ export interface IDataState { secretBundles?: plugins.interfaces.data.ISecretBundle[]; clusters?: plugins.interfaces.data.ICluster[]; images?: any[]; - services?: any[]; - deployments?: any[]; + services?: plugins.interfaces.data.IService[]; + deployments?: plugins.interfaces.data.IDeployment[]; dns?: any[]; mails?: any[]; logs?: any[]; @@ -136,9 +136,90 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => { clusters: responseClusters.clusters, } + // Services + const trGetServices = + new domtools.plugins.typedrequest.TypedRequest( + '/typedrequest', + 'getServices' + ); + const responseServices = await trGetServices.fire({ + identity: loginStatePart.getState().identity, + }); + currentState = { + ...currentState, + services: responseServices.services, + }; + + // Deployments + const trGetDeployments = + new domtools.plugins.typedrequest.TypedRequest( + '/typedrequest', + 'getDeployments' + ); + const responseDeployments = await trGetDeployments.fire({ + identity: loginStatePart.getState().identity, + }); + currentState = { + ...currentState, + deployments: responseDeployments.deployments, + }; + 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( + '/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( + '/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( + '/typedrequest', + 'deleteServiceById' + ); + const response = await trDeleteService.fire({ + identity: loginStatePart.getState().identity, + serviceId: payloadArg.serviceId, + }); + currentState = await dataState.dispatchAction(getAllDataAction, null); + return currentState; + } +); + // SecretGroup Actions export const createSecretGroupAction = dataState.createAction( 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 }) => { + let currentState = statePartArg.getState(); + const trCreateDeployment = + new domtools.plugins.typedrequest.TypedRequest( + '/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 }) => { + let currentState = statePartArg.getState(); + const trUpdateDeployment = + new domtools.plugins.typedrequest.TypedRequest( + '/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( + '/typedrequest', + 'deleteDeploymentById' + ); + const response = await trDeleteDeployment.fire({ + identity: loginStatePart.getState().identity, + deploymentId: payloadArg.deploymentId, + }); + currentState = await dataState.dispatchAction(getAllDataAction, null); + return currentState; + } +); + // cluster export const addClusterAction = dataState.createAction( async ( diff --git a/ts_web/elements/cloudly-view-deployments.ts b/ts_web/elements/cloudly-view-deployments.ts index 40f6d51..307863b 100644 --- a/ts_web/elements/cloudly-view-deployments.ts +++ b/ts_web/elements/cloudly-view-deployments.ts @@ -22,62 +22,222 @@ export class CloudlyViewDeployments extends DeesElement { constructor() { super(); - const subecription = appstate.dataState + const subscription = appstate.dataState .select((stateArg) => stateArg) .subscribe((dataArg) => { this.data = dataArg; }); - this.rxSubscriptions.push(subecription); + this.rxSubscriptions.push(subscription); } public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css` - + .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`${status}`; + } + + private getHealthIndicatorHtml(health?: string): any { + if (!health) health = 'unknown'; + const className = `health-indicator health-${health}`; + const icon = health === 'healthy' ? '✓' : health === 'unhealthy' ? '✗' : '?'; + return html`${icon} ${health}`; + } + + private getResourceUsageHtml(deployment: plugins.interfaces.data.IDeployment): any { + if (!deployment.resourceUsage) { + return html`N/A`; + } + const { cpuUsagePercent, memoryUsedMB } = deployment.resourceUsage; + return html` +
+
+ + ${cpuUsagePercent?.toFixed(1) || 0}% +
+
+ + ${memoryUsedMB || 0} MB +
+
+ `; + } + public render() { return html` Deployments { + .heading2=${'Service deployments running on cluster nodes'} + .data=${this.data.deployments || []} + .displayFunction=${(itemArg: plugins.interfaces.data.IDeployment) => { return { - id: itemArg.id, - serverAmount: itemArg.data.servers.length, + 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`${itemArg.containerId.substring(0, 12)}` : + html`N/A`, + Version: itemArg.version || 'latest', + 'Resource Usage': this.getResourceUsageHtml(itemArg), + 'Last Updated': itemArg.deployedAt ? + new Date(itemArg.deployedAt).toLocaleString() : + 'Never', }; }} .dataActions=${[ { - name: 'add configBundle', + 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` +
+ +
Please create a service first before creating deployments.
+
+ `, + menuOptions: [ + { + name: 'OK', + action: async (modalArg) => { + await modalArg.destroy(); + }, + }, + ], + }); + return; + } + const modal = await plugins.deesCatalog.DeesModal.createAndShow({ - heading: 'Add ConfigBundle', + heading: 'Deploy Service', content: html` - - - + ({ key: s.id, value: s.data.name }))} + .required=${true}> + + + + + + + `, 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) => { modalArg.destroy(); }, @@ -87,34 +247,96 @@ export class CloudlyViewDeployments extends DeesElement { }, }, { - name: 'delete', - iconName: 'trash', + 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: `Delete ConfigBundle ${actionDataArg.item.id}`, + heading: `Restart Deployment`, content: html`
- Do you really want to delete the ConfigBundle? + Are you sure you want to restart this deployment?
-
- ${actionDataArg.item.id} +
+
+ ${this.getServiceName(deployment.serviceId)} +
+
+ Node: ${this.getNodeName(deployment.nodeId)} +
`, menuOptions: [ { - name: 'cancel', + name: 'Cancel', action: async (modalArg) => { await modalArg.destroy(); }, }, { - name: 'delete', + name: 'Restart', action: async (modalArg) => { - appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { - configBundleId: actionDataArg.item.id, + // 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` +
+ Are you sure you want to delete this deployment? +
+
+
+ ${this.getServiceName(deployment.serviceId)} +
+
+ Node: ${this.getNodeName(deployment.nodeId)} +
+
+ This action cannot be undone. +
+
+ `, + 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(); }, @@ -127,4 +349,4 @@ export class CloudlyViewDeployments extends DeesElement { > `; } -} +} \ No newline at end of file diff --git a/ts_web/elements/cloudly-view-services.ts b/ts_web/elements/cloudly-view-services.ts index 9035016..41cf455 100644 --- a/ts_web/elements/cloudly-view-services.ts +++ b/ts_web/elements/cloudly-view-services.ts @@ -22,62 +22,187 @@ export class CloudlyViewServices extends DeesElement { constructor() { super(); - const subecription = appstate.dataState + const subscription = appstate.dataState .select((stateArg) => stateArg) .subscribe((dataArg) => { this.data = dataArg; }); - this.rxSubscriptions.push(subecription); + this.rxSubscriptions.push(subscription); } public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css` - + .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`${category}`; + } + + private getStrategyBadgeHtml(strategy: string): any { + return html`${strategy}`; + } + public render() { return html` Services { + .heading2=${'Service configuration and deployment management'} + .data=${this.data.services || []} + .displayFunction=${(itemArg: plugins.interfaces.data.IService) => { return { - id: itemArg.id, - serverAmount: itemArg.data.servers.length, + Name: itemArg.data.name, + Description: itemArg.data.description, + Category: this.getCategoryBadgeHtml(itemArg.data.serviceCategory || 'workload'), + 'Deployment Strategy': html` + ${this.getStrategyBadgeHtml(itemArg.data.deploymentStrategy || 'custom')} + ${itemArg.data.maxReplicas ? html`Max: ${itemArg.data.maxReplicas}` : ''} + ${itemArg.data.antiAffinity ? html`⚡ Anti-affinity` : ''} + `, + 'Image': `${itemArg.data.imageId}:${itemArg.data.imageVersion}`, + 'Scale Factor': itemArg.data.scaleFactor, + 'Balancing': itemArg.data.balancingStrategy, + 'Deployments': itemArg.data.deploymentIds?.length || 0, }; }} .dataActions=${[ { - name: 'add configBundle', + name: 'Add Service', iconName: 'plus', type: ['header', 'footer'], actionFunc: async (dataActionArg) => { const modal = await plugins.deesCatalog.DeesModal.createAndShow({ - heading: 'Add ConfigBundle', + heading: 'Add Service', content: html` - - - + + + + + + + + + + + + + + + + + + `, 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) => { 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` + + + + + + + + + + + + + + + + + + `, + 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', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg) => { + const service = actionDataArg.item as plugins.interfaces.data.IService; plugins.deesCatalog.DeesModal.createAndShow({ - heading: `Delete ConfigBundle ${actionDataArg.item.id}`, + heading: `Delete Service: ${service.data.name}`, content: html`
- Do you really want to delete the ConfigBundle? + Are you sure you want to delete this service?
-
- ${actionDataArg.item.id} +
+
${service.data.name}
+
${service.data.description}
+
+ This will also delete ${service.data.deploymentIds?.length || 0} deployment(s) +
`, menuOptions: [ { - name: 'cancel', + name: 'Cancel', action: async (modalArg) => { await modalArg.destroy(); }, }, { - name: 'delete', + name: 'Delete', action: async (modalArg) => { - appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { - configBundleId: actionDataArg.item.id, + await appstate.dataState.dispatchAction(appstate.deleteServiceAction, { + serviceId: service.id, }); await modalArg.destroy(); }, @@ -127,4 +355,4 @@ export class CloudlyViewServices extends DeesElement { > `; } -} +} \ No newline at end of file