Skip to content
Merged
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
627910e
upcoming: [DI-28503] - Add initial changes for create notification ch…
ankita-akamai Dec 9, 2025
1f08e37
upcoming: [DI-28503] - Update type of name field, update schema and f…
ankita-akamai Dec 11, 2025
8dddf15
upcoming: [DI-28503] - Fix linting issue
ankita-akamai Dec 11, 2025
83fde02
upcoming: [DI-28503] - Make notification type select a reusable compo…
ankita-akamai Dec 12, 2025
d0238d2
upcoming: [DI-28503] - Use recipients filter, handle submit
ankita-akamai Dec 15, 2025
d93c981
upcoming: [DI-28503] - Add UT
ankita-akamai Dec 15, 2025
6217759
upcoming: [DI-28503] - Use reusable component, remove redundancy
ankita-akamai Dec 16, 2025
31af668
upcoming: [DI-28503] - Update type
ankita-akamai Dec 16, 2025
5f32505
upcoming: [DI-28503] - Add mocks
ankita-akamai Dec 18, 2025
3831ab7
upcoming: [DI-28503] - Use reusable component, remove redundancy
ankita-akamai Dec 18, 2025
3392510
upcoming: [DI-28503] - Handle errors gracefully for non-arrays
ankita-akamai Dec 19, 2025
e78ab80
Revert "upcoming: [DI-28503] - Handle errors gracefully for non-arrays"
ankita-akamai Dec 19, 2025
f8f032f
upcoming: [DI-28503] - linting fix
ankita-akamai Dec 19, 2025
8b3c7e8
upcoming: [DI-28503] - Add mocks
ankita-akamai Dec 19, 2025
ce2b23c
upcoming: [DI-28503] - Add changesets
ankita-akamai Dec 24, 2025
8770c92
upcoming: [DI-28503] - Remove unnecessery fallback
ankita-akamai Dec 24, 2025
56b5a5f
Merge branch 'develop' into feature/createChannelIntegrationAlerts
venkymano-akamai Dec 24, 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/api-v4": Upcoming Features
---

