[SIEM] [Cases] External services not getting all comments bug fix (#65307)
This commit is contained in:
parent
c00b36e9e3
commit
c03bdccce1
|
@ -50,19 +50,11 @@ export const REOPENED_CASES = ({
|
||||||
defaultMessage: 'Reopened {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}',
|
defaultMessage: 'Reopened {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const TAG_FETCH_FAILURE = i18n.translate(
|
export const SUCCESS_SEND_TO_EXTERNAL_SERVICE = (serviceName: string) =>
|
||||||
'xpack.siem.containers.case.tagFetchFailDescription',
|
i18n.translate('xpack.siem.containers.case.pushToExternalService', {
|
||||||
{
|
values: { serviceName },
|
||||||
defaultMessage: 'Failed to fetch Tags',
|
defaultMessage: 'Successfully sent to { serviceName }',
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const SUCCESS_SEND_TO_EXTERNAL_SERVICE = i18n.translate(
|
|
||||||
'xpack.siem.containers.case.pushToExterService',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Successfully sent to ServiceNow',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ERROR_PUSH_TO_SERVICE = i18n.translate(
|
export const ERROR_PUSH_TO_SERVICE = i18n.translate(
|
||||||
'xpack.siem.case.configure.errorPushingToService',
|
'xpack.siem.case.configure.errorPushingToService',
|
||||||
|
|
|
@ -122,13 +122,14 @@ describe('useGetCaseUserActions', () => {
|
||||||
...basicPush,
|
...basicPush,
|
||||||
firstPushIndex: 3,
|
firstPushIndex: 3,
|
||||||
lastPushIndex: 3,
|
lastPushIndex: 3,
|
||||||
|
commentsToUpdate: [],
|
||||||
hasDataToPush: false,
|
hasDataToPush: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Correctly marks first/last index - hasDataToPush: true', () => {
|
it('Correctly marks first/last index and comment id - hasDataToPush: true', () => {
|
||||||
const userActions = [
|
const userActions = [
|
||||||
...caseUserActions,
|
...caseUserActions,
|
||||||
getUserAction(['pushed'], 'push-to-service'),
|
getUserAction(['pushed'], 'push-to-service'),
|
||||||
|
@ -142,6 +143,83 @@ describe('useGetCaseUserActions', () => {
|
||||||
...basicPush,
|
...basicPush,
|
||||||
firstPushIndex: 3,
|
firstPushIndex: 3,
|
||||||
lastPushIndex: 3,
|
lastPushIndex: 3,
|
||||||
|
commentsToUpdate: [userActions[userActions.length - 1].commentId],
|
||||||
|
hasDataToPush: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Correctly marks first/last index and multiple comment ids, both needs push', () => {
|
||||||
|
const userActions = [
|
||||||
|
...caseUserActions,
|
||||||
|
getUserAction(['pushed'], 'push-to-service'),
|
||||||
|
getUserAction(['comment'], 'create'),
|
||||||
|
{ ...getUserAction(['comment'], 'create'), commentId: 'muahaha' },
|
||||||
|
];
|
||||||
|
const result = getPushedInfo(userActions, '123');
|
||||||
|
expect(result).toEqual({
|
||||||
|
hasDataToPush: true,
|
||||||
|
caseServices: {
|
||||||
|
'123': {
|
||||||
|
...basicPush,
|
||||||
|
firstPushIndex: 3,
|
||||||
|
lastPushIndex: 3,
|
||||||
|
commentsToUpdate: [
|
||||||
|
userActions[userActions.length - 2].commentId,
|
||||||
|
userActions[userActions.length - 1].commentId,
|
||||||
|
],
|
||||||
|
hasDataToPush: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Correctly marks first/last index and multiple comment ids, one needs push', () => {
|
||||||
|
const userActions = [
|
||||||
|
...caseUserActions,
|
||||||
|
getUserAction(['pushed'], 'push-to-service'),
|
||||||
|
getUserAction(['comment'], 'create'),
|
||||||
|
getUserAction(['pushed'], 'push-to-service'),
|
||||||
|
{ ...getUserAction(['comment'], 'create'), commentId: 'muahaha' },
|
||||||
|
];
|
||||||
|
const result = getPushedInfo(userActions, '123');
|
||||||
|
expect(result).toEqual({
|
||||||
|
hasDataToPush: true,
|
||||||
|
caseServices: {
|
||||||
|
'123': {
|
||||||
|
...basicPush,
|
||||||
|
firstPushIndex: 3,
|
||||||
|
lastPushIndex: 5,
|
||||||
|
commentsToUpdate: [userActions[userActions.length - 1].commentId],
|
||||||
|
hasDataToPush: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Correctly marks first/last index and multiple comment ids, one needs push and one needs update', () => {
|
||||||
|
const userActions = [
|
||||||
|
...caseUserActions,
|
||||||
|
getUserAction(['pushed'], 'push-to-service'),
|
||||||
|
getUserAction(['comment'], 'create'),
|
||||||
|
getUserAction(['pushed'], 'push-to-service'),
|
||||||
|
{ ...getUserAction(['comment'], 'create'), commentId: 'muahaha' },
|
||||||
|
getUserAction(['comment'], 'update'),
|
||||||
|
getUserAction(['comment'], 'update'),
|
||||||
|
];
|
||||||
|
const result = getPushedInfo(userActions, '123');
|
||||||
|
expect(result).toEqual({
|
||||||
|
hasDataToPush: true,
|
||||||
|
caseServices: {
|
||||||
|
'123': {
|
||||||
|
...basicPush,
|
||||||
|
firstPushIndex: 3,
|
||||||
|
lastPushIndex: 5,
|
||||||
|
commentsToUpdate: [
|
||||||
|
userActions[userActions.length - 3].commentId,
|
||||||
|
userActions[userActions.length - 1].commentId,
|
||||||
|
],
|
||||||
hasDataToPush: true,
|
hasDataToPush: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -162,6 +240,7 @@ describe('useGetCaseUserActions', () => {
|
||||||
...basicPush,
|
...basicPush,
|
||||||
firstPushIndex: 3,
|
firstPushIndex: 3,
|
||||||
lastPushIndex: 3,
|
lastPushIndex: 3,
|
||||||
|
commentsToUpdate: [],
|
||||||
hasDataToPush: false,
|
hasDataToPush: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -182,11 +261,34 @@ describe('useGetCaseUserActions', () => {
|
||||||
...basicPush,
|
...basicPush,
|
||||||
firstPushIndex: 3,
|
firstPushIndex: 3,
|
||||||
lastPushIndex: 5,
|
lastPushIndex: 5,
|
||||||
|
commentsToUpdate: [],
|
||||||
hasDataToPush: false,
|
hasDataToPush: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('Correctly handles comment update with multiple push actions', () => {
|
||||||
|
const userActions = [
|
||||||
|
...caseUserActions,
|
||||||
|
getUserAction(['pushed'], 'push-to-service'),
|
||||||
|
getUserAction(['comment'], 'create'),
|
||||||
|
getUserAction(['pushed'], 'push-to-service'),
|
||||||
|
getUserAction(['comment'], 'update'),
|
||||||
|
];
|
||||||
|
const result = getPushedInfo(userActions, '123');
|
||||||
|
expect(result).toEqual({
|
||||||
|
hasDataToPush: true,
|
||||||
|
caseServices: {
|
||||||
|
'123': {
|
||||||
|
...basicPush,
|
||||||
|
firstPushIndex: 3,
|
||||||
|
lastPushIndex: 5,
|
||||||
|
commentsToUpdate: [userActions[userActions.length - 1].commentId],
|
||||||
|
hasDataToPush: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('Multiple connector tracking - hasDataToPush: true', () => {
|
it('Multiple connector tracking - hasDataToPush: true', () => {
|
||||||
const pushAction123 = getUserAction(['pushed'], 'push-to-service');
|
const pushAction123 = getUserAction(['pushed'], 'push-to-service');
|
||||||
|
@ -215,6 +317,7 @@ describe('useGetCaseUserActions', () => {
|
||||||
...basicPush,
|
...basicPush,
|
||||||
firstPushIndex: 3,
|
firstPushIndex: 3,
|
||||||
lastPushIndex: 3,
|
lastPushIndex: 3,
|
||||||
|
commentsToUpdate: [userActions[userActions.length - 2].commentId],
|
||||||
hasDataToPush: true,
|
hasDataToPush: true,
|
||||||
},
|
},
|
||||||
'456': {
|
'456': {
|
||||||
|
@ -224,6 +327,7 @@ describe('useGetCaseUserActions', () => {
|
||||||
externalId: 'other_external_id',
|
externalId: 'other_external_id',
|
||||||
firstPushIndex: 5,
|
firstPushIndex: 5,
|
||||||
lastPushIndex: 5,
|
lastPushIndex: 5,
|
||||||
|
commentsToUpdate: [],
|
||||||
hasDataToPush: false,
|
hasDataToPush: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -257,6 +361,7 @@ describe('useGetCaseUserActions', () => {
|
||||||
...basicPush,
|
...basicPush,
|
||||||
firstPushIndex: 3,
|
firstPushIndex: 3,
|
||||||
lastPushIndex: 3,
|
lastPushIndex: 3,
|
||||||
|
commentsToUpdate: [userActions[userActions.length - 2].commentId],
|
||||||
hasDataToPush: true,
|
hasDataToPush: true,
|
||||||
},
|
},
|
||||||
'456': {
|
'456': {
|
||||||
|
@ -266,6 +371,7 @@ describe('useGetCaseUserActions', () => {
|
||||||
externalId: 'other_external_id',
|
externalId: 'other_external_id',
|
||||||
firstPushIndex: 5,
|
firstPushIndex: 5,
|
||||||
lastPushIndex: 5,
|
lastPushIndex: 5,
|
||||||
|
commentsToUpdate: [],
|
||||||
hasDataToPush: false,
|
hasDataToPush: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,9 +14,10 @@ import { CaseExternalService, CaseUserActions, ElasticUser } from './types';
|
||||||
import { convertToCamelCase, parseString } from './utils';
|
import { convertToCamelCase, parseString } from './utils';
|
||||||
import { CaseFullExternalService } from '../../../../case/common/api/cases';
|
import { CaseFullExternalService } from '../../../../case/common/api/cases';
|
||||||
|
|
||||||
interface CaseService extends CaseExternalService {
|
export interface CaseService extends CaseExternalService {
|
||||||
firstPushIndex: number;
|
firstPushIndex: number;
|
||||||
lastPushIndex: number;
|
lastPushIndex: number;
|
||||||
|
commentsToUpdate: string[];
|
||||||
hasDataToPush: boolean;
|
hasDataToPush: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,6 +49,10 @@ export interface UseGetCaseUserActions extends CaseUserActionsState {
|
||||||
|
|
||||||
const getExternalService = (value: string): CaseExternalService | null =>
|
const getExternalService = (value: string): CaseExternalService | null =>
|
||||||
convertToCamelCase<CaseFullExternalService, CaseExternalService>(parseString(`${value}`));
|
convertToCamelCase<CaseFullExternalService, CaseExternalService>(parseString(`${value}`));
|
||||||
|
interface CommentsAndIndex {
|
||||||
|
commentId: string;
|
||||||
|
commentIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const getPushedInfo = (
|
export const getPushedInfo = (
|
||||||
caseUserActions: CaseUserActions[],
|
caseUserActions: CaseUserActions[],
|
||||||
|
@ -69,11 +74,25 @@ export const getPushedInfo = (
|
||||||
.action !== 'push-to-service'
|
.action !== 'push-to-service'
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
const commentsAndIndex = caseUserActions.reduce<CommentsAndIndex[]>(
|
||||||
|
(bacc, mua, index) =>
|
||||||
|
mua.actionField[0] === 'comment' && mua.commentId != null
|
||||||
|
? [
|
||||||
|
...bacc,
|
||||||
|
{
|
||||||
|
commentId: mua.commentId,
|
||||||
|
commentIndex: index,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: bacc,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const caseServices = caseUserActions.reduce<CaseServices>((acc, cua, i) => {
|
let caseServices = caseUserActions.reduce<CaseServices>((acc, cua, i) => {
|
||||||
if (cua.action !== 'push-to-service') {
|
if (cua.action !== 'push-to-service') {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
const externalService = getExternalService(`${cua.newValue}`);
|
const externalService = getExternalService(`${cua.newValue}`);
|
||||||
if (externalService === null) {
|
if (externalService === null) {
|
||||||
return acc;
|
return acc;
|
||||||
|
@ -87,6 +106,7 @@ export const getPushedInfo = (
|
||||||
...acc[externalService.connectorId],
|
...acc[externalService.connectorId],
|
||||||
...externalService,
|
...externalService,
|
||||||
lastPushIndex: i,
|
lastPushIndex: i,
|
||||||
|
commentsToUpdate: [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
|
@ -95,11 +115,31 @@ export const getPushedInfo = (
|
||||||
firstPushIndex: i,
|
firstPushIndex: i,
|
||||||
lastPushIndex: i,
|
lastPushIndex: i,
|
||||||
hasDataToPush: hasDataToPushForConnector(externalService.connectorId),
|
hasDataToPush: hasDataToPushForConnector(externalService.connectorId),
|
||||||
|
commentsToUpdate: [],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
caseServices = Object.keys(caseServices).reduce<CaseServices>((acc, key) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[key]: {
|
||||||
|
...caseServices[key],
|
||||||
|
// if the comment happens after the lastUpdateToCaseIndex, it should be included in commentsToUpdate
|
||||||
|
commentsToUpdate: commentsAndIndex.reduce<string[]>(
|
||||||
|
(bacc, currentComment) =>
|
||||||
|
currentComment.commentIndex > caseServices[key].lastPushIndex
|
||||||
|
? bacc.indexOf(currentComment.commentId) > -1
|
||||||
|
? [...bacc.filter(e => e !== currentComment.commentId), currentComment.commentId]
|
||||||
|
: [...bacc, currentComment.commentId]
|
||||||
|
: bacc,
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
|
||||||
const hasDataToPush =
|
const hasDataToPush =
|
||||||
caseServices[caseConnectorId] != null ? caseServices[caseConnectorId].hasDataToPush : true;
|
caseServices[caseConnectorId] != null ? caseServices[caseConnectorId].hasDataToPush : true;
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
serviceConnectorUser,
|
serviceConnectorUser,
|
||||||
} from './mock';
|
} from './mock';
|
||||||
import * as api from './api';
|
import * as api from './api';
|
||||||
|
import { CaseServices } from './use_get_case_user_actions';
|
||||||
|
|
||||||
jest.mock('./api');
|
jest.mock('./api');
|
||||||
|
|
||||||
|
@ -32,6 +33,7 @@ describe('usePostPushToService', () => {
|
||||||
...basicPush,
|
...basicPush,
|
||||||
firstPushIndex: 1,
|
firstPushIndex: 1,
|
||||||
lastPushIndex: 1,
|
lastPushIndex: 1,
|
||||||
|
commentsToUpdate: [basicComment.id],
|
||||||
hasDataToPush: false,
|
hasDataToPush: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -64,6 +66,7 @@ describe('usePostPushToService', () => {
|
||||||
...basicPush,
|
...basicPush,
|
||||||
firstPushIndex: 1,
|
firstPushIndex: 1,
|
||||||
lastPushIndex: 1,
|
lastPushIndex: 1,
|
||||||
|
commentsToUpdate: [basicComment.id],
|
||||||
hasDataToPush: true,
|
hasDataToPush: true,
|
||||||
},
|
},
|
||||||
'456': {
|
'456': {
|
||||||
|
@ -71,6 +74,7 @@ describe('usePostPushToService', () => {
|
||||||
connectorId: '456',
|
connectorId: '456',
|
||||||
externalId: 'other_external_id',
|
externalId: 'other_external_id',
|
||||||
firstPushIndex: 4,
|
firstPushIndex: 4,
|
||||||
|
commentsToUpdate: [basicComment.id],
|
||||||
lastPushIndex: 6,
|
lastPushIndex: 6,
|
||||||
hasDataToPush: false,
|
hasDataToPush: false,
|
||||||
},
|
},
|
||||||
|
@ -127,6 +131,31 @@ describe('usePostPushToService', () => {
|
||||||
await waitForNextUpdate();
|
await waitForNextUpdate();
|
||||||
expect(spyOnPushToService).toBeCalledWith(
|
expect(spyOnPushToService).toBeCalledWith(
|
||||||
samplePush.connectorId,
|
samplePush.connectorId,
|
||||||
|
formatServiceRequestData(basicCase, '123', sampleCaseServices as CaseServices),
|
||||||
|
abortCtrl.signal
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls pushToService with correct arguments when no push history', async () => {
|
||||||
|
const samplePush2 = {
|
||||||
|
caseId: pushedCase.id,
|
||||||
|
caseServices: {},
|
||||||
|
connectorName: 'connector name',
|
||||||
|
connectorId: 'none',
|
||||||
|
updateCase,
|
||||||
|
};
|
||||||
|
const spyOnPushToService = jest.spyOn(api, 'pushToService');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
const { result, waitForNextUpdate } = renderHook<string, UsePostPushToService>(() =>
|
||||||
|
usePostPushToService()
|
||||||
|
);
|
||||||
|
await waitForNextUpdate();
|
||||||
|
result.current.postPushToService(samplePush2);
|
||||||
|
await waitForNextUpdate();
|
||||||
|
expect(spyOnPushToService).toBeCalledWith(
|
||||||
|
samplePush2.connectorId,
|
||||||
formatServiceRequestData(basicCase, 'none', {}),
|
formatServiceRequestData(basicCase, 'none', {}),
|
||||||
abortCtrl.signal
|
abortCtrl.signal
|
||||||
);
|
);
|
||||||
|
|
|
@ -122,7 +122,10 @@ export const usePostPushToService = (): UsePostPushToService => {
|
||||||
dispatch({ type: 'FETCH_SUCCESS_PUSH_SERVICE', payload: responseService });
|
dispatch({ type: 'FETCH_SUCCESS_PUSH_SERVICE', payload: responseService });
|
||||||
dispatch({ type: 'FETCH_SUCCESS_PUSH_CASE', payload: responseCase });
|
dispatch({ type: 'FETCH_SUCCESS_PUSH_CASE', payload: responseCase });
|
||||||
updateCase(responseCase);
|
updateCase(responseCase);
|
||||||
displaySuccessToast(i18n.SUCCESS_SEND_TO_EXTERNAL_SERVICE, dispatchToaster);
|
displaySuccessToast(
|
||||||
|
i18n.SUCCESS_SEND_TO_EXTERNAL_SERVICE(connectorName),
|
||||||
|
dispatchToaster
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!cancel) {
|
if (!cancel) {
|
||||||
|
@ -156,25 +159,12 @@ export const formatServiceRequestData = (
|
||||||
createdBy,
|
createdBy,
|
||||||
comments,
|
comments,
|
||||||
description,
|
description,
|
||||||
externalService,
|
|
||||||
title,
|
title,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
updatedBy,
|
updatedBy,
|
||||||
} = myCase;
|
} = myCase;
|
||||||
let actualExternalService = externalService;
|
const actualExternalService = caseServices[connectorId] ?? null;
|
||||||
if (
|
|
||||||
externalService != null &&
|
|
||||||
externalService.connectorId !== connectorId &&
|
|
||||||
caseServices[connectorId]
|
|
||||||
) {
|
|
||||||
actualExternalService = caseServices[connectorId];
|
|
||||||
} else if (
|
|
||||||
externalService != null &&
|
|
||||||
externalService.connectorId !== connectorId &&
|
|
||||||
!caseServices[connectorId]
|
|
||||||
) {
|
|
||||||
actualExternalService = null;
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
caseId,
|
caseId,
|
||||||
createdAt,
|
createdAt,
|
||||||
|
@ -183,17 +173,9 @@ export const formatServiceRequestData = (
|
||||||
username: createdBy?.username ?? '',
|
username: createdBy?.username ?? '',
|
||||||
},
|
},
|
||||||
comments: comments
|
comments: comments
|
||||||
.filter(c => {
|
.filter(
|
||||||
const lastPush = c.pushedAt != null ? new Date(c.pushedAt) : null;
|
c => actualExternalService == null || actualExternalService.commentsToUpdate.includes(c.id)
|
||||||
const lastUpdate = c.updatedAt != null ? new Date(c.updatedAt) : null;
|
)
|
||||||
if (
|
|
||||||
lastPush === null ||
|
|
||||||
(lastPush != null && lastUpdate != null && lastPush.getTime() < lastUpdate?.getTime())
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
.map(c => ({
|
.map(c => ({
|
||||||
commentId: c.id,
|
commentId: c.id,
|
||||||
comment: c.comment,
|
comment: c.comment,
|
||||||
|
|
|
@ -20,6 +20,7 @@ import * as i18n from '../case_view/translations';
|
||||||
import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date';
|
import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date';
|
||||||
import { CaseViewActions } from '../case_view/actions';
|
import { CaseViewActions } from '../case_view/actions';
|
||||||
import { Case } from '../../../../containers/case/types';
|
import { Case } from '../../../../containers/case/types';
|
||||||
|
import { CaseService } from '../../../../containers/case/use_get_case_user_actions';
|
||||||
|
|
||||||
const MyDescriptionList = styled(EuiDescriptionList)`
|
const MyDescriptionList = styled(EuiDescriptionList)`
|
||||||
${({ theme }) => css`
|
${({ theme }) => css`
|
||||||
|
@ -35,6 +36,7 @@ interface CaseStatusProps {
|
||||||
badgeColor: string;
|
badgeColor: string;
|
||||||
buttonLabel: string;
|
buttonLabel: string;
|
||||||
caseData: Case;
|
caseData: Case;
|
||||||
|
currentExternalIncident: CaseService | null;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
icon: string;
|
icon: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
@ -50,6 +52,7 @@ const CaseStatusComp: React.FC<CaseStatusProps> = ({
|
||||||
badgeColor,
|
badgeColor,
|
||||||
buttonLabel,
|
buttonLabel,
|
||||||
caseData,
|
caseData,
|
||||||
|
currentExternalIncident,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
icon,
|
icon,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
@ -100,7 +103,11 @@ const CaseStatusComp: React.FC<CaseStatusProps> = ({
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<CaseViewActions caseData={caseData} disabled={disabled} />
|
<CaseViewActions
|
||||||
|
caseData={caseData}
|
||||||
|
currentExternalIncident={currentExternalIncident}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
|
|
@ -9,8 +9,9 @@ import { mount } from 'enzyme';
|
||||||
|
|
||||||
import { useDeleteCases } from '../../../../containers/case/use_delete_cases';
|
import { useDeleteCases } from '../../../../containers/case/use_delete_cases';
|
||||||
import { TestProviders } from '../../../../mock';
|
import { TestProviders } from '../../../../mock';
|
||||||
import { basicCase } from '../../../../containers/case/mock';
|
import { basicCase, basicPush } from '../../../../containers/case/mock';
|
||||||
import { CaseViewActions } from './actions';
|
import { CaseViewActions } from './actions';
|
||||||
|
import * as i18n from './translations';
|
||||||
jest.mock('../../../../containers/case/use_delete_cases');
|
jest.mock('../../../../containers/case/use_delete_cases');
|
||||||
const useDeleteCasesMock = useDeleteCases as jest.Mock;
|
const useDeleteCasesMock = useDeleteCases as jest.Mock;
|
||||||
|
|
||||||
|
@ -34,7 +35,7 @@ describe('CaseView actions', () => {
|
||||||
it('clicking trash toggles modal', () => {
|
it('clicking trash toggles modal', () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<TestProviders>
|
<TestProviders>
|
||||||
<CaseViewActions caseData={basicCase} />
|
<CaseViewActions caseData={basicCase} currentExternalIncident={null} />
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -54,7 +55,7 @@ describe('CaseView actions', () => {
|
||||||
}));
|
}));
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<TestProviders>
|
<TestProviders>
|
||||||
<CaseViewActions caseData={basicCase} />
|
<CaseViewActions caseData={basicCase} currentExternalIncident={null} />
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -64,4 +65,33 @@ describe('CaseView actions', () => {
|
||||||
{ id: basicCase.id, title: basicCase.title },
|
{ id: basicCase.id, title: basicCase.title },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
it('displays active incident link', () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<TestProviders>
|
||||||
|
<CaseViewActions
|
||||||
|
caseData={basicCase}
|
||||||
|
currentExternalIncident={{
|
||||||
|
...basicPush,
|
||||||
|
firstPushIndex: 5,
|
||||||
|
lastPushIndex: 5,
|
||||||
|
commentsToUpdate: [],
|
||||||
|
hasDataToPush: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TestProviders>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeFalsy();
|
||||||
|
|
||||||
|
wrapper
|
||||||
|
.find('button[data-test-subj="property-actions-ellipses"]')
|
||||||
|
.first()
|
||||||
|
.simulate('click');
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('[data-test-subj="property-actions-popout"]')
|
||||||
|
.first()
|
||||||
|
.prop('aria-label')
|
||||||
|
).toEqual(i18n.VIEW_INCIDENT(basicPush.externalTitle));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,13 +13,19 @@ import { ConfirmDeleteCaseModal } from '../confirm_delete_case';
|
||||||
import { SiemPageName } from '../../../home/types';
|
import { SiemPageName } from '../../../home/types';
|
||||||
import { PropertyActions } from '../property_actions';
|
import { PropertyActions } from '../property_actions';
|
||||||
import { Case } from '../../../../containers/case/types';
|
import { Case } from '../../../../containers/case/types';
|
||||||
|
import { CaseService } from '../../../../containers/case/use_get_case_user_actions';
|
||||||
|
|
||||||
interface CaseViewActions {
|
interface CaseViewActions {
|
||||||
caseData: Case;
|
caseData: Case;
|
||||||
|
currentExternalIncident: CaseService | null;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CaseViewActionsComponent: React.FC<CaseViewActions> = ({ caseData, disabled = false }) => {
|
const CaseViewActionsComponent: React.FC<CaseViewActions> = ({
|
||||||
|
caseData,
|
||||||
|
currentExternalIncident,
|
||||||
|
disabled = false,
|
||||||
|
}) => {
|
||||||
// Delete case
|
// Delete case
|
||||||
const {
|
const {
|
||||||
handleToggleModal,
|
handleToggleModal,
|
||||||
|
@ -48,17 +54,17 @@ const CaseViewActionsComponent: React.FC<CaseViewActions> = ({ caseData, disable
|
||||||
label: i18n.DELETE_CASE,
|
label: i18n.DELETE_CASE,
|
||||||
onClick: handleToggleModal,
|
onClick: handleToggleModal,
|
||||||
},
|
},
|
||||||
...(caseData.externalService != null && !isEmpty(caseData.externalService?.externalUrl)
|
...(currentExternalIncident != null && !isEmpty(currentExternalIncident?.externalUrl)
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
iconType: 'popout',
|
iconType: 'popout',
|
||||||
label: i18n.VIEW_INCIDENT(caseData.externalService?.externalTitle ?? ''),
|
label: i18n.VIEW_INCIDENT(currentExternalIncident?.externalTitle ?? ''),
|
||||||
onClick: () => window.open(caseData.externalService?.externalUrl, '_blank'),
|
onClick: () => window.open(currentExternalIncident?.externalUrl, '_blank'),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
],
|
],
|
||||||
[disabled, handleToggleModal, caseData]
|
[disabled, handleToggleModal, currentExternalIncident]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
|
|
|
@ -70,6 +70,7 @@ describe('CaseView ', () => {
|
||||||
|
|
||||||
const defaultUseGetCaseUserActions = {
|
const defaultUseGetCaseUserActions = {
|
||||||
caseUserActions,
|
caseUserActions,
|
||||||
|
caseServices: {},
|
||||||
fetchCaseUserActions,
|
fetchCaseUserActions,
|
||||||
firstIndexPushToService: -1,
|
firstIndexPushToService: -1,
|
||||||
hasDataToPush: false,
|
hasDataToPush: false,
|
||||||
|
|
|
@ -164,6 +164,15 @@ export const CaseComponent = React.memo<CaseProps>(
|
||||||
() => connectors.find(c => c.id === caseData.connectorId)?.name ?? 'none',
|
() => connectors.find(c => c.id === caseData.connectorId)?.name ?? 'none',
|
||||||
[connectors, caseData.connectorId]
|
[connectors, caseData.connectorId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const currentExternalIncident = useMemo(
|
||||||
|
() =>
|
||||||
|
caseServices != null && caseServices[caseData.connectorId] != null
|
||||||
|
? caseServices[caseData.connectorId]
|
||||||
|
: null,
|
||||||
|
[caseServices, caseData.connectorId]
|
||||||
|
);
|
||||||
|
|
||||||
const { pushButton, pushCallouts } = usePushToService({
|
const { pushButton, pushCallouts } = usePushToService({
|
||||||
caseConnectorId: caseData.connectorId,
|
caseConnectorId: caseData.connectorId,
|
||||||
caseConnectorName,
|
caseConnectorName,
|
||||||
|
@ -254,6 +263,7 @@ export const CaseComponent = React.memo<CaseProps>(
|
||||||
title={caseData.title}
|
title={caseData.title}
|
||||||
>
|
>
|
||||||
<CaseStatus
|
<CaseStatus
|
||||||
|
currentExternalIncident={currentExternalIncident}
|
||||||
caseData={caseData}
|
caseData={caseData}
|
||||||
disabled={!userCanCrud}
|
disabled={!userCanCrud}
|
||||||
isLoading={isLoading && updateKey === 'status'}
|
isLoading={isLoading && updateKey === 'status'}
|
||||||
|
|
|
@ -32,7 +32,7 @@ export const getKibanaConfigError = () => ({
|
||||||
title: i18n.PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE,
|
title: i18n.PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE,
|
||||||
description: (
|
description: (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="The kibana.yml file is configured to only allow specific connectors. To enable opening a case in external systems, add .servicenow to the xpack.actions.enabledActiontypes setting. For more information, see {link}."
|
defaultMessage="The kibana.yml file is configured to only allow specific connectors. To enable opening a case in external systems, add .[actionTypeId] (ex: .servicenow | .jira) to the xpack.actions.enabledActiontypes setting. For more information, see {link}."
|
||||||
id="xpack.siem.case.caseView.pushToServiceDisableByConfigDescription"
|
id="xpack.siem.case.caseView.pushToServiceDisableByConfigDescription"
|
||||||
values={{
|
values={{
|
||||||
link: (
|
link: (
|
||||||
|
|
|
@ -33,6 +33,7 @@ describe('usePushToService', () => {
|
||||||
...basicPush,
|
...basicPush,
|
||||||
firstPushIndex: 0,
|
firstPushIndex: 0,
|
||||||
lastPushIndex: 0,
|
lastPushIndex: 0,
|
||||||
|
commentsToUpdate: [],
|
||||||
hasDataToPush: true,
|
hasDataToPush: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -60,7 +60,7 @@ export const PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE = i18n.translate(
|
||||||
export const PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE = i18n.translate(
|
export const PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE = i18n.translate(
|
||||||
'xpack.siem.case.caseView.pushToServiceDisableByConfigTitle',
|
'xpack.siem.case.caseView.pushToServiceDisableByConfigTitle',
|
||||||
{
|
{
|
||||||
defaultMessage: 'Enable ServiceNow in Kibana configuration file',
|
defaultMessage: 'Enable external service in Kibana configuration file',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,7 @@ describe('UserActionTree ', () => {
|
||||||
...basicPush,
|
...basicPush,
|
||||||
firstPushIndex: 0,
|
firstPushIndex: 0,
|
||||||
lastPushIndex: 0,
|
lastPushIndex: 0,
|
||||||
|
commentsToUpdate: [`${ourActions[ourActions.length - 1].commentId}`],
|
||||||
hasDataToPush: true,
|
hasDataToPush: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -111,6 +112,7 @@ describe('UserActionTree ', () => {
|
||||||
...basicPush,
|
...basicPush,
|
||||||
firstPushIndex: 0,
|
firstPushIndex: 0,
|
||||||
lastPushIndex: 0,
|
lastPushIndex: 0,
|
||||||
|
commentsToUpdate: [],
|
||||||
hasDataToPush: false,
|
hasDataToPush: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -13281,8 +13281,6 @@
|
||||||
"xpack.siem.containers.anomalies.stackByJobId": "ジョブ",
|
"xpack.siem.containers.anomalies.stackByJobId": "ジョブ",
|
||||||
"xpack.siem.containers.anomalies.title": "異常",
|
"xpack.siem.containers.anomalies.title": "異常",
|
||||||
"xpack.siem.containers.case.errorTitle": "データの取得中にエラーが発生",
|
"xpack.siem.containers.case.errorTitle": "データの取得中にエラーが発生",
|
||||||
"xpack.siem.containers.case.pushToExterService": "ServiceNow への送信が正常に完了しました",
|
|
||||||
"xpack.siem.containers.case.tagFetchFailDescription": "タグを取得できませんでした",
|
|
||||||
"xpack.siem.containers.detectionEngine.addRuleFailDescription": "ルールを追加できませんでした",
|
"xpack.siem.containers.detectionEngine.addRuleFailDescription": "ルールを追加できませんでした",
|
||||||
"xpack.siem.containers.detectionEngine.createPrePackagedRuleFailDescription": "Elasticから事前にパッケージ化されているルールをインストールすることができませんでした",
|
"xpack.siem.containers.detectionEngine.createPrePackagedRuleFailDescription": "Elasticから事前にパッケージ化されているルールをインストールすることができませんでした",
|
||||||
"xpack.siem.containers.detectionEngine.createPrePackagedRuleSuccesDescription": "Elasticから事前にパッケージ化されているルールをインストールしました",
|
"xpack.siem.containers.detectionEngine.createPrePackagedRuleSuccesDescription": "Elasticから事前にパッケージ化されているルールをインストールしました",
|
||||||
|
|
|
@ -13288,8 +13288,6 @@
|
||||||
"xpack.siem.containers.anomalies.stackByJobId": "作业",
|
"xpack.siem.containers.anomalies.stackByJobId": "作业",
|
||||||
"xpack.siem.containers.anomalies.title": "异常",
|
"xpack.siem.containers.anomalies.title": "异常",
|
||||||
"xpack.siem.containers.case.errorTitle": "提取数据时出错",
|
"xpack.siem.containers.case.errorTitle": "提取数据时出错",
|
||||||
"xpack.siem.containers.case.pushToExterService": "已成功发送到 ServiceNow",
|
|
||||||
"xpack.siem.containers.case.tagFetchFailDescription": "无法提取标记",
|
|
||||||
"xpack.siem.containers.detectionEngine.addRuleFailDescription": "无法添加规则",
|
"xpack.siem.containers.detectionEngine.addRuleFailDescription": "无法添加规则",
|
||||||
"xpack.siem.containers.detectionEngine.createPrePackagedRuleFailDescription": "无法安装 elastic 的预打包规则",
|
"xpack.siem.containers.detectionEngine.createPrePackagedRuleFailDescription": "无法安装 elastic 的预打包规则",
|
||||||
"xpack.siem.containers.detectionEngine.createPrePackagedRuleSuccesDescription": "已安装 elastic 的预打包规则",
|
"xpack.siem.containers.detectionEngine.createPrePackagedRuleSuccesDescription": "已安装 elastic 的预打包规则",
|
||||||
|
|
Loading…
Reference in a new issue