From ccb32ce74dce16ab4f0c98d85fa413b612bdb125 Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Mon, 11 Aug 2025 15:30:04 +0530 Subject: [PATCH 01/39] upcoming: [DI-22916] - Added group by drawer component --- .../GroupBy/CloudPulseGroupByDrawer.test.tsx | 170 ++++++++++++++++++ .../GroupBy/CloudPulseGroupByDrawer.tsx | 135 ++++++++++++++ 2 files changed, 305 insertions(+) create mode 100644 packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.test.tsx create mode 100644 packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.test.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.test.tsx new file mode 100644 index 00000000000..483425d61a4 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.test.tsx @@ -0,0 +1,170 @@ +import { screen, waitFor, within } from '@testing-library/react'; +import React from 'react'; + +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { + CloudPulseGroupByDrawer, + type GroupByDrawerProps, +} from './CloudPulseGroupByDrawer'; + +import type { GroupByOption } from './CloudPulseGroupByDrawer'; + +const onApply = vi.fn(); +const onCancel = vi.fn(); +const title = 'Global Group By'; +const subtitle = 'Dashboard: Resource Usage'; +const message = 'Group by different dimensions to analyze resource usage.'; +const options: GroupByOption[] = Array.from({ length: 7 }, (_, index) => ({ + label: `Dimension ${index + 1}`, + value: `dimension_${index + 1}`, +})); +const defaultValue = [options[0], options[2]]; + +const drawerProps: GroupByDrawerProps = { + onApply, + onCancel, + title, + subtitle, + message, + options, + open: true, +}; + +describe('Cloud Pulse Group By Drawer Component', () => { + it('should not open drawer', () => { + renderWithTheme(); + + expect(screen.queryByTestId('drawer')).not.toBeInTheDocument(); + expect(screen.queryByText(title)).not.toBeInTheDocument(); + expect(screen.queryByText(subtitle)).not.toBeInTheDocument(); + expect(screen.queryByText(message)).not.toBeInTheDocument(); + }); + + it('Should open and show title, message and subtitle', () => { + renderWithTheme(); + const drawer = screen.getByTestId('drawer'); + const titleElement = screen.getByText(title); + const subtitleElement = screen.getByText(subtitle); + const messageElement = screen.getByText(message); + const applyButton = screen.getByTestId('apply'); + const cancelButton = screen.getByTestId('cancel'); + + expect(drawer).toBeInTheDocument(); + expect(titleElement).toBeInTheDocument(); + expect(subtitleElement).toBeInTheDocument(); + expect(messageElement).toBeInTheDocument(); + expect(applyButton).toBeInTheDocument(); + expect(cancelButton).toBeInTheDocument(); + }); + + it('Should have options and default values selected', async () => { + renderWithTheme( + + ); + + // Check if all the default values are selected when autocomplete is closed + defaultValue.forEach((value) => { + const option = screen.getByRole('button', { name: value.label }); + expect(option).toBeInTheDocument(); + }); + + // Should have all the options + await screen.getByRole('button', { name: 'Open' }).click(); + + const optionsList = screen.getAllByRole('option'); + + expect(optionsList).toHaveLength(options.length); + }); + + it('Should disable options after maximum limit', async () => { + renderWithTheme( + + ); + + await screen.getByRole('button', { name: 'Open' }).click(); + + const disabledOption = screen.getByRole('option', { + name: options[3].label, + }); + + expect(disabledOption).toHaveAttribute('aria-disabled', 'true'); + + const enabledOption = screen.getByRole('option', { + name: options[0].label, + }); + + expect(enabledOption).toHaveAttribute('aria-disabled', 'false'); + }); + + it('Should revert the state on click of cancel button', async () => { + renderWithTheme( + + ); + + // Option currently not selected + const newOption = options[1]; + + // Option is not selected + expect( + screen.queryByRole('button', { name: newOption.label }) + ).not.toBeInTheDocument(); + + await screen.getByRole('button', { name: 'Open' }).click(); + + // select the option + await screen.getByRole('option', { name: newOption.label }).click(); + + await screen.getByRole('button', { name: 'Close' }).click(); + + const element = screen.getByRole('button', { name: newOption.label }); + + // Element is selected + expect(element).toBeInTheDocument(); + + // cancel button clicked + + await screen.getByTestId('cancel').click(); + + // New selection is reverted + expect( + screen.queryByRole('button', { name: newOption.label }) + ).not.toBeInTheDocument(); + + // Default values are still there + defaultValue.forEach((value) => { + const option = screen.getByRole('button', { name: value.label }); + expect(option).toBeInTheDocument(); + }); + }); + + it('Should save the changes on apply button clicked', async () => { + renderWithTheme( + + ); + + // Option currently not selected + const newOption = options[1]; + + await screen.getByRole('button', { name: 'Open' }).click(); + + // select the option + await screen.getByRole('option', { name: newOption.label }).click(); + + await screen.getByRole('button', { name: 'Close' }).click(); + + const element = screen.getByRole('button', { name: newOption.label }); + + // Element is selected + expect(element).toBeInTheDocument(); + + // cancel button clicked + + await screen.getByTestId('apply').click(); + + expect(onApply).toHaveBeenCalledWith([...defaultValue, newOption]); + }); +}); diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx new file mode 100644 index 00000000000..821353105b9 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx @@ -0,0 +1,135 @@ +import { + Autocomplete, + Box, + Button, + Drawer, + SelectedIcon, + Stack, + StyledListItem, + Typography, +} from '@linode/ui'; +import * as React from 'react'; + +import NullComponent from 'src/components/NullComponent'; + +export interface GroupByDrawerProps { + defaultValue?: GroupByOption[]; + message?: string; + onApply: (value: GroupByOption[]) => void; + onCancel: () => void; + open?: boolean; + options: GroupByOption[]; + subtitle?: string; + title: string; +} + +export interface GroupByOption { + label: string; + value: string; +} + +const groupBySelectionLimit = 3; + +export const CloudPulseGroupByDrawer = (props: GroupByDrawerProps) => { + const { + open, + title, + subtitle, + message, + options, + defaultValue = [], + onApply, + onCancel, + } = props; + + const [selectedValue, setSelectedValue] = React.useState( + defaultValue?.slice( + 0, + Math.min(defaultValue.length ?? 0, groupBySelectionLimit) + ) + ); + const previousValueRef = React.useRef(defaultValue); + + const handleApply = () => { + previousValueRef.current = selectedValue; + onApply(selectedValue); + }; + + const handleClose = () => { + setSelectedValue(previousValueRef.current); + onCancel(); + }; + + return ( + handleClose()} open={open} title={title}> + + ({ marginTop: -1, font: theme.font.normal })} + variant="h2" + > + {subtitle} + + ({ font: theme.font.normal })} variant="h2"> + {message} + + option.value === value.value} + label="Select Dimensions" + multiple + onChange={(_event, value) => { + setSelectedValue(value); + }} + options={options} + placeholder="Select Dimnensions" + renderOption={(props, option) => { + const { key, ...rest } = props; + const isSelectAllORDeslectAllOption = + option.label === 'Select All ' || + option.label === 'Deselect All '; + const isSelected = props['aria-selected'] === true; + const ListItem = isSelectAllORDeslectAllOption + ? StyledListItem + : 'li'; + const isDisabled = + selectedValue.length >= groupBySelectionLimit && !isSelected; + const isHidden = + isSelectAllORDeslectAllOption && + options.length > groupBySelectionLimit; + + if (isHidden) { + return ; + } + return ( + + {option.label} + + + ); + }} + value={selectedValue} + /> + + + + + + + + ); +}; From cc05634974af438374f72d0f334c51ccc751549a Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Wed, 20 Aug 2025 14:17:07 +0530 Subject: [PATCH 02/39] upcoming: [DI-22916] - Added memo to group by drawer component --- .../GroupBy/CloudPulseGroupByDrawer.tsx | 210 ++++++++++-------- 1 file changed, 113 insertions(+), 97 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx index 821353105b9..38b5572cfd8 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx @@ -19,6 +19,7 @@ export interface GroupByDrawerProps { onCancel: () => void; open?: boolean; options: GroupByOption[]; + serviceType: string; subtitle?: string; title: string; } @@ -29,107 +30,122 @@ export interface GroupByOption { } const groupBySelectionLimit = 3; +export const CloudPulseGroupByDrawer = React.memo( + (props: GroupByDrawerProps) => { + const { + open, + title, + subtitle, + message, + options, + defaultValue = [], + onApply, + onCancel, + serviceType, + } = props; -export const CloudPulseGroupByDrawer = (props: GroupByDrawerProps) => { - const { - open, - title, - subtitle, - message, - options, - defaultValue = [], - onApply, - onCancel, - } = props; + const [selectedValue, setSelectedValue] = React.useState( + defaultValue?.slice( + 0, + Math.min(defaultValue.length ?? 0, groupBySelectionLimit) + ) + ); + const previousValueRef = React.useRef(defaultValue); - const [selectedValue, setSelectedValue] = React.useState( - defaultValue?.slice( - 0, - Math.min(defaultValue.length ?? 0, groupBySelectionLimit) - ) - ); - const previousValueRef = React.useRef(defaultValue); + const handleApply = () => { + previousValueRef.current = selectedValue; + onApply(selectedValue); + }; - const handleApply = () => { - previousValueRef.current = selectedValue; - onApply(selectedValue); - }; + React.useEffect(() => { + const value = defaultValue?.slice( + 0, + Math.min(defaultValue.length ?? 0, groupBySelectionLimit) + ); + onApply(value); + setSelectedValue(value); + }, [serviceType]); + const handleClose = () => { + setSelectedValue(previousValueRef.current); + onCancel(); + }; - const handleClose = () => { - setSelectedValue(previousValueRef.current); - onCancel(); - }; - - return ( - handleClose()} open={open} title={title}> - - ({ marginTop: -1, font: theme.font.normal })} - variant="h2" - > - {subtitle} - - ({ font: theme.font.normal })} variant="h2"> - {message} - - option.value === value.value} - label="Select Dimensions" - multiple - onChange={(_event, value) => { - setSelectedValue(value); - }} - options={options} - placeholder="Select Dimnensions" - renderOption={(props, option) => { - const { key, ...rest } = props; - const isSelectAllORDeslectAllOption = - option.label === 'Select All ' || - option.label === 'Deselect All '; - const isSelected = props['aria-selected'] === true; - const ListItem = isSelectAllORDeslectAllOption - ? StyledListItem - : 'li'; - const isDisabled = - selectedValue.length >= groupBySelectionLimit && !isSelected; - const isHidden = - isSelectAllORDeslectAllOption && - options.length > groupBySelectionLimit; - - if (isHidden) { - return ; - } - return ( - - {option.label} - - - ); - }} - value={selectedValue} - /> - - - - + {message} + + + option.value === value.value + } + label="Select Dimensions" + multiple + onChange={(_event, value) => { + setSelectedValue(value); + }} + options={options} + placeholder="Select Dimnensions" + renderOption={(props, option) => { + const { key, ...rest } = props; + const isSelectAllORDeslectAllOption = + option.label === 'Select All ' || + option.label === 'Deselect All '; + const isSelected = props['aria-selected'] === true; + const ListItem = isSelectAllORDeslectAllOption + ? StyledListItem + : 'li'; + const isDisabled = + selectedValue.length >= groupBySelectionLimit && !isSelected; + const isHidden = + isSelectAllORDeslectAllOption && + options.length > groupBySelectionLimit; + + if (isHidden) { + return ; + } + return ( + + {option.label} + + + ); + }} + value={selectedValue} + /> + + + + + - - - ); -}; + + ); + } +); From ddb89978fe7c31349658da59f29a2c8826106e74 Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Wed, 20 Aug 2025 14:17:44 +0530 Subject: [PATCH 03/39] upcoming: [DI-22916] - Added hook to fetch dimension for group by drop down --- .../GroupBy/useGroupByDimension.tsx | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.tsx diff --git a/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.tsx b/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.tsx new file mode 100644 index 00000000000..7bd890c535d --- /dev/null +++ b/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.tsx @@ -0,0 +1,103 @@ +import { useCloudPulseDashboardByIdQuery } from 'src/queries/cloudpulse/dashboards'; +import { useGetCloudPulseMetricDefinitionsByServiceType } from 'src/queries/cloudpulse/services'; + +import type { GroupByOption } from './CloudPulseGroupByDrawer'; +import type { + CloudPulseServiceType, + Dimension, + MetricDefinition, +} from '@linode/api-v4'; + +const defaultOption: GroupByOption = { + label: 'Entity Id', + value: 'entity_id', +}; + +interface GroupByDimension { + defaultValue: GroupByOption[]; + isLoading: boolean; + options: GroupByOption[]; +} + +interface MetricDimension { + [metric: string]: Dimension[]; +} + +export const useGlobalDimensions = ( + dashboardId: number | undefined, + serviceType: CloudPulseServiceType | undefined +): GroupByDimension => { + const { data: dashboard, isLoading: dashboardLoading } = + useCloudPulseDashboardByIdQuery(dashboardId); + const { data: metricDefinition, isLoading: metricLoading } = + useGetCloudPulseMetricDefinitionsByServiceType( + serviceType, + serviceType !== undefined + ); + + if (metricLoading || dashboardLoading) { + return { options: [], defaultValue: [], isLoading: true }; + } + const metricDimensions = getMetricDimensions(metricDefinition?.data ?? []); + const commonDimensions = getCommonDimensions(metricDimensions); + + const commonGroups = getCommonGroups( + dashboard?.group_by ?? [], + commonDimensions + ); + return { + options: [defaultOption, ...commonDimensions], + defaultValue: [defaultOption, ...commonGroups], + isLoading: false, + }; +}; + +const getCommonGroups = ( + groupBy: string[], + commonDimensions: GroupByOption[] +): GroupByOption[] => { + if (groupBy.length === 0 || commonDimensions.length === 0) return []; + + return commonDimensions.filter((group) => { + return groupBy.includes(group.value); + }); +}; + +const getMetricDimensions = ( + metricDefinition: MetricDefinition[] +): MetricDimension => { + return metricDefinition.reduce((acc, { metric, dimensions }) => { + return { + ...acc, + [metric]: dimensions, + }; + }, {}); +}; + +const getCommonDimensions = ( + metricDimensions: MetricDimension +): GroupByOption[] => { + const metrics = Object.keys(metricDimensions); + if (metrics.length === 0) { + return []; + } + + // Get dimensions from first metric + const firstMetricDimensions = metricDimensions[metrics[0]]; + + // Find dimensions that exist in all metrics + return firstMetricDimensions + .filter(({ dimension_label: queried }) => { + return metrics.every((metric) => { + return metricDimensions[metric].some( + ({ dimension_label }) => dimension_label === queried + ); + }); + }) + .map(({ label, dimension_label }) => { + return { + label, + value: dimension_label, + }; + }); +}; From 8cf75b14de5aff661adb4771fe6b697fb07f1b50 Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Wed, 20 Aug 2025 14:22:25 +0530 Subject: [PATCH 04/39] upcoming: [DI-22916] - Updated test case --- .../CloudPulse/GroupBy/CloudPulseGroupByDrawer.test.tsx | 1 + .../features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.test.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.test.tsx index 483425d61a4..a13be0c5616 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.test.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.test.tsx @@ -29,6 +29,7 @@ const drawerProps: GroupByDrawerProps = { message, options, open: true, + serviceType: 'linode', }; describe('Cloud Pulse Group By Drawer Component', () => { diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx index 38b5572cfd8..eb7efa18fc1 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx @@ -12,6 +12,8 @@ import * as React from 'react'; import NullComponent from 'src/components/NullComponent'; +import type { CloudPulseServiceType } from '@linode/api-v4'; + export interface GroupByDrawerProps { defaultValue?: GroupByOption[]; message?: string; @@ -19,7 +21,7 @@ export interface GroupByDrawerProps { onCancel: () => void; open?: boolean; options: GroupByOption[]; - serviceType: string; + serviceType: CloudPulseServiceType; subtitle?: string; title: string; } From c530be9b643647fd9e6b3075bc39cb38d5173e26 Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Tue, 9 Sep 2025 19:22:30 +0530 Subject: [PATCH 05/39] upcoming: [DI-22916] - Remove unused imports --- .../CloudPulse/GroupBy/CloudPulseGroupByDrawer.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.test.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.test.tsx index a13be0c5616..f2636521958 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.test.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.test.tsx @@ -1,4 +1,4 @@ -import { screen, waitFor, within } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import React from 'react'; import { renderWithTheme } from 'src/utilities/testHelpers'; From 0bc537a8bed3f617dd40f261ff90df6e81c368f0 Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Mon, 25 Aug 2025 17:53:45 +0530 Subject: [PATCH 06/39] upcoming: [DI-26666] - Added useWidgetDimension hook to generate widget level dimension --- .../GroupBy/useGroupByDimension.tsx | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.tsx b/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.tsx index 7bd890c535d..2b465248d5e 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.tsx @@ -63,6 +63,52 @@ const getCommonGroups = ( }); }; +export const useWidgetDimension = ( + dashboardId: number | undefined, + serviceType: CloudPulseServiceType | undefined, + globalDimensions: GroupByOption[], + metric: string | undefined +) => { + const { data: dashboard, isLoading: dashboardLoading } = + useCloudPulseDashboardByIdQuery(dashboardId); + const { data: metricDefinition, isLoading: metricLoading } = + useGetCloudPulseMetricDefinitionsByServiceType( + serviceType, + serviceType !== undefined + ); + + if (metricLoading || dashboardLoading) { + return { options: [], defaultValue: [], isLoading: true }; + } + + const metricDimensions: GroupByOption[] = + metricDefinition?.data + .find((def) => def.metric === metric) + ?.dimensions?.map(({ label, dimension_label }) => ({ + label, + value: dimension_label, + })) ?? []; + const defaultGroupBy = + dashboard?.widgets.find((widget) => widget.metric === metric)?.group_by ?? + []; + const options = metricDimensions.filter( + (metricDimension) => + !globalDimensions.some( + (dimension) => dimension.label === metricDimension.label + ) + ); + + const defaultValue = options.filter((options) => + defaultGroupBy.includes(options.value) + ); + + return { + options, + defaultValue, + isLoading: false, + }; +}; + const getMetricDimensions = ( metricDefinition: MetricDefinition[] ): MetricDimension => { From 65922d81647878ae268c3f15591066752e5982bd Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Tue, 9 Sep 2025 19:27:17 +0530 Subject: [PATCH 07/39] upcoming: [DI-22916] - Updated interface documentation --- .../GroupBy/CloudPulseGroupByDrawer.tsx | 27 +++++++++++++++++++ ...ByDimension.tsx => useGroupByDimension.ts} | 9 +++++++ 2 files changed, 36 insertions(+) rename packages/manager/src/features/CloudPulse/GroupBy/{useGroupByDimension.tsx => useGroupByDimension.ts} (95%) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx index eb7efa18fc1..bd5a534ee31 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx @@ -15,14 +15,41 @@ import NullComponent from 'src/components/NullComponent'; import type { CloudPulseServiceType } from '@linode/api-v4'; export interface GroupByDrawerProps { + /** + * Default selected group by options + */ defaultValue?: GroupByOption[]; + /** + * Optional message to display in the drawer + */ message?: string; + /** + * Callback function triggered when apply button is clicked + */ onApply: (value: GroupByOption[]) => void; + /** + * Callback function triggered when cancel button is clicked + */ onCancel: () => void; + /** + * Controls whether the drawer is open + */ open?: boolean; + /** + * Available group by options to select from + */ options: GroupByOption[]; + /** + * Type of cloud service being monitored + */ serviceType: CloudPulseServiceType; + /** + * Optional subtitle for the drawer + */ subtitle?: string; + /** + * Title for the drawer + */ title: string; } diff --git a/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.tsx b/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.ts similarity index 95% rename from packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.tsx rename to packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.ts index 2b465248d5e..ff63d27b940 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.ts @@ -14,8 +14,17 @@ const defaultOption: GroupByOption = { }; interface GroupByDimension { + /** + * The default grouping options to use + */ defaultValue: GroupByOption[]; + /** + * Indicates if the grouping options are currently loading + */ isLoading: boolean; + /** + * Available grouping options + */ options: GroupByOption[]; } From 844adc6e1d0a886c7406973666d4c194052e1d86 Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Tue, 26 Aug 2025 13:59:18 +0530 Subject: [PATCH 08/39] upcoming: [DI-26666] - Updated logic to no disable select all button --- .../features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx index bd5a534ee31..671b0626e1e 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx @@ -147,7 +147,11 @@ export const CloudPulseGroupByDrawer = React.memo( return ; } return ( - + {option.label} From 5463d9c83f3936f64f7696869578df69fedab6f3 Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Tue, 2 Sep 2025 15:53:22 +0530 Subject: [PATCH 09/39] upcoming: [DI-26666] - Dynamically hiding maximum selection limit message --- .../CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx index 671b0626e1e..731b37a26cb 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx @@ -117,7 +117,11 @@ export const CloudPulseGroupByDrawer = React.memo( 3 + ? `You can select upto ${groupBySelectionLimit} dimensions.` + : undefined + } isOptionEqualToValue={(option, value) => option.value === value.value } @@ -127,7 +131,7 @@ export const CloudPulseGroupByDrawer = React.memo( setSelectedValue(value); }} options={options} - placeholder="Select Dimnensions" + placeholder="Select Dimensions" renderOption={(props, option) => { const { key, ...rest } = props; const isSelectAllORDeslectAllOption = From 274d010c9987a7a57271f0e68c3b640e4cc4a0f4 Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Tue, 2 Sep 2025 15:55:09 +0530 Subject: [PATCH 10/39] upcoming: [DI-26666] - Show default options from dashboard api in global group by --- .../features/CloudPulse/GroupBy/useGroupByDimension.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.ts b/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.ts index ff63d27b940..17f9f9147d5 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.ts +++ b/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.ts @@ -48,15 +48,18 @@ export const useGlobalDimensions = ( return { options: [], defaultValue: [], isLoading: true }; } const metricDimensions = getMetricDimensions(metricDefinition?.data ?? []); - const commonDimensions = getCommonDimensions(metricDimensions); + const commonDimensions = [ + defaultOption, + ...getCommonDimensions(metricDimensions), + ]; const commonGroups = getCommonGroups( dashboard?.group_by ?? [], commonDimensions ); return { - options: [defaultOption, ...commonDimensions], - defaultValue: [defaultOption, ...commonGroups], + options: commonDimensions, + defaultValue: commonGroups, isLoading: false, }; }; From 3a0064da34696134789b67751ece6dd6e12d1035 Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Tue, 2 Sep 2025 15:57:34 +0530 Subject: [PATCH 11/39] upcoming: [DI-26666] - Updated default value logic --- .../GroupBy/CloudPulseGroupByDrawer.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx index 731b37a26cb..b5cfbe6669c 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx @@ -74,12 +74,17 @@ export const CloudPulseGroupByDrawer = React.memo( } = props; const [selectedValue, setSelectedValue] = React.useState( - defaultValue?.slice( + defaultValue.slice( 0, - Math.min(defaultValue.length ?? 0, groupBySelectionLimit) + Math.min(defaultValue.length, groupBySelectionLimit) + ) + ); + const previousValueRef = React.useRef( + defaultValue.slice( + 0, + Math.min(defaultValue.length, groupBySelectionLimit) ) ); - const previousValueRef = React.useRef(defaultValue); const handleApply = () => { previousValueRef.current = selectedValue; @@ -87,9 +92,9 @@ export const CloudPulseGroupByDrawer = React.memo( }; React.useEffect(() => { - const value = defaultValue?.slice( + const value = defaultValue.slice( 0, - Math.min(defaultValue.length ?? 0, groupBySelectionLimit) + Math.min(defaultValue.length, groupBySelectionLimit) ); onApply(value); setSelectedValue(value); From 9dcfb9f631056fa725ecdd2af8db860e9d70786c Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Wed, 3 Sep 2025 10:57:52 +0530 Subject: [PATCH 12/39] upcoming: [DI-26666] - moved option rendering to a function --- .../GroupBy/CloudPulseGroupByDrawer.tsx | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx index b5cfbe6669c..e0d066d726f 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx @@ -104,6 +104,37 @@ export const CloudPulseGroupByDrawer = React.memo( onCancel(); }; + const renderOptions = ( + props: React.HTMLAttributes & { + key: string; + }, + option: GroupByOption + ) => { + const { key, ...rest } = props; + const isSelectAllORDeslectAllOption = + option.label === 'Select All ' || option.label === 'Deselect All '; + const isSelected = props['aria-selected'] === true; + const ListItem = isSelectAllORDeslectAllOption ? StyledListItem : 'li'; + const isDisabled = + selectedValue.length >= groupBySelectionLimit && !isSelected; + const isHidden = + isSelectAllORDeslectAllOption && options.length > groupBySelectionLimit; + + if (isHidden) { + return ; + } + return ( + + {option.label} + + + ); + }; + return ( handleClose()} open={open} title={title}> @@ -137,35 +168,7 @@ export const CloudPulseGroupByDrawer = React.memo( }} options={options} placeholder="Select Dimensions" - renderOption={(props, option) => { - const { key, ...rest } = props; - const isSelectAllORDeslectAllOption = - option.label === 'Select All ' || - option.label === 'Deselect All '; - const isSelected = props['aria-selected'] === true; - const ListItem = isSelectAllORDeslectAllOption - ? StyledListItem - : 'li'; - const isDisabled = - selectedValue.length >= groupBySelectionLimit && !isSelected; - const isHidden = - isSelectAllORDeslectAllOption && - options.length > groupBySelectionLimit; - - if (isHidden) { - return ; - } - return ( - - {option.label} - - - ); - }} + renderOption={renderOptions} value={selectedValue} /> From 17bb0242864d3824c4db470e25e85db27acf8eaa Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Tue, 9 Sep 2025 19:35:41 +0530 Subject: [PATCH 13/39] upcoming: [DI-26666] - Added group by in dashboard interface --- packages/api-v4/src/cloudpulse/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/api-v4/src/cloudpulse/types.ts b/packages/api-v4/src/cloudpulse/types.ts index 72b3d72d9db..8e2974fb5a2 100644 --- a/packages/api-v4/src/cloudpulse/types.ts +++ b/packages/api-v4/src/cloudpulse/types.ts @@ -39,6 +39,7 @@ type AlertNotificationPagerDuty = 'pagerduty'; type AlertNotificationWebHook = 'webhook'; export interface Dashboard { created: string; + group_by?: string[]; id: number; label: string; service_type: CloudPulseServiceType; From 7dc426feb47a5f30b31918df6cf0164105de4486 Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Tue, 9 Sep 2025 19:50:11 +0530 Subject: [PATCH 14/39] Added changeset --- packages/api-v4/.changeset/pr-12843-added-1757427551332.md | 5 +++++ .../.changeset/pr-12843-upcoming-features-1757427598805.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 packages/api-v4/.changeset/pr-12843-added-1757427551332.md create mode 100644 packages/manager/.changeset/pr-12843-upcoming-features-1757427598805.md diff --git a/packages/api-v4/.changeset/pr-12843-added-1757427551332.md b/packages/api-v4/.changeset/pr-12843-added-1757427551332.md new file mode 100644 index 00000000000..52b85e8de56 --- /dev/null +++ b/packages/api-v4/.changeset/pr-12843-added-1757427551332.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Added +--- + +ACLP: `group_by` property in Dashboard interface ([#12843](https://github.com/linode/manager/pull/12843)) diff --git a/packages/manager/.changeset/pr-12843-upcoming-features-1757427598805.md b/packages/manager/.changeset/pr-12843-upcoming-features-1757427598805.md new file mode 100644 index 00000000000..343b265d4e4 --- /dev/null +++ b/packages/manager/.changeset/pr-12843-upcoming-features-1757427598805.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +ACLP: add `CloudPulseGroupByDrawer` compoennt ([#12843](https://github.com/linode/manager/pull/12843)) From d54bd2eea7f6f2bb48f3d8f8059f43f4da95dc7b Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Wed, 10 Sep 2025 14:10:52 +0530 Subject: [PATCH 15/39] upcoming: [DI-26666] - Added method documentation --- .../CloudPulse/GroupBy/useGroupByDimension.ts | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.ts b/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.ts index 17f9f9147d5..667833c09fb 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.ts +++ b/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.ts @@ -32,6 +32,12 @@ interface MetricDimension { [metric: string]: Dimension[]; } +/** + * + * @param dashboardId The ID of the dashboard being queried + * @param serviceType The type of cloud service (e.g., 'linode', 'dbaas') + * @returns A GroupByDimension object containing available options, default values, and loading state + */ export const useGlobalDimensions = ( dashboardId: number | undefined, serviceType: CloudPulseServiceType | undefined @@ -64,6 +70,12 @@ export const useGlobalDimensions = ( }; }; +/** + * + * @param groupBy Default group by list from dashboard + * @param commonDimensions The available common dimensions across all metrics + * @returns An array of GroupByOption objects that exist in both the dashboard config and common dimensions + */ const getCommonGroups = ( groupBy: string[], commonDimensions: GroupByOption[] @@ -75,12 +87,20 @@ const getCommonGroups = ( }); }; +/** + * + * @param dashboardId The ID of the dashboard being queried + * @param serviceType The type of cloud service (e.g., 'linode', 'dbaas') + * @param globalDimensions - Common dimensions that are already selected at the dashboard level + * @param metric - The specific metric for which to retrieve available dimensions + * @returns A GroupByDimension object containing available options, default values, and loading state + */ export const useWidgetDimension = ( dashboardId: number | undefined, serviceType: CloudPulseServiceType | undefined, globalDimensions: GroupByOption[], metric: string | undefined -) => { +): GroupByDimension => { const { data: dashboard, isLoading: dashboardLoading } = useCloudPulseDashboardByIdQuery(dashboardId); const { data: metricDefinition, isLoading: metricLoading } = @@ -121,6 +141,11 @@ export const useWidgetDimension = ( }; }; +/** + * + * @param metricDefinition List of metric definitions, each containing a metric name and its associated dimensions. + * @returns transform dimension object with metric as key and dimensions as value + */ const getMetricDimensions = ( metricDefinition: MetricDefinition[] ): MetricDimension => { @@ -132,6 +157,11 @@ const getMetricDimensions = ( }, {}); }; +/** + * + * @param metricDimensions An object where keys are metric names and values are arrays of dimensions associated with those metrics. + * @returns list of common dimensions across all metrics + */ const getCommonDimensions = ( metricDimensions: MetricDimension ): GroupByOption[] => { @@ -143,7 +173,7 @@ const getCommonDimensions = ( // Get dimensions from first metric const firstMetricDimensions = metricDimensions[metrics[0]]; - // Find dimensions that exist in all metrics + // filter dimensions that exist in all metrics return firstMetricDimensions .filter(({ dimension_label: queried }) => { return metrics.every((metric) => { From 88152b44e7ae7f42113222225d01279d08c2bde8 Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Thu, 11 Sep 2025 12:57:59 +0530 Subject: [PATCH 16/39] upcoming: [DI-26666] - Updated typos --- .../CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx index e0d066d726f..016634404c2 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx @@ -111,14 +111,15 @@ export const CloudPulseGroupByDrawer = React.memo( option: GroupByOption ) => { const { key, ...rest } = props; - const isSelectAllORDeslectAllOption = + const isSelectAllORDeselectAllOption = option.label === 'Select All ' || option.label === 'Deselect All '; const isSelected = props['aria-selected'] === true; - const ListItem = isSelectAllORDeslectAllOption ? StyledListItem : 'li'; + const ListItem = isSelectAllORDeselectAllOption ? StyledListItem : 'li'; const isDisabled = selectedValue.length >= groupBySelectionLimit && !isSelected; const isHidden = - isSelectAllORDeslectAllOption && options.length > groupBySelectionLimit; + isSelectAllORDeselectAllOption && + options.length > groupBySelectionLimit; if (isHidden) { return ; @@ -127,7 +128,7 @@ export const CloudPulseGroupByDrawer = React.memo( {option.label} @@ -155,7 +156,7 @@ export const CloudPulseGroupByDrawer = React.memo( data-testid="dimension-select" helperText={ options.length > 3 - ? `You can select upto ${groupBySelectionLimit} dimensions.` + ? `You can select up to ${groupBySelectionLimit} dimensions.` : undefined } isOptionEqualToValue={(option, value) => From b1fc0a12eaad7a0bee096817169d8304138c2c0f Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Thu, 11 Sep 2025 12:59:21 +0530 Subject: [PATCH 17/39] upcoming: [DI-26666] - Renamed file --- .../CloudPulse/GroupBy/{useGroupByDimension.ts => utils.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/manager/src/features/CloudPulse/GroupBy/{useGroupByDimension.ts => utils.ts} (100%) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.ts b/packages/manager/src/features/CloudPulse/GroupBy/utils.ts similarity index 100% rename from packages/manager/src/features/CloudPulse/GroupBy/useGroupByDimension.ts rename to packages/manager/src/features/CloudPulse/GroupBy/utils.ts From 4a16b7a77c09b2c96d734af08ed78f52c644aaaf Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Thu, 11 Sep 2025 17:19:50 +0530 Subject: [PATCH 18/39] upcoming: [DI-26666] - Added test cases --- .../features/CloudPulse/GroupBy/utils.test.ts | 238 ++++++++++++++++++ .../src/features/CloudPulse/GroupBy/utils.ts | 8 +- 2 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 packages/manager/src/features/CloudPulse/GroupBy/utils.test.ts diff --git a/packages/manager/src/features/CloudPulse/GroupBy/utils.test.ts b/packages/manager/src/features/CloudPulse/GroupBy/utils.test.ts new file mode 100644 index 00000000000..9d648715557 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/GroupBy/utils.test.ts @@ -0,0 +1,238 @@ +import { dashboardFactory } from 'src/factories'; + +import { + defaultOption, + getCommonDimensions, + getCommonGroups, + getMetricDimensions, + useGlobalDimensions, + useWidgetDimension, +} from './utils'; + +import type { MetricDefinition } from '@linode/api-v4'; + +const metricDefinitions: MetricDefinition[] = [ + { + metric: 'Metric 1', + dimensions: [ + { label: 'Dim 1', dimension_label: 'Dim 1', values: [] }, + { label: 'Dim 2', dimension_label: 'Dim 2', values: [] }, + ], + available_aggregate_functions: [], + is_alertable: false, + label: '', + metric_type: '', + scrape_interval: '', + unit: '', + }, + { + metric: 'Metric 2', + dimensions: [ + { label: 'Dim 2', dimension_label: 'Dim 2', values: [] }, + { label: 'Dim 3', dimension_label: 'Dim 3', values: [] }, + ], + available_aggregate_functions: [], + is_alertable: false, + label: '', + metric_type: '', + scrape_interval: '', + unit: '', + }, + { + metric: 'Metric 3', + dimensions: [ + { label: 'Dim 1', dimension_label: 'Dim 1', values: [] }, + { label: 'Dim 2', dimension_label: 'Dim 2', values: [] }, + { label: 'Dim 3', dimension_label: 'Dim 3', values: [] }, + ], + available_aggregate_functions: [], + is_alertable: false, + label: '', + metric_type: '', + scrape_interval: '', + unit: '', + }, +]; + +const queryMocks = vi.hoisted(() => ({ + useCloudPulseDashboardByIdQuery: vi.fn().mockReturnValue({}), + useGetCloudPulseMetricDefinitionsByServiceType: vi.fn().mockReturnValue({}), +})); + +vi.mock('src/queries/cloudpulse/dashboards', async (importActual) => ({ + ...importActual(), + useCloudPulseDashboardByIdQuery: queryMocks.useCloudPulseDashboardByIdQuery, +})); + +vi.mock('src/queries/cloudpulse/services', async (importActual) => ({ + ...importActual(), + useGetCloudPulseMetricDefinitionsByServiceType: + queryMocks.useGetCloudPulseMetricDefinitionsByServiceType, +})); + +describe('useGlobalDimensions method test', () => { + it('should return loading state when data is being fetched', () => { + queryMocks.useCloudPulseDashboardByIdQuery.mockReturnValue({ + data: null, + isLoading: true, + }); + queryMocks.useGetCloudPulseMetricDefinitionsByServiceType.mockReturnValue({ + data: null, + isLoading: true, + }); + + const result = useGlobalDimensions(1, 'linode'); + expect(result).toEqual({ options: [], defaultValue: [], isLoading: true }); + }); + + it('should return empty options and defaultValue if no common dimensions', () => { + queryMocks.useCloudPulseDashboardByIdQuery.mockReturnValue({ + data: dashboardFactory.build(), + isLoading: false, + }); + queryMocks.useGetCloudPulseMetricDefinitionsByServiceType.mockReturnValue({ + data: { + data: metricDefinitions, + }, + isLoading: false, + }); + const result = useGlobalDimensions(1, 'linode'); + expect(result).toEqual({ + options: [defaultOption, { label: 'Dim 2', value: 'Dim 2' }], + defaultValue: [], + isLoading: false, + }); + }); +}); + +describe('useWidgetDimension method test', () => { + it('should return empty options and defaultValue', () => { + queryMocks.useCloudPulseDashboardByIdQuery.mockReturnValue({ + data: dashboardFactory.build(), + isLoading: false, + }); + + queryMocks.useGetCloudPulseMetricDefinitionsByServiceType.mockReturnValue({ + data: { + data: metricDefinitions, + }, + isLoading: false, + }); + + const result = useWidgetDimension( + 1, + 'linode', + [ + { label: 'Dim 1', value: 'Dim 1' }, + { label: 'Dim 2', value: 'Dim 2' }, + ], + 'Metric 1' + ); + + expect(result.options).toHaveLength(0); + expect(result.defaultValue).toHaveLength(0); + expect(result.isLoading).toBe(false); + }); + + it('should return non-empty options and empty defaultValue', () => { + queryMocks.useCloudPulseDashboardByIdQuery.mockReturnValue({ + data: dashboardFactory.build(), + isLoading: false, + }); + + queryMocks.useGetCloudPulseMetricDefinitionsByServiceType.mockReturnValue({ + data: { + data: metricDefinitions, + }, + isLoading: false, + }); + + const result = useWidgetDimension( + 1, + 'linode', + [{ label: 'Dim 1', value: 'Dim 1' }], + 'Metric 1' + ); + + expect(result.options).toHaveLength(1); + expect(result.defaultValue).toHaveLength(0); + expect(result.isLoading).toBe(false); + }); +}); +describe('getCommonGroups method test', () => { + it('should return empty list if groups or commonDimensions are empty', () => { + const result = getCommonGroups([], []); + expect(result).toHaveLength(0); + }); + + it('should return common groups', () => { + const groups: string[] = ['Group 1', 'Group 2']; + const commonDimensions = [ + { label: 'Group 1', value: 'Group 1' }, + { label: 'Group 2', value: 'Group 2' }, + { label: 'Group 3', value: 'Group 3' }, + ]; + const result = getCommonGroups(groups, commonDimensions); + expect(result).toHaveLength(2); + expect(result).toEqual([ + { label: 'Group 1', value: 'Group 1' }, + { label: 'Group 2', value: 'Group 2' }, + ]); + }); +}); + +describe('getMetricDimensions method test', () => { + it('should return empty object if metric definitions are empty', () => { + const result = getMetricDimensions([]); + expect(result).toEqual({}); + }); + + it('should return unique dimensions from metric definitions', () => { + const result = getMetricDimensions(metricDefinitions); + expect(result).toEqual({ + 'Metric 1': [ + { label: 'Dim 1', dimension_label: 'Dim 1', values: [] }, + { label: 'Dim 2', dimension_label: 'Dim 2', values: [] }, + ], + 'Metric 2': [ + { label: 'Dim 2', dimension_label: 'Dim 2', values: [] }, + { label: 'Dim 3', dimension_label: 'Dim 3', values: [] }, + ], + 'Metric 3': [ + { label: 'Dim 1', dimension_label: 'Dim 1', values: [] }, + { label: 'Dim 2', dimension_label: 'Dim 2', values: [] }, + { label: 'Dim 3', dimension_label: 'Dim 3', values: [] }, + ], + }); + }); + + describe('getCommonDimensions method test', () => { + it('should return empty list if metricDimensions is empty', () => { + const result = getCommonDimensions({}); + expect(result).toHaveLength(0); + }); + + it('should return common dimensions across all metrics', () => { + const metricDimensions = { + 'Metric 1': [ + { label: 'Dim 1', dimension_label: 'Dim 1', values: [] }, + { label: 'Dim 2', dimension_label: 'Dim 2', values: [] }, + { label: 'Dim 3', dimension_label: 'Dim 3', values: [] }, + ], + 'Metric 2': [ + { label: 'Dim 2', dimension_label: 'Dim 2', values: [] }, + { label: 'Dim 3', dimension_label: 'Dim 3', values: [] }, + { label: 'Dim 4', dimension_label: 'Dim 4', values: [] }, + ], + 'Metric 3': [ + { label: 'Dim 1', dimension_label: 'Dim 1', values: [] }, + { label: 'Dim 3', dimension_label: 'Dim 3', values: [] }, + { label: 'Dim 4', dimension_label: 'Dim 4', values: [] }, + ], + }; + const result = getCommonDimensions(metricDimensions); + expect(result).toHaveLength(1); + expect(result).toEqual([{ label: 'Dim 3', value: 'Dim 3' }]); + }); + }); +}); diff --git a/packages/manager/src/features/CloudPulse/GroupBy/utils.ts b/packages/manager/src/features/CloudPulse/GroupBy/utils.ts index 667833c09fb..4b642e1a5ef 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/utils.ts +++ b/packages/manager/src/features/CloudPulse/GroupBy/utils.ts @@ -8,7 +8,7 @@ import type { MetricDefinition, } from '@linode/api-v4'; -const defaultOption: GroupByOption = { +export const defaultOption: GroupByOption = { label: 'Entity Id', value: 'entity_id', }; @@ -76,7 +76,7 @@ export const useGlobalDimensions = ( * @param commonDimensions The available common dimensions across all metrics * @returns An array of GroupByOption objects that exist in both the dashboard config and common dimensions */ -const getCommonGroups = ( +export const getCommonGroups = ( groupBy: string[], commonDimensions: GroupByOption[] ): GroupByOption[] => { @@ -146,7 +146,7 @@ export const useWidgetDimension = ( * @param metricDefinition List of metric definitions, each containing a metric name and its associated dimensions. * @returns transform dimension object with metric as key and dimensions as value */ -const getMetricDimensions = ( +export const getMetricDimensions = ( metricDefinition: MetricDefinition[] ): MetricDimension => { return metricDefinition.reduce((acc, { metric, dimensions }) => { @@ -162,7 +162,7 @@ const getMetricDimensions = ( * @param metricDimensions An object where keys are metric names and values are arrays of dimensions associated with those metrics. * @returns list of common dimensions across all metrics */ -const getCommonDimensions = ( +export const getCommonDimensions = ( metricDimensions: MetricDimension ): GroupByOption[] => { const metrics = Object.keys(metricDimensions); From fef65f37bed2ae03f81961205dd374bf1e02b6c7 Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Thu, 11 Sep 2025 18:08:12 +0530 Subject: [PATCH 19/39] upcoming: [DI-26666] - Updated subtitle and message component --- .../CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx index 016634404c2..018532d6579 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx @@ -142,14 +142,11 @@ export const CloudPulseGroupByDrawer = React.memo( ({ marginTop: -1, font: theme.font.normal })} - variant="h2" + variant="h3" > {subtitle} - ({ font: theme.font.normal })} - variant="h2" - > + ({ font: theme.font.normal })}> {message} Date: Thu, 11 Sep 2025 21:24:20 +0530 Subject: [PATCH 20/39] upcoming: [DI-26666] - Removed typography styling --- .../features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx index 018532d6579..f5b35e22c3a 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx @@ -137,7 +137,7 @@ export const CloudPulseGroupByDrawer = React.memo( }; return ( - handleClose()} open={open} title={title}> + handleClose()} open={true} title={title}> {subtitle} - ({ font: theme.font.normal })}> - {message} - + {message} Date: Thu, 11 Sep 2025 21:25:12 +0530 Subject: [PATCH 21/39] reverted hardcoded value --- .../src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx index f5b35e22c3a..fd4af363852 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx @@ -137,7 +137,7 @@ export const CloudPulseGroupByDrawer = React.memo( }; return ( - handleClose()} open={true} title={title}> + handleClose()} open={open} title={title}> Date: Wed, 20 Aug 2025 14:16:24 +0530 Subject: [PATCH 22/39] upcoming: [DI-22916] - Added group by svg file --- packages/manager/src/assets/icons/group-by.svg | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 packages/manager/src/assets/icons/group-by.svg diff --git a/packages/manager/src/assets/icons/group-by.svg b/packages/manager/src/assets/icons/group-by.svg new file mode 100644 index 00000000000..4035c0e3b72 --- /dev/null +++ b/packages/manager/src/assets/icons/group-by.svg @@ -0,0 +1,6 @@ + + + + + + From 07e1dfd4eb51a60f4d6de8284e7531161425782f Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Wed, 20 Aug 2025 14:20:34 +0530 Subject: [PATCH 23/39] upcoming: [DI-22916] - Added Global Filter group by renderer --- .../GroupBy/CloudPulseGroupByDrawer.tsx | 16 ++-- .../GroupBy/GlobalFilterGroupByRenderer.tsx | 83 +++++++++++++++++++ .../features/CloudPulse/GroupBy/constants.ts | 4 + 3 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx create mode 100644 packages/manager/src/features/CloudPulse/GroupBy/constants.ts diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx index fd4af363852..3a5e3fecdf5 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx @@ -12,6 +12,8 @@ import * as React from 'react'; import NullComponent from 'src/components/NullComponent'; +import { GROUP_BY_SELECTION_LIMIT } from './constants'; + import type { CloudPulseServiceType } from '@linode/api-v4'; export interface GroupByDrawerProps { @@ -57,8 +59,6 @@ export interface GroupByOption { label: string; value: string; } - -const groupBySelectionLimit = 3; export const CloudPulseGroupByDrawer = React.memo( (props: GroupByDrawerProps) => { const { @@ -76,13 +76,13 @@ export const CloudPulseGroupByDrawer = React.memo( const [selectedValue, setSelectedValue] = React.useState( defaultValue.slice( 0, - Math.min(defaultValue.length, groupBySelectionLimit) + Math.min(defaultValue.length ?? 0, GROUP_BY_SELECTION_LIMIT) ) ); const previousValueRef = React.useRef( defaultValue.slice( 0, - Math.min(defaultValue.length, groupBySelectionLimit) + Math.min(defaultValue.length, GROUP_BY_SELECTION_LIMIT) ) ); @@ -94,7 +94,7 @@ export const CloudPulseGroupByDrawer = React.memo( React.useEffect(() => { const value = defaultValue.slice( 0, - Math.min(defaultValue.length, groupBySelectionLimit) + Math.min(defaultValue.length ?? 0, GROUP_BY_SELECTION_LIMIT) ); onApply(value); setSelectedValue(value); @@ -116,10 +116,10 @@ export const CloudPulseGroupByDrawer = React.memo( const isSelected = props['aria-selected'] === true; const ListItem = isSelectAllORDeselectAllOption ? StyledListItem : 'li'; const isDisabled = - selectedValue.length >= groupBySelectionLimit && !isSelected; + selectedValue.length >= GROUP_BY_SELECTION_LIMIT && !isSelected; const isHidden = isSelectAllORDeselectAllOption && - options.length > groupBySelectionLimit; + options.length > GROUP_BY_SELECTION_LIMIT; if (isHidden) { return ; @@ -151,7 +151,7 @@ export const CloudPulseGroupByDrawer = React.memo( data-testid="dimension-select" helperText={ options.length > 3 - ? `You can select up to ${groupBySelectionLimit} dimensions.` + ? `You can select up to ${GROUP_BY_SELECTION_LIMIT} dimensions.` : undefined } isOptionEqualToValue={(option, value) => diff --git a/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx new file mode 100644 index 00000000000..33e1ca43e5d --- /dev/null +++ b/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx @@ -0,0 +1,83 @@ +import { IconButton } from '@linode/ui'; +import React from 'react'; + +import GroupByIcon from 'src/assets/icons/group-by.svg'; + +import { CloudPulseTooltip } from '../shared/CloudPulseTooltip'; +import { CloudPulseGroupByDrawer } from './CloudPulseGroupByDrawer'; +import { GLOBAL_GROUP_BY_MESSAGE } from './constants'; +import { useGlobalDimensions } from './utils'; + +import type { GroupByOption } from './CloudPulseGroupByDrawer'; +import type { Dashboard } from '@linode/api-v4'; + +interface GlobalFilterGroupByRendererProps { + handleChange: (selectedValue: string[]) => void; + selectedDashboard?: Dashboard; +} + +export const GlobalFilterGroupByRenderer = ( + props: GlobalFilterGroupByRendererProps +) => { + const { selectedDashboard, handleChange } = props; + const [isSelected, setIsSelected] = React.useState(false); + + const { options, defaultValue, isLoading } = useGlobalDimensions( + selectedDashboard?.id, + selectedDashboard?.service_type + ); + + const [open, setOpen] = React.useState(false); + + const onApply = React.useCallback( + (selectedValue: GroupByOption[]) => { + if (selectedValue.length === 0) { + setIsSelected(false); + } else { + setIsSelected(true); + } + handleChange(selectedValue.map(({ value }) => value)); + onCancel(); + }, + [handleChange] + ); + + const onCancel = React.useCallback(() => { + setOpen(false); + }, []); + return ( + <> + + setOpen(true)} + size="small" + sx={(theme) => ({ + marginBlockEnd: 'auto', + marginTop: { md: theme.spacing(3.5) }, + color: isSelected ? theme.color.buttonPrimaryHover : 'inherit', + })} + > + + + + + {!isLoading && selectedDashboard && ( + + )} + + ); +}; diff --git a/packages/manager/src/features/CloudPulse/GroupBy/constants.ts b/packages/manager/src/features/CloudPulse/GroupBy/constants.ts new file mode 100644 index 00000000000..cf4e43fed4b --- /dev/null +++ b/packages/manager/src/features/CloudPulse/GroupBy/constants.ts @@ -0,0 +1,4 @@ +export const GLOBAL_GROUP_BY_MESSAGE = + 'Use Global Group by to visually split data into separate time series lines by dimension values. This setting applies to all widgets, and only dimensions supported by all widgets are available. You can group by additional dimensions at the widget level. Dimensions are applied and displayed in the order selected.'; + +export const GROUP_BY_SELECTION_LIMIT = 3; From 372651baa548235f913892fa7c403ddbe9e1eb1f Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Mon, 25 Aug 2025 17:54:23 +0530 Subject: [PATCH 24/39] upcoming: [DI-26666] - Added widget filter group by renderer --- .../GroupBy/WidgetFilterGroupByRenderer.tsx | 92 +++++++++++++++++++ .../features/CloudPulse/GroupBy/constants.ts | 3 + 2 files changed, 95 insertions(+) create mode 100644 packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx diff --git a/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx new file mode 100644 index 00000000000..26afcea6402 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx @@ -0,0 +1,92 @@ +import { IconButton } from '@linode/ui'; +import React from 'react'; + +import GroupByIcon from 'src/assets/icons/group-by.svg'; + +import { CloudPulseTooltip } from '../shared/CloudPulseTooltip'; +import { CloudPulseGroupByDrawer } from './CloudPulseGroupByDrawer'; +import { WIDGET_GROUP_BY_MESSAGE } from './constants'; +import { useGlobalDimensions, useWidgetDimension } from './useGroupByDimension'; + +import type { GroupByOption } from './CloudPulseGroupByDrawer'; +import type { CloudPulseServiceType } from '@linode/api-v4'; + +interface WidgetFilterGroupByRendererProps { + dashboardId: number; + handleChange: (selectedValue: string[]) => void; + label: string; + metric: string; + serviceType: CloudPulseServiceType; +} + +export const WidgetFilterGroupByRenderer = ( + props: WidgetFilterGroupByRendererProps +) => { + const { metric, dashboardId, serviceType, label, handleChange } = props; + const [isSelected, setIsSelected] = React.useState(false); + + const { isLoading: globalDimensionLoading, options: globalDimensions } = + useGlobalDimensions(dashboardId, serviceType); + const { + isLoading: widgetDimensionLoading, + options: widgetDimensions, + defaultValue, + } = useWidgetDimension(dashboardId, serviceType, globalDimensions, metric); + const [open, setOpen] = React.useState(false); + const onCancel = React.useCallback(() => { + setOpen(false); + }, []); + const onApply = React.useCallback( + (selectedValue: GroupByOption[]) => { + if (selectedValue.length === 0) { + setIsSelected(false); + } else { + setIsSelected(true); + } + handleChange(selectedValue.map(({ value }) => value)); + onCancel(); + }, + [handleChange, onCancel] + ); + + const isDisabled = + globalDimensionLoading || + widgetDimensionLoading || + widgetDimensions.length === 0; + + return ( + <> + + setOpen(true)} + size="small" + sx={(theme) => ({ + marginBlockEnd: 'auto', + color: isSelected ? theme.color.buttonPrimaryHover : 'inherit', + padding: 0, + })} + > + + + + + {!isDisabled && ( + + )} + + ); +}; diff --git a/packages/manager/src/features/CloudPulse/GroupBy/constants.ts b/packages/manager/src/features/CloudPulse/GroupBy/constants.ts index cf4e43fed4b..33895b8bb99 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/constants.ts +++ b/packages/manager/src/features/CloudPulse/GroupBy/constants.ts @@ -2,3 +2,6 @@ export const GLOBAL_GROUP_BY_MESSAGE = 'Use Global Group by to visually split data into separate time series lines by dimension values. This setting applies to all widgets, and only dimensions supported by all widgets are available. You can group by additional dimensions at the widget level. Dimensions are applied and displayed in the order selected.'; export const GROUP_BY_SELECTION_LIMIT = 3; + +export const WIDGET_GROUP_BY_MESSAGE = + 'The Widget-level Group By setting applies only to this widget. All dimensions available for this widget can be selected, including those not supported globally. Dimensions are applied and displayed in the order they are selected.'; From 3942ca6c7f63c3491fb656c0af783f725127bb5e Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Tue, 26 Aug 2025 19:48:00 +0530 Subject: [PATCH 25/39] upcoming: [DI-26666] - Added widget group by renderer test cases --- .../GroupBy/WidgetGroupByRenderer.test.tsx | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 packages/manager/src/features/CloudPulse/GroupBy/WidgetGroupByRenderer.test.tsx diff --git a/packages/manager/src/features/CloudPulse/GroupBy/WidgetGroupByRenderer.test.tsx b/packages/manager/src/features/CloudPulse/GroupBy/WidgetGroupByRenderer.test.tsx new file mode 100644 index 00000000000..1b3e822e760 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/GroupBy/WidgetGroupByRenderer.test.tsx @@ -0,0 +1,130 @@ +import { screen } from '@testing-library/react'; +import React from 'react'; + +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { WidgetGroupByRenderer } from './WidgetGroupByRenderer'; + +import type { GroupByOption } from './CloudPulseGroupByDrawer'; +import type { CloudPulseServiceType } from '@linode/api-v4'; + +const mocks = vi.hoisted(() => ({ + useGlobalDimensions: vi.fn(), + useWidgetDimension: vi.fn(), +})); + +vi.mock('./useGroupByDimension', () => { + return { + ...mocks, + }; +}); +const handleChange = vi.fn(); +const props = { + dashboardId: 1, + serviceType: 'linode' as CloudPulseServiceType, + label: 'Label 1', + metric: 'metric-1', + handleChange, +}; + +const globalDimension = { + isLoading: false, + options: [], + defaultValue: [], +}; + +const widgetGroupBy: GroupByOption[] = [ + { value: 'value-1', label: 'Value 1' }, + { value: 'value-2', label: 'Value 2' }, + { value: 'value-3', label: 'Value 3' }, +]; + +const component = ; + +describe('Widget Group By Renderer', () => { + beforeAll(() => { + mocks.useGlobalDimensions.mockRejectedValue(globalDimension); + }); + it('Should render group by icon in disabled mode', async () => { + mocks.useWidgetDimension.mockReturnValue({ + isLoading: true, + options: [], + defaultValue: [], + }); + renderWithTheme(component); + + const groupByIcon = screen.getByTestId('group-by'); + expect(groupByIcon).toBeInTheDocument(); + expect(groupByIcon).toBeDisabled(); + + await groupByIcon.click(); + + const drawer = screen.queryByTestId('drawer'); + expect(drawer).not.toBeInTheDocument(); + }); + + it('Should open drawer on click of group by icon', async () => { + mocks.useWidgetDimension.mockReturnValue({ + isLoading: false, + options: widgetGroupBy, + defaultValue: [], + }); + + renderWithTheme(component); + + const groupByIcon = screen.getByTestId('group-by'); + + await groupByIcon.click(); + + const drawer = screen.getByTestId('drawer'); + expect(drawer).toBeInTheDocument(); + + const labelText = screen.getByText('Label 1'); + expect(labelText).toBeInTheDocument(); + + const title = screen.getByText('Group By'); + expect(title).toBeInTheDocument(); + + expect(handleChange).toHaveBeenCalledWith([]); + }); + + it('Should not open drawer but group by icon should be enabled', async () => { + mocks.useWidgetDimension.mockReturnValue({ + isLoading: false, + options: widgetGroupBy, + defaultValue: [], + }); + renderWithTheme(component); + + const groupByIcon = screen.getByTestId('group-by'); + expect(groupByIcon).toBeEnabled(); + + const drawer = screen.queryByTestId('drawer'); + expect(drawer).not.toBeInTheDocument(); + }); + + it('Should open drawer on group by icon click and have default selected values', async () => { + const defaultValue = [widgetGroupBy[0]]; + + mocks.useWidgetDimension.mockReturnValue({ + isLoading: false, + options: widgetGroupBy, + defaultValue, + }); + renderWithTheme(component); + + const groupByIcon = screen.getByTestId('group-by'); + + await groupByIcon.click(); + + const drawer = screen.getByTestId('drawer'); + expect(drawer).toBeInTheDocument(); + + expect(handleChange).toHaveBeenCalledWith([defaultValue[0].value]); + + defaultValue.forEach((value) => { + const option = screen.getByRole('button', { name: value.label }); + expect(option).toBeInTheDocument(); + }); + }); +}); From 0bc0e4b40400c0a5d28e5cb07f4cc74277ccc32e Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Tue, 26 Aug 2025 14:33:24 +0530 Subject: [PATCH 26/39] upcoming: [DI-26666] - Added test cases for Global Group By Renderer --- .../GroupBy/GlobalGroupByRenderer.test.tsx | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 packages/manager/src/features/CloudPulse/GroupBy/GlobalGroupByRenderer.test.tsx diff --git a/packages/manager/src/features/CloudPulse/GroupBy/GlobalGroupByRenderer.test.tsx b/packages/manager/src/features/CloudPulse/GroupBy/GlobalGroupByRenderer.test.tsx new file mode 100644 index 00000000000..3f4d9ef34b9 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/GroupBy/GlobalGroupByRenderer.test.tsx @@ -0,0 +1,141 @@ +import { screen } from '@testing-library/react'; +import React from 'react'; + +import { dashboardFactory } from 'src/factories'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { GlobalGroupByRenderer } from './GlobalGroupByRenderer'; + +import type { GroupByOption } from './CloudPulseGroupByDrawer'; + +const handleChange = vi.fn(); +const dashboard = dashboardFactory.build(); + +const mocks = vi.hoisted(() => ({ + useGlobalDimensions: vi.fn(), +})); + +const mockGroupByOptions: GroupByOption[] = [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + { value: 'option3', label: 'Option 3' }, +]; + +vi.mock('./useGroupByDimension', async () => { + const actual = await vi.importActual('./useGroupByDimension'); + + return { + ...actual, + useGlobalDimensions: mocks.useGlobalDimensions, + }; +}); + +describe('Global Group By Renderer Component', () => { + it('should render group by icon in disabled mode on undefined dashboard', () => { + mocks.useGlobalDimensions.mockReturnValue({ + isLoading: true, + options: [], + defaultValue: [], + }); + renderWithTheme( + + ); + + const groupByIcon = screen.getByTestId('group-by'); + expect(groupByIcon).toBeDisabled(); + }); + + it('should render group by icon in disabled mode on data loading', () => { + mocks.useGlobalDimensions.mockReturnValue({ + isLoading: true, + options: [], + defaultValue: [], + }); + renderWithTheme( + + ); + + const groupByIcon = screen.getByTestId('group-by'); + expect(groupByIcon).toBeDisabled(); + }); + + it('Should render group by icon as enabled and open drawer on click', async () => { + mocks.useGlobalDimensions.mockReturnValue({ + isLoading: false, + options: mockGroupByOptions, + defaultValue: [], + }); + renderWithTheme( + + ); + + const groupByIcon = screen.getByTestId('group-by'); + expect(groupByIcon).toBeEnabled(); + + await groupByIcon.click(); + + const drawer = screen.getByTestId('drawer'); + expect(drawer).toBeInTheDocument(); + + expect(handleChange).toHaveBeenCalledWith([]); + }); + + it('Should not open drawer but group by icon should be enabled', async () => { + mocks.useGlobalDimensions.mockReturnValue({ + isLoading: false, + options: mockGroupByOptions, + defaultValue: [], + }); + renderWithTheme( + + ); + + const groupByIcon = screen.getByTestId('group-by'); + expect(groupByIcon).toBeEnabled(); + + const drawer = screen.queryByTestId('drawer'); + expect(drawer).not.toBeInTheDocument(); + }); + + it('Should open drawer on group by icon click and have default selected values', async () => { + const defaultValue = [mockGroupByOptions[0]]; + + mocks.useGlobalDimensions.mockReturnValue({ + isLoading: false, + options: mockGroupByOptions, + defaultValue, + }); + renderWithTheme( + + ); + + const groupByIcon = screen.getByTestId('group-by'); + + await groupByIcon.click(); + + const drawer = screen.getByTestId('drawer'); + expect(drawer).toBeInTheDocument(); + + expect(handleChange).toHaveBeenCalledWith([defaultValue[0].value]); + + defaultValue.forEach((value) => { + const option = screen.getByRole('button', { name: value.label }); + expect(option).toBeInTheDocument(); + }); + }); +}); From f74409d8622fbf8a1c88ae9bb27f0d27070dee3b Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Thu, 11 Sep 2025 22:29:11 +0530 Subject: [PATCH 27/39] upcoming: [DI-26666] - Updated import statement --- .../features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx index 26afcea6402..aeaed7e58c6 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx @@ -6,7 +6,7 @@ import GroupByIcon from 'src/assets/icons/group-by.svg'; import { CloudPulseTooltip } from '../shared/CloudPulseTooltip'; import { CloudPulseGroupByDrawer } from './CloudPulseGroupByDrawer'; import { WIDGET_GROUP_BY_MESSAGE } from './constants'; -import { useGlobalDimensions, useWidgetDimension } from './useGroupByDimension'; +import { useGlobalDimensions, useWidgetDimension } from './utils'; import type { GroupByOption } from './CloudPulseGroupByDrawer'; import type { CloudPulseServiceType } from '@linode/api-v4'; From 68e2e45cd00e6449951c8b6f43773891242c0d2a Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Wed, 3 Sep 2025 10:59:28 +0530 Subject: [PATCH 28/39] upcoming: [DI-26666] - Code refactor --- .../features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx | 2 +- .../features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx index 33e1ca43e5d..9c403af9748 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx @@ -37,7 +37,7 @@ export const GlobalFilterGroupByRenderer = ( setIsSelected(true); } handleChange(selectedValue.map(({ value }) => value)); - onCancel(); + setOpen(false); }, [handleChange] ); diff --git a/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx index aeaed7e58c6..26d3d450ec1 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx @@ -44,7 +44,7 @@ export const WidgetFilterGroupByRenderer = ( setIsSelected(true); } handleChange(selectedValue.map(({ value }) => value)); - onCancel(); + setOpen(false); }, [handleChange, onCancel] ); From b75c114e9809af470fb5fd0497208d9ccd43e548 Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Mon, 8 Sep 2025 16:04:59 +0530 Subject: [PATCH 29/39] Hide dimension select placeholder when atleast 1 option is selected --- .../features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx index 3a5e3fecdf5..a9ac125451d 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx @@ -157,13 +157,13 @@ export const CloudPulseGroupByDrawer = React.memo( isOptionEqualToValue={(option, value) => option.value === value.value } - label="Select Dimensions" + label="Dimensions" multiple onChange={(_event, value) => { setSelectedValue(value); }} options={options} - placeholder="Select Dimensions" + placeholder={selectedValue.length > 0 ? '' : 'Select Dimensions'} renderOption={renderOptions} value={selectedValue} /> From 2550baf1bafb0ef304bad30c58f53a69bebeddfa Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Mon, 8 Sep 2025 18:22:18 +0530 Subject: [PATCH 30/39] upcoming: [DI-26666] - Added qa id --- .../CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx | 3 ++- .../CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx index 9c403af9748..eb1ebf2815c 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx @@ -51,6 +51,7 @@ export const GlobalFilterGroupByRenderer = ( setOpen(true)} @@ -74,7 +75,7 @@ export const GlobalFilterGroupByRenderer = ( open={open} options={options} serviceType={selectedDashboard.service_type} - subtitle={`Dashboard: ${selectedDashboard.label}`} + subtitle={selectedDashboard.label} title="Global Group By" /> )} diff --git a/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx index 26d3d450ec1..4eac9f18d98 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx @@ -60,6 +60,7 @@ export const WidgetFilterGroupByRenderer = ( setOpen(true)} From d3b01b85e4039f7bff9109f6c182591220d4ecb8 Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Thu, 11 Sep 2025 22:38:08 +0530 Subject: [PATCH 31/39] upcoming: [DI-26666] - Updated file name --- ...test.tsx => GlobalFilterGroupByRenderer.test.tsx} | 12 ++++++------ ...test.tsx => WidgetFilterGroupByRenderer.test.tsx} | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) rename packages/manager/src/features/CloudPulse/GroupBy/{GlobalGroupByRenderer.test.tsx => GlobalFilterGroupByRenderer.test.tsx} (93%) rename packages/manager/src/features/CloudPulse/GroupBy/{WidgetGroupByRenderer.test.tsx => WidgetFilterGroupByRenderer.test.tsx} (96%) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/GlobalGroupByRenderer.test.tsx b/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.test.tsx similarity index 93% rename from packages/manager/src/features/CloudPulse/GroupBy/GlobalGroupByRenderer.test.tsx rename to packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.test.tsx index 3f4d9ef34b9..8dbfdd6f4f8 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/GlobalGroupByRenderer.test.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.test.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { dashboardFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import { GlobalGroupByRenderer } from './GlobalGroupByRenderer'; +import { GlobalFilterGroupByRenderer } from './GlobalFilterGroupByRenderer'; import type { GroupByOption } from './CloudPulseGroupByDrawer'; @@ -38,7 +38,7 @@ describe('Global Group By Renderer Component', () => { defaultValue: [], }); renderWithTheme( - @@ -55,7 +55,7 @@ describe('Global Group By Renderer Component', () => { defaultValue: [], }); renderWithTheme( - @@ -72,7 +72,7 @@ describe('Global Group By Renderer Component', () => { defaultValue: [], }); renderWithTheme( - @@ -96,7 +96,7 @@ describe('Global Group By Renderer Component', () => { defaultValue: [], }); renderWithTheme( - @@ -118,7 +118,7 @@ describe('Global Group By Renderer Component', () => { defaultValue, }); renderWithTheme( - diff --git a/packages/manager/src/features/CloudPulse/GroupBy/WidgetGroupByRenderer.test.tsx b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.test.tsx similarity index 96% rename from packages/manager/src/features/CloudPulse/GroupBy/WidgetGroupByRenderer.test.tsx rename to packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.test.tsx index 1b3e822e760..a420a800aec 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/WidgetGroupByRenderer.test.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import { WidgetGroupByRenderer } from './WidgetGroupByRenderer'; +import { WidgetFilterGroupByRenderer } from './WidgetFilterGroupByRenderer'; import type { GroupByOption } from './CloudPulseGroupByDrawer'; import type { CloudPulseServiceType } from '@linode/api-v4'; @@ -39,7 +39,7 @@ const widgetGroupBy: GroupByOption[] = [ { value: 'value-3', label: 'Value 3' }, ]; -const component = ; +const component = ; describe('Widget Group By Renderer', () => { beforeAll(() => { From f5d96d2544432126374f060cd88b860fd1750cae Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Thu, 11 Sep 2025 22:40:01 +0530 Subject: [PATCH 32/39] upcoming: [DI-26666] - Updated failing test cases --- .../CloudPulse/GroupBy/GlobalFilterGroupByRenderer.test.tsx | 4 ++-- .../CloudPulse/GroupBy/WidgetFilterGroupByRenderer.test.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.test.tsx b/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.test.tsx index 8dbfdd6f4f8..cac5537d38a 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.test.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.test.tsx @@ -21,8 +21,8 @@ const mockGroupByOptions: GroupByOption[] = [ { value: 'option3', label: 'Option 3' }, ]; -vi.mock('./useGroupByDimension', async () => { - const actual = await vi.importActual('./useGroupByDimension'); +vi.mock('./utils', async () => { + const actual = await vi.importActual('./utils'); return { ...actual, diff --git a/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.test.tsx b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.test.tsx index a420a800aec..1b820c0aa08 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.test.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.test.tsx @@ -13,7 +13,7 @@ const mocks = vi.hoisted(() => ({ useWidgetDimension: vi.fn(), })); -vi.mock('./useGroupByDimension', () => { +vi.mock('./utils', () => { return { ...mocks, }; From 9a394f8df19a1483106eff68461d853922dcfec6 Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Thu, 11 Sep 2025 22:53:37 +0530 Subject: [PATCH 33/39] Added changeset --- .../.changeset/pr-12865-upcoming-features-1757611405994.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-12865-upcoming-features-1757611405994.md diff --git a/packages/manager/.changeset/pr-12865-upcoming-features-1757611405994.md b/packages/manager/.changeset/pr-12865-upcoming-features-1757611405994.md new file mode 100644 index 00000000000..ef51e72e065 --- /dev/null +++ b/packages/manager/.changeset/pr-12865-upcoming-features-1757611405994.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +ACLP: add `GlobalFilterGroupByRendererComponent` and `WidgetFilterGroupByRendererComponent` ([#12865](https://github.com/linode/manager/pull/12865)) From 3752bd1f2d279eda602a2b2968590778f05bec5a Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Mon, 15 Sep 2025 18:53:04 +0530 Subject: [PATCH 34/39] upcoming: [DI-27104] - Updated interface documentation --- .../GroupBy/GlobalFilterGroupByRenderer.tsx | 6 ++++++ .../GroupBy/WidgetFilterGroupByRenderer.tsx | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx index eb1ebf2815c..7058219dcb8 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx @@ -12,7 +12,13 @@ import type { GroupByOption } from './CloudPulseGroupByDrawer'; import type { Dashboard } from '@linode/api-v4'; interface GlobalFilterGroupByRendererProps { + /** + * Callback to handle the selected values + */ handleChange: (selectedValue: string[]) => void; + /** + * Currently selected dashboard + */ selectedDashboard?: Dashboard; } diff --git a/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx index 4eac9f18d98..0a3df5a99e5 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx @@ -12,10 +12,25 @@ import type { GroupByOption } from './CloudPulseGroupByDrawer'; import type { CloudPulseServiceType } from '@linode/api-v4'; interface WidgetFilterGroupByRendererProps { + /** + * Id of the selected dashboard + */ dashboardId: number; + /** + * Callback function to handle the selected values + */ handleChange: (selectedValue: string[]) => void; + /** + * Label for the widget metric + */ label: string; + /** + * Name of the metric + */ metric: string; + /** + * Service type of the selected dashboard + */ serviceType: CloudPulseServiceType; } From 330e239e007ce68479afac823b892819ac89ee34 Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Mon, 15 Sep 2025 18:53:42 +0530 Subject: [PATCH 35/39] upcoming: [DI-27104] - Updated dependency array --- .../features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx index 0a3df5a99e5..20d03f9cd88 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx @@ -61,7 +61,7 @@ export const WidgetFilterGroupByRenderer = ( handleChange(selectedValue.map(({ value }) => value)); setOpen(false); }, - [handleChange, onCancel] + [handleChange] ); const isDisabled = From fc5375167365785c6c0812eabbead876b3accc2c Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Mon, 15 Sep 2025 18:57:30 +0530 Subject: [PATCH 36/39] upcoming: [DI-27104] - Updated spacing function --- .../features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx index 7058219dcb8..193ee4480f6 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/GlobalFilterGroupByRenderer.tsx @@ -64,7 +64,7 @@ export const GlobalFilterGroupByRenderer = ( size="small" sx={(theme) => ({ marginBlockEnd: 'auto', - marginTop: { md: theme.spacing(3.5) }, + marginTop: { md: theme.spacingFunction(28) }, color: isSelected ? theme.color.buttonPrimaryHover : 'inherit', })} > From f22c5a6144dbc58a344f34c87d7d2f398785a34f Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Tue, 16 Sep 2025 16:59:04 +0530 Subject: [PATCH 37/39] upcoming: [DI-27104] - Updated tokens color --- .../GroupBy/GlobalGroupByRenderer.tsx | 91 +++++++++++++++++++ .../GroupBy/WidgetFilterGroupByRenderer.tsx | 4 +- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 packages/manager/src/features/CloudPulse/GroupBy/GlobalGroupByRenderer.tsx diff --git a/packages/manager/src/features/CloudPulse/GroupBy/GlobalGroupByRenderer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/GlobalGroupByRenderer.tsx new file mode 100644 index 00000000000..aeeecd32934 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/GroupBy/GlobalGroupByRenderer.tsx @@ -0,0 +1,91 @@ +import { IconButton } from '@linode/ui'; +import React from 'react'; + +import GroupByIcon from 'src/assets/icons/group-by.svg'; + +import { CloudPulseTooltip } from '../shared/CloudPulseTooltip'; +import { CloudPulseGroupByDrawer } from './CloudPulseGroupByDrawer'; +import { GLOBAL_GROUP_BY_MESSAGE } from './constants'; +import { useGlobalDimensions } from './utils'; + +import type { GroupByOption } from './CloudPulseGroupByDrawer'; +import type { Dashboard } from '@linode/api-v4'; + +interface GlobalGroupByRendererProps { + /** + * Handler function called when the group by selection changes. + * @param selectedValue - Array of selected group by values. + */ + handleChange: (selectedValue: string[]) => void; + /** + * The currently selected dashboard, if any. + */ + selectedDashboard?: Dashboard; +} + +export const GlobalGroupByRenderer = (props: GlobalGroupByRendererProps) => { + const { selectedDashboard, handleChange } = props; + const [isSelected, setIsSelected] = React.useState(false); + + const { options, defaultValue, isLoading } = useGlobalDimensions( + selectedDashboard?.id, + selectedDashboard?.service_type + ); + + const [open, setOpen] = React.useState(false); + + const onApply = React.useCallback( + (selectedValue: GroupByOption[]) => { + if (selectedValue.length === 0) { + setIsSelected(false); + } else { + setIsSelected(true); + } + handleChange(selectedValue.map(({ value }) => value)); + setOpen(false); + }, + [handleChange] + ); + + const onCancel = React.useCallback(() => { + setOpen(false); + }, []); + return ( + <> + + setOpen(true)} + size="small" + sx={(theme) => ({ + marginBlockEnd: 'auto', + marginTop: { md: theme.spacing(3.5) }, + color: isSelected + ? theme.tokens.component.Button.Primary.Hover.Background + : 'inherit', + })} + > + + + + + {!isLoading && selectedDashboard && ( + + )} + + ); +}; diff --git a/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx index 20d03f9cd88..8d892d5bec8 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/WidgetFilterGroupByRenderer.tsx @@ -82,7 +82,9 @@ export const WidgetFilterGroupByRenderer = ( size="small" sx={(theme) => ({ marginBlockEnd: 'auto', - color: isSelected ? theme.color.buttonPrimaryHover : 'inherit', + color: isSelected + ? theme.tokens.component.Button.Primary.Hover.Background + : 'inherit', padding: 0, })} > From 88e27e0c789f23fe330e4352095e98ae9e705cb2 Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Tue, 16 Sep 2025 17:04:21 +0530 Subject: [PATCH 38/39] upcoming: [DI-27104] - Updated stack gap --- .../src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx index a9ac125451d..13a8694df9e 100644 --- a/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx +++ b/packages/manager/src/features/CloudPulse/GroupBy/CloudPulseGroupByDrawer.tsx @@ -138,7 +138,7 @@ export const CloudPulseGroupByDrawer = React.memo( return ( handleClose()} open={open} title={title}> - + ({ marginTop: -1, font: theme.font.normal })} From dc3fbfa2300817ceeda6c38a448376f6a07403ea Mon Sep 17 00:00:00 2001 From: nikhagra-akamai Date: Tue, 16 Sep 2025 17:23:42 +0530 Subject: [PATCH 39/39] Removed redundant file --- .../GroupBy/GlobalGroupByRenderer.tsx | 91 ------------------- 1 file changed, 91 deletions(-) delete mode 100644 packages/manager/src/features/CloudPulse/GroupBy/GlobalGroupByRenderer.tsx diff --git a/packages/manager/src/features/CloudPulse/GroupBy/GlobalGroupByRenderer.tsx b/packages/manager/src/features/CloudPulse/GroupBy/GlobalGroupByRenderer.tsx deleted file mode 100644 index aeeecd32934..00000000000 --- a/packages/manager/src/features/CloudPulse/GroupBy/GlobalGroupByRenderer.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { IconButton } from '@linode/ui'; -import React from 'react'; - -import GroupByIcon from 'src/assets/icons/group-by.svg'; - -import { CloudPulseTooltip } from '../shared/CloudPulseTooltip'; -import { CloudPulseGroupByDrawer } from './CloudPulseGroupByDrawer'; -import { GLOBAL_GROUP_BY_MESSAGE } from './constants'; -import { useGlobalDimensions } from './utils'; - -import type { GroupByOption } from './CloudPulseGroupByDrawer'; -import type { Dashboard } from '@linode/api-v4'; - -interface GlobalGroupByRendererProps { - /** - * Handler function called when the group by selection changes. - * @param selectedValue - Array of selected group by values. - */ - handleChange: (selectedValue: string[]) => void; - /** - * The currently selected dashboard, if any. - */ - selectedDashboard?: Dashboard; -} - -export const GlobalGroupByRenderer = (props: GlobalGroupByRendererProps) => { - const { selectedDashboard, handleChange } = props; - const [isSelected, setIsSelected] = React.useState(false); - - const { options, defaultValue, isLoading } = useGlobalDimensions( - selectedDashboard?.id, - selectedDashboard?.service_type - ); - - const [open, setOpen] = React.useState(false); - - const onApply = React.useCallback( - (selectedValue: GroupByOption[]) => { - if (selectedValue.length === 0) { - setIsSelected(false); - } else { - setIsSelected(true); - } - handleChange(selectedValue.map(({ value }) => value)); - setOpen(false); - }, - [handleChange] - ); - - const onCancel = React.useCallback(() => { - setOpen(false); - }, []); - return ( - <> - - setOpen(true)} - size="small" - sx={(theme) => ({ - marginBlockEnd: 'auto', - marginTop: { md: theme.spacing(3.5) }, - color: isSelected - ? theme.tokens.component.Button.Primary.Hover.Background - : 'inherit', - })} - > - - - - - {!isLoading && selectedDashboard && ( - - )} - - ); -};