mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Remove all cases related code from timelines (#127003)
This commit is contained in:
parent
fc3aedcf78
commit
5ad355e8c7
32 changed files with 2 additions and 1420 deletions
|
@ -15,18 +15,4 @@ export const mockTimelines = {
|
|||
onBlur: jest.fn(),
|
||||
onKeyDown: jest.fn(),
|
||||
}),
|
||||
getAddToCasePopover: jest
|
||||
.fn()
|
||||
.mockReturnValue(<div data-test-subj="add-to-case-action">{'Add to case'}</div>),
|
||||
getAddToCaseAction: jest.fn(),
|
||||
getAddToExistingCaseButton: jest.fn().mockReturnValue(
|
||||
<div key="add-to-existing-case-action" data-test-subj="add-to-existing-case-action">
|
||||
{'Add to existing case'}
|
||||
</div>
|
||||
),
|
||||
getAddToNewCaseButton: jest.fn().mockReturnValue(
|
||||
<div key="add-to-new-case-action" data-test-subj="add-to-new-case-action">
|
||||
{'Add to new case'}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
|
|
@ -52,17 +52,6 @@ jest.mock('../../../../../common/lib/kibana', () => ({
|
|||
useGetUserCasesPermissions: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'../../../../../../../timelines/public/components/actions/timeline/cases/add_to_case_action',
|
||||
() => {
|
||||
return {
|
||||
AddToCasePopover: () => {
|
||||
return <div data-test-subj="add-to-case-action">{'Add to case'}</div>;
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
describe('EventColumnView', () => {
|
||||
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
|
||||
(useShallowEqualSelector as jest.Mock).mockReturnValue(TimelineType.default);
|
||||
|
|
|
@ -61,10 +61,6 @@ jest.mock('../../../../common/lib/kibana', () => {
|
|||
onBlur: jest.fn(),
|
||||
onKeyDown: jest.fn(),
|
||||
}),
|
||||
getAddToCasePopover: jest
|
||||
.fn()
|
||||
.mockReturnValue(<div data-test-subj="add-to-case-action">{'Add to case'}</div>),
|
||||
getAddToCaseAction: jest.fn(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
|
|
@ -1,8 +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.
|
||||
*/
|
||||
|
||||
export * from './timeline';
|
|
@ -1,195 +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 {
|
||||
TestProviders,
|
||||
mockGetAllCasesSelectorModal,
|
||||
mockGetCreateCaseFlyout,
|
||||
} from '../../../../mock';
|
||||
import { AddToCaseAction } from './add_to_case_action';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../../../../cases/common';
|
||||
import { AddToCaseActionButton } from './add_to_case_action_button';
|
||||
import { ALERT_RULE_UUID } from '@kbn/rule-data-utils';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useLocation: () => ({
|
||||
search: '',
|
||||
}),
|
||||
}));
|
||||
jest.mock('./helpers');
|
||||
|
||||
describe('AddToCaseAction', () => {
|
||||
const props = {
|
||||
event: {
|
||||
_id: 'test-id',
|
||||
data: [],
|
||||
ecs: {
|
||||
_id: 'test-id',
|
||||
_index: 'test-index',
|
||||
signal: { rule: { id: ['rule-id'], name: ['rule-name'], false_positives: [] } },
|
||||
},
|
||||
},
|
||||
casePermissions: {
|
||||
crud: true,
|
||||
read: true,
|
||||
},
|
||||
appId: 'securitySolutionUI',
|
||||
owner: 'securitySolution',
|
||||
onClose: () => null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('it renders', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AddToCaseActionButton {...props} />
|
||||
<AddToCaseAction {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it opens the context menu', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AddToCaseActionButton {...props} />
|
||||
<AddToCaseAction {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click');
|
||||
expect(wrapper.find(`[data-test-subj="add-new-case-item"]`).exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="add-existing-case-menu-item"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it opens the create case flyout', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AddToCaseActionButton {...props} />
|
||||
<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(mockGetCreateCaseFlyout).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('it opens the all cases modal', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AddToCaseActionButton {...props} />
|
||||
<AddToCaseAction {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
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(mockGetAllCasesSelectorModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('it set rule information as null when missing', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AddToCaseActionButton
|
||||
{...props}
|
||||
event={{
|
||||
_id: 'test-id',
|
||||
data: [{ field: ALERT_RULE_UUID, value: ['rule-id'] }],
|
||||
ecs: {
|
||||
_id: 'test-id',
|
||||
_index: 'test-index',
|
||||
signal: { rule: { id: ['rule-id'], false_positives: [] } },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<AddToCaseAction
|
||||
{...props}
|
||||
event={{
|
||||
_id: 'test-id',
|
||||
data: [{ field: ALERT_RULE_UUID, value: ['rule-id'] }],
|
||||
ecs: {
|
||||
_id: 'test-id',
|
||||
_index: 'test-index',
|
||||
signal: { rule: { id: ['rule-id'], false_positives: [] } },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
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(mockGetAllCasesSelectorModal.mock.calls[0][0].alertData).toEqual({
|
||||
alertId: 'test-id',
|
||||
index: 'test-index',
|
||||
rule: {
|
||||
id: 'rule-id',
|
||||
name: null,
|
||||
},
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
});
|
||||
});
|
||||
|
||||
it('disabled when event type is not supported', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AddToCaseActionButton
|
||||
{...props}
|
||||
event={{
|
||||
_id: 'test-id',
|
||||
data: [],
|
||||
ecs: {
|
||||
_id: 'test-id',
|
||||
_index: 'test-index',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<AddToCaseAction
|
||||
{...props}
|
||||
event={{
|
||||
_id: 'test-id',
|
||||
data: [],
|
||||
ecs: {
|
||||
_id: 'test-id',
|
||||
_index: 'test-index',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('isDisabled')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('hides the icon when user does not have crud permissions', () => {
|
||||
const newProps = {
|
||||
...props,
|
||||
casePermissions: {
|
||||
crud: false,
|
||||
read: true,
|
||||
},
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AddToCaseActionButton {...newProps} />
|
||||
<AddToCaseAction {...newProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).exists()).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -1,147 +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, { memo, useMemo, useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
GetAllCasesSelectorModalProps,
|
||||
GetCreateCaseFlyoutProps,
|
||||
} from '../../../../../../cases/public';
|
||||
import {
|
||||
CaseStatuses,
|
||||
StatusAll,
|
||||
CasesFeatures,
|
||||
CommentType,
|
||||
} from '../../../../../../cases/common';
|
||||
import { TimelineItem } from '../../../../../common/search_strategy';
|
||||
import { useAddToCase, normalizedEventFields } from '../../../../hooks/use_add_to_case';
|
||||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { TimelinesStartServices } from '../../../../types';
|
||||
import { setOpenAddToExistingCase, setOpenAddToNewCase } from '../../../../store/t_grid/actions';
|
||||
|
||||
export interface AddToCaseActionProps {
|
||||
event?: TimelineItem;
|
||||
useInsertTimeline?: Function;
|
||||
casePermissions: {
|
||||
crud: boolean;
|
||||
read: boolean;
|
||||
} | null;
|
||||
appId: string;
|
||||
owner: string;
|
||||
onClose?: Function;
|
||||
casesFeatures?: CasesFeatures;
|
||||
}
|
||||
|
||||
const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
|
||||
event,
|
||||
useInsertTimeline,
|
||||
casePermissions,
|
||||
appId,
|
||||
owner,
|
||||
onClose,
|
||||
casesFeatures,
|
||||
}) => {
|
||||
const eventId = event?.ecs._id ?? '';
|
||||
const eventIndex = event?.ecs._index ?? '';
|
||||
const dispatch = useDispatch();
|
||||
const { cases } = useKibana<TimelinesStartServices>().services;
|
||||
const {
|
||||
onCaseClicked,
|
||||
onCaseSuccess,
|
||||
onCaseCreated,
|
||||
isAllCaseModalOpen,
|
||||
isCreateCaseFlyoutOpen,
|
||||
} = useAddToCase({ event, casePermissions, appId, owner, onClose });
|
||||
|
||||
const allCasesSelectorModalProps: GetAllCasesSelectorModalProps = useMemo(() => {
|
||||
const { ruleId, ruleName } = normalizedEventFields(event);
|
||||
return {
|
||||
alertData: {
|
||||
alertId: eventId,
|
||||
index: eventIndex ?? '',
|
||||
rule: {
|
||||
id: ruleId,
|
||||
name: ruleName,
|
||||
},
|
||||
owner,
|
||||
},
|
||||
hooks: {
|
||||
useInsertTimeline,
|
||||
},
|
||||
hiddenStatuses: [CaseStatuses.closed, StatusAll],
|
||||
onRowClick: onCaseClicked,
|
||||
updateCase: onCaseSuccess,
|
||||
userCanCrud: casePermissions?.crud ?? false,
|
||||
owner: [owner],
|
||||
onClose: () => dispatch(setOpenAddToExistingCase({ id: eventId, isOpen: false })),
|
||||
};
|
||||
}, [
|
||||
casePermissions?.crud,
|
||||
onCaseSuccess,
|
||||
onCaseClicked,
|
||||
eventId,
|
||||
eventIndex,
|
||||
dispatch,
|
||||
owner,
|
||||
useInsertTimeline,
|
||||
event,
|
||||
]);
|
||||
|
||||
const closeCaseFlyoutOpen = useCallback(() => {
|
||||
dispatch(setOpenAddToNewCase({ id: eventId, isOpen: false }));
|
||||
}, [dispatch, eventId]);
|
||||
|
||||
const createCaseFlyoutProps: GetCreateCaseFlyoutProps = useMemo(() => {
|
||||
const { ruleId, ruleName } = normalizedEventFields(event);
|
||||
const attachments = [
|
||||
{
|
||||
alertId: eventId,
|
||||
index: eventIndex ?? '',
|
||||
rule: {
|
||||
id: ruleId,
|
||||
name: ruleName,
|
||||
},
|
||||
owner,
|
||||
type: CommentType.alert as const,
|
||||
},
|
||||
];
|
||||
return {
|
||||
afterCaseCreated: onCaseCreated,
|
||||
onClose: closeCaseFlyoutOpen,
|
||||
onSuccess: onCaseSuccess,
|
||||
useInsertTimeline,
|
||||
owner: [owner],
|
||||
userCanCrud: casePermissions?.crud ?? false,
|
||||
features: casesFeatures,
|
||||
attachments,
|
||||
};
|
||||
}, [
|
||||
event,
|
||||
eventId,
|
||||
eventIndex,
|
||||
owner,
|
||||
onCaseCreated,
|
||||
closeCaseFlyoutOpen,
|
||||
onCaseSuccess,
|
||||
useInsertTimeline,
|
||||
casePermissions?.crud,
|
||||
casesFeatures,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isCreateCaseFlyoutOpen && cases.getCreateCaseFlyout(createCaseFlyoutProps)}
|
||||
{isAllCaseModalOpen && cases.getAllCasesSelectorModal(allCasesSelectorModalProps)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
AddToCaseActionComponent.displayName = 'AddToCaseAction';
|
||||
|
||||
export const AddToCaseAction = memo(AddToCaseActionComponent);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default AddToCaseAction;
|
|
@ -1,109 +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, { memo, useMemo } from 'react';
|
||||
import {
|
||||
EuiPopover,
|
||||
EuiButtonIcon,
|
||||
EuiContextMenuPanel,
|
||||
EuiText,
|
||||
EuiContextMenuItem,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { AddToCaseActionProps } from './add_to_case_action';
|
||||
import { useAddToCase } from '../../../../hooks/use_add_to_case';
|
||||
import { ActionIconItem } from '../../action_icon_item';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const AddToCaseActionButtonComponent: React.FC<AddToCaseActionProps> = ({
|
||||
event,
|
||||
useInsertTimeline,
|
||||
casePermissions,
|
||||
appId,
|
||||
owner,
|
||||
onClose,
|
||||
}) => {
|
||||
const {
|
||||
addNewCaseClick,
|
||||
addExistingCaseClick,
|
||||
isDisabled,
|
||||
userCanCrud,
|
||||
isEventSupported,
|
||||
openPopover,
|
||||
closePopover,
|
||||
isPopoverOpen,
|
||||
} = useAddToCase({ event, useInsertTimeline, casePermissions, appId, owner, onClose });
|
||||
const tooltipContext = userCanCrud
|
||||
? isEventSupported
|
||||
? i18n.ACTION_ADD_TO_CASE_TOOLTIP
|
||||
: i18n.UNSUPPORTED_EVENTS_MSG
|
||||
: i18n.PERMISSIONS_MSG;
|
||||
const items = useMemo(
|
||||
() => [
|
||||
<EuiContextMenuItem
|
||||
key="add-new-case-menu-item"
|
||||
onClick={addNewCaseClick}
|
||||
aria-label={i18n.ACTION_ADD_NEW_CASE}
|
||||
data-test-subj="add-new-case-item"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<EuiText size="m">{i18n.ACTION_ADD_NEW_CASE}</EuiText>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
key="add-existing-case-menu-item"
|
||||
onClick={addExistingCaseClick}
|
||||
aria-label={i18n.ACTION_ADD_EXISTING_CASE}
|
||||
data-test-subj="add-existing-case-menu-item"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<EuiText size="m">{i18n.ACTION_ADD_EXISTING_CASE}</EuiText>
|
||||
</EuiContextMenuItem>,
|
||||
],
|
||||
[addExistingCaseClick, addNewCaseClick, isDisabled]
|
||||
);
|
||||
|
||||
const button = useMemo(
|
||||
() => (
|
||||
<EuiToolTip data-test-subj="attach-alert-to-case-tooltip" content={tooltipContext}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="attach-alert-to-case-button"
|
||||
size="s"
|
||||
iconType="folderClosed"
|
||||
onClick={openPopover}
|
||||
isDisabled={isDisabled}
|
||||
aria-label={tooltipContext}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
),
|
||||
[isDisabled, openPopover, tooltipContext]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{userCanCrud && (
|
||||
<ActionIconItem>
|
||||
<EuiPopover
|
||||
id="attachAlertToCasePanel"
|
||||
button={button}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
repositionOnScroll
|
||||
>
|
||||
<EuiContextMenuPanel items={items} />
|
||||
</EuiPopover>
|
||||
</ActionIconItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddToCaseActionButton = memo(AddToCaseActionButtonComponent);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default AddToCaseActionButton;
|
|
@ -1,76 +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, { memo } from 'react';
|
||||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
|
||||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { TimelinesStartServices } from '../../../../types';
|
||||
import { useAddToCase } from '../../../../hooks/use_add_to_case';
|
||||
import { AddToCaseActionProps } from './add_to_case_action';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface AddToCaseActionButtonProps extends AddToCaseActionProps {
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
const AddToCaseActionButtonComponent: React.FC<AddToCaseActionButtonProps> = ({
|
||||
ariaLabel = i18n.ACTION_ADD_TO_CASE_ARIA_LABEL,
|
||||
event,
|
||||
useInsertTimeline,
|
||||
casePermissions,
|
||||
appId,
|
||||
owner,
|
||||
onClose,
|
||||
}) => {
|
||||
const { onCaseSuccess, onCaseClicked, isDisabled, userCanCrud, caseAttachments } = useAddToCase({
|
||||
event,
|
||||
useInsertTimeline,
|
||||
casePermissions,
|
||||
appId,
|
||||
owner,
|
||||
onClose,
|
||||
});
|
||||
const { cases } = useKibana<TimelinesStartServices>().services;
|
||||
const addToCaseModal = cases.hooks.getUseCasesAddToExistingCaseModal({
|
||||
attachments: caseAttachments,
|
||||
updateCase: onCaseSuccess,
|
||||
onRowClick: onCaseClicked,
|
||||
});
|
||||
|
||||
// TODO To be further refactored and moved to cases plugins
|
||||
// https://github.com/elastic/kibana/issues/123183
|
||||
const handleClick = () => {
|
||||
// close the popover
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
addToCaseModal.open();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{userCanCrud && (
|
||||
<EuiContextMenuItem
|
||||
aria-label={ariaLabel}
|
||||
data-test-subj="add-existing-case-menu-item"
|
||||
onClick={handleClick}
|
||||
// needs forced size="s" since it is lazy loaded and the EuiContextMenuPanel can not initialize the size
|
||||
size="s"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{i18n.ACTION_ADD_EXISTING_CASE}
|
||||
</EuiContextMenuItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddToExistingCaseButton = memo(AddToCaseActionButtonComponent);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default AddToExistingCaseButton;
|
|
@ -1,77 +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, { memo } from 'react';
|
||||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
|
||||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { TimelinesStartServices } from '../../../../types';
|
||||
import { useAddToCase } from '../../../../hooks/use_add_to_case';
|
||||
import { AddToCaseActionProps } from './add_to_case_action';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface AddToNewCaseButtonProps extends AddToCaseActionProps {
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
const AddToNewCaseButtonComponent: React.FC<AddToNewCaseButtonProps> = ({
|
||||
ariaLabel = i18n.ACTION_ADD_TO_CASE_ARIA_LABEL,
|
||||
event,
|
||||
useInsertTimeline,
|
||||
casePermissions,
|
||||
appId,
|
||||
owner,
|
||||
onClose,
|
||||
}) => {
|
||||
const { isDisabled, userCanCrud, caseAttachments, onCaseSuccess, onCaseCreated } = useAddToCase({
|
||||
event,
|
||||
useInsertTimeline,
|
||||
casePermissions,
|
||||
appId,
|
||||
owner,
|
||||
onClose,
|
||||
});
|
||||
const { cases } = useKibana<TimelinesStartServices>().services;
|
||||
const createCaseFlyout = cases.hooks.getUseCasesAddToNewCaseFlyout({
|
||||
attachments: caseAttachments,
|
||||
afterCaseCreated: onCaseCreated,
|
||||
onSuccess: onCaseSuccess,
|
||||
});
|
||||
|
||||
// TODO To be further refactored and moved to cases plugins
|
||||
// https://github.com/elastic/kibana/issues/123183
|
||||
const handleClick = () => {
|
||||
// close the popover
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
createCaseFlyout.open();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{userCanCrud && (
|
||||
<EuiContextMenuItem
|
||||
aria-label={ariaLabel}
|
||||
data-test-subj="add-new-case-item"
|
||||
onClick={handleClick}
|
||||
// needs forced size="s" since it is lazy loaded and the EuiContextMenuPanel can not initialize the size
|
||||
size="s"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{i18n.ACTION_ADD_NEW_CASE}
|
||||
</EuiContextMenuItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
AddToNewCaseButtonComponent.displayName = 'AddToNewCaseButton';
|
||||
|
||||
export const AddToNewCaseButton = memo(AddToNewCaseButtonComponent);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default AddToNewCaseButton;
|
|
@ -1,45 +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 '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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,43 +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 styled from 'styled-components';
|
||||
import { ToasterContent } from './toaster_content';
|
||||
import * as i18n from './translations';
|
||||
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;
|
||||
|
||||
const Title = styled.span`
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: ${LINE_CLAMP};
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const createUpdateSuccessToaster = (
|
||||
toasts: ToastsStart,
|
||||
theCase: Case,
|
||||
onViewCaseClick: (id: string) => void
|
||||
): Toast => {
|
||||
return toasts.addSuccess({
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
title: toMountPoint(<Title>{i18n.CASE_CREATED_SUCCESS_TOAST(theCase.title)}</Title>),
|
||||
text: toMountPoint(
|
||||
<ToasterContent
|
||||
caseId={theCase.id}
|
||||
syncAlerts={theCase.settings.syncAlerts}
|
||||
onViewCaseClick={onViewCaseClick}
|
||||
/>
|
||||
),
|
||||
});
|
||||
};
|
|
@ -1,11 +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.
|
||||
*/
|
||||
|
||||
export * from './add_to_case_action';
|
||||
export * from './toaster_content';
|
||||
export * from './add_to_existing_case_button';
|
||||
export * from './add_to_new_case_button';
|
|
@ -1,46 +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 { ToasterContent } from './toaster_content';
|
||||
|
||||
describe('ToasterContent', () => {
|
||||
const onViewCaseClick = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders with syncAlerts=true', () => {
|
||||
const wrapper = mount(
|
||||
<ToasterContent caseId="case-id" syncAlerts={true} onViewCaseClick={onViewCaseClick} />
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="toaster-content-case-view-link"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="toaster-content-sync-text"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders with syncAlerts=false', () => {
|
||||
const wrapper = mount(
|
||||
<ToasterContent caseId="case-id" syncAlerts={false} onViewCaseClick={onViewCaseClick} />
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="toaster-content-case-view-link"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="toaster-content-sync-text"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('calls onViewCaseClick', () => {
|
||||
const wrapper = mount(
|
||||
<ToasterContent caseId="case-id" syncAlerts={false} onViewCaseClick={onViewCaseClick} />
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="toaster-content-case-view-link"]').first().simulate('click');
|
||||
expect(onViewCaseClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -1,47 +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, { memo, useCallback } from 'react';
|
||||
import { EuiButtonEmpty, EuiText } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
const EuiTextStyled = styled(EuiText)`
|
||||
${({ theme }) => `
|
||||
margin-bottom: ${theme.eui?.paddingSizes?.s ?? 8}px;
|
||||
`}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
caseId: string;
|
||||
syncAlerts: boolean;
|
||||
onViewCaseClick: (id: string) => void;
|
||||
}
|
||||
|
||||
const ToasterContentComponent: React.FC<Props> = ({ caseId, syncAlerts, onViewCaseClick }) => {
|
||||
const onClick = useCallback(() => onViewCaseClick(caseId), [caseId, onViewCaseClick]);
|
||||
return (
|
||||
<>
|
||||
{syncAlerts && (
|
||||
<EuiTextStyled size="s" data-test-subj="toaster-content-sync-text">
|
||||
{i18n.CASE_CREATED_SUCCESS_TOAST_TEXT}
|
||||
</EuiTextStyled>
|
||||
)}
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
flush="left"
|
||||
onClick={onClick}
|
||||
data-test-subj="toaster-content-case-view-link"
|
||||
>
|
||||
{i18n.VIEW_CASE}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ToasterContent = memo(ToasterContentComponent);
|
|
@ -1,75 +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 { i18n } from '@kbn/i18n';
|
||||
|
||||
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.timelines.cases.timeline.actions.addNewCase',
|
||||
{
|
||||
defaultMessage: 'Add to new case',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_ADD_EXISTING_CASE = i18n.translate(
|
||||
'xpack.timelines.cases.timeline.actions.addExistingCase',
|
||||
{
|
||||
defaultMessage: 'Add to existing case',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_ADD_TO_CASE_ARIA_LABEL = i18n.translate(
|
||||
'xpack.timelines.cases.timeline.actions.addToCaseAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Attach alert to case',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_ADD_TO_CASE_TOOLTIP = i18n.translate(
|
||||
'xpack.timelines.cases.timeline.actions.addToCaseTooltip',
|
||||
{
|
||||
defaultMessage: 'Add to case',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASE_CREATED_SUCCESS_TOAST = (title: string) =>
|
||||
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.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.timelines.cases.timeline.actions.caseCreatedSuccessToastViewCaseLink',
|
||||
{
|
||||
defaultMessage: 'View Case',
|
||||
}
|
||||
);
|
||||
|
||||
export const PERMISSIONS_MSG = i18n.translate(
|
||||
'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.',
|
||||
}
|
||||
);
|
||||
|
||||
export const UNSUPPORTED_EVENTS_MSG = i18n.translate(
|
||||
'xpack.timelines.cases.timeline.actions.unsupportedEventsMessage',
|
||||
{
|
||||
defaultMessage: 'This event cannot be attached to a case',
|
||||
}
|
||||
);
|
|
@ -1,8 +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.
|
||||
*/
|
||||
|
||||
export * from './cases';
|
|
@ -8,7 +8,7 @@ import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
|
|||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { useEffect, useMemo, useState, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { Filter, Query } from '@kbn/es-query';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
|
@ -39,7 +39,6 @@ import { LastUpdatedAt } from '../..';
|
|||
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem, UpdatedFlexGroup } from '../styles';
|
||||
import { InspectButton, InspectButtonContainer } from '../../inspect';
|
||||
import { useFetchIndex } from '../../../container/source';
|
||||
import { AddToCaseAction } from '../../actions/timeline/cases/add_to_case_action';
|
||||
import { TGridLoading, TGridEmpty, TimelineContext } from '../shared';
|
||||
|
||||
const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>`
|
||||
|
@ -76,16 +75,7 @@ const ScrollableFlexItem = styled(EuiFlexItem)`
|
|||
overflow: auto;
|
||||
`;
|
||||
|
||||
const casesFeatures = { alerts: { sync: false } };
|
||||
|
||||
export interface TGridStandaloneProps {
|
||||
appId: string;
|
||||
casesOwner: string;
|
||||
casePermissions: {
|
||||
crud: boolean;
|
||||
read: boolean;
|
||||
} | null;
|
||||
afterCaseSelection?: Function;
|
||||
columns: ColumnHeaderOptions[];
|
||||
dataViewId?: string | null;
|
||||
defaultCellActions?: TGridCellAction[];
|
||||
|
@ -127,10 +117,6 @@ export interface TGridStandaloneProps {
|
|||
}
|
||||
|
||||
const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
||||
afterCaseSelection,
|
||||
appId,
|
||||
casesOwner,
|
||||
casePermissions,
|
||||
columns,
|
||||
dataViewId = null,
|
||||
defaultCellActions,
|
||||
|
@ -272,26 +258,6 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
);
|
||||
const hasAlerts = totalCountMinusDeleted > 0;
|
||||
|
||||
const activeCaseFlowId = useSelector((state: State) => tGridSelectors.activeCaseFlowId(state));
|
||||
const selectedEvent = useMemo(() => {
|
||||
const matchedEvent = events.find((event) => event.ecs._id === activeCaseFlowId);
|
||||
if (matchedEvent) {
|
||||
return matchedEvent;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}, [events, activeCaseFlowId]);
|
||||
|
||||
const addToCaseActionProps = useMemo(() => {
|
||||
return {
|
||||
event: selectedEvent,
|
||||
casePermissions: casePermissions ?? null,
|
||||
appId,
|
||||
owner: casesOwner,
|
||||
onClose: afterCaseSelection,
|
||||
};
|
||||
}, [appId, casePermissions, afterCaseSelection, selectedEvent, casesOwner]);
|
||||
|
||||
const nonDeletedEvents = useMemo(
|
||||
() => events.filter((e) => !deletedEventIds.includes(e._id)),
|
||||
[deletedEventIds, events]
|
||||
|
@ -425,7 +391,6 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
</EventsContainerLoading>
|
||||
</TimelineContext.Provider>
|
||||
) : null}
|
||||
<AddToCaseAction {...addToCaseActionProps} casesFeatures={casesFeatures} />
|
||||
</AlertsTableWrapper>
|
||||
</InspectButtonContainer>
|
||||
);
|
||||
|
|
|
@ -1,71 +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 { normalizedEventFields } from './use_add_to_case';
|
||||
import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils';
|
||||
import { merge } from 'lodash';
|
||||
|
||||
const defaultArgs = {
|
||||
_id: 'test-id',
|
||||
data: [
|
||||
{ field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] },
|
||||
{ field: ALERT_RULE_UUID, value: ['data-rule-id'] },
|
||||
{ field: ALERT_RULE_NAME, value: ['data-rule-name'] },
|
||||
],
|
||||
ecs: {
|
||||
_id: 'test-id',
|
||||
_index: 'test-index',
|
||||
signal: { rule: { id: ['rule-id'], name: ['rule-name'], false_positives: [] } },
|
||||
},
|
||||
};
|
||||
describe('normalizedEventFields', () => {
|
||||
it('uses rule data when provided', () => {
|
||||
const result = normalizedEventFields(defaultArgs);
|
||||
expect(result).toEqual({
|
||||
ruleId: 'data-rule-id',
|
||||
ruleName: 'data-rule-name',
|
||||
});
|
||||
});
|
||||
const makeObj = (s: string, v: string[]) => {
|
||||
const keys = s.split('.');
|
||||
return keys
|
||||
.reverse()
|
||||
.reduce((prev, current, i) => (i === 0 ? { [current]: v } : { [current]: { ...prev } }), {});
|
||||
};
|
||||
it('uses rule/ecs combo Xavier thinks is a thing but Steph has yet to see', () => {
|
||||
const args = {
|
||||
...defaultArgs,
|
||||
data: [],
|
||||
ecs: {
|
||||
_id: 'string',
|
||||
...merge(
|
||||
makeObj(ALERT_RULE_UUID, ['xavier-rule-id']),
|
||||
makeObj(ALERT_RULE_NAME, ['xavier-rule-name'])
|
||||
),
|
||||
},
|
||||
};
|
||||
const result = normalizedEventFields(args);
|
||||
expect(result).toEqual({
|
||||
ruleId: 'xavier-rule-id',
|
||||
ruleName: 'xavier-rule-name',
|
||||
});
|
||||
});
|
||||
it('falls back to use ecs data', () => {
|
||||
const result = normalizedEventFields({ ...defaultArgs, data: [] });
|
||||
expect(result).toEqual({
|
||||
ruleId: 'rule-id',
|
||||
ruleName: 'rule-name',
|
||||
});
|
||||
});
|
||||
it('returns null when all the data is bad', () => {
|
||||
const result = normalizedEventFields({ ...defaultArgs, data: [], ecs: { _id: 'bad' } });
|
||||
expect(result).toEqual({
|
||||
ruleId: null,
|
||||
ruleName: null,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,200 +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 { get, isEmpty } from 'lodash/fp';
|
||||
import { useState, useCallback, useMemo, SyntheticEvent } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils';
|
||||
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { Case, CommentType } from '../../../cases/common';
|
||||
import { TimelinesStartServices } from '../types';
|
||||
import { TimelineItem } from '../../common/search_strategy';
|
||||
import { tGridActions } from '../store/t_grid';
|
||||
import { useDeepEqualSelector } from './use_selector';
|
||||
import { createUpdateSuccessToaster } from '../components/actions/timeline/cases/helpers';
|
||||
import { AddToCaseActionProps } from '../components/actions';
|
||||
import { CaseAttachments, CasesDeepLinkId, generateCaseViewPath } from '../../../cases/public';
|
||||
|
||||
interface UseAddToCase {
|
||||
addNewCaseClick: () => void;
|
||||
addExistingCaseClick: () => void;
|
||||
onCaseClicked: (theCase?: Case) => void;
|
||||
onCaseSuccess: (theCase: Case) => Promise<void>;
|
||||
onCaseCreated: () => Promise<void>;
|
||||
isAllCaseModalOpen: boolean;
|
||||
isDisabled: boolean;
|
||||
userCanCrud: boolean;
|
||||
isEventSupported: boolean;
|
||||
openPopover: (event: SyntheticEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
closePopover: () => void;
|
||||
isPopoverOpen: boolean;
|
||||
isCreateCaseFlyoutOpen: boolean;
|
||||
caseAttachments?: CaseAttachments;
|
||||
}
|
||||
|
||||
export const useAddToCase = ({
|
||||
event,
|
||||
casePermissions,
|
||||
appId,
|
||||
onClose,
|
||||
owner,
|
||||
}: AddToCaseActionProps): UseAddToCase => {
|
||||
const eventId = event?.ecs._id ?? '';
|
||||
const dispatch = useDispatch();
|
||||
// TODO: use correct value in standalone or integrated.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const timelineById = useDeepEqualSelector((state: any) => {
|
||||
if (state.timeline) {
|
||||
return state.timeline.timelineById[eventId];
|
||||
} else {
|
||||
return state.timelineById[eventId];
|
||||
}
|
||||
});
|
||||
const isAllCaseModalOpen = useMemo(() => {
|
||||
if (timelineById) {
|
||||
return timelineById.isAddToExistingCaseOpen;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}, [timelineById]);
|
||||
const isCreateCaseFlyoutOpen = useMemo(() => {
|
||||
if (timelineById) {
|
||||
return timelineById.isCreateNewCaseOpen;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}, [timelineById]);
|
||||
const {
|
||||
application: { navigateToApp },
|
||||
notifications: { toasts },
|
||||
} = useKibana<TimelinesStartServices>().services;
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const openPopover = useCallback(() => setIsPopoverOpen(true), []);
|
||||
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
|
||||
|
||||
const isEventSupported = useMemo(() => {
|
||||
if (event !== undefined) {
|
||||
if (event.data.some(({ field }) => field === 'kibana.alert.rule.uuid')) {
|
||||
return true;
|
||||
}
|
||||
return !isEmpty(event.ecs.signal?.rule?.id ?? event.ecs.kibana?.alert?.rule?.uuid);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}, [event]);
|
||||
|
||||
const userCanCrud = casePermissions?.crud ?? false;
|
||||
const isDisabled = !userCanCrud || !isEventSupported;
|
||||
|
||||
const onViewCaseClick = useCallback(
|
||||
(id) => {
|
||||
navigateToApp(appId, {
|
||||
deepLinkId: CasesDeepLinkId.cases,
|
||||
path: generateCaseViewPath({ detailName: id }),
|
||||
});
|
||||
},
|
||||
[navigateToApp, appId]
|
||||
);
|
||||
|
||||
const onCaseCreated = useCallback(async () => {
|
||||
dispatch(tGridActions.setOpenAddToNewCase({ id: eventId, isOpen: false }));
|
||||
}, [eventId, dispatch]);
|
||||
|
||||
const onCaseSuccess = useCallback(
|
||||
async (theCase: Case) => {
|
||||
dispatch(tGridActions.setOpenAddToExistingCase({ id: eventId, isOpen: false }));
|
||||
createUpdateSuccessToaster(toasts, theCase, onViewCaseClick);
|
||||
},
|
||||
[onViewCaseClick, toasts, dispatch, eventId]
|
||||
);
|
||||
const caseAttachments: CaseAttachments = useMemo(() => {
|
||||
const eventIndex = event?.ecs._index ?? '';
|
||||
const { ruleId, ruleName } = normalizedEventFields(event);
|
||||
const attachments = [
|
||||
{
|
||||
alertId: eventId,
|
||||
index: eventIndex ?? '',
|
||||
rule: {
|
||||
id: ruleId,
|
||||
name: ruleName,
|
||||
},
|
||||
owner,
|
||||
type: CommentType.alert as const,
|
||||
},
|
||||
];
|
||||
return attachments;
|
||||
}, [event, eventId, owner]);
|
||||
|
||||
const onCaseClicked = useCallback(
|
||||
(theCase?: Case) => {
|
||||
/**
|
||||
* No cases listed on the table.
|
||||
* The user pressed the add new case table's button.
|
||||
* We gonna open the create case modal.
|
||||
*/
|
||||
if (theCase == null) {
|
||||
dispatch(tGridActions.setOpenAddToNewCase({ id: eventId, isOpen: true }));
|
||||
}
|
||||
dispatch(tGridActions.setOpenAddToExistingCase({ id: eventId, isOpen: false }));
|
||||
},
|
||||
[dispatch, eventId]
|
||||
);
|
||||
const addNewCaseClick = useCallback(() => {
|
||||
closePopover();
|
||||
dispatch(tGridActions.setOpenAddToNewCase({ id: eventId, isOpen: true }));
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
}, [onClose, closePopover, dispatch, eventId]);
|
||||
|
||||
const addExistingCaseClick = useCallback(() => {
|
||||
closePopover();
|
||||
dispatch(tGridActions.setOpenAddToExistingCase({ id: eventId, isOpen: true }));
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
}, [onClose, closePopover, dispatch, eventId]);
|
||||
return {
|
||||
caseAttachments,
|
||||
addNewCaseClick,
|
||||
addExistingCaseClick,
|
||||
onCaseClicked,
|
||||
onCaseSuccess,
|
||||
onCaseCreated,
|
||||
isAllCaseModalOpen,
|
||||
isDisabled,
|
||||
userCanCrud,
|
||||
isEventSupported,
|
||||
openPopover,
|
||||
closePopover,
|
||||
isPopoverOpen,
|
||||
isCreateCaseFlyoutOpen,
|
||||
};
|
||||
};
|
||||
|
||||
export function normalizedEventFields(event?: TimelineItem) {
|
||||
const ruleUuidData = event && event.data.find(({ field }) => field === ALERT_RULE_UUID);
|
||||
const ruleNameData = event && event.data.find(({ field }) => field === ALERT_RULE_NAME);
|
||||
const ruleUuidValueData = ruleUuidData && ruleUuidData.value && ruleUuidData.value[0];
|
||||
const ruleNameValueData = ruleNameData && ruleNameData.value && ruleNameData.value[0];
|
||||
|
||||
const ruleUuid =
|
||||
ruleUuidValueData ??
|
||||
get(`ecs.${ALERT_RULE_UUID}[0]`, event) ??
|
||||
get(`ecs.signal.rule.id[0]`, event) ??
|
||||
null;
|
||||
const ruleName =
|
||||
ruleNameValueData ??
|
||||
get(`ecs.${ALERT_RULE_NAME}[0]`, event) ??
|
||||
get(`ecs.signal.rule.name[0]`, event) ??
|
||||
null;
|
||||
|
||||
return {
|
||||
ruleId: ruleUuid,
|
||||
ruleName,
|
||||
};
|
||||
}
|
|
@ -7,14 +7,11 @@
|
|||
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import type { Store } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
import type { Storage } from '../../../../../src/plugins/kibana_utils/public';
|
||||
import type { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
|
||||
import type { TGridProps } from '../types';
|
||||
import type { LastUpdatedAtProps, LoadingPanelProps, FieldBrowserProps } from '../components';
|
||||
import type { AddToCaseActionProps } from '../components/actions/timeline/cases/add_to_case_action';
|
||||
import { initialTGridState } from '../store/t_grid/reducer';
|
||||
import { createStore } from '../store/t_grid';
|
||||
import { TGridLoading } from '../components/t_grid/shared';
|
||||
|
@ -84,76 +81,3 @@ export const getFieldsBrowserLazy = (props: FieldBrowserProps, { store }: { stor
|
|||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
const AddToCaseLazy = lazy(() => import('../components/actions/timeline/cases/add_to_case_action'));
|
||||
export const getAddToCaseLazy = (
|
||||
props: AddToCaseActionProps,
|
||||
{ store, storage, setStore }: { store: Store; storage: Storage; setStore: (store: Store) => void }
|
||||
) => {
|
||||
return (
|
||||
<Suspense fallback={<span />}>
|
||||
<Provider store={store}>
|
||||
<I18nProvider>
|
||||
<AddToCaseLazy {...props} />
|
||||
</I18nProvider>
|
||||
</Provider>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
const AddToCasePopover = lazy(
|
||||
() => import('../components/actions/timeline/cases/add_to_case_action_button')
|
||||
);
|
||||
export const getAddToCasePopoverLazy = (
|
||||
props: AddToCaseActionProps,
|
||||
{ store, storage, setStore }: { store: Store; storage: Storage; setStore: (store: Store) => void }
|
||||
) => {
|
||||
initializeStore({ store, storage, setStore });
|
||||
return (
|
||||
<Suspense fallback={<span />}>
|
||||
<Provider store={store}>
|
||||
<I18nProvider>
|
||||
<AddToCasePopover {...props} />
|
||||
</I18nProvider>
|
||||
</Provider>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
const AddToExistingButton = lazy(
|
||||
() => import('../components/actions/timeline/cases/add_to_existing_case_button')
|
||||
);
|
||||
export const getAddToExistingCaseButtonLazy = (
|
||||
props: AddToCaseActionProps,
|
||||
{ store, storage, setStore }: { store: Store; storage: Storage; setStore: (store: Store) => void }
|
||||
) => {
|
||||
initializeStore({ store, storage, setStore });
|
||||
return (
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<Provider store={store}>
|
||||
<I18nProvider>
|
||||
<AddToExistingButton {...props} />
|
||||
</I18nProvider>
|
||||
</Provider>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
const AddToNewCaseButton = lazy(
|
||||
() => import('../components/actions/timeline/cases/add_to_new_case_button')
|
||||
);
|
||||
export const getAddToNewCaseButtonLazy = (
|
||||
props: AddToCaseActionProps,
|
||||
{ store, storage, setStore }: { store: Store; storage: Storage; setStore: (store: Store) => void }
|
||||
) => {
|
||||
initializeStore({ store, storage, setStore });
|
||||
return (
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<Provider store={store}>
|
||||
<I18nProvider>
|
||||
<AddToNewCaseButton {...props} />
|
||||
</I18nProvider>
|
||||
</Provider>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -34,8 +34,6 @@ export const mockGlobalState: TimelineState = {
|
|||
'packetbeat-*',
|
||||
'winlogbeat-*',
|
||||
],
|
||||
isAddToExistingCaseOpen: false,
|
||||
isCreateNewCaseOpen: false,
|
||||
isLoading: false,
|
||||
isSelectAllChecked: false,
|
||||
itemsPerPage: 5,
|
||||
|
|
|
@ -1566,8 +1566,6 @@ export const mockTgridModel: TGridModel = {
|
|||
selectAll: false,
|
||||
id: 'ef579e40-jibber-jabber',
|
||||
indexNames: [],
|
||||
isAddToExistingCaseOpen: false,
|
||||
isCreateNewCaseOpen: false,
|
||||
isLoading: false,
|
||||
isSelectAllChecked: false,
|
||||
kqlQuery: {
|
||||
|
|
|
@ -24,6 +24,4 @@ export const createTGridMocks = () => ({
|
|||
getUseAddToTimeline: () => useAddToTimeline,
|
||||
getUseAddToTimelineSensor: () => useAddToTimelineSensor,
|
||||
getUseDraggableKeyboardWrapper: () => useDraggableKeyboardWrapper,
|
||||
getAddToExistingCaseButton: () => <div data-test-subj="add-to-existing-case" />,
|
||||
getAddToNewCaseButton: () => <div data-test-subj="add-to-new-case" />,
|
||||
});
|
||||
|
|
|
@ -16,10 +16,6 @@ import {
|
|||
getLoadingPanelLazy,
|
||||
getTGridLazy,
|
||||
getFieldsBrowserLazy,
|
||||
getAddToCaseLazy,
|
||||
getAddToExistingCaseButtonLazy,
|
||||
getAddToNewCaseButtonLazy,
|
||||
getAddToCasePopoverLazy,
|
||||
} from './methods';
|
||||
import type { TimelinesUIStart, TGridProps, TimelinesStartPlugins } from './types';
|
||||
import { tGridReducer } from './store/t_grid/reducer';
|
||||
|
@ -88,38 +84,6 @@ export class TimelinesPlugin implements Plugin<void, TimelinesUIStart> {
|
|||
setTGridEmbeddedStore: (store: Store) => {
|
||||
this.setStore(store);
|
||||
},
|
||||
getAddToCaseAction: (props) => {
|
||||
return getAddToCaseLazy(props, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
store: this._store!,
|
||||
storage: this._storage,
|
||||
setStore: this.setStore.bind(this),
|
||||
});
|
||||
},
|
||||
getAddToCasePopover: (props) => {
|
||||
return getAddToCasePopoverLazy(props, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
store: this._store!,
|
||||
storage: this._storage,
|
||||
setStore: this.setStore.bind(this),
|
||||
});
|
||||
},
|
||||
getAddToExistingCaseButton: (props) => {
|
||||
return getAddToExistingCaseButtonLazy(props, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
store: this._store!,
|
||||
storage: this._storage,
|
||||
setStore: this.setStore.bind(this),
|
||||
});
|
||||
},
|
||||
getAddToNewCaseButton: (props) => {
|
||||
return getAddToNewCaseButtonLazy(props, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
store: this._store!,
|
||||
storage: this._storage,
|
||||
setStore: this.setStore.bind(this),
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -117,11 +117,3 @@ export const setTimelineUpdatedAt =
|
|||
export const addProviderToTimeline = actionCreator<{ id: string; dataProvider: DataProvider }>(
|
||||
'ADD_PROVIDER_TO_TIMELINE'
|
||||
);
|
||||
|
||||
export const setOpenAddToExistingCase = actionCreator<{ id: string; isOpen: boolean }>(
|
||||
'SET_OPEN_ADD_TO_EXISTING_CASE'
|
||||
);
|
||||
|
||||
export const setOpenAddToNewCase = actionCreator<{ id: string; isOpen: boolean }>(
|
||||
'SET_OPEN_ADD_TO_NEW_CASE'
|
||||
);
|
||||
|
|
|
@ -66,8 +66,6 @@ export interface TGridModel extends TGridModelSettings {
|
|||
/** Uniquely identifies the timeline */
|
||||
id: string;
|
||||
indexNames: string[];
|
||||
isAddToExistingCaseOpen: boolean;
|
||||
isCreateNewCaseOpen: boolean;
|
||||
isLoading: boolean;
|
||||
/** If selectAll checkbox in header is checked **/
|
||||
isSelectAllChecked: boolean;
|
||||
|
|
|
@ -18,8 +18,6 @@ import {
|
|||
setEventsDeleted,
|
||||
setEventsLoading,
|
||||
setTGridSelectAll,
|
||||
setOpenAddToExistingCase,
|
||||
setOpenAddToNewCase,
|
||||
setSelected,
|
||||
setTimelineUpdatedAt,
|
||||
toggleDetailPanel,
|
||||
|
@ -239,26 +237,6 @@ export const tGridReducer = reducerWithInitialState(initialTGridState)
|
|||
...state,
|
||||
timelineById: addProviderToTimelineHelper(id, dataProvider, state.timelineById),
|
||||
}))
|
||||
.case(setOpenAddToExistingCase, (state, { id, isOpen }) => ({
|
||||
...state,
|
||||
timelineById: {
|
||||
...state.timelineById,
|
||||
[id]: {
|
||||
...state.timelineById[id],
|
||||
isAddToExistingCaseOpen: isOpen,
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(setOpenAddToNewCase, (state, { id, isOpen }) => ({
|
||||
...state,
|
||||
timelineById: {
|
||||
...state.timelineById,
|
||||
[id]: {
|
||||
...state.timelineById[id],
|
||||
isCreateNewCaseOpen: isOpen,
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(setTimelineUpdatedAt, (state, { id, updated }) => ({
|
||||
...state,
|
||||
timelineById: {
|
||||
|
|
|
@ -6,26 +6,11 @@
|
|||
*/
|
||||
import { getOr } from 'lodash/fp';
|
||||
import { createSelector } from 'reselect';
|
||||
import { TGridModel, State } from '.';
|
||||
import { TGridModel } from '.';
|
||||
import { tGridDefaults, getTGridManageDefaults } from './defaults';
|
||||
|
||||
interface TGridById {
|
||||
[id: string]: TGridModel;
|
||||
}
|
||||
|
||||
const getDefaultTgrid = (id: string) => ({ ...tGridDefaults, ...getTGridManageDefaults(id) });
|
||||
|
||||
const standaloneTGridById = (state: State): TGridById => state.timelineById;
|
||||
|
||||
export const activeCaseFlowId = createSelector(standaloneTGridById, (tGrid) => {
|
||||
return (
|
||||
tGrid &&
|
||||
Object.entries(tGrid)
|
||||
.map(([id, data]) => (data.isAddToExistingCaseOpen || data.isCreateNewCaseOpen ? id : null))
|
||||
.find((id) => id)
|
||||
);
|
||||
});
|
||||
|
||||
export const selectTGridById = (state: unknown, timelineId: string): TGridModel => {
|
||||
return getOr(
|
||||
getOr(getDefaultTgrid(timelineId), ['timelineById', timelineId], state),
|
||||
|
|
|
@ -23,7 +23,6 @@ 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';
|
||||
import { TimelineTabs } from '../common/types';
|
||||
export * from './store/t_grid';
|
||||
export interface TimelinesUIStart {
|
||||
|
@ -42,10 +41,6 @@ export interface TimelinesUIStart {
|
|||
props: UseDraggableKeyboardWrapperProps
|
||||
) => UseDraggableKeyboardWrapper;
|
||||
setTGridEmbeddedStore: (store: Store) => void;
|
||||
getAddToCaseAction: (props: AddToCaseActionProps) => ReactElement<AddToCaseActionProps>;
|
||||
getAddToCasePopover: (props: AddToCaseActionProps) => ReactElement<AddToCaseActionProps>;
|
||||
getAddToExistingCaseButton: (props: AddToCaseActionProps) => ReactElement<AddToCaseActionProps>;
|
||||
getAddToNewCaseButton: (props: AddToCaseActionProps) => ReactElement<AddToCaseActionProps>;
|
||||
}
|
||||
|
||||
export interface TimelinesStartPlugins {
|
||||
|
|
|
@ -27003,16 +27003,6 @@
|
|||
"xpack.timelines.alerts.summaryView.options.summaryView.description": "各アラートのイベントフローのレンダリングを表示",
|
||||
"xpack.timelines.beatFields.errorSearchDescription": "Beatフィールドの取得でエラーが発生しました",
|
||||
"xpack.timelines.beatFields.failSearchDescription": "Beat フィールドで検索を実行できませんでした",
|
||||
"xpack.timelines.cases.timeline.actions.addCase": "ケースに追加",
|
||||
"xpack.timelines.cases.timeline.actions.addExistingCase": "既存のケースに追加",
|
||||
"xpack.timelines.cases.timeline.actions.addNewCase": "新しいケースに追加",
|
||||
"xpack.timelines.cases.timeline.actions.addToCaseAriaLabel": "アラートをケースに関連付ける",
|
||||
"xpack.timelines.cases.timeline.actions.addToCaseTooltip": "ケースに追加",
|
||||
"xpack.timelines.cases.timeline.actions.caseCreatedSuccessToast": "アラートが「{title}」に追加されました",
|
||||
"xpack.timelines.cases.timeline.actions.caseCreatedSuccessToastText": "このケースのアラートはステータスがケースステータスと同期されました",
|
||||
"xpack.timelines.cases.timeline.actions.caseCreatedSuccessToastViewCaseLink": "ケースの表示",
|
||||
"xpack.timelines.cases.timeline.actions.permissionsMessage": "現在、アラートをケースに関連付けるための必要な権限がありません。サポートについては、管理者にお問い合わせください。",
|
||||
"xpack.timelines.cases.timeline.actions.unsupportedEventsMessage": "このイベントはケースに関連付けられません",
|
||||
"xpack.timelines.clipboard.copied": "コピー完了",
|
||||
"xpack.timelines.clipboard.copy": "コピー",
|
||||
"xpack.timelines.clipboard.copy.successToastTitle": "フィールド{field}をクリップボードにコピーしました",
|
||||
|
|
|
@ -27035,16 +27035,6 @@
|
|||
"xpack.timelines.alerts.summaryView.options.summaryView.description": "查看每个告警的事件渲染",
|
||||
"xpack.timelines.beatFields.errorSearchDescription": "获取 Beat 字段时发生错误",
|
||||
"xpack.timelines.beatFields.failSearchDescription": "无法对 Beat 字段执行搜索",
|
||||
"xpack.timelines.cases.timeline.actions.addCase": "添加到案例",
|
||||
"xpack.timelines.cases.timeline.actions.addExistingCase": "添加到现有案例",
|
||||
"xpack.timelines.cases.timeline.actions.addNewCase": "添加到新案例",
|
||||
"xpack.timelines.cases.timeline.actions.addToCaseAriaLabel": "将告警附加到案例",
|
||||
"xpack.timelines.cases.timeline.actions.addToCaseTooltip": "添加到案例",
|
||||
"xpack.timelines.cases.timeline.actions.caseCreatedSuccessToast": "告警已添加到“{title}”",
|
||||
"xpack.timelines.cases.timeline.actions.caseCreatedSuccessToastText": "此案例中的告警的状态已经与案例状态同步",
|
||||
"xpack.timelines.cases.timeline.actions.caseCreatedSuccessToastViewCaseLink": "查看案例",
|
||||
"xpack.timelines.cases.timeline.actions.permissionsMessage": "您当前缺少所需的权限,无法向案例附加告警。有关进一步帮助,请联系您的管理员。",
|
||||
"xpack.timelines.cases.timeline.actions.unsupportedEventsMessage": "此事件无法附加到案例",
|
||||
"xpack.timelines.clipboard.copied": "已复制",
|
||||
"xpack.timelines.clipboard.copy": "复制",
|
||||
"xpack.timelines.clipboard.copy.successToastTitle": "已将字段 {field} 复制到剪贴板",
|
||||
|
|
|
@ -65,13 +65,7 @@ const AppRoot = React.memo(
|
|||
{(timelinesPluginSetup &&
|
||||
timelinesPluginSetup.getTGrid &&
|
||||
timelinesPluginSetup.getTGrid<'standalone'>({
|
||||
appId: 'securitySolution',
|
||||
casesOwner: 'securitySolutionUI',
|
||||
type: 'standalone',
|
||||
casePermissions: {
|
||||
read: true,
|
||||
crud: true,
|
||||
},
|
||||
columns: [],
|
||||
indexNames: [],
|
||||
deletedEventIds: [],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue