Skip to content

Commit

Permalink
Improving date/time picker for time range filters in paginated data t…
Browse files Browse the repository at this point in the history
…able. (#21704)

* Extracting `DateTimePicker` component.

* Reusing `DateTimePicker` in `DateRangeForm`.

* Disable picker when checkbox is clicked.

* Adjusting tests.

* Adding license header.
  • Loading branch information
dennisoelkers authored Feb 20, 2025
1 parent a494fb4 commit 3496d08
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,11 @@ describe('<EntityFilters />', () => {

const timeRangeForm = await screen.findByTestId('time-range-form');

const fromInput = within(timeRangeForm).getByRole('textbox', { name: /from/i });
userEvent.clear(fromInput);
userEvent.paste(fromInput, '2020-01-01 00:55:00.000');
const fromPicker = await screen.findByTestId('date-picker-from');
userEvent.click(await within(fromPicker).findByRole('gridcell', { name: /Jan 13 2020/i }));
userEvent.type(await screen.findByRole('spinbutton', { name: /from hour/i }), '{selectall}13');
userEvent.type(await screen.findByRole('spinbutton', { name: /from minutes/i }), '{selectall}42');
userEvent.type(await screen.findByRole('spinbutton', { name: /from seconds/i }), '{selectall}23');

const submitButton = within(timeRangeForm).getByRole('button', {
name: /create filter/i,
Expand All @@ -361,18 +363,18 @@ describe('<EntityFilters />', () => {
OrderedMap({
created_at: [
{
title: '2020-01-01 00:55:00.000 - Now',
value: '2019-12-31T23:55:00.000+00:00><',
title: '2020-01-13 13:42:23 - Now',
value: '2020-01-13T12:42:23.000+00:00><',
},
],
}),
OrderedMap({ created_at: ['2019-12-31T23:55:00.000+00:00><'] }),
OrderedMap({ created_at: ['2020-01-13T12:42:23.000+00:00><'] }),
),
);

await waitFor(() =>
expect(setUrlQueryFilters).toHaveBeenCalledWith(
OrderedMap({ created_at: ['2019-12-31T23:55:00.000+00:00><'] }),
OrderedMap({ created_at: ['2020-01-13T12:42:23.000+00:00><'] }),
),
);
await waitFor(() => dropdownIsHidden('create created filter'));
Expand Down Expand Up @@ -401,7 +403,11 @@ describe('<EntityFilters />', () => {
});
userEvent.click(toggleFilterButton);

userEvent.type(await screen.findByRole('textbox', { name: /from/i }), '{backspace}1');
const fromPicker = await screen.findByTestId('date-picker-from');
userEvent.click(await within(fromPicker).findByRole('gridcell', { name: /Jan 13 2020/i }));
userEvent.type(await screen.findByRole('spinbutton', { name: /from hour/i }), '{selectall}13');
userEvent.type(await screen.findByRole('spinbutton', { name: /from minutes/i }), '{selectall}42');
userEvent.type(await screen.findByRole('spinbutton', { name: /from seconds/i }), '{selectall}23');

const timeRangeForm = await screen.findByTestId('time-range-form');
const submitButton = within(timeRangeForm).getByRole('button', {
Expand All @@ -414,18 +420,18 @@ describe('<EntityFilters />', () => {
OrderedMap({
created_at: [
{
title: '2020-01-01 00:55:00.001 - Now',
value: '2019-12-31T23:55:00.001+00:00><',
title: '2020-01-13 13:42:23 - Now',
value: '2020-01-13T12:42:23.000+00:00><',
},
],
}),
OrderedMap({ created_at: ['2019-12-31T23:55:00.001+00:00><'] }),
OrderedMap({ created_at: ['2020-01-13T12:42:23.000+00:00><'] }),
),
);

await waitFor(() =>
expect(setUrlQueryFilters).toHaveBeenCalledWith(
OrderedMap({ created_at: ['2019-12-31T23:55:00.001+00:00><'] }),
OrderedMap({ created_at: ['2020-01-13T12:42:23.000+00:00><'] }),
),
);
await waitFor(() => dropdownIsHidden('edit created filter'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,23 @@
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import React from 'react';
import * as React from 'react';
import { useCallback } from 'react';
import styled, { css } from 'styled-components';
import { Formik, Form, Field } from 'formik';
import { Formik, Form, useField } from 'formik';
import moment from 'moment/moment';

import useUserDateTime from 'hooks/useUserDateTime';
import AbsoluteDateInput from 'views/components/searchbar/time-range-filter/time-range-picker/AbsoluteDateInput';
import { ModalSubmit } from 'components/common';
import { ModalSubmit, Icon } from 'components/common';
import { Checkbox } from 'components/bootstrap';
import { isValidDate, toUTCFromTz, adjustFormat } from 'util/DateTime';
import {
DATE_SEPARATOR,
extractRangeFromString,
timeRangeTitle,
} from 'components/common/EntityFilters/helpers/timeRange';
import DateTimePicker from 'views/components/searchbar/time-range-filter/time-range-picker/DateTimePicker';
import StringUtils from 'util/StringUtils';

import type { Filter } from '../types';

Expand All @@ -39,7 +41,7 @@ type FormValues = {

const Container = styled.div`
padding: 3px 10px;
max-width: 250px;
max-width: fit-content;
`;

const Info = styled.p(
Expand All @@ -49,16 +51,6 @@ const Info = styled.p(
`,
);

const Sections = styled.div`
margin-bottom: 10px;
`;

const Section = styled.div`
&:not(:last-child) {
margin-bottom: 10px;
}
`;

const SectionHeader = styled.div`
display: flex;
align-items: center;
Expand All @@ -76,54 +68,6 @@ const StyledCheckbox = styled(Checkbox)`
}
`;

const DateTimeFormat = styled.code`
padding: 0;
`;

const ErrorMessage = styled.span(
({ theme }) => css`
color: ${theme.colors.variant.dark.danger};
font-size: ${theme.fonts.size.small};
font-style: italic;
padding: 3px 3px 9px;
height: 1.5em;
`,
);

const DateConfiguration = ({
name: fieldName,
label,
checkboxLabel,
}: {
name: string;
label: string;
checkboxLabel: string;
}) => {
const { formatTime } = useUserDateTime();

return (
<Field name={fieldName}>
{({ field: { value, onChange, name }, meta: { error } }) => {
const _onChange = (newValue: string) => onChange({ target: { name, value: newValue } });
const onChangeAllTime = () => _onChange(value ? undefined : formatTime(new Date(), 'complete'));

return (
<div>
<SectionHeader>
<StyledLabel htmlFor={`date-input-${name}`}>{label}</StyledLabel>
<StyledCheckbox onChange={onChangeAllTime} checked={!value}>
{checkboxLabel}
</StyledCheckbox>
</SectionHeader>
<AbsoluteDateInput name="from" value={value} disabled={value === undefined} onChange={_onChange} />
{error && <ErrorMessage>{error}</ErrorMessage>}
</div>
);
}}
</Field>
);
};

const useInitialValues = (filter: Filter | undefined) => {
const { formatTime } = useUserDateTime();

Expand Down Expand Up @@ -170,6 +114,45 @@ const validate = (values: FormValues) => {
return errors;
};

const PickerContainer = styled.div`
display: flex;
align-items: center;
flex-direction: row;
gap: 10px;
`;

const PickerWrap = styled.div`
max-width: 240px;
`;

type PickerProps = { name: 'from' | 'until' };
const Picker = ({ name }: PickerProps) => {
const { formatTime } = useUserDateTime();
const label = StringUtils.capitalizeFirstLetter(name);
const [{ onChange, value }, meta] = useField(name);
const _onChange = useCallback(
(newValue: string) => onChange({ target: { name, value: newValue } }),
[onChange, name],
);
const onChangeAllTime = () => _onChange(value ? undefined : formatTime(new Date(), 'complete'));
const checkboxLabel = name === 'from' ? 'All Time' : 'Now';
const isChecked = !value;

return (
<PickerWrap data-testid={`date-picker-${name}`}>
<SectionHeader>
<StyledLabel htmlFor={`date-input-${name}`}>{label}</StyledLabel>
<StyledCheckbox onChange={onChangeAllTime} checked={isChecked}>
{checkboxLabel}
</StyledCheckbox>
</SectionHeader>
<DateTimePicker disabled={isChecked} error={meta.error} onChange={_onChange} value={value} range={label} />
</PickerWrap>
);
};
const FromPicker = () => <Picker name="from" />;
const UntilPicker = () => <Picker name="until" />;

type Props = {
onSubmit: (filter: { title: string; value: string }) => void;
filter: Filter | undefined;
Expand All @@ -195,16 +178,14 @@ const DateRangeForm = ({ filter, onSubmit }: Props) => {
<Formik initialValues={initialValues} onSubmit={_onSubmit} validate={validate}>
{({ isValid }) => (
<Form>
<Sections>
<Section>
<DateConfiguration name="from" label="From" checkboxLabel="All time" />
</Section>
<Section>
<DateConfiguration name="until" label="Until" checkboxLabel="Now" />
</Section>
</Sections>
<PickerContainer>
<FromPicker />

<Icon name="arrow_right_alt" />

<UntilPicker />
</PickerContainer>
<Info>
Format: <DateTimeFormat>YYYY-MM-DD [HH:mm:ss[.SSS]]</DateTimeFormat>.<br />
All timezones using: <b>{userTimezone}</b>.
</Info>
<ModalSubmit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,45 +15,24 @@
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import styled, { css } from 'styled-components';
import { Field } from 'formik';

import type { AbsoluteTimeRange } from 'views/logic/queries/Query';

import AbsoluteDatePicker from './AbsoluteDatePicker';
import AbsoluteTimeInput from './AbsoluteTimeInput';
import DateTimePicker from 'views/components/searchbar/time-range-filter/time-range-picker/DateTimePicker';

type Props = {
startDate?: Date;
range: 'to' | 'from';
timeRange: AbsoluteTimeRange;
};

const ErrorMessage = styled.span(
({ theme }) => css`
color: ${theme.colors.variant.dark.danger};
font-size: ${theme.fonts.size.small};
font-style: italic;
padding: 3px 3px 9px;
height: 1.5em;
`,
);

const AbsoluteCalendar = ({ startDate, timeRange, range }: Props) => (
const AbsoluteCalendar = ({ startDate = undefined, timeRange, range }: Props) => (
<Field name={`timeRangeTabs.absolute.${range}`}>
{({ field: { value, onChange, name }, meta: { error } }) => {
const _onChange = (newValue) => onChange({ target: { name, value: newValue } });
const _onChange = (newValue: string) => onChange({ target: { name, value: newValue } });
const dateTime = error ? timeRange[range] : value || timeRange[range];

return (
<>
<AbsoluteDatePicker onChange={_onChange} startDate={startDate} dateTime={dateTime} />

<AbsoluteTimeInput onChange={_onChange} range={range} dateTime={dateTime} />

<ErrorMessage>{error}</ErrorMessage>
</>
);
return <DateTimePicker error={error} onChange={_onChange} value={dateTime} range={range} startDate={startDate} />;
}}
</Field>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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 * as React from 'react';
import styled, { css } from 'styled-components';

import AbsoluteDatePicker from 'views/components/searchbar/time-range-filter/time-range-picker/AbsoluteDatePicker';
import AbsoluteTimeInput from 'views/components/searchbar/time-range-filter/time-range-picker/AbsoluteTimeInput';

const ErrorMessage = styled.span(
({ theme }) => css`
color: ${theme.colors.variant.dark.danger};
font-size: ${theme.fonts.size.small};
font-style: italic;
padding: 3px 3px 9px;
height: 1.5em;
`,
);

const Overlay = styled.div`
opacity: 0.1;
`;
const Disabled = ({ disabled, children = undefined }: React.PropsWithChildren<{ disabled: boolean }>) =>
disabled ? (
<Overlay>
<div inert="">{children}</div>
</Overlay>
) : (
children
);

type Props = {
disabled?: boolean;
error: string;
onChange: (newValue: string) => void;
startDate?: Date;
value: string;
range?: string;
};

const DateTimePicker = ({
disabled = false,
error,
onChange,
startDate = undefined,
value,
range = 'Range',
}: Props) => (
<>
<Disabled disabled={disabled}>
<AbsoluteDatePicker onChange={onChange} startDate={startDate} dateTime={value} />

<AbsoluteTimeInput onChange={onChange} range={range} dateTime={value} />
</Disabled>

<ErrorMessage>{error}</ErrorMessage>
</>
);

export default DateTimePicker;

0 comments on commit 3496d08

Please sign in to comment.