feat(cloudly-view-tasks): add search and category filters, implement auto-refresh for task executions
This commit is contained in:
		| @@ -27,6 +27,17 @@ export class CloudlyViewTasks extends DeesElement { | ||||
|   @state() | ||||
|   private filterStatus: string = 'all'; | ||||
|  | ||||
|   @state() | ||||
|   private searchQuery: string = ''; | ||||
|  | ||||
|   @state() | ||||
|   private categoryFilter: string = 'all'; | ||||
|  | ||||
|   @state() | ||||
|   private autoRefresh: boolean = true; | ||||
|  | ||||
|   private _refreshHandle: any = null; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subscription = appstate.dataState | ||||
| @@ -38,6 +49,9 @@ export class CloudlyViewTasks extends DeesElement { | ||||
|      | ||||
|     // Load initial data (non-blocking) | ||||
|     this.loadInitialData(); | ||||
|  | ||||
|     // Start periodic refresh (lightweight; executions only by default) | ||||
|     this.startAutoRefresh(); | ||||
|   } | ||||
|  | ||||
|   private async loadInitialData() { | ||||
| @@ -49,48 +63,117 @@ export class CloudlyViewTasks extends DeesElement { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private startAutoRefresh() { | ||||
|     this.stopAutoRefresh(); | ||||
|     if (!this.autoRefresh) return; | ||||
|     this._refreshHandle = setInterval(async () => { | ||||
|       try { | ||||
|         await this.loadExecutionsWithFilter(); | ||||
|       } catch (err) { | ||||
|         // ignore transient errors during refresh | ||||
|       } | ||||
|     }, 5000); | ||||
|   } | ||||
|  | ||||
|   private stopAutoRefresh() { | ||||
|     if (this._refreshHandle) { | ||||
|       clearInterval(this._refreshHandle); | ||||
|       this._refreshHandle = null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public async disconnectedCallback(): Promise<void> { | ||||
|     await (super.disconnectedCallback?.()); | ||||
|     this.stopAutoRefresh(); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css` | ||||
|       .toolbar { | ||||
|         display: flex; | ||||
|         gap: 12px; | ||||
|         align-items: center; | ||||
|         margin: 4px 0 16px 0; | ||||
|         flex-wrap: wrap; | ||||
|       } | ||||
|  | ||||
|       .toolbar .spacer { | ||||
|         flex: 1 1 auto; | ||||
|       } | ||||
|  | ||||
|       .search-input { | ||||
|         background: #111; | ||||
|         color: #ddd; | ||||
|         border: 1px solid #333; | ||||
|         border-radius: 6px; | ||||
|         padding: 8px 10px; | ||||
|         min-width: 220px; | ||||
|       } | ||||
|  | ||||
|       .chipbar { | ||||
|         display: flex; | ||||
|         gap: 8px; | ||||
|         flex-wrap: wrap; | ||||
|       } | ||||
|  | ||||
|       .chip { | ||||
|         padding: 6px 10px; | ||||
|         background: #2a2a2a; | ||||
|         color: #bbb; | ||||
|         border: 1px solid #444; | ||||
|         border-radius: 16px; | ||||
|         cursor: pointer; | ||||
|         transition: all 0.2s; | ||||
|         user-select: none; | ||||
|       } | ||||
|  | ||||
|       .chip.active { | ||||
|         background: #2196f3; | ||||
|         border-color: #2196f3; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .task-grid { | ||||
|         display: grid; | ||||
|         grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | ||||
|         grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); | ||||
|         gap: 16px; | ||||
|         margin-bottom: 32px; | ||||
|       } | ||||
|  | ||||
|       .task-card { | ||||
|         background: #1a1a1a; | ||||
|         border: 1px solid #333; | ||||
|         border-radius: 8px; | ||||
|         background: #131313; | ||||
|         border: 1px solid #2a2a2a; | ||||
|         border-radius: 10px; | ||||
|         padding: 16px; | ||||
|         cursor: pointer; | ||||
|         transition: all 0.2s; | ||||
|         transition: border-color 0.2s, background 0.2s; | ||||
|       } | ||||
|  | ||||
|       .task-card:hover { | ||||
|         background: #222; | ||||
|         border-color: #555; | ||||
|       } | ||||
|       .task-card:hover { border-color: #3a3a3a; } | ||||
|  | ||||
|       .task-header { | ||||
|       .card-header { | ||||
|         display: flex; | ||||
|         justify-content: space-between; | ||||
|         align-items: center; | ||||
|         gap: 12px; | ||||
|         margin-bottom: 12px; | ||||
|       } | ||||
|  | ||||
|       .task-name { | ||||
|         font-size: 1.1em; | ||||
|         font-weight: 600; | ||||
|         color: #fff; | ||||
|       } | ||||
|       .header-left { display: flex; align-items: center; gap: 12px; min-width: 0; } | ||||
|       .header-right { display: flex; align-items: center; gap: 8px; } | ||||
|       .task-icon { color: #cfcfcf; } | ||||
|       .task-name { font-size: 1.05em; font-weight: 650; color: #fff; letter-spacing: 0.1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | ||||
|       .task-subtitle { color: #8c8c8c; font-size: 0.9em; } | ||||
|  | ||||
|       .task-description { | ||||
|         color: #999; | ||||
|         font-size: 0.9em; | ||||
|         color: #b5b5b5; | ||||
|         font-size: 0.95em; | ||||
|         margin-bottom: 12px; | ||||
|         display: -webkit-box; | ||||
|         -webkit-line-clamp: 2; | ||||
|         -webkit-box-orient: vertical; | ||||
|         overflow: hidden; | ||||
|       } | ||||
|  | ||||
|       .task-meta { | ||||
| @@ -166,7 +249,7 @@ export class CloudlyViewTasks extends DeesElement { | ||||
|         background: #2196f3; | ||||
|         color: white; | ||||
|         border: none; | ||||
|         border-radius: 4px; | ||||
|         border-radius: 6px; | ||||
|         cursor: pointer; | ||||
|         font-size: 0.9em; | ||||
|         transition: background 0.2s; | ||||
| @@ -196,7 +279,7 @@ export class CloudlyViewTasks extends DeesElement { | ||||
|       .execution-logs { | ||||
|         background: #0a0a0a; | ||||
|         border: 1px solid #333; | ||||
|         border-radius: 4px; | ||||
|         border-radius: 6px; | ||||
|         padding: 16px; | ||||
|         margin-top: 16px; | ||||
|         max-height: 400px; | ||||
| @@ -279,21 +362,150 @@ export class CloudlyViewTasks extends DeesElement { | ||||
|         font-size: 1.1em; | ||||
|         font-weight: 600; | ||||
|       } | ||||
|  | ||||
|       .card-actions, .task-header .right { | ||||
|         display: flex; | ||||
|         gap: 8px; | ||||
|         align-items: center; | ||||
|       } | ||||
|  | ||||
|       .metrics-grid { | ||||
|         display: grid; | ||||
|         grid-template-columns: repeat(3, minmax(0, 1fr)); | ||||
|         gap: 10px; | ||||
|         margin-top: 8px; | ||||
|       } | ||||
|  | ||||
|       .metric-item { | ||||
|         background: #0f0f0f; | ||||
|         border: 1px solid #2c2c2c; | ||||
|         border-radius: 8px; | ||||
|         padding: 10px 12px; | ||||
|       } | ||||
|  | ||||
|       .metric-item .label { | ||||
|         color: #8d8d8d; | ||||
|         font-size: 0.8em; | ||||
|       } | ||||
|  | ||||
|       .metric-item .value { | ||||
|         color: #eaeaea; | ||||
|         font-weight: 600; | ||||
|         margin-top: 4px; | ||||
|       } | ||||
|  | ||||
|       .lastline { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         gap: 8px; | ||||
|         color: #a0a0a0; | ||||
|         font-size: 0.9em; | ||||
|         margin-top: 10px; | ||||
|       } | ||||
|  | ||||
|       .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } | ||||
|       .dot.info { background: #2196f3; } | ||||
|       .dot.success { background: #4caf50; } | ||||
|       .dot.warning { background: #ff9800; } | ||||
|       .dot.error { background: #f44336; } | ||||
|  | ||||
|       .card-footer { | ||||
|         display: flex; | ||||
|         gap: 12px; | ||||
|         margin-top: 12px; | ||||
|       } | ||||
|  | ||||
|       .link-button { | ||||
|         background: transparent; | ||||
|         border: none; | ||||
|         color: #8ab4ff; | ||||
|         cursor: pointer; | ||||
|         padding: 0; | ||||
|         font-size: 0.95em; | ||||
|       } | ||||
|       .link-button:hover { text-decoration: underline; } | ||||
|  | ||||
|       .secondary-button { | ||||
|         padding: 6px 12px; | ||||
|         background: #2b2b2b; | ||||
|         color: #ccc; | ||||
|         border: 1px solid #444; | ||||
|         border-radius: 6px; | ||||
|         cursor: pointer; | ||||
|         font-size: 0.9em; | ||||
|         transition: background 0.2s, border-color 0.2s; | ||||
|       } | ||||
|  | ||||
|       .secondary-button:hover { | ||||
|         background: #363636; | ||||
|         border-color: #555; | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|  | ||||
|   private async triggerTask(taskName: string) { | ||||
|     try { | ||||
|       const modal = await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|         heading: `Run Task: ${taskName}`, | ||||
|         content: html` | ||||
|           <div> | ||||
|             <p>Do you want to trigger this task now?</p> | ||||
|           </div> | ||||
|         `, | ||||
|         menuOptions: [ | ||||
|           { | ||||
|             name: 'Run now', | ||||
|             action: async (modalArg: any) => { | ||||
|               await appstate.dataState.dispatchAction( | ||||
|                 appstate.taskActions.triggerTask, { taskName } | ||||
|               ); | ||||
|        | ||||
|       // Reload tasks and executions to show the new execution | ||||
|       await appstate.dataState.dispatchAction(appstate.taskActions.getTasks, {}); | ||||
|               plugins.deesCatalog.DeesToast.createAndShow({ | ||||
|                 message: `Task ${taskName} triggered`, | ||||
|                 type: 'success', | ||||
|               }); | ||||
|               await modalArg.destroy(); | ||||
|               // Refresh executions to reflect the new run quickly | ||||
|               await this.loadExecutionsWithFilter(); | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             name: 'Cancel', | ||||
|             action: async (modalArg: any) => modalArg.destroy() | ||||
|           } | ||||
|         ] | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to trigger task:', error); | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ | ||||
|         message: `Failed to trigger: ${error.message}`, | ||||
|         type: 'error', | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async cancelTaskFor(taskName: string) { | ||||
|     try { | ||||
|       const executions = (this.data.taskExecutions || []) | ||||
|         .filter(e => e.data.taskName === taskName && e.data.status === 'running') | ||||
|         .sort((a, b) => (b.data.startedAt || 0) - (a.data.startedAt || 0)); | ||||
|       const running = executions[0]; | ||||
|       if (!running) return; | ||||
|  | ||||
|       await appstate.dataState.dispatchAction( | ||||
|         appstate.taskActions.cancelTask, { executionId: running.id } | ||||
|       ); | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ | ||||
|         message: `Cancelled ${taskName}`, | ||||
|         type: 'success', | ||||
|       }); | ||||
|       await this.loadExecutionsWithFilter(); | ||||
|     } catch (err) { | ||||
|       console.error('Failed to cancel task:', err); | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ | ||||
|         message: `Cancel failed: ${err.message}`, | ||||
|         type: 'error', | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -323,6 +535,16 @@ export class CloudlyViewTasks extends DeesElement { | ||||
|     return `${(ms / 3600000).toFixed(1)}h`; | ||||
|   } | ||||
|  | ||||
|   private formatRelativeTime(ts?: number): string { | ||||
|     if (!ts) return '-'; | ||||
|     const diff = Date.now() - ts; | ||||
|     const abs = Math.abs(diff); | ||||
|     if (abs < 60_000) return `${Math.round(abs / 1000)}s ago`; | ||||
|     if (abs < 3_600_000) return `${Math.round(abs / 60_000)}m ago`; | ||||
|     if (abs < 86_400_000) return `${Math.round(abs / 3_600_000)}h ago`; | ||||
|     return `${Math.round(abs / 86_400_000)}d ago`; | ||||
|   } | ||||
|  | ||||
|   private getCategoryIcon(category: string): string { | ||||
|     switch (category) { | ||||
|       case 'maintenance': | ||||
| @@ -344,6 +566,43 @@ export class CloudlyViewTasks extends DeesElement { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private getCategoryHue(category: string): number { | ||||
|     switch (category) { | ||||
|       case 'maintenance': return 28;    // orange | ||||
|       case 'deployment': return 208;    // blue | ||||
|       case 'backup': return 122;        // green | ||||
|       case 'monitoring': return 280;    // purple | ||||
|       case 'cleanup': return 20;        // brownish | ||||
|       case 'system': return 200;        // steel | ||||
|       case 'security': return 0;        // red | ||||
|       default: return 210;              // default blue | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private getStatusColor(status?: string): string { | ||||
|     switch (status) { | ||||
|       case 'running': return '#2196f3'; | ||||
|       case 'completed': return '#4caf50'; | ||||
|       case 'failed': return '#f44336'; | ||||
|       case 'cancelled': return '#ff9800'; | ||||
|       default: return '#3a3a3a'; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private formatCronFriendly(cron?: string): string { | ||||
|     if (!cron) return ''; | ||||
|     const parts = cron.trim().split(/\s+/); | ||||
|     if (parts.length !== 5) return cron; // fallback | ||||
|     const [min, hour, dom, mon, dow] = parts; | ||||
|     if (min === '*/1' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'every minute'; | ||||
|     if (min.startsWith('*/') && hour === '*' && dom === '*' && mon === '*' && dow === '*') return `every ${min.replace('*/','')} min`; | ||||
|     if (min === '0' && hour.startsWith('*/') && dom === '*' && mon === '*' && dow === '*') return `every ${hour.replace('*/','')} hours`; | ||||
|     if (min === '0' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'hourly'; | ||||
|     if (min === '0' && hour === '0' && dom === '*' && mon === '*' && dow === '*') return 'daily'; | ||||
|     if (min === '0' && hour === '0' && dom === '1' && mon === '*' && dow === '*') return 'monthly'; | ||||
|     return cron; | ||||
|   } | ||||
|  | ||||
|   private renderTaskCard(task: any) { | ||||
|     const executions = this.data.taskExecutions || []; | ||||
|     const lastExecution = executions | ||||
| @@ -351,65 +610,129 @@ export class CloudlyViewTasks extends DeesElement { | ||||
|       .sort((a, b) => (b.data.startedAt || 0) - (a.data.startedAt || 0))[0]; | ||||
|      | ||||
|     const isRunning = lastExecution?.data.status === 'running'; | ||||
|     const executionsForTask = executions.filter(e => e.data.taskName === task.name); | ||||
|     const now = Date.now(); | ||||
|     const last24hCount = executionsForTask.filter(e => (e.data.startedAt || 0) > now - 86_400_000).length; | ||||
|     const completed = executionsForTask.filter(e => e.data.status === 'completed'); | ||||
|     const successRate = executionsForTask.length ? Math.round((completed.length * 100) / executionsForTask.length) : 0; | ||||
|     const avgDuration = completed.length ? Math.round(completed.reduce((acc, e) => acc + (e.data.duration || 0), 0) / completed.length) : undefined; | ||||
|     const lastLog = lastExecution?.data.logs && lastExecution.data.logs.length > 0 ? lastExecution.data.logs[lastExecution.data.logs.length - 1] : null; | ||||
|      | ||||
|     const status = lastExecution?.data.status as ('running'|'completed'|'failed'|'cancelled'|undefined); | ||||
|     const hue = this.getCategoryHue(task.category); | ||||
|     const subtitle = [ | ||||
|       task.category, | ||||
|       task.schedule ? `⏱ ${this.formatCronFriendly(task.schedule)}` : null, | ||||
|       isRunning | ||||
|         ? (lastExecution?.data.startedAt ? `Started ${this.formatRelativeTime(lastExecution.data.startedAt)}` : 'Running') | ||||
|         : (task.lastRun ? `Last ${this.formatRelativeTime(task.lastRun)}` : 'Never run') | ||||
|     ].filter(Boolean).join(' • '); | ||||
|  | ||||
|     return html` | ||||
|       <div class="task-card"> | ||||
|         <div class="task-header"> | ||||
|           <div class="task-name"> | ||||
|             <dees-icon .iconName=${this.getCategoryIcon(task.category)}></dees-icon> | ||||
|             ${task.name} | ||||
|         <div class="card-header"> | ||||
|           <div class="header-left"> | ||||
|             <dees-icon class="task-icon" .iconName=${this.getCategoryIcon(task.category)}></dees-icon> | ||||
|             <div> | ||||
|               <div class="task-name" title=${task.name}>${task.name}</div> | ||||
|               <div class="task-subtitle" title=${task.schedule || ''}>${subtitle}</div> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="header-right"> | ||||
|             ${lastExecution ? html`<span class="status-badge status-${lastExecution.data.status}">${lastExecution.data.status}</span>` : html`<span class="status-badge" style="background:#2e2e2e;color:#ddd;border:1px solid #3a3a3a;">idle</span>`} | ||||
|             ${isRunning ? html` | ||||
|               <dees-spinner style="--size: 18px"></dees-spinner> | ||||
|               <dees-button .text=${'Cancel'} .type=${'secondary'} @click=${() => this.cancelTaskFor(task.name)}></dees-button> | ||||
|             ` : html` | ||||
|               <dees-button .text=${'Run'} .type=${'primary'} .disabled=${!task.enabled} @click=${() => this.triggerTask(task.name)}></dees-button> | ||||
|             `} | ||||
|           </div> | ||||
|           <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> | ||||
|         ` : ''} | ||||
|         <div class="task-description" title=${task.description || ''}>${task.description}</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 class="metrics-grid"> | ||||
|             <div class="metric-item"> | ||||
|               <div class="label">Last Status</div> | ||||
|               <div class="value"> | ||||
|                 <span class="status-badge status-${lastExecution.data.status}">${lastExecution.data.status}</span> | ||||
|               </div> | ||||
|             ${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 class="metric-item"> | ||||
|               <div class="label">Avg Duration</div> | ||||
|               <div class="value">${avgDuration ? this.formatDuration(avgDuration) : '-'}</div> | ||||
|             </div> | ||||
|         ` : ''} | ||||
|             <div class="metric-item"> | ||||
|               <div class="label">24h Runs · Success</div> | ||||
|               <div class="value">${last24hCount} · ${successRate}%</div> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="lastline"> | ||||
|             ${lastLog ? html`<span class="dot ${lastLog.severity}"></span> ${lastLog.message}` : 'No recent logs'} | ||||
|           </div> | ||||
|           <div class="card-footer"> | ||||
|             <button class="link-button" @click=${() => this.openExecutionDetails(lastExecution)}>Details</button> | ||||
|             ${lastExecution.data.logs?.length ? html`<button class="link-button" @click=${() => this.openLogsModal(lastExecution)}>Logs</button>` : ''} | ||||
|           </div> | ||||
|         ` : html` | ||||
|           <div class="metrics-grid"> | ||||
|             <div class="metric-item"> | ||||
|               <div class="label">Last Status</div> | ||||
|               <div class="value">—</div> | ||||
|             </div> | ||||
|             <div class="metric-item"> | ||||
|               <div class="label">Avg Duration</div> | ||||
|               <div class="value">—</div> | ||||
|             </div> | ||||
|             <div class="metric-item"> | ||||
|               <div class="label">24h Runs · Success</div> | ||||
|               <div class="value">0 · 0%</div> | ||||
|             </div> | ||||
|           </div> | ||||
|         `} | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private async openExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) { | ||||
|     this.selectedExecution = execution; | ||||
|     // Scroll into view of the details section | ||||
|     requestAnimationFrame(() => { | ||||
|       this.shadowRoot?.querySelector('cloudly-sectionheading + .execution-details')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private async openLogsModal(execution: plugins.interfaces.data.ITaskExecution) { | ||||
|     await plugins.deesCatalog.DeesModal.createAndShow({ | ||||
|       heading: `Logs: ${execution.data.taskName}`, | ||||
|       content: html` | ||||
|         <div class="execution-logs"> | ||||
|           ${(execution.data.logs || []).map(log => html` | ||||
|             <div class="log-entry log-${log.severity}"> | ||||
|               <span>${this.formatDate(log.timestamp)}</span> - ${log.message} | ||||
|             </div> | ||||
|           `)} | ||||
|         </div> | ||||
|       `, | ||||
|       menuOptions: [ | ||||
|         { | ||||
|           name: 'Copy All', | ||||
|           action: async (modalArg: any) => { | ||||
|             try { | ||||
|               await navigator.clipboard.writeText((execution.data.logs || []) | ||||
|                 .map(l => `${new Date(l.timestamp).toISOString()} [${l.severity}] ${l.message}`).join('\n')); | ||||
|               plugins.deesCatalog.DeesToast.createAndShow({ message: 'Logs copied', type: 'success' }); | ||||
|             } catch (e) { | ||||
|               plugins.deesCatalog.DeesToast.createAndShow({ message: 'Copy failed', type: 'error' }); | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         { name: 'Close', action: async (modalArg: any) => modalArg.destroy() } | ||||
|       ] | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private renderExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) { | ||||
|     return html` | ||||
|       <div class="execution-details"> | ||||
| @@ -476,42 +799,59 @@ export class CloudlyViewTasks extends DeesElement { | ||||
|   } | ||||
|  | ||||
|   public render() { | ||||
|     const tasks = (this.data.tasks || []) as any[]; | ||||
|     const categories = Array.from(new Set(tasks.map(t => t.category))).sort(); | ||||
|     const filteredTasks = tasks | ||||
|       .filter(t => this.categoryFilter === 'all' || t.category === this.categoryFilter) | ||||
|       .filter(t => !this.searchQuery || t.name.toLowerCase().includes(this.searchQuery.toLowerCase()) || (t.description || '').toLowerCase().includes(this.searchQuery.toLowerCase())); | ||||
|  | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Tasks</cloudly-sectionheading> | ||||
|  | ||||
|       <div class="filter-bar"> | ||||
|         <button | ||||
|           class="filter-button ${this.filterStatus === 'all' ? 'active' : ''}" | ||||
|           @click=${() => { this.filterStatus = 'all'; this.loadExecutionsWithFilter(); }} | ||||
|       <dees-panel | ||||
|         .title=${'Task Library'} | ||||
|         .subtitle=${'Run maintenance, monitoring and system tasks'} | ||||
|         .variant=${'outline'} | ||||
|       > | ||||
|         <div class="toolbar"> | ||||
|           <div class="chipbar"> | ||||
|             <div class="chip ${this.categoryFilter === 'all' ? 'active' : ''}" | ||||
|               @click=${() => { this.categoryFilter = 'all'; }}> | ||||
|               All | ||||
|         </button> | ||||
|         <button | ||||
|           class="filter-button ${this.filterStatus === 'running' ? 'active' : ''}" | ||||
|           @click=${() => { this.filterStatus = 'running'; this.loadExecutionsWithFilter(); }} | ||||
|         > | ||||
|           Running | ||||
|         </button> | ||||
|         <button | ||||
|           class="filter-button ${this.filterStatus === 'completed' ? 'active' : ''}" | ||||
|           @click=${() => { this.filterStatus = 'completed'; this.loadExecutionsWithFilter(); }} | ||||
|         > | ||||
|           Completed | ||||
|         </button> | ||||
|         <button | ||||
|           class="filter-button ${this.filterStatus === 'failed' ? 'active' : ''}" | ||||
|           @click=${() => { this.filterStatus = 'failed'; this.loadExecutionsWithFilter(); }} | ||||
|         > | ||||
|           Failed | ||||
|             </div> | ||||
|             ${categories.map(cat => html` | ||||
|               <div class="chip ${this.categoryFilter === cat ? 'active' : ''}" | ||||
|                 @click=${() => { this.categoryFilter = cat; }}> | ||||
|                 ${cat} | ||||
|               </div> | ||||
|             `)} | ||||
|           </div> | ||||
|           <div class="spacer"></div> | ||||
|           <input class="search-input" placeholder="Search tasks" .value=${this.searchQuery} | ||||
|             @input=${(e: any) => { this.searchQuery = e.target.value; }} /> | ||||
|           <button class="secondary-button" @click=${async () => { | ||||
|             await this.loadExecutionsWithFilter(); | ||||
|           }}>Refresh</button> | ||||
|           <button class="secondary-button" @click=${() => { | ||||
|             this.autoRefresh = !this.autoRefresh; | ||||
|             this.autoRefresh ? this.startAutoRefresh() : this.stopAutoRefresh(); | ||||
|           }}> | ||||
|             ${this.autoRefresh ? 'Auto-Refresh: On' : 'Auto-Refresh: Off'} | ||||
|           </button> | ||||
|         </div> | ||||
|  | ||||
|         <div class="task-grid"> | ||||
|         ${(this.data.tasks || []).map(task => this.renderTaskCard(task))} | ||||
|           ${filteredTasks.map(task => this.renderTaskCard(task))} | ||||
|         </div> | ||||
|       </dees-panel> | ||||
|  | ||||
|       <cloudly-sectionheading>Execution History</cloudly-sectionheading> | ||||
|  | ||||
|       <dees-panel | ||||
|         .title=${'Recent Executions'} | ||||
|         .subtitle=${'History of task runs and their outcomes'} | ||||
|         .variant=${'outline'} | ||||
|       > | ||||
|         <dees-table | ||||
|           .heading1=${'Task Executions'} | ||||
|           .heading2=${'History of task runs and their outcomes'} | ||||
| @@ -526,7 +866,8 @@ export class CloudlyViewTasks extends DeesElement { | ||||
|               Logs: itemArg.data.logs?.length || 0, | ||||
|             }; | ||||
|           }} | ||||
|         .actionFunction=${async (itemArg) => [ | ||||
|           .actionFunction=${async (itemArg: plugins.interfaces.data.ITaskExecution) => { | ||||
|             const actions: any[] = [ | ||||
|               { | ||||
|                 name: 'View Details', | ||||
|                 iconName: 'lucide:Eye', | ||||
| @@ -534,9 +875,23 @@ export class CloudlyViewTasks extends DeesElement { | ||||
|                 actionFunc: async () => { | ||||
|                   this.selectedExecution = itemArg; | ||||
|                 }, | ||||
|               } | ||||
|             ]; | ||||
|             if (itemArg.data.status === 'running') { | ||||
|               actions.push({ | ||||
|                 name: 'Cancel', | ||||
|                 iconName: 'lucide:SquareX', | ||||
|                 type: ['inRow'], | ||||
|                 actionFunc: async () => { | ||||
|                   await appstate.dataState.dispatchAction(appstate.taskActions.cancelTask, { executionId: itemArg.id }); | ||||
|                   await this.loadExecutionsWithFilter(); | ||||
|                 }, | ||||
|         ]} | ||||
|               }); | ||||
|             } | ||||
|             return actions; | ||||
|           }} | ||||
|         ></dees-table> | ||||
|       </dees-panel> | ||||
|  | ||||
|       ${this.selectedExecution ? html` | ||||
|         <cloudly-sectionheading>Execution Details</cloudly-sectionheading> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user