feat(studio): apply timezone picker to observability/reports charts (#45667)

## 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/<ref>/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.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Jordi Enric authored and GitHub committed 2026-05-07 19:31:59 +02:00
1 parent 2ef843fb17
commit bef828ae48
8 files changed
+77 -55

No files matched your search

@@ -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<string>(defaultChartStyle)
const logScale = useMemo(() => defaultLogScale, [defaultLogScale])
const [latestValue, setLatestValue] = useState<string | undefined>()
const formatChartDate = useFormatDateTime()
const databaseIdentifier = state.selectedDatabaseId
@@ -316,7 +318,7 @@ export const ChartBlock = ({
<ChartTooltipContent
className="min-w-[200px]"
labelSuffix={isPercentage ? '%' : ''}
labelFormatter={(x) => dayjs(x).format('DD MMM YYYY')}
labelFormatter={(x) => formatChartDate(x as string | number, 'DD MMM YYYY')}
/>
}
/>
@@ -343,7 +345,7 @@ export const ChartBlock = ({
content={
<ChartTooltipContent
labelSuffix={chartData?.format === '%' ? '%' : ''}
labelFormatter={(x) => dayjs(x).format('DD MMM YYYY')}
labelFormatter={(x) => formatChartDate(x as string | number, 'DD MMM YYYY')}
/>
}
/>
+13 -6
View File
@@ -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<D = Datum> extends CommonChartProps<D> {
yAxisKey: string
@@ -40,12 +40,19 @@ const AreaChart = ({
)
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(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 ? (
<div className="bg-black/90 text-white p-2 rounded-sm text-xs">
<div className="font-medium">
{dayjs(data[hoveredIndex]?.[xAxisKey]).format(customDateFormat)}
{formatChartDate(data[hoveredIndex]?.[xAxisKey] as number | string)}
</div>
<div>
{numberFormatter(Number(data[hoveredIndex]?.[yAxisKey]) || 0, valuePrecision)}
@@ -150,8 +157,8 @@ const AreaChart = ({
</Container>
{data && (
<div className="text-foreground-lighter -mt-8 flex items-center justify-between text-xs">
<span>{dayjs(data[0][xAxisKey]).format(customDateFormat)}</span>
<span>{dayjs(data[data?.length - 1]?.[xAxisKey]).format(customDateFormat)}</span>
<span>{formatChartDate(data[0][xAxisKey] as number | string)}</span>
<span>{formatChartDate(data[data?.length - 1]?.[xAxisKey] as number | string)}</span>
</div>
)}
</div>
+15 -6
View File
@@ -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<D = Datum> extends CommonChartProps<D> {
yAxisKey: string
@@ -83,7 +83,14 @@ function BarChart<D extends Datum = Datum>({
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<D extends Datum = Datum>({
(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<D extends Datum = Datum>({
syncId && isHovered && isCurrentChart && hoveredIndex !== null ? (
<div className="bg-black/90 text-white p-2 rounded-sm text-xs">
<div className="font-medium">
{dayjs(data[hoveredIndex]?.[xAxisKey]).format(customDateFormat)}
{formatChartDate(data[hoveredIndex]?.[xAxisKey] as number | string)}
</div>
<div>
{numberFormatter(Number(data[hoveredIndex]?.[yAxisKey]) || 0, valuePrecision)}
@@ -209,11 +216,13 @@ function BarChart<D extends Datum = Datum>({
{data && (
<div className="text-foreground-lighter -mt-10 flex items-center justify-between text-[10px] font-mono">
<span>
{xAxisIsDate ? day(data[0][xAxisKey]).format(customDateFormat) : data[0][xAxisKey]}
{xAxisIsDate
? formatChartDate(data[0][xAxisKey] as number | string)
: data[0][xAxisKey]}
</span>
<span>
{xAxisIsDate
? day(data[data?.length - 1]?.[xAxisKey]).format(customDateFormat)
? formatChartDate(data[data?.length - 1]?.[xAxisKey] as number | string)
: data[data?.length - 1]?.[xAxisKey]}
</span>
</div>
@@ -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 = (
@@ -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 = ({
/>
<DropdownMenuContent className="flex flex-col gap-1 p-1 w-fit text-left">
<DropdownMenuLabel className="flex items-center justify-center text-foreground-light font-mono gap-x-2 text-xs">
<span>{dayjs(selectedRangeStart).format('MMM D, H:mm')}</span>
<span>{formatChartDate(selectedRangeStart!, 'MMM D, H:mm')}</span>
<ArrowRight size={10} />
<span>{dayjs(selectedRangeEnd).format('MMM D, H:mm')}</span>
<span>{formatChartDate(selectedRangeEnd!, 'MMM D, H:mm')}</span>
</DropdownMenuLabel>
<DropdownMenuSeparator className="my-0" />
{allActions.map((action) => {
@@ -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<D = Datum> extends CommonChartProps<D> {
@@ -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 || {
@@ -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 = ({
)}
>
<p className="text-foreground-light text-xs">{localTimeZone}</p>
<p className="font-medium">{dayjs(timestamp).format(DateTimeFormats.FULL_SECONDS)}</p>
<p className="font-medium">{formatDateTime(timestamp, DateTimeFormats.FULL_SECONDS)}</p>
<div className="grid gap-0">
{[...payload].reverse().map((entry: any, index: number) => (
<LabelItem key={`${entry.name}-${index}`} entry={entry} />
@@ -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<any> {
xAxisKey: string
@@ -69,12 +63,19 @@ const StackedBarChart: React.FC<Props> = ({
})
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(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<Props> = ({
))}
<Tooltip
labelFormatter={
xAxisFormatAsDate
? (label) => 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<Props> = ({
</Container>
{stackedData && stackedData[0] && (
<div className="text-foreground-lighter -mt-5 flex items-center justify-between text-xs">
<span>{formatChartDate(stackedData[0][xAxisKey] as number | string)}</span>
<span>
{timestampFormatter(
stackedData[0][xAxisKey] as string,
customDateFormat,
displayDateInUtc
)}
</span>
<span>
{timestampFormatter(
stackedData[stackedData?.length - 1][xAxisKey] as string,
customDateFormat,
displayDateInUtc
)}
{formatChartDate(stackedData[stackedData?.length - 1][xAxisKey] as number | string)}
</span>
</div>
)}