mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
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:
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')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user