Skip to content

Commit 7e23502

Browse files
upcoming: [DI-23769] - multiple error handling (#11874)
* upcoming: [DI-23769] - Added util to handle and render Multiple Errors in forms, Added a Notice based component to render list or single accordingly * upcoming: [DI-23769] - Fixed linting issue * upcoming: [DI-23769] - Typecheck fix * upcoming: [DI-23769] - Added changeset * upcoming: [DI-23769] - Removed AlertsNoticeMessage component, enhanced the AlertListNoticeMessages to remove redundant components * upcoming: [DI-23769] - Fixing the failing tests * upcoming: [DI-23769] - Removed the wrapper component * upcoming: [DI-23769] - Minor UI changes * upcoming: [DI-23769] - Addressing review comments * upcoming: [DI-23769] - Fixing spacing inconsistency * upcoming: [DI-23769] - Fixed Error Notice message width in Resources across all window sizes --------- Co-authored-by: Nikhil Agrawal <165884194+nikhagra-akamai@users.noreply.github.com>
1 parent 7f827bc commit 7e23502

File tree

12 files changed

+481
-124
lines changed

12 files changed

+481
-124
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 AlertListNoticeMessages component for handling multiple API error messages, update AddChannelListing and MetricCriteria components to display these errors, Add handleMultipleError util method for aggregating, mapping the errors to fields. ([#11874](https://github.com/linode/manager/pull/11874))

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

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useRegionsQuery } from '@linode/queries';
22
import { Checkbox, CircleProgress, Stack, Typography } from '@linode/ui';
3-
import { Grid } from '@mui/material';
3+
import { Grid, useTheme } from '@mui/material';
44
import React from 'react';
55

66
import EntityIcon from 'src/assets/icons/entityIcons/alertsresources.svg';
@@ -9,6 +9,8 @@ import { useFlags } from 'src/hooks/useFlags';
99
import { useResourcesQuery } from 'src/queries/cloudpulse/resources';
1010

1111
import { StyledPlaceholder } from '../AlertsDetail/AlertDetail';
12+
import { MULTILINE_ERROR_SEPARATOR } from '../constants';
13+
import { AlertListNoticeMessages } from '../Utils/AlertListNoticeMessages';
1214
import {
1315
getAlertResourceFilterProps,
1416
getFilteredResources,
@@ -17,7 +19,6 @@ import {
1719
getSupportedRegionIds,
1820
scrollToElement,
1921
} from '../Utils/AlertResourceUtils';
20-
import { AlertsNoticeMessage } from '../Utils/AlertsNoticeMessage';
2122
import { AlertResourcesFilterRenderer } from './AlertsResourcesFilterRenderer';
2223
import { AlertsResourcesNotice } from './AlertsResourcesNotice';
2324
import { databaseTypeClassMap, serviceToFiltersMap } from './constants';
@@ -126,6 +127,7 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => {
126127
} = useRegionsQuery();
127128

128129
const flags = useFlags();
130+
const theme = useTheme();
129131

130132
// Validate launchDarkly region ids with the ids from regionOptions prop
131133
const supportedRegionIds = getSupportedRegionIds(
@@ -336,7 +338,15 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => {
336338
}
337339

338340
const filtersToRender = serviceToFiltersMap[serviceType ?? ''];
339-
341+
const noticeStyles: React.CSSProperties = {
342+
alignItems: 'center',
343+
backgroundColor: theme.tokens.alias.Background.Normal,
344+
borderRadius: 1,
345+
display: 'flex',
346+
flexWrap: 'nowrap',
347+
marginBottom: 0,
348+
padding: theme.spacingFunction(16),
349+
};
340350
return (
341351
<Stack gap={2}>
342352
{!hideLabel && (
@@ -415,13 +425,24 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => {
415425
</Grid>
416426
)}
417427
{errorText?.length && (
418-
<AlertsNoticeMessage text={errorText} variant="error" />
428+
<Grid item xs={12}>
429+
<AlertListNoticeMessages
430+
errorMessage={errorText}
431+
separator={MULTILINE_ERROR_SEPARATOR}
432+
style={noticeStyles}
433+
variant="error"
434+
/>
435+
</Grid>
419436
)}
420437
{maxSelectionCount !== undefined && (
421-
<AlertsNoticeMessage
422-
text={`You can select up to ${maxSelectionCount} resources.`}
423-
variant="warning"
424-
/>
438+
<Grid item xs={12}>
439+
<AlertListNoticeMessages
440+
errorMessage={`You can select up to ${maxSelectionCount} resources.`}
441+
separator={MULTILINE_ERROR_SEPARATOR}
442+
style={noticeStyles}
443+
variant="warning"
444+
/>
445+
</Grid>
425446
)}
426447
{isSelectionsNeeded &&
427448
!isDataLoadingError &&

packages/manager/src/features/CloudPulse/Alerts/CreateAlert/CreateAlertDefinition.tsx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,16 @@ import { DocumentTitleSegment } from 'src/components/DocumentTitle';
1212
import { useFlags } from 'src/hooks/useFlags';
1313
import { useCreateAlertDefinition } from 'src/queries/cloudpulse/alerts';
1414

15-
import { CREATE_ALERT_SUCCESS_MESSAGE } from '../constants';
16-
import { enhanceValidationSchemaWithEntityIdValidation } from '../Utils/utils';
15+
import {
16+
CREATE_ALERT_ERROR_FIELD_MAP,
17+
MULTILINE_ERROR_SEPARATOR,
18+
SINGLELINE_ERROR_SEPARATOR,
19+
CREATE_ALERT_SUCCESS_MESSAGE
20+
} from '../constants';
21+
import {
22+
enhanceValidationSchemaWithEntityIdValidation,
23+
handleMultipleError,
24+
} from '../Utils/utils';
1725
import { MetricCriteriaField } from './Criteria/MetricCriteria';
1826
import { TriggerConditions } from './Criteria/TriggerConditions';
1927
import { CloudPulseAlertSeveritySelect } from './GeneralInformation/AlertSeveritySelect';
@@ -28,6 +36,7 @@ import type {
2836
MetricCriteriaForm,
2937
TriggerConditionForm,
3038
} from './types';
39+
import type { APIError } from '@linode/api-v4';
3140
import type { ObjectSchema } from 'yup';
3241

3342
const triggerConditionInitialValues: TriggerConditionForm = {
@@ -116,15 +125,19 @@ export const CreateAlertDefinition = () => {
116125
});
117126
alertCreateExit();
118127
} catch (errors) {
119-
for (const error of errors) {
120-
if (error.field) {
121-
setError(error.field, { message: error.reason });
122-
} else {
123-
enqueueSnackbar(`Alert failed: ${error.reason}`, {
124-
variant: 'error',
125-
});
126-
setError('root', { message: error.reason });
127-
}
128+
handleMultipleError<CreateAlertDefinitionForm>({
129+
errorFieldMap: CREATE_ALERT_ERROR_FIELD_MAP,
130+
errors,
131+
multiLineErrorSeparator: MULTILINE_ERROR_SEPARATOR,
132+
setError,
133+
singleLineErrorSeparator: SINGLELINE_ERROR_SEPARATOR,
134+
});
135+
136+
const rootError = errors.find((error: APIError) => !error.field);
137+
if (rootError) {
138+
enqueueSnackbar(`Creating alert failed: ${rootError.reason}`, {
139+
variant: 'error',
140+
});
128141
}
129142
}
130143
});

packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/MetricCriteria.tsx

Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import { Button, Stack, Typography } from '@linode/ui';
22
import * as React from 'react';
3-
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
3+
import {
4+
Controller,
5+
useFieldArray,
6+
useFormContext,
7+
useWatch,
8+
} from 'react-hook-form';
49

510
import { useGetCloudPulseMetricDefinitionsByServiceType } from 'src/queries/cloudpulse/services';
611

12+
import { MULTILINE_ERROR_SEPARATOR } from '../../constants';
13+
import { AlertListNoticeMessages } from '../../Utils/AlertListNoticeMessages';
714
import { convertToSeconds } from '../utilities';
815
import { Metric } from './Metric';
916

@@ -66,45 +73,60 @@ export const MetricCriteriaField = (props: MetricCriteriaProps) => {
6673
});
6774

6875
return (
69-
<Stack mt={3} spacing={2}>
70-
<Typography variant="h2">3. Criteria</Typography>
71-
<Stack spacing={2}>
72-
{fields !== null &&
73-
fields.length !== 0 &&
74-
fields.map((field, index) => {
75-
return (
76-
<Metric
77-
data={metricDefinitions ? metricDefinitions.data : []}
78-
isMetricDefinitionError={isMetricDefinitionError}
79-
isMetricDefinitionLoading={isMetricDefinitionLoading}
80-
key={field.id}
81-
name={`rule_criteria.rules.${index}`}
82-
onMetricDelete={() => remove(index)}
83-
showDeleteIcon={fields.length > 1}
76+
<Controller
77+
render={({ fieldState, formState }) => (
78+
<Stack mt={3} spacing={2}>
79+
<Typography variant="h2">3. Criteria</Typography>
80+
{formState.isSubmitted &&
81+
fieldState.error &&
82+
fieldState.error.message?.length && (
83+
<AlertListNoticeMessages
84+
errorMessage={fieldState.error.message}
85+
separator={MULTILINE_ERROR_SEPARATOR}
86+
variant="error"
8487
/>
85-
);
86-
})}
87-
</Stack>
88-
<Button
89-
onClick={() =>
90-
append({
91-
aggregate_function: null,
92-
dimension_filters: [],
93-
metric: null,
94-
operator: null,
95-
threshold: 0,
96-
})
97-
}
98-
sx={{
99-
width: '130px', // added a nice width for the button
100-
}}
101-
buttonType="outlined"
102-
disabled={metricCriteriaWatcher.length === 5}
103-
size="medium"
104-
tooltipText="You can add up to 5 metrics."
105-
>
106-
Add metric
107-
</Button>
108-
</Stack>
88+
)}
89+
<Stack spacing={2}>
90+
{fields !== null &&
91+
fields.length !== 0 &&
92+
fields.map((field, index) => {
93+
return (
94+
<Metric
95+
data={metricDefinitions ? metricDefinitions.data : []}
96+
isMetricDefinitionError={isMetricDefinitionError}
97+
isMetricDefinitionLoading={isMetricDefinitionLoading}
98+
key={field.id}
99+
name={`rule_criteria.rules.${index}`}
100+
onMetricDelete={() => remove(index)}
101+
showDeleteIcon={fields.length > 1}
102+
/>
103+
);
104+
})}
105+
</Stack>
106+
<Button
107+
onClick={() =>
108+
append({
109+
aggregate_function: null,
110+
dimension_filters: [],
111+
metric: null,
112+
operator: null,
113+
threshold: 0,
114+
})
115+
}
116+
sx={{
117+
width: '130px',
118+
}}
119+
buttonType="outlined"
120+
disabled={metricCriteriaWatcher.length === 5}
121+
size="medium"
122+
tooltipText="You can add up to 5 metrics."
123+
>
124+
Add metric
125+
</Button>
126+
</Stack>
127+
)}
128+
control={control}
129+
name={name}
130+
/>
109131
);
110132
};

packages/manager/src/features/CloudPulse/Alerts/CreateAlert/NotificationChannels/AddChannelListing.tsx

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { Box, Button, Notice, Stack, Typography } from '@linode/ui';
1+
import { Box, Button, Stack, Typography } from '@linode/ui';
22
import { capitalize } from '@linode/utilities';
33
import React from 'react';
44
import { Controller, useFormContext, useWatch } from 'react-hook-form';
55

66
import { useAllAlertNotificationChannelsQuery } from 'src/queries/cloudpulse/alerts';
77

8-
import { channelTypeOptions } from '../../constants';
8+
import { MULTILINE_ERROR_SEPARATOR, channelTypeOptions } from '../../constants';
9+
import { AlertListNoticeMessages } from '../../Utils/AlertListNoticeMessages';
910
import { getAlertBoxStyles } from '../../Utils/utils';
1011
import { ClearIconButton } from '../Criteria/ClearIconButton';
1112
import { AddNotificationChannelDrawer } from './AddNotificationChannelDrawer';
@@ -140,32 +141,38 @@ export const AddChannelListing = (props: AddChannelListingProps) => {
140141
return (
141142
<Controller
142143
render={({ fieldState, formState }) => (
143-
<>
144-
<Typography marginBottom={1} marginTop={3} variant="h2">
145-
4. Notification Channels
146-
</Typography>
147-
{(formState.isSubmitted || fieldState.isTouched) && fieldState.error && (
148-
<Notice spacingBottom={0} spacingTop={12} variant="error">
149-
{fieldState.error.message}
150-
</Notice>
151-
)}
152-
<Stack spacing={1}>
153-
{selectedNotifications.length > 0 &&
154-
selectedNotifications.map((notification, id) => (
144+
<Stack mt={3} spacing={2}>
145+
<Typography variant="h2">4. Notification Channels</Typography>
146+
{(formState.isSubmitted || fieldState.isTouched) &&
147+
fieldState.error &&
148+
fieldState.error.message?.length && (
149+
<AlertListNoticeMessages
150+
errorMessage={fieldState.error.message}
151+
separator={MULTILINE_ERROR_SEPARATOR}
152+
variant="error"
153+
/>
154+
)}
155+
{selectedNotifications.length > 0 && (
156+
<Stack spacing={2}>
157+
{selectedNotifications.map((notification, id) => (
155158
<NotificationChannelCard
156159
id={id}
157160
key={id}
158161
notification={notification}
159162
/>
160163
))}
161-
</Stack>
164+
</Stack>
165+
)}
162166
<Button
167+
sx={{
168+
width:
169+
notificationChannelWatcher.length === 5 ? '215px' : '190px',
170+
}}
163171
buttonType="outlined"
164172
data-qa-buttons="true"
165173
disabled={notificationChannelWatcher.length === 5}
166174
onClick={handleOpenDrawer}
167175
size="medium"
168-
sx={(theme) => ({ marginTop: theme.spacing(2) })}
169176
tooltipText="You can add up to 5 notification channels."
170177
>
171178
Add notification channel
@@ -179,7 +186,7 @@ export const AddChannelListing = (props: AddChannelListingProps) => {
179186
open={openAddNotification}
180187
templateData={notifications ?? []}
181188
/>
182-
</>
189+
</Stack>
183190
)}
184191
control={control}
185192
name={name}

packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertDefinition.tsx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ import { Breadcrumb } from 'src/components/Breadcrumb/Breadcrumb';
1111
import { useFlags } from 'src/hooks/useFlags';
1212
import { useEditAlertDefinition } from 'src/queries/cloudpulse/alerts';
1313

14-
import { UPDATE_ALERT_SUCCESS_MESSAGE } from '../constants';
14+
import {
15+
EDIT_ALERT_ERROR_FIELD_MAP,
16+
MULTILINE_ERROR_SEPARATOR,
17+
SINGLELINE_ERROR_SEPARATOR,
18+
UPDATE_ALERT_SUCCESS_MESSAGE
19+
} from '../constants';
20+
1521
import { MetricCriteriaField } from '../CreateAlert/Criteria/MetricCriteria';
1622
import { TriggerConditions } from '../CreateAlert/Criteria/TriggerConditions';
1723
import { CloudPulseAlertSeveritySelect } from '../CreateAlert/GeneralInformation/AlertSeveritySelect';
@@ -21,10 +27,12 @@ import { CloudPulseModifyAlertResources } from '../CreateAlert/Resources/CloudPu
2127
import {
2228
convertAlertDefinitionValues,
2329
enhanceValidationSchemaWithEntityIdValidation,
30+
handleMultipleError,
2431
} from '../Utils/utils';
2532
import { EditAlertDefinitionFormSchema } from './schemas';
2633

2734
import type {
35+
APIError,
2836
Alert,
2937
AlertServiceType,
3038
EditAlertDefinitionPayload,
@@ -79,15 +87,19 @@ export const EditAlertDefinition = (props: EditAlertProps) => {
7987
});
8088
history.push(definitionLanding);
8189
} catch (errors) {
82-
for (const error of errors) {
83-
if (error.field) {
84-
setError(error.field, { message: error.reason });
85-
} else {
86-
enqueueSnackbar(`Alert update failed: ${error.reason}`, {
87-
variant: 'error',
88-
});
89-
setError('root', { message: error.reason });
90-
}
90+
handleMultipleError<EditAlertDefinitionPayload>({
91+
errorFieldMap: EDIT_ALERT_ERROR_FIELD_MAP,
92+
errors,
93+
multiLineErrorSeparator: MULTILINE_ERROR_SEPARATOR,
94+
setError,
95+
singleLineErrorSeparator: SINGLELINE_ERROR_SEPARATOR,
96+
});
97+
98+
const rootError = errors.find((error: APIError) => !error.field);
99+
if (rootError) {
100+
enqueueSnackbar(`Editing alert failed: ${rootError.reason}`, {
101+
variant: 'error',
102+
});
91103
}
92104
}
93105
});

0 commit comments

Comments
 (0)