From 627910e2d19d854489424d706563151d74052ec2 Mon Sep 17 00:00:00 2001 From: ankitaakamai Date: Tue, 9 Dec 2025 18:33:26 +0530 Subject: [PATCH 01/16] upcoming: [DI-28503] - Add initial changes for create notification channel page --- .../CreateNotificationChannel.test.tsx | 130 ++++++++++++++++++ .../CreateNotificationChannel.tsx | 80 +++++++++++ ...PulseCreateNotificationChannelLazyRoute.ts | 9 ++ .../CreateChannel/schemas.ts | 16 +++ .../CreateChannel/types.ts | 6 + .../NotificationChannelListing.tsx | 29 +++- packages/manager/src/routes/alerts/index.ts | 10 ++ 7 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx create mode 100644 packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx create mode 100644 packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/cloudPulseCreateNotificationChannelLazyRoute.ts create mode 100644 packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts create mode 100644 packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/types.ts diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx new file mode 100644 index 00000000000..e6f8a1f547a --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx @@ -0,0 +1,130 @@ +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 { CreateNotificationChannel } from './CreateNotificationChannel'; + +const queryMocks = vi.hoisted(() => ({ + useCreateNotificationChannel: vi.fn().mockReturnValue({ + mutateAsync: vi.fn(), + }), +})); + +vi.mock('src/queries/cloudpulse/alerts', async () => { + const actual = await vi.importActual('src/queries/cloudpulse/alerts'); + return { + ...actual, + useCreateNotificationChannel: queryMocks.useCreateNotificationChannel, + }; +}); + +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(() => { + queryMocks.useCreateNotificationChannel.mockReturnValue({ + mutateAsync: vi.fn(), + }); + }); + + it('should render the breadcrumb and form title', () => { + renderWithTheme(); + + expect(screen.getByText('Notification Channels')).toBeVisible(); + expect(screen.getByText('Channel Settings')).toBeVisible(); + }); + + it('should render the channel type select component', async() => { + renderWithTheme(); + + 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(); + + // 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(); + + // 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(); + + // 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(); + + // 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); + }); +}); diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx new file mode 100644 index 00000000000..de3562093c2 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx @@ -0,0 +1,80 @@ +import { yupResolver } from '@hookform/resolvers/yup'; +import { TextField, Typography } from '@linode/ui'; +import { Paper } from '@mui/material'; +import React from 'react'; +import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form'; + +import { Breadcrumb } from 'src/components/Breadcrumb/Breadcrumb'; + +import { NotificationChannelTypeSelect } from './NotificationChannelTypeSelect'; +import { createNotificationChannelSchema } from './schemas'; + +import type { CreateNotificationChannelForm } from './types'; +import type { CrumbOverridesProps } from 'src/components/Breadcrumb/Crumbs'; + +const overrides: CrumbOverridesProps[] = [ + { + label: 'Notification Channels', + linkTo: '/alerts/notification-channels', + position: 1, + }, +]; + +const initialValues: CreateNotificationChannelForm = { + type: null, + name: '', +}; + +export const CreateNotificationChannel = () => { + const formMethods = useForm({ + defaultValues: initialValues, + mode: 'onBlur', + resolver: yupResolver(createNotificationChannelSchema), + }); + + const { control, resetField } = formMethods; + + const channelTypeWatcher = useWatch({ control, name: 'type' }); + + // reset the name field when the channel type changes + const handleChannelTypeChange = React.useCallback(() => { + resetField('name', { defaultValue: '' }); + }, [resetField]); + + return ( + + + +
+ + Channel Settings + + + {channelTypeWatcher && ( + ( + + )} + /> + )} + +
+
+ ); +}; diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/cloudPulseCreateNotificationChannelLazyRoute.ts b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/cloudPulseCreateNotificationChannelLazyRoute.ts new file mode 100644 index 00000000000..acd8099c759 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/cloudPulseCreateNotificationChannelLazyRoute.ts @@ -0,0 +1,9 @@ +import { createLazyRoute } from '@tanstack/react-router'; + +import { CreateNotificationChannel } from './CreateNotificationChannel'; + +export const cloudPulseCreateNotificationChannelLazyRoute = createLazyRoute( + '/alerts/notification-channels/create' +)({ + component: CreateNotificationChannel, +}); diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts new file mode 100644 index 00000000000..e8613f8d497 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts @@ -0,0 +1,16 @@ +import { mixed, object, string } from 'yup'; + +import type { ChannelType } from '@linode/api-v4'; + +const fieldErrorMessage = 'This field is required.'; + +export const createNotificationChannelSchema = object({ + name: string() + .required(fieldErrorMessage) + .nullable() + .test('nonNull', fieldErrorMessage, (value) => value !== null), + type: mixed() + .required(fieldErrorMessage) + .nullable() + .test('nonNull', fieldErrorMessage, (value) => value !== null), +}); diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/types.ts b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/types.ts new file mode 100644 index 00000000000..c8ebcc515b6 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/types.ts @@ -0,0 +1,6 @@ +import type { ChannelType } from '@linode/api-v4'; + +export interface CreateNotificationChannelForm { + name: null | string; + type: ChannelType | null; +} diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/NotificationsChannelsListing/NotificationChannelListing.tsx b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/NotificationsChannelsListing/NotificationChannelListing.tsx index 4423cadcde4..b47ada6f4c3 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/NotificationsChannelsListing/NotificationChannelListing.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/NotificationsChannelsListing/NotificationChannelListing.tsx @@ -1,4 +1,5 @@ -import { Box, Stack } from '@linode/ui'; +import { Box, Button, Stack } from '@linode/ui'; +import { useNavigate } from '@tanstack/react-router'; import React from 'react'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; @@ -14,6 +15,8 @@ export const NotificationChannelListing = () => { isLoading, } = useAllAlertNotificationChannelsQuery(); + const navigate = useNavigate(); + const [searchText, setSearchText] = React.useState(''); const topRef = React.useRef(null); @@ -33,8 +36,11 @@ export const NotificationChannelListing = () => { return ( @@ -47,6 +53,25 @@ export const NotificationChannelListing = () => { sx={{ width: '270px' }} value={searchText} /> + m.cloudPulseAlertsNotificationChannelDetailLazyRoute) ); +export const cloudPulseNotificationChannelsCreateRoute = createRoute({ + getParentRoute: () => cloudPulseAlertsRoute, + path: 'notification-channels/create', +}).lazy(() => + import( + 'src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/cloudPulseCreateNotificationChannelLazyRoute' + ).then((m) => m.cloudPulseCreateNotificationChannelLazyRoute) +); + export const cloudPulseAlertsRouteTree = cloudPulseAlertsRoute.addChildren([ cloudPulseAlertsIndexRoute, cloudPulseAlertsDefinitionsRoute.addChildren([ @@ -96,5 +105,6 @@ export const cloudPulseAlertsRouteTree = cloudPulseAlertsRoute.addChildren([ cloudPulseAlertsDefinitionsCatchAllRoute, cloudPulseNotificationChannelsRoute.addChildren([ cloudPulseNotificationChannelDetailRoute, + cloudPulseNotificationChannelsCreateRoute, ]), ]); From 1f08e3725f5bf3fdfea961701cba14152d51d351 Mon Sep 17 00:00:00 2001 From: ankitaakamai Date: Thu, 11 Dec 2025 11:35:44 +0530 Subject: [PATCH 02/16] upcoming: [DI-28503] - Update type of name field, update schema and fix linting issue --- .../CreateChannel/CreateNotificationChannel.test.tsx | 2 +- .../Alerts/NotificationChannels/CreateChannel/schemas.ts | 7 ++----- .../Alerts/NotificationChannels/CreateChannel/types.ts | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx index e6f8a1f547a..83485317ee5 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx @@ -41,7 +41,7 @@ describe('CreateNotificationChannel', () => { expect(screen.getByText('Channel Settings')).toBeVisible(); }); - it('should render the channel type select component', async() => { + it('should render the channel type select component', async () => { renderWithTheme(); expect(screen.getByTestId(CHANNEL_TYPE_SELECT_TESTID)).toBeVisible(); diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts index e8613f8d497..c949e6cb92f 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts @@ -1,14 +1,11 @@ -import { mixed, object, string } from 'yup'; +import { array, mixed, object, string } from 'yup'; import type { ChannelType } from '@linode/api-v4'; const fieldErrorMessage = 'This field is required.'; export const createNotificationChannelSchema = object({ - name: string() - .required(fieldErrorMessage) - .nullable() - .test('nonNull', fieldErrorMessage, (value) => value !== null), + name: string().required(fieldErrorMessage), type: mixed() .required(fieldErrorMessage) .nullable() diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/types.ts b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/types.ts index c8ebcc515b6..63cbccbbb4d 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/types.ts +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/types.ts @@ -1,6 +1,6 @@ import type { ChannelType } from '@linode/api-v4'; export interface CreateNotificationChannelForm { - name: null | string; + name: string; type: ChannelType | null; } From 8dddf15b1eaf7bfef8892a083e4b4d1cc1bee2d6 Mon Sep 17 00:00:00 2001 From: ankitaakamai Date: Thu, 11 Dec 2025 11:37:56 +0530 Subject: [PATCH 03/16] upcoming: [DI-28503] - Fix linting issue --- .../Alerts/NotificationChannels/CreateChannel/schemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts index c949e6cb92f..1c57b7f0140 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts @@ -1,4 +1,4 @@ -import { array, mixed, object, string } from 'yup'; +import { mixed, object, string } from 'yup'; import type { ChannelType } from '@linode/api-v4'; From 83fde02cb1b8a002615067e4a7342e694f438dd5 Mon Sep 17 00:00:00 2001 From: ankitaakamai Date: Fri, 12 Dec 2025 17:12:05 +0530 Subject: [PATCH 04/16] upcoming: [DI-28503] - Make notification type select a reusable component, move controller to parent --- .../CreateNotificationChannel.tsx | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx index de3562093c2..ca8e5ea9e52 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx @@ -6,10 +6,12 @@ import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form'; import { Breadcrumb } from 'src/components/Breadcrumb/Breadcrumb'; +import { channelTypeOptions } from '../../constants'; import { NotificationChannelTypeSelect } from './NotificationChannelTypeSelect'; import { createNotificationChannelSchema } from './schemas'; import type { CreateNotificationChannelForm } from './types'; +import type { ChannelType } from '@linode/api-v4'; import type { CrumbOverridesProps } from 'src/components/Breadcrumb/Crumbs'; const overrides: CrumbOverridesProps[] = [ @@ -36,11 +38,6 @@ export const CreateNotificationChannel = () => { const channelTypeWatcher = useWatch({ control, name: 'type' }); - // reset the name field when the channel type changes - const handleChannelTypeChange = React.useCallback(() => { - resetField('name', { defaultValue: '' }); - }, [resetField]); - return ( { Channel Settings - { + // Reset the name field when the channel type changes + const handleChannelTypeChange = (value: ChannelType | null) => { + field.onChange(value); + resetField('name', { defaultValue: '' }); + }; + + return ( + + ); + }} /> {channelTypeWatcher && ( Date: Mon, 15 Dec 2025 14:09:50 +0530 Subject: [PATCH 05/16] upcoming: [DI-28503] - Use recipients filter, handle submit --- packages/api-v4/src/cloudpulse/alerts.ts | 11 +++ packages/api-v4/src/cloudpulse/types.ts | 19 ++++ .../CreateNotificationChannel.test.tsx | 90 +++++++++++++++++-- .../CreateNotificationChannel.tsx | 58 +++++++++++- .../CreateChannel/schemas.ts | 9 +- .../CreateChannel/types.ts | 1 + .../CreateChannel/utilities.ts | 16 ++++ .../features/CloudPulse/Alerts/constants.ts | 6 ++ .../manager/src/queries/cloudpulse/alerts.ts | 27 ++++++ packages/validation/src/cloudpulse.schema.ts | 15 ++++ 10 files changed, 240 insertions(+), 12 deletions(-) create mode 100644 packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/utilities.ts diff --git a/packages/api-v4/src/cloudpulse/alerts.ts b/packages/api-v4/src/cloudpulse/alerts.ts index 4518b92c486..eb36d3fb5e4 100644 --- a/packages/api-v4/src/cloudpulse/alerts.ts +++ b/packages/api-v4/src/cloudpulse/alerts.ts @@ -1,5 +1,6 @@ import { createAlertDefinitionSchema, + createNotificationChannelPayloadSchema, editAlertDefinitionSchema, } from '@linode/validation'; @@ -17,6 +18,7 @@ import type { Alert, CloudPulseAlertsPayload, CreateAlertDefinitionPayload, + CreateNotificationChannelPayload, EditAlertDefinitionPayload, NotificationChannel, } from './types'; @@ -139,3 +141,12 @@ export const updateServiceAlerts = ( setMethod('PUT'), setData(payload), ); + +export const createNotificationChannel = ( + data: CreateNotificationChannelPayload, +) => + Request( + setURL(`${API_ROOT}/monitor/alert-channels`), + setMethod('POST'), + setData(data, createNotificationChannelPayloadSchema), + ); diff --git a/packages/api-v4/src/cloudpulse/types.ts b/packages/api-v4/src/cloudpulse/types.ts index 15fed6ab371..9f714a05b01 100644 --- a/packages/api-v4/src/cloudpulse/types.ts +++ b/packages/api-v4/src/cloudpulse/types.ts @@ -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; +} diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx index 83485317ee5..26c89abf9e0 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx @@ -4,19 +4,47 @@ 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(() => ({ - useCreateNotificationChannel: vi.fn().mockReturnValue({ - mutateAsync: vi.fn(), - }), + 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: queryMocks.useCreateNotificationChannel, + 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, + })), }; }); @@ -29,9 +57,8 @@ const CHANNEL_NAME_VALUE = 'My Email Channel'; describe('CreateNotificationChannel', () => { beforeEach(() => { - queryMocks.useCreateNotificationChannel.mockReturnValue({ - mutateAsync: vi.fn(), - }); + vi.clearAllMocks(); + queryMocks.mutateAsync.mockResolvedValue({}); }); it('should render the breadcrumb and form title', () => { @@ -127,4 +154,53 @@ describe('CreateNotificationChannel', () => { await screen.findByText(REQUIRED_FIELD_ERROR); }); + + it('should display validation error for recipients field with no value', async () => { + const user = userEvent.setup(); + renderWithTheme(); + + // 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(); + + // 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', + }); + }); }); diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx index ca8e5ea9e52..3bc23f93cd9 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx @@ -1,14 +1,23 @@ import { yupResolver } from '@hookform/resolvers/yup'; -import { TextField, Typography } from '@linode/ui'; +import { ActionsPanel, TextField, Typography } from '@linode/ui'; import { Paper } from '@mui/material'; +import { useNavigate } from '@tanstack/react-router'; +import { useSnackbar } from 'notistack'; import React from 'react'; import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form'; import { Breadcrumb } from 'src/components/Breadcrumb/Breadcrumb'; +import { useCreateNotificationChannel } from 'src/queries/cloudpulse/alerts'; -import { channelTypeOptions } from '../../constants'; +import { + channelTypeOptions, + CREATE_CHANNEL_FAILED_MESSAGE, + CREATE_CHANNEL_SUCCESS_MESSAGE, +} from '../../constants'; import { NotificationChannelTypeSelect } from './NotificationChannelTypeSelect'; +import { NotificationRecipients } from './NotificationRecipients'; import { createNotificationChannelSchema } from './schemas'; +import { filterCreateChannelFormValues } from './utilities'; import type { CreateNotificationChannelForm } from './types'; import type { ChannelType } from '@linode/api-v4'; @@ -25,19 +34,44 @@ const overrides: CrumbOverridesProps[] = [ const initialValues: CreateNotificationChannelForm = { type: null, name: '', + recipients: [], }; export const CreateNotificationChannel = () => { + const navigate = useNavigate(); + // Navigate to the notification channels listing page on exit, e.g. on cancel or successful save + const createChannelExit = () => { + navigate({ to: '/alerts/notification-channels' }); + }; + const formMethods = useForm({ defaultValues: initialValues, mode: 'onBlur', resolver: yupResolver(createNotificationChannelSchema), }); - const { control, resetField } = formMethods; + const { control, resetField, handleSubmit } = formMethods; const channelTypeWatcher = useWatch({ control, name: 'type' }); + const { mutateAsync: createChannel } = useCreateNotificationChannel(); + + const { enqueueSnackbar } = useSnackbar(); + + // submit the form and create the notification channel on success and show snackbar message on success or failure + const onSubmit = handleSubmit(async (values) => { + createChannel(filterCreateChannelFormValues(values)) + .then(() => { + enqueueSnackbar(CREATE_CHANNEL_SUCCESS_MESSAGE, { + variant: 'success', + }); + createChannelExit(); + }) + .catch(() => { + enqueueSnackbar(CREATE_CHANNEL_FAILED_MESSAGE, { variant: 'error' }); + }); + }); + return ( { pathname="/NotificationChannels/Create Channel" /> -
+ Channel Settings @@ -57,6 +91,7 @@ export const CreateNotificationChannel = () => { const handleChannelTypeChange = (value: ChannelType | null) => { field.onChange(value); resetField('name', { defaultValue: '' }); + resetField('recipients', { defaultValue: [] }); }; return ( @@ -87,6 +122,21 @@ export const CreateNotificationChannel = () => { )} /> )} + {channelTypeWatcher === 'email' && ( + + )} +
diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts index 1c57b7f0140..60b9a3783d7 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts @@ -1,4 +1,4 @@ -import { mixed, object, string } from 'yup'; +import { array, mixed, object, string } from 'yup'; import type { ChannelType } from '@linode/api-v4'; @@ -10,4 +10,11 @@ export const createNotificationChannelSchema = object({ .required(fieldErrorMessage) .nullable() .test('nonNull', fieldErrorMessage, (value) => value !== null), + recipients: array() + .of(string().defined()) + .required(fieldErrorMessage) + .nullable() + .test('nonNullAndNotEmpty', fieldErrorMessage, (value) => { + return value !== null && Array.isArray(value) && value.length > 0; + }), }); diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/types.ts b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/types.ts index 63cbccbbb4d..ea4ba9ccd97 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/types.ts +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/types.ts @@ -2,5 +2,6 @@ import type { ChannelType } from '@linode/api-v4'; export interface CreateNotificationChannelForm { name: string; + recipients: null | string[]; type: ChannelType | null; } diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/utilities.ts b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/utilities.ts new file mode 100644 index 00000000000..fc59c579ae8 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/utilities.ts @@ -0,0 +1,16 @@ +import type { CreateNotificationChannelForm } from './types'; +import type { CreateNotificationChannelPayload } from '@linode/api-v4'; + +export const filterCreateChannelFormValues = ( + formValues: CreateNotificationChannelForm +): CreateNotificationChannelPayload => { + return { + channel_type: formValues.type ?? 'email', + details: { + email: { + usernames: formValues.recipients ?? [], + }, + }, + label: formValues.name ?? '', + }; +}; diff --git a/packages/manager/src/features/CloudPulse/Alerts/constants.ts b/packages/manager/src/features/CloudPulse/Alerts/constants.ts index 95670a13860..6140b807a45 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/constants.ts +++ b/packages/manager/src/features/CloudPulse/Alerts/constants.ts @@ -301,3 +301,9 @@ export const entityLabelMap = { export const entityTypeTooltipText = 'Select a firewall entity type to filter the list in the Entities section. The metrics and dimensions in the Criteria section will update automatically based on your selection.'; + +export const CREATE_CHANNEL_SUCCESS_MESSAGE = + 'Notification channel created successfully. You can now use it to deliver alert notifications.'; + +export const CREATE_CHANNEL_FAILED_MESSAGE = + 'Failed to create the notification channel. Verify the configuration details and try again.'; diff --git a/packages/manager/src/queries/cloudpulse/alerts.ts b/packages/manager/src/queries/cloudpulse/alerts.ts index c0ea1b86b13..b13f0e2ef04 100644 --- a/packages/manager/src/queries/cloudpulse/alerts.ts +++ b/packages/manager/src/queries/cloudpulse/alerts.ts @@ -1,6 +1,7 @@ import { addEntityToAlert, createAlertDefinition, + createNotificationChannel, deleteAlertDefinition, deleteEntityFromAlert, editAlertDefinition, @@ -21,6 +22,7 @@ import type { Alert, CloudPulseAlertsPayload, CreateAlertDefinitionPayload, + CreateNotificationChannelPayload, DeleteAlertPayload, EditAlertPayloadWithService, EntityAlertUpdatePayload, @@ -258,3 +260,28 @@ export const useServiceAlertsMutation = ( }, }); }; + +export const useCreateNotificationChannel = () => { + const queryClient = useQueryClient(); + return useMutation< + NotificationChannel, + APIError[], + CreateNotificationChannelPayload + >({ + mutationFn: (data) => createNotificationChannel(data), + onSuccess: async (newChannel) => { + const allChannelsKey = + queryFactory.notificationChannels._ctx.all().queryKey; + const oldChannels = + queryClient.getQueryData(allChannelsKey); + + // Use cached alerts list if available to avoid refetching from API. + if (oldChannels) { + queryClient.setQueryData(allChannelsKey, [ + ...oldChannels, + newChannel, + ]); + } + }, + }); +}; diff --git a/packages/validation/src/cloudpulse.schema.ts b/packages/validation/src/cloudpulse.schema.ts index bc09c7b2b8d..69b2dbe268e 100644 --- a/packages/validation/src/cloudpulse.schema.ts +++ b/packages/validation/src/cloudpulse.schema.ts @@ -133,3 +133,18 @@ export const editAlertDefinitionSchema = object({ scope: string().oneOf(['entity', 'region', 'account']).nullable().optional(), regions: array().of(string().defined()).optional(), }); + +export const createNotificationChannelPayloadSchema = object({ + label: string().required(fieldErrorMessage), + channel_type: string() + .oneOf(['email', 'webhook', 'pagerduty', 'slack']) + .required(fieldErrorMessage), + details: object({ + email: object({ + usernames: array() + .of(string()) + .min(1, fieldErrorMessage) + .required(fieldErrorMessage), + }).required(), + }).required(), +}); From d93c9812ac18b5782075f103efddc0a223edbd27 Mon Sep 17 00:00:00 2001 From: ankitaakamai Date: Mon, 15 Dec 2025 16:10:15 +0530 Subject: [PATCH 06/16] upcoming: [DI-28503] - Add UT --- .../CreateNotificationChannel.test.tsx | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx index 26c89abf9e0..21cbe0fe8cb 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx @@ -4,7 +4,10 @@ import * as React from 'react'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import { CREATE_CHANNEL_SUCCESS_MESSAGE } from '../../constants'; +import { + CREATE_CHANNEL_FAILED_MESSAGE, + CREATE_CHANNEL_SUCCESS_MESSAGE, +} from '../../constants'; import { CreateNotificationChannel } from './CreateNotificationChannel'; const queryMocks = vi.hoisted(() => ({ @@ -203,4 +206,32 @@ describe('CreateNotificationChannel', () => { to: '/alerts/notification-channels', }); }); + + it('should show error snackbar message when creating notification channel fails', async () => { + queryMocks.mutateAsync.mockRejectedValue( + new Error('Failed to create notification channel') + ); + const user = userEvent.setup(); + renderWithTheme(); + + // 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_FAILED_MESSAGE); + }); }); From 6217759fd3974ff2f9edfbf20adc9564b7e78bdb Mon Sep 17 00:00:00 2001 From: ankitaakamai Date: Tue, 16 Dec 2025 14:24:40 +0530 Subject: [PATCH 07/16] upcoming: [DI-28503] - Use reusable component, remove redundancy --- .../CreateNotificationChannel.tsx | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx index 3bc23f93cd9..75f6a4820b4 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx @@ -50,7 +50,12 @@ export const CreateNotificationChannel = () => { resolver: yupResolver(createNotificationChannelSchema), }); - const { control, resetField, handleSubmit } = formMethods; + const { + control, + resetField, + handleSubmit, + formState: { isSubmitting }, + } = formMethods; const channelTypeWatcher = useWatch({ control, name: 'type' }); @@ -123,12 +128,23 @@ export const CreateNotificationChannel = () => { /> )} {channelTypeWatcher === 'email' && ( - + ( + + )} + /> )} Date: Tue, 16 Dec 2025 15:08:34 +0530 Subject: [PATCH 08/16] upcoming: [DI-28503] - Update type --- .../Alerts/NotificationChannels/CreateChannel/schemas.ts | 5 +---- .../Alerts/NotificationChannels/CreateChannel/types.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts index 60b9a3783d7..8a2cec032b6 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/schemas.ts @@ -13,8 +13,5 @@ export const createNotificationChannelSchema = object({ recipients: array() .of(string().defined()) .required(fieldErrorMessage) - .nullable() - .test('nonNullAndNotEmpty', fieldErrorMessage, (value) => { - return value !== null && Array.isArray(value) && value.length > 0; - }), + .min(1, fieldErrorMessage), }); diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/types.ts b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/types.ts index ea4ba9ccd97..680f1a919ed 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/types.ts +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/types.ts @@ -2,6 +2,6 @@ import type { ChannelType } from '@linode/api-v4'; export interface CreateNotificationChannelForm { name: string; - recipients: null | string[]; + recipients: string[]; type: ChannelType | null; } From 5f32505725b2717ed86adc59bde788d8c12b308f Mon Sep 17 00:00:00 2001 From: ankitaakamai Date: Thu, 18 Dec 2025 16:36:50 +0530 Subject: [PATCH 09/16] upcoming: [DI-28503] - Add mocks --- packages/manager/src/mocks/serverHandlers.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 90421bd0b4f..3053176c855 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -4440,4 +4440,9 @@ export const handlers = [ makeResourcePage(maintenancePolicyFactory.buildList(2)) ); }), + http.post('*/v4beta/monitor/alert-channels', () => { + return HttpResponse.json( + makeResourcePage(notificationChannelFactory.buildList(3)) + ); + }), ]; From 3831ab70d20a9f65bd78f50864b20ccdc96d7a38 Mon Sep 17 00:00:00 2001 From: ankitaakamai Date: Thu, 18 Dec 2025 19:18:18 +0530 Subject: [PATCH 10/16] upcoming: [DI-28503] - Use reusable component, remove redundancy --- .../CreateNotificationChannel.test.tsx | 6 ++-- .../CreateNotificationChannel.tsx | 28 +++++++++++++------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx index 21cbe0fe8cb..638cd5ff9d4 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx @@ -208,9 +208,7 @@ describe('CreateNotificationChannel', () => { }); it('should show error snackbar message when creating notification channel fails', async () => { - queryMocks.mutateAsync.mockRejectedValue( - new Error('Failed to create notification channel') - ); + queryMocks.mutateAsync.mockRejectedValue([{ reason: 'There is an error' }]); const user = userEvent.setup(); renderWithTheme(); @@ -232,6 +230,6 @@ describe('CreateNotificationChannel', () => { await user.click(screen.getByRole('option', { name: 'testuser1' })); await user.click(screen.getByRole('button', { name: 'Submit' })); - await screen.findByText(CREATE_CHANNEL_FAILED_MESSAGE); + await screen.findByText('There is an error'); }); }); diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx index 75f6a4820b4..d741b0fa820 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx @@ -55,6 +55,7 @@ export const CreateNotificationChannel = () => { resetField, handleSubmit, formState: { isSubmitting }, + setError, } = formMethods; const channelTypeWatcher = useWatch({ control, name: 'type' }); @@ -65,16 +66,25 @@ export const CreateNotificationChannel = () => { // submit the form and create the notification channel on success and show snackbar message on success or failure const onSubmit = handleSubmit(async (values) => { - createChannel(filterCreateChannelFormValues(values)) - .then(() => { - enqueueSnackbar(CREATE_CHANNEL_SUCCESS_MESSAGE, { - variant: 'success', - }); - createChannelExit(); - }) - .catch(() => { - enqueueSnackbar(CREATE_CHANNEL_FAILED_MESSAGE, { variant: 'error' }); + try { + await createChannel(filterCreateChannelFormValues(values)); + enqueueSnackbar(CREATE_CHANNEL_SUCCESS_MESSAGE, { + variant: 'success', }); + createChannelExit(); + } catch (errors) { + for (const error of errors) { + if (error.field) { + setError(error.field, { + message: error.reason ?? CREATE_CHANNEL_FAILED_MESSAGE, + }); + } else { + enqueueSnackbar(error.reason ?? CREATE_CHANNEL_FAILED_MESSAGE, { + variant: 'error', + }); + } + } + } }); return ( From 3392510b2bbb65f61b7724166e971181b07ce8ba Mon Sep 17 00:00:00 2001 From: ankitaakamai Date: Fri, 19 Dec 2025 10:53:45 +0530 Subject: [PATCH 11/16] upcoming: [DI-28503] - Handle errors gracefully for non-arrays --- .../CreateChannel/CreateNotificationChannel.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx index d741b0fa820..14cf5e88175 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx @@ -72,7 +72,10 @@ export const CreateNotificationChannel = () => { variant: 'success', }); createChannelExit(); - } catch (errors) { + } catch (error) { + // Handle both array and non-array errors + const errors = Array.isArray(error) ? error : [error]; + for (const error of errors) { if (error.field) { setError(error.field, { From e78ab803c5c89e4f4f928664ecebc2222af8bf35 Mon Sep 17 00:00:00 2001 From: ankitaakamai Date: Fri, 19 Dec 2025 11:24:56 +0530 Subject: [PATCH 12/16] Revert "upcoming: [DI-28503] - Handle errors gracefully for non-arrays" This reverts commit fb0907e9351122e9179e89542fcab67146142b59. --- .../CreateChannel/CreateNotificationChannel.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx index 14cf5e88175..d741b0fa820 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.tsx @@ -72,10 +72,7 @@ export const CreateNotificationChannel = () => { variant: 'success', }); createChannelExit(); - } catch (error) { - // Handle both array and non-array errors - const errors = Array.isArray(error) ? error : [error]; - + } catch (errors) { for (const error of errors) { if (error.field) { setError(error.field, { From f8f032f05753aa779d94b13fe30f70e2067f3aff Mon Sep 17 00:00:00 2001 From: ankitaakamai Date: Fri, 19 Dec 2025 12:02:34 +0530 Subject: [PATCH 13/16] upcoming: [DI-28503] - linting fix --- .../CreateChannel/CreateNotificationChannel.test.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx index 638cd5ff9d4..2ad9a847ae6 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/CreateNotificationChannel.test.tsx @@ -4,10 +4,7 @@ import * as React from 'react'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import { - CREATE_CHANNEL_FAILED_MESSAGE, - CREATE_CHANNEL_SUCCESS_MESSAGE, -} from '../../constants'; +import { CREATE_CHANNEL_SUCCESS_MESSAGE } from '../../constants'; import { CreateNotificationChannel } from './CreateNotificationChannel'; const queryMocks = vi.hoisted(() => ({ From 8b3c7e8c88c5c5e1bfdfd382bbbcf0a09df929ef Mon Sep 17 00:00:00 2001 From: ankitaakamai Date: Fri, 19 Dec 2025 13:09:19 +0530 Subject: [PATCH 14/16] upcoming: [DI-28503] - Add mocks --- packages/manager/src/mocks/serverHandlers.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 3053176c855..79aa9ae194c 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -4441,8 +4441,6 @@ export const handlers = [ ); }), http.post('*/v4beta/monitor/alert-channels', () => { - return HttpResponse.json( - makeResourcePage(notificationChannelFactory.buildList(3)) - ); + return HttpResponse.json(notificationChannelFactory.build()); }), ]; From ce2b23c798a96cc328948343764cbaebe5b1e991 Mon Sep 17 00:00:00 2001 From: ankitaakamai Date: Wed, 24 Dec 2025 13:45:41 +0530 Subject: [PATCH 15/16] upcoming: [DI-28503] - Add changesets --- .../.changeset/pr-13225-upcoming-features-1766563927589.md | 5 +++++ .../.changeset/pr-13225-upcoming-features-1766563953980.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 packages/api-v4/.changeset/pr-13225-upcoming-features-1766563927589.md create mode 100644 packages/manager/.changeset/pr-13225-upcoming-features-1766563953980.md diff --git a/packages/api-v4/.changeset/pr-13225-upcoming-features-1766563927589.md b/packages/api-v4/.changeset/pr-13225-upcoming-features-1766563927589.md new file mode 100644 index 00000000000..746d1b880ad --- /dev/null +++ b/packages/api-v4/.changeset/pr-13225-upcoming-features-1766563927589.md @@ -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)) diff --git a/packages/manager/.changeset/pr-13225-upcoming-features-1766563953980.md b/packages/manager/.changeset/pr-13225-upcoming-features-1766563953980.md new file mode 100644 index 00000000000..d141c68e899 --- /dev/null +++ b/packages/manager/.changeset/pr-13225-upcoming-features-1766563953980.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +CloudPulse-Alerts: Add create notification channel page ([#13225](https://github.com/linode/manager/pull/13225)) From 8770c92de8b7dc3642ff775e7f1519084f8d0cdb Mon Sep 17 00:00:00 2001 From: ankitaakamai Date: Wed, 24 Dec 2025 15:09:27 +0530 Subject: [PATCH 16/16] upcoming: [DI-28503] - Remove unnecessery fallback --- .../Alerts/NotificationChannels/CreateChannel/utilities.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/utilities.ts b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/utilities.ts index fc59c579ae8..239a148af5d 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/utilities.ts +++ b/packages/manager/src/features/CloudPulse/Alerts/NotificationChannels/CreateChannel/utilities.ts @@ -8,9 +8,9 @@ export const filterCreateChannelFormValues = ( channel_type: formValues.type ?? 'email', details: { email: { - usernames: formValues.recipients ?? [], + usernames: formValues.recipients, }, }, - label: formValues.name ?? '', + label: formValues.name, }; };