feat: Implement Cloudly Task Manager with predefined tasks and execution tracking
- Added CloudlyTaskManager class for managing tasks, including registration, execution, scheduling, and cancellation. - Created predefined tasks: DNS Sync, Certificate Renewal, Cleanup, Health Check, Resource Report, Database Maintenance, Security Scan, and Docker Cleanup. - Introduced ITaskExecution interface for tracking task execution details and outcomes. - Developed API request interfaces for task management operations (getTasks, getTaskExecutions, triggerTask, cancelTask). - Implemented CloudlyViewTasks web component for displaying tasks and their execution history, including filtering and detailed views.
This commit is contained in:
		| @@ -663,6 +663,89 @@ export const verifyExternalRegistryAction = dataState.createAction( | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // Task Actions | ||||
| export const taskActions = { | ||||
|   getTasks: dataState.createAction( | ||||
|     async (statePartArg, payloadArg: {}) => { | ||||
|       const currentState = statePartArg.getState(); | ||||
|       const trGetTasks = | ||||
|         new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.task.IRequest_Any_Cloudly_GetTasks>( | ||||
|           '/typedrequest', | ||||
|           'getTasks' | ||||
|         ); | ||||
|       const response = await trGetTasks.fire({ | ||||
|         identity: loginStatePart.getState().identity, | ||||
|       }); | ||||
|       return response as any; | ||||
|     } | ||||
|   ), | ||||
|    | ||||
|   getTaskExecutions: dataState.createAction( | ||||
|     async (statePartArg, payloadArg: { filter?: any }) => { | ||||
|       const currentState = statePartArg.getState(); | ||||
|       const trGetTaskExecutions = | ||||
|         new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutions>( | ||||
|           '/typedrequest', | ||||
|           'getTaskExecutions' | ||||
|         ); | ||||
|       const response = await trGetTaskExecutions.fire({ | ||||
|         identity: loginStatePart.getState().identity, | ||||
|         filter: payloadArg.filter, | ||||
|       }); | ||||
|       return response as any; | ||||
|     } | ||||
|   ), | ||||
|    | ||||
|   getTaskExecutionById: dataState.createAction( | ||||
|     async (statePartArg, payloadArg: { executionId: string }) => { | ||||
|       const currentState = statePartArg.getState(); | ||||
|       const trGetTaskExecutionById = | ||||
|         new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutionById>( | ||||
|           '/typedrequest', | ||||
|           'getTaskExecutionById' | ||||
|         ); | ||||
|       const response = await trGetTaskExecutionById.fire({ | ||||
|         identity: loginStatePart.getState().identity, | ||||
|         executionId: payloadArg.executionId, | ||||
|       }); | ||||
|       return response as any; | ||||
|     } | ||||
|   ), | ||||
|    | ||||
|   triggerTask: dataState.createAction( | ||||
|     async (statePartArg, payloadArg: { taskName: string; userId?: string }) => { | ||||
|       const currentState = statePartArg.getState(); | ||||
|       const trTriggerTask = | ||||
|         new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.task.IRequest_Any_Cloudly_TriggerTask>( | ||||
|           '/typedrequest', | ||||
|           'triggerTask' | ||||
|         ); | ||||
|       const response = await trTriggerTask.fire({ | ||||
|         identity: loginStatePart.getState().identity, | ||||
|         taskName: payloadArg.taskName, | ||||
|         userId: payloadArg.userId, | ||||
|       }); | ||||
|       return currentState; | ||||
|     } | ||||
|   ), | ||||
|    | ||||
|   cancelTask: dataState.createAction( | ||||
|     async (statePartArg, payloadArg: { executionId: string }) => { | ||||
|       const currentState = statePartArg.getState(); | ||||
|       const trCancelTask = | ||||
|         new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.task.IRequest_Any_Cloudly_CancelTask>( | ||||
|           '/typedrequest', | ||||
|           'cancelTask' | ||||
|         ); | ||||
|       const response = await trCancelTask.fire({ | ||||
|         identity: loginStatePart.getState().identity, | ||||
|         executionId: payloadArg.executionId, | ||||
|       }); | ||||
|       return currentState; | ||||
|     } | ||||
|   ), | ||||
| }; | ||||
|  | ||||
| // cluster | ||||
| export const addClusterAction = dataState.createAction( | ||||
|   async ( | ||||
|   | ||||
| @@ -27,6 +27,7 @@ import { CloudlyViewSecretGroups } from './cloudly-view-secretgroups.js'; | ||||
| import { CloudlyViewServices } from './cloudly-view-services.js'; | ||||
| import { CloudlyViewExternalRegistries } from './cloudly-view-externalregistries.js'; | ||||
| import { CloudlyViewSettings } from './cloudly-view-settings.js'; | ||||
| import { CloudlyViewTasks } from './cloudly-view-tasks.js'; | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
| @@ -126,6 +127,11 @@ export class CloudlyDashboard extends DeesElement { | ||||
|                 iconName: 'lucide:Rocket', | ||||
|                 element: CloudlyViewDeployments, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'Tasks', | ||||
|                 iconName: 'lucide:ListChecks', | ||||
|                 element: CloudlyViewTasks, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'Domains', | ||||
|                 iconName: 'lucide:Globe2', | ||||
|   | ||||
							
								
								
									
										560
									
								
								ts_web/elements/cloudly-view-tasks.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										560
									
								
								ts_web/elements/cloudly-view-tasks.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,560 @@ | ||||
| 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 tasks: any[] = []; | ||||
|  | ||||
|   @state() | ||||
|   private executions: plugins.interfaces.data.ITaskExecution[] = []; | ||||
|  | ||||
|   @state() | ||||
|   private selectedExecution: plugins.interfaces.data.ITaskExecution | null = null; | ||||
|  | ||||
|   @state() | ||||
|   private loading = false; | ||||
|  | ||||
|   @state() | ||||
|   private filterStatus: string = 'all'; | ||||
|  | ||||
|   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` | ||||
|       .task-grid { | ||||
|         display: grid; | ||||
|         grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | ||||
|         gap: 16px; | ||||
|         margin-bottom: 32px; | ||||
|       } | ||||
|  | ||||
|       .task-card { | ||||
|         background: #1a1a1a; | ||||
|         border: 1px solid #333; | ||||
|         border-radius: 8px; | ||||
|         padding: 16px; | ||||
|         cursor: pointer; | ||||
|         transition: all 0.2s; | ||||
|       } | ||||
|  | ||||
|       .task-card:hover { | ||||
|         background: #222; | ||||
|         border-color: #555; | ||||
|       } | ||||
|  | ||||
|       .task-header { | ||||
|         display: flex; | ||||
|         justify-content: space-between; | ||||
|         align-items: center; | ||||
|         margin-bottom: 12px; | ||||
|       } | ||||
|  | ||||
|       .task-name { | ||||
|         font-size: 1.1em; | ||||
|         font-weight: 600; | ||||
|         color: #fff; | ||||
|       } | ||||
|  | ||||
|       .task-description { | ||||
|         color: #999; | ||||
|         font-size: 0.9em; | ||||
|         margin-bottom: 12px; | ||||
|       } | ||||
|  | ||||
|       .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: 4px; | ||||
|         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: 4px; | ||||
|         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; | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   async connectedCallback() { | ||||
|     super.connectedCallback(); | ||||
|     await this.loadTasks(); | ||||
|     await this.loadExecutions(); | ||||
|   } | ||||
|  | ||||
|   private async loadTasks() { | ||||
|     this.loading = true; | ||||
|     try { | ||||
|       const response: any = await appstate.dataState.dispatchAction( | ||||
|         appstate.taskActions.getTasks, {} | ||||
|       ); | ||||
|       this.tasks = response.tasks || []; | ||||
|     } catch (error) { | ||||
|       console.error('Failed to load tasks:', error); | ||||
|     } finally { | ||||
|       this.loading = false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async loadExecutions() { | ||||
|     try { | ||||
|       const filter: any = {}; | ||||
|       if (this.filterStatus !== 'all') { | ||||
|         filter.status = this.filterStatus; | ||||
|       } | ||||
|        | ||||
|       const response: any = await appstate.dataState.dispatchAction( | ||||
|         appstate.taskActions.getTaskExecutions, { filter } | ||||
|       ); | ||||
|       this.executions = response.executions || []; | ||||
|     } catch (error) { | ||||
|       console.error('Failed to load executions:', error); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async triggerTask(taskName: string) { | ||||
|     try { | ||||
|       await appstate.dataState.dispatchAction( | ||||
|         appstate.taskActions.triggerTask, { taskName } | ||||
|       ); | ||||
|        | ||||
|       // Reload tasks and executions to show the new execution | ||||
|       await this.loadTasks(); | ||||
|       await this.loadExecutions(); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to trigger task:', 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 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 renderTaskCard(task: any) { | ||||
|     const lastExecution = this.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'; | ||||
|      | ||||
|     return html` | ||||
|       <div class="task-card"> | ||||
|         <div class="task-header"> | ||||
|           <div class="task-name"> | ||||
|             <dees-icon .iconName=${this.getCategoryIcon(task.category)}></dees-icon> | ||||
|             ${task.name} | ||||
|           </div> | ||||
|           <button | ||||
|             class="trigger-button" | ||||
|             ?disabled=${isRunning || !task.enabled} | ||||
|             @click=${() => this.triggerTask(task.name)} | ||||
|           > | ||||
|             ${isRunning ? 'Running...' : 'Run'} | ||||
|           </button> | ||||
|         </div> | ||||
|          | ||||
|         <div class="task-description">${task.description}</div> | ||||
|          | ||||
|         <div class="task-meta"> | ||||
|           <span class="category-badge category-${task.category}">${task.category}</span> | ||||
|           ${!task.enabled ? html`<span class="status-badge status-cancelled">Disabled</span>` : ''} | ||||
|         </div> | ||||
|          | ||||
|         ${task.schedule ? html` | ||||
|           <div class="schedule-info"> | ||||
|             <dees-icon .iconName=${'lucide:Clock'}></dees-icon> | ||||
|             Schedule: ${task.schedule} | ||||
|           </div> | ||||
|         ` : ''} | ||||
|          | ||||
|         ${task.lastRun ? html` | ||||
|           <div class="last-run"> | ||||
|             Last run: ${this.formatDate(task.lastRun)} | ||||
|           </div> | ||||
|         ` : ''} | ||||
|          | ||||
|         ${lastExecution ? html` | ||||
|           <div class="metrics"> | ||||
|             <div class="metric"> | ||||
|               <span class="metric-label">Status</span> | ||||
|               <span class="metric-value"> | ||||
|                 <span class="status-badge status-${lastExecution.data.status}"> | ||||
|                   ${lastExecution.data.status} | ||||
|                 </span> | ||||
|               </span> | ||||
|             </div> | ||||
|             ${lastExecution.data.duration ? html` | ||||
|               <div class="metric"> | ||||
|                 <span class="metric-label">Duration</span> | ||||
|                 <span class="metric-value">${this.formatDuration(lastExecution.data.duration)}</span> | ||||
|               </div> | ||||
|             ` : ''} | ||||
|           </div> | ||||
|         ` : ''} | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   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() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Tasks</cloudly-sectionheading> | ||||
|        | ||||
|       <div class="filter-bar"> | ||||
|         <button | ||||
|           class="filter-button ${this.filterStatus === 'all' ? 'active' : ''}" | ||||
|           @click=${() => { this.filterStatus = 'all'; this.loadExecutions(); }} | ||||
|         > | ||||
|           All | ||||
|         </button> | ||||
|         <button | ||||
|           class="filter-button ${this.filterStatus === 'running' ? 'active' : ''}" | ||||
|           @click=${() => { this.filterStatus = 'running'; this.loadExecutions(); }} | ||||
|         > | ||||
|           Running | ||||
|         </button> | ||||
|         <button | ||||
|           class="filter-button ${this.filterStatus === 'completed' ? 'active' : ''}" | ||||
|           @click=${() => { this.filterStatus = 'completed'; this.loadExecutions(); }} | ||||
|         > | ||||
|           Completed | ||||
|         </button> | ||||
|         <button | ||||
|           class="filter-button ${this.filterStatus === 'failed' ? 'active' : ''}" | ||||
|           @click=${() => { this.filterStatus = 'failed'; this.loadExecutions(); }} | ||||
|         > | ||||
|           Failed | ||||
|         </button> | ||||
|       </div> | ||||
|        | ||||
|       <div class="task-grid"> | ||||
|         ${this.tasks.map(task => this.renderTaskCard(task))} | ||||
|       </div> | ||||
|        | ||||
|       <cloudly-sectionheading>Execution History</cloudly-sectionheading> | ||||
|        | ||||
|       <dees-table | ||||
|         .heading1=${'Task Executions'} | ||||
|         .heading2=${'History of task runs and their outcomes'} | ||||
|         .data=${this.executions} | ||||
|         .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) => [ | ||||
|           { | ||||
|             name: 'View Details', | ||||
|             iconName: 'lucide:Eye', | ||||
|             type: ['inRow'], | ||||
|             actionFunc: async () => { | ||||
|               this.selectedExecution = itemArg; | ||||
|             }, | ||||
|           }, | ||||
|         ]} | ||||
|       ></dees-table> | ||||
|        | ||||
|       ${this.selectedExecution ? html` | ||||
|         <cloudly-sectionheading>Execution Details</cloudly-sectionheading> | ||||
|         ${this.renderExecutionDetails(this.selectedExecution)} | ||||
|       ` : ''} | ||||
|     `; | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user