Skip to content

Commit

Permalink
upcoming: [DI - 22836] - Added AddNotificationChannel component (#11511)
Browse files Browse the repository at this point in the history
* upcoming: [22836] - Added Notification Channel Drawer component with relevant types,schemas

* upcoming: [DI-22836] - adding changesets

* upcoming: [DI-22836] - Renaming the type from ChannelTypes to ChannelType

* upcoming: [DI-22836] - Fixing failing test

* upcoming :[DI-22836] - Review changes: removing conditional rendering of Drawer component

* upcoming: [DI-22836] - Review changes

* upcoming: [DI-22836] - Removed the To label as per review comment
  • Loading branch information
santoshp210-akamai authored Jan 21, 2025
1 parent a7ea42f commit e51862d
Show file tree
Hide file tree
Showing 11 changed files with 504 additions and 6 deletions.
5 changes: 5 additions & 0 deletions packages/api-v4/.changeset/pr-11511-added-1736761634591.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Added
---

Notification Channel related types to cloudpulse/alerts.ts ([#11511](https://github.com/linode/manager/pull/11511))
76 changes: 76 additions & 0 deletions packages/api-v4/src/cloudpulse/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ export type MetricUnitType =
| 'KB'
| 'MB'
| 'GB';
export type NotificationStatus = 'Enabled' | 'Disabled';
export type ChannelType = 'email' | 'slack' | 'pagerduty' | 'webhook';
export type AlertNotificationType = 'default' | 'custom';
type AlertNotificationEmail = 'email';
type AlertNotificationSlack = 'slack';
type AlertNotificationPagerDuty = 'pagerduty';
type AlertNotificationWebHook = 'webhook';
export interface Dashboard {
id: number;
label: string;
Expand Down Expand Up @@ -218,3 +225,72 @@ export interface Alert {
created: string;
updated: string;
}

interface NotificationChannelAlerts {
id: number;
label: string;
url: string;
type: 'alerts-definitions';
}
interface NotificationChannelBase {
id: number;
label: string;
channel_type: ChannelType;
type: AlertNotificationType;
status: NotificationStatus;
alerts: NotificationChannelAlerts[];
created_by: string;
updated_by: string;
created_at: string;
updated_at: string;
}

interface NotificationChannelEmail extends NotificationChannelBase {
channel_type: AlertNotificationEmail;
content: {
email: {
email_addresses: string[];
subject: string;
message: string;
};
};
}

interface NotificationChannelSlack extends NotificationChannelBase {
channel_type: AlertNotificationSlack;
content: {
slack: {
slack_webhook_url: string;
slack_channel: string;
message: string;
};
};
}

interface NotificationChannelPagerDuty extends NotificationChannelBase {
channel_type: AlertNotificationPagerDuty;
content: {
pagerduty: {
service_api_key: string;
attributes: string[];
description: string;
};
};
}
interface NotificationChannelWebHook extends NotificationChannelBase {
channel_type: AlertNotificationWebHook;
content: {
webhook: {
webhook_url: string;
http_headers: {
header_key: string;
header_value: string;
}[];
};
};
}
export type NotificationChannel =
| NotificationChannelEmail
| NotificationChannelSlack
| NotificationChannelWebHook
| NotificationChannelPagerDuty;
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-11511-added-1736761595455.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Added
---

AddNotificationChannel component with Unit tests with necessary changes for constants, CreateAlertDefinition and other components. ([#11511](https://github.com/linode/manager/pull/11511))
32 changes: 32 additions & 0 deletions packages/manager/src/factories/cloudpulse/channels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Factory from 'src/factories/factoryProxy';

import type { NotificationChannel } from '@linode/api-v4';

export const notificationChannelFactory = Factory.Sync.makeFactory<NotificationChannel>(
{
alerts: [
{
id: Number(Factory.each((i) => i)),
label: String(Factory.each((id) => `Alert-${id}`)),
type: 'alerts-definitions',
url: 'Sample',
},
],
channel_type: 'email',
content: {
email: {
email_addresses: ['[email protected]', '[email protected]'],
message: 'You have a new Alert',
subject: 'Sample Alert',
},
},
created_at: new Date().toISOString(),
created_by: 'user1',
id: Factory.each((i) => i),
label: Factory.each((id) => `Channel-${id}`),
status: 'Enabled',
type: 'custom',
updated_at: new Date().toISOString(),
updated_by: 'user1',
}
);
1 change: 1 addition & 0 deletions packages/manager/src/factories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export * from './vpcs';
export * from './dashboards';
export * from './cloudpulse/services';
export * from './cloudpulse/alerts';
export * from './cloudpulse/channels';

// Convert factory output to our itemsById pattern
export const normalizeEntities = (entities: any[]) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { yupResolver } from '@hookform/resolvers/yup';
import { Paper, TextField, Typography } from '@linode/ui';
import { Box, Button, Paper, TextField, Typography } from '@linode/ui';
import { useSnackbar } from 'notistack';
import * as React from 'react';
import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form';
import { useHistory } from 'react-router-dom';

import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { Breadcrumb } from 'src/components/Breadcrumb/Breadcrumb';
import { Drawer } from 'src/components/Drawer';
import { notificationChannelFactory } from 'src/factories';
import { useCreateAlertDefinition } from 'src/queries/cloudpulse/alerts';

import { MetricCriteriaField } from './Criteria/MetricCriteria';
Expand All @@ -16,6 +18,7 @@ import { EngineOption } from './GeneralInformation/EngineOption';
import { CloudPulseRegionSelect } from './GeneralInformation/RegionSelect';
import { CloudPulseMultiResourceSelect } from './GeneralInformation/ResourceMultiSelect';
import { CloudPulseServiceSelect } from './GeneralInformation/ServiceTypeSelect';
import { AddNotificationChannel } from './NotificationChannels/AddNotificationChannel';
import { CreateAlertDefinitionFormSchema } from './schemas';
import { filterFormValues } from './utilities';

Expand Down Expand Up @@ -78,18 +81,34 @@ export const CreateAlertDefinition = () => {
),
});

const { control, formState, getValues, handleSubmit, setError } = formMethods;
const {
control,
formState,
getValues,
handleSubmit,
setError,
setValue,
} = formMethods;
const { enqueueSnackbar } = useSnackbar();
const { mutateAsync: createAlert } = useCreateAlertDefinition(
getValues('serviceType')!
);

/**
* The maxScrapeInterval variable will be required for the Trigger Conditions part of the Critieria section.
*/
const notificationChannelWatcher = useWatch({ control, name: 'channel_ids' });
const serviceTypeWatcher = useWatch({ control, name: 'serviceType' });

const [openAddNotification, setOpenAddNotification] = React.useState(false);
const [maxScrapeInterval, setMaxScrapeInterval] = React.useState<number>(0);

const serviceTypeWatcher = useWatch({ control, name: 'serviceType' });
const onSubmitAddNotification = (notificationId: number) => {
setValue('channel_ids', [...notificationChannelWatcher, notificationId], {
shouldDirty: false,
shouldTouch: false,
shouldValidate: false,
});
setOpenAddNotification(false);
};

const onSubmit = handleSubmit(async (values) => {
try {
await createAlert(filterFormValues(values));
Expand All @@ -111,6 +130,13 @@ export const CreateAlertDefinition = () => {
}
});

const onExitNotifications = () => {
setOpenAddNotification(false);
};

const onAddNotifications = () => {
setOpenAddNotification(true);
};
return (
<Paper sx={{ paddingLeft: 1, paddingRight: 1, paddingTop: 2 }}>
<Breadcrumb crumbOverrides={overrides} pathname="/Definitions/Create" />
Expand Down Expand Up @@ -172,6 +198,15 @@ export const CreateAlertDefinition = () => {
maxScrapingInterval={maxScrapeInterval}
name="trigger_conditions"
/>
<Box mt={1}>
<Button
buttonType="outlined"
onClick={onAddNotifications}
size="medium"
>
Add notification channel
</Button>
</Box>
<ActionsPanel
primaryButtonProps={{
label: 'Submit',
Expand All @@ -184,6 +219,19 @@ export const CreateAlertDefinition = () => {
}}
sx={{ display: 'flex', justifyContent: 'flex-end' }}
/>
<Drawer
onClose={onExitNotifications}
open={openAddNotification}
title="Add Notification Channel"
>
<AddNotificationChannel
isNotificationChannelsError={false}
isNotificationChannelsLoading={false}
onCancel={onExitNotifications}
onSubmitAddNotification={onSubmitAddNotification}
templateData={notificationChannelFactory.buildList(2)}
/>
</Drawer>
</form>
</FormProvider>
</Paper>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';

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

import { channelTypeOptions } from '../../constants';
import { AddNotificationChannel } from './AddNotificationChannel';

const mockData = [notificationChannelFactory.build()];

describe('AddNotificationChannel component', () => {
const user = userEvent.setup();
it('should render the components', () => {
const { getByLabelText, getByText } = renderWithTheme(
<AddNotificationChannel
isNotificationChannelsError={false}
isNotificationChannelsLoading={false}
onCancel={vi.fn()}
onSubmitAddNotification={vi.fn()}
templateData={mockData}
/>
);
expect(getByText('Channel Settings')).toBeVisible();
expect(getByLabelText('Type')).toBeVisible();
expect(getByLabelText('Channel')).toBeVisible();
});

it('should render the type component with happy path and able to select an option', async () => {
const { findByRole, getByTestId } = renderWithTheme(
<AddNotificationChannel
isNotificationChannelsError={false}
isNotificationChannelsLoading={false}
onCancel={vi.fn()}
onSubmitAddNotification={vi.fn()}
templateData={mockData}
/>
);
const channelTypeContainer = getByTestId('channel-type');
const channelLabel = channelTypeOptions.find(
(option) => option.value === mockData[0].channel_type
)?.label;
user.click(
within(channelTypeContainer).getByRole('button', { name: 'Open' })
);
expect(
await findByRole('option', {
name: channelLabel,
})
).toBeInTheDocument();

await userEvent.click(await findByRole('option', { name: channelLabel }));
expect(within(channelTypeContainer).getByRole('combobox')).toHaveAttribute(
'value',
channelLabel
);
});
it('should render the label component with happy path and able to select an option', async () => {
const { findByRole, getByRole, getByTestId } = renderWithTheme(
<AddNotificationChannel
isNotificationChannelsError={false}
isNotificationChannelsLoading={false}
onCancel={vi.fn()}
onSubmitAddNotification={vi.fn()}
templateData={mockData}
/>
);
// selecting the type as the label field is disabled with type is null
const channelTypeContainer = getByTestId('channel-type');
await user.click(
within(channelTypeContainer).getByRole('button', { name: 'Open' })
);
await user.click(
await findByRole('option', {
name: 'Email',
})
);
expect(within(channelTypeContainer).getByRole('combobox')).toHaveAttribute(
'value',
'Email'
);

const channelLabelContainer = getByTestId('channel-label');
await user.click(
within(channelLabelContainer).getByRole('button', { name: 'Open' })
);
expect(
getByRole('option', {
name: mockData[0].label,
})
).toBeInTheDocument();

await userEvent.click(
await findByRole('option', {
name: mockData[0].label,
})
);
expect(within(channelLabelContainer).getByRole('combobox')).toHaveAttribute(
'value',
mockData[0].label
);
});

it('should render the error messages from the client side validation', async () => {
const { getAllByText, getByRole } = renderWithTheme(
<AddNotificationChannel
isNotificationChannelsError={false}
isNotificationChannelsLoading={false}
onCancel={vi.fn()}
onSubmitAddNotification={vi.fn()}
templateData={mockData}
/>
);
await user.click(getByRole('button', { name: 'Add channel' }));
expect(getAllByText('This field is required.').length).toBe(2);
getAllByText('This field is required.').forEach((element) => {
expect(element).toBeVisible();
});
});
});
Loading

0 comments on commit e51862d

Please sign in to comment.