Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 83 additions & 29 deletions queries/cdmq/web-ui/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import DebugConsole from './components/DebugConsole';
import './index.css';

// Encode workflow state into a URL hash string
function encodeState(filters, selectedRunIds, view, groupByList, hiddenFields, supplementalMetrics) {
function encodeState(filters, selectedIterationIds, view, groupByList, hiddenFields, supplementalMetrics, deepDiveMetrics, deepDiveIterations, columnOrder, columnHidden) {
var state = {};
if (filters) {
if (filters.benchmark) state.benchmark = filters.benchmark;
Expand All @@ -21,19 +21,26 @@ function encodeState(filters, selectedRunIds, view, groupByList, hiddenFields, s
if (filters.tags && filters.tags.length > 0) state.tags = filters.tags;
if (filters.params && filters.params.length > 0) state.params = filters.params;
}
if (selectedRunIds && selectedRunIds.length > 0) state.selectedRuns = selectedRunIds;
if (selectedIterationIds && selectedIterationIds.length > 0) state.selectedIterations = selectedIterationIds;
if (columnOrder && columnOrder.length > 0) state.columnOrder = columnOrder;
if (columnHidden && columnHidden.length > 0) state.columnHidden = columnHidden;
if (view && view !== 'search') state.view = view;
if (groupByList && groupByList.length > 0) state.groupBy = groupByList;
if (hiddenFields && hiddenFields.length > 0) state.hidden = hiddenFields;
if (supplementalMetrics && supplementalMetrics.length > 0) {
state.metrics = supplementalMetrics.map(function (m) {
var entry = { source: m.source, type: m.type, display: m.display };
if (m.chartType && m.chartType !== 'bar') entry.chartType = m.chartType;
if (m.breakouts && m.breakouts.length > 0) entry.breakouts = m.breakouts;
if (m.filter) entry.filter = m.filter;
if (m.sampleIndex != null) entry.sampleIndex = m.sampleIndex;
return entry;
});
// Only encode compare/dive state when not on search view
if (view && view !== 'search') {
if (groupByList && groupByList.length > 0) state.groupBy = groupByList;
if (hiddenFields && hiddenFields.length > 0) state.hidden = hiddenFields;
if (supplementalMetrics && supplementalMetrics.length > 0) {
state.metrics = supplementalMetrics.map(function (m) {
var entry = { source: m.source, type: m.type, display: m.display };
if (m.chartType && m.chartType !== 'bar') entry.chartType = m.chartType;
if (m.breakouts && m.breakouts.length > 0) entry.breakouts = m.breakouts;
if (m.filter) entry.filter = m.filter;
if (m.sampleIndex != null) entry.sampleIndex = m.sampleIndex;
return entry;
});
}
if (deepDiveMetrics && deepDiveMetrics.size > 0) state.deepDiveMetrics = Array.from(deepDiveMetrics);
if (deepDiveIterations && deepDiveIterations.size > 0) state.deepDiveIterations = Array.from(deepDiveIterations);
}
return '#' + encodeURIComponent(JSON.stringify(state));
}
Expand Down Expand Up @@ -68,9 +75,11 @@ export default function App() {
const lastFilters = useRef(null);
const restoredState = useRef(null);
const [restoredMetrics, setRestoredMetrics] = useState(null);
const [supplementalMetrics, setSupplementalMetrics] = useState([]); // lifted from CompareView
const [tableColumnOrder, setTableColumnOrder] = useState(null); // array of dim strings, null = auto
const [tableHiddenDims, setTableHiddenDims] = useState([]); // array of hidden dim strings
const [deepDiveMetrics, setDeepDiveMetrics] = useState(new Set()); // Set of "source::type" strings
const [deepDiveIterations, setDeepDiveIterations] = useState(new Set()); // Set of iterationId strings (max 6)
const [deepDiveConfigs, setDeepDiveConfigs] = useState([]); // snapshot of supplemental metrics for deep dive

// On mount, check for state in URL hash
// Don't switch view yet — wait until search completes and selections are applied
Expand Down Expand Up @@ -111,17 +120,56 @@ export default function App() {
// Save current filters for Share button (SearchPanel may not be mounted in compare view)
if (searchRef.current) lastFilters.current = searchRef.current.getFilters();
var state = restoredState.current;
if (state && state.selectedRuns && state.selectedRuns.length > 0) {
var runSet = new Set(state.selectedRuns);
var hasSelections = (state && state.selectedIterations && state.selectedIterations.length > 0) ||
(state && state.selectedRuns && state.selectedRuns.length > 0);
if (hasSelections) {
var toSelect = new Map();
results.forEach(function (it) {
if (runSet.has(it.runId)) toSelect.set(it.iterationId, it);
});
if (state.selectedIterations) {
var iterSet = new Set(state.selectedIterations);
results.forEach(function (it) {
if (iterSet.has(it.iterationId)) toSelect.set(it.iterationId, it);
});
} else {
var runSet = new Set(state.selectedRuns);
results.forEach(function (it) {
if (runSet.has(it.runId)) toSelect.set(it.iterationId, it);
});
}
if (toSelect.size > 0) setSelected(toSelect);
// Restore deep dive state from URL
if (state.deepDiveMetrics) setDeepDiveMetrics(new Set(state.deepDiveMetrics));
if (state.deepDiveIterations) setDeepDiveIterations(new Set(state.deepDiveIterations));
// Hydrate supplementalMetrics from restored metrics configs
// so deep dive and compare have breakout/filter configs immediately
if (state.metrics && state.metrics.length > 0) {
setSupplementalMetrics(state.metrics.map(function (m) {
return {
source: m.source, type: m.type, values: {},
display: m.display || 'panel', chartType: m.chartType || 'bar',
filter: m.filter || '', sampleIndex: m.sampleIndex || null,
breakouts: m.breakouts || [], remainingBreakouts: [],
loading: false,
};
}));
}
// Restore column ordering
if (state.columnOrder) setTableColumnOrder(state.columnOrder);
if (state.columnHidden) setTableHiddenDims(state.columnHidden);
// Switch to the saved view now that selections are ready
if (state.view) setView(state.view);
// Clear restored state so it doesn't re-apply on next search
restoredState.current = null;
} else {
// New search (not URL restore): reset compare/dive state
setSelected(new Map());
setGroupByList([]);
setHiddenFields([]);
setSupplementalMetrics([]);
setDeepDiveMetrics(new Set());
setDeepDiveIterations(new Set());
setRestoredMetrics(null);
setTableColumnOrder(null);
setTableHiddenDims([]);
}
}, []);

Expand Down Expand Up @@ -160,15 +208,20 @@ export default function App() {

const clearSelected = useCallback(function () {
setSelected(new Map());
setGroupByList([]);
setHiddenFields([]);
setSupplementalMetrics([]);
setDeepDiveMetrics(new Set());
setDeepDiveIterations(new Set());
setRestoredMetrics(null);
setTableColumnOrder(null);
setTableHiddenDims([]);
}, []);

function handleShare() {
var filters = (searchRef.current ? searchRef.current.getFilters() : null) || lastFilters.current;
var runIdSet = new Set();
selected.forEach(function (it) { runIdSet.add(it.runId); });
var selectedRunIds = Array.from(runIdSet);
var suppMetrics = compareRef.current ? compareRef.current.getSupplementalMetrics() : null;
var hash = encodeState(filters, selectedRunIds, view, groupByList, hiddenFields, suppMetrics);
var selectedIterIds = Array.from(selected.keys());
var hash = encodeState(filters, selectedIterIds, view, groupByList, hiddenFields, supplementalMetrics, deepDiveMetrics, deepDiveIterations, tableColumnOrder, tableHiddenDims);
var url = window.location.origin + window.location.pathname + hash;
// Update the URL bar so the user can see and copy it directly
window.history.replaceState(null, '', hash);
Expand Down Expand Up @@ -212,9 +265,6 @@ export default function App() {
<button
className={view === 'deepdive' ? 'active' : ''}
onClick={() => {
if (compareRef.current) {
setDeepDiveConfigs(compareRef.current.getSupplementalMetrics() || []);
}
setView('deepdive');
}}
disabled={deepDiveIterations.size === 0 || deepDiveMetrics.size === 0}
Expand Down Expand Up @@ -250,16 +300,20 @@ export default function App() {
loading={loading}
onAddTagFilter={function (name, val) { if (searchRef.current) searchRef.current.addTagFilter(name, val); }}
onAddParamFilter={function (arg, val) { if (searchRef.current) searchRef.current.addParamFilter(arg, val); }}
columnOrder={tableColumnOrder}
onColumnOrderChange={setTableColumnOrder}
columnHidden={tableHiddenDims}
onColumnHiddenChange={setTableHiddenDims}
/>
</>
)}

{view === 'compare' && (
<CompareView ref={compareRef} selected={selected} groupByList={groupByList} setGroupByList={setGroupByList} hiddenFields={hiddenFields} setHiddenFields={setHiddenFields} restoredMetrics={restoredMetrics} deepDiveMetrics={deepDiveMetrics} setDeepDiveMetrics={setDeepDiveMetrics} deepDiveIterations={deepDiveIterations} setDeepDiveIterations={setDeepDiveIterations} />
<CompareView ref={compareRef} selected={selected} groupByList={groupByList} setGroupByList={setGroupByList} hiddenFields={hiddenFields} setHiddenFields={setHiddenFields} restoredMetrics={restoredMetrics} setRestoredMetrics={setRestoredMetrics} supplementalMetrics={supplementalMetrics} setSupplementalMetrics={setSupplementalMetrics} deepDiveMetrics={deepDiveMetrics} setDeepDiveMetrics={setDeepDiveMetrics} deepDiveIterations={deepDiveIterations} setDeepDiveIterations={setDeepDiveIterations} />
)}

{view === 'deepdive' && (
<DeepDiveView selected={(() => { var m = new Map(); selected.forEach(function (it, id) { if (deepDiveIterations.has(id)) m.set(id, it); }); return m; })()} deepDiveMetrics={deepDiveMetrics} metricConfigs={deepDiveConfigs} hiddenFields={hiddenFields} />
<DeepDiveView selected={(() => { var m = new Map(); selected.forEach(function (it, id) { if (deepDiveIterations.has(id)) m.set(id, it); }); return m; })()} deepDiveMetrics={deepDiveMetrics} metricConfigs={supplementalMetrics} hiddenFields={hiddenFields} />
)}

<DebugConsole />
Expand Down
73 changes: 38 additions & 35 deletions queries/cdmq/web-ui/src/components/CompareView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -506,10 +506,9 @@ function buildDimOptions(iterations) {
return opts;
}

const CompareView = forwardRef(function CompareView({ selected, groupByList, setGroupByList, hiddenFields, setHiddenFields, restoredMetrics, deepDiveMetrics, setDeepDiveMetrics, deepDiveIterations, setDeepDiveIterations }, ref) {
const CompareView = forwardRef(function CompareView({ selected, groupByList, setGroupByList, hiddenFields, setHiddenFields, restoredMetrics, setRestoredMetrics, supplementalMetrics, setSupplementalMetrics, deepDiveMetrics, setDeepDiveMetrics, deepDiveIterations, setDeepDiveIterations }, ref) {
var [metricValues, setMetricValues] = useState({});
var [loading, setLoading] = useState(false);
var [supplementalMetrics, setSupplementalMetrics] = useState([]); // [{ source, type, values: {iterId: {mean,...}} }]
var [availableSources, setAvailableSources] = useState(null);
var [availableTypes, setAvailableTypes] = useState(null);
var [addMetricSource, setAddMetricSource] = useState('');
Expand Down Expand Up @@ -541,12 +540,6 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set
return Array.from(selected.values());
}, [selected]);

useImperativeHandle(ref, function () {
return {
getSupplementalMetrics: function () { return supplementalMetrics; },
};
}, [supplementalMetrics]);

// Helper to get run IDs and date range from iterations
function getRunContext() {
var runIdSet = new Set();
Expand All @@ -565,7 +558,6 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set
if (iterations.length === 0) return;
var ctx = getRunContext();
setLoading(true);
setSupplementalMetrics([]);
timeWork('Fetch metric values for compare (' + iterations.length + ' iterations)', function () {
return api.getIterationMetricValues(ctx.runIds, ctx.start, ctx.end);
}).then(function (res) {
Expand Down Expand Up @@ -617,43 +609,41 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set
}
}, [iterations.length > 0 && dimOptions.length > 1]);

// Restore supplemental metrics from URL state
var restoredMetricsApplied = useRef(false);
// Fetch values for supplemental metrics that have empty values (e.g., hydrated from URL with configs only)
useEffect(function () {
if (restoredMetricsApplied.current) return;
if (!restoredMetrics || restoredMetrics.length === 0) return;
if (iterations.length === 0) return;
restoredMetricsApplied.current = true;
if (supplementalMetrics.length === 0) return;
var needsFetch = supplementalMetrics.some(function (m) { return !m.values || Object.keys(m.values).length === 0; });
if (!needsFetch) return;
var ctx = getRunContext();
restoredMetrics.forEach(function (rm) {
supplementalMetrics.forEach(function (sm, si) {
if (sm.values && Object.keys(sm.values).length > 0) return;
var bestIndices = computeBestSampleIndices();
var sIdx = rm.sampleIndex != null ? rm.sampleIndex : bestIndices;
timeWork('Restore ' + rm.source + '::' + rm.type, function () {
var sIdx = sm.sampleIndex != null ? sm.sampleIndex : bestIndices;
timeWork('Fetch ' + sm.source + '::' + sm.type, function () {
return api.getSupplementalMetric({
iterations: ctx.iterations, start: ctx.start, end: ctx.end,
source: rm.source, type: rm.type,
breakout: rm.breakouts || [],
filter: rm.filter || null,
source: sm.source, type: sm.type,
breakout: sm.breakouts || [],
filter: sm.filter || null,
sampleIndex: sIdx,
});
}).then(function (res) {
setSupplementalMetrics(function (prev) {
return prev.concat([{
source: rm.source,
type: rm.type,
values: res.values || {},
display: rm.display || 'panel',
chartType: rm.chartType || 'bar',
filter: rm.filter || '',
sampleIndex: sIdx,
breakouts: rm.breakouts || [],
remainingBreakouts: res.remainingBreakouts || [],
loading: false,
}]);
var next = prev.slice();
var idx = next.findIndex(function (m) { return m.source === sm.source && m.type === sm.type; });
if (idx >= 0) {
next[idx] = Object.assign({}, next[idx], {
values: res.values || {},
remainingBreakouts: res.remainingBreakouts || [],
loading: false,
});
}
return next;
});
});
});
}, [iterations.length > 0]);
}, [iterations.length > 0, supplementalMetrics.length]);

var handleShowAddMetric = useCallback(function () {
setShowAddMetric(true);
Expand Down Expand Up @@ -960,8 +950,21 @@ const CompareView = forwardRef(function CompareView({ selected, groupByList, set
}, []);

var handleRemoveMetric = useCallback(function (idx) {
setSupplementalMetrics(function (prev) { return prev.filter(function (_, i) { return i !== idx; }); });
}, []);
setSupplementalMetrics(function (prev) {
var removed = prev[idx];
if (removed && deepDiveMetrics) {
var metricKey = removed.source + '::' + removed.type;
if (deepDiveMetrics.has(metricKey)) {
setDeepDiveMetrics(function (prevDD) {
var next = new Set(prevDD);
next.delete(metricKey);
return next;
});
}
}
return prev.filter(function (_, i) { return i !== idx; });
});
}, [deepDiveMetrics]);

// Build chart data: one entry per iteration, sorted/grouped, with gaps between groups
var charts = useMemo(function () {
Expand Down
2 changes: 1 addition & 1 deletion queries/cdmq/web-ui/src/components/DeepDiveView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export default function DeepDiveView({ selected, deepDiveMetrics, metricConfigs:
});

return function () { abortRef.current = true; };
}, [iterations.length, metricList.join(','), resolution, zoomRange]);
}, [iterations.length, metricList.join(','), JSON.stringify(metricConfigsProp), resolution, zoomRange]);

if (loadingPeriods) {
return (
Expand Down
46 changes: 40 additions & 6 deletions queries/cdmq/web-ui/src/components/IterationTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ function flattenTree(node, depth, coveredDims, groupDims) {
return rows;
}

export default function IterationTable({ iterations, selected, onToggleSelect, onToggleSelectAll, loading, onAddTagFilter, onAddParamFilter }) {
export default function IterationTable({ iterations, selected, onToggleSelect, onToggleSelectAll, loading, onAddTagFilter, onAddParamFilter, columnOrder, onColumnOrderChange, columnHidden, onColumnHiddenChange }) {
const [sortKey, setSortKey] = useState(null);
const [sortDir, setSortDir] = useState('asc');
const [paramFilter, setParamFilter] = useState('');
Expand Down Expand Up @@ -377,17 +377,51 @@ export default function IterationTable({ iterations, selected, onToggleSelect, o
}, [iterations]);

// Group-by dimensions as state so user can reorder/hide/sort
const [groupDims, setGroupDims] = useState([]);
const [hiddenDims, setHiddenDims] = useState([]);
const [groupDims, setGroupDimsLocal] = useState([]);
const [hiddenDims, setHiddenDimsLocal] = useState([]);
const [dimSortDir, setDimSortDir] = useState({}); // { dim: 'asc' | 'desc' }, default asc
const prevIterCount = useRef(0);

// Auto-compute group dims when iterations change (reset hidden/sort)
function setGroupDims(newDims) {
setGroupDimsLocal(newDims);
if (onColumnOrderChange) onColumnOrderChange(newDims);
}
function setHiddenDims(val) {
if (typeof val === 'function') {
setHiddenDimsLocal(function (prev) {
var next = val(prev);
if (onColumnHiddenChange) onColumnHiddenChange(next);
return next;
});
} else {
setHiddenDimsLocal(val);
if (onColumnHiddenChange) onColumnHiddenChange(val);
}
}

// Auto-compute group dims when iterations change, merging with saved order
useEffect(function () {
if (iterations.length !== prevIterCount.current) {
prevIterCount.current = iterations.length;
setGroupDims(computeGroupDims(iterations));
setHiddenDims([]);
var computed = computeGroupDims(iterations);
if (columnOrder && columnOrder.length > 0) {
// Merge: keep saved order for dims that still exist, append new ones
var computedSet = new Set(computed);
var merged = columnOrder.filter(function (d) { return computedSet.has(d); });
computed.forEach(function (d) { if (merged.indexOf(d) < 0) merged.push(d); });
setGroupDimsLocal(merged);
if (onColumnOrderChange) onColumnOrderChange(merged);
} else {
setGroupDimsLocal(computed);
if (onColumnOrderChange) onColumnOrderChange(computed);
}
if (columnHidden && columnHidden.length > 0) {
var validHidden = columnHidden.filter(function (d) { return new Set(computed).has(d); });
setHiddenDimsLocal(validHidden);
if (onColumnHiddenChange) onColumnHiddenChange(validHidden);
} else {
setHiddenDimsLocal([]);
}
setDimSortDir({});
}
}, [iterations]);
Expand Down
Loading