mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
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 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
66691ad9ce
commit
b02ad91319
3 files changed
+74
-30
No files matched your search
@@ -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 (
|
||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DropdownMenu key={positionKey} open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DropdownMenuTrigger
|
||||
className="w-auto p-0"
|
||||
style={{
|
||||
@@ -91,7 +95,7 @@ export const ChartHighlightActions = ({
|
||||
}}
|
||||
/>
|
||||
<DropdownMenuContent
|
||||
className="flex flex-col gap-1 p-1 w-fit text-left"
|
||||
className="flex flex-col gap-1 p-1 w-fit text-left data-[state=open]:animate-none! data-[state=closed]:animate-none!"
|
||||
onEscapeKeyDown={() => clearHighlight?.()}
|
||||
onInteractOutside={(e) => {
|
||||
const target = e.target as Element | null
|
||||
|
||||
@@ -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<string | undefined>(undefined)
|
||||
const [anchorPixel, setAnchorPixel] = useState<Pixel | undefined>(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 {
|
||||
|
||||
@@ -48,8 +48,7 @@ export function TimelineChart<TChart extends BaseChartSchema>({
|
||||
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<TChart extends BaseChartSchema>({
|
||||
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<TChart extends BaseChartSchema>({
|
||||
label: 'Filter logs to selected range',
|
||||
icon: <SearchIcon className="text-foreground-lighter" size={12} />,
|
||||
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<TChart extends BaseChartSchema>({
|
||||
<BarChart
|
||||
data={chart}
|
||||
margin={{ top: 0, left: 0, right: 0, bottom: 0 }}
|
||||
onMouseDown={({ activeLabel, activeTooltipIndex }) => {
|
||||
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<TChart extends BaseChartSchema>({
|
||||
return format(date, 'LLL dd, y')
|
||||
}}
|
||||
/>
|
||||
{!chartHighlight.popoverPosition && (
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(value) => {
|
||||
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')
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<ChartTooltip
|
||||
active={
|
||||
chartHighlight.popoverPosition || chartHighlight.isSelecting ? false : undefined
|
||||
}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(value) => {
|
||||
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 */}
|
||||
<Bar dataKey="error" stackId="a" fill="var(--color-error)" />
|
||||
<Bar dataKey="warning" stackId="a" fill="var(--color-warning)" />
|
||||
<Bar dataKey="success" stackId="a" fill="var(--color-success)" />
|
||||
<Bar dataKey="error" stackId="a" fill="var(--color-error)" isAnimationActive={false} />
|
||||
<Bar
|
||||
dataKey="warning"
|
||||
stackId="a"
|
||||
fill="var(--color-warning)"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="success"
|
||||
stackId="a"
|
||||
fill="var(--color-success)"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
{showHighlight && (
|
||||
<ReferenceArea
|
||||
x1={chartHighlight.left}
|
||||
|
||||
Reference in new issue
Block a user