Skip to content

Commit

Permalink
[Security Solution] Grouping - fix null group missing (elastic#155763)
Browse files Browse the repository at this point in the history
(cherry picked from commit 1a39471)
  • Loading branch information
stephmilovic committed Apr 27, 2023
1 parent f657db7 commit 8e7f916
Show file tree
Hide file tree
Showing 23 changed files with 1,975 additions and 1,925 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,22 @@ export const createGroupFilter = (selectedGroup: string, query?: string) =>
},
]
: [];

export const getNullGroupFilter = (selectedGroup: string) => [
{
meta: {
disabled: false,
negate: true,
alias: null,
key: selectedGroup,
field: selectedGroup,
value: 'exists',
type: 'exists',
},
query: {
exists: {
field: selectedGroup,
},
},
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
* Side Public License, v 1.
*/

import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiIconTip } from '@elastic/eui';
import type { Filter } from '@kbn/es-query';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { firstNonNullValue } from '../../helpers';
import type { RawBucket } from '../types';
import { createGroupFilter } from './helpers';
import { createGroupFilter, getNullGroupFilter } from './helpers';

interface GroupPanelProps<T> {
customAccordionButtonClassName?: string;
Expand All @@ -22,20 +22,35 @@ interface GroupPanelProps<T> {
groupPanelRenderer?: JSX.Element;
groupingLevel?: number;
isLoading: boolean;
isNullGroup?: boolean;
nullGroupMessage?: string;
onGroupClose: () => void;
onToggleGroup?: (isOpen: boolean, groupBucket: RawBucket<T>) => void;
renderChildComponent: (groupFilter: Filter[]) => React.ReactElement;
selectedGroup: string;
}

const DefaultGroupPanelRenderer = ({ title }: { title: string }) => (
const DefaultGroupPanelRenderer = ({
isNullGroup,
title,
nullGroupMessage,
}: {
isNullGroup: boolean;
title: string;
nullGroupMessage?: string;
}) => (
<div>
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiTitle size="xs" className="euiAccordionForm__title">
<h4 className="eui-textTruncate">{title}</h4>
</EuiTitle>
</EuiFlexItem>
{isNullGroup && nullGroupMessage && (
<EuiFlexItem grow={false} data-test-subj="null-group-icon">
<EuiIconTip content={nullGroupMessage} position="right" />
</EuiFlexItem>
)}
</EuiFlexGroup>
</div>
);
Expand All @@ -49,10 +64,12 @@ const GroupPanelComponent = <T,>({
groupPanelRenderer,
groupingLevel = 0,
isLoading,
isNullGroup = false,
onGroupClose,
onToggleGroup,
renderChildComponent,
selectedGroup,
nullGroupMessage,
}: GroupPanelProps<T>) => {
const lastForceState = useRef(forceState);
useEffect(() => {
Expand All @@ -68,8 +85,11 @@ const GroupPanelComponent = <T,>({
const groupFieldValue = useMemo(() => firstNonNullValue(groupBucket.key), [groupBucket.key]);

const groupFilters = useMemo(
() => createGroupFilter(selectedGroup, groupFieldValue),
[groupFieldValue, selectedGroup]
() =>
isNullGroup
? getNullGroupFilter(selectedGroup)
: createGroupFilter(selectedGroup, groupFieldValue),
[groupFieldValue, isNullGroup, selectedGroup]
);

const onToggle = useCallback(
Expand All @@ -86,7 +106,13 @@ const GroupPanelComponent = <T,>({
buttonClassName={customAccordionButtonClassName}
buttonContent={
<div data-test-subj="group-panel-toggle" className="groupingPanelRenderer">
{groupPanelRenderer ?? <DefaultGroupPanelRenderer title={groupFieldValue} />}
{groupPanelRenderer ?? (
<DefaultGroupPanelRenderer
title={groupFieldValue}
isNullGroup={isNullGroup}
nullGroupMessage={nullGroupMessage}
/>
)}
</div>
}
buttonElement="div"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@

import React from 'react';
import { EuiContextMenuItem } from '@elastic/eui';
export const rule1Name = 'Rule 1 name';
const rule1Desc = 'Rule 1 description';
export const rule2Name = 'Rule 2 name';
const rule2Desc = 'Rule 2 description';
export const host1Name = 'nice-host';
export const host2Name = 'cool-host';

export const mockGroupingProps = {
activePage: 0,
Expand All @@ -24,13 +22,13 @@ export const mockGroupingProps = {
sum_other_doc_count: 0,
buckets: [
{
key: [rule1Name, rule1Desc],
key_as_string: `${rule1Name}|${rule1Desc}`,
key: [host1Name],
key_as_string: `${host1Name}`,
doc_count: 1,
hostsCountAggregation: {
value: 1,
},
ruleTags: {
hostTags: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [],
Expand All @@ -56,13 +54,13 @@ export const mockGroupingProps = {
},
},
{
key: [rule2Name, rule2Desc],
key_as_string: `${rule2Name}|${rule2Desc}`,
key: [host2Name],
key_as_string: `${host2Name}`,
doc_count: 1,
hostsCountAggregation: {
value: 1,
},
ruleTags: {
hostTags: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [],
Expand All @@ -87,18 +85,51 @@ export const mockGroupingProps = {
value: 1,
},
},
{
key: ['-'],
key_as_string: `-`,
isNullGroup: true,
doc_count: 11,
hostsCountAggregation: {
value: 11,
},
hostTags: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [],
},
alertsCount: {
value: 11,
},
severitiesSubAggregation: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: 'low',
doc_count: 11,
},
],
},
countSeveritySubAggregation: {
value: 11,
},
usersCountAggregation: {
value: 11,
},
},
],
},
unitsCount: {
value: 2,
value: 3,
},
},
groupingId: 'test-grouping-id',
isLoading: false,
itemsPerPage: 25,
renderChildComponent: () => <p>{'child component'}</p>,
onGroupClose: () => {},
selectedGroup: 'kibana.alert.rule.name',
selectedGroup: 'host.name',
takeActionItems: () => [
<EuiContextMenuItem key="acknowledged" onClick={() => {}}>
{'Mark as acknowledged'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ export default {
},
};

export const Emtpy: Story<void> = () => {
export const Empty: Story<void> = () => {
return <Grouping {...mockGroupingProps} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import { fireEvent, render, within } from '@testing-library/react';
import React from 'react';
import { I18nProvider } from '@kbn/i18n-react';
import { Grouping } from './grouping';
import { createGroupFilter } from './accordion_panel/helpers';
import { createGroupFilter, getNullGroupFilter } from './accordion_panel/helpers';
import { METRIC_TYPE } from '@kbn/analytics';
import { getTelemetryEvent } from '../telemetry/const';

import { mockGroupingProps, rule1Name, rule2Name } from './grouping.mock';
import { mockGroupingProps, host1Name, host2Name } from './grouping.mock';

const renderChildComponent = jest.fn();
const takeActionItems = jest.fn();
Expand All @@ -37,9 +37,9 @@ describe('grouping container', () => {
<Grouping {...testProps} />
</I18nProvider>
);
expect(getByTestId('unit-count').textContent).toBe('2 events');
expect(getByTestId('group-count').textContent).toBe('2 groups');
expect(getAllByTestId('grouping-accordion').length).toBe(2);
expect(getByTestId('unit-count').textContent).toBe('14 events');
expect(getByTestId('group-count').textContent).toBe('3 groups');
expect(getAllByTestId('grouping-accordion').length).toBe(3);
expect(queryByTestId('empty-results-panel')).not.toBeInTheDocument();
});

Expand Down Expand Up @@ -79,12 +79,12 @@ describe('grouping container', () => {
fireEvent.click(group1);
expect(renderChildComponent).toHaveBeenNthCalledWith(
1,
createGroupFilter(testProps.selectedGroup, rule1Name)
createGroupFilter(testProps.selectedGroup, host1Name)
);
fireEvent.click(group2);
expect(renderChildComponent).toHaveBeenNthCalledWith(
2,
createGroupFilter(testProps.selectedGroup, rule2Name)
createGroupFilter(testProps.selectedGroup, host2Name)
);
});

Expand Down Expand Up @@ -116,4 +116,24 @@ describe('grouping container', () => {
})
);
});

it('Renders a null group and passes the correct filter to take actions and child component', () => {
takeActionItems.mockReturnValue([]);
const { getAllByTestId, getByTestId } = render(
<I18nProvider>
<Grouping {...testProps} />
</I18nProvider>
);
expect(getByTestId('null-group-icon')).toBeInTheDocument();

let lastGroup = getAllByTestId('grouping-accordion').at(-1);
fireEvent.click(within(lastGroup!).getByTestId('take-action-button'));

expect(takeActionItems).toHaveBeenCalledWith(getNullGroupFilter('host.name'), 2);

lastGroup = getAllByTestId('grouping-accordion').at(-1);
fireEvent.click(within(lastGroup!).getByTestId('group-panel-toggle'));

expect(renderChildComponent).toHaveBeenCalledWith(getNullGroupFilter('host.name'));
});
});
35 changes: 29 additions & 6 deletions packages/kbn-securitysolution-grouping/src/components/grouping.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ import type { Filter } from '@kbn/es-query';
import React, { useMemo, useState } from 'react';
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
import { defaultUnit, firstNonNullValue } from '../helpers';
import { createGroupFilter } from './accordion_panel/helpers';
import { createGroupFilter, getNullGroupFilter } from './accordion_panel/helpers';
import { GroupPanel } from './accordion_panel';
import { GroupStats } from './accordion_panel/group_stats';
import { EmptyGroupingComponent } from './empty_results_panel';
import { countCss, groupingContainerCss, groupingContainerCssLevel } from './styles';
import { GROUPS_UNIT } from './translations';
import { GROUPS_UNIT, NULL_GROUP } from './translations';
import type { GroupingAggregation, GroupPanelRenderer } from './types';
import { GroupStatsRenderer, OnGroupToggle } from './types';
import { getTelemetryEvent } from '../telemetry/const';
Expand Down Expand Up @@ -78,13 +78,20 @@ const GroupingComponent = <T,>({
const [trigger, setTrigger] = useState<Record<string, { state: 'open' | 'closed' | undefined }>>(
{}
);
const [nullCount, setNullCount] = useState({ unit: 0, group: 0 });

const unitCount = data?.unitsCount?.value ?? 0;
const unitCount = useMemo(
() => (data?.unitsCount?.value ?? 0) + nullCount.unit,
[data?.unitsCount?.value, nullCount.unit]
);
const unitCountText = useMemo(() => {
return `${unitCount.toLocaleString()} ${unit && unit(unitCount)}`;
}, [unitCount, unit]);

const groupCount = data?.groupsCount?.value ?? 0;
const groupCount = useMemo(
() => (data?.groupsCount?.value ?? 0) + nullCount.group,
[data?.groupsCount?.value, nullCount.group]
);
const groupCountText = useMemo(
() => `${groupCount.toLocaleString()} ${GROUPS_UNIT(groupCount)}`,
[groupCount]
Expand All @@ -95,15 +102,28 @@ const GroupingComponent = <T,>({
data?.groupByFields?.buckets?.map((groupBucket, groupNumber) => {
const group = firstNonNullValue(groupBucket.key);
const groupKey = `group-${groupNumber}-${group}`;
const isNullGroup = groupBucket.isNullGroup ?? false;
const nullGroupMessage = isNullGroup
? NULL_GROUP(selectedGroup, unit(groupBucket.doc_count))
: undefined;
if (isNullGroup) {
setNullCount({ unit: groupBucket.doc_count, group: 1 });
}

return (
<span key={groupKey}>
<GroupPanel
isNullGroup={isNullGroup}
nullGroupMessage={nullGroupMessage}
onGroupClose={onGroupClose}
extraAction={
<GroupStats
bucketKey={groupKey}
groupFilter={createGroupFilter(selectedGroup, group)}
groupFilter={
isNullGroup
? getNullGroupFilter(selectedGroup)
: createGroupFilter(selectedGroup, group)
}
groupNumber={groupNumber}
statRenderers={
groupStatsRenderer && groupStatsRenderer(selectedGroup, groupBucket)
Expand All @@ -114,7 +134,8 @@ const GroupingComponent = <T,>({
forceState={(trigger[groupKey] && trigger[groupKey].state) ?? 'closed'}
groupBucket={groupBucket}
groupPanelRenderer={
groupPanelRenderer && groupPanelRenderer(selectedGroup, groupBucket)
groupPanelRenderer &&
groupPanelRenderer(selectedGroup, groupBucket, nullGroupMessage)
}
isLoading={isLoading}
onToggleGroup={(isOpen) => {
Expand Down Expand Up @@ -157,8 +178,10 @@ const GroupingComponent = <T,>({
takeActionItems,
tracker,
trigger,
unit,
]
);

const pageCount = useMemo(
() => (groupCount ? Math.ceil(groupCount / itemsPerPage) : 1),
[groupCount, itemsPerPage]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,10 @@ export const DEFAULT_UNIT = (totalCount: number) =>
values: { totalCount },
defaultMessage: `{totalCount, plural, =1 {event} other {events}}`,
});

export const NULL_GROUP = (selectedGroup: string, unit: string) =>
i18n.translate('grouping.nullGroup.title', {
values: { selectedGroup, unit },
defaultMessage:
'The selected group by field, {selectedGroup}, is missing a value for this group of {unit}.',
});

0 comments on commit 8e7f916

Please sign in to comment.