Skip to content

Commit 3612a7a

Browse files
[Security Solution][RAC] Migrate add to case action to timelines plugin (elastic#106205)
* First pass add to case action in timelines plugin * Fix fake duplicate import lint rule and some type errors * Fix some tests * Remove use_insert_timeline and pass as prop * Remove unneeded ports, fix types/tests * Finish fixing types and tests for add to case action * Remove duplicated security_solution code * Pass appId as props * Fix lint and a type error * Use react-router-dom instead of window.location.search * Fix broken test * Remove unused imports * Remove unused export and related code
1 parent dfb1b61 commit 3612a7a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+506
-352
lines changed

.eslintrc.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -895,7 +895,8 @@ module.exports = {
895895
{
896896
files: ['x-pack/plugins/cases/**/*.{js,mjs,ts,tsx}'],
897897
rules: {
898-
'no-duplicate-imports': 'error',
898+
'no-duplicate-imports': 'off',
899+
'@typescript-eslint/no-duplicate-imports': ['error'],
899900
},
900901
},
901902

@@ -912,6 +913,8 @@ module.exports = {
912913
],
913914
rules: {
914915
'import/no-nodejs-modules': 'error',
916+
'no-duplicate-imports': 'off',
917+
'@typescript-eslint/no-duplicate-imports': ['error'],
915918
'no-restricted-imports': [
916919
'error',
917920
{
@@ -954,7 +957,7 @@ module.exports = {
954957
'no-continue': 'error',
955958
'no-dupe-keys': 'error',
956959
'no-duplicate-case': 'error',
957-
'no-duplicate-imports': 'error',
960+
'no-duplicate-imports': 'off',
958961
'no-empty-character-class': 'error',
959962
'no-empty-pattern': 'error',
960963
'no-ex-assign': 'error',
@@ -1025,6 +1028,7 @@ module.exports = {
10251028
'require-atomic-updates': 'error',
10261029
'symbol-description': 'error',
10271030
'vars-on-top': 'error',
1031+
'@typescript-eslint/no-duplicate-imports': ['error'],
10281032
},
10291033
},
10301034

x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.test.tsx

-55
This file was deleted.

x-pack/plugins/security_solution/public/resolver/types.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
* 2.0.
66
*/
77

8-
/* eslint-disable no-duplicate-imports */
98
import type ResizeObserver from 'resize-observer-polyfill';
109
import type React from 'react';
11-
import { Store } from 'redux';
12-
import { Middleware, Dispatch } from 'redux';
10+
import { Store, Middleware, Dispatch } from 'redux';
1311
import { BBox } from 'rbush';
1412
import { Provider } from 'react-redux';
1513
import { ResolverAction } from './store/actions';

x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77

88
/* eslint-disable @elastic/eui/href-or-on-click */
99

10-
/* eslint-disable no-duplicate-imports */
11-
12-
import { useDispatch } from 'react-redux';
10+
import { useDispatch, useSelector } from 'react-redux';
1311

1412
/* eslint-disable react/display-name */
1513

@@ -22,7 +20,6 @@ import {
2220
EuiInMemoryTable,
2321
} from '@elastic/eui';
2422
import { i18n } from '@kbn/i18n';
25-
import { useSelector } from 'react-redux';
2623
import { SideEffectContext } from '../side_effect_context';
2724
import { StyledPanel } from '../styles';
2825
import {

x-pack/plugins/security_solution/public/resolver/view/panels/styles.tsx

+1-6
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,9 @@
55
* 2.0.
66
*/
77

8-
import { EuiCode } from '@elastic/eui';
9-
10-
/* eslint-disable no-duplicate-imports */
11-
12-
import { EuiBreadcrumbs } from '@elastic/eui';
8+
import { EuiCode, EuiBreadcrumbs, EuiDescriptionList } from '@elastic/eui';
139

1410
import styled from 'styled-components';
15-
import { EuiDescriptionList } from '@elastic/eui';
1611

1712
/**
1813
* Used by the nodeDetail view to show attributes of the related events.

x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ jest.mock('../../../../../common/hooks/use_selector', () => ({
2020
useShallowEqualSelector: jest.fn(),
2121
}));
2222

23+
jest.mock('../../../../../common/lib/kibana', () => {
24+
const useKibana = jest.requireActual('../../../../../common/lib/kibana');
25+
return {
26+
...useKibana,
27+
useGetUserCasesPermissions: jest.fn(),
28+
};
29+
});
30+
2331
describe('Actions', () => {
2432
beforeEach(() => {
2533
(useShallowEqualSelector as jest.Mock).mockReturnValue(mockTimelineModel);

x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx

+17-10
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ import { InvestigateInTimelineAction } from '../../../../../detections/component
2222
import { AddEventNoteAction } from '../actions/add_note_icon_item';
2323
import { PinEventAction } from '../actions/pin_event_action';
2424
import { EventsTdContent } from '../../styles';
25+
import { useKibana, useGetUserCasesPermissions } from '../../../../../common/lib/kibana';
26+
import { APP_ID } from '../../../../../../common/constants';
2527
import * as i18n from '../translations';
2628
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
2729
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
28-
import { AddToCaseAction } from '../../../../../cases/components/timeline_actions/add_to_case_action';
30+
import { useInsertTimeline } from '../../../../../cases/components/use_insert_timeline';
2931
import { TimelineId, ActionProps, OnPinEvent } from '../../../../../../common/types/timeline';
3032
import { timelineActions, timelineSelectors } from '../../../../store/timeline';
3133
import { timelineDefaults } from '../../../../store/timeline/defaults';
@@ -59,6 +61,7 @@ const ActionsComponent: React.FC<ActionProps> = ({
5961
const dispatch = useDispatch();
6062
const emptyNotes: string[] = [];
6163
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
64+
const { timelines: timelinesUi } = useKibana().services;
6265

6366
const onPinEvent: OnPinEvent = useCallback(
6467
(evtId) => dispatch(timelineActions.pinEvent({ id: timelineId, eventId: evtId })),
@@ -93,12 +96,21 @@ const ActionsComponent: React.FC<ActionProps> = ({
9396
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).timelineType
9497
);
9598
const eventType = getEventType(ecsData);
96-
99+
const casePermissions = useGetUserCasesPermissions();
100+
const insertTimelineHook = useInsertTimeline;
97101
const isEventContextMenuEnabledForEndpoint = useMemo(
98102
() => ecsData.event?.kind?.includes('event') && ecsData.agent?.type?.includes('endpoint'),
99103
[ecsData.event?.kind, ecsData.agent?.type]
100104
);
101-
105+
const addToCaseActionProps = useMemo(() => {
106+
return {
107+
ariaLabel: i18n.ATTACH_ALERT_TO_CASE_FOR_ROW({ ariaRowindex, columnValues }),
108+
ecsRowData: ecsData,
109+
useInsertTimeline: insertTimelineHook,
110+
casePermissions,
111+
appId: APP_ID,
112+
};
113+
}, [ariaRowindex, ecsData, casePermissions, insertTimelineHook, columnValues]);
102114
return (
103115
<ActionsContainer>
104116
{showCheckboxes && (
@@ -169,13 +181,8 @@ const ActionsComponent: React.FC<ActionProps> = ({
169181
TimelineId.detectionsPage,
170182
TimelineId.detectionsRulesDetailsPage,
171183
TimelineId.active,
172-
].includes(timelineId as TimelineId) && (
173-
<AddToCaseAction
174-
ariaLabel={i18n.ATTACH_ALERT_TO_CASE_FOR_ROW({ ariaRowindex, columnValues })}
175-
key="attach-to-case"
176-
ecsRowData={ecsData}
177-
/>
178-
)}
184+
].includes(timelineId as TimelineId) &&
185+
timelinesUi.getAddToCaseAction(addToCaseActionProps)}
179186
<AlertContextMenu
180187
ariaLabel={i18n.MORE_ACTIONS_FOR_ROW({ ariaRowindex, columnValues })}
181188
key="alert-context-menu"

x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx

+25-8
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,33 @@ import { testLeadingControlColumn } from '../../../../../common/mock/mock_timeli
2323

2424
jest.mock('../../../../../common/hooks/use_experimental_features');
2525
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
26-
2726
jest.mock('../../../../../common/hooks/use_selector');
28-
29-
jest.mock('../../../../../cases/components/timeline_actions/add_to_case_action', () => {
30-
return {
31-
AddToCaseAction: () => {
32-
return <div data-test-subj="add-to-case-action">{'Add to case'}</div>;
27+
jest.mock('../../../../../common/lib/kibana', () => ({
28+
useKibana: () => ({
29+
services: {
30+
timelines: {
31+
getAddToCaseAction: () => <div data-test-subj="add-to-case-action">{'Add to case'}</div>,
32+
},
3333
},
34-
};
35-
});
34+
}),
35+
useToasts: jest.fn().mockReturnValue({
36+
addError: jest.fn(),
37+
addSuccess: jest.fn(),
38+
addWarning: jest.fn(),
39+
}),
40+
useGetUserCasesPermissions: jest.fn(),
41+
}));
42+
43+
jest.mock(
44+
'../../../../../../../timelines/public/components/actions/timeline/cases/add_to_case_action',
45+
() => {
46+
return {
47+
AddToCaseAction: () => {
48+
return <div data-test-subj="add-to-case-action">{'Add to case'}</div>;
49+
},
50+
};
51+
}
52+
);
3653

3754
describe('EventColumnView', () => {
3855
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);

x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from '../../../timelines/components/timeline/data_providers/data_provider';
1616

1717
import { KqlMode, TimelineModel } from './model';
18+
import { InsertTimeline } from './types';
1819
import { FieldsEqlOptions } from '../../../../common/search_strategy/timeline';
1920
import {
2021
TimelineEventsType,
@@ -23,7 +24,6 @@ import {
2324
TimelinePersistInput,
2425
SerializedFilterQuery,
2526
} from '../../../../common/types/timeline';
26-
import { InsertTimeline } from './types';
2727
import { tGridActions } from '../../../../../timelines/public';
2828
export const {
2929
applyDeltaToColumnWidth,
@@ -55,6 +55,10 @@ export const addNoteToEvent = actionCreator<{ id: string; noteId: string; eventI
5555
'ADD_NOTE_TO_EVENT'
5656
);
5757

58+
export const showTimeline = actionCreator<{ id: string; show: boolean }>('SHOW_TIMELINE');
59+
60+
export const setInsertTimeline = actionCreator<InsertTimeline | null>('SET_INSERT_TIMELINE');
61+
5862
export const addProvider = actionCreator<{ id: string; provider: DataProvider }>('ADD_PROVIDER');
5963

6064
export const saveTimeline = actionCreator<TimelinePersistInput>('SAVE_TIMELINE');
@@ -69,8 +73,6 @@ export const removeProvider = actionCreator<{
6973
andProviderId?: string;
7074
}>('REMOVE_PROVIDER');
7175

72-
export const showTimeline = actionCreator<{ id: string; show: boolean }>('SHOW_TIMELINE');
73-
7476
export const updateTimelineGraphEventId = actionCreator<{ id: string; graphEventId: string }>(
7577
'UPDATE_TIMELINE_GRAPH_EVENT_ID'
7678
);
@@ -88,8 +90,6 @@ export const addTimeline = actionCreator<{
8890
savedTimeline?: boolean;
8991
}>('ADD_TIMELINE');
9092

91-
export const setInsertTimeline = actionCreator<InsertTimeline | null>('SET_INSERT_TIMELINE');
92-
9393
export const startTimelineSaving = actionCreator<{
9494
id: string;
9595
}>('START_TIMELINE_SAVING');

x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts

+8
Original file line numberDiff line numberDiff line change
@@ -364,4 +364,12 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
364364
},
365365
},
366366
}))
367+
.case(setInsertTimeline, (state, insertTimeline) => ({
368+
...state,
369+
insertTimeline,
370+
}))
371+
.case(showTimeline, (state, { id, show }) => ({
372+
...state,
373+
timelineById: updateTimelineShowTimeline({ id, show, timelineById: state.timelineById }),
374+
}))
367375
.build();

x-pack/plugins/timelines/kibana.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
"extraPublicDirs": ["common"],
77
"server": true,
88
"ui": true,
9-
"requiredPlugins": ["alerting", "data", "dataEnhanced", "kibanaReact", "kibanaUtils"],
9+
"requiredPlugins": ["alerting", "cases", "data", "dataEnhanced", "kibanaReact", "kibanaUtils"],
1010
"optionalPlugins": []
1111
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React, { MouseEvent } from 'react';
9+
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
10+
11+
import { EventsTdContent } from '../t_grid/styles';
12+
import { DEFAULT_ICON_BUTTON_WIDTH } from '../t_grid/helpers';
13+
14+
interface ActionIconItemProps {
15+
ariaLabel?: string;
16+
width?: number;
17+
dataTestSubj?: string;
18+
content?: string;
19+
iconType?: string;
20+
isDisabled?: boolean;
21+
onClick?: (event: MouseEvent) => void;
22+
children?: React.ReactNode;
23+
}
24+
25+
const ActionIconItemComponent: React.FC<ActionIconItemProps> = ({
26+
width = DEFAULT_ICON_BUTTON_WIDTH,
27+
dataTestSubj,
28+
content,
29+
ariaLabel,
30+
iconType = '',
31+
isDisabled = false,
32+
onClick,
33+
children,
34+
}) => (
35+
<div>
36+
<EventsTdContent textAlign="center" width={width}>
37+
{children ?? (
38+
<EuiToolTip data-test-subj={`${dataTestSubj}-tool-tip`} content={content}>
39+
<EuiButtonIcon
40+
aria-label={ariaLabel}
41+
data-test-subj={`${dataTestSubj}-button`}
42+
iconType={iconType}
43+
isDisabled={isDisabled}
44+
onClick={onClick}
45+
/>
46+
</EuiToolTip>
47+
)}
48+
</EventsTdContent>
49+
</div>
50+
);
51+
52+
ActionIconItemComponent.displayName = 'ActionIconItemComponent';
53+
54+
export const ActionIconItem = React.memo(ActionIconItemComponent);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
export * from './timeline';

0 commit comments

Comments
 (0)