Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [DI-22550] - Enhance date range picker component #11495

Merged
merged 16 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/manager/src/components/DatePicker/DateTimePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ import type { DateTime } from 'luxon';
export interface DateTimePickerProps {
/** Additional props for the DateCalendar */
dateCalendarProps?: Partial<DateCalendarProps<DateTime>>;
disabledTimeZone?: boolean;
/** Error text for the date picker field */
errorText?: string;
/** Format for displaying the date-time */
format?: string;
/** Label for the input field */
label?: string;
/** Minimum date-time before which all date-time will be disabled */
minDate?: DateTime;
/** Callback when the "Apply" button is clicked */
onApply?: () => void;
/** Callback when the "Cancel" button is clicked */
Expand Down Expand Up @@ -61,9 +64,11 @@ export interface DateTimePickerProps {

export const DateTimePicker = ({
dateCalendarProps = {},
disabledTimeZone = false,
errorText = '',
format = 'yyyy-MM-dd HH:mm',
label = 'Select Date and Time',
minDate,
onApply,
onCancel,
onChange,
Expand Down Expand Up @@ -193,6 +198,7 @@ export const DateTimePicker = ({
>
<Box padding={2}>
<DateCalendar
minDate={minDate}
onChange={handleDateChange}
value={selectedDateTime || null}
{...dateCalendarProps}
Expand Down Expand Up @@ -228,6 +234,12 @@ export const DateTimePicker = ({
{showTime && (
<Grid item xs={4}>
<TimePicker
minTime={
minDate &&
minDate.toISODate() === selectedDateTime?.toISODate()
? minDate
: undefined
}
slotProps={{
actionBar: {
sx: (theme: Theme) => ({
Expand Down Expand Up @@ -266,6 +278,7 @@ export const DateTimePicker = ({
{showTimeZone && (
<Grid item xs={7}>
<TimeZoneSelect
disabled={disabledTimeZone}
label={timeZoneSelectProps?.label || 'Timezone'}
noMarginTop
onChange={handleTimeZoneChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@ type Story = StoryObj<typeof DateTimeRangePicker>;

export const Default: Story = {
args: {
enablePresets: true,
endDateProps: {
errorMessage: '',
label: 'End Date and Time',
placeholder: '',
showTimeZone: false,
value: null,
},

format: 'yyyy-MM-dd HH:mm',
onChange: action('DateTime range changed'),
presetsProps: {
defaultValue: { label: '', value: '' },
enablePresets: true,
defaultValue: '',
label: '',
placeholder: '',
},
Expand All @@ -40,16 +41,17 @@ export const Default: Story = {

export const WithInitialValues: Story = {
args: {
enablePresets: true,
endDateProps: {
label: 'End Date and Time',
showTimeZone: true,
value: DateTime.now(),
},

format: 'yyyy-MM-dd HH:mm',
onChange: action('DateTime range changed'),
presetsProps: {
defaultValue: { label: 'Last 7 Days', value: '7days' },
enablePresets: true,
defaultValue: '7days',
label: 'Time Range',
placeholder: 'Select Range',
},
Expand All @@ -65,6 +67,7 @@ export const WithInitialValues: Story = {

export const WithCustomErrors: Story = {
args: {
enablePresets: true,
endDateProps: {
errorMessage: 'End date must be after the start date.',
label: 'Custom End Label',
Expand All @@ -75,8 +78,8 @@ export const WithCustomErrors: Story = {
format: 'yyyy-MM-dd HH:mm',
onChange: action('DateTime range changed'),
presetsProps: {
defaultValue: { label: '', value: '' },
enablePresets: true,
defaultValue: '',

label: '',
placeholder: '',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import type { DateTimeRangePickerProps } from './DateTimeRangePicker';
const onChangeMock = vi.fn();

const Props: DateTimeRangePickerProps = {
enablePresets: true,
endDateProps: {
label: 'End Date and Time',
},
onChange: onChangeMock,
presetsProps: {
enablePresets: true,
label: 'Date Presets',
},

Expand Down Expand Up @@ -74,7 +74,7 @@ describe('DateTimeRangePicker Component', () => {
});
});

it('should show error when end date-time is before start date-time', async () => {
it('should disable the end date-time which is before the selected start date-time', async () => {
renderWithTheme(<DateTimeRangePicker onChange={onChangeMock} />);

// Set start date-time to the 15th
Expand All @@ -87,20 +87,14 @@ describe('DateTimeRangePicker Component', () => {
const endDateField = screen.getByLabelText('End Date and Time');
await userEvent.click(endDateField);

// Set start date-time to the 10th
await userEvent.click(screen.getByRole('gridcell', { name: '10' }));
await userEvent.click(screen.getByRole('button', { name: 'Apply' }));

// Confirm error message is displayed
expect(
screen.getByText('End date/time cannot be before the start date/time.')
).toBeInTheDocument();
expect(screen.getByRole('gridcell', { name: '10' })).toBeDisabled();
});

it('should show error when start date-time is after end date-time', async () => {
const updateProps = {
...Props,
presetsProps: { ...Props.presetsProps, enablePresets: false },
enablePresets: false,
presetsProps: { ...Props.presetsProps },
};
renderWithTheme(<DateTimeRangePicker {...updateProps} />);

Expand All @@ -125,6 +119,7 @@ describe('DateTimeRangePicker Component', () => {
it('should display custom error messages when start date-time is after end date-time', async () => {
const updatedProps = {
...Props,
enablePresets: false,
endDateProps: {
...Props.endDateProps,
errorMessage: 'Custom end date error',
Expand Down Expand Up @@ -323,24 +318,9 @@ describe('DateTimeRangePicker Component', () => {
await userEvent.click(endDateField);

// Set start date-time to the 12th
await userEvent.click(screen.getByRole('gridcell', { name: '12' }));
await userEvent.click(screen.getByRole('gridcell', { name: '17' }));
await userEvent.click(screen.getByRole('button', { name: 'Apply' }));

// Confirm error message is shown since the click was blocked
expect(
screen.getByText('End date/time cannot be before the start date/time.')
).toBeInTheDocument();

// Set start date-time to the 11th
await userEvent.click(startDateField);
await userEvent.click(screen.getByRole('gridcell', { name: '11' }));
await userEvent.click(screen.getByRole('button', { name: 'Apply' }));

// Confirm error message is not displayed
expect(
screen.queryByText('End date/time cannot be before the start date/time.')
).not.toBeInTheDocument();

// Set start date-time to the 20th
await userEvent.click(startDateField);
await userEvent.click(screen.getByRole('gridcell', { name: '20' }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import { DateTimePicker } from './DateTimePicker';
import type { SxProps, Theme } from '@mui/material/styles';

export interface DateTimeRangePickerProps {
/** If true, disable the timezone drop down */
disabledTimeZone?: boolean;

/** If true, shows the date presets field instead of the date pickers */
enablePresets?: boolean;

/** Properties for the end date field */
endDateProps?: {
/** Custom error message for invalid end date */
Expand Down Expand Up @@ -40,9 +46,7 @@ export interface DateTimeRangePickerProps {
/** Additional settings for the presets dropdown */
presetsProps?: {
/** Default value for the presets field */
defaultValue?: { label: string; value: string };
/** If true, shows the date presets field instead of the date pickers */
enablePresets?: boolean;
defaultValue?: string;
/** Label for the presets field */
label?: string;
/** placeholder for the presets field */
Expand Down Expand Up @@ -71,13 +75,17 @@ export interface DateTimeRangePickerProps {

type DatePresetType =
| '7days'
| '12hours'
| '24hours'
| '30days'
| '30minutes'
| 'custom_range'
| 'last_month'
| 'this_month';

const presetsOptions: { label: string; value: DatePresetType }[] = [
{ label: 'Last 30 Minutes', value: '30minutes' },
{ label: 'Last 12 Hours', value: '12hours' },
{ label: 'Last 24 Hours', value: '24hours' },
{ label: 'Last 7 Days', value: '7days' },
{ label: 'Last 30 Days', value: '30days' },
Expand All @@ -88,21 +96,21 @@ const presetsOptions: { label: string; value: DatePresetType }[] = [

export const DateTimeRangePicker = (props: DateTimeRangePickerProps) => {
const {
disabledTimeZone = false,

enablePresets = false,

endDateProps: {
errorMessage: endDateErrorMessage = 'End date/time cannot be before the start date/time.',
label: endLabel = 'End Date and Time',
placeholder: endDatePlaceholder,
showTimeZone: showEndTimeZone = false,
value: endDateTimeValue = null,
} = {},

format = 'yyyy-MM-dd HH:mm',

onChange,

presetsProps: {
defaultValue: presetsDefaultValue = { label: '', value: '' },
enablePresets = false,
defaultValue: presetsDefaultValue = presetsOptions[0].value,
label: presetsLabel = 'Time Range',
placeholder: presetsPlaceholder = 'Select a preset',
} = {},
Expand All @@ -123,17 +131,26 @@ export const DateTimeRangePicker = (props: DateTimeRangePickerProps) => {
const [endDateTime, setEndDateTime] = useState<DateTime | null>(
endDateTimeValue
);
const [presetValue, setPresetValue] = useState<{
label: string;
value: string;
}>(presetsDefaultValue);
const [presetValue, setPresetValue] = useState<
| {
label: string;
value: string;
}
| undefined
>(
presetsOptions.find((option) => option.value === presetsDefaultValue) ??
presetsOptions[0]
);
const [startTimeZone, setStartTimeZone] = useState<null | string>(
startTimeZoneValue
);
const [startDateError, setStartDateError] = useState<null | string>(null);
const [endDateError, setEndDateError] = useState<null | string>(null);
const [showPresets, setShowPresets] = useState(enablePresets);

const [showPresets, setShowPresets] = useState(
presetsDefaultValue
? presetsDefaultValue !== 'custom_range' && enablePresets
: enablePresets
);
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nikhagra-akamai I think we might need to update the logic for validateDates for end date validation, since we are disabling the end date dates based on the start date selection.

This condition may not required any more.

 if (source === 'end' && end < start) {
        setEndDateError(endDateErrorMessage);
        return;
      }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

Expand All @@ -160,20 +177,23 @@ export const DateTimeRangePicker = (props: DateTimeRangePickerProps) => {
const handlePresetSelection = (value: DatePresetType) => {
const now = DateTime.now();
let newStartDateTime: DateTime | null = null;
let newEndDateTime: DateTime | null = null;
let newEndDateTime: DateTime | null = now;

switch (value) {
case '30minutes':
newStartDateTime = now.minus({ minutes: 30 });
break;
case '12hours':
newStartDateTime = now.minus({ hours: 12 });
break;
case '24hours':
newStartDateTime = now.minus({ hours: 24 });
newEndDateTime = now;
break;
case '7days':
newStartDateTime = now.minus({ days: 7 });
newEndDateTime = now;
break;
case '30days':
newStartDateTime = now.minus({ days: 30 });
newEndDateTime = now;
break;
case 'this_month':
newStartDateTime = now.startOf('month');
Expand All @@ -196,7 +216,7 @@ export const DateTimeRangePicker = (props: DateTimeRangePickerProps) => {
setEndDateTime(newEndDateTime);
setPresetValue(
presetsOptions.find((option) => option.value === value) ??
presetsDefaultValue
presetsOptions[0]
);

if (onChange) {
Expand Down Expand Up @@ -248,7 +268,8 @@ export const DateTimeRangePicker = (props: DateTimeRangePickerProps) => {
handlePresetSelection(selection.value as DatePresetType);
}
}}
defaultValue={presetsDefaultValue}
data-qa-preset="preset-select"
data-testid="preset-select"
disableClearable
fullWidth
label={presetsLabel}
Expand All @@ -269,6 +290,7 @@ export const DateTimeRangePicker = (props: DateTimeRangePickerProps) => {
onChange: (value) => setStartTimeZone(value),
value: startTimeZone,
}}
disabledTimeZone={disabledTimeZone}
errorText={startDateError ?? undefined}
format={format}
label={startLabel}
Expand All @@ -282,9 +304,11 @@ export const DateTimeRangePicker = (props: DateTimeRangePickerProps) => {
timeZoneSelectProps={{
value: startTimeZone,
}}
disabledTimeZone={disabledTimeZone}
errorText={endDateError ?? undefined}
format={format}
label={endLabel}
minDate={startDateTime || undefined}
onChange={handleEndDateTimeChange}
placeholder={endDatePlaceholder}
showTimeZone={showEndTimeZone}
Expand All @@ -299,7 +323,7 @@ export const DateTimeRangePicker = (props: DateTimeRangePickerProps) => {
<StyledActionButton
onClick={() => {
setShowPresets(true);
setPresetValue(presetsDefaultValue);
setPresetValue(undefined);
}}
variant="text"
>
Expand Down
Loading