[Security Solution][RAC] Migrate add to case action to timelines plugin (#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
This commit is contained in:
Kevin Qualters 2021-07-28 17:10:37 -04:00 committed by GitHub
parent dfb1b615e9
commit 3612a7a300
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 504 additions and 350 deletions

View file

@ -895,7 +895,8 @@ module.exports = {
{
files: ['x-pack/plugins/cases/**/*.{js,mjs,ts,tsx}'],
rules: {
'no-duplicate-imports': 'error',
'no-duplicate-imports': 'off',
'@typescript-eslint/no-duplicate-imports': ['error'],
},
},
@ -912,6 +913,8 @@ module.exports = {
],
rules: {
'import/no-nodejs-modules': 'error',
'no-duplicate-imports': 'off',
'@typescript-eslint/no-duplicate-imports': ['error'],
'no-restricted-imports': [
'error',
{
@ -954,7 +957,7 @@ module.exports = {
'no-continue': 'error',
'no-dupe-keys': 'error',
'no-duplicate-case': 'error',
'no-duplicate-imports': 'error',
'no-duplicate-imports': 'off',
'no-empty-character-class': 'error',
'no-empty-pattern': 'error',
'no-ex-assign': 'error',
@ -1025,6 +1028,7 @@ module.exports = {
'require-atomic-updates': 'error',
'symbol-description': 'error',
'vars-on-top': 'error',
'@typescript-eslint/no-duplicate-imports': ['error'],
},
},

View file

@ -1,55 +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 React from 'react';
import { mount } from 'enzyme';
import 'jest-styled-components';
import { createUpdateSuccessToaster } from './helpers';
import { Case } from '../../../../../cases/common';
const theCase = {
id: 'case-id',
title: 'My case',
settings: {
syncAlerts: true,
},
} as Case;
describe('helpers', () => {
const onViewCaseClick = jest.fn();
describe('createUpdateSuccessToaster', () => {
it('creates the correct toast when the sync alerts is on', () => {
// We remove the id as is randomly generated and the text as it is a React component
// which is being test on toaster_content.test.tsx
const { id, text, title, ...toast } = createUpdateSuccessToaster(theCase, onViewCaseClick);
const mountedTitle = mount(<>{title}</>);
expect(toast).toEqual({
color: 'success',
iconType: 'check',
});
expect(mountedTitle).toMatchInlineSnapshot(`
.c0 {
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
<styled.span>
<span
className="c0"
>
An alert has been added to "My case"
</span>
</styled.span>
`);
});
});
});

View file

@ -5,11 +5,9 @@
* 2.0.
*/
/* eslint-disable no-duplicate-imports */
import type ResizeObserver from 'resize-observer-polyfill';
import type React from 'react';
import { Store } from 'redux';
import { Middleware, Dispatch } from 'redux';
import { Store, Middleware, Dispatch } from 'redux';
import { BBox } from 'rbush';
import { Provider } from 'react-redux';
import { ResolverAction } from './store/actions';

View file

@ -7,9 +7,7 @@
/* eslint-disable @elastic/eui/href-or-on-click */
/* eslint-disable no-duplicate-imports */
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
/* eslint-disable react/display-name */
@ -22,7 +20,6 @@ import {
EuiInMemoryTable,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useSelector } from 'react-redux';
import { SideEffectContext } from '../side_effect_context';
import { StyledPanel } from '../styles';
import {

View file

@ -5,14 +5,9 @@
* 2.0.
*/
import { EuiCode } from '@elastic/eui';
/* eslint-disable no-duplicate-imports */
import { EuiBreadcrumbs } from '@elastic/eui';
import { EuiCode, EuiBreadcrumbs, EuiDescriptionList } from '@elastic/eui';
import styled from 'styled-components';
import { EuiDescriptionList } from '@elastic/eui';
/**
* Used by the nodeDetail view to show attributes of the related events.

View file

@ -20,6 +20,14 @@ jest.mock('../../../../../common/hooks/use_selector', () => ({
useShallowEqualSelector: jest.fn(),
}));
jest.mock('../../../../../common/lib/kibana', () => {
const useKibana = jest.requireActual('../../../../../common/lib/kibana');
return {
...useKibana,
useGetUserCasesPermissions: jest.fn(),
};
});
describe('Actions', () => {
beforeEach(() => {
(useShallowEqualSelector as jest.Mock).mockReturnValue(mockTimelineModel);

View file

@ -22,10 +22,12 @@ import { InvestigateInTimelineAction } from '../../../../../detections/component
import { AddEventNoteAction } from '../actions/add_note_icon_item';
import { PinEventAction } from '../actions/pin_event_action';
import { EventsTdContent } from '../../styles';
import { useKibana, useGetUserCasesPermissions } from '../../../../../common/lib/kibana';
import { APP_ID } from '../../../../../../common/constants';
import * as i18n from '../translations';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
import { AddToCaseAction } from '../../../../../cases/components/timeline_actions/add_to_case_action';
import { useInsertTimeline } from '../../../../../cases/components/use_insert_timeline';
import { TimelineId, ActionProps, OnPinEvent } from '../../../../../../common/types/timeline';
import { timelineActions, timelineSelectors } from '../../../../store/timeline';
import { timelineDefaults } from '../../../../store/timeline/defaults';
@ -59,6 +61,7 @@ const ActionsComponent: React.FC<ActionProps> = ({
const dispatch = useDispatch();
const emptyNotes: string[] = [];
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const { timelines: timelinesUi } = useKibana().services;
const onPinEvent: OnPinEvent = useCallback(
(evtId) => dispatch(timelineActions.pinEvent({ id: timelineId, eventId: evtId })),
@ -93,12 +96,21 @@ const ActionsComponent: React.FC<ActionProps> = ({
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).timelineType
);
const eventType = getEventType(ecsData);
const casePermissions = useGetUserCasesPermissions();
const insertTimelineHook = useInsertTimeline;
const isEventContextMenuEnabledForEndpoint = useMemo(
() => ecsData.event?.kind?.includes('event') && ecsData.agent?.type?.includes('endpoint'),
[ecsData.event?.kind, ecsData.agent?.type]
);
const addToCaseActionProps = useMemo(() => {
return {
ariaLabel: i18n.ATTACH_ALERT_TO_CASE_FOR_ROW({ ariaRowindex, columnValues }),
ecsRowData: ecsData,
useInsertTimeline: insertTimelineHook,
casePermissions,
appId: APP_ID,
};
}, [ariaRowindex, ecsData, casePermissions, insertTimelineHook, columnValues]);
return (
<ActionsContainer>
{showCheckboxes && (
@ -169,13 +181,8 @@ const ActionsComponent: React.FC<ActionProps> = ({
TimelineId.detectionsPage,
TimelineId.detectionsRulesDetailsPage,
TimelineId.active,
].includes(timelineId as TimelineId) && (
<AddToCaseAction
ariaLabel={i18n.ATTACH_ALERT_TO_CASE_FOR_ROW({ ariaRowindex, columnValues })}
key="attach-to-case"
ecsRowData={ecsData}
/>
)}
].includes(timelineId as TimelineId) &&
timelinesUi.getAddToCaseAction(addToCaseActionProps)}
<AlertContextMenu
ariaLabel={i18n.MORE_ACTIONS_FOR_ROW({ ariaRowindex, columnValues })}
key="alert-context-menu"

View file

@ -23,16 +23,33 @@ import { testLeadingControlColumn } from '../../../../../common/mock/mock_timeli
jest.mock('../../../../../common/hooks/use_experimental_features');
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
jest.mock('../../../../../common/hooks/use_selector');
jest.mock('../../../../../cases/components/timeline_actions/add_to_case_action', () => {
return {
AddToCaseAction: () => {
return <div data-test-subj="add-to-case-action">{'Add to case'}</div>;
jest.mock('../../../../../common/lib/kibana', () => ({
useKibana: () => ({
services: {
timelines: {
getAddToCaseAction: () => <div data-test-subj="add-to-case-action">{'Add to case'}</div>,
},
},
};
});
}),
useToasts: jest.fn().mockReturnValue({
addError: jest.fn(),
addSuccess: jest.fn(),
addWarning: jest.fn(),
}),
useGetUserCasesPermissions: jest.fn(),
}));
jest.mock(
'../../../../../../../timelines/public/components/actions/timeline/cases/add_to_case_action',
() => {
return {
AddToCaseAction: () => {
return <div data-test-subj="add-to-case-action">{'Add to case'}</div>;
},
};
}
);
describe('EventColumnView', () => {
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);

View file

@ -15,6 +15,7 @@ import {
} from '../../../timelines/components/timeline/data_providers/data_provider';
import { KqlMode, TimelineModel } from './model';
import { InsertTimeline } from './types';
import { FieldsEqlOptions } from '../../../../common/search_strategy/timeline';
import {
TimelineEventsType,
@ -23,7 +24,6 @@ import {
TimelinePersistInput,
SerializedFilterQuery,
} from '../../../../common/types/timeline';
import { InsertTimeline } from './types';
import { tGridActions } from '../../../../../timelines/public';
export const {
applyDeltaToColumnWidth,
@ -55,6 +55,10 @@ export const addNoteToEvent = actionCreator<{ id: string; noteId: string; eventI
'ADD_NOTE_TO_EVENT'
);
export const showTimeline = actionCreator<{ id: string; show: boolean }>('SHOW_TIMELINE');
export const setInsertTimeline = actionCreator<InsertTimeline | null>('SET_INSERT_TIMELINE');
export const addProvider = actionCreator<{ id: string; provider: DataProvider }>('ADD_PROVIDER');
export const saveTimeline = actionCreator<TimelinePersistInput>('SAVE_TIMELINE');
@ -69,8 +73,6 @@ export const removeProvider = actionCreator<{
andProviderId?: string;
}>('REMOVE_PROVIDER');
export const showTimeline = actionCreator<{ id: string; show: boolean }>('SHOW_TIMELINE');
export const updateTimelineGraphEventId = actionCreator<{ id: string; graphEventId: string }>(
'UPDATE_TIMELINE_GRAPH_EVENT_ID'
);
@ -88,8 +90,6 @@ export const addTimeline = actionCreator<{
savedTimeline?: boolean;
}>('ADD_TIMELINE');
export const setInsertTimeline = actionCreator<InsertTimeline | null>('SET_INSERT_TIMELINE');
export const startTimelineSaving = actionCreator<{
id: string;
}>('START_TIMELINE_SAVING');

View file

@ -364,4 +364,12 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
},
},
}))
.case(setInsertTimeline, (state, insertTimeline) => ({
...state,
insertTimeline,
}))
.case(showTimeline, (state, { id, show }) => ({
...state,
timelineById: updateTimelineShowTimeline({ id, show, timelineById: state.timelineById }),
}))
.build();

View file

@ -6,6 +6,6 @@
"extraPublicDirs": ["common"],
"server": true,
"ui": true,
"requiredPlugins": ["alerting", "data", "dataEnhanced", "kibanaReact", "kibanaUtils"],
"requiredPlugins": ["alerting", "cases", "data", "dataEnhanced", "kibanaReact", "kibanaUtils"],
"optionalPlugins": []
}

View file

@ -0,0 +1,54 @@
/*
* 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 React, { MouseEvent } from 'react';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { EventsTdContent } from '../t_grid/styles';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../t_grid/helpers';
interface ActionIconItemProps {
ariaLabel?: string;
width?: number;
dataTestSubj?: string;
content?: string;
iconType?: string;
isDisabled?: boolean;
onClick?: (event: MouseEvent) => void;
children?: React.ReactNode;
}
const ActionIconItemComponent: React.FC<ActionIconItemProps> = ({
width = DEFAULT_ICON_BUTTON_WIDTH,
dataTestSubj,
content,
ariaLabel,
iconType = '',
isDisabled = false,
onClick,
children,
}) => (
<div>
<EventsTdContent textAlign="center" width={width}>
{children ?? (
<EuiToolTip data-test-subj={`${dataTestSubj}-tool-tip`} content={content}>
<EuiButtonIcon
aria-label={ariaLabel}
data-test-subj={`${dataTestSubj}-button`}
iconType={iconType}
isDisabled={isDisabled}
onClick={onClick}
/>
</EuiToolTip>
)}
</EventsTdContent>
</div>
);
ActionIconItemComponent.displayName = 'ActionIconItemComponent';
export const ActionIconItem = React.memo(ActionIconItemComponent);

View file

@ -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 './timeline';

View file

@ -7,36 +7,17 @@
import React from 'react';
import { mount } from 'enzyme';
import { EuiGlobalToastList } from '@elastic/eui';
import { useKibana, useGetUserCasesPermissions } from '../../../common/lib/kibana';
import { useStateToaster } from '../../../common/components/toasters';
import { TestProviders } from '../../../common/mock';
import { TestProviders, mockGetAllCasesSelectorModal } from '../../../../mock';
import { AddToCaseAction } from './add_to_case_action';
import { basicCase } from '../../../../../cases/public/containers/mock';
import { Case, SECURITY_SOLUTION_OWNER } from '../../../../../cases/common';
import { APP_ID, SecurityPageName } from '../../../../common/constants';
import { SECURITY_SOLUTION_OWNER } from '../../../../../../cases/common';
jest.mock('../../../common/lib/kibana');
jest.mock('../../../common/components/link_to', () => {
const original = jest.requireActual('../../../common/components/link_to');
return {
...original,
useFormatUrl: jest.fn().mockReturnValue({
formatUrl: jest.fn(),
search: '',
}),
};
});
jest.mock('../../../common/components/toasters', () => {
const actual = jest.requireActual('../../../common/components/toasters');
return {
...actual,
useStateToaster: jest.fn(),
};
});
jest.mock('react-router-dom', () => ({
useLocation: () => ({
search: '',
}),
}));
jest.mock('./helpers');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
describe('AddToCaseAction', () => {
const props = {
ecsRowData: {
@ -44,29 +25,15 @@ describe('AddToCaseAction', () => {
_index: 'test-index',
signal: { rule: { id: ['rule-id'], name: ['rule-name'], false_positives: [] } },
},
casePermissions: {
crud: true,
read: true,
},
appId: 'securitySolution',
};
const mockDispatchToaster = jest.fn();
const mockNavigateToApp = jest.fn();
const mockCreateCase = jest.fn();
const mockAllCasesModal = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
useKibanaMock().services.application.navigateToApp = mockNavigateToApp;
useKibanaMock().services.cases = {
getAllCases: jest.fn(),
getCaseView: jest.fn(),
getConfigureCases: jest.fn(),
getRecentCases: jest.fn(),
getCreateCase: mockCreateCase,
getAllCasesSelectorModal: mockAllCasesModal.mockImplementation(() => <>{'test'}</>),
};
(useStateToaster as jest.Mock).mockReturnValue([jest.fn(), mockDispatchToaster]);
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
crud: true,
read: true,
});
});
it('it renders', () => {
@ -100,7 +67,7 @@ describe('AddToCaseAction', () => {
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click');
wrapper.find(`[data-test-subj="add-new-case-item"]`).first().simulate('click');
expect(mockCreateCase).toHaveBeenCalled();
expect(wrapper.find('[data-test-subj="create-case-flyout"]').exists()).toBeTruthy();
});
it('it opens the all cases modal', () => {
@ -113,12 +80,7 @@ describe('AddToCaseAction', () => {
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click');
wrapper.find(`[data-test-subj="add-existing-case-menu-item"]`).first().simulate('click');
expect(mockAllCasesModal.mock.calls[0][0].alertData).toEqual({
alertId: 'test-id',
index: 'test-index',
rule: { id: 'rule-id', name: 'rule-name' },
owner: SECURITY_SOLUTION_OWNER,
});
expect(wrapper.find('[data-test-subj="all-cases-modal"]')).toBeTruthy();
});
it('it set rule information as null when missing', () => {
@ -137,7 +99,7 @@ describe('AddToCaseAction', () => {
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click');
wrapper.find(`[data-test-subj="add-existing-case-menu-item"]`).first().simulate('click');
expect(mockAllCasesModal.mock.calls[0][0].alertData).toEqual({
expect(mockGetAllCasesSelectorModal.mock.calls[0][0].alertData).toEqual({
alertId: 'test-id',
index: 'test-index',
rule: {
@ -148,42 +110,6 @@ describe('AddToCaseAction', () => {
});
});
it('onSuccess triggers toaster that links to case view', () => {
// @ts-ignore
useKibanaMock().services.cases.getCreateCase = ({
onSuccess,
}: {
onSuccess: (theCase: Case) => Promise<void>;
}) => {
onSuccess(basicCase);
};
const wrapper = mount(
<TestProviders>
<AddToCaseAction {...props} />
</TestProviders>
);
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click');
wrapper.find(`[data-test-subj="add-new-case-item"]`).first().simulate('click');
expect(mockDispatchToaster).toHaveBeenCalled();
const toast = mockDispatchToaster.mock.calls[0][0].toast;
const toastWrapper = mount(
<EuiGlobalToastList toasts={[toast]} toastLifeTimeMs={6000} dismissToast={() => {}} />
);
toastWrapper
.find('[data-test-subj="toaster-content-case-view-link"]')
.first()
.simulate('click');
expect(mockNavigateToApp).toHaveBeenCalledWith(APP_ID, {
path: '/basic-case-id',
deepLinkId: SecurityPageName.case,
});
});
it('disabled when event type is not supported', () => {
const wrapper = mount(
<TestProviders>
@ -203,14 +129,16 @@ describe('AddToCaseAction', () => {
});
it('hides the icon when user does not have crud permissions', () => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
crud: false,
read: true,
});
const newProps = {
...props,
casePermissions: {
crud: false,
read: true,
},
};
const wrapper = mount(
<TestProviders>
<AddToCaseAction {...props} />
<AddToCaseAction {...newProps} />
</TestProviders>
);

View file

@ -7,6 +7,7 @@
import { isEmpty } from 'lodash';
import React, { memo, useState, useCallback, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import {
EuiPopover,
EuiButtonIcon,
@ -16,28 +17,52 @@ import {
EuiToolTip,
} from '@elastic/eui';
import { Case, CaseStatuses, StatusAll } from '../../../../../cases/common';
import { APP_ID } from '../../../../common/constants';
import { Ecs } from '../../../../common/ecs';
import { SecurityPageName } from '../../../app/types';
import {
getCaseDetailsUrl,
getCreateCaseUrl,
useFormatUrl,
} from '../../../common/components/link_to';
import { useStateToaster } from '../../../common/components/toasters';
import { useControl } from '../../../common/hooks/use_control';
import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana';
import { ActionIconItem } from '../../../timelines/components/timeline/body/actions/action_icon_item';
import { CreateCaseFlyout } from '../create/flyout';
import { Case, CaseStatuses, StatusAll } from '../../../../../../cases/common';
import { Ecs } from '../../../../../common/ecs';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { TimelinesStartServices } from '../../../../types';
import { ActionIconItem } from '../../action_icon_item';
import { CreateCaseFlyout } from './create/flyout';
import { createUpdateSuccessToaster } from './helpers';
import * as i18n from './translations';
interface AddToCaseActionProps {
export interface AddToCaseActionProps {
ariaLabel?: string;
ecsRowData: Ecs;
useInsertTimeline?: Function;
casePermissions: {
crud: boolean;
read: boolean;
} | null;
appId: string;
}
interface UseControlsReturn {
isControlOpen: boolean;
openControl: () => void;
closeControl: () => void;
}
const appendSearch = (search?: string) =>
isEmpty(search) ? '' : `${search?.startsWith('?') ? search : `?${search}`}`;
const getCreateCaseUrl = (search?: string | null) => `/create${appendSearch(search ?? undefined)}`;
const getCaseDetailsUrl = ({
id,
search,
subCaseId,
}: {
id: string;
search?: string | null;
subCaseId?: string;
}) => {
if (subCaseId) {
return `/${encodeURIComponent(id)}/sub-cases/${encodeURIComponent(subCaseId)}${appendSearch(
search ?? undefined
)}`;
}
return `/${encodeURIComponent(id)}${appendSearch(search ?? undefined)}`;
};
interface PostCommentArg {
caseId: string;
data: {
@ -54,23 +79,31 @@ interface PostCommentArg {
const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
ariaLabel = i18n.ACTION_ADD_TO_CASE_ARIA_LABEL,
ecsRowData,
useInsertTimeline,
casePermissions,
appId,
}) => {
const eventId = ecsRowData._id;
const eventIndex = ecsRowData._index;
const rule = ecsRowData.signal?.rule;
const {
application: { navigateToApp },
application: { navigateToApp, getUrlForApp },
cases,
} = useKibana().services;
const [, dispatchToaster] = useStateToaster();
notifications: { toasts },
} = useKibana<TimelinesStartServices>().services;
const useControl = (): UseControlsReturn => {
const [isControlOpen, setIsControlOpen] = useState<boolean>(false);
const openControl = useCallback(() => setIsControlOpen(true), []);
const closeControl = useCallback(() => setIsControlOpen(false), []);
return { isControlOpen, openControl, closeControl };
};
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const openPopover = useCallback(() => setIsPopoverOpen(true), []);
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
const userPermissions = useGetUserCasesPermissions();
const isEventSupported = !isEmpty(ecsRowData.signal?.rule?.id);
const userCanCrud = userPermissions?.crud ?? false;
const userCanCrud = casePermissions?.crud ?? false;
const isDisabled = !userCanCrud || !isEventSupported;
const tooltipContext = userCanCrud
? isEventSupported
@ -80,13 +113,19 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
const onViewCaseClick = useCallback(
(id) => {
navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.case,
navigateToApp(appId, {
deepLinkId: appId === 'securitySolution' ? 'case' : 'cases',
path: getCaseDetailsUrl({ id }),
});
},
[navigateToApp]
[navigateToApp, appId]
);
const currentSearch = useLocation().search;
const urlSearch = useMemo(() => currentSearch, [currentSearch]);
const createCaseUrl = useMemo(() => getUrlForApp('cases') + getCreateCaseUrl(urlSearch), [
getUrlForApp,
urlSearch,
]);
const {
isControlOpen: isCreateCaseFlyoutOpen,
@ -112,35 +151,31 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
id: rule?.id != null ? rule.id[0] : null,
name: rule?.name != null ? rule.name[0] : null,
},
owner: APP_ID,
owner: appId,
},
updateCase,
});
}
},
[closeCaseFlyoutOpen, eventId, eventIndex, rule]
[closeCaseFlyoutOpen, eventId, eventIndex, rule, appId]
);
const onCaseSuccess = useCallback(
async (theCase: Case) => {
closeCaseFlyoutOpen();
return dispatchToaster({
type: 'addToaster',
toast: createUpdateSuccessToaster(theCase, onViewCaseClick),
});
createUpdateSuccessToaster(toasts, theCase, onViewCaseClick);
},
[closeCaseFlyoutOpen, dispatchToaster, onViewCaseClick]
[closeCaseFlyoutOpen, onViewCaseClick, toasts]
);
const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.case);
const goToCreateCase = useCallback(
async (ev) => {
ev.preventDefault();
return navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.case,
return navigateToApp(appId, {
deepLinkId: appId === 'securitySolution' ? 'case' : 'cases',
path: getCreateCaseUrl(urlSearch),
});
},
[navigateToApp, urlSearch]
[navigateToApp, urlSearch, appId]
);
const [isAllCaseModalOpen, openAllCaseModal] = useState(false);
@ -208,6 +243,40 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
[ariaLabel, isDisabled, openPopover, tooltipContext]
);
const getAllCasesSelectorModalProps = useMemo(() => {
return {
alertData: {
alertId: eventId,
index: eventIndex ?? '',
rule: {
id: rule?.id != null ? rule.id[0] : null,
name: rule?.name != null ? rule.name[0] : null,
},
owner: appId,
},
createCaseNavigation: {
href: createCaseUrl,
onClick: goToCreateCase,
},
hiddenStatuses: [CaseStatuses.closed, StatusAll],
onRowClick: onCaseClicked,
updateCase: onCaseSuccess,
userCanCrud: casePermissions?.crud ?? false,
owner: [appId],
};
}, [
casePermissions?.crud,
onCaseSuccess,
onCaseClicked,
createCaseUrl,
goToCreateCase,
eventId,
eventIndex,
rule?.id,
rule?.name,
appId,
]);
return (
<>
{userCanCrud && (
@ -230,31 +299,16 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
afterCaseCreated={attachAlertToCase}
onCloseFlyout={closeCaseFlyoutOpen}
onSuccess={onCaseSuccess}
useInsertTimeline={useInsertTimeline}
appId={appId}
/>
)}
{isAllCaseModalOpen &&
cases.getAllCasesSelectorModal({
alertData: {
alertId: eventId,
index: eventIndex ?? '',
rule: {
id: rule?.id != null ? rule.id[0] : null,
name: rule?.name != null ? rule.name[0] : null,
},
owner: APP_ID,
},
createCaseNavigation: {
href: formatUrl(getCreateCaseUrl()),
onClick: goToCreateCase,
},
hiddenStatuses: [CaseStatuses.closed, StatusAll],
onRowClick: onCaseClicked,
updateCase: onCaseSuccess,
userCanCrud: userPermissions?.crud ?? false,
owner: [APP_ID],
})}
{isAllCaseModalOpen && cases.getAllCasesSelectorModal(getAllCasesSelectorModalProps)}
</>
);
};
export const AddToCaseAction = memo(AddToCaseActionComponent);
// eslint-disable-next-line import/no-default-export
export default AddToCaseAction;

View file

@ -8,24 +8,15 @@
import React from 'react';
import { mount } from 'enzyme';
import '../../../common/mock/match_media';
import { CreateCaseFlyout } from './flyout';
import { TestProviders } from '../../../common/mock';
import { TestProviders } from '../../../../../mock';
jest.mock('../../../common/lib/kibana', () => ({
useKibana: () => ({
services: {
cases: {
getCreateCase: () => {},
},
},
}),
}));
const onCloseFlyout = jest.fn();
const onSuccess = jest.fn();
const defaultProps = {
onCloseFlyout,
onSuccess,
appId: 'securitySolution',
};
describe('CreateCaseFlyout', () => {

View file

@ -5,19 +5,21 @@
* 2.0.
*/
import React, { memo } from 'react';
import React, { memo, useMemo } from 'react';
import styled from 'styled-components';
import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui';
import * as i18n from '../../translations';
import { useKibana } from '../../../common/lib/kibana';
import { Case } from '../../../../../cases/common';
import { APP_ID } from '../../../../common/constants';
import * as i18n from '../translations';
import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
import { Case } from '../../../../../../../cases/common';
import type { TimelinesStartServices } from '../../../../../types';
export interface CreateCaseModalProps {
afterCaseCreated?: (theCase: Case) => Promise<void>;
onCloseFlyout: () => void;
onSuccess: (theCase: Case) => Promise<void>;
useInsertTimeline?: Function;
appId: string;
}
const StyledFlyout = styled(EuiFlyout)`
@ -50,8 +52,18 @@ const CreateCaseFlyoutComponent: React.FC<CreateCaseModalProps> = ({
afterCaseCreated,
onCloseFlyout,
onSuccess,
appId,
}) => {
const { cases } = useKibana().services;
const { cases } = useKibana<TimelinesStartServices>().services;
const createCaseProps = useMemo(() => {
return {
afterCaseCreated,
onCancel: onCloseFlyout,
onSuccess,
withSteps: false,
owner: [appId],
};
}, [afterCaseCreated, onCloseFlyout, onSuccess, appId]);
return (
<StyledFlyout onClose={onCloseFlyout} data-test-subj="create-case-flyout">
<EuiFlyoutHeader hasBorder>
@ -60,15 +72,7 @@ const CreateCaseFlyoutComponent: React.FC<CreateCaseModalProps> = ({
</EuiTitle>
</EuiFlyoutHeader>
<StyledEuiFlyoutBody>
<FormWrapper>
{cases.getCreateCase({
afterCaseCreated,
onCancel: onCloseFlyout,
onSuccess,
withSteps: false,
owner: [APP_ID],
})}
</FormWrapper>
<FormWrapper>{cases.getCreateCase(createCaseProps)}</FormWrapper>
</StyledEuiFlyoutBody>
</StyledFlyout>
);

View file

@ -0,0 +1,45 @@
/*
* 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 'jest-styled-components';
import type { MockedKeys } from '@kbn/utility-types/jest';
import { CoreStart } from 'kibana/public';
import { coreMock } from 'src/core/public/mocks';
import type { IToasts } from '../../../../../../../../src/core/public';
import { createUpdateSuccessToaster } from './helpers';
import { Case } from '../../../../../../cases/common';
let mockCoreStart: MockedKeys<CoreStart>;
let toasts: IToasts;
let toastsSpy: jest.SpyInstance;
const theCase = {
id: 'case-id',
title: 'My case',
settings: {
syncAlerts: true,
},
} as Case;
describe('helpers', () => {
beforeEach(() => {
mockCoreStart = coreMock.createStart();
});
describe('createUpdateSuccessToaster', () => {
it('creates the correct toast when the sync alerts is on', () => {
const onViewCaseClick = jest.fn();
toasts = mockCoreStart.notifications.toasts;
toastsSpy = jest.spyOn(mockCoreStart.notifications.toasts, 'addSuccess');
createUpdateSuccessToaster(toasts, theCase, onViewCaseClick);
expect(toastsSpy).toHaveBeenCalled();
});
});
});

View file

@ -6,12 +6,12 @@
*/
import React from 'react';
import uuid from 'uuid';
import styled from 'styled-components';
import { AppToast } from '../../../common/components/toasters';
import { ToasterContent } from './toaster_content';
import * as i18n from './translations';
import { Case } from '../../../../../cases/common';
import type { Case } from '../../../../../../cases/common';
import type { ToastsStart, Toast } from '../../../../../../../../src/core/public';
import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public';
const LINE_CLAMP = 3;
@ -24,20 +24,20 @@ const Title = styled.span`
`;
export const createUpdateSuccessToaster = (
toasts: ToastsStart,
theCase: Case,
onViewCaseClick: (id: string) => void
): AppToast => {
return {
id: uuid.v4(),
): Toast => {
return toasts.addSuccess({
color: 'success',
iconType: 'check',
title: <Title>{i18n.CASE_CREATED_SUCCESS_TOAST(theCase.title)}</Title>,
text: (
title: toMountPoint(<Title>{i18n.CASE_CREATED_SUCCESS_TOAST(theCase.title)}</Title>),
text: toMountPoint(
<ToasterContent
caseId={theCase.id}
syncAlerts={theCase.settings.syncAlerts}
onViewCaseClick={onViewCaseClick}
/>
),
};
});
};

View file

@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './add_to_case_action';
export * from './toaster_content';

View file

@ -7,63 +7,60 @@
import { i18n } from '@kbn/i18n';
export const ACTION_ADD_CASE = i18n.translate(
'xpack.securitySolution.cases.timeline.actions.addCase',
{
defaultMessage: 'Add to case',
}
);
export const ACTION_ADD_CASE = i18n.translate('xpack.timelines.cases.timeline.actions.addCase', {
defaultMessage: 'Add to case',
});
export const ACTION_ADD_NEW_CASE = i18n.translate(
'xpack.securitySolution.cases.timeline.actions.addNewCase',
'xpack.timelines.cases.timeline.actions.addNewCase',
{
defaultMessage: 'Add to new case',
}
);
export const ACTION_ADD_EXISTING_CASE = i18n.translate(
'xpack.securitySolution.cases.timeline.actions.addExistingCase',
'xpack.timelines.cases.timeline.actions.addExistingCase',
{
defaultMessage: 'Add to existing case',
}
);
export const ACTION_ADD_TO_CASE_ARIA_LABEL = i18n.translate(
'xpack.securitySolution.cases.timeline.actions.addToCaseAriaLabel',
'xpack.timelines.cases.timeline.actions.addToCaseAriaLabel',
{
defaultMessage: 'Attach alert to case',
}
);
export const ACTION_ADD_TO_CASE_TOOLTIP = i18n.translate(
'xpack.securitySolution.cases.timeline.actions.addToCaseTooltip',
'xpack.timelines.cases.timeline.actions.addToCaseTooltip',
{
defaultMessage: 'Add to case',
}
);
export const CASE_CREATED_SUCCESS_TOAST = (title: string) =>
i18n.translate('xpack.securitySolution.cases.timeline.actions.caseCreatedSuccessToast', {
i18n.translate('xpack.timelines.cases.timeline.actions.caseCreatedSuccessToast', {
values: { title },
defaultMessage: 'An alert has been added to "{title}"',
});
export const CASE_CREATED_SUCCESS_TOAST_TEXT = i18n.translate(
'xpack.securitySolution.cases.timeline.actions.caseCreatedSuccessToastText',
'xpack.timelines.cases.timeline.actions.caseCreatedSuccessToastText',
{
defaultMessage: 'Alerts in this case have their status synched with the case status',
}
);
export const VIEW_CASE = i18n.translate(
'xpack.securitySolution.cases.timeline.actions.caseCreatedSuccessToastViewCaseLink',
'xpack.timelines.cases.timeline.actions.caseCreatedSuccessToastViewCaseLink',
{
defaultMessage: 'View Case',
}
);
export const PERMISSIONS_MSG = i18n.translate(
'xpack.securitySolution.cases.timeline.actions.permissionsMessage',
'xpack.timelines.cases.timeline.actions.permissionsMessage',
{
defaultMessage:
'You are currently missing the required permissions to attach alerts to cases. Please contact your administrator for further assistance.',
@ -71,8 +68,12 @@ export const PERMISSIONS_MSG = i18n.translate(
);
export const UNSUPPORTED_EVENTS_MSG = i18n.translate(
'xpack.securitySolution.cases.timeline.actions.unsupportedEventsMessage',
'xpack.timelines.cases.timeline.actions.unsupportedEventsMessage',
{
defaultMessage: 'This event cannot be attached to a case',
}
);
export const CREATE_TITLE = i18n.translate('xpack.timelines.cases.caseView.create', {
defaultMessage: 'Create new case',
});

View file

@ -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 './cases';

View file

@ -11,7 +11,6 @@ import { Dispatch } from 'redux';
import { isString, keyBy } from 'lodash/fp';
import { stopPropagationAndPreventDefault, TimelineId } from '../../../common';
// eslint-disable-next-line no-duplicate-imports
import type { BrowserField, BrowserFields, ColumnHeaderOptions } from '../../../common';
import { tGridActions } from '../../store/t_grid';
import { DEFAULT_COLUMN_MIN_WIDTH } from '../t_grid/body/constants';

View file

@ -28,7 +28,6 @@ import { Header } from './header';
import * as i18n from './translations';
import { tGridActions } from '../../../../store/t_grid';
import { TimelineTabs } from '../../../../../common/types/timeline';
// eslint-disable-next-line no-duplicate-imports
import type { ColumnHeaderOptions } from '../../../../../common/types/timeline';
import { Direction } from '../../../../../common/search_strategy';

View file

@ -6,7 +6,6 @@
*/
import { Direction } from '../../../../../../common';
// eslint-disable-next-line no-duplicate-imports
import type { ColumnHeaderOptions } from '../../../../../../common';
import { assertUnreachable } from '../../../../../../common/utility_types';
import { Sort, SortDirection } from '../../sort';

View file

@ -11,7 +11,6 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd';
import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
// eslint-disable-next-line no-duplicate-imports
import type {
ControlColumnProps,
ColumnHeaderOptions,

View file

@ -23,7 +23,6 @@ import {
import { StatefulCell } from './stateful_cell';
import * as i18n from './translations';
import { TimelineTabs } from '../../../../../common/types/timeline';
// eslint-disable-next-line no-duplicate-imports
import type {
ActionProps,
CellValueElementProps,

View file

@ -9,7 +9,6 @@ import React, { HTMLAttributes, useState } from 'react';
import type { TimelineNonEcsData } from '../../../../../common/search_strategy';
import { TimelineTabs } from '../../../../../common/types/timeline';
// eslint-disable-next-line no-duplicate-imports
import type {
CellValueElementProps,
ColumnHeaderOptions,

View file

@ -11,7 +11,6 @@ import type { OnRowSelected } from '../../types';
import { EventsTrData, EventsTdGroupActions } from '../../styles';
import { DataDrivenColumns, getMappedNonEcsValue } from '../data_driven_columns';
import { TimelineTabs } from '../../../../../common/types/timeline';
// eslint-disable-next-line no-duplicate-imports
import type {
CellValueElementProps,
ColumnHeaderOptions,

View file

@ -12,7 +12,6 @@ import { EventsTbody } from '../../styles';
import { StatefulEvent } from './stateful_event';
import type { BrowserFields } from '../../../../../common/search_strategy/index_fields';
import { TimelineTabs } from '../../../../../common/types/timeline';
// eslint-disable-next-line no-duplicate-imports
import type {
CellValueElementProps,
ColumnHeaderOptions,

View file

@ -19,7 +19,6 @@ import { getMappedNonEcsValue } from '../data_driven_columns';
import { StatefulEventContext } from './stateful_event_context';
import type { BrowserFields } from '../../../../../common/search_strategy/index_fields';
import { TimelineTabs } from '../../../../../common/types/timeline';
// eslint-disable-next-line no-duplicate-imports
import type {
CellValueElementProps,
ColumnHeaderOptions,

View file

@ -7,7 +7,6 @@
import React, { useCallback, useState, useMemo } from 'react';
import { focusColumn, isArrowDownOrArrowUp, isArrowUp, isEscape } from '../../../../../../common';
// eslint-disable-next-line no-duplicate-imports
import type { OnColumnFocused } from '../../../../../../common';
type FocusOwnership = 'not-owned' | 'owned';

View file

@ -18,7 +18,6 @@ import React, { ComponentType, useCallback, useEffect, useMemo, useState } from
import { connect, ConnectedProps } from 'react-redux';
import { TimelineId, TimelineTabs } from '../../../../common/types/timeline';
// eslint-disable-next-line no-duplicate-imports
import type {
CellValueElementProps,
ColumnHeaderOptions,

View file

@ -8,7 +8,6 @@
import React from 'react';
import { RowRendererId } from '../../../../../common/types/timeline';
// eslint-disable-next-line no-duplicate-imports
import type { RowRenderer } from '../../../../../common/types/timeline';
const PlainRowRenderer = () => <></>;

View file

@ -6,7 +6,6 @@
*/
import { SortDirection } from '../../../../../common/types/timeline';
// eslint-disable-next-line no-duplicate-imports
import type { SortColumnTimeline } from '../../../../../common/types/timeline';
// TODO: Cleanup this type to match SortColumnTimeline

View file

@ -18,7 +18,6 @@ import {
import type { IIndexPattern } from '../../../../../../src/plugins/data/public';
import type { BrowserFields } from '../../../common/search_strategy/index_fields';
import { DataProviderType, EXISTS_OPERATOR } from '../../../common/types/timeline';
// eslint-disable-next-line no-duplicate-imports
import type { DataProvider, DataProvidersAnd } from '../../../common/types/timeline';
import { convertToBuildEsQuery, escapeQueryValue } from '../utils/keury';

View file

@ -12,12 +12,10 @@ import { useDispatch } from 'react-redux';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { Direction } from '../../../../common/search_strategy';
// eslint-disable-next-line no-duplicate-imports
import type { DocValueFields } from '../../../../common/search_strategy';
import type { CoreStart } from '../../../../../../../src/core/public';
import type { BrowserFields } from '../../../../common/search_strategy/index_fields';
import { TimelineId, TimelineTabs } from '../../../../common/types/timeline';
// eslint-disable-next-line no-duplicate-imports
import type {
CellValueElementProps,
ColumnHeaderOptions,

View file

@ -14,7 +14,6 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'
import { Direction } from '../../../../common/search_strategy';
import type { CoreStart } from '../../../../../../../src/core/public';
import { TimelineTabs } from '../../../../common/types/timeline';
// eslint-disable-next-line no-duplicate-imports
import type {
CellValueElementProps,
ColumnHeaderOptions,

View file

@ -14,7 +14,6 @@ import {
DATA_ROWINDEX_ATTRIBUTE,
onKeyDownFocusHandler,
} from '../../../../../common';
// eslint-disable-next-line no-duplicate-imports
import type { BrowserFields } from '../../../../../common';
import { getCategoryColumns } from './category_columns';
import { CATEGORIES_PANE_CLASS_NAME, TABLE_HEIGHT } from './helpers';

View file

@ -15,12 +15,10 @@ import {
DATA_ROWINDEX_ATTRIBUTE,
onKeyDownFocusHandler,
} from '../../../../../common';
// eslint-disable-next-line no-duplicate-imports
import type { BrowserFields, OnUpdateColumns } from '../../../../../common';
import { CategoryTitle } from './category_title';
import { getFieldColumns } from './field_items';
// eslint-disable-next-line no-duplicate-imports
import type { FieldItem } from './field_items';
import { CATEGORY_TABLE_CLASS_NAME, TABLE_HEIGHT } from './helpers';

View file

@ -20,7 +20,6 @@ import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import type { BrowserFields, ColumnHeaderOptions } from '../../../../../common';
// eslint-disable-next-line no-duplicate-imports
import { isEscape, isTab, stopPropagationAndPreventDefault } from '../../../../../common';
import { CategoriesPane } from './categories_pane';
import { FieldsPane } from './fields_pane';

View file

@ -22,7 +22,6 @@ import {
TimelineFactoryQueryTypes,
TimelineEventsQueries,
} from '../../common/search_strategy';
// eslint-disable-next-line no-duplicate-imports
import type {
DocValueFields,
Inspect,

View file

@ -8,7 +8,6 @@
import { useCallback, useRef } from 'react';
import { isString } from 'lodash/fp';
import { isAppError, isKibanaError, isSecurityAppError } from '@kbn/securitysolution-t-grid';
// eslint-disable-next-line no-duplicate-imports
import type { AppError } from '@kbn/securitysolution-t-grid';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';

View file

@ -16,6 +16,7 @@ import type {
LoadingPanelProps,
FieldBrowserWrappedProps,
} from '../components';
import type { AddToCaseActionProps } from '../components/actions/timeline/cases/add_to_case_action';
const TimelineLazy = lazy(() => import('../components'));
export const getTGridLazy = (
@ -68,3 +69,12 @@ export const getFieldsBrowserLazy = (
</Suspense>
);
};
const AddToCaseLazy = lazy(() => import('../components/actions/timeline/cases/add_to_case_action'));
export const getAddToCaseLazy = (props: AddToCaseActionProps) => {
return (
<Suspense fallback={<EuiLoadingSpinner />}>
<AddToCaseLazy {...props} />
</Suspense>
);
};

View file

@ -8,6 +8,7 @@
export * from './browser_fields';
export * from './header';
export * from './index_pattern';
export * from './kibana_react.mock';
export * from './mock_and_providers';
export * from './mock_data_providers';
export * from './mock_timeline_control_columns';

View file

@ -13,8 +13,27 @@ import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/p
import { EuiTheme } from '../../../../../src/plugins/kibana_react/common';
import { CoreStart } from '../../../../../src/core/public';
export const createStartServicesMock = (): CoreStart =>
(coreMock.createStart() as unknown) as CoreStart;
export const mockGetAllCasesSelectorModal = jest.fn();
export const mockNavigateToApp = jest.fn();
export const createStartServicesMock = (): CoreStart => {
const coreServices = coreMock.createStart();
return ({
...coreServices,
cases: {
getAllCases: jest.fn(),
getCaseView: jest.fn(),
getConfigureCases: jest.fn(),
getCreateCase: jest.fn(),
getRecentCases: jest.fn(),
getAllCasesSelectorModal: mockGetAllCasesSelectorModal,
},
application: {
...coreServices.application,
navigateToApp: mockNavigateToApp,
},
} as unknown) as CoreStart;
};
export const createWithKibanaMock = () => {
const services = createStartServicesMock();

View file

@ -6,7 +6,6 @@
*/
import { IS_OPERATOR } from '../../common/types/timeline';
// eslint-disable-next-line no-duplicate-imports
import type { DataProvider, DataProvidersAnd } from '../../common/types/timeline';
export const providerA: DataProvidersAnd = {

View file

@ -6,7 +6,6 @@
*/
import { IS_OPERATOR } from '../../common/types/timeline';
// eslint-disable-next-line no-duplicate-imports
import type { DataProvider } from '../../common/types/timeline';
interface NameToEventCount<TValue> {

View file

@ -6,7 +6,8 @@
*/
import type { Ecs } from '../../common/ecs';
import type { TimelineItem } from '../../common/search_strategy';
import { TimelineItem, Direction } from '../../common/search_strategy';
import type { TGridModel } from '../store/t_grid/model';
export const mockTimelineData: TimelineItem[] = [
{
@ -1509,3 +1510,76 @@ export const mockEndpointRegistryModificationEvent: Ecs = {
timestamp: '2021-02-04T13:44:31.559Z',
_id: '4cxLbXcBGrBB52F2uOfF',
};
export const mockTgridModel: TGridModel = {
columns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
initialWidth: 190,
},
{
columnHeaderType: 'not-filtered',
id: 'message',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.category',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'host.name',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'source.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'destination.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'user.name',
initialWidth: 180,
},
],
defaultColumns: [],
queryFields: [],
dateRange: {
end: '2020-03-18T13:52:38.929Z',
start: '2020-03-18T13:46:38.929Z',
},
deletedEventIds: [],
excludedRowRendererIds: [],
expandedDetail: {},
documentType: '',
selectAll: false,
id: 'ef579e40-jibber-jabber',
indexNames: [],
isLoading: false,
isSelectAllChecked: false,
kqlQuery: {
filterQuery: null,
},
itemsPerPage: 25,
itemsPerPageOptions: [10, 25, 50, 100],
loadingEventIds: [],
savedObjectId: 'ef579e40-jibber-jabber',
selectedEventIds: {},
showCheckboxes: false,
sort: [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: Direction.desc,
},
],
title: 'Test rule',
version: '1',
};

View file

@ -8,21 +8,21 @@
import { Store } from 'redux';
import { Storage } from '../../../../src/plugins/kibana_utils/public';
import type { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import type {
CoreSetup,
Plugin,
PluginInitializerContext,
CoreStart,
} from '../../../../src/core/public';
import type { TimelinesUIStart, TGridProps } from './types';
import type { LastUpdatedAtProps, LoadingPanelProps, FieldBrowserWrappedProps } from './components';
import {
getLastUpdatedLazy,
getLoadingPanelLazy,
getTGridLazy,
getFieldsBrowserLazy,
getAddToCaseLazy,
} from './methods';
import type { TimelinesUIStart, TGridProps, TimelinesStartPlugins } from './types';
import { tGridReducer } from './store/t_grid/reducer';
import { useDraggableKeyboardWrapper } from './components/drag_and_drop/draggable_keyboard_wrapper_hook';
import { useAddToTimeline, useAddToTimelineSensor } from './hooks/use_add_to_timeline';
@ -34,7 +34,7 @@ export class TimelinesPlugin implements Plugin<void, TimelinesUIStart> {
public setup(core: CoreSetup) {}
public start(core: CoreStart, { data }: { data: DataPublicPluginStart }): TimelinesUIStart {
public start(core: CoreStart, { data }: TimelinesStartPlugins): TimelinesUIStart {
const config = this.initializerContext.config.get<{ enabled: boolean }>();
if (!config.enabled) {
return {} as TimelinesUIStart;
@ -77,6 +77,9 @@ export class TimelinesPlugin implements Plugin<void, TimelinesUIStart> {
setTGridEmbeddedStore: (store: Store) => {
this.setStore(store);
},
getAddToCaseAction: (props) => {
return getAddToCaseLazy(props);
},
};
}

View file

@ -12,7 +12,6 @@ import type {
SortColumnTimeline,
TimelineExpandedDetailType,
} from '../../../common/types/timeline';
// eslint-disable-next-line no-duplicate-imports
import { TimelineTabs } from '../../../common/types/timeline';
import { InitialyzeTGridSettings, TGridPersistInput } from './types';

View file

@ -14,7 +14,6 @@ import type {
SortColumnTimeline,
SerializedFilterQuery,
} from '../../../common/types/timeline';
// eslint-disable-next-line no-duplicate-imports
import { RowRendererId } from '../../../common/types/timeline';
export interface TGridModelSettings {

View file

@ -8,6 +8,9 @@
import { ReactElement } from 'react';
import type { SensorAPI } from 'react-beautiful-dnd';
import { Store } from 'redux';
import { CoreStart } from '../../../../src/core/public';
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { CasesUiStart } from '../../cases/public';
import type {
LastUpdatedAtProps,
LoadingPanelProps,
@ -19,6 +22,7 @@ import type { TGridIntegratedProps } from './components/t_grid/integrated';
import type { TGridStandaloneProps } from './components/t_grid/standalone';
import type { UseAddToTimelineProps, UseAddToTimeline } from './hooks/use_add_to_timeline';
import { HoverActionsConfig } from './components/hover_actions/index';
import type { AddToCaseActionProps } from './components/actions/timeline/cases/add_to_case_action';
export * from './store/t_grid';
export interface TimelinesUIStart {
getHoverActions: () => HoverActionsConfig;
@ -36,7 +40,15 @@ export interface TimelinesUIStart {
props: UseDraggableKeyboardWrapperProps
) => UseDraggableKeyboardWrapper;
setTGridEmbeddedStore: (store: Store) => void;
getAddToCaseAction: (props: AddToCaseActionProps) => ReactElement<AddToCaseActionProps>;
}
export interface TimelinesStartPlugins {
data: DataPublicPluginStart;
cases: CasesUiStart;
}
export type TimelinesStartServices = CoreStart & TimelinesStartPlugins;
interface TGridStandaloneCompProps extends TGridStandaloneProps {
type: 'standalone';
}

View file

@ -23,6 +23,7 @@
{ "path": "../../../src/plugins/home/tsconfig.json" },
{ "path": "../data_enhanced/tsconfig.json" },
{ "path": "../features/tsconfig.json" },
{ "path": "../cases/tsconfig.json" },
{ "path": "../licensing/tsconfig.json" },
{ "path": "../spaces/tsconfig.json" },
{ "path": "../alerting/tsconfig.json" }

View file

@ -20358,16 +20358,6 @@
"xpack.securitySolution.cases.createCase.titleFieldRequiredError": "タイトルが必要です。",
"xpack.securitySolution.cases.dismissErrorsPushServiceCallOutTitle": "閉じる",
"xpack.securitySolution.cases.pageTitle": "ケース",
"xpack.securitySolution.cases.timeline.actions.addCase": "ケースに追加",
"xpack.securitySolution.cases.timeline.actions.addExistingCase": "既存のケースに追加",
"xpack.securitySolution.cases.timeline.actions.addNewCase": "新しいケースに追加",
"xpack.securitySolution.cases.timeline.actions.addToCaseAriaLabel": "アラートをケースに関連付ける",
"xpack.securitySolution.cases.timeline.actions.addToCaseTooltip": "ケースに追加",
"xpack.securitySolution.cases.timeline.actions.caseCreatedSuccessToast": "アラートが「{title}」に追加されました",
"xpack.securitySolution.cases.timeline.actions.caseCreatedSuccessToastText": "このケースのアラートはステータスがケースステータスと同期されました",
"xpack.securitySolution.cases.timeline.actions.caseCreatedSuccessToastViewCaseLink": "ケースの表示",
"xpack.securitySolution.cases.timeline.actions.permissionsMessage": "現在、アラートをケースに関連付けるための必要な権限がありません。サポートについては、管理者にお問い合わせください。",
"xpack.securitySolution.cases.timeline.actions.unsupportedEventsMessage": "このイベントはケースに関連付けられません",
"xpack.securitySolution.certificate.fingerprint.clientCertLabel": "クライアント証明書",
"xpack.securitySolution.certificate.fingerprint.serverCertLabel": "サーバー証明書",
"xpack.securitySolution.chart.allOthersGroupingLabel": "その他すべて",

View file

@ -20638,16 +20638,6 @@
"xpack.securitySolution.cases.createCase.titleFieldRequiredError": "标题必填。",
"xpack.securitySolution.cases.dismissErrorsPushServiceCallOutTitle": "关闭",
"xpack.securitySolution.cases.pageTitle": "案例",
"xpack.securitySolution.cases.timeline.actions.addCase": "添加到案例",
"xpack.securitySolution.cases.timeline.actions.addExistingCase": "添加到现有案例",
"xpack.securitySolution.cases.timeline.actions.addNewCase": "添加到新案例",
"xpack.securitySolution.cases.timeline.actions.addToCaseAriaLabel": "将告警附加到案例",
"xpack.securitySolution.cases.timeline.actions.addToCaseTooltip": "添加到案例",
"xpack.securitySolution.cases.timeline.actions.caseCreatedSuccessToast": "告警已添加到“{title}”",
"xpack.securitySolution.cases.timeline.actions.caseCreatedSuccessToastText": "此案例中的告警的状态已经与案例状态同步",
"xpack.securitySolution.cases.timeline.actions.caseCreatedSuccessToastViewCaseLink": "查看案例",
"xpack.securitySolution.cases.timeline.actions.permissionsMessage": "您当前缺少所需的权限,无法向案例附加告警。有关进一步帮助,请联系您的管理员。",
"xpack.securitySolution.cases.timeline.actions.unsupportedEventsMessage": "此事件无法附加到案例",
"xpack.securitySolution.certificate.fingerprint.clientCertLabel": "客户端证书",
"xpack.securitySolution.certificate.fingerprint.serverCertLabel": "服务器证书",
"xpack.securitySolution.chart.allOthersGroupingLabel": "所有其他",