Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Upcoming Features
---

New type `NotificationChannelAlerts`, request `getAlertsByNotificationChannelId` to fetch alerts associated to a notification channel ([#13294](https://github.com/linode/manager/pull/13294))
15 changes: 15 additions & 0 deletions packages/api-v4/src/cloudpulse/alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
EditAlertDefinitionPayload,
EditNotificationChannelPayload,
NotificationChannel,
NotificationChannelAlerts,
} from './types';

export const createAlertDefinition = (
Expand Down Expand Up @@ -180,3 +181,17 @@ export const deleteNotificationChannel = (channelId: number) =>
),
setMethod('DELETE'),
);

export const getAlertsByNotificationChannelId = (
channelId: number,
params?: Params,
filters?: Filter,
) =>
Request<ResourcePage<NotificationChannelAlerts>>(
setURL(
`${API_ROOT}/monitor/alert-channels/${encodeURIComponent(channelId)}/alerts`,
),
setMethod('GET'),
setParams(params),
setXFilter(filters),
);
8 changes: 8 additions & 0 deletions packages/api-v4/src/cloudpulse/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,3 +496,11 @@ export interface DeleteChannelPayload {
*/
channelId: number;
}

export interface NotificationChannelAlerts {
id: number;
label: string;
service_type: CloudPulseServiceType;
type: 'alerts-definitions';
url: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Associated Alerts Table to ACLP-Alerting Notification Channel Details ([#13294](https://github.com/linode/manager/pull/13294))
14 changes: 13 additions & 1 deletion packages/manager/src/factories/cloudpulse/channels.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Factory } from '@linode/utilities';

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

export const notificationChannelFactory =
Factory.Sync.makeFactory<NotificationChannel>({
Expand All @@ -26,3 +29,12 @@ export const notificationChannelFactory =
updated: new Date().toISOString(),
updated_by: 'user1',
});

export const notificationChannelAlertsFactory =
Factory.Sync.makeFactory<NotificationChannelAlerts>({
type: 'alerts-definitions',
id: Factory.each((i) => i),
service_type: 'linode',
label: Factory.each((id) => `Alert-${id}`),
url: Factory.each((i) => `monitor/alert-definitions/${i}`),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { screen } from '@testing-library/react';
import React from 'react';

import { notificationChannelAlertsFactory } from 'src/factories/cloudpulse/channels';
import { serviceTypesFactory } from 'src/factories/cloudpulse/services';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { NotificationChannelAlerts } from './NotificationChannelAlerts';

const queryMocks = vi.hoisted(() => ({
useAllAlertsByNotificationChannelIdQuery: vi.fn(),
useCloudPulseServiceTypes: vi.fn(),
}));

const hookMocks = vi.hoisted(() => ({
useOrderV2: vi.fn(),
}));

vi.mock('src/queries/cloudpulse/alerts', () => ({
useAllAlertsByNotificationChannelIdQuery:
queryMocks.useAllAlertsByNotificationChannelIdQuery,
}));

vi.mock('src/queries/cloudpulse/services', () => ({
useCloudPulseServiceTypes: queryMocks.useCloudPulseServiceTypes,
}));

vi.mock('src/hooks/useOrderV2', () => ({
useOrderV2: hookMocks.useOrderV2,
}));

describe('NotificationChannelAlerts', () => {
const mockServiceTypes = serviceTypesFactory.buildList(3);
const associatedAlertsText = 'Associated Alerts';
const alertNameText = 'Alert Name';
const serviceTypeText = 'Service';

beforeEach(() => {
queryMocks.useCloudPulseServiceTypes.mockReturnValue({
data: {
data: mockServiceTypes,
},
isFetching: false,
});

hookMocks.useOrderV2.mockReturnValue({
handleOrderChange: vi.fn(),
order: 'asc',
orderBy: 'label',
sortedData: [],
});
});

afterEach(() => {
vi.clearAllMocks();
});

it('should render loading state while fetching alerts', () => {
queryMocks.useAllAlertsByNotificationChannelIdQuery.mockReturnValue({
data: null,
isError: false,
isLoading: true,
});

renderWithTheme(<NotificationChannelAlerts channelId={1} />);

expect(screen.getByText(associatedAlertsText)).toBeVisible();
screen.getByTestId('circle-progress');
});

it('should render loading state while fetching service types', () => {
queryMocks.useAllAlertsByNotificationChannelIdQuery.mockReturnValue({
data: [],
isError: false,
isLoading: false,
});

queryMocks.useCloudPulseServiceTypes.mockReturnValue({
data: {
data: mockServiceTypes,
},
isFetching: true,
});

renderWithTheme(<NotificationChannelAlerts channelId={1} />);

expect(screen.getByText(associatedAlertsText)).toBeVisible();
screen.getByTestId('circle-progress');
});

it('should render error state when alerts query fails', () => {
queryMocks.useAllAlertsByNotificationChannelIdQuery.mockReturnValue({
data: null,
isError: true,
isLoading: false,
});

renderWithTheme(<NotificationChannelAlerts channelId={1} />);

expect(screen.getByText(associatedAlertsText)).toBeVisible();
expect(
screen.getByText('Unable to load alerts for this channel.')
).toBeVisible();
});

it('should render notice when no alerts are associated', () => {
queryMocks.useAllAlertsByNotificationChannelIdQuery.mockReturnValue({
data: [],
isError: false,
isLoading: false,
});

renderWithTheme(<NotificationChannelAlerts channelId={1} />);

expect(screen.getByText(associatedAlertsText)).toBeVisible();
expect(
screen.getByText(
/No alerts are associated with this notification channel./
)
).toBeVisible();
expect(
screen.getByText(
/Add or assign alerts to start receiving notifications through this channel./
)
).toBeVisible();
});

it('should render table with alerts when service_type is present', async () => {
const alerts = notificationChannelAlertsFactory.buildList(3, {
service_type: 'linode',
});

queryMocks.useAllAlertsByNotificationChannelIdQuery.mockReturnValue({
data: alerts,
isError: false,
isLoading: false,
});

hookMocks.useOrderV2.mockReturnValue({
handleOrderChange: vi.fn(),
order: 'asc',
orderBy: 'label',
sortedData: alerts,
});

renderWithTheme(<NotificationChannelAlerts channelId={1} />);

expect(screen.getByText(associatedAlertsText)).toBeVisible();
expect(screen.getByText(alertNameText)).toBeVisible();
expect(screen.getByText(serviceTypeText)).toBeVisible();

alerts.forEach((alert) => {
expect(screen.getByText(alert.label)).toBeVisible();
});
});

it('should render alerts with multiple service types correctly', () => {
const alerts = [
...notificationChannelAlertsFactory.buildList(2, {
service_type: 'linode',
}),
...notificationChannelAlertsFactory.buildList(2, {
service_type: 'dbaas',
}),
];

queryMocks.useAllAlertsByNotificationChannelIdQuery.mockReturnValue({
data: alerts,
isError: false,
isLoading: false,
});

hookMocks.useOrderV2.mockReturnValue({
handleOrderChange: vi.fn(),
order: 'asc',
orderBy: 'label',
sortedData: alerts,
});

renderWithTheme(<NotificationChannelAlerts channelId={1} />);

expect(screen.getByText(associatedAlertsText)).toBeVisible();
expect(screen.getByText(alertNameText)).toBeVisible();
expect(screen.getByText(serviceTypeText)).toBeVisible();

alerts.forEach((alert) => {
expect(screen.getByText(alert.label)).toBeVisible();
});
});
});
Loading