Skip to content

Commit

Permalink
[Security Solution] Multi level grouping for alerts table (elastic#15…
Browse files Browse the repository at this point in the history
…2862)

## Multi Level Grouping

Resolves elastic#150516
Resolves elastic#150514

Implements multi level grouping in Alerts table and Rule details table.
Supports 3 levels deep.


https://user-images.githubusercontent.com/6935300/232547389-7d778f69-d96d-4bd8-8560-f5ddd9fe8060.mov

### Test plan


https://docs.google.com/document/d/15oseanNzF-u-Xeoahy1IVxI4oV3wOuO8VhA886cA1U8/edit#

### To do

- [Cypress](elastic#150666)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co>
  • Loading branch information
3 people authored and nikitaindik committed Apr 25, 2023
1 parent d8b819e commit 0c3d700
Show file tree
Hide file tree
Showing 61 changed files with 3,631 additions and 1,216 deletions.
2 changes: 2 additions & 0 deletions .buildkite/scripts/steps/storybooks/build_and_upload.ts
Expand Up @@ -15,6 +15,7 @@ const STORYBOOKS = [
'apm',
'canvas',
'cases',
'cell_actions',
'ci_composite',
'cloud_chat',
'coloring',
Expand All @@ -34,6 +35,7 @@ const STORYBOOKS = [
'expression_shape',
'expression_tagcloud',
'fleet',
'grouping',
'home',
'infra',
'kibana_react',
Expand Down
9 changes: 9 additions & 0 deletions packages/kbn-securitysolution-grouping/.storybook/main.js
@@ -0,0 +1,9 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

module.exports = require('@kbn/storybook').defaultConfig;
3 changes: 0 additions & 3 deletions packages/kbn-securitysolution-grouping/README.md

This file was deleted.

3 changes: 3 additions & 0 deletions packages/kbn-securitysolution-grouping/README.mdx
@@ -0,0 +1,3 @@
# @kbn/securitysolution-grouping

Grouping component and query. Currently only consumed by security solution alerts table.
8 changes: 5 additions & 3 deletions packages/kbn-securitysolution-grouping/index.tsx
Expand Up @@ -6,20 +6,22 @@
* Side Public License, v 1.
*/

import { RawBucket, StatRenderer, getGroupingQuery, isNoneGroup, useGrouping } from './src';
import { getGroupingQuery, isNoneGroup, useGrouping } from './src';
import type {
DynamicGroupingProps,
GroupOption,
GroupingAggregation,
GroupingFieldTotalAggregation,
NamedAggregation,
RawBucket,
StatRenderer,
} from './src';

export { getGroupingQuery, isNoneGroup, useGrouping };

export type {
DynamicGroupingProps,
GroupOption,
GroupingAggregation,
GroupingFieldTotalAggregation,
NamedAggregation,
RawBucket,
StatRenderer,
Expand Down
Expand Up @@ -13,6 +13,8 @@ import { GroupStats } from './group_stats';
const onTakeActionsOpen = jest.fn();
const testProps = {
bucketKey: '9nk5mo2fby',
groupFilter: [],
groupNumber: 0,
onTakeActionsOpen,
statRenderers: [
{
Expand All @@ -23,7 +25,7 @@ const testProps = {
{ title: 'Rules:', badge: { value: 2 } },
{ title: 'Alerts:', badge: { value: 2, width: 50, color: '#a83632' } },
],
takeActionItems: [
takeActionItems: () => [
<p data-test-subj="takeActionItem-1" key={1} />,
<p data-test-subj="takeActionItem-2" key={2} />,
],
Expand Down
Expand Up @@ -16,29 +16,44 @@ import {
EuiToolTip,
} from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import { Filter } from '@kbn/es-query';
import { StatRenderer } from '../types';
import { statsContainerCss } from '../styles';
import { TAKE_ACTION } from '../translations';

interface GroupStatsProps<T> {
bucketKey: string;
statRenderers?: StatRenderer[];
groupFilter: Filter[];
groupNumber: number;
onTakeActionsOpen?: () => void;
takeActionItems: JSX.Element[];
statRenderers?: StatRenderer[];
takeActionItems: (groupFilters: Filter[], groupNumber: number) => JSX.Element[];
}

const GroupStatsComponent = <T,>({
bucketKey,
statRenderers,
groupFilter,
groupNumber,
onTakeActionsOpen,
takeActionItems,
statRenderers,
takeActionItems: getTakeActionItems,
}: GroupStatsProps<T>) => {
const [isPopoverOpen, setPopover] = useState(false);
const [takeActionItems, setTakeActionItems] = useState<JSX.Element[]>([]);

const onButtonClick = useCallback(
() => (!isPopoverOpen && onTakeActionsOpen ? onTakeActionsOpen() : setPopover(!isPopoverOpen)),
[isPopoverOpen, onTakeActionsOpen]
);
const onButtonClick = useCallback(() => {
if (!isPopoverOpen && takeActionItems.length === 0) {
setTakeActionItems(getTakeActionItems(groupFilter, groupNumber));
}
return !isPopoverOpen && onTakeActionsOpen ? onTakeActionsOpen() : setPopover(!isPopoverOpen);
}, [
getTakeActionItems,
groupFilter,
groupNumber,
isPopoverOpen,
onTakeActionsOpen,
takeActionItems.length,
]);

const statsComponent = useMemo(
() =>
Expand Down
Expand Up @@ -55,6 +55,7 @@ const testProps = {
},
renderChildComponent,
selectedGroup: 'kibana.alert.rule.name',
onGroupClose: () => {},
};

describe('grouping accordion panel', () => {
Expand Down
Expand Up @@ -8,7 +8,7 @@

import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import type { Filter } from '@kbn/es-query';
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { firstNonNullValue } from '../../helpers';
import type { RawBucket } from '../types';
import { createGroupFilter } from './helpers';
Expand All @@ -20,8 +20,9 @@ interface GroupPanelProps<T> {
forceState?: 'open' | 'closed';
groupBucket: RawBucket<T>;
groupPanelRenderer?: JSX.Element;
groupingLevel?: number;
isLoading: boolean;
level?: number;
onGroupClose: () => void;
onToggleGroup?: (isOpen: boolean, groupBucket: RawBucket<T>) => void;
renderChildComponent: (groupFilter: Filter[]) => React.ReactElement;
selectedGroup: string;
Expand All @@ -40,18 +41,30 @@ const DefaultGroupPanelRenderer = ({ title }: { title: string }) => (
);

const GroupPanelComponent = <T,>({
customAccordionButtonClassName = 'groupingAccordionForm__button',
customAccordionButtonClassName,
customAccordionClassName = 'groupingAccordionForm',
extraAction,
forceState,
groupBucket,
groupPanelRenderer,
groupingLevel = 0,
isLoading,
level = 0,
onGroupClose,
onToggleGroup,
renderChildComponent,
selectedGroup,
}: GroupPanelProps<T>) => {
const lastForceState = useRef(forceState);
useEffect(() => {
if (lastForceState.current === 'open' && forceState === 'closed') {
// when parent group closes, reset pagination of any child groups
onGroupClose();
lastForceState.current = 'closed';
} else if (lastForceState.current === 'closed' && forceState === 'open') {
lastForceState.current = 'open';
}
}, [onGroupClose, forceState, selectedGroup]);

const groupFieldValue = useMemo(() => firstNonNullValue(groupBucket.key), [groupBucket.key]);

const groupFilters = useMemo(
Expand All @@ -72,20 +85,21 @@ const GroupPanelComponent = <T,>({
<EuiAccordion
buttonClassName={customAccordionButtonClassName}
buttonContent={
<div className="groupingPanelRenderer">
<div data-test-subj="group-panel-toggle" className="groupingPanelRenderer">
{groupPanelRenderer ?? <DefaultGroupPanelRenderer title={groupFieldValue} />}
</div>
}
className={customAccordionClassName}
buttonElement="div"
className={groupingLevel > 0 ? 'groupingAccordionFormLevel' : customAccordionClassName}
data-test-subj="grouping-accordion"
extraAction={extraAction}
forceState={forceState}
isLoading={isLoading}
id={`group${level}-${groupFieldValue}`}
id={`group${groupingLevel}-${groupFieldValue}`}
onToggle={onToggle}
paddingSize="m"
>
{renderChildComponent(groupFilters)}
<span data-test-subj="grouping-accordion-content">{renderChildComponent(groupFilters)}</span>
</EuiAccordion>
);
};
Expand Down
Expand Up @@ -43,7 +43,7 @@ const testProps = {
esTypes: ['ip'],
},
],
groupSelected: 'kibana.alert.rule.name',
groupsSelected: ['kibana.alert.rule.name'],
onGroupChange,
options: [
{
Expand Down Expand Up @@ -90,4 +90,38 @@ describe('group selector', () => {
fireEvent.click(getByTestId('panel-none'));
expect(onGroupChange).toHaveBeenCalled();
});
it('Labels button in correct selection order', () => {
const { getByTestId, rerender } = render(
<GroupSelector
{...testProps}
groupsSelected={[...testProps.groupsSelected, 'user.name', 'host.name']}
/>
);
expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, User name, Host name');
rerender(
<GroupSelector
{...testProps}
groupsSelected={[...testProps.groupsSelected, 'host.name', 'user.name']}
/>
);
expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, Host name, User name');
});
it('Labels button with selection not in options', () => {
const { getByTestId } = render(
<GroupSelector
{...testProps}
groupsSelected={[...testProps.groupsSelected, 'ugly.name', 'host.name']}
/>
);
expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, Host name');
});
it('Labels button when `none` is selected', () => {
const { getByTestId } = render(
<GroupSelector
{...testProps}
groupsSelected={[...testProps.groupsSelected, 'ugly.name', 'host.name']}
/>
);
expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, Host name');
});
});

0 comments on commit 0c3d700

Please sign in to comment.