import { DeesElement, customElement, html, css, cssManager, property } from '@design.estate/dees-element'; import { formatCronFriendly, formatDuration, formatRelativeTime, getCategoryHue, getCategoryIcon } from '../utils.js'; @customElement('cloudly-task-panel') export class CloudlyTaskPanel extends DeesElement { @property({ type: Object }) task: any; @property({ type: Array }) executions: any[] = []; @property({ type: Object }) canceling: Record = {}; // Callbacks provided by parent view @property({ attribute: false }) onRun?: (taskName: string) => void; @property({ attribute: false }) onCancel?: (taskName: string) => void; @property({ attribute: false }) onOpenDetails?: (execution: any) => void; @property({ attribute: false }) onOpenLogs?: (execution: any) => void; public static styles = [ cssManager.defaultStyles, css` .task-panel { background: #131313; border: 1px solid #2a2a2a; border-radius: 10px; padding: 16px; transition: border-color 0.2s, background 0.2s; } .task-panel:hover { border-color: #3a3a3a; } .panel-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 12px; } .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; font-size: 28px; } .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: #b5b5b5; font-size: 0.95em; margin-bottom: 12px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .metrics-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 8px; width: 100%; max-width: 760px; } .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; } .panel-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; } .status-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; } .status-running { background: #2196f3; color: white; } .status-completed { background: #4caf50; color: white; } .status-failed { background: #f44336; color: white; } .status-cancelled { background: #ff9800; color: white; } `, ]; private computeData() { const task = this.task || {}; const executions = this.executions || []; const lastExecution = executions .filter((e: any) => e.data.taskName === task.name) .sort((a: any, b: any) => (b.data.startedAt || 0) - (a.data.startedAt || 0))[0]; const isRunning = lastExecution?.data.status === 'running'; const executionsForTask = executions.filter((e: any) => e.data.taskName === task.name); const now = Date.now(); const last24hCount = executionsForTask.filter((e: any) => (e.data.startedAt || 0) > now - 86_400_000).length; const completed = executionsForTask.filter((e: any) => 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: number, e: any) => 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 subtitle = [ task.category, task.schedule ? `⏱ ${formatCronFriendly(task.schedule)}` : null, isRunning ? (lastExecution?.data.startedAt ? `Started ${formatRelativeTime(lastExecution.data.startedAt)}` : 'Running') : (task.lastRun ? `Last ${formatRelativeTime(task.lastRun)}` : 'Never run') ].filter(Boolean).join(' • '); return { lastExecution, isRunning, last24hCount, successRate, avgDuration, lastLog, subtitle }; } public render() { const task = this.task; const { lastExecution, isRunning, last24hCount, successRate, avgDuration, lastLog, subtitle } = this.computeData(); return html`
${task.name}
${subtitle}
${lastExecution ? html`${lastExecution.data.status}` : html`idle`} ${isRunning ? html` this.onCancel?.(task.name)} > ` : html` this.onRun?.(task.name)}> `}
${task.description}
${lastExecution ? html`
Last Status
${lastExecution.data.status}
Avg Duration
${avgDuration ? formatDuration(avgDuration) : '-'}
24h Runs · Success
${last24hCount} · ${successRate}%
${lastLog ? html` ${lastLog.message}` : 'No recent logs'}
` : html`
Last Status
Avg Duration
24h Runs · Success
0 · 0%
`}
`; } } declare global { interface HTMLElementTagNameMap { 'cloudly-task-panel': CloudlyTaskPanel; } }