Skip to content

Commit 86f92b9

Browse files
committed
asimview: add drag-and-drop to reorder metric checkboxes
The metric checkboxes can now be dragged and dropped to customize the order in which charts are rendered. The hover indicator shows on the left or right side of the target based on where the item will be inserted. The custom order is persisted to localStorage and restored across browser sessions.
1 parent 39b6044 commit 86f92b9

File tree

1 file changed

+177
-28
lines changed

1 file changed

+177
-28
lines changed

pkg/kv/kvserver/asim/tests/cmd/asimview/viewer.html

Lines changed: 177 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,36 @@
194194
background: #f0f8ff;
195195
color: #333;
196196
}
197+
198+
.metric-checkbox-label {
199+
display: flex;
200+
align-items: center;
201+
gap: 0.5rem;
202+
cursor: grab;
203+
padding: 0.5rem;
204+
border-radius: 4px;
205+
transition: background 0.2s;
206+
user-select: none;
207+
}
208+
209+
.metric-checkbox-label:hover {
210+
background: #f0f0f0;
211+
}
212+
213+
.metric-checkbox-label.dragging {
214+
opacity: 0.5;
215+
cursor: grabbing;
216+
}
217+
218+
.metric-checkbox-label.drag-over-left {
219+
background: #e0e0e0;
220+
border-left: 3px solid #007acc;
221+
}
222+
223+
.metric-checkbox-label.drag-over-right {
224+
background: #e0e0e0;
225+
border-right: 3px solid #007acc;
226+
}
197227
</style>
198228
</head>
199229
<body>
@@ -222,7 +252,7 @@ <h3>Select Test Files</h3>
222252
</div>
223253

224254
<div class="file-selector" id="metricSelector" style="display: none;">
225-
<h3>Select Metrics to Display</h3>
255+
<h3>Select Metrics to Display <span style="font-size: 0.8em; font-weight: normal; color: #666;">(drag to reorder)</span></h3>
226256
<div id="metricCheckboxes" style="display: flex; flex-wrap: wrap; gap: 1rem;"></div>
227257
</div>
228258

@@ -240,6 +270,7 @@ <h3>Select Metrics to Display</h3>
240270
// Default metrics to show (in order)
241271
const defaultMetrics = ['cpu', 'qps', 'write_bytes_per_second', 'replicas', 'leases'];
242272
let selectedMetrics = new Set();
273+
let metricOrder = []; // Custom order for metrics
243274

244275
// Clean Zoom State Manager - Option 1: Plugin for input only
245276
const ZoomManager = {
@@ -459,6 +490,23 @@ <h3>Select Metrics to Display</h3>
459490
localStorage.setItem('asimview-selected-metrics', JSON.stringify([...selectedMetrics]));
460491
}
461492

493+
// Load metric order from localStorage
494+
function loadMetricOrder() {
495+
const saved = localStorage.getItem('asimview-metric-order');
496+
if (saved) {
497+
try {
498+
metricOrder = JSON.parse(saved);
499+
} catch (e) {
500+
metricOrder = [];
501+
}
502+
}
503+
}
504+
505+
// Save metric order to localStorage
506+
function saveMetricOrder() {
507+
localStorage.setItem('asimview-metric-order', JSON.stringify(metricOrder));
508+
}
509+
462510
// Load file selection from localStorage
463511
function loadFileSelection() {
464512
const saved = localStorage.getItem('asimview-selected-files');
@@ -480,6 +528,7 @@ <h3>Select Metrics to Display</h3>
480528
// Fetch available files on page load
481529
window.addEventListener('DOMContentLoaded', async () => {
482530
loadMetricPreferences();
531+
loadMetricOrder();
483532
await fetchAvailableFiles();
484533

485534
// Restore saved file selection if any files match
@@ -877,26 +926,19 @@ <h3>Select Metrics to Display</h3>
877926
}
878927
});
879928

880-
// Update metric selector checkboxes
929+
// Update metric selector checkboxes (this also updates metricOrder)
881930
updateMetricSelector(allMetrics);
882931

