Skip to content

Commit 9b53376

Browse files
upcoming: [DI-24743] - Disabling ability to create based on account limits (linode#12096)
* upcoming: [DI-24743] - Disabling ability to create an alert based on number of alerts and metrics at account level * upcoming: [DI-24743] - Added changesets * upcoming: [DI-24743] - Fixing reviewdog eslint issues * upcoming: [DI-24743] - Addressing review comments: simplifying the logic , ESlint fixes
1 parent 3fec3ec commit 9b53376

File tree

6 files changed

+169
-47
lines changed

6 files changed

+169
-47
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+
Disabled the Create Alert button with tooltip text and Added Notice banners to show the message when either alert limit or metric limit or both of them are triggered. Temporarily used ul, li tags in AlertListNoticeMessages.tsx to fix the alignment issues of the Notification Banner icon. ([#12096](https://github.com/linode/manager/pull/12096))

packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListing.test.tsx

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
import { act, waitFor, within } from '@testing-library/react';
2-
import { screen } from '@testing-library/react';
1+
import { act, screen, waitFor, within } from '@testing-library/react';
32
import userEvent from '@testing-library/user-event';
43
import * as React from 'react';
54

6-
import { alertFactory } from 'src/factories/cloudpulse/alerts';
5+
import {
6+
alertFactory,
7+
alertRulesFactory,
8+
} from 'src/factories/cloudpulse/alerts';
79
import { renderWithTheme } from 'src/utilities/testHelpers';
810

911
import { AlertListing } from './AlertListing';
12+
import {
13+
alertLimitMessage,
14+
alertToolTipText,
15+
metricLimitMessage,
16+
} from './constants';
1017

1118
const queryMocks = vi.hoisted(() => ({
1219
useAllAlertDefinitionsQuery: vi.fn().mockReturnValue({}),
@@ -174,4 +181,55 @@ describe('Alert Listing', () => {
174181
expect(queryByText(alert2.label)).not.toBeInTheDocument();
175182
});
176183
});
184+
185+
it('should show the banner and disable the create button when the user has reached the maximum allowed user alerts', async () => {
186+
const userAlerts = alertFactory.buildList(100, { type: 'user' });
187+
const systemAlerts = alertFactory.buildList(10, { type: 'system' });
188+
189+
queryMocks.useAllAlertDefinitionsQuery.mockReturnValueOnce({
190+
data: [...userAlerts, ...systemAlerts],
191+
isError: false,
192+
isLoading: false,
193+
status: 'success',
194+
});
195+
196+
renderWithTheme(<AlertListing />);
197+
198+
expect(screen.getByText(alertLimitMessage)).toBeVisible();
199+
const createButton = screen.getByRole('button', { name: 'Create Alert' });
200+
201+
expect(createButton).toBeDisabled();
202+
await userEvent.hover(createButton);
203+
await waitFor(() => {
204+
expect(screen.getByText(alertToolTipText)).toBeVisible();
205+
});
206+
});
207+
208+
it('should show the banner and disable the create button when the user has reached the maximum allowed user metrics', async () => {
209+
const userAlerts = alertFactory.buildList(25, {
210+
rule_criteria: {
211+
rules: alertRulesFactory.buildList(4, { dimension_filters: [] }),
212+
},
213+
type: 'user',
214+
});
215+
const systemAlerts = alertFactory.buildList(10, { type: 'system' });
216+
217+
queryMocks.useAllAlertDefinitionsQuery.mockReturnValueOnce({
218+
data: [...userAlerts, ...systemAlerts],
219+
isError: false,
220+
isLoading: false,
221+
status: 'success',
222+
});
223+
224+
renderWithTheme(<AlertListing />);
225+
226+
expect(screen.getByText(metricLimitMessage)).toBeVisible();
227+
const createButton = screen.getByRole('button', { name: 'Create Alert' });
228+
229+
expect(createButton).toBeDisabled();
230+
await userEvent.hover(createButton);
231+
await waitFor(() => {
232+
expect(screen.getByText(alertToolTipText)).toBeVisible();
233+
});
234+
});
177235
});

packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListing.tsx

Lines changed: 87 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@ import { useCloudPulseServiceTypes } from 'src/queries/cloudpulse/services';
1010

1111
import { usePreferencesToggle } from '../../Utils/UserPreference';
1212
import { alertStatusOptions } from '../constants';
13+
import { AlertListNoticeMessages } from '../Utils/AlertListNoticeMessages';
1314
import { scrollToElement } from '../Utils/AlertResourceUtils';
1415
import { AlertsListTable } from './AlertListTable';
16+
import {
17+
alertLimitMessage,
18+
alertToolTipText,
19+
metricLimitMessage,
20+
} from './constants';
1521

1622
import type { Item } from '../constants';
1723
import type { Alert, AlertServiceType, AlertStatusType } from '@linode/api-v4';
@@ -22,6 +28,13 @@ const searchAndSelectSx = {
2228
sm: '400px',
2329
xs: '300px',
2430
};
31+
// hardcoding the value is temporary solution until a solution from API side is confirmed.
32+
const maxAllowedAlerts = 100;
33+
const maxAllowedMetrics = 100;
34+
interface AlertsLimitErrorMessageProps {
35+
isAlertLimitReached: boolean;
36+
isMetricLimitReached: boolean;
37+
}
2538

2639
export const AlertListing = () => {
2740
const { url } = useRouteMatch();
@@ -33,6 +46,15 @@ export const AlertListing = () => {
3346
isLoading: serviceTypesLoading,
3447
} = useCloudPulseServiceTypes(true);
3548

49+
const userAlerts = alerts?.filter(({ type }) => type === 'user') ?? [];
50+
const isAlertLimitReached = userAlerts.length >= maxAllowedAlerts;
51+
52+
const isMetricLimitReached =
53+
userAlerts.reduce(
54+
(total, alert) => total + (alert.rule_criteria?.rules?.length ?? 0),
55+
0
56+
) >= maxAllowedMetrics;
57+
3658
const topRef = React.useRef<HTMLButtonElement>(null);
3759
const getServicesList = React.useMemo((): Item<
3860
string,
@@ -150,6 +172,12 @@ export const AlertListing = () => {
150172

151173
return (
152174
<Stack spacing={2}>
175+
{(isAlertLimitReached || isMetricLimitReached) && (
176+
<AlertsLimitErrorMessage
177+
isAlertLimitReached={isAlertLimitReached}
178+
isMetricLimitReached={isMetricLimitReached}
179+
/>
180+
)}
153181
<Box
154182
alignItems={{ lg: 'flex-end', md: 'flex-start' }}
155183
display="flex"
@@ -160,70 +188,74 @@ export const AlertListing = () => {
160188
ref={topRef}
161189
>
162190
<Box
191+
display="flex"
163192
flexDirection={{
164193
lg: 'row',
165194
md: 'column',
166195
sm: 'column',
167196
xs: 'column',
168197
}}
169-
display="flex"
170198
gap={2}
171199
>
172200
<DebouncedSearchTextField
173-
sx={{
174-
width: searchAndSelectSx,
175-
}}
176201
data-qa-filter="alert-search"
177202
label=""
178203
noMarginTop
179204
onSearch={setSearchText}
180205
placeholder="Search for Alerts"
206+
sx={{
207+
width: searchAndSelectSx,
208+
}}
181209
value={searchText}
182210
/>
183211
<Autocomplete
212+
autoHighlight
213+
data-qa-filter="alert-service-filter"
214+
data-testid="alert-service-filter"
184215
errorText={
185216
serviceTypesError
186217
? 'There was an error in fetching the services.'
187218
: ''
188219
}
189-
onChange={(_, selected) => {
190-
setServiceFilters(selected);
191-
}}
192-
sx={{
193-
width: searchAndSelectSx,
194-
}}
195-
autoHighlight
196-
data-qa-filter="alert-service-filter"
197-
data-testid="alert-service-filter"
198220
label=""
199221
limitTags={1}
200222
loading={serviceTypesLoading}
201223
multiple
202224
noMarginTop
203-
options={getServicesList}
204-
placeholder={serviceFilters.length > 0 ? '' : 'Select a Service'}
205-
value={serviceFilters}
206-
/>
207-
<Autocomplete
208225
onChange={(_, selected) => {
209-
setStatusFilters(selected);
226+
setServiceFilters(selected);
210227
}}
228+
options={getServicesList}
229+
placeholder={serviceFilters.length > 0 ? '' : 'Select a Service'}
211230
sx={{
212231
width: searchAndSelectSx,
213232
}}
233+
value={serviceFilters}
234+
/>
235+
<Autocomplete
214236
autoHighlight
215237
data-qa-filter="alert-status-filter"
216238
data-testid="alert-status-filter"
217239
label=""
218240
limitTags={1}
219241
multiple
220242
noMarginTop
243+
onChange={(_, selected) => {
244+
setStatusFilters(selected);
245+
}}
221246
options={alertStatusOptions}
222247
placeholder={statusFilters.length > 0 ? '' : 'Select a Status'}
248+
sx={{
249+
width: searchAndSelectSx,
250+
}}
223251
value={statusFilters}
224252
/>
225253
</Box>
226254
<Button
255+
buttonType="primary"
256+
data-qa-button="create-alert"
257+
data-qa-buttons="true"
258+
disabled={isAlertLimitReached || isMetricLimitReached}
227259
onClick={() => {
228260
history.push(`${url}/create`);
229261
}}
@@ -234,9 +266,7 @@ export const AlertListing = () => {
234266
whiteSpace: 'noWrap',
235267
width: { lg: '120px', md: '120px', sm: '150px', xs: '150px' },
236268
}}
237-
buttonType="primary"
238-
data-qa-button="create-alert"
239-
data-qa-buttons="true"
269+
tooltipText={alertToolTipText}
240270
variant="contained"
241271
>
242272
Create Alert
@@ -254,3 +284,38 @@ export const AlertListing = () => {
254284
</Stack>
255285
);
256286
};
287+
288+
const AlertsLimitErrorMessage = ({
289+
isAlertLimitReached,
290+
isMetricLimitReached,
291+
}: AlertsLimitErrorMessageProps) => {
292+
if (isAlertLimitReached && isMetricLimitReached) {
293+
return (
294+
<AlertListNoticeMessages
295+
errorMessage={`${alertLimitMessage}:${metricLimitMessage}`}
296+
separator=":"
297+
variant="warning"
298+
/>
299+
);
300+
}
301+
302+
if (isAlertLimitReached) {
303+
return (
304+
<AlertListNoticeMessages
305+
errorMessage={alertLimitMessage}
306+
variant="warning"
307+
/>
308+
);
309+
}
310+
311+
if (isMetricLimitReached) {
312+
return (
313+
<AlertListNoticeMessages
314+
errorMessage={metricLimitMessage}
315+
variant="warning"
316+
/>
317+
);
318+
}
319+
320+
return null;
321+
};

packages/manager/src/features/CloudPulse/Alerts/AlertsListing/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,10 @@ export const AlertContextualViewTableHeaderMap: TableColumnHeader[] = [
3939
{ columnName: 'Metric Threshold', label: 'id' },
4040
{ columnName: 'Alert Type', label: 'type' },
4141
];
42+
43+
export const alertLimitMessage =
44+
'You have reached the maximum number of definitions created per account.';
45+
export const metricLimitMessage =
46+
'You have reached the maximum number of metrics that can be evaluated by alerts created on this account.';
47+
export const alertToolTipText =
48+
'You have reached your limit of definitions for this account.';

packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,6 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => {
445445
<Grid item xs={12}>
446446
<AlertListNoticeMessages
447447
errorMessage={`You can select up to ${maxSelectionCount} entities.`}
448-
separator={MULTILINE_ERROR_SEPARATOR}
449448
style={noticeStyles}
450449
variant="warning"
451450
/>

packages/manager/src/features/CloudPulse/Alerts/Utils/AlertListNoticeMessages.tsx

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { List, ListItem, Notice, Typography } from '@linode/ui';
1+
import { Notice, Typography } from '@linode/ui';
22
import React from 'react';
33

44
import type { NoticeProps } from '@linode/ui';
@@ -11,49 +11,37 @@ interface AlertListNoticeMessagesProps extends NoticeProps {
1111
/**
1212
* The separator used to split the error message into individual errors
1313
*/
14-
separator: string;
14+
separator?: string;
1515
}
1616

1717
export const AlertListNoticeMessages = (
1818
props: AlertListNoticeMessagesProps
1919
) => {
2020
const { errorMessage, separator, style, sx, variant } = props;
21-
const errorList = errorMessage.split(separator);
21+
const errorList = separator ? errorMessage.split(separator) : [errorMessage];
2222

2323
if (errorList.length > 1) {
2424
return (
2525
<Notice data-alert-notice style={style} sx={sx} variant={variant}>
26-
<List
27-
sx={(theme) => ({
28-
listStyleType: 'disc',
29-
pl: theme.spacingFunction(8),
30-
})}
31-
>
26+
{/* temporarily using `ul`, `li` tags instead of `List`, `ListItem` till we figure out the alignment issue with the icon and messages in the Notice */}
27+
<ul style={{ margin: 0, paddingLeft: 20 }}>
3228
{errorList.map((error, index) => (
33-
<ListItem
34-
sx={(theme) => ({
35-
display: 'list-item',
36-
pl: theme.spacingFunction(4),
37-
py: theme.spacingFunction(4),
38-
})}
39-
data-testid="alert_notice_message_list"
40-
key={index}
41-
>
29+
<li data-testid="alert_notice_message_list" key={index}>
4230
{error}
43-
</ListItem>
31+
</li>
4432
))}
45-
</List>
33+
</ul>
4634
</Notice>
4735
);
4836
}
4937

5038
return (
5139
<Notice data-alert-notice style={style} sx={sx} variant={variant}>
5240
<Typography
41+
data-testid="alert_message_notice"
5342
sx={(theme) => ({
5443
fontFamily: theme.tokens.font.FontWeight.Extrabold,
5544
})}
56-
data-testid="alert_message_notice"
5745
>
5846
{errorList[0]}
5947
</Typography>

0 commit comments

Comments
 (0)