541 lines
15 KiB
TypeScript
541 lines
15 KiB
TypeScript
import { DeesElement, customElement, html, css, property, state, cssManager } from '@design.estate/dees-element';
|
|
import type { TaskManager, ITaskMetadata, IScheduledTaskInfo } from '../ts/index.js';
|
|
|
|
/**
|
|
* A web component that displays TaskManager tasks with progress visualization
|
|
*/
|
|
@customElement('taskbuffer-dashboard')
|
|
export class TaskbufferDashboard extends DeesElement {
|
|
// Properties
|
|
@property({ type: Object })
|
|
public taskManager: TaskManager | null = null;
|
|
|
|
@property({ type: Number })
|
|
public refreshInterval: number = 1000; // milliseconds
|
|
|
|
// Internal state
|
|
@state()
|
|
private tasks: ITaskMetadata[] = [];
|
|
|
|
@state()
|
|
private scheduledTasks: IScheduledTaskInfo[] = [];
|
|
|
|
@state()
|
|
private isRunning: boolean = false;
|
|
|
|
private refreshTimer: any;
|
|
|
|
// Styles
|
|
static styles = [
|
|
cssManager.defaultStyles,
|
|
css`
|
|
:host {
|
|
display: block;
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
}
|
|
|
|
.dashboard-container {
|
|
padding: 24px;
|
|
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.dashboard-header {
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.dashboard-title {
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
|
margin-bottom: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.status-indicator {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background: ${cssManager.bdTheme('#22c55e', '#22c55e')};
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
.status-indicator.inactive {
|
|
background: ${cssManager.bdTheme('#94a3b8', '#475569')};
|
|
animation: none;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% {
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
opacity: 0.5;
|
|
}
|
|
}
|
|
|
|
.dashboard-subtitle {
|
|
font-size: 14px;
|
|
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
|
}
|
|
|
|
.tasks-section {
|
|
margin-bottom: 48px;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: ${cssManager.bdTheme('#18181b', '#e4e4e7')};
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.tasks-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.task-card {
|
|
background: ${cssManager.bdTheme('#f8fafc', '#18181b')};
|
|
border: 1px solid ${cssManager.bdTheme('#e2e8f0', '#27272a')};
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.task-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
|
|
}
|
|
|
|
.task-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.task-name {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: ${cssManager.bdTheme('#0f172a', '#f1f5f9')};
|
|
}
|
|
|
|
.task-status {
|
|
display: inline-block;
|
|
padding: 4px 8px;
|
|
border-radius: 6px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.task-status.idle {
|
|
background: ${cssManager.bdTheme('#f1f5f9', '#27272a')};
|
|
color: ${cssManager.bdTheme('#64748b', '#94a3b8')};
|
|
}
|
|
|
|
.task-status.running {
|
|
background: ${cssManager.bdTheme('#dbeafe', '#1e3a8a')};
|
|
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
|
|
}
|
|
|
|
.task-status.completed {
|
|
background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
|
|
color: ${cssManager.bdTheme('#15803d', '#86efac')};
|
|
}
|
|
|
|
.task-status.failed {
|
|
background: ${cssManager.bdTheme('#fee2e2', '#7f1d1d')};
|
|
color: ${cssManager.bdTheme('#dc2626', '#fca5a5')};
|
|
}
|
|
|
|
.task-info {
|
|
display: flex;
|
|
gap: 16px;
|
|
margin-bottom: 16px;
|
|
font-size: 13px;
|
|
color: ${cssManager.bdTheme('#64748b', '#94a3b8')};
|
|
}
|
|
|
|
.task-info-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.task-info-icon {
|
|
width: 14px;
|
|
height: 14px;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.progress-container {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.progress-label {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 8px;
|
|
font-size: 13px;
|
|
color: ${cssManager.bdTheme('#475569', '#cbd5e1')};
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 8px;
|
|
background: ${cssManager.bdTheme('#e2e8f0', '#27272a')};
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #3b82f6, #6366f1);
|
|
border-radius: 4px;
|
|
transition: width 0.3s ease;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-fill::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: linear-gradient(
|
|
90deg,
|
|
transparent,
|
|
rgba(255, 255, 255, 0.3),
|
|
transparent
|
|
);
|
|
animation: shimmer 2s infinite;
|
|
}
|
|
|
|
@keyframes shimmer {
|
|
0% {
|
|
transform: translateX(-100%);
|
|
}
|
|
100% {
|
|
transform: translateX(100%);
|
|
}
|
|
}
|
|
|
|
.steps-container {
|
|
border-top: 1px solid ${cssManager.bdTheme('#e2e8f0', '#27272a')};
|
|
padding-top: 12px;
|
|
}
|
|
|
|
.steps-title {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: ${cssManager.bdTheme('#64748b', '#94a3b8')};
|
|
margin-bottom: 8px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.step-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 0;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.step-indicator {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.step-indicator.pending {
|
|
background: ${cssManager.bdTheme('#f1f5f9', '#27272a')};
|
|
color: ${cssManager.bdTheme('#94a3b8', '#64748b')};
|
|
border: 2px solid ${cssManager.bdTheme('#cbd5e1', '#3f3f46')};
|
|
}
|
|
|
|
.step-indicator.active {
|
|
background: linear-gradient(135deg, #3b82f6, #6366f1);
|
|
color: white;
|
|
animation: pulse 1.5s infinite;
|
|
}
|
|
|
|
.step-indicator.completed {
|
|
background: ${cssManager.bdTheme('#22c55e', '#22c55e')};
|
|
color: white;
|
|
}
|
|
|
|
.step-details {
|
|
flex: 1;
|
|
}
|
|
|
|
.step-name {
|
|
font-weight: 500;
|
|
color: ${cssManager.bdTheme('#1e293b', '#e2e8f0')};
|
|
}
|
|
|
|
.step-description {
|
|
font-size: 11px;
|
|
color: ${cssManager.bdTheme('#64748b', '#94a3b8')};
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.step-percentage {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: ${cssManager.bdTheme('#94a3b8', '#64748b')};
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 48px;
|
|
color: ${cssManager.bdTheme('#94a3b8', '#64748b')};
|
|
}
|
|
|
|
.empty-state-icon {
|
|
width: 64px;
|
|
height: 64px;
|
|
margin: 0 auto 16px;
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.empty-state-text {
|
|
font-size: 16px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.empty-state-subtext {
|
|
font-size: 14px;
|
|
color: ${cssManager.bdTheme('#cbd5e1', '#475569')};
|
|
}
|
|
|
|
.scheduled-section {
|
|
margin-top: 32px;
|
|
}
|
|
|
|
.schedule-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-top: 8px;
|
|
font-size: 12px;
|
|
color: ${cssManager.bdTheme('#64748b', '#94a3b8')};
|
|
}
|
|
|
|
.schedule-icon {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
`,
|
|
];
|
|
|
|
// Lifecycle
|
|
async connectedCallback() {
|
|
await super.connectedCallback();
|
|
this.startRefreshing();
|
|
}
|
|
|
|
async disconnectedCallback() {
|
|
await super.disconnectedCallback();
|
|
this.stopRefreshing();
|
|
}
|
|
|
|
// Methods
|
|
private startRefreshing() {
|
|
if (this.refreshTimer) {
|
|
clearInterval(this.refreshTimer);
|
|
}
|
|
|
|
this.updateData();
|
|
this.refreshTimer = setInterval(() => {
|
|
this.updateData();
|
|
}, this.refreshInterval);
|
|
this.isRunning = true;
|
|
}
|
|
|
|
private stopRefreshing() {
|
|
if (this.refreshTimer) {
|
|
clearInterval(this.refreshTimer);
|
|
this.refreshTimer = null;
|
|
}
|
|
this.isRunning = false;
|
|
}
|
|
|
|
private updateData() {
|
|
if (!this.taskManager) {
|
|
this.tasks = [];
|
|
this.scheduledTasks = [];
|
|
return;
|
|
}
|
|
|
|
this.tasks = this.taskManager.getAllTasksMetadata();
|
|
this.scheduledTasks = this.taskManager.getScheduledTasks();
|
|
}
|
|
|
|
private formatNextRun(date: Date): string {
|
|
const now = new Date();
|
|
const diff = date.getTime() - now.getTime();
|
|
|
|
if (diff < 0) return 'Past due';
|
|
if (diff < 60000) return 'Less than a minute';
|
|
if (diff < 3600000) return `${Math.floor(diff / 60000)} minutes`;
|
|
if (diff < 86400000) return `${Math.floor(diff / 3600000)} hours`;
|
|
return `${Math.floor(diff / 86400000)} days`;
|
|
}
|
|
|
|
private formatDuration(ms?: number): string {
|
|
if (!ms) return '-';
|
|
if (ms < 1000) return `${ms}ms`;
|
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
return `${(ms / 60000).toFixed(1)}m`;
|
|
}
|
|
|
|
// Render
|
|
render() {
|
|
return html`
|
|
<div class="dashboard-container">
|
|
<div class="dashboard-header">
|
|
<div class="dashboard-title">
|
|
<span>TaskBuffer Dashboard</span>
|
|
<span class="status-indicator ${this.isRunning ? '' : 'inactive'}"></span>
|
|
</div>
|
|
<div class="dashboard-subtitle">
|
|
${this.tasks.length} task${this.tasks.length !== 1 ? 's' : ''} registered
|
|
${this.scheduledTasks.length > 0 ? ` • ${this.scheduledTasks.length} scheduled` : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tasks-section">
|
|
<h2 class="section-title">Active Tasks</h2>
|
|
|
|
${this.tasks.length > 0 ? html`
|
|
<div class="tasks-grid">
|
|
${this.tasks.map(task => this.renderTaskCard(task))}
|
|
</div>
|
|
` : html`
|
|
<div class="empty-state">
|
|
<svg class="empty-state-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
|
</svg>
|
|
<div class="empty-state-text">No tasks registered</div>
|
|
<div class="empty-state-subtext">Tasks will appear here when added to the TaskManager</div>
|
|
</div>
|
|
`}
|
|
</div>
|
|
|
|
${this.scheduledTasks.length > 0 ? html`
|
|
<div class="scheduled-section">
|
|
<h2 class="section-title">Scheduled Tasks</h2>
|
|
<div class="tasks-grid">
|
|
${this.scheduledTasks.map(task => this.renderScheduledTaskCard(task))}
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderTaskCard(task: ITaskMetadata) {
|
|
return html`
|
|
<div class="task-card">
|
|
<div class="task-header">
|
|
<div class="task-name">${task.name || 'Unnamed Task'}</div>
|
|
<div class="task-status ${task.status}">${task.status}</div>
|
|
</div>
|
|
|
|
<div class="task-info">
|
|
<div class="task-info-item">
|
|
<svg class="task-info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
<span>Run ${task.runCount || 0} times</span>
|
|
</div>
|
|
${task.buffered ? html`
|
|
<div class="task-info-item">
|
|
<svg class="task-info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
</svg>
|
|
<span>Buffer: ${task.bufferMax || 0}</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
|
|
${task.steps && task.steps.length > 0 ? html`
|
|
<div class="progress-container">
|
|
<div class="progress-label">
|
|
<span>Progress</span>
|
|
<span>${task.currentProgress || 0}%</span>
|
|
</div>
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" style="width: ${task.currentProgress || 0}%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="steps-container">
|
|
<div class="steps-title">Steps</div>
|
|
${task.steps.map(step => html`
|
|
<div class="step-item">
|
|
<div class="step-indicator ${step.status}">
|
|
${step.status === 'completed' ? '✓' :
|
|
step.status === 'active' ? '•' :
|
|
''}
|
|
</div>
|
|
<div class="step-details">
|
|
<div class="step-name">${step.name}</div>
|
|
${step.description ? html`
|
|
<div class="step-description">${step.description}</div>
|
|
` : ''}
|
|
</div>
|
|
<div class="step-percentage">${step.percentage}%</div>
|
|
</div>
|
|
`)}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderScheduledTaskCard(task: IScheduledTaskInfo) {
|
|
return html`
|
|
<div class="task-card">
|
|
<div class="task-header">
|
|
<div class="task-name">${task.name}</div>
|
|
<div class="task-status idle">scheduled</div>
|
|
</div>
|
|
|
|
<div class="schedule-info">
|
|
<svg class="schedule-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<span>Next run: ${this.formatNextRun(task.nextRun)}</span>
|
|
</div>
|
|
|
|
<div class="schedule-info">
|
|
<svg class="schedule-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
<span>Schedule: ${task.schedule}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
} |