Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 2 additions & 2 deletions packages/api-v4/src/cloudpulse/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,13 +286,13 @@ interface NotificationChannelAlerts {
interface NotificationChannelBase {
alerts: NotificationChannelAlerts[];
channel_type: ChannelType;
created_at: string;
created: string;
created_by: string;
id: number;
label: string;
status: NotificationStatus;
type: AlertNotificationType;
updated_at: string;
updated: string;
updated_by: string;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Introduce Listing for ACLP-Alerting Notification channels with ordering, pagination ([#13193](https://github.com/linode/manager/pull/13193))
4 changes: 2 additions & 2 deletions packages/manager/src/factories/cloudpulse/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ export const notificationChannelFactory =
subject: 'Sample Alert',
},
},
created_at: new Date().toISOString(),
created: 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: new Date().toISOString(),
updated_by: 'user1',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

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

import { NotificationChannelListTable } from './NotificationChannelListTable';

const mockScrollToElement = vi.fn();

const ALERT_TYPE = 'alerts-definitions';

describe('NotificationChannelListTable', () => {
it('should render the notification channel table headers', () => {
renderWithTheme(
<NotificationChannelListTable
isLoading={false}
notificationChannels={[]}
scrollToElement={mockScrollToElement}
/>
);

expect(screen.getByText('Channel Name')).toBeVisible();
expect(screen.getByText('Alerts')).toBeVisible();
expect(screen.getByText('Channel Type')).toBeVisible();
expect(screen.getByText('Created By')).toBeVisible();
expect(screen.getByText('Last Modified')).toBeVisible();
expect(screen.getByText('Last Modified By')).toBeVisible();
});

it('should render the error message when error is provided', () => {
renderWithTheme(
<NotificationChannelListTable
error={[{ reason: 'Error in fetching the notification channels' }]}
isLoading={false}
notificationChannels={[]}
scrollToElement={mockScrollToElement}
/>
);

expect(
screen.getByText('Error in fetching the notification channels')
).toBeVisible();
});

it('should render notification channel rows', () => {
const updated = new Date().toISOString();
const channel = notificationChannelFactory.build({
channel_type: 'email',
created_by: 'user1',
label: 'Test Channel',
updated_by: 'user2',
updated,
});

renderWithTheme(
<NotificationChannelListTable
isLoading={false}
notificationChannels={[channel]}
scrollToElement={mockScrollToElement}
/>
);

expect(screen.getByText('Test Channel')).toBeVisible();
expect(screen.getByText('Email')).toBeVisible();
expect(screen.getByText('user1')).toBeVisible();
expect(screen.getByText('user2')).toBeVisible();
expect(
screen.getByText(
formatDate(updated, {
format: 'MMM dd, yyyy, h:mm a',
})
)
).toBeVisible();
});

it('should render the loading state', () => {
renderWithTheme(
<NotificationChannelListTable
isLoading={true}
notificationChannels={[]}
scrollToElement={mockScrollToElement}
/>
);

screen.getByTestId('table-row-loading');
});

it('should render tooltip for Alerts column', async () => {
renderWithTheme(
<NotificationChannelListTable
isLoading={false}
notificationChannels={[]}
scrollToElement={mockScrollToElement}
/>
);

const tooltipIcon = screen.getByTestId('tooltip-info-icon');
await userEvent.hover(tooltipIcon);

await waitFor(() => {
expect(
screen.getByText(
'The number of alert definitions associated with the notification channel.'
)
).toBeVisible();
});
});

it('should render multiple notification channels', () => {
const channels = notificationChannelFactory.buildList(5);

renderWithTheme(
<NotificationChannelListTable
isLoading={false}
notificationChannels={channels}
scrollToElement={mockScrollToElement}
/>
);

channels.forEach((channel) => {
expect(screen.getByText(channel.label)).toBeVisible();
});
});

it('should display correct alerts count', () => {
const channel = notificationChannelFactory.build({
alerts: [
{ id: 1, label: 'Alert 1', type: ALERT_TYPE, url: 'url1' },
{ id: 2, label: 'Alert 2', type: ALERT_TYPE, url: 'url2' },
{ id: 3, label: 'Alert 3', type: ALERT_TYPE, url: 'url3' },
],
});

renderWithTheme(
<NotificationChannelListTable
isLoading={false}
notificationChannels={[channel]}
scrollToElement={mockScrollToElement}
/>
);

expect(screen.getByText('3')).toBeVisible();
});

it('should render pagination footer', () => {
const channels = notificationChannelFactory.buildList(30);

renderWithTheme(
<NotificationChannelListTable
isLoading={false}
notificationChannels={channels}
scrollToElement={mockScrollToElement}
/>
);

screen.getByRole('button', { name: /next/i });
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { TooltipIcon } from '@linode/ui';
import { GridLegacy, TableBody, TableHead } from '@mui/material';
import React from 'react';

import Paginate from 'src/components/Paginate';
import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter';
import { Table } from 'src/components/Table';
import { TableCell } from 'src/components/TableCell';
import { TableContentWrapper } from 'src/components/TableContentWrapper/TableContentWrapper';
import { TableRow } from 'src/components/TableRow';
import { TableSortCell } from 'src/components/TableSortCell';
import { useOrderV2 } from 'src/hooks/useOrderV2';
import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';

import {
ChannelAlertsTooltipText,
ChannelListingTableLabelMap,
} from './constants';
import { NotificationChannelTableRow } from './NotificationChannelTableRow';

import type { APIError, NotificationChannel } from '@linode/api-v4';
import type { Order } from '@linode/utilities';

export interface NotificationChannelListTableProps {
/**
* The error returned from the API call to fetch notification channels
*/
error?: APIError[];
/**
* Indicates if the data is loading
*/
isLoading: boolean;
/**
* The list of notification channels to display in the table
*/
notificationChannels: NotificationChannel[];
/**
* Function to scroll to a specific element on the page
* @returns void
*/
scrollToElement: () => void;
}

export const NotificationChannelListTable = React.memo(
(props: NotificationChannelListTableProps) => {
const { error, isLoading, notificationChannels, scrollToElement } = props;

const _error = error
? getAPIErrorOrDefault(
error,
'Error in fetching the notification channels.'
)
: undefined;

const handleScrollAndPageChange = (
page: number,
handlePageChange: (p: number) => void
) => {
handlePageChange(page);
requestAnimationFrame(() => {
scrollToElement();
});
};

const handleScrollAndPageSizeChange = (
pageSize: number,
handlePageChange: (p: number) => void,
handlePageSizeChange: (p: number) => void
) => {
handlePageSizeChange(pageSize);
handlePageChange(1);
requestAnimationFrame(() => {
scrollToElement();
});
};

const handleSortClick = (
orderBy: string,
handleOrderChange: (orderBy: string, order?: Order) => void,
handlePageChange: (page: number) => void,
order?: Order
) => {
if (order) {
handleOrderChange(orderBy, order);
handlePageChange(1);
}
};

const { order, orderBy, handleOrderChange, sortedData } = useOrderV2({
data: notificationChannels,
initialRoute: {
defaultOrder: {
order: 'asc',
orderBy: 'label',
},
from: '/alerts/notification-channels',
},
preferenceKey: 'alerts-notification-channels',
});

return (
<Paginate data={sortedData ?? []}>
{({
count,
data: paginatedAndOrderedNotificationChannels,
handlePageChange,
handlePageSizeChange,
page,
pageSize,
}) => {
const handleTableSort = (orderBy: string, order?: Order) =>
handleSortClick(
orderBy,
handleOrderChange,
handlePageChange,
order
);

return (
<>
<GridLegacy sx={{ marginTop: 2 }}>
<Table
colCount={7}
data-qa="notification-channels-table"
size="small"
>
<TableHead>
<TableRow>
{ChannelListingTableLabelMap.map((value) => (
<TableSortCell
active={orderBy === value.label}
direction={order}
handleClick={handleTableSort}
key={value.label}
label={value.label}
noWrap
>
{value.colName}
{value.colName === 'Alerts' && (
<TooltipIcon
status="info"
sxTooltipIcon={{ margin: 0, padding: 0 }}
text={ChannelAlertsTooltipText}
/>
)}
</TableSortCell>
))}
<TableCell />
</TableRow>
</TableHead>
<TableBody>
<TableContentWrapper
error={_error}
length={paginatedAndOrderedNotificationChannels.length}
loading={isLoading}
loadingProps={{ columns: 7 }}
/>
</TableBody>
<TableBody>
{paginatedAndOrderedNotificationChannels.map(
(channel: NotificationChannel) => (
<NotificationChannelTableRow
key={channel.id}
notificationChannel={channel}
/>
)
)}
</TableBody>
</Table>
</GridLegacy>
<PaginationFooter
count={count}
eventCategory="Notification Channels Table"
handlePageChange={(page) =>
handleScrollAndPageChange(page, handlePageChange)
}
handleSizeChange={(pageSize) => {
handleScrollAndPageSizeChange(
pageSize,
handlePageChange,
handlePageSizeChange
);
}}
page={page}
pageSize={pageSize}
sx={{ border: 0 }}
/>
</>
);
}}
</Paginate>
);
}
);
Loading