883-
// Sort metrics according to defaultMetrics order, then alphabetically for others
884-
const sortedMetrics = Array.from(allMetrics).sort((a, b) => {
885-
const aIndex = defaultMetrics.indexOf(a);
886-
const bIndex = defaultMetrics.indexOf(b);
887-
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
888-
if (aIndex !== -1) return -1;
889-
if (bIndex !== -1) return 1;
890-
return a.localeCompare(b);
891-
});
932+
// Use metricOrder for rendering charts
933+
const orderedMetrics = metricOrder.filter(m => allMetrics.has(m));
892934

893935
// Filter to only selected metrics
894-
const metricsToRender = sortedMetrics.filter(m => selectedMetrics.has(m));
936+
const metricsToRender = orderedMetrics.filter(m => selectedMetrics.has(m));
895937

896938
// Pre-calculate all Y-axis ranges once (for all metrics, not just selected)
897939
const metricRanges = new Map();
898940

899-
sortedMetrics.forEach(metricName => {
941+
orderedMetrics.forEach(metricName => {
900942
let globalMin = Infinity;
901943
let globalMax = -Infinity;
902944

@@ -925,7 +967,7 @@ <h3>Select Metrics to Display</h3>
925967
});
926968

927969
// Now create sections for ALL metrics (we'll hide/show them based on selection)
928-
sortedMetrics.forEach(metricName => {
970+
orderedMetrics.forEach(metricName => {
929971
const section = document.createElement('div');
930972
section.className = 'metric-section';
931973
section.dataset.metric = metricName;
@@ -1069,22 +1111,31 @@ <h3>Select Metrics to Display</h3>
10691111
selectorDiv.style.display = 'block';
10701112
checkboxContainer.innerHTML = '';
10711113

1072-
// Sort metrics according to defaultMetrics order, then alphabetically
1073-
const sortedMetrics = Array.from(availableMetrics).sort((a, b) => {
1074-
const aIndex = defaultMetrics.indexOf(a);
1075-
const bIndex = defaultMetrics.indexOf(b);
1076-
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
1077-
if (aIndex !== -1) return -1;
1078-
if (bIndex !== -1) return 1;
1079-
return a.localeCompare(b);
1080-
});
1114+
// Update metric order: add new metrics, remove unavailable ones
1115+
const metricsArray = Array.from(availableMetrics);
1116+
const newMetrics = metricsArray.filter(m => !metricOrder.includes(m));
1117+
metricOrder = metricOrder.filter(m => availableMetrics.has(m));
10811118

1082-
sortedMetrics.forEach(metric => {
1119+
// Add new metrics in default order
1120+
if (newMetrics.length > 0) {
1121+
const sortedNew = newMetrics.sort((a, b) => {
1122+
const aIndex = defaultMetrics.indexOf(a);
1123+
const bIndex = defaultMetrics.indexOf(b);
1124+
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
1125+
if (aIndex !== -1) return -1;
1126+
if (bIndex !== -1) return 1;
1127+
return a.localeCompare(b);
1128+
});
1129+
metricOrder.push(...sortedNew);
1130+
}
1131+
1132+
// Create draggable checkbox labels
1133+
metricOrder.forEach((metric, index) => {
10831134
const label = document.createElement('label');
1084-
label.style.display = 'flex';
1085-
label.style.alignItems = 'center';
1086-
label.style.gap = '0.5rem';
1087-
label.style.cursor = 'pointer';
1135+
label.className = 'metric-checkbox-label';
1136+
label.draggable = true;
1137+
label.dataset.metric = metric;
1138+
label.dataset.index = index;
10881139

10891140
const checkbox = document.createElement('input');
10901141
checkbox.type = 'checkbox';
@@ -1113,6 +1164,104 @@ <h3>Select Metrics to Display</h3>
11131164
label.appendChild(checkbox);
11141165
label.appendChild(text);
11151166
checkboxContainer.appendChild(label);
1167+
1168+
// Drag and drop handlers
1169+
label.addEventListener('dragstart', (e) => {
1170+
label.classList.add('dragging');
1171+
e.dataTransfer.effectAllowed = 'move';
1172+
e.dataTransfer.setData('text/plain', metric);
1173+
});
1174+
1175+
label.addEventListener('dragend', (e) => {
1176+
label.classList.remove('dragging');
1177+
document.querySelectorAll('.metric-checkbox-label').forEach(l => {
1178+
l.classList.remove('drag-over-left', 'drag-over-right');
1179+
});
1180+
});
1181+
1182+
label.addEventListener('dragover', (e) => {
1183+
e.preventDefault();
1184+
e.dataTransfer.dropEffect = 'move';
1185+
1186+
const dragging = document.querySelector('.dragging');
1187+
if (dragging && dragging !== label) {
1188+
// Determine which side of the target we're hovering over
1189+
const rect = label.getBoundingClientRect();
1190+
const midpoint = rect.left + rect.width / 2;
1191+
const isLeftSide = e.clientX < midpoint;
1192+
1193+
// Remove both classes first
1194+
label.classList.remove('drag-over-left', 'drag-over-right');
1195+
1196+
// Add the appropriate class
1197+
if (isLeftSide) {
1198+
label.classList.add('drag-over-left');
1199+
label.dataset.dropSide = 'left';
1200+
} else {
1201+
label.classList.add('drag-over-right');
1202+
label.dataset.dropSide = 'right';
1203+
}
1204+
}
1205+
});
1206+
1207+
label.addEventListener('dragleave', (e) => {
1208+
label.classList.remove('drag-over-left', 'drag-over-right');
1209+
delete label.dataset.dropSide;
1210+
});
1211+
1212+
label.addEventListener('drop', (e) => {
1213+
e.preventDefault();
1214+
const dropSide = label.dataset.dropSide || 'right';
1215+
label.classList.remove('drag-over-left', 'drag-over-right');
1216+
delete label.dataset.dropSide;
1217+
1218+
const draggedMetric = e.dataTransfer.getData('text/plain');
1219+
if (draggedMetric && draggedMetric !== metric) {
1220+
// Reorder the metrics
1221+
const oldIndex = metricOrder.indexOf(draggedMetric);
1222+
const targetIndex = metricOrder.indexOf(metric);
1223+
1224+
if (oldIndex !== -1 && targetIndex !== -1) {
1225+
// Remove the dragged item
1226+
metricOrder.splice(oldIndex, 1);
1227+
1228+
// Calculate new insert position
1229+
let insertIndex = metricOrder.indexOf(metric);
1230+
if (dropSide === 'right') {
1231+
insertIndex += 1;
1232+
}
1233+
1234+
// Insert at the new position
1235+
metricOrder.splice(insertIndex, 0, draggedMetric);
1236+
1237+
saveMetricOrder();
1238+
1239+
// Re-render checkboxes and charts
1240+
updateMetricSelector(availableMetrics);
1241+
reorderChartSections();
1242+
}
1243+
}
1244+
});
1245+
});
1246+
}
1247+
1248+
// Reorder chart sections to match metricOrder
1249+
function reorderChartSections() {
1250+
const chartsDiv = document.getElementById('charts');
1251+
const sections = Array.from(chartsDiv.querySelectorAll('.metric-section'));
1252+
1253+
// Sort sections based on metricOrder
1254+
sections.sort((a, b) => {
1255+
const aMetric = a.dataset.metric;
1256+
const bMetric = b.dataset.metric;
1257+
const aIndex = metricOrder.indexOf(aMetric);
1258+
const bIndex = metricOrder.indexOf(bMetric);
1259+
return aIndex - bIndex;
1260+
});
1261+
1262+
// Re-append in correct order
1263+
sections.forEach(section => {
1264+
chartsDiv.appendChild(section);
11161265
});
11171266
}
11181267

0 commit comments

Comments
 (0)