From 73f1cd7badccdf78f89328783b0ef691fc74cdb3 Mon Sep 17 00:00:00 2001 From: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com> Date: Mon, 17 Apr 2023 17:19:44 +0200 Subject: [PATCH] TimePicker: Fixes issues with "Recently used absolute ranges" section (#66281) * TimePicker: Fixes issues with "Recently used absolute ranges" section Squashed commit of the following: commit 99d5076ce1fadde1f22ed372f372656b58efa1c4 Author: Joao Silva Date: Tue Apr 11 14:06:27 2023 +0100 user essentials mob! :trident: lastFile:public/app/core/components/TimePicker/TimePickerWithHistory.tsx commit cad0201df452f956a422b030d5b15e8ba4aed9a9 Author: eledobleefe Date: Tue Apr 11 11:44:34 2023 +0200 user essentials mob! :trident: lastFile:public/app/core/components/TimePicker/TimePickerWithHistory.tsx Co-authored-by: eledobleefe * TimePicker: Add correct date format * Add convertRawToRange tests * Rename test variables * RTL tests * Proper RTL tests * Apply suggestions from code review Co-authored-by: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com> * Remove commented line * Fix linting --------- Co-authored-by: eledobleefe Co-authored-by: Tobias Skarhed Co-authored-by: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> --- jest.config.js | 2 +- .../grafana-data/src/datetime/parser.test.ts | 5 ++ .../src/datetime/rangeutil.test.ts | 49 +++++++++++++++++- .../grafana-data/src/datetime/rangeutil.ts | 11 ++-- .../TimePicker/TimePickerWithHistory.test.tsx | 50 ++++++++++++++++++- .../TimePicker/TimePickerWithHistory.tsx | 9 ++-- 6 files changed, 115 insertions(+), 11 deletions(-) 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[] {