diff --git a/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx b/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx index 4edbc9fe85a32..6c702bb20fd90 100644 --- a/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx +++ b/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx @@ -20,6 +20,7 @@ jest.mock('react-router-dom', () => { useLocation: jest.fn().mockReturnValue({ pathname: '/test' }), }; }); +jest.mock('../../common/components/visualization_actions'); const casesService = { ui: { getCasesContext: () => mockCasesContext }, diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx index 98b82a8d5b8fa..29abcc5c475ea 100644 --- a/x-pack/plugins/security_solution/public/app/index.tsx +++ b/x-pack/plugins/security_solution/public/app/index.tsx @@ -49,5 +49,8 @@ export const renderApp = ({ , element ); - return () => unmountComponentAtNode(element); + return () => { + services.data.search.session.clear(); + unmountComponentAtNode(element); + }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/charts/donutchart.tsx b/x-pack/plugins/security_solution/public/common/components/charts/donutchart.tsx index 1358739742b6e..8b968077f3dcb 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/donutchart.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/donutchart.tsx @@ -46,7 +46,6 @@ export interface DonutChartProps { data: DonutChartData[] | null | undefined; fillColor: FillColor; height?: number; - isChartEmbeddablesEnabled?: boolean; label: React.ReactElement | string; legendItems?: LegendItem[] | null | undefined; onElementClick?: ElementClickListener; @@ -67,10 +66,10 @@ export interface DonutChartWrapperProps { /* Make this position absolute in order to overlap the text onto the donut */ export const DonutTextWrapper = styled(EuiFlexGroup)< EuiFlexGroupProps & { - $isChartEmbeddablesEnabled?: boolean; $dataExists?: boolean; + $donutTextWrapperStyles?: FlattenSimpleInterpolation; + $isChartEmbeddablesEnabled?: boolean; className?: string; - donutTextWrapperStyles?: FlattenSimpleInterpolation; } >` top: ${({ $isChartEmbeddablesEnabled, $dataExists }) => @@ -80,8 +79,8 @@ export const DonutTextWrapper = styled(EuiFlexGroup)< position: absolute; z-index: 1; - ${({ className, donutTextWrapperStyles }) => - className && donutTextWrapperStyles ? `&.${className} {${donutTextWrapperStyles}}` : ''} + ${({ className, $donutTextWrapperStyles }) => + className && $donutTextWrapperStyles ? `&.${className} {${$donutTextWrapperStyles}}` : ''} `; export const StyledEuiFlexItem = styled(EuiFlexItem)` @@ -117,11 +116,11 @@ const DonutChartWrapperComponent: React.FC = ({ @@ -151,7 +150,6 @@ export const DonutChart = ({ data, fillColor, height = 90, - isChartEmbeddablesEnabled, label, legendItems, onElementClick, @@ -165,7 +163,7 @@ export const DonutChart = ({ dataExists={data != null && data.length > 0} label={label} title={title} - isChartEmbeddablesEnabled={isChartEmbeddablesEnabled} + isChartEmbeddablesEnabled={false} > <> {data == null || totalCount == null || totalCount === 0 ? ( diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx index c06390b4f9a16..1387784d82860 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx @@ -47,6 +47,9 @@ jest.mock('../../lib/kibana', () => { }; }); +jest.mock('../visualization_actions'); +jest.mock('../visualization_actions/lens_embeddable'); + jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useHistory: () => mockHistory, diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/chart_content.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/chart_content.tsx new file mode 100644 index 0000000000000..65d3774af1eef --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/chart_content.tsx @@ -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 React from 'react'; +import type { BarChartComponentProps } from '../charts/barchart'; +import { BarChart } from '../charts/barchart'; +import { MatrixLoader } from './matrix_loader'; + +const MatrixHistogramChartContentComponent = ({ + isInitialLoading, + barChart, + configs, + stackByField, + scopeId, +}: BarChartComponentProps & { isInitialLoading: boolean }) => { + return isInitialLoading ? ( + + ) : ( + + ); +}; + +export const MatrixHistogramChartContent = React.memo(MatrixHistogramChartContentComponent); + +MatrixHistogramChartContentComponent.displayName = 'MatrixHistogramChartContentComponent'; diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx index ac5a43d23a28b..ec4a25039e912 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx @@ -176,22 +176,7 @@ describe('Matrix Histogram Component', () => { }); describe('Inspect button', () => { - test("it doesn't render Inspect button by default on Host page", () => { - mockLocation.mockReturnValue({ pathname: '/hosts' }); - - const testProps = { - ...mockMatrixOverTimeHistogramProps, - lensAttributes: dnsTopDomainsLensAttributes, - }; - wrapper = mount(, { - wrappingComponent: TestProviders, - }); - expect(wrapper.find('[data-test-subj="inspect-icon-button"]').exists()).toBe(false); - }); - - test("it doesn't render Inspect button by default on Network page", () => { - mockLocation.mockReturnValue({ pathname: '/network' }); - + test("it doesn't render Inspect button by default", () => { const testProps = { ...mockMatrixOverTimeHistogramProps, lensAttributes: dnsTopDomainsLensAttributes, @@ -201,41 +186,10 @@ describe('Matrix Histogram Component', () => { }); expect(wrapper.find('[data-test-subj="inspect-icon-button"]').exists()).toBe(false); }); - - test('it render Inspect button by default on other pages', () => { - mockLocation.mockReturnValue({ pathname: '/overview' }); - - const testProps = { - ...mockMatrixOverTimeHistogramProps, - lensAttributes: dnsTopDomainsLensAttributes, - }; - wrapper = mount(, { - wrappingComponent: TestProviders, - }); - expect(wrapper.find('[data-test-subj="inspect-icon-button"]').exists()).toBe(true); - }); }); describe('VisualizationActions', () => { - test('it renders VisualizationActions on Host page if lensAttributes is provided', () => { - mockLocation.mockReturnValue({ pathname: '/hosts' }); - - const testProps = { - ...mockMatrixOverTimeHistogramProps, - lensAttributes: dnsTopDomainsLensAttributes, - }; - wrapper = mount(, { - wrappingComponent: TestProviders, - }); - expect(wrapper.find('[data-test-subj="mock-viz-actions"]').exists()).toBe(true); - expect(wrapper.find('[data-test-subj="mock-viz-actions"]').prop('className')).toEqual( - 'histogram-viz-actions' - ); - }); - - test('it renders VisualizationActions on Network page if lensAttributes is provided', () => { - mockLocation.mockReturnValue({ pathname: '/network' }); - + test('it renders VisualizationActions if lensAttributes is provided', () => { const testProps = { ...mockMatrixOverTimeHistogramProps, lensAttributes: dnsTopDomainsLensAttributes, @@ -248,20 +202,6 @@ describe('Matrix Histogram Component', () => { 'histogram-viz-actions' ); }); - - test("it doesn't renders VisualizationActions except Host / Network pages", () => { - const testProps = { - ...mockMatrixOverTimeHistogramProps, - lensAttributes: dnsTopDomainsLensAttributes, - }; - - mockLocation.mockReturnValue({ pathname: '/overview' }); - - wrapper = mount(, { - wrappingComponent: TestProviders, - }); - expect(wrapper.find('[data-test-subj="mock-viz-actions"]').exists()).toBe(false); - }); }); describe('toggle query', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index 7b44b9295218c..48e812ff2afa3 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -11,11 +11,8 @@ import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSelect, EuiSpacer } from '@elastic/eui'; import { useDispatch } from 'react-redux'; -import { useLocation } from 'react-router-dom'; import * as i18n from './translations'; -import { BarChart } from '../charts/barchart'; import { HeaderSection } from '../header_section'; -import { MatrixLoader } from './matrix_loader'; import { Panel } from '../panel'; import { getBarchartConfigs, getCustomChartData } from './utils'; import { useMatrixHistogramCombined } from '../../containers/matrix_histogram'; @@ -35,8 +32,10 @@ import { HoverVisibilityContainer } from '../hover_visibility_container'; import { VisualizationActions } from '../visualization_actions'; import type { GetLensAttributes, LensAttributes } from '../visualization_actions/types'; import { useQueryToggle } from '../../containers/query_toggle'; +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { VISUALIZATION_ACTIONS_BUTTON_CLASS } from '../visualization_actions/utils'; -import { isExplorePage } from '../../../helpers'; +import { VisualizationEmbeddable } from '../visualization_actions/visualization_embeddable'; +import { MatrixHistogramChartContent } from './chart_content'; export type MatrixHistogramComponentProps = MatrixHistogramProps & Omit & { @@ -71,6 +70,8 @@ const HistogramPanel = styled(Panel)<{ height?: number }>` ${({ height }) => (height != null ? `min-height: ${height}px;` : '')} `; +const CHART_HEIGHT = '150px'; + export const MatrixHistogramComponent: React.FC = ({ chartHeight, defaultStackByOption, @@ -107,7 +108,6 @@ export const MatrixHistogramComponent: React.FC = hideQueryToggle = false, }) => { const dispatch = useDispatch(); - const { pathname } = useLocation(); const handleBrushEnd = useCallback( ({ x }) => { @@ -169,6 +169,8 @@ export const MatrixHistogramComponent: React.FC = [setQuerySkip, setToggleStatus] ); + const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled'); + const matrixHistogramRequest = { endDate, errorMessage, @@ -180,11 +182,10 @@ export const MatrixHistogramComponent: React.FC = stackByField: selectedStackByOption.value, runtimeMappings, isPtrIncluded, - skip: querySkip, + skip: querySkip || isChartEmbeddablesEnabled, }; const [loading, { data, inspect, totalCount, refetch }] = useMatrixHistogramCombined(matrixHistogramRequest); - const onExplorePage = isExplorePage(pathname); const titleWithStackByField = useMemo( () => (title != null && typeof title === 'function' ? title(selectedStackByOption) : title), @@ -209,22 +210,28 @@ export const MatrixHistogramComponent: React.FC = useEffect(() => { if (!loading && !isInitialLoading) { - setQuery({ id, inspect, loading, refetch }); + setQuery({ + id, + inspect, + loading, + refetch, + }); } if (isInitialLoading && !!barChartData && data) { setIsInitialLoading(false); } }, [ - setQuery, + barChartData, + data, id, inspect, + isChartEmbeddablesEnabled, + isInitialLoading, loading, refetch, - isInitialLoading, - barChartData, - data, setIsInitialLoading, + setQuery, ]); const timerange = useMemo(() => ({ from: startDate, to: endDate }), [startDate, endDate]); @@ -261,11 +268,11 @@ export const MatrixHistogramComponent: React.FC = toggleQuery={hideQueryToggle ? undefined : toggleQuery} subtitle={subtitleWithCounts} inspectMultiple - showInspectButton={showInspectButton || !onExplorePage} + showInspectButton={showInspectButton && !isChartEmbeddablesEnabled} isInspectDisabled={filterQuery === undefined} > - {onExplorePage && (getLensAttributes || lensAttributes) && timerange && ( + {(getLensAttributes || lensAttributes) && timerange && ( = {toggleStatus ? ( - isInitialLoading ? ( - + isChartEmbeddablesEnabled ? ( + ) : ( - ; refetchByRestartingSession: Refetch; + refetchByDeletingSession: Refetch; } => { const dispatch = useDispatch(); const { data } = useKibana().services; @@ -44,6 +45,7 @@ export const useRefetchByRestartingSession = ({ ); const refetchByRestartingSession = useCallback(() => { + const searchSessionId = session.current.start(); dispatch( inputsActions.setInspectionParameter({ id: queryId, @@ -54,13 +56,21 @@ export const useRefetchByRestartingSession = ({ * like most of our components, it refetches when receiving a new search * session ID. **/ - searchSessionId: skip ? undefined : session.current.start(), + searchSessionId: skip ? undefined : searchSessionId, }) ); }, [dispatch, queryId, selectedInspectIndex, skip]); + /** + * This is for refetching alert index when the first rule just created + */ + const refetchByDeletingSession = useCallback(() => { + dispatch(inputsActions.deleteOneQuery({ inputId: InputsModelId.global, id: queryId })); + }, [dispatch, queryId]); + return { session, refetchByRestartingSession, + refetchByDeletingSession, }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/authentication.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/authentication.test.ts.snap index 84d29e3e7fd9d..a00e6517914da 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/authentication.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/authentication.test.ts.snap @@ -251,7 +251,7 @@ Object { ], "legend": Object { "isVisible": true, - "position": "right", + "position": "left", }, "preferredSeriesType": "bar_stacked", "title": "Empty XY chart", diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/event.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/event.test.ts.snap index 93bdbe2a0dca5..ab1eed774e8aa 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/event.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/event.test.ts.snap @@ -175,7 +175,8 @@ Object { ], "legend": Object { "isVisible": true, - "position": "right", + "legendSize": "xlarge", + "position": "left", }, "preferredSeriesType": "bar_stacked", "title": "Empty XY chart", diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap index d53499bd57c8a..7d06e63032e95 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap @@ -206,7 +206,7 @@ Object { ], "legend": Object { "isVisible": true, - "position": "right", + "position": "left", }, "preferredSeriesType": "bar_stacked", "title": "Empty XY chart", diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_by_status_donut.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_by_status_donut.test.ts.snap new file mode 100644 index 0000000000000..12de83c43fc33 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_by_status_donut.test.ts.snap @@ -0,0 +1,166 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getAlertsByStatusAttributes should render without extra options 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-b9b43606-7ff7-46ae-a47c-85bed80fab9a", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "a1aaa83b-5026-444e-9465-50e0afade01c", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "b9b43606-7ff7-46ae-a47c-85bed80fab9a": Object { + "columnOrder": Array [ + "a9b43606-7ff7-46ae-a47c-85bed80fab9a", + "21cc4a49-3780-4b1a-be28-f02fa5303d24", + ], + "columns": Object { + "21cc4a49-3780-4b1a-be28-f02fa5303d24": Object { + "dataType": "number", + "filter": Object { + "language": "kuery", + "query": "", + }, + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, + "scale": "ratio", + "sourceField": "___records___", + }, + "a9b43606-7ff7-46ae-a47c-85bed80fab9a": Object { + "dataType": "string", + "isBucketed": true, + "label": "Filters", + "operationType": "filters", + "params": Object { + "filters": Array [ + Object { + "input": Object { + "language": "kuery", + "query": "kibana.alert.severity: \\"critical\\"", + }, + "label": "Critical", + }, + Object { + "input": Object { + "language": "kuery", + "query": "kibana.alert.severity : \\"high\\" ", + }, + "label": "High", + }, + Object { + "input": Object { + "language": "kuery", + "query": "kibana.alert.severity: \\"medium\\"", + }, + "label": "Medium", + }, + Object { + "input": Object { + "language": "kuery", + "query": "kibana.alert.severity : \\"low\\" ", + }, + "label": "Low", + }, + ], + }, + "scale": "ordinal", + }, + }, + "incompleteColumns": Object {}, + "sampling": 1, + }, + }, + }, + "textBased": Object { + "layers": Object {}, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "layers": Array [ + Object { + "categoryDisplay": "hide", + "emptySizeRatio": 0.85, + "layerId": "b9b43606-7ff7-46ae-a47c-85bed80fab9a", + "layerType": "data", + "legendDisplay": "hide", + "metrics": Array [ + "21cc4a49-3780-4b1a-be28-f02fa5303d24", + ], + "nestedLegend": true, + "numberDisplay": "value", + "percentDecimals": 2, + "primaryGroups": Array [ + "a9b43606-7ff7-46ae-a47c-85bed80fab9a", + ], + }, + ], + "shape": "donut", + }, + }, + "title": "Alerts", + "visualizationType": "lnsPie", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap new file mode 100644 index 0000000000000..d0b6f7a79ce34 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap @@ -0,0 +1,343 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getAlertsHistogramLensAttributes should render with extra options - filters 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-0039eb0c-9a1a-4687-ae54-0f4e239bec75", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "0039eb0c-9a1a-4687-ae54-0f4e239bec75": Object { + "columnOrder": Array [ + "34919782-4546-43a5-b668-06ac934d3acd", + "aac9d7d0-13a3-480a-892b-08207a787926", + "e09e0380-0740-4105-becc-0a4ca12e3944", + ], + "columns": Object { + "34919782-4546-43a5-b668-06ac934d3acd": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of event.category", + "operationType": "terms", + "params": Object { + "missingBucket": false, + "orderBy": Object { + "columnId": "e09e0380-0740-4105-becc-0a4ca12e3944", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "secondaryFields": Array [], + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "aac9d7d0-13a3-480a-892b-08207a787926": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + "e09e0380-0740-4105-becc-0a4ca12e3944": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + ".alerts-security.alerts-default", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": ".alerts-security.alerts-default", + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": true, + }, + "layers": Array [ + Object { + "accessors": Array [ + "e09e0380-0740-4105-becc-0a4ca12e3944", + ], + "layerId": "0039eb0c-9a1a-4687-ae54-0f4e239bec75", + "layerType": "data", + "position": "top", + "seriesType": "bar_stacked", + "showGridlines": false, + "splitAccessor": "34919782-4546-43a5-b668-06ac934d3acd", + "xAccessor": "aac9d7d0-13a3-480a-892b-08207a787926", + }, + ], + "legend": Object { + "isVisible": true, + "legendSize": "xlarge", + "position": "left", + }, + "preferredSeriesType": "bar_stacked", + "title": "Empty XY chart", + "valueLabels": "hide", + "valuesInLegend": true, + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "Alerts", + "visualizationType": "lnsXY", +} +`; + +exports[`getAlertsHistogramLensAttributes should render without extra options 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-0039eb0c-9a1a-4687-ae54-0f4e239bec75", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "0039eb0c-9a1a-4687-ae54-0f4e239bec75": Object { + "columnOrder": Array [ + "34919782-4546-43a5-b668-06ac934d3acd", + "aac9d7d0-13a3-480a-892b-08207a787926", + "e09e0380-0740-4105-becc-0a4ca12e3944", + ], + "columns": Object { + "34919782-4546-43a5-b668-06ac934d3acd": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of event.category", + "operationType": "terms", + "params": Object { + "missingBucket": false, + "orderBy": Object { + "columnId": "e09e0380-0740-4105-becc-0a4ca12e3944", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "secondaryFields": Array [], + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "aac9d7d0-13a3-480a-892b-08207a787926": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + "e09e0380-0740-4105-becc-0a4ca12e3944": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": true, + }, + "layers": Array [ + Object { + "accessors": Array [ + "e09e0380-0740-4105-becc-0a4ca12e3944", + ], + "layerId": "0039eb0c-9a1a-4687-ae54-0f4e239bec75", + "layerType": "data", + "position": "top", + "seriesType": "bar_stacked", + "showGridlines": false, + "splitAccessor": "34919782-4546-43a5-b668-06ac934d3acd", + "xAccessor": "aac9d7d0-13a3-480a-892b-08207a787926", + }, + ], + "legend": Object { + "isVisible": true, + "legendSize": "xlarge", + "position": "left", + }, + "preferredSeriesType": "bar_stacked", + "title": "Empty XY chart", + "valueLabels": "hide", + "valuesInLegend": true, + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "Alerts", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_table.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_table.test.ts.snap new file mode 100644 index 0000000000000..58ecf5d44d015 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_table.test.ts.snap @@ -0,0 +1,523 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getAlertsTableLensAttributes should render with extra options - breakdownField 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b": Object { + "columnOrder": Array [ + "2881fedd-54b7-42ba-8c97-5175dec86166", + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "f04a71a3-399f-4d32-9efc-8a005e989991", + ], + "columns": Object { + "2881fedd-54b7-42ba-8c97-5175dec86166": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of event.category", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of agent.type", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "agent.type", + }, + "f04a71a3-399f-4d32-9efc-8a005e989991": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of agent.type", + "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, + "scale": "ratio", + "sourceField": "agent.type", + }, + }, + "incompleteColumns": Object {}, + "sampling": 1, + }, + }, + }, + "textBased": Object { + "layers": Object {}, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "columns": Array [ + Object { + "columnId": "2881fedd-54b7-42ba-8c97-5175dec86166", + "isTransposed": false, + "width": 362, + }, + Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "isTransposed": false, + }, + Object { + "columnId": "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "isTransposed": false, + }, + ], + "layerId": "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "layerType": "data", + }, + }, + "title": "Alerts", + "visualizationType": "lnsDatatable", +} +`; + +exports[`getAlertsTableLensAttributes should render with extra options - filters 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b": Object { + "columnOrder": Array [ + "2881fedd-54b7-42ba-8c97-5175dec86166", + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "f04a71a3-399f-4d32-9efc-8a005e989991", + ], + "columns": Object { + "2881fedd-54b7-42ba-8c97-5175dec86166": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of event.category", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of undefined", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": undefined, + }, + "f04a71a3-399f-4d32-9efc-8a005e989991": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of undefined", + "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, + "scale": "ratio", + "sourceField": undefined, + }, + }, + "incompleteColumns": Object {}, + "sampling": 1, + }, + }, + }, + "textBased": Object { + "layers": Object {}, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + ".alerts-security.alerts-default", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": ".alerts-security.alerts-default", + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "columns": Array [ + Object { + "columnId": "2881fedd-54b7-42ba-8c97-5175dec86166", + "isTransposed": false, + "width": 362, + }, + Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "isTransposed": false, + }, + Object { + "columnId": "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "isTransposed": false, + }, + ], + "layerId": "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "layerType": "data", + }, + }, + "title": "Alerts", + "visualizationType": "lnsDatatable", +} +`; + +exports[`getAlertsTableLensAttributes should render without extra options 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b": Object { + "columnOrder": Array [ + "2881fedd-54b7-42ba-8c97-5175dec86166", + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "f04a71a3-399f-4d32-9efc-8a005e989991", + ], + "columns": Object { + "2881fedd-54b7-42ba-8c97-5175dec86166": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of event.category", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of undefined", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": undefined, + }, + "f04a71a3-399f-4d32-9efc-8a005e989991": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of undefined", + "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, + "scale": "ratio", + "sourceField": undefined, + }, + }, + "incompleteColumns": Object {}, + "sampling": 1, + }, + }, + }, + "textBased": Object { + "layers": Object {}, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "columns": Array [ + Object { + "columnId": "2881fedd-54b7-42ba-8c97-5175dec86166", + "isTransposed": false, + "width": 362, + }, + Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "isTransposed": false, + }, + Object { + "columnId": "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "isTransposed": false, + }, + ], + "layerId": "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "layerType": "data", + }, + }, + "title": "Alerts", + "visualizationType": "lnsDatatable", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/rule_preview.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/rule_preview.test.ts.snap new file mode 100644 index 0000000000000..5a841f2bc942a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/rule_preview.test.ts.snap @@ -0,0 +1,173 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getRulePreviewLensAttributes should render without extra options 1`] = ` +Object { + "description": "", + "references": Array [], + "state": Object { + "adHocDataViews": Object { + "mockInternalReferenceId": Object { + "allowNoIndex": false, + "fieldAttrs": Object {}, + "fieldFormats": Object {}, + "id": "mockInternalReferenceId", + "name": ".preview.alerts-security.alerts-undefined", + "runtimeFieldMap": Object {}, + "sourceFilters": Array [], + "timeFieldName": "@timestamp", + "title": ".preview.alerts-security.alerts-undefined", + }, + }, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "mockLayerId": Object { + "columnOrder": Array [ + "e92c8920-0449-4564-81f4-8945517817a4", + "eba07b4d-766d-49d7-8435-d40367d3d055", + "9c89324b-0c59-4403-9698-d989a09dc5a8", + ], + "columns": Object { + "9c89324b-0c59-4403-9698-d989a09dc5a8": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, + "scale": "ratio", + "sourceField": "___records___", + }, + "e92c8920-0449-4564-81f4-8945517817a4": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top 10 values of event.category", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "9c89324b-0c59-4403-9698-d989a09dc5a8", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 10, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "eba07b4d-766d-49d7-8435-d40367d3d055": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "dropPartials": false, + "includeEmptyRows": true, + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + }, + "incompleteColumns": Object {}, + "sampling": 1, + }, + }, + }, + "textBased": Object { + "layers": Object {}, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "field": "kibana.alert.rule.uuid", + "index": "mockInternalReferenceId", + "key": "kibana.alert.rule.uuid", + "negate": false, + "params": Object { + "query": undefined, + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "kibana.alert.rule.uuid": undefined, + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + ], + "internalReferences": Array [ + Object { + "id": "mockInternalReferenceId", + "name": "indexpattern-datasource-layer-mockLayerId", + "type": "index-pattern", + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": true, + }, + "layers": Array [ + Object { + "accessors": Array [ + "9c89324b-0c59-4403-9698-d989a09dc5a8", + ], + "layerId": "mockLayerId", + "layerType": "data", + "position": "top", + "seriesType": "bar_stacked", + "showGridlines": false, + "splitAccessor": "e92c8920-0449-4564-81f4-8945517817a4", + "xAccessor": "eba07b4d-766d-49d7-8435-d40367d3d055", + }, + ], + "legend": Object { + "isVisible": false, + "position": "left", + }, + "preferredSeriesType": "bar_stacked", + "title": "Empty XY chart", + "valueLabels": "hide", + "valuesInLegend": true, + "yTitle": "", + }, + }, + "title": "Rule preview", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.test.ts new file mode 100644 index 0000000000000..109bf7da68bee --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.test.ts @@ -0,0 +1,63 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { mockExtraFilter, wrapper } from '../../../mocks'; + +import { useLensAttributes } from '../../../use_lens_attributes'; + +import { getAlertsByStatusAttributes } from './alerts_by_status_donut'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('b9b43606-7ff7-46ae-a47c-85bed80fab9a'), +})); + +jest.mock('../../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + dataViewId: 'security-solution-my-test', + indicesExist: true, + selectedPatterns: ['signal-index'], + }), +})); + +jest.mock('../../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + pageName: 'alerts', + }, + ]), +})); + +describe('getAlertsByStatusAttributes', () => { + it('should render without extra options', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getAlertsByStatusAttributes, + stackByField: 'kibana.alert.workflow_status', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); + + it('should render with extra options - filters', () => { + const { result } = renderHook( + () => + useLensAttributes({ + extraOptions: { + filters: mockExtraFilter, + }, + getLensAttributes: getAlertsByStatusAttributes, + stackByField: 'kibana.alert.workflow_status', + }), + { wrapper } + ); + + expect(result?.current?.state.filters).toEqual(expect.arrayContaining(mockExtraFilter)); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.ts index 6875a2c7f6b55..33bc6827fa020 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.ts @@ -4,14 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import type { GetLensAttributes, LensAttributes } from '../../../types'; +import { v4 as uuidv4 } from 'uuid'; +import type { GetLensAttributes } from '../../../types'; +const layerId = uuidv4(); export const getAlertsByStatusAttributes: GetLensAttributes = ( stackByField = 'kibana.alert.workflow_status', extraOptions -) => - ({ +) => { + return { title: 'Alerts', description: '', visualizationType: 'lnsPie', @@ -20,7 +21,7 @@ export const getAlertsByStatusAttributes: GetLensAttributes = ( shape: 'donut', layers: [ { - layerId: '51ed355e-6e23-4038-a417-f653a1160370', + layerId, primaryGroups: ['a9b43606-7ff7-46ae-a47c-85bed80fab9a'], metrics: ['21cc4a49-3780-4b1a-be28-f02fa5303d24'], numberDisplay: 'value', @@ -38,30 +39,35 @@ export const getAlertsByStatusAttributes: GetLensAttributes = ( language: 'kuery', }, filters: [ - { - meta: { - disabled: false, - negate: false, - alias: null, - index: 'a1aaa83b-5026-444e-9465-50e0afade01c', - key: stackByField, - field: stackByField, - params: { - query: extraOptions?.status, - }, - type: 'phrase', - }, - query: { - match_phrase: { - [stackByField]: extraOptions?.status, - }, - }, - }, + ...(extraOptions?.status && stackByField + ? [ + { + meta: { + disabled: false, + negate: false, + alias: null, + index: 'a1aaa83b-5026-444e-9465-50e0afade01c', + key: stackByField, + field: stackByField, + params: { + query: extraOptions?.status, + }, + type: 'phrase', + }, + query: { + match_phrase: { + [stackByField]: extraOptions?.status, + }, + }, + }, + ] + : []), + ...(extraOptions?.filters ? extraOptions.filters : []), ], datasourceStates: { formBased: { layers: { - '51ed355e-6e23-4038-a417-f653a1160370': { + [layerId]: { columns: { 'a9b43606-7ff7-46ae-a47c-85bed80fab9a': { label: 'Filters', @@ -138,7 +144,7 @@ export const getAlertsByStatusAttributes: GetLensAttributes = ( { type: 'index-pattern', id: '{dataViewId}', - name: 'indexpattern-datasource-layer-51ed355e-6e23-4038-a417-f653a1160370', + name: `indexpattern-datasource-layer-${layerId}`, }, { type: 'index-pattern', @@ -146,4 +152,5 @@ export const getAlertsByStatusAttributes: GetLensAttributes = ( id: '{dataViewId}', }, ], - } as LensAttributes); + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.test.ts new file mode 100644 index 0000000000000..bf84e4999faa5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.test.ts @@ -0,0 +1,88 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../../mocks'; + +import { useLensAttributes } from '../../../use_lens_attributes'; + +import { getAlertsHistogramLensAttributes } from './alerts_histogram'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('0039eb0c-9a1a-4687-ae54-0f4e239bec75'), +})); + +jest.mock('../../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + dataViewId: 'security-solution-my-test', + indicesExist: true, + selectedPatterns: ['signal-index'], + }), +})); + +jest.mock('../../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockRule', + pageName: 'rules', + tabName: 'alerts', + }, + ]), +})); + +describe('getAlertsHistogramLensAttributes', () => { + it('should render without extra options', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getAlertsHistogramLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); + + it('should render with extra options - filters', () => { + const { result } = renderHook( + () => + useLensAttributes({ + extraOptions: { + filters: [ + { + meta: { + type: 'phrases', + key: '_index', + params: ['.alerts-security.alerts-default'], + alias: null, + negate: false, + disabled: false, + }, + query: { + bool: { + should: [ + { + match_phrase: { + _index: '.alerts-security.alerts-default', + }, + }, + ], + minimum_should_match: 1, + }, + }, + }, + ], + }, + getLensAttributes: getAlertsHistogramLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts new file mode 100644 index 0000000000000..78b4a134a7620 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts @@ -0,0 +1,127 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import type { GetLensAttributes } from '../../../types'; +const layerId = uuidv4(); + +export const getAlertsHistogramLensAttributes: GetLensAttributes = ( + stackByField = 'kibana.alert.rule.name', + extraOptions +) => { + return { + title: 'Alerts', + description: '', + visualizationType: 'lnsXY', + state: { + visualization: { + title: 'Empty XY chart', + legend: { + isVisible: true, + position: 'left', + legendSize: 'xlarge', + }, + valueLabels: 'hide', + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId, + accessors: ['e09e0380-0740-4105-becc-0a4ca12e3944'], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + layerType: 'data', + xAccessor: 'aac9d7d0-13a3-480a-892b-08207a787926', + splitAccessor: '34919782-4546-43a5-b668-06ac934d3acd', + }, + ], + yRightExtent: { + mode: 'full', + }, + yLeftExtent: { + mode: 'full', + }, + axisTitlesVisibilitySettings: { + x: false, + yLeft: false, + yRight: true, + }, + valuesInLegend: true, + }, + query: { + query: '', + language: 'kuery', + }, + filters: extraOptions?.filters ? extraOptions.filters : [], + datasourceStates: { + formBased: { + layers: { + [layerId]: { + columns: { + 'aac9d7d0-13a3-480a-892b-08207a787926': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { + interval: 'auto', + }, + }, + 'e09e0380-0740-4105-becc-0a4ca12e3944': { + label: 'Count of records', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: '___records___', + }, + '34919782-4546-43a5-b668-06ac934d3acd': { + label: `Top values of ${stackByField}`, + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: stackByField, + isBucketed: true, + params: { + size: 1000, + orderBy: { + type: 'column', + columnId: 'e09e0380-0740-4105-becc-0a4ca12e3944', + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + secondaryFields: [], + }, + }, + }, + columnOrder: [ + '34919782-4546-43a5-b668-06ac934d3acd', + 'aac9d7d0-13a3-480a-892b-08207a787926', + 'e09e0380-0740-4105-becc-0a4ca12e3944', + ], + incompleteColumns: {}, + }, + }, + }, + }, + internalReferences: [], + adHocDataViews: {}, + }, + references: [ + { + type: 'index-pattern', + id: '{dataViewId}', + name: `indexpattern-datasource-layer-${layerId}`, + }, + ], + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.test.ts new file mode 100644 index 0000000000000..e8457e8dfb533 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.test.ts @@ -0,0 +1,100 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../../mocks'; + +import { useLensAttributes } from '../../../use_lens_attributes'; + +import { getAlertsTableLensAttributes } from './alerts_table'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b'), +})); + +jest.mock('../../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + dataViewId: 'security-solution-my-test', + indicesExist: true, + selectedPatterns: ['signal-index'], + }), +})); + +jest.mock('../../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + pageName: 'alerts', + }, + ]), +})); + +describe('getAlertsTableLensAttributes', () => { + it('should render without extra options', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getAlertsTableLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); + + it('should render with extra options - filters', () => { + const { result } = renderHook( + () => + useLensAttributes({ + extraOptions: { + filters: [ + { + meta: { + type: 'phrases', + key: '_index', + params: ['.alerts-security.alerts-default'], + alias: null, + negate: false, + disabled: false, + }, + query: { + bool: { + should: [ + { + match_phrase: { + _index: '.alerts-security.alerts-default', + }, + }, + ], + minimum_should_match: 1, + }, + }, + }, + ], + }, + getLensAttributes: getAlertsTableLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); + + it('should render with extra options - breakdownField', () => { + const { result } = renderHook( + () => + useLensAttributes({ + extraOptions: { breakdownField: 'agent.type' }, + getLensAttributes: getAlertsTableLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.ts new file mode 100644 index 0000000000000..678179855557c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.ts @@ -0,0 +1,137 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import type { GetLensAttributes } from '../../../types'; + +const layerId = uuidv4(); + +export const getAlertsTableLensAttributes: GetLensAttributes = ( + stackByField = 'kibana.alert.rule.name', + extraOptions +) => { + return { + title: 'Alerts', + description: '', + visualizationType: 'lnsDatatable', + state: { + visualization: { + columns: [ + { + columnId: '2881fedd-54b7-42ba-8c97-5175dec86166', + isTransposed: false, + width: 362, + }, + { + columnId: 'f04a71a3-399f-4d32-9efc-8a005e989991', + isTransposed: false, + }, + { + columnId: '75ce269b-ee9c-4c7d-a14e-9226ba0fe059', + isTransposed: false, + }, + ], + layerId, + layerType: 'data', + }, + query: { + query: '', + language: 'kuery', + }, + filters: extraOptions?.filters ? extraOptions.filters : [], + datasourceStates: { + formBased: { + layers: { + [layerId]: { + columns: { + '2881fedd-54b7-42ba-8c97-5175dec86166': { + label: `Top values of ${stackByField}`, + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: stackByField, + isBucketed: true, + params: { + size: 1000, + orderBy: { + type: 'column', + columnId: 'f04a71a3-399f-4d32-9efc-8a005e989991', + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + include: [], + exclude: [], + includeIsRegex: false, + excludeIsRegex: false, + }, + }, + 'f04a71a3-399f-4d32-9efc-8a005e989991': { + label: `Count of ${extraOptions?.breakdownField}`, + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: extraOptions?.breakdownField, + params: { + emptyAsNull: true, + }, + }, + '75ce269b-ee9c-4c7d-a14e-9226ba0fe059': { + label: `Top values of ${extraOptions?.breakdownField}`, + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: extraOptions?.breakdownField, + isBucketed: true, + params: { + size: 1000, + orderBy: { + type: 'column', + columnId: 'f04a71a3-399f-4d32-9efc-8a005e989991', + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + include: [], + exclude: [], + includeIsRegex: false, + excludeIsRegex: false, + }, + }, + }, + columnOrder: [ + '2881fedd-54b7-42ba-8c97-5175dec86166', + '75ce269b-ee9c-4c7d-a14e-9226ba0fe059', + 'f04a71a3-399f-4d32-9efc-8a005e989991', + ], + sampling: 1, + incompleteColumns: {}, + }, + }, + }, + textBased: { + layers: {}, + }, + }, + internalReferences: [], + adHocDataViews: {}, + }, + references: [ + { + type: 'index-pattern', + id: '{dataViewId}', + name: `indexpattern-datasource-layer-${layerId}`, + }, + ], + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.test.ts new file mode 100644 index 0000000000000..85b4a11bbc7f9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.test.ts @@ -0,0 +1,70 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { mockRulePreviewFilter, wrapper } from '../../../mocks'; + +import { useLensAttributes } from '../../../use_lens_attributes'; + +import { getRulePreviewLensAttributes } from './rule_preview'; +const mockInternalReferenceId = 'mockInternalReferenceId'; +const mockRuleId = 'mockRuleId'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValueOnce('mockLayerId').mockReturnValueOnce('mockInternalReferenceId'), +})); + +jest.mock('../../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + dataViewId: 'security-solution-my-test', + indicesExist: true, + selectedPatterns: ['signal-index'], + }), +})); + +jest.mock('../../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + pageName: 'alerts', + }, + ]), +})); + +describe('getRulePreviewLensAttributes', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should render without extra options', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getRulePreviewLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); + + it('should render with extra options - filters', () => { + const { result } = renderHook( + () => + useLensAttributes({ + extraOptions: { + ruleId: mockRuleId, + }, + getLensAttributes: getRulePreviewLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current?.state.filters).toEqual( + expect.arrayContaining(mockRulePreviewFilter(mockInternalReferenceId, mockRuleId)) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.ts new file mode 100644 index 0000000000000..33d59c358ea5f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.ts @@ -0,0 +1,167 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import type { GetLensAttributes } from '../../../types'; + +const layerId = uuidv4(); +const internalReferenceId = uuidv4(); + +export const getRulePreviewLensAttributes: GetLensAttributes = ( + stackByField = 'event.category', + extraOptions +) => { + return { + title: 'Rule preview', + description: '', + visualizationType: 'lnsXY', + state: { + visualization: { + title: 'Empty XY chart', + legend: { + isVisible: false, + position: 'left', + }, + valueLabels: 'hide', + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId, + accessors: ['9c89324b-0c59-4403-9698-d989a09dc5a8'], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + layerType: 'data', + xAccessor: 'eba07b4d-766d-49d7-8435-d40367d3d055', + splitAccessor: 'e92c8920-0449-4564-81f4-8945517817a4', + }, + ], + valuesInLegend: true, + yTitle: '', + axisTitlesVisibilitySettings: { + x: false, + yLeft: false, + yRight: true, + }, + }, + query: { + query: '', + language: 'kuery', + }, + filters: [ + { + meta: { + disabled: false, + negate: false, + alias: null, + index: internalReferenceId, + key: 'kibana.alert.rule.uuid', + field: 'kibana.alert.rule.uuid', + params: { + query: extraOptions?.ruleId, + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'kibana.alert.rule.uuid': extraOptions?.ruleId, + }, + }, + }, + ], + datasourceStates: { + formBased: { + layers: { + [layerId]: { + columns: { + '9c89324b-0c59-4403-9698-d989a09dc5a8': { + label: 'Count of records', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: '___records___', + params: { + emptyAsNull: true, + }, + }, + 'eba07b4d-766d-49d7-8435-d40367d3d055': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { + interval: 'auto', + includeEmptyRows: true, + dropPartials: false, + }, + }, + 'e92c8920-0449-4564-81f4-8945517817a4': { + label: `Top 10 values of ${stackByField}`, + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: stackByField, + isBucketed: true, + params: { + size: 10, + orderBy: { + type: 'column', + columnId: '9c89324b-0c59-4403-9698-d989a09dc5a8', + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + include: [], + exclude: [], + includeIsRegex: false, + excludeIsRegex: false, + }, + }, + }, + columnOrder: [ + 'e92c8920-0449-4564-81f4-8945517817a4', + 'eba07b4d-766d-49d7-8435-d40367d3d055', + '9c89324b-0c59-4403-9698-d989a09dc5a8', + ], + sampling: 1, + incompleteColumns: {}, + }, + }, + }, + textBased: { + layers: {}, + }, + }, + internalReferences: [ + { + type: 'index-pattern', + id: internalReferenceId, + name: `indexpattern-datasource-layer-${layerId}`, + }, + ], + adHocDataViews: { + [internalReferenceId]: { + id: internalReferenceId, + title: `.preview.alerts-security.alerts-${extraOptions?.spaceId}`, + timeFieldName: '@timestamp', + sourceFilters: [], + fieldFormats: {}, + runtimeFieldMap: {}, + fieldAttrs: {}, + allowNoIndex: false, + name: `.preview.alerts-security.alerts-${extraOptions?.spaceId}`, + }, + }, + }, + references: [], + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.ts index 4e69bac6287ec..4378b74400aa2 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.ts @@ -20,7 +20,7 @@ export const authenticationLensAttributes: LensAttributes = { title: 'Empty XY chart', legend: { isVisible: true, - position: 'right', + position: 'left', }, valueLabels: 'hide', preferredSeriesType: 'bar_stacked', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/event.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/event.test.ts index 87d246fc2350b..a9a1c3951de5a 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/event.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/event.test.ts @@ -12,6 +12,10 @@ import { useLensAttributes } from '../../use_lens_attributes'; import { getEventsHistogramLensAttributes } from './events'; +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('0039eb0c-9a1a-4687-ae54-0f4e239bec75'), +})); + jest.mock('../../../../containers/sourcerer', () => ({ useSourcererDataView: jest.fn().mockReturnValue({ selectedPatterns: ['auditbeat-mytest-*'], diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts index e48f6aa6c1a87..61e9bac0cb3ac 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts @@ -4,13 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { v4 as uuidv4 } from 'uuid'; +import type { GetLensAttributes } from '../../types'; -import type { GetLensAttributes, LensAttributes } from '../../types'; +const layerId = uuidv4(); export const getEventsHistogramLensAttributes: GetLensAttributes = ( stackByField = 'event.action' -) => - ({ +) => { + return { title: 'Events', description: '', visualizationType: 'lnsXY', @@ -19,13 +21,14 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = ( title: 'Empty XY chart', legend: { isVisible: true, - position: 'right', + position: 'left', + legendSize: 'xlarge', }, valueLabels: 'hide', preferredSeriesType: 'bar_stacked', layers: [ { - layerId: '0039eb0c-9a1a-4687-ae54-0f4e239bec75', + layerId, accessors: ['e09e0380-0740-4105-becc-0a4ca12e3944'], position: 'top', seriesType: 'bar_stacked', @@ -55,7 +58,7 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = ( datasourceStates: { formBased: { layers: { - '0039eb0c-9a1a-4687-ae54-0f4e239bec75': { + [layerId]: { columns: { 'aac9d7d0-13a3-480a-892b-08207a787926': { label: '@timestamp', @@ -113,7 +116,8 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = ( { type: 'index-pattern', id: '{dataViewId}', - name: 'indexpattern-datasource-layer-0039eb0c-9a1a-4687-ae54-0f4e239bec75', + name: `indexpattern-datasource-layer-${layerId}`, }, ], - } as LensAttributes); + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts index f5a664b98161b..44aa790332ba0 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts @@ -20,7 +20,7 @@ export const getExternalAlertLensAttributes: GetLensAttributes = ( title: 'Empty XY chart', legend: { isVisible: true, - position: 'right', + position: 'left', }, valueLabels: 'hide', preferredSeriesType: 'bar_stacked', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap index 38ee4c908fce0..a261abe99ffcf 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap @@ -231,7 +231,7 @@ Object { ], "legend": Object { "isVisible": true, - "position": "right", + "position": "left", }, "preferredSeriesType": "bar", "tickLabelsVisibilitySettings": Object { diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts index 0f195bdeaa8d4..2e9ff92261518 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts @@ -17,7 +17,7 @@ export const dnsTopDomainsLensAttributes: LensAttributes = { visualization: { legend: { isVisible: true, - position: 'right', + position: 'left', }, valueLabels: 'hide', fittingFunction: 'None', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx index 985a9881d330b..19805b8ce96f3 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx @@ -31,7 +31,7 @@ const LensComponentWrapper = styled.div<{ height?: string; width?: string }>` background-color: transparent; } .expExpressionRenderer__expression { - padding: 0 !important; + padding: 2px 0 0 0 !important; } .legacyMtrVis__container { padding: 0; @@ -48,8 +48,6 @@ const initVisualizationData: { isLoading: true, }; -const style = { height: '100%', minWidth: '100px' }; - const LensEmbeddableComponent: React.FC = ({ applyGlobalQueriesAndFilters = true, extraActions, @@ -65,7 +63,16 @@ const LensEmbeddableComponent: React.FC = ({ stackByField, timerange, width: wrapperWidth, + withActions = true, }) => { + const style = useMemo( + () => ({ + height: wrapperHeight ?? '100%', + minWidth: '100px', + width: wrapperWidth ?? '100%', + }), + [wrapperHeight, wrapperWidth] + ); const { lens } = useKibana().services; const dispatch = useDispatch(); const [isShowingModal, setIsShowingModal] = useState(false); @@ -81,7 +88,6 @@ const LensEmbeddableComponent: React.FC = ({ stackByField, title: '', }); - const LensComponent = lens.EmbeddableComponent; const inspectActionProps = useMemo( () => ({ @@ -98,7 +104,7 @@ const LensEmbeddableComponent: React.FC = ({ extraActions, inspectActionProps, timeRange: timerange, - withActions: true, + withActions, }); const handleCloseModal = useCallback(() => { @@ -165,6 +171,10 @@ const LensEmbeddableComponent: React.FC = ({ [attributes?.state?.adHocDataViews] ); + if (!searchSessionId) { + return null; + } + if ( !attributes || (visualizationData?.responses != null && visualizationData?.responses?.length === 0) diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/mocks.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/mocks.tsx index e0f6a5ab56d6d..77fa5b02cede0 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/mocks.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/mocks.tsx @@ -129,3 +129,50 @@ export const mockAttributes: LensAttributes = { }, ], }; + +export const mockExtraFilter = [ + { + meta: { + type: 'phrases', + key: '_index', + params: ['.alerts-security.alerts-default'], + alias: null, + negate: false, + disabled: false, + }, + query: { + bool: { + should: [ + { + match_phrase: { + _index: '.alerts-security.alerts-default', + }, + }, + ], + minimum_should_match: 1, + }, + }, + }, +]; + +export const mockRulePreviewFilter = (internalReferenceId: string, ruleId: string) => [ + { + meta: { + disabled: false, + negate: false, + alias: null, + index: internalReferenceId, + key: 'kibana.alert.rule.uuid', + field: 'kibana.alert.rule.uuid', + params: { + query: ruleId, + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'kibana.alert.rule.uuid': ruleId, + }, + }, + }, +]; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/types.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/types.ts index 5ef9b3eda38b4..b761aba812100 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/types.ts @@ -57,6 +57,7 @@ export interface LensEmbeddableComponentProps { stackByField?: string; timerange: { from: string; to: string }; width?: string; + withActions?: boolean; } export enum RequestStatus { diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.ts index 9c8ccdfc51cfb..0de49b52d66ff 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.ts @@ -66,11 +66,11 @@ describe(`useActions`, () => { expect(result.current[0].id).toEqual('inspect'); expect(result.current[0].order).toEqual(4); - expect(result.current[1].id).toEqual('openInLens'); + expect(result.current[1].id).toEqual('addToNewCase'); expect(result.current[1].order).toEqual(3); - expect(result.current[2].id).toEqual('addToNewCase'); + expect(result.current[2].id).toEqual('addToExistingCase'); expect(result.current[2].order).toEqual(2); - expect(result.current[3].id).toEqual('addToExistingCase'); + expect(result.current[3].id).toEqual('openInLens'); expect(result.current[3].order).toEqual(1); }); @@ -110,11 +110,11 @@ describe(`useActions`, () => { expect(result.current[0].id).toEqual('inspect'); expect(result.current[0].order).toEqual(4); - expect(result.current[1].id).toEqual('openInLens'); + expect(result.current[1].id).toEqual('addToNewCase'); expect(result.current[1].order).toEqual(3); - expect(result.current[2].id).toEqual('addToNewCase'); + expect(result.current[2].id).toEqual('addToExistingCase'); expect(result.current[2].order).toEqual(2); - expect(result.current[3].id).toEqual('addToExistingCase'); + expect(result.current[3].id).toEqual('openInLens'); expect(result.current[3].order).toEqual(1); expect(result.current[4].id).toEqual('mockExtraAction'); expect(result.current[4].order).toEqual(0); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.ts index eb8097ee77ade..504f30511cafc 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.ts @@ -34,9 +34,9 @@ export const useActions = ({ const { navigateToPrefilledEditor } = lens; const [defaultActions, setDefaultActions] = useState([ 'inspect', - 'openInLens', 'addToNewCase', 'addToExistingCase', + 'openInLens', ]); useEffect(() => { @@ -74,7 +74,7 @@ export const useActions = ({ const actions = useMemo( () => - defaultActions.reduce((acc, action) => { + defaultActions?.reduce((acc, action) => { if (action === 'inspect' && inspectActionProps != null) { return [ ...acc, @@ -141,7 +141,7 @@ const getOpenInLensAction = ({ callback }: { callback: () => void }): Action => async execute(context: ActionExecutionContext): Promise { callback(); }, - order: 3, + order: 1, }; }; @@ -168,7 +168,7 @@ const getAddToNewCaseAction = ({ callback(); }, disabled, - order: 2, + order: 3, }; }; @@ -222,6 +222,6 @@ const getAddToExistingCaseAction = ({ callback(); }, disabled, - order: 1, + order: 2, }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx index 7451209120e06..6fd82d4b0e1e9 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx @@ -205,6 +205,45 @@ describe('useLensAttributes', () => { expect(result?.current).toBeNull(); }); + it('should return null if stackByField is an empty string', () => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + dataViewId: 'security-solution-default', + indicesExist: false, + selectedPatterns: ['auditbeat-*'], + }); + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getExternalAlertLensAttributes, + stackByField: '', + }), + { wrapper } + ); + + expect(result?.current).toBeNull(); + }); + + it('should return null if extraOptions.breakDownField is an empty string', () => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + dataViewId: 'security-solution-default', + indicesExist: false, + selectedPatterns: ['auditbeat-*'], + }); + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getExternalAlertLensAttributes, + stackByField: 'kibana.alert.rule.name', + extraOptions: { + breakdownField: '', + }, + }), + { wrapper } + ); + + expect(result?.current).toBeNull(); + }); + it('should return Lens attributes if adHocDataViews exist', () => { (useSourcererDataView as jest.Mock).mockReturnValue({ dataViewId: 'security-solution-default', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx index 1976a743e5fa1..9b5ef16dddd22 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx @@ -89,7 +89,13 @@ export const useLensAttributes = ({ const hasAdHocDataViews = Object.values(attrs?.state?.adHocDataViews ?? {}).length > 0; const lensAttrsWithInjectedData = useMemo(() => { - if (lensAttributes == null && (getLensAttributes == null || stackByField == null)) { + if ( + lensAttributes == null && + (getLensAttributes == null || + stackByField == null || + stackByField?.length === 0 || + (extraOptions?.breakdownField != null && extraOptions?.breakdownField.length === 0)) + ) { return null; } @@ -117,6 +123,7 @@ export const useLensAttributes = ({ applyGlobalQueriesAndFilters, attrs, dataViewId, + extraOptions?.breakdownField, filters, getLensAttributes, hasAdHocDataViews, diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.test.tsx index c53835c0f86b9..ee77776e60a91 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import type { RenderResult } from '@testing-library/react'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric'; import { VisualizationEmbeddable } from './visualization_embeddable'; import * as inputActions from '../../store/inputs/actions'; @@ -29,11 +29,12 @@ jest.mock('./lens_embeddable'); jest.mock('../page/use_refetch_by_session', () => ({ useRefetchByRestartingSession: jest.fn(), })); - +jest.useFakeTimers(); let res: RenderResult; const mockSearchSessionId = 'mockSearchSessionId'; const mockSearchSessionIdDefault = 'mockSearchSessionIdDefault'; const mockRefetchByRestartingSession = jest.fn(); +const mockRefetchByDeletingSession = jest.fn(); const mockSetQuery = jest.spyOn(inputActions, 'setQuery'); const mockDeleteQuery = jest.spyOn(inputActions, 'deleteOneQuery'); const state: State = { @@ -41,6 +42,7 @@ const state: State = { }; const { storage } = createSecuritySolutionStorageMock(); const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + describe('VisualizationEmbeddable', () => { describe('when isDonut = false', () => { beforeEach(() => { @@ -55,6 +57,7 @@ describe('VisualizationEmbeddable', () => { }, }, refetchByRestartingSession: mockRefetchByRestartingSession, + refetchByDeletingSession: mockRefetchByDeletingSession, }); res = render( @@ -71,15 +74,16 @@ describe('VisualizationEmbeddable', () => { expect(res.getByTestId('lens-embeddable')).toBeInTheDocument(); }); - it('should set query', () => { - expect(mockSetQuery).toHaveBeenCalledTimes(1); - expect(mockSetQuery).toHaveBeenCalledWith({ - inputId: InputsModelId.global, - id: 'testId', - searchSessionId: mockSearchSessionId, - refetch: mockRefetchByRestartingSession, - loading: false, - inspect: null, + it('should refetch by delete session when no data exists', async () => { + await waitFor(() => { + expect(mockSetQuery).toHaveBeenCalledWith({ + inputId: InputsModelId.global, + id: 'testId', + searchSessionId: mockSearchSessionId, + refetch: mockRefetchByDeletingSession, + loading: false, + inspect: null, + }); }); }); @@ -92,6 +96,73 @@ describe('VisualizationEmbeddable', () => { }); }); + describe('when data exists', () => { + const mockState = { + ...mockGlobalState, + inputs: { + ...mockGlobalState.inputs, + global: { + ...mockGlobalState.inputs.global, + queries: [ + { + id: 'testId', + inspect: { + dsl: [], + response: [ + '{\n "took": 4,\n "timed_out": false,\n "_shards": {\n "total": 3,\n "successful": 3,\n "skipped": 2,\n "failed": 0\n },\n "hits": {\n "total": 21300,\n "max_score": null,\n "hits": []\n },\n "aggregations": {\n "0": {\n "buckets": {\n "Critical": {\n "doc_count": 0\n },\n "High": {\n "doc_count": 0\n },\n "Low": {\n "doc_count": 21300\n },\n "Medium": {\n "doc_count": 0\n }\n }\n }\n }\n}', + ], + }, + isInspected: false, + loading: false, + selectedInspectIndex: 0, + searchSessionId: undefined, + refetch: jest.fn(), + }, + ], + }, + }, + }; + const mockStore = createStore(mockState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + beforeEach(() => { + jest.clearAllMocks(); + (useRefetchByRestartingSession as jest.Mock).mockReturnValue({ + session: { + current: { + start: jest + .fn() + .mockReturnValueOnce(mockSearchSessionId) + .mockReturnValue(mockSearchSessionIdDefault), + }, + }, + refetchByRestartingSession: mockRefetchByRestartingSession, + refetchByDeletingSession: mockRefetchByDeletingSession, + }); + res = render( + + + + ); + }); + + it('should refetch by restart session', async () => { + await waitFor(() => { + expect(mockSetQuery).toHaveBeenCalledWith({ + inputId: InputsModelId.global, + id: 'testId', + searchSessionId: mockSearchSessionId, + refetch: mockRefetchByRestartingSession, + loading: false, + inspect: null, + }); + }); + }); + }); + describe('when isDonut = true', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx index 7ec7c9ee168ac..9bf8d14d15336 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { css } from 'styled-components'; import { ChartLabel } from '../../../overview/components/detection_response/alerts_by_status/chart_label'; @@ -18,6 +18,7 @@ import { InputsModelId } from '../../store/inputs/constants'; import { useRefetchByRestartingSession } from '../page/use_refetch_by_session'; import { LensEmbeddable } from './lens_embeddable'; import type { EmbeddableData, VisualizationEmbeddableProps } from './types'; +import { useSourcererDataView } from '../../containers/sourcerer'; const VisualizationEmbeddableComponent: React.FC = (props) => { const dispatch = useDispatch(); @@ -28,18 +29,22 @@ const VisualizationEmbeddableComponent: React.FC = label, donutTextWrapperClassName, onLoad, - ...lensPorps + ...lensProps } = props; - const { session, refetchByRestartingSession } = useRefetchByRestartingSession({ - inputId, - queryId: id, - }); + const { session, refetchByRestartingSession, refetchByDeletingSession } = + useRefetchByRestartingSession({ + inputId, + queryId: id, + }); + const { indicesExist } = useSourcererDataView(lensProps.scopeId); + + const memorizedTimerange = useRef(lensProps.timerange); const getGlobalQuery = inputsSelectors.globalQueryByIdSelector(); - const { inspect } = useDeepEqualSelector((state) => getGlobalQuery(state, id)); + const { inspect, searchSessionId } = useDeepEqualSelector((state) => getGlobalQuery(state, id)); const visualizationData = inspect?.response ? parseVisualizationData(inspect?.response) : null; - const dataExists = visualizationData != null && visualizationData[0]?.hits.total !== 0; + const dataExists = visualizationData != null && visualizationData[0]?.hits?.total !== 0; const donutTextWrapperStyles = dataExists ? css` top: 40%; @@ -70,17 +75,42 @@ const VisualizationEmbeddableComponent: React.FC = ); useEffect(() => { - dispatch( - inputsActions.setQuery({ - inputId, - id, - searchSessionId: session.current.start(), - refetch: refetchByRestartingSession, - loading: false, - inspect: null, - }) - ); - }, [dispatch, inputId, id, refetchByRestartingSession, session]); + // This handles timerange update when (alert) indices not found + if ( + (!indicesExist && memorizedTimerange.current?.from !== lensProps.timerange.from) || + memorizedTimerange.current?.to !== lensProps.timerange.to + ) { + memorizedTimerange.current = lensProps.timerange; + dispatch(inputsActions.deleteOneQuery({ inputId, id })); + } + }, [dispatch, id, indicesExist, inputId, lensProps.timerange]); + + useEffect(() => { + // This handles initial mount and refetch when (alert) indices not found + if (!searchSessionId) { + setTimeout(() => { + dispatch( + inputsActions.setQuery({ + inputId, + id, + searchSessionId: session.current.start(), + refetch: dataExists ? refetchByRestartingSession : refetchByDeletingSession, + loading: false, + inspect: null, + }) + ); + }, 200); + } + }, [ + dispatch, + inputId, + id, + session, + dataExists, + refetchByRestartingSession, + searchSessionId, + refetchByDeletingSession, + ]); useEffect(() => { return () => { @@ -88,22 +118,26 @@ const VisualizationEmbeddableComponent: React.FC = }; }, [dispatch, id, inputId]); + if ((!lensProps.getLensAttributes && !lensProps.lensAttributes) || !lensProps.timerange) { + return null; + } + if (isDonut) { return ( : null} + title={dataExists ? : null} donutTextWrapperClassName={donutTextWrapperClassName} donutTextWrapperStyles={donutTextWrapperStyles} > - + ); } - return ; + return ; }; export const VisualizationEmbeddable = React.memo(VisualizationEmbeddableComponent); diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index 85512855580c3..f75385fdd4955 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -249,7 +249,10 @@ export const useMatrixHistogramCombined = ( const [missingDataLoading, missingDataResponse] = useMatrixHistogram({ ...matrixHistogramQueryProps, includeMissingData: false, - skip: skipMissingData || matrixHistogramQueryProps.filterQuery === undefined, + skip: + skipMissingData || + matrixHistogramQueryProps.filterQuery === undefined || + matrixHistogramQueryProps.skip, }); const combinedLoading = useMemo( diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx index 4f538b64b31ee..6c2e3fc008cfb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx @@ -12,8 +12,8 @@ import styled from 'styled-components'; import { useUiSetting$ } from '../../../../common/lib/kibana'; import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; -import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; import type { AlertsCountAggregation } from './types'; +import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; import { getMaxRiskSubAggregations, getUpToMaxBuckets, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/chart_content.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/chart_content.tsx new file mode 100644 index 0000000000000..93b3f92642941 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/chart_content.tsx @@ -0,0 +1,61 @@ +/* + * 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 React from 'react'; +import type { VisualizationEmbeddableProps } from '../../../../common/components/visualization_actions/types'; +import { VisualizationEmbeddable } from '../../../../common/components/visualization_actions/visualization_embeddable'; +import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; +import { AlertsCount } from './alerts_count'; +import type { AlertsCountAggregation } from './types'; + +type ChartContentProps = { + isChartEmbeddablesEnabled: boolean; +} & VisualizationEmbeddableProps & { + isLoadingAlerts: boolean; + alertsData: AlertSearchResponse | null; + stackByField0: string; + stackByField1: string | undefined; + }; + +const ChartContentComponent = ({ + alertsData, + extraActions, + extraOptions, + getLensAttributes, + height, + id, + inspectTitle, + isChartEmbeddablesEnabled, + isLoadingAlerts, + scopeId, + stackByField0, + stackByField1, + timerange, +}: ChartContentProps) => { + return isChartEmbeddablesEnabled ? ( + + ) : alertsData != null ? ( + + ) : null; +}; + +export const ChartContent = React.memo(ChartContentComponent); 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 fc752b243f9b1..cc2e5ca8c78d0 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 @@ -10,12 +10,15 @@ import { waitFor, act } from '@testing-library/react'; import { mount } from 'enzyme'; import { AlertsCountPanel } from '.'; + +import type { Status } from '../../../../../common/detection_engine/schemas/common'; import { useQueryToggle } from '../../../../common/containers/query_toggle'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { DEFAULT_STACK_BY_FIELD, DEFAULT_STACK_BY_FIELD1 } from '../common/config'; import { TestProviders } from '../../../../common/mock'; import { ChartContextMenu } from '../../../pages/detection_engine/chart_panels/chart_context_menu'; import { TABLE } from '../../../pages/detection_engine/chart_panels/chart_select/translations'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; const from = '2022-07-28T08:20:18.966Z'; const to = '2022-07-28T08:20:18.966Z'; @@ -50,18 +53,30 @@ jest.mock('../../../containers/detection_engine/alerts/use_query', () => { }; }); -describe('AlertsCountPanel', () => { - const defaultProps = { - inspectTitle: TABLE, - signalIndexName: 'signalIndexName', - stackByField0: DEFAULT_STACK_BY_FIELD, - stackByField1: DEFAULT_STACK_BY_FIELD1, - setStackByField0: jest.fn(), - setStackByField1: jest.fn(), - }; - const mockSetToggle = jest.fn(); - const mockUseQueryToggle = useQueryToggle as jest.Mock; +jest.mock('../../../../common/hooks/use_experimental_features'); +jest.mock('../../../../common/components/page/use_refetch_by_session'); +jest.mock('../../../../common/components/visualization_actions/lens_embeddable'); +jest.mock('../../../../common/components/page/use_refetch_by_session'); +jest.mock('../common/hooks', () => ({ + useInspectButton: jest.fn(), + useStackByFields: jest.fn(), +})); + +const defaultProps = { + inspectTitle: TABLE, + setStackByField0: jest.fn(), + setStackByField1: 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(); +describe('AlertsCountPanel', () => { beforeEach(() => { jest.clearAllMocks(); mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); @@ -197,3 +212,35 @@ describe('AlertsCountPanel', () => { }); }); }); + +describe('when isChartEmbeddablesEnabled = true', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + }); + + it('renders LensEmbeddable', async () => { + await act(async () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="lens-embeddable"]').exists()).toBeTruthy(); + }); + }); + + it('should skip calling getAlertsRiskQuery', async () => { + await act(async () => { + mount( + + + + ); + expect(mockUseQueryAlerts.mock.calls[0][0].skip).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 d35960cd2777f..c0b4d8bff6dfa 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 @@ -6,6 +6,7 @@ */ import type { EuiComboBox } from '@elastic/eui'; +import type { Action } from '@kbn/ui-actions-plugin/public'; import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; import React, { memo, useMemo, useState, useEffect, useCallback } from 'react'; import { v4 as uuidv4 } from 'uuid'; @@ -21,22 +22,27 @@ import { InspectButtonContainer } from '../../../../common/components/inspect'; import { getAlertsCountQuery } from './helpers'; import * as i18n from './translations'; -import { AlertsCount } from './alerts_count'; import type { AlertsCountAggregation } from './types'; import { KpiPanel } from '../common/components'; import { useInspectButton } from '../common/hooks'; import { useQueryToggle } from '../../../../common/containers/query_toggle'; import { FieldSelection } from '../../../../common/components/field_selection'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { getAlertsTableLensAttributes as getLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/common/alerts/alerts_table'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { ChartContent } from './chart_content'; export const DETECTIONS_ALERTS_COUNT_ID = 'detections-alerts-count'; interface AlertsCountPanelProps { alignHeader?: 'center' | 'baseline' | 'stretch' | 'flexStart' | 'flexEnd'; chartOptionsContextMenu?: (queryId: string) => React.ReactNode; + extraActions?: Action[]; filters?: Filter[]; inspectTitle: string; panelHeight?: number; query?: Query; + runtimeMappings?: MappingRuntimeFields; setStackByField0: (stackBy: string) => void; setStackByField0ComboboxInputRef?: (inputRef: HTMLInputElement | null) => void; setStackByField1: (stackBy: string | undefined) => void; @@ -48,13 +54,14 @@ interface AlertsCountPanelProps { stackByField1ComboboxRef?: React.RefObject>; stackByWidth?: number; title?: React.ReactNode; - runtimeMappings?: MappingRuntimeFields; } +const CHART_HEIGHT = '180px'; export const AlertsCountPanel = memo( ({ alignHeader, chartOptionsContextMenu, + extraActions, filters, inspectTitle, panelHeight, @@ -100,14 +107,24 @@ export const AlertsCountPanel = memo( setQuerySkip(!toggleStatus); }, [toggleStatus]); const toggleQuery = useCallback( - (status: boolean) => { - setToggleStatus(status); + (newToggleStatus: boolean) => { + setToggleStatus(newToggleStatus); // toggle on = skipQuery false - setQuerySkip(!status); + setQuerySkip(!newToggleStatus); }, [setQuerySkip, setToggleStatus] ); + const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled'); + const timerange = useMemo(() => ({ from, to }), [from, to]); + + const extraVisualizationOptions = useMemo( + () => ({ + breakdownField: stackByField1, + filters, + }), + [filters, stackByField1] + ); const { loading: isLoadingAlerts, data: alertsData, @@ -125,7 +142,7 @@ export const AlertsCountPanel = memo( runtimeMappings, }), indexName: signalIndexName, - skip: querySkip, + skip: querySkip || isChartEmbeddablesEnabled, queryName: ALERTS_QUERY_NAMES.COUNT, }); @@ -151,13 +168,13 @@ export const AlertsCountPanel = memo( ]); useInspectButton({ - setQuery, - response, - request, - refetch, - uniqueQueryId, deleteQuery, loading: isLoadingAlerts, + refetch, + request, + response, + setQuery, + uniqueQueryId, }); return ( @@ -181,7 +198,9 @@ export const AlertsCountPanel = memo( toggleQuery={toggleQuery} > ( uniqueQueryId={uniqueQueryId} /> - {toggleStatus && alertsData != null && ( - - )} + ) : null} ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx index c47353aa04b06..a90b86dc4f4cd 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx @@ -21,6 +21,7 @@ import * as helpers from './helpers'; import { mockAlertSearchResponse } from './mock_data'; import { ChartContextMenu } from '../../../pages/detection_engine/chart_panels/chart_context_menu'; import { AlertsHistogramPanel, LEGEND_WITH_COUNTS_WIDTH } from '.'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; jest.mock('../../../../common/containers/query_toggle'); @@ -95,11 +96,25 @@ jest.mock('../../../containers/detection_engine/alerts/use_query', () => { useQueryAlerts: (...props: unknown[]) => mockUseQueryAlerts(...props), }; }); +jest.mock('../../../../common/hooks/use_experimental_features'); +jest.mock('../../../../common/components/page/use_refetch_by_session'); +jest.mock('../../../../common/components/visualization_actions/lens_embeddable'); + +jest.mock('../../../../common/components/page/use_refetch_by_session'); +jest.mock('../common/hooks', () => { + const actual = jest.requireActual('../common/hooks'); + return { + ...actual, + useInspectButton: jest.fn(), + }; +}); describe('AlertsHistogramPanel', () => { const defaultProps = { - signalIndexName: 'signalIndexName', setQuery: jest.fn(), + showBuildingBlockAlerts: false, + showOnlyThreatIndicatorAlerts: false, + signalIndexName: 'signalIndexName', updateDateRange: jest.fn(), }; @@ -698,4 +713,36 @@ describe('AlertsHistogramPanel', () => { }); }); }); + + describe('when isChartEmbeddablesEnabled = true', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + }); + + it('renders LensEmbeddable', async () => { + await act(async () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="lens-embeddable"]').exists()).toBeTruthy(); + }); + }); + + it('should skip calling getAlertsRiskQuery', async () => { + await act(async () => { + mount( + + + + ); + expect(mockUseQueryAlerts.mock.calls[0][0].skip).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 96f5b938a136a..c948eb26a1ba8 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 @@ -6,6 +6,7 @@ */ import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; +import type { Action } from '@kbn/ui-actions-plugin/public'; import type { Position } from '@elastic/charts'; import type { EuiComboBox, EuiTitleSize } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiToolTip } from '@elastic/eui'; @@ -50,6 +51,10 @@ import { KpiPanel, StackByComboBox } from '../common/components'; import { useInspectButton } from '../common/hooks'; import { useQueryToggle } from '../../../../common/containers/query_toggle'; import { GROUP_BY_TOP_LABEL } from '../common/translations'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { getAlertsHistogramLensAttributes as getLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { VisualizationEmbeddable } from '../../../../common/components/visualization_actions/visualization_embeddable'; const defaultTotalAlertsObj: AlertsTotal = { value: 0, @@ -68,6 +73,8 @@ const OptionsFlexItem = styled(EuiFlexItem)` export const LEGEND_WITH_COUNTS_WIDTH = 300; // px +const ChartHeight = '170px'; + interface AlertsHistogramPanelProps { alignHeader?: 'center' | 'baseline' | 'stretch' | 'flexStart' | 'flexEnd'; chartHeight?: number; @@ -75,31 +82,32 @@ interface AlertsHistogramPanelProps { combinedQueries?: string; comboboxRef?: React.RefObject>; defaultStackByOption?: string; + extraActions?: Action[]; filters?: Filter[]; headerChildren?: React.ReactNode; inspectTitle?: string; + legendPosition?: Position; onFieldSelected?: (field: string) => void; /** Override all defaults, and only display this field */ onlyField?: AlertsStackByField; paddingSize?: 's' | 'm' | 'l' | 'none'; panelHeight?: number; - titleSize?: EuiTitleSize; query?: Query; - legendPosition?: Position; + runtimeMappings?: MappingRuntimeFields; setComboboxInputRef?: (inputRef: HTMLInputElement | null) => void; - signalIndexName: string | null; showCountsInLegend?: boolean; showGroupByPlaceholder?: boolean; showLegend?: boolean; showLinkToAlerts?: boolean; - showTotalAlertsCount?: boolean; showStackBy?: boolean; + showTotalAlertsCount?: boolean; + signalIndexName: string | null; stackByLabel?: string; stackByWidth?: number; timelineId?: string; title?: React.ReactNode; + titleSize?: EuiTitleSize; updateDateRange: UpdateDateRange; - runtimeMappings?: MappingRuntimeFields; hideQueryToggle?: boolean; } @@ -113,30 +121,31 @@ export const AlertsHistogramPanel = memo( combinedQueries, comboboxRef, defaultStackByOption = DEFAULT_STACK_BY_FIELD, + extraActions, filters, headerChildren, inspectTitle, + legendPosition = 'right', onFieldSelected, onlyField, paddingSize = 'm', panelHeight = PANEL_HEIGHT, query, - legendPosition = 'right', + runtimeMappings, setComboboxInputRef, - signalIndexName, showCountsInLegend = false, showGroupByPlaceholder = false, showLegend = true, showLinkToAlerts = false, - showTotalAlertsCount = false, showStackBy = true, + showTotalAlertsCount = false, + signalIndexName, stackByLabel, stackByWidth, timelineId, title = i18n.HISTOGRAM_HEADER, - updateDateRange, titleSize = 'm', - runtimeMappings, + updateDateRange, hideQueryToggle = false, }) => { const { to, from, deleteQuery, setQuery } = useGlobalTime(false); @@ -170,13 +179,17 @@ export const AlertsHistogramPanel = memo( setQuerySkip(!toggleStatus); }, [toggleStatus]); const toggleQuery = useCallback( - (status: boolean) => { - setToggleStatus(status); + (newToggleStatus: boolean) => { + setToggleStatus(newToggleStatus); // toggle on = skipQuery false - setQuerySkip(!status); + setQuerySkip(!newToggleStatus); }, [setQuerySkip, setToggleStatus] ); + + const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled'); + const timerange = useMemo(() => ({ from, to }), [from, to]); + const { loading: isLoadingAlerts, data: alertsData, @@ -193,7 +206,7 @@ export const AlertsHistogramPanel = memo( runtimeMappings ), indexName: signalIndexName, - skip: querySkip, + skip: querySkip || isChartEmbeddablesEnabled, queryName: ALERTS_QUERY_NAMES.HISTOGRAM, }); @@ -260,13 +273,13 @@ export const AlertsHistogramPanel = memo( }, [isInitialLoading, isLoadingAlerts, setIsInitialLoading]); useInspectButton({ - setQuery, - response, - request, - refetch, - uniqueQueryId, deleteQuery, loading: isLoadingAlerts, + refetch, + request, + response, + setQuery, + uniqueQueryId, }); useEffect(() => { @@ -352,7 +365,7 @@ export const AlertsHistogramPanel = memo( titleSize={titleSize} toggleStatus={toggleStatus} toggleQuery={hideQueryToggle ? undefined : toggleQuery} - showInspectButton={chartOptionsContextMenu == null} + showInspectButton={isChartEmbeddablesEnabled ? false : chartOptionsContextMenu == null} subtitle={!isInitialLoading && showTotalAlertsCount && totalAlerts} isInspectDisabled={isInspectDisabled} hideSubtitle @@ -392,7 +405,7 @@ export const AlertsHistogramPanel = memo( )} {headerChildren != null && headerChildren} - {chartOptionsContextMenu != null && ( + {chartOptionsContextMenu != null && !isChartEmbeddablesEnabled && ( {chartOptionsContextMenu(uniqueQueryId)} @@ -403,7 +416,22 @@ export const AlertsHistogramPanel = memo( {toggleStatus ? ( - isInitialLoading ? ( + isChartEmbeddablesEnabled ? ( + + ) : isInitialLoading ? ( ) : ( void) | null; uniqueQueryId: string; loading: boolean; + searchSessionId?: string; } /** @@ -33,6 +36,7 @@ export const useInspectButton = ({ uniqueQueryId, deleteQuery, loading, + searchSessionId, }: UseInspectButtonParams) => { useEffect(() => { if (refetch != null && setQuery != null) { @@ -44,6 +48,7 @@ export const useInspectButton = ({ }, loading, refetch, + searchSessionId, }); } @@ -52,15 +57,24 @@ export const useInspectButton = ({ deleteQuery({ id: uniqueQueryId }); } }; - }, [setQuery, loading, response, request, refetch, uniqueQueryId, deleteQuery]); + }, [setQuery, loading, response, request, refetch, uniqueQueryId, deleteQuery, searchSessionId]); }; +export function isDataViewFieldSubtypeNested(field: Partial) { + const subTypeNested = field?.subType as IFieldSubTypeNested; + return !!subTypeNested?.nested?.path; +} + +export function isKeyword(field: Partial) { + return field.esTypes && field.esTypes?.indexOf('keyword') >= 0; +} + export function getAggregatableFields(fields: { [fieldName: string]: Partial; }): EuiComboBoxOptionOption[] { const result = []; for (const [key, field] of Object.entries(fields)) { - if (field.aggregatable === true) { + if (field.aggregatable === true && isKeyword(field) && !isDataViewFieldSubtypeNested(field)) { result.push({ label: key, value: key }); } } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx index 66fa9023eb3d2..ed24aa3771463 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx @@ -38,6 +38,9 @@ import { useGlobalFullScreen } from '../../../../common/containers/use_full_scre import type { TimeframePreviewOptions } from '../../../pages/detection_engine/rules/types'; import { useLicense } from '../../../../common/hooks/use_license'; import { useKibana } from '../../../../common/lib/kibana'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { getRulePreviewLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/common/alerts/rule_preview'; +import { VisualizationEmbeddable } from '../../../../common/components/visualization_actions/visualization_embeddable'; const LoadingChart = styled(EuiLoadingChart)` display: block; @@ -53,6 +56,8 @@ const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>` export const ID = 'previewHistogram'; +const CHART_HEIGHT = 150; + interface PreviewHistogramProps { previewId: string; addNoiseWarning: () => void; @@ -89,6 +94,17 @@ export const PreviewHistogram = ({ const isEqlRule = useMemo(() => ruleType === 'eql', [ruleType]); const isMlRule = useMemo(() => ruleType === 'machine_learning', [ruleType]); + const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled'); + const timerange = useMemo(() => ({ from: startDate, to: endDate }), [startDate, endDate]); + + const extraVisualizationOptions = useMemo( + () => ({ + ruleId: previewId, + spaceId, + }), + [previewId, spaceId] + ); + const [isLoading, { data, inspect, totalCount, refetch }] = usePreviewHistogram({ previewId, startDate, @@ -96,12 +112,14 @@ export const PreviewHistogram = ({ spaceId, indexPattern, ruleType, + skip: isChartEmbeddablesEnabled, }); const license = useLicense(); const { browserFields, runtimeMappings } = useSourcererDataView(SourcererScopeName.detections); const { globalFullScreen } = useGlobalFullScreen(); const previousPreviewId = usePrevious(previewId); + const previewQueryId = `${ID}-${previewId}`; useEffect(() => { if (previousPreviewId !== previewId && totalCount > 0) { @@ -113,9 +131,23 @@ export const PreviewHistogram = ({ useEffect((): void => { if (!isLoading && !isInitializing) { - setQuery({ id: `${ID}-${previewId}`, inspect, loading: isLoading, refetch }); + setQuery({ + id: previewQueryId, + inspect, + loading: isLoading, + refetch, + }); } - }, [setQuery, inspect, isLoading, isInitializing, refetch, previewId]); + }, [ + setQuery, + inspect, + isLoading, + isInitializing, + refetch, + previewId, + isChartEmbeddablesEnabled, + previewQueryId, + ]); const barConfig = useMemo( (): ChartSeriesConfigs => getHistogramConfig(endDate, startDate, !isEqlRule), @@ -158,14 +190,28 @@ export const PreviewHistogram = ({ {isLoading ? ( + ) : isChartEmbeddablesEnabled ? ( + ) : ( { const { uiSettings } = useKibana().services; @@ -55,9 +57,9 @@ export const usePreviewHistogram = ({ stackByField, startDate, includeMissingData: false, - skip: error != null, + skip: skip || error != null, }; - }, [startDate, endDate, filterQuery, spaceId, error, stackByField]); + }, [endDate, filterQuery, spaceId, stackByField, startDate, skip, error]); return useMatrixHistogramCombined(matrixHistogramRequest); }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.test.tsx index 4cef9f95dcc6e..f87b1b3fcbbf7 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.test.tsx @@ -9,6 +9,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { useAlertsLocalStorage } from './alerts_local_storage'; +import type { Status } from '../../../../../common/detection_engine/schemas/common'; import { RESET_GROUP_BY_FIELDS } from '../../../../common/components/chart_settings_popover/configurations/default/translations'; import { CHART_SETTINGS_POPOVER_ARIA_LABEL } from '../../../../common/components/chart_settings_popover/translations'; import { mockBrowserFields } from '../../../../common/containers/source/mock'; @@ -17,9 +18,16 @@ import { TestProviders } from '../../../../common/mock'; import { ChartPanels } from '.'; jest.mock('./alerts_local_storage'); - jest.mock('../../../../common/containers/sourcerer'); +jest.mock('../../../../common/components/visualization_actions/lens_embeddable'); +jest.mock('../../../../common/components/page/use_refetch_by_session', () => ({ + useRefetchByRestartingSession: jest.fn().mockReturnValue({ + searchSessionId: 'mockSearchSessionId', + refetchByRestartingSession: jest.fn(), + }), +})); + jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); @@ -104,6 +112,7 @@ const defaultProps = { }, }, ], + filterGroup: 'open' as Status, isLoadingIndexPattern: false, query: { query: '', @@ -111,6 +120,8 @@ const defaultProps = { }, runtimeMappings: {}, signalIndexName: '.alerts-security.alerts-default', + showBuildingBlockAlerts: false, + showOnlyThreatIndicatorAlerts: false, updateDateRangeCallback: jest.fn(), }; 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 6ed2353165f1d..1ee5bbf7cfe9b 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 @@ -6,6 +6,7 @@ */ 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 } from 'react'; @@ -28,6 +29,7 @@ import { } from '../../../components/alerts_kpis/common/config'; import { AlertsCountPanel } from '../../../components/alerts_kpis/alerts_count_panel'; import { GROUP_BY_LABEL } from '../../../components/alerts_kpis/common/translations'; +import { RESET_GROUP_BY_FIELDS } from '../../../../common/components/chart_settings_popover/configurations/default/translations'; const TABLE_PANEL_HEIGHT = 330; // px const TRENT_CHART_HEIGHT = 127; // px @@ -112,6 +114,35 @@ const ChartPanelsComponent: React.FC = ({ onResetStackByField1(); }, [onResetStackByField0, onResetStackByField1]); + const resetGroupByFieldAction = useMemo( + () => [ + { + id: 'resetGroupByField', + + getDisplayName(context: ActionExecutionContext): string { + return RESET_GROUP_BY_FIELDS; + }, + getIconType(context: ActionExecutionContext): string | undefined { + return 'editorRedo'; + }, + type: 'actionButton', + async isCompatible(context: ActionExecutionContext): Promise { + return true; + }, + async execute(context: ActionExecutionContext): Promise { + onReset(); + updateCommonStackBy0(DEFAULT_STACK_BY_FIELD); + + if (updateCommonStackBy1 != null) { + updateCommonStackBy1(DEFAULT_STACK_BY_FIELD1); + } + }, + order: 5, + }, + ], + [onReset, updateCommonStackBy0, updateCommonStackBy1] + ); + const chartOptionsContextMenu = useCallback( (queryId: string) => ( = ({ [alertViewSelection, setAlertViewSelection] ); const isAlertsPageChartsEnabled = useIsExperimentalFeatureEnabled('alertsPageChartsEnabled'); + return (
{alertViewSelection === 'trend' && ( @@ -151,21 +183,22 @@ const ChartPanelsComponent: React.FC = ({ chartOptionsContextMenu={chartOptionsContextMenu} comboboxRef={stackByField0ComboboxRef} defaultStackByOption={trendChartStackBy} + extraActions={resetGroupByFieldAction} filters={alertsHistogramDefaultFilters} inspectTitle={i18n.TREND} - setComboboxInputRef={setStackByField0ComboboxInputRef} onFieldSelected={updateCommonStackBy0} panelHeight={TREND_CHART_PANEL_HEIGHT} query={query} + runtimeMappings={runtimeMappings} + setComboboxInputRef={setStackByField0ComboboxInputRef} showCountsInLegend={true} - showGroupByPlaceholder={true} + showGroupByPlaceholder={false} showTotalAlertsCount={false} + signalIndexName={signalIndexName} stackByLabel={GROUP_BY_LABEL} title={title} titleSize={'s'} - signalIndexName={signalIndexName} updateDateRange={updateDateRangeCallback} - runtimeMappings={runtimeMappings} /> )} @@ -179,6 +212,7 @@ const ChartPanelsComponent: React.FC = ({ = ({ runtimeMappings={runtimeMappings} setStackByField0={updateCommonStackBy0} setStackByField0ComboboxInputRef={setStackByField0ComboboxInputRef} - stackByField0ComboboxRef={stackByField0ComboboxRef} setStackByField1={updateCommonStackBy1} setStackByField1ComboboxInputRef={setStackByField1ComboboxInputRef} - stackByField1ComboboxRef={stackByField1ComboboxRef} signalIndexName={signalIndexName} stackByField0={countTableStackBy0} + stackByField0ComboboxRef={stackByField0ComboboxRef} stackByField1={countTableStackBy1} + stackByField1ComboboxRef={stackByField1ComboboxRef} title={title} /> )} @@ -208,23 +242,23 @@ const ChartPanelsComponent: React.FC = ({ addFilter={addFilter} alignHeader="flexStart" chartOptionsContextMenu={chartOptionsContextMenu} + filters={alertsHistogramDefaultFilters} inspectTitle={i18n.TREEMAP} isPanelExpanded={isTreemapPanelExpanded} - filters={alertsHistogramDefaultFilters} query={query} + riskSubAggregationField="kibana.alert.risk_score" + runtimeMappings={runtimeMappings} setIsPanelExpanded={setIsTreemapPanelExpanded} setStackByField0={updateCommonStackBy0} setStackByField0ComboboxInputRef={setStackByField0ComboboxInputRef} - stackByField0ComboboxRef={stackByField0ComboboxRef} setStackByField1={updateCommonStackBy1} setStackByField1ComboboxInputRef={setStackByField1ComboboxInputRef} - stackByField1ComboboxRef={stackByField1ComboboxRef} signalIndexName={signalIndexName} stackByField0={riskChartStackBy0} + stackByField0ComboboxRef={stackByField0ComboboxRef} stackByField1={riskChartStackBy1} + stackByField1ComboboxRef={stackByField1ComboboxRef} title={title} - riskSubAggregationField="kibana.alert.risk_score" - runtimeMappings={runtimeMappings} /> )} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index a64ff6ecd3d47..f5a044a0b287d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -139,6 +139,9 @@ jest.mock('../../components/alerts_table/timeline_actions/use_bulk_add_to_case_a useBulkAddToCaseActions: jest.fn(() => []), })); +jest.mock('../../../common/components/visualization_actions/lens_embeddable'); +jest.mock('../../../common/components/page/use_refetch_by_session'); + describe('DetectionEnginePageComponent', () => { beforeAll(() => { (useParams as jest.Mock).mockReturnValue({}); diff --git a/x-pack/plugins/security_solution/public/explore/users/pages/navigation/authentications_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/explore/users/pages/navigation/authentications_query_tab_body.test.tsx index ee2d86faca6c2..7d860e5a99611 100644 --- a/x-pack/plugins/security_solution/public/explore/users/pages/navigation/authentications_query_tab_body.test.tsx +++ b/x-pack/plugins/security_solution/public/explore/users/pages/navigation/authentications_query_tab_body.test.tsx @@ -17,10 +17,8 @@ jest.mock('../../../containers/authentications'); jest.mock('../../../../common/containers/query_toggle'); 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('../../../../common/components/visualization_actions'); +jest.mock('../../../../common/components/visualization_actions/lens_embeddable'); describe('Authentications query tab body', () => { const mockUseAuthentications = useAuthentications as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/helpers.tsx b/x-pack/plugins/security_solution/public/helpers.tsx index 713ea0876ade6..7a0f918d4e169 100644 --- a/x-pack/plugins/security_solution/public/helpers.tsx +++ b/x-pack/plugins/security_solution/public/helpers.tsx @@ -20,13 +20,10 @@ import { CASES_FEATURE_ID, CASES_PATH, EXCEPTIONS_PATH, - HOSTS_PATH, LANDING_PATH, - NETWORK_PATH, RULES_PATH, SERVER_APP_ID, THREAT_INTELLIGENCE_PATH, - USERS_PATH, } from '../common/constants'; import type { FactoryQueryTypes, @@ -195,13 +192,6 @@ export const isThreatIntelligencePath = (pathname: string): boolean => { }); }; -export const isExplorePage = (pathname: string): boolean => { - return !!matchPath(pathname, { - path: `(${HOSTS_PATH}|${USERS_PATH}|${NETWORK_PATH})`, - strict: false, - }); -}; - export const getSubPluginRoutesByCapabilities = ( subPlugins: StartedSubPlugins, capabilities: Capabilities diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx index cc960b4130ff1..e3ab3b292aaba 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx @@ -242,7 +242,6 @@ export const AlertsByStatus = ({ label={STATUS_OPEN} title={} totalCount={openCount} - isChartEmbeddablesEnabled={isChartEmbeddablesEnabled} /> )} @@ -272,7 +271,6 @@ export const AlertsByStatus = ({ label={STATUS_ACKNOWLEDGED} title={} totalCount={acknowledgedCount} - isChartEmbeddablesEnabled={isChartEmbeddablesEnabled} /> )} @@ -299,7 +297,6 @@ export const AlertsByStatus = ({ label={STATUS_CLOSED} title={} totalCount={closedCount} - isChartEmbeddablesEnabled={isChartEmbeddablesEnabled} /> )}