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-20357] - Changes for ACLP Dashboard with Filters component #10845

Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
76cc88e
upcoming: [DI-20357] - Changes for single reusable component
venkat199501 Aug 28, 2024
5ecc869
upcoming: [DI-20357] - Added a divider after filter
venkat199501 Aug 28, 2024
d4e96a0
upcoming: [DI-20357] - Add changeset
venkat199501 Aug 28, 2024
2f8ab02
upcoming: [DI-20357] - Code Refactoring
venkat199501 Aug 28, 2024
d1394da
upcoming: [DI-20357] - Code splitting between utility and component
venkat199501 Aug 28, 2024
1b76d81
upcoming: [DI-20357] - More clean ups
venkat199501 Aug 28, 2024
a51eb65
upcoming: [DI-20357] - Initial PR review comments
venkat199501 Aug 28, 2024
5cafe55
upcoming: [DI-20357] - Moving condition checks
venkat199501 Aug 28, 2024
99de3a4
upcoming: [DI-20357] - Destructure config changes
venkat199501 Aug 28, 2024
b34c0b3
upcoming: [DI-20357] - PR comments
venkat199501 Aug 30, 2024
698b53c
upcoming: [DI-20357] - PR comments
venkat199501 Aug 30, 2024
9d22900
upcoming: [DI-20357] - Update checks for isFilterBuilderNeeded
venkat199501 Aug 30, 2024
c27a0e7
Merge branch 'develop' of https://github.com/linode/manager into feat…
venkat199501 Aug 30, 2024
27ac4c6
upcoming: [DI-20357] - More test cases
venkat199501 Aug 30, 2024
226eca2
upcoming: [DI-20357] - Use title
venkat199501 Sep 3, 2024
92004ac
Merge branch 'develop' of https://github.com/linode/manager into feat…
venkat199501 Sep 3, 2024
85b2283
upcoming: [DI-20357] - Code simplifications and PR comments
venkat199501 Sep 3, 2024
e6a6c97
upcoming: [DI-20357] - Code simplifications and PR comments
venkat199501 Sep 4, 2024
85a09eb
Merge branch 'develop' of https://github.com/linode/manager into feat…
venkat199501 Sep 4, 2024
9679979
upcoming: [DI-20357] - As per develop
venkat199501 Sep 4, 2024
339f3df
upcoming: [DI-20357] - As per develop
venkat199501 Sep 4, 2024
618a7c9
upcoming: [DI-20357] - Added for undefined case as well
venkat199501 Sep 4, 2024
3d6dbc2
upcoming: [DI-20357] - Added for resource 0 case as well
venkat199501 Sep 4, 2024
9f2b0f0
upcoming: [DI-20357] - Server handler fixes
venkat199501 Sep 4, 2024
8a2cbcf
upcoming: [DI-20357] - Server handler fixes
venkat199501 Sep 4, 2024
11a7434
upcoming: [DI-20357] - nodeType to role
venkat199501 Sep 4, 2024
a311411
upcoming: [DI-20357] - ES lint fix
venkat199501 Sep 4, 2024
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 new CloudPulseDashboardWithFilters component that will be used as a reusable component in service provider pages ([#10845](https://github.com/linode/manager/pull/10845))
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { fireEvent } from '@testing-library/react';
import React from 'react';

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

import { CloudPulseDashboardWithFilters } from './CloudPulseDashboardWithFilters';

const queryMocks = vi.hoisted(() => ({
useCloudPulseDashboardByIdQuery: vi.fn().mockReturnValue({}),
}));

const selectTimeDurationPlaceholder = 'Select Time Duration';
const circleProgress = 'circle-progress';
const mandatoryFiltersError = 'Mandatory Filters not Selected';
const customNodeTypePlaceholder = 'Select Node Type';

vi.mock('src/queries/cloudpulse/dashboards', async () => {
const actual = await vi.importActual('src/queries/cloudpulse/dashboards');
return {
...actual,
useCloudPulseDashboardByIdQuery: queryMocks.useCloudPulseDashboardByIdQuery,
};
});
const mockDashboard = dashboardFactory.build();

queryMocks.useCloudPulseDashboardByIdQuery.mockReturnValue({
data: {
data: mockDashboard,
},
error: false,
isLoading: false,
});

describe('CloudPulseDashboardWithFilters component tests', () => {
it('renders a CloudPulseDashboardWithFilters component with error placeholder', () => {
queryMocks.useCloudPulseDashboardByIdQuery.mockReturnValue({
data: {
data: mockDashboard,
},
error: false,
isError: true,
isLoading: false,
});

const screen = renderWithTheme(
<CloudPulseDashboardWithFilters dashboardId={1} resource={1} />
);

expect(
screen.getByText('Error while loading Dashboard with Id - 1')
).toBeDefined();
});

it('renders a CloudPulseDashboardWithFilters component successfully without error placeholders', () => {
queryMocks.useCloudPulseDashboardByIdQuery.mockReturnValue({
data: mockDashboard,
error: false,
isError: false,
isLoading: false,
});

const screen = renderWithTheme(
<CloudPulseDashboardWithFilters dashboardId={1} resource={1} />
);

expect(screen.getByText(selectTimeDurationPlaceholder)).toBeDefined();
expect(screen.getByTestId(circleProgress)).toBeDefined(); // the dashboards started to render
});

it('renders a CloudPulseDashboardWithFilters component successfully for dbaas', () => {
queryMocks.useCloudPulseDashboardByIdQuery.mockReturnValue({
data: { ...mockDashboard, service_type: 'dbaas' },
error: false,
isError: false,
isLoading: false,
});

const screen = renderWithTheme(
<CloudPulseDashboardWithFilters dashboardId={1} resource={1} />
);

expect(screen.getByText(selectTimeDurationPlaceholder)).toBeDefined();
expect(screen.getByTestId(circleProgress)).toBeDefined(); // the dashboards started to render
});

it('renders a CloudPulseDashboardWithFilters component with mandatory filter error for dbaas', () => {
queryMocks.useCloudPulseDashboardByIdQuery.mockReturnValue({
data: { ...mockDashboard, service_type: 'dbaas' },
error: false,
isError: false,
isLoading: false,
});

const screen = renderWithTheme(
<CloudPulseDashboardWithFilters dashboardId={1} resource={1} />
);

expect(screen.getByTestId('CloseIcon')).toBeDefined();

const inputBox = screen.getByPlaceholderText(customNodeTypePlaceholder);
fireEvent.change(inputBox, { target: { value: '' } }); // clear the value
expect(screen.getByText(mandatoryFiltersError)).toBeDefined();
});

it('renders a CloudPulseDashboardWithFilters component with no filters configured error', () => {
queryMocks.useCloudPulseDashboardByIdQuery.mockReturnValue({
data: { ...mockDashboard, service_type: 'xyz' },
error: false,
isError: false,
isLoading: false,
});

const screen = renderWithTheme(
<CloudPulseDashboardWithFilters dashboardId={1} resource={1} />
);

expect(
screen.getByText('No Filters Configured for Service Type - xyz')
).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { Divider, Grid, styled } from '@mui/material';
import React from 'react';

import CloudPulseIcon from 'src/assets/icons/entityIcons/monitor.svg';
import { CircleProgress } from 'src/components/CircleProgress';
import { ErrorState } from 'src/components/ErrorState/ErrorState';
import { Paper } from 'src/components/Paper';
import { Placeholder } from 'src/components/Placeholder/Placeholder';
import { useCloudPulseDashboardByIdQuery } from 'src/queries/cloudpulse/dashboards';

import { CloudPulseDashboardFilterBuilder } from '../shared/CloudPulseDashboardFilterBuilder';
import { CloudPulseTimeRangeSelect } from '../shared/CloudPulseTimeRangeSelect';
import { FILTER_CONFIG } from '../Utils/FilterConfig';
import {
checkIfFilterBuilderNeeded,
checkMandatoryFiltersSelected,
getDashboardProperties,
} from '../Utils/ReusableDashboardFilterUtils';
import { CloudPulseDashboard } from './CloudPulseDashboard';

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

/**
* These properties are required for rendering the component
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick - this comment could be removed, since the props are already required props/not marked as optional

export interface CloudPulseDashboardWithFiltersProp {
/**
* The id of the dashboard that needs to be rendered
*/
dashboardId: number;
/**
* The resource id for which the metrics will be listed
*/
resource: number;
}

export const CloudPulseDashboardWithFilters = React.memo(
(props: CloudPulseDashboardWithFiltersProp) => {
const { dashboardId, resource } = props;
const { data: dashboard, isError } = useCloudPulseDashboardByIdQuery(
dashboardId
);

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

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

const onFilterChange = React.useCallback(
(filterKey: string, value: FilterValueType) => {
setFilterValue((prev) => ({ ...prev, [filterKey]: value }));
},
[]
);

const handleTimeRangeChange = React.useCallback(
(timeDuration: TimeDuration) => {
setTimeDuration(timeDuration);
},
[]
);

const renderPlaceHolder = (title: string) => {
return (
<Paper>
<StyledPlaceholder icon={CloudPulseIcon} isEntity title={title} />
</Paper>
);
};

if (isError) {
return (
<ErrorState
errorText={`Error while loading Dashboard with Id - ${dashboardId}`}
/>
);
}

if (!dashboard) {
return <CircleProgress />;
}

if (!FILTER_CONFIG.get(dashboard.service_type)) {
return (
<ErrorState
errorText={`No Filters Configured for Service Type - ${dashboard.service_type}`}
/>
);
}

const isFilterBuilderNeeded = checkIfFilterBuilderNeeded(dashboard);
const isMandatoryFiltersSelected = checkMandatoryFiltersSelected({
dashboardObj: dashboard,
filterValue,
resource,
timeDuration,
});

return (
<>
<Paper>
<Grid
justifyContent={{
sm: 'flex-end',
xs: 'center',
}}
columnSpacing={2}
container
display={'flex'}
item
maxHeight={'120px'}
mb={1}
overflow={'auto'}
px={2}
py={1}
rowGap={2}
xs={12}
>
<Grid item md={4} sm={6} xs={12}>
<CloudPulseTimeRangeSelect
disabled={!dashboard}
handleStatsChange={handleTimeRangeChange}
savePreferences={true}
/>
</Grid>
</Grid>
<Divider />
{isFilterBuilderNeeded && (
<>
<CloudPulseDashboardFilterBuilder
dashboard={dashboard}
emitFilterChange={onFilterChange}
isServiceAnalyticsIntegration={true}
/>
<Divider />
</>
)}
</Paper>
{isMandatoryFiltersSelected ? (
<CloudPulseDashboard
{...getDashboardProperties({
dashboardObj: dashboard,
filterValue,
resource,
timeDuration,
})}
/>
) : (
renderPlaceHolder('Mandatory Filters not Selected')
)}
</>
);
}
);

// keeping it here to avoid recreating
const StyledPlaceholder = styled(Placeholder, {
label: 'StyledPlaceholder',
})({
flex: 'auto',
});
29 changes: 27 additions & 2 deletions packages/manager/src/features/CloudPulse/Utils/FilterConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const LINODE_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
isMetricsFilter: true,
isMultiSelect: false,
name: TIME_DURATION,
neededInServicePage: true,
neededInServicePage: false,
placeholder: 'Select Duration',
priority: 3,
},
Expand Down Expand Up @@ -113,12 +113,37 @@ export const DBAAS_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
isMetricsFilter: true,
isMultiSelect: false,
name: TIME_DURATION,
neededInServicePage: true,
neededInServicePage: false, // we will have a static time duration component, no need render from filter builder
placeholder: 'Select Duration',
priority: 4,
},
name: TIME_DURATION,
},
{
configuration: {
filterKey: 'nodeType',
filterType: 'string',
isFilterable: true, // isFilterable -- this determines whether you need to pass it metrics api
isMetricsFilter: false, // if it is false, it will go as a part of filter params, else global filter
isMultiSelect: false,
name: 'Node Type',
neededInServicePage: true,
options: [
{
id: 'primary',
label: 'Primary',
},
{
id: 'secondary',
label: 'Secondary',
},
],
placeholder: 'Select Node Type',
priority: 5,
type: CloudPulseSelectTypes.static,
},
name: 'Node Type',
},
],
serviceType: 'dbaas',
};
Expand Down
Loading