Skip to content

Commit 53d2633

Browse files
upcoming: [DI-22596] - Changes for adding overview section in the alert details page in Cloud Pulse. (#11466)
* upcoming: [DI-22596] - Overview changes * upcoming: [DI-22131] - CSS and factory updates for alert detail overview * upcoming: [DI-22131] - Code clean up and refactoring * upcoming: [DI-22131] - Type updates and es lint fixes * upcoming: [DI-22131] - Add more UT's * upcoming: [DI-22131] - Add changeset * upcoming: [DI-22131] - Code reusability * upcoming: [DI-22131] - Code refactoring, comments update * upcoming: [DI-22131] - Code refactoring, todo comments update * upcoming: [DI-22131] - Code refactoring * upcoming: [DI-22131] - Code refactoring * upcoming: [DI-22131] - Code refactoring * upcoming: [DI-22131] - Edit Changeset * upcoming: [DI-22131] - Typography font updates * upcoming: [DI-22131] - Util function test optimisation * upcoming: [DI-22131] - Code clean up and refactoring * upcoming: [DI-22131] - Code clean up and refactoring * upcoming: [DI-22131] - Code clean up and refactoring * upcoming: [DI-22131] - Use imports from linode UI * upcoming: [DI-22131] - Update UT's * upcoming: [DI-22131] - Comment updates * upcoming: [DI-22131] - Comment updates * upcoming: [DI-22131] - Rename service type to service type list * upcoming: [DI-22131] - Use shared mock data * upcoming: [DI-22131] - Changed text and background color --------- Co-authored-by: vmangalr <[email protected]>
1 parent 18cfc7e commit 53d2633

File tree

11 files changed

+364
-6
lines changed

11 files changed

+364
-6
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Upcoming Features
3+
---
4+
5+
Add Alert Details Overview section in Cloud Pulse Alert Details page ([#11466](https://github.com/linode/manager/pull/11466))
Lines changed: 10 additions & 0 deletions
Loading

packages/manager/src/factories/cloudpulse/alerts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const alertFactory = Factory.Sync.makeFactory<Alert>({
66
channels: [],
77
created: new Date().toISOString(),
88
created_by: 'user1',
9-
description: '',
9+
description: 'Test description',
1010
entity_ids: ['0', '1', '2', '3'],
1111
has_more_resources: true,
1212
id: Factory.each((i) => i),

packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.test.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22

3-
import { alertFactory } from 'src/factories/';
3+
import { alertFactory, serviceTypesFactory } from 'src/factories/';
44
import { renderWithTheme } from 'src/utilities/testHelpers';
55

66
import { AlertDetail } from './AlertDetail';
@@ -11,20 +11,32 @@ const alertDetails = alertFactory.build({ service_type: 'linode' });
1111
// Mock Queries
1212
const queryMocks = vi.hoisted(() => ({
1313
useAlertDefinitionQuery: vi.fn(),
14+
useCloudPulseServiceTypes: vi.fn(),
1415
}));
1516

1617
vi.mock('src/queries/cloudpulse/alerts', () => ({
1718
...vi.importActual('src/queries/cloudpulse/alerts'),
1819
useAlertDefinitionQuery: queryMocks.useAlertDefinitionQuery,
1920
}));
2021

22+
vi.mock('src/queries/cloudpulse/services', () => {
23+
return {
24+
...vi.importActual('src/queries/cloudpulse/services'),
25+
useCloudPulseServiceTypes: queryMocks.useCloudPulseServiceTypes,
26+
};
27+
});
28+
2129
// Shared Setup
2230
beforeEach(() => {
2331
queryMocks.useAlertDefinitionQuery.mockReturnValue({
2432
data: alertDetails,
2533
isError: false,
2634
isFetching: false,
2735
});
36+
queryMocks.useCloudPulseServiceTypes.mockReturnValue({
37+
data: { data: serviceTypesFactory.buildList(1) },
38+
isFetching: false,
39+
});
2840
});
2941

3042
describe('AlertDetail component tests', () => {
@@ -64,6 +76,14 @@ describe('AlertDetail component tests', () => {
6476
// validate breadcrumbs on loading state
6577
validateBreadcrumbs(getByTestId('link-text'));
6678
});
79+
80+
it('should render the component successfully with alert details overview', () => {
81+
const { getByText } = renderWithTheme(<AlertDetail />);
82+
// validate overview is present with its couple of properties (values will be validated in its own components test)
83+
expect(getByText('Overview')).toBeInTheDocument();
84+
expect(getByText('Name:')).toBeInTheDocument();
85+
expect(getByText('Description:')).toBeInTheDocument();
86+
});
6787
});
6888

6989
const validateBreadcrumbs = (link: HTMLElement) => {

packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.tsx

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { Box, CircleProgress } from '@linode/ui';
2-
import { useTheme } from '@mui/material';
2+
import { styled, useTheme } from '@mui/material';
33
import React from 'react';
44
import { useParams } from 'react-router-dom';
55

6+
import AlertsIcon from 'src/assets/icons/entityIcons/alerts.svg';
67
import { Breadcrumb } from 'src/components/Breadcrumb/Breadcrumb';
78
import { ErrorState } from 'src/components/ErrorState/ErrorState';
9+
import { Placeholder } from 'src/components/Placeholder/Placeholder';
810
import { useAlertDefinitionQuery } from 'src/queries/cloudpulse/alerts';
911

12+
import { getAlertBoxStyles } from '../Utils/utils';
13+
import { AlertDetailOverview } from './AlertDetailOverview';
14+
1015
interface RouteParams {
1116
/**
1217
* The id of the alert for which the data needs to be shown
@@ -21,7 +26,7 @@ interface RouteParams {
2126
export const AlertDetail = () => {
2227
const { alertId, serviceType } = useParams<RouteParams>();
2328

24-
const { isError, isFetching } = useAlertDefinitionQuery(
29+
const { data: alertDetails, isError, isFetching } = useAlertDefinitionQuery(
2530
Number(alertId),
2631
serviceType
2732
);
@@ -65,6 +70,47 @@ export const AlertDetail = () => {
6570
</>
6671
);
6772
}
68-
// TODO: The overview, criteria, resources details for alerts will be added by consuming the results of useAlertDefinitionQuery call in the coming PR's
69-
return <Breadcrumb crumbOverrides={crumbOverrides} pathname={pathname} />;
73+
74+
if (!alertDetails) {
75+
return (
76+
<>
77+
<Breadcrumb crumbOverrides={crumbOverrides} pathname={pathname} />
78+
<Box alignContent="center" height={theme.spacing(75)}>
79+
<StyledPlaceholder
80+
icon={AlertsIcon}
81+
isEntity
82+
title="No data to display."
83+
/>
84+
</Box>
85+
</>
86+
);
87+
}
88+
// TODO: The criteria, resources details for alerts will be added by consuming the results of useAlertDefinitionQuery call in the coming PR's
89+
return (
90+
<>
91+
<Breadcrumb crumbOverrides={crumbOverrides} pathname={pathname} />
92+
<Box display="flex" flexDirection="column" gap={2}>
93+
<Box display="flex" flexDirection={{ md: 'row', xs: 'column' }} gap={2}>
94+
<Box
95+
flexBasis="50%"
96+
maxHeight={theme.spacing(98.125)}
97+
sx={{ ...getAlertBoxStyles(theme), overflow: 'auto' }}
98+
>
99+
<AlertDetailOverview alertDetails={alertDetails} />
100+
</Box>
101+
</Box>
102+
</Box>
103+
</>
104+
);
70105
};
106+
107+
export const StyledPlaceholder = styled(Placeholder, {
108+
label: 'StyledPlaceholder',
109+
})(({ theme }) => ({
110+
h1: {
111+
fontSize: theme.spacing(2),
112+
},
113+
svg: {
114+
maxHeight: theme.spacing(10),
115+
},
116+
}));
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React from 'react';
2+
3+
import { alertFactory, serviceTypesFactory } from 'src/factories';
4+
import { renderWithTheme } from 'src/utilities/testHelpers';
5+
6+
import { convertStringToCamelCasesWithSpaces } from '../../Utils/utils';
7+
import { severityMap } from '../constants';
8+
import { AlertDetailOverview } from './AlertDetailOverview';
9+
10+
// Mock Data
11+
const serviceTypes = serviceTypesFactory.buildList(1, {
12+
label: 'Databases',
13+
service_type: 'dbaas',
14+
});
15+
16+
const alertDetails = alertFactory.build({
17+
description: 'This is test description',
18+
label: 'Test alert',
19+
severity: 3,
20+
});
21+
// Mock Queries
22+
const queryMocks = vi.hoisted(() => ({
23+
useCloudPulseServiceTypes: vi.fn(),
24+
}));
25+
26+
vi.mock('src/queries/cloudpulse/services', () => ({
27+
...vi.importActual('src/queries/cloudpulse/services'),
28+
useCloudPulseServiceTypes: queryMocks.useCloudPulseServiceTypes,
29+
}));
30+
31+
// Shared Setup
32+
beforeEach(() => {
33+
queryMocks.useCloudPulseServiceTypes.mockReturnValue({
34+
data: { data: serviceTypes },
35+
isError: false,
36+
isFetching: false,
37+
});
38+
});
39+
40+
describe('AlertDetailOverview component tests', () => {
41+
it('should render alert detail overview with required props', () => {
42+
const { getByText } = renderWithTheme(
43+
<AlertDetailOverview alertDetails={alertDetails} />
44+
);
45+
46+
const { description, label, severity, type } = alertDetails;
47+
48+
expect(getByText(description)).toBeInTheDocument();
49+
expect(getByText(severityMap[severity])).toBeInTheDocument();
50+
expect(getByText(label)).toBeInTheDocument();
51+
expect(
52+
getByText(convertStringToCamelCasesWithSpaces(type))
53+
).toBeInTheDocument();
54+
});
55+
56+
it('should render circle progress if the service types call is fetching', () => {
57+
queryMocks.useCloudPulseServiceTypes.mockReturnValue({
58+
data: { data: serviceTypes },
59+
isError: false,
60+
isFetching: true,
61+
});
62+
const { getByTestId, queryByText } = renderWithTheme(
63+
<AlertDetailOverview alertDetails={alertDetails} />
64+
);
65+
66+
expect(getByTestId('circle-progress')).toBeInTheDocument();
67+
expect(queryByText('Overview')).not.toBeInTheDocument();
68+
});
69+
});
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { CircleProgress, Typography } from '@linode/ui';
2+
import { Grid } from '@mui/material';
3+
import React from 'react';
4+
5+
import { useCloudPulseServiceTypes } from 'src/queries/cloudpulse/services';
6+
import { formatDate } from 'src/utilities/formatDate';
7+
8+
import { convertStringToCamelCasesWithSpaces } from '../../Utils/utils';
9+
import { alertStatusToIconStatusMap, severityMap } from '../constants';
10+
import { getServiceTypeLabel } from '../Utils/utils';
11+
import { AlertDetailRow } from './AlertDetailRow';
12+
13+
import type { Alert } from '@linode/api-v4';
14+
15+
interface OverviewProps {
16+
/*
17+
* The alert object containing all the details (e.g., description, severity, status) for which the overview needs to be displayed.
18+
*/
19+
alertDetails: Alert;
20+
}
21+
export const AlertDetailOverview = React.memo((props: OverviewProps) => {
22+
const { alertDetails } = props;
23+
24+
const {
25+
created_by: createdBy,
26+
description,
27+
label,
28+
service_type: serviceType,
29+
severity,
30+
status,
31+
type,
32+
updated,
33+
} = alertDetails;
34+
35+
const { data: serviceTypeList, isFetching } = useCloudPulseServiceTypes(true);
36+
37+
if (isFetching) {
38+
return <CircleProgress />;
39+
}
40+
41+
return (
42+
<>
43+
<Typography marginBottom={2} variant="h2">
44+
Overview
45+
</Typography>
46+
<Grid alignItems="center" container spacing={2}>
47+
<AlertDetailRow label="Name" value={label} />
48+
<AlertDetailRow label="Description" value={description} />
49+
<AlertDetailRow
50+
label="Status"
51+
status={alertStatusToIconStatusMap[status]}
52+
value={convertStringToCamelCasesWithSpaces(status)}
53+
/>
54+
<AlertDetailRow label="Severity" value={severityMap[severity]} />
55+
<AlertDetailRow
56+
label="Service"
57+
value={getServiceTypeLabel(serviceType, serviceTypeList)}
58+
/>
59+
<AlertDetailRow
60+
label="Type"
61+
value={convertStringToCamelCasesWithSpaces(type)}
62+
/>
63+
<AlertDetailRow label="Created By" value={createdBy} />
64+
<AlertDetailRow
65+
value={formatDate(updated, {
66+
format: 'MMM dd, yyyy, h:mm a',
67+
})}
68+
label="Last Modified"
69+
/>
70+
</Grid>
71+
</>
72+
);
73+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Typography } from '@linode/ui';
2+
import { Grid, useTheme } from '@mui/material';
3+
import React from 'react';
4+
5+
import { StatusIcon } from 'src/components/StatusIcon/StatusIcon';
6+
7+
import type { Status } from 'src/components/StatusIcon/StatusIcon';
8+
9+
interface AlertDetailRowProps {
10+
/**
11+
* The label or title of the row
12+
*/
13+
label: string;
14+
/**
15+
* Number of grid columns for the label on small and larger screens.
16+
* Defaults to 4. This controls the width of the label in the grid layout.
17+
*/
18+
labelGridColumns?: number;
19+
/**
20+
* The status icon to be displayed in the row. It can represent states like "active", "inactive", etc.
21+
* Pass a valid status (e.g., 'active', 'inactive') to display the appropriate status icon.
22+
*/
23+
status?: Status;
24+
/**
25+
* The value of the row
26+
*/
27+
value: null | string;
28+
/**
29+
* Number of grid columns for the value on medium and larger screens.
30+
* Defaults to 8. This controls the width of the value in the grid layout.
31+
*/
32+
valueGridColumns?: number;
33+
}
34+
35+
export const AlertDetailRow = React.memo((props: AlertDetailRowProps) => {
36+
const {
37+
label,
38+
labelGridColumns = 4,
39+
status,
40+
value,
41+
valueGridColumns = 8,
42+
} = props;
43+
44+
const theme = useTheme();
45+
46+
return (
47+
<Grid container item xs={12}>
48+
<Grid item sm={labelGridColumns} xs={12}>
49+
<Typography
50+
color={theme.tokens.content.Text.Primary.Default}
51+
fontFamily={theme.font.bold}
52+
variant="body1"
53+
>
54+
{label}:
55+
</Typography>
56+
</Grid>
57+
<Grid container item sm={valueGridColumns} xs={12}>
58+
{status && (
59+
<StatusIcon
60+
marginTop={theme.spacing(0.7)}
61+
maxHeight={theme.spacing(1)}
62+
maxWidth={theme.spacing(1)}
63+
status={status}
64+
/>
65+
)}
66+
<Typography
67+
color={theme.tokens.content.Text.Primary.Default}
68+
variant="body1"
69+
>
70+
{value}
71+
</Typography>
72+
</Grid>
73+
</Grid>
74+
);
75+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { serviceTypesFactory } from 'src/factories';
2+
3+
import { getServiceTypeLabel } from './utils';
4+
5+
it('test getServiceTypeLabel method', () => {
6+
const services = serviceTypesFactory.buildList(3);
7+
services.forEach((service) => {
8+
// validate for proper labels
9+
expect(getServiceTypeLabel(service.service_type, { data: services })).toBe(
10+
service.label
11+
);
12+
});
13+
expect(getServiceTypeLabel('test', { data: services })).toBe('test');
14+
expect(getServiceTypeLabel('', { data: services })).toBe('');
15+
});

0 commit comments

Comments
 (0)