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 index c18b413cd2a5..6148e986fa5e 100644 --- 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 @@ -6,7 +6,7 @@ Object { "references": Array [ Object { "id": "security-solution-my-test", - "name": "indexpattern-datasource-layer-4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "name": "indexpattern-datasource-layer-mockLayerId", "type": "index-pattern", }, ], @@ -15,17 +15,28 @@ Object { "datasourceStates": Object { "formBased": Object { "layers": Object { - "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b": Object { + "mockLayerId": Object { "columnOrder": Array [ - "2881fedd-54b7-42ba-8c97-5175dec86166", - "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", - "f04a71a3-399f-4d32-9efc-8a005e989991", + "mockTopValuesOfStackByFieldColumnId", + "mockTopValuesOfBreakdownFieldColumnId", + "mockCountColumnId", ], "columns": Object { - "2881fedd-54b7-42ba-8c97-5175dec86166": Object { + "mockCountColumnId": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of agent.type", + "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, + "scale": "ratio", + "sourceField": "agent.type", + }, + "mockTopValuesOfBreakdownFieldColumnId": Object { "dataType": "string", "isBucketed": true, - "label": "Top values of event.category", + "label": "Top values of agent.type", "operationType": "terms", "params": Object { "exclude": Array [], @@ -34,7 +45,7 @@ Object { "includeIsRegex": false, "missingBucket": false, "orderBy": Object { - "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "columnId": "mockCountColumnId", "type": "column", }, "orderDirection": "desc", @@ -45,12 +56,12 @@ Object { "size": 1000, }, "scale": "ordinal", - "sourceField": "event.category", + "sourceField": "agent.type", }, - "75ce269b-ee9c-4c7d-a14e-9226ba0fe059": Object { + "mockTopValuesOfStackByFieldColumnId": Object { "dataType": "string", "isBucketed": true, - "label": "Top values of agent.type", + "label": "Top values of event.category", "operationType": "terms", "params": Object { "exclude": Array [], @@ -59,7 +70,7 @@ Object { "includeIsRegex": false, "missingBucket": false, "orderBy": Object { - "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "columnId": "mockCountColumnId", "type": "column", }, "orderDirection": "desc", @@ -70,18 +81,7 @@ Object { "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", + "sourceField": "event.category", }, }, "incompleteColumns": Object {}, @@ -144,20 +144,20 @@ Object { "visualization": Object { "columns": Array [ Object { - "columnId": "2881fedd-54b7-42ba-8c97-5175dec86166", + "columnId": "mockTopValuesOfStackByFieldColumnId", "isTransposed": false, "width": 362, }, Object { - "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "columnId": "mockCountColumnId", "isTransposed": false, }, Object { - "columnId": "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "columnId": "mockTopValuesOfBreakdownFieldColumnId", "isTransposed": false, }, ], - "layerId": "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "layerId": "mockLayerId", "layerType": "data", }, }, @@ -172,7 +172,7 @@ Object { "references": Array [ Object { "id": "security-solution-my-test", - "name": "indexpattern-datasource-layer-4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "name": "indexpattern-datasource-layer-mockLayerId", "type": "index-pattern", }, ], @@ -181,42 +181,27 @@ Object { "datasourceStates": Object { "formBased": Object { "layers": Object { - "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b": Object { + "mockLayerId": Object { "columnOrder": Array [ - "2881fedd-54b7-42ba-8c97-5175dec86166", - "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", - "f04a71a3-399f-4d32-9efc-8a005e989991", + "mockTopValuesOfStackByFieldColumnId", + "mockCountColumnId", ], "columns": Object { - "2881fedd-54b7-42ba-8c97-5175dec86166": Object { - "dataType": "string", - "isBucketed": true, - "label": "Top values of event.category", - "operationType": "terms", + "mockCountColumnId": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of event.category", + "operationType": "count", "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, + "emptyAsNull": true, }, - "scale": "ordinal", + "scale": "ratio", "sourceField": "event.category", }, - "75ce269b-ee9c-4c7d-a14e-9226ba0fe059": Object { + "mockTopValuesOfStackByFieldColumnId": Object { "dataType": "string", "isBucketed": true, - "label": "Top values of undefined", + "label": "Top values of event.category", "operationType": "terms", "params": Object { "exclude": Array [], @@ -225,7 +210,7 @@ Object { "includeIsRegex": false, "missingBucket": false, "orderBy": Object { - "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "columnId": "mockCountColumnId", "type": "column", }, "orderDirection": "desc", @@ -236,18 +221,7 @@ Object { "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, + "sourceField": "event.category", }, }, "incompleteColumns": Object {}, @@ -334,20 +308,16 @@ Object { "visualization": Object { "columns": Array [ Object { - "columnId": "2881fedd-54b7-42ba-8c97-5175dec86166", + "columnId": "mockTopValuesOfStackByFieldColumnId", "isTransposed": false, "width": 362, }, Object { - "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", - "isTransposed": false, - }, - Object { - "columnId": "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "columnId": "mockCountColumnId", "isTransposed": false, }, ], - "layerId": "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "layerId": "mockLayerId", "layerType": "data", }, }, @@ -362,7 +332,7 @@ Object { "references": Array [ Object { "id": "security-solution-my-test", - "name": "indexpattern-datasource-layer-4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "name": "indexpattern-datasource-layer-mockLayerId", "type": "index-pattern", }, ], @@ -371,42 +341,27 @@ Object { "datasourceStates": Object { "formBased": Object { "layers": Object { - "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b": Object { + "mockLayerId": Object { "columnOrder": Array [ - "2881fedd-54b7-42ba-8c97-5175dec86166", - "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", - "f04a71a3-399f-4d32-9efc-8a005e989991", + "mockTopValuesOfStackByFieldColumnId", + "mockCountColumnId", ], "columns": Object { - "2881fedd-54b7-42ba-8c97-5175dec86166": Object { - "dataType": "string", - "isBucketed": true, - "label": "Top values of event.category", - "operationType": "terms", + "mockCountColumnId": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of event.category", + "operationType": "count", "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, + "emptyAsNull": true, }, - "scale": "ordinal", + "scale": "ratio", "sourceField": "event.category", }, - "75ce269b-ee9c-4c7d-a14e-9226ba0fe059": Object { + "mockTopValuesOfStackByFieldColumnId": Object { "dataType": "string", "isBucketed": true, - "label": "Top values of undefined", + "label": "Top values of event.category", "operationType": "terms", "params": Object { "exclude": Array [], @@ -415,7 +370,7 @@ Object { "includeIsRegex": false, "missingBucket": false, "orderBy": Object { - "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "columnId": "mockCountColumnId", "type": "column", }, "orderDirection": "desc", @@ -426,18 +381,7 @@ Object { "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, + "sourceField": "event.category", }, }, "incompleteColumns": Object {}, @@ -500,20 +444,16 @@ Object { "visualization": Object { "columns": Array [ Object { - "columnId": "2881fedd-54b7-42ba-8c97-5175dec86166", + "columnId": "mockTopValuesOfStackByFieldColumnId", "isTransposed": false, "width": 362, }, Object { - "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", - "isTransposed": false, - }, - Object { - "columnId": "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "columnId": "mockCountColumnId", "isTransposed": false, }, ], - "layerId": "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "layerId": "mockLayerId", "layerType": "data", }, }, 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 index e8457e8dfb53..82303e529db3 100644 --- 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 @@ -11,8 +11,20 @@ import { useLensAttributes } from '../../../use_lens_attributes'; import { getAlertsTableLensAttributes } from './alerts_table'; +interface VisualizationState { + visualization: { columns: {} }; + datasourceStates: { + formBased: { layers: Record }; + }; +} + jest.mock('uuid', () => ({ - v4: jest.fn().mockReturnValue('4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b'), + v4: jest + .fn() + .mockReturnValueOnce('mockLayerId') + .mockReturnValueOnce('mockTopValuesOfStackByFieldColumnId') + .mockReturnValueOnce('mockCountColumnId') + .mockReturnValueOnce('mockTopValuesOfBreakdownFieldColumnId'), })); jest.mock('../../../../../containers/sourcerer', () => ({ @@ -95,6 +107,49 @@ describe('getAlertsTableLensAttributes', () => { { wrapper } ); + const state = result?.current?.state as VisualizationState; expect(result?.current).toMatchSnapshot(); + + expect(state.datasourceStates.formBased.layers.mockLayerId.columnOrder).toMatchInlineSnapshot(` + Array [ + "mockTopValuesOfStackByFieldColumnId", + "mockTopValuesOfBreakdownFieldColumnId", + "mockCountColumnId", + ] + `); + }); + + it('should render Without extra options - breakdownField', () => { + const { result } = renderHook( + () => + useLensAttributes({ + extraOptions: { breakdownField: '' }, + getLensAttributes: getAlertsTableLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + const state = result?.current?.state as VisualizationState; + expect(state.visualization?.columns).toMatchInlineSnapshot(` + Array [ + Object { + "columnId": "mockTopValuesOfStackByFieldColumnId", + "isTransposed": false, + "width": 362, + }, + Object { + "columnId": "mockCountColumnId", + "isTransposed": false, + }, + ] + `); + + expect(state.datasourceStates.formBased.layers.mockLayerId.columnOrder).toMatchInlineSnapshot(` + Array [ + "mockTopValuesOfStackByFieldColumnId", + "mockCountColumnId", + ] + `); }); }); 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 index 678179855557..9358bcc5db81 100644 --- 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 @@ -5,35 +5,127 @@ * 2.0. */ import { v4 as uuidv4 } from 'uuid'; -import type { GetLensAttributes } from '../../../types'; + +import { isEmpty } from 'lodash'; +import type { GetLensAttributes, LensEmbeddableDataTableColumn } from '../../../types'; +import { COUNT_OF, TOP_VALUE } from '../../../translations'; const layerId = uuidv4(); +const topValuesOfStackByFieldColumnId = uuidv4(); +const countColumnId = uuidv4(); +const topValuesOfBreakdownFieldColumnId = uuidv4(); +const defaultColumns = [ + { + columnId: topValuesOfStackByFieldColumnId, + isTransposed: false, + width: 362, + }, + { + columnId: countColumnId, + isTransposed: false, + }, +]; +const breakdownFieldColumns = [ + { + columnId: topValuesOfBreakdownFieldColumnId, + isTransposed: false, + }, +]; +const defaultColumnOrder = [topValuesOfStackByFieldColumnId]; +const getTopValuesOfBreakdownFieldColumnSettings = ( + breakdownField: string +): Record => ({ + [topValuesOfBreakdownFieldColumnId]: { + label: TOP_VALUE(breakdownField), + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: breakdownField, + isBucketed: true, + params: { + size: 1000, + orderBy: { + type: 'column', + columnId: countColumnId, + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + include: [], + exclude: [], + includeIsRegex: false, + excludeIsRegex: false, + }, + }, +}); export const getAlertsTableLensAttributes: GetLensAttributes = ( stackByField = 'kibana.alert.rule.name', extraOptions ) => { + const breakdownFieldProvided = !isEmpty(extraOptions?.breakdownField); + const countField = + extraOptions?.breakdownField && breakdownFieldProvided + ? extraOptions?.breakdownField + : stackByField; + const columnOrder = breakdownFieldProvided + ? [...defaultColumnOrder, topValuesOfBreakdownFieldColumnId, countColumnId] + : [...defaultColumnOrder, countColumnId]; + + const columnSettings: Record = { + [topValuesOfStackByFieldColumnId]: { + label: TOP_VALUE(stackByField), + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: stackByField, + isBucketed: true, + params: { + size: 1000, + orderBy: { + type: 'column', + columnId: countColumnId, + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + include: [], + exclude: [], + includeIsRegex: false, + excludeIsRegex: false, + }, + }, + [countColumnId]: { + label: COUNT_OF(countField), + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: countField, + params: { + emptyAsNull: true, + }, + }, + ...(extraOptions?.breakdownField && breakdownFieldProvided + ? getTopValuesOfBreakdownFieldColumnSettings(extraOptions?.breakdownField) + : {}), + }; + 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, - }, - ], + columns: breakdownFieldProvided + ? [...defaultColumns, ...breakdownFieldColumns] + : defaultColumns, layerId, layerType: 'data', }, @@ -46,74 +138,16 @@ export const getAlertsTableLensAttributes: GetLensAttributes = ( 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, - }, + columns: columnOrder.reduce>( + (acc, colId) => { + if (colId && columnSettings[colId]) { + acc[colId] = columnSettings[colId]; + } + return acc; }, - }, - columnOrder: [ - '2881fedd-54b7-42ba-8c97-5175dec86166', - '75ce269b-ee9c-4c7d-a14e-9226ba0fe059', - 'f04a71a3-399f-4d32-9efc-8a005e989991', - ], + {} + ), + columnOrder, sampling: 1, incompleteColumns: {}, }, diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/translations.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/translations.ts index 4dcceb29323b..8f21d61bc869 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/translations.ts @@ -105,3 +105,9 @@ export const TOP_VALUE = (field: string) => export const COUNT = i18n.translate('xpack.securitySolution.visualizationActions.countLabel', { defaultMessage: 'Count of records', }); + +export const COUNT_OF = (field: string) => + i18n.translate('xpack.securitySolution.visualizationActions.countOfFieldLabel', { + values: { field }, + defaultMessage: 'Count of {field}', + }); 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 71c7440d5f1e..dd2c7f7cd609 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 @@ -7,6 +7,7 @@ import type { DatatableVisualizationState, + FieldBasedIndexPatternColumn, FormBasedPersistedState, TypedLensByValueInput, } from '@kbn/lens-plugin/public'; @@ -181,3 +182,8 @@ export interface LensDataTableEmbeddable { id: string; timeRange: { from: string; to: string; fromStr: string; toStr: string }; } + +export interface LensEmbeddableDataTableColumn extends FieldBasedIndexPatternColumn { + operationType: string; + params?: unknown; +} 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 7276bb9d2e11..81c2708d961c 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 @@ -81,10 +81,7 @@ export const useLensAttributes = ({ const lensAttrsWithInjectedData = useMemo(() => { if ( lensAttributes == null && - (getLensAttributes == null || - stackByField == null || - stackByField?.length === 0 || - (extraOptions?.breakdownField != null && extraOptions?.breakdownField.length === 0)) + (getLensAttributes == null || stackByField == null || stackByField?.length === 0) ) { return null; } @@ -113,7 +110,6 @@ export const useLensAttributes = ({ applyGlobalQueriesAndFilters, attrs, dataViewId, - extraOptions?.breakdownField, filters, getLensAttributes, hasAdHocDataViews, 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 index 93b3f9264294..5e0077918b71 100644 --- 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 @@ -37,7 +37,7 @@ const ChartContentComponent = ({ }: ChartContentProps) => { return isChartEmbeddablesEnabled ? (