From 2c021fbd66f3c397c22365a2e9ea411c2b17fd7d Mon Sep 17 00:00:00 2001
From: christineweng <18648970+christineweng@users.noreply.github.com>
Date: Mon, 18 Nov 2024 13:42:34 -0600
Subject: [PATCH] [Security Solution] Update alert kpi to exclude closed alerts
in document details flyout (#200268)
## Summary
This PR made some changes to the alert count API in document details
flyout. Closed alerts are now removed when showing total count and
distributions. Data fetching logic is updated to match the one used in
host flyout (https://github.com/elastic/kibana/pull/197102).
Notable changes:
- Closed alerts are now excluded
- Number of alerts in alerts flyout should match the ones in host flyout
- Clicking the number will open timeline with the specific entity and
`NOT kibana.alert.workflow_status: closed` filters
- If a host/user does not have active alerts (all closed), no
distribution bar is shown
https://github.com/user-attachments/assets/3a1d6e36-527e-4b62-816b-e1f4de7314ca
### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
(cherry picked from commit a37a02efcff84282aafb1b94778fd09f0f2025f0)
---
.../left/components/host_details.test.tsx | 19 +++-
.../left/components/user_details.test.tsx | 19 +++-
.../components/host_entity_overview.test.tsx | 19 +++-
.../components/user_entity_overview.test.tsx | 19 +++-
.../components/alert_count_insight.test.tsx | 98 ++++++++++++++---
.../shared/components/alert_count_insight.tsx | 104 ++++++++++++------
6 files changed, 211 insertions(+), 67 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx
index e998c29d8ab6f..21346628b1f91 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx
@@ -40,7 +40,7 @@ import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview
import { UserPreviewPanelKey } from '../../../entity_details/user_right';
import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview';
import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details';
-import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
+import { useAlertsByStatus } from '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';
jest.mock('@kbn/expandable-flyout');
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
@@ -113,8 +113,17 @@ jest.mock('../../../../entity_analytics/api/hooks/use_risk_score');
const mockUseRiskScore = useRiskScore as jest.Mock;
jest.mock(
- '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
+ '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status'
);
+const mockAlertData = {
+ open: {
+ total: 2,
+ severities: [
+ { key: 'high', value: 1, label: 'High' },
+ { key: 'low', value: 1, label: 'Low' },
+ ],
+ },
+};
const timestamp = '2022-07-25T08:20:18.966Z';
@@ -172,7 +181,7 @@ describe('', () => {
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
(useMisconfigurationPreview as jest.Mock).mockReturnValue({});
(useVulnerabilitiesPreview as jest.Mock).mockReturnValue({});
- (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
+ (useAlertsByStatus as jest.Mock).mockReturnValue({ isLoading: false, items: {} });
});
it('should render host details correctly', () => {
@@ -321,9 +330,9 @@ describe('', () => {
});
it('should render alert count when data is available', () => {
- (useSummaryChartData as jest.Mock).mockReturnValue({
+ (useAlertsByStatus as jest.Mock).mockReturnValue({
isLoading: false,
- items: [{ key: 'high', value: 78, label: 'High' }],
+ items: mockAlertData,
});
const { getByTestId } = renderHostDetails(mockContextValue);
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx
index 17ec5f052be32..dd92106aa3cea 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx
@@ -38,7 +38,7 @@ import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview
import { UserPreviewPanelKey } from '../../../entity_details/user_right';
import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview';
import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details';
-import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
+import { useAlertsByStatus } from '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';
jest.mock('@kbn/expandable-flyout');
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
@@ -107,8 +107,17 @@ jest.mock('../../../../entity_analytics/api/hooks/use_risk_score');
const mockUseRiskScore = useRiskScore as jest.Mock;
jest.mock(
- '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
+ '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status'
);
+const mockAlertData = {
+ open: {
+ total: 2,
+ severities: [
+ { key: 'high', value: 1, label: 'High' },
+ { key: 'low', value: 1, label: 'Low' },
+ ],
+ },
+};
const timestamp = '2022-07-25T08:20:18.966Z';
@@ -165,7 +174,7 @@ describe('', () => {
mockUseUsersRelatedHosts.mockReturnValue(mockRelatedHostsResponse);
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
(useMisconfigurationPreview as jest.Mock).mockReturnValue({});
- (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
+ (useAlertsByStatus as jest.Mock).mockReturnValue({ isLoading: false, items: {} });
});
it('should render user details correctly', () => {
@@ -298,9 +307,9 @@ describe('', () => {
});
it('should render alert count when data is available', () => {
- (useSummaryChartData as jest.Mock).mockReturnValue({
+ (useAlertsByStatus as jest.Mock).mockReturnValue({
isLoading: false,
- items: [{ key: 'high', value: 78, label: 'High' }],
+ items: mockAlertData,
});
const { getByTestId } = renderUserDetails(mockContextValue);
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx
index 6ad90adb28997..4c29a84d431ae 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx
@@ -34,7 +34,7 @@ import { ENTITIES_TAB_ID } from '../../left/components/entities_details';
import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score';
import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context';
import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock';
-import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
+import { useAlertsByStatus } from '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';
const hostName = 'host';
const osFamily = 'Windows';
@@ -61,8 +61,17 @@ jest.mock('react-router-dom', () => {
});
jest.mock(
- '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
+ '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status'
);
+const mockAlertData = {
+ open: {
+ total: 2,
+ severities: [
+ { key: 'high', value: 1, label: 'High' },
+ { key: 'low', value: 1, label: 'Low' },
+ ],
+ },
+};
const mockedTelemetry = createTelemetryServiceMock();
jest.mock('../../../../common/lib/kibana', () => {
@@ -118,7 +127,7 @@ describe('', () => {
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
(useMisconfigurationPreview as jest.Mock).mockReturnValue({});
(useVulnerabilitiesPreview as jest.Mock).mockReturnValue({});
- (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
+ (useAlertsByStatus as jest.Mock).mockReturnValue({ isLoading: false, items: {} });
});
describe('license is valid', () => {
@@ -248,9 +257,9 @@ describe('', () => {
});
it('should render alert count when data is available', () => {
- (useSummaryChartData as jest.Mock).mockReturnValue({
+ (useAlertsByStatus as jest.Mock).mockReturnValue({
isLoading: false,
- items: [{ key: 'high', value: 78, label: 'High' }],
+ items: mockAlertData,
});
const { getByTestId } = renderHostEntityContent();
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx
index 95c399ca4362e..5df159c2e5a29 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx
@@ -31,7 +31,7 @@ import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context';
import { UserPreviewPanelKey } from '../../../entity_details/user_right';
-import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
+import { useAlertsByStatus } from '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';
const userName = 'user';
const domain = 'n54bg2lfc7';
@@ -59,8 +59,17 @@ jest.mock('react-router-dom', () => {
});
jest.mock(
- '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
+ '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status'
);
+const mockAlertData = {
+ open: {
+ total: 2,
+ severities: [
+ { key: 'high', value: 1, label: 'High' },
+ { key: 'low', value: 1, label: 'Low' },
+ ],
+ },
+};
jest.mock('../../../../common/hooks/use_experimental_features');
const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
@@ -102,7 +111,7 @@ describe('', () => {
jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi);
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
(useMisconfigurationPreview as jest.Mock).mockReturnValue({});
- (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
+ (useAlertsByStatus as jest.Mock).mockReturnValue({ isLoading: false, items: {} });
});
describe('license is valid', () => {
@@ -245,9 +254,9 @@ describe('', () => {
});
it('should render alert count when data is available', () => {
- (useSummaryChartData as jest.Mock).mockReturnValue({
+ (useAlertsByStatus as jest.Mock).mockReturnValue({
isLoading: false,
- items: [{ key: 'high', value: 78, label: 'High' }],
+ items: mockAlertData,
});
const { getByTestId } = renderUserEntityOverview();
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx
index 5e4650179291d..d9ae4673b1749 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx
@@ -8,8 +8,10 @@
import React from 'react';
import { render } from '@testing-library/react';
import { TestProviders } from '../../../../common/mock';
-import { AlertCountInsight } from './alert_count_insight';
-import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
+import { AlertCountInsight, getFormattedAlertStats } from './alert_count_insight';
+import { useAlertsByStatus } from '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';
+import type { ParsedAlertsData } from '../../../../overview/components/detection_response/alerts_by_status/types';
+import { SEVERITY_COLOR } from '../../../../overview/components/detection_response/utils';
jest.mock('../../../../common/lib/kibana');
@@ -19,12 +21,41 @@ jest.mock('react-router-dom', () => {
});
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
jest.mock(
- '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
+ '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status'
);
const fieldName = 'host.name';
const name = 'test host';
const testId = 'test';
+const mockAlertData: ParsedAlertsData = {
+ open: {
+ total: 4,
+ severities: [
+ { key: 'high', value: 1, label: 'High' },
+ { key: 'low', value: 1, label: 'Low' },
+ { key: 'medium', value: 1, label: 'Medium' },
+ { key: 'critical', value: 1, label: 'Critical' },
+ ],
+ },
+ acknowledged: {
+ total: 4,
+ severities: [
+ { key: 'high', value: 1, label: 'High' },
+ { key: 'low', value: 1, label: 'Low' },
+ { key: 'medium', value: 1, label: 'Medium' },
+ { key: 'critical', value: 1, label: 'Critical' },
+ ],
+ },
+ closed: {
+ total: 6,
+ severities: [
+ { key: 'high', value: 1, label: 'High' },
+ { key: 'low', value: 1, label: 'Low' },
+ { key: 'medium', value: 2, label: 'Medium' },
+ { key: 'critical', value: 2, label: 'Critical' },
+ ],
+ },
+};
const renderAlertCountInsight = () => {
return render(
@@ -36,30 +67,69 @@ const renderAlertCountInsight = () => {
describe('AlertCountInsight', () => {
it('renders', () => {
- (useSummaryChartData as jest.Mock).mockReturnValue({
+ (useAlertsByStatus as jest.Mock).mockReturnValue({
isLoading: false,
- items: [
- { key: 'high', value: 78, label: 'High' },
- { key: 'low', value: 46, label: 'Low' },
- { key: 'medium', value: 32, label: 'Medium' },
- { key: 'critical', value: 21, label: 'Critical' },
- ],
+ items: mockAlertData,
});
const { getByTestId } = renderAlertCountInsight();
expect(getByTestId(testId)).toBeInTheDocument();
expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument();
- expect(getByTestId(`${testId}-count`)).toHaveTextContent('177');
+ expect(getByTestId(`${testId}-count`)).toHaveTextContent('8');
});
it('renders loading spinner if data is being fetched', () => {
- (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: true, items: [] });
+ (useAlertsByStatus as jest.Mock).mockReturnValue({ isLoading: true, items: {} });
const { getByTestId } = renderAlertCountInsight();
expect(getByTestId(`${testId}-loading-spinner`)).toBeInTheDocument();
});
- it('renders null if no misconfiguration data found', () => {
- (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
+ it('renders null if no alert data found', () => {
+ (useAlertsByStatus as jest.Mock).mockReturnValue({ isLoading: false, items: {} });
+ const { container } = renderAlertCountInsight();
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('renders null if no non-closed alert data found', () => {
+ (useAlertsByStatus as jest.Mock).mockReturnValue({
+ isLoading: false,
+ items: {
+ closed: {
+ total: 6,
+ severities: [
+ { key: 'high', value: 1, label: 'High' },
+ { key: 'low', value: 1, label: 'Low' },
+ { key: 'medium', value: 2, label: 'Medium' },
+ { key: 'critical', value: 2, label: 'Critical' },
+ ],
+ },
+ },
+ });
const { container } = renderAlertCountInsight();
expect(container).toBeEmptyDOMElement();
});
});
+
+describe('getFormattedAlertStats', () => {
+ it('should return alert stats', () => {
+ const alertStats = getFormattedAlertStats(mockAlertData);
+ expect(alertStats).toEqual([
+ { key: 'High', count: 2, color: SEVERITY_COLOR.high },
+ { key: 'Low', count: 2, color: SEVERITY_COLOR.low },
+ { key: 'Medium', count: 2, color: SEVERITY_COLOR.medium },
+ { key: 'Critical', count: 2, color: SEVERITY_COLOR.critical },
+ ]);
+ });
+
+ it('should return empty array if no active alerts are available', () => {
+ const alertStats = getFormattedAlertStats({
+ closed: {
+ total: 2,
+ severities: [
+ { key: 'high', value: 1, label: 'High' },
+ { key: 'low', value: 1, label: 'Low' },
+ ],
+ },
+ });
+ expect(alertStats).toEqual([]);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx
index 08325584bd8cb..9b5b056311354 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx
@@ -6,22 +6,26 @@
*/
import React, { useMemo } from 'react';
-import { v4 as uuid } from 'uuid';
+import { capitalize } from 'lodash';
import { EuiLoadingSpinner, EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { InsightDistributionBar } from './insight_distribution_bar';
-import { severityAggregations } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/aggregations';
-import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
-import {
- getIsAlertsBySeverityData,
- getSeverityColor,
-} from '../../../../detections/components/alerts_kpis/severity_level_panel/helpers';
+import { getSeverityColor } from '../../../../detections/components/alerts_kpis/severity_level_panel/helpers';
import { FormattedCount } from '../../../../common/components/formatted_number';
import { InvestigateInTimelineButton } from '../../../../common/components/event_details/investigate_in_timeline_button';
-import { getDataProvider } from '../../../../common/components/event_details/use_action_cell_data_provider';
-
-const ENTITY_ALERT_COUNT_ID = 'entity-alert-count';
-const SEVERITIES = ['unknown', 'low', 'medium', 'high', 'critical'];
+import {
+ getDataProvider,
+ getDataProviderAnd,
+} from '../../../../common/components/event_details/use_action_cell_data_provider';
+import { FILTER_CLOSED, IS_OPERATOR } from '../../../../../common/types';
+import { useGlobalTime } from '../../../../common/containers/use_global_time';
+import { useAlertsByStatus } from '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';
+import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
+import { DETECTION_RESPONSE_ALERTS_BY_STATUS_ID } from '../../../../overview/components/detection_response/alerts_by_status/types';
+import type {
+ AlertsByStatus,
+ ParsedAlertsData,
+} from '../../../../overview/components/detection_response/alerts_by_status/types';
interface AlertCountInsightProps {
/**
@@ -42,6 +46,33 @@ interface AlertCountInsightProps {
['data-test-subj']?: string;
}
+/**
+ * Filters closed alerts and format the alert stats for the distribution bar
+ */
+export const getFormattedAlertStats = (alertsData: ParsedAlertsData) => {
+ const severityMap = new Map();
+
+ const filteredAlertsData: ParsedAlertsData = alertsData
+ ? Object.fromEntries(Object.entries(alertsData).filter(([key]) => key !== FILTER_CLOSED))
+ : {};
+
+ (Object.keys(filteredAlertsData || {}) as AlertsByStatus[]).forEach((status) => {
+ if (filteredAlertsData?.[status]?.severities) {
+ filteredAlertsData?.[status]?.severities.forEach((severity) => {
+ const currentSeverity = severityMap.get(severity.key) || 0;
+ severityMap.set(severity.key, currentSeverity + severity.value);
+ });
+ }
+ });
+
+ const alertStats = Array.from(severityMap, ([key, count]) => ({
+ key: capitalize(key),
+ count,
+ color: getSeverityColor(key),
+ }));
+ return alertStats;
+};
+
/*
* Displays a distribution bar with the total alert count for a given entity
*/
@@ -51,37 +82,44 @@ export const AlertCountInsight: React.FC = ({
direction,
'data-test-subj': dataTestSubj,
}) => {
- const uniqueQueryId = useMemo(() => `${ENTITY_ALERT_COUNT_ID}-${uuid()}`, []);
const entityFilter = useMemo(() => ({ field: fieldName, value: name }), [fieldName, name]);
+ const { to, from } = useGlobalTime();
+ const { signalIndexName } = useSignalIndex();
- const { items, isLoading } = useSummaryChartData({
- aggregations: severityAggregations,
+ const { items, isLoading } = useAlertsByStatus({
entityFilter,
- uniqueQueryId,
- signalIndexName: null,
+ signalIndexName,
+ queryId: DETECTION_RESPONSE_ALERTS_BY_STATUS_ID,
+ to,
+ from,
});
- const dataProviders = useMemo(
- () => [getDataProvider(fieldName, `timeline-indicator-${fieldName}-${name}`, name)],
- [fieldName, name]
- );
- const data = useMemo(() => (getIsAlertsBySeverityData(items) ? items : []), [items]);
+ const alertStats = useMemo(() => getFormattedAlertStats(items), [items]);
- const alertStats = useMemo(
- () =>
- data
- .map((item) => ({
- key: item.key,
- count: item.value,
- color: getSeverityColor(item.key),
- }))
- .sort((a, b) => SEVERITIES.indexOf(a.key) - SEVERITIES.indexOf(b.key)),
- [data]
+ const totalAlertCount = useMemo(
+ () => alertStats.reduce((acc, item) => acc + item.count, 0),
+ [alertStats]
);
- const totalAlertCount = useMemo(() => data.reduce((acc, item) => acc + item.value, 0), [data]);
+ const dataProviders = useMemo(
+ () => [
+ {
+ ...getDataProvider(fieldName, `timeline-indicator-${fieldName}-${name}`, name),
+ and: [
+ getDataProviderAnd(
+ 'kibana.alert.workflow_status',
+ `timeline-indicator-kibana.alert.workflow_status-not-closed}`,
+ FILTER_CLOSED,
+ IS_OPERATOR,
+ true
+ ),
+ ],
+ },
+ ],
+ [fieldName, name]
+ );
- if (!isLoading && items.length === 0) return null;
+ if (!isLoading && totalAlertCount === 0) return null;
return (