From 39c33d421bb18781ffac0b2e43bd7b6c615ee7ac Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Mon, 31 Mar 2025 07:25:17 -0600 Subject: [PATCH] Dashboards: Fix time range bug when use_browser_locale is enabled 2 (#102750) * revert * make it work on initial load * add integration test; cleanup * fix test * fix --- .../grafana-data/src/datetime/parser.test.ts | 2 +- packages/grafana-data/src/datetime/parser.ts | 20 +++++---- .../TimeRangePicker/TimeRangeContent.tsx | 2 +- .../pages/DashboardScenePage.test.tsx | 43 ++++++++++++++++++- 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/packages/grafana-data/src/datetime/parser.test.ts b/packages/grafana-data/src/datetime/parser.test.ts index 0edfd577612..b984b5cdf92 100644 --- a/packages/grafana-data/src/datetime/parser.test.ts +++ b/packages/grafana-data/src/datetime/parser.test.ts @@ -32,7 +32,7 @@ describe('dateTimeParse', () => { const date = dateTimeParse('2025-03-12T07:09:37.253Z', { timeZone: 'browser' }); expect(date.isValid()).toBe(true); - expect(date.format()).toEqual('2025-03-12T02:09:37-05:00'); + expect(date.format()).toEqual('2025-03-12T07:09:37Z'); }); it('should be able to parse array formats used by calendar', () => { diff --git a/packages/grafana-data/src/datetime/parser.ts b/packages/grafana-data/src/datetime/parser.ts index 6f3473d7858..11758b3cfb2 100644 --- a/packages/grafana-data/src/datetime/parser.ts +++ b/packages/grafana-data/src/datetime/parser.ts @@ -53,29 +53,31 @@ export const dateTimeParse: DateTimeParser = (value, }; const parseString = (value: string, options?: DateTimeOptionsWhenParsing): DateTime => { - const parsed = parse(value, options?.roundUp, options?.timeZone, options?.fiscalYearStartMonth); if (value.indexOf('now') !== -1) { if (!isValid(value)) { return dateTime(); } + const parsed = parse(value, options?.roundUp, options?.timeZone, options?.fiscalYearStartMonth); return parsed || dateTime(); } - const timeZone = getTimeZone(options); + let timeZone = getTimeZone(options); + let format = options?.format ?? systemDateFormats.fullDate; + if (value.endsWith('Z')) { + // This is a special case when we have an ISO date string + // In this case we want to force the format to be ISO and the timeZone to be UTC + // This logic is needed for initial load when parsing the URL params + format = 'YYYY-MM-DDTHH:mm:ss.SSSZ'; + timeZone = 'utc'; + } + const zone = moment.tz.zone(timeZone); - const format = options?.format ?? systemDateFormats.fullDate; if (zone && zone.name) { return dateTimeForTimeZone(zone.name, value, format); } - if (format === systemDateFormats.fullDate) { - // We use parsed here to handle case when `use_browser_locale` is true - // We need to pass the parsed value to handle case when value is an ISO 8601 date string - return dateTime(parsed, format); - } - switch (lowerCase(timeZone)) { case 'utc': return toUtc(value, format); diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.tsx index eafb4b92df1..c2a8d6301e6 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.tsx @@ -269,7 +269,7 @@ function valueAsString(value: DateTime | string, timeZone?: TimeZone): string { } if (value.endsWith('Z')) { - const dt = dateTimeParse(value, { timeZone: 'utc', format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }); + const dt = dateTimeParse(value); return dateTimeFormat(dt, { timeZone }); } diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx index 8a58e5c8917..3ca8b9c3e05 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx @@ -5,7 +5,7 @@ import { useParams } from 'react-router-dom-v5-compat'; import { TestProvider } from 'test/helpers/TestProvider'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; -import { PanelProps } from '@grafana/data'; +import { PanelProps, systemDateFormats, SystemDateFormatsState } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test'; import { selectors } from '@grafana/e2e-selectors'; import { @@ -229,6 +229,42 @@ describe('DashboardScenePage', () => { expect(await screen.findByTitle('Panel B')).toBeInTheDocument(); }); + describe('absolute time range', () => { + it('should render with absolute time range when use_browser_locale is true', async () => { + locationService.push('/d/my-dash-uid?from=2025-03-11T07:09:37.253Z&to=2025-03-12T07:09:37.253Z'); + systemDateFormats.update({ + fullDate: 'YYYY-MM-DD HH:mm:ss.SSS', + interval: {} as SystemDateFormatsState['interval'], + useBrowserLocale: true, + }); + setup(); + + await waitForDashboardToRenderWithTimeRange({ + from: '03/11/2025, 02:09:37 AM', + to: '03/12/2025, 02:09:37 AM', + }); + }); + + it('should render correct time range when use_browser_locale is true and time range is other than default system date format', async () => { + locationService.push('/d/my-dash-uid?from=2025-03-11T07:09:37.253Z&to=2025-03-12T07:09:37.253Z'); + // mocking navigator.languages to return 'de' + // this property configured in the browser settings + Object.defineProperty(navigator, 'languages', { value: ['de'] }); + systemDateFormats.update({ + // left fullDate empty to show that this should be overridden by the browser locale + fullDate: '', + interval: {} as SystemDateFormatsState['interval'], + useBrowserLocale: true, + }); + setup(); + + await waitForDashboardToRenderWithTimeRange({ + from: '11.03.2025, 02:09:37', + to: '12.03.2025, 02:09:37', + }); + }); + }); + describe('empty state', () => { it('Shows empty state when dashboard is empty', async () => { loadDashboardMock.mockResolvedValue({ dashboard: { uid: 'my-dash-uid', panels: [] }, meta: {} }); @@ -373,3 +409,8 @@ async function waitForDashboardToRender() { expect(await screen.findByText('Last 6 hours')).toBeInTheDocument(); expect(await screen.findByTitle('Panel A')).toBeInTheDocument(); } + +async function waitForDashboardToRenderWithTimeRange(timeRange: { from: string; to: string }) { + expect(await screen.findByText(`${timeRange.from} to ${timeRange.to}`)).toBeInTheDocument(); + expect(await screen.findByTitle('Panel A')).toBeInTheDocument(); +}