[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:
Esteban Beltran 2022-05-18 11:30:14 +02:00 committed by GitHub
parent 59c55a4434
commit d638b188dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 549 additions and 217 deletions

View file

@ -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);
};

View file

@ -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(

View file

@ -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(
() => {

View file

@ -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);

View file

@ -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} />;

View file

@ -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,

View file

@ -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 }
);

View file

@ -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,

View file

@ -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 {

View file

@ -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,
},
};
}

View file

@ -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;
};
}

View file

@ -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]);
};

View file

@ -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);

View file

@ -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',
}
);

View file

@ -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,

View file

@ -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',
});

View file

@ -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,

View file

@ -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);

View file

@ -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,

View file

@ -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 (

View file

@ -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,

View file

@ -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]);
};

View file

@ -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',
}
);

View file

@ -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;

View file

@ -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,
]
);

View file

@ -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>

View file

@ -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 };

View file

@ -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"

View file

@ -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 },

View file

@ -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;
};

View file

@ -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,

View file

@ -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 lenvironnement par défaut pour lapplication 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.",

View file

@ -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": "アラートが見つかりません。",

View file

@ -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": "未找到告警。",

View file

@ -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';

View file

@ -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';

View file

@ -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({

View file

@ -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;