Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
ccb32ce
upcoming: [DI-22916] - Added group by drawer component
nikhagra-akamai Aug 11, 2025
cc05634
upcoming: [DI-22916] - Added memo to group by drawer component
nikhagra-akamai Aug 20, 2025
ddb8997
upcoming: [DI-22916] - Added hook to fetch dimension for group by dro…
nikhagra-akamai Aug 20, 2025
8cf75b1
upcoming: [DI-22916] - Updated test case
nikhagra-akamai Aug 20, 2025
c530be9
upcoming: [DI-22916] - Remove unused imports
nikhagra-akamai Sep 9, 2025
0bc537a
upcoming: [DI-26666] - Added useWidgetDimension hook to generate widg…
nikhagra-akamai Aug 25, 2025
65922d8
upcoming: [DI-22916] - Updated interface documentation
nikhagra-akamai Sep 9, 2025
844adc6
upcoming: [DI-26666] - Updated logic to no disable select all button
nikhagra-akamai Aug 26, 2025
5463d9c
upcoming: [DI-26666] - Dynamically hiding maximum selection limit mes…
nikhagra-akamai Sep 2, 2025
274d010
upcoming: [DI-26666] - Show default options from dashboard api in glo…
nikhagra-akamai Sep 2, 2025
3a0064d
upcoming: [DI-26666] - Updated default value logic
nikhagra-akamai Sep 2, 2025
9dcfb9f
upcoming: [DI-26666] - moved option rendering to a function
nikhagra-akamai Sep 3, 2025
17bb024
upcoming: [DI-26666] - Added group by in dashboard interface
nikhagra-akamai Sep 9, 2025
21f6e10
Merge branch 'develop' of github.com:linode/manager into feature/grou…
nikhagra-akamai Sep 9, 2025
7dc426f
Added changeset
nikhagra-akamai Sep 9, 2025
d54bd2e
upcoming: [DI-26666] - Added method documentation
nikhagra-akamai Sep 10, 2025
27f0d5d
Merge branch 'develop' of github.com:linode/manager into feature/grou…
nikhagra-akamai Sep 10, 2025
88152b4
upcoming: [DI-26666] - Updated typos
nikhagra-akamai Sep 11, 2025
b1fc0a1
upcoming: [DI-26666] - Renamed file
nikhagra-akamai Sep 11, 2025
4a16b7a
upcoming: [DI-26666] - Added test cases
nikhagra-akamai Sep 11, 2025
536766b
Merge branch 'develop' of github.com:linode/manager into feature/grou…
nikhagra-akamai Sep 11, 2025
fef65f3
upcoming: [DI-26666] - Updated subtitle and message component
nikhagra-akamai Sep 11, 2025
9a780c8
Merge branch 'develop' of github.com:linode/manager into feature/grou…
nikhagra-akamai Sep 11, 2025
18206fe
upcoming: [DI-26666] - Removed typography styling
nikhagra-akamai Sep 11, 2025
3c031d6
reverted hardcoded value
nikhagra-akamai Sep 11, 2025
8e701b8
Merge branch 'develop' into feature/group-by
nikhagra-akamai Sep 11, 2025
099393d
upcoming: [DI-22916] - Added group by svg file
nikhagra-akamai Aug 20, 2025
07e1dfd
upcoming: [DI-22916] - Added Global Filter group by renderer
nikhagra-akamai Aug 20, 2025
372651b
upcoming: [DI-26666] - Added widget filter group by renderer
nikhagra-akamai Aug 25, 2025
3942ca6
upcoming: [DI-26666] - Added widget group by renderer test cases
nikhagra-akamai Aug 26, 2025
0bc0e4b
upcoming: [DI-26666] - Added test cases for Global Group By Renderer
nikhagra-akamai Aug 26, 2025
f74409d
upcoming: [DI-26666] - Updated import statement
nikhagra-akamai Sep 11, 2025
81003f5
Merge branch 'feature/group-by' of github.com:nikhagra-akamai/manager…
nikhagra-akamai Sep 11, 2025
6870759
Merge conflict resolved
nikhagra-akamai Sep 11, 2025
68e2e45
upcoming: [DI-26666] - Code refactor
nikhagra-akamai Sep 3, 2025
b75c114
Hide dimension select placeholder when atleast 1 option is selected
nikhagra-akamai Sep 8, 2025
2550baf
upcoming: [DI-26666] - Added qa id
nikhagra-akamai Sep 8, 2025
d3b01b8
upcoming: [DI-26666] - Updated file name
nikhagra-akamai Sep 11, 2025
f5d96d2
upcoming: [DI-26666] - Updated failing test cases
nikhagra-akamai Sep 11, 2025
9a394f8
Added changeset
nikhagra-akamai Sep 11, 2025
b91fe19
Merge branch 'develop' into feature/group-by
nikhagra-akamai Sep 12, 2025
d4bb76d
Merge branch 'develop' of github.com:linode/manager into feature/grou…
nikhagra-akamai Sep 15, 2025
09632f6
Merge branch 'feature/group-by' of github.com:nikhagra-akamai/manager…
nikhagra-akamai Sep 15, 2025
3752bd1
upcoming: [DI-27104] - Updated interface documentation
nikhagra-akamai Sep 15, 2025
330e239
upcoming: [DI-27104] - Updated dependency array
nikhagra-akamai Sep 15, 2025
fc53751
upcoming: [DI-27104] - Updated spacing function
nikhagra-akamai Sep 15, 2025
bb547d8
Merge branch 'develop' of github.com:linode/manager into feature/grou…
nikhagra-akamai Sep 15, 2025
f22c5a6
upcoming: [DI-27104] - Updated tokens color
nikhagra-akamai Sep 16, 2025
88e27e0
upcoming: [DI-27104] - Updated stack gap
nikhagra-akamai Sep 16, 2025
64c64cb
Merge branch 'develop' of github.com:linode/manager into feature/grou…
nikhagra-akamai Sep 16, 2025
dc3fbfa
Removed redundant file
nikhagra-akamai Sep 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

ACLP: add `GlobalFilterGroupByRendererComponent` and `WidgetFilterGroupByRendererComponent` ([#12865](https://github.com/linode/manager/pull/12865))
6 changes: 6 additions & 0 deletions packages/manager/src/assets/icons/group-by.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -57,8 +59,6 @@ export interface GroupByOption {
label: string;
value: string;
}

const groupBySelectionLimit = 3;
export const CloudPulseGroupByDrawer = React.memo(
(props: GroupByDrawerProps) => {
const {
Expand All @@ -76,13 +76,13 @@ export const CloudPulseGroupByDrawer = React.memo(
const [selectedValue, setSelectedValue] = React.useState<GroupByOption[]>(
defaultValue.slice(
0,
Math.min(defaultValue.length, groupBySelectionLimit)
Math.min(defaultValue.length ?? 0, GROUP_BY_SELECTION_LIMIT)
)
);
const previousValueRef = React.useRef<GroupByOption[]>(
defaultValue.slice(
0,
Math.min(defaultValue.length, groupBySelectionLimit)
Math.min(defaultValue.length, GROUP_BY_SELECTION_LIMIT)
)
);

Expand All @@ -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);
Expand All @@ -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 <NullComponent />;
Expand All @@ -138,7 +138,7 @@ export const CloudPulseGroupByDrawer = React.memo(

return (
<Drawer onClose={(_) => handleClose()} open={open} title={title}>
<Stack gap={4}>
<Stack gap={3}>
<Typography
component="p"
sx={(theme) => ({ marginTop: -1, font: theme.font.normal })}
Expand All @@ -151,19 +151,19 @@ 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) =>
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}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 { GlobalFilterGroupByRenderer } from './GlobalFilterGroupByRenderer';

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('./utils', async () => {
const actual = await vi.importActual('./utils');

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(
<GlobalFilterGroupByRenderer
handleChange={handleChange}
selectedDashboard={undefined}
/>
);

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(
<GlobalFilterGroupByRenderer
handleChange={handleChange}
selectedDashboard={dashboard}
/>
);

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(
<GlobalFilterGroupByRenderer
handleChange={handleChange}
selectedDashboard={dashboard}
/>
);

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(
<GlobalFilterGroupByRenderer
handleChange={handleChange}
selectedDashboard={dashboard}
/>
);

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(
<GlobalFilterGroupByRenderer
handleChange={handleChange}
selectedDashboard={dashboard}
/>
);

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();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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 {
/**
* Callback to handle the selected values
*/
handleChange: (selectedValue: string[]) => void;
/**
* Currently selected dashboard
*/
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));
setOpen(false);
},
[handleChange]
);

const onCancel = React.useCallback(() => {
setOpen(false);
}, []);
return (
<>
<CloudPulseTooltip placement="bottom-end" title="Group By">
<IconButton
aria-label="Group By Dashboard Metrics"
color="inherit"
data-qa-selected={isSelected}
data-testid="group-by"
disabled={!selectedDashboard || isLoading}
onClick={() => setOpen(true)}
size="small"
sx={(theme) => ({
marginBlockEnd: 'auto',
marginTop: { md: theme.spacingFunction(28) },
color: isSelected ? theme.color.buttonPrimaryHover : 'inherit',
Comment thread
nikhagra-akamai marked this conversation as resolved.
})}
>
<GroupByIcon height="24px" width="24px" />
</IconButton>
</CloudPulseTooltip>

{!isLoading && selectedDashboard && (
<CloudPulseGroupByDrawer
defaultValue={defaultValue}
message={GLOBAL_GROUP_BY_MESSAGE}
onApply={onApply}
onCancel={onCancel}
open={open}
options={options}
serviceType={selectedDashboard.service_type}
subtitle={selectedDashboard.label}
title="Global Group By"
/>
)}
</>
);
};
Loading