feat(cloudly-view-tasks): add search and category filters, implement auto-refresh for task executions
This commit is contained in:
@@ -27,6 +27,17 @@ export class CloudlyViewTasks extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
private filterStatus: string = 'all';
|
private filterStatus: string = 'all';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private searchQuery: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private categoryFilter: string = 'all';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private autoRefresh: boolean = true;
|
||||||
|
|
||||||
|
private _refreshHandle: any = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const subscription = appstate.dataState
|
const subscription = appstate.dataState
|
||||||
@@ -38,6 +49,9 @@ export class CloudlyViewTasks extends DeesElement {
|
|||||||
|
|
||||||
// Load initial data (non-blocking)
|
// Load initial data (non-blocking)
|
||||||
this.loadInitialData();
|
this.loadInitialData();
|
||||||
|
|
||||||
|
// Start periodic refresh (lightweight; executions only by default)
|
||||||
|
this.startAutoRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadInitialData() {
|
private async loadInitialData() {
|
||||||
@@ -49,48 +63,117 @@ export class CloudlyViewTasks extends DeesElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<void> {
|
||||||
|
await (super.disconnectedCallback?.());
|
||||||
|
this.stopAutoRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
shared.viewHostCss,
|
shared.viewHostCss,
|
||||||
css`
|
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-grid {
|
.task-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-card {
|
.task-card {
|
||||||
background: #1a1a1a;
|
background: #131313;
|
||||||
border: 1px solid #333;
|
border: 1px solid #2a2a2a;
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
cursor: pointer;
|
transition: border-color 0.2s, background 0.2s;
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-card:hover {
|
.task-card:hover { border-color: #3a3a3a; }
|
||||||
background: #222;
|
|
||||||
border-color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-name {
|
.header-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||||
font-size: 1.1em;
|
.header-right { display: flex; align-items: center; gap: 8px; }
|
||||||
font-weight: 600;
|
.task-icon { color: #cfcfcf; }
|
||||||
color: #fff;
|
.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 {
|
.task-description {
|
||||||
color: #999;
|
color: #b5b5b5;
|
||||||
font-size: 0.9em;
|
font-size: 0.95em;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-meta {
|
.task-meta {
|
||||||
@@ -166,7 +249,7 @@ export class CloudlyViewTasks extends DeesElement {
|
|||||||
background: #2196f3;
|
background: #2196f3;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
@@ -196,7 +279,7 @@ export class CloudlyViewTasks extends DeesElement {
|
|||||||
.execution-logs {
|
.execution-logs {
|
||||||
background: #0a0a0a;
|
background: #0a0a0a;
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
@@ -279,21 +362,150 @@ export class CloudlyViewTasks extends DeesElement {
|
|||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-actions, .task-header .right {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
|
||||||
|
.card-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; }
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
private async triggerTask(taskName: string) {
|
private async triggerTask(taskName: string) {
|
||||||
try {
|
try {
|
||||||
await appstate.dataState.dispatchAction(
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
appstate.taskActions.triggerTask, { taskName }
|
heading: `Run Task: ${taskName}`,
|
||||||
);
|
content: html`
|
||||||
|
<div>
|
||||||
// Reload tasks and executions to show the new execution
|
<p>Do you want to trigger this task now?</p>
|
||||||
await appstate.dataState.dispatchAction(appstate.taskActions.getTasks, {});
|
</div>
|
||||||
await this.loadExecutionsWithFilter();
|
`,
|
||||||
|
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();
|
||||||
|
// Refresh executions to reflect the new run quickly
|
||||||
|
await this.loadExecutionsWithFilter();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modalArg: any) => modalArg.destroy()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to trigger task:', 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 => e.data.taskName === taskName && e.data.status === 'running')
|
||||||
|
.sort((a, b) => (b.data.startedAt || 0) - (a.data.startedAt || 0));
|
||||||
|
const running = executions[0];
|
||||||
|
if (!running) return;
|
||||||
|
|
||||||
|
await appstate.dataState.dispatchAction(
|
||||||
|
appstate.taskActions.cancelTask, { executionId: running.id }
|
||||||
|
);
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({
|
||||||
|
message: `Cancelled ${taskName}`,
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
await this.loadExecutionsWithFilter();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to cancel task:', err);
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({
|
||||||
|
message: `Cancel failed: ${err.message}`,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,6 +535,16 @@ export class CloudlyViewTasks extends DeesElement {
|
|||||||
return `${(ms / 3600000).toFixed(1)}h`;
|
return `${(ms / 3600000).toFixed(1)}h`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatRelativeTime(ts?: number): string {
|
||||||
|
if (!ts) return '-';
|
||||||
|
const diff = Date.now() - ts;
|
||||||
|
const abs = Math.abs(diff);
|
||||||
|
if (abs < 60_000) return `${Math.round(abs / 1000)}s ago`;
|
||||||
|
if (abs < 3_600_000) return `${Math.round(abs / 60_000)}m ago`;
|
||||||
|
if (abs < 86_400_000) return `${Math.round(abs / 3_600_000)}h ago`;
|
||||||
|
return `${Math.round(abs / 86_400_000)}d ago`;
|
||||||
|
}
|
||||||
|
|
||||||
private getCategoryIcon(category: string): string {
|
private getCategoryIcon(category: string): string {
|
||||||
switch (category) {
|
switch (category) {
|
||||||
case 'maintenance':
|
case 'maintenance':
|
||||||
@@ -344,6 +566,43 @@ export class CloudlyViewTasks extends DeesElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getCategoryHue(category: string): number {
|
||||||
|
switch (category) {
|
||||||
|
case 'maintenance': return 28; // orange
|
||||||
|
case 'deployment': return 208; // blue
|
||||||
|
case 'backup': return 122; // green
|
||||||
|
case 'monitoring': return 280; // purple
|
||||||
|
case 'cleanup': return 20; // brownish
|
||||||
|
case 'system': return 200; // steel
|
||||||
|
case 'security': return 0; // red
|
||||||
|
default: return 210; // default blue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStatusColor(status?: string): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'running': return '#2196f3';
|
||||||
|
case 'completed': return '#4caf50';
|
||||||
|
case 'failed': return '#f44336';
|
||||||
|
case 'cancelled': return '#ff9800';
|
||||||
|
default: return '#3a3a3a';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatCronFriendly(cron?: string): string {
|
||||||
|
if (!cron) return '';
|
||||||
|
const parts = cron.trim().split(/\s+/);
|
||||||
|
if (parts.length !== 5) return cron; // fallback
|
||||||
|
const [min, hour, dom, mon, dow] = parts;
|
||||||
|
if (min === '*/1' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'every minute';
|
||||||
|
if (min.startsWith('*/') && hour === '*' && dom === '*' && mon === '*' && dow === '*') return `every ${min.replace('*/','')} min`;
|
||||||
|
if (min === '0' && hour.startsWith('*/') && dom === '*' && mon === '*' && dow === '*') return `every ${hour.replace('*/','')} hours`;
|
||||||
|
if (min === '0' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'hourly';
|
||||||
|
if (min === '0' && hour === '0' && dom === '*' && mon === '*' && dow === '*') return 'daily';
|
||||||
|
if (min === '0' && hour === '0' && dom === '1' && mon === '*' && dow === '*') return 'monthly';
|
||||||
|
return cron;
|
||||||
|
}
|
||||||
|
|
||||||
private renderTaskCard(task: any) {
|
private renderTaskCard(task: any) {
|
||||||
const executions = this.data.taskExecutions || [];
|
const executions = this.data.taskExecutions || [];
|
||||||
const lastExecution = executions
|
const lastExecution = executions
|
||||||
@@ -351,65 +610,129 @@ export class CloudlyViewTasks extends DeesElement {
|
|||||||
.sort((a, b) => (b.data.startedAt || 0) - (a.data.startedAt || 0))[0];
|
.sort((a, b) => (b.data.startedAt || 0) - (a.data.startedAt || 0))[0];
|
||||||
|
|
||||||
const isRunning = lastExecution?.data.status === 'running';
|
const isRunning = lastExecution?.data.status === 'running';
|
||||||
|
const executionsForTask = executions.filter(e => e.data.taskName === task.name);
|
||||||
|
const now = Date.now();
|
||||||
|
const last24hCount = executionsForTask.filter(e => (e.data.startedAt || 0) > now - 86_400_000).length;
|
||||||
|
const completed = executionsForTask.filter(e => 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, e) => 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 status = lastExecution?.data.status as ('running'|'completed'|'failed'|'cancelled'|undefined);
|
||||||
|
const hue = this.getCategoryHue(task.category);
|
||||||
|
const subtitle = [
|
||||||
|
task.category,
|
||||||
|
task.schedule ? `⏱ ${this.formatCronFriendly(task.schedule)}` : null,
|
||||||
|
isRunning
|
||||||
|
? (lastExecution?.data.startedAt ? `Started ${this.formatRelativeTime(lastExecution.data.startedAt)}` : 'Running')
|
||||||
|
: (task.lastRun ? `Last ${this.formatRelativeTime(task.lastRun)}` : 'Never run')
|
||||||
|
].filter(Boolean).join(' • ');
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="task-card">
|
<div class="task-card">
|
||||||
<div class="task-header">
|
<div class="card-header">
|
||||||
<div class="task-name">
|
<div class="header-left">
|
||||||
<dees-icon .iconName=${this.getCategoryIcon(task.category)}></dees-icon>
|
<dees-icon class="task-icon" .iconName=${this.getCategoryIcon(task.category)}></dees-icon>
|
||||||
${task.name}
|
<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=${'Cancel'} .type=${'secondary'} @click=${() => this.cancelTaskFor(task.name)}></dees-button>
|
||||||
|
` : html`
|
||||||
|
<dees-button .text=${'Run'} .type=${'primary'} .disabled=${!task.enabled} @click=${() => this.triggerTask(task.name)}></dees-button>
|
||||||
|
`}
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
class="trigger-button"
|
|
||||||
?disabled=${isRunning || !task.enabled}
|
|
||||||
@click=${() => this.triggerTask(task.name)}
|
|
||||||
>
|
|
||||||
${isRunning ? 'Running...' : 'Run'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="task-description">${task.description}</div>
|
<div class="task-description" title=${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`
|
${lastExecution ? html`
|
||||||
<div class="metrics">
|
<div class="metrics-grid">
|
||||||
<div class="metric">
|
<div class="metric-item">
|
||||||
<span class="metric-label">Status</span>
|
<div class="label">Last Status</div>
|
||||||
<span class="metric-value">
|
<div class="value">
|
||||||
<span class="status-badge status-${lastExecution.data.status}">
|
<span class="status-badge status-${lastExecution.data.status}">${lastExecution.data.status}</span>
|
||||||
${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>
|
||||||
|
<div class="metric-item">
|
||||||
|
<div class="label">Avg Duration</div>
|
||||||
|
<div class="value">${avgDuration ? this.formatDuration(avgDuration) : '-'}</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<div class="label">24h Runs · Success</div>
|
||||||
|
<div class="value">${last24hCount} · ${successRate}%</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
<div class="lastline">
|
||||||
|
${lastLog ? html`<span class="dot ${lastLog.severity}"></span> ${lastLog.message}` : 'No recent logs'}
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<button class="link-button" @click=${() => this.openExecutionDetails(lastExecution)}>Details</button>
|
||||||
|
${lastExecution.data.logs?.length ? html`<button class="link-button" @click=${() => this.openLogsModal(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>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async openExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) {
|
||||||
|
this.selectedExecution = execution;
|
||||||
|
// Scroll into view of the details section
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.shadowRoot?.querySelector('cloudly-sectionheading + .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`
|
||||||
|
<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>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Copy All',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText((execution.data.logs || [])
|
||||||
|
.map(l => `${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() }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private renderExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) {
|
private renderExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) {
|
||||||
return html`
|
return html`
|
||||||
<div class="execution-details">
|
<div class="execution-details">
|
||||||
@@ -476,67 +799,99 @@ export class CloudlyViewTasks extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
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`
|
return html`
|
||||||
<cloudly-sectionheading>Tasks</cloudly-sectionheading>
|
<cloudly-sectionheading>Tasks</cloudly-sectionheading>
|
||||||
|
|
||||||
<div class="filter-bar">
|
<dees-panel
|
||||||
<button
|
.title=${'Task Library'}
|
||||||
class="filter-button ${this.filterStatus === 'all' ? 'active' : ''}"
|
.subtitle=${'Run maintenance, monitoring and system tasks'}
|
||||||
@click=${() => { this.filterStatus = 'all'; this.loadExecutionsWithFilter(); }}
|
.variant=${'outline'}
|
||||||
>
|
>
|
||||||
All
|
<div class="toolbar">
|
||||||
</button>
|
<div class="chipbar">
|
||||||
<button
|
<div class="chip ${this.categoryFilter === 'all' ? 'active' : ''}"
|
||||||
class="filter-button ${this.filterStatus === 'running' ? 'active' : ''}"
|
@click=${() => { this.categoryFilter = 'all'; }}>
|
||||||
@click=${() => { this.filterStatus = 'running'; this.loadExecutionsWithFilter(); }}
|
All
|
||||||
>
|
</div>
|
||||||
Running
|
${categories.map(cat => html`
|
||||||
</button>
|
<div class="chip ${this.categoryFilter === cat ? 'active' : ''}"
|
||||||
<button
|
@click=${() => { this.categoryFilter = cat; }}>
|
||||||
class="filter-button ${this.filterStatus === 'completed' ? 'active' : ''}"
|
${cat}
|
||||||
@click=${() => { this.filterStatus = 'completed'; this.loadExecutionsWithFilter(); }}
|
</div>
|
||||||
>
|
`)}
|
||||||
Completed
|
</div>
|
||||||
</button>
|
<div class="spacer"></div>
|
||||||
<button
|
<input class="search-input" placeholder="Search tasks" .value=${this.searchQuery}
|
||||||
class="filter-button ${this.filterStatus === 'failed' ? 'active' : ''}"
|
@input=${(e: any) => { this.searchQuery = e.target.value; }} />
|
||||||
@click=${() => { this.filterStatus = 'failed'; this.loadExecutionsWithFilter(); }}
|
<button class="secondary-button" @click=${async () => {
|
||||||
>
|
await this.loadExecutionsWithFilter();
|
||||||
Failed
|
}}>Refresh</button>
|
||||||
</button>
|
<button class="secondary-button" @click=${() => {
|
||||||
</div>
|
this.autoRefresh = !this.autoRefresh;
|
||||||
|
this.autoRefresh ? this.startAutoRefresh() : this.stopAutoRefresh();
|
||||||
|
}}>
|
||||||
|
${this.autoRefresh ? 'Auto-Refresh: On' : 'Auto-Refresh: Off'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="task-grid">
|
<div class="task-grid">
|
||||||
${(this.data.tasks || []).map(task => this.renderTaskCard(task))}
|
${filteredTasks.map(task => this.renderTaskCard(task))}
|
||||||
</div>
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
<cloudly-sectionheading>Execution History</cloudly-sectionheading>
|
<cloudly-sectionheading>Execution History</cloudly-sectionheading>
|
||||||
|
|
||||||
<dees-table
|
<dees-panel
|
||||||
.heading1=${'Task Executions'}
|
.title=${'Recent Executions'}
|
||||||
.heading2=${'History of task runs and their outcomes'}
|
.subtitle=${'History of task runs and their outcomes'}
|
||||||
.data=${this.data.taskExecutions || []}
|
.variant=${'outline'}
|
||||||
.displayFunction=${(itemArg: plugins.interfaces.data.ITaskExecution) => {
|
>
|
||||||
return {
|
<dees-table
|
||||||
Task: itemArg.data.taskName,
|
.heading1=${'Task Executions'}
|
||||||
Status: html`<span class="status-badge status-${itemArg.data.status}">${itemArg.data.status}</span>`,
|
.heading2=${'History of task runs and their outcomes'}
|
||||||
'Started At': this.formatDate(itemArg.data.startedAt),
|
.data=${this.data.taskExecutions || []}
|
||||||
Duration: itemArg.data.duration ? this.formatDuration(itemArg.data.duration) : '-',
|
.displayFunction=${(itemArg: plugins.interfaces.data.ITaskExecution) => {
|
||||||
'Triggered By': itemArg.data.triggeredBy,
|
return {
|
||||||
Logs: itemArg.data.logs?.length || 0,
|
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),
|
||||||
.actionFunction=${async (itemArg) => [
|
Duration: itemArg.data.duration ? this.formatDuration(itemArg.data.duration) : '-',
|
||||||
{
|
'Triggered By': itemArg.data.triggeredBy,
|
||||||
name: 'View Details',
|
Logs: itemArg.data.logs?.length || 0,
|
||||||
iconName: 'lucide:Eye',
|
};
|
||||||
type: ['inRow'],
|
}}
|
||||||
actionFunc: async () => {
|
.actionFunction=${async (itemArg: plugins.interfaces.data.ITaskExecution) => {
|
||||||
this.selectedExecution = itemArg;
|
const actions: any[] = [
|
||||||
},
|
{
|
||||||
},
|
name: 'View Details',
|
||||||
]}
|
iconName: 'lucide:Eye',
|
||||||
></dees-table>
|
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;
|
||||||
|
}}
|
||||||
|
></dees-table>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
${this.selectedExecution ? html`
|
${this.selectedExecution ? html`
|
||||||
<cloudly-sectionheading>Execution Details</cloudly-sectionheading>
|
<cloudly-sectionheading>Execution Details</cloudly-sectionheading>
|
||||||
|
Reference in New Issue
Block a user