diff --git a/packages/manager/.changeset/pr-13216-upcoming-features-1766163912058.md b/packages/manager/.changeset/pr-13216-upcoming-features-1766163912058.md new file mode 100644 index 00000000000..2c206aa6713 --- /dev/null +++ b/packages/manager/.changeset/pr-13216-upcoming-features-1766163912058.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +UX enhancements of `CloudPulseDateTimeRangePicker` and `DateTimeRangePicker` components in cloudpulse metrics ([#13216](https://github.com/linode/manager/pull/13216)) diff --git a/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts index 4f3d43a6bb1..c5bb1da2c8b 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts @@ -290,12 +290,11 @@ describe('Integration Tests for DBaaS Dashboard ', () => { .click(); // Select a time duration from the autocomplete input. - // Updated selector for MUI x-date-pickers v8 - click on the wrapper div - cy.get('[aria-labelledby="start-date"]').parent().as('startDateInput'); + ui.button.findByTitle('Last hour').as('timeRangeTrigger'); + cy.get('@timeRangeTrigger').click(); - cy.get('@startDateInput').click(); - - cy.get('[data-qa-preset="Last day"]').click(); + // select a different preset but cancel + ui.button.findByTitle('Last day').click(); // Click the "Apply" button to confirm the end date and time cy.get('[data-qa-buttons="apply"]') diff --git a/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts index e0b04829dfa..450fdda6a91 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts @@ -179,12 +179,11 @@ describe('Integration Tests for Linode Dashboard ', () => { .should('be.visible') .click(); // Select a time duration from the autocomplete input. - // Updated selector for MUI x-date-pickers v8 - click on the wrapper div - cy.get('[aria-labelledby="start-date"]').parent().as('startDateInput'); + ui.button.findByTitle('Last hour').as('timeRangeTrigger'); + cy.get('@timeRangeTrigger').click(); - cy.get('@startDateInput').click(); - - cy.get('[data-qa-preset="Last day"]').click(); + // select a different preset but cancel + ui.button.findByTitle('Last day').click(); // Click the "Apply" button to confirm the end date and time cy.get('[data-qa-buttons="apply"]') diff --git a/packages/manager/cypress/e2e/core/cloudpulse/nodebalancer-widget-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/nodebalancer-widget-verification.spec.ts index dd26d1d73e5..6a515339b71 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/nodebalancer-widget-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/nodebalancer-widget-verification.spec.ts @@ -182,12 +182,11 @@ describe('Integration Tests for Nodebalancer Dashboard ', () => { .click(); // Select a time duration from the autocomplete input. - // Updated selector for MUI x-date-pickers v8 - click on the wrapper div - cy.get('[aria-labelledby="start-date"]').parent().as('startDateInput'); + ui.button.findByTitle('Last hour').as('timeRangeTrigger'); + cy.get('@timeRangeTrigger').click(); - cy.get('@startDateInput').click(); - - cy.get('[data-qa-preset="Last day"]').click(); + // select a different preset but cancel + ui.button.findByTitle('Last day').click(); cy.get('[data-qa-buttons="apply"]') .should('be.visible') diff --git a/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts index 3ce18db0626..71705981c27 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts @@ -1,4 +1,5 @@ /* eslint-disable cypress/no-unnecessary-waiting */ + /** * @file Integration Tests for CloudPulse Custom and Preset Verification */ @@ -94,17 +95,22 @@ const databaseMock: Database = databaseFactory.build({ region: mockRegion.id, type: engine, }); +// Profile timezone is set to 'UTC' const mockProfile = profileFactory.build({ - timezone: 'GMT', + timezone: 'UTC', }); /** - * Generates a date in UTC based on a specified number of hours and minutes offset. The function also provides individual date components such as day, hour, + * Generates a date in Indian Standard Time (IST) based on a specified number of days offset, + * hour, and minute. The function also provides individual date components such as day, hour, * minute, month, and AM/PM. + * + * @param {number} daysOffset - The number of days to adjust from the current date. Positive + * values give a future date, negative values give a past date. * @param {number} hour - The hour to set for the resulting date (0-23). * @param {number} [minute=0] - The minute to set for the resulting date (0-59). Defaults to 0. * * @returns {Object} - Returns an object containing: - * - `actualDate`: The formatted date and time in UTC (YYYY-MM-DD HH:mm). + * - `actualDate`: The formatted date and time in GMT (YYYY-MM-DD HH:mm). * - `day`: The day of the month as a number. * - `hour`: The hour in the 24-hour format as a number. * - `minute`: The minute of the hour as a number. @@ -121,12 +127,18 @@ const getDateRangeInGMT = ( : now.set({ hour, minute }).setZone('GMT'); const actualDate = targetDate.setZone('GMT').toFormat('yyyy-LL-dd HH:mm'); + const previousMonthDate = targetDate.minus({ months: 1 }); + return { actualDate, day: targetDate.day, hour: targetDate.hour, minute: targetDate.minute, - month: targetDate.month, + month: targetDate.toFormat('LLLL'), + year: targetDate.year, + daysInMonth: targetDate.daysInMonth, + previousMonth: previousMonthDate.toFormat('LLLL'), + previousYear: previousMonthDate.year, }; }; @@ -226,9 +238,6 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura '@fetchPreferences', '@fetchDatabases', ]); - - // Scroll to the top of the page to ensure consistent test behavior - cy.scrollTo('top'); }); it('should implement and validate custom date/time picker for a specific date and time range', () => { @@ -238,6 +247,11 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura day: startDay, hour: startHour, minute: startMinute, + month: startMonth, + year: startYear, + previousMonth, + previousYear, + daysInMonth, } = getDateRangeInGMT(12, 15, true); const { @@ -249,43 +263,52 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura cy.wait(1000); // --- Select start date --- - // Updated selector for MUI x-date-pickers v8 - click on the wrapper div - cy.get('[aria-labelledby="start-date"]').parent().as('startDateInput'); + ui.button.findByTitle('Last hour').as('startDateInput'); + + cy.get('@startDateInput').scrollIntoView(); + cy.get('@startDateInput').click(); + cy.get('[role="dialog"]').within(() => { cy.findAllByText(startDay).first().click(); cy.findAllByText(endDay).first().click(); }); - // Updated selector for MUI x-date-pickers v8 time picker button - cy.get('button[aria-label*="time"]') + ui.button + .findByAttribute('aria-label^', 'Choose time') .first() .should('be.visible', { timeout: 10000 }) // waits up to 10 seconds .as('timePickerButton'); - cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); - cy.get('@timePickerButton', { timeout: 15000 }) - .wait(300) // ⛔ doesn't work like this! (cy.wait isn't chainable on element) - .click(); + cy.get('@timePickerButton', { timeout: 15000 }).wait(300).click(); // Selects the start hour, minute, and meridiem (AM/PM) in the time picker. + + cy.get(`[aria-label="${startHour} hours"]`).click(); + cy.wait(1000); - cy.findByLabelText('Select hours') - .as('selectHours') - .scrollIntoView({ easing: 'linear' }); + ui.button + .findByAttribute('aria-label^', 'Choose time') + .first() + .should('be.visible', { timeout: 10000 }) + .as('timePickerButton'); - cy.get('@selectHours').within(() => { - cy.get(`[aria-label="${startHour} hours"]`).click(); - }); + cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); - cy.findByLabelText('Select minutes') - .as('selectMinutes') - .scrollIntoView({ duration: 500, easing: 'linear' }); + cy.get('@timePickerButton', { timeout: 15000 }).wait(300).click(); - cy.get('@selectMinutes').within(() => { - cy.get(`[aria-label="${startMinute} minutes"]`).click(); - }); + cy.get(`[aria-label="${startMinute} minutes"]`).click(); + + ui.button + .findByAttribute('aria-label^', 'Choose time') + .first() + .should('be.visible', { timeout: 10000 }) + .as('timePickerButton'); + + cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); + + cy.get('@timePickerButton', { timeout: 15000 }).wait(300).click(); cy.findByLabelText('Select meridiem') .as('startMeridiemSelect') @@ -293,8 +316,8 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura cy.get('@startMeridiemSelect').find('[aria-label="PM"]').click(); // --- Select end time --- - // Updated selector for MUI x-date-pickers v8 time picker button - cy.get('button[aria-label*="time"]') + ui.button + .findByAttribute('aria-label^', 'Choose time') .last() .should('be.visible', { timeout: 10000 }) .as('timePickerButton'); @@ -306,13 +329,23 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura duration: 500, easing: 'linear', }); - cy.get('@selectHours').within(() => { - cy.get(`[aria-label="${endHour} hours"]`).click(); - }); + cy.get(`[aria-label="${endHour} hours"]`).click(); - cy.get('@selectMinutes').within(() => { - cy.get(`[aria-label="${endMinute} minutes"]`).click(); - }); + cy.get('[aria-label^="Choose time"]') + .last() + .should('be.visible') + .as('timePickerButton'); + + cy.get('@timePickerButton', { timeout: 15000 }).wait(300).click(); + + cy.get(`[aria-label="${endMinute} minutes"]`).click(); + + cy.get('[aria-label^="Choose time"]') + .last() + .should('be.visible', { timeout: 10000 }) + .as('timePickerButton'); + + cy.get('@timePickerButton', { timeout: 15000 }).wait(300).click(); cy.findByLabelText('Select meridiem') .as('endMeridiemSelect') @@ -320,11 +353,8 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura cy.get('@endMeridiemSelect').find('[aria-label="PM"]').click(); // --- Set timezone --- - cy.findByPlaceholderText('Choose a Timezone').as('timezoneInput').click(); - cy.get('@timezoneInput').clear(); - cy.get('@timezoneInput') - .should('not.be.disabled') - .type('(GMT +0:00) Greenwich Mean Time{enter}'); + cy.findByPlaceholderText('Choose a Timezone').as('timezoneInput').clear(); + cy.get('@timezoneInput').type('(GMT +0:00) Greenwich Mean Time{enter}'); // --- Apply date/time range --- cy.get('[data-qa-buttons="apply"]') @@ -333,7 +363,6 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura .click(); // --- Re-validate after apply --- - cy.get('[aria-labelledby="start-date"]').should( 'have.value', `${startActualDate} PM` @@ -343,6 +372,8 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura `${endActualDate} PM` ); + ui.button.findByTitle('Cancel').and('be.enabled').click(); + // --- Select Node Type --- ui.autocomplete.findByLabel('Node Type').type('Primary{enter}'); @@ -354,17 +385,12 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura const { request: { body }, } = xhr as Interception; - expect(formatToUtcDateTime(body.absolute_time_duration.start)).to.equal( convertToGmt(startActualDate) ); expect(formatToUtcDateTime(body.absolute_time_duration.end)).to.equal( convertToGmt(endActualDate) ); - - // Keep a minimal structural assertion so the request shape is still validated - expect(body).to.have.nested.property('absolute_time_duration.start'); - expect(body).to.have.nested.property('absolute_time_duration.end'); }); // --- Test Time Range Presets --- @@ -372,14 +398,70 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura 'getPresets' ); + // Open the date range picker to apply the "Last 30 Days" preset + + cy.get('[aria-labelledby="start-date"]').parent().as('startDateInput'); cy.get('@startDateInput').click(); - cy.get('[data-qa-preset="Last 30 days"]').click(); + ui.button.findByTitle('Last 30 days').should('be.visible').click(); + + cy.get('[data-qa-preset="Last 30 days"]').should( + 'have.attr', + 'aria-selected', + 'true' + ); + + cy.contains(`${previousMonth} ${previousYear}`) + .closest('div') + .next() + .find('[aria-selected="true"]') + .then(($els) => { + const selectedDays = Array.from($els).map((el) => + Number(el.textContent?.trim()) + ); + + expect(daysInMonth, 'daysInMonth should be defined').to.be.a('number'); + + const totalDays = daysInMonth as number; + const expectedCount = totalDays - endDay; + + expect( + selectedDays.length, + 'number of selected days from the previous month for the last-30-days range' + ).to.eq(expectedCount); + expect( + totalDays - selectedDays.length, + 'start day of Last 30 days' + ).to.eq(endDay); + }); + + cy.contains(`${startMonth} ${startYear}`) + .closest('div') + .next() + .find('[aria-selected="true"]') + .then(($els) => { + const selectedDays = Array.from($els).map((el) => + Number(el.textContent?.trim()) + ); + + expect( + selectedDays.length, + 'number of selected days in the current month for the last-30-days range' + ).to.eq(endDay); + expect(Math.max(...selectedDays), 'end day of Last 30 days').to.eq( + endDay + ); + }); cy.get('[data-qa-buttons="apply"]') .should('be.visible') - .and('be.enabled') + .should('be.enabled') .click(); + ui.button + .findByTitle('Last 30 days') + .should('be.visible') + .should('be.enabled'); + cy.get('@getPresets.all') .should('have.length', 4) .each((xhr: unknown) => { @@ -399,9 +481,19 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura timeRanges.forEach((range) => { it(`Select and validate the functionality of the "${range.label}" preset from the "Time Range" dropdown`, () => { - cy.get('[aria-labelledby="start-date"]').parent().as('startDateInput'); + ui.button.findByTitle('Last hour').as('startDateInput'); + cy.get('@startDateInput').scrollIntoView(); + cy.get('@startDateInput').click(); - cy.get(`[data-qa-preset="${range.label}"]`).click(); + + ui.button.findByTitle(range.label).click(); + + cy.get(`[data-qa-preset="${range.label}"]`).should( + 'have.attr', + 'aria-selected', + 'true' + ); + cy.get('[data-qa-buttons="apply"]') .should('be.visible') .should('be.enabled') @@ -432,9 +524,17 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura it('Select the "Last Month" preset from the "Time Range" dropdown and verify its functionality.', () => { const { end, start } = getLastMonthRange(); - cy.get('[aria-labelledby="start-date"]').parent().as('startDateInput'); + ui.button.findByTitle('Last hour').as('startDateInput'); + cy.get('@startDateInput').scrollIntoView(); + cy.get('@startDateInput').click(); - cy.get('[data-qa-preset="Last month"]').click(); + + ui.button.findByTitle('Last month').click(); + + cy.get('[data-qa-preset="Last month"]') + .should('exist') + .and('have.attr', 'aria-selected', 'true'); + cy.get('[data-qa-buttons="apply"]') .should('be.visible') .should('be.enabled') @@ -460,9 +560,16 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura it('Select the "This Month" preset from the "Time Range" dropdown and verify its functionality.', () => { const { end, start } = getThisMonthRange(); - cy.get('[aria-labelledby="start-date"]').parent().as('startDateInput'); + ui.button.findByTitle('Last hour').as('startDateInput'); + cy.get('@startDateInput').scrollIntoView(); + cy.get('@startDateInput').click(); - cy.get('[data-qa-preset="This month"]').click(); + + ui.button.findByTitle('This month').click(); + + cy.get('[data-qa-preset="This month"]') + .should('exist') + .and('have.attr', 'aria-selected', 'true'); cy.get('[data-qa-buttons="apply"]') .should('be.visible') .should('be.enabled') @@ -488,4 +595,31 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura ).to.equal(formatDate(end, { format: 'yyyy-MM-dd hh:mm' })); }); }); + + it('should not change the selected preset when a new preset selection is cancelled', () => { + // open the time range picker + ui.button.findByTitle('Last hour').as('timeRangeTrigger'); + cy.get('@timeRangeTrigger').click(); + + // verify initial preset + cy.get('[data-qa-preset="Last hour"]').should( + 'have.attr', + 'aria-selected', + 'true' + ); + + // select a different preset but cancel + ui.button.findByTitle('Last month').click(); + ui.button.findByTitle('Cancel').should('be.visible').click(); + + // reopen picker + cy.get('@timeRangeTrigger').click(); + + // original preset should remain selected + cy.get('[data-qa-preset="Last hour"]').should( + 'have.attr', + 'aria-selected', + 'true' + ); + }); }); diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx index 62f8769bdba..0079167f476 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx @@ -1,5 +1,7 @@ +import { useProfile } from '@linode/queries'; import { Box, Paper } from '@linode/ui'; import { GridLegacy } from '@mui/material'; +import { DateTime } from 'luxon'; import * as React from 'react'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; @@ -8,6 +10,7 @@ import { SuspenseLoader } from 'src/components/SuspenseLoader'; import { GlobalFilters } from '../Overview/GlobalFilters'; import { CloudPulseAppliedFilterRenderer } from '../shared/CloudPulseAppliedFilterRenderer'; +import { defaultTimeDuration } from '../Utils/CloudPulseDateTimePickerUtils'; import { CloudPulseDashboardRenderer } from './CloudPulseDashboardRenderer'; import type { Dashboard, DateTimeWithPreset } from '@linode/api-v4'; @@ -29,6 +32,7 @@ export interface DashboardProp { } export const CloudPulseDashboardLanding = () => { + const { data: profile } = useProfile(); const [filterData, setFilterData] = React.useState({ id: {}, label: {}, @@ -45,6 +49,11 @@ export const CloudPulseDashboardLanding = () => { const [showAppliedFilters, setShowAppliedFilters] = React.useState(false); + const timezone = + profile?.timezone === 'GMT' + ? 'Etc/GMT' // this is present in timezone list for GMT + : (profile?.timezone ?? DateTime.local().zoneName); + const toggleAppliedFilter = (isVisible: boolean) => { setShowAppliedFilters(isVisible); }; @@ -71,13 +80,17 @@ export const CloudPulseDashboardLanding = () => { [] ); - const onDashboardChange = React.useCallback((dashboardObj: Dashboard) => { - setDashboard(dashboardObj); - setFilterData({ - id: {}, - label: {}, - }); // clear the filter values on dashboard change - }, []); + const onDashboardChange = React.useCallback( + (dashboardObj: Dashboard) => { + setDashboard(dashboardObj); + setFilterData({ + id: {}, + label: {}, + }); // clear the filter values on dashboard change + setTimeDuration(defaultTimeDuration(timezone)); // clear time duration on dashboard change + }, + [timezone] + ); const onTimeDurationChange = React.useCallback( (timeDurationObj: DateTimeWithPreset) => { setTimeDuration(timeDurationObj); diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.test.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.test.tsx index 238f49432d9..5009ecfbf02 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.test.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.test.tsx @@ -39,6 +39,7 @@ vi.mock('../GroupBy/utils', async () => { }; }); const mockDashboard = dashboardFactory.build(); +const PRESET_BUTTON_ID = 'preset-button'; describe('CloudPulseDashboardWithFilters component tests', () => { it('renders a CloudPulseDashboardWithFilters component with error placeholder', () => { @@ -90,9 +91,9 @@ describe('CloudPulseDashboardWithFilters component tests', () => { const groupByIcon = screen.getByTestId('group-by'); expect(groupByIcon).toBeEnabled(); - const startDate = screen.getByText('Start Date'); + const presetButton = screen.getByTestId(PRESET_BUTTON_ID); const nodeTypeSelect = screen.getByTestId('node-type-select'); - expect(startDate).toBeInTheDocument(); + expect(presetButton).toBeInTheDocument(); expect(nodeTypeSelect).toBeInTheDocument(); }); @@ -141,9 +142,9 @@ describe('CloudPulseDashboardWithFilters component tests', () => { renderWithTheme( ); - const startDate = screen.getByText('Start Date'); + const presetButton = screen.getByTestId(PRESET_BUTTON_ID); const portsSelect = screen.getByPlaceholderText('e.g., 80,443,3000'); - expect(startDate).toBeInTheDocument(); + expect(presetButton).toBeInTheDocument(); expect(portsSelect).toBeInTheDocument(); }); @@ -159,8 +160,8 @@ describe('CloudPulseDashboardWithFilters component tests', () => { renderWithTheme( ); - const startDate = screen.getByText('Start Date'); - expect(startDate).toBeInTheDocument(); + const presetButton = screen.getByTestId(PRESET_BUTTON_ID); + expect(presetButton).toBeInTheDocument(); expect(screen.getByPlaceholderText('Select a Linode Region')).toBeVisible(); expect(screen.getByPlaceholderText('Select Interface Types')).toBeVisible(); expect(screen.getByPlaceholderText('e.g., 1234,5678')).toBeVisible(); @@ -181,8 +182,8 @@ describe('CloudPulseDashboardWithFilters component tests', () => { /> ); - const startDate = screen.getByText('Start Date'); - expect(startDate).toBeInTheDocument(); + const presetButton = screen.getByTestId(PRESET_BUTTON_ID); + expect(presetButton).toBeInTheDocument(); }); it('renders a CloudPulseDashboardWithFilters component with mandatory filter error for objectstorage if region is not provided', () => { @@ -212,8 +213,8 @@ describe('CloudPulseDashboardWithFilters component tests', () => { ); - const startDate = screen.getByText('Start Date'); - expect(startDate).toBeInTheDocument(); + const presetButton = screen.getByTestId(PRESET_BUTTON_ID); + expect(presetButton).toBeInTheDocument(); }); it('renders a CloudPulseDashboardWithFilters component successfully for firewall nodebalancer', async () => { @@ -240,8 +241,8 @@ describe('CloudPulseDashboardWithFilters component tests', () => { ); - const startDate = screen.getByText('Start Date'); - expect(startDate).toBeInTheDocument(); + const presetButton = screen.getByTestId(PRESET_BUTTON_ID); + expect(presetButton).toBeInTheDocument(); await userEvent.click(screen.getByPlaceholderText('Select a Dashboard')); await userEvent.click(screen.getByText('nodebalancer_firewall_dashbaord')); expect( diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.tsx index 8d139630e7a..088f5fbfcc7 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.tsx @@ -1,5 +1,7 @@ +import { useProfile } from '@linode/queries'; import { Box, CircleProgress, Divider, ErrorState, Paper } from '@linode/ui'; import { GridLegacy } from '@mui/material'; +import { DateTime } from 'luxon'; import React from 'react'; import { @@ -13,7 +15,10 @@ import { CloudPulseDashboardFilterBuilder } from '../shared/CloudPulseDashboardF import { CloudPulseDashboardSelect } from '../shared/CloudPulseDashboardSelect'; import { CloudPulseDateTimeRangePicker } from '../shared/CloudPulseDateTimeRangePicker'; import { CloudPulseErrorPlaceholder } from '../shared/CloudPulseErrorPlaceholder'; -import { convertToGmt } from '../Utils/CloudPulseDateTimePickerUtils'; +import { + convertToGmt, + defaultTimeDuration, +} from '../Utils/CloudPulseDateTimePickerUtils'; import { PARENT_ENTITY_REGION } from '../Utils/constants'; import { FILTER_CONFIG } from '../Utils/FilterConfig'; import { @@ -62,6 +67,8 @@ export const CloudPulseDashboardWithFilters = React.memo( serviceType ? [serviceType] : [] ); + const { data: profile } = useProfile(); + const [filterData, setFilterData] = React.useState({ id: {}, label: {}, @@ -86,6 +93,11 @@ export const CloudPulseDashboardWithFilters = React.memo( const [showAppliedFilters, setShowAppliedFilters] = React.useState(false); + const timezone = + profile?.timezone === 'GMT' + ? 'Etc/GMT' // this is present in timezone list for GMT + : (profile?.timezone ?? DateTime.local().zoneName); + const toggleAppliedFilter = (isVisible: boolean) => { setShowAppliedFilters(isVisible); }; @@ -116,8 +128,9 @@ export const CloudPulseDashboardWithFilters = React.memo( (dashboard: Dashboard | undefined) => { setFilterData({ id: {}, label: {} }); setDashboard(dashboard); + setTimeDuration(defaultTimeDuration(timezone)); // clear time duration on dashboard change }, - [] + [timezone] ); const handleTimeRangeChange = React.useCallback( @@ -197,6 +210,7 @@ export const CloudPulseDashboardWithFilters = React.memo( gap={2} > diff --git a/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.test.tsx b/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.test.tsx index b3539af8a8f..351b918ea32 100644 --- a/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.test.tsx +++ b/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.test.tsx @@ -52,7 +52,7 @@ describe('Global filters component test', () => { it('Should have time range select with default value', () => { setup(); - const timeRangeSelect = screen.getByText('Start Date'); + const timeRangeSelect = screen.getByTestId('preset-button'); expect(timeRangeSelect).toBeInTheDocument(); }); diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseDateTimeRangePicker.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseDateTimeRangePicker.tsx index f63645f5688..caa1f259206 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseDateTimeRangePicker.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseDateTimeRangePicker.tsx @@ -1,5 +1,6 @@ import { useProfile } from '@linode/queries'; -import { DateTimeRangePicker } from '@linode/ui'; +import { Box, Button, CalendarIcon, DateTimeRangePicker } from '@linode/ui'; +import { useTheme } from '@mui/material/styles'; import { DateTime } from 'luxon'; import React from 'react'; @@ -32,30 +33,44 @@ export const CloudPulseDateTimeRangePicker = React.memo( const { defaultValue, handleStatsChange, savePreferences } = props; const { data: profile } = useProfile(); let defaultSelected = defaultValue as DateTimeWithPreset; - + const RESET = 'Reset'; + const theme = useTheme(); const timezone = defaultSelected?.timeZone ?? - profile?.timezone ?? - DateTime.local().zoneName; + (profile?.timezone === 'GMT' + ? 'Etc/GMT' // this is present in timezone list for GMT + : (profile?.timezone ?? DateTime.local().zoneName)); if (!defaultSelected) { defaultSelected = defaultTimeDuration(timezone); } else { defaultSelected = getTimeFromPreset(defaultSelected, timezone); } + // Show button with preset value only if selected or default preset is not 'reset' + const [selectedPreset, setSelectedPreset] = React.useState< + string | undefined + >(defaultSelected.preset); + // Show calendar only if selected or default preset is 'reset' or button is clicked + const [openCalendar, setOpenCalendar] = React.useState(false); React.useEffect(() => { if (defaultSelected) { handleStatsChange(defaultSelected); } }, []); + const handleClose = (selectedPreset: string) => { + setOpenCalendar(false); + setSelectedPreset(selectedPreset); + }; + const handleDateChange = (params: DateChangeProps) => { const { endDate, selectedPreset, startDate, timeZone } = params; if (!endDate || !startDate || !selectedPreset || !timeZone) { return; } - + setOpenCalendar(selectedPreset !== RESET ? false : true); + setSelectedPreset(selectedPreset); handleStatsChange( { end: endDate, @@ -75,33 +90,66 @@ export const CloudPulseDateTimeRangePicker = React.memo( : end; return ( - + + {selectedPreset !== RESET && !openCalendar && ( + + )} + {(selectedPreset === RESET || openCalendar) && ( + + )} + ); } ); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseMonitor/DatabaseMonitor.test.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseMonitor/DatabaseMonitor.test.tsx index daec81168d5..c372790dece 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseMonitor/DatabaseMonitor.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseMonitor/DatabaseMonitor.test.tsx @@ -40,7 +40,7 @@ describe('database monitor', () => { expect(loadingElement).toBeInTheDocument(); await waitForElementToBeRemoved(loadingElement); - const startDate = screen.getByText('Start Date'); - expect(startDate).toBeInTheDocument(); + const presetButton = screen.getByTestId('preset-button'); + expect(presetButton).toBeInTheDocument(); }); }); diff --git a/packages/ui/src/components/DatePicker/DateRangePicker/Presets.tsx b/packages/ui/src/components/DatePicker/DateRangePicker/Presets.tsx index 0fa38dad15e..b6a5a5f65ad 100644 --- a/packages/ui/src/components/DatePicker/DateRangePicker/Presets.tsx +++ b/packages/ui/src/components/DatePicker/DateRangePicker/Presets.tsx @@ -120,6 +120,7 @@ export const Presets = ({ return ( { diff --git a/packages/ui/src/components/DatePicker/DateTimeRangePicker/DateTimeRangePicker.tsx b/packages/ui/src/components/DatePicker/DateTimeRangePicker/DateTimeRangePicker.tsx index 4719d4f34d0..5b6b863c5d2 100644 --- a/packages/ui/src/components/DatePicker/DateTimeRangePicker/DateTimeRangePicker.tsx +++ b/packages/ui/src/components/DatePicker/DateTimeRangePicker/DateTimeRangePicker.tsx @@ -52,6 +52,12 @@ export interface DateTimeRangePickerProps { timeZone: null | string; }) => void; + /** Callback when the popover is closed */ + onClose?: (selectedPreset: string) => void; + + /** Property to control whether the calendar popover is open */ + openCalendar?: boolean; + /** Additional settings for the presets dropdown */ presetsProps?: { /** Default value for the presets field */ @@ -108,6 +114,8 @@ export const DateTimeRangePicker = ({ startDateProps, sx, timeZoneProps, + openCalendar, + onClose, }: DateTimeRangePickerProps) => { const [startDate, setStartDate] = useState( startDateProps?.value ?? null, @@ -122,7 +130,7 @@ export const DateTimeRangePicker = ({ startDateProps?.errorMessage, ); const [endDateError, setEndDateError] = useState(endDateProps?.errorMessage); - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(openCalendar ?? false); const [anchorEl, setAnchorEl] = useState(null); const [currentMonth, setCurrentMonth] = useState(DateTime.now()); const [focusedField, setFocusedField] = useState<'end' | 'start'>('start'); // Tracks focused input field @@ -170,6 +178,7 @@ export const DateTimeRangePicker = ({ setEndDateError(''); setOpen(false); setAnchorEl(null); + onClose?.(previousValues.current.selectedPreset ?? ''); }; const handleApply = () => { @@ -275,10 +284,18 @@ export const DateTimeRangePicker = ({ setEndDateError(''); }; + React.useEffect(() => { + if (!anchorEl && startDateInputRef.current) { + setAnchorEl( + startDateInputRef.current?.parentElement || startDateInputRef.current, + ); + } + }, []); + return ( - + { if (newTime) { + setSelectedPreset(PRESET_LABELS.RESET); // Reset preset on manual time change setStartDate((prev) => { const updatedValue = prev?.set({ @@ -404,6 +422,7 @@ export const DateTimeRangePicker = ({ label="End Time" onChange={(newTime: DateTime | null) => { if (newTime) { + setSelectedPreset(PRESET_LABELS.RESET); // Reset preset on manual time change setEndDate((prev) => { const updatedValue = prev?.set({