Skip to content

Commit

Permalink
[Cloud Security] [Findings] Adding grouping component (#169884)
Browse files Browse the repository at this point in the history
## Summary

It closes #168542

This PR introduces a new Grouping feature to the Findings page as
described in #168542. It uses the Grouping component from the
`@kbn/securitysolution-grouping` package abstracts all common code
related to tables across our solutions.

### Changes included

#### @kbn/securitysolution-grouping: 

- Updated grouping component logic to behave in toggle mode when
maxGroupingLeves = 1
- Added an alternative label to the grouping component when when
maxGroupingLeves = 1 ("Select grouping" instead of "Select up to 1
groupings")

#### Findings page

- Added group by component
- Added default group by options: None (default), Resource, Rule name,
Cloud account, Kubernetes
- Reusing the latest findings table for rendering the table
visualizations when expanding a group with a filter (added
`nonPersistedFilter` to combine the group by filter with the Url Params
filtering)

#### Dashboard

- Changed redirect link from the findings resources page to the findings
page.

### Out of scope (not included)

- Removing the code for the group by resource pages that are no longer
used will be done in a separate ticket alongside code optimizations due
to refactoring.
- Case insensitive sort of the results as it will need to be addressed
in a separate effort.
- Custom rendering for each of the group by default views (to be
addressed in separate efforts)

### Screenshot

<img width="1495" alt="image"
src="https://github.com/elastic/kibana/assets/19270322/72fe9119-5c7a-454a-a08b-dfc19baa6af4">


### Recordings



https://github.com/elastic/kibana/assets/19270322/f8897c96-5509-45df-a612-38e82d4fe6d1




https://github.com/elastic/kibana/assets/19270322/31867c62-d09f-4b71-b886-aeee83e6db8a

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
opauloh and kibanamachine authored Nov 28, 2023
1 parent 82173b5 commit 9c4847f
Show file tree
Hide file tree
Showing 32 changed files with 1,517 additions and 240 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,52 @@ describe('group selector', () => {
);
expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, Host name');
});
describe('when maxGroupingLevels is 1', () => {
it('Presents single option selector label when dropdown is clicked', () => {
const { getByTestId } = render(
<GroupSelector {...testProps} maxGroupingLevels={1} groupsSelected={[]} />
);
fireEvent.click(getByTestId('group-selector-dropdown'));
expect(getByTestId('contextMenuPanelTitle').textContent).toMatch(/select grouping/i);
});
it('Does not disable any options when maxGroupingLevels is 1 and one option is selected', () => {
const groupSelected = ['kibana.alert.rule.name'];

const { getByTestId } = render(
<GroupSelector {...testProps} maxGroupingLevels={1} groupsSelected={groupSelected} />
);

fireEvent.click(getByTestId('group-selector-dropdown'));

[...testProps.options, { key: 'custom', label: 'Custom field' }].forEach((o) => {
expect(getByTestId(`panel-${o.key}`)).not.toHaveAttribute('disabled');
});
});
});
describe('when maxGroupingLevels is greater than 1', () => {
it('Presents select up to "X" groupings when dropdown is clicked', () => {
const { getByTestId } = render(
<GroupSelector {...testProps} maxGroupingLevels={3} groupsSelected={[]} />
);
fireEvent.click(getByTestId('group-selector-dropdown'));
expect(getByTestId('contextMenuPanelTitle').textContent).toMatch(/select up to 3 groupings/i);
});
it('Disables non-selected options when maxGroupingLevels is greater than 1 and the selects items reaches the maxGroupingLevels', () => {
const groupSelected = ['kibana.alert.rule.name', 'user.name'];

const { getByTestId } = render(
<GroupSelector {...testProps} maxGroupingLevels={2} groupsSelected={groupSelected} />
);

fireEvent.click(getByTestId('group-selector-dropdown'));

[...testProps.options, { key: 'custom', label: 'Custom field' }].forEach((o) => {
if (groupSelected.includes(o.key) || o.key === 'none') {
expect(getByTestId(`panel-${o.key}`)).not.toHaveAttribute('disabled');
} else {
expect(getByTestId(`panel-${o.key}`)).toHaveAttribute('disabled');
}
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,21 @@ const GroupSelectorComponent = ({
[groupsSelected]
);

const panels: EuiContextMenuPanelDescriptor[] = useMemo(
() => [
const panels: EuiContextMenuPanelDescriptor[] = useMemo(() => {
const isOptionDisabled = (key?: string) => {
// Do not disable when maxGroupingLevels is 1 to allow toggling between groups
if (maxGroupingLevels === 1) {
return false;
}
// Disable all non selected options when the maxGroupingLevels is reached
return groupsSelected.length === maxGroupingLevels && (key ? !isGroupSelected(key) : true);
};

return [
{
id: 'firstPanel',
title: i18n.SELECT_FIELD(maxGroupingLevels),
title:
maxGroupingLevels === 1 ? i18n.SELECT_SINGLE_FIELD : i18n.SELECT_FIELD(maxGroupingLevels),
items: [
{
'data-test-subj': 'panel-none',
Expand All @@ -57,7 +67,7 @@ const GroupSelectorComponent = ({
},
...options.map<EuiContextMenuPanelItemDescriptor>((o) => ({
'data-test-subj': `panel-${o.key}`,
disabled: groupsSelected.length === maxGroupingLevels && !isGroupSelected(o.key),
disabled: isOptionDisabled(o.key),
name: o.label,
onClick: () => onGroupChange(o.key),
icon: isGroupSelected(o.key) ? 'check' : 'empty',
Expand All @@ -66,7 +76,7 @@ const GroupSelectorComponent = ({
'data-test-subj': `panel-custom`,
name: i18n.CUSTOM_FIELD,
icon: 'empty',
disabled: groupsSelected.length === maxGroupingLevels,
disabled: isOptionDisabled(),
panel: 'customPanel',
hasPanel: true,
},
Expand All @@ -87,9 +97,8 @@ const GroupSelectorComponent = ({
/>
),
},
],
[fields, groupsSelected.length, isGroupSelected, maxGroupingLevels, onGroupChange, options]
);
];
}, [fields, groupsSelected.length, isGroupSelected, maxGroupingLevels, onGroupChange, options]);
const selectedOptions = useMemo(
() => options.filter((groupOption) => isGroupSelected(groupOption.key)),
[isGroupSelected, options]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export const SELECT_FIELD = (groupingLevelsCount: number) =>
defaultMessage: 'Select up to {groupingLevelsCount} groupings',
});

export const SELECT_SINGLE_FIELD = i18n.translate('grouping.groupBySingleField', {
defaultMessage: 'Select grouping',
});

export const NONE = i18n.translate('grouping.noneGroupByOptionName', {
defaultMessage: 'None',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ type RunTimeMappings =
| Record<string, Omit<RuntimeFieldSpec, 'type'> & { type: RuntimePrimitiveTypes }>
| undefined;

interface BoolAgg {
export interface BoolAgg {
bool: BoolQuery;
}

interface RangeAgg {
export interface RangeAgg {
range: { '@timestamp': { gte: string; lte: string } };
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,34 @@ describe('Group Selector Hooks', () => {
});
});

it('On group change when maxGroupingLevels is 1, remove previously selected group', () => {
const testGroup = {
[groupingId]: {
...defaultGroup,
options: defaultGroupingOptions,
activeGroups: ['host.name'],
},
};
const { result } = renderHook((props) => useGetGroupSelector(props), {
initialProps: {
...defaultArgs,
maxGroupingLevels: 1,
groupingState: {
groupById: testGroup,
},
},
});
act(() => result.current.props.onGroupChange('user.name'));

expect(dispatch).toHaveBeenCalledWith({
payload: {
id: groupingId,
activeGroups: ['user.name'],
},
type: ActionType.updateActiveGroups,
});
});

it('On group change, resets active page, sets active group, and leaves options alone', () => {
const testGroup = {
[groupingId]: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface UseGetGroupSelectorArgs {
event: string | string[],
count?: number | undefined
) => void;
title?: string;
}

interface UseGetGroupSelectorStateless
Expand Down Expand Up @@ -84,6 +85,7 @@ export const useGetGroupSelector = ({
onGroupChange,
onOptionsChange,
tracker,
title,
}: UseGetGroupSelectorArgs) => {
const { activeGroups: selectedGroups, options } =
groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup;
Expand All @@ -110,20 +112,25 @@ export const useGetGroupSelector = ({

const onChange = useCallback(
(groupSelection: string) => {
if (selectedGroups.find((selected) => selected === groupSelection)) {
const groups = selectedGroups.filter((selectedGroup) => selectedGroup !== groupSelection);
if (groups.length === 0) {
setSelectedGroups(['none']);
} else {
setSelectedGroups(groups);
// Simulate a toggle behavior when maxGroupingLevels is 1
if (maxGroupingLevels === 1) {
setSelectedGroups([groupSelection]);
} else {
if (selectedGroups.find((selected) => selected === groupSelection)) {
const groups = selectedGroups.filter((selectedGroup) => selectedGroup !== groupSelection);
if (groups.length === 0) {
setSelectedGroups(['none']);
} else {
setSelectedGroups(groups);
}
return;
}
return;
}

const newSelectedGroups = isNoneGroup([groupSelection])
? [groupSelection]
: [...selectedGroups.filter((selectedGroup) => selectedGroup !== 'none'), groupSelection];
setSelectedGroups(newSelectedGroups);
const newSelectedGroups = isNoneGroup([groupSelection])
? [groupSelection]
: [...selectedGroups.filter((selectedGroup) => selectedGroup !== 'none'), groupSelection];
setSelectedGroups(newSelectedGroups);
}

// built-in telemetry: UI-counter
tracker?.(
Expand All @@ -133,7 +140,7 @@ export const useGetGroupSelector = ({

onGroupChange?.({ tableId: groupingId, groupByField: groupSelection });
},
[groupingId, onGroupChange, selectedGroups, setSelectedGroups, tracker]
[groupingId, maxGroupingLevels, onGroupChange, selectedGroups, setSelectedGroups, tracker]
);

useEffect(() => {
Expand Down Expand Up @@ -184,6 +191,7 @@ export const useGetGroupSelector = ({
fields,
maxGroupingLevels,
options,
title,
}}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { Grouping as GroupingComponent } from '../components/grouping';
/** Interface for grouping object where T is the `GroupingAggregation`
* @interface GroupingArgs<T>
*/
interface Grouping<T> {
export interface UseGrouping<T> {
getGrouping: (props: DynamicGroupingProps<T>) => React.ReactElement;
groupSelector: React.ReactElement<GroupSelectorProps>;
selectedGroups: string[];
Expand Down Expand Up @@ -72,6 +72,7 @@ interface GroupingArgs<T> {
event: string | string[],
count?: number | undefined
) => void;
title?: string;
}

/**
Expand All @@ -85,6 +86,7 @@ interface GroupingArgs<T> {
* @param onGroupChange callback executed when selected group is changed, used for tracking
* @param onOptionsChange callback executed when grouping options are changed, used for consumer grouping selector
* @param tracker telemetry handler
* @param title of the grouping selector component
* @returns {@link Grouping} the grouping constructor { getGrouping, groupSelector, pagination, selectedGroups }
*/
export const useGrouping = <T,>({
Expand All @@ -96,7 +98,8 @@ export const useGrouping = <T,>({
onGroupChange,
onOptionsChange,
tracker,
}: GroupingArgs<T>): Grouping<T> => {
title,
}: GroupingArgs<T>): UseGrouping<T> => {
const [groupingState, dispatch] = useReducer(groupsReducerWithStorage, initialState);
const { activeGroups: selectedGroups } = useMemo(
() => groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup,
Expand Down Expand Up @@ -125,6 +128,7 @@ export const useGrouping = <T,>({
onGroupChange,
onOptionsChange,
tracker,
title,
});

const getGrouping = useCallback(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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.
*/

export * from './use_cloud_posture_data_table';
Loading

0 comments on commit 9c4847f

Please sign in to comment.