Skip to content

Commit

Permalink
upcoming: [DI-23083] - Migrated CloudPulseTimeRangeSelect to DateTime…
Browse files Browse the repository at this point in the history
…RangePicker (#11573)

* upcoming: [DI-20931] - Replaced CloudPulseTimeRange with date time range picker

* upcoming: [DI-20931] - Added preference capability to date time range picker

* upcoming: [DI-20931] - Added date range picker support in contextual view

* upcoming: [DI-20931] - Updated test cases

* upcoming: [DI-20933] - Added default timezone value

* upcoming: [DI-20933] - Added disabled state for timezone

* upcoming: [DI-20933] - Added relative time duration in metric request for preset value

* upcoming: [DI-20933] - Removed widget time duration from metric request

* upcoming: [DI-20933] - Updated value to be saved in preferences

* upcoming: [DI-20933] - Updated ifelse ladder to switch statement

* upcoming: [DI-20933] - Updated contextual view

* upcoming: [DI-20933] - Moved common methods to utils file

* upcoming: [DI-20933] - Renamed type name

* upcoming: [DI-20933] - Renamed disabledTimeZone to disableTimeZone

* upcoming: [DI-20933] - Code refactoring

* upcoming: [DI-20933] - Removed unused variabled

* upcoming: [DI-20933] - Updatef failing test cases

* upcoming: [DI-20931] - Updated logic to retain date time value for previous preset selected

* upcoming: [DI-20931] - Updated metrics call logic

* upcoming: [DI-20933] - Removed unused variable

* upcoming: [DI-20933] - Added 1 hour in preset

* upcoming: [DI-20933] - bug fix for 1 hour preset

* upcoming: [DI-20933] - Updated failing test cases

* upcoming: [DI-20932] - Made input readonly for time range picker

* upcoming: [DI-20932] - Added qa id

* upcoiming: [DI-20931] - Added default start & end date for undefined default value

* upcoming: [DI-23187] - Changed end date for this month preset option

* upcoming: [DI-23186] - Changed format of date in date picker

* upcoming: [DI-20931] - Set 0 seconds to selected date

* upcoming: [DI-20931] - Set seconds value to 0 in default value

* upcoming: [DI-20931] - Set seconds value to 0 in default value

* upcoming: [DI-20931] - Updated test cases

* upcoming: [DI-20931] - Updated logic to set 0 seconds

* upcoming: [DI-20931] - updated logic to set 0 for seconds

* upcoming: [DI-20931] - Added support for multiple screen resolution

* upcoming: [DI-23083] - Added changeset

* upcoming: [DI-20931] - Added test cases

* upcoming: [DI-23083
] - Removed read-only from time picker

* upcoming: [DI-23083] - Added styling for MuiOutlinedInput in light & dark theme ts file

* added changeset

* upcoming: [DI-23083] - Update function parameters

* upcoming: [DI-23083] - Updated timezone value in date picker input field
  • Loading branch information
nikhagra-akamai authored Feb 6, 2025
1 parent c19c8ce commit be5d96c
Show file tree
Hide file tree
Showing 26 changed files with 550 additions and 175 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Upcoming Features
---

Add `DateTimeWithPreset` type in cloudpulse types ([#11573](https://github.com/linode/manager/pull/11573))
10 changes: 9 additions & 1 deletion packages/api-v4/src/cloudpulse/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ export interface TimeDuration {
value: number;
}

export interface DateTimeWithPreset {
end: string;
start: string;
preset?: string;
}

export interface Widgets {
label: string;
metric: string;
Expand Down Expand Up @@ -80,6 +86,7 @@ export type FilterValue =
| string[]
| number[]
| WidgetFilterValue
| DateTimeWithPreset
| undefined;

type WidgetFilterValue = { [key: string]: AclpWidget };
Expand Down Expand Up @@ -125,7 +132,8 @@ export interface CloudPulseMetricsRequest {
filters?: Filters[];
aggregate_function: string;
group_by: string;
relative_time_duration: TimeDuration;
relative_time_duration: TimeDuration | undefined;
absolute_time_duration: DateTimeWithPreset | undefined;
time_granularity: TimeGranularity | undefined;
entity_ids: number[];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Replace `CloudPulseTimeRangeSelect` with `CloudPulseDateTimeRangePicker`, Change metric request body to use `absolute_time_duration` for custom date and `relative_time_duration` for presets, add `1hr` preset option in `DateTimeRangePicker`, Change time select input field to `read-only` in `DateTimePicker ([#11573](https://github.com/linode/manager/pull/11573))
44 changes: 29 additions & 15 deletions packages/manager/src/components/DatePicker/DateTimePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Divider } from '@linode/ui';
import { InputAdornment, TextField } from '@linode/ui';
import { Divider } from '@linode/ui';
import { Box } from '@linode/ui';
import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
import { Grid, Popover } from '@mui/material';
Expand All @@ -9,11 +9,11 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { TimePicker } from '@mui/x-date-pickers/TimePicker';
import React, { useEffect, useState } from 'react';

import { timezones } from 'src/assets/timezones/timezones';
import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';

import { TimeZoneSelect } from './TimeZoneSelect';

import type { TextFieldProps } from '@linode/ui';
import type { SxProps, Theme } from '@mui/material/styles';
import type { DateCalendarProps } from '@mui/x-date-pickers/DateCalendar';
import type { DateTime } from 'luxon';
Expand Down Expand Up @@ -97,11 +97,6 @@ export const DateTimePicker = ({
timeZoneSelectProps.value || null
);

const TimePickerFieldProps: TextFieldProps = {
label: timeSelectProps?.label ?? 'Select Time',
noMarginTop: true,
};

const handleDateChange = (newDate: DateTime | null) => {
setSelectedDateTime((prev) =>
newDate
Expand All @@ -114,7 +109,7 @@ export const DateTimePicker = ({
};

const handleTimeChange = (newTime: DateTime | null) => {
if (newTime) {
if (newTime && !newTime.invalidReason) {
setSelectedDateTime((prev) =>
prev ? prev.set({ hour: newTime.hour, minute: newTime.minute }) : prev
);
Expand Down Expand Up @@ -177,9 +172,9 @@ export const DateTimePicker = ({
}}
value={
selectedDateTime
? `${selectedDateTime.toFormat(format)}${
selectedTimeZone ? ` (${selectedTimeZone})` : ''
}`
? `${selectedDateTime.toFormat(format)}${generateTimeZone(
selectedTimeZone
)}`
: ''
}
errorText={errorText}
Expand Down Expand Up @@ -235,8 +230,7 @@ export const DateTimePicker = ({
<Grid item xs={4}>
<TimePicker
minTime={
minDate &&
minDate.toISODate() === selectedDateTime?.toISODate()
minDate?.toISODate() === selectedDateTime?.toISODate()
? minDate
: undefined
}
Expand All @@ -249,6 +243,7 @@ export const DateTimePicker = ({
padding: 0,
}),
},

layout: {
sx: (theme: Theme) => ({
'& .MuiPickersLayout-contentWrapper': {
Expand All @@ -267,10 +262,13 @@ export const DateTimePicker = ({
},
}),
},
textField: TimePickerFieldProps,
}}
sx={{
marginTop: 0,
}}
data-qa-time="time-picker"
label={timeSelectProps?.label || 'Select Time'}
onChange={handleTimeChange}
slots={{ textField: TextField }}
value={selectedDateTime || null}
/>
</Grid>
Expand Down Expand Up @@ -307,3 +305,19 @@ export const DateTimePicker = ({
</LocalizationProvider>
);
};

const generateTimeZone = (selectedTimezone: null | string): string => {
const offset = timezones.find((zone) => zone.name === selectedTimezone)
?.offset;
if (!offset) {
return '';
}
const minutes = (Math.abs(offset * 60) % 60).toLocaleString(undefined, {
minimumIntegerDigits: 2,
useGrouping: false,
});
const hours = Math.floor(Math.abs(offset));
const isPositive = Math.abs(offset) === offset ? '+' : '-';

return ` (GMT${isPositive}${hours}:${minutes})`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,24 @@ describe('DateTimeRangePicker Component', () => {
vi.setSystemTime(vi.getRealSystemTime());

renderWithTheme(<DateTimeRangePicker onChange={onChangeMock} />);

const now = DateTime.now().set({ second: 0 });
// Open start date picker
await userEvent.click(screen.getByLabelText('Start Date and Time'));

await userEvent.click(screen.getByRole('gridcell', { name: '10' }));
await userEvent.click(screen.getByRole('button', { name: 'Apply' }));

const expectedStartTime = DateTime.fromObject({
day: 10,
month: DateTime.now().month,
year: DateTime.now().year,
}).toISO();
const expectedStartTime = now
.set({
day: 10,
month: now.month,
year: now.year,
})
.minus({ minutes: 30 })
.toISO();

// Check if the onChange function is called with the expected value
expect(onChangeMock).toHaveBeenCalledWith({

Check failure on line 71 in packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx

View workflow job for this annotation

GitHub Actions / test-manager

src/components/DatePicker/DateTimeRangePicker.test.tsx > DateTimeRangePicker Component > should call onChange when start date is changed

AssertionError: expected "spy" to be called with arguments: [ { …(4) } ] Received: 1st spy call: [ { "end": "2025-02-06T00:04:00.973+00:00", "preset": "custom_range", - "start": "2025-02-09T23:34:00.973+00:00", + "start": "2025-02-10T23:34:00.973+00:00", "timeZone": null, }, ] Number of calls: 1 ❯ src/components/DatePicker/DateTimeRangePicker.test.tsx:71:26
end: null,
end: now.toISO(),
preset: 'custom_range',
start: expectedStartTime,
timeZone: null,
Expand Down Expand Up @@ -97,17 +99,21 @@ describe('DateTimeRangePicker Component', () => {
presetsProps: { ...Props.presetsProps },
};
renderWithTheme(<DateTimeRangePicker {...updateProps} />);

const now = DateTime.now().set({ second: 0 });
// Set the end date-time to the 15th
const endDateField = screen.getByLabelText('End Date and Time');
await userEvent.click(endDateField);
await userEvent.click(screen.getByRole('gridcell', { name: '15' }));
await userEvent.click(
screen.getByRole('gridcell', { name: now.day.toString() })
);
await userEvent.click(screen.getByRole('button', { name: 'Apply' }));

// Set the start date-time to the 10th (which is earlier than the end date-time)
const startDateField = screen.getByLabelText('Start Date and Time');
await userEvent.click(startDateField);
await userEvent.click(screen.getByRole('gridcell', { name: '20' })); // Invalid date
await userEvent.click(
screen.getByRole('gridcell', { name: (now.day + 1).toString() })
); // Invalid date
await userEvent.click(screen.getByRole('button', { name: 'Apply' }));

// Confirm the error message is displayed
Expand All @@ -133,17 +139,21 @@ describe('DateTimeRangePicker Component', () => {
},
};
renderWithTheme(<DateTimeRangePicker {...updatedProps} />);

const now = DateTime.now().set({ second: 0 });
// Set the end date-time to the 15th
const endDateField = screen.getByLabelText('End Date and Time');
await userEvent.click(endDateField);
await userEvent.click(screen.getByRole('gridcell', { name: '15' }));
await userEvent.click(
screen.getByRole('gridcell', { name: now.day.toString() })
);
await userEvent.click(screen.getByRole('button', { name: 'Apply' }));

// Set the start date-time to the 20th (which is earlier than the end date-time)
const startDateField = screen.getByLabelText('Start Date and Time');
await userEvent.click(startDateField);
await userEvent.click(screen.getByRole('gridcell', { name: '20' })); // Invalid date
await userEvent.click(
screen.getByRole('gridcell', { name: (now.day + 1).toString() })
); // Invalid date
await userEvent.click(screen.getByRole('button', { name: 'Apply' }));

// Confirm the custom error message is displayed for the start date
Expand All @@ -152,7 +162,7 @@ describe('DateTimeRangePicker Component', () => {

it('should set the date range for the last 24 hours when the "Last 24 Hours" preset is selected', async () => {
renderWithTheme(<DateTimeRangePicker {...Props} />);

const now = DateTime.now().set({ second: 0 });
// Open the presets dropdown
const presetsDropdown = screen.getByLabelText('Date Presets');
await userEvent.click(presetsDropdown);
Expand All @@ -162,8 +172,8 @@ describe('DateTimeRangePicker Component', () => {
await userEvent.click(last24HoursOption);

// Expected start and end dates in ISO format
const expectedStartDateISO = DateTime.now().minus({ hours: 24 }).toISO(); // 2024-12-17T00:28:27.071-06:00
const expectedEndDateISO = DateTime.now().toISO(); // 2024-12-18T00:28:27.071-06:00
const expectedStartDateISO = now.minus({ hours: 24 }).toISO(); // 2024-12-17T00:28:27.071-06:00
const expectedEndDateISO = now.toISO(); // 2024-12-18T00:28:27.071-06:00

// Verify onChangeMock was called with correct ISO strings
expect(onChangeMock).toHaveBeenCalledWith({
Expand All @@ -179,7 +189,7 @@ describe('DateTimeRangePicker Component', () => {

it('should set the date range for the last 7 days when the "Last 7 Days" preset is selected', async () => {
renderWithTheme(<DateTimeRangePicker {...Props} />);

const now = DateTime.now().set({ second: 0 });
// Open the presets dropdown
const presetsDropdown = screen.getByLabelText('Date Presets');
await userEvent.click(presetsDropdown);
Expand All @@ -189,8 +199,8 @@ describe('DateTimeRangePicker Component', () => {
await userEvent.click(last7DaysOption);

// Expected start and end dates in ISO format
const expectedStartDateISO = DateTime.now().minus({ days: 7 }).toISO();
const expectedEndDateISO = DateTime.now().toISO();
const expectedStartDateISO = now.minus({ days: 7 }).toISO();
const expectedEndDateISO = now.toISO();

// Verify that onChange is called with the correct date range
expect(onChangeMock).toHaveBeenCalledWith({
Expand All @@ -206,7 +216,7 @@ describe('DateTimeRangePicker Component', () => {

it('should set the date range for the last 30 days when the "Last 30 Days" preset is selected', async () => {
renderWithTheme(<DateTimeRangePicker {...Props} />);

const now = DateTime.now().set({ second: 0 });
// Open the presets dropdown
const presetsDropdown = screen.getByLabelText('Date Presets');
await userEvent.click(presetsDropdown);
Expand All @@ -216,8 +226,8 @@ describe('DateTimeRangePicker Component', () => {
await userEvent.click(last30DaysOption);

// Expected start and end dates in ISO format
const expectedStartDateISO = DateTime.now().minus({ days: 30 }).toISO();
const expectedEndDateISO = DateTime.now().toISO();
const expectedStartDateISO = now.minus({ days: 30 }).toISO();
const expectedEndDateISO = now.toISO();

// Verify that onChange is called with the correct date range
expect(onChangeMock).toHaveBeenCalledWith({
Expand All @@ -233,7 +243,7 @@ describe('DateTimeRangePicker Component', () => {

it('should set the date range for this month when the "This Month" preset is selected', async () => {
renderWithTheme(<DateTimeRangePicker {...Props} />);

const now = DateTime.now();
// Open the presets dropdown
const presetsDropdown = screen.getByLabelText('Date Presets');
await userEvent.click(presetsDropdown);
Expand All @@ -243,8 +253,8 @@ describe('DateTimeRangePicker Component', () => {
await userEvent.click(thisMonthOption);

// Expected start and end dates in ISO format
const expectedStartDateISO = DateTime.now().startOf('month').toISO();
const expectedEndDateISO = DateTime.now().endOf('month').toISO();
const expectedStartDateISO = now.startOf('month').toISO();
const expectedEndDateISO = now.toISO();

// Verify that onChange is called with the correct date range
expect(onChangeMock).toHaveBeenCalledWith({
Expand All @@ -269,7 +279,7 @@ describe('DateTimeRangePicker Component', () => {
const lastMonthOption = screen.getByText('Last Month');
await userEvent.click(lastMonthOption);

const lastMonth = DateTime.now().minus({ months: 1 });
const lastMonth = DateTime.now().set({ second: 0 }).minus({ months: 1 });

// Expected start and end dates in ISO format
const expectedStartDateISO = lastMonth.startOf('month').toISO();
Expand All @@ -287,8 +297,14 @@ describe('DateTimeRangePicker Component', () => {
).not.toBeInTheDocument();
});

it('should display the date range fields with empty values when the "Custom Range" preset is selected', async () => {
renderWithTheme(<DateTimeRangePicker {...Props} />);
it('should display the date range fields with 30 min difference values when the "Custom Range" preset is selected', async () => {
const timezone = 'Asia/Kolkata';
renderWithTheme(
<DateTimeRangePicker
{...Props}
startDateProps={{ ...Props.startDateProps, timeZoneValue: timezone }}
/>
);

// Open the presets dropdown
const presetsDropdown = screen.getByLabelText('Date Presets');
Expand All @@ -297,14 +313,18 @@ describe('DateTimeRangePicker Component', () => {
// Select the "Custom Range" option
const customRange = screen.getByText('Custom');
await userEvent.click(customRange);
const format = 'yyyy-MM-dd HH:mm';
const now = DateTime.now().set({ second: 0 });
const start = now.minus({ minutes: 30 });

// Verify the input fields display the correct values
expect(
screen.getByRole('textbox', { name: 'Start Date and Time' })
).toHaveValue('');
).toHaveValue(`${start.toFormat(format)} (GMT+5:30)`);

expect(
screen.getByRole('textbox', { name: 'End Date and Time' })
).toHaveValue('');
).toHaveValue(`${now.toFormat(format)} (GMT+5:30)`);
expect(screen.getByRole('button', { name: 'Presets' })).toBeInTheDocument();

// Set start date-time to the 15th
Expand Down
Loading

0 comments on commit be5d96c

Please sign in to comment.