diff --git a/frontend/public/components/__tests__/notification-drawer.spec.tsx b/frontend/public/components/__tests__/notification-drawer.spec.tsx
new file mode 100644
index 00000000000..c7abebeb320
--- /dev/null
+++ b/frontend/public/components/__tests__/notification-drawer.spec.tsx
@@ -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(
+
+
+
+ }
+ />
+ Alerts} />
+
+ ,
+ );
+
+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();
+ });
+ });
+});
diff --git a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/status-card.tsx b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/status-card.tsx
index 78a988b0f93..ca1d6a46f85 100644
--- a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/status-card.tsx
+++ b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/status-card.tsx
@@ -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';
@@ -84,8 +92,11 @@ const cvResource: WatchK8sResource = {
isList: false,
};
+const MAX_ALERTS = 50;
+
export const DashboardAlerts: React.FC = ({ labelSelector }) => {
const { t } = useTranslation();
+ const [, setActiveNamespace] = useActiveNamespace();
const hasCVResource = useFlag(FLAGS.CLUSTER_VERSION);
const [alerts, , loadError] = useNotificationAlerts(labelSelector);
const [cv, cvLoaded] = useK8sWatchResource(
@@ -106,9 +117,19 @@ export const DashboardAlerts: React.FC = ({ labelSelector
{t('public~Update cluster')}
)}
- {alerts.map((alert) => (
+ {alerts.slice(0, MAX_ALERTS).map((alert) => (
))}
+ {alerts.length > MAX_ALERTS && (
+ <>
+
+
+ setActiveNamespace(ALL_NAMESPACES_KEY)}>
+ {t('public~View all {{count}} alerts', { count: alerts.length })}
+
+
+ >
+ )}
);
};
@@ -116,13 +137,24 @@ export const DashboardAlerts: React.FC = ({ labelSelector
export const DashboardNamespacedAlerts: React.FC = ({
namespace,
}) => {
+ const { t } = useTranslation();
const [namespacedAlerts, , loadError] = useNamespacedNotificationAlerts(namespace);
return (
- {namespacedAlerts.map((alert) => (
+ {namespacedAlerts.slice(0, MAX_ALERTS).map((alert) => (
))}
+ {namespacedAlerts.length > MAX_ALERTS && (
+ <>
+
+
+
+ {t('public~View all {{count}} alerts', { count: namespacedAlerts.length })}
+
+
+ >
+ )}
);
};
diff --git a/frontend/public/components/notification-drawer.tsx b/frontend/public/components/notification-drawer.tsx
index bde6d9ada5f..2776f84ed0f 100644
--- a/frontend/public/components/notification-drawer.tsx
+++ b/frontend/public/components/notification-drawer.tsx
@@ -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 = ({ errorText }) => {
const { t } = useTranslation();
@@ -241,6 +245,7 @@ export const NotificationDrawer: FC = ({
drawerRef,
}) => {
const { t } = useTranslation();
+ const [, setActiveNamespace] = useActiveNamespace();
const clusterID = getClusterID(useClusterVersion());
const showServiceLevelNotification = useShowServiceLevelNotifications(clusterID);
const pluginInfoEntries = usePluginInfo();
@@ -264,6 +269,10 @@ export const NotificationDrawer: FC = ({
toggleNotificationDrawer();
navigate(location);
};
+ const viewAllAlerts = () => {
+ setActiveNamespace(ALL_NAMESPACES_KEY);
+ itemOnClick('/monitoring/alerts');
+ };
const alertActionExtensionsMap = useMemo(() => getAlertActions(alertActionExtensions, navigate), [
alertActionExtensions,
@@ -349,7 +358,7 @@ export const NotificationDrawer: FC = ({
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 (
@@ -377,6 +386,14 @@ export const NotificationDrawer: FC = ({
);
})
: emptyState}
+ {criticalAlerts.length > MAX_ALERTS_PER_GROUP && (
+
+
+
+ )}
);
@@ -395,7 +412,7 @@ export const NotificationDrawer: FC = ({
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 (
@@ -422,6 +439,14 @@ export const NotificationDrawer: FC = ({
);
})}
+ {nonCriticalAlerts.length > MAX_ALERTS_PER_GROUP && (
+
+
+
+ )}
) : null;
diff --git a/frontend/public/locales/en/public.json b/frontend/public/locales/en/public.json
index b01eda6e317..582e72622ad 100644
--- a/frontend/public/locales/en/public.json
+++ b/frontend/public/locales/en/public.json
@@ -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",