/* DCTS Dashboard Nodes Table Component (增量 DOM 更新与 Skeleton) */
import { escapeHtml } from '../api.js';
export function renderNodesTableSkeleton() {
const tbody = document.getElementById('nodes-table-body');
if (!tbody) return;
const skeletonRows = Array.from({ length: 4 }).map(() => `
|
|
|
|
|
|
|
`).join('');
tbody.innerHTML = skeletonRows;
}
// 心跳时间容错:字段缺失或非法时间戳一律显示占位符,避免渲染出 "Invalid Date"
function formatHeartbeat(ts) {
if (!ts) return '—';
const d = new Date(ts);
if (Number.isNaN(d.getTime())) return '—';
return d.toLocaleTimeString('zh-CN');
}
// 行重建签名:仅状态/凭据决定徽章与操作按钮。槽位、负载、心跳是秒级高频字段,
// 走 textContent 原位刷新——整行 innerHTML 重建会销毁用户正悬停/聚焦的按钮。
function rowSignature(node) {
return `${node.status || ''}|${node.token_status || ''}`;
}
function volatileValues(node) {
return {
slots: `${Number(node.active_slots || 0)} / ${Number(node.max_slots || 4)}`,
usage: `${Number(node.cpu_usage || 0).toFixed(1)}% / ${Number(node.memory_usage || 0).toFixed(1)}%`,
heartbeat: formatHeartbeat(node.last_heartbeat),
};
}
function updateVolatileCells(tr, node) {
const v = volatileValues(node);
const slotsCell = tr.querySelector('.col-slots');
if (slotsCell && slotsCell.textContent !== v.slots) slotsCell.textContent = v.slots;
const usageCell = tr.querySelector('.col-usage');
if (usageCell && usageCell.textContent !== v.usage) usageCell.textContent = v.usage;
const hbCell = tr.querySelector('.col-heartbeat');
if (hbCell && hbCell.textContent !== v.heartbeat) hbCell.textContent = v.heartbeat;
}
function buildRowHtml(node, nodeId) {
const isPending = node.status === 'pending_approval';
const isOnline = node.status === 'online' || node.status === 'active';
const isDisabled = node.status === 'disabled';
let statusBadge;
if (isPending) {
statusBadge = '待审批';
} else if (isDisabled) {
statusBadge = '已停用';
} else if (isOnline) {
statusBadge = '在线';
} else {
statusBadge = '离线';
}
let tokenBadge;
if (isPending) {
tokenBadge = '待授权';
} else if (node.token_status === 'active') {
tokenBadge = '有效';
} else {
tokenBadge = '无凭据';
}
let actionBtns;
if (isPending) {
actionBtns = `
`;
} else {
// 启停切换:disabled 时显示「启用」(绿 ▶),否则显示「停用」(琥珀 ⏸)。
// 停用用琥珀+暂停符号,表达"可恢复的软暂停"——节点保持存活待命,与「重发 Token」
// (蓝 🔄,轮换凭据、旧 token 立即失效)是两类不同运维操作,颜色与图标刻意区分。
const toggleBtn = isDisabled
? ``
: ``;
actionBtns = `
`;
}
const v = volatileValues(node);
return `
${nodeId} |
${v.slots} |
${v.usage} |
${statusBadge} |
${tokenBadge} |
${v.heartbeat} |
${actionBtns} |
`;
}
export function renderNodesTable(nodes) {
const tbody = document.getElementById('nodes-table-body');
const countBadge = document.getElementById('node-count-badge');
if (countBadge) {
countBadge.textContent = `${nodes ? nodes.length : 0} 个节点`;
}
if (!tbody) return;
if (!nodes || nodes.length === 0) {
const emptyHtml = `
|
暂无计算节点记录。启动 DCTS Node Worker 提交注册申请后,在此审批授权接入集群。
|
`;
if (tbody.innerHTML.trim() !== emptyHtml.trim()) {
tbody.innerHTML = emptyHtml;
}
return;
}
// 构建目前现有 tr 的 Map (nodeId -> tr Element)
const existingRows = new Map();
tbody.querySelectorAll('tr[data-node-row-id]').forEach(tr => {
existingRows.set(tr.getAttribute('data-node-row-id'), tr);
});
// 如果 tbody 里包含 skeleton 或 empty-cell,清空
if (tbody.querySelector('.skeleton-row, .empty-cell')) {
tbody.innerHTML = '';
existingRows.clear();
}
const updatedRowElements = [];
nodes.forEach(node => {
const nodeId = escapeHtml(node.node_id || node.id || 'N/A');
const sig = rowSignature(node);
let tr = existingRows.get(nodeId);
if (tr && tr.getAttribute('data-sig') === sig) {
// 状态/凭据未变:仅原位刷新高频数值单元格,不触碰徽章与操作按钮
updateVolatileCells(tr, node);
} else {
const html = buildRowHtml(node, nodeId);
if (!tr) {
tr = document.createElement('tr');
tr.setAttribute('data-node-row-id', nodeId);
}
tr.innerHTML = html;
tr.setAttribute('data-sig', sig);
}
existingRows.delete(nodeId);
updatedRowElements.push(tr);
});
// 移除不再存在的旧 tr
existingRows.forEach(tr => tr.remove());
// 按顺序重排/挂载到 tbody
updatedRowElements.forEach((tr, idx) => {
if (tbody.children[idx] !== tr) {
tbody.insertBefore(tr, tbody.children[idx] || null);
}
});
}