mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Cases] Add to new and existing cases bulk actions in the timelines and security_solution (#130958)
Co-authored-by: mgiota <panagiota.mitsopoulou@elastic.co>
This commit is contained in:
parent
59c55a4434
commit
d638b188dc
38 changed files with 549 additions and 217 deletions
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { CommentRequestAlertType } from '../../../common/api';
|
||||
import { CommentType, Ecs } from '../../../common';
|
||||
import { getRuleIdFromEvent } from './get_rule_id_from_event';
|
||||
import { CaseAttachments } from '../../types';
|
||||
|
||||
type Maybe<T> = T | null;
|
||||
interface Event {
|
||||
data: EventNonEcsData[];
|
||||
ecs: Ecs;
|
||||
}
|
||||
interface EventNonEcsData {
|
||||
field: string;
|
||||
value?: Maybe<string[]>;
|
||||
}
|
||||
|
||||
export const groupAlertsByRule = (items: Event[], owner: string): CaseAttachments => {
|
||||
const attachmentsByRule = items.reduce<Record<string, CommentRequestAlertType>>((acc, item) => {
|
||||
const rule = getRuleIdFromEvent(item);
|
||||
if (!acc[rule.id]) {
|
||||
acc[rule.id] = {
|
||||
alertId: [],
|
||||
index: [],
|
||||
owner,
|
||||
type: CommentType.alert as const,
|
||||
rule,
|
||||
};
|
||||
}
|
||||
const alerts = acc[rule.id].alertId;
|
||||
const indexes = acc[rule.id].index;
|
||||
if (Array.isArray(alerts) && Array.isArray(indexes)) {
|
||||
alerts.push(item.ecs._id ?? '');
|
||||
indexes.push(item.ecs._index ?? '');
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return Object.values(attachmentsByRule);
|
||||
};
|
|
@ -264,10 +264,10 @@ export const CASE_SUCCESS_TOAST = (title: string) =>
|
|||
defaultMessage: '{title} has been updated',
|
||||
});
|
||||
|
||||
export const CASE_ALERT_SUCCESS_TOAST = (title: string) =>
|
||||
export const CASE_ALERT_SUCCESS_TOAST = (title: string, quantity: number = 1) =>
|
||||
i18n.translate('xpack.cases.actions.caseAlertSuccessToast', {
|
||||
values: { title },
|
||||
defaultMessage: 'An alert was added to "{title}"',
|
||||
values: { quantity, title },
|
||||
defaultMessage: '{quantity, plural, =1 {An alert was} other {Alerts were}} added to "{title}"',
|
||||
});
|
||||
|
||||
export const CASE_ALERT_SUCCESS_SYNC_TEXT = i18n.translate(
|
||||
|
|
|
@ -72,7 +72,7 @@ describe('Use cases toast hook', () => {
|
|||
validateTitle('Custom title');
|
||||
});
|
||||
|
||||
it('should display the alert sync title when called with an alert attachment ', () => {
|
||||
it('should display the alert sync title when called with an alert attachment (1 alert)', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
return useCasesToast();
|
||||
|
@ -86,6 +86,25 @@ describe('Use cases toast hook', () => {
|
|||
validateTitle('An alert was added to "Another horrible breach!!');
|
||||
});
|
||||
|
||||
it('should display the alert sync title when called with an alert attachment (multiple alerts)', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
return useCasesToast();
|
||||
},
|
||||
{ wrapper: TestProviders }
|
||||
);
|
||||
const alert = {
|
||||
...alertComment,
|
||||
alertId: ['1234', '54321'],
|
||||
} as SupportedCaseAttachment;
|
||||
|
||||
result.current.showSuccessAttach({
|
||||
theCase: mockCase,
|
||||
attachments: [alert],
|
||||
});
|
||||
validateTitle('Alerts were added to "Another horrible breach!!');
|
||||
});
|
||||
|
||||
it('should display a generic title when called with a non-alert attachament', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
|
|
|
@ -34,6 +34,22 @@ const EuiTextStyled = styled(EuiText)`
|
|||
`}
|
||||
`;
|
||||
|
||||
function getAlertsCount(attachments: CaseAttachments): number {
|
||||
let alertsCount = 0;
|
||||
for (const attachment of attachments) {
|
||||
if (attachment.type === CommentType.alert) {
|
||||
// alertId might be an array
|
||||
if (Array.isArray(attachment.alertId) && attachment.alertId.length > 1) {
|
||||
alertsCount += attachment.alertId.length;
|
||||
} else {
|
||||
// or might be a single string
|
||||
alertsCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return alertsCount;
|
||||
}
|
||||
|
||||
function getToastTitle({
|
||||
theCase,
|
||||
title,
|
||||
|
@ -47,10 +63,9 @@ function getToastTitle({
|
|||
return title;
|
||||
}
|
||||
if (attachments !== undefined) {
|
||||
for (const attachment of attachments) {
|
||||
if (attachment.type === CommentType.alert) {
|
||||
return CASE_ALERT_SUCCESS_TOAST(theCase.title);
|
||||
}
|
||||
const alertsCount = getAlertsCount(attachments);
|
||||
if (alertsCount > 0) {
|
||||
return CASE_ALERT_SUCCESS_TOAST(theCase.title, alertsCount);
|
||||
}
|
||||
}
|
||||
return CASE_SUCCESS_TOAST(theCase.title);
|
||||
|
|
|
@ -15,7 +15,6 @@ import { AppMockRenderer, createAppMockRenderer } from '../../../common/mock';
|
|||
import { useCasesToast } from '../../../common/use_cases_toast';
|
||||
import { alertComment } from '../../../containers/mock';
|
||||
import { useCreateAttachments } from '../../../containers/use_create_attachments';
|
||||
import { SupportedCaseAttachment } from '../../../types';
|
||||
import { CasesContext } from '../../cases_context';
|
||||
import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer';
|
||||
import { useCasesAddToExistingCaseModal } from './use_cases_add_to_existing_case_modal';
|
||||
|
@ -35,12 +34,10 @@ const AllCasesSelectorModalMock = AllCasesSelectorModal as unknown as jest.Mock;
|
|||
|
||||
// test component to test the hook integration
|
||||
const TestComponent: React.FC = () => {
|
||||
const hook = useCasesAddToExistingCaseModal({
|
||||
attachments: [alertComment as SupportedCaseAttachment],
|
||||
});
|
||||
const hook = useCasesAddToExistingCaseModal();
|
||||
|
||||
const onClick = () => {
|
||||
hook.open();
|
||||
hook.open({ attachments: [alertComment] });
|
||||
};
|
||||
|
||||
return <button type="button" data-test-subj="open-modal" onClick={onClick} />;
|
||||
|
|
|
@ -19,12 +19,10 @@ import { useCreateAttachments } from '../../../containers/use_create_attachments
|
|||
type AddToExistingFlyoutProps = AllCasesSelectorModalProps & {
|
||||
toastTitle?: string;
|
||||
toastContent?: string;
|
||||
attachments?: CaseAttachments;
|
||||
};
|
||||
|
||||
export const useCasesAddToExistingCaseModal = (props: AddToExistingFlyoutProps) => {
|
||||
export const useCasesAddToExistingCaseModal = (props: AddToExistingFlyoutProps = {}) => {
|
||||
const createNewCaseFlyout = useCasesAddToNewCaseFlyout({
|
||||
attachments: props.attachments,
|
||||
onClose: props.onClose,
|
||||
// TODO there's no need for onSuccess to be async. This will be fixed
|
||||
// in a follow up clean up
|
||||
|
@ -53,18 +51,17 @@ export const useCasesAddToExistingCaseModal = (props: AddToExistingFlyoutProps)
|
|||
}, [dispatch]);
|
||||
|
||||
const handleOnRowClick = useCallback(
|
||||
async (theCase?: Case) => {
|
||||
async (theCase: Case | undefined, attachments: CaseAttachments) => {
|
||||
// when the case is undefined in the modal
|
||||
// the user clicked "create new case"
|
||||
if (theCase === undefined) {
|
||||
closeModal();
|
||||
createNewCaseFlyout.open();
|
||||
createNewCaseFlyout.open({ attachments });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// add attachments to the case
|
||||
const attachments = props.attachments;
|
||||
if (attachments !== undefined && attachments.length > 0) {
|
||||
await createAttachments({
|
||||
caseId: theCase.id,
|
||||
|
@ -74,7 +71,7 @@ export const useCasesAddToExistingCaseModal = (props: AddToExistingFlyoutProps)
|
|||
|
||||
casesToasts.showSuccessAttach({
|
||||
theCase,
|
||||
attachments: props.attachments,
|
||||
attachments,
|
||||
title: props.toastTitle,
|
||||
content: props.toastContent,
|
||||
});
|
||||
|
@ -91,22 +88,28 @@ export const useCasesAddToExistingCaseModal = (props: AddToExistingFlyoutProps)
|
|||
[casesToasts, closeModal, createNewCaseFlyout, createAttachments, props]
|
||||
);
|
||||
|
||||
const openModal = useCallback(() => {
|
||||
dispatch({
|
||||
type: CasesContextStoreActionsList.OPEN_ADD_TO_CASE_MODAL,
|
||||
payload: {
|
||||
...props,
|
||||
hiddenStatuses: [CaseStatuses.closed, StatusAll],
|
||||
onRowClick: handleOnRowClick,
|
||||
onClose: () => {
|
||||
closeModal();
|
||||
if (props.onClose) {
|
||||
return props.onClose();
|
||||
}
|
||||
const openModal = useCallback(
|
||||
({ attachments }: { attachments?: CaseAttachments } = {}) => {
|
||||
dispatch({
|
||||
type: CasesContextStoreActionsList.OPEN_ADD_TO_CASE_MODAL,
|
||||
payload: {
|
||||
...props,
|
||||
hiddenStatuses: [CaseStatuses.closed, StatusAll],
|
||||
onRowClick: (theCase?: Case) => {
|
||||
const caseAttachments = attachments ?? [];
|
||||
handleOnRowClick(theCase, caseAttachments);
|
||||
},
|
||||
onClose: () => {
|
||||
closeModal();
|
||||
if (props.onClose) {
|
||||
return props.onClose();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [closeModal, dispatch, handleOnRowClick, props]);
|
||||
});
|
||||
},
|
||||
[closeModal, dispatch, handleOnRowClick, props]
|
||||
);
|
||||
return {
|
||||
open: openModal,
|
||||
close: closeModal,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
import { alertComment } from '../../../containers/mock';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import React from 'react';
|
||||
import { CasesContext } from '../../cases_context';
|
||||
|
@ -41,17 +42,17 @@ describe('use cases add to new case flyout hook', () => {
|
|||
|
||||
it('should throw if called outside of a cases context', () => {
|
||||
const { result } = renderHook(() => {
|
||||
useCasesAddToNewCaseFlyout({});
|
||||
useCasesAddToNewCaseFlyout();
|
||||
});
|
||||
expect(result.error?.message).toContain(
|
||||
'useCasesContext must be used within a CasesProvider and have a defined value'
|
||||
);
|
||||
});
|
||||
|
||||
it('should dispatch the open action when invoked', () => {
|
||||
it('should dispatch the open action when invoked without attachments', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
return useCasesAddToNewCaseFlyout({});
|
||||
return useCasesAddToNewCaseFlyout();
|
||||
},
|
||||
{ wrapper }
|
||||
);
|
||||
|
@ -59,6 +60,27 @@ describe('use cases add to new case flyout hook', () => {
|
|||
expect(dispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: CasesContextStoreActionsList.OPEN_CREATE_CASE_FLYOUT,
|
||||
payload: expect.objectContaining({
|
||||
attachments: undefined,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should dispatch the open action when invoked with attachments', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
return useCasesAddToNewCaseFlyout();
|
||||
},
|
||||
{ wrapper }
|
||||
);
|
||||
result.current.open({ attachments: [alertComment] });
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: CasesContextStoreActionsList.OPEN_CREATE_CASE_FLYOUT,
|
||||
payload: expect.objectContaining({
|
||||
attachments: [alertComment],
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -66,7 +88,7 @@ describe('use cases add to new case flyout hook', () => {
|
|||
it('should dispatch the close action when invoked', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
return useCasesAddToNewCaseFlyout({});
|
||||
return useCasesAddToNewCaseFlyout();
|
||||
},
|
||||
{ wrapper }
|
||||
);
|
||||
|
|
|
@ -6,18 +6,19 @@
|
|||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { CaseAttachments } from '../../../types';
|
||||
import { useCasesToast } from '../../../common/use_cases_toast';
|
||||
import { Case } from '../../../containers/types';
|
||||
import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer';
|
||||
import { useCasesContext } from '../../cases_context/use_cases_context';
|
||||
import { CreateCaseFlyoutProps } from './create_case_flyout';
|
||||
|
||||
type AddToNewCaseFlyoutProps = CreateCaseFlyoutProps & {
|
||||
type AddToNewCaseFlyoutProps = Omit<CreateCaseFlyoutProps, 'attachments'> & {
|
||||
toastTitle?: string;
|
||||
toastContent?: string;
|
||||
};
|
||||
|
||||
export const useCasesAddToNewCaseFlyout = (props: AddToNewCaseFlyoutProps) => {
|
||||
export const useCasesAddToNewCaseFlyout = (props: AddToNewCaseFlyoutProps = {}) => {
|
||||
const { dispatch } = useCasesContext();
|
||||
const casesToasts = useCasesToast();
|
||||
|
||||
|
@ -27,39 +28,43 @@ export const useCasesAddToNewCaseFlyout = (props: AddToNewCaseFlyoutProps) => {
|
|||
});
|
||||
}, [dispatch]);
|
||||
|
||||
const openFlyout = useCallback(() => {
|
||||
dispatch({
|
||||
type: CasesContextStoreActionsList.OPEN_CREATE_CASE_FLYOUT,
|
||||
payload: {
|
||||
...props,
|
||||
onClose: () => {
|
||||
closeFlyout();
|
||||
if (props.onClose) {
|
||||
return props.onClose();
|
||||
}
|
||||
const openFlyout = useCallback(
|
||||
({ attachments }: { attachments?: CaseAttachments } = {}) => {
|
||||
dispatch({
|
||||
type: CasesContextStoreActionsList.OPEN_CREATE_CASE_FLYOUT,
|
||||
payload: {
|
||||
...props,
|
||||
attachments,
|
||||
onClose: () => {
|
||||
closeFlyout();
|
||||
if (props.onClose) {
|
||||
return props.onClose();
|
||||
}
|
||||
},
|
||||
onSuccess: async (theCase: Case) => {
|
||||
if (theCase) {
|
||||
casesToasts.showSuccessAttach({
|
||||
theCase,
|
||||
attachments: attachments ?? [],
|
||||
title: props.toastTitle,
|
||||
content: props.toastContent,
|
||||
});
|
||||
}
|
||||
if (props.onSuccess) {
|
||||
return props.onSuccess(theCase);
|
||||
}
|
||||
},
|
||||
afterCaseCreated: async (...args) => {
|
||||
closeFlyout();
|
||||
if (props.afterCaseCreated) {
|
||||
return props.afterCaseCreated(...args);
|
||||
}
|
||||
},
|
||||
},
|
||||
onSuccess: async (theCase: Case) => {
|
||||
if (theCase) {
|
||||
casesToasts.showSuccessAttach({
|
||||
theCase,
|
||||
attachments: props.attachments,
|
||||
title: props.toastTitle,
|
||||
content: props.toastContent,
|
||||
});
|
||||
}
|
||||
if (props.onSuccess) {
|
||||
return props.onSuccess(theCase);
|
||||
}
|
||||
},
|
||||
afterCaseCreated: async (...args) => {
|
||||
closeFlyout();
|
||||
if (props.afterCaseCreated) {
|
||||
return props.afterCaseCreated(...args);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [casesToasts, closeFlyout, dispatch, props]);
|
||||
});
|
||||
},
|
||||
[casesToasts, closeFlyout, dispatch, props]
|
||||
);
|
||||
return {
|
||||
open: openFlyout,
|
||||
close: closeFlyout,
|
||||
|
|
|
@ -29,6 +29,7 @@ const hooksMock: jest.Mocked<CasesUiStart['hooks']> = {
|
|||
const helpersMock: jest.Mocked<CasesUiStart['helpers']> = {
|
||||
canUseCases: jest.fn(),
|
||||
getRuleIdFromEvent: jest.fn(),
|
||||
groupAlertsByRule: jest.fn(),
|
||||
};
|
||||
|
||||
export interface CaseUiClientMock {
|
||||
|
|
|
@ -23,6 +23,7 @@ import { getCasesLazy } from './client/ui/get_cases';
|
|||
import { getCasesContextLazy } from './client/ui/get_cases_context';
|
||||
import { getCreateCaseFlyoutLazy } from './client/ui/get_create_case_flyout';
|
||||
import { getRecentCasesLazy } from './client/ui/get_recent_cases';
|
||||
import { groupAlertsByRule } from './client/helpers/group_alerts_by_rule';
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -102,6 +103,7 @@ export class CasesUiPlugin
|
|||
helpers: {
|
||||
canUseCases: canUseCases(core.application.capabilities),
|
||||
getRuleIdFromEvent,
|
||||
groupAlertsByRule,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import { GetAllCasesSelectorModalProps } from './client/ui/get_all_cases_selecto
|
|||
import { GetCreateCaseFlyoutProps } from './client/ui/get_create_case_flyout';
|
||||
import { GetRecentCasesProps } from './client/ui/get_recent_cases';
|
||||
import { Cases, CasesStatus, CasesMetrics } from '../common/ui';
|
||||
import { groupAlertsByRule } from './client/helpers/group_alerts_by_rule';
|
||||
|
||||
export interface CasesPluginSetup {
|
||||
security: SecurityPluginSetup;
|
||||
|
@ -129,6 +130,7 @@ export interface CasesUiStart {
|
|||
*/
|
||||
canUseCases: (owners?: CasesOwners[]) => { crud: boolean; read: boolean };
|
||||
getRuleIdFromEvent: typeof getRuleIdFromEvent;
|
||||
groupAlertsByRule: typeof groupAlertsByRule;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import type { TimelineItem } from '@kbn/timelines-plugin/common';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import {
|
||||
ADD_TO_CASE_DISABLED,
|
||||
ADD_TO_EXISTING_CASE,
|
||||
ADD_TO_NEW_CASE,
|
||||
} from '../pages/alerts/containers/alerts_table_t_grid/translations';
|
||||
import { useGetUserCasesPermissions } from './use_get_user_cases_permissions';
|
||||
import { ObservabilityAppServices } from '../application/types';
|
||||
import { observabilityFeatureId } from '../../common';
|
||||
|
||||
export interface UseAddToCaseActions {
|
||||
onClose?: () => void;
|
||||
onSuccess?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActions = {}) => {
|
||||
const { cases: casesUi } = useKibana<ObservabilityAppServices>().services;
|
||||
|
||||
const casePermissions = useGetUserCasesPermissions();
|
||||
const hasWritePermissions = casePermissions?.crud ?? false;
|
||||
|
||||
const createCaseFlyout = casesUi.hooks.getUseCasesAddToNewCaseFlyout({
|
||||
onClose,
|
||||
onSuccess,
|
||||
});
|
||||
const selectCaseModal = casesUi.hooks.getUseCasesAddToExistingCaseModal({
|
||||
onClose,
|
||||
onRowClick: onSuccess,
|
||||
});
|
||||
|
||||
return useMemo(() => {
|
||||
return hasWritePermissions
|
||||
? [
|
||||
{
|
||||
label: ADD_TO_NEW_CASE,
|
||||
key: 'attach-new-case',
|
||||
'data-test-subj': 'attach-new-case',
|
||||
disableOnQuery: true,
|
||||
disabledLabel: ADD_TO_CASE_DISABLED,
|
||||
onClick: (items?: TimelineItem[]) => {
|
||||
const caseAttachments = items
|
||||
? casesUi.helpers.groupAlertsByRule(items, observabilityFeatureId)
|
||||
: [];
|
||||
createCaseFlyout.open({ attachments: caseAttachments });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: ADD_TO_EXISTING_CASE,
|
||||
key: 'attach-existing-case',
|
||||
disableOnQuery: true,
|
||||
disabledLabel: ADD_TO_CASE_DISABLED,
|
||||
'data-test-subj': 'attach-existing-case',
|
||||
onClick: (items?: TimelineItem[]) => {
|
||||
const caseAttachments = items
|
||||
? casesUi.helpers.groupAlertsByRule(items, observabilityFeatureId)
|
||||
: [];
|
||||
selectCaseModal.open({ attachments: caseAttachments });
|
||||
},
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}, [casesUi.helpers, createCaseFlyout, hasWritePermissions, selectCaseModal]);
|
||||
};
|
|
@ -69,6 +69,7 @@ import { translations, paths } from '../../../../config';
|
|||
import { addDisplayNames } from './add_display_names';
|
||||
import { ADD_TO_EXISTING_CASE, ADD_TO_NEW_CASE } from './translations';
|
||||
import { ObservabilityAppServices } from '../../../../application/types';
|
||||
import { useBulkAddToCaseActions } from '../../../../hooks/use_alert_bulk_case_actions';
|
||||
|
||||
interface AlertsTableTGridProps {
|
||||
indexNames: string[];
|
||||
|
@ -185,23 +186,19 @@ function ObservabilityActions({
|
|||
: [];
|
||||
}, [ecsData, cases.helpers, data]);
|
||||
|
||||
const createCaseFlyout = cases.hooks.getUseCasesAddToNewCaseFlyout({
|
||||
attachments: caseAttachments,
|
||||
});
|
||||
const createCaseFlyout = cases.hooks.getUseCasesAddToNewCaseFlyout();
|
||||
|
||||
const selectCaseModal = cases.hooks.getUseCasesAddToExistingCaseModal({
|
||||
attachments: caseAttachments,
|
||||
});
|
||||
const selectCaseModal = cases.hooks.getUseCasesAddToExistingCaseModal();
|
||||
|
||||
const handleAddToNewCaseClick = useCallback(() => {
|
||||
createCaseFlyout.open();
|
||||
createCaseFlyout.open({ attachments: caseAttachments });
|
||||
closeActionsPopover();
|
||||
}, [createCaseFlyout, closeActionsPopover]);
|
||||
}, [createCaseFlyout, caseAttachments, closeActionsPopover]);
|
||||
|
||||
const handleAddToExistingCaseClick = useCallback(() => {
|
||||
selectCaseModal.open();
|
||||
selectCaseModal.open({ attachments: caseAttachments });
|
||||
closeActionsPopover();
|
||||
}, [closeActionsPopover, selectCaseModal]);
|
||||
}, [caseAttachments, closeActionsPopover, selectCaseModal]);
|
||||
|
||||
const actionsMenuItems = useMemo(() => {
|
||||
return [
|
||||
|
@ -404,6 +401,14 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
|
|||
[tGridState]
|
||||
);
|
||||
|
||||
const addToCaseBulkActions = useBulkAddToCaseActions();
|
||||
const bulkActions = useMemo(
|
||||
() => ({
|
||||
alertStatusActions: false,
|
||||
customBulkActions: addToCaseBulkActions,
|
||||
}),
|
||||
[addToCaseBulkActions]
|
||||
);
|
||||
const tGridProps = useMemo(() => {
|
||||
const type: TGridType = 'standalone';
|
||||
const sortDirection: SortDirection = 'desc';
|
||||
|
@ -434,7 +439,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
|
|||
runtimeMappings: {},
|
||||
start: rangeFrom,
|
||||
setRefetch,
|
||||
showCheckboxes: false,
|
||||
bulkActions,
|
||||
sort: tGridState?.sort ?? [
|
||||
{
|
||||
columnId: '@timestamp',
|
||||
|
@ -460,17 +465,19 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
|
|||
};
|
||||
}, [
|
||||
casePermissions,
|
||||
tGridState?.columns,
|
||||
tGridState?.sort,
|
||||
deletedEventIds,
|
||||
rangeTo,
|
||||
hasAlertsCrudPermissions,
|
||||
indexNames,
|
||||
itemsPerPage,
|
||||
onStateChange,
|
||||
kuery,
|
||||
rangeFrom,
|
||||
setRefetch,
|
||||
bulkActions,
|
||||
leadingControlColumns,
|
||||
deletedEventIds,
|
||||
onStateChange,
|
||||
tGridState,
|
||||
itemsPerPage,
|
||||
]);
|
||||
|
||||
const handleFlyoutClose = () => setFlyoutAlert(undefined);
|
||||
|
|
|
@ -7,16 +7,17 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ADD_TO_EXISTING_CASE = i18n.translate(
|
||||
'xpack.observability.detectionEngine.alerts.actions.addToCase',
|
||||
{
|
||||
defaultMessage: 'Add to existing case',
|
||||
}
|
||||
);
|
||||
export const ADD_TO_EXISTING_CASE = i18n.translate('xpack.observability.alerts.actions.addToCase', {
|
||||
defaultMessage: 'Add to existing case',
|
||||
});
|
||||
|
||||
export const ADD_TO_NEW_CASE = i18n.translate(
|
||||
'xpack.observability.detectionEngine.alerts.actions.addToNewCase',
|
||||
export const ADD_TO_NEW_CASE = i18n.translate('xpack.observability.alerts.actions.addToNewCase', {
|
||||
defaultMessage: 'Add to new case',
|
||||
});
|
||||
|
||||
export const ADD_TO_CASE_DISABLED = i18n.translate(
|
||||
'xpack.observability.alerts.actions.addToCaseDisabled',
|
||||
{
|
||||
defaultMessage: 'Add to new case',
|
||||
defaultMessage: 'Add to case is not supported for this selection',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -11,6 +11,7 @@ import styled from 'styled-components';
|
|||
import type { Filter } from '@kbn/es-query';
|
||||
import type { EntityType } from '@kbn/timelines-plugin/common';
|
||||
import { TGridCellAction } from '@kbn/timelines-plugin/common/types';
|
||||
import { useBulkAddToCaseActions } from '../../../detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions';
|
||||
import { inputsModel, State } from '../../store';
|
||||
import { inputsActions } from '../../store/actions';
|
||||
import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline';
|
||||
|
@ -186,14 +187,21 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => {
|
||||
newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)());
|
||||
};
|
||||
const onAlertStatusActionSuccess = useCallback(() => {
|
||||
if (id === TimelineId.active) {
|
||||
refetchQuery([timelineQuery]);
|
||||
} else {
|
||||
refetchQuery(globalQueries);
|
||||
}
|
||||
}, [id, timelineQuery, globalQueries]);
|
||||
const bulkActions = useMemo(() => ({ onAlertStatusActionSuccess }), [onAlertStatusActionSuccess]);
|
||||
|
||||
const addToCaseBulkActions = useBulkAddToCaseActions();
|
||||
const bulkActions = useMemo(
|
||||
() => ({
|
||||
onAlertStatusActionSuccess: () => {
|
||||
if (id === TimelineId.active) {
|
||||
refetchQuery([timelineQuery]);
|
||||
} else {
|
||||
refetchQuery(globalQueries);
|
||||
}
|
||||
},
|
||||
customBulkActions: addToCaseBulkActions,
|
||||
}),
|
||||
[addToCaseBulkActions, globalQueries, id, timelineQuery]
|
||||
);
|
||||
|
||||
const fieldBrowserOptions = useFieldBrowserOptions({
|
||||
sourcererScope: scopeId,
|
||||
|
|
|
@ -19,8 +19,6 @@ describe('useAddToExistingCase', () => {
|
|||
from: '2022-03-06T16:00:00.000Z',
|
||||
to: '2022-03-07T15:59:59.999Z',
|
||||
};
|
||||
const owner = 'securitySolution';
|
||||
const type = 'user';
|
||||
beforeEach(() => {
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
|
@ -39,16 +37,6 @@ describe('useAddToExistingCase', () => {
|
|||
})
|
||||
);
|
||||
expect(mockCases.hooks.getUseCasesAddToExistingCaseModal).toHaveBeenCalledWith({
|
||||
attachments: [
|
||||
{
|
||||
comment: `!{lens${JSON.stringify({
|
||||
timeRange,
|
||||
attributes: kpiHostMetricLensAttributes,
|
||||
})}}`,
|
||||
owner,
|
||||
type,
|
||||
},
|
||||
],
|
||||
onClose: mockOnAddToCaseClicked,
|
||||
toastContent: 'Successfully added visualization to the case',
|
||||
});
|
||||
|
|
|
@ -41,7 +41,6 @@ export const useAddToExistingCase = ({
|
|||
}, [lensAttributes, timeRange]);
|
||||
|
||||
const selectCaseModal = cases.hooks.getUseCasesAddToExistingCaseModal({
|
||||
attachments,
|
||||
onClose: onAddToCaseClicked,
|
||||
toastContent: ADD_TO_CASE_SUCCESS,
|
||||
});
|
||||
|
@ -50,8 +49,8 @@ export const useAddToExistingCase = ({
|
|||
if (onAddToCaseClicked) {
|
||||
onAddToCaseClicked();
|
||||
}
|
||||
selectCaseModal.open();
|
||||
}, [onAddToCaseClicked, selectCaseModal]);
|
||||
selectCaseModal.open({ attachments });
|
||||
}, [attachments, onAddToCaseClicked, selectCaseModal]);
|
||||
|
||||
return {
|
||||
onAddToExistingCaseClicked,
|
||||
|
|
|
@ -18,8 +18,6 @@ describe('useAddToNewCase', () => {
|
|||
from: '2022-03-06T16:00:00.000Z',
|
||||
to: '2022-03-07T15:59:59.999Z',
|
||||
};
|
||||
const owner = 'securitySolution';
|
||||
const type = 'user';
|
||||
beforeEach(() => {
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
|
@ -37,16 +35,6 @@ describe('useAddToNewCase', () => {
|
|||
})
|
||||
);
|
||||
expect(mockCases.hooks.getUseCasesAddToNewCaseFlyout).toHaveBeenCalledWith({
|
||||
attachments: [
|
||||
{
|
||||
comment: `!{lens${JSON.stringify({
|
||||
timeRange,
|
||||
attributes: kpiHostMetricLensAttributes,
|
||||
})}}`,
|
||||
owner,
|
||||
type,
|
||||
},
|
||||
],
|
||||
toastContent: 'Successfully added visualization to the case',
|
||||
});
|
||||
expect(result.current.disabled).toEqual(false);
|
||||
|
|
|
@ -44,7 +44,6 @@ export const useAddToNewCase = ({
|
|||
}, [lensAttributes, timeRange]);
|
||||
|
||||
const createCaseFlyout = cases.hooks.getUseCasesAddToNewCaseFlyout({
|
||||
attachments,
|
||||
toastContent: ADD_TO_CASE_SUCCESS,
|
||||
});
|
||||
|
||||
|
@ -53,8 +52,8 @@ export const useAddToNewCase = ({
|
|||
onClick();
|
||||
}
|
||||
|
||||
createCaseFlyout.open();
|
||||
}, [createCaseFlyout, onClick]);
|
||||
createCaseFlyout.open({ attachments });
|
||||
}, [attachments, createCaseFlyout, onClick]);
|
||||
|
||||
return {
|
||||
onAddToNewCaseClicked,
|
||||
|
|
|
@ -52,13 +52,11 @@ export const useAddToCaseActions = ({
|
|||
}, [casesUi.helpers, ecsData, nonEcsData]);
|
||||
|
||||
const createCaseFlyout = casesUi.hooks.getUseCasesAddToNewCaseFlyout({
|
||||
attachments: caseAttachments,
|
||||
onClose: onMenuItemClick,
|
||||
onSuccess,
|
||||
});
|
||||
|
||||
const selectCaseModal = casesUi.hooks.getUseCasesAddToExistingCaseModal({
|
||||
attachments: caseAttachments,
|
||||
onClose: onMenuItemClick,
|
||||
onRowClick: onSuccess,
|
||||
});
|
||||
|
@ -66,14 +64,14 @@ export const useAddToCaseActions = ({
|
|||
const handleAddToNewCaseClick = useCallback(() => {
|
||||
// TODO rename this, this is really `closePopover()`
|
||||
onMenuItemClick();
|
||||
createCaseFlyout.open();
|
||||
}, [onMenuItemClick, createCaseFlyout]);
|
||||
createCaseFlyout.open({ attachments: caseAttachments });
|
||||
}, [onMenuItemClick, createCaseFlyout, caseAttachments]);
|
||||
|
||||
const handleAddToExistingCaseClick = useCallback(() => {
|
||||
// TODO rename this, this is really `closePopover()`
|
||||
onMenuItemClick();
|
||||
selectCaseModal.open();
|
||||
}, [onMenuItemClick, selectCaseModal]);
|
||||
selectCaseModal.open({ attachments: caseAttachments });
|
||||
}, [caseAttachments, onMenuItemClick, selectCaseModal]);
|
||||
|
||||
const addToCaseActionItems = useMemo(() => {
|
||||
if (
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useStatusBulkActionItems } from '@kbn/timelines-plugin/public';
|
||||
import { useBulkActionItems } from '@kbn/timelines-plugin/public';
|
||||
import { Status } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { timelineActions } from '../../../../timelines/store/timeline';
|
||||
import { useAlertsPrivileges } from '../../../containers/detection_engine/alerts/use_alerts_privileges';
|
||||
|
@ -54,7 +54,7 @@ export const useAlertsActions = ({
|
|||
[dispatch, timelineId]
|
||||
);
|
||||
|
||||
const actionItems = useStatusBulkActionItems({
|
||||
const actionItems = useBulkActionItems({
|
||||
eventIds: [eventId],
|
||||
currentStatus: alertStatus,
|
||||
indexName,
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import { APP_ID } from '../../../../../common/constants';
|
||||
import type { TimelineItem } from '../../../../../common/search_strategy';
|
||||
import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana';
|
||||
import { ADD_TO_CASE_DISABLED, ADD_TO_EXISTING_CASE, ADD_TO_NEW_CASE } from '../translations';
|
||||
|
||||
export interface UseAddToCaseActions {
|
||||
onClose?: () => void;
|
||||
onSuccess?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActions = {}) => {
|
||||
const { cases: casesUi } = useKibana().services;
|
||||
|
||||
const casePermissions = useGetUserCasesPermissions();
|
||||
const hasWritePermissions = casePermissions?.crud ?? false;
|
||||
|
||||
const createCaseFlyout = casesUi.hooks.getUseCasesAddToNewCaseFlyout({
|
||||
onClose,
|
||||
onSuccess,
|
||||
});
|
||||
const selectCaseModal = casesUi.hooks.getUseCasesAddToExistingCaseModal({
|
||||
onClose,
|
||||
onRowClick: onSuccess,
|
||||
});
|
||||
|
||||
return useMemo(() => {
|
||||
return hasWritePermissions
|
||||
? [
|
||||
{
|
||||
label: ADD_TO_NEW_CASE,
|
||||
key: 'attach-new-case',
|
||||
'data-test-subj': 'attach-new-case',
|
||||
disableOnQuery: true,
|
||||
disabledLabel: ADD_TO_CASE_DISABLED,
|
||||
onClick: (items?: TimelineItem[]) => {
|
||||
const caseAttachments = items ? casesUi.helpers.groupAlertsByRule(items, APP_ID) : [];
|
||||
createCaseFlyout.open({ attachments: caseAttachments });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: ADD_TO_EXISTING_CASE,
|
||||
key: 'attach-existing-case',
|
||||
disableOnQuery: true,
|
||||
disabledLabel: ADD_TO_CASE_DISABLED,
|
||||
'data-test-subj': 'attach-existing-case',
|
||||
onClick: (items?: TimelineItem[]) => {
|
||||
const caseAttachments = items ? casesUi.helpers.groupAlertsByRule(items, APP_ID) : [];
|
||||
selectCaseModal.open({ attachments: caseAttachments });
|
||||
},
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}, [casesUi.helpers, createCaseFlyout, hasWritePermissions, selectCaseModal]);
|
||||
};
|
|
@ -307,3 +307,10 @@ export const ADD_TO_NEW_CASE = i18n.translate(
|
|||
defaultMessage: 'Add to new case',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_TO_CASE_DISABLED = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.alerts.actions.addToCaseDisabled',
|
||||
{
|
||||
defaultMessage: 'Add to case is not supported for this selection',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -10,7 +10,7 @@ import { EuiDataGridControlColumn, EuiDataGridCellValueElementProps } from '@ela
|
|||
import { OnRowSelected, SortColumnTimeline, TimelineTabs } from '..';
|
||||
import { BrowserFields } from '../../../search_strategy/index_fields';
|
||||
import { ColumnHeaderOptions } from '../columns';
|
||||
import { TimelineNonEcsData } from '../../../search_strategy';
|
||||
import { TimelineItem, TimelineNonEcsData } from '../../../search_strategy';
|
||||
import { Ecs } from '../../../ecs';
|
||||
import { FieldBrowserOptions } from '../../fields_browser';
|
||||
|
||||
|
@ -53,15 +53,30 @@ export type OnUpdateAlertStatusSuccess = (
|
|||
) => void;
|
||||
export type OnUpdateAlertStatusError = (status: AlertStatus, error: Error) => void;
|
||||
|
||||
export interface StatusBulkActionsProps {
|
||||
export interface CustomBulkAction {
|
||||
key: string;
|
||||
label: string;
|
||||
disableOnQuery?: boolean;
|
||||
disabledLabel?: string;
|
||||
onClick: (items?: TimelineItem[]) => void;
|
||||
['data-test-subj']?: string;
|
||||
}
|
||||
|
||||
export type CustomBulkActionProp = Omit<CustomBulkAction, 'onClick'> & {
|
||||
onClick: (eventIds: string[]) => void;
|
||||
};
|
||||
|
||||
export interface BulkActionsProps {
|
||||
eventIds: string[];
|
||||
currentStatus?: AlertStatus;
|
||||
query?: string;
|
||||
indexName: string;
|
||||
setEventsLoading: SetEventsLoading;
|
||||
setEventsDeleted: SetEventsDeleted;
|
||||
showAlertStatusActions?: boolean;
|
||||
onUpdateSuccess?: OnUpdateAlertStatusSuccess;
|
||||
onUpdateFailure?: OnUpdateAlertStatusError;
|
||||
customBulkActions?: CustomBulkActionProp[];
|
||||
timelineId?: string;
|
||||
}
|
||||
export interface HeaderActionProps {
|
||||
|
@ -117,6 +132,7 @@ export interface BulkActionsObjectProp {
|
|||
alertStatusActions?: boolean;
|
||||
onAlertStatusActionSuccess?: OnUpdateAlertStatusSuccess;
|
||||
onAlertStatusActionFailure?: OnUpdateAlertStatusError;
|
||||
customBulkActions?: CustomBulkAction[];
|
||||
}
|
||||
export type BulkActionsProp = boolean | BulkActionsObjectProp;
|
||||
|
||||
|
|
|
@ -80,9 +80,7 @@ import { ViewSelection } from '../event_rendered_view/selector';
|
|||
import { EventRenderedView } from '../event_rendered_view';
|
||||
import { REMOVE_COLUMN } from './column_headers/translations';
|
||||
|
||||
const StatefulAlertStatusBulkActions = lazy(
|
||||
() => import('../toolbar/bulk_actions/alert_status_bulk_actions')
|
||||
);
|
||||
const StatefulAlertBulkActions = lazy(() => import('../toolbar/bulk_actions/alert_bulk_actions'));
|
||||
|
||||
interface OwnProps {
|
||||
activePage: number;
|
||||
|
@ -425,6 +423,32 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
}
|
||||
}, [bulkActions]);
|
||||
|
||||
const additionalBulkActions = useMemo(() => {
|
||||
if (bulkActions && bulkActions !== true && bulkActions.customBulkActions !== undefined) {
|
||||
return bulkActions.customBulkActions.map((action) => {
|
||||
return {
|
||||
...action,
|
||||
onClick: (eventIds: string[]) => {
|
||||
const items = data.filter((item) => {
|
||||
return eventIds.find((event) => item._id === event);
|
||||
});
|
||||
action.onClick(items);
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}, [bulkActions, data]);
|
||||
|
||||
const showAlertStatusActions = useMemo(() => {
|
||||
if (!hasAlertsCrud) {
|
||||
return false;
|
||||
}
|
||||
if (typeof bulkActions === 'boolean') {
|
||||
return bulkActions;
|
||||
}
|
||||
return bulkActions.alertStatusActions ?? true;
|
||||
}, [bulkActions, hasAlertsCrud]);
|
||||
|
||||
const showBulkActions = useMemo(() => {
|
||||
if (!hasAlertsCrud) {
|
||||
return false;
|
||||
|
@ -436,7 +460,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
if (typeof bulkActions === 'boolean') {
|
||||
return bulkActions;
|
||||
}
|
||||
return bulkActions.alertStatusActions ?? true;
|
||||
return (bulkActions?.customBulkActions?.length || bulkActions?.alertStatusActions) ?? true;
|
||||
}, [hasAlertsCrud, selectedCount, showCheckboxes, bulkActions]);
|
||||
|
||||
const alertToolbar = useMemo(
|
||||
|
@ -447,7 +471,8 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
</EuiFlexItem>
|
||||
{showBulkActions && (
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<StatefulAlertStatusBulkActions
|
||||
<StatefulAlertBulkActions
|
||||
showAlertStatusActions={showAlertStatusActions}
|
||||
data-test-subj="bulk-actions"
|
||||
id={id}
|
||||
totalItems={totalSelectAllAlerts ?? totalItems}
|
||||
|
@ -456,6 +481,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
indexName={indexNames.join()}
|
||||
onActionSuccess={onAlertStatusActionSuccess}
|
||||
onActionFailure={onAlertStatusActionFailure}
|
||||
customBulkActions={additionalBulkActions}
|
||||
refetch={refetch}
|
||||
/>
|
||||
</Suspense>
|
||||
|
@ -463,6 +489,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
</EuiFlexGroup>
|
||||
),
|
||||
[
|
||||
additionalBulkActions,
|
||||
alertCountText,
|
||||
filterQuery,
|
||||
filterStatus,
|
||||
|
@ -471,6 +498,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
onAlertStatusActionFailure,
|
||||
onAlertStatusActionSuccess,
|
||||
refetch,
|
||||
showAlertStatusActions,
|
||||
showBulkActions,
|
||||
totalItems,
|
||||
totalSelectAllAlerts,
|
||||
|
@ -486,7 +514,8 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
{showBulkActions ? (
|
||||
<>
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<StatefulAlertStatusBulkActions
|
||||
<StatefulAlertBulkActions
|
||||
showAlertStatusActions={showAlertStatusActions}
|
||||
data-test-subj="bulk-actions"
|
||||
id={id}
|
||||
totalItems={totalSelectAllAlerts ?? totalItems}
|
||||
|
@ -495,6 +524,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
indexName={indexNames.join()}
|
||||
onActionSuccess={onAlertStatusActionSuccess}
|
||||
onActionFailure={onAlertStatusActionFailure}
|
||||
customBulkActions={additionalBulkActions}
|
||||
refetch={refetch}
|
||||
/>
|
||||
</Suspense>
|
||||
|
@ -528,21 +558,23 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
showDisplaySelector: false,
|
||||
}),
|
||||
[
|
||||
isLoading,
|
||||
alertCountText,
|
||||
showBulkActions,
|
||||
showAlertStatusActions,
|
||||
id,
|
||||
totalSelectAllAlerts,
|
||||
totalItems,
|
||||
fieldBrowserOptions,
|
||||
filterStatus,
|
||||
filterQuery,
|
||||
indexNames,
|
||||
onAlertStatusActionSuccess,
|
||||
onAlertStatusActionFailure,
|
||||
additionalBulkActions,
|
||||
refetch,
|
||||
isLoading,
|
||||
additionalControls,
|
||||
browserFields,
|
||||
fieldBrowserOptions,
|
||||
columnHeaders,
|
||||
]
|
||||
);
|
||||
|
|
|
@ -147,6 +147,7 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
data,
|
||||
unit,
|
||||
showCheckboxes = true,
|
||||
bulkActions = {},
|
||||
queryFields = [],
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -387,6 +388,7 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
filterStatus={filterStatus}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
showCheckboxes={showCheckboxes}
|
||||
bulkActions={bulkActions}
|
||||
/>
|
||||
</ScrollableFlexItem>
|
||||
</FullWidthFlexGroup>
|
||||
|
|
|
@ -13,11 +13,12 @@ import type {
|
|||
SetEventsDeleted,
|
||||
OnUpdateAlertStatusSuccess,
|
||||
OnUpdateAlertStatusError,
|
||||
CustomBulkActionProp,
|
||||
} from '../../../../../common/types';
|
||||
import type { Refetch } from '../../../../store/t_grid/inputs';
|
||||
import { tGridActions, TGridModel, tGridSelectors, TimelineState } from '../../../../store/t_grid';
|
||||
import { BulkActions } from '.';
|
||||
import { useStatusBulkActionItems } from '../../../../hooks/use_status_bulk_action_items';
|
||||
import { useBulkActionItems } from '../../../../hooks/use_bulk_action_items';
|
||||
|
||||
interface OwnProps {
|
||||
id: string;
|
||||
|
@ -25,17 +26,19 @@ interface OwnProps {
|
|||
filterStatus?: AlertStatus;
|
||||
query?: string;
|
||||
indexName: string;
|
||||
showAlertStatusActions?: boolean;
|
||||
onActionSuccess?: OnUpdateAlertStatusSuccess;
|
||||
onActionFailure?: OnUpdateAlertStatusError;
|
||||
customBulkActions?: CustomBulkActionProp[];
|
||||
refetch: Refetch;
|
||||
}
|
||||
|
||||
export type StatefulAlertStatusBulkActionsProps = OwnProps & PropsFromRedux;
|
||||
export type StatefulAlertBulkActionsProps = OwnProps & PropsFromRedux;
|
||||
|
||||
/**
|
||||
* Component to render status bulk actions
|
||||
*/
|
||||
export const AlertStatusBulkActionsComponent = React.memo<StatefulAlertStatusBulkActionsProps>(
|
||||
export const AlertBulkActionsComponent = React.memo<StatefulAlertBulkActionsProps>(
|
||||
({
|
||||
id,
|
||||
totalItems,
|
||||
|
@ -45,8 +48,10 @@ export const AlertStatusBulkActionsComponent = React.memo<StatefulAlertStatusBul
|
|||
isSelectAllChecked,
|
||||
clearSelected,
|
||||
indexName,
|
||||
showAlertStatusActions,
|
||||
onActionSuccess,
|
||||
onActionFailure,
|
||||
customBulkActions,
|
||||
refetch,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -111,15 +116,17 @@ export const AlertStatusBulkActionsComponent = React.memo<StatefulAlertStatusBul
|
|||
[dispatch, id]
|
||||
);
|
||||
|
||||
const statusBulkActionItems = useStatusBulkActionItems({
|
||||
const bulkActionItems = useBulkActionItems({
|
||||
indexName,
|
||||
eventIds: Object.keys(selectedEventIds),
|
||||
currentStatus: filterStatus,
|
||||
...(showClearSelection ? { query } : {}),
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
showAlertStatusActions,
|
||||
onUpdateSuccess,
|
||||
onUpdateFailure,
|
||||
customBulkActions,
|
||||
timelineId: id,
|
||||
});
|
||||
|
||||
|
@ -131,13 +138,13 @@ export const AlertStatusBulkActionsComponent = React.memo<StatefulAlertStatusBul
|
|||
showClearSelection={showClearSelection}
|
||||
onSelectAll={onSelectAll}
|
||||
onClearSelection={onClearSelection}
|
||||
bulkActionItems={statusBulkActionItems}
|
||||
bulkActionItems={bulkActionItems}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
AlertStatusBulkActionsComponent.displayName = 'AlertStatusBulkActionsComponent';
|
||||
AlertBulkActionsComponent.displayName = 'AlertBulkActionsComponent';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getTGrid = tGridSelectors.getTGridByIdSelector();
|
||||
|
@ -161,7 +168,7 @@ const connector = connect(makeMapStateToProps, mapDispatchToProps);
|
|||
|
||||
type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export const StatefulAlertStatusBulkActions = connector(AlertStatusBulkActionsComponent);
|
||||
export const StatefulAlertBulkActions = connector(AlertBulkActionsComponent);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { StatefulAlertStatusBulkActions as default };
|
||||
export { StatefulAlertBulkActions as default };
|
|
@ -60,6 +60,12 @@ const BulkActionsComponent: React.FC<BulkActionsProps> = ({
|
|||
setIsActionsPopoverOpen(false);
|
||||
}, [setIsActionsPopoverOpen]);
|
||||
|
||||
const closeIfPopoverIsOpen = useCallback(() => {
|
||||
if (isActionsPopoverOpen) {
|
||||
setIsActionsPopoverOpen(false);
|
||||
}
|
||||
}, [isActionsPopoverOpen]);
|
||||
|
||||
const toggleSelectAll = useCallback(() => {
|
||||
if (!showClearSelection) {
|
||||
onSelectAll();
|
||||
|
@ -91,7 +97,10 @@ const BulkActionsComponent: React.FC<BulkActionsProps> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<BulkActionsContainer data-test-subj="bulk-actions-button-container">
|
||||
<BulkActionsContainer
|
||||
onClick={closeIfPopoverIsOpen}
|
||||
data-test-subj="bulk-actions-button-container"
|
||||
>
|
||||
<EuiPopover
|
||||
isOpen={isActionsPopoverOpen}
|
||||
anchorPosition="upCenter"
|
||||
|
|
|
@ -47,6 +47,20 @@ export const BULK_ACTION_FAILED_SINGLE_ALERT = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const BULK_ACTION_ATTACH_NEW_CASE = i18n.translate(
|
||||
'xpack.timelines.timeline.attachNewCase',
|
||||
{
|
||||
defaultMessage: 'Attach to new case',
|
||||
}
|
||||
);
|
||||
|
||||
export const BULK_ACTION_ATTACH_EXISTING_CASE = i18n.translate(
|
||||
'xpack.timelines.timeline.attachExistingCase',
|
||||
{
|
||||
defaultMessage: 'Attach to existing case',
|
||||
}
|
||||
);
|
||||
|
||||
export const CLOSED_ALERT_SUCCESS_TOAST = (totalAlerts: number) =>
|
||||
i18n.translate('xpack.timelines.timeline.closedAlertSuccessToastMessage', {
|
||||
values: { totalAlerts },
|
||||
|
|
|
@ -9,7 +9,7 @@ import React, { useMemo, useCallback } from 'react';
|
|||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
import { FILTER_CLOSED, FILTER_ACKNOWLEDGED, FILTER_OPEN } from '../../common/constants';
|
||||
import * as i18n from '../components/t_grid/translations';
|
||||
import type { AlertStatus, StatusBulkActionsProps } from '../../common/types/timeline';
|
||||
import type { AlertStatus, BulkActionsProps } from '../../common/types/timeline';
|
||||
import { useUpdateAlertsStatus } from '../container/use_update_alerts';
|
||||
import { useAppToasts } from './use_app_toasts';
|
||||
import { STANDALONE_ID } from '../components/t_grid/standalone';
|
||||
|
@ -18,17 +18,19 @@ export const getUpdateAlertsQuery = (eventIds: Readonly<string[]>) => {
|
|||
return { bool: { filter: { terms: { _id: eventIds } } } };
|
||||
};
|
||||
|
||||
export const useStatusBulkActionItems = ({
|
||||
export const useBulkActionItems = ({
|
||||
eventIds,
|
||||
currentStatus,
|
||||
query,
|
||||
indexName,
|
||||
setEventsLoading,
|
||||
showAlertStatusActions = true,
|
||||
setEventsDeleted,
|
||||
onUpdateSuccess,
|
||||
onUpdateFailure,
|
||||
customBulkActions,
|
||||
timelineId,
|
||||
}: StatusBulkActionsProps) => {
|
||||
}: BulkActionsProps) => {
|
||||
const { updateAlertStatus } = useUpdateAlertsStatus(timelineId !== STANDALONE_ID);
|
||||
const { addSuccess, addError, addWarning } = useAppToasts();
|
||||
|
||||
|
@ -122,42 +124,63 @@ export const useStatusBulkActionItems = ({
|
|||
);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const actionItems = [];
|
||||
if (currentStatus !== FILTER_OPEN) {
|
||||
actionItems.push(
|
||||
<EuiContextMenuItem
|
||||
key="open"
|
||||
data-test-subj="open-alert-status"
|
||||
onClick={() => onClickUpdate(FILTER_OPEN)}
|
||||
>
|
||||
{i18n.BULK_ACTION_OPEN_SELECTED}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
const actionItems: JSX.Element[] = [];
|
||||
if (showAlertStatusActions) {
|
||||
if (currentStatus !== FILTER_OPEN) {
|
||||
actionItems.push(
|
||||
<EuiContextMenuItem
|
||||
key="open"
|
||||
data-test-subj="open-alert-status"
|
||||
onClick={() => onClickUpdate(FILTER_OPEN)}
|
||||
>
|
||||
{i18n.BULK_ACTION_OPEN_SELECTED}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
if (currentStatus !== FILTER_ACKNOWLEDGED) {
|
||||
actionItems.push(
|
||||
<EuiContextMenuItem
|
||||
key="acknowledge"
|
||||
data-test-subj="acknowledged-alert-status"
|
||||
onClick={() => onClickUpdate(FILTER_ACKNOWLEDGED)}
|
||||
>
|
||||
{i18n.BULK_ACTION_ACKNOWLEDGED_SELECTED}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
if (currentStatus !== FILTER_CLOSED) {
|
||||
actionItems.push(
|
||||
<EuiContextMenuItem
|
||||
key="close"
|
||||
data-test-subj="close-alert-status"
|
||||
onClick={() => onClickUpdate(FILTER_CLOSED)}
|
||||
>
|
||||
{i18n.BULK_ACTION_CLOSE_SELECTED}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (currentStatus !== FILTER_ACKNOWLEDGED) {
|
||||
actionItems.push(
|
||||
<EuiContextMenuItem
|
||||
key="acknowledge"
|
||||
data-test-subj="acknowledged-alert-status"
|
||||
onClick={() => onClickUpdate(FILTER_ACKNOWLEDGED)}
|
||||
>
|
||||
{i18n.BULK_ACTION_ACKNOWLEDGED_SELECTED}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
if (currentStatus !== FILTER_CLOSED) {
|
||||
actionItems.push(
|
||||
<EuiContextMenuItem
|
||||
key="close"
|
||||
data-test-subj="close-alert-status"
|
||||
onClick={() => onClickUpdate(FILTER_CLOSED)}
|
||||
>
|
||||
{i18n.BULK_ACTION_CLOSE_SELECTED}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
return actionItems;
|
||||
}, [currentStatus, onClickUpdate]);
|
||||
|
||||
const additionalItems = customBulkActions
|
||||
? customBulkActions.reduce<JSX.Element[]>((acc, action) => {
|
||||
const isDisabled = !!(query && action.disableOnQuery);
|
||||
acc.push(
|
||||
<EuiContextMenuItem
|
||||
key={action.key}
|
||||
disabled={isDisabled}
|
||||
data-test-subj={action['data-test-subj']}
|
||||
toolTipContent={isDisabled ? action.disabledLabel : null}
|
||||
onClick={() => action.onClick(eventIds)}
|
||||
>
|
||||
{action.label}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
return acc;
|
||||
}, [])
|
||||
: [];
|
||||
|
||||
return [...actionItems, ...additionalItems];
|
||||
}, [currentStatus, customBulkActions, eventIds, onClickUpdate, query, showAlertStatusActions]);
|
||||
|
||||
return items;
|
||||
};
|
|
@ -75,7 +75,7 @@ export {
|
|||
|
||||
export { getActionsColumnWidth } from './components/t_grid/body/column_headers/helpers';
|
||||
export { DEFAULT_ACTION_BUTTON_WIDTH } from './components/t_grid/body/constants';
|
||||
export { useStatusBulkActionItems } from './hooks/use_status_bulk_action_items';
|
||||
export { useBulkActionItems } from './hooks/use_bulk_action_items';
|
||||
export { getPageRowIndex } from '../common/utils/pagination';
|
||||
|
||||
// This exports static code and TypeScript types,
|
||||
|
|
|
@ -9402,7 +9402,6 @@
|
|||
"xpack.canvas.workpadTemplates.table.nameColumnTitle": "Nom de modèle",
|
||||
"xpack.canvas.workpadTemplates.table.tagsColumnTitle": "Balises",
|
||||
"xpack.cases.actions.caseAlertSuccessSyncText": "Les alertes dans ce cas ont leur statut synchronisé avec celui du cas",
|
||||
"xpack.cases.actions.caseAlertSuccessToast": "Une alerte a été ajoutée à \"{title}\"",
|
||||
"xpack.cases.actions.caseSuccessToast": "{title} a été mis à jour",
|
||||
"xpack.cases.actions.viewCase": "Afficher le cas",
|
||||
"xpack.cases.addConnector.title": "Ajouter un connecteur",
|
||||
|
@ -21383,8 +21382,6 @@
|
|||
"xpack.observability.cases.caseView.goToDocumentationButton": "Afficher la documentation",
|
||||
"xpack.observability.defaultApmServiceEnvironment": "Environnement de service par défaut",
|
||||
"xpack.observability.defaultApmServiceEnvironmentDescription": "Définissez l’environnement par défaut pour l’application APM. Sinon, les données de tous les environnements sont affichées par défaut.",
|
||||
"xpack.observability.detectionEngine.alerts.actions.addToCase": "Ajouter à un cas existant",
|
||||
"xpack.observability.detectionEngine.alerts.actions.addToNewCase": "Ajouter au nouveau cas",
|
||||
"xpack.observability.emptySection.apps.alert.description": "Détectez des conditions complexes dans Observability et déclenchez des actions lorsque ces conditions sont satisfaites.",
|
||||
"xpack.observability.emptySection.apps.alert.link": "Créer une règle",
|
||||
"xpack.observability.emptySection.apps.alert.title": "Aucune alerte n'a été trouvée.",
|
||||
|
|
|
@ -9495,7 +9495,6 @@
|
|||
"xpack.canvas.workpadTemplates.table.nameColumnTitle": "テンプレート名",
|
||||
"xpack.canvas.workpadTemplates.table.tagsColumnTitle": "タグ",
|
||||
"xpack.cases.actions.caseAlertSuccessSyncText": "このケースのアラートはステータスがケースステータスと同期されました",
|
||||
"xpack.cases.actions.caseAlertSuccessToast": "アラートが「{title}」に追加されました",
|
||||
"xpack.cases.actions.caseSuccessToast": "{title}が更新されました",
|
||||
"xpack.cases.actions.viewCase": "ケースの表示",
|
||||
"xpack.cases.addConnector.title": "コネクターの追加",
|
||||
|
@ -21532,8 +21531,6 @@
|
|||
"xpack.observability.cases.caseView.goToDocumentationButton": "ドキュメンテーションを表示",
|
||||
"xpack.observability.defaultApmServiceEnvironment": "デフォルトのサービス環境",
|
||||
"xpack.observability.defaultApmServiceEnvironmentDescription": "APMアプリのデフォルト環境を設定します。空にすると、すべての環境からのデータがデフォルトで表示されます。",
|
||||
"xpack.observability.detectionEngine.alerts.actions.addToCase": "既存のケースに追加",
|
||||
"xpack.observability.detectionEngine.alerts.actions.addToNewCase": "新しいケースに追加",
|
||||
"xpack.observability.emptySection.apps.alert.description": "オブザーバビリティで複雑な条件を検出し、それらの条件が満たされたときにアクションをトリガーします。",
|
||||
"xpack.observability.emptySection.apps.alert.link": "ルールを作成",
|
||||
"xpack.observability.emptySection.apps.alert.title": "アラートが見つかりません。",
|
||||
|
|
|
@ -9516,7 +9516,6 @@
|
|||
"xpack.canvas.workpadTemplates.table.nameColumnTitle": "模板名称",
|
||||
"xpack.canvas.workpadTemplates.table.tagsColumnTitle": "标签",
|
||||
"xpack.cases.actions.caseAlertSuccessSyncText": "此案例中的告警的状态已经与案例状态同步",
|
||||
"xpack.cases.actions.caseAlertSuccessToast": "告警已添加到“{title}”",
|
||||
"xpack.cases.actions.caseSuccessToast": "{title} 已更新",
|
||||
"xpack.cases.actions.viewCase": "查看案例",
|
||||
"xpack.cases.addConnector.title": "添加连接器",
|
||||
|
@ -21564,8 +21563,6 @@
|
|||
"xpack.observability.cases.caseView.goToDocumentationButton": "查看文档",
|
||||
"xpack.observability.defaultApmServiceEnvironment": "默认服务环境",
|
||||
"xpack.observability.defaultApmServiceEnvironmentDescription": "为 APM 应用设置默认环境。如果留空,默认情况下将显示所有环境中的数据。",
|
||||
"xpack.observability.detectionEngine.alerts.actions.addToCase": "添加到现有案例",
|
||||
"xpack.observability.detectionEngine.alerts.actions.addToNewCase": "添加到新案例",
|
||||
"xpack.observability.emptySection.apps.alert.description": "检测 Observability 内的复杂条件,并在满足那些条件时触发操作。",
|
||||
"xpack.observability.emptySection.apps.alert.link": "创建规则",
|
||||
"xpack.observability.emptySection.apps.alert.title": "未找到告警。",
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
const ADD_TO_EXISTING_CASE_SELECTOR = 'add-existing-case-menu-item';
|
||||
const ADD_TO_NEW_CASE_SELECTOR = 'add-new-case-item';
|
||||
const ADD_TO_EXISTING_CASE_SELECTOR = 'add-to-existing-case-action';
|
||||
const ADD_TO_NEW_CASE_SELECTOR = 'add-to-new-case-action';
|
||||
const CREATE_CASE_FLYOUT = 'create-case-flyout';
|
||||
const SELECT_CASE_MODAL = 'all-cases-modal';
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ const ALERTS_TABLE_CONTAINER_SELECTOR = 'events-viewer-panel';
|
|||
const VIEW_RULE_DETAILS_SELECTOR = 'viewRuleDetails';
|
||||
const VIEW_RULE_DETAILS_FLYOUT_SELECTOR = 'viewRuleDetailsFlyout';
|
||||
|
||||
const ACTION_COLUMN_INDEX = 0;
|
||||
const ACTION_COLUMN_INDEX = 1;
|
||||
|
||||
type WorkflowStatus = 'open' | 'acknowledged' | 'closed';
|
||||
|
||||
|
|
|
@ -25,8 +25,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
|||
await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts');
|
||||
});
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/116064
|
||||
describe.skip('When user has all priviledges for cases', () => {
|
||||
describe('When user has all priviledges for cases', () => {
|
||||
before(async () => {
|
||||
await observability.users.setTestUserRole(
|
||||
observability.users.defineBasicObservabilityRole({
|
||||
|
|
|
@ -10,8 +10,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
|
|||
import { asyncForEach } from '../helpers';
|
||||
|
||||
const ACTIVE_ALERTS_CELL_COUNT = 78;
|
||||
const RECOVERED_ALERTS_CELL_COUNT = 150;
|
||||
const TOTAL_ALERTS_CELL_COUNT = 200;
|
||||
const RECOVERED_ALERTS_CELL_COUNT = 180;
|
||||
const TOTAL_ALERTS_CELL_COUNT = 240;
|
||||
|
||||
const DISABLED_ALERTS_CHECKBOX = 6;
|
||||
const ENABLED_ALERTS_CHECKBOX = 4;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue