Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

upcoming: [DI-20929] - Added applied filters view in CloudPulse #11354

Merged
merged 16 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

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

Add `CloudPulseAppliedFilter` and `CloudPulseAppliedFilterRenderer` components, update filter change handler function to add another parameter `label` ([#11354](https://github.com/linode/manager/pull/11354))
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { Grid, Paper } from '@mui/material';
import { Box, Grid, Paper } from '@mui/material';
import * as React from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

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

could we import Box and Paper from our linode/ui package?


import { GlobalFilters } from '../Overview/GlobalFilters';
import { CloudPulseAppliedFilterRenderer } from '../shared/CloudPulseAppliedFilterRenderer';
import { CloudPulseDashboardRenderer } from './CloudPulseDashboardRenderer';

import type { Dashboard, TimeDuration } from '@linode/api-v4';

export type FilterValueType = number | number[] | string | string[] | undefined;

export interface FilterValue {
id: { [key: string]: FilterValueType };
label: { [key: string]: string[] };
}
export interface DashboardProp {
Copy link
Contributor

Choose a reason for hiding this comment

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

Wondering if this interface should be named differently, like ExtendedFilterValue, or if the field in DashboardProp below should be named something else - it threw me off a bit that filterValue in DashboardProp does not have a type of FilterValue πŸ˜†

Copy link
Contributor Author

@nikhagra-akamai nikhagra-akamai Dec 6, 2024

Choose a reason for hiding this comment

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

updated thanks for pointing out

dashboard?: Dashboard;
filterValue: {
Expand All @@ -17,26 +22,47 @@ export interface DashboardProp {
}

export const CloudPulseDashboardLanding = () => {
const [filterValue, setFilterValue] = React.useState<{
[key: string]: FilterValueType;
}>({});
const [filterValue, setFilterValue] = React.useState<FilterValue>({
id: {},
label: {},
});

const [timeDuration, setTimeDuration] = React.useState<TimeDuration>();

const [dashboard, setDashboard] = React.useState<Dashboard>();

const [showAppliedFilters, setShowAppliedFilters] = React.useState<boolean>(
false
);

const toggleAppliedFilter = (isVisible: boolean) => {
setShowAppliedFilters(isVisible);
};

const onFilterChange = React.useCallback(
(filterKey: string, filterValue: FilterValueType) => {
setFilterValue((prev: { [key: string]: FilterValueType }) => ({
...prev,
[filterKey]: filterValue,
}));
(filterKey: string, filterValue: FilterValueType, labels: string[]) => {
setFilterValue((prev: FilterValue) => {
return {
id: {
...prev.id,
[filterKey]: filterValue,
},
label: {
...prev.label,
[filterKey]: labels,
},
};
});
},
[]
);

const onDashboardChange = React.useCallback((dashboardObj: Dashboard) => {
setDashboard(dashboardObj);
setFilterValue({}); // clear the filter values on dashboard change
setFilterValue({
id: {},
label: {},
}); // clear the filter values on dashboard change
}, []);
const onTimeDurationChange = React.useCallback(
(timeDurationObj: TimeDuration) => {
Expand All @@ -48,16 +74,25 @@ export const CloudPulseDashboardLanding = () => {
<Grid container spacing={2}>
<Grid item xs={12}>
<Paper>
<GlobalFilters
handleAnyFilterChange={onFilterChange}
handleDashboardChange={onDashboardChange}
handleTimeDurationChange={onTimeDurationChange}
/>
<Box display={'flex'} flexDirection={'column'}>
<GlobalFilters
Copy link
Contributor

Choose a reason for hiding this comment

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

heads up, since Paper is now from linode/ui, it has built in padding - you need to add sx={{ padding: 0 }} to the Paper component to get the same padding as develop

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks for informing

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
<Box display={'flex'} flexDirection={'column'}>
<Box display="flex" flexDirection="column">

nit: we don't need object notation here

handleAnyFilterChange={onFilterChange}
handleDashboardChange={onDashboardChange}
handleTimeDurationChange={onTimeDurationChange}
handleToggleAppliedFilter={toggleAppliedFilter}
/>
{dashboard?.service_type && showAppliedFilters && (
<CloudPulseAppliedFilterRenderer
filters={filterValue.label}
serviceType={dashboard.service_type}
/>
)}
</Box>
</Paper>
</Grid>
<CloudPulseDashboardRenderer
dashboard={dashboard}
filterValue={filterValue}
filterValue={filterValue.id}
timeDuration={timeDuration}
/>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React from 'react';
import { ErrorState } from 'src/components/ErrorState/ErrorState';
import { useCloudPulseDashboardByIdQuery } from 'src/queries/cloudpulse/dashboards';

import { CloudPulseAppliedFilterRenderer } from '../shared/CloudPulseAppliedFilterRenderer';
import { CloudPulseDashboardFilterBuilder } from '../shared/CloudPulseDashboardFilterBuilder';
import { CloudPulseErrorPlaceholder } from '../shared/CloudPulseErrorPlaceholder';
import { CloudPulseTimeRangeSelect } from '../shared/CloudPulseTimeRangeSelect';
Expand All @@ -16,7 +17,10 @@ import {
} from '../Utils/ReusableDashboardFilterUtils';
import { CloudPulseDashboard } from './CloudPulseDashboard';

import type { FilterValueType } from './CloudPulseDashboardLanding';
import type {
FilterValue,
FilterValueType,
} from './CloudPulseDashboardLanding';
import type { TimeDuration } from '@linode/api-v4';

export interface CloudPulseDashboardWithFiltersProp {
Expand All @@ -37,18 +41,38 @@ export const CloudPulseDashboardWithFilters = React.memo(
dashboardId
);

const [filterValue, setFilterValue] = React.useState<{
[key: string]: FilterValueType;
}>({});
const [filterValue, setFilterValue] = React.useState<FilterValue>({
id: {},
label: {},
});

const [timeDuration, setTimeDuration] = React.useState<TimeDuration>({
unit: 'min',
value: 30,
});

const [showAppliedFilters, setShowAppliedFilters] = React.useState<boolean>(
false
);

const toggleAppliedFilter = (isVisible: boolean) => {
setShowAppliedFilters(isVisible);
};

const onFilterChange = React.useCallback(
(filterKey: string, value: FilterValueType) => {
setFilterValue((prev) => ({ ...prev, [filterKey]: value }));
(filterKey: string, value: FilterValueType, labels: string[]) => {
setFilterValue((prev) => {
return {
id: {
...prev.id,
[filterKey]: value,
},
label: {
...prev.label,
[filterKey]: labels,
},
};
});
},
[]
);
Expand Down Expand Up @@ -91,7 +115,7 @@ export const CloudPulseDashboardWithFilters = React.memo(
const isFilterBuilderNeeded = checkIfFilterBuilderNeeded(dashboard);
const isMandatoryFiltersSelected = checkMandatoryFiltersSelected({
dashboardObj: dashboard,
filterValue,
filterValue: filterValue.id,
resource,
timeDuration,
});
Expand Down Expand Up @@ -132,15 +156,24 @@ export const CloudPulseDashboardWithFilters = React.memo(
<CloudPulseDashboardFilterBuilder
dashboard={dashboard}
emitFilterChange={onFilterChange}
handleToggleAppliedFilter={toggleAppliedFilter}
isServiceAnalyticsIntegration={true}
/>
)}
<Grid item mb={3} mt={-3} xs={12}>
{showAppliedFilters && (
<CloudPulseAppliedFilterRenderer
filters={filterValue.label}
serviceType={dashboard.service_type}
/>
)}
</Grid>
</Paper>
{isMandatoryFiltersSelected ? (
<CloudPulseDashboard
{...getDashboardProperties({
dashboardObj: dashboard,
filterValue,
filterValue: filterValue.id,
resource,
timeDuration,
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import { GlobalFilters } from './GlobalFilters';
const mockHandleAnyFilterChange = vi.fn();
const mockHandleDashboardChange = vi.fn();
const mockHandleTimeDurationChange = vi.fn();
const mockHandleToggleAppliedFilter = vi.fn();
const timeRangeSelectId = 'cloudpulse-time-duration';
const setup = () => {
return renderWithTheme(
<GlobalFilters
handleAnyFilterChange={mockHandleAnyFilterChange}
handleDashboardChange={mockHandleDashboardChange}
handleTimeDurationChange={mockHandleTimeDurationChange}
handleToggleAppliedFilter={mockHandleToggleAppliedFilter}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,22 @@ import type { FilterValueType } from '../Dashboard/CloudPulseDashboardLanding';
import type { AclpConfig, Dashboard, TimeDuration } from '@linode/api-v4';

export interface GlobalFilterProperties {
handleAnyFilterChange(filterKey: string, filterValue: FilterValueType): void;
handleAnyFilterChange(
filterKey: string,
filterValue: FilterValueType,
labels: string[]
): void;
handleDashboardChange(dashboard: Dashboard | undefined): void;
handleTimeDurationChange(timeDuration: TimeDuration): void;
handleToggleAppliedFilter(isVisible: boolean): void;
}

export const GlobalFilters = React.memo((props: GlobalFilterProperties) => {
const {
handleAnyFilterChange,
handleDashboardChange,
handleTimeDurationChange,
handleToggleAppliedFilter,
} = props;

const {
Expand Down Expand Up @@ -68,19 +74,20 @@ export const GlobalFilters = React.memo((props: GlobalFilterProperties) => {
(
filterKey: string,
value: FilterValueType,
labels: string[],
savePref: boolean = false,
updatedPreferenceData: AclpConfig = {}
) => {
if (savePref) {
updatePreferences(updatedPreferenceData);
}
handleAnyFilterChange(filterKey, value);
handleAnyFilterChange(filterKey, value, labels);
},
[]
);

const handleGlobalRefresh = React.useCallback(() => {
handleAnyFilterChange(REFRESH, Date.now());
handleAnyFilterChange(REFRESH, Date.now(), []);
}, []);

const theme = useTheme();
Copy link
Contributor

Choose a reason for hiding this comment

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

optional: I wonder if we could avoid importing this useTheme and instead access/use the theme directly in the styles, like this sx={(theme)=> ({})}

Expand Down Expand Up @@ -143,6 +150,7 @@ export const GlobalFilters = React.memo((props: GlobalFilterProperties) => {
<CloudPulseDashboardFilterBuilder
dashboard={selectedDashboard}
emitFilterChange={emitFilterChange}
handleToggleAppliedFilter={handleToggleAppliedFilter}
isServiceAnalyticsIntegration={false}
preferences={preferences}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ interface CloudPulseMandatoryFilterCheckProps {
*/
export const getRegionProperties = (
props: CloudPulseFilterProperties,
handleRegionChange: (region: string | undefined, savePref?: boolean) => void
handleRegionChange: (
region: string | undefined,
labels: [],
savePref?: boolean
) => void
): CloudPulseRegionSelectProps => {
const { name: label, placeholder } = props.config.configuration;
const { dashboard, isServiceAnalyticsIntegration, preferences } = props;
Expand Down Expand Up @@ -119,6 +123,7 @@ export const getCustomSelectProperties = (
handleCustomSelectChange: (
filterKey: string,
value: FilterValueType,
labels: string[],
savePref?: boolean,
updatedPreferenceData?: {}
) => void
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';

import { renderWithTheme } from 'src/utilities/testHelpers';

import { CloudPulseAppliedFilter } from './CloudPulseAppliedFilter';
import { CloudPulseAppliedFilterRenderer } from './CloudPulseAppliedFilterRenderer';

const data = {
region: ['us-east'],
resource: ['res1', 'res2'],
};

const testId = 'applied-filter';

describe('CloudPulse Applied Filter', () => {
it('should render applied filter component', () => {
const { getByTestId } = renderWithTheme(
<CloudPulseAppliedFilter filters={data} />
);
expect(getByTestId(testId)).toBeInTheDocument();
});

it('should render the applied filter key & values', () => {
const { getByTestId } = renderWithTheme(
<CloudPulseAppliedFilter filters={data} />
);
expect(getByTestId(testId)).toHaveTextContent('region');
expect(getByTestId(testId)).toHaveTextContent('res1');
expect(getByTestId(testId)).not.toHaveTextContent('resources');
});

it('should not render the applied filter component', () => {
const { queryByTestId } = renderWithTheme(
<CloudPulseAppliedFilterRenderer filters={{}} serviceType="abc" />
);

expect(queryByTestId(testId)).toBe(null);
});
});
Loading
Loading