diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts
index 52ab4a8cb8d732..5863d525ca0f89 100644
--- a/x-pack/plugins/security_solution/common/experimental_features.ts
+++ b/x-pack/plugins/security_solution/common/experimental_features.ts
@@ -89,7 +89,7 @@ export const allowedExperimentalValues = Object.freeze({
/**
* Enables top charts on Alerts Page
*/
- alertsPageChartsEnabled: false,
+ alertsPageChartsEnabled: true,
/**
* Keep DEPRECATED experimental flags that are documented to prevent failed upgrades.
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_by_type_panel/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_by_type_panel/helpers.tsx
index ad357b81b5e22a..c0e794317205c9 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_by_type_panel/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_by_type_panel/helpers.tsx
@@ -7,7 +7,7 @@
import { has } from 'lodash';
import type { AlertType, AlertsByTypeAgg, AlertsTypeData } from './types';
import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types';
-import type { SummaryChartsData } from '../alerts_summary_charts_panel/types';
+import type { SummaryChartsData, SummaryChartsAgg } from '../alerts_summary_charts_panel/types';
export const ALERT_TYPE_COLOR = {
Detection: '#D36086',
@@ -62,3 +62,9 @@ const getAggregateAlerts = (
export const isAlertsTypeData = (data: SummaryChartsData[]): data is AlertsTypeData[] => {
return data?.every((x) => has(x, 'type'));
};
+
+export const isAlertsByTypeAgg = (
+ data: AlertSearchResponse<{}, SummaryChartsAgg>
+): data is AlertSearchResponse<{}, AlertsByTypeAgg> => {
+ return has(data, 'aggregations.alertsByRule');
+};
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx
index cc2e5ca8c78d01..067b5e2e86f149 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx
@@ -8,7 +8,6 @@
import React from 'react';
import { waitFor, act } from '@testing-library/react';
import { mount } from 'enzyme';
-
import { AlertsCountPanel } from '.';
import type { Status } from '../../../../../common/detection_engine/schemas/common';
@@ -61,25 +60,31 @@ jest.mock('../common/hooks', () => ({
useInspectButton: jest.fn(),
useStackByFields: jest.fn(),
}));
+const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
+jest.mock('../../../../common/hooks/use_experimental_features');
const defaultProps = {
inspectTitle: TABLE,
+ signalIndexName: 'signalIndexName',
+ stackByField0: DEFAULT_STACK_BY_FIELD,
+ stackByField1: DEFAULT_STACK_BY_FIELD1,
setStackByField0: jest.fn(),
setStackByField1: jest.fn(),
+ isExpanded: true,
+ setIsExpanded: jest.fn(),
showBuildingBlockAlerts: false,
showOnlyThreatIndicatorAlerts: false,
- signalIndexName: 'signalIndexName',
- stackByField0: DEFAULT_STACK_BY_FIELD,
- stackByField1: DEFAULT_STACK_BY_FIELD1,
status: 'open' as Status,
};
-const mockUseQueryToggle = useQueryToggle as jest.Mock;
const mockSetToggle = jest.fn();
+const mockUseQueryToggle = useQueryToggle as jest.Mock;
describe('AlertsCountPanel', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
+ mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(false); // for chartEmbeddablesEnabled flag
+ mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(false); // for alertsPageChartsEnabled flag
});
it('renders correctly', async () => {
@@ -177,6 +182,7 @@ describe('AlertsCountPanel', () => {
});
});
});
+
describe('toggleQuery', () => {
it('toggles', async () => {
await act(async () => {
@@ -189,7 +195,7 @@ describe('AlertsCountPanel', () => {
expect(mockSetToggle).toBeCalledWith(false);
});
});
- it('toggleStatus=true, render', async () => {
+ it('alertsPageChartsEnabled is false and toggleStatus=true, render', async () => {
await act(async () => {
const wrapper = mount(
@@ -199,7 +205,7 @@ describe('AlertsCountPanel', () => {
expect(wrapper.find('[data-test-subj="alertsCountTable"]').exists()).toEqual(true);
});
});
- it('toggleStatus=false, hide', async () => {
+ it('alertsPageChartsEnabled is false and toggleStatus=false, hide', async () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle });
await act(async () => {
const wrapper = mount(
@@ -210,16 +216,41 @@ describe('AlertsCountPanel', () => {
expect(wrapper.find('[data-test-subj="alertsCountTable"]').exists()).toEqual(false);
});
});
+
+ it('alertsPageChartsEnabled is true and isExpanded=true, render', async () => {
+ mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(false); // for chartEmbeddablesEnabled flag
+ mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(true); // for alertsPageChartsEnabled flag
+ await act(async () => {
+ mockUseIsExperimentalFeatureEnabled('charts', true);
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find('[data-test-subj="alertsCountTable"]').exists()).toEqual(true);
+ });
+ });
+ it('alertsPageChartsEnabled is true and isExpanded=false, hide', async () => {
+ mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(false); // for chartEmbeddablesEnabled flag
+ mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(true); // for alertsPageChartsEnabled flag
+ await act(async () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find('[data-test-subj="alertsCountTable"]').exists()).toEqual(false);
+ });
+ });
});
});
describe('when isChartEmbeddablesEnabled = true', () => {
beforeEach(() => {
jest.clearAllMocks();
-
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
-
- (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true);
+ mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(true); // for chartEmbeddablesEnabled flag
+ mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(false); // for alertsPageChartsEnabled flag
});
it('renders LensEmbeddable', async () => {
@@ -229,7 +260,7 @@ describe('when isChartEmbeddablesEnabled = true', () => {
);
- expect(wrapper.find('[data-test-subj="lens-embeddable"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="embeddable-count-table"]').exists()).toBeTruthy();
});
});
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx
index 9836cf80ff196f..5a1acfbc5b96b1 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx
@@ -84,6 +84,7 @@ export const AlertsCountPanel = memo(
setIsExpanded,
}) => {
const { to, from, deleteQuery, setQuery } = useGlobalTime(false);
+ const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled');
const isAlertsPageChartsEnabled = useIsExperimentalFeatureEnabled('alertsPageChartsEnabled');
// create a unique, but stable (across re-renders) query id
const uniqueQueryId = useMemo(() => `${DETECTIONS_ALERTS_COUNT_ID}-${uuidv4()}`, []);
@@ -130,7 +131,6 @@ export const AlertsCountPanel = memo(
[setQuerySkip, setToggleStatus, setIsExpanded, isAlertsPageChartsEnabled]
);
- const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled');
const timerange = useMemo(() => ({ from, to }), [from, to]);
const extraVisualizationOptions = useMemo(
@@ -234,7 +234,7 @@ export const AlertsCountPanel = memo(
(!isAlertsPageChartsEnabled && toggleStatus) ? (
{
};
});
-describe('AlertsHistogramPanel', () => {
- const defaultProps = {
- setQuery: jest.fn(),
- showBuildingBlockAlerts: false,
- showOnlyThreatIndicatorAlerts: false,
- signalIndexName: 'signalIndexName',
- updateDateRange: jest.fn(),
- };
+const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
+jest.mock('../../../../common/hooks/use_experimental_features');
- const mockSetToggle = jest.fn();
- const mockUseQueryToggle = useQueryToggle as jest.Mock;
- beforeEach(() => {
- mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
- });
+const defaultProps = {
+ setQuery: jest.fn(),
+ showBuildingBlockAlerts: false,
+ showOnlyThreatIndicatorAlerts: false,
+ signalIndexName: 'signalIndexName',
+ updateDateRange: jest.fn(),
+};
+const mockSetToggle = jest.fn();
+const mockUseQueryToggle = useQueryToggle as jest.Mock;
- afterEach(() => {
+describe('AlertsHistogramPanel', () => {
+ beforeEach(() => {
jest.clearAllMocks();
- jest.restoreAllMocks();
+ mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
});
it('renders correctly', () => {
@@ -690,26 +689,85 @@ describe('AlertsHistogramPanel', () => {
expect(mockSetToggle).toBeCalledWith(false);
});
});
- it('toggleStatus=true, render', async () => {
- await act(async () => {
- const wrapper = mount(
-
-
-
- );
- expect(wrapper.find(MatrixLoader).exists()).toEqual(true);
+ describe('when alertsPageChartsEnabled = false', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(false); // for chartEmbeddablesEnabled flag
+ mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(false); // for alertsPageChartsEnabled flag
+ });
+
+ it('toggleStatus=true, render', async () => {
+ await act(async () => {
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find(MatrixLoader).exists()).toEqual(true);
+ });
+ });
+ it('toggleStatus=false, hide', async () => {
+ mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle });
+ await act(async () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find(MatrixLoader).exists()).toEqual(false);
+ });
});
});
- it('toggleStatus=false, hide', async () => {
- mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle });
- await act(async () => {
- const wrapper = mount(
-
-
-
- );
- expect(wrapper.find(MatrixLoader).exists()).toEqual(false);
+
+ describe('when alertsPageChartsEnabled = true', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(false); // for chartEmbeddablesEnabled flag
+ mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(true); // for alertsPageChartsEnabled flag
+ });
+
+ it('isExpanded=true, render', async () => {
+ await act(async () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find(MatrixLoader).exists()).toEqual(true);
+ });
+ });
+ it('isExpanded=false, hide', async () => {
+ await act(async () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find(MatrixLoader).exists()).toEqual(false);
+ });
+ });
+ it('isExpanded is not passed in and toggleStatus =true, render', async () => {
+ await act(async () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find(MatrixLoader).exists()).toEqual(true);
+ });
+ });
+ it('isExpanded is not passed in and toggleStatus =false, hide', async () => {
+ mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle });
+ await act(async () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find(MatrixLoader).exists()).toEqual(false);
+ });
});
});
});
@@ -717,10 +775,9 @@ describe('AlertsHistogramPanel', () => {
describe('when isChartEmbeddablesEnabled = true', () => {
beforeEach(() => {
jest.clearAllMocks();
-
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
-
- (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true);
+ mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(true); // for chartEmbeddablesEnabled flag
+ mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(false); // for alertsPageChartsEnabled flag
});
it('renders LensEmbeddable', async () => {
@@ -730,7 +787,9 @@ describe('AlertsHistogramPanel', () => {
);
- expect(wrapper.find('[data-test-subj="lens-embeddable"]').exists()).toBeTruthy();
+ expect(
+ wrapper.find('[data-test-subj="embeddable-matrix-histogram"]').exists()
+ ).toBeTruthy();
});
});
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx
index 47e8dcc213f0f0..de52db990d5c79 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx
@@ -163,6 +163,7 @@ export const AlertsHistogramPanel = memo(
const [selectedStackByOption, setSelectedStackByOption] = useState(
onlyField == null ? defaultStackByOption : onlyField
);
+ const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled');
const isAlertsPageChartsEnabled = useIsExperimentalFeatureEnabled('alertsPageChartsEnabled');
const onSelect = useCallback(
@@ -184,7 +185,7 @@ export const AlertsHistogramPanel = memo(
isAlertsPageChartsEnabled ? !isExpanded : !toggleStatus
);
useEffect(() => {
- if (isAlertsPageChartsEnabled) {
+ if (isAlertsPageChartsEnabled && isExpanded !== undefined) {
setQuerySkip(!isExpanded);
} else {
setQuerySkip(!toggleStatus);
@@ -193,7 +194,7 @@ export const AlertsHistogramPanel = memo(
const toggleQuery = useCallback(
(newToggleStatus: boolean) => {
- if (isAlertsPageChartsEnabled && setIsExpanded) {
+ if (isAlertsPageChartsEnabled && setIsExpanded !== undefined) {
setIsExpanded(newToggleStatus);
} else {
setToggleStatus(newToggleStatus);
@@ -204,7 +205,6 @@ export const AlertsHistogramPanel = memo(
[setQuerySkip, setToggleStatus, setIsExpanded, isAlertsPageChartsEnabled]
);
- const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled');
const timerange = useMemo(() => ({ from, to }), [from, to]);
const {
@@ -364,24 +364,28 @@ export const AlertsHistogramPanel = memo(
[onlyField, title]
);
- const toggle = useMemo(
- () => (isAlertsPageChartsEnabled && isExpanded !== undefined ? isExpanded : toggleStatus),
- [isAlertsPageChartsEnabled, isExpanded, toggleStatus]
- );
+ const showHistogram = useMemo(() => {
+ if (isAlertsPageChartsEnabled) {
+ if (isExpanded !== undefined) {
+ // alerts page
+ return isExpanded;
+ } else {
+ // rule details page and overview page
+ return toggleStatus;
+ }
+ } else {
+ return toggleStatus;
+ }
+ }, [isAlertsPageChartsEnabled, isExpanded, toggleStatus]);
- const showHistogram = useMemo(
- () =>
- (isAlertsPageChartsEnabled && isExpanded) || (!isAlertsPageChartsEnabled && toggleStatus),
- [isAlertsPageChartsEnabled, isExpanded, toggleStatus]
- );
return (
-
+
(
outerDirection="row"
title={titleText}
titleSize={titleSize}
- toggleStatus={toggle}
+ toggleStatus={showHistogram}
toggleQuery={hideQueryToggle ? undefined : toggleQuery}
showInspectButton={isChartEmbeddablesEnabled ? false : chartOptionsContextMenu == null}
subtitle={!isInitialLoading && showTotalAlertsCount && totalAlerts}
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/alerts_progress_bar.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/alerts_progress_bar.test.tsx
index 5f8141ad57e8e9..1d972ff929e0f0 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/alerts_progress_bar.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/alerts_progress_bar.test.tsx
@@ -9,6 +9,7 @@ import React from 'react';
import { TestProviders } from '../../../../common/mock';
import { AlertsProgressBar } from './alerts_progress_bar';
import { parsedAlerts } from './mock_data';
+import type { GroupBySelection } from './types';
jest.mock('../../../../common/lib/kibana');
@@ -21,7 +22,7 @@ describe('Alert by grouping', () => {
const defaultProps = {
data: [],
isLoading: false,
- stackByField: 'host.name',
+ groupBySelection: 'host.name' as GroupBySelection,
};
afterEach(() => {
@@ -38,7 +39,7 @@ describe('Alert by grouping', () => {
);
expect(
container.querySelector(`[data-test-subj="alerts-progress-bar-title"]`)?.textContent
- ).toEqual(defaultProps.stackByField);
+ ).toEqual(defaultProps.groupBySelection);
expect(container.querySelector(`[data-test-subj="empty-proress-bar"]`)).toBeInTheDocument();
expect(container.querySelector(`[data-test-subj="empty-proress-bar"]`)?.textContent).toEqual(
'No items found'
@@ -50,7 +51,7 @@ describe('Alert by grouping', () => {
act(() => {
const { container } = render(
-
+
);
expect(
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/alerts_progress_bar.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/alerts_progress_bar.tsx
index e20f10543845e0..8e1c1ea65d1cd7 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/alerts_progress_bar.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/alerts_progress_bar.tsx
@@ -7,7 +7,7 @@
import { EuiProgress, EuiSpacer, EuiText, EuiHorizontalRule } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
-import type { AlertsProgressBarData } from './types';
+import type { AlertsProgressBarData, GroupBySelection } from './types';
import { DefaultDraggable } from '../../../../common/components/draggables';
import * as i18n from './translations';
@@ -22,19 +22,19 @@ const StyledEuiText = styled(EuiText)`
export interface AlertsProcessBarProps {
data: AlertsProgressBarData[];
isLoading: boolean;
- stackByField: string;
addFilter?: ({ field, value }: { field: string; value: string | number }) => void;
+ groupBySelection: GroupBySelection;
}
export const AlertsProgressBar: React.FC = ({
data,
isLoading,
- stackByField,
+ groupBySelection,
}) => {
return (
<>
- {stackByField}
+ {groupBySelection}
{!isLoading && data.length === 0 ? (
@@ -64,7 +64,7 @@ export const AlertsProgressBar: React.FC = ({
) : (
{
return data?.every((x) => has(x, 'percentage'));
};
+
+export const isAlertsByGroupingAgg = (
+ data: AlertSearchResponse<{}, SummaryChartsAgg>
+): data is AlertSearchResponse<{}, AlertsByGroupingAgg> => {
+ return has(data, 'aggregations.alertsByGrouping');
+};
+
+const labels = {
+ 'host.name': i18n.HOST_NAME_LABEL,
+ 'user.name': i18n.USER_NAME_LABEL,
+ 'source.ip': i18n.SOURCE_LABEL,
+ 'destination.ip': i18n.DESTINATION_LABEL,
+};
+
+export const getGroupByLabel = (option: GroupBySelection): string => {
+ return labels[option];
+};
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/index.test.tsx
index 5016e98140d324..776e4fffc89abe 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/index.test.tsx
@@ -4,12 +4,13 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { act, render, screen } from '@testing-library/react';
+import { act, render, screen, fireEvent } from '@testing-library/react';
import React from 'react';
import { TestProviders } from '../../../../common/mock';
import { AlertsProgressBarPanel } from '.';
import { useSummaryChartData } from '../alerts_summary_charts_panel/use_summary_chart_data';
import { STACK_BY_ARIA_LABEL } from '../common/translations';
+import type { GroupBySelection } from './types';
jest.mock('../../../../common/lib/kibana');
@@ -27,6 +28,8 @@ describe('Alert by grouping', () => {
const defaultProps = {
signalIndexName: 'signalIndexName',
skip: false,
+ groupBySelection: 'host.name' as GroupBySelection,
+ setGroupBySelection: jest.fn(),
};
beforeEach(() => {
@@ -74,6 +77,8 @@ describe('Alert by grouping', () => {
});
describe('combo box', () => {
+ const setGroupBySelection = jest.fn();
+
test('renders combo box', async () => {
await act(async () => {
const { container } = render(
@@ -89,7 +94,7 @@ describe('Alert by grouping', () => {
await act(async () => {
render(
-
+
);
const comboBox = screen.getByRole('combobox', { name: STACK_BY_ARIA_LABEL });
@@ -102,5 +107,24 @@ describe('Alert by grouping', () => {
expect(optionsFound[i]).toEqual(option);
});
});
+
+ test('it invokes setGroupBySelection when an option is selected', async () => {
+ const toBeSelected = 'user.name';
+ await act(async () => {
+ render(
+
+
+
+ );
+ const comboBox = screen.getByRole('combobox', { name: STACK_BY_ARIA_LABEL });
+ if (comboBox) {
+ comboBox.focus(); // display the combo box options
+ }
+ });
+ const button = await screen.findByText(toBeSelected);
+ fireEvent.click(button);
+
+ expect(setGroupBySelection).toBeCalledWith(toBeSelected);
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/index.tsx
index 0baeeb8e36d083..1e38e1cd402a52 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/index.tsx
@@ -7,7 +7,8 @@
import { EuiPanel, EuiLoadingSpinner } from '@elastic/eui';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { v4 as uuid } from 'uuid';
-import type { ChartsPanelProps } from '../alerts_summary_charts_panel/types';
+import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
+import type { Filter, Query } from '@kbn/es-query';
import { HeaderSection } from '../../../../common/components/header_section';
import { InspectButtonContainer } from '../../../../common/components/inspect';
import { StackByComboBox } from '../common/components';
@@ -17,28 +18,45 @@ import { alertsGroupingAggregations } from '../alerts_summary_charts_panel/aggre
import { showInitialLoadingSpinner } from '../alerts_histogram_panel/helpers';
import { isAlertsProgressBarData } from './helpers';
import * as i18n from './translations';
+import type { GroupBySelection } from './types';
const TOP_ALERTS_CHART_ID = 'alerts-summary-top-alerts';
const DEFAULT_COMBOBOX_WIDTH = 150;
const DEFAULT_OPTIONS = ['host.name', 'user.name', 'source.ip', 'destination.ip'];
-export const AlertsProgressBarPanel: React.FC = ({
+interface Props {
+ filters?: Filter[];
+ query?: Query;
+ signalIndexName: string | null;
+ runtimeMappings?: MappingRuntimeFields;
+ skip?: boolean;
+ groupBySelection: GroupBySelection;
+ setGroupBySelection: (groupBySelection: GroupBySelection) => void;
+}
+export const AlertsProgressBarPanel: React.FC = ({
filters,
query,
signalIndexName,
runtimeMappings,
skip,
+ groupBySelection,
+ setGroupBySelection,
}) => {
- const [stackByField, setStackByField] = useState('host.name');
const [isInitialLoading, setIsInitialLoading] = useState(true);
const uniqueQueryId = useMemo(() => `${TOP_ALERTS_CHART_ID}-${uuid()}`, []);
const dropDownOptions = DEFAULT_OPTIONS.map((field) => {
return { value: field, label: field };
});
- const aggregations = useMemo(() => alertsGroupingAggregations(stackByField), [stackByField]);
- const onSelect = useCallback((field: string) => {
- setStackByField(field);
- }, []);
+ const aggregations = useMemo(
+ () => alertsGroupingAggregations(groupBySelection),
+ [groupBySelection]
+ );
+ const onSelect = useCallback(
+ (field: string) => {
+ setGroupBySelection(field as GroupBySelection);
+ },
+ [setGroupBySelection]
+ );
const { items, isLoading } = useSummaryChartData({
aggregations,
@@ -61,7 +79,7 @@ export const AlertsProgressBarPanel: React.FC = ({
= ({
>
= ({
{isInitialLoading ? (
) : (
-
+
)}
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/translations.ts
index 514aff9fef4e1d..da6b33ad60c085 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/translations.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/translations.ts
@@ -26,3 +26,28 @@ export const OTHER = i18n.translate(
defaultMessage: 'Other',
}
);
+
+export const HOST_NAME_LABEL = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.alerts.alertsByGrouping.hostNameLabel',
+ {
+ defaultMessage: 'host',
+ }
+);
+export const USER_NAME_LABEL = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.alerts.alertsByGrouping.userNameLabel',
+ {
+ defaultMessage: 'user',
+ }
+);
+export const DESTINATION_LABEL = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.alerts.alertsByGrouping.destinationLabel',
+ {
+ defaultMessage: 'destination',
+ }
+);
+export const SOURCE_LABEL = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.alerts.alertsByGrouping.sourceLabel',
+ {
+ defaultMessage: 'source',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/types.ts
index efc278f9b4c47f..7b5b2042a63396 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_progress_bar_panel/types.ts
@@ -6,6 +6,7 @@
*/
import type { BucketItem } from '../../../../../common/search_strategy/security_solution/cti';
+export type GroupBySelection = 'host.name' | 'user.name' | 'source.ip' | 'destination.ip';
export interface AlertsByGroupingAgg {
alertsByGrouping: {
doc_count_error_upper_bound: number;
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/aggregations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/aggregations.ts
index 80151de9252bcc..e4883c022711e4 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/aggregations.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/aggregations.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
import { ALERT_SEVERITY, ALERT_RULE_NAME } from '@kbn/rule-data-utils';
+import type { GroupBySelection } from '../alerts_progress_bar_panel/types';
const DEFAULT_QUERY_SIZE = 1000;
@@ -33,7 +34,7 @@ export const alertTypeAggregations = {
},
};
-export const alertsGroupingAggregations = (stackByField: string) => {
+export const alertsGroupingAggregations = (stackByField: GroupBySelection) => {
return {
alertsByGrouping: {
terms: {
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/helpers.tsx
index 6ea064bf7b7be5..e4c7bc3f57c96a 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/helpers.tsx
@@ -4,12 +4,18 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { isAlertsBySeverityAgg, isAlertsByTypeAgg, isAlertsByGroupingAgg } from './types';
import type { SummaryChartsAgg } from './types';
import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types';
-import { parseSeverityData } from '../severity_level_panel/helpers';
-import { parseAlertsTypeData } from '../alerts_by_type_panel/helpers';
-import { parseAlertsGroupingData } from '../alerts_progress_bar_panel/helpers';
+import { parseSeverityData, isAlertsBySeverityAgg } from '../severity_level_panel/helpers';
+import { parseAlertsTypeData, isAlertsByTypeAgg } from '../alerts_by_type_panel/helpers';
+import {
+ parseAlertsGroupingData,
+ isAlertsByGroupingAgg,
+} from '../alerts_progress_bar_panel/helpers';
+import {
+ parseChartCollapseData,
+ isChartCollapseAgg,
+} from '../../../pages/detection_engine/chart_panels/chart_collapse/helpers';
export const parseData = (data: AlertSearchResponse<{}, SummaryChartsAgg>) => {
if (isAlertsBySeverityAgg(data)) {
@@ -21,5 +27,8 @@ export const parseData = (data: AlertSearchResponse<{}, SummaryChartsAgg>) => {
if (isAlertsByGroupingAgg(data)) {
return parseAlertsGroupingData(data);
}
+ if (isChartCollapseAgg(data)) {
+ return parseChartCollapseData(data);
+ }
return [];
};
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/index.test.tsx
index ac5d20fcdcfa98..f02043f3d7c55c 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/index.test.tsx
@@ -9,6 +9,8 @@ import React from 'react';
import { useQueryToggle } from '../../../../common/containers/query_toggle';
import { TestProviders } from '../../../../common/mock';
import { AlertsSummaryChartsPanel } from '.';
+import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
+import type { GroupBySelection } from '../alerts_progress_bar_panel/types';
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../../common/containers/query_toggle');
@@ -18,14 +20,22 @@ jest.mock('react-router-dom', () => {
return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) };
});
+const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
+jest.mock('../../../../common/hooks/use_experimental_features');
+
describe('AlertsSummaryChartsPanel', () => {
const defaultProps = {
signalIndexName: 'signalIndexName',
+ isExpanded: true,
+ setIsExpanded: jest.fn(),
+ groupBySelection: 'host.name' as GroupBySelection,
+ setGroupBySelection: jest.fn(),
};
const mockSetToggle = jest.fn();
const mockUseQueryToggle = useQueryToggle as jest.Mock;
beforeEach(() => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
+ mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
});
test('renders correctly', async () => {
@@ -89,7 +99,7 @@ describe('AlertsSummaryChartsPanel', () => {
});
});
- test('toggleStatus=true, render', async () => {
+ it('alertsPageChartsEnabled is false and toggleStatus=true, render', async () => {
await act(async () => {
const { container } = render(
@@ -102,7 +112,7 @@ describe('AlertsSummaryChartsPanel', () => {
});
});
- test('toggleStatus=false, hide', async () => {
+ it('alertsPageChartsEnabled is false and toggleStatus=false, hide', async () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle });
await act(async () => {
const { container } = render(
@@ -115,5 +125,32 @@ describe('AlertsSummaryChartsPanel', () => {
).not.toBeInTheDocument();
});
});
+
+ it('alertsPageChartsEnabled is true and isExpanded=true, render', async () => {
+ mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
+ await act(async () => {
+ const { container } = render(
+
+
+
+ );
+ expect(
+ container.querySelector('[data-test-subj="alerts-charts-container"]')
+ ).toBeInTheDocument();
+ });
+ });
+ it('alertsPageChartsEnabled is true and isExpanded=false, hide', async () => {
+ mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
+ await act(async () => {
+ const { container } = render(
+
+
+
+ );
+ expect(
+ container.querySelector('[data-test-subj="alerts-charts-container"]')
+ ).not.toBeInTheDocument();
+ });
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/index.tsx
index 813cc19c882214..f7c8889bfc8ecf 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/index.tsx
@@ -17,6 +17,7 @@ import { AlertsByTypePanel } from '../alerts_by_type_panel';
import { AlertsProgressBarPanel } from '../alerts_progress_bar_panel';
import { useQueryToggle } from '../../../../common/containers/query_toggle';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
+import type { GroupBySelection } from '../alerts_progress_bar_panel/types';
const StyledFlexGroup = styled(EuiFlexGroup)`
@media only screen and (min-width: ${({ theme }) => theme.eui.euiBreakpoints.l});
@@ -39,6 +40,8 @@ interface Props {
runtimeMappings?: MappingRuntimeFields;
isExpanded?: boolean;
setIsExpanded?: (status: boolean) => void;
+ groupBySelection: GroupBySelection;
+ setGroupBySelection: (groupBySelection: GroupBySelection) => void;
}
export const AlertsSummaryChartsPanel: React.FC = ({
@@ -52,6 +55,8 @@ export const AlertsSummaryChartsPanel: React.FC = ({
title = i18n.CHARTS_TITLE,
isExpanded,
setIsExpanded,
+ groupBySelection,
+ setGroupBySelection,
}: Props) => {
const isAlertsPageChartsEnabled = useIsExperimentalFeatureEnabled('alertsPageChartsEnabled');
@@ -137,12 +142,13 @@ export const AlertsSummaryChartsPanel: React.FC = ({
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/types.ts
index 6e25ac136281a7..8d3821ce1db625 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/types.ts
@@ -6,8 +6,6 @@
*/
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
import type { Filter, Query } from '@kbn/es-query';
-import { has } from 'lodash';
-import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types';
import type { SeverityBuckets as SeverityData } from '../../../../overview/components/detection_response/alerts_by_status/types';
import type { AlertsBySeverityAgg } from '../severity_level_panel/types';
import type { AlertsByTypeAgg, AlertsTypeData } from '../alerts_by_type_panel/types';
@@ -15,10 +13,20 @@ import type {
AlertsByGroupingAgg,
AlertsProgressBarData,
} from '../alerts_progress_bar_panel/types';
+import type {
+ ChartCollapseAgg,
+ ChartCollapseData,
+} from '../../../pages/detection_engine/chart_panels/chart_collapse/types';
-export type SummaryChartsAgg = Partial;
+export type SummaryChartsAgg = Partial<
+ AlertsBySeverityAgg | AlertsByTypeAgg | AlertsByGroupingAgg | ChartCollapseAgg
+>;
-export type SummaryChartsData = SeverityData | AlertsTypeData | AlertsProgressBarData;
+export type SummaryChartsData =
+ | SeverityData
+ | AlertsTypeData
+ | AlertsProgressBarData
+ | ChartCollapseData;
export interface ChartsPanelProps {
filters?: Filter[];
@@ -28,21 +36,3 @@ export interface ChartsPanelProps {
skip?: boolean;
addFilter?: ({ field, value }: { field: string; value: string | number }) => void;
}
-
-export const isAlertsBySeverityAgg = (
- data: AlertSearchResponse<{}, SummaryChartsAgg>
-): data is AlertSearchResponse<{}, AlertsBySeverityAgg> => {
- return has(data, 'aggregations.statusBySeverity');
-};
-
-export const isAlertsByTypeAgg = (
- data: AlertSearchResponse<{}, SummaryChartsAgg>
-): data is AlertSearchResponse<{}, AlertsByTypeAgg> => {
- return has(data, 'aggregations.alertsByRule');
-};
-
-export const isAlertsByGroupingAgg = (
- data: AlertSearchResponse<{}, SummaryChartsAgg>
-): data is AlertSearchResponse<{}, AlertsByGroupingAgg> => {
- return has(data, 'aggregations.alertsByGrouping');
-};
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/severity_level_panel/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/severity_level_panel/helpers.tsx
index 165bbef35fcfca..2ba07124595ab5 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/severity_level_panel/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/severity_level_panel/helpers.tsx
@@ -9,7 +9,7 @@ import { has } from 'lodash';
import type { AlertsBySeverityAgg } from './types';
import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types';
import type { SeverityBuckets as SeverityData } from '../../../../overview/components/detection_response/alerts_by_status/types';
-import type { SummaryChartsData } from '../alerts_summary_charts_panel/types';
+import type { SummaryChartsData, SummaryChartsAgg } from '../alerts_summary_charts_panel/types';
import { severityLabels } from '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';
import { emptyDonutColor } from '../../../../common/components/charts/donutchart_empty';
import { SEVERITY_COLOR } from '../../../../overview/components/detection_response/utils';
@@ -38,3 +38,9 @@ export const parseSeverityData = (
export const isAlertsBySeverityData = (data: SummaryChartsData[]): data is SeverityData[] => {
return data?.every((x) => has(x, 'key'));
};
+
+export const isAlertsBySeverityAgg = (
+ data: AlertSearchResponse<{}, SummaryChartsAgg>
+): data is AlertSearchResponse<{}, AlertsBySeverityAgg> => {
+ return has(data, 'aggregations.statusBySeverity');
+};
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/alerts_local_storage/constants.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/alerts_local_storage/constants.ts
index 6df717c6b541ef..8dc25558f4b6f9 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/alerts_local_storage/constants.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/alerts_local_storage/constants.ts
@@ -34,3 +34,6 @@ export const TREND_CHART_CATEGORY = 'trend';
/** settings for view selection are grouped under this category */
export const VIEW_CATEGORY = 'view';
+
+/** settings for group by selection on summary tab */
+export const GROUP_BY_SETTING_NAME = 'group-by';
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/alerts_local_storage/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/alerts_local_storage/index.test.tsx
index 6731bee771a3dd..1787955fb1b521 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/alerts_local_storage/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/alerts_local_storage/index.test.tsx
@@ -27,6 +27,7 @@ describe('useAlertsLocalStorage', () => {
alertViewSelection: 'trend', // default to the trend chart
countTableStackBy0: 'kibana.alert.rule.name',
countTableStackBy1: 'host.name',
+ groupBySelection: 'host.name',
isTreemapPanelExpanded: true,
riskChartStackBy0: 'kibana.alert.rule.name',
riskChartStackBy1: 'host.name',
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/alerts_local_storage/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/alerts_local_storage/index.tsx
index 5ac129b6368e31..4af424c0ead8b0 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/alerts_local_storage/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/alerts_local_storage/index.tsx
@@ -22,6 +22,7 @@ import {
STACK_BY_SETTING_NAME,
TREND_CHART_CATEGORY,
VIEW_CATEGORY,
+ GROUP_BY_SETTING_NAME,
} from './constants';
import {
DEFAULT_STACK_BY_FIELD,
@@ -30,6 +31,7 @@ import {
import type { AlertsSettings } from './types';
import type { AlertViewSelection } from '../chart_select/helpers';
import { TREND_ID } from '../chart_select/helpers';
+import type { GroupBySelection } from '../../../../components/alerts_kpis/alerts_progress_bar_panel/types';
export const useAlertsLocalStorage = (): AlertsSettings => {
const [alertViewSelection, setAlertViewSelection] = useLocalStorage({
@@ -42,6 +44,16 @@ export const useAlertsLocalStorage = (): AlertsSettings => {
isInvalidDefault: isDefaultWhenEmptyString,
});
+ const [groupBySelection, setGroupBySelection] = useLocalStorage({
+ defaultValue: 'host.name',
+ key: getSettingKey({
+ category: VIEW_CATEGORY,
+ page: ALERTS_PAGE,
+ setting: GROUP_BY_SETTING_NAME,
+ }),
+ isInvalidDefault: isDefaultWhenEmptyString,
+ });
+
const [isTreemapPanelExpanded, setIsTreemapPanelExpanded] = useLocalStorage({
defaultValue: true,
key: getSettingKey({
@@ -103,12 +115,14 @@ export const useAlertsLocalStorage = (): AlertsSettings => {
alertViewSelection,
countTableStackBy0,
countTableStackBy1,
+ groupBySelection,
isTreemapPanelExpanded,
riskChartStackBy0,
riskChartStackBy1,
setAlertViewSelection,
setCountTableStackBy0,
setCountTableStackBy1,
+ setGroupBySelection,
setIsTreemapPanelExpanded,
setRiskChartStackBy0,
setRiskChartStackBy1,
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/alerts_local_storage/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/alerts_local_storage/types.ts
index e909fc66f11db3..88a73579c764fa 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/alerts_local_storage/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/alerts_local_storage/types.ts
@@ -6,17 +6,19 @@
*/
import type { AlertViewSelection } from '../chart_select/helpers';
-
+import type { GroupBySelection } from '../../../../components/alerts_kpis/alerts_progress_bar_panel/types';
export interface AlertsSettings {
alertViewSelection: AlertViewSelection;
countTableStackBy0: string;
countTableStackBy1: string | undefined;
+ groupBySelection: GroupBySelection;
isTreemapPanelExpanded: boolean;
riskChartStackBy0: string;
riskChartStackBy1: string | undefined;
setAlertViewSelection: (alertViewSelection: AlertViewSelection) => void;
setCountTableStackBy0: (value: string) => void;
setCountTableStackBy1: (value: string | undefined) => void;
+ setGroupBySelection: (groupBySelection: GroupBySelection) => void;
setIsTreemapPanelExpanded: (value: boolean) => void;
setRiskChartStackBy0: (value: string) => void;
setRiskChartStackBy1: (value: string | undefined) => void;
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/helpers.test.tsx
new file mode 100644
index 00000000000000..ff03ddb279b722
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/helpers.test.tsx
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { parseChartCollapseData } from './helpers';
+import * as mock from './mock_data';
+import type { ChartCollapseAgg } from './types';
+import type { AlertSearchResponse } from '../../../../containers/detection_engine/alerts/types';
+import { getGroupByLabel } from '../../../../components/alerts_kpis/alerts_progress_bar_panel/helpers';
+import * as i18n from '../../../../components/alerts_kpis/alerts_progress_bar_panel/translations';
+
+describe('parse chart collapse data', () => {
+ test('parse alerts with data', () => {
+ const res = parseChartCollapseData(
+ mock.mockAlertsData as AlertSearchResponse<{}, ChartCollapseAgg>
+ );
+ expect(res).toEqual(mock.parsedAlerts);
+ });
+
+ test('parse alerts without data', () => {
+ const res = parseChartCollapseData(
+ mock.mockAlertsEmptyData as AlertSearchResponse<{}, ChartCollapseAgg>
+ );
+ expect(res).toEqual([]);
+ });
+});
+
+describe('get group by label', () => {
+ test('should return correct label for group by selections', () => {
+ expect(getGroupByLabel('host.name')).toEqual(i18n.HOST_NAME_LABEL);
+ expect(getGroupByLabel('user.name')).toEqual(i18n.USER_NAME_LABEL);
+ expect(getGroupByLabel('source.ip')).toEqual(i18n.SOURCE_LABEL);
+ expect(getGroupByLabel('destination.ip')).toEqual(i18n.DESTINATION_LABEL);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/helpers.tsx
new file mode 100644
index 00000000000000..12651da52415bd
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/helpers.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { has } from 'lodash';
+import type { ChartCollapseAgg, ChartCollapseData } from './types';
+import type { AlertSearchResponse } from '../../../../containers/detection_engine/alerts/types';
+import type {
+ SummaryChartsData,
+ SummaryChartsAgg,
+} from '../../../../components/alerts_kpis/alerts_summary_charts_panel/types';
+import { severityLabels } from '../../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';
+import { UNKNOWN_SEVERITY } from '../../../../components/alerts_kpis/severity_level_panel/translations';
+
+export const parseChartCollapseData = (
+ response: AlertSearchResponse<{}, ChartCollapseAgg>
+): ChartCollapseData[] => {
+ const ret: ChartCollapseData = { rule: null, group: null, severities: [] };
+ ret.rule = response?.aggregations?.topRule?.buckets?.at(0)?.key ?? null;
+ ret.group = response?.aggregations?.topGrouping?.buckets?.at(0)?.key ?? null;
+
+ const severityBuckets = response?.aggregations?.severities?.buckets ?? [];
+ if (severityBuckets.length > 0) {
+ ret.severities = severityBuckets.map((severity) => {
+ return {
+ key: severity.key,
+ value: severity.doc_count,
+ label: severityLabels[severity.key] ?? UNKNOWN_SEVERITY,
+ };
+ });
+ return [ret];
+ }
+ return [];
+};
+
+export const isChartCollapseData = (data: SummaryChartsData[]): data is ChartCollapseData[] => {
+ return data?.every((x) => has(x, 'rule') && has(x, 'group') && has(x, 'severities'));
+};
+
+export const isChartCollapseAgg = (
+ data: AlertSearchResponse<{}, SummaryChartsAgg>
+): data is AlertSearchResponse<{}, ChartCollapseAgg> => {
+ return (
+ has(data, 'aggregations.severities') &&
+ has(data, 'aggregations.topRule') &&
+ has(data, 'aggregations.topGrouping')
+ );
+};
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/index.test.tsx
new file mode 100644
index 00000000000000..1327fadef7f7ed
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/index.test.tsx
@@ -0,0 +1,95 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { render } from '@testing-library/react';
+import React from 'react';
+import type { GroupBySelection } from '../../../../components/alerts_kpis/alerts_progress_bar_panel/types';
+import { TestProviders } from '../../../../../common/mock';
+import { ChartCollapse } from '.';
+import { useSummaryChartData } from '../../../../components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
+import * as mock from './mock_data';
+
+jest.mock('../../../../../common/lib/kibana');
+
+jest.mock('react-router-dom', () => {
+ const actual = jest.requireActual('react-router-dom');
+ return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) };
+});
+jest.mock('../../../../components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data');
+
+const defaultProps = {
+ groupBySelection: 'host.name' as GroupBySelection,
+ signalIndexName: 'signalIndexName',
+};
+
+const severitiesId = '[data-test-subj="chart-collapse-severities"]';
+const ruleId = '[data-test-subj="chart-collapse-top-rule"]';
+const groupId = '[data-test-subj="chart-collapse-top-group"]';
+
+describe('ChartCollapse', () => {
+ const mockUseSummaryChartData = useSummaryChartData as jest.Mock;
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ jest.restoreAllMocks();
+ });
+
+ test('it renders the chart collapse panel and the 3 summary componenets', () => {
+ mockUseSummaryChartData.mockReturnValue({ items: [], isLoading: false });
+ const { container } = render(
+
+
+
+ );
+ expect(container.querySelector('[data-test-subj="chart-collapse"]')).toBeInTheDocument();
+
+ expect(container.querySelector(severitiesId)).toBeInTheDocument();
+ expect(container.querySelector(ruleId)).toBeInTheDocument();
+ expect(container.querySelector(groupId)).toBeInTheDocument();
+ });
+
+ test('it renders chart collapse with data', () => {
+ mockUseSummaryChartData.mockReturnValue({ items: mock.parsedAlerts, isLoading: false });
+ const { container } = render(
+
+
+
+ );
+
+ mock.parsedAlerts.at(0)?.severities.forEach((severity) => {
+ expect(container.querySelector(severitiesId)).toHaveTextContent(
+ `${severity.label}: ${severity.value}`
+ );
+ });
+ expect(container.querySelector(ruleId)).toHaveTextContent('Top alerted rule: Test rule');
+ expect(container.querySelector(groupId)).toHaveTextContent('Top alerted host: Test group');
+ });
+
+ test('it renders chart collapse without data', () => {
+ mockUseSummaryChartData.mockReturnValue({ items: [], isLoading: false });
+ const { container } = render(
+
+
+
+ );
+ mock.parsedAlerts.at(0)?.severities.forEach((severity) => {
+ expect(container.querySelector(severitiesId)).toHaveTextContent(`${severity.label}: 0`);
+ });
+ expect(container.querySelector(ruleId)).toHaveTextContent('Top alerted rule: None');
+ expect(container.querySelector(groupId)).toHaveTextContent('Top alerted host: None');
+ });
+
+ test('it renders group by label correctly', () => {
+ mockUseSummaryChartData.mockReturnValue({ items: [], isLoading: false });
+ const { container } = render(
+
+
+
+ );
+ expect(container.querySelector(groupId)).toHaveTextContent('Top alerted user: None');
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/index.tsx
index 0765f1471925b5..bd18195311f180 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/index.tsx
@@ -5,26 +5,32 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiText } from '@elastic/eui';
+import { ALERT_SEVERITY, ALERT_RULE_NAME } from '@kbn/rule-data-utils';
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
import type { Filter, Query } from '@kbn/es-query';
import { v4 as uuid } from 'uuid';
+import { capitalize } from 'lodash';
import React, { useMemo } from 'react';
-import { ALERT_SEVERITY, ALERT_RULE_NAME } from '@kbn/rule-data-utils';
import styled from 'styled-components';
+import type { GroupBySelection } from '../../../../components/alerts_kpis/alerts_progress_bar_panel/types';
+import { getGroupByLabel } from '../../../../components/alerts_kpis/alerts_progress_bar_panel/helpers';
+import { InspectButton, InspectButtonContainer } from '../../../../../common/components/inspect';
import { useSummaryChartData } from '../../../../components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
-import {
- isAlertsBySeverityData,
- getSeverityColor,
-} from '../../../../components/alerts_kpis/alerts_summary_charts_panel/severity_level_panel/helpers';
+import { getSeverityColor } from '../../../../components/alerts_kpis/severity_level_panel/helpers';
import { FormattedCount } from '../../../../../common/components/formatted_number';
+import { isChartCollapseData } from './helpers';
+import * as i18n from './translations';
+
+import { SEVERITY_COLOR } from '../../../../../overview/components/detection_response/utils';
const DETECTIONS_ALERTS_COLLAPSED_CHART_ID = 'detectioin-alerts-collapsed-chart';
-const combinedAggregations = (stackByValue: string) => {
+const combinedAggregations = (groupBySelection: GroupBySelection) => {
return {
- statusBySeverity: {
+ severities: {
terms: {
field: ALERT_SEVERITY,
+ min_doc_count: 0,
},
},
topRule: {
@@ -35,19 +41,29 @@ const combinedAggregations = (stackByValue: string) => {
},
topGrouping: {
terms: {
- field: stackByValue,
+ field: groupBySelection,
size: 1,
},
},
};
};
-const ChartCollapseWrapper = styled.div`
- margin: ${({ theme }) => theme.eui.euiSizeS};
+const StyledEuiFlexGroup = styled(EuiFlexGroup)`
+ margin-top: ${({ theme }) => theme.eui.euiSizeXS};
+ @media only screen and (min-width: ${({ theme }) => theme.eui.euiBreakpoints.l});
`;
+const SeverityWrapper = styled(EuiFlexItem)`
+ min-width: 380px;
+`;
+
+const StyledEuiText = styled(EuiText)`
+ border-left: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
+ padding-left: ${({ theme }) => theme.eui.euiSizeL};
+ white-space: nowrap;
+`;
interface Props {
- stackByValue: string;
+ groupBySelection: GroupBySelection;
filters?: Filter[];
query?: Query;
signalIndexName: string | null;
@@ -55,49 +71,78 @@ interface Props {
}
export const ChartCollapse: React.FC = ({
- stackByValue,
+ groupBySelection,
filters,
query,
signalIndexName,
runtimeMappings,
}: Props) => {
const uniqueQueryId = useMemo(() => `${DETECTIONS_ALERTS_COLLAPSED_CHART_ID}-${uuid()}`, []);
+ const aggregations = useMemo(() => combinedAggregations(groupBySelection), [groupBySelection]);
- const { items } = useSummaryChartData({
- aggregations: combinedAggregations,
+ const { items, isLoading } = useSummaryChartData({
+ aggregations,
filters,
query,
signalIndexName,
runtimeMappings,
uniqueQueryId,
});
- const data = useMemo(() => (isAlertsBySeverityData(items) ? items : []), [items]);
+ const data = useMemo(() => (isChartCollapseData(items) ? items : []), [items]);
+ const topRule = useMemo(() => data.at(0)?.rule ?? i18n.NO_RESULT_MESSAGE, [data]);
+ const topGroup = useMemo(() => data.at(0)?.group ?? i18n.NO_RESULT_MESSAGE, [data]);
+ const severities = useMemo(() => {
+ const severityData = data.at(0)?.severities ?? [];
+ return Object.keys(SEVERITY_COLOR).map((severity) => {
+ const obj = severityData.find((s) => s.key === severity);
+ if (obj) {
+ return { key: obj.key, label: obj.label, value: obj.value };
+ } else {
+ return { key: severity, label: capitalize(severity), value: 0 };
+ }
+ });
+ }, [data]);
+ const groupBy = useMemo(() => getGroupByLabel(groupBySelection), [groupBySelection]);
+ // className="eui-alignMiddle"
return (
-
-
-
-
- {data.map((severity) => (
-
-
-
-
- {`${severity.label}:`}
-
-
-
+
+ {!isLoading && (
+
+
+
+ {severities.map((severity) => (
+
+
+ {`${severity.label}: `}
-
-
-
- ))}
+
+
+ ))}
+
+
+
+
+ {i18n.TOP_RULE_TITLE}
+ {topRule}
+
+
+
+
+
+ {`${i18n.TOP_GROUP_TITLE} ${groupBy}: `}
+ {topGroup}
+
+
+
+
+
-
-
-
+
+ )}
+
);
};
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/mock_data.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/mock_data.ts
new file mode 100644
index 00000000000000..0b7ba68c99ec67
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/mock_data.ts
@@ -0,0 +1,152 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+const from = '2022-04-05T12:00:00.000Z';
+const to = '2022-04-08T12:00:00.000Z';
+
+export const mockAlertsData = {
+ took: 0,
+ timeout: false,
+ _shards: {
+ total: 1,
+ successful: 1,
+ skipped: 0,
+ failed: 0,
+ },
+ hits: {
+ total: {
+ value: 570,
+ relation: 'eq',
+ },
+ max_score: null,
+ hits: [],
+ },
+ aggregations: {
+ severities: {
+ doc_count_error_upper_bound: 0,
+ sum_other_doc_count: 0,
+ buckets: [
+ {
+ key: 'high',
+ doc_count: 78,
+ },
+ {
+ key: 'low',
+ doc_count: 46,
+ },
+ {
+ key: 'medium',
+ doc_count: 32,
+ },
+ {
+ key: 'critical',
+ doc_count: 21,
+ },
+ ],
+ },
+ topRule: {
+ doc_count_error_upper_bound: 0,
+ sum_other_doc_count: 0,
+ buckets: [
+ {
+ key: 'Test rule',
+ doc_count: 234,
+ },
+ ],
+ },
+ topGrouping: {
+ doc_count_error_upper_bound: 0,
+ sum_other_doc_count: 0,
+ buckets: [
+ {
+ key: 'Test group',
+ doc_count: 100,
+ },
+ ],
+ },
+ },
+};
+
+export const mockAlertsEmptyData = {
+ took: 0,
+ timeout: false,
+ _shards: {
+ total: 1,
+ successful: 1,
+ skipped: 0,
+ failed: 0,
+ },
+ hits: {
+ total: {
+ value: 0,
+ relation: 'eq',
+ },
+ max_score: null,
+ hits: [],
+ },
+ aggregations: {
+ severities: {
+ doc_count_error_upper_bound: 0,
+ sum_other_doc_count: 0,
+ buckets: [],
+ },
+ topRule: {
+ doc_count_error_upper_bound: 0,
+ sum_other_doc_count: 0,
+ buckets: [],
+ },
+ topGrouping: {
+ doc_count_error_upper_bound: 0,
+ sum_other_doc_count: 0,
+ buckets: [],
+ },
+ },
+};
+
+export const query = {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ { bool: { filter: [], must: [], must_not: [], should: [] } },
+ { range: { '@timestamp': { gte: from, lte: to } } },
+ ],
+ },
+ },
+ aggs: {
+ severities: {
+ terms: {
+ field: 'kibana.alert.severity',
+ },
+ },
+ topRule: {
+ terms: {
+ field: 'kibana.alert.rule.name',
+ size: 1000,
+ },
+ },
+ topGrouping: {
+ terms: {
+ field: 'host.name',
+ size: 1,
+ },
+ },
+ },
+ runtime_mappings: undefined,
+};
+
+export const parsedAlerts = [
+ {
+ rule: 'Test rule',
+ group: 'Test group',
+ severities: [
+ { key: 'high', value: 78, label: 'High' },
+ { key: 'low', value: 46, label: 'Low' },
+ { key: 'medium', value: 32, label: 'Medium' },
+ { key: 'critical', value: 21, label: 'Critical' },
+ ],
+ },
+];
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/translations.ts
new file mode 100644
index 00000000000000..adc53e8859f394
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/translations.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const TOP_RULE_TITLE = i18n.translate(
+ 'xpack.securitySolution.components.chartCollapse.topRule',
+ {
+ defaultMessage: 'Top alerted rule: ',
+ }
+);
+
+export const TOP_GROUP_TITLE = i18n.translate(
+ 'xpack.securitySolution.components.chartCollapse.topGroup',
+ {
+ defaultMessage: 'Top alerted',
+ }
+);
+
+export const NO_RESULT_MESSAGE = i18n.translate(
+ 'xpack.securitySolution.components.chartCollapse.noResultMessage',
+ {
+ defaultMessage: 'None',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/types.ts
index 9feccb45423f86..1dfa7ea3163728 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_collapse/types.ts
@@ -5,10 +5,13 @@
* 2.0.
*/
import type { BucketItem } from '../../../../../../common/search_strategy/security_solution/cti';
-import type { SeverityBucket } from '../../../../../overview/components/detection_response/alerts_by_status/types';
+import type {
+ SeverityBucket,
+ SeverityBuckets as SeverityData,
+} from '../../../../../overview/components/detection_response/alerts_by_status/types';
export interface ChartCollapseAgg {
- statusBySeverity: {
+ severities: {
doc_count_error_upper_bound: number;
sum_other_doc_count: number;
buckets: SeverityBucket[];
@@ -25,8 +28,7 @@ export interface ChartCollapseAgg {
};
}
export interface ChartCollapseData {
- key: string;
- value: number;
- percentage: number;
- label: string;
+ rule: string | null;
+ group: string | null;
+ severities: SeverityData[];
}
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_select/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_select/helpers.test.ts
index fc24e1777f186b..60f55d79c895ea 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_select/helpers.test.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_select/helpers.test.ts
@@ -104,33 +104,37 @@ describe('helpers', () => {
describe('getOptionProperties', () => {
test('it returns the expected properties when alertViewSelection is Trend', () => {
expect(getOptionProperties(TREND_ID)).toEqual({
- id: CHARTS_ID,
- 'data-test-subj': TREND_ID,
+ id: TREND_ID,
+ 'data-test-subj': `chart-select-${TREND_ID}`,
label: i18n.TREND_TITLE,
+ value: TREND_ID,
});
});
test('it returns the expected properties when alertViewSelection is Table', () => {
expect(getOptionProperties(TABLE_ID)).toEqual({
id: TABLE_ID,
- 'data-test-subj': TABLE_ID,
- label: i18n.TABLE,
+ 'data-test-subj': `chart-select-${TABLE_ID}`,
+ label: i18n.TABLE_TITLE,
+ value: TABLE_ID,
});
});
test('it returns the expected properties when alertViewSelection is Treemap', () => {
expect(getOptionProperties(TREEMAP_ID)).toEqual({
id: TREEMAP_ID,
- 'data-test-subj': TREEMAP_ID,
- label: i18n.TREEMAP,
+ 'data-test-subj': `chart-select-${TREEMAP_ID}`,
+ label: i18n.TREEMAP_TITLE,
+ value: TREEMAP_ID,
});
});
test('it returns the expected properties when alertViewSelection is charts', () => {
expect(getOptionProperties(CHARTS_ID)).toEqual({
id: CHARTS_ID,
- 'data-test-subj': CHARTS_ID,
- label: i18n.CHARTS,
+ 'data-test-subj': `chart-select-${CHARTS_ID}`,
+ label: i18n.CHARTS_TITLE,
+ value: CHARTS_ID,
});
});
});
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_select/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_select/helpers.ts
index 0ebe4fb80e4e7f..24d85b251e1cb5 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_select/helpers.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_select/helpers.ts
@@ -96,19 +96,35 @@ export const getContextMenuPanels = ({
export const getOptionProperties = (
alertViewSelection: AlertViewSelection
): EuiButtonGroupOptionProps => {
- const charts = { id: CHARTS_ID, 'data-test-subj': alertViewSelection, label: i18n.CHARTS_TITLE };
+ const charts = {
+ id: CHARTS_ID,
+ 'data-test-subj': `chart-select-${CHARTS_ID}`,
+ label: i18n.CHARTS_TITLE,
+ value: CHARTS_ID,
+ };
switch (alertViewSelection) {
case TABLE_ID:
- return { id: TABLE_ID, 'data-test-subj': alertViewSelection, label: i18n.TABLE_TITLE };
+ return {
+ id: TABLE_ID,
+ 'data-test-subj': `chart-select-${TABLE_ID}`,
+ label: i18n.TABLE_TITLE,
+ value: TABLE_ID,
+ };
case TREND_ID:
return {
id: TREND_ID,
- 'data-test-subj': alertViewSelection,
+ 'data-test-subj': `chart-select-${TREND_ID}`,
label: i18n.TREND_TITLE,
+ value: TREND_ID,
};
case TREEMAP_ID:
- return { id: TREEMAP_ID, 'data-test-subj': alertViewSelection, label: i18n.TREEMAP_TITLE };
+ return {
+ id: TREEMAP_ID,
+ 'data-test-subj': `chart-select-${TREEMAP_ID}`,
+ label: i18n.TREEMAP_TITLE,
+ value: TREEMAP_ID,
+ };
case CHARTS_ID:
return charts;
default:
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_select/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_select/index.test.tsx
index a666a64f7941e9..a5110a96a56e92 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_select/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_select/index.test.tsx
@@ -4,42 +4,82 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
-import { render, screen } from '@testing-library/react';
+import { render, screen, fireEvent } from '@testing-library/react';
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
import React from 'react';
import { TestProviders } from '../../../../../common/mock';
-import { SELECT_A_CHART_ARIA_LABEL, TREEMAP } from './translations';
+import * as i18n from './translations';
import { ChartSelect } from '.';
+import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
+
+const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
+jest.mock('../../../../../common/hooks/use_experimental_features');
describe('ChartSelect', () => {
- test('it renders the chart select button', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.restoreAllMocks();
+ });
+
+ test('it renders the chart select button when alertsPageChartsEnabled is false', () => {
+ mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
render(
);
- expect(screen.getByRole('button', { name: SELECT_A_CHART_ARIA_LABEL })).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: i18n.SELECT_A_CHART_ARIA_LABEL })
+ ).toBeInTheDocument();
});
- test('it invokes `setAlertViewSelection` with the expected value when a chart is selected', async () => {
+ test('it invokes `setAlertViewSelection` with the expected value when a chart is selected and alertsPageChartsEnabled is false', async () => {
+ mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
const setAlertViewSelection = jest.fn();
-
render(
);
- const selectButton = screen.getByRole('button', { name: SELECT_A_CHART_ARIA_LABEL });
+ const selectButton = screen.getByRole('button', { name: i18n.SELECT_A_CHART_ARIA_LABEL });
selectButton.click();
await waitForEuiPopoverOpen();
- const treemapMenuItem = screen.getByRole('button', { name: TREEMAP });
+ const treemapMenuItem = screen.getByRole('button', { name: i18n.TREEMAP });
treemapMenuItem.click();
expect(setAlertViewSelection).toBeCalledWith('treemap');
});
+
+ test('it renders the chart select tabs when alertsPageChartsEnabled is true', () => {
+ mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
+ render(
+
+
+
+ );
+
+ expect(screen.getByTestId('chart-select-tabs')).toBeInTheDocument();
+ expect(screen.getByTestId('trend')).toBeChecked();
+ });
+
+ test('changing selection render correctly when alertsPageChartsEnabled is true', async () => {
+ mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
+ const setAlertViewSelection = jest.fn();
+ const { container } = render(
+
+
+
+ );
+
+ const button = container.querySelector('input[value="treemap"]');
+ if (button) {
+ fireEvent.change(button, { target: { checked: true, type: 'radio' } });
+ }
+ expect(screen.getByTestId('treemap')).toBeChecked();
+ expect(screen.getByTestId('trend')).not.toBeChecked();
+ });
});
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_select/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_select/index.tsx
index 2748050bb92f7c..c31e04afeb301e 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_select/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/chart_select/index.tsx
@@ -70,13 +70,14 @@ const ChartSelectComponent: React.FC = ({
<>
{isAlertsPageChartsEnabled ? (
setAlertViewSelection(id as AlertViewSelection)}
buttonSize="compressed"
color="primary"
+ data-test-subj="chart-select-tabs"
/>
) : (
{
};
});
+const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
+jest.mock('../../../../common/hooks/use_experimental_features');
+
+const mockSetToggle = jest.fn();
+const mockUseQueryToggle = useQueryToggle as jest.Mock;
+jest.mock('../../../../common/containers/query_toggle');
+
const defaultAlertSettings = {
alertViewSelection: 'trend',
countTableStackBy0: 'kibana.alert.rule.name',
countTableStackBy1: 'host.name',
isTreemapPanelExpanded: true,
+ groupBySelection: 'host.name',
riskChartStackBy0: 'kibana.alert.rule.name',
riskChartStackBy1: 'host.name',
setAlertViewSelection: jest.fn(),
setCountTableStackBy0: jest.fn(),
setCountTableStackBy1: jest.fn(),
+ setGroupBySelection: jest.fn(),
setIsTreemapPanelExpanded: jest.fn(),
setRiskChartStackBy0: jest.fn(),
setRiskChartStackBy1: jest.fn(),
@@ -78,7 +89,7 @@ const defaultAlertSettings = {
const defaultProps = {
addFilter: jest.fn(),
- alertsHistogramDefaultFilters: [
+ alertsDefaultFilters: [
{
meta: {
alias: null,
@@ -136,6 +147,8 @@ const resetGroupByFields = () => {
describe('ChartPanels', () => {
beforeEach(() => {
jest.clearAllMocks();
+ mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
+ mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
(useSourcererDataView as jest.Mock).mockReturnValue({
indicesExist: true,
@@ -148,7 +161,7 @@ describe('ChartPanels', () => {
});
});
- test('it renders the chart selector', async () => {
+ test('it renders the chart selector when alertsPageChartsEnabled is false', async () => {
render(
@@ -160,6 +173,34 @@ describe('ChartPanels', () => {
});
});
+ test('it renders the chart selector tabs when alertsPageChartsEnabled is true and toggle is true', async () => {
+ mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
+ mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
+ render(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('chart-select-tabs')).toBeInTheDocument();
+ });
+ });
+
+ test('it renders the chart collapse when alertsPageChartsEnabled is true and toggle is false', async () => {
+ mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
+ mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle });
+ render(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('chart-collapse')).toBeInTheDocument();
+ });
+ });
+
test('it renders the trend loading spinner when data is loading and `alertViewSelection` is trend', async () => {
render(
@@ -315,7 +356,7 @@ describe('ChartPanels', () => {
});
});
- test('it renders the alerts count panel when `alertViewSelection` is treemap', async () => {
+ test('it renders the treemap panel when `alertViewSelection` is treemap', async () => {
(useAlertsLocalStorage as jest.Mock).mockReturnValue({
...defaultAlertSettings,
alertViewSelection: 'treemap',
@@ -331,4 +372,38 @@ describe('ChartPanels', () => {
expect(screen.getByTestId('treemapPanel')).toBeInTheDocument();
});
});
+
+ test('it renders the charts loading spinner when data is loading and `alertViewSelection` is charts', async () => {
+ (useAlertsLocalStorage as jest.Mock).mockReturnValue({
+ ...defaultAlertSettings,
+ alertViewSelection: 'charts',
+ });
+ mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
+ render(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('chartsLoadingSpinner')).toBeInTheDocument();
+ });
+ });
+
+ test('it renders the charts panel when `alertViewSelection` is charts', async () => {
+ (useAlertsLocalStorage as jest.Mock).mockReturnValue({
+ ...defaultAlertSettings,
+ alertViewSelection: 'charts',
+ });
+
+ render(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('chartPanels')).toBeInTheDocument();
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.tsx
index 113261ba0b22fd..d26a3e6ccf2ed2 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.tsx
@@ -8,8 +8,8 @@
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
import type { Filter, Query } from '@kbn/es-query';
-import { EuiFlexItem, EuiLoadingContent, EuiLoadingSpinner } from '@elastic/eui';
-import React, { useCallback, useMemo, useState } from 'react';
+import { EuiFlexItem, EuiLoadingContent } from '@elastic/eui';
+import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
@@ -17,6 +17,7 @@ import { useAlertsLocalStorage } from './alerts_local_storage';
import type { AlertsSettings } from './alerts_local_storage/types';
import { ChartContextMenu } from './chart_context_menu';
import { ChartSelect } from './chart_select';
+import { ChartCollapse } from './chart_collapse';
import * as i18n from './chart_select/translations';
import { AlertsTreemapPanel } from '../../../../common/components/alerts_treemap_panel';
import type { UpdateDateRange } from '../../../../common/components/charts/common';
@@ -49,7 +50,7 @@ const ChartSelectContainer = styled.div`
export interface Props {
addFilter: ({ field, value }: { field: string; value: string | number }) => void;
- alertsHistogramDefaultFilters: Filter[];
+ alertsDefaultFilters: Filter[];
isLoadingIndexPattern: boolean;
query: Query;
runtimeMappings: MappingRuntimeFields;
@@ -59,7 +60,7 @@ export interface Props {
const ChartPanelsComponent: React.FC = ({
addFilter,
- alertsHistogramDefaultFilters,
+ alertsDefaultFilters,
isLoadingIndexPattern,
query,
runtimeMappings,
@@ -69,19 +70,20 @@ const ChartPanelsComponent: React.FC = ({
const { toggleStatus: isExpanded, setToggleStatus: setIsExpanded } = useQueryToggle(
DETECTIONS_ALERTS_CHARTS_PANEL_ID
);
- const [stackByValue, _] = useState('host.name');
const isAlertsPageChartsEnabled = useIsExperimentalFeatureEnabled('alertsPageChartsEnabled');
const {
alertViewSelection,
countTableStackBy0,
countTableStackBy1,
+ groupBySelection,
isTreemapPanelExpanded,
riskChartStackBy0,
riskChartStackBy1,
setAlertViewSelection,
setCountTableStackBy0,
setCountTableStackBy1,
+ setGroupBySelection,
setIsTreemapPanelExpanded,
setRiskChartStackBy0,
setRiskChartStackBy1,
@@ -176,7 +178,13 @@ const ChartPanelsComponent: React.FC = ({
/>
) : (
- {stackByValue}
// collapsed
+
);
} else {
return (
@@ -193,7 +201,11 @@ const ChartPanelsComponent: React.FC = ({
setAlertViewSelection,
isAlertsPageChartsEnabled,
isExpanded,
- stackByValue,
+ groupBySelection,
+ alertsDefaultFilters,
+ query,
+ signalIndexName,
+ runtimeMappings,
]);
return (
@@ -210,7 +222,7 @@ const ChartPanelsComponent: React.FC = ({
comboboxRef={stackByField0ComboboxRef}
defaultStackByOption={trendChartStackBy}
extraActions={resetGroupByFieldAction}
- filters={alertsHistogramDefaultFilters}
+ filters={alertsDefaultFilters}
inspectTitle={i18n.TREND}
onFieldSelected={updateCommonStackBy0}
panelHeight={TREND_CHART_PANEL_HEIGHT}
@@ -241,7 +253,7 @@ const ChartPanelsComponent: React.FC = ({
alignHeader="flexStart"
chartOptionsContextMenu={chartOptionsContextMenu}
extraActions={resetGroupByFieldAction}
- filters={alertsHistogramDefaultFilters}
+ filters={alertsDefaultFilters}
inspectTitle={i18n.TABLE}
panelHeight={TABLE_PANEL_HEIGHT}
query={query}
@@ -272,9 +284,9 @@ const ChartPanelsComponent: React.FC = ({
addFilter={addFilter}
alignHeader="flexStart"
chartOptionsContextMenu={chartOptionsContextMenu}
- filters={alertsHistogramDefaultFilters}
inspectTitle={i18n.TREEMAP}
isPanelExpanded={isAlertsPageChartsEnabled ? isExpanded : isTreemapPanelExpanded}
+ filters={alertsDefaultFilters}
query={query}
riskSubAggregationField="kibana.alert.risk_score"
setIsPanelExpanded={
@@ -298,12 +310,12 @@ const ChartPanelsComponent: React.FC = ({
{isAlertsPageChartsEnabled && alertViewSelection === 'charts' && (
{isLoadingIndexPattern ? (
-
+
) : (
= ({
runtimeMappings={runtimeMappings}
isExpanded={isExpanded}
setIsExpanded={setIsExpanded}
+ groupBySelection={groupBySelection}
+ setGroupBySelection={setGroupBySelection}
/>
)}
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
index dfb4b0c299d0c7..de109fbd691d1a 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
@@ -217,7 +217,7 @@ const DetectionEnginePageComponent: React.FC = ({
[formatUrl, navigateToUrl]
);
- const alertsHistogramDefaultFilters = useMemo(
+ const alertsDefaultFilters = useMemo(
() => [
...filters,
...buildShowBuildingBlockFilter(showBuildingBlockAlerts),
@@ -437,7 +437,7 @@ const DetectionEnginePageComponent: React.FC = ({