Skip to content
Open
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
113 changes: 113 additions & 0 deletions frontend/public/components/__tests__/notification-drawer.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Route, Routes } from 'react-router-dom-v5-compat';
import { NamespaceContext } from '@console/app/src/components/detect-namespace/namespace';
import type { Alert } from '@console/dynamic-plugin-sdk/src/api/common-types';
import { AlertStates, RuleStates } from '@console/dynamic-plugin-sdk/src/api/common-types';
import { ALL_NAMESPACES_KEY } from '@console/shared/src/constants/common';
import { useNotificationAlerts } from '@console/shared/src/hooks/useNotificationAlerts';
import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
import { NotificationDrawer } from '../notification-drawer';

jest.mock('@console/shared/src/hooks/useNotificationAlerts', () => ({
useNotificationAlerts: jest.fn(() => [[], true, null]),
}));

jest.mock('@console/shared/src/hooks/useCanClusterUpgrade', () => ({
useCanClusterUpgrade: jest.fn(() => false),
}));

jest.mock('@console/dynamic-plugin-sdk/src/api/useResolvedExtensions', () => ({
useResolvedExtensions: jest.fn(() => [[], true]),
}));

// Importing the Console plugin store starts plugins that require webpack's populated share scope.
// The drawer resolves no alert action extensions, so skip application startup in Jest.
jest.mock('@console/dynamic-plugin-sdk/src/runtime/plugin-init', () => ({
initConsolePlugins: jest.fn(),
}));

const useNotificationAlertsMock = useNotificationAlerts as jest.Mock;

const makeAlerts = (count: number, severity: string): Alert[] =>
Array.from({ length: count }, (_, i) => ({
activeAt: '2025-01-01T00:00:00Z',
annotations: { description: `Alert ${i}` },
labels: { alertname: `TestAlert${i}`, severity },
rule: {
id: `rule-${i}`,
name: `TestAlert${i}`,
alerts: [],
annotations: {},
duration: 0,
labels: {},
query: 'vector(1)',
state: RuleStates.Firing,
type: 'alerting',
},
state: AlertStates.Firing,
}));

const renderDrawer = (setNamespace = jest.fn()) =>
renderWithProviders(
<NamespaceContext.Provider value={{ namespace: 'test-namespace', setNamespace }}>
<Routes>
<Route
path="/"
element={
<NotificationDrawer
isDrawerExpanded
onDrawerChange={jest.fn()}
drawerRef={{ current: null }}
/>
}
/>
<Route path="/monitoring/alerts" element={<h1>Alerts</h1>} />
</Routes>
</NamespaceContext.Provider>,
);

const severities: [string, string][] = [
['critical', 'Notifications in the critical alerts group'],
['warning', 'Notifications in the other alerts group'],
];

