import * as shared from '../../shared/index.js'; import * as plugins from '../../../plugins.js'; import { DeesElement, customElement, html, state, css, cssManager, } from '@design.estate/dees-element'; import * as appstate from '../../../appstate.js'; import './parts/cloudly-task-panel.js'; import './parts/cloudly-execution-details.js'; import { formatCronFriendly, formatDate, formatDuration } from './utils.js'; @customElement('cloudly-view-tasks') export class CloudlyViewTasks extends DeesElement { @state() private data: appstate.IDataState = {} as any; @state() private selectedExecution: plugins.interfaces.data.ITaskExecution | null = null; @state() private loading = false; @state() private filterStatus: string = 'all'; @state() private searchQuery: string = ''; @state() private categoryFilter: string = 'all'; @state() private autoRefresh: boolean = true; private _refreshHandle: any = null; @state() private canceling: Record = {}; constructor() { super(); const subscription = appstate.dataState .select((stateArg) => stateArg) .subscribe((dataArg) => { this.data = dataArg; }); this.rxSubscriptions.push(subscription); // Load initial data (non-blocking) this.loadInitialData(); // Start periodic refresh (lightweight; executions only by default) this.startAutoRefresh(); } private async loadInitialData() { try { await appstate.dataState.dispatchAction(appstate.taskActions.getTasks, {}); await appstate.dataState.dispatchAction(appstate.taskActions.getTaskExecutions, {}); } catch (error) { console.error('Failed to load initial task data:', error); } } 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 { 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-list { display: flex; flex-direction: column; gap: 16px; margin-bottom: 32px; } .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; } /* Shared badge styles used within the table content */ .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; } .execution-logs { background: #0a0a0a; border: 1px solid #333; border-radius: 6px; 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); } `, ]; private async triggerTask(taskName: string) { try { const modal = await plugins.deesCatalog.DeesModal.createAndShow({ heading: `Run Task: ${taskName}`, content: html`

Do you want to trigger this task now?

`, menuOptions: [ { name: 'Run now', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.taskActions.triggerTask, { taskName }); plugins.deesCatalog.DeesToast.createAndShow({ message: `Task ${taskName} triggered`, type: 'success' }); await modalArg.destroy(); 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: any) => e.data.taskName === taskName && e.data.status === 'running') .sort((a: any, b: any) => (b.data.startedAt || 0) - (a.data.startedAt || 0)); const running = executions[0]; if (!running) return; this.canceling = { ...this.canceling, [running.id]: true }; try { await appstate.dataState.dispatchAction(appstate.taskActions.cancelTask, { executionId: running.id }); plugins.deesCatalog.DeesToast.createAndShow({ message: `Cancelled ${taskName}`, type: 'success' }); } finally { this.canceling = { ...this.canceling, [running.id]: false }; await this.loadExecutionsWithFilter(); } } catch (err) { console.error('Failed to cancel task:', err); plugins.deesCatalog.DeesToast.createAndShow({ message: `Cancel failed: ${err.message}`, type: 'error' }); } } private async loadExecutionsWithFilter() { try { const filter: any = {}; if (this.filterStatus !== 'all') { filter.status = this.filterStatus; } await appstate.dataState.dispatchAction(appstate.taskActions.getTaskExecutions, { filter }); } catch (error) { console.error('Failed to load executions:', error); } } private async openExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) { this.selectedExecution = execution; requestAnimationFrame(() => { this.shadowRoot?.querySelector('cloudly-sectionheading + cloudly-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`
${(execution.data.logs || []).map((log: any) => html`
${formatDate(log.timestamp)} - ${log.message}
`)}
`, menuOptions: [ { name: 'Copy All', action: async (modalArg: any) => { try { await navigator.clipboard.writeText((execution.data.logs || []) .map((l: any) => `${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() } ] }); } 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` Tasks
{ this.categoryFilter = 'all'; }}> All
${categories.map(cat => html`
{ this.categoryFilter = cat; }}> ${cat}
`)}
{ this.searchQuery = e.target.value; }} />
${filteredTasks.map(task => html` this.triggerTask(name)} .onCancel=${(name: string) => this.cancelTaskFor(name)} .onOpenDetails=${(exec: any) => this.openExecutionDetails(exec)} .onOpenLogs=${(exec: any) => this.openLogsModal(exec)} > `)}
Execution History { return { Task: itemArg.data.taskName, Status: html`${itemArg.data.status}`, 'Started At': formatDate(itemArg.data.startedAt), Duration: itemArg.data.duration ? formatDuration(itemArg.data.duration) : '-', 'Triggered By': itemArg.data.triggeredBy, Logs: itemArg.data.logs?.length || 0, } as any; }} .actionFunction=${async (itemArg: plugins.interfaces.data.ITaskExecution) => { const actions: any[] = [ { name: 'View Details', iconName: 'lucide:Eye', type: ['inRow'], 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; }} > ${this.selectedExecution ? html` Execution Details ` : ''} `; } } declare global { interface HTMLElementTagNameMap { 'cloudly-view-tasks': CloudlyViewTasks; } }