diff --git a/jest.config.js b/jest.config.js
index 3bb98ec70d4..1bfdda0b359 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,7 +1,7 @@
// We set this specifically for 2 reasons.
// 1. It makes sense for both CI tests and local tests to behave the same so issues are found earlier
// 2. Any wrong timezone handling could be hidden if we use UTC/GMT local time (which would happen in CI).
-process.env.TZ = 'Pacific/Easter';
+process.env.TZ = 'Pacific/Easter'; // UTC-06:00 or UTC-05:00 depending on daylight savings
const esModules = ['ol', 'd3', 'd3-color', 'd3-interpolate', 'delaunator', 'internmap', 'robust-predicates'].join('|');
diff --git a/packages/grafana-data/src/datetime/parser.test.ts b/packages/grafana-data/src/datetime/parser.test.ts
index f9756c8b736..f19ddb87ae7 100644
--- a/packages/grafana-data/src/datetime/parser.test.ts
+++ b/packages/grafana-data/src/datetime/parser.test.ts
@@ -2,6 +2,11 @@ import { systemDateFormats, SystemDateFormatsState } from './formats';
import { dateTimeParse } from './parser';
describe('dateTimeParse', () => {
+ it('should parse using the systems configured timezone', () => {
+ const date = dateTimeParse('2020-03-02 15:00:22');
+ expect(date.format()).toEqual('2020-03-02T15:00:22-05:00');
+ });
+
it('should be able to parse using default format', () => {
const date = dateTimeParse('2020-03-02 15:00:22', { timeZone: 'utc' });
expect(date.format()).toEqual('2020-03-02T15:00:22Z');
diff --git a/packages/grafana-data/src/datetime/rangeutil.test.ts b/packages/grafana-data/src/datetime/rangeutil.test.ts
index 7f48135d928..8fa40c02444 100644
--- a/packages/grafana-data/src/datetime/rangeutil.test.ts
+++ b/packages/grafana-data/src/datetime/rangeutil.test.ts
@@ -1,10 +1,57 @@
-import { TimeRange } from '../types/time';
+import { RawTimeRange, TimeRange } from '../types/time';
import { timeRangeToRelative } from './rangeutil';
import { dateTime, rangeUtil } from './index';
describe('Range Utils', () => {
+ // These tests probably wrap the dateTimeParser tests to some extent
+ describe('convertRawToRange', () => {
+ const DEFAULT_DATE_VALUE = '1996-07-30 16:00:00'; // Default format YYYY-MM-DD HH:mm:ss
+ const DEFAULT_DATE_VALUE_FORMATTED = '1996-07-30T16:00:00-06:00';
+ const defaultRawTimeRange = {
+ from: DEFAULT_DATE_VALUE,
+ to: '1996-07-30 16:20:00',
+ };
+
+ it('should serialize the default format by default', () => {
+ const deserialized = rangeUtil.convertRawToRange(defaultRawTimeRange);
+ expect(deserialized.from.format()).toBe(DEFAULT_DATE_VALUE_FORMATTED);
+ });
+
+ it('should serialize using custom formats', () => {
+ const NON_DEFAULT_FORMAT = 'DD-MM-YYYY HH:mm:ss';
+ const nonDefaultRawTimeRange: RawTimeRange = {
+ from: '30-07-1996 16:00:00',
+ to: '30-07-1996 16:20:00',
+ };
+
+ const deserializedTimeRange = rangeUtil.convertRawToRange(
+ nonDefaultRawTimeRange,
+ undefined,
+ undefined,
+ NON_DEFAULT_FORMAT
+ );
+ expect(deserializedTimeRange.from.format()).toBe(DEFAULT_DATE_VALUE_FORMATTED);
+ });
+
+ it('should take timezone into account', () => {
+ const deserializedTimeRange = rangeUtil.convertRawToRange(defaultRawTimeRange, 'UTC');
+ expect(deserializedTimeRange.from.format()).toBe('1996-07-30T16:00:00Z');
+ });
+
+ it('should leave the raw part intact if it has calulactions', () => {
+ const timeRange = {
+ from: DEFAULT_DATE_VALUE,
+ to: 'now',
+ };
+
+ const deserialized = rangeUtil.convertRawToRange(timeRange);
+ expect(deserialized.raw).toStrictEqual(timeRange);
+ expect(deserialized.to.toString()).not.toBe(deserialized.raw.to);
+ });
+ });
+
describe('relative time', () => {
it('should identify absolute vs relative', () => {
expect(
diff --git a/packages/grafana-data/src/datetime/rangeutil.ts b/packages/grafana-data/src/datetime/rangeutil.ts
index 626780d7f25..29e4d820cc3 100644
--- a/packages/grafana-data/src/datetime/rangeutil.ts
+++ b/packages/grafana-data/src/datetime/rangeutil.ts
@@ -198,9 +198,14 @@ export const describeTimeRangeAbbreviation = (range: TimeRange, timeZone?: TimeZ
return parsed ? timeZoneAbbrevation(parsed, { timeZone }) : '';
};
-export const convertRawToRange = (raw: RawTimeRange, timeZone?: TimeZone, fiscalYearStartMonth?: number): TimeRange => {
- const from = dateTimeParse(raw.from, { roundUp: false, timeZone, fiscalYearStartMonth });
- const to = dateTimeParse(raw.to, { roundUp: true, timeZone, fiscalYearStartMonth });
+export const convertRawToRange = (
+ raw: RawTimeRange,
+ timeZone?: TimeZone,
+ fiscalYearStartMonth?: number,
+ format?: string
+): TimeRange => {
+ const from = dateTimeParse(raw.from, { roundUp: false, timeZone, fiscalYearStartMonth, format });
+ const to = dateTimeParse(raw.to, { roundUp: true, timeZone, fiscalYearStartMonth, format });
if (dateMath.isMathString(raw.from) || dateMath.isMathString(raw.to)) {
return { from, to, raw };
diff --git a/public/app/core/components/TimePicker/TimePickerWithHistory.test.tsx b/public/app/core/components/TimePicker/TimePickerWithHistory.test.tsx
index c0f294619d5..c946efe0547 100644
--- a/public/app/core/components/TimePicker/TimePickerWithHistory.test.tsx
+++ b/public/app/core/components/TimePicker/TimePickerWithHistory.test.tsx
@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
-import { getDefaultTimeRange } from '@grafana/data';
+import { getDefaultTimeRange, systemDateFormats } from '@grafana/data';
import { TimePickerWithHistory } from './TimePickerWithHistory';
@@ -41,7 +41,9 @@ describe('TimePickerWithHistory', () => {
onZoom: () => {},
};
- afterEach(() => window.localStorage.clear());
+ afterEach(() => {
+ window.localStorage.clear();
+ });
it('Should load with no history', async () => {
const timeRange = getDefaultTimeRange();
@@ -124,6 +126,50 @@ describe('TimePickerWithHistory', () => {
const newLsValue = JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_KEY) ?? '[]');
expect(newLsValue).toEqual(expectedLocalStorage);
});
+
+ it('Should display handle timezones correctly', async () => {
+ const timeRange = getDefaultTimeRange();
+ render();
+ await userEvent.click(screen.getByLabelText(/Time range selected/));
+
+ await clearAndType(getFromField(), '2022-12-10 00:00:00');
+ await clearAndType(getToField(), '2022-12-10 23:59:59');
+ await userEvent.click(getApplyButton());
+
+ await userEvent.click(screen.getByLabelText(/Time range selected/));
+
+ expect(screen.getByText(/2022-12-10 00:00:00 to 2022-12-10 23:59:59/i)).toBeInTheDocument();
+ });
+
+ it('Should display history correctly with custom time format', async () => {
+ const timeRange = getDefaultTimeRange();
+
+ const interval = {
+ millisecond: 'HH:mm:ss.SSS',
+ second: 'HH:mm:ss',
+ minute: 'HH:mm',
+ hour: 'DD-MM HH:mm',
+ day: 'DD-MM',
+ month: 'MM-YYYY',
+ year: 'YYYY',
+ };
+
+ systemDateFormats.update({
+ fullDate: 'DD-MM-YYYY HH:mm:ss',
+ interval: interval,
+ useBrowserLocale: false,
+ });
+ render();
+ await userEvent.click(screen.getByLabelText(/Time range selected/));
+
+ await clearAndType(getFromField(), '03-12-2022 00:00:00');
+ await clearAndType(getToField(), '03-12-2022 23:59:59');
+ await userEvent.click(getApplyButton());
+
+ await userEvent.click(screen.getByLabelText(/Time range selected/));
+
+ expect(screen.getByText(/03-12-2022 00:00:00 to 03-12-2022 23:59:59/i)).toBeInTheDocument();
+ });
});
async function clearAndType(field: HTMLElement, text: string) {
diff --git a/public/app/core/components/TimePicker/TimePickerWithHistory.tsx b/public/app/core/components/TimePicker/TimePickerWithHistory.tsx
index 4a18ada7875..eb399d90f21 100644
--- a/public/app/core/components/TimePicker/TimePickerWithHistory.tsx
+++ b/public/app/core/components/TimePicker/TimePickerWithHistory.tsx
@@ -1,7 +1,7 @@
import { uniqBy } from 'lodash';
import React from 'react';
-import { TimeRange, isDateTime, rangeUtil, TimeZone } from '@grafana/data';
+import { TimeRange, isDateTime, rangeUtil } from '@grafana/data';
import { TimeRangePickerProps, TimeRangePicker } from '@grafana/ui';
import { LocalStorageValueProvider } from '../LocalStorageValueProvider';
@@ -24,7 +24,7 @@ export const TimePickerWithHistory = (props: Props) => {
storageKey={LOCAL_STORAGE_KEY} defaultValue={[]}>
{(rawValues, onSaveToStore) => {
const values = migrateHistory(rawValues);
- const history = deserializeHistory(values, props.timeZone);
+ const history = deserializeHistory(values);
return (
{
);
};
-function deserializeHistory(values: TimePickerHistoryItem[], timeZone: TimeZone | undefined): TimeRange[] {
- return values.map((item) => rangeUtil.convertRawToRange(item, timeZone));
+function deserializeHistory(values: TimePickerHistoryItem[]): TimeRange[] {
+ // The history is saved in UTC and with the default date format, so we need to pass those values to the convertRawToRange
+ return values.map((item) => rangeUtil.convertRawToRange(item, 'utc', undefined, 'YYYY-MM-DD HH:mm:ss'));
}
function migrateHistory(values: LSTimePickerHistoryItem[]): TimePickerHistoryItem[] {