DatePicker: Fix calendar not showing correct selected range when changing time zones (#73273)

* user essentials mob! 🔱

* user essentials mob! 🔱

lastFile:packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarBody.tsx

* user essentials mob! 🔱

lastFile:packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarBody.tsx

* user essentials mob! 🔱

* user essentials mob! 🔱

lastFile:e2e/dashboards-suite/dashboard-timepicker.spec.ts

* user essentials mob! 🔱

lastFile:e2e/dashboards-suite/dashboard-timepicker.spec.ts

* user essentials mob! 🔱

* user essentials mob! 🔱

* restore custom.ini

* run betterer + prettier

---------

Co-authored-by: joshhunt <josh@trtr.co>
Co-authored-by: Roxana Turc <anamaria-roxana.turc@grafana.com>
Co-authored-by: Tobias Skarhed <tobias.skarhed@gmail.com>
Co-authored-by: eledobleefe <laura.fernandez@grafana.com>
This commit is contained in:
Ashley Harrison 2023-08-23 13:57:32 +01:00 committed by GitHub
parent fc9b8f6be1
commit 45ff0b5cf6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 159 additions and 23 deletions

View File

@ -758,8 +758,7 @@ exports[`better eslint`] = {
],
"packages/grafana-e2e/src/flows/revertAllChanges.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"packages/grafana-e2e/src/flows/selectOption.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@ -774,9 +773,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "5"]
],
"packages/grafana-e2e/src/support/scenarioContext.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-e2e/src/support/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@ -0,0 +1,54 @@
import { e2e } from '@grafana/e2e';
e2e.scenario({
describeName: 'Dashboard timepicker',
itName: 'Shows the correct calendar days with custom timezone set via preferences',
addScenarioDataSource: false,
addScenarioDashBoard: false,
skipScenario: false,
scenario: () => {
e2e.flows.setUserPreferences({
timezone: 'Asia/Tokyo',
});
// Open dashboard with time range from 8th to end of 10th.
// Will be Tokyo time because of above preference
e2e.flows.openDashboard({
uid: '5SdHCasdf',
timeRange: {
zone: 'Default',
from: '2022-06-08 00:00:00',
to: '2022-06-10 23:59:59',
},
});
// Assert that the calendar shows 08 and 09 and 10 as selected days
e2e.components.TimePicker.openButton().click();
e2e.components.TimePicker.calendar.openButton().first().click();
e2e().get('.react-calendar__tile--active, .react-calendar__tile--hasActive').should('have.length', 3);
},
});
e2e.scenario({
describeName: 'Dashboard timepicker',
itName: 'Shows the correct calendar days with custom timezone set via time picker',
addScenarioDataSource: false,
addScenarioDashBoard: false,
skipScenario: false,
scenario: () => {
// Open dashboard with time range from 2022-06-08 00:00:00 to 2022-06-10 23:59:59 in Tokyo time
e2e.flows.openDashboard({
uid: '5SdHCasdf',
timeRange: {
zone: 'Asia/Tokyo',
from: '2022-06-08 00:00:00',
to: '2022-06-10 23:59:59',
},
});
// Assert that the calendar shows 08 and 09 and 10 as selected days
e2e.components.TimePicker.openButton().click();
e2e.components.TimePicker.calendar.openButton().first().click();
e2e().get('.react-calendar__tile--active, .react-calendar__tile--hasActive').should('have.length', 3);
},
});

View File

@ -115,6 +115,7 @@ function join () {
echo "$res"
}
export TZ="Pacific/Honolulu"
yarn $CMD --env "$(join env)" \
--config "$(join cypressConfig)" \

View File

@ -22,6 +22,10 @@ export const timeZoneFormatUserFriendly = (timeZone: TimeZone | undefined) => {
}
};
export const getZone = (timeZone: string) => {
return moment.tz.zone(timeZone);
};
export interface TimeZoneCountry {
code: string;
name: string;

View File

@ -320,4 +320,7 @@ export const Pages = {
},
},
},
ProfilePage: {
url: '/profile',
},
};

View File

@ -64,6 +64,7 @@
"@babel/preset-env": "7.22.9",
"@cypress/webpack-preprocessor": "5.17.1",
"@grafana/e2e-selectors": "10.2.0-pre",
"@grafana/schema": "10.2.0-pre",
"@grafana/tsconfig": "^1.2.0-rc1",
"@mochajs/json-file-reporter": "^1.2.0",
"babel-loader": "9.1.3",

