[Security solution] Grouping package - state management (#152627)

This commit is contained in:
Steph Milovic 2023-03-06 18:28:34 -07:00 committed by GitHub
parent 36e8f0d169
commit 8e2ea3ebea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 614 additions and 417 deletions

View file

@ -8,24 +8,30 @@
import React from 'react';
import {
GroupSelectorProps,
Grouping,
GroupingProps,
GroupSelector,
GroupSelectorProps,
RawBucket,
getGroupingQuery,
isNoneGroup,
useGrouping,
} from './src';
import type {
GroupOption,
GroupingAggregation,
GroupingFieldTotalAggregation,
NamedAggregation,
} from './src';
import type { NamedAggregation, GroupingFieldTotalAggregation, GroupingAggregation } from './src';
export const getGrouping = <T,>(props: GroupingProps<T>): React.ReactElement<GroupingProps<T>> => (
<Grouping {...props} />
);
export const getGroupSelector = (
props: GroupSelectorProps
): React.ReactElement<GroupSelectorProps> => <GroupSelector {...props} />;
export { isNoneGroup, getGroupingQuery };
export { getGroupingQuery, isNoneGroup, useGrouping };
export type { GroupingAggregation, GroupingFieldTotalAggregation, NamedAggregation, RawBucket };
export type {
GroupOption,
GroupingAggregation,
GroupingFieldTotalAggregation,
NamedAggregation,
RawBucket,
};

View file

@ -48,7 +48,7 @@ export const EmptyGroupingComponent: React.FC<{ height?: keyof typeof heights }>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiImage size="200" alt="" src={noResultsIllustrationLight} />
<EuiImage size="200px" alt="" src={noResultsIllustrationLight} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>

View file

@ -51,10 +51,10 @@ export class CustomFieldPanel extends React.PureComponent<Props, State> {
<div data-test-subj="custom-field-panel" style={{ padding: 16 }}>
<EuiForm>
<EuiFormRow
label={i18n.translate('grouping.groupsSelector.customGroupByFieldLabel', {
label={i18n.translate('grouping.groupSelector.customGroupByFieldLabel', {
defaultMessage: 'Field',
})}
helpText={i18n.translate('grouping.groupsSelector.customGroupByHelpText', {
helpText={i18n.translate('grouping.groupSelector.customGroupByHelpText', {
defaultMessage: 'This is the field used for the terms aggregation',
})}
display="rowCompressed"
@ -63,7 +63,7 @@ export class CustomFieldPanel extends React.PureComponent<Props, State> {
<EuiComboBox
data-test-subj="groupByCustomField"
placeholder={i18n.translate(
'grouping.groupsSelector.customGroupByDropdownPlacehoder',
'grouping.groupSelector.customGroupByDropdownPlacehoder',
{
defaultMessage: 'Select one',
}

View file

@ -20,7 +20,7 @@ import { createGroupFilter } from './accordion_panel/helpers';
import type { BadgeMetric, CustomMetric } from './accordion_panel';
import { GroupPanel } from './accordion_panel';
import { GroupStats } from './accordion_panel/group_stats';
import { EmptyGroupingComponent } from './empty_resuls_panel';
import { EmptyGroupingComponent } from './empty_results_panel';
import { groupingContainerCss, groupsUnitCountCss } from './styles';
import { GROUPS_UNIT } from './translations';
import type { GroupingAggregation, GroupingFieldTotalAggregation, RawBucket } from './types';
@ -30,7 +30,7 @@ export interface GroupingProps<T> {
customMetricStats?: (fieldBucket: RawBucket<T>) => CustomMetric[];
data?: GroupingAggregation<T> & GroupingFieldTotalAggregation;
groupPanelRenderer?: (fieldBucket: RawBucket<T>) => JSX.Element | undefined;
groupsSelector?: JSX.Element;
groupSelector?: JSX.Element;
inspectButton?: JSX.Element;
isLoading: boolean;
pagination: {
@ -51,7 +51,7 @@ const GroupingComponent = <T,>({
customMetricStats,
data,
groupPanelRenderer,
groupsSelector,
groupSelector,
inspectButton,
isLoading,
pagination,
@ -163,7 +163,7 @@ const GroupingComponent = <T,>({
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs">
{inspectButton && <EuiFlexItem>{inspectButton}</EuiFlexItem>}
<EuiFlexItem>{groupsSelector}</EuiFlexItem>
<EuiFlexItem>{groupSelector}</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -12,4 +12,4 @@ export * from './group_selector';
export * from './types';
export * from './grouping';
export const isNoneGroup = (groupKey: string) => groupKey === NONE_GROUP_KEY;
export const isNoneGroup = (groupKey: string | null) => groupKey === NONE_GROUP_KEY;

View file

@ -5,6 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EMPTY_GROUP_BY_ID, GroupModel, GroupsById, Storage } from './hooks/types';
import * as i18n from './components/translations';
/**
@ -30,3 +31,23 @@ export function firstNonNullValue<T>(valueOrCollection: ECSField<T>): T | undefi
}
export const defaultUnit = (n: number) => i18n.ALERTS_UNIT(n);
const LOCAL_STORAGE_GROUPING_KEY = 'groups';
export const getAllGroupsInStorage = (storage: Storage): GroupsById => {
const allGroups = storage.getItem(LOCAL_STORAGE_GROUPING_KEY);
if (!allGroups) {
return EMPTY_GROUP_BY_ID;
}
return JSON.parse(allGroups);
};
export const addGroupsToStorage = (storage: Storage, groupingId: string, group: GroupModel) => {
const groups = getAllGroupsInStorage(storage);
storage.setItem(
LOCAL_STORAGE_GROUPING_KEY,
JSON.stringify({
...groups,
[groupingId]: group,
})
);
};

View file

@ -0,0 +1,79 @@
/*
* 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.
*/
import {
ActionType,
GroupOption,
UpdateActiveGroup,
UpdateGroupActivePage,
UpdateGroupItemsPerPage,
UpdateGroupOptions,
} from '../types';
const updateActiveGroup = ({
activeGroup,
id,
}: {
activeGroup: string;
id: string;
}): UpdateActiveGroup => ({
payload: {
activeGroup,
id,
},
type: ActionType.updateActiveGroup,
});
const updateGroupActivePage = ({
activePage,
id,
}: {
activePage: number;
id: string;
}): UpdateGroupActivePage => ({
payload: {
activePage,
id,
},
type: ActionType.updateGroupActivePage,
});
const updateGroupItemsPerPage = ({
itemsPerPage,
id,
}: {
itemsPerPage: number;
id: string;
}): UpdateGroupItemsPerPage => ({
payload: {
itemsPerPage,
id,
},
type: ActionType.updateGroupItemsPerPage,
});
const updateGroupOptions = ({
newOptionList,
id,
}: {
newOptionList: GroupOption[];
id: string;
}): UpdateGroupOptions => ({
payload: {
newOptionList,
id,
},
type: ActionType.updateGroupOptions,
});
export const groupActions = {
updateActiveGroup,
updateGroupActivePage,
updateGroupItemsPerPage,
updateGroupOptions,
};

View file

@ -0,0 +1,11 @@
/*
* 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.
*/
export * from './actions';
export * from './reducer';
export * from './selectors';

View file

@ -0,0 +1,103 @@
/*
* 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.
*/
import {
Action,
ActionType,
defaultGroup,
EMPTY_GROUP_BY_ID,
GroupMap,
GroupsById,
Storage,
} from '../types';
import { addGroupsToStorage, getAllGroupsInStorage } from '../..';
const storage: Storage = window.localStorage;
export const initialState: GroupMap = {
groupById: EMPTY_GROUP_BY_ID,
};
const groupsReducer = (state: GroupMap, action: Action, groupsById: GroupsById) => {
switch (action.type) {
case ActionType.updateActiveGroup: {
const { id, activeGroup } = action.payload;
return {
...state,
groupById: {
...groupsById,
[id]: {
...groupsById[id],
activeGroup,
},
},
};
}
case ActionType.updateGroupActivePage: {
const { id, activePage } = action.payload;
return {
...state,
groupById: {
...groupsById,
[id]: {
...groupsById[id],
activePage,
},
},
};
}
case ActionType.updateGroupItemsPerPage: {
const { id, itemsPerPage } = action.payload;
return {
...state,
groupById: {
...groupsById,
[id]: {
...groupsById[id],
itemsPerPage,
},
},
};
}
case ActionType.updateGroupOptions: {
const { id, newOptionList } = action.payload;
return {
...state,
groupById: {
...groupsById,
[id]: {
...defaultGroup,
...groupsById[id],
options: newOptionList,
},
},
};
}
}
throw Error(`Unknown grouping action`);
};
export const groupsReducerWithStorage = (state: GroupMap, action: Action) => {
let groupsInStorage = {};
if (storage) {
groupsInStorage = getAllGroupsInStorage(storage);
}
const groupsById: GroupsById = {
...state.groupById,
...groupsInStorage,
};
const newState = groupsReducer(state, action, groupsById);
if (storage) {
const groupId: string = action.payload.id;
addGroupsToStorage(storage, groupId, newState.groupById[groupId]);
}
return newState;
};

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
import { GroupsById, GroupState } from '../types';
const selectGroupById = (state: GroupState): GroupsById => state.groups.groupById;
export const groupByIdSelector = (state: GroupState, id: string) => selectGroupById(state)[id];

View file

@ -0,0 +1,80 @@
/*
* 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.
*/
// action types
export enum ActionType {
updateActiveGroup = 'UPDATE_ACTIVE_GROUP',
updateGroupActivePage = 'UPDATE_GROUP_ACTIVE_PAGE',
updateGroupItemsPerPage = 'UPDATE_GROUP_ITEMS_PER_PAGE',
updateGroupOptions = 'UPDATE_GROUP_OPTIONS',
}
export interface UpdateActiveGroup {
type: ActionType.updateActiveGroup;
payload: { activeGroup: string; id: string };
}
export interface UpdateGroupActivePage {
type: ActionType.updateGroupActivePage;
payload: { activePage: number; id: string };
}
export interface UpdateGroupItemsPerPage {
type: ActionType.updateGroupItemsPerPage;
payload: { itemsPerPage: number; id: string };
}
export interface UpdateGroupOptions {
type: ActionType.updateGroupOptions;
payload: { newOptionList: GroupOption[]; id: string };
}
export type Action =
| UpdateActiveGroup
| UpdateGroupActivePage
| UpdateGroupItemsPerPage
| UpdateGroupOptions;
// state
export interface GroupOption {
key: string;
label: string;
}
export interface GroupModel {
activeGroup: string;
options: GroupOption[];
activePage: number;
itemsPerPage: number;
}
export interface GroupsById {
[id: string]: GroupModel;
}
export interface GroupMap {
groupById: GroupsById;
}
export interface GroupState {
groups: GroupMap;
}
export interface Storage<T = any, S = void> {
getItem: (key: string) => T | null;
setItem: (key: string, value: T) => S;
removeItem: (key: string) => T | null;
clear: () => void;
}
export const EMPTY_GROUP_BY_ID: GroupsById = {};
export const defaultGroup: GroupModel = {
activePage: 0,
itemsPerPage: 25,
activeGroup: 'none',
options: [],
};

View file

@ -1,34 +1,36 @@
/*
* 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.
* 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.
*/
import type { FieldSpec } from '@kbn/data-views-plugin/common';
import { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getGroupSelector, isNoneGroup } from '@kbn/securitysolution-grouping';
import type { TableId } from '../../../../../common/types';
import { getDefaultGroupingOptions } from '../../../../detections/components/alerts_table/grouping_settings';
import type { State } from '../../../store';
import { defaultGroup } from '../../../store/grouping/defaults';
import type { GroupOption } from '../../../store/grouping';
import { groupActions, groupSelectors } from '../../../store/grouping';
import { getGroupSelector, isNoneGroup } from '../..';
import { groupActions, groupByIdSelector } from './state';
import type { GroupOption } from './types';
import { Action, defaultGroup, GroupMap } from './types';
export interface UseGetGroupSelectorArgs {
defaultGroupingOptions: GroupOption[];
dispatch: React.Dispatch<Action>;
fields: FieldSpec[];
groupingId: string;
tableId: TableId;
groupingState: GroupMap;
}
export const useGetGroupSelector = ({ fields, groupingId, tableId }: UseGetGroupSelectorArgs) => {
const dispatch = useDispatch();
const getGroupByIdSelector = groupSelectors.getGroupByIdSelector();
export const useGetGroupSelector = ({
defaultGroupingOptions,
dispatch,
fields,
groupingId,
groupingState,
}: UseGetGroupSelectorArgs) => {
const { activeGroup: selectedGroup, options } =
useSelector((state: State) => getGroupByIdSelector(state, groupingId)) ?? defaultGroup;
groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup;
const setGroupsActivePage = useCallback(
(activePage: number) => {
@ -50,7 +52,6 @@ export const useGetGroupSelector = ({ fields, groupingId, tableId }: UseGetGroup
},
[dispatch, groupingId]
);
const defaultGroupingOptions = getDefaultGroupingOptions(tableId);
useEffect(() => {
if (options.length > 0) return;
@ -71,7 +72,7 @@ export const useGetGroupSelector = ({ fields, groupingId, tableId }: UseGetGroup
);
}, [defaultGroupingOptions, selectedGroup, setOptions, options]);
const groupsSelector = getGroupSelector({
return getGroupSelector({
groupSelected: selectedGroup,
'data-test-subj': 'alerts-table-group-selector',
onGroupChange: (groupSelection: string) => {
@ -81,7 +82,10 @@ export const useGetGroupSelector = ({ fields, groupingId, tableId }: UseGetGroup
setGroupsActivePage(0);
setSelectedGroup(groupSelection);
if (!isNoneGroup(groupSelection) && !options.find((o) => o.key === groupSelection)) {
if (
!isNoneGroup(groupSelection) &&
!options.find((o: GroupOption) => o.key === groupSelection)
) {
setOptions([
...defaultGroupingOptions,
{
@ -96,6 +100,4 @@ export const useGetGroupSelector = ({ fields, groupingId, tableId }: UseGetGroup
fields,
options,
});
return groupsSelector;
};

View file

@ -0,0 +1,52 @@
/*
* 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.
*/
import { renderHook } from '@testing-library/react-hooks';
import { useGrouping } from './use_grouping';
const defaultGroupingOptions = [
{ label: 'ruleName', key: 'kibana.alert.rule.name' },
{ label: 'userName', key: 'user.name' },
{ label: 'hostName', key: 'host.name' },
{ label: 'sourceIP', key: 'source.ip' },
];
const groupingId = 'test-table';
const defaultArgs = {
defaultGroupingOptions,
fields: [],
groupingId,
};
const groupingArgs = {
from: '2020-07-07T08:20:18.966Z',
globalFilters: [],
hasIndexMaintenance: true,
globalQuery: {
query: 'query',
language: 'language',
},
hasIndexWrite: true,
isLoading: false,
renderChildComponent: jest.fn(),
runtimeMappings: {},
signalIndexName: 'test',
tableId: groupingId,
takeActionItems: jest.fn(),
to: '2020-07-08T08:20:18.966Z',
};
describe('use_grouping', () => {
it('Returns the expected default results on initial mount', () => {
const { result } = renderHook(() => useGrouping(defaultArgs));
expect(result.current.selectedGroup).toEqual('none');
expect(result.current.getGrouping(groupingArgs).props.selectedGroup).toEqual('none');
expect(result.current.groupSelector.props.options).toEqual(defaultGroupingOptions);
expect(result.current.pagination).toEqual({ pageIndex: 0, pageSize: 25 });
});
});

View file

@ -0,0 +1,85 @@
/*
* 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.
*/
import { FieldSpec } from '@kbn/data-views-plugin/common';
import React, { useCallback, useMemo, useReducer } from 'react';
import { groupsReducerWithStorage, initialState } from './state/reducer';
import { GroupingProps, GroupSelectorProps } from '..';
import { useGroupingPagination } from './use_grouping_pagination';
import { groupByIdSelector } from './state';
import { useGetGroupSelector } from './use_get_group_selector';
import { defaultGroup, GroupOption } from './types';
import { Grouping as GroupingComponent } from '../components/grouping';
interface Grouping<T> {
getGrouping: (
props: Omit<GroupingProps<T>, 'groupSelector' | 'pagination' | 'selectedGroup'>
) => React.ReactElement<GroupingProps<T>>;
groupSelector: React.ReactElement<GroupSelectorProps>;
pagination: {
pageIndex: number;
pageSize: number;
};
selectedGroup: string;
}
interface GroupingArgs {
defaultGroupingOptions: GroupOption[];
fields: FieldSpec[];
groupingId: string;
}
export const useGrouping = <T,>({
defaultGroupingOptions,
fields,
groupingId,
}: GroupingArgs): Grouping<T> => {
const [groupingState, dispatch] = useReducer(groupsReducerWithStorage, initialState);
const { activeGroup: selectedGroup } = useMemo(
() => groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup,
[groupingId, groupingState]
);
const groupSelector = useGetGroupSelector({
defaultGroupingOptions,
dispatch,
fields,
groupingId,
groupingState,
});
const pagination = useGroupingPagination({ groupingId, groupingState, dispatch });
const getGrouping = useCallback(
(
props: Omit<GroupingProps<T>, 'groupSelector' | 'pagination' | 'selectedGroup'>
): React.ReactElement<GroupingProps<T>> => (
<GroupingComponent
{...props}
groupSelector={groupSelector}
pagination={pagination}
selectedGroup={selectedGroup}
/>
),
[groupSelector, pagination, selectedGroup]
);
return useMemo(
() => ({
getGrouping,
groupSelector,
selectedGroup,
pagination: {
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
},
}),
[getGrouping, groupSelector, pagination.pageIndex, pagination.pageSize, selectedGroup]
);
};

View file

@ -1,28 +1,28 @@
/*
* 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.
* 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.
*/
import { useDispatch, useSelector } from 'react-redux';
import { useCallback, useMemo } from 'react';
import { tableDefaults } from '../../../store/data_table/defaults';
import { groupActions, groupSelectors } from '../../../store/grouping';
import type { State } from '../../../store';
import { defaultGroup } from '../../../store/grouping/defaults';
import { groupActions, groupByIdSelector } from './state';
import { Action, defaultGroup, GroupMap } from './types';
export interface UseGroupingPaginationArgs {
dispatch: React.Dispatch<Action>;
groupingId: string;
groupingState: GroupMap;
}
export const useGroupingPagination = ({ groupingId }: UseGroupingPaginationArgs) => {
const dispatch = useDispatch();
const getGroupByIdSelector = groupSelectors.getGroupByIdSelector();
export const useGroupingPagination = ({
groupingId,
groupingState,
dispatch,
}: UseGroupingPaginationArgs) => {
const { activePage, itemsPerPage } =
useSelector((state: State) => getGroupByIdSelector(state, groupingId)) ?? defaultGroup;
groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup;
const setGroupsActivePage = useCallback(
(newActivePage: number) => {
@ -40,16 +40,14 @@ export const useGroupingPagination = ({ groupingId }: UseGroupingPaginationArgs)
[dispatch, groupingId]
);
const pagination = useMemo(
return useMemo(
() => ({
pageIndex: activePage,
pageSize: itemsPerPage,
onChangeItemsPerPage: setGroupsItemsPerPage,
onChangePage: setGroupsActivePage,
itemsPerPageOptions: tableDefaults.itemsPerPageOptions,
itemsPerPageOptions: [10, 25, 50, 100],
}),
[activePage, itemsPerPage, setGroupsActivePage, setGroupsItemsPerPage]
);
return pagination;
};

View file

@ -9,3 +9,5 @@
export * from './components';
export * from './containers';
export * from './helpers';
export * from './hooks/use_grouping';
export * from './hooks/types';

View file

@ -23,6 +23,6 @@
"@kbn/i18n-react",
"@kbn/kibana-react-plugin",
"@kbn/shared-svg",
"@kbn/ui-theme"
"@kbn/ui-theme",
]
}

View file

@ -35,7 +35,7 @@ import type { State, SubPluginsInitReducer } from '../common/store';
import type { Immutable } from '../../common/endpoint/types';
import type { AppAction } from '../common/store/actions';
import type { TableState } from '../common/store/data_table/types';
import type { GroupMap } from '../common/store/grouping';
import type { GroupModel } from '../common/store/grouping';
export { SecurityPageName } from '../../common/constants';
@ -50,7 +50,7 @@ export type SecuritySubPluginRoutes = RouteProps[];
export interface SecuritySubPlugin {
routes: SecuritySubPluginRoutes;
storageDataTables?: Pick<TableState, 'tableById'>;
groups?: Pick<GroupMap, 'groupById'>;
groups?: GroupModel;
exploreDataTables?: {
network: Pick<TableState, 'tableById'>;
hosts: Pick<TableState, 'tableById'>;

View file

@ -45,7 +45,7 @@ import { getScopePatternListSelection } from '../store/sourcerer/helpers';
import { mockBrowserFields, mockIndexFields, mockRuntimeMappings } from '../containers/source/mock';
import { usersModel } from '../../explore/users/store';
import { UsersFields } from '../../../common/search_strategy/security_solution/users/common';
import { defaultGroup } from '../store/grouping/defaults';
import { initialGroupingState } from '../store/grouping/reducer';
export const mockSourcererState = {
...initialSourcererState,
@ -415,13 +415,7 @@ export const mockGlobalState: State = {
},
},
},
groups: {
groupById: {
testing: {
...defaultGroup,
},
},
},
groups: initialGroupingState,
sourcerer: {
...mockSourcererState,
defaultDataView: {

View file

@ -15,7 +15,6 @@ import { createTimelineNoteEpic } from '../../timelines/store/timeline/epic_note
import { createTimelinePinnedEventEpic } from '../../timelines/store/timeline/epic_pinned_event';
import type { TimelineEpicDependencies } from '../../timelines/store/timeline/types';
import { createDataTableLocalStorageEpic } from './data_table/epic_local_storage';
import { createGroupingLocalStorageEpic } from './grouping/epic_local_storage_epic';
export const createRootEpic = <State>(): Epic<
Action,
@ -28,6 +27,5 @@ export const createRootEpic = <State>(): Epic<
createTimelineFavoriteEpic<State>(),
createTimelineNoteEpic<State>(),
createTimelinePinnedEventEpic<State>(),
createDataTableLocalStorageEpic<State>(),
createGroupingLocalStorageEpic<State>()
createDataTableLocalStorageEpic<State>()
);

View file

@ -6,30 +6,14 @@
*/
import actionCreatorFactory from 'typescript-fsa';
import type { GroupOption } from './types';
import type React from 'react';
const actionCreator = actionCreatorFactory('x-pack/security_solution/groups');
export const updateActiveGroup = actionCreator<{
id: string;
activeGroup: string;
}>('UPDATE_ACTIVE_GROUP');
export const updateGroupSelector = actionCreator<{
groupSelector: React.ReactElement;
}>('UPDATE_GROUP_SELECTOR');
export const updateGroupActivePage = actionCreator<{
id: string;
activePage: number;
}>('UPDATE_GROUP_ACTIVE_PAGE');
export const updateGroupItemsPerPage = actionCreator<{
id: string;
itemsPerPage: number;
}>('UPDATE_GROUP_ITEMS_PER_PAGE');
export const updateGroupOptions = actionCreator<{
id: string;
newOptionList: GroupOption[];
}>('UPDATE_GROUP_OPTIONS');
export const initGrouping = actionCreator<{
id: string;
}>('INIT_GROUPING');
export const updateSelectedGroup = actionCreator<{
selectedGroup: string;
}>('UPDATE_SELECTED_GROUP');

View file

@ -1,10 +0,0 @@
/*
* 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 { GroupsById } from './types';
export const EMPTY_GROUP_BY_ID: GroupsById = {};

View file

@ -1,15 +0,0 @@
/*
* 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 { GroupModel } from './types';
export const defaultGroup: GroupModel = {
activePage: 0,
itemsPerPage: 25,
activeGroup: 'none',
options: [],
};

View file

@ -1,50 +0,0 @@
/*
* 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 { Action } from 'redux';
import { map, filter, ignoreElements, tap, withLatestFrom, delay } from 'rxjs/operators';
import type { Epic } from 'redux-observable';
import { get } from 'lodash/fp';
import {
updateGroupOptions,
updateActiveGroup,
updateGroupItemsPerPage,
updateGroupActivePage,
initGrouping,
} from './actions';
import type { TimelineEpicDependencies } from '../../../timelines/store/timeline/types';
import { addGroupsToStorage } from '../../../timelines/containers/local_storage/groups';
export const isNotNull = <T>(value: T | null): value is T => value !== null;
const groupingActionTypes = [
updateActiveGroup.type,
updateGroupActivePage.type,
updateGroupItemsPerPage.type,
updateGroupOptions.type,
initGrouping.type,
];
export const createGroupingLocalStorageEpic =
<State>(): Epic<Action, Action, State, TimelineEpicDependencies<State>> =>
(action$, state$, { groupByIdSelector, storage }) => {
const group$ = state$.pipe(map(groupByIdSelector), filter(isNotNull));
return action$.pipe(
delay(500),
withLatestFrom(group$),
tap(([action, groupById]) => {
if (groupingActionTypes.includes(action.type)) {
if (storage) {
const groupId: string = get('payload.id', action);
addGroupsToStorage(storage, groupId, groupById[groupId]);
}
}
}),
ignoreElements()
);
};

View file

@ -5,15 +5,9 @@
* 2.0.
*/
import type { AnyAction, CombinedState, Reducer } from 'redux';
import * as groupActions from './actions';
import * as groupSelectors from './selectors';
import type { GroupState } from './types';
export * from './types';
export { groupActions, groupSelectors };
export interface GroupsReducer {
groups: Reducer<CombinedState<GroupState>, AnyAction>;
}

View file

@ -6,70 +6,20 @@
*/
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import {
initGrouping,
updateActiveGroup,
updateGroupActivePage,
updateGroupItemsPerPage,
updateGroupOptions,
} from './actions';
import { EMPTY_GROUP_BY_ID } from './constants';
import { defaultGroup } from './defaults';
import type { GroupMap } from './types';
import { updateGroupSelector, updateSelectedGroup } from './actions';
import type { GroupModel } from './types';
const initialGroupState: GroupMap = {
groupById: EMPTY_GROUP_BY_ID,
export const initialGroupingState: GroupModel = {
groupSelector: null,
selectedGroup: null,
};
export const groupsReducer = reducerWithInitialState(initialGroupState)
.case(updateActiveGroup, (state, { id, activeGroup }) => ({
export const groupsReducer = reducerWithInitialState(initialGroupingState)
.case(updateSelectedGroup, (state, { selectedGroup }) => ({
...state,
groupById: {
...state.groupById,
[id]: {
...state.groupById[id],
activeGroup,
},
},
selectedGroup,
}))
.case(updateGroupActivePage, (state, { id, activePage }) => ({
.case(updateGroupSelector, (state, { groupSelector }) => ({
...state,
groupById: {
...state.groupById,
[id]: {
...state.groupById[id],
activePage,
},
},
}))
.case(updateGroupItemsPerPage, (state, { id, itemsPerPage }) => ({
...state,
groupById: {
...state.groupById,
[id]: {
...state.groupById[id],
itemsPerPage,
},
},
}))
.case(updateGroupOptions, (state, { id, newOptionList }) => ({
...state,
groupById: {
...state.groupById,
[id]: {
...state.groupById[id],
options: newOptionList,
},
},
}))
.case(initGrouping, (state, { id }) => ({
...state,
groupById: {
...state.groupById,
[id]: {
...defaultGroup,
...state.groupById[id],
},
},
groupSelector,
}));

View file

@ -6,16 +6,12 @@
*/
import { createSelector } from 'reselect';
import type { GroupModel, GroupsById, GroupState } from './types';
import type { GroupState } from './types';
const selectGroupByEntityId = (state: GroupState): GroupsById => state.groups.groupById;
const groupSelector = (state: GroupState) => state.groups.groupSelector;
export const groupByIdSelector = createSelector(
selectGroupByEntityId,
(groupsByEntityId) => groupsByEntityId
);
export const getGroupSelector = () => createSelector(groupSelector, (selector) => selector);
export const selectGroup = (state: GroupState, entityId: string): GroupModel =>
state.groups.groupById[entityId];
export const selectedGroup = (state: GroupState) => state.groups.selectedGroup;
export const getGroupByIdSelector = () => createSelector(selectGroup, (group) => group);
export const getSelectedGroup = () => createSelector(selectedGroup, (group) => group);

View file

@ -5,26 +5,11 @@
* 2.0.
*/
export interface GroupOption {
key: string;
label: string;
}
export interface GroupModel {
activeGroup: string;
options: GroupOption[];
activePage: number;
itemsPerPage: number;
}
export interface GroupsById {
[id: string]: GroupModel;
}
export interface GroupMap {
groupById: GroupsById;
groupSelector: React.ReactElement | null;
selectedGroup: string | null;
}
export interface GroupState {
groups: GroupMap;
groups: GroupModel;
}

View file

@ -12,6 +12,7 @@ import { mockIndexPattern, mockSourcererState } from '../mock';
import { useSourcererDataView } from '../containers/sourcerer';
import { useDeepEqualSelector } from '../hooks/use_selector';
import { renderHook } from '@testing-library/react-hooks';
import { initialGroupingState } from './grouping/reducer';
jest.mock('../hooks/use_selector');
jest.mock('../lib/kibana', () => ({
@ -45,7 +46,7 @@ describe('createInitialState', () => {
dataTable: { tableById: {} },
},
{
groups: { groupById: {} },
groups: initialGroupingState,
}
);
beforeEach(() => {
@ -82,9 +83,7 @@ describe('createInitialState', () => {
},
},
{
groups: {
groupById: {},
},
groups: initialGroupingState,
}
);
(useDeepEqualSelector as jest.Mock).mockImplementation((cb) => cb(state));

View file

@ -24,6 +24,8 @@ import { BehaviorSubject, pluck } from 'rxjs';
import type { Storage } from '@kbn/kibana-utils-plugin/public';
import type { CoreStart } from '@kbn/core/public';
import reduceReducers from 'reduce-reducers';
import { initialGroupingState } from './grouping/reducer';
import type { GroupState } from './grouping/types';
import {
DEFAULT_DATA_VIEW_ID,
DEFAULT_INDEX_KEY,
@ -47,8 +49,6 @@ import { initDataView } from './sourcerer/model';
import type { AppObservableLibs, StartedSubPlugins, StartPlugins } from '../../types';
import type { ExperimentalFeatures } from '../../../common/experimental_features';
import { createSourcererDataView } from '../containers/sourcerer/create_sourcerer_data_view';
import type { GroupState } from './grouping/types';
import { groupSelectors } from './grouping';
type ComposeType = typeof compose;
declare global {
@ -130,12 +130,7 @@ export const createStoreFactory = async (
};
const groupsInitialState: GroupState = {
groups: {
groupById: {
/* eslint-disable @typescript-eslint/no-non-null-assertion */
...subPlugins.alerts.groups!.groupById,
},
},
groups: initialGroupingState,
};
const timelineReducer = reduceReducers(
@ -190,7 +185,6 @@ export const createStore = (
timelineByIdSelector: timelineSelectors.timelineByIdSelector,
timelineTimeRangeSelector: inputsSelectors.timelineTimeRangeSelector,
tableByIdSelector: dataTableSelectors.tableByIdSelector,
groupByIdSelector: groupSelectors.groupByIdSelector,
storage,
};

View file

@ -8,7 +8,7 @@
import { isEmpty } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo } from 'react';
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { v4 as uuidv4 } from 'uuid';
import type { Filter, Query } from '@kbn/es-query';
import { buildEsQuery } from '@kbn/es-query';
@ -18,12 +18,9 @@ import type {
GroupingAggregation,
RawBucket,
} from '@kbn/securitysolution-grouping';
import { getGrouping, isNoneGroup } from '@kbn/securitysolution-grouping';
import { isNoneGroup, useGrouping } from '@kbn/securitysolution-grouping';
import type { AlertsGroupingAggregation } from './grouping_settings/types';
import { useGetGroupSelector } from '../../../common/containers/grouping/hooks/use_get_group_selector';
import type { Status } from '../../../../common/detection_engine/schemas/common';
import { defaultGroup } from '../../../common/store/grouping/defaults';
import { groupSelectors } from '../../../common/store/grouping';
import { InspectButton } from '../../../common/components/inspect';
import { defaultUnit } from '../../../common/components/toolbar/unit';
import { useGlobalTime } from '../../../common/containers/use_global_time';
@ -32,7 +29,6 @@ import type { TableIdLiteral } from '../../../../common/types';
import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
import { useKibana } from '../../../common/lib/kibana';
import type { State } from '../../../common/store';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { useInspectButton } from '../alerts_kpis/common/hooks';
@ -42,13 +38,13 @@ import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_que
import { ALERTS_QUERY_NAMES } from '../../containers/detection_engine/alerts/constants';
import {
getAlertsGroupingQuery,
getDefaultGroupingOptions,
getSelectedGroupBadgeMetrics,
getSelectedGroupButtonContent,
getSelectedGroupCustomMetrics,
useGroupTakeActionsItems,
} from './grouping_settings';
import { initGrouping } from '../../../common/store/grouping/actions';
import { useGroupingPagination } from '../../../common/containers/grouping/hooks/use_grouping_pagination';
import { updateGroupSelector, updateSelectedGroup } from '../../../common/store/grouping/actions';
const ALERTS_GROUPING_ID = 'alerts-grouping';
@ -86,13 +82,6 @@ export const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> =
renderChildComponent,
}) => {
const dispatch = useDispatch();
const groupingId = tableId;
const getGroupByIdSelector = groupSelectors.getGroupByIdSelector();
const { activeGroup: selectedGroup } =
useSelector((state: State) => getGroupByIdSelector(state, groupingId)) ?? defaultGroup;
const { browserFields, indexPattern, selectedPatterns } = useSourcererDataView(
SourcererScopeName.detections
);
@ -121,6 +110,20 @@ export const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> =
[browserFields, defaultFilters, globalFilters, globalQuery, indexPattern, kibana, to, from]
);
const { groupSelector, getGrouping, selectedGroup, pagination } = useGrouping({
defaultGroupingOptions: getDefaultGroupingOptions(tableId),
groupingId: tableId,
fields: indexPattern.fields,
});
useEffect(() => {
dispatch(updateGroupSelector({ groupSelector }));
}, [dispatch, groupSelector]);
useEffect(() => {
dispatch(updateSelectedGroup({ selectedGroup }));
}, [dispatch, selectedGroup]);
useInvalidFilterQuery({
id: tableId,
filterQuery: getGlobalQuery([])?.filterQuery,
@ -130,10 +133,6 @@ export const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> =
endDate: to,
});
useEffect(() => {
dispatch(initGrouping({ id: tableId }));
}, [dispatch, tableId]);
const { deleteQuery, setQuery } = useGlobalTime(false);
// create a unique, but stable (across re-renders) query id
const uniqueQueryId = useMemo(() => `${ALERTS_GROUPING_ID}-${uuidv4()}`, []);
@ -151,10 +150,6 @@ export const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> =
}
}, [defaultFilters, globalFilters, globalQuery]);
const pagination = useGroupingPagination({
groupingId,
});
const queryGroups = useMemo(
() =>
getAlertsGroupingQuery({
@ -217,12 +212,6 @@ export const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> =
[uniqueQueryId]
);
const groupsSelector = useGetGroupSelector({
tableId,
groupingId,
fields: indexPattern.fields,
});
const takeActionItems = useGroupTakeActionsItems({
indexName: indexPattern.title,
currentStatus: currentAlertStatusFilterValue,
@ -247,23 +236,19 @@ export const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> =
data: alertsGroupsData?.aggregations,
groupPanelRenderer: (fieldBucket: RawBucket<AlertsGroupingAggregation>) =>
getSelectedGroupButtonContent(selectedGroup, fieldBucket),
groupsSelector,
inspectButton: inspect,
isLoading: loading || isLoadingGroups,
pagination,
renderChildComponent,
selectedGroup,
takeActionItems: getTakeActionItems,
unit: defaultUnit,
}),
[
alertsGroupsData?.aggregations,
getGrouping,
getTakeActionItems,
groupsSelector,
inspect,
isLoadingGroups,
loading,
pagination,
renderChildComponent,
selectedGroup,
]

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { GroupOption } from '@kbn/securitysolution-grouping';
import { TableId } from '../../../../../common/types';
import * as i18n from '../translations';
@ -12,7 +13,7 @@ export * from './group_panel_renderers';
export * from './group_take_action_items';
export * from './query_builder';
export const getDefaultGroupingOptions = (tableId: TableId) => {
export const getDefaultGroupingOptions = (tableId: TableId): GroupOption[] => {
if (tableId === TableId.alertsOnAlertsPage) {
return [
{

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { waitFor, render, fireEvent } from '@testing-library/react';
import { render } from '@testing-library/react';
import type { Filter } from '@kbn/es-query';
import useResizeObserver from 'use-resize-observer/polyfilled';
@ -29,7 +29,9 @@ import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_ma
import type { State } from '../../../common/store';
import { createStore } from '../../../common/store';
import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock';
import { defaultGroup } from '../../../common/store/grouping/defaults';
import { isNoneGroup, useGrouping } from '@kbn/securitysolution-grouping';
jest.mock('@kbn/securitysolution-grouping');
jest.mock('../../../common/containers/sourcerer');
jest.mock('../../../common/containers/use_global_time', () => ({
@ -44,22 +46,10 @@ jest.mock('../../../common/containers/use_global_time', () => ({
jest.mock('./grouping_settings', () => ({
getAlertsGroupingQuery: jest.fn(),
getDefaultGroupingOptions: () => [
{
label: 'ruleName',
key: 'kibana.alert.rule.name',
},
{
label: 'userName',
key: 'user.name',
},
{
label: 'hostName',
key: 'host.name',
},
{
label: 'sourceIP',
key: 'source.ip',
},
{ label: 'ruleName', key: 'kibana.alert.rule.name' },
{ label: 'userName', key: 'user.name' },
{ label: 'hostName', key: 'host.name' },
{ label: 'sourceIP', key: 'source.ip' },
],
getSelectedGroupBadgeMetrics: jest.fn(),
getSelectedGroupButtonContent: jest.fn(),
@ -153,7 +143,8 @@ const groupingStore = createStore(
{
...state,
groups: {
groupById: { [`${TableId.test}`]: { ...defaultGroup, activeGroup: 'host.name' } },
groupSelector: <></>,
selectedGroup: 'host.name',
},
},
SUB_PLUGINS_REDUCER,
@ -198,12 +189,35 @@ const testProps: AlertsTableComponentProps = {
};
describe('GroupedAlertsTable', () => {
const getGrouping = jest.fn().mockReturnValue(<span data-test-subj={'grouping-table'} />);
beforeEach(() => {
jest.clearAllMocks();
(useSourcererDataView as jest.Mock).mockReturnValue({
...sourcererDataView,
selectedPatterns: ['myFakebeat-*'],
});
(isNoneGroup as jest.Mock).mockReturnValue(true);
(useGrouping as jest.Mock).mockReturnValue({
groupSelector: <></>,
getGrouping,
selectedGroup: 'host.name',
pagination: { pageSize: 1, pageIndex: 0 },
});
});
it('calls the proper initial dispatch actions for groups', () => {
render(
<TestProviders store={store}>
<GroupedAlertsTableComponent {...testProps} />
</TestProviders>
);
expect(mockDispatch).toHaveBeenCalledTimes(2);
expect(mockDispatch.mock.calls[0][0].type).toEqual(
'x-pack/security_solution/groups/UPDATE_GROUP_SELECTOR'
);
expect(mockDispatch.mock.calls[1][0].type).toEqual(
'x-pack/security_solution/groups/UPDATE_SELECTED_GROUP'
);
});
it('renders alerts table when no group selected', () => {
@ -216,7 +230,9 @@ describe('GroupedAlertsTable', () => {
expect(queryByTestId('grouping-table')).not.toBeInTheDocument();
});
it('renders grouped alerts when group selected', () => {
it('renders grouped alerts when group selected', async () => {
(isNoneGroup as jest.Mock).mockReturnValue(false);
const { getByTestId, queryByTestId } = render(
<TestProviders store={groupingStore}>
<GroupedAlertsTableComponent {...testProps} />
@ -224,46 +240,16 @@ describe('GroupedAlertsTable', () => {
);
expect(getByTestId('grouping-table')).toBeInTheDocument();
expect(queryByTestId('alerts-table')).not.toBeInTheDocument();
expect(queryByTestId('is-loading-grouping-table')).not.toBeInTheDocument();
expect(getGrouping.mock.calls[0][0].isLoading).toEqual(false);
});
it('renders loading when expected', () => {
const { getByTestId } = render(
(isNoneGroup as jest.Mock).mockReturnValue(false);
render(
<TestProviders store={groupingStore}>
<GroupedAlertsTableComponent {...testProps} loading={true} />
</TestProviders>
);
expect(getByTestId('is-loading-grouping-table')).toBeInTheDocument();
});
// Not a valid test as of now.. because, table is used from trigger actions..
// Need to find a better way to test grouping
// Need to make grouping_alerts independent of Alerts Table.
it.skip('it renders groupping fields options when the grouping field is selected', async () => {
const { getByTestId, getAllByTestId } = render(
<TestProviders store={store}>
<GroupedAlertsTableComponent
tableId={TableId.test}
hasIndexWrite
hasIndexMaintenance
from={'2020-07-07T08:20:18.966Z'}
loading={false}
to={'2020-07-08T08:20:18.966Z'}
globalQuery={{
query: 'query',
language: 'language',
}}
globalFilters={[]}
runtimeMappings={{}}
signalIndexName={'test'}
renderChildComponent={() => <></>}
/>
</TestProviders>
);
await waitFor(() => {
expect(getByTestId('[data-test-subj="group-selector-dropdown"]')).toBeVisible();
fireEvent.click(getAllByTestId('group-selector-dropdown')[0]);
expect(getByTestId('[data-test-subj="panel-kibana.alert.rule.name"]')).toBeVisible();
});
expect(getGrouping.mock.calls[0][0].isLoading).toEqual(true);
});
});

View file

@ -8,11 +8,7 @@
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { isNoneGroup } from '@kbn/securitysolution-grouping';
import { useGetGroupSelector } from '../../../common/containers/grouping/hooks/use_get_group_selector';
import { defaultGroup } from '../../../common/store/grouping/defaults';
import type { State } from '../../../common/store';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { useDataTableFilters } from '../../../common/hooks/use_data_table_filters';
import { dataTableSelectors } from '../../../common/store/data_table';
import { changeViewMode } from '../../../common/store/data_table/actions';
@ -26,18 +22,12 @@ import { groupSelectors } from '../../../common/store/grouping';
export const getPersistentControlsHook = (tableId: TableId) => {
const usePersistentControls = () => {
const dispatch = useDispatch();
const getGroupbyIdSelector = groupSelectors.getGroupByIdSelector();
const getGroupSelector = groupSelectors.getGroupSelector();
const { activeGroup: selectedGroup } =
useSelector((state: State) => getGroupbyIdSelector(state, tableId)) ?? defaultGroup;
const groupSelector = useSelector((state: State) => getGroupSelector(state));
const getSelectedGroup = groupSelectors.getSelectedGroup();
const { indexPattern: indexPatterns } = useSourcererDataView(SourcererScopeName.detections);
const groupsSelector = useGetGroupSelector({
fields: indexPatterns.fields,
groupingId: tableId,
tableId,
});
const selectedGroup = useSelector((state: State) => getSelectedGroup(state));
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
@ -94,10 +84,10 @@ export const getPersistentControlsHook = (tableId: TableId) => {
hasRightOffset={false}
additionalFilters={additionalFiltersComponent}
showInspect={false}
additionalMenuOptions={isNoneGroup(selectedGroup) ? [groupsSelector] : []}
additionalMenuOptions={isNoneGroup(selectedGroup) ? [groupSelector] : []}
/>
),
[tableView, handleChangeTableView, additionalFiltersComponent, groupsSelector, selectedGroup]
[tableView, handleChangeTableView, additionalFiltersComponent, groupSelector, selectedGroup]
);
return {

View file

@ -11,7 +11,6 @@ import { TableId } from '../../common/types';
import { getDataTablesInStorageByIds } from '../timelines/containers/local_storage';
import { routes } from './routes';
import type { SecuritySubPlugin } from '../app/types';
import { getAllGroupsInStorage } from '../timelines/containers/local_storage/groups';
export const DETECTIONS_TABLE_IDS: TableIdLiteral[] = [
TableId.alertsOnRuleDetailsPage,
@ -26,9 +25,6 @@ export class Detections {
storageDataTables: {
tableById: getDataTablesInStorageByIds(storage, DETECTIONS_TABLE_IDS),
},
groups: {
groupById: getAllGroupsInStorage(storage),
},
routes,
};
}

View file

@ -1,30 +0,0 @@
/*
* 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 { Storage } from '@kbn/kibana-utils-plugin/public';
import type { GroupModel } from '../../../common/store/grouping';
const LOCAL_STORAGE_GROUPING_KEY = 'groups';
const EMPTY_GROUP = {} as {
[K: string]: GroupModel;
};
export const getAllGroupsInStorage = (storage: Storage) => {
const allGroups = storage.get(LOCAL_STORAGE_GROUPING_KEY);
if (!allGroups) {
return EMPTY_GROUP;
}
return allGroups;
};
export const addGroupsToStorage = (storage: Storage, groupingId: string, group: GroupModel) => {
const groups = getAllGroupsInStorage(storage);
storage.set(LOCAL_STORAGE_GROUPING_KEY, {
...groups,
[groupingId]: group,
});
};

View file

@ -11,7 +11,6 @@ import type { Observable } from 'rxjs';
import type { Storage } from '@kbn/kibana-utils-plugin/public';
import type { CoreStart } from '@kbn/core/public';
import type { FilterManager } from '@kbn/data-plugin/public';
import type { GroupsById } from '../../../common/store/grouping';
import type {
ColumnHeaderOptions,
RowRendererId,
@ -64,7 +63,6 @@ export interface TimelineEpicDependencies<State> {
selectAllTimelineQuery: () => (state: State, id: string) => inputsModel.GlobalQuery;
selectNotesByIdSelector: (state: State) => NotesById;
tableByIdSelector: (state: State) => TableById;
groupByIdSelector: (state: State) => GroupsById;
kibana$: Observable<CoreStart>;
storage: Storage;
}