From bef828ae4825ca30bec0820c8f395d1db024e854 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Thu, 7 May 2026 19:31:59 +0200 Subject: [PATCH] feat(studio): apply timezone picker to observability/reports charts (#45667) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The dashboard's timezone picker (#45517) propagates to log timestamps and the shared TimestampInfo component, but observability and reports charts still render their X-axis labels, range labels, and tooltip headers in the browser's local timezone. The result is jarring once a user picks a non-local timezone: hover a chart and you get one tz, hover a log row and you get another. ## Fix Routes all display-side timestamp formatting in the chart layer through the existing picker-aware helpers (\`useFormatDateTime\` / \`formatDateTime\`) so chart UI matches the rest of the dashboard. - **ComposedChart.utils** \`CustomTooltip\` (the hotspot — drives every observability dashboard tooltip): reads the active timezone via \`useTimezone\` for both the header label and the formatted timestamp. - **AreaChart** / **BarChart**: introduce a \`formatChartDate\` helper that honours each component's existing \`displayDateInUtc\` prop, otherwise routes through the picker. - **ChartBlock**: the two recharts \`labelFormatter\` arrows now close over \`useFormatDateTime\`. - **ChartHighlightActions**: range labels in the zoom dropdown migrated to the same hook. Intentionally untouched (must stay UTC): - \`ChartHandler\` / \`ChartBlock\` \`startDate\`/\`endDate\` (API range params, day boundary). - \`ChartBlock.tsx:166\` explicit \`.utc()\` data-key normalisation. - \`useFillTimeseriesSorted\` and friends (range math, no display). ## How to test - Sign in. Open the avatar dropdown, pick a timezone different from your browser local (e.g. Asia/Tokyo). - Visit any project, then \`/project//reports/database\` (or any \`/observability/...\` page). - Hover any chart series — the tooltip header should display the chosen IANA name and times in that timezone. - Click-drag a range on a chart to open the zoom dropdown — start/end labels in the menu should also be in the chosen timezone. - Switch back to "Auto detect" and confirm everything reverts to browser-local. - For an AreaChart/BarChart that uses \`displayDateInUtc\` (e.g. some legacy reports), confirm those still render in UTC regardless of picker. ## Summary by CodeRabbit * **Refactor** * Standardized date/time formatting across charts, tooltips, axis labels, header/footer labels, and highlight range labels in Reports and chart components. * Switched to a shared, timezone-aware formatter that respects UTC display mode or the selected picker/timezone, ensuring consistent, human-readable timestamps throughout the UI. --- .../Reports/ReportBlock/ChartBlock.tsx | 6 ++- .../studio/components/ui/Charts/AreaChart.tsx | 19 ++++++--- apps/studio/components/ui/Charts/BarChart.tsx | 21 +++++++--- .../components/ui/Charts/ChartHeader.tsx | 17 +++++--- .../ui/Charts/ChartHighlightActions.tsx | 6 ++- .../components/ui/Charts/ComposedChart.tsx | 15 +++++-- .../ui/Charts/ComposedChart.utils.tsx | 9 +++-- .../components/ui/Charts/StackedBarChart.tsx | 39 +++++++------------ 8 files changed, 77 insertions(+), 55 deletions(-) diff --git a/apps/studio/components/interfaces/Reports/ReportBlock/ChartBlock.tsx b/apps/studio/components/interfaces/Reports/ReportBlock/ChartBlock.tsx index 0ba4453a9a6..81fb051976e 100644 --- a/apps/studio/components/interfaces/Reports/ReportBlock/ChartBlock.tsx +++ b/apps/studio/components/interfaces/Reports/ReportBlock/ChartBlock.tsx @@ -27,6 +27,7 @@ import { useProjectDailyStatsQuery, } from '@/data/analytics/project-daily-stats-query' import { METRICS } from '@/lib/constants/metrics' +import { useFormatDateTime } from '@/lib/datetime' import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector' import type { Dashboards } from '@/types' @@ -72,6 +73,7 @@ export const ChartBlock = ({ const [chartStyle, setChartStyle] = useState(defaultChartStyle) const logScale = useMemo(() => defaultLogScale, [defaultLogScale]) const [latestValue, setLatestValue] = useState() + const formatChartDate = useFormatDateTime() const databaseIdentifier = state.selectedDatabaseId @@ -316,7 +318,7 @@ export const ChartBlock = ({ dayjs(x).format('DD MMM YYYY')} + labelFormatter={(x) => formatChartDate(x as string | number, 'DD MMM YYYY')} /> } /> @@ -343,7 +345,7 @@ export const ChartBlock = ({ content={ dayjs(x).format('DD MMM YYYY')} + labelFormatter={(x) => formatChartDate(x as string | number, 'DD MMM YYYY')} /> } /> diff --git a/apps/studio/components/ui/Charts/AreaChart.tsx b/apps/studio/components/ui/Charts/AreaChart.tsx index f8e5c343b9b..e04c26c6a5e 100644 --- a/apps/studio/components/ui/Charts/AreaChart.tsx +++ b/apps/studio/components/ui/Charts/AreaChart.tsx @@ -1,4 +1,3 @@ -import dayjs from 'dayjs' import { useState } from 'react' import { Area, AreaChart as RechartAreaChart, Tooltip, XAxis } from 'recharts' @@ -8,6 +7,7 @@ import { numberFormatter, useChartSize } from './Charts.utils' import NoDataPlaceholder from './NoDataPlaceholder' import { useChartHoverState } from './useChartHoverState' import { CHART_COLORS, DateTimeFormats } from '@/components/ui/Charts/Charts.constants' +import { formatDateTime, useFormatDateTime } from '@/lib/datetime' export interface AreaChartProps extends CommonChartProps { yAxisKey: string @@ -40,12 +40,19 @@ const AreaChart = ({ ) const [focusDataIndex, setFocusDataIndex] = useState(null) - const day = (value: number | string) => (displayDateInUtc ? dayjs(value).utc() : dayjs(value)) + // When `displayDateInUtc` is set the chart explicitly wants UTC labels (used + // by views that display server time). Otherwise honour the user's selected + // timezone via the picker, which `useFormatDateTime` reads from context. + const formatPickerDate = useFormatDateTime() + const formatChartDate = (value: number | string) => + displayDateInUtc + ? formatDateTime(value, { tz: 'UTC', format: customDateFormat }) + : formatPickerDate(value, customDateFormat) const resolvedHighlightedLabel = (focusDataIndex !== null && data && data[focusDataIndex] !== undefined && - day(data[focusDataIndex][xAxisKey]).format(customDateFormat)) || + formatChartDate(data[focusDataIndex][xAxisKey])) || highlightedLabel const resolvedHighlightedValue = @@ -129,7 +136,7 @@ const AreaChart = ({ syncId && syncTooltip && hoveredIndex !== null ? (
- {dayjs(data[hoveredIndex]?.[xAxisKey]).format(customDateFormat)} + {formatChartDate(data[hoveredIndex]?.[xAxisKey] as number | string)}
{numberFormatter(Number(data[hoveredIndex]?.[yAxisKey]) || 0, valuePrecision)} @@ -150,8 +157,8 @@ const AreaChart = ({ {data && (
- {dayjs(data[0][xAxisKey]).format(customDateFormat)} - {dayjs(data[data?.length - 1]?.[xAxisKey]).format(customDateFormat)} + {formatChartDate(data[0][xAxisKey] as number | string)} + {formatChartDate(data[data?.length - 1]?.[xAxisKey] as number | string)}
)}
diff --git a/apps/studio/components/ui/Charts/BarChart.tsx b/apps/studio/components/ui/Charts/BarChart.tsx index 4b49f61fc9d..ee94de47981 100644 --- a/apps/studio/components/ui/Charts/BarChart.tsx +++ b/apps/studio/components/ui/Charts/BarChart.tsx @@ -1,4 +1,3 @@ -import dayjs from 'dayjs' import { ComponentProps, useMemo, useState } from 'react' import { Bar, @@ -18,6 +17,7 @@ import { numberFormatter, useChartSize } from './Charts.utils' import NoDataPlaceholder from './NoDataPlaceholder' import { useChartHoverState } from './useChartHoverState' import { CHART_COLORS, DateTimeFormats } from '@/components/ui/Charts/Charts.constants' +import { formatDateTime, useFormatDateTime } from '@/lib/datetime' export interface BarChartProps extends CommonChartProps { yAxisKey: string @@ -83,7 +83,14 @@ function BarChart({ width: 0, } - const day = (value: number | string) => (displayDateInUtc ? dayjs(value).utc() : dayjs(value)) + // When `displayDateInUtc` is set the chart explicitly wants UTC labels. + // Otherwise honour the user's selected timezone via the picker, which + // `useFormatDateTime` reads from context. + const formatPickerDate = useFormatDateTime() + const formatChartDate = (value: number | string) => + displayDateInUtc + ? formatDateTime(value, { tz: 'UTC', format: customDateFormat }) + : formatPickerDate(value, customDateFormat) function getHeaderLabel() { if (!xAxisIsDate) { @@ -94,7 +101,7 @@ function BarChart({ (focusDataIndex !== null && data && data[focusDataIndex] !== undefined && - day(data[focusDataIndex][xAxisKey]).format(customDateFormat)) || + formatChartDate(data[focusDataIndex][xAxisKey] as number | string)) || highlightedLabel ) } @@ -175,7 +182,7 @@ function BarChart({ syncId && isHovered && isCurrentChart && hoveredIndex !== null ? (
- {dayjs(data[hoveredIndex]?.[xAxisKey]).format(customDateFormat)} + {formatChartDate(data[hoveredIndex]?.[xAxisKey] as number | string)}
{numberFormatter(Number(data[hoveredIndex]?.[yAxisKey]) || 0, valuePrecision)} @@ -209,11 +216,13 @@ function BarChart({ {data && (
- {xAxisIsDate ? day(data[0][xAxisKey]).format(customDateFormat) : data[0][xAxisKey]} + {xAxisIsDate + ? formatChartDate(data[0][xAxisKey] as number | string) + : data[0][xAxisKey]} {xAxisIsDate - ? day(data[data?.length - 1]?.[xAxisKey]).format(customDateFormat) + ? formatChartDate(data[data?.length - 1]?.[xAxisKey] as number | string) : data[data?.length - 1]?.[xAxisKey]}
diff --git a/apps/studio/components/ui/Charts/ChartHeader.tsx b/apps/studio/components/ui/Charts/ChartHeader.tsx index d7c2e8d0203..6a77a2a8375 100644 --- a/apps/studio/components/ui/Charts/ChartHeader.tsx +++ b/apps/studio/components/ui/Charts/ChartHeader.tsx @@ -1,5 +1,4 @@ import { useParams } from 'common' -import dayjs from 'dayjs' import { Activity, BarChartIcon, @@ -15,6 +14,7 @@ import { InfoTooltip } from 'ui-patterns/info-tooltip' import { formatPercentage, numberFormatter } from './Charts.utils' import { useChartHoverState } from './useChartHoverState' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' +import { formatDateTime, useFormatDateTime } from '@/lib/datetime' import { formatBytes } from '@/lib/helpers' export interface ChartHeaderProps { @@ -83,6 +83,10 @@ export const ChartHeader = ({ const [localHighlightedValue, setLocalHighlightedValue] = useState(highlightedValue) const [localHighlightedLabel, setLocalHighlightedLabel] = useState(highlightedLabel) + // When `displayDateInUtc` is set the chart explicitly wants UTC labels. + // Otherwise honour the user's selected timezone via the picker. + const formatPickerDate = useFormatDateTime() + const formatHighlightedValue = (value: any) => { if (typeof value !== 'number') { return value @@ -146,11 +150,11 @@ export const ChartHeader = ({ // Update highlighted label based on sync state let newLabel = highlightedLabel if (xAxisIsDate && activeDataPoint[xAxisKey]) { - const day = (value: number | string) => - displayDateInUtc ? dayjs(value).utc() : dayjs(value) - newLabel = day(activeDataPoint[xAxisKey]).format( - customDateFormat || 'YYYY-MM-DD HH:mm:ss' - ) + const value = activeDataPoint[xAxisKey] as number | string + const fmt = customDateFormat || 'YYYY-MM-DD HH:mm:ss' + newLabel = displayDateInUtc + ? formatDateTime(value, { tz: 'UTC', format: fmt }) + : formatPickerDate(value, fmt) } else if (activeDataPoint[xAxisKey]) { newLabel = activeDataPoint[xAxisKey] } @@ -174,6 +178,7 @@ export const ChartHeader = ({ highlightedValue, highlightedLabel, attributes, + formatPickerDate, ]) const chartTitle = ( diff --git a/apps/studio/components/ui/Charts/ChartHighlightActions.tsx b/apps/studio/components/ui/Charts/ChartHighlightActions.tsx index f4841fb7ccb..2babb10d0f6 100644 --- a/apps/studio/components/ui/Charts/ChartHighlightActions.tsx +++ b/apps/studio/components/ui/Charts/ChartHighlightActions.tsx @@ -12,6 +12,7 @@ import { } from 'ui' import { ChartHighlight } from './useChartHighlight' +import { useFormatDateTime } from '@/lib/datetime' export type UpdateDateRange = (from: string, to: string) => void @@ -44,6 +45,7 @@ export const ChartHighlightActions = ({ }) => { const { left: selectedRangeStart, right: selectedRangeEnd, clearHighlight } = chartHighlight ?? {} const [isOpen, setIsOpen] = useState(!!chartHighlight?.popoverPosition) + const formatChartDate = useFormatDateTime() useEffect(() => { setIsOpen(!!chartHighlight?.popoverPosition && selectedRangeStart !== selectedRangeEnd) @@ -90,9 +92,9 @@ export const ChartHighlightActions = ({ /> - {dayjs(selectedRangeStart).format('MMM D, H:mm')} + {formatChartDate(selectedRangeStart!, 'MMM D, H:mm')} - {dayjs(selectedRangeEnd).format('MMM D, H:mm')} + {formatChartDate(selectedRangeEnd!, 'MMM D, H:mm')} {allActions.map((action) => { diff --git a/apps/studio/components/ui/Charts/ComposedChart.tsx b/apps/studio/components/ui/Charts/ComposedChart.tsx index f07c6a62b9e..188e2c479cc 100644 --- a/apps/studio/components/ui/Charts/ComposedChart.tsx +++ b/apps/studio/components/ui/Charts/ComposedChart.tsx @@ -1,4 +1,3 @@ -import dayjs from 'dayjs' import { useTheme } from 'next-themes' import { ComponentProps, useEffect, useMemo, useState } from 'react' import { @@ -44,6 +43,7 @@ import { import NoDataPlaceholder from './NoDataPlaceholder' import { ChartHighlight } from './useChartHighlight' import { useChartHoverState } from './useChartHoverState' +import { formatDateTime, useFormatDateTime } from '@/lib/datetime' import { formatBytes } from '@/lib/helpers' export interface ComposedChartProps extends CommonChartProps { @@ -155,7 +155,13 @@ export function ComposedChart({ const { Container } = useChartSize(size) - const day = (value: number | string) => (displayDateInUtc ? dayjs(value).utc() : dayjs(value)) + // When `displayDateInUtc` is set the chart explicitly wants UTC labels. + // Otherwise honour the user's selected timezone via the picker. + const formatPickerDate = useFormatDateTime() + const formatChartDate = (value: number | string) => + displayDateInUtc + ? formatDateTime(value, { tz: 'UTC', format: customDateFormat }) + : formatPickerDate(value, customDateFormat) const formatTimestamp = (ts: unknown) => { if (typeof ts !== 'number' && typeof ts !== 'string') { @@ -163,10 +169,11 @@ export function ComposedChart({ } if (typeof ts === 'number' && ts > 1e14) { - return day(ts / 1000).format(customDateFormat) + // Microsecond timestamp; convert to milliseconds before formatting. + return formatChartDate(ts / 1000) } - return day(ts).format(customDateFormat) + return formatChartDate(ts) } const _XAxisProps = XAxisProps || { diff --git a/apps/studio/components/ui/Charts/ComposedChart.utils.tsx b/apps/studio/components/ui/Charts/ComposedChart.utils.tsx index f7f635d722b..288fe4033f3 100644 --- a/apps/studio/components/ui/Charts/ComposedChart.utils.tsx +++ b/apps/studio/components/ui/Charts/ComposedChart.utils.tsx @@ -1,12 +1,11 @@ 'use client' -import dayjs from 'dayjs' import { useState } from 'react' import { cn, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'ui' import { CHART_COLORS, DateTimeFormats } from './Charts.constants' import { formatPercentage, numberFormatter } from './Charts.utils' -import { guessLocalTimezone } from '@/lib/dayjs' +import { useFormatDateTime, useTimezone } from '@/lib/datetime' import { formatBytes } from '@/lib/helpers' export interface ReportAttributes { @@ -154,6 +153,8 @@ export const CustomTooltip = ({ showTotal, isActiveHoveredChart, }: TooltipProps) => { + const formatDateTime = useFormatDateTime() + const { timezone } = useTimezone() if (active && payload && payload.length) { /** * Depending on the data source, the timestamp key could be 'timestamp' or 'period_start' @@ -194,7 +195,7 @@ export const CustomTooltip = ({ ...(maxValueAttribute?.attribute ? [maxValueAttribute.attribute] : []), ] - const localTimeZone = guessLocalTimezone() + const localTimeZone = timezone const rawPayload = payload.map((entry: any) => ({ ...entry, @@ -256,7 +257,7 @@ export const CustomTooltip = ({ )} >

{localTimeZone}

-

{dayjs(timestamp).format(DateTimeFormats.FULL_SECONDS)}

+

{formatDateTime(timestamp, DateTimeFormats.FULL_SECONDS)}

{[...payload].reverse().map((entry: any, index: number) => ( diff --git a/apps/studio/components/ui/Charts/StackedBarChart.tsx b/apps/studio/components/ui/Charts/StackedBarChart.tsx index bd6bc28ba56..444b3f67970 100644 --- a/apps/studio/components/ui/Charts/StackedBarChart.tsx +++ b/apps/studio/components/ui/Charts/StackedBarChart.tsx @@ -1,4 +1,3 @@ -import dayjs from 'dayjs' import { useState } from 'react' import { Bar, BarChart, Cell, Legend, Tooltip, XAxis } from 'recharts' @@ -11,15 +10,10 @@ import { ValidStackColor, } from './Charts.constants' import type { CommonChartProps } from './Charts.types' -import { - numberFormatter, - precisionFormatter, - timestampFormatter, - useChartSize, - useStacked, -} from './Charts.utils' +import { numberFormatter, precisionFormatter, useChartSize, useStacked } from './Charts.utils' import NoDataPlaceholder from './NoDataPlaceholder' import { useChartHoverState } from './useChartHoverState' +import { formatDateTime, useFormatDateTime } from '@/lib/datetime' interface Props extends CommonChartProps { xAxisKey: string @@ -69,12 +63,19 @@ const StackedBarChart: React.FC = ({ }) const [focusDataIndex, setFocusDataIndex] = useState(null) - const day = (value: number | string) => (displayDateInUtc ? dayjs(value).utc() : dayjs(value)) + // When `displayDateInUtc` is set the chart explicitly wants UTC labels. + // Otherwise honour the user's selected timezone via the picker. + const formatPickerDate = useFormatDateTime() + const formatChartDate = (value: number | string) => + displayDateInUtc + ? formatDateTime(value, { tz: 'UTC', format: customDateFormat }) + : formatPickerDate(value, customDateFormat) + const resolvedHighlightedLabel = (focusDataIndex !== null && data && data[focusDataIndex] !== undefined && - day(data[focusDataIndex][xAxisKey]).format(customDateFormat)) || + formatChartDate(data[focusDataIndex][xAxisKey])) || highlightedLabel const resolvedHighlightedValue = @@ -179,9 +180,7 @@ const StackedBarChart: React.FC = ({ ))} timestampFormatter(label, customDateFormat, displayDateInUtc) - : undefined + xAxisFormatAsDate ? (label) => formatChartDate(label as number | string) : undefined } formatter={(value, name, props) => { const suffix = format || '' @@ -209,19 +208,9 @@ const StackedBarChart: React.FC = ({ {stackedData && stackedData[0] && (
+ {formatChartDate(stackedData[0][xAxisKey] as number | string)} - {timestampFormatter( - stackedData[0][xAxisKey] as string, - customDateFormat, - displayDateInUtc - )} - - - {timestampFormatter( - stackedData[stackedData?.length - 1][xAxisKey] as string, - customDateFormat, - displayDateInUtc - )} + {formatChartDate(stackedData[stackedData?.length - 1][xAxisKey] as number | string)}
)}