支持查看CPU Busy Others
This commit is contained in:
@@ -663,6 +663,45 @@ input:checked+.slider:before {
|
|||||||
background: rgba(99, 102, 241, 0.05);
|
background: rgba(99, 102, 241, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#serverSearchInput {
|
||||||
|
background: var(--bg-input);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 6px 12px 6px 34px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
width: 100%;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#serverSearchInput:focus {
|
||||||
|
border-color: var(--accent-indigo);
|
||||||
|
background: rgba(99, 102, 241, 0.05);
|
||||||
|
box-shadow: 0 0 12px rgba(99, 102, 241, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box .search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#serverSearchInput:focus + .search-icon {
|
||||||
|
color: var(--accent-indigo);
|
||||||
|
}
|
||||||
|
|
||||||
.btn-icon-sm {
|
.btn-icon-sm {
|
||||||
background: var(--bg-input);
|
background: var(--bg-input);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
|
|||||||
@@ -283,6 +283,14 @@
|
|||||||
服务器详情
|
服务器详情
|
||||||
</h2>
|
</h2>
|
||||||
<div class="chart-header-right">
|
<div class="chart-header-right">
|
||||||
|
<div class="search-box">
|
||||||
|
<input type="text" id="serverSearchInput" placeholder="检索服务器名称..." autocomplete="off">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" class="search-icon">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
<button id="btnResetSort" class="btn-icon-sm" title="重置排序">
|
<button id="btnResetSort" class="btn-icon-sm" title="重置排序">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
stroke-linejoin="round">
|
stroke-linejoin="round">
|
||||||
@@ -506,6 +514,10 @@
|
|||||||
<span class="info-label">运行时间 (Uptime)</span>
|
<span class="info-label">运行时间 (Uptime)</span>
|
||||||
<span class="info-value" id="detailUptime">0天 0小时</span>
|
<span class="info-value" id="detailUptime">0天 0小时</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">硬盘总量统计</span>
|
||||||
|
<span class="info-value" id="detailDiskTotal">0 GB</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
totalBandwidthTx: document.getElementById('totalBandwidthTx'),
|
totalBandwidthTx: document.getElementById('totalBandwidthTx'),
|
||||||
totalBandwidthRx: document.getElementById('totalBandwidthRx'),
|
totalBandwidthRx: document.getElementById('totalBandwidthRx'),
|
||||||
traffic24hRx: document.getElementById('traffic24hRx'),
|
traffic24hRx: document.getElementById('traffic24hRx'),
|
||||||
|
serverSearchInput: document.getElementById('serverSearchInput'),
|
||||||
traffic24hTx: document.getElementById('traffic24hTx'),
|
traffic24hTx: document.getElementById('traffic24hTx'),
|
||||||
traffic24hTotal: document.getElementById('traffic24hTotal'),
|
traffic24hTotal: document.getElementById('traffic24hTotal'),
|
||||||
trafficP95: document.getElementById('trafficP95'),
|
trafficP95: document.getElementById('trafficP95'),
|
||||||
@@ -65,6 +66,7 @@
|
|||||||
legendTx: document.getElementById('legendTx'),
|
legendTx: document.getElementById('legendTx'),
|
||||||
p95LabelText: document.getElementById('p95LabelText'),
|
p95LabelText: document.getElementById('p95LabelText'),
|
||||||
p95TypeSelect: document.getElementById('p95TypeSelect'),
|
p95TypeSelect: document.getElementById('p95TypeSelect'),
|
||||||
|
detailDiskTotal: document.getElementById('detailDiskTotal'),
|
||||||
// Server Details Modal
|
// Server Details Modal
|
||||||
serverDetailModal: document.getElementById('serverDetailModal'),
|
serverDetailModal: document.getElementById('serverDetailModal'),
|
||||||
serverDetailClose: document.getElementById('serverDetailClose'),
|
serverDetailClose: document.getElementById('serverDetailClose'),
|
||||||
@@ -268,6 +270,14 @@
|
|||||||
dom.btnResetSort.addEventListener('click', resetSort);
|
dom.btnResetSort.addEventListener('click', resetSort);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server list search
|
||||||
|
if (dom.serverSearchInput) {
|
||||||
|
dom.serverSearchInput.addEventListener('input', () => {
|
||||||
|
currentPage = 1; // Reset page on search
|
||||||
|
renderFilteredServers();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Check auth status
|
// Check auth status
|
||||||
checkAuthStatus();
|
checkAuthStatus();
|
||||||
|
|
||||||
@@ -624,6 +634,15 @@
|
|||||||
filtered = allServersData.filter(s => s.source === currentSourceFilter);
|
filtered = allServersData.filter(s => s.source === currentSourceFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply search filter
|
||||||
|
const searchTerm = (dom.serverSearchInput?.value || '').toLowerCase().trim();
|
||||||
|
if (searchTerm) {
|
||||||
|
filtered = filtered.filter(s =>
|
||||||
|
(s.job || '').toLowerCase().includes(searchTerm) ||
|
||||||
|
(s.instance || '').toLowerCase().includes(searchTerm)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Sort servers: online first, then by currentSort
|
// Sort servers: online first, then by currentSort
|
||||||
filtered.sort((a, b) => {
|
filtered.sort((a, b) => {
|
||||||
// Primary sort: Always put online servers first unless sorting by 'up' explicitly
|
// Primary sort: Always put online servers first unless sorting by 'up' explicitly
|
||||||
@@ -889,11 +908,17 @@
|
|||||||
const mins = Math.floor((uptimeSec % 3600) / 60);
|
const mins = Math.floor((uptimeSec % 3600) / 60);
|
||||||
dom.detailUptime.textContent = `${days}天 ${hours}小时 ${mins}分`;
|
dom.detailUptime.textContent = `${days}天 ${hours}小时 ${mins}分`;
|
||||||
|
|
||||||
|
// Disk Total
|
||||||
|
const totalDiskSize = (data.partitions || []).reduce((sum, p) => sum + (p.size || 0), 0);
|
||||||
|
if (dom.detailDiskTotal) {
|
||||||
|
dom.detailDiskTotal.textContent = formatBytes(totalDiskSize);
|
||||||
|
}
|
||||||
|
|
||||||
// Define metrics to show
|
// Define metrics to show
|
||||||
const cpuValueHtml = `
|
const cpuValueHtml = `
|
||||||
<div style="display: flex; align-items: baseline; gap: 8px;">
|
<div style="display: flex; align-items: baseline; gap: 8px;">
|
||||||
<span style="font-weight: 700; font-size: 1.1rem;">${formatPercent(data.cpuBusy)}</span>
|
<span style="font-weight: 700; font-size: 1.1rem;">${formatPercent(data.cpuBusy)}</span>
|
||||||
<span style="font-size: 0.7rem; color: var(--text-secondary); font-weight: normal;">(IO Wait: ${data.cpuIowait.toFixed(1)}%)</span>
|
<span style="font-size: 0.7rem; color: var(--text-secondary); font-weight: normal;">(IO Wait: ${data.cpuIowait.toFixed(1)}%, Busy Others: ${data.cpuOther.toFixed(1)}%)</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
// Define metrics to show
|
// Define metrics to show
|
||||||
@@ -904,6 +929,7 @@
|
|||||||
{ key: 'rootFsUsedPct', label: '根分区使用率 (/)', value: formatPercent(data.rootFsUsedPct) },
|
{ key: 'rootFsUsedPct', label: '根分区使用率 (/)', value: formatPercent(data.rootFsUsedPct) },
|
||||||
{ key: 'netRx', label: '网络接收速率 (RX)', value: formatBandwidth(data.netRx) },
|
{ key: 'netRx', label: '网络接收速率 (RX)', value: formatBandwidth(data.netRx) },
|
||||||
{ key: 'netTx', label: '网络发送速率 (TX)', value: formatBandwidth(data.netTx) },
|
{ key: 'netTx', label: '网络发送速率 (TX)', value: formatBandwidth(data.netTx) },
|
||||||
|
{ key: 'networkTrend', label: '网络流量趋势 (24h)', value: '<span style="font-size: 0.75rem; color: var(--accent-indigo);">查看实时趋势线</span>' },
|
||||||
{ key: 'sockstatTcp', label: 'TCP 链接数 (Sockstat)', value: data.sockstatTcp.toFixed(0) },
|
{ key: 'sockstatTcp', label: 'TCP 链接数 (Sockstat)', value: data.sockstatTcp.toFixed(0) },
|
||||||
{ key: 'sockstatTcpMem', label: 'TCP 内存占用', value: formatBytes(data.sockstatTcpMem) }
|
{ key: 'sockstatTcpMem', label: 'TCP 内存占用', value: formatBytes(data.sockstatTcpMem) }
|
||||||
];
|
];
|
||||||
@@ -1015,7 +1041,12 @@
|
|||||||
if (metricKey.startsWith('net')) unit = 'B/s';
|
if (metricKey.startsWith('net')) unit = 'B/s';
|
||||||
if (metricKey === 'sockstatTcpMem') unit = 'B';
|
if (metricKey === 'sockstatTcpMem') unit = 'B';
|
||||||
|
|
||||||
chart = new MetricChart(canvas, unit);
|
if (metricKey === 'networkTrend') {
|
||||||
|
chart = new AreaChart(canvas);
|
||||||
|
chart.padding = { top: 15, right: 15, bottom: 35, left: 65 };
|
||||||
|
} else {
|
||||||
|
chart = new MetricChart(canvas, unit);
|
||||||
|
}
|
||||||
currentServerDetail.charts[metricKey] = chart;
|
currentServerDetail.charts[metricKey] = chart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -639,11 +639,28 @@ async function getServerHistory(baseUrl, instance, job, metric, range = '1h', st
|
|||||||
sockstatTcpMem: `node_sockstat_TCP_mem{instance="${node}",job="${job}"} * 4096`
|
sockstatTcpMem: `node_sockstat_TCP_mem{instance="${node}",job="${job}"} * 4096`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rangeObj = parseRange(range, start, end);
|
||||||
|
|
||||||
|
if (metric === 'networkTrend') {
|
||||||
|
const txExpr = metricMap.netTx;
|
||||||
|
const rxExpr = metricMap.netRx;
|
||||||
|
const [txResult, rxResult] = await Promise.all([
|
||||||
|
queryRange(url, txExpr, rangeObj.queryStart, rangeObj.queryEnd, rangeObj.step),
|
||||||
|
queryRange(url, rxExpr, rangeObj.queryStart, rangeObj.queryEnd, rangeObj.step)
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (txResult.length === 0 && rxResult.length === 0) return { timestamps: [], rx: [], tx: [] };
|
||||||
|
|
||||||
|
const timestamps = (txResult.length > 0 ? txResult[0] : rxResult[0]).values.map(v => v[0] * 1000);
|
||||||
|
const tx = txResult.length > 0 ? txResult[0].values.map(v => parseFloat(v[1])) : new Array(timestamps.length).fill(0);
|
||||||
|
const rx = rxResult.length > 0 ? rxResult[0].values.map(v => parseFloat(v[1])) : new Array(timestamps.length).fill(0);
|
||||||
|
|
||||||
|
return { timestamps, tx, rx };
|
||||||
|
}
|
||||||
|
|
||||||
const expr = metricMap[metric];
|
const expr = metricMap[metric];
|
||||||
if (!expr) throw new Error('Invalid metric for history');
|
if (!expr) throw new Error('Invalid metric for history');
|
||||||
|
|
||||||
const rangeObj = parseRange(range, start, end);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await queryRange(url, expr, rangeObj.queryStart, rangeObj.queryEnd, rangeObj.step);
|
const result = await queryRange(url, expr, rangeObj.queryStart, rangeObj.queryEnd, rangeObj.step);
|
||||||
if (!result || result.length === 0) return { timestamps: [], values: [] };
|
if (!result || result.length === 0) return { timestamps: [], values: [] };
|
||||||
|
|||||||
Reference in New Issue
Block a user