Skip to content

Commit d8b7978

Browse files
Merge pull request #11286 from santoshp210-akamai/feature/create-alert-flow-general-information
upcoming: [DI: 21998] - Added Service Type, Engine Option and Region Select component to Create Alert form
2 parents c42edd0 + 7d562ef commit d8b7978

File tree

23 files changed

+586
-117
lines changed

23 files changed

+586
-117
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": Added
3+
---
4+
5+
service_type as parameter for the Create Alert POST request ([#11286](https://github.com/linode/manager/pull/11286))
Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import { createAlertDefinitionSchema } from '@linode/validation';
22
import Request, { setURL, setMethod, setData } from '../request';
3-
import { Alert, CreateAlertDefinitionPayload } from './types';
3+
import { Alert, AlertServiceType, CreateAlertDefinitionPayload } from './types';
44
import { BETA_API_ROOT as API_ROOT } from 'src/constants';
55

6-
export const createAlertDefinition = (data: CreateAlertDefinitionPayload) =>
6+
export const createAlertDefinition = (
7+
data: CreateAlertDefinitionPayload,
8+
service_type: AlertServiceType
9+
) =>
710
Request<Alert>(
8-
setURL(`${API_ROOT}/monitor/alert-definitions`),
11+
setURL(
12+
`${API_ROOT}/monitor/services/${encodeURIComponent(
13+
service_type!
14+
)}/alert-definitions`
15+
),
916
setMethod('POST'),
1017
setData(data, createAlertDefinitionSchema)
1118
);

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

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1-
export type AlertSeverityType = 0 | 1 | 2 | 3 | null;
2-
type MetricAggregationType = 'avg' | 'sum' | 'min' | 'max' | 'count' | null;
3-
type MetricOperatorType = 'eq' | 'gt' | 'lt' | 'gte' | 'lte' | null;
4-
type DimensionFilterOperatorType =
5-
| 'eq'
6-
| 'neq'
7-
| 'startswith'
8-
| 'endswith'
9-
| null;
10-
type AlertDefinitionType = 'default' | 'custom';
11-
type AlertStatusType = 'enabled' | 'disabled';
1+
export type AlertSeverityType = 0 | 1 | 2 | 3;
2+
export type MetricAggregationType = 'avg' | 'sum' | 'min' | 'max' | 'count';
3+
export type MetricOperatorType = 'eq' | 'gt' | 'lt' | 'gte' | 'lte';
4+
export type AlertServiceType = 'linode' | 'dbaas';
5+
type DimensionFilterOperatorType = 'eq' | 'neq' | 'startswith' | 'endswith';
6+
export type AlertDefinitionType = 'default' | 'custom';
7+
export type AlertStatusType = 'enabled' | 'disabled';
128
export interface Dashboard {
139
id: number;
1410
label: string;
@@ -155,12 +151,6 @@ export interface CreateAlertDefinitionPayload {
155151
triggerCondition: TriggerCondition;
156152
channel_ids: number[];
157153
}
158-
export interface CreateAlertDefinitionForm
159-
extends CreateAlertDefinitionPayload {
160-
region: string;
161-
service_type: string;
162-
engine_type: string;
163-
}
164154
export interface MetricCriteria {
165155
metric: string;
166156
aggregation_type: MetricAggregationType;
@@ -187,7 +177,7 @@ export interface Alert {
187177
status: AlertStatusType;
188178
type: AlertDefinitionType;
189179
severity: AlertSeverityType;
190-
service_type: string;
180+
service_type: AlertServiceType;
191181
resource_ids: string[];
192182
rule_criteria: {
193183
rules: MetricCriteria[];
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Added
3+
---
4+
5+
Service, Engine Option, Region components to the Create Alert form ([#11286](https://github.com/linode/manager/pull/11286))
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Factory from 'src/factories/factoryProxy';
2+
3+
import type { Alert } from '@linode/api-v4';
4+
5+
export const alertFactory = Factory.Sync.makeFactory<Alert>({
6+
channels: [],
7+
created: new Date().toISOString(),
8+
created_by: 'user1',
9+
description: '',
10+
id: Factory.each((i) => i),
11+
label: Factory.each((id) => `Alert-${id}`),
12+
resource_ids: ['0', '1', '2', '3'],
13+
rule_criteria: {
14+
rules: [],
15+
},
16+
service_type: 'linode',
17+
severity: 0,
18+
status: 'enabled',
19+
triggerCondition: {
20+
evaluation_period_seconds: 0,
21+
polling_interval_seconds: 0,
22+
trigger_occurrences: 0,
23+
},
24+
type: 'default',
25+
updated: new Date().toISOString(),
26+
updated_by: 'user1',
27+
});
Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
1-
import { fireEvent, screen, within } from '@testing-library/react';
1+
import { screen, within } from '@testing-library/react';
22
import userEvent from '@testing-library/user-event';
33
import * as React from 'react';
44

55
import { renderWithTheme } from 'src/utilities/testHelpers';
66

77
import { CreateAlertDefinition } from './CreateAlertDefinition';
88
describe('AlertDefinition Create', () => {
9-
it('should render input components', () => {
9+
it('should render input components', async () => {
1010
const { getByLabelText } = renderWithTheme(<CreateAlertDefinition />);
1111

1212
expect(getByLabelText('Name')).toBeVisible();
1313
expect(getByLabelText('Description (optional)')).toBeVisible();
1414
expect(getByLabelText('Severity')).toBeVisible();
1515
});
16-
it('should be able to enter a value in the textbox', () => {
16+
it('should be able to enter a value in the textbox', async () => {
1717
const { getByLabelText } = renderWithTheme(<CreateAlertDefinition />);
1818
const input = getByLabelText('Name');
1919

20-
fireEvent.change(input, { target: { value: 'text' } });
20+
await userEvent.type(input, 'text');
2121
const specificInput = within(screen.getByTestId('alert-name')).getByTestId(
2222
'textfield-input'
2323
);
@@ -30,7 +30,9 @@ describe('AlertDefinition Create', () => {
3030

3131
await userEvent.click(submitButton!);
3232

33-
expect(getByText('Name is required')).toBeVisible();
34-
expect(getByText('Severity is required')).toBeVisible();
33+
expect(getByText('Name is required.')).toBeVisible();
34+
expect(getByText('Severity is required.')).toBeVisible();
35+
expect(getByText('Service is required.')).toBeVisible();
36+
expect(getByText('Region is required.')).toBeVisible();
3537
});
3638
});

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

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { yupResolver } from '@hookform/resolvers/yup';
22
import { Paper, TextField, Typography } from '@linode/ui';
3-
import { createAlertDefinitionSchema } from '@linode/validation';
43
import { useSnackbar } from 'notistack';
54
import * as React from 'react';
65
import { Controller, FormProvider, useForm } from 'react-hook-form';
@@ -11,36 +10,37 @@ import { Breadcrumb } from 'src/components/Breadcrumb/Breadcrumb';
1110
import { useCreateAlertDefinition } from 'src/queries/cloudpulse/alerts';
1211

1312
import { CloudPulseAlertSeveritySelect } from './GeneralInformation/AlertSeveritySelect';
13+
import { EngineOption } from './GeneralInformation/EngineOption';
14+
import { CloudPulseRegionSelect } from './GeneralInformation/RegionSelect';
15+
import { CloudPulseServiceSelect } from './GeneralInformation/ServiceTypeSelect';
16+
import { CreateAlertDefinitionFormSchema } from './schemas';
17+
import { filterFormValues, filterMetricCriteriaFormValues } from './utilities';
1418

15-
import type {
16-
CreateAlertDefinitionForm,
17-
CreateAlertDefinitionPayload,
18-
MetricCriteria,
19-
TriggerCondition,
20-
} from '@linode/api-v4/lib/cloudpulse/types';
19+
import type { CreateAlertDefinitionForm, MetricCriteriaForm } from './types';
20+
import type { TriggerCondition } from '@linode/api-v4/lib/cloudpulse/types';
2121

2222
const triggerConditionInitialValues: TriggerCondition = {
2323
evaluation_period_seconds: 0,
2424
polling_interval_seconds: 0,
2525
trigger_occurrences: 0,
2626
};
27-
const criteriaInitialValues: MetricCriteria[] = [
28-
{
29-
aggregation_type: null,
30-
dimension_filters: [],
31-
metric: '',
32-
operator: null,
33-
value: 0,
34-
},
35-
];
27+
const criteriaInitialValues: MetricCriteriaForm = {
28+
aggregation_type: null,
29+
dimension_filters: [],
30+
metric: '',
31+
operator: null,
32+
value: 0,
33+
};
3634
const initialValues: CreateAlertDefinitionForm = {
3735
channel_ids: [],
38-
engine_type: '',
36+
engine_type: null,
3937
label: '',
4038
region: '',
4139
resource_ids: [],
42-
rule_criteria: { rules: criteriaInitialValues },
43-
service_type: '',
40+
rule_criteria: {
41+
rules: filterMetricCriteriaFormValues(criteriaInitialValues),
42+
},
43+
service_type: null,
4444
severity: null,
4545
triggerCondition: triggerConditionInitialValues,
4646
};
@@ -62,19 +62,29 @@ export const CreateAlertDefinition = () => {
6262
const alertCreateExit = () =>
6363
history.push('/monitor/cloudpulse/alerts/definitions');
6464

65-
const formMethods = useForm<CreateAlertDefinitionPayload>({
65+
const formMethods = useForm<CreateAlertDefinitionForm>({
6666
defaultValues: initialValues,
6767
mode: 'onBlur',
68-
resolver: yupResolver(createAlertDefinitionSchema),
68+
resolver: yupResolver(CreateAlertDefinitionFormSchema),
6969
});
7070

71-
const { control, formState, handleSubmit, setError } = formMethods;
71+
const {
72+
control,
73+
formState,
74+
getValues,
75+
handleSubmit,
76+
setError,
77+
watch,
78+
} = formMethods;
7279
const { enqueueSnackbar } = useSnackbar();
73-
const { mutateAsync: createAlert } = useCreateAlertDefinition();
80+
const { mutateAsync: createAlert } = useCreateAlertDefinition(
81+
getValues('service_type')!
82+
);
7483

84+
const serviceWatcher = watch('service_type');
7585
const onSubmit = handleSubmit(async (values) => {
7686
try {
77-
await createAlert(values);
87+
await createAlert(filterFormValues(values));
7888
enqueueSnackbar('Alert successfully created', {
7989
variant: 'success',
8090
});
@@ -130,6 +140,9 @@ export const CreateAlertDefinition = () => {
130140
control={control}
131141
name="description"
132142
/>
143+
<CloudPulseServiceSelect name="service_type" />
144+
{serviceWatcher === 'dbaas' && <EngineOption name="engine_type" />}
145+
<CloudPulseRegionSelect name="region" />
133146
<CloudPulseAlertSeveritySelect name="severity" />
134147
<ActionsPanel
135148
primaryButtonProps={{

packages/manager/src/features/CloudPulse/Alerts/CreateAlert/GeneralInformation/AlertSeveritySelect.test.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { fireEvent, screen } from '@testing-library/react';
1+
import { screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
23
import * as React from 'react';
34

45
import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers';
@@ -13,20 +14,22 @@ describe('EngineOption component tests', () => {
1314
expect(getByLabelText('Severity')).toBeInTheDocument();
1415
expect(getByTestId('severity')).toBeInTheDocument();
1516
});
16-
it('should render the options happy path', () => {
17+
it('should render the options happy path', async () => {
1718
renderWithThemeAndHookFormContext({
1819
component: <CloudPulseAlertSeveritySelect name="severity" />,
1920
});
20-
fireEvent.click(screen.getByRole('button', { name: 'Open' }));
21-
expect(screen.getByRole('option', { name: 'Info' }));
21+
userEvent.click(screen.getByRole('button', { name: 'Open' }));
22+
expect(await screen.findByRole('option', { name: 'Info' }));
2223
expect(screen.getByRole('option', { name: 'Low' }));
2324
});
24-
it('should be able to select an option', () => {
25+
it('should be able to select an option', async () => {
2526
renderWithThemeAndHookFormContext({
2627
component: <CloudPulseAlertSeveritySelect name="severity" />,
2728
});
28-
fireEvent.click(screen.getByRole('button', { name: 'Open' }));
29-
fireEvent.click(screen.getByRole('option', { name: 'Medium' }));
29+
userEvent.click(screen.getByRole('button', { name: 'Open' }));
30+
await userEvent.click(
31+
await screen.findByRole('option', { name: 'Medium' })
32+
);
3033
expect(screen.getByRole('combobox')).toHaveAttribute('value', 'Medium');
3134
});
3235
it('should render the tooltip text', () => {
@@ -35,12 +38,12 @@ describe('EngineOption component tests', () => {
3538
});
3639

3740
const severityContainer = container.getByTestId('severity');
38-
fireEvent.click(severityContainer);
41+
userEvent.click(severityContainer);
3942

4043
expect(
4144
screen.getByRole('button', {
4245
name:
43-
'Define a severity level associated with the alert to help you prioritize and manage alerts in the Recent activity tab',
46+
'Define a severity level associated with the alert to help you prioritize and manage alerts in the Recent activity tab.',
4447
})
4548
).toBeVisible();
4649
});

packages/manager/src/features/CloudPulse/Alerts/CreateAlert/GeneralInformation/AlertSeveritySelect.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,14 @@ import { Autocomplete } from 'src/components/Autocomplete/Autocomplete';
55

66
import { alertSeverityOptions } from '../../constants';
77

8-
import type {
9-
AlertSeverityType,
10-
CreateAlertDefinitionForm,
11-
} from '@linode/api-v4';
8+
import type { CreateAlertDefinitionForm } from '../types';
9+
import type { AlertSeverityType } from '@linode/api-v4';
1210
import type { FieldPathByValue } from 'react-hook-form';
1311
export interface CloudPulseAlertSeveritySelectProps {
1412
/**
1513
* name used for the component in the form
1614
*/
17-
name: FieldPathByValue<CreateAlertDefinitionForm, AlertSeverityType>;
15+
name: FieldPathByValue<CreateAlertDefinitionForm, AlertSeverityType | null>;
1816
}
1917

2018
export const CloudPulseAlertSeveritySelect = (
@@ -41,7 +39,7 @@ export const CloudPulseAlertSeveritySelect = (
4139
}}
4240
textFieldProps={{
4341
labelTooltipText:
44-
'Define a severity level associated with the alert to help you prioritize and manage alerts in the Recent activity tab',
42+
'Define a severity level associated with the alert to help you prioritize and manage alerts in the Recent activity tab.',
4543
}}
4644
value={
4745
field.value !== null
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import * as React from 'react';
4+
5+
import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers';
6+
7+
import { EngineOption } from './EngineOption';
8+
9+
describe('EngineOption component tests', () => {
10+
it('should render the component when resource type is dbaas', () => {
11+
const { getByLabelText, getByTestId } = renderWithThemeAndHookFormContext({
12+
component: <EngineOption name={'engine_type'} />,
13+
});
14+
expect(getByLabelText('Engine Option')).toBeInTheDocument();
15+
expect(getByTestId('engine-option')).toBeInTheDocument();
16+
});
17+
it('should render the options happy path', async () => {
18+
const user = userEvent.setup();
19+
renderWithThemeAndHookFormContext({
20+
component: <EngineOption name={'engine_type'} />,
21+
});
22+
user.click(screen.getByRole('button', { name: 'Open' }));
23+
expect(await screen.findByRole('option', { name: 'MySQL' }));
24+
expect(screen.getByRole('option', { name: 'PostgreSQL' }));
25+
});
26+
it('should be able to select an option', async () => {
27+
const user = userEvent.setup();
28+
renderWithThemeAndHookFormContext({
29+
component: <EngineOption name={'engine_type'} />,
30+
});
31+
user.click(screen.getByRole('button', { name: 'Open' }));
32+
await user.click(await screen.findByRole('option', { name: 'MySQL' }));
33+
expect(screen.getByRole('combobox')).toHaveAttribute('value', 'MySQL');
34+
});
35+
});

0 commit comments

Comments
 (0)