feat(metrics): Add drag-and-drop reordering to metric panels#112671
feat(metrics): Add drag-and-drop reordering to metric panels#112671nsdeschenes merged 28 commits intomasterfrom
Conversation
Add a hook that encodes a reordered list of metric queries into the URL query params and navigates, providing the foundation for drag-and-drop reordering of metric panels. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…olbar MetricPanel now accepts ref, style, dragListeners, and isAnyDragging props. When a drag is active, chart content is replaced with a placeholder to avoid expensive re-renders. MetricToolbar renders a DragReorderButton when drag listeners are provided. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add SortableMetricPanel wrapper using @dnd-kit/sortable and integrate DndContext into MetricsTabBodySection. Panels can now be reordered via drag-and-drop, with the new order persisted to URL query params. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Hide the drag reorder button and remove its grid column when there is only a single metric query, since reordering is not meaningful with one item. Co-Authored-By: Claude Opus 4.6 <noreply@example.com>
Forward dnd-kit aria attributes through MetricPanel to the DOM so keyboard and screen reader users can interact with sortable panels. Replace index-based uniqueId array with a Map keyed by encoded query params so deletions and mid-list insertions no longer desync React keys with their queries. Move offsetHeight measurement from the render body into a useLayoutEffect to avoid forced synchronous reflow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add dependency array to useLayoutEffect to avoid forcing layout reflow on every render. Include index in sortable key generation so duplicate metric queries get distinct stable IDs. Wrap sortableItems in useMemo so onDragEnd useCallback actually memoizes. Fix leading space in grid columns template when drag handle is absent. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously all panels displayed the placeholder text during drag. Now only the panel being actively dragged shows the message, while other panels show a blank placeholder to avoid chart rendering. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…component Replace useLayoutEffect height capture with a ref callback on the container, removing an extra ref and effect. Extract the drag placeholder into a DnDPlaceholder component for clarity. Co-Authored-By: Claude Opus 4.6 <noreply@example.com>
…lines Front-load the informational content before the personality quip, per content & voice guidelines. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The sortable key included the array index, so every reorder changed moved items' keys, causing React to unmount and remount panels. This lost local state and forced full ECharts re-initialization — the exact cost the placeholder optimization was designed to avoid. Use occurrence count instead of index to disambiguate duplicate queries, keeping keys stable across reorders. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move the sortable dnd hook out of metricsTab.tsx into a dedicated hooks file for better organization. Co-Authored-By: Claude Opus 4.6 <noreply@example.com>
static/app/views/explore/metrics/hooks/useSortableMetricQueries.tsx
Outdated
Show resolved
Hide resolved
static/app/views/explore/metrics/hooks/useSortableMetricQueries.tsx
Outdated
Show resolved
Hide resolved
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show the Samples/Aggregates tab list with placeholder content on the table side during drag-and-drop reordering, giving a better visual indication of the panel layout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap SideBySideOrientation in a React Activity so it stays mounted but hidden during drag-and-drop reordering. This prevents the table from refetching data when dragging ends. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace Map iterator .forEach() with for...of loop for broader JS engine compatibility. Simplify the DnD table placeholder to use a plain Placeholder instead of rendering MetricInfoTabList outside its required TabStateProvider context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d5eb0ae. Configure here.
MetricInfoTabList is only used internally within its module, so the export keyword is unnecessary. Co-Authored-By: Claude Opus 4.6 <noreply@example.com>
The tab list (Samples/Aggregates) should render before additionalActions in the Flex container so it appears on the left, with the PanelPositionSelector and HideContentButton on the right. Co-Authored-By: Claude Opus 4.6 <noreply@example.com>
…cemetrics-add-dnd-to-metrics-page # Conflicts: # static/app/views/explore/metrics/metricPanel/index.tsx # static/app/views/explore/metrics/metricToolbar/index.tsx # static/app/views/explore/metrics/metricsTab.tsx
Move stable label state alongside metric query reorder operations so drag-and-drop does not relabel toolbar badges by position. Add a regression test that verifies labels remain attached to query identity after reorder. Co-Authored-By: Codex <noreply@openai.com>
| expect(result.current[1]).toEqual(expect.objectContaining({label: 'ƒ1'})); | ||
| }); | ||
|
|
||
| it('keeps labels attached to query identity when reordering', () => { |
static/app/views/explore/metrics/hooks/useSortableMetricQueries.tsx
Outdated
Show resolved
Hide resolved
|
|
||
| const sensors = useSensors( | ||
| useSensor(PointerSensor), | ||
| useSensor(KeyboardSensor, { |
There was a problem hiding this comment.
Whoa, keyboard navigation for DnD is so cool.
There was a problem hiding this comment.
Yeah!! I was impressed trying it out, they really nailed it. What a relief for accessibility too.
|
Ah, one thing I noticed was, we tried to make inserts for metric queries add between aggregates and equations and it gets a little funky now that we can also rearrange functions. Is it possible to do something like.. If you haven't re-arranged them then they add between aggregates and equations, otherwise we can just append them to the bottom? Not entirely sure what the best way to handle it is, but this seems reasonable. Open to suggestions on dealing with these! This doesn't need to block your PR though if you want to handle it separately |
Replace the metrics drag-and-drop UUID bookkeeping with the stable query labels that now persist across query mutations. Keep the sortable panel IDs and React keys aligned with those labels, and add a focused hook test that covers duplicate-query reordering. Co-Authored-By: Codex <noreply@openai.com>
I don't think this should be too hard, we just need to split up them into two DnD contexts', i'll take a stab at it here |
Allow the sortable metrics hook to expose a filtered view while still reordering against the original query list. This keeps sectioned views stable and ignores drops that cross section boundaries. Co-Authored-By: Codex <noreply@openai.com>
Render aggregate queries and equations in their own sortable sections on the refreshed metrics tab. This keeps each panel group visually distinct while preserving the drag state needed for section-local reordering. Co-Authored-By: Codex <noreply@openai.com>
Sentry Snapshot Testing
|

Users with multiple metric panels can now grab the drag handle in the toolbar and reorder panels. The new order is persisted to URL query params so it survives page reloads. During a drag, chart content is replaced with a lightweight placeholder to avoid expensive re-renders of ECharts instances.
Key details:
useReorderMetricQuerieshook encodes the reordered list back into URL paramsCloses EXP-827
Example:
Screen.Recording.2026-04-10.at.11.58.27.mov