View File

@ -13,6 +13,7 @@ export * from './saveDashboard';
export * from './selectOption';
export * from './importDashboard';
export * from './importDashboards';
export * from './userPreferences';
export {
VISUALIZATION_ALERT_LIST,

View File

@ -1,8 +1,12 @@
import { e2e } from '../index';
export const revertAllChanges = () => {
e2e.getScenarioContext().then(({ addedDashboards, addedDataSources }: any) => {
e2e.getScenarioContext().then(({ addedDashboards, addedDataSources, hasChangedUserPreferences }) => {
addedDashboards.forEach((dashboard: any) => e2e.flows.deleteDashboard({ ...dashboard, quick: true }));
addedDataSources.forEach((dataSource: any) => e2e.flows.deleteDataSource({ ...dataSource, quick: true }));
if (hasChangedUserPreferences) {
e2e.flows.setDefaultUserPreferences();
}
});
};

View File

@ -13,6 +13,7 @@ export const setTimeRange = ({ from, to, zone }: TimeRangeConfig) => {
if (zone) {
e2e().contains('button', 'Change time settings').click();
e2e().log('setting time zone to ' + zone);
if (e2e.components.TimeZonePicker.containerV2) {
selectOption({

View File

@ -0,0 +1,25 @@
import { Preferences as UserPreferencesDTO } from '@grafana/schema/src/raw/preferences/x/preferences_types.gen';
import { e2e } from '..';
import { fromBaseUrl } from '../support/url';
const defaultUserPreferences = {
timezone: '', // "Default" option
} as const; // TODO: when we update typescript >4.9 change to `as const satisfies UserPreferencesDTO`
// Only accept preferences we have defaults for as arguments. To allow a new preference to be set, add a default for it
type UserPreferences = Pick<UserPreferencesDTO, keyof typeof defaultUserPreferences>;
export function setUserPreferences(prefs: UserPreferences) {
e2e.setScenarioContext({ hasChangedUserPreferences: prefs !== defaultUserPreferences });
return cy.request({
method: 'PUT',
url: fromBaseUrl('/api/user/preferences'),
body: prefs,
});
}
export function setDefaultUserPreferences() {
return setUserPreferences(defaultUserPreferences);
}

View File

@ -23,7 +23,10 @@ export const e2eScenario = ({
if (skipScenario) {
it.skip(itName, () => scenario());
} else {
before(() => e2e.flows.login(e2e.env('USERNAME'), e2e.env('PASSWORD'), loginViaApi));
before(() => {
e2e.flows.login(e2e.env('USERNAME'), e2e.env('PASSWORD'), loginViaApi);
e2e.flows.setDefaultUserPreferences();
});
beforeEach(() => {
Cypress.Cookies.preserveOnce('grafana_session');

View File

@ -9,12 +9,14 @@ export interface ScenarioContext {
lastAddedDashboardUid: string;
lastAddedDataSource: string; // @todo rename to `lastAddedDataSourceName`
lastAddedDataSourceId: string;
hasChangedUserPreferences: boolean;
[key: string]: any;
}
const scenarioContext: ScenarioContext = {
addedDashboards: [],
addedDataSources: [],
hasChangedUserPreferences: false,
get lastAddedDashboard() {
return lastProperty(this.addedDashboards, 'title');
},
@ -34,8 +36,7 @@ const lastProperty = <T extends DeleteDashboardConfig | DeleteDataSourceConfig,
key: K
) => items[items.length - 1]?.[key] ?? '';
// @todo this actually returns type `Cypress.Chainable`
export const getScenarioContext = (): any =>
export const getScenarioContext = (): Cypress.Chainable<ScenarioContext> =>
e2e()
.wrap(
{
@ -45,8 +46,7 @@ export const getScenarioContext = (): any =>
)
.invoke({ log: false }, 'getScenarioContext');
// @todo this actually returns type `Cypress.Chainable`
export const setScenarioContext = (newContext: Partial<ScenarioContext>): any =>
export const setScenarioContext = (newContext: Partial<ScenarioContext>): Cypress.Chainable<ScenarioContext> =>
e2e()
.wrap(
{

View File

@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import React, { useCallback } from 'react';
import Calendar from 'react-calendar';
import { GrafanaTheme2, dateTime, dateTimeParse, DateTime, TimeZone } from '@grafana/data';
import { GrafanaTheme2, dateTimeParse, DateTime, TimeZone, getZone } from '@grafana/data';
import { useStyles2 } from '../../../themes';
import { Icon } from '../../Icon/Icon';
@ -10,7 +10,7 @@ import { Icon } from '../../Icon/Icon';
import { TimePickerCalendarProps } from './TimePickerCalendar';
export function Body({ onChange, from, to, timeZone }: TimePickerCalendarProps) {
const value = inputToValue(from, to);
const value = inputToValue(from, to, new Date(), timeZone);
const onCalendarChange = useOnCalendarChange(onChange, timeZone);
const styles = useStyles2(getBodyStyles);
@ -32,16 +32,57 @@ export function Body({ onChange, from, to, timeZone }: TimePickerCalendarProps)
Body.displayName = 'Body';
export function inputToValue(from: DateTime, to: DateTime, invalidDateDefault: Date = new Date()): [Date, Date] {
const fromAsDate = from.toDate();
const toAsDate = to.toDate();
const fromAsValidDate = dateTime(fromAsDate).isValid() ? fromAsDate : invalidDateDefault;
const toAsValidDate = dateTime(toAsDate).isValid() ? toAsDate : invalidDateDefault;
export function inputToValue(
from: DateTime,
to: DateTime,
invalidDateDefault: Date = new Date(),
timezone?: string
): [Date, Date] {
let fromAsDate = from.isValid() ? from.toDate() : invalidDateDefault;
let toAsDate = to.isValid() ? to.toDate() : invalidDateDefault;
if (fromAsValidDate > toAsValidDate) {
return [toAsValidDate, fromAsValidDate];
if (timezone) {
[fromAsDate, toAsDate] = adjustDateForReactCalendar(fromAsDate, toAsDate, timezone);
}
return [fromAsValidDate, toAsValidDate];
if (fromAsDate > toAsDate) {
return [toAsDate, fromAsDate];
}
return [fromAsDate, toAsDate];
}
/**
* React calendar doesn't support showing ranges in other time zones, so attempting to show
* 10th midnight - 11th midnight in another time zone than your browsers will span three days
* instead of two.
*
* This function adjusts the dates by "moving" the time to appear as if it's local.
* e.g. make 5 PM New York "look like" 5 PM in the user's local browser time.
* See also https://github.com/wojtekmaj/react-calendar/issues/511#issuecomment-835333976
*/
function adjustDateForReactCalendar(from: Date, to: Date, timeZone: string): [Date, Date] {
const zone = getZone(timeZone);
if (!zone) {
return [from, to];
}
// get utc offset for timezone preference
const timezonePrefFromOffset = zone.utcOffset(from.getTime());
const timezonePrefToOffset = zone.utcOffset(to.getTime());
// get utc offset for local timezone
const localFromOffset = from.getTimezoneOffset();
const localToOffset = to.getTimezoneOffset();
// calculate difference between timezone preference and local timezone
// we keep these as separate variables in case one of them crosses a daylight savings boundary
const fromDiff = timezonePrefFromOffset - localFromOffset;
const toDiff = timezonePrefToOffset - localToOffset;
const newFromDate = new Date(from.getTime() - fromDiff * 1000 * 60);
const newToDate = new Date(to.getTime() - toDiff * 1000 * 60);
return [newFromDate, newToDate];
}
function useOnCalendarChange(onChange: (from: DateTime, to: DateTime) => void, timeZone?: TimeZone) {

View File

@ -159,8 +159,8 @@ export const TimeRangeContent = (props: Props) => {
<TimePickerCalendar
isFullscreen={isFullscreen}
isOpen={isOpen}
from={dateTimeParse(from.value)}
to={dateTimeParse(to.value)}
from={dateTimeParse(from.value, { timeZone })}
to={dateTimeParse(to.value, { timeZone })}
onApply={onApply}
onClose={() => setOpen(false)}
onChange={onChange}

View File

@ -3719,6 +3719,7 @@ __metadata:
"@babel/preset-env": 7.22.9
"@cypress/webpack-preprocessor": 5.17.1
"@grafana/e2e-selectors": 10.2.0-pre
"@grafana/schema": 10.2.0-pre
"@grafana/tsconfig": ^1.2.0-rc1
"@mochajs/json-file-reporter": ^1.2.0
"@rollup/plugin-node-resolve": 15.1.0