f40ef6b7c0
Align Cloudly with the current typedserver, smartconfig, smartstate, and Docker tooling releases so builds and Docker output stay compatible with the upgraded stack.
206 lines
8.6 KiB
TypeScript
206 lines
8.6 KiB
TypeScript
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 }) accessor task: any = undefined;
|
|
@property({ type: Array }) accessor executions: any[] = [];
|
|
@property({ type: Object }) accessor canceling: Record<string, boolean> = {};
|
|
|
|
// Callbacks provided by parent view
|
|
@property({ attribute: false }) accessor onRun: ((taskName: string) => void) | undefined = undefined;
|
|
@property({ attribute: false }) accessor onCancel: ((taskName: string) => void) | undefined = undefined;
|
|
@property({ attribute: false }) accessor onOpenDetails: ((execution: any) => void) | undefined = undefined;
|
|
@property({ attribute: false }) accessor onOpenLogs: ((execution: any) => void) | undefined = undefined;
|
|
|
|
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`
|
|
<div class="task-panel">
|
|
<div class="panel-header">
|
|
<div class="header-left">
|
|
<dees-icon class="task-icon" .icon=${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=${this.canceling[lastExecution!.id] ? 'Cancelling…' : 'Cancel'}
|
|
.type=${'secondary'}
|
|
.disabled=${!!this.canceling[lastExecution!.id]}
|
|
@click=${() => this.onCancel?.(task.name)}
|
|
></dees-button>
|
|
` : html`
|
|
<dees-button .text=${'Run'} .type=${'primary'} .disabled=${!task.enabled} @click=${() => this.onRun?.(task.name)}></dees-button>
|
|
`}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="task-description" title=${task.description || ''}>${task.description}</div>
|
|
|
|
${lastExecution ? html`
|
|
<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>
|
|
</div>
|
|
<div class="metric-item">
|
|
<div class="label">Avg Duration</div>
|
|
<div class="value">${avgDuration ? 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="panel-footer">
|
|
<button class="link-button" @click=${() => this.onOpenDetails?.(lastExecution)}>Details</button>
|
|
${lastExecution.data.logs?.length ? html`<button class="link-button" @click=${() => this.onOpenLogs?.(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>
|
|
`;
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'cloudly-task-panel': CloudlyTaskPanel;
|
|
}
|
|
}
|