Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

upcoming: [DI-23769] - multiple error handling #11874

Open
wants to merge 17 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0355dae
upcoming: [DI-23769] - Added util to handle and render Multiple Error…
santoshp210-akamai Mar 17, 2025
2f9d305
upcoming: [DI-23769] - Fixed linting issue
santoshp210-akamai Mar 18, 2025
c433779
upcoming: [DI-23769] - Typecheck fix
santoshp210-akamai Mar 18, 2025
f3325f6
Merge branch 'develop' into feature/multiple-error-handling
santoshp210-akamai Mar 18, 2025
9810b63
upcoming: [DI-23769] - Added changeset
santoshp210-akamai Mar 18, 2025
2d3c26d
Merge branch 'develop' into feature/multiple-error-handling
nikhagra-akamai Mar 19, 2025
a1b9f0a
upcoming: [DI-23769] - Removed AlertsNoticeMessage component, enhance…
santoshp210-akamai Mar 24, 2025
52f50d7
merging latest linode/develop
santoshp210-akamai Mar 24, 2025
48e0dc7
upcoming: [DI-23769] - Fixing the failing tests
santoshp210-akamai Mar 24, 2025
3478462
Merge branch 'develop' into feature/multiple-error-handling
nikhagra-akamai Mar 25, 2025
c854880
upcoming: [DI-23769] - Removed the wrapper component
santoshp210-akamai Mar 25, 2025
63cf6f3
upcoming: [DI-23769] - Minor UI changes
santoshp210-akamai Mar 25, 2025
aa8a1e2
Merge branch 'develop' into feature/multiple-error-handling
santoshp210-akamai Mar 26, 2025
13eccb3
upcoming: [DI-23769] - Addressing review comments
santoshp210-akamai Mar 27, 2025
60d6cca
Merge branch 'develop' into feature/multiple-error-handling
santoshp210-akamai Mar 27, 2025
6853775
upcoming: [DI-23769] - Fixing spacing inconsistency
santoshp210-akamai Mar 27, 2025
4773618
upcoming: [DI-23769] - Fixed Error Notice message width in Resources …
santoshp210-akamai Mar 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

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))
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useRegionsQuery } from '@linode/queries';
import { Checkbox, CircleProgress, Stack, Typography } from '@linode/ui';
import { Grid } from '@mui/material';
import { Grid, useTheme } from '@mui/material';
import React from 'react';

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

