Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/manager": Upcoming Features
---

Allow system channel selection based on selected service type in `CloudPulse` create and edit `alerts` ([#13219](https://github.com/linode/manager/pull/13219))
1 change: 1 addition & 0 deletions packages/manager/src/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ interface AclpAlerting {
editDisabledStatuses?: AlertStatusType[];
notificationChannels: boolean;
recentActivity: boolean;
systemChannelSupportedServices?: CloudPulseServiceType[]; // linode, dbaas, etc.
}

interface LimitsEvolution {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export const CreateAlertDefinition = () => {
});
resetField('scope', { defaultValue: null });
resetField('entity_type', { defaultValue: 'linode' });
resetField('channel_ids', { defaultValue: [] });
}, [resetField]);

const handleEntityTypeChange = React.useCallback(() => {
Expand Down Expand Up @@ -256,7 +257,10 @@ export const CreateAlertDefinition = () => {
serviceMetadataError={serviceMetadataError}
serviceMetadataLoading={serviceMetadataLoading}
/>
<AddChannelListing name="channel_ids" />
<AddChannelListing
name="channel_ids"
serviceType={serviceTypeWatcher}
/>
<ActionsPanel
primaryButtonProps={{
label: 'Submit',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ describe('Channel Listing component', () => {

const { getByText } =
renderWithThemeAndHookFormContext<CreateAlertDefinitionForm>({
component: <AddChannelListing name="channel_ids" />,
component: (
<AddChannelListing name="channel_ids" serviceType={'linode'} />
),
useFormOptions: {
defaultValues: {
channel_ids: [mockNotificationData[0].id],
Expand All @@ -59,10 +61,30 @@ describe('Channel Listing component', () => {
expect(getByText(emailAddresses[1])).toBeInTheDocument();
});

it('should disable the add notification button when service type is null', () => {
const { getByText, getByRole } =
renderWithThemeAndHookFormContext<CreateAlertDefinitionForm>({
component: <AddChannelListing name="channel_ids" serviceType={null} />,
useFormOptions: {
defaultValues: {
channel_ids: [],
},
},
});
expect(getByText('4. Notification Channels')).toBeVisible();
const addButton = getByRole('button', {
name: 'Add notification channel',
});

expect(addButton).toBeDisabled();
});

it('should remove the fields', async () => {
const { getByTestId } =
renderWithThemeAndHookFormContext<CreateAlertDefinitionForm>({
component: <AddChannelListing name="channel_ids" />,
component: (
<AddChannelListing name="channel_ids" serviceType={'linode'} />
),
useFormOptions: {
defaultValues: {
channel_ids: [mockNotificationData[0].id],
Expand All @@ -82,7 +104,9 @@ describe('Channel Listing component', () => {
const mockMaxLimit = 5;
const { getByRole, findByText } =
renderWithThemeAndHookFormContext<CreateAlertDefinitionForm>({
component: <AddChannelListing name="channel_ids" />,
component: (
<AddChannelListing name="channel_ids" serviceType={'linode'} />
),
useFormOptions: {
defaultValues: {
channel_ids: Array(mockMaxLimit).fill(mockNotificationData[0].id), // simulate 5 channels
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
import type { FieldPathByValue } from 'react-hook-form';

import { useFlags } from 'src/hooks/useFlags';
import { useAllAlertNotificationChannelsQuery } from 'src/queries/cloudpulse/alerts';

import { channelTypeOptions, MULTILINE_ERROR_SEPARATOR } from '../../constants';
Expand All @@ -14,13 +15,21 @@ import { AddNotificationChannelDrawer } from './AddNotificationChannelDrawer';
import { RenderChannelDetails } from './RenderChannelDetails';

import type { CreateAlertDefinitionForm } from '../types';
import type { NotificationChannel } from '@linode/api-v4';
import type {
CloudPulseServiceType,
NotificationChannel,
} from '@linode/api-v4';

interface AddChannelListingProps {
/**
* FieldPathByValue for the notification channel ids
*/
name: FieldPathByValue<CreateAlertDefinitionForm, number[]>;

/**
* Service type of the CloudPulse alert
*/
serviceType: CloudPulseServiceType | null;
}

interface NotificationChannelsProps {
Expand All @@ -34,9 +43,10 @@ interface NotificationChannelsProps {
notification: NotificationChannel;
}
export const AddChannelListing = (props: AddChannelListingProps) => {
const { name } = props;
const { name, serviceType } = props;
const { control, setValue } = useFormContext<CreateAlertDefinitionForm>();
const [openAddNotification, setOpenAddNotification] = React.useState(false);
const flags = useFlags();

const notificationChannelWatcher = useWatch({
control,
Expand All @@ -49,12 +59,31 @@ export const AddChannelListing = (props: AddChannelListingProps) => {
} = useAllAlertNotificationChannelsQuery();

const notifications = React.useMemo(() => {
return (
notificationData?.filter(
({ id }) => !notificationChannelWatcher.includes(id)
) ?? []
);
}, [notificationChannelWatcher, notificationData]);
if (!notificationData) return [];

return notificationData.filter(({ id, type }) => {
if (notificationChannelWatcher.includes(id)) return false; // id already selected

const systemSupportedTypes =
flags.aclpAlerting?.systemChannelSupportedServices;

if (serviceType && systemSupportedTypes?.includes(serviceType)) {
return true; // show all types of channels if serviceType is supported
}

if (serviceType && systemSupportedTypes) {
return type === 'user'; // only show user-defined alert channels
}

// if no flags, show all types
return true;
});
}, [
flags.aclpAlerting?.systemChannelSupportedServices,
notificationChannelWatcher,
notificationData,
serviceType,
]);

const selectedNotifications = React.useMemo(() => {
return (
Expand Down Expand Up @@ -168,12 +197,14 @@ export const AddChannelListing = (props: AddChannelListingProps) => {
<Button
buttonType="outlined"
data-qa-buttons="true"
disabled={notificationChannelWatcher.length === 5}
disabled={notificationChannelWatcher.length === 5 || !serviceType}
onClick={handleOpenDrawer}
size="medium"
sx={{
width:
notificationChannelWatcher.length === 5 ? '215px' : '190px',
notificationChannelWatcher.length === 5 || !serviceType
? '215px'
: '190px',
}}
tooltipText="You can add up to 5 notification channels."
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe('AddNotificationChannelDrawer component', () => {
);
});
it('should render the label component with happy path and able to select an option', async () => {
const { findByRole, getByRole, getByTestId } = renderWithTheme(
const { findByRole, getByRole, getByTestId, findByText } = renderWithTheme(
<AddNotificationChannelDrawer
handleCloseDrawer={vi.fn()}
isNotificationChannelsError={false}
Expand Down Expand Up @@ -94,6 +94,9 @@ describe('AddNotificationChannelDrawer component', () => {
})
).toBeInTheDocument();

const type = await findByText(mockData[0].type);
expect(type).toBeVisible();

await userEvent.click(
await findByRole('option', {
name: mockData[0].label,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,11 @@ export const AddNotificationChannelDrawer = (
const channelLabelWatcher = useWatch({ control, name: 'label' });
const selectedChannelTypeTemplate =
channelTypeWatcher && templateData
? templateData.filter(
(template) => template.channel_type === channelTypeWatcher
)
? templateData
.filter((template) => template.channel_type === channelTypeWatcher)
.sort((channelA, channelB) =>
channelA.type.localeCompare(channelB.type)
) // sorting needed to group by type in Autocomplete
: null;

const selectedTemplate = selectedChannelTypeTemplate?.find(
Expand Down Expand Up @@ -168,12 +170,15 @@ export const AddNotificationChannelDrawer = (
? 'Error in fetching the data.'
: '')
}
groupBy={({ type }) => type}
key={channelTypeWatcher}
label="Channel"
onBlur={field.onBlur}
onChange={(_, selected: { label: string }, reason) => {
onChange={(_, selected, reason) => {
field.onChange(
reason === 'selectOption' ? selected.label : null
reason === 'selectOption' && selected
? selected.label
: null
);
}}
options={selectedChannelTypeTemplate ?? []}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export const EditAlertDefinition = (props: EditAlertProps) => {
serviceMetadataError={serviceMetadataError}
serviceMetadataLoading={serviceMetadataLoading}
/>
<AddChannelListing name="channel_ids" />
<AddChannelListing name="channel_ids" serviceType={serviceType} />
<ActionsPanel
primaryButtonProps={{
label: 'Submit',
Expand Down
9 changes: 9 additions & 0 deletions packages/manager/src/mocks/serverHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3578,6 +3578,15 @@ export const handlers = [
created_by: 'admin',
})
);
notificationChannels.push(
notificationChannelFactory.build({
label: 'System channel',
updated: '2023-11-05T04:00:00',
updated_by: 'user5',
created_by: 'admin',
type: 'system',
})
);
notificationChannels.push(...notificationChannelFactory.buildList(75));
return HttpResponse.json(makeResourcePage(notificationChannels));
}),
Expand Down