Skip to content

Commit 738f57d

Browse files
upcoming: [DI-28502] - Alerts Notification Channels Listing (#13193)
* upcoming: [DI-28502] - Alerts Notification Channels Listing * add changeset * add api-v4 changeset
1 parent b2ffffc commit 738f57d

File tree

13 files changed

+796
-8
lines changed

13 files changed

+796
-8
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/api-v4": Changed
3+
---
4+
5+
Renamed updated_at, created_at to updated,created in NotificationChannelBase interface ([#13193](https://github.com/linode/manager/pull/13193))

packages/api-v4/src/cloudpulse/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,13 +286,13 @@ interface NotificationChannelAlerts {
286286
interface NotificationChannelBase {
287287
alerts: NotificationChannelAlerts[];
288288
channel_type: ChannelType;
289-
created_at: string;
289+
created: string;
290290
created_by: string;
291291
id: number;
292292
label: string;
293293
status: NotificationStatus;
294294
type: AlertNotificationType;
295-
updated_at: string;
295+
updated: string;
296296
updated_by: string;
297297
}
298298

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Upcoming Features
3+
---
4+
5+
Introduce Listing for ACLP-Alerting Notification channels with ordering, pagination ([#13193](https://github.com/linode/manager/pull/13193))

packages/manager/src/factories/cloudpulse/channels.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ export const notificationChannelFactory =
2020
subject: 'Sample Alert',
2121
},
2222
},
23-
created_at: new Date().toISOString(),
23+
created: new Date().toISOString(),
2424
created_by: 'user1',
2525
id: Factory.each((i) => i),
2626
label: Factory.each((id) => `Channel-${id}`),
2727
status: 'Enabled',
2828
type: 'custom',
29-
updated_at: new Date().toISOString(),
29+
updated: new Date().toISOString(),
3030
updated_by: 'user1',
3131
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { screen, waitFor } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import React from 'react';
4+
5+
import { notificationChannelFactory } from 'src/factories/cloudpulse/channels';
6+
import { formatDate } from 'src/utilities/formatDate';
7+
import { renderWithTheme } from 'src/utilities/testHelpers';
8+
9+
import { NotificationChannelListTable } from './NotificationChannelListTable';
10+
11+
const mockScrollToElement = vi.fn();
12+
13+
const ALERT_TYPE = 'alerts-definitions';
14+
15+
describe('NotificationChannelListTable', () => {
16+
it('should render the notification channel table headers', () => {
17+
renderWithTheme(
18+
<NotificationChannelListTable
19+
isLoading={false}
20+
notificationChannels={[]}
21+
scrollToElement={mockScrollToElement}
22+
/>
23+
);
24+
25+
expect(screen.getByText('Channel Name')).toBeVisible();
26+
expect(screen.getByText('Alerts')).toBeVisible();
27+
expect(screen.getByText('Channel Type')).toBeVisible();
28+
expect(screen.getByText('Created By')).toBeVisible();
29+
expect(screen.getByText('Last Modified')).toBeVisible();
30+
expect(screen.getByText('Last Modified By')).toBeVisible();
31+
});
32+
33+
it('should render the error message when error is provided', () => {
34+
renderWithTheme(
35+
<NotificationChannelListTable
36+
error={[{ reason: 'Error in fetching the notification channels' }]}
37+
isLoading={false}
38+
notificationChannels={[]}
39+
scrollToElement={mockScrollToElement}
40+
/>
41+
);
42+
43+
expect(
44+
screen.getByText('Error in fetching the notification channels')
45+
).toBeVisible();
46+
});
47+
48+
it('should render notification channel rows', () => {
49+
const updated = new Date().toISOString();
50+
const channel = notificationChannelFactory.build({
51+
channel_type: 'email',
52+
created_by: 'user1',
53+
label: 'Test Channel',
54+
updated_by: 'user2',
55+
updated,
56+
});
57+
58+
renderWithTheme(
59+
<NotificationChannelListTable
60+
isLoading={false}
61+
notificationChannels={[channel]}
62+
scrollToElement={mockScrollToElement}
63+
/>
64+
);
65+
66+
expect(screen.getByText('Test Channel')).toBeVisible();
67+
expect(screen.getByText('Email')).toBeVisible();
68+
expect(screen.getByText('user1')).toBeVisible();
69+
expect(screen.getByText('user2')).toBeVisible();
70+
expect(
71+
screen.getByText(
72+
formatDate(updated, {
73+
format: 'MMM dd, yyyy, h:mm a',
74+
})
75+
)
76+
).toBeVisible();
77+
});
78+
79+
it('should render the loading state', () => {
80+
renderWithTheme(
81+
<NotificationChannelListTable
82+
isLoading={true}
83+
notificationChannels={[]}
84+
scrollToElement={mockScrollToElement}
85+
/>
86+
);
87+
88+
screen.getByTestId('table-row-loading');
89+
});
90+
91+
it('should render tooltip for Alerts column', async () => {
92+
renderWithTheme(
93+
<NotificationChannelListTable
94+
isLoading={false}
95+
notificationChannels={[]}
96+
scrollToElement={mockScrollToElement}
97+
/>
98+
);
99+
100+
const tooltipIcon = screen.getByTestId('tooltip-info-icon');
101+
await userEvent.hover(tooltipIcon);
102+
103+
await waitFor(() => {
104+
expect(
105+
screen.getByText(
106+
'The number of alert definitions associated with the notification channel.'
107+
)
108+
).toBeVisible();
109+
});
110+
});
111+
112+
it('should render multiple notification channels', () => {
113+
const channels = notificationChannelFactory.buildList(5);
114+
115+
renderWithTheme(
116+
<NotificationChannelListTable
117+
isLoading={false}
118+
notificationChannels={channels}
119+
scrollToElement={mockScrollToElement}
120+
/>
121+
);
122+
123+
channels.forEach((channel) => {
124+
expect(screen.getByText(channel.label)).toBeVisible();
125+
});
126+
});
127+
128+
it('should display correct alerts count', () => {
129+
const channel = notificationChannelFactory.build({
130+
alerts: [
131+
{ id: 1, label: 'Alert 1', type: ALERT_TYPE, url: 'url1' },
132+
{ id: 2, label: 'Alert 2', type: ALERT_TYPE, url: 'url2' },
133+
{ id: 3, label: 'Alert 3', type: ALERT_TYPE, url: 'url3' },
134+
],
135+
});
136+
137+
renderWithTheme(
138+
<NotificationChannelListTable
139+
isLoading={false}
140+
notificationChannels={[channel]}
141+
scrollToElement={mockScrollToElement}
142+
/>
143+
);
144+
145+
expect(screen.getByText('3')).toBeVisible();
146+
});
147+
148+
it('should render pagination footer', () => {
149+
const channels = notificationChannelFactory.buildList(30);
150+
151+
renderWithTheme(
152+
<NotificationChannelListTable
153+
isLoading={false}
154+
notificationChannels={channels}
155+
scrollToElement={mockScrollToElement}
156+
/>
157+
);
158+
159+
screen.getByRole('button', { name: /next/i });
160+
});
161+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { TooltipIcon } from '@linode/ui';
2+
import { GridLegacy, TableBody, TableHead } from '@mui/material';
3+
import React from 'react';
4+
5+
import Paginate from 'src/components/Paginate';
6+
import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter';
7+
import { Table } from 'src/components/Table';
8+
import { TableCell } from 'src/components/TableCell';
9+
import { TableContentWrapper } from 'src/components/TableContentWrapper/TableContentWrapper';
10+
import { TableRow } from 'src/components/TableRow';
11+
import { TableSortCell } from 'src/components/TableSortCell';
12+
import { useOrderV2 } from 'src/hooks/useOrderV2';
13+
import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';
14+
15+
import {
16+
ChannelAlertsTooltipText,
17+
ChannelListingTableLabelMap,
18+
} from './constants';
19+
import { NotificationChannelTableRow } from './NotificationChannelTableRow';
20+
21+
import type { APIError, NotificationChannel } from '@linode/api-v4';
22+
import type { Order } from '@linode/utilities';
23+
24+
export interface NotificationChannelListTableProps {
25+
/**
26+
* The error returned from the API call to fetch notification channels
27+
*/
28+
error?: APIError[];
29+
/**
30+
* Indicates if the data is loading
31+
*/
32+
isLoading: boolean;
33+
/**
34+
* The list of notification channels to display in the table
35+
*/
36+
notificationChannels: NotificationChannel[];
37+
/**
38+
* Function to scroll to a specific element on the page
39+
* @returns void
40+
*/
41+
scrollToElement: () => void;
42+
}
43+
44+
export const NotificationChannelListTable = React.memo(
45+
(props: NotificationChannelListTableProps) => {
46+
const { error, isLoading, notificationChannels, scrollToElement } = props;
47+
48+
const _error = error
49+
? getAPIErrorOrDefault(
50+
error,
51+
'Error in fetching the notification channels.'
52+
)
53+
: undefined;
54+
55+
const handleScrollAndPageChange = (
56+
page: number,
57+
handlePageChange: (p: number) => void
58+
) => {
59+
handlePageChange(page);
60+
requestAnimationFrame(() => {
61+
scrollToElement();
62+
});
63+
};
64+
65+
const handleScrollAndPageSizeChange = (
66+
pageSize: number,
67+
handlePageChange: (p: number) => void,
68+
handlePageSizeChange: (p: number) => void
69+
) => {
70+
handlePageSizeChange(pageSize);
71+
handlePageChange(1);
72+
requestAnimationFrame(() => {
73+
scrollToElement();
74+
});
75+
};
76+
77+
const handleSortClick = (
78+
orderBy: string,
79+
handleOrderChange: (orderBy: string, order?: Order) => void,
80+
handlePageChange: (page: number) => void,
81+
order?: Order
82+
) => {
83+
if (order) {
84+
handleOrderChange(orderBy, order);
85+
handlePageChange(1);
86+
}
87+
};
88+
89+
const { order, orderBy, handleOrderChange, sortedData } = useOrderV2({
90+
data: notificationChannels,
91+
initialRoute: {
92+
defaultOrder: {
93+
order: 'asc',
94+
orderBy: 'label',
95+
},
96+
from: '/alerts/notification-channels',
97+
},
98+
preferenceKey: 'alerts-notification-channels',
99+
});
100+
101+
return (
102+
<Paginate data={sortedData ?? []}>
103+
{({
104+
count,
105+
data: paginatedAndOrderedNotificationChannels,
106+
handlePageChange,
107+
handlePageSizeChange,
108+
page,
109+
pageSize,
110+
}) => {
111+
const handleTableSort = (orderBy: string, order?: Order) =>
112+
handleSortClick(
113+
orderBy,
114+
handleOrderChange,
115+
handlePageChange,
116+
order
117+
);
118+
119+
return (
120+
<>
121+
<GridLegacy sx={{ marginTop: 2 }}>
122+
<Table
123+
colCount={7}
124+
data-qa="notification-channels-table"
125+
size="small"
126+
>
127+
<TableHead>
128+
<TableRow>
129+
{ChannelListingTableLabelMap.map((value) => (
130+
<TableSortCell
131+
active={orderBy === value.label}
132+
direction={order}
133+
handleClick={handleTableSort}
134+
key={value.label}
135+
label={value.label}
136+
noWrap
137+
>
138+
{value.colName}
139+
{value.colName === 'Alerts' && (
140+
<TooltipIcon
141+
status="info"
142+
sxTooltipIcon={{ margin: 0, padding: 0 }}
143+
text={ChannelAlertsTooltipText}
144+
/>
145+
)}
146+
</TableSortCell>
147+
))}
148+
<TableCell />
149+
</TableRow>
150+
</TableHead>
151+
<TableBody>
152+
<TableContentWrapper
153+
error={_error}
154+
length={paginatedAndOrderedNotificationChannels.length}
155+
loading={isLoading}
156+
loadingProps={{ columns: 7 }}
157+
/>
158+
</TableBody>
159+
<TableBody>
160+
{paginatedAndOrderedNotificationChannels.map(
161+
(channel: NotificationChannel) => (
162+
<NotificationChannelTableRow
163+
key={channel.id}
164+
notificationChannel={channel}
165+
/>
166+
)
167+
)}
168+
</TableBody>
169+
</Table>
170+
</GridLegacy>
171+
<PaginationFooter
172+
count={count}
173+
eventCategory="Notification Channels Table"
174+
handlePageChange={(page) =>
175+
handleScrollAndPageChange(page, handlePageChange)
176+
}
177+
handleSizeChange={(pageSize) => {
178+
handleScrollAndPageSizeChange(
179+
pageSize,
180+
handlePageChange,
181+
handlePageSizeChange
182+
);
183+
}}
184+
page={page}
185+
pageSize={pageSize}
186+
sx={{ border: 0 }}
187+
/>
188+
</>
189+
);
190+
}}
191+
</Paginate>
192+
);
193+
}
194+
);

0 commit comments

Comments
 (0)