Skip to content

Commit 7bc85dc

Browse files
Merge branch 'develop' into UIE-9910
2 parents 66a85dd + e8035fa commit 7bc85dc

File tree

16 files changed

+579
-2
lines changed

16 files changed

+579
-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+
CloudPulse-Alerts: Add `CreateNotificationChannelPayload` in types.ts and add request function `createNotificationChannel` in alerts.ts ([#13225](https://github.com/linode/manager/pull/13225))

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
createAlertDefinitionSchema,
3+
createNotificationChannelPayloadSchema,
34
editAlertDefinitionSchema,
45
} from '@linode/validation';
56

@@ -17,6 +18,7 @@ import type {
1718
Alert,
1819
CloudPulseAlertsPayload,
1920
CreateAlertDefinitionPayload,
21+
CreateNotificationChannelPayload,
2022
EditAlertDefinitionPayload,
2123
NotificationChannel,
2224
} from './types';
@@ -139,3 +141,12 @@ export const updateServiceAlerts = (
139141
setMethod('PUT'),
140142
setData(payload),
141143
);
144+
145+
export const createNotificationChannel = (
146+
data: CreateNotificationChannelPayload,
147+
) =>
148+
Request<NotificationChannel>(
149+
setURL(`${API_ROOT}/monitor/alert-channels`),
150+
setMethod('POST'),
151+
setData(data, createNotificationChannelPayloadSchema),
152+
);

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,3 +411,22 @@ export interface CloudPulseAlertsPayload {
411411
*/
412412
user_alerts?: number[];
413413
}
414+
415+
export interface CreateNotificationChannelPayload {
416+
/**
417+
* The type of channel to create.
418+
*/
419+
channel_type: ChannelType;
420+
/**
421+
* The details of the channel to create.
422+
*/
423+
details: {
424+
email: {
425+
usernames: string[];
426+
};
427+
};
428+
/**
429+
* The label of the channel to create.
430+
*/
431+
label: string;
432+
}
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+
CloudPulse-Alerts: Add create notification channel page ([#13225](https://github.com/linode/manager/pull/13225))
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { screen, within } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import * as React from 'react';
4+
5+
import { renderWithTheme } from 'src/utilities/testHelpers';
6+
7+
import { CREATE_CHANNEL_SUCCESS_MESSAGE } from '../../constants';
8+
import { CreateNotificationChannel } from './CreateNotificationChannel';
9+
10+
const queryMocks = vi.hoisted(() => ({
11+
mutateAsync: vi.fn().mockResolvedValue({}),
12+
navigate: vi.fn(),
13+
}));
14+
15+
vi.mock('src/queries/cloudpulse/alerts', async () => {
16+
const actual = await vi.importActual('src/queries/cloudpulse/alerts');
17+
return {
18+
...actual,
19+
useCreateNotificationChannel: vi.fn(() => ({
20+
mutateAsync: queryMocks.mutateAsync,
21+
})),
22+
};
23+
});
24+
25+
vi.mock('@tanstack/react-router', async () => {
26+
const actual = await vi.importActual('@tanstack/react-router');
27+
return {
28+
...actual,
29+
useNavigate: vi.fn(() => queryMocks.navigate),
30+
};
31+
});
32+
33+
vi.mock('@linode/queries', async () => {
34+
const actual = await vi.importActual('@linode/queries');
35+
return {
36+
...actual,
37+
useAccountUsersInfiniteQuery: vi.fn(() => ({
38+
data: {
39+
pages: [
40+
{ data: [{ username: 'testuser1' }, { username: 'testuser2' }] },
41+
],
42+
},
43+
fetchNextPage: vi.fn(),
44+
hasNextPage: false,
45+
isFetching: false,
46+
isLoading: false,
47+
})),
48+
};
49+
});
50+
51+
const CHANNEL_TYPE_SELECT_TESTID = 'channel-type-select';
52+
const OPEN_BUTTON_LABEL = 'Open';
53+
const EMAIL_OPTION_LABEL = 'Email';
54+
const NAME_LABEL = 'Name';
55+
const REQUIRED_FIELD_ERROR = 'This field is required.';
56+
const CHANNEL_NAME_VALUE = 'My Email Channel';
57+
58+
describe('CreateNotificationChannel', () => {
59+
beforeEach(() => {
60+
vi.clearAllMocks();
61+
queryMocks.mutateAsync.mockResolvedValue({});
62+
});
63+
64+
it('should render the breadcrumb and form title', () => {
65+
renderWithTheme(<CreateNotificationChannel />);
66+
67+
expect(screen.getByText('Notification Channels')).toBeVisible();
68+
expect(screen.getByText('Channel Settings')).toBeVisible();
69+
});
70+
71+
it('should render the channel type select component', async () => {
72+
renderWithTheme(<CreateNotificationChannel />);
73+
74+
expect(screen.getByTestId(CHANNEL_TYPE_SELECT_TESTID)).toBeVisible();
75+
expect(screen.getByText('Type')).toBeVisible();
76+
expect(screen.getByPlaceholderText('Select a Channel Type')).toBeVisible();
77+
// Verify that the options are rendered
78+
await userEvent.click(screen.getByRole('button', { name: 'Open' }));
79+
expect(
80+
screen.getByRole('option', { name: EMAIL_OPTION_LABEL })
81+
).toBeVisible();
82+
});
83+
84+
it('should render name field when a channel type is selected', async () => {
85+
const user = userEvent.setup();
86+
renderWithTheme(<CreateNotificationChannel />);
87+
88+
// verify the name field is not visible before a channel type is selected
89+
expect(screen.queryByLabelText('Name')).not.toBeInTheDocument();
90+
91+
// Select a channel type
92+
const channelTypeSelect = screen.getByTestId(CHANNEL_TYPE_SELECT_TESTID);
93+
await user.click(
94+
within(channelTypeSelect).getByRole('button', { name: OPEN_BUTTON_LABEL })
95+
);
96+
await user.click(screen.getByRole('option', { name: EMAIL_OPTION_LABEL }));
97+
98+
// Name field should now be visible
99+
expect(screen.getByLabelText(NAME_LABEL)).toBeVisible();
100+
expect(
101+
screen.getByPlaceholderText('Enter a name for the channel')
102+
).toBeVisible();
103+
});
104+
105+
it('should be able to enter a value in the name field', async () => {
106+
const user = userEvent.setup();
107+
renderWithTheme(<CreateNotificationChannel />);
108+
109+
// Select a channel type first
110+
const channelTypeSelect = screen.getByTestId(CHANNEL_TYPE_SELECT_TESTID);
111+
await user.click(
112+
within(channelTypeSelect).getByRole('button', { name: OPEN_BUTTON_LABEL })
113+
);
114+
await user.click(screen.getByRole('option', { name: EMAIL_OPTION_LABEL }));
115+
116+
// Type in the name field
117+
const nameInput = screen.getByLabelText(NAME_LABEL);
118+
await user.type(nameInput, CHANNEL_NAME_VALUE);
119+
120+
const textfieldInput = within(screen.getByTestId('alert-name')).getByTestId(
121+
'textfield-input'
122+
);
123+
expect(textfieldInput).toHaveAttribute('value', CHANNEL_NAME_VALUE);
124+
});
125+
126+
it('should display validation error for channel type field with no selection', async () => {
127+
const user = userEvent.setup();
128+
renderWithTheme(<CreateNotificationChannel />);
129+
130+
// Trigger validation by blurring the channel type field
131+
const channelTypeSelect = screen.getByTestId(CHANNEL_TYPE_SELECT_TESTID);
132+
const combobox = within(channelTypeSelect).getByRole('combobox');
133+
await user.click(combobox);
134+
await user.tab();
135+
136+
await screen.findByText(REQUIRED_FIELD_ERROR);
137+
});
138+
139+
it('should display validation error for name field with no value', async () => {
140+
const user = userEvent.setup();
141+
renderWithTheme(<CreateNotificationChannel />);
142+
143+
// Select a channel type
144+
const channelTypeSelect = screen.getByTestId(CHANNEL_TYPE_SELECT_TESTID);
145+
await user.click(
146+
within(channelTypeSelect).getByRole('button', { name: OPEN_BUTTON_LABEL })
147+
);
148+
await user.click(screen.getByRole('option', { name: EMAIL_OPTION_LABEL }));
149+
150+
// Focus and blur the name field without entering a value
151+
const nameInput = screen.getByLabelText(NAME_LABEL);
152+
await user.click(nameInput);
153+
await user.tab();
154+
155+
await screen.findByText(REQUIRED_FIELD_ERROR);
156+
});
157+
158+
it('should display validation error for recipients field with no value', async () => {
159+
const user = userEvent.setup();
160+
renderWithTheme(<CreateNotificationChannel />);
161+
162+
// Select a channel type
163+
const channelTypeSelect = screen.getByTestId(CHANNEL_TYPE_SELECT_TESTID);
164+
await user.click(
165+
within(channelTypeSelect).getByRole('button', { name: OPEN_BUTTON_LABEL })
166+
);
167+
await user.click(screen.getByRole('option', { name: EMAIL_OPTION_LABEL }));
168+
169+
const recipientsInput = screen.getByLabelText('Recipients');
170+
await user.click(recipientsInput);
171+
await user.tab();
172+
173+
await screen.findByText(REQUIRED_FIELD_ERROR);
174+
});
175+
176+
it('should be able to submit the form with valid values', async () => {
177+
const user = userEvent.setup();
178+
renderWithTheme(<CreateNotificationChannel />);
179+
180+
// Select a channel type
181+
const channelTypeSelect = screen.getByTestId(CHANNEL_TYPE_SELECT_TESTID);
182+
await user.click(
183+
within(channelTypeSelect).getByRole('button', { name: OPEN_BUTTON_LABEL })
184+
);
185+
await user.click(screen.getByRole('option', { name: EMAIL_OPTION_LABEL }));
186+
187+
const nameInput = screen.getByLabelText(NAME_LABEL);
188+
await user.type(nameInput, CHANNEL_NAME_VALUE);
189+
190+
// Select a recipient from the autocomplete dropdown
191+
const recipientsSelect = screen.getByTestId('recipients-select');
192+
await user.click(
193+
within(recipientsSelect).getByRole('button', { name: OPEN_BUTTON_LABEL })
194+
);
195+
await user.click(screen.getByRole('option', { name: 'testuser1' }));
196+
197+
await user.click(screen.getByRole('button', { name: 'Submit' }));
198+
199+
await screen.findByText(CREATE_CHANNEL_SUCCESS_MESSAGE);
200+
201+
expect(queryMocks.mutateAsync).toHaveBeenCalled();
202+
expect(queryMocks.navigate).toHaveBeenCalledWith({
203+
to: '/alerts/notification-channels',
204+
});
205+
});
206+
207+
it('should show error snackbar message when creating notification channel fails', async () => {
208+
queryMocks.mutateAsync.mockRejectedValue([{ reason: 'There is an error' }]);
209+
const user = userEvent.setup();
210+
renderWithTheme(<CreateNotificationChannel />);
211+
212+
// Select a channel type
213+
const channelTypeSelect = screen.getByTestId(CHANNEL_TYPE_SELECT_TESTID);
214+
await user.click(
215+
within(channelTypeSelect).getByRole('button', { name: OPEN_BUTTON_LABEL })
216+
);
217+
await user.click(screen.getByRole('option', { name: EMAIL_OPTION_LABEL }));
218+
219+
const nameInput = screen.getByLabelText(NAME_LABEL);
220+
await user.type(nameInput, CHANNEL_NAME_VALUE);
221+
222+
// Select a recipient from the autocomplete dropdown
223+
const recipientsSelect = screen.getByTestId('recipients-select');
224+
await user.click(
225+
within(recipientsSelect).getByRole('button', { name: OPEN_BUTTON_LABEL })
226+
);
227+
await user.click(screen.getByRole('option', { name: 'testuser1' }));
228+
await user.click(screen.getByRole('button', { name: 'Submit' }));
229+
230+
await screen.findByText('There is an error');
231+
});
232+
});

0 commit comments

Comments
 (0)