CloudPulse-Alerts: Add `CreateNotificationChannelPayload` in types.ts and add request function `createNotificationChannel` in alerts.ts ([#13225](https://github.com/linode/manager/pull/13225))
11 changes: 11 additions & 0 deletions packages/api-v4/src/cloudpulse/alerts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
createAlertDefinitionSchema,
createNotificationChannelPayloadSchema,
editAlertDefinitionSchema,
} from '@linode/validation';

Expand All @@ -17,6 +18,7 @@ import type {
Alert,
CloudPulseAlertsPayload,
CreateAlertDefinitionPayload,
CreateNotificationChannelPayload,
EditAlertDefinitionPayload,
NotificationChannel,
} from './types';
Expand Down Expand Up @@ -139,3 +141,12 @@ export const updateServiceAlerts = (
setMethod('PUT'),
setData(payload),
);

export const createNotificationChannel = (
data: CreateNotificationChannelPayload,
) =>
Request<NotificationChannel>(
setURL(`${API_ROOT}/monitor/alert-channels`),
setMethod('POST'),
setData(data, createNotificationChannelPayloadSchema),
);
19 changes: 19 additions & 0 deletions packages/api-v4/src/cloudpulse/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,3 +411,22 @@ export interface CloudPulseAlertsPayload {
*/
user_alerts?: number[];
}

export interface CreateNotificationChannelPayload {
/**
* The type of channel to create.
*/
channel_type: ChannelType;
/**
* The details of the channel to create.
*/
details: {
email: {
usernames: string[];
};
};
/**
* The label of the channel to create.
*/
label: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

CloudPulse-Alerts: Add create notification channel page ([#13225](https://github.com/linode/manager/pull/13225))
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';

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

import { CREATE_CHANNEL_SUCCESS_MESSAGE } from '../../constants';
import { CreateNotificationChannel } from './CreateNotificationChannel';

const queryMocks = vi.hoisted(() => ({
mutateAsync: vi.fn().mockResolvedValue({}),
navigate: vi.fn(),
}));

vi.mock('src/queries/cloudpulse/alerts', async () => {
const actual = await vi.importActual('src/queries/cloudpulse/alerts');
return {
...actual,
useCreateNotificationChannel: vi.fn(() => ({
mutateAsync: queryMocks.mutateAsync,
})),
};
});

vi.mock('@tanstack/react-router', async () => {
const actual = await vi.importActual('@tanstack/react-router');
return {
...actual,
useNavigate: vi.fn(() => queryMocks.navigate),
};
});

vi.mock('@linode/queries', async () => {
const actual = await vi.importActual('@linode/queries');
return {
...actual,
useAccountUsersInfiniteQuery: vi.fn(() => ({
data: {
pages: [
{ data: [{ username: 'testuser1' }, { username: 'testuser2' }] },
],
},
fetchNextPage: vi.fn(),
hasNextPage: false,
isFetching: false,
isLoading: false,
})),
};
});

const CHANNEL_TYPE_SELECT_TESTID = 'channel-type-select';
const OPEN_BUTTON_LABEL = 'Open';
const EMAIL_OPTION_LABEL = 'Email';
const NAME_LABEL = 'Name';
const REQUIRED_FIELD_ERROR = 'This field is required.';
const CHANNEL_NAME_VALUE = 'My Email Channel';

describe('CreateNotificationChannel', () => {
beforeEach(() => {
vi.clearAllMocks();
queryMocks.mutateAsync.mockResolvedValue({});
});

it('should render the breadcrumb and form title', () => {
renderWithTheme(<CreateNotificationChannel />);

expect(screen.getByText('Notification Channels')).toBeVisible();
expect(screen.getByText('Channel Settings')).toBeVisible();
});

it('should render the channel type select component', async () => {
renderWithTheme(<CreateNotificationChannel />);

expect(screen.getByTestId(CHANNEL_TYPE_SELECT_TESTID)).toBeVisible();
expect(screen.getByText('Type')).toBeVisible();
expect(screen.getByPlaceholderText('Select a Channel Type')).toBeVisible();
// Verify that the options are rendered
await userEvent.click(screen.getByRole('button', { name: 'Open' }));
expect(
screen.getByRole('option', { name: EMAIL_OPTION_LABEL })
).toBeVisible();
});

it('should render name field when a channel type is selected', async () => {
const user = userEvent.setup();
renderWithTheme(<CreateNotificationChannel />);

// verify the name field is not visible before a channel type is selected
expect(screen.queryByLabelText('Name')).not.toBeInTheDocument();

// Select a channel type
const channelTypeSelect = screen.getByTestId(CHANNEL_TYPE_SELECT_TESTID);
await user.click(
within(channelTypeSelect).getByRole('button', { name: OPEN_BUTTON_LABEL })
);
await user.click(screen.getByRole('option', { name: EMAIL_OPTION_LABEL }));

// Name field should now be visible
expect(screen.getByLabelText(NAME_LABEL)).toBeVisible();
expect(
screen.getByPlaceholderText('Enter a name for the channel')
).toBeVisible();
});

it('should be able to enter a value in the name field', async () => {
const user = userEvent.setup();
renderWithTheme(<CreateNotificationChannel />);

// Select a channel type first
const channelTypeSelect = screen.getByTestId(CHANNEL_TYPE_SELECT_TESTID);
await user.click(
within(channelTypeSelect).getByRole('button', { name: OPEN_BUTTON_LABEL })
);
await user.click(screen.getByRole('option', { name: EMAIL_OPTION_LABEL }));

// Type in the name field
const nameInput = screen.getByLabelText(NAME_LABEL);
await user.type(nameInput, CHANNEL_NAME_VALUE);

const textfieldInput = within(screen.getByTestId('alert-name')).getByTestId(
'textfield-input'
);
expect(textfieldInput).toHaveAttribute('value', CHANNEL_NAME_VALUE);
});

it('should display validation error for channel type field with no selection', async () => {
const user = userEvent.setup();
renderWithTheme(<CreateNotificationChannel />);

// Trigger validation by blurring the channel type field
const channelTypeSelect = screen.getByTestId(CHANNEL_TYPE_SELECT_TESTID);
const combobox = within(channelTypeSelect).getByRole('combobox');
await user.click(combobox);
await user.tab();

await screen.findByText(REQUIRED_FIELD_ERROR);
});

it('should display validation error for name field with no value', async () => {
const user = userEvent.setup();
renderWithTheme(<CreateNotificationChannel />);

// Select a channel type
const channelTypeSelect = screen.getByTestId(CHANNEL_TYPE_SELECT_TESTID);
await user.click(
within(channelTypeSelect).getByRole('button', { name: OPEN_BUTTON_LABEL })
);
await user.click(screen.getByRole('option', { name: EMAIL_OPTION_LABEL }));

// Focus and blur the name field without entering a value
const nameInput = screen.getByLabelText(NAME_LABEL);
await user.click(nameInput);
await user.tab();

await screen.findByText(REQUIRED_FIELD_ERROR);
});

it('should display validation error for recipients field with no value', async () => {
const user = userEvent.setup();
renderWithTheme(<CreateNotificationChannel />);

// Select a channel type
const channelTypeSelect = screen.getByTestId(CHANNEL_TYPE_SELECT_TESTID);
await user.click(
within(channelTypeSelect).getByRole('button', { name: OPEN_BUTTON_LABEL })
);
await user.click(screen.getByRole('option', { name: EMAIL_OPTION_LABEL }));

const recipientsInput = screen.getByLabelText('Recipients');
await user.click(recipientsInput);
await user.tab();

await screen.findByText(REQUIRED_FIELD_ERROR);
});

it('should be able to submit the form with valid values', async () => {
const user = userEvent.setup();
renderWithTheme(<CreateNotificationChannel />);

// Select a channel type
const channelTypeSelect = screen.getByTestId(CHANNEL_TYPE_SELECT_TESTID);
await user.click(
within(channelTypeSelect).getByRole('button', { name: OPEN_BUTTON_LABEL })
);
await user.click(screen.getByRole('option', { name: EMAIL_OPTION_LABEL }));

const nameInput = screen.getByLabelText(NAME_LABEL);
await user.type(nameInput, CHANNEL_NAME_VALUE);

// Select a recipient from the autocomplete dropdown
const recipientsSelect = screen.getByTestId('recipients-select');
await user.click(
within(recipientsSelect).getByRole('button', { name: OPEN_BUTTON_LABEL })
);
await user.click(screen.getByRole('option', { name: 'testuser1' }));

await user.click(screen.getByRole('button', { name: 'Submit' }));

await screen.findByText(CREATE_CHANNEL_SUCCESS_MESSAGE);

expect(queryMocks.mutateAsync).toHaveBeenCalled();
expect(queryMocks.navigate).toHaveBeenCalledWith({
to: '/alerts/notification-channels',
});
});

it('should show error snackbar message when creating notification channel fails', async () => {
queryMocks.mutateAsync.mockRejectedValue([{ reason: 'There is an error' }]);
const user = userEvent.setup();
renderWithTheme(<CreateNotificationChannel />);

// Select a channel type
const channelTypeSelect = screen.getByTestId(CHANNEL_TYPE_SELECT_TESTID);
await user.click(
within(channelTypeSelect).getByRole('button', { name: OPEN_BUTTON_LABEL })
);
await user.click(screen.getByRole('option', { name: EMAIL_OPTION_LABEL }));

const nameInput = screen.getByLabelText(NAME_LABEL);
await user.type(nameInput, CHANNEL_NAME_VALUE);

// Select a recipient from the autocomplete dropdown
const recipientsSelect = screen.getByTestId('recipients-select');
await user.click(
within(recipientsSelect).getByRole('button', { name: OPEN_BUTTON_LABEL })
);
await user.click(screen.getByRole('option', { name: 'testuser1' }));
await user.click(screen.getByRole('button', { name: 'Submit' }));

await screen.findByText('There is an error');
});
});
Loading