Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as runtimeTypes from 'io-ts';
export { Direction };

export type SortDirectionTable = 'none' | 'asc' | 'desc' | Direction;

export interface SortColumnTable {
columnId: string;
columnType: string;
Expand All @@ -25,6 +26,7 @@ export enum TableId {
hostsPageSessions = 'hosts-page-sessions-v2', // the v2 is to cache bust localstorage settings as default columns were reworked.
alertsOnRuleDetailsPage = 'alerts-rules-details-page',
alertsOnAlertsPage = 'alerts-page',
alertsOnAlertSummaryPage = 'alert-summary-page',
test = 'table-test', // Reserved for testing purposes
alternateTest = 'alternateTest',
rulePreview = 'rule-preview',
Expand All @@ -43,6 +45,7 @@ export enum TableEntityType {

export const tableEntity: Record<TableId, TableEntityType> = {
[TableId.alertsOnAlertsPage]: TableEntityType.alert,
[TableId.alertsOnAlertSummaryPage]: TableEntityType.alert,
[TableId.alertsOnCasePage]: TableEntityType.alert,
[TableId.alertsOnRuleDetailsPage]: TableEntityType.alert,
[TableId.hostsPageEvents]: TableEntityType.event,
Expand All @@ -64,6 +67,7 @@ const TableIdLiteralRt = runtimeTypes.union([
runtimeTypes.literal(TableId.hostsPageSessions),
runtimeTypes.literal(TableId.alertsOnRuleDetailsPage),
runtimeTypes.literal(TableId.alertsOnAlertsPage),
runtimeTypes.literal(TableId.alertsOnAlertSummaryPage),
runtimeTypes.literal(TableId.test),
runtimeTypes.literal(TableId.rulePreview),
runtimeTypes.literal(TableId.kubernetesPageSessions),
Expand Down
Original file line number Diff line number Diff line change
@@ -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 React from 'react';
import { AdditionalToolbarControls } from './additional_toolbar_controls';
import { TableId } from '@kbn/securitysolution-data-table';
import { fireEvent, render, screen } from '@testing-library/react';
import { createMockStore, mockGlobalState, TestProviders } from '../../../../common/mock';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import type { DataView } from '@kbn/data-views-plugin/common';
import { createStubDataView } from '@kbn/data-views-plugin/common/data_views/data_view.stub';

const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return {
...original,
useDispatch: () => mockDispatch,
};
});
jest.mock('../../../../common/hooks/use_selector');

const dataView: DataView = createStubDataView({ spec: {} });
const mockOptions = [
{ label: 'ruleName', key: 'kibana.alert.rule.name' },
{ label: 'userName', key: 'user.name' },
{ label: 'hostName', key: 'host.name' },
{ label: 'sourceIP', key: 'source.ip' },
];
const tableId = TableId.alertsOnAlertSummaryPage;

const groups = {
[tableId]: { options: mockOptions, activeGroups: ['kibana.alert.rule.name'] },
};

describe('AdditionalToolbarControls', () => {
beforeEach(() => {
(useDeepEqualSelector as jest.Mock).mockImplementation(() => groups[tableId]);
});

test('should render the group selector component and allow the user to select a grouping field', () => {
const store = createMockStore({
...mockGlobalState,
groups,
});
render(
<TestProviders store={store}>
<AdditionalToolbarControls dataView={dataView} />
</TestProviders>
);

fireEvent.click(screen.getByTestId('group-selector-dropdown'));
fireEvent.click(screen.getByTestId('panel-user.name'));
expect(mockDispatch.mock.calls[0][0].payload).toEqual({
activeGroups: ['user.name'],
tableId,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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, { memo, useCallback, useMemo } from 'react';
import type { DataView } from '@kbn/data-views-plugin/common';
import { TableId } from '@kbn/securitysolution-data-table';
import { useGetGroupSelectorStateless } from '@kbn/grouping/src/hooks/use_get_group_selector';
import { useDispatch } from 'react-redux';
import { groupIdSelector } from '../../../../common/store/grouping/selectors';
import { updateGroups } from '../../../../common/store/grouping/actions';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';

const TABLE_ID = TableId.alertsOnAlertSummaryPage;
const MAX_GROUPING_LEVELS = 3;
const NO_OPTIONS = { options: [] };

export interface RenderAdditionalToolbarControlsProps {
/**
* DataView created for the alert summary page
*/
dataView: DataView;
}

/**
* Renders a button that when clicked shows a dropdown to allow selecting a group for the GroupedAlertTable.
* Handles further communication with the kbn-grouping package via redux.
*/
export const AdditionalToolbarControls = memo(
({ dataView }: RenderAdditionalToolbarControlsProps) => {
const dispatch = useDispatch();

const onGroupChange = useCallback(
(selectedGroups: string[]) =>
dispatch(updateGroups({ activeGroups: selectedGroups, tableId: TABLE_ID })),
[dispatch]
);

const groupId = useMemo(() => groupIdSelector(), []);
const { options: defaultGroupingOptions } =
useDeepEqualSelector((state) => groupId(state, TABLE_ID)) ?? NO_OPTIONS;

const groupSelector = useGetGroupSelectorStateless({
groupingId: TABLE_ID,
onGroupChange,
fields: dataView.fields,
defaultGroupingOptions,
maxGroupingLevels: MAX_GROUPING_LEVELS,
});

return <>{groupSelector}</>;
}
);

AdditionalToolbarControls.displayName = 'AdditionalToolbarControls';
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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 { groupStatsAggregations } from './group_stats_aggregations';

describe('groupStatsAggregations', () => {
it('should return values depending for signal.rule.id input field', () => {
const aggregations = groupStatsAggregations('signal.rule.id');
expect(aggregations).toEqual([
{
unitsCount: {
cardinality: {
field: 'kibana.alert.uuid',
},
},
},
{
severitiesSubAggregation: {
terms: {
field: 'kibana.alert.severity',
},
},
},
{
rulesCountAggregation: {
cardinality: {
field: 'kibana.alert.rule.rule_id',
},
},
},
]);
});

it('should return values depending for kibana.alert.severity input field', () => {
const aggregations = groupStatsAggregations('kibana.alert.severity');
expect(aggregations).toEqual([
{
unitsCount: {
cardinality: {
field: 'kibana.alert.uuid',
},
},
},
{
signalRuleIdSubAggregation: {
terms: {
field: 'signal.rule.id',
},
},
},
{
rulesCountAggregation: {
cardinality: {
field: 'kibana.alert.rule.rule_id',
},
},
},
]);
});

it('should return values depending for kibana.alert.rule.name input field', () => {
const aggregations = groupStatsAggregations('kibana.alert.rule.name');
expect(aggregations).toEqual([
{
unitsCount: {
cardinality: {
field: 'kibana.alert.uuid',
},
},
},
{
signalRuleIdSubAggregation: {
terms: {
field: 'signal.rule.id',
},
},
},
{
severitiesSubAggregation: {
terms: {
field: 'kibana.alert.severity',
},
},
},
]);
});

it('should return the default values if the field is not supported', () => {
const aggregations = groupStatsAggregations('unknown');
expect(aggregations).toEqual([
{
unitsCount: {
cardinality: {
field: 'kibana.alert.uuid',
},
},
},
{
signalRuleIdSubAggregation: {
terms: {
field: 'signal.rule.id',
},
},
},
{
severitiesSubAggregation: {
terms: {
field: 'kibana.alert.severity',
},
},
},
{
rulesCountAggregation: {
cardinality: {
field: 'kibana.alert.rule.rule_id',
},
},
},
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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 type { NamedAggregation } from '@kbn/grouping';
import { DEFAULT_GROUP_STATS_AGGREGATION } from '../../alerts_table/alerts_grouping';
import {
RULE_COUNT_AGGREGATION,
SEVERITY_SUB_AGGREGATION,
} from '../../alerts_table/grouping_settings';

const RULE_SIGNAL_ID_SUB_AGGREGATION = {
signalRuleIdSubAggregation: {
terms: {
field: 'signal.rule.id',
},
},
};

/**
* Returns aggregations to be used to calculate the statistics to be used in the`extraAction` property of the EuiAccordion component.
* It handles custom renders for the following fields:
* - signal.rule.id
* - kibana.alert.severity
* - kibana.alert.rule.name
* And returns a default set of aggregation for all the other fields.
*
* These go hand in hand with groupingOptions and groupPanelRenderers.
*/
export const groupStatsAggregations = (field: string): NamedAggregation[] => {
const aggMetrics: NamedAggregation[] = DEFAULT_GROUP_STATS_AGGREGATION('');

switch (field) {
case 'signal.rule.id':
aggMetrics.push(SEVERITY_SUB_AGGREGATION, RULE_COUNT_AGGREGATION);
break;
case 'kibana.alert.severity':
aggMetrics.push(RULE_SIGNAL_ID_SUB_AGGREGATION, RULE_COUNT_AGGREGATION);
break;
case 'kibana.alert.rule.name':
aggMetrics.push(RULE_SIGNAL_ID_SUB_AGGREGATION, SEVERITY_SUB_AGGREGATION);
break;
default:
aggMetrics.push(
RULE_SIGNAL_ID_SUB_AGGREGATION,
SEVERITY_SUB_AGGREGATION,
RULE_COUNT_AGGREGATION
);
}
return aggMetrics;
};
Loading