Skip to content

Commit 90b7318

Browse files
upcoming: [DI-23548] - Added reusable component for alerts contextual view (linode#11685)
* upcoming: [DI-23311] - Added reusable component for alerts contextual view * added changeset * upcoming:[DI-23311] - Remove unused variable * upcoming: [DI-23548] - Updated test case * upcoming: [DI-23548] - Remove unused code * upcoming: [DI-23548] - Hide label in search & autocomplete * upcoming: [DI-23548] - Removed unused variable * upcoming: [DI-23548] - Updated button type to outlined
1 parent 418aa4d commit 90b7318

File tree

11 files changed

+309
-2
lines changed

11 files changed

+309
-2
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/api-v4": Upcoming Features
3+
---
4+
5+
add `getAlertDefinitionByServiceType` in alerts.ts ([#11685](https://github.com/linode/manager/pull/11685))

packages/api-v4/src/cloudpulse/alerts.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,13 @@ export const getNotificationChannels = (params?: Params, filters?: Filter) =>
7272
setParams(params),
7373
setXFilter(filters)
7474
);
75+
76+
export const getAlertDefinitionByServiceType = (serviceType: string) =>
77+
Request<ResourcePage<Alert>>(
78+
setURL(
79+
`${API_ROOT}/monitor/services/${encodeURIComponent(
80+
serviceType
81+
)}/alert-definitions`
82+
),
83+
setMethod('GET')
84+
);
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 `AlertReusableComponent`, add `convertAlertsToTypeSet` and `filterAlertsByStatusAndType` methods in utils.ts, add `useAlertDefinitionByServiceTypeQuery` in alerts.ts ([#11685](https://github.com/linode/manager/pull/11685))
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import userEvent from '@testing-library/user-event';
2+
import React from 'react';
3+
4+
import { alertFactory } from 'src/factories';
5+
import { renderWithTheme } from 'src/utilities/testHelpers';
6+
7+
import { AlertReusableComponent } from './AlertReusableComponent';
8+
9+
const mockQuery = vi.hoisted(() => ({
10+
useAddEntityToAlert: vi.fn(),
11+
useAlertDefinitionByServiceTypeQuery: vi.fn(),
12+
useRemoveEntityFromAlert: vi.fn(),
13+
}));
14+
15+
vi.mock('src/queries/cloudpulse/alerts', async () => {
16+
const actual = vi.importActual('src/queries/cloudpulse/alerts');
17+
return {
18+
...actual,
19+
useAddEntityToAlert: mockQuery.useAddEntityToAlert,
20+
useAlertDefinitionByServiceTypeQuery:
21+
mockQuery.useAlertDefinitionByServiceTypeQuery,
22+
useRemoveEntityFromAlert: mockQuery.useRemoveEntityFromAlert,
23+
};
24+
});
25+
const serviceType = 'linode';
26+
const entityId = '123';
27+
const entityName = 'test-instance';
28+
const alerts = [
29+
...alertFactory.buildList(3, { service_type: serviceType }),
30+
...alertFactory.buildList(7, {
31+
entity_ids: [entityId],
32+
service_type: serviceType,
33+
}),
34+
...alertFactory.buildList(1, {
35+
entity_ids: [entityId],
36+
service_type: serviceType,
37+
status: 'enabled',
38+
type: 'system',
39+
}),
40+
];
41+
42+
const mockReturnValue = {
43+
data: alerts,
44+
isError: false,
45+
isLoading: false,
46+
};
47+
48+
const component = (
49+
<AlertReusableComponent
50+
entityId={entityId}
51+
entityName={entityName}
52+
serviceType={serviceType}
53+
/>
54+
);
55+
56+
mockQuery.useAlertDefinitionByServiceTypeQuery.mockReturnValue(mockReturnValue);
57+
mockQuery.useAddEntityToAlert.mockReturnValue({
58+
mutateAsync: vi.fn(),
59+
});
60+
mockQuery.useRemoveEntityFromAlert.mockReturnValue({
61+
mutateAsync: vi.fn(),
62+
});
63+
64+
const mockHistory = {
65+
push: vi.fn(),
66+
replace: vi.fn(),
67+
};
68+
69+
vi.mock('react-router-dom', async () => {
70+
const actual = await vi.importActual('react-router-dom');
71+
return {
72+
...actual,
73+
useHistory: vi.fn(() => mockHistory),
74+
};
75+
});
76+
77+
describe('Alert Resuable Component for contextual view', () => {
78+
it('Should go to alerts definition page on clicking manage alerts button', async () => {
79+
const { getByTestId } = renderWithTheme(component);
80+
await userEvent.click(getByTestId('manage-alerts'));
81+
82+
expect(mockHistory.push).toHaveBeenCalledWith(
83+
'/monitor/alerts/definitions'
84+
);
85+
});
86+
});
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import {
2+
Autocomplete,
3+
Box,
4+
Button,
5+
CircleProgress,
6+
Paper,
7+
Stack,
8+
Tooltip,
9+
Typography,
10+
} from '@linode/ui';
11+
import React from 'react';
12+
import { useHistory } from 'react-router-dom';
13+
14+
import InfoIcon from 'src/assets/icons/info.svg';
15+
import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField';
16+
import { useAlertDefinitionByServiceTypeQuery } from 'src/queries/cloudpulse/alerts';
17+
18+
import { convertAlertsToTypeSet } from '../Utils/utils';
19+
20+
import type { AlertDefinitionType } from '@linode/api-v4';
21+
22+
interface AlertReusableComponentProps {
23+
/**
24+
* Id for the selected entity
25+
*/
26+
entityId: string;
27+
28+
/**
29+
* Name of the selected entity
30+
*/
31+
entityName: string;
32+
33+
/**
34+
* Service type of selected entity
35+
*/
36+
serviceType: string;
37+
}
38+
39+
export const AlertReusableComponent = (props: AlertReusableComponentProps) => {
40+
const { serviceType } = props;
41+
const { data: alerts, isLoading } = useAlertDefinitionByServiceTypeQuery(
42+
serviceType
43+
);
44+
45+
const [searchText, setSearchText] = React.useState<string>('');
46+
// This will be replaced with a variable in next PR
47+
const [_, setSelectedType] = React.useState<
48+
AlertDefinitionType | undefined
49+
>();
50+
51+
const history = useHistory();
52+
53+
// Filter unique alert types from alerts list
54+
const types = convertAlertsToTypeSet(alerts);
55+
56+
if (isLoading) {
57+
return <CircleProgress />;
58+
}
59+
return (
60+
<Paper>
61+
<Stack gap={3}>
62+
<Box display="flex" justifyContent="space-between">
63+
<Box alignItems="center" display="flex" gap={0.5}>
64+
<Typography variant="h2">Alerts</Typography>
65+
<Tooltip title="The list contains only the alerts enabled in the Monitor centralized view.">
66+
<span>
67+
<InfoIcon />
68+
</span>
69+
</Tooltip>
70+
</Box>
71+
<Button
72+
buttonType="outlined"
73+
data-testid="manage-alerts"
74+
onClick={() => history.push('/monitor/alerts/definitions')}
75+
>
76+
Manage Alerts
77+
</Button>
78+
</Box>
79+
<Stack gap={2}>
80+
<Box display="flex" gap={2}>
81+
<DebouncedSearchTextField
82+
data-testid="search-alert"
83+
hideLabel
84+
label="Search Alerts"
85+
noMarginTop
86+
onSearch={setSearchText}
87+
placeholder="Search for Alerts"
88+
sx={{ width: '250px' }}
89+
value={searchText}
90+
/>
91+
<Autocomplete
92+
onChange={(_, selectedValue) => {
93+
setSelectedType(selectedValue?.label);
94+
}}
95+
textFieldProps={{
96+
hideLabel: true,
97+
}}
98+
autoHighlight
99+
data-testid="alert-type-select"
100+
label="Select Type"
101+
noMarginTop
102+
options={types}
103+
placeholder="Select Alert Type"
104+
sx={{ width: '250px' }}
105+
/>
106+
</Box>
107+
</Stack>
108+
</Stack>
109+
</Paper>
110+
);
111+
};

packages/manager/src/features/CloudPulse/Alerts/Utils/utils.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { alertFactory, serviceTypesFactory } from 'src/factories';
22

33
import {
44
convertAlertDefinitionValues,
5+
convertAlertsToTypeSet,
56
convertSecondsToMinutes,
7+
filterAlertsByStatusAndType,
68
getServiceTypeLabel,
79
} from './utils';
810

@@ -28,6 +30,20 @@ it('test convertSecondsToMinutes method', () => {
2830
expect(convertSecondsToMinutes(59)).toBe('59 seconds');
2931
});
3032

33+
it('test filterAlertsByStatusAndType method', () => {
34+
const alerts = alertFactory.buildList(12, { created_by: 'system' });
35+
expect(filterAlertsByStatusAndType(alerts, '', 'system')).toHaveLength(12);
36+
expect(filterAlertsByStatusAndType(alerts, '', 'user')).toHaveLength(0);
37+
expect(filterAlertsByStatusAndType(alerts, 'Alert-1', 'system')).toHaveLength(
38+
4
39+
);
40+
});
41+
it('test convertAlertsToTypeSet method', () => {
42+
const alerts = alertFactory.buildList(12, { created_by: 'user' });
43+
44+
expect(convertAlertsToTypeSet(alerts)).toHaveLength(1);
45+
});
46+
3147
it('should correctly convert an alert definition values to the required format', () => {
3248
const alert: Alert = alertFactory.build();
3349
const serviceType = 'linode';

packages/manager/src/features/CloudPulse/Alerts/Utils/utils.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { AlertDimensionsProp } from '../AlertsDetail/DisplayAlertDetailChips';
22
import type {
33
Alert,
4+
AlertDefinitionType,
45
AlertServiceType,
56
EditAlertPayloadWithService,
67
NotificationChannel,
@@ -125,6 +126,45 @@ export const getChipLabels = (
125126
}
126127
};
127128

129+
/**
130+
*
131+
* @param alerts list of alerts to be filtered
132+
* @param searchText text to be searched in alert name
133+
* @param selectedType selecte alert type
134+
* @returns list of filtered alerts based on searchText & selectedType
135+
*/
136+
export const filterAlertsByStatusAndType = (
137+
alerts: Alert[] | undefined,
138+
searchText: string,
139+
selectedType: string | undefined
140+
): Alert[] => {
141+
return (
142+
alerts?.filter(({ label, status, type }) => {
143+
return (
144+
status === 'enabled' &&
145+
(!selectedType || type === selectedType) &&
146+
(!searchText || label.toLowerCase().includes(searchText.toLowerCase()))
147+
);
148+
}) ?? []
149+
);
150+
};
151+
152+
/**
153+
*
154+
* @param alerts list of alerts
155+
* @returns list of unique alert types in the alerts list in the form of json object
156+
*/
157+
export const convertAlertsToTypeSet = (
158+
alerts: Alert[] | undefined
159+
): { label: AlertDefinitionType }[] => {
160+
const types = new Set(alerts?.map(({ type }) => type) ?? []);
161+
162+
return Array.from(types).reduce(
163+
(previousValue, type) => [...previousValue, { label: type }],
164+
[]
165+
);
166+
};
167+
128168
/**
129169
* Filters and maps the alert data to match the form structure.
130170
* @param alert The alert object to be mapped.

packages/manager/src/mocks/serverHandlers.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2469,6 +2469,17 @@ export const handlers = [
24692469
return HttpResponse.json(response);
24702470
}
24712471
),
2472+
http.get(
2473+
'*/monitor/services/:serviceType/alert-definitions',
2474+
async ({ params }) => {
2475+
const serviceType = params.serviceType;
2476+
return HttpResponse.json({
2477+
data: alertFactory.buildList(20, {
2478+
service_type: serviceType === 'dbaas' ? 'dbaas' : 'linode',
2479+
}),
2480+
});
2481+
}
2482+
),
24722483
http.get('*/monitor/alert-definitions', async () => {
24732484
const customAlerts = alertFactory.buildList(10, {
24742485
created_by: 'user1',

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ export const useAllAlertDefinitionsQuery = (
4444
});
4545
};
4646

47+
export const useAlertDefinitionByServiceTypeQuery = (serviceType: string) => {
48+
return useQuery<Alert[], APIError[]>({
49+
...queryFactory.alerts._ctx.alertsByServiceType(serviceType),
50+
});
51+
};
52+
4753
export const useAlertDefinitionQuery = (
4854
alertId: string,
4955
serviceType: string

packages/manager/src/queries/cloudpulse/queries.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import { databaseQueries } from '../databases/databases';
1212
import { getAllLinodesRequest } from '../linodes/requests';
1313
import { volumeQueries } from '../volumes/volumes';
1414
import { fetchCloudPulseMetrics } from './metrics';
15-
import { getAllAlertsRequest, getAllNotificationChannels } from './requests';
15+
import {
16+
getAllAlertsRequest,
17+
getAllNotificationChannels,
18+
getAllertsByServiceTypeRequest,
19+
} from './requests';
1620

1721
import type {
1822
CloudPulseMetricsRequest,
@@ -35,6 +39,10 @@ export const queryFactory = createQueryKeys(key, {
3539
getAlertDefinitionByServiceTypeAndId(serviceType, alertId),
3640
queryKey: [alertId, serviceType],
3741
}),
42+
alertsByServiceType: (serviceType) => ({
43+
queryFn: () => getAllertsByServiceTypeRequest(serviceType),
44+
queryKey: [serviceType],
45+
}),
3846
all: (params: Params = {}, filter: Filter = {}) => ({
3947
queryFn: () => getAllAlertsRequest(params, filter),
4048
queryKey: [params, filter],

0 commit comments

Comments
 (0)