Files
cloudly/ts_web/elements/cloudly-view-tasks.ts

547 lines
14 KiB
TypeScript
Raw Normal View History

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 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);
// Load initial data (non-blocking)
this.loadInitialData();
}
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);
}
}
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;
}
`,
];
private async triggerTask(taskName: string) {
try {
await appstate.dataState.dispatchAction(
appstate.taskActions.triggerTask, { taskName }
);
// Reload tasks and executions to show the new execution
await appstate.dataState.dispatchAction(appstate.taskActions.getTasks, {});
await this.loadExecutionsWithFilter();
} catch (error) {
console.error('Failed to trigger task:', 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 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 executions = this.data.taskExecutions || [];
const lastExecution = 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.loadExecutionsWithFilter(); }}
>
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
</button>
</div>
<div class="task-grid">
${(this.data.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.data.taskExecutions || []}
.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)}
` : ''}
`;
}
}