f40ef6b7c0
Align Cloudly with the current typedserver, smartconfig, smartstate, and Docker tooling releases so builds and Docker output stay compatible with the upgraded stack.
309 lines
12 KiB
TypeScript
309 lines
12 KiB
TypeScript
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 accessor data: appstate.IDataState = {} as any;
|
|
|
|
@state()
|
|
private accessor selectedExecution: plugins.interfaces.data.ITaskExecution | null = null;
|
|
|
|
@state()
|
|
private accessor loading = false;
|
|
|
|
@state()
|
|
private accessor filterStatus: string = 'all';
|
|
|
|
@state()
|
|
private accessor searchQuery: string = '';
|
|
|
|
@state()
|
|
private accessor categoryFilter: string = 'all';
|
|
|
|
@state()
|
|
private accessor autoRefresh: boolean = true;
|
|
|
|
private _refreshHandle: any = null;
|
|
@state()
|
|
private accessor canceling: Record<string, boolean> = {};
|
|
|
|
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<void> {
|
|
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`<div><p>Do you want to trigger this task now?</p></div>`,
|
|
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 instanceof Error ? error.message : String(error)}`, 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 instanceof Error ? err.message : String(err)}`, 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`
|
|
<div class="execution-logs">
|
|
${(execution.data.logs || []).map((log: any) => html`
|
|
<div class="log-entry log-${log.severity}"><span>${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: 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`
|
|
<cloudly-sectionheading>Tasks</cloudly-sectionheading>
|
|
|
|
<dees-panel .title=${'Task Library'} .subtitle=${'Run maintenance, monitoring and system tasks'} .variant=${'outline'}>
|
|
<div class="toolbar">
|
|
<div class="chipbar">
|
|
<div class="chip ${this.categoryFilter === 'all' ? 'active' : ''}"
|
|
@click=${() => { this.categoryFilter = 'all'; }}>
|
|
All
|
|
</div>
|
|
${categories.map(cat => html`
|
|
<div class="chip ${this.categoryFilter === cat ? 'active' : ''}"
|
|
@click=${() => { this.categoryFilter = cat; }}>
|
|
${cat}
|
|
</div>
|
|
`)}
|
|
</div>
|
|
<div class="spacer"></div>
|
|
<input class="search-input" placeholder="Search tasks" .value=${this.searchQuery}
|
|
@input=${(e: any) => { this.searchQuery = e.target.value; }} />
|
|
<button class="secondary-button" @click=${async () => { await this.loadExecutionsWithFilter(); }}>Refresh</button>
|
|
<button class="secondary-button" @click=${() => { this.autoRefresh = !this.autoRefresh; this.autoRefresh ? this.startAutoRefresh() : this.stopAutoRefresh(); }}>
|
|
${this.autoRefresh ? 'Auto-Refresh: On' : 'Auto-Refresh: Off'}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="task-list">
|
|
${filteredTasks.map(task => html`
|
|
<cloudly-task-panel
|
|
.task=${task}
|
|
.executions=${this.data.taskExecutions || []}
|
|
.canceling=${this.canceling}
|
|
.onRun=${(name: string) => this.triggerTask(name)}
|
|
.onCancel=${(name: string) => this.cancelTaskFor(name)}
|
|
.onOpenDetails=${(exec: any) => this.openExecutionDetails(exec)}
|
|
.onOpenLogs=${(exec: any) => this.openLogsModal(exec)}
|
|
></cloudly-task-panel>
|
|
`)}
|
|
</div>
|
|
</dees-panel>
|
|
|
|
<cloudly-sectionheading>Execution History</cloudly-sectionheading>
|
|
|
|
<dees-panel .title=${'Recent Executions'} .subtitle=${'History of task runs and their outcomes'} .variant=${'outline'}>
|
|
<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': 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;
|
|
}}
|
|
></dees-table>
|
|
</dees-panel>
|
|
|
|
${this.selectedExecution ? html`
|
|
<cloudly-sectionheading>Execution Details</cloudly-sectionheading>
|
|
<cloudly-execution-details .execution=${this.selectedExecution}></cloudly-execution-details>
|
|
` : ''}
|
|
`;
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'cloudly-view-tasks': CloudlyViewTasks;
|
|
}
|
|
}
|