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() |   @state() | ||||||
|   private filterStatus: string = 'all'; |   private filterStatus: string = 'all'; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   private searchQuery: string = ''; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   private categoryFilter: string = 'all'; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   private autoRefresh: boolean = true; | ||||||
|  |  | ||||||
|  |   private _refreshHandle: any = null; | ||||||
|  |  | ||||||
|   constructor() { |   constructor() { | ||||||
|     super(); |     super(); | ||||||
|     const subscription = appstate.dataState |     const subscription = appstate.dataState | ||||||
| @@ -38,6 +49,9 @@ export class CloudlyViewTasks extends DeesElement { | |||||||
|      |      | ||||||
|     // Load initial data (non-blocking) |     // Load initial data (non-blocking) | ||||||
|     this.loadInitialData(); |     this.loadInitialData(); | ||||||
|  |  | ||||||
|  |     // Start periodic refresh (lightweight; executions only by default) | ||||||
|  |     this.startAutoRefresh(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async loadInitialData() { |   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 = [ |   public static styles = [ | ||||||
|     cssManager.defaultStyles, |     cssManager.defaultStyles, | ||||||
|     shared.viewHostCss, |     shared.viewHostCss, | ||||||
|     css` |     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 { |       .task-grid { | ||||||
|         display: grid; |         display: grid; | ||||||
|         grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); |         grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); | ||||||
|         gap: 16px; |         gap: 16px; | ||||||
|         margin-bottom: 32px; |         margin-bottom: 32px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .task-card { |       .task-card { | ||||||
|         background: #1a1a1a; |         background: #131313; | ||||||
|         border: 1px solid #333; |         border: 1px solid #2a2a2a; | ||||||
|         border-radius: 8px; |         border-radius: 10px; | ||||||
|         padding: 16px; |         padding: 16px; | ||||||
|         cursor: pointer; |         transition: border-color 0.2s, background 0.2s; | ||||||
|         transition: all 0.2s; |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .task-card:hover { |       .task-card:hover { border-color: #3a3a3a; } | ||||||
|         background: #222; |  | ||||||
|         border-color: #555; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .task-header { |       .card-header { | ||||||
|         display: flex; |         display: flex; | ||||||
|         justify-content: space-between; |         justify-content: space-between; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|  |         gap: 12px; | ||||||
|         margin-bottom: 12px; |         margin-bottom: 12px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .task-name { |       .header-left { display: flex; align-items: center; gap: 12px; min-width: 0; } | ||||||
|         font-size: 1.1em; |       .header-right { display: flex; align-items: center; gap: 8px; } | ||||||
|         font-weight: 600; |       .task-icon { color: #cfcfcf; } | ||||||
|         color: #fff; |       .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 { |       .task-description { | ||||||
|         color: #999; |         color: #b5b5b5; | ||||||
|         font-size: 0.9em; |         font-size: 0.95em; | ||||||
|         margin-bottom: 12px; |         margin-bottom: 12px; | ||||||
|  |         display: -webkit-box; | ||||||
|  |         -webkit-line-clamp: 2; | ||||||
|  |         -webkit-box-orient: vertical; | ||||||
|  |         overflow: hidden; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .task-meta { |       .task-meta { | ||||||
| @@ -166,7 +249,7 @@ export class CloudlyViewTasks extends DeesElement { | |||||||
|         background: #2196f3; |         background: #2196f3; | ||||||
|         color: white; |         color: white; | ||||||
|         border: none; |         border: none; | ||||||
|         border-radius: 4px; |         border-radius: 6px; | ||||||
|         cursor: pointer; |         cursor: pointer; | ||||||
|         font-size: 0.9em; |         font-size: 0.9em; | ||||||
|         transition: background 0.2s; |         transition: background 0.2s; | ||||||
| @@ -196,7 +279,7 @@ export class CloudlyViewTasks extends DeesElement { | |||||||
|       .execution-logs { |       .execution-logs { | ||||||
|         background: #0a0a0a; |         background: #0a0a0a; | ||||||
|         border: 1px solid #333; |         border: 1px solid #333; | ||||||
|         border-radius: 4px; |         border-radius: 6px; | ||||||
|         padding: 16px; |         padding: 16px; | ||||||
|         margin-top: 16px; |         margin-top: 16px; | ||||||
|         max-height: 400px; |         max-height: 400px; | ||||||
| @@ -279,21 +362,150 @@ export class CloudlyViewTasks extends DeesElement { | |||||||
|         font-size: 1.1em; |         font-size: 1.1em; | ||||||
|         font-weight: 600; |         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) { |   private async triggerTask(taskName: string) { | ||||||
|     try { |     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( |               await appstate.dataState.dispatchAction( | ||||||
|                 appstate.taskActions.triggerTask, { taskName } |                 appstate.taskActions.triggerTask, { taskName } | ||||||
|               ); |               ); | ||||||
|        |               plugins.deesCatalog.DeesToast.createAndShow({ | ||||||
|       // Reload tasks and executions to show the new execution |                 message: `Task ${taskName} triggered`, | ||||||
|       await appstate.dataState.dispatchAction(appstate.taskActions.getTasks, {}); |                 type: 'success', | ||||||
|  |               }); | ||||||
|  |               await modalArg.destroy(); | ||||||
|  |               // Refresh executions to reflect the new run quickly | ||||||
|               await this.loadExecutionsWithFilter(); |               await this.loadExecutionsWithFilter(); | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             name: 'Cancel', | ||||||
|  |             action: async (modalArg: any) => modalArg.destroy() | ||||||
|  |           } | ||||||
|  |         ] | ||||||
|  |       }); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.error('Failed to trigger task:', 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`; |     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 { |   private getCategoryIcon(category: string): string { | ||||||
|     switch (category) { |     switch (category) { | ||||||
|       case 'maintenance': |       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) { |   private renderTaskCard(task: any) { | ||||||
|     const executions = this.data.taskExecutions || []; |     const executions = this.data.taskExecutions || []; | ||||||
|     const lastExecution = executions |     const lastExecution = executions | ||||||
| @@ -351,65 +610,129 @@ export class CloudlyViewTasks extends DeesElement { | |||||||
|       .sort((a, b) => (b.data.startedAt || 0) - (a.data.startedAt || 0))[0]; |       .sort((a, b) => (b.data.startedAt || 0) - (a.data.startedAt || 0))[0]; | ||||||
|      |      | ||||||
|     const isRunning = lastExecution?.data.status === 'running'; |     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` |     return html` | ||||||
|       <div class="task-card"> |       <div class="task-card"> | ||||||
|         <div class="task-header"> |         <div class="card-header"> | ||||||
|           <div class="task-name"> |           <div class="header-left"> | ||||||
|             <dees-icon .iconName=${this.getCategoryIcon(task.category)}></dees-icon> |             <dees-icon class="task-icon" .iconName=${this.getCategoryIcon(task.category)}></dees-icon> | ||||||
|             ${task.name} |             <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> |           </div> | ||||||
|           <button |  | ||||||
|             class="trigger-button" |  | ||||||
|             ?disabled=${isRunning || !task.enabled} |  | ||||||
|             @click=${() => this.triggerTask(task.name)} |  | ||||||
|           > |  | ||||||
|             ${isRunning ? 'Running...' : 'Run'} |  | ||||||
|           </button> |  | ||||||
|         </div> |         </div> | ||||||
|          |          | ||||||
|         <div class="task-description">${task.description}</div> |         <div class="task-description" title=${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` |         ${lastExecution ? html` | ||||||
|           <div class="metrics"> |           <div class="metrics-grid"> | ||||||
|             <div class="metric"> |             <div class="metric-item"> | ||||||
|               <span class="metric-label">Status</span> |               <div class="label">Last Status</div> | ||||||
|               <span class="metric-value"> |               <div class="value"> | ||||||
|                 <span class="status-badge status-${lastExecution.data.status}"> |                 <span class="status-badge status-${lastExecution.data.status}">${lastExecution.data.status}</span> | ||||||
|                   ${lastExecution.data.status} |  | ||||||
|                 </span> |  | ||||||
|               </span> |  | ||||||
|               </div> |               </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 class="metric-item"> | ||||||
|  |               <div class="label">Avg Duration</div> | ||||||
|  |               <div class="value">${avgDuration ? this.formatDuration(avgDuration) : '-'}</div> | ||||||
|             </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> |       </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) { |   private renderExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) { | ||||||
|     return html` |     return html` | ||||||
|       <div class="execution-details"> |       <div class="execution-details"> | ||||||
| @@ -476,42 +799,59 @@ export class CloudlyViewTasks extends DeesElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   public render() { |   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` |     return html` | ||||||
|       <cloudly-sectionheading>Tasks</cloudly-sectionheading> |       <cloudly-sectionheading>Tasks</cloudly-sectionheading> | ||||||
|  |  | ||||||
|       <div class="filter-bar"> |       <dees-panel | ||||||
|         <button |         .title=${'Task Library'} | ||||||
|           class="filter-button ${this.filterStatus === 'all' ? 'active' : ''}" |         .subtitle=${'Run maintenance, monitoring and system tasks'} | ||||||
|           @click=${() => { this.filterStatus = 'all'; this.loadExecutionsWithFilter(); }} |         .variant=${'outline'} | ||||||
|       > |       > | ||||||
|  |         <div class="toolbar"> | ||||||
|  |           <div class="chipbar"> | ||||||
|  |             <div class="chip ${this.categoryFilter === 'all' ? 'active' : ''}" | ||||||
|  |               @click=${() => { this.categoryFilter = 'all'; }}> | ||||||
|               All |               All | ||||||
|         </button> |             </div> | ||||||
|         <button |             ${categories.map(cat => html` | ||||||
|           class="filter-button ${this.filterStatus === 'running' ? 'active' : ''}" |               <div class="chip ${this.categoryFilter === cat ? 'active' : ''}" | ||||||
|           @click=${() => { this.filterStatus = 'running'; this.loadExecutionsWithFilter(); }} |                 @click=${() => { this.categoryFilter = cat; }}> | ||||||
|         > |                 ${cat} | ||||||
|           Running |               </div> | ||||||
|         </button> |             `)} | ||||||
|         <button |           </div> | ||||||
|           class="filter-button ${this.filterStatus === 'completed' ? 'active' : ''}" |           <div class="spacer"></div> | ||||||
|           @click=${() => { this.filterStatus = 'completed'; this.loadExecutionsWithFilter(); }} |           <input class="search-input" placeholder="Search tasks" .value=${this.searchQuery} | ||||||
|         > |             @input=${(e: any) => { this.searchQuery = e.target.value; }} /> | ||||||
|           Completed |           <button class="secondary-button" @click=${async () => { | ||||||
|         </button> |             await this.loadExecutionsWithFilter(); | ||||||
|         <button |           }}>Refresh</button> | ||||||
|           class="filter-button ${this.filterStatus === 'failed' ? 'active' : ''}" |           <button class="secondary-button" @click=${() => { | ||||||
|           @click=${() => { this.filterStatus = 'failed'; this.loadExecutionsWithFilter(); }} |             this.autoRefresh = !this.autoRefresh; | ||||||
|         > |             this.autoRefresh ? this.startAutoRefresh() : this.stopAutoRefresh(); | ||||||
|           Failed |           }}> | ||||||
|  |             ${this.autoRefresh ? 'Auto-Refresh: On' : 'Auto-Refresh: Off'} | ||||||
|           </button> |           </button> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <div class="task-grid"> |         <div class="task-grid"> | ||||||
|         ${(this.data.tasks || []).map(task => this.renderTaskCard(task))} |           ${filteredTasks.map(task => this.renderTaskCard(task))} | ||||||
|         </div> |         </div> | ||||||
|  |       </dees-panel> | ||||||
|  |  | ||||||
|       <cloudly-sectionheading>Execution History</cloudly-sectionheading> |       <cloudly-sectionheading>Execution History</cloudly-sectionheading> | ||||||
|  |  | ||||||
|  |       <dees-panel | ||||||
|  |         .title=${'Recent Executions'} | ||||||
|  |         .subtitle=${'History of task runs and their outcomes'} | ||||||
|  |         .variant=${'outline'} | ||||||
|  |       > | ||||||
|         <dees-table |         <dees-table | ||||||
|           .heading1=${'Task Executions'} |           .heading1=${'Task Executions'} | ||||||
|           .heading2=${'History of task runs and their outcomes'} |           .heading2=${'History of task runs and their outcomes'} | ||||||
| @@ -526,7 +866,8 @@ export class CloudlyViewTasks extends DeesElement { | |||||||
|               Logs: itemArg.data.logs?.length || 0, |               Logs: itemArg.data.logs?.length || 0, | ||||||
|             }; |             }; | ||||||
|           }} |           }} | ||||||
|         .actionFunction=${async (itemArg) => [ |           .actionFunction=${async (itemArg: plugins.interfaces.data.ITaskExecution) => { | ||||||
|  |             const actions: any[] = [ | ||||||
|               { |               { | ||||||
|                 name: 'View Details', |                 name: 'View Details', | ||||||
|                 iconName: 'lucide:Eye', |                 iconName: 'lucide:Eye', | ||||||
| @@ -534,9 +875,23 @@ export class CloudlyViewTasks extends DeesElement { | |||||||
|                 actionFunc: async () => { |                 actionFunc: async () => { | ||||||
|                   this.selectedExecution = itemArg; |                   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-table> | ||||||
|  |       </dees-panel> | ||||||
|  |  | ||||||
|       ${this.selectedExecution ? html` |       ${this.selectedExecution ? html` | ||||||
|         <cloudly-sectionheading>Execution Details</cloudly-sectionheading> |         <cloudly-sectionheading>Execution Details</cloudly-sectionheading> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user