Skip to content

Commit

Permalink
Correctly highlight selected day in date picker. (#16973) (#17121)
Browse files Browse the repository at this point in the history
* Consider user timezone when displaying indicator for active date in date picker.

* Use moment.utc for date time utils to make them predictable.

* Fix linter hint

* Adding changelog.

* Updating tests.

* Define time zone provided for calendar in search date picker.

* Adding test

* Updating prop types.

* Fix typo.
  • Loading branch information
linuspahl authored Oct 31, 2023
1 parent c337b37 commit 309c7c6
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 43 deletions.
5 changes: 5 additions & 0 deletions changelog/unreleased/issue-16096.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type = "fixed"
message = "Fix timezone issue with date picker, which resulted in highlighting the wrong selected day."

issues = ["16096"]
pulls = ["16973"]
50 changes: 50 additions & 0 deletions graylog2-web-interface/src/components/common/DatePicker.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import React from 'react';
import { render, screen } from 'wrappedTestingLibrary';

import { alice } from 'fixtures/users';

import DatePicker from './DatePicker';

const mockCurrentUser = alice.toBuilder()
.timezone('Europe/Berlin')
.build();

jest.mock('hooks/useCurrentUser', () => () => mockCurrentUser);

describe('DatePicker', () => {
describe('should consider user time zone when displaying selected date', () => {
it('for beginning of day (date with user tz)', async () => {
render(<DatePicker date="2023-10-19T00:00:00.000+02:00" onChange={() => {}} />);

expect(await screen.findByText('19')).toHaveAttribute('aria-selected', 'true');
});

it('for end of day (date with user tz)', async () => {
render(<DatePicker date="2023-10-19T00:00:00.000+02:00" onChange={() => {}} />);

expect(await screen.findByText('19')).toHaveAttribute('aria-selected', 'true');
});

it('for end of day (date with UTC tz)', async () => {
render(<DatePicker date="2023-10-19T23:59:00.000+00:00" onChange={() => {}} />);

expect(await screen.findByText('20')).toHaveAttribute('aria-selected', 'true');
});
});
});
49 changes: 28 additions & 21 deletions graylog2-web-interface/src/components/common/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import PropTypes from 'prop-types';
import React from 'react';
import React, { useMemo } from 'react';
import type { DayModifiers } from 'react-day-picker';
import DayPicker from 'react-day-picker';
import styled, { css } from 'styled-components';

import 'react-day-picker/lib/style.css';
import { toDateObject, adjustFormat } from 'util/DateTime';

import type { DateTime } from 'util/DateTime';
import { isValidDate } from 'util/DateTime';
import useUserDateTime from 'hooks/useUserDateTime';

const StyledDayPicker = styled(DayPicker)(({ theme }) => css`
width: 100%;
Expand Down Expand Up @@ -58,38 +61,39 @@ const StyledDayPicker = styled(DayPicker)(({ theme }) => css`
}
`);

const useSelectedDate = (date: DateTime | undefined) => {
const { toUserTimezone } = useUserDateTime();

if (!isValidDate(date)) {
return undefined;
}

return toUserTimezone(date);
};

type Props = {
date?: string | undefined,
date?: DateTime | undefined,
onChange: (day: Date, modifiers: DayModifiers, event: React.MouseEvent<HTMLDivElement>) => void,
fromDate: Date,
showOutsideDays: boolean,
fromDate?: Date,
showOutsideDays?: boolean,
};

const DatePicker = ({ date, fromDate, onChange, showOutsideDays }: Props) => {
let selectedDate;
const { formatTime } = useUserDateTime();
const selectedDate = useSelectedDate(date);

if (date) {
try {
selectedDate = toDateObject(date);
} catch (e) {
// don't do anything
}
}

const modifiers = {
selected: (moddedDate) => {
const modifiers = useMemo(() => ({
selected: (moddedDate: Date) => {
if (!selectedDate) {
return false;
}

const dateTime = toDateObject(adjustFormat(moddedDate, 'complete'));

return (adjustFormat(selectedDate, 'date') === adjustFormat(dateTime, 'date'));
return formatTime(selectedDate, 'date') === formatTime(moddedDate, 'date');
},
disabled: {
before: new Date(fromDate),
},
};
}), [formatTime, fromDate, selectedDate]);

return (
<StyledDayPicker initialMonth={selectedDate ? selectedDate.toDate() : undefined}
Expand All @@ -101,7 +105,10 @@ const DatePicker = ({ date, fromDate, onChange, showOutsideDays }: Props) => {

DatePicker.propTypes = {
/** Initial date to select in the date picker. */
date: PropTypes.string,
date: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string,
]),
/**
* Callback that will be called when user picks a date. It will receive the new selected day,
* `react-day-picker`'s modifiers, and the original event as arguments.
Expand Down
2 changes: 1 addition & 1 deletion graylog2-web-interface/src/util/DateTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const getFormatStringsForDateTimeFormats = (dateTimeFormats: Array<DateTimeForma

export const toDateObject = (dateTime: DateTime, acceptedFormats?: Array<DateTimeFormats>, tz = DEFAULT_OUTPUT_TZ) => {
const acceptedFormatStrings = getFormatStringsForDateTimeFormats(acceptedFormats);
const dateObject = moment(dateTime, acceptedFormatStrings, true).tz(tz);
const dateObject = moment.utc(dateTime, acceptedFormatStrings, true).tz(tz);
const validationInfo = acceptedFormats?.length ? `Expected formats: ${acceptedFormatStrings.join(', ')}.` : undefined;

return validateDateTime(dateObject, dateTime, validationInfo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,6 @@ import WidgetContext from './contexts/WidgetContext';

jest.mock('views/components/searchbar/queryvalidation/QueryValidation', () => mockComponent('QueryValidation'));
jest.mock('views/components/searchbar/queryinput/QueryInput', () => ({ value = '' }: { value: string }) => <span>{value}</span>);

jest.mock('moment', () => {
const mockMoment = jest.requireActual('moment');

return Object.assign(() => mockMoment('2019-10-10T12:26:31.146Z'), mockMoment);
});

jest.mock('views/components/searchbar/queryvalidation/QueryValidation', () => mockComponent('QueryValidation'));
jest.mock('views/components/searchbar/queryinput/BasicQueryInput', () => ({ value = '' }: { value: string }) => <span>{value}</span>);
jest.mock('views/components/searchbar/queryinput/QueryInput', () => ({ value = '' }: { value: string }) => <span>{value}</span>);
Expand Down Expand Up @@ -121,7 +114,7 @@ describe('WidgetQueryControls', () => {
describe('displays if global override is set', () => {
const resetTimeRangeButtonTitle = /reset global override/i;
const resetQueryButtonTitle = /reset global filter/i;
const timeRangeOverrideInfo = '2019-10-10 14:26:31.146 - 2019-10-10 14:26:31.146';
const timeRangeOverrideInfo = '2020-01-01 11:00:00.850 - 2020-01-02 11:00:00.000';
const queryOverrideInfo = globalOverrideWithQuery.query.query_string;

it('shows preview of global override time range', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { DateUtils } from 'react-day-picker';
import moment from 'moment';

import { DatePicker } from 'components/common';
import { DATE_TIME_FORMATS } from 'util/DateTime';
import { toUTCFromTz, toDateObject } from 'util/DateTime';
import useUserDateTime from 'hooks/useUserDateTime';

type Props = {
dateTime: string,
Expand All @@ -29,25 +29,26 @@ type Props = {
}

const AbsoluteDatePicker = ({ dateTime, onChange, startDate }: Props) => {
const initialDateTime = moment(dateTime).toObject();
const { userTimezone, formatTime } = useUserDateTime();
const initialDateTime = toUTCFromTz(dateTime, userTimezone);

const _onDatePicked = (date) => {
if (!!startDate && DateUtils.isDayBefore(date, startDate)) {
const _onDatePicked = (selectedDate: Date) => {
if (!!startDate && DateUtils.isDayBefore(selectedDate, startDate)) {
return false;
}

const newDate = moment(date).toObject();
const selectedDateObject = toDateObject(selectedDate);
const newDate = initialDateTime.set({
year: selectedDateObject.year(),
month: selectedDateObject.month(),
date: selectedDateObject.date(),
});

return onChange(moment({
...initialDateTime,
years: newDate.years,
months: newDate.months,
date: newDate.date,
}).format(DATE_TIME_FORMATS.default));
return onChange(formatTime(newDate, 'default'));
};

return (
<DatePicker date={dateTime}
<DatePicker date={initialDateTime}
onChange={_onDatePicked}
fromDate={startDate} />
);
Expand Down

0 comments on commit 309c7c6

Please sign in to comment.