Skip to content

Commit

Permalink
update alert kpi in flyout
Browse files Browse the repository at this point in the history
  • Loading branch information
christineweng committed Nov 14, 2024
1 parent 16127fc commit 42bcabf
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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(
Expand All @@ -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([]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand All @@ -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<string, number>();

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
*/
Expand All @@ -51,37 +82,44 @@ export const AlertCountInsight: React.FC<AlertCountInsightProps> = ({
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 (
<EuiFlexItem data-test-subj={dataTestSubj}>
Expand Down

0 comments on commit 42bcabf

Please sign in to comment.