Skip to content

Commit

Permalink
upcoming: [DI-20357] - Changes for ACLP Dashboard with Filters compon…
Browse files Browse the repository at this point in the history
…ent (#10845)

* upcoming: [DI-20357] - Changes for single reusable component

* upcoming: [DI-20357] - Added a divider after filter

* upcoming: [DI-20357] - Add changeset

* upcoming: [DI-20357] - Code Refactoring

* upcoming: [DI-20357] - Code splitting between utility and component

* upcoming: [DI-20357] - More clean ups

* upcoming: [DI-20357] - Initial PR review comments

* upcoming: [DI-20357] - Moving condition checks

* upcoming: [DI-20357] - Destructure config changes

* upcoming: [DI-20357] - PR comments

* upcoming: [DI-20357] - PR comments

* upcoming: [DI-20357] - Update checks for isFilterBuilderNeeded

* upcoming: [DI-20357] - More test cases

* upcoming: [DI-20357] - Use title

* upcoming: [DI-20357] - Code simplifications and PR comments

* upcoming: [DI-20357] - Code simplifications and PR comments

* upcoming: [DI-20357] - As per develop

* upcoming: [DI-20357] - As per develop

* upcoming: [DI-20357] - Added for undefined case as well

* upcoming: [DI-20357] - Added for resource 0 case as well

* upcoming: [DI-20357] - Server handler fixes

* upcoming: [DI-20357] - Server handler fixes

* upcoming: [DI-20357] - nodeType to role

* upcoming: [DI-20357] - ES lint fix

---------

Co-authored-by: vmangalr <vmangalr@akamai.com>
  • Loading branch information
venkymano-akamai and vmangalr authored Sep 4, 2024
1 parent c02a7df commit 2630aed
Show file tree
Hide file tree
Showing 7 changed files with 614 additions and 7 deletions.
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,163 @@
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';

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: 'role',
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

0 comments on commit 2630aed

Please sign in to comment.