import { StyledPlaceholder } from '../AlertsDetail/AlertDetail';
import { MULTILINE_ERROR_SEPARATOR } from '../constants';
import { AlertListNoticeMessages } from '../Utils/AlertListNoticeMessages';
import {
getAlertResourceFilterProps,
getFilteredResources,
Expand All @@ -17,7 +19,6 @@ import {
getSupportedRegionIds,
scrollToElement,
} from '../Utils/AlertResourceUtils';
import { AlertsNoticeMessage } from '../Utils/AlertsNoticeMessage';
import { AlertResourcesFilterRenderer } from './AlertsResourcesFilterRenderer';
import { AlertsResourcesNotice } from './AlertsResourcesNotice';
import { databaseTypeClassMap, serviceToFiltersMap } from './constants';
Expand Down Expand Up @@ -126,6 +127,7 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => {
} = useRegionsQuery();

const flags = useFlags();
const theme = useTheme();

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

const filtersToRender = serviceToFiltersMap[serviceType ?? ''];

const noticeStyles: React.CSSProperties = {
alignItems: 'center',
backgroundColor: theme.tokens.alias.Background.Normal,
borderRadius: 1,
display: 'flex',
flexWrap: 'nowrap',
marginBottom: 0,
padding: theme.spacingFunction(16),
};
return (
<Stack gap={2}>
{!hideLabel && (
Expand Down Expand Up @@ -415,13 +425,24 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => {
</Grid>
)}
{errorText?.length && (
<AlertsNoticeMessage text={errorText} variant="error" />
<Grid item xs={12}>
<AlertListNoticeMessages
errorMessage={errorText}
separator={MULTILINE_ERROR_SEPARATOR}
style={noticeStyles}
variant="error"
/>
</Grid>
)}
{maxSelectionCount !== undefined && (
<AlertsNoticeMessage
text={`You can select up to ${maxSelectionCount} resources.`}
variant="warning"
/>
<Grid item xs={12}>
<AlertListNoticeMessages
errorMessage={`You can select up to ${maxSelectionCount} resources.`}
separator={MULTILINE_ERROR_SEPARATOR}
style={noticeStyles}
variant="warning"
/>
</Grid>
)}
{isSelectionsNeeded &&
!isDataLoadingError &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,16 @@ import { DocumentTitleSegment } from 'src/components/DocumentTitle';
import { useFlags } from 'src/hooks/useFlags';
import { useCreateAlertDefinition } from 'src/queries/cloudpulse/alerts';

import { CREATE_ALERT_SUCCESS_MESSAGE } from '../constants';
import { enhanceValidationSchemaWithEntityIdValidation } from '../Utils/utils';
import {
CREATE_ALERT_ERROR_FIELD_MAP,
MULTILINE_ERROR_SEPARATOR,
SINGLELINE_ERROR_SEPARATOR,
CREATE_ALERT_SUCCESS_MESSAGE
} from '../constants';
import {
enhanceValidationSchemaWithEntityIdValidation,
handleMultipleError,
} from '../Utils/utils';
import { MetricCriteriaField } from './Criteria/MetricCriteria';
import { TriggerConditions } from './Criteria/TriggerConditions';
import { CloudPulseAlertSeveritySelect } from './GeneralInformation/AlertSeveritySelect';
Expand All @@ -28,6 +36,7 @@ import type {
MetricCriteriaForm,
TriggerConditionForm,
} from './types';
import type { APIError } from '@linode/api-v4';
import type { ObjectSchema } from 'yup';

const triggerConditionInitialValues: TriggerConditionForm = {
Expand Down Expand Up @@ -116,15 +125,19 @@ export const CreateAlertDefinition = () => {
});
alertCreateExit();
} catch (errors) {
for (const error of errors) {
if (error.field) {
setError(error.field, { message: error.reason });
} else {
enqueueSnackbar(`Alert failed: ${error.reason}`, {
variant: 'error',
});
setError('root', { message: error.reason });
}
handleMultipleError<CreateAlertDefinitionForm>({
errorFieldMap: CREATE_ALERT_ERROR_FIELD_MAP,
errors,
multiLineErrorSeparator: MULTILINE_ERROR_SEPARATOR,
setError,
singleLineErrorSeparator: SINGLELINE_ERROR_SEPARATOR,
});

const rootError = errors.find((error: APIError) => !error.field);
if (rootError) {
enqueueSnackbar(`Creating alert failed: ${rootError.reason}`, {
variant: 'error',
});
}
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { Button, Stack, Typography } from '@linode/ui';
import * as React from 'react';
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
import {
Controller,
useFieldArray,
useFormContext,
useWatch,
} from 'react-hook-form';

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

import { MULTILINE_ERROR_SEPARATOR } from '../../constants';
import { AlertListNoticeMessages } from '../../Utils/AlertListNoticeMessages';
import { convertToSeconds } from '../utilities';
import { Metric } from './Metric';

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

return (
<Stack mt={3} spacing={2}>
<Typography variant="h2">3. Criteria</Typography>
<Stack spacing={2}>
{fields !== null &&
fields.length !== 0 &&
fields.map((field, index) => {
return (
<Metric
data={metricDefinitions ? metricDefinitions.data : []}
isMetricDefinitionError={isMetricDefinitionError}
isMetricDefinitionLoading={isMetricDefinitionLoading}
key={field.id}
name={`rule_criteria.rules.${index}`}
onMetricDelete={() => remove(index)}
showDeleteIcon={fields.length > 1}
<Controller
render={({ fieldState, formState }) => (
<Stack mt={3} spacing={2}>
<Typography variant="h2">3. Criteria</Typography>
{formState.isSubmitted &&
fieldState.error &&
fieldState.error.message?.length && (
<AlertListNoticeMessages
errorMessage={fieldState.error.message}
separator={MULTILINE_ERROR_SEPARATOR}
variant="error"
/>
);
})}
</Stack>
<Button
onClick={() =>
append({
aggregate_function: null,
dimension_filters: [],
metric: null,
operator: null,
threshold: 0,
})
}
sx={{
width: '130px', // added a nice width for the button
}}
buttonType="outlined"
disabled={metricCriteriaWatcher.length === 5}
size="medium"
tooltipText="You can add up to 5 metrics."
>
Add metric
</Button>
</Stack>
)}
<Stack spacing={2}>
{fields !== null &&
fields.length !== 0 &&
fields.map((field, index) => {
return (
<Metric
data={metricDefinitions ? metricDefinitions.data : []}
isMetricDefinitionError={isMetricDefinitionError}
isMetricDefinitionLoading={isMetricDefinitionLoading}
key={field.id}
name={`rule_criteria.rules.${index}`}
onMetricDelete={() => remove(index)}
showDeleteIcon={fields.length > 1}
/>
);
})}
</Stack>
<Button
onClick={() =>
append({
aggregate_function: null,
dimension_filters: [],
metric: null,
operator: null,
threshold: 0,
})
}
sx={{
width: '130px',
}}
buttonType="outlined"
disabled={metricCriteriaWatcher.length === 5}
size="medium"
tooltipText="You can add up to 5 metrics."
>
Add metric
</Button>
</Stack>
)}
control={control}
name={name}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Box, Button, Notice, Stack, Typography } from '@linode/ui';
import { Box, Button, Stack, Typography } from '@linode/ui';
import { capitalize } from '@linode/utilities';
import React from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';

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

import { channelTypeOptions } from '../../constants';
import { MULTILINE_ERROR_SEPARATOR, channelTypeOptions } from '../../constants';
import { AlertListNoticeMessages } from '../../Utils/AlertListNoticeMessages';
import { getAlertBoxStyles } from '../../Utils/utils';
import { ClearIconButton } from '../Criteria/ClearIconButton';
import { AddNotificationChannelDrawer } from './AddNotificationChannelDrawer';
Expand Down Expand Up @@ -140,32 +141,38 @@ export const AddChannelListing = (props: AddChannelListingProps) => {
return (
<Controller
render={({ fieldState, formState }) => (
<>
<Typography marginBottom={1} marginTop={3} variant="h2">
4. Notification Channels
</Typography>
{(formState.isSubmitted || fieldState.isTouched) && fieldState.error && (
<Notice spacingBottom={0} spacingTop={12} variant="error">
{fieldState.error.message}
</Notice>
)}
<Stack spacing={1}>
{selectedNotifications.length > 0 &&
selectedNotifications.map((notification, id) => (
<Stack mt={3} spacing={2}>
<Typography variant="h2">4. Notification Channels</Typography>
{(formState.isSubmitted || fieldState.isTouched) &&
fieldState.error &&
fieldState.error.message?.length && (
<AlertListNoticeMessages
errorMessage={fieldState.error.message}
separator={MULTILINE_ERROR_SEPARATOR}
variant="error"
/>
)}
{selectedNotifications.length > 0 && (
<Stack spacing={2}>
{selectedNotifications.map((notification, id) => (
<NotificationChannelCard
id={id}
key={id}
notification={notification}
/>
))}
</Stack>
</Stack>
)}
<Button
sx={{
width:
notificationChannelWatcher.length === 5 ? '215px' : '190px',
}}
buttonType="outlined"
data-qa-buttons="true"
disabled={notificationChannelWatcher.length === 5}
onClick={handleOpenDrawer}
size="medium"
sx={(theme) => ({ marginTop: theme.spacing(2) })}
tooltipText="You can add up to 5 notification channels."
>
Add notification channel
Expand All @@ -179,7 +186,7 @@ export const AddChannelListing = (props: AddChannelListingProps) => {
open={openAddNotification}
templateData={notifications ?? []}
/>
</>
</Stack>
)}
control={control}
name={name}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import { Breadcrumb } from 'src/components/Breadcrumb/Breadcrumb';
import { useFlags } from 'src/hooks/useFlags';
import { useEditAlertDefinition } from 'src/queries/cloudpulse/alerts';

import { UPDATE_ALERT_SUCCESS_MESSAGE } from '../constants';
import {
EDIT_ALERT_ERROR_FIELD_MAP,
MULTILINE_ERROR_SEPARATOR,
SINGLELINE_ERROR_SEPARATOR,
UPDATE_ALERT_SUCCESS_MESSAGE
} from '../constants';

import { MetricCriteriaField } from '../CreateAlert/Criteria/MetricCriteria';
import { TriggerConditions } from '../CreateAlert/Criteria/TriggerConditions';
import { CloudPulseAlertSeveritySelect } from '../CreateAlert/GeneralInformation/AlertSeveritySelect';
Expand All @@ -21,10 +27,12 @@ import { CloudPulseModifyAlertResources } from '../CreateAlert/Resources/CloudPu
import {
convertAlertDefinitionValues,
enhanceValidationSchemaWithEntityIdValidation,
handleMultipleError,
} from '../Utils/utils';
import { EditAlertDefinitionFormSchema } from './schemas';

import type {
APIError,
Alert,
AlertServiceType,
EditAlertDefinitionPayload,
Expand Down Expand Up @@ -79,15 +87,19 @@ export const EditAlertDefinition = (props: EditAlertProps) => {
});
history.push(definitionLanding);
} catch (errors) {
for (const error of errors) {
if (error.field) {
setError(error.field, { message: error.reason });
} else {
enqueueSnackbar(`Alert update failed: ${error.reason}`, {
variant: 'error',
});
setError('root', { message: error.reason });
}
handleMultipleError<EditAlertDefinitionPayload>({
errorFieldMap: EDIT_ALERT_ERROR_FIELD_MAP,
errors,
multiLineErrorSeparator: MULTILINE_ERROR_SEPARATOR,
setError,
singleLineErrorSeparator: SINGLELINE_ERROR_SEPARATOR,
});

const rootError = errors.find((error: APIError) => !error.field);
if (rootError) {
enqueueSnackbar(`Editing alert failed: ${rootError.reason}`, {
variant: 'error',
});
}
}
});
Expand Down
Loading