mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
fc9b8f6be1
commit
45ff0b5cf6
@ -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"],
|
||||
|
54
e2e/dashboards-suite/dashboard-timepicker.spec.ts
Normal file
54
e2e/dashboards-suite/dashboard-timepicker.spec.ts
Normal 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);
|
||||
},
|
||||
});
|
@ -115,6 +115,7 @@ function join () {
|
||||
echo "$res"
|
||||
}
|
||||
|
||||
export TZ="Pacific/Honolulu"
|
||||
|
||||
yarn $CMD --env "$(join env)" \
|
||||
--config "$(join cypressConfig)" \
|
||||
|
@ -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;
|
||||
|
@ -320,4 +320,7 @@ export const Pages = {
|
||||
},
|
||||
},
|
||||
},
|
||||
ProfilePage: {
|
||||
url: '/profile',
|
||||
},
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -13,6 +13,7 @@ export * from './saveDashboard';
|
||||
export * from './selectOption';
|
||||
export * from './importDashboard';
|
||||
export * from './importDashboards';
|
||||
export * from './userPreferences';
|
||||
|
||||
export {
|
||||
VISUALIZATION_ALERT_LIST,
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -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({
|
||||
|
25
packages/grafana-e2e/src/flows/userPreferences.ts
Normal file
25
packages/grafana-e2e/src/flows/userPreferences.ts
Normal 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);
|
||||
}
|
@ -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');
|
||||
|
@ -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(
|
||||
{
|
||||
|
@ -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) {
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user