severities.forEach(([severity, groupName]) => {
describe(`NotificationDrawer ${severity} alert cap`, () => {
afterEach(() => {
jest.clearAllMocks();
});

[5, 50].forEach((count) => {
it(`renders all ${count} alerts without an overflow entry`, async () => {
useNotificationAlertsMock.mockReturnValue([makeAlerts(count, severity), true, null]);
renderDrawer();

const group = within(await screen.findByRole('list', { name: groupName }));
expect(group.getAllByText(/^Alert \d+$/)).toHaveLength(count);
expect(group.queryByText(/View all/)).not.toBeInTheDocument();
});
});

it('caps rendered alerts at 50 and shows the full count in the overflow entry', async () => {
useNotificationAlertsMock.mockReturnValue([makeAlerts(200, severity), true, null]);
renderDrawer();

const group = within(await screen.findByRole('list', { name: groupName }));
expect(group.getAllByText(/^Alert \d+$/)).toHaveLength(50);
expect(group.getByText('View all 200 alerts')).toBeVisible();
});

it('selects all namespaces when opening the full alert list', () => {
const setNamespace = jest.fn();
useNotificationAlertsMock.mockReturnValue([makeAlerts(200, severity), true, null]);
renderDrawer(setNamespace);

const group = within(screen.getByRole('list', { name: groupName }));
userEvent.click(group.getByText('View all 200 alerts'));

expect(setNamespace).toHaveBeenCalledWith(ALL_NAMESPACES_KEY);
expect(screen.getByRole('heading', { name: 'Alerts' })).toBeVisible();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,15 @@ import {
useResolvedExtensions,
WatchK8sResource,
} from '@console/dynamic-plugin-sdk';
import { Gallery, GalleryItem, Card, CardHeader, CardTitle } from '@patternfly/react-core';
import {
Gallery,
GalleryItem,
Card,
CardHeader,
CardTitle,
CardFooter,
Divider,
} from '@patternfly/react-core';
import { BlueArrowCircleUpIcon } from '@console/shared/src/components/status/icons';
import { ALL_NAMESPACES_KEY, FLAGS } from '@console/shared/src/constants/common';
import { useCanClusterUpgrade } from '@console/shared/src/hooks/useCanClusterUpgrade';
Expand Down Expand Up @@ -84,8 +92,11 @@ const cvResource: WatchK8sResource = {
isList: false,
};

const MAX_ALERTS = 50;

export const DashboardAlerts: React.FC<DashboardAlertsProps> = ({ labelSelector }) => {
const { t } = useTranslation();
const [, setActiveNamespace] = useActiveNamespace();
const hasCVResource = useFlag(FLAGS.CLUSTER_VERSION);
const [alerts, , loadError] = useNotificationAlerts(labelSelector);
const [cv, cvLoaded] = useK8sWatchResource<ClusterVersionKind>(
Expand All @@ -106,23 +117,44 @@ export const DashboardAlerts: React.FC<DashboardAlertsProps> = ({ labelSelector
<Link to="/settings/cluster?showVersions">{t('public~Update cluster')}</Link>
</StatusItem>
)}
{alerts.map((alert) => (
{alerts.slice(0, MAX_ALERTS).map((alert) => (
<AlertItem key={alertURL(alert, alert.rule.id)} alert={alert} />
))}
{alerts.length > MAX_ALERTS && (
<>
<Divider />
<CardFooter>
<Link to="/monitoring/alerts" onClick={() => setActiveNamespace(ALL_NAMESPACES_KEY)}>
{t('public~View all {{count}} alerts', { count: alerts.length })}
</Link>
</CardFooter>
</>
)}
</AlertsBody>
);
};

export const DashboardNamespacedAlerts: React.FC<DashboardNamespacedAlertsProps> = ({
namespace,
}) => {
const { t } = useTranslation();
const [namespacedAlerts, , loadError] = useNamespacedNotificationAlerts(namespace);

return (
<AlertsBody error={!_.isEmpty(loadError)}>
{namespacedAlerts.map((alert) => (
{namespacedAlerts.slice(0, MAX_ALERTS).map((alert) => (
<AlertItem key={alertURL(alert, alert.rule.id)} alert={alert} />
))}
{namespacedAlerts.length > MAX_ALERTS && (
<>
<Divider />
<CardFooter>
<Link to="/monitoring/alerts">
{t('public~View all {{count}} alerts', { count: namespacedAlerts.length })}
</Link>
</CardFooter>
</>
)}
</AlertsBody>
);
};
Expand Down
29 changes: 27 additions & 2 deletions frontend/public/components/notification-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ import { LabelSelector } from '@console/internal/module/k8s/label-selector';
import { useNotificationAlerts } from '@console/shared/src/hooks/useNotificationAlerts';
import { useModal } from '@console/dynamic-plugin-sdk/src/lib-core';
import { NotificationTypes } from './utils/types';
import { ALL_NAMESPACES_KEY } from '@console/shared/src/constants/common';
import { useActiveNamespace } from '@console/shared/src/hooks/useActiveNamespace';

const MAX_ALERTS_PER_GROUP = 50;

const AlertErrorState: FC<AlertErrorProps> = ({ errorText }) => {
const { t } = useTranslation();
Expand Down Expand Up @@ -241,6 +245,7 @@ export const NotificationDrawer: FC<NotificationDrawerProps> = ({
drawerRef,
}) => {
const { t } = useTranslation();
const [, setActiveNamespace] = useActiveNamespace();
const clusterID = getClusterID(useClusterVersion());
const showServiceLevelNotification = useShowServiceLevelNotifications(clusterID);
const pluginInfoEntries = usePluginInfo();
Expand All @@ -264,6 +269,10 @@ export const NotificationDrawer: FC<NotificationDrawerProps> = ({
toggleNotificationDrawer();
navigate(location);
};
const viewAllAlerts = () => {
setActiveNamespace(ALL_NAMESPACES_KEY);
itemOnClick('/monitoring/alerts');
};

const alertActionExtensionsMap = useMemo(() => getAlertActions(alertActionExtensions, navigate), [
alertActionExtensions,
Expand Down Expand Up @@ -349,7 +358,7 @@ export const NotificationDrawer: FC<NotificationDrawerProps> = ({
aria-label={t('public~Notifications in the critical alerts group')}
>
{criticalAlerts.length > 0
? criticalAlerts.map((alert, i) => {
? criticalAlerts.slice(0, MAX_ALERTS_PER_GROUP).map((alert, i) => {
const alertVariant = NotificationTypes[getAlertSeverity(alert)];
const alertTime = getAlertTime(alert);
return (
Expand Down Expand Up @@ -377,6 +386,14 @@ export const NotificationDrawer: FC<NotificationDrawerProps> = ({
);
})
: emptyState}
{criticalAlerts.length > MAX_ALERTS_PER_GROUP && (
<NotificationDrawerListItem variant={NotificationTypes.info} onClick={viewAllAlerts}>
<NotificationDrawerListItemHeader
variant={NotificationTypes.info}
title={t('public~View all {{count}} alerts', { count: criticalAlerts.length })}
/>
</NotificationDrawerListItem>
)}
</NotificationDrawerList>
</NotificationDrawerGroup>
);
Expand All @@ -395,7 +412,7 @@ export const NotificationDrawer: FC<NotificationDrawerProps> = ({
isHidden={!isNonCriticalAlertExpanded}
aria-label={t('public~Notifications in the other alerts group')}
>
{nonCriticalAlerts.map((alert, i) => {
{nonCriticalAlerts.slice(0, MAX_ALERTS_PER_GROUP).map((alert, i) => {
const alertVariant = NotificationTypes[getAlertSeverity(alert)];
const alertTime = getAlertTime(alert);
return (
Expand All @@ -422,6 +439,14 @@ export const NotificationDrawer: FC<NotificationDrawerProps> = ({
</NotificationDrawerListItem>
);
})}
{nonCriticalAlerts.length > MAX_ALERTS_PER_GROUP && (
<NotificationDrawerListItem variant={NotificationTypes.info} onClick={viewAllAlerts}>
<NotificationDrawerListItemHeader
variant={NotificationTypes.info}
title={t('public~View all {{count}} alerts', { count: nonCriticalAlerts.length })}
/>
</NotificationDrawerListItem>
)}
</NotificationDrawerList>
</NotificationDrawerGroup>
) : null;
Expand Down
2 changes: 2 additions & 0 deletions frontend/public/locales/en/public.json
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,8 @@
"Operator status": "Operator status",
"Cluster inventory": "Cluster inventory",
"A cluster version update is available": "A cluster version update is available",
"View all {{count}} alerts_one": "View {{count}} alert",
"View all {{count}} alerts_other": "View all {{count}} alerts",
"View alerts": "View alerts",
"Filter by Node type": "Filter by Node type",
"Cluster utilization": "Cluster utilization",
Expand Down