Files
crank/apps/ui/js/async-jobs.js
T
2026-05-02 10:55:26 +00:00

233 lines
7.9 KiB
JavaScript

document.addEventListener('DOMContentLoaded', function () {
var state = {
items: [],
workspaceId: null,
loading: false,
error: '',
openId: null,
};
var list = document.getElementById('async-jobs-list');
var summary = document.getElementById('async-jobs-summary');
var refreshButton = document.getElementById('async-jobs-refresh');
var statusFilter = document.getElementById('async-jobs-status');
function tKey(key, vars) {
if (!window.t) return key;
return t(key, vars);
}
function currentWorkspaceId() {
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
return workspace ? workspace.id : null;
}
function formatJson(value) {
if (value === null || value === undefined) return '';
if (typeof value === 'string') return value;
return JSON.stringify(value, null, 2);
}
function formatDateTime(value) {
if (!value) return '—';
return new Date(value).toLocaleString();
}
function element(tag, className, text) {
var node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined && text !== null) node.textContent = text;
return node;
}
function renderEmpty(title, body) {
list.innerHTML = '';
var empty = element('div', 'empty-state');
empty.appendChild(element('div', 'empty-state-title', title));
empty.appendChild(element('div', 'empty-state-text', body));
list.appendChild(empty);
}
function appendMetaItem(parent, label, value) {
var item = element('div', 'resource-meta-item');
item.appendChild(element('div', 'resource-meta-label', label));
item.appendChild(element('div', 'resource-meta-value', value));
parent.appendChild(item);
}
function appendDetailBlock(parent, title, value, dataResultFor) {
var block = element('div', 'resource-detail-block');
block.appendChild(element('div', 'resource-detail-title', title));
var pre = element('pre', 'resource-detail-pre', value);
if (dataResultFor) pre.setAttribute('data-result-for', dataResultFor);
block.appendChild(pre);
parent.appendChild(block);
}
function renderJobCard(item) {
var open = state.openId === item.id;
var card = element('div', 'resource-card');
card.setAttribute('data-job-id', item.id);
var header = element('div', 'resource-card-header');
var headerMain = element('div');
headerMain.appendChild(element('div', 'resource-card-title', item.id));
headerMain.appendChild(element('div', 'resource-card-subtitle', tKey('async_jobs.operation', { operation: item.operation_id })));
var pillRow = element('div', 'resource-pill-row');
pillRow.appendChild(
element('span', 'resource-status-pill ' + String(item.status || '').toLowerCase(), tKey('async_jobs.status.' + item.status))
);
headerMain.appendChild(pillRow);
header.appendChild(headerMain);
var actions = element('div', 'resource-card-actions');
if (item.status === 'created' || item.status === 'running') {
var cancelButton = element('button', 'btn-secondary', tKey('async_jobs.cancel'));
cancelButton.setAttribute('data-action', 'cancel');
cancelButton.setAttribute('data-job-id', item.id);
actions.appendChild(cancelButton);
}
var toggleButton = element(
'button',
'btn-secondary',
open ? tKey('async_jobs.hide_details') : tKey('async_jobs.show_details')
);
toggleButton.setAttribute('data-action', 'toggle');
toggleButton.setAttribute('data-job-id', item.id);
actions.appendChild(toggleButton);
header.appendChild(actions);
card.appendChild(header);
var metaGrid = element('div', 'resource-meta-grid');
appendMetaItem(metaGrid, tKey('async_jobs.meta.created'), formatDateTime(item.created_at));
appendMetaItem(metaGrid, tKey('async_jobs.meta.updated'), formatDateTime(item.updated_at));
appendMetaItem(metaGrid, tKey('async_jobs.meta.finished'), formatDateTime(item.finished_at));
appendMetaItem(metaGrid, tKey('async_jobs.meta.expires'), formatDateTime(item.expires_at));
card.appendChild(metaGrid);
if (open) {
appendDetailBlock(card, tKey('async_jobs.progress'), formatJson(item.progress));
appendDetailBlock(card, tKey('async_jobs.error_preview'), formatJson(item.error));
if (
item.status === 'completed' ||
item.status === 'failed' ||
item.status === 'cancelled' ||
item.status === 'expired'
) {
appendDetailBlock(card, tKey('async_jobs.result'), tKey('async_jobs.result_loading'), item.id);
}
}
return card;
}
function render() {
summary.textContent = tKey('async_jobs.summary', { count: state.items.length });
if (state.loading && state.items.length === 0) {
renderEmpty(tKey('async_jobs.loading.title'), tKey('async_jobs.loading.body'));
return;
}
if (state.error) {
renderEmpty(tKey('async_jobs.error.title'), state.error);
return;
}
if (!state.items.length) {
renderEmpty(tKey('async_jobs.empty.title'), tKey('async_jobs.empty.body'));
return;
}
list.innerHTML = '';
state.items.forEach(function(item) {
list.appendChild(renderJobCard(item));
});
list.querySelectorAll('[data-action="toggle"]').forEach(function(button) {
button.addEventListener('click', function () {
var jobId = this.getAttribute('data-job-id');
state.openId = state.openId === jobId ? null : jobId;
if (state.openId) {
void loadDetail(jobId);
} else {
render();
}
});
});
list.querySelectorAll('[data-action="cancel"]').forEach(function(button) {
button.addEventListener('click', async function () {
try {
var jobId = this.getAttribute('data-job-id');
await window.CrankApi.cancelAsyncJob(state.workspaceId, jobId);
if (window.CrankUi) {
window.CrankUi.success(tKey('async_jobs.toast.cancel.body'), tKey('async_jobs.toast.cancel.title'));
}
await load();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('async_jobs.toast.cancel_error.body'), tKey('async_jobs.toast.cancel_error.title'));
}
}
});
});
}
async function loadResult(jobId) {
try {
var result = await window.CrankApi.getAsyncJobResult(state.workspaceId, jobId);
var node = document.querySelector('[data-result-for="' + jobId + '"]');
if (node) node.textContent = formatJson(result);
} catch (error) {
var failedNode = document.querySelector('[data-result-for="' + jobId + '"]');
if (failedNode) failedNode.textContent = error.message || tKey('async_jobs.result_error');
}
}
async function loadDetail(jobId) {
var detail = await window.CrankApi.getAsyncJob(state.workspaceId, jobId);
state.items = state.items.map(function(item) {
return item.id === jobId ? detail : item;
});
render();
if (detail.status === 'completed' || detail.status === 'failed' || detail.status === 'cancelled' || detail.status === 'expired') {
await loadResult(jobId);
}
}
async function load() {
state.workspaceId = currentWorkspaceId();
if (!state.workspaceId) {
state.items = [];
state.error = tKey('async_jobs.error.workspace');
render();
return;
}
state.loading = true;
state.error = '';
render();
try {
var response = await window.CrankApi.listAsyncJobs(state.workspaceId, {
status: statusFilter.value || null,
page: 1,
page_size: 50,
});
state.items = response.items || [];
} catch (error) {
state.error = error.message || tKey('async_jobs.error.load');
} finally {
state.loading = false;
render();
}
}
refreshButton.addEventListener('click', load);
statusFilter.addEventListener('change', load);
document.addEventListener('workspace:changed', load);
void load();
});