From b02ad91319cce0921168fcbd714896525dcffd22 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:24:07 +0200 Subject: [PATCH] fix(studio): stop unified logs bar chart animation replay on click FE-3912 (#47910) ## Problem In the unified logs dashboard, clicking an activity bar in the chart restarted the fly-by entrance animation instead of selecting that bar. There was also no way to select a single bar by clicking it (only drag-to-select worked), and the selection menu was centered over the selection rather than anchored to its start. ## Fix - Disabled Recharts' `isAnimationActive` on the stacked `Bar` elements so re-renders from selection state no longer replay the entrance animation. - Clicking a single bar now selects that bar's full time bucket and opens the "Filter logs to selected range" menu, the same as dragging across it. - The menu is anchored to the start (leftmost pixel) of the selection instead of the mouse release position. ## How to test - Open a project's unified logs page - Wait for the activity chart to finish its initial load - Click on a single bar - Expected result: the bar is highlighted, the chart does not replay its entrance animation, and the range-filter menu appears anchored at the start of that bar - Drag across multiple bars - Expected result: the range-filter menu appears anchored at the start of the dragged selection, not centered over it ## Summary by CodeRabbit - **Bug Fixes** - Fixed chart highlight popovers to anchor consistently to the start of the selected range. - Improved popover behavior so it updates correctly when the position changes. - Corrected zoom-in filtering when the selected range contains a single timestamp. - **UI Improvements** - Reduced conflicts between tooltips and selection popovers by rendering tooltip content only when appropriate. - Disabled stacked-bar animations for error/warning/success to make chart interactions feel steadier. --- .../ui/Charts/ChartHighlightActions.tsx | 10 ++- .../ui/Charts/useChartHighlight.tsx | 15 +++- .../components/ui/DataTable/TimelineChart.tsx | 79 +++++++++++++------ 3 files changed, 74 insertions(+), 30 deletions(-) diff --git a/apps/studio/components/ui/Charts/ChartHighlightActions.tsx b/apps/studio/components/ui/Charts/ChartHighlightActions.tsx index ddc0460555e..2184b6ff766 100644 --- a/apps/studio/components/ui/Charts/ChartHighlightActions.tsx +++ b/apps/studio/components/ui/Charts/ChartHighlightActions.tsx @@ -48,7 +48,7 @@ export const ChartHighlightActions = ({ const formatChartDate = useFormatDateTime() useEffect(() => { - setIsOpen(!!chartHighlight?.popoverPosition && selectedRangeStart !== selectedRangeEnd) + setIsOpen(!!chartHighlight?.popoverPosition) }, [chartHighlight?.popoverPosition]) const ctx: ChartHighlightActionContext | undefined = @@ -80,8 +80,12 @@ export const ChartHighlightActions = ({ return [...defaultActions, ...provided] }, [defaultActions, actions]) + const positionKey = chartHighlight?.popoverPosition + ? `${chartHighlight.popoverPosition.x}-${chartHighlight.popoverPosition.y}` + : 'closed' + return ( - + clearHighlight?.()} onInteractOutside={(e) => { const target = e.target as Element | null diff --git a/apps/studio/components/ui/Charts/useChartHighlight.tsx b/apps/studio/components/ui/Charts/useChartHighlight.tsx index e4f8593e9af..1fa248b8b80 100644 --- a/apps/studio/components/ui/Charts/useChartHighlight.tsx +++ b/apps/studio/components/ui/Charts/useChartHighlight.tsx @@ -4,8 +4,12 @@ import { useState } from 'react' type ChartHighlightMouseEvent = { activeLabel?: string coordinates?: string + chartX?: number + chartY?: number } +type Pixel = { x: number; y: number } + export interface ChartHighlight { left: string | undefined right: string | undefined @@ -28,6 +32,7 @@ export function useChartHighlight(): ChartHighlight { const [isSelecting, setIsSelecting] = useState(false) const [popoverPosition, setPopoverPosition] = useState<{ x: number; y: number } | null>(null) const [initialPoint, setInitialPoint] = useState(undefined) + const [anchorPixel, setAnchorPixel] = useState(undefined) const handleMouseDown = (e: ChartHighlightMouseEvent) => { clearHighlight() @@ -37,6 +42,9 @@ export function useChartHighlight(): ChartHighlight { setRight(e.activeLabel) setInitialPoint(e.activeLabel) setCoordinates({ left: e.coordinates, right: e.coordinates }) + if (typeof e.chartX === 'number' && typeof e.chartY === 'number') { + setAnchorPixel({ x: e.chartX, y: e.chartY }) + } } const handleMouseMove = (e: ChartHighlightMouseEvent) => { @@ -69,7 +77,11 @@ export function useChartHighlight(): ChartHighlight { setIsSelecting(false) setInitialPoint(undefined) - if ( + // Anchor the popover to where the selection started rather than wherever + // the mouse happened to be released. + if (anchorPixel) { + setPopoverPosition(anchorPixel) + } else if ( typeof e === 'object' && e !== null && 'chartX' in e && @@ -87,6 +99,7 @@ export function useChartHighlight(): ChartHighlight { setCoordinates({ left: undefined, right: undefined }) setPopoverPosition(null) setInitialPoint(undefined) + setAnchorPixel(undefined) } return { diff --git a/apps/studio/components/ui/DataTable/TimelineChart.tsx b/apps/studio/components/ui/DataTable/TimelineChart.tsx index 64524e9cc1d..3532853b4e3 100644 --- a/apps/studio/components/ui/DataTable/TimelineChart.tsx +++ b/apps/studio/components/ui/DataTable/TimelineChart.tsx @@ -48,8 +48,7 @@ export function TimelineChart({ const { table } = useDataTable() const chartHighlight = useChartHighlight() - const showHighlight = - chartHighlight?.left && chartHighlight?.right && chartHighlight?.left !== chartHighlight?.right + const showHighlight = !!chartHighlight?.left && !!chartHighlight?.right // REMINDER: date has to be a string for tooltip label to work - don't ask me why const chart = useMemo( @@ -69,6 +68,11 @@ export function TimelineChart({ return { interval, period: calculatePeriod(interval) } }, [data]) + const bucketWidthMs = useMemo( + () => (data.length > 1 ? Math.abs(data[1].timestamp - data[0].timestamp) : 0), + [data] + ) + const highlightActions: ChartHighlightAction[] = useMemo( () => [ { @@ -76,7 +80,9 @@ export function TimelineChart({ label: 'Filter logs to selected range', icon: , onSelect: ({ start, end, clear }) => { - const [left, right] = [start, end].sort( + const resolvedEnd = + start === end ? new Date(new Date(start).getTime() + bucketWidthMs).toString() : end + const [left, right] = [start, resolvedEnd].sort( (a, b) => new Date(a).getTime() - new Date(b).getTime() ) table.getColumn(resolvedFilterColumnId)?.setFilterValue([new Date(left), new Date(right)]) @@ -101,13 +107,23 @@ export function TimelineChart({ { + onMouseDown={({ activeLabel, activeTooltipIndex, chartX, chartY }) => { if (activeTooltipIndex === undefined || activeTooltipIndex === null) return - chartHighlight.handleMouseDown({ activeLabel, coordinates: activeLabel }) + chartHighlight.handleMouseDown({ + activeLabel, + coordinates: activeLabel, + chartX, + chartY, + }) }} - onMouseMove={({ activeLabel, activeTooltipIndex }) => { + onMouseMove={({ activeLabel, activeTooltipIndex, chartX, chartY }) => { if (activeTooltipIndex === undefined || activeTooltipIndex === null) return - chartHighlight.handleMouseMove({ activeLabel, coordinates: activeLabel }) + chartHighlight.handleMouseMove({ + activeLabel, + coordinates: activeLabel, + chartX, + chartY, + }) }} onMouseUp={chartHighlight.handleMouseUp} style={{ cursor: 'crosshair' }} @@ -131,26 +147,37 @@ export function TimelineChart({ return format(date, 'LLL dd, y') }} /> - {!chartHighlight.popoverPosition && ( - { - const date = new Date(value) - if (isNaN(date.getTime())) return 'N/A' - if (timerange.period === '10m') { - return format(date, 'LLL dd, HH:mm:ss') - } - return format(date, 'LLL dd, y HH:mm') - }} - /> - } - /> - )} + { + const date = new Date(value) + if (isNaN(date.getTime())) return 'N/A' + if (timerange.period === '10m') { + return format(date, 'LLL dd, HH:mm:ss') + } + return format(date, 'LLL dd, y HH:mm') + }} + /> + } + /> {/* TODO: we could use the `{timestamp, ...rest} = data[0]` to dynamically create the bars but that would mean the order can be very much random */} - - - + + + {showHighlight && (