feat: replace console connection panel with runtime stats
This commit is contained in:
@@ -52,11 +52,19 @@
|
||||
$('queue-id').value = defaultQueue;
|
||||
sessionStorage.setItem('orbit.console.queue', defaultQueue);
|
||||
text('queue-hint', '已使用服务端默认队列配置;也可选择凭据 JSON 覆盖。');
|
||||
renderConnectionSummary();
|
||||
}
|
||||
} catch {
|
||||
// 连接表单仍可使用手工 API 地址和凭据,不因可选配置失败而阻断页面。
|
||||
}
|
||||
}
|
||||
function renderConnectionSummary() {
|
||||
text('summary-api', state.base || location.origin);
|
||||
text('summary-api-hint', state.connected ? '接口响应正常,按 10 秒周期刷新' : 'API 地址自动跟随当前页面');
|
||||
text('summary-queue', state.queue ? shortID(state.queue) : '未配置');
|
||||
text('summary-credential', state.token ? '已载入' : '未载入');
|
||||
text('summary-note', state.token && state.queue ? '已具备读取业务数据的连接条件。' : '服务状态会自动检查;需要业务数据时再加载凭据。');
|
||||
}
|
||||
function setConnection(connected, detail = '') {
|
||||
state.connected = connected;
|
||||
const authenticated = connected && Boolean(state.token && state.queue);
|
||||
@@ -68,6 +76,7 @@
|
||||
document.querySelector('.status-dot').classList.toggle('ready', connected);
|
||||
document.querySelector('.live-chip').classList.toggle('ready', connected);
|
||||
$('live-label').textContent = connected ? '环境在线' : '本地环境';
|
||||
renderConnectionSummary();
|
||||
if (detail) showToast(detail);
|
||||
}
|
||||
async function loadHealth() {
|
||||
@@ -124,6 +133,35 @@
|
||||
row.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openSubscriptionDetail(row.dataset.subscriptionId); } });
|
||||
});
|
||||
}
|
||||
function renderRuntimeStats(tasks, schedules, workflows, subscriptions) {
|
||||
if (Array.isArray(tasks)) {
|
||||
const counts = tasks.reduce((acc, item) => { const key = stateClass(item.state); acc[key] = (acc[key] || 0) + 1; return acc; }, {});
|
||||
const total = tasks.length;
|
||||
text('stat-success-rate', total ? `${Math.round((counts.succeeded || 0) / total * 100)}%` : '—');
|
||||
text('stat-success-rate-sub', total ? `${counts.succeeded || 0} 成功 / ${total} 项` : '当前没有任务');
|
||||
text('stat-task-total', `${total} 项`);
|
||||
const waiting = (counts.waiting || 0) + (counts.pending || 0);
|
||||
const other = (counts.failed || 0) + (counts.cancelled || 0) + (counts.needs_review || 0);
|
||||
const width = value => `${total ? value / total * 100 : 0}%`;
|
||||
[['succeeded', counts.succeeded || 0], ['running', counts.running || 0], ['waiting', waiting], ['other', other]].forEach(([name, value]) => { $(`stat-bar-${name}`).style.width = width(value); text(`stat-count-${name}`, String(value)); });
|
||||
} else {
|
||||
text('stat-success-rate', '—'); text('stat-success-rate-sub', '暂无任务权限'); text('stat-task-total', '暂无数据');
|
||||
['succeeded', 'running', 'waiting', 'other'].forEach(name => { $(`stat-bar-${name}`).style.width = '0%'; text(`stat-count-${name}`, '—'); });
|
||||
}
|
||||
if (Array.isArray(schedules)) {
|
||||
const active = schedules.filter(item => item.paused !== true).length;
|
||||
text('stat-active-schedules', String(active)); text('stat-active-schedules-sub', `${schedules.length} 项计划 · ${schedules.length - active} 项暂停`);
|
||||
} else { text('stat-active-schedules', '—'); text('stat-active-schedules-sub', '暂无计划权限'); }
|
||||
if (Array.isArray(workflows)) {
|
||||
const review = workflows.filter(item => stateClass(item.state) === 'needs_review').length;
|
||||
text('stat-review-workflows', String(review)); text('stat-review-workflows-sub', `${workflows.length} 项流程实例`);
|
||||
} else { text('stat-review-workflows', '—'); text('stat-review-workflows-sub', '暂无流程权限'); }
|
||||
if (Array.isArray(subscriptions)) {
|
||||
const pending = subscriptions.reduce((sum, item) => sum + (Number(item.pending_count) || 0), 0);
|
||||
text('stat-pending-deliveries', String(pending)); text('stat-pending-deliveries-sub', `${subscriptions.length} 个订阅 · 待确认结果`);
|
||||
} else { text('stat-pending-deliveries', '—'); text('stat-pending-deliveries-sub', '暂无事件权限'); }
|
||||
text('stats-snapshot', state.connected ? `更新于 ${formatTime(new Date())}` : '等待连接');
|
||||
}
|
||||
function escapeHTML(value) { return String(value ?? '').replace(/[&<>'"]/g, char => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[char])); }
|
||||
function formatJSON(value) {
|
||||
if (value === undefined || value === null || value === '') return '';
|
||||
@@ -134,6 +172,8 @@
|
||||
return `console-${prefix}-${id}`.slice(0, 128);
|
||||
}
|
||||
function terminalState(value) { return ['SUCCEEDED', 'FAILED', 'CANCELLED'].includes(value); }
|
||||
function openConnectionSettings() { $('connection-settings-backdrop').hidden = false; document.body.classList.add('drawer-open'); $('api-url').focus(); }
|
||||
function closeConnectionSettings() { $('connection-settings-backdrop').hidden = true; document.body.classList.remove('drawer-open'); }
|
||||
function showTaskDrawer() { $('task-drawer-backdrop').hidden = false; document.body.classList.add('drawer-open'); }
|
||||
function closeTaskDrawer() { $('task-drawer-backdrop').hidden = true; document.body.classList.remove('drawer-open'); closeActionForm(); }
|
||||
function renderDetailList(id, items, emptyText, renderItem) {
|
||||
@@ -413,17 +453,20 @@
|
||||
if (state.loading) return; state.loading = true; $('refresh-button').classList.add('spinning');
|
||||
try {
|
||||
await loadHealth();
|
||||
if (!state.token || !state.queue) { setConnection(true); text('last-updated', `服务在线 · ${formatTime(new Date())}`); return; }
|
||||
const tasks = await request(`/v1/tasks?queue_id=${encodeURIComponent(state.queue)}&limit=20`); renderTasks(extractItems(tasks));
|
||||
if (!state.token || !state.queue) { renderRuntimeStats(null, null, null, null); setConnection(true); text('last-updated', `服务在线 · ${formatTime(new Date())}`); return; }
|
||||
const tasks = await request(`/v1/tasks?queue_id=${encodeURIComponent(state.queue)}&limit=20`); const taskItems = extractItems(tasks); renderTasks(taskItems);
|
||||
const optional = await Promise.allSettled([
|
||||
request(`/v1/schedules?queue_id=${encodeURIComponent(state.queue)}&limit=20`),
|
||||
request('/v1/workflows?limit=20'),
|
||||
request(`/v1/subscriptions?queue_id=${encodeURIComponent(state.queue)}&limit=20`),
|
||||
]);
|
||||
if (optional[0].status === 'fulfilled') renderSchedules(extractItems(optional[0].value));
|
||||
if (optional[1].status === 'fulfilled') renderWorkflows(extractItems(optional[1].value));
|
||||
if (optional[2].status === 'fulfilled') renderSubscriptions(extractItems(optional[2].value));
|
||||
setConnection(true); text('last-updated', `刚刚更新 · ${formatTime(new Date())}`); text('metric-health-sub', '数据库与 Schema 正常');
|
||||
const scheduleItems = optional[0].status === 'fulfilled' ? extractItems(optional[0].value) : null;
|
||||
const workflowItems = optional[1].status === 'fulfilled' ? extractItems(optional[1].value) : null;
|
||||
const subscriptionItems = optional[2].status === 'fulfilled' ? extractItems(optional[2].value) : null;
|
||||
if (scheduleItems) renderSchedules(scheduleItems);
|
||||
if (workflowItems) renderWorkflows(workflowItems);
|
||||
if (subscriptionItems) renderSubscriptions(subscriptionItems);
|
||||
setConnection(true); renderRuntimeStats(taskItems, scheduleItems, workflowItems, subscriptionItems); text('last-updated', `刚刚更新 · ${formatTime(new Date())}`); text('metric-health-sub', '数据库与 Schema 正常');
|
||||
} catch (error) { setConnection(false); showToast(`连接失败:${error.message}`); text('last-updated', '刷新失败'); }
|
||||
finally { state.loading = false; $('refresh-button').classList.remove('spinning'); }
|
||||
}
|
||||
@@ -433,14 +476,15 @@
|
||||
sessionStorage.setItem('orbit.console.queue', state.queue);
|
||||
if (!state.token) showToast('服务状态可以直接查看;选择凭据 JSON 后可读取业务数据');
|
||||
else if (!state.queue) showToast('队列会从凭据 JSON 自动读取,也可配置 ORBIT_CONSOLE_QUEUE_ID');
|
||||
closeConnectionSettings();
|
||||
refresh();
|
||||
}
|
||||
$('connect-button').addEventListener('click', connect); $('connect-hero').addEventListener('click', () => $('connection-panel').scrollIntoView({ behavior: 'smooth', block: 'center' })); $('refresh-button').addEventListener('click', refresh);
|
||||
$('close-task-drawer').addEventListener('click', closeTaskDrawer); $('task-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'task-drawer-backdrop') closeTaskDrawer(); }); $('action-cancel').addEventListener('click', closeActionForm); $('task-action-form').addEventListener('submit', submitAction); $('task-cancel-button').addEventListener('click', () => openActionForm('cancel')); $('task-retry-button').addEventListener('click', () => openActionForm('retry')); $('close-schedule-drawer').addEventListener('click', closeScheduleDrawer); $('schedule-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'schedule-drawer-backdrop') closeScheduleDrawer(); }); $('schedule-control-button').addEventListener('click', openScheduleAction); $('schedule-action-cancel').addEventListener('click', closeScheduleAction); $('schedule-action-form').addEventListener('submit', submitScheduleAction); $('close-workflow-drawer').addEventListener('click', closeWorkflowDrawer); $('workflow-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'workflow-drawer-backdrop') closeWorkflowDrawer(); }); $('workflow-control-button').addEventListener('click', () => openWorkflowAction(state.selectedWorkflow?.paused ? 'RESUME' : 'PAUSE')); $('workflow-cancel-button').addEventListener('click', () => openWorkflowAction('CANCEL')); $('workflow-action-cancel').addEventListener('click', closeWorkflowAction); $('workflow-action-form').addEventListener('submit', submitWorkflowAction); $('close-subscription-drawer').addEventListener('click', closeSubscriptionDrawer); $('subscription-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'subscription-drawer-backdrop') closeSubscriptionDrawer(); }); $('subscription-control-button').addEventListener('click', openSubscriptionAction); $('subscription-action-cancel').addEventListener('click', closeSubscriptionAction); $('subscription-action-form').addEventListener('submit', submitSubscriptionAction); $('subscription-retry-cancel').addEventListener('click', closeSubscriptionRetry); $('subscription-retry-form').addEventListener('submit', submitSubscriptionRetry); document.addEventListener('keydown', event => { if (event.key === 'Escape') { if (!$('task-action-form').hidden) closeActionForm(); else if (!$('schedule-action-form').hidden) closeScheduleAction(); else if (!$('workflow-action-form').hidden) closeWorkflowAction(); else if (!$('subscription-action-form').hidden) closeSubscriptionAction(); else if (!$('subscription-retry-form').hidden) closeSubscriptionRetry(); else if (!$('task-drawer-backdrop').hidden) closeTaskDrawer(); else if (!$('schedule-drawer-backdrop').hidden) closeScheduleDrawer(); else if (!$('workflow-drawer-backdrop').hidden) closeWorkflowDrawer(); else if (!$('subscription-drawer-backdrop').hidden) closeSubscriptionDrawer(); } });
|
||||
$('connect-button').addEventListener('click', connect); $('connect-hero').addEventListener('click', openConnectionSettings); $('connection-settings-button').addEventListener('click', openConnectionSettings); $('connection-settings-inline').addEventListener('click', openConnectionSettings); $('close-connection-settings').addEventListener('click', closeConnectionSettings); $('connection-settings-backdrop').addEventListener('click', event => { if (event.target.id === 'connection-settings-backdrop') closeConnectionSettings(); }); $('refresh-button').addEventListener('click', refresh);
|
||||
$('close-task-drawer').addEventListener('click', closeTaskDrawer); $('task-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'task-drawer-backdrop') closeTaskDrawer(); }); $('action-cancel').addEventListener('click', closeActionForm); $('task-action-form').addEventListener('submit', submitAction); $('task-cancel-button').addEventListener('click', () => openActionForm('cancel')); $('task-retry-button').addEventListener('click', () => openActionForm('retry')); $('close-schedule-drawer').addEventListener('click', closeScheduleDrawer); $('schedule-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'schedule-drawer-backdrop') closeScheduleDrawer(); }); $('schedule-control-button').addEventListener('click', openScheduleAction); $('schedule-action-cancel').addEventListener('click', closeScheduleAction); $('schedule-action-form').addEventListener('submit', submitScheduleAction); $('close-workflow-drawer').addEventListener('click', closeWorkflowDrawer); $('workflow-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'workflow-drawer-backdrop') closeWorkflowDrawer(); }); $('workflow-control-button').addEventListener('click', () => openWorkflowAction(state.selectedWorkflow?.paused ? 'RESUME' : 'PAUSE')); $('workflow-cancel-button').addEventListener('click', () => openWorkflowAction('CANCEL')); $('workflow-action-cancel').addEventListener('click', closeWorkflowAction); $('workflow-action-form').addEventListener('submit', submitWorkflowAction); $('close-subscription-drawer').addEventListener('click', closeSubscriptionDrawer); $('subscription-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'subscription-drawer-backdrop') closeSubscriptionDrawer(); }); $('subscription-control-button').addEventListener('click', openSubscriptionAction); $('subscription-action-cancel').addEventListener('click', closeSubscriptionAction); $('subscription-action-form').addEventListener('submit', submitSubscriptionAction); $('subscription-retry-cancel').addEventListener('click', closeSubscriptionRetry); $('subscription-retry-form').addEventListener('submit', submitSubscriptionRetry); document.addEventListener('keydown', event => { if (event.key === 'Escape') { if (!$('connection-settings-backdrop').hidden) closeConnectionSettings(); else if (!$('task-action-form').hidden) closeActionForm(); else if (!$('schedule-action-form').hidden) closeScheduleAction(); else if (!$('workflow-action-form').hidden) closeWorkflowAction(); else if (!$('subscription-action-form').hidden) closeSubscriptionAction(); else if (!$('subscription-retry-form').hidden) closeSubscriptionRetry(); else if (!$('task-drawer-backdrop').hidden) closeTaskDrawer(); else if (!$('schedule-drawer-backdrop').hidden) closeScheduleDrawer(); else if (!$('workflow-drawer-backdrop').hidden) closeWorkflowDrawer(); else if (!$('subscription-drawer-backdrop').hidden) closeSubscriptionDrawer(); } });
|
||||
$('workflow-recovery-actions').addEventListener('click', event => { const button = event.target.closest('[data-recovery-action]'); if (!button) return; let context = {}; try { context = JSON.parse(button.dataset.recoveryContext || '{}'); } catch { showToast('恢复按钮上下文无效,请重新读取流程'); return; } openWorkflowRecovery(button.dataset.recoveryAction, context); }); $('workflow-recovery-cancel').addEventListener('click', closeWorkflowRecovery); $('workflow-recovery-form').addEventListener('submit', submitWorkflowRecovery);
|
||||
document.addEventListener('keydown', event => { if (event.key === 'Escape' && !$('workflow-recovery-form').hidden) closeWorkflowRecovery(); });
|
||||
$('reveal-token').addEventListener('click', () => { const field = $('token'); field.type = field.type === 'password' ? 'text' : 'password'; $('reveal-token').textContent = field.type === 'password' ? '显示' : '隐藏'; });
|
||||
$('credential-file').addEventListener('change', event => { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { try { const credential = JSON.parse(reader.result); const queue = String(credential.queue_id || state.consoleConfig?.default_queue_id || '').trim(); $('token').value = credential.token || ''; $('queue-id').value = queue; state.queue = queue; sessionStorage.setItem('orbit.console.queue', queue); text('queue-hint', queue ? '队列已从凭据 JSON 自动读取;无需手工填写。' : '凭据未附带队列,请配置 ORBIT_CONSOLE_QUEUE_ID。'); showToast(queue ? '凭据已读取,队列已自动填充' : '凭据已读取,但缺少队列配置'); } catch { showToast('凭据 JSON 无法解析'); } }; reader.readAsText(file); });
|
||||
$('credential-file').addEventListener('change', event => { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { try { const credential = JSON.parse(reader.result); const queue = String(credential.queue_id || state.consoleConfig?.default_queue_id || '').trim(); $('token').value = credential.token || ''; $('queue-id').value = queue; state.queue = queue; state.token = String(credential.token || '').trim(); sessionStorage.setItem('orbit.console.queue', queue); text('queue-hint', queue ? '队列已从凭据 JSON 自动读取;无需手工填写。' : '凭据未附带队列,请配置 ORBIT_CONSOLE_QUEUE_ID。'); renderConnectionSummary(); showToast(queue ? '凭据已读取,队列已自动填充' : '凭据已读取,但缺少队列配置'); } catch { showToast('凭据 JSON 无法解析'); } }; reader.readAsText(file); });
|
||||
document.querySelectorAll('[data-view]').forEach(button => button.addEventListener('click', event => { event.preventDefault(); setView(button.dataset.view); }));
|
||||
window.addEventListener('hashchange', () => setView(currentView(), false));
|
||||
setView(currentView(), false);
|
||||
|
||||
Reference in New Issue
Block a user