diff --git a/public/css/style.css b/public/css/style.css
index c7b2fb6..9538b26 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -877,6 +877,52 @@ input:checked+.slider:before {
color: var(--text-muted);
border-bottom: 1px solid var(--border-color);
background: rgba(0, 0, 0, 0.2);
+ transition: all 0.2s ease;
+}
+
+.server-table th.sortable {
+ cursor: pointer;
+ position: relative;
+ user-select: none;
+}
+
+.server-table th.sortable:hover {
+ background: rgba(99, 102, 241, 0.08);
+ color: var(--text-primary);
+}
+
+.server-table th.sortable.active {
+ color: var(--accent-indigo);
+ background: rgba(99, 102, 241, 0.05);
+ border-bottom-color: var(--accent-indigo);
+}
+
+.sort-icon {
+ display: inline-block;
+ width: 12px;
+ height: 12px;
+ vertical-align: middle;
+ margin-left: 4px;
+ opacity: 0.3;
+ transition: all 0.2s ease;
+ position: relative;
+}
+
+.server-table th.sortable.active .sort-icon {
+ opacity: 1;
+}
+
+.sort-icon::after {
+ content: '↕';
+ font-size: 0.8rem;
+}
+
+.server-table th.sortable.active[data-dir="asc"] .sort-icon::after {
+ content: '↑';
+}
+
+.server-table th.sortable.active[data-dir="desc"] .sort-icon::after {
+ content: '↓';
}
.server-table td {
diff --git a/public/index.html b/public/index.html
index e1a56c1..10ecfff 100644
--- a/public/index.html
+++ b/public/index.html
@@ -288,14 +288,14 @@
- | 状态 |
- Job / 实例 |
- 数据源 |
- CPU |
- 内存 |
- 磁盘 |
- 网络 ↓ |
- 网络 ↑ |
+ 状态 |
+ Job / 实例 |
+ 数据源 |
+ CPU |
+ 内存 |
+ 磁盘 |
+ 网络 ↓ |
+ 网络 ↑ |
diff --git a/public/js/app.js b/public/js/app.js
index 87f483a..c6a34a8 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -94,6 +94,7 @@
let currentSourceFilter = 'all';
let currentPage = 1;
let pageSize = 20;
+ let currentSort = { column: 'up', direction: 'desc' };
let myMap2D = null;
// ---- Initialize ----
@@ -161,6 +162,7 @@
// Server table row click delegator
dom.serverTableBody.addEventListener('click', (e) => {
+ // Don't trigger detail if clicking a button or something interactive inside (none currently)
const row = e.target.closest('tr');
if (row && !row.classList.contains('empty-row')) {
const instance = row.getAttribute('data-instance');
@@ -172,6 +174,18 @@
}
});
+ // Server table header sorting
+ const tableHeader = document.querySelector('.server-table thead');
+ if (tableHeader) {
+ tableHeader.addEventListener('click', (e) => {
+ const th = e.target.closest('th.sortable');
+ if (th) {
+ const column = th.getAttribute('data-sort');
+ handleHeaderSort(column);
+ }
+ });
+ }
+
// P95 Toggle
if (dom.legendP95) {
dom.legendP95.addEventListener('click', () => {
@@ -428,6 +442,7 @@
series: [{
type: 'effectScatter',
coordinateSystem: 'geo',
+ geoIndex: 0,
showEffectOn: 'render',
rippleEffect: { brushType: 'stroke', scale: 4, period: 4 },
symbolSize: 6,
@@ -485,7 +500,11 @@
}));
myMap2D.setOption({
- series: [{ data: geoData }]
+ series: [{
+ coordinateSystem: 'geo',
+ geoIndex: 0,
+ data: geoData
+ }]
});
// Update footer stats
@@ -549,12 +568,60 @@
filtered = allServersData.filter(s => s.source === currentSourceFilter);
}
- // Sort servers: online first, then alphabetically by name (job)
+ // Sort servers: online first, then by currentSort
filtered.sort((a, b) => {
- if (a.up !== b.up) return a.up ? -1 : 1;
- const nameA = a.job || '';
- const nameB = b.job || '';
- return nameA.localeCompare(nameB);
+ // Primary sort: Always put online servers first unless sorting by 'up' explicitly
+ if (currentSort.column !== 'up') {
+ if (a.up !== b.up) return a.up ? -1 : 1;
+ } else {
+ // Specifically sorting by status: Online vs Offline
+ if (a.up !== b.up) {
+ const val = a.up ? -1 : 1;
+ return currentSort.direction === 'asc' ? -val : val;
+ }
+ }
+
+ // Secondary sort based on user choice
+ let valA, valB;
+ switch (currentSort.column) {
+ case 'up':
+ return 0; // Already handled above
+ case 'job':
+ valA = a.job || '';
+ valB = b.job || '';
+ return currentSort.direction === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA);
+ case 'source':
+ valA = a.source || '';
+ valB = b.source || '';
+ return currentSort.direction === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA);
+ case 'cpu':
+ valA = a.cpuPercent || 0;
+ valB = b.cpuPercent || 0;
+ break;
+ case 'mem':
+ valA = a.memTotal > 0 ? (a.memUsed / a.memTotal) : 0;
+ valB = b.memTotal > 0 ? (b.memUsed / b.memTotal) : 0;
+ break;
+ case 'disk':
+ valA = a.diskTotal > 0 ? (a.diskUsed / a.diskTotal) : 0;
+ valB = b.diskTotal > 0 ? (b.diskUsed / b.diskTotal) : 0;
+ break;
+ case 'netRx':
+ valA = a.netRx || 0;
+ valB = b.netRx || 0;
+ break;
+ case 'netTx':
+ valA = a.netTx || 0;
+ valB = b.netTx || 0;
+ break;
+ default:
+ valA = a.job || '';
+ valB = b.job || '';
+ return valA.localeCompare(valB);
+ }
+
+ if (currentSort.direction === 'asc') return valA - valB;
+ return valB - valA;
});
const totalFiltered = filtered.length;
@@ -601,6 +668,31 @@
renderFilteredServers();
};
+ function handleHeaderSort(column) {
+ if (currentSort.column === column) {
+ // Toggle direction
+ currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
+ } else {
+ currentSort.column = column;
+ currentSort.direction = 'desc'; // Default to desc for most metrics
+ }
+
+ // Update UI headers
+ const headers = document.querySelectorAll('.server-table th.sortable');
+ headers.forEach(th => {
+ const col = th.getAttribute('data-sort');
+ if (col === currentSort.column) {
+ th.classList.add('active');
+ th.setAttribute('data-dir', currentSort.direction);
+ } else {
+ th.classList.remove('active');
+ th.removeAttribute('data-dir');
+ }
+ });
+
+ renderFilteredServers();
+ }
+
// ---- Server Table ----
function updateServerTable(servers) {
if (!servers || servers.length === 0) {