From 528c29ec4a428ee75f4743df8e887127519af8e5 Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Date: Wed, 18 Mar 2020 21:11:28 -0400
Subject: [PATCH 01/17] modify API to get the total comments in _find + Add
user action to track what user are doing + create _pushed api to know when
case have been pushed
---
x-pack/plugins/case/common/api/cases/case.ts | 16 +-
.../plugins/case/common/api/cases/comment.ts | 2 +
x-pack/plugins/case/common/api/cases/index.ts | 1 +
.../case/common/api/cases/user_actions.ts | 54 +++++
x-pack/plugins/case/server/plugin.ts | 9 +
.../routes/api/__fixtures__/mock_router.ts | 5 +-
.../api/__fixtures__/mock_saved_objects.ts | 12 +-
.../api/cases/comments/delete_all_comments.ts | 27 ++-
.../api/cases/comments/delete_comment.ts | 44 ++--
.../routes/api/cases/comments/get_comment.ts | 11 -
.../api/cases/comments/patch_comment.ts | 43 +++-
.../routes/api/cases/comments/post_comment.ts | 36 +--
.../server/routes/api/cases/delete_cases.ts | 27 ++-
.../server/routes/api/cases/find_cases.ts | 39 +++-
.../case/server/routes/api/cases/helpers.ts | 14 +-
.../server/routes/api/cases/patch_cases.ts | 19 +-
.../case/server/routes/api/cases/post_case.ts | 21 +-
.../server/routes/api/cases/pushed_case.ts | 144 ++++++++++++
.../user_actions/get_all_user_actions.ts | 44 ++++
.../plugins/case/server/routes/api/types.ts | 12 +-
.../plugins/case/server/routes/api/utils.ts | 32 ++-
.../case/server/saved_object_types/cases.ts | 36 +++
.../server/saved_object_types/comments.ts | 13 ++
.../case/server/saved_object_types/index.ts | 1 +
.../server/saved_object_types/user_actions.ts | 44 ++++
x-pack/plugins/case/server/services/index.ts | 50 +++-
.../server/services/user_actions/helpers.ts | 215 ++++++++++++++++++
.../server/services/user_actions/index.ts | 77 +++++++
28 files changed, 943 insertions(+), 105 deletions(-)
create mode 100644 x-pack/plugins/case/common/api/cases/user_actions.ts
create mode 100644 x-pack/plugins/case/server/routes/api/cases/pushed_case.ts
create mode 100644 x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts
create mode 100644 x-pack/plugins/case/server/saved_object_types/user_actions.ts
create mode 100644 x-pack/plugins/case/server/services/user_actions/helpers.ts
create mode 100644 x-pack/plugins/case/server/services/user_actions/index.ts
diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts
index 6f58e2702ec5b..3b9fd0c603123 100644
--- a/x-pack/plugins/case/common/api/cases/case.ts
+++ b/x-pack/plugins/case/common/api/cases/case.ts
@@ -20,14 +20,24 @@ const CaseBasicRt = rt.type({
title: rt.string,
});
+const CasePushBasicRt = rt.type({
+ at: rt.union([rt.string, rt.null]),
+ by: rt.union([UserRT, rt.null]),
+ connector_id: rt.union([rt.string, rt.null]),
+ connector_name: rt.union([rt.string, rt.null]),
+ external_id: rt.union([rt.string, rt.null]),
+ external_title: rt.union([rt.string, rt.null]),
+ external_url: rt.union([rt.string, rt.null]),
+});
+
export const CaseAttributesRt = rt.intersection([
CaseBasicRt,
rt.type({
- comment_ids: rt.array(rt.string),
closed_at: rt.union([rt.string, rt.null]),
closed_by: rt.union([UserRT, rt.null]),
created_at: rt.string,
created_by: UserRT,
+ pushed: rt.union([CasePushBasicRt, rt.null]),
updated_at: rt.union([rt.string, rt.null]),
updated_by: rt.union([UserRT, rt.null]),
}),
@@ -35,6 +45,8 @@ export const CaseAttributesRt = rt.intersection([
export const CaseRequestRt = CaseBasicRt;
+export const CasePushRequestRt = CasePushBasicRt;
+
export const CasesFindRequestRt = rt.partial({
tags: rt.union([rt.array(rt.string), rt.string]),
status: StatusRt,
@@ -53,6 +65,7 @@ export const CaseResponseRt = rt.intersection([
CaseAttributesRt,
rt.type({
id: rt.string,
+ totalComment: rt.number,
version: rt.string,
}),
rt.partial({
@@ -85,3 +98,4 @@ export type CasesResponse = rt.TypeOf;
export type CasesFindResponse = rt.TypeOf;
export type CasePatchRequest = rt.TypeOf;
export type CasesPatchRequest = rt.TypeOf;
+export type CasePushRequest = rt.TypeOf;
diff --git a/x-pack/plugins/case/common/api/cases/comment.ts b/x-pack/plugins/case/common/api/cases/comment.ts
index cebfa00425728..4549b1c31a7cf 100644
--- a/x-pack/plugins/case/common/api/cases/comment.ts
+++ b/x-pack/plugins/case/common/api/cases/comment.ts
@@ -17,6 +17,8 @@ export const CommentAttributesRt = rt.intersection([
rt.type({
created_at: rt.string,
created_by: UserRT,
+ pushed_at: rt.union([rt.string, rt.null]),
+ pushed_by: rt.union([UserRT, rt.null]),
updated_at: rt.union([rt.string, rt.null]),
updated_by: rt.union([UserRT, rt.null]),
}),
diff --git a/x-pack/plugins/case/common/api/cases/index.ts b/x-pack/plugins/case/common/api/cases/index.ts
index 5fbee98bc57ad..ffcd4d25eecf5 100644
--- a/x-pack/plugins/case/common/api/cases/index.ts
+++ b/x-pack/plugins/case/common/api/cases/index.ts
@@ -8,3 +8,4 @@ export * from './case';
export * from './configure';
export * from './comment';
export * from './status';
+export * from './user_actions';
diff --git a/x-pack/plugins/case/common/api/cases/user_actions.ts b/x-pack/plugins/case/common/api/cases/user_actions.ts
new file mode 100644
index 0000000000000..910c7d39771b8
--- /dev/null
+++ b/x-pack/plugins/case/common/api/cases/user_actions.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import * as rt from 'io-ts';
+
+import { UserRT } from '../user';
+
+const UserActionFieldRt = rt.array(
+ rt.union([
+ rt.literal('comment'),
+ rt.literal('description'),
+ rt.literal('tags'),
+ rt.literal('title'),
+ rt.literal('status'),
+ ])
+);
+const UserActionRt = rt.union([
+ rt.literal('add'),
+ rt.literal('create'),
+ rt.literal('delete'),
+ rt.literal('update'),
+ rt.literal('push-to-service'),
+]);
+
+// TO DO change state to status
+const CaseUserActionBasicRT = rt.type({
+ action_field: UserActionFieldRt,
+ action: UserActionRt,
+ action_at: rt.string,
+ action_by: UserRT,
+ new_value: rt.union([rt.string, rt.null]),
+ old_value: rt.union([rt.string, rt.null]),
+});
+
+const CaseUserActionResponseRT = rt.intersection([
+ CaseUserActionBasicRT,
+ rt.type({
+ case_id: rt.string,
+ comment_id: rt.union([rt.string, rt.null]),
+ }),
+]);
+
+export const CaseUserActionAttributesRt = CaseUserActionBasicRT;
+
+export const CaseUserActionsResponseRt = rt.array(CaseUserActionResponseRT);
+
+export type CaseUserActionAttributes = rt.TypeOf;
+export type CaseUserActionsResponse = rt.TypeOf;
+
+export type UserAction = rt.TypeOf;
+export type UserActionField = rt.TypeOf;
diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts
index 1d6495c2d81f3..3ccb699b85306 100644
--- a/x-pack/plugins/case/server/plugin.ts
+++ b/x-pack/plugins/case/server/plugin.ts
@@ -12,12 +12,18 @@ import { SecurityPluginSetup } from '../../security/server';
import { ConfigType } from './config';
import { initCaseApi } from './routes/api';
+<<<<<<< HEAD
import {
caseSavedObjectType,
caseConfigureSavedObjectType,
caseCommentSavedObjectType,
} from './saved_object_types';
import { CaseConfigureService, CaseService } from './services';
+=======
+import { caseSavedObjectType, caseCommentSavedObjectType } from './saved_object_types';
+import { CaseService } from './services';
+import { CaseUserActionService } from './services/user_actions';
+>>>>>>> modify API to get the total comments in _find + Add user action to track what user are doing + create _pushed api to know when case have been pushed
function createConfig$(context: PluginInitializerContext) {
return context.config.create().pipe(map(config => config));
@@ -49,6 +55,7 @@ export class CasePlugin {
const caseServicePlugin = new CaseService(this.log);
const caseConfigureServicePlugin = new CaseConfigureService(this.log);
+ const userActionServicePlugin = new CaseUserActionService(this.log);
this.log.debug(
`Setting up Case Workflow with core contract [${Object.keys(
@@ -60,11 +67,13 @@ export class CasePlugin {
authentication: plugins.security.authc,
});
const caseConfigureService = await caseConfigureServicePlugin.setup();
+ const userActionService = await userActionServicePlugin.setup();
const router = core.http.createRouter();
initCaseApi({
caseConfigureService,
caseService,
+ userActionService,
router,
});
}
diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts
index bc41ddbeff1f9..0945ded8d57c3 100644
--- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts
+++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts
@@ -6,7 +6,7 @@
import { IRouter } from 'kibana/server';
import { loggingServiceMock, httpServiceMock } from '../../../../../../../src/core/server/mocks';
-import { CaseService, CaseConfigureService } from '../../../services';
+import { CaseService, CaseConfigureService, CaseUserActionService } from '../../../services';
import { authenticationMock } from '../__fixtures__';
import { RouteDeps } from '../types';
@@ -22,16 +22,19 @@ export const createRoute = async (
const caseServicePlugin = new CaseService(log);
const caseConfigureServicePlugin = new CaseConfigureService(log);
+ const userActionServicePlugin = new CaseUserActionService(log);
const caseService = await caseServicePlugin.setup({
authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(),
});
const caseConfigureService = await caseConfigureServicePlugin.setup();
+ const userActionService = await userActionServicePlugin.setup();
api({
caseConfigureService,
caseService,
router,
+ userActionService,
});
return router[method].mock.calls[0][1];
diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts
index 5aa8b93f17b08..d3b7051b0defc 100644
--- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts
+++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts
@@ -14,7 +14,6 @@ export const mockCases: Array> = [
attributes: {
closed_at: null,
closed_by: null,
- comment_ids: ['mock-comment-1'],
created_at: '2019-11-25T21:54:48.952Z',
created_by: {
full_name: 'elastic',
@@ -22,6 +21,7 @@ export const mockCases: Array> = [
username: 'elastic',
},
description: 'This is a brand new case of a bad meanie defacing data',
+ pushed: null,
title: 'Super Bad Security Issue',
status: 'open',
tags: ['defacement'],
@@ -42,7 +42,6 @@ export const mockCases: Array> = [
attributes: {
closed_at: null,
closed_by: null,
- comment_ids: [],
created_at: '2019-11-25T22:32:00.900Z',
created_by: {
full_name: 'elastic',
@@ -50,6 +49,7 @@ export const mockCases: Array> = [
username: 'elastic',
},
description: 'Oh no, a bad meanie destroying data!',
+ pushed: null,
title: 'Damaging Data Destruction Detected',
status: 'open',
tags: ['Data Destruction'],
@@ -70,7 +70,6 @@ export const mockCases: Array> = [
attributes: {
closed_at: null,
closed_by: null,
- comment_ids: [],
created_at: '2019-11-25T22:32:17.947Z',
created_by: {
full_name: 'elastic',
@@ -78,6 +77,7 @@ export const mockCases: Array> = [
username: 'elastic',
},
description: 'Oh no, a bad meanie going LOLBins all over the place!',
+ pushed: null,
title: 'Another bad one',
status: 'open',
tags: ['LOLBins'],
@@ -147,6 +147,8 @@ export const mockCaseComments: Array> = [
email: 'testemail@elastic.co',
username: 'elastic',
},
+ pushed_at: null,
+ pushed_by: null,
updated_at: '2019-11-25T21:55:00.177Z',
updated_by: {
full_name: 'elastic',
@@ -175,6 +177,8 @@ export const mockCaseComments: Array> = [
email: 'testemail@elastic.co',
username: 'elastic',
},
+ pushed_at: null,
+ pushed_by: null,
updated_at: '2019-11-25T21:55:14.633Z',
updated_by: {
full_name: 'elastic',
@@ -204,6 +208,8 @@ export const mockCaseComments: Array> = [
email: 'testemail@elastic.co',
username: 'elastic',
},
+ pushed_at: null,
+ pushed_by: null,
updated_at: '2019-11-25T22:32:30.608Z',
updated_by: {
full_name: 'elastic',
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts
index 00d06bfdd2677..8f0e616ceacbb 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts
@@ -5,10 +5,12 @@
*/
import { schema } from '@kbn/config-schema';
+
+import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers';
import { RouteDeps } from '../../types';
import { wrapError } from '../../utils';
-export function initDeleteAllCommentsApi({ caseService, router }: RouteDeps) {
+export function initDeleteAllCommentsApi({ caseService, router, userActionService }: RouteDeps) {
router.delete(
{
path: '/api/cases/{case_id}/comments',
@@ -21,6 +23,8 @@ export function initDeleteAllCommentsApi({ caseService, router }: RouteDeps) {
async (context, request, response) => {
try {
const client = context.core.savedObjects.client;
+ const deletedBy = await caseService.getUser({ request, response });
+ const deleteDate = new Date().toISOString();
const comments = await caseService.getAllCaseComments({
client: context.core.savedObjects.client,
@@ -35,15 +39,18 @@ export function initDeleteAllCommentsApi({ caseService, router }: RouteDeps) {
)
);
- const updateCase = {
- comment_ids: [],
- };
- await caseService.patchCase({
- client: context.core.savedObjects.client,
- caseId: request.params.case_id,
- updatedAttributes: {
- ...updateCase,
- },
+ await userActionService.postUserActions({
+ client,
+ actions: comments.saved_objects.map(comment =>
+ buildCommentUserActionItem({
+ action: 'delete',
+ actionAt: deleteDate,
+ actionBy: deletedBy,
+ caseId: request.params.case_id,
+ commentId: comment.id,
+ fields: ['comment'],
+ })
+ ),
});
return response.ok({ body: 'true' });
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts
index 85c4701f82e1d..8587d24a85102 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts
@@ -6,10 +6,13 @@
import Boom from 'boom';
import { schema } from '@kbn/config-schema';
+
+import { CASE_SAVED_OBJECT } from '../../../../saved_object_types';
+import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers';
import { RouteDeps } from '../../types';
import { wrapError } from '../../utils';
-export function initDeleteCommentApi({ caseService, router }: RouteDeps) {
+export function initDeleteCommentApi({ caseService, router, userActionService }: RouteDeps) {
router.delete(
{
path: '/api/cases/{case_id}/comments/{comment_id}',
@@ -23,14 +26,22 @@ export function initDeleteCommentApi({ caseService, router }: RouteDeps) {
async (context, request, response) => {
try {
const client = context.core.savedObjects.client;
- const myCase = await caseService.getCase({
+ const deletedBy = await caseService.getUser({ request, response });
+ const deleteDate = new Date().toISOString();
+
+ const myComment = await caseService.getComment({
client: context.core.savedObjects.client,
- caseId: request.params.case_id,
+ commentId: request.params.comment_id,
});
- if (!myCase.attributes.comment_ids.includes(request.params.comment_id)) {
+ if (myComment == null) {
+ throw Boom.notFound(`This comment ${request.params.comment_id} does not exist anymore.`);
+ }
+
+ const caseRef = myComment.references.find(c => c.type === CASE_SAVED_OBJECT);
+ if (caseRef == null || (caseRef != null && caseRef.id !== request.params.case_id)) {
throw Boom.notFound(
- `This comment ${request.params.comment_id} does not exist in ${myCase.attributes.title} (id: ${request.params.case_id}).`
+ `This comment ${request.params.comment_id} does not exist in ${request.params.case_id}).`
);
}
@@ -39,17 +50,18 @@ export function initDeleteCommentApi({ caseService, router }: RouteDeps) {
commentId: request.params.comment_id,
});
- const updateCase = {
- comment_ids: myCase.attributes.comment_ids.filter(
- cId => cId !== request.params.comment_id
- ),
- };
- await caseService.patchCase({
- client: context.core.savedObjects.client,
- caseId: request.params.case_id,
- updatedAttributes: {
- ...updateCase,
- },
+ await userActionService.postUserActions({
+ client,
+ actions: [
+ buildCommentUserActionItem({
+ action: 'delete',
+ actionAt: deleteDate,
+ actionBy: deletedBy,
+ caseId: request.params.case_id,
+ commentId: request.params.comment_id,
+ fields: ['comment'],
+ }),
+ ],
});
return response.ok({ body: 'true' });
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts
index 06619abae8487..24f44a5f5129b 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts
@@ -5,7 +5,6 @@
*/
import { schema } from '@kbn/config-schema';
-import Boom from 'boom';
import { CommentResponseRt } from '../../../../../common/api';
import { RouteDeps } from '../../types';
@@ -25,16 +24,6 @@ export function initGetCommentApi({ caseService, router }: RouteDeps) {
async (context, request, response) => {
try {
const client = context.core.savedObjects.client;
- const myCase = await caseService.getCase({
- client,
- caseId: request.params.case_id,
- });
-
- if (!myCase.attributes.comment_ids.includes(request.params.comment_id)) {
- throw Boom.notFound(
- `This comment ${request.params.comment_id} does not exist in ${myCase.attributes.title} (id: ${request.params.case_id}).`
- );
- }
const comment = await caseService.getComment({
client,
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts
index c14a94e84e51c..ded5a8eefc03a 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts
@@ -11,11 +11,12 @@ import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { CommentPatchRequestRt, CommentResponseRt, throwErrors } from '../../../../../common/api';
-
+import { CASE_SAVED_OBJECT } from '../../../../saved_object_types';
+import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers';
import { RouteDeps } from '../../types';
import { escapeHatch, wrapError, flattenCommentSavedObject } from '../../utils';
-export function initPatchCommentApi({ caseService, router }: RouteDeps) {
+export function initPatchCommentApi({ caseService, router, userActionService }: RouteDeps) {
router.patch(
{
path: '/api/cases/{case_id}/comments',
@@ -28,27 +29,28 @@ export function initPatchCommentApi({ caseService, router }: RouteDeps) {
},
async (context, request, response) => {
try {
+ const client = context.core.savedObjects.client;
const query = pipe(
CommentPatchRequestRt.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
- const myCase = await caseService.getCase({
+ const myComment = await caseService.getComment({
client: context.core.savedObjects.client,
- caseId: request.params.case_id,
+ commentId: query.id,
});
- if (!myCase.attributes.comment_ids.includes(query.id)) {
+ if (myComment == null) {
+ throw Boom.notFound(`This comment ${query.id} does not exist anymore.`);
+ }
+
+ const caseRef = myComment.references.find(c => c.type === CASE_SAVED_OBJECT);
+ if (caseRef == null || (caseRef != null && caseRef.id !== request.params.case_id)) {
throw Boom.notFound(
- `This comment ${query.id} does not exist in ${myCase.attributes.title} (id: ${request.params.case_id}).`
+ `This comment ${query.id} does not exist in ${request.params.case_id}).`
);
}
- const myComment = await caseService.getComment({
- client: context.core.savedObjects.client,
- commentId: query.id,
- });
-
if (query.version !== myComment.version) {
throw Boom.conflict(
'This case has been updated. Please refresh before saving additional updates.'
@@ -56,18 +58,35 @@ export function initPatchCommentApi({ caseService, router }: RouteDeps) {
}
const updatedBy = await caseService.getUser({ request, response });
+ const updatedDate = new Date().toISOString();
const { email, full_name, username } = updatedBy;
const updatedComment = await caseService.patchComment({
client: context.core.savedObjects.client,
commentId: query.id,
updatedAttributes: {
comment: query.comment,
- updated_at: new Date().toISOString(),
+ updated_at: updatedDate,
updated_by: { email, full_name, username },
},
version: query.version,
});
+ await userActionService.postUserActions({
+ client,
+ actions: [
+ buildCommentUserActionItem({
+ action: 'update',
+ actionAt: updatedDate,
+ actionBy: updatedBy,
+ caseId: request.params.case_id,
+ commentId: updatedComment.id,
+ fields: ['comment'],
+ newValue: query.comment,
+ oldValue: myComment.attributes.comment,
+ }),
+ ],
+ });
+
return response.ok({
body: CommentResponseRt.encode(
flattenCommentSavedObject({
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts
index 9e82a8ffaaec7..4c5405a1aef5e 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts
@@ -12,6 +12,7 @@ import { identity } from 'fp-ts/lib/function';
import { CommentRequestRt, CommentResponseRt, throwErrors } from '../../../../../common/api';
import { CASE_SAVED_OBJECT } from '../../../../saved_object_types';
+import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers';
import {
escapeHatch,
transformNewComment,
@@ -20,7 +21,7 @@ import {
} from '../../utils';
import { RouteDeps } from '../../types';
-export function initPostCommentApi({ caseService, router }: RouteDeps) {
+export function initPostCommentApi({ caseService, router, userActionService }: RouteDeps) {
router.post(
{
path: '/api/cases/{case_id}/comments',
@@ -33,21 +34,17 @@ export function initPostCommentApi({ caseService, router }: RouteDeps) {
},
async (context, request, response) => {
try {
+ const client = context.core.savedObjects.client;
const query = pipe(
CommentRequestRt.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
- const myCase = await caseService.getCase({
- client: context.core.savedObjects.client,
- caseId: request.params.case_id,
- });
-
const createdBy = await caseService.getUser({ request, response });
const createdDate = new Date().toISOString();
const newComment = await caseService.postNewComment({
- client: context.core.savedObjects.client,
+ client,
attributes: transformNewComment({
createdDate,
...query,
@@ -57,21 +54,24 @@ export function initPostCommentApi({ caseService, router }: RouteDeps) {
{
type: CASE_SAVED_OBJECT,
name: `associated-${CASE_SAVED_OBJECT}`,
- id: myCase.id,
+ id: request.params.case_id,
},
],
});
- const updateCase = {
- comment_ids: [...myCase.attributes.comment_ids, newComment.id],
- };
-
- await caseService.patchCase({
- client: context.core.savedObjects.client,
- caseId: request.params.case_id,
- updatedAttributes: {
- ...updateCase,
- },
+ await userActionService.postUserActions({
+ client,
+ actions: [
+ buildCommentUserActionItem({
+ action: 'create',
+ actionAt: createdDate,
+ actionBy: createdBy,
+ caseId: request.params.case_id,
+ commentId: newComment.id,
+ fields: ['comment'],
+ newValue: query.comment,
+ }),
+ ],
});
return response.ok({
diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts
index 559a477a83a6c..ea97122bd322c 100644
--- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts
@@ -5,10 +5,12 @@
*/
import { schema } from '@kbn/config-schema';
+
+import { buildCaseUserActionItem } from '../../../services/user_actions/helpers';
import { RouteDeps } from '../types';
import { wrapError } from '../utils';
-export function initDeleteCasesApi({ caseService, router }: RouteDeps) {
+export function initDeleteCasesApi({ caseService, router, userActionService }: RouteDeps) {
router.delete(
{
path: '/api/cases',
@@ -20,10 +22,11 @@ export function initDeleteCasesApi({ caseService, router }: RouteDeps) {
},
async (context, request, response) => {
try {
+ const client = context.core.savedObjects.client;
await Promise.all(
request.query.ids.map(id =>
caseService.deleteCase({
- client: context.core.savedObjects.client,
+ client,
caseId: id,
})
)
@@ -31,7 +34,7 @@ export function initDeleteCasesApi({ caseService, router }: RouteDeps) {
const comments = await Promise.all(
request.query.ids.map(id =>
caseService.getAllCaseComments({
- client: context.core.savedObjects.client,
+ client,
caseId: id,
})
)
@@ -43,7 +46,7 @@ export function initDeleteCasesApi({ caseService, router }: RouteDeps) {
Promise.all(
c.saved_objects.map(({ id }) =>
caseService.deleteComment({
- client: context.core.savedObjects.client,
+ client,
commentId: id,
})
)
@@ -51,6 +54,22 @@ export function initDeleteCasesApi({ caseService, router }: RouteDeps) {
)
);
}
+ const deletedBy = await caseService.getUser({ request, response });
+ const deleteDate = new Date().toISOString();
+
+ await userActionService.postUserActions({
+ client,
+ actions: request.query.ids.map(id =>
+ buildCaseUserActionItem({
+ action: 'create',
+ actionAt: deleteDate,
+ actionBy: deletedBy,
+ caseId: id,
+ fields: ['comment', 'description', 'status', 'tags', 'title'],
+ })
+ ),
+ });
+
return response.ok({ body: 'true' });
} catch (error) {
return response.customError(wrapError(error));
diff --git a/x-pack/plugins/case/server/routes/api/cases/find_cases.ts b/x-pack/plugins/case/server/routes/api/cases/find_cases.ts
index 76a1992c64270..e7b2044f2badf 100644
--- a/x-pack/plugins/case/server/routes/api/cases/find_cases.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/find_cases.ts
@@ -13,7 +13,7 @@ import { identity } from 'fp-ts/lib/function';
import { isEmpty } from 'lodash';
import { CasesFindResponseRt, CasesFindRequestRt, throwErrors } from '../../../../common/api';
import { transformCases, sortToSnake, wrapError, escapeHatch } from '../utils';
-import { RouteDeps } from '../types';
+import { RouteDeps, TotalCommentByCase } from '../types';
import { CASE_SAVED_OBJECT } from '../../../saved_object_types';
const combineFilters = (filters: string[], operator: 'OR' | 'AND'): string =>
@@ -97,9 +97,44 @@ export function initFindCasesApi({ caseService, router }: RouteDeps) {
caseService.findCases(argsOpenCases),
caseService.findCases(argsClosedCases),
]);
+
+ const totalCommentsFindByCases = await Promise.all(
+ cases.saved_objects.map(c =>
+ caseService.getAllCaseComments({
+ client,
+ caseId: c.id,
+ options: {
+ fields: [],
+ page: 1,
+ perPage: 1,
+ },
+ })
+ )
+ );
+
+ const totalCommentsByCases = totalCommentsFindByCases.reduce(
+ (acc, itemFind) => {
+ if (itemFind.saved_objects.length > 0) {
+ const caseId =
+ itemFind.saved_objects[0].references.find(r => r.type === CASE_SAVED_OBJECT)?.id ??
+ null;
+ if (caseId != null) {
+ return [...acc, { caseId, totalComments: itemFind.total }];
+ }
+ }
+ return [...acc];
+ },
+ []
+ );
+
return response.ok({
body: CasesFindResponseRt.encode(
- transformCases(cases, openCases.total ?? 0, closesCases.total ?? 0)
+ transformCases(
+ cases,
+ openCases.total ?? 0,
+ closesCases.total ?? 0,
+ totalCommentsByCases
+ )
),
});
} catch (error) {
diff --git a/x-pack/plugins/case/server/routes/api/cases/helpers.ts b/x-pack/plugins/case/server/routes/api/cases/helpers.ts
index 3bf46cadc83c8..537568310a06f 100644
--- a/x-pack/plugins/case/server/routes/api/cases/helpers.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/helpers.ts
@@ -8,6 +8,13 @@ import { difference, get } from 'lodash';
import { CaseAttributes, CasePatchRequest } from '../../../../common/api';
+export const isTwoArraysDifference = (origVal: unknown, updatedVal: unknown) =>
+ origVal != null &&
+ updatedVal != null &&
+ Array.isArray(updatedVal) &&
+ Array.isArray(origVal) &&
+ difference(origVal, updatedVal).length !== 0;
+
export const getCaseToUpdate = (
currentCase: CaseAttributes,
queryCase: CasePatchRequest
@@ -15,12 +22,7 @@ export const getCaseToUpdate = (
Object.entries(queryCase).reduce(
(acc, [key, value]) => {
const currentValue = get(currentCase, key);
- if (
- currentValue != null &&
- Array.isArray(value) &&
- Array.isArray(currentValue) &&
- difference(value, currentValue).length !== 0
- ) {
+ if (isTwoArraysDifference(value, currentValue)) {
return {
...acc,
[key]: value,
diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts
index 4aa0d8daf5b34..73bd38fd04b88 100644
--- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts
@@ -18,8 +18,9 @@ import {
import { escapeHatch, wrapError, flattenCaseSavedObject } from '../utils';
import { RouteDeps } from '../types';
import { getCaseToUpdate } from './helpers';
+import { buildCaseUserActions } from '../../../services/user_actions/helpers';
-export function initPatchCasesApi({ caseService, router }: RouteDeps) {
+export function initPatchCasesApi({ caseService, router, userActionService }: RouteDeps) {
router.patch(
{
path: '/api/cases',
@@ -29,12 +30,13 @@ export function initPatchCasesApi({ caseService, router }: RouteDeps) {
},
async (context, request, response) => {
try {
+ const client = context.core.savedObjects.client;
const query = pipe(
CasesPatchRequestRt.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
const myCases = await caseService.getCases({
- client: context.core.savedObjects.client,
+ client,
caseIds: query.cases.map(q => q.id),
});
let nonExistingCases: CasePatchRequest[] = [];
@@ -76,7 +78,7 @@ export function initPatchCasesApi({ caseService, router }: RouteDeps) {
const { email, full_name, username } = updatedBy;
const updatedDt = new Date().toISOString();
const updatedCases = await caseService.patchCases({
- client: context.core.savedObjects.client,
+ client,
cases: updateFilterCases.map(thisCase => {
const { id: caseId, version, ...updateCaseAttributes } = thisCase;
let closedInfo = {};
@@ -116,6 +118,17 @@ export function initPatchCasesApi({ caseService, router }: RouteDeps) {
references: myCase.references,
});
});
+
+ await userActionService.postUserActions({
+ client,
+ actions: buildCaseUserActions({
+ originalCases: myCases.saved_objects,
+ updatedCases: updatedCases.saved_objects,
+ actionDate: updatedDt,
+ actionBy: { full_name, username },
+ }),
+ });
+
return response.ok({
body: CasesResponseRt.encode(returnUpdatedCase),
});
diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.ts
index 9e854c3178e1e..0c881f1b11c87 100644
--- a/x-pack/plugins/case/server/routes/api/cases/post_case.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/post_case.ts
@@ -12,9 +12,10 @@ import { identity } from 'fp-ts/lib/function';
import { flattenCaseSavedObject, transformNewCase, wrapError, escapeHatch } from '../utils';
import { CaseRequestRt, throwErrors, CaseResponseRt } from '../../../../common/api';
+import { buildCaseUserActionItem } from '../../../services/user_actions/helpers';
import { RouteDeps } from '../types';
-export function initPostCaseApi({ caseService, router }: RouteDeps) {
+export function initPostCaseApi({ caseService, router, userActionService }: RouteDeps) {
router.post(
{
path: '/api/cases',
@@ -24,6 +25,7 @@ export function initPostCaseApi({ caseService, router }: RouteDeps) {
},
async (context, request, response) => {
try {
+ const client = context.core.savedObjects.client;
const query = pipe(
CaseRequestRt.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
@@ -32,13 +34,28 @@ export function initPostCaseApi({ caseService, router }: RouteDeps) {
const createdBy = await caseService.getUser({ request, response });
const createdDate = new Date().toISOString();
const newCase = await caseService.postNewCase({
- client: context.core.savedObjects.client,
+ client,
attributes: transformNewCase({
createdDate,
newCase: query,
...createdBy,
}),
});
+
+ await userActionService.postUserActions({
+ client,
+ actions: [
+ buildCaseUserActionItem({
+ action: 'create',
+ actionAt: createdDate,
+ actionBy: createdBy,
+ caseId: newCase.id,
+ fields: ['description', 'status', 'tags', 'title'],
+ newValue: JSON.stringify(query),
+ }),
+ ],
+ });
+
return response.ok({ body: CaseResponseRt.encode(flattenCaseSavedObject(newCase, [])) });
} catch (error) {
return response.customError(wrapError(error));
diff --git a/x-pack/plugins/case/server/routes/api/cases/pushed_case.ts b/x-pack/plugins/case/server/routes/api/cases/pushed_case.ts
new file mode 100644
index 0000000000000..4297be93899da
--- /dev/null
+++ b/x-pack/plugins/case/server/routes/api/cases/pushed_case.ts
@@ -0,0 +1,144 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { schema } from '@kbn/config-schema';
+import Boom from 'boom';
+import { pipe } from 'fp-ts/lib/pipeable';
+import { fold } from 'fp-ts/lib/Either';
+import { identity } from 'fp-ts/lib/function';
+
+import { flattenCaseSavedObject, wrapError, escapeHatch } from '../utils';
+
+import { CasePushRequestRt, CaseResponseRt, throwErrors } from '../../../../common/api';
+import { buildCaseUserActionItem } from '../../../services/user_actions/helpers';
+import { RouteDeps } from '../types';
+
+export function initPostCaseApi({ caseService, router, userActionService }: RouteDeps) {
+ router.post(
+ {
+ path: '/api/cases/{case_id}/_pushed',
+ validate: {
+ params: schema.object({
+ case_id: schema.string(),
+ }),
+ query: escapeHatch,
+ },
+ },
+ async (context, request, response) => {
+ try {
+ const client = context.core.savedObjects.client;
+ const caseId = request.params.case_id;
+ const query = pipe(
+ CasePushRequestRt.decode(request.query),
+ fold(throwErrors(Boom.badRequest), identity)
+ );
+ const pushedBy = await caseService.getUser({ request, response });
+ const pushedDate = new Date().toISOString();
+
+ const myCase = await caseService.getCase({
+ client: context.core.savedObjects.client,
+ caseId: request.params.case_id,
+ });
+
+ const totalCommentsFindByCases = await caseService.getAllCaseComments({
+ client,
+ caseId,
+ options: {
+ fields: [],
+ page: 1,
+ perPage: 1,
+ },
+ });
+
+ const comments = await caseService.getAllCaseComments({
+ client,
+ caseId,
+ options: {
+ fields: [],
+ page: 1,
+ perPage: totalCommentsFindByCases.total,
+ },
+ });
+
+ const pushed = {
+ at: pushedDate,
+ by: pushedBy,
+ ...query,
+ };
+
+ const [updatedCase, updatedComments] = await Promise.all([
+ caseService.patchCase({
+ client,
+ caseId,
+ updatedAttributes: {
+ pushed,
+ updated_at: pushedDate,
+ updated_by: pushedBy,
+ },
+ version: myCase.version,
+ }),
+ caseService.patchComments({
+ client,
+ comments: comments.saved_objects.map(comment => ({
+ commentId: comment.id,
+ updatedAttributes: {
+ pushed_at: pushedDate,
+ pushed_by: pushedBy,
+ updated_at: pushedDate,
+ updated_by: pushedBy,
+ },
+ version: comment.version,
+ })),
+ }),
+ ]);
+
+ await userActionService.postUserActions({
+ client,
+ actions: [
+ buildCaseUserActionItem({
+ action: 'push-to-service',
+ actionAt: pushedDate,
+ actionBy: pushedBy,
+ caseId,
+ fields: [],
+ newValue: JSON.stringify(pushed),
+ }),
+ ],
+ });
+
+ return response.ok({
+ body: CaseResponseRt.encode(
+ flattenCaseSavedObject(
+ {
+ ...myCase,
+ ...updatedCase,
+ attributes: { ...myCase.attributes, ...updatedCase?.attributes },
+ references: myCase.references,
+ },
+ comments.saved_objects.map(origComment => {
+ const updatedComment = updatedComments.saved_objects.find(
+ c => c.id === origComment.id
+ );
+ return {
+ ...origComment,
+ ...updatedComment,
+ attributes: {
+ ...origComment.attributes,
+ ...updatedComment?.attributes,
+ },
+ version: updatedComment?.version ?? origComment.version,
+ references: origComment?.references ?? [],
+ };
+ })
+ )
+ ),
+ });
+ } catch (error) {
+ return response.customError(wrapError(error));
+ }
+ }
+ );
+}
diff --git a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts
new file mode 100644
index 0000000000000..c5ac87f56d3de
--- /dev/null
+++ b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { schema } from '@kbn/config-schema';
+
+import { CaseUserActionsResponseRt } from '../../../../../common/api';
+import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT } from '../../../../saved_object_types';
+import { RouteDeps } from '../../types';
+import { wrapError } from '../../utils';
+
+export function initGetAllUserActionsApi({ userActionService, router }: RouteDeps) {
+ router.get(
+ {
+ path: '/api/cases/{case_id}/user_actions',
+ validate: {
+ params: schema.object({
+ case_id: schema.string(),
+ }),
+ },
+ },
+ async (context, request, response) => {
+ try {
+ const userActions = await userActionService.getUserActions({
+ client: context.core.savedObjects.client,
+ caseId: request.params.case_id,
+ });
+ return response.ok({
+ body: CaseUserActionsResponseRt.encode(
+ userActions.saved_objects.map(ua => ({
+ ...ua.attributes,
+ case_id: ua.references.find(r => r.type === CASE_SAVED_OBJECT)?.id ?? '',
+ comment_id: ua.references.find(r => r.type === CASE_COMMENT_SAVED_OBJECT)?.id ?? null,
+ }))
+ ),
+ });
+ } catch (error) {
+ return response.customError(wrapError(error));
+ }
+ }
+ );
+}
diff --git a/x-pack/plugins/case/server/routes/api/types.ts b/x-pack/plugins/case/server/routes/api/types.ts
index 7af3e7b70d96f..e532a7b618b5c 100644
--- a/x-pack/plugins/case/server/routes/api/types.ts
+++ b/x-pack/plugins/case/server/routes/api/types.ts
@@ -5,11 +5,16 @@
*/
import { IRouter } from 'src/core/server';
-import { CaseConfigureServiceSetup, CaseServiceSetup } from '../../services';
+import {
+ CaseConfigureServiceSetup,
+ CaseServiceSetup,
+ CaseUserActionServiceSetup,
+} from '../../services';
export interface RouteDeps {
caseConfigureService: CaseConfigureServiceSetup;
caseService: CaseServiceSetup;
+ userActionService: CaseUserActionServiceSetup;
router: IRouter;
}
@@ -18,3 +23,8 @@ export enum SortFieldCase {
createdAt = 'created_at',
status = 'status',
}
+
+export interface TotalCommentByCase {
+ caseId: string;
+ totalComments: number;
+}
diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts
index 19dbb024d1e0b..27553b25f89fd 100644
--- a/x-pack/plugins/case/server/routes/api/utils.ts
+++ b/x-pack/plugins/case/server/routes/api/utils.ts
@@ -22,7 +22,8 @@ import {
CommentsResponse,
CommentAttributes,
} from '../../../common/api';
-import { SortFieldCase } from './types';
+
+import { SortFieldCase, TotalCommentByCase } from './types';
export const transformNewCase = ({
createdDate,
@@ -37,11 +38,11 @@ export const transformNewCase = ({
newCase: CaseRequest;
username: string;
}): CaseAttributes => ({
- closed_at: newCase.status === 'closed' ? createdDate : null,
- closed_by: newCase.status === 'closed' ? { email, full_name, username } : null,
- comment_ids: [],
+ closed_at: null,
+ closed_by: null,
created_at: createdDate,
created_by: { email, full_name, username },
+ pushed: null,
updated_at: null,
updated_by: null,
...newCase,
@@ -64,6 +65,8 @@ export const transformNewComment = ({
comment,
created_at: createdDate,
created_by: { email, full_name, username },
+ pushed_at: null,
+ pushed_by: null,
updated_at: null,
updated_by: null,
});
@@ -81,30 +84,41 @@ export function wrapError(error: any): CustomHttpResponseOptions
export const transformCases = (
cases: SavedObjectsFindResponse,
countOpenCases: number,
- countClosedCases: number
+ countClosedCases: number,
+ totalCommentByCase: TotalCommentByCase[]
): CasesFindResponse => ({
page: cases.page,
per_page: cases.per_page,
total: cases.total,
- cases: flattenCaseSavedObjects(cases.saved_objects),
+ cases: flattenCaseSavedObjects(cases.saved_objects, totalCommentByCase),
count_open_cases: countOpenCases,
count_closed_cases: countClosedCases,
});
export const flattenCaseSavedObjects = (
- savedObjects: SavedObjectsFindResponse['saved_objects']
+ savedObjects: SavedObjectsFindResponse['saved_objects'],
+ totalCommentByCase: TotalCommentByCase[]
): CaseResponse[] =>
savedObjects.reduce((acc: CaseResponse[], savedObject: SavedObject) => {
- return [...acc, flattenCaseSavedObject(savedObject, [])];
+ return [
+ ...acc,
+ flattenCaseSavedObject(
+ savedObject,
+ [],
+ totalCommentByCase.find(tc => tc.caseId === savedObject.id)?.totalComments ?? 0
+ ),
+ ];
}, []);
export const flattenCaseSavedObject = (
savedObject: SavedObject,
- comments: Array> = []
+ comments: Array> = [],
+ totalComment: number = 0
): CaseResponse => ({
id: savedObject.id,
version: savedObject.version ?? '0',
comments: flattenCommentSavedObjects(comments),
+ totalComment: 0,
...savedObject.attributes,
});
diff --git a/x-pack/plugins/case/server/saved_object_types/cases.ts b/x-pack/plugins/case/server/saved_object_types/cases.ts
index 8eab040b9ca9c..b8d91eadddc4c 100644
--- a/x-pack/plugins/case/server/saved_object_types/cases.ts
+++ b/x-pack/plugins/case/server/saved_object_types/cases.ts
@@ -14,6 +14,7 @@ export const caseSavedObjectType: SavedObjectsType = {
namespaceAgnostic: false,
mappings: {
properties: {
+<<<<<<< HEAD
closed_at: {
type: 'date',
},
@@ -33,6 +34,8 @@ export const caseSavedObjectType: SavedObjectsType = {
comment_ids: {
type: 'keyword',
},
+=======
+>>>>>>> modify API to get the total comments in _find + Add user action to track what user are doing + create _pushed api to know when case have been pushed
created_at: {
type: 'date',
},
@@ -52,6 +55,38 @@ export const caseSavedObjectType: SavedObjectsType = {
description: {
type: 'text',
},
+ pushed: {
+ properties: {
+ at: {
+ type: 'date',
+ },
+ by: {
+ properties: {
+ username: {
+ type: 'keyword',
+ },
+ full_name: {
+ type: 'keyword',
+ },
+ },
+ },
+ connector_id: {
+ type: 'keyword',
+ },
+ connector_name: {
+ type: 'keyword',
+ },
+ external_id: {
+ type: 'keyword',
+ },
+ external_title: {
+ type: 'text',
+ },
+ external_url: {
+ type: 'text',
+ },
+ },
+ },
title: {
type: 'keyword',
},
@@ -61,6 +96,7 @@ export const caseSavedObjectType: SavedObjectsType = {
tags: {
type: 'keyword',
},
+
updated_at: {
type: 'date',
},
diff --git a/x-pack/plugins/case/server/saved_object_types/comments.ts b/x-pack/plugins/case/server/saved_object_types/comments.ts
index f52da886e7611..8776dd39b11fa 100644
--- a/x-pack/plugins/case/server/saved_object_types/comments.ts
+++ b/x-pack/plugins/case/server/saved_object_types/comments.ts
@@ -33,6 +33,19 @@ export const caseCommentSavedObjectType: SavedObjectsType = {
},
},
},
+ pushed_at: {
+ type: 'date',
+ },
+ pushed_by: {
+ properties: {
+ username: {
+ type: 'keyword',
+ },
+ full_name: {
+ type: 'keyword',
+ },
+ },
+ },
updated_at: {
type: 'date',
},
diff --git a/x-pack/plugins/case/server/saved_object_types/index.ts b/x-pack/plugins/case/server/saved_object_types/index.ts
index 978b3d35ee5c6..0e4b9fa3e2eee 100644
--- a/x-pack/plugins/case/server/saved_object_types/index.ts
+++ b/x-pack/plugins/case/server/saved_object_types/index.ts
@@ -7,3 +7,4 @@
export { caseSavedObjectType, CASE_SAVED_OBJECT } from './cases';
export { caseConfigureSavedObjectType, CASE_CONFIGURE_SAVED_OBJECT } from './configure';
export { caseCommentSavedObjectType, CASE_COMMENT_SAVED_OBJECT } from './comments';
+export { caseUserActionSavedObjectType, CASE_USER_ACTION_SAVED_OBJECT } from './user_actions';
diff --git a/x-pack/plugins/case/server/saved_object_types/user_actions.ts b/x-pack/plugins/case/server/saved_object_types/user_actions.ts
new file mode 100644
index 0000000000000..7e823e4550d36
--- /dev/null
+++ b/x-pack/plugins/case/server/saved_object_types/user_actions.ts
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObjectsType } from 'src/core/server';
+
+export const CASE_USER_ACTION_SAVED_OBJECT = 'cases-user-actions';
+
+export const caseUserActionSavedObjectType: SavedObjectsType = {
+ name: CASE_USER_ACTION_SAVED_OBJECT,
+ hidden: false,
+ namespaceAgnostic: false,
+ mappings: {
+ properties: {
+ action_field: {
+ type: 'keyword',
+ },
+ action: {
+ type: 'keyword',
+ },
+ action_at: {
+ type: 'date',
+ },
+ action_by: {
+ properties: {
+ username: {
+ type: 'keyword',
+ },
+ full_name: {
+ type: 'keyword',
+ },
+ },
+ },
+ new_value: {
+ type: 'text',
+ },
+ old_value: {
+ type: 'text',
+ },
+ },
+ },
+};
diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts
index 4bbffddf63251..01d55e993ad8a 100644
--- a/x-pack/plugins/case/server/services/index.ts
+++ b/x-pack/plugins/case/server/services/index.ts
@@ -24,11 +24,17 @@ import { readReporters } from './reporters/read_reporters';
import { readTags } from './tags/read_tags';
export { CaseConfigureService, CaseConfigureServiceSetup } from './configure';
+export { CaseUserActionService, CaseUserActionServiceSetup } from './user_actions';
-interface ClientArgs {
+export interface ClientArgs {
client: SavedObjectsClientContract;
}
+interface PushedArgs {
+ pushed_at: string;
+ pushed_by: User;
+}
+
interface GetCaseArgs extends ClientArgs {
caseId: string;
}
@@ -37,7 +43,7 @@ interface GetCasesArgs extends ClientArgs {
caseIds: string[];
}
-interface GetCommentsArgs extends GetCaseArgs {
+interface FindCommentsArgs extends GetCaseArgs {
options?: SavedObjectFindOptions;
}
@@ -47,6 +53,7 @@ interface FindCasesArgs extends ClientArgs {
interface GetCommentArgs extends ClientArgs {
commentId: string;
}
+
interface PostCaseArgs extends ClientArgs {
attributes: CaseAttributes;
}
@@ -58,7 +65,7 @@ interface PostCommentArgs extends ClientArgs {
interface PatchCase {
caseId: string;
- updatedAttributes: Partial;
+ updatedAttributes: Partial;
version?: string;
}
type PatchCaseArgs = PatchCase & ClientArgs;
@@ -68,10 +75,20 @@ interface PatchCasesArgs extends ClientArgs {
}
interface UpdateCommentArgs extends ClientArgs {
commentId: string;
- updatedAttributes: Partial;
+ updatedAttributes: Partial;
version?: string;
}
+interface PatchComment {
+ commentId: string;
+ updatedAttributes: Partial;
+ version?: string;
+}
+
+interface PatchComments extends ClientArgs {
+ comments: PatchComment[];
+}
+
interface GetUserArgs {
request: KibanaRequest;
response: KibanaResponseFactory;
@@ -84,7 +101,7 @@ export interface CaseServiceSetup {
deleteCase(args: GetCaseArgs): Promise<{}>;
deleteComment(args: GetCommentArgs): Promise<{}>;
findCases(args: FindCasesArgs): Promise>;
- getAllCaseComments(args: GetCommentsArgs): Promise>;
+ getAllCaseComments(args: FindCommentsArgs): Promise>;
getCase(args: GetCaseArgs): Promise>;
getCases(args: GetCasesArgs): Promise>;
getComment(args: GetCommentArgs): Promise>;
@@ -96,6 +113,7 @@ export interface CaseServiceSetup {
patchCase(args: PatchCaseArgs): Promise>;
patchCases(args: PatchCasesArgs): Promise>;
patchComment(args: UpdateCommentArgs): Promise>;
+ patchComments(args: PatchComments): Promise>;
}
export class CaseService {
@@ -157,7 +175,7 @@ export class CaseService {
throw error;
}
},
- getAllCaseComments: async ({ client, caseId, options }: GetCommentsArgs) => {
+ getAllCaseComments: async ({ client, caseId, options }: FindCommentsArgs) => {
try {
this.log.debug(`Attempting to GET all comments for case ${caseId}`);
return await client.find({
@@ -261,5 +279,25 @@ export class CaseService {
throw error;
}
},
+ patchComments: async ({ client, comments }: PatchComments) => {
+ try {
+ this.log.debug(
+ `Attempting to UPDATE comments ${comments.map(c => c.commentId).join(', ')}`
+ );
+ return await client.bulkUpdate(
+ comments.map(c => ({
+ type: CASE_COMMENT_SAVED_OBJECT,
+ id: c.commentId,
+ attributes: c.updatedAttributes,
+ version: c.version,
+ }))
+ );
+ } catch (error) {
+ this.log.debug(
+ `Error on UPDATE comments ${comments.map(c => c.commentId).join(', ')}: ${error}`
+ );
+ throw error;
+ }
+ },
});
}
diff --git a/x-pack/plugins/case/server/services/user_actions/helpers.ts b/x-pack/plugins/case/server/services/user_actions/helpers.ts
new file mode 100644
index 0000000000000..3e56cf54ad2c2
--- /dev/null
+++ b/x-pack/plugins/case/server/services/user_actions/helpers.ts
@@ -0,0 +1,215 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObject, SavedObjectsUpdateResponse } from 'kibana/server';
+import { get } from 'lodash';
+
+import {
+ CaseUserActionAttributes,
+ UserAction,
+ UserActionField,
+ CaseAttributes,
+ User,
+} from '../../../common/api';
+import { isTwoArraysDifference } from '../../routes/api/cases/helpers';
+import { UserActionItem } from '.';
+import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT } from '../../saved_object_types';
+
+export const transformNewUserAction = ({
+ actionField,
+ action,
+ actionAt,
+ full_name,
+ newValue = null,
+ oldValue = null,
+ username,
+}: {
+ actionField: UserActionField;
+ action: UserAction;
+ actionAt: string;
+ full_name?: string;
+ newValue?: string | null;
+ oldValue?: string | null;
+ username: string;
+}): CaseUserActionAttributes => ({
+ action_field: actionField,
+ action,
+ action_at: actionAt,
+ action_by: { full_name, username },
+ new_value: newValue,
+ old_value: oldValue,
+});
+
+interface BuildCaseUserAction {
+ action: UserAction;
+ actionAt: string;
+ actionBy: User;
+ caseId: string;
+ fields: UserActionField | unknown[];
+ newValue?: string | unknown;
+ oldValue?: string | unknown;
+}
+
+interface BuildCommentUserActionItem extends BuildCaseUserAction {
+ commentId: string;
+}
+
+export const buildCommentUserActionItem = ({
+ action,
+ actionAt,
+ actionBy,
+ caseId,
+ commentId,
+ fields,
+ newValue,
+ oldValue,
+}: BuildCommentUserActionItem): UserActionItem => ({
+ attributes: transformNewUserAction({
+ actionField: fields as UserActionField,
+ action,
+ actionAt,
+ ...actionBy,
+ newValue: newValue as string,
+ oldValue: oldValue as string,
+ }),
+ references: [
+ {
+ type: CASE_SAVED_OBJECT,
+ name: `associated-${CASE_SAVED_OBJECT}`,
+ id: caseId,
+ },
+ {
+ type: CASE_COMMENT_SAVED_OBJECT,
+ name: `associated-${CASE_COMMENT_SAVED_OBJECT}`,
+ id: commentId,
+ },
+ ],
+});
+
+export const buildCaseUserActionItem = ({
+ action,
+ actionAt,
+ actionBy,
+ caseId,
+ fields,
+ newValue,
+ oldValue,
+}: BuildCaseUserAction): UserActionItem => ({
+ attributes: transformNewUserAction({
+ actionField: fields as UserActionField,
+ action,
+ actionAt,
+ ...actionBy,
+ newValue: newValue as string,
+ oldValue: oldValue as string,
+ }),
+ references: [
+ {
+ type: CASE_SAVED_OBJECT,
+ name: `associated-${CASE_SAVED_OBJECT}`,
+ id: caseId,
+ },
+ ],
+});
+
+interface CompareArray {
+ addedItems: string[];
+ deletedItems: string[];
+}
+const compareArray = ({
+ originalValue,
+ updatedValue,
+}: {
+ originalValue: string[];
+ updatedValue: string[];
+}): CompareArray => {
+ const result: CompareArray = {
+ addedItems: [],
+ deletedItems: [],
+ };
+ originalValue.forEach(origVal => {
+ if (!updatedValue.includes(origVal)) {
+ result.deletedItems = [...result.deletedItems, origVal];
+ }
+ });
+ updatedValue.forEach(updatedVal => {
+ if (!originalValue.includes(updatedVal)) {
+ result.addedItems = [...result.addedItems, updatedVal];
+ }
+ });
+
+ return result;
+};
+
+export const buildCaseUserActions = ({
+ actionDate,
+ actionBy,
+ originalCases,
+ updatedCases,
+}: {
+ actionDate: string;
+ actionBy: User;
+ originalCases: Array>;
+ updatedCases: Array>;
+}): UserActionItem[] =>
+ updatedCases.reduce((acc, updatedItem) => {
+ const originalItem = originalCases.find(oItem => oItem.id === updatedItem.id);
+ if (originalItem != null) {
+ let userActions: UserActionItem[] = [];
+ const updatedFields = Object.keys(updatedItem.attributes);
+ updatedFields.forEach(field => {
+ const origValue = get(originalItem, ['attributes', field]);
+ const updatedValue = get(updatedItem, ['attributes', field]);
+ if (isTwoArraysDifference(origValue, updatedValue)) {
+ const arrayDiff = compareArray({
+ originalValue: origValue as string[],
+ updatedValue: updatedValue as string[],
+ });
+ if (arrayDiff.addedItems.length > 0) {
+ userActions = [
+ ...userActions,
+ buildCaseUserActionItem({
+ action: 'add',
+ actionAt: actionDate,
+ actionBy,
+ caseId: updatedItem.id,
+ fields: [field],
+ newValue: arrayDiff.addedItems.join(', '),
+ }),
+ ];
+ }
+ if (arrayDiff.deletedItems.length > 0) {
+ userActions = [
+ ...userActions,
+ buildCaseUserActionItem({
+ action: 'delete',
+ actionAt: actionDate,
+ actionBy,
+ caseId: updatedItem.id,
+ fields: [field],
+ newValue: arrayDiff.deletedItems.join(', '),
+ }),
+ ];
+ }
+ } else if (origValue !== updatedValue) {
+ userActions = [
+ ...userActions,
+ buildCaseUserActionItem({
+ action: 'update',
+ actionAt: actionDate,
+ actionBy,
+ caseId: updatedItem.id,
+ fields: [field],
+ newValue: updatedValue,
+ oldValue: origValue,
+ }),
+ ];
+ }
+ });
+ return [...acc, ...userActions];
+ }
+ return acc;
+ }, []);
diff --git a/x-pack/plugins/case/server/services/user_actions/index.ts b/x-pack/plugins/case/server/services/user_actions/index.ts
new file mode 100644
index 0000000000000..d14fa13dd8582
--- /dev/null
+++ b/x-pack/plugins/case/server/services/user_actions/index.ts
@@ -0,0 +1,77 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ SavedObjectsFindResponse,
+ Logger,
+ SavedObjectsBulkResponse,
+ SavedObjectReference,
+} from 'kibana/server';
+
+import { CaseUserActionAttributes } from '../../../common/api';
+import { CASE_USER_ACTION_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../../saved_object_types';
+import { ClientArgs } from '..';
+
+interface GetCaseUserActionArgs extends ClientArgs {
+ caseId: string;
+}
+
+export interface UserActionItem {
+ attributes: CaseUserActionAttributes;
+ references: SavedObjectReference[];
+}
+
+interface PostCaseUserActionArgs extends ClientArgs {
+ actions: UserActionItem[];
+}
+
+export interface CaseUserActionServiceSetup {
+ getUserActions(
+ args: GetCaseUserActionArgs
+ ): Promise>;
+ postUserActions(
+ args: PostCaseUserActionArgs
+ ): Promise>;
+}
+
+export class CaseUserActionService {
+ constructor(private readonly log: Logger) {}
+ public setup = async (): Promise => ({
+ getUserActions: async ({ client, caseId }: GetCaseUserActionArgs) => {
+ try {
+ const caseUserActionInfo = await client.find({
+ type: CASE_USER_ACTION_SAVED_OBJECT,
+ fields: [],
+ hasReference: { type: CASE_SAVED_OBJECT, id: caseId },
+ page: 1,
+ perPage: 1,
+ });
+ return await client.find({
+ type: CASE_USER_ACTION_SAVED_OBJECT,
+ hasReference: { type: CASE_SAVED_OBJECT, id: caseId },
+ page: 1,
+ perPage: caseUserActionInfo.total,
+ sortField: 'action_date',
+ sortOrder: 'asc',
+ });
+ } catch (error) {
+ this.log.debug(`Error on GET case user action: ${error}`);
+ throw error;
+ }
+ },
+ postUserActions: async ({ client, actions }: PostCaseUserActionArgs) => {
+ try {
+ this.log.debug(`Attempting to POST a new case user action`);
+ return await client.bulkCreate(
+ actions.map(action => ({ type: CASE_USER_ACTION_SAVED_OBJECT, ...action }))
+ );
+ } catch (error) {
+ this.log.debug(`Error on POST a new case user action: ${error}`);
+ throw error;
+ }
+ },
+ });
+}
From 9c764a45c67ebbf6e07609ba161e7b9e56eca30b Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Date: Wed, 18 Mar 2020 21:24:40 -0400
Subject: [PATCH 02/17] fix rebase
---
x-pack/plugins/case/server/plugin.ts | 10 +++-------
1 file changed, 3 insertions(+), 7 deletions(-)
diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts
index 3ccb699b85306..a6a459373b0ed 100644
--- a/x-pack/plugins/case/server/plugin.ts
+++ b/x-pack/plugins/case/server/plugin.ts
@@ -12,18 +12,13 @@ import { SecurityPluginSetup } from '../../security/server';
import { ConfigType } from './config';
import { initCaseApi } from './routes/api';
-<<<<<<< HEAD
import {
caseSavedObjectType,
caseConfigureSavedObjectType,
caseCommentSavedObjectType,
+ caseUserActionSavedObjectType,
} from './saved_object_types';
-import { CaseConfigureService, CaseService } from './services';
-=======
-import { caseSavedObjectType, caseCommentSavedObjectType } from './saved_object_types';
-import { CaseService } from './services';
-import { CaseUserActionService } from './services/user_actions';
->>>>>>> modify API to get the total comments in _find + Add user action to track what user are doing + create _pushed api to know when case have been pushed
+import { CaseConfigureService, CaseService, CaseUserActionService } from './services';
function createConfig$(context: PluginInitializerContext) {
return context.config.create().pipe(map(config => config));
@@ -52,6 +47,7 @@ export class CasePlugin {
core.savedObjects.registerType(caseSavedObjectType);
core.savedObjects.registerType(caseCommentSavedObjectType);
core.savedObjects.registerType(caseConfigureSavedObjectType);
+ core.savedObjects.registerType(caseUserActionSavedObjectType);
const caseServicePlugin = new CaseService(this.log);
const caseConfigureServicePlugin = new CaseConfigureService(this.log);
From e493f6bb745107dc26f10ec975ed17fe9fc91808 Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Date: Wed, 18 Mar 2020 21:38:18 -0400
Subject: [PATCH 03/17] add connector name in case configuration saved object
---
.../public/containers/case/configure/use_configure.tsx | 9 +++++++--
.../pages/case/components/configure_cases/index.tsx | 8 ++++++--
x-pack/plugins/case/common/api/cases/configure.ts | 1 +
x-pack/plugins/case/server/saved_object_types/cases.ts | 6 ------
.../plugins/case/server/saved_object_types/configure.ts | 3 +++
5 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx
index 22ac54093d1dc..b81be645fa3a4 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx
@@ -13,6 +13,7 @@ import { ClosureType } from './types';
interface PersistCaseConfigure {
connectorId: string;
+ connectorName: string;
closureType: ClosureType;
}
@@ -74,7 +75,7 @@ export const useCaseConfigure = ({
}, []);
const persistCaseConfigure = useCallback(
- async ({ connectorId, closureType }: PersistCaseConfigure) => {
+ async ({ connectorId, connectorName, closureType }: PersistCaseConfigure) => {
let didCancel = false;
const abortCtrl = new AbortController();
const saveCaseConfiguration = async () => {
@@ -83,7 +84,11 @@ export const useCaseConfigure = ({
const res =
version.length === 0
? await postCaseConfigure(
- { connector_id: connectorId, closure_type: closureType },
+ {
+ connector_id: connectorId,
+ connector_name: connectorName,
+ closure_type: closureType,
+ },
abortCtrl.signal
)
: await patchCaseConfigure(
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx
index c8ef6e32595d0..8a6d8a40e560a 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx
@@ -128,9 +128,13 @@ const ConfigureCasesComponent: React.FC = () => {
// TO DO give a warning/error to user when field are not mapped so they have chance to do it
() => {
setActionBarVisible(false);
- persistCaseConfigure({ connectorId, closureType });
+ persistCaseConfigure({
+ connectorId,
+ connectorName: connectors.find(c => c.id === connectorId)?.name ?? '',
+ closureType,
+ });
},
- [connectorId, closureType, mapping]
+ [connectorId, connectors, closureType, mapping]
);
const onChangeConnector = useCallback((newConnectorId: string) => {
diff --git a/x-pack/plugins/case/common/api/cases/configure.ts b/x-pack/plugins/case/common/api/cases/configure.ts
index e0489ed7270fa..9b210c2aa05ad 100644
--- a/x-pack/plugins/case/common/api/cases/configure.ts
+++ b/x-pack/plugins/case/common/api/cases/configure.ts
@@ -73,6 +73,7 @@ const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-b
const CasesConfigureBasicRt = rt.type({
connector_id: rt.string,
+ connector_name: rt.string,
closure_type: ClosureTypeRT,
});
diff --git a/x-pack/plugins/case/server/saved_object_types/cases.ts b/x-pack/plugins/case/server/saved_object_types/cases.ts
index b8d91eadddc4c..c445374c37adc 100644
--- a/x-pack/plugins/case/server/saved_object_types/cases.ts
+++ b/x-pack/plugins/case/server/saved_object_types/cases.ts
@@ -14,7 +14,6 @@ export const caseSavedObjectType: SavedObjectsType = {
namespaceAgnostic: false,
mappings: {
properties: {
-<<<<<<< HEAD
closed_at: {
type: 'date',
},
@@ -31,11 +30,6 @@ export const caseSavedObjectType: SavedObjectsType = {
},
},
},
- comment_ids: {
- type: 'keyword',
- },
-=======
->>>>>>> modify API to get the total comments in _find + Add user action to track what user are doing + create _pushed api to know when case have been pushed
created_at: {
type: 'date',
},
diff --git a/x-pack/plugins/case/server/saved_object_types/configure.ts b/x-pack/plugins/case/server/saved_object_types/configure.ts
index 8ea6f6bba7d4f..a4e0607fe2844 100644
--- a/x-pack/plugins/case/server/saved_object_types/configure.ts
+++ b/x-pack/plugins/case/server/saved_object_types/configure.ts
@@ -30,6 +30,9 @@ export const caseConfigureSavedObjectType: SavedObjectsType = {
connector_id: {
type: 'keyword',
},
+ connector_name: {
+ type: 'keyword',
+ },
closure_type: {
type: 'keyword',
},
From 33fc7d51c30f102e9c4d8bc53805f4086ad3975c Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Date: Wed, 18 Mar 2020 21:52:02 -0400
Subject: [PATCH 04/17] fix total comment in all cases
---
.../plugins/siem/public/containers/case/types.ts | 2 +-
.../siem/public/containers/case/use_get_case.tsx | 2 +-
.../case/components/all_cases/__mock__/index.tsx | 14 +++++++-------
.../pages/case/components/all_cases/columns.tsx | 7 ++++---
.../case/components/case_view/__mock__/index.tsx | 3 ++-
5 files changed, 15 insertions(+), 13 deletions(-)
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts
index 44519031e91cb..1cfdc0c34f0cc 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts
@@ -21,13 +21,13 @@ export interface Case {
closedAt: string | null;
closedBy: ElasticUser | null;
comments: Comment[];
- commentIds: string[];
createdAt: string;
createdBy: ElasticUser;
description: string;
status: string;
tags: string[];
title: string;
+ totalComment: number;
updatedAt: string | null;
updatedBy: ElasticUser | null;
version: string;
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx
index b70195e2c126f..7127ad7932a0b 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx
@@ -53,7 +53,6 @@ const initialData: Case = {
closedBy: null,
createdAt: '',
comments: [],
- commentIds: [],
createdBy: {
username: '',
},
@@ -61,6 +60,7 @@ const initialData: Case = {
status: '',
tags: [],
title: '',
+ totalComment: 0,
updatedAt: null,
updatedBy: null,
version: '',
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx
index 48fbb4e74c407..7bb97a75f4515 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx
@@ -18,12 +18,12 @@ export const useGetCasesMockState: UseGetCasesState = {
id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15',
createdAt: '2020-02-13T19:44:23.627Z',
createdBy: { username: 'elastic' },
- commentIds: [],
comments: [],
description: 'Security banana Issue',
status: 'open',
tags: ['defacement'],
title: 'Another horrible breach',
+ totalComment: 0,
updatedAt: null,
updatedBy: null,
version: 'WzQ3LDFd',
@@ -34,12 +34,12 @@ export const useGetCasesMockState: UseGetCasesState = {
id: '362a5c10-4e99-11ea-9290-35d05cb55c15',
createdAt: '2020-02-13T19:44:13.328Z',
createdBy: { username: 'elastic' },
- commentIds: [],
comments: [],
description: 'Security banana Issue',
status: 'open',
tags: ['phishing'],
title: 'Bad email',
+ totalComment: 0,
updatedAt: null,
updatedBy: null,
version: 'WzQ3LDFd',
@@ -50,12 +50,12 @@ export const useGetCasesMockState: UseGetCasesState = {
id: '34f8b9e0-4e99-11ea-9290-35d05cb55c15',
createdAt: '2020-02-13T19:44:11.328Z',
createdBy: { username: 'elastic' },
- commentIds: [],
comments: [],
description: 'Security banana Issue',
status: 'open',
tags: ['phishing'],
title: 'Bad email',
+ totalComment: 0,
updatedAt: null,
updatedBy: null,
version: 'WzQ3LDFd',
@@ -66,14 +66,14 @@ export const useGetCasesMockState: UseGetCasesState = {
id: '31890e90-4e99-11ea-9290-35d05cb55c15',
createdAt: '2020-02-13T19:44:05.563Z',
createdBy: { username: 'elastic' },
- commentIds: [],
comments: [],
description: 'Security banana Issue',
status: 'closed',
tags: ['phishing'],
title: 'Uh oh',
- updatedAt: '2020-02-13T19:44:13.328Z',
- updatedBy: { username: 'elastic' },
+ totalComment: 0,
+ updatedAt: null,
+ updatedBy: null,
version: 'WzQ3LDFd',
},
{
@@ -82,12 +82,12 @@ export const useGetCasesMockState: UseGetCasesState = {
id: '2f5b3210-4e99-11ea-9290-35d05cb55c15',
createdAt: '2020-02-13T19:44:01.901Z',
createdBy: { username: 'elastic' },
- commentIds: [],
comments: [],
description: 'Security banana Issue',
status: 'open',
tags: ['phishing'],
title: 'Uh oh',
+ totalComment: 0,
updatedAt: null,
updatedBy: null,
version: 'WzQ3LDFd',
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx
index b9e1113c486ad..32a29483e9c75 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx
@@ -35,6 +35,7 @@ const Spacer = styled.span`
const renderStringField = (field: string, dataTestSubj: string) =>
field != null ? {field} : getEmptyTagValue();
+
export const getCasesColumns = (
actions: Array>,
filterStatus: string
@@ -108,11 +109,11 @@ export const getCasesColumns = (
},
{
align: 'right',
- field: 'commentIds',
+ field: 'totalComment',
name: i18n.COMMENTS,
sortable: true,
- render: (comments: Case['commentIds']) =>
- renderStringField(`${comments.length}`, `case-table-column-commentCount`),
+ render: (totalComment: Case['totalComment']) =>
+ renderStringField(`${totalComment}`, `case-table-column-commentCount`),
},
filterStatus === 'open'
? {
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx
index e11441eac3a9d..be937411c495c 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx
@@ -13,7 +13,6 @@ export const caseProps: CaseProps = {
closedAt: null,
closedBy: null,
id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15',
- commentIds: ['a357c6a0-5435-11ea-b427-fb51a1fcb7b8'],
comments: [
{
comment: 'Solve this fast!',
@@ -37,6 +36,7 @@ export const caseProps: CaseProps = {
status: 'open',
tags: ['defacement'],
title: 'Another horrible breach!!',
+ totalComment: 1,
updatedAt: '2020-02-19T15:02:57.995Z',
updatedBy: {
username: 'elastic',
@@ -44,6 +44,7 @@ export const caseProps: CaseProps = {
version: 'WzQ3LDFd',
},
};
+
export const caseClosedProps: CaseProps = {
...caseProps,
initialData: {
From 687765597345a50db8b08c15e6d14f4d9170af51 Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Date: Wed, 18 Mar 2020 22:17:48 -0400
Subject: [PATCH 05/17] totalComment bug on the API
---
x-pack/plugins/case/server/routes/api/utils.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts
index 27553b25f89fd..c026541787dca 100644
--- a/x-pack/plugins/case/server/routes/api/utils.ts
+++ b/x-pack/plugins/case/server/routes/api/utils.ts
@@ -118,7 +118,7 @@ export const flattenCaseSavedObject = (
id: savedObject.id,
version: savedObject.version ?? '0',
comments: flattenCommentSavedObjects(comments),
- totalComment: 0,
+ totalComment,
...savedObject.attributes,
});
From 34c59154cd3f2a93a54f66f253e2fdc4e7f71f89 Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Date: Fri, 20 Mar 2020 00:00:09 -0400
Subject: [PATCH 06/17] integrate user action API with UI
---
.../siem/public/components/url_state/types.ts | 16 +-
.../siem/public/containers/case/api.ts | 18 +++
.../public/containers/case/configure/types.ts | 1 +
.../siem/public/containers/case/types.ts | 25 ++-
.../case/use_get_case_user_actions.tsx | 89 +++++++++++
.../containers/case/use_update_case.tsx | 4 +-
.../containers/case/use_update_comment.tsx | 12 +-
.../siem/public/containers/case/utils.ts | 8 +
.../case/components/add_comment/index.tsx | 129 ++++++++-------
.../pages/case/components/case_view/index.tsx | 51 ++++--
.../case/components/case_view/translations.ts | 31 +++-
.../components/user_action_tree/helpers.tsx | 57 +++++++
.../components/user_action_tree/index.tsx | 151 ++++++++++++++----
.../user_action_tree/user_action_item.tsx | 42 ++++-
.../user_action_tree/user_action_title.tsx | 130 ++++++++++++---
.../plugins/siem/public/pages/case/index.tsx | 4 +
.../siem/public/pages/case/translations.ts | 24 ++-
x-pack/plugins/case/common/api/cases/case.ts | 23 ++-
.../case/common/api/cases/user_actions.ts | 4 +
.../server/routes/api/cases/pushed_case.ts | 2 +-
.../user_actions/get_all_user_actions.ts | 1 +
.../plugins/case/server/routes/api/index.ts | 16 +-
.../server/services/user_actions/helpers.ts | 83 +++++-----
.../server/services/user_actions/index.ts | 2 +-
24 files changed, 720 insertions(+), 203 deletions(-)
create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx
create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx
diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts
index 2cb1b0c96ad79..c6f49d8a0e49b 100644
--- a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts
+++ b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts
@@ -60,8 +60,20 @@ export const URL_STATE_KEYS: Record = {
CONSTANTS.timerange,
CONSTANTS.timeline,
],
- timeline: [CONSTANTS.timeline, CONSTANTS.timerange],
- case: [],
+ timeline: [
+ CONSTANTS.appQuery,
+ CONSTANTS.filters,
+ CONSTANTS.savedQuery,
+ CONSTANTS.timeline,
+ CONSTANTS.timerange,
+ ],
+ case: [
+ CONSTANTS.appQuery,
+ CONSTANTS.filters,
+ CONSTANTS.savedQuery,
+ CONSTANTS.timeline,
+ CONSTANTS.timerange,
+ ],
};
export type LocationTypes =
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts
index 5ba1f010e0d52..d95d8d6bbc93d 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts
@@ -13,6 +13,7 @@ import {
CommentRequest,
CommentResponse,
User,
+ CaseUserActionsResponse,
} from '../../../../../../plugins/case/common/api';
import { KibanaServices } from '../../lib/kibana';
import {
@@ -23,16 +24,19 @@ import {
Comment,
FetchCasesProps,
SortFieldCase,
+ CaseUserActions,
} from './types';
import { CASES_URL } from './constants';
import {
convertToCamelCase,
convertAllCasesToCamel,
+ convertArrayToCamelCase,
decodeCaseResponse,
decodeCasesResponse,
decodeCasesFindResponse,
decodeCasesStatusResponse,
decodeCommentResponse,
+ decodeCaseUserActionsResponse,
} from './utils';
export const getCase = async (caseId: string, includeComments: boolean = true): Promise => {
@@ -71,6 +75,20 @@ export const getReporters = async (signal: AbortSignal): Promise => {
return response ?? [];
};
+export const getCaseUserActions = async (
+ caseId: string,
+ signal: AbortSignal
+): Promise => {
+ const response = await KibanaServices.get().http.fetch(
+ `${CASES_URL}/${caseId}/user_actions`,
+ {
+ method: 'GET',
+ signal,
+ }
+ );
+ return convertArrayToCamelCase(decodeCaseUserActionsResponse(response)) as CaseUserActions[];
+};
+
export const getCases = async ({
filterOptions = {
search: '',
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/types.ts
index fc7aaa3643d77..d69c23fe02ec9 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/configure/types.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/types.ts
@@ -26,6 +26,7 @@ export interface CaseConfigure {
createdAt: string;
createdBy: ElasticUser;
connectorId: string;
+ connectorName: string;
closureType: ClosureType;
updatedAt: string;
updatedBy: ElasticUser;
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts
index 1cfdc0c34f0cc..04d0090084937 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts
@@ -4,18 +4,40 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { User } from '../../../../../../plugins/case/common/api';
+import { User, UserActionField, UserAction } from '../../../../../../plugins/case/common/api';
export interface Comment {
id: string;
createdAt: string;
createdBy: ElasticUser;
comment: string;
+ pushedAt: string | null;
+ pushedBy: string | null;
updatedAt: string | null;
updatedBy: ElasticUser | null;
version: string;
}
+export interface CaseUserActions {
+ actionId: string;
+ actionField: UserActionField;
+ action: UserAction;
+ actionAt: string;
+ actionBy: ElasticUser;
+ caseId: string;
+ commentId: string | null;
+ newValue: string | null;
+ oldValue: string | null;
+}
+export interface CasePush {
+ at: string;
+ by: string;
+ connectorId: string;
+ connectorName: string;
+ externalId: string;
+ externalTitle: string;
+ externalUrl: string;
+}
export interface Case {
id: string;
closedAt: string | null;
@@ -24,6 +46,7 @@ export interface Case {
createdAt: string;
createdBy: ElasticUser;
description: string;
+ pushed: CasePush;
status: string;
tags: string[];
title: string;
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx
new file mode 100644
index 0000000000000..0e3e5fe59d54f
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx
@@ -0,0 +1,89 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { isEmpty } from 'lodash/fp';
+import { useCallback, useEffect, useState } from 'react';
+
+import { errorToToaster, useStateToaster } from '../../components/toasters';
+import { getCaseUserActions } from './api';
+import * as i18n from './translations';
+import { CaseUserActions } from './types';
+
+interface CaseUserActionsState {
+ caseUserActions: CaseUserActions[];
+ isLoading: boolean;
+ isError: boolean;
+}
+
+const initialData: CaseUserActionsState = {
+ caseUserActions: [],
+ isLoading: true,
+ isError: false,
+};
+
+interface UseGetCaseUserActions extends CaseUserActionsState {
+ fetchCaseUserActions: (caseId: string) => void;
+}
+
+export const useGetCaseUserActions = (caseId: string): UseGetCaseUserActions => {
+ const [caseUserActionsState, setCaseUserActionsState] = useState(
+ initialData
+ );
+
+ const [, dispatchToaster] = useStateToaster();
+
+ const fetchCaseUserActions = useCallback(
+ (thisCaseId: string) => {
+ let didCancel = false;
+ const abortCtrl = new AbortController();
+ const fetchData = async () => {
+ setCaseUserActionsState({
+ ...caseUserActionsState,
+ isLoading: true,
+ });
+ try {
+ const response = await getCaseUserActions(thisCaseId, abortCtrl.signal);
+ if (!didCancel) {
+ // Attention Future developer
+ // We are removing the first item because it will always the creation of the case
+ // and we do not want it to simplify our life
+ setCaseUserActionsState({
+ caseUserActions: !isEmpty(response) ? response.slice(1) : [],
+ isLoading: false,
+ isError: false,
+ });
+ }
+ } catch (error) {
+ if (!didCancel) {
+ errorToToaster({
+ title: i18n.ERROR_TITLE,
+ error: error.body && error.body.message ? new Error(error.body.message) : error,
+ dispatchToaster,
+ });
+ setCaseUserActionsState({
+ caseUserActions: [],
+ isLoading: false,
+ isError: true,
+ });
+ }
+ }
+ };
+ fetchData();
+ return () => {
+ didCancel = true;
+ abortCtrl.abort();
+ };
+ },
+ [caseUserActionsState]
+ );
+
+ useEffect(() => {
+ if (!isEmpty(caseId)) {
+ fetchCaseUserActions(caseId);
+ }
+ }, [caseId]);
+ return { ...caseUserActionsState, fetchCaseUserActions };
+};
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx
index 987620469901b..5cf01bd0be195 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx
@@ -25,6 +25,7 @@ interface NewCaseState {
export interface UpdateByKey {
updateKey: UpdateKey;
updateValue: CaseRequest[UpdateKey];
+ fetchCaseUserActions: (caseId: string) => void;
}
type Action =
@@ -75,7 +76,7 @@ export const useUpdateCase = (caseId: string, initialData: Case): UseUpdateCase
const [, dispatchToaster] = useStateToaster();
const dispatchUpdateCaseProperty = useCallback(
- async ({ updateKey, updateValue }: UpdateByKey) => {
+ async ({ fetchCaseUserActions, updateKey, updateValue }: UpdateByKey) => {
let cancel = false;
try {
dispatch({ type: 'FETCH_INIT', payload: updateKey });
@@ -85,6 +86,7 @@ export const useUpdateCase = (caseId: string, initialData: Case): UseUpdateCase
state.caseData.version
);
if (!cancel) {
+ fetchCaseUserActions(caseId);
dispatch({ type: 'FETCH_SUCCESS', payload: response[0] });
}
} catch (error) {
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx
index a40a1100ca735..c1b2bfde30126 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx
@@ -70,8 +70,15 @@ const dataFetchReducer = (state: CommentUpdateState, action: Action): CommentUpd
}
};
+interface UpdateComment {
+ caseId: string;
+ commentId: string;
+ commentUpdate: string;
+ fetchUserActions: () => void;
+}
+
interface UseUpdateComment extends CommentUpdateState {
- updateComment: (caseId: string, commentId: string, commentUpdate: string) => void;
+ updateComment: ({ caseId, commentId, commentUpdate, fetchUserActions }: UpdateComment) => void;
addPostedComment: Dispatch;
}
@@ -84,7 +91,7 @@ export const useUpdateComment = (comments: Comment[]): UseUpdateComment => {
const [, dispatchToaster] = useStateToaster();
const dispatchUpdateComment = useCallback(
- async (caseId: string, commentId: string, commentUpdate: string) => {
+ async ({ caseId, commentId, commentUpdate, fetchUserActions }: UpdateComment) => {
let cancel = false;
try {
dispatch({ type: 'FETCH_INIT', payload: commentId });
@@ -98,6 +105,7 @@ export const useUpdateComment = (comments: Comment[]): UseUpdateComment => {
currentComment.version
);
if (!cancel) {
+ fetchUserActions();
dispatch({ type: 'FETCH_SUCCESS', payload: { update: response, commentId } });
}
} catch (error) {
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/utils.ts b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts
index 8f24d5a435240..63a1d3f3620f6 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/utils.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts
@@ -23,6 +23,8 @@ import {
CommentResponseRt,
CasesConfigureResponse,
CaseConfigureResponseRt,
+ CaseUserActionsResponse,
+ CaseUserActionsResponseRt,
} from '../../../../../../plugins/case/common/api';
import { ToasterError } from '../../components/toasters';
import { AllCases, Case } from './types';
@@ -86,3 +88,9 @@ export const decodeCaseConfigureResponse = (respCase?: CasesConfigureResponse) =
CaseConfigureResponseRt.decode(respCase),
fold(throwErrors(createToasterPlainError), identity)
);
+
+export const decodeCaseUserActionsResponse = (respUserActions?: CaseUserActionsResponse) =>
+ pipe(
+ CaseUserActionsResponseRt.decode(respUserActions),
+ fold(throwErrors(createToasterPlainError), identity)
+ );
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx
index 0b3b0daaf4bbc..836595c7c45d9 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx
@@ -30,72 +30,79 @@ const initialCommentValue: CommentRequest = {
interface AddCommentProps {
caseId: string;
+ onCommentSaving?: () => void;
onCommentPosted: (commentResponse: Comment) => void;
+ showLoading?: boolean;
}
-export const AddComment = React.memo(({ caseId, onCommentPosted }) => {
- const { commentData, isLoading, postComment, resetCommentData } = usePostComment(caseId);
- const { form } = useForm({
- defaultValue: initialCommentValue,
- options: { stripEmptyFields: false },
- schema,
- });
- const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline(
- form,
- 'comment'
- );
+export const AddComment = React.memo(
+ ({ caseId, showLoading = true, onCommentPosted, onCommentSaving }) => {
+ const { commentData, isLoading, postComment, resetCommentData } = usePostComment(caseId);
+ const { form } = useForm({
+ defaultValue: initialCommentValue,
+ options: { stripEmptyFields: false },
+ schema,
+ });
+ const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline(
+ form,
+ 'comment'
+ );
- useEffect(() => {
- if (commentData !== null) {
- onCommentPosted(commentData);
- form.reset();
- resetCommentData();
- }
- }, [commentData]);
+ useEffect(() => {
+ if (commentData !== null) {
+ onCommentPosted(commentData);
+ form.reset();
+ resetCommentData();
+ }
+ }, [commentData]);
- const onSubmit = useCallback(async () => {
- const { isValid, data } = await form.submit();
- if (isValid) {
- await postComment(data);
- }
- }, [form]);
+ const onSubmit = useCallback(async () => {
+ const { isValid, data } = await form.submit();
+ if (isValid) {
+ if (onCommentSaving != null) {
+ onCommentSaving();
+ }
+ await postComment(data);
+ }
+ }, [form]);
- return (
- <>
- {isLoading && }
-
- >
- );
-});
+ return (
+ <>
+ {isLoading && showLoading && }
+
+ >
+ );
+ }
+);
AddComment.displayName = 'AddComment';
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
index 0ac3adeb860ff..4c8c2de2dd463 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
@@ -4,10 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useCallback, useMemo } from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiLoadingContent, EuiLoadingSpinner } from '@elastic/eui';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import styled, { css } from 'styled-components';
+import { Redirect } from 'react-router-dom';
-import styled from 'styled-components';
import * as i18n from './translations';
import { Case } from '../../../../containers/case/types';
import { getCaseUrl } from '../../../../components/link_to';
@@ -24,6 +25,10 @@ import { WhitePageWrapper } from '../wrappers';
import { useBasePath } from '../../../../lib/kibana';
import { CaseStatus } from '../case_status';
import { SpyRoute } from '../../../../utils/route/spy_routes';
+import { useDeleteCases } from '../../../../containers/case/use_delete_cases';
+import { SiemPageName } from '../../../home/types';
+import { ConfirmDeleteCaseModal } from '../confirm_delete_case';
+import { useGetCaseUserActions } from '../../../../containers/case/use_get_case_user_actions';
interface Props {
caseId: string;
@@ -45,6 +50,12 @@ export interface CaseProps {
export const CaseComponent = React.memo(({ caseId, initialData }) => {
const basePath = window.location.origin + useBasePath();
const caseLink = `${basePath}/app/siem#/case/${caseId}`;
+ const [initLoadingData, setInitLoadingData] = useState(true);
+ const {
+ caseUserActions,
+ isLoading: isLoadingUserActions,
+ fetchCaseUserActions,
+ } = useGetCaseUserActions(caseId);
const { caseData, isLoading, updateKey, updateCaseProperty } = useUpdateCase(caseId, initialData);
// Update Fields
@@ -55,6 +66,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
const titleUpdate = getTypedPayload(updateValue);
if (titleUpdate.length > 0) {
updateCaseProperty({
+ fetchCaseUserActions,
updateKey: 'title',
updateValue: titleUpdate,
});
@@ -64,6 +76,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
const descriptionUpdate = getTypedPayload(updateValue);
if (descriptionUpdate.length > 0) {
updateCaseProperty({
+ fetchCaseUserActions,
updateKey: 'description',
updateValue: descriptionUpdate,
});
@@ -72,6 +85,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
case 'tags':
const tagsUpdate = getTypedPayload(updateValue);
updateCaseProperty({
+ fetchCaseUserActions,
updateKey: 'tags',
updateValue: tagsUpdate,
});
@@ -80,6 +94,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
const statusUpdate = getTypedPayload(updateValue);
if (caseData.status !== updateValue) {
updateCaseProperty({
+ fetchCaseUserActions,
updateKey: 'status',
updateValue: statusUpdate,
});
@@ -88,7 +103,12 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
return null;
}
},
- [caseData.status]
+ [fetchCaseUserActions, updateCaseProperty, caseData.status]
+ );
+
+ const toggleStatusCase = useCallback(
+ e => onUpdateField('status', e.target.checked ? 'open' : 'closed'),
+ [onUpdateField]
);
const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]);
const onSubmitTitle = useCallback(newTitle => onUpdateField('title', newTitle), [onUpdateField]);
@@ -128,6 +148,13 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
}),
[caseData.title]
);
+
+ useEffect(() => {
+ if (initLoadingData && !isLoadingUserActions) {
+ setInitLoadingData(false);
+ }
+ }, [initLoadingData, isLoadingUserActions]);
+
return (
<>
@@ -159,11 +186,17 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
-
+ {initLoadingData && }
+ {!initLoadingData && (
+
+ )}
{
+ if (field === 'tags' && action.action === 'add') {
+ return (
+
+
+ {i18n.ADDED_FIELD} {i18n.TAGS.toLowerCase()}
+
+ {action.newValue != null &&
+ action.newValue.split(',').map(tag => (
+
+ {tag}
+
+ ))}
+
+ );
+ }
+ if (field === 'tags' && action.action === 'delete') {
+ return (
+
+
+ {i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()}
+
+ {action.newValue != null &&
+ action.newValue.split(',').map(tag => (
+
+ {tag}
+
+ ))}
+
+ );
+ } else if (field === 'title' && action.action === 'update') {
+ return `${i18n.CHANGED_FIELD.toLowerCase()} ${i18n.CASE_NAME.toLowerCase()} ${i18n.TO} "${
+ action.newValue
+ }"`;
+ } else if (field === 'description' && action.action === 'update') {
+ return `${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`;
+ } else if (field === 'status' && action.action === 'update') {
+ return `${
+ action.newValue === 'open' ? i18n.REOPENED_CASE.toLowerCase() : i18n.CLOSED_CASE.toLowerCase()
+ } ${i18n.CASE}`;
+ } else if (field === 'comment' && action.action === 'update') {
+ return `${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`;
+ }
+ return '';
+};
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx
index 6a3d319561353..81181d64b169d 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx
@@ -4,27 +4,47 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useCallback, useMemo, useState } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
+import React, { useCallback, useMemo, useRef, useState } from 'react';
+import styled from 'styled-components';
+
import * as i18n from '../case_view/translations';
-import { Case } from '../../../../containers/case/types';
+import { Case, CaseUserActions, Comment } from '../../../../containers/case/types';
import { useUpdateComment } from '../../../../containers/case/use_update_comment';
+import { useCurrentUser } from '../../../../lib/kibana';
+import { AddComment } from '../add_comment';
+import { getLabelTitle } from './helpers';
import { UserActionItem } from './user_action_item';
import { UserActionMarkdown } from './user_action_markdown';
-import { AddComment } from '../add_comment';
-import { useCurrentUser } from '../../../../lib/kibana';
export interface UserActionTreeProps {
data: Case;
+ caseUserActions: CaseUserActions[];
isLoadingDescription: boolean;
+ isLoadingUserActions: boolean;
+ fetchUserActions: () => void;
onUpdateField: (updateKey: keyof Case, updateValue: string | string[]) => void;
}
+const MyEuiFlexGroup = styled(EuiFlexGroup)`
+ margin-bottom: 8px;
+`;
+
const DescriptionId = 'description';
const NewId = 'newComment';
export const UserActionTree = React.memo(
- ({ data: caseData, onUpdateField, isLoadingDescription }: UserActionTreeProps) => {
+ ({
+ data: caseData,
+ caseUserActions,
+ fetchUserActions,
+ isLoadingDescription,
+ isLoadingUserActions,
+ onUpdateField,
+ }: UserActionTreeProps) => {
+ const handlerTimeoutId = useRef(0);
+ const [selectedOutlineCommentId, setSelectedOutlineCommentId] = useState('');
const { comments, isLoadingIds, updateComment, addPostedComment } = useUpdateComment(
caseData.comments
);
@@ -45,11 +65,36 @@ export const UserActionTree = React.memo(
const handleSaveComment = useCallback(
(id: string, content: string) => {
handleManageMarkdownEditId(id);
- updateComment(caseData.id, id, content);
+ updateComment({
+ caseId: caseData.id,
+ commentId: id,
+ commentUpdate: content,
+ fetchUserActions,
+ });
},
[handleManageMarkdownEditId, updateComment]
);
+ const handleOutlineComment = useCallback(
+ (id: string) => {
+ window.clearTimeout(handlerTimeoutId.current);
+ setSelectedOutlineCommentId(id);
+ handlerTimeoutId.current = window.setTimeout(() => {
+ setSelectedOutlineCommentId('');
+ window.clearTimeout(handlerTimeoutId.current);
+ }, 2400);
+ },
+ [handlerTimeoutId.current]
+ );
+
+ const handleUpdate = useCallback(
+ (comment: Comment) => {
+ addPostedComment(comment);
+ fetchUserActions();
+ },
+ [addPostedComment, fetchUserActions]
+ );
+
const MarkdownDescription = useMemo(
() => (
,
- [caseData.id]
+ () => (
+
+ ),
+ [caseData.id, handleUpdate]
);
return (
@@ -85,29 +137,68 @@ export const UserActionTree = React.memo(
onEdit={handleManageMarkdownEditId.bind(null, DescriptionId)}
userName={caseData.createdBy.username}
/>
- {comments.map(comment => (
-
+ {caseUserActions.map(action => {
+ if (action.commentId != null && action.action === 'create') {
+ const comment = comments.find(c => c.id === action.commentId);
+ if (comment != null) {
+ return (
+
+ {pushCallouts != null && pushCallouts}
{initLoadingData && }
{!initLoadingData && (
-
+ <>
+
+
+
+
+
+
+ {pushButton}
+
+ >
)}
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/push_to_service.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/push_to_service.tsx
new file mode 100644
index 0000000000000..7d975d21e44ce
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/push_to_service.tsx
@@ -0,0 +1,137 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiButton, EuiToolTip } from '@elastic/eui';
+import React, { useCallback, useState, useMemo } from 'react';
+
+import { useCaseConfigure } from '../../../../containers/case/configure/use_configure';
+import { Case } from '../../../../containers/case/types';
+import { useGetActionLicense } from '../../../../containers/case/use_get_action_license';
+import { usePostPushToService } from '../../../../containers/case/use_post_push_to_service';
+
+import * as i18n from './translations';
+import { ErrorsPushServiceCallOut } from '../errors_push_service_callout';
+
+interface UsePushToService {
+ caseData: Case;
+ isNew: boolean;
+ updateCase: (newCase: Case) => void;
+}
+
+interface Connector {
+ connectorId: string;
+ connectorName: string;
+}
+
+interface ReturnUsePushToService {
+ pushButton: JSX.Element;
+ pushCallouts: JSX.Element | null;
+}
+
+export const usePushToService = ({
+ caseData,
+ updateCase,
+ isNew,
+}: UsePushToService): ReturnUsePushToService => {
+ const [connector, setConnector] = useState(null);
+
+ const { isLoading, postPushToService } = usePostPushToService();
+
+ const handleSetConnector = useCallback((connectorId: string, connectorName?: string) => {
+ setConnector({ connectorId, connectorName: connectorName ?? '' });
+ }, []);
+
+ const { loading: loadingCaseConfigure } = useCaseConfigure({
+ setConnectorId: handleSetConnector,
+ });
+
+ const { isLoading: loadingLicense, actionLicense } = useGetActionLicense();
+
+ const handlePushToService = useCallback(() => {
+ if (connector != null) {
+ postPushToService({
+ caseToPush: caseData,
+ ...connector,
+ updateCase,
+ });
+ }
+ }, [caseData, connector, postPushToService, updateCase]);
+
+ const errorsMsg = useMemo(() => {
+ let errors: Array<{ title: string; description: string }> = [];
+ if (caseData.status === 'closed') {
+ errors = [
+ ...errors,
+ {
+ title: i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE,
+ description: i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_DESCRIPTION,
+ },
+ ];
+ }
+ if (connector == null && !loadingCaseConfigure && !loadingLicense) {
+ errors = [
+ ...errors,
+ {
+ title: i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE,
+ description: i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_DESCRIPTION,
+ },
+ ];
+ }
+ if (actionLicense != null && !actionLicense.enabledInLicense) {
+ errors = [
+ ...errors,
+ {
+ title: i18n.PUSH_DISABLE_BY_LICENSE_TITLE,
+ description: i18n.PUSH_DISABLE_BY_LICENSE_DESCRIPTION,
+ },
+ ];
+ }
+ if (actionLicense != null && !actionLicense.enabledInConfig) {
+ errors = [
+ ...errors,
+ {
+ title: i18n.PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE,
+ description: i18n.PUSH_DISABLE_BY_KIBANA_CONFIG_DESCRIPTION,
+ },
+ ];
+ }
+ return errors;
+ }, [actionLicense, caseData, connector, loadingCaseConfigure, loadingLicense]);
+
+ const pushToServiceButton = useMemo(
+ () => (
+ 0}
+ isLoading={isLoading}
+ >
+ {isNew ? i18n.PUSH_SERVICENOW : i18n.UPDATE_PUSH_SERVICENOW}
+
+ ),
+ [isNew, handlePushToService, isLoading, loadingLicense, loadingCaseConfigure, errorsMsg]
+ );
+
+ const objToReturn = useMemo(
+ () => ({
+ pushButton:
+ errorsMsg.length > 0 ? (
+ {errorsMsg[0].description}
}
+ >
+ {pushToServiceButton}
+
+ ) : (
+ <>{pushToServiceButton}>
+ ),
+ pushCallouts: errorsMsg.length > 0 ? : null,
+ }),
+ [errorsMsg, pushToServiceButton]
+ );
+ return objToReturn;
+};
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts
index a2fcc5d07980a..73aa5a47182c4 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts
@@ -88,3 +88,70 @@ export const EMAIL_BODY = (caseUrl: string) =>
values: { caseUrl },
defaultMessage: 'Case reference: {caseUrl}',
});
+
+export const PUSH_SERVICENOW = i18n.translate('xpack.siem.case.caseView.pushAsServicenowIncident', {
+ defaultMessage: 'Push as ServiceNow incident',
+});
+
+export const UPDATE_PUSH_SERVICENOW = i18n.translate(
+ 'xpack.siem.case.caseView.updatePushAsServicenowIncident',
+ {
+ defaultMessage: 'Update ServiceNow incident',
+ }
+);
+
+export const PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE = i18n.translate(
+ 'xpack.siem.case.caseView.pushToServiceDisableByNoCaseConfigTitle',
+ {
+ defaultMessage: 'Configure case',
+ }
+);
+
+export const PUSH_DISABLE_BY_NO_CASE_CONFIG_DESCRIPTION = i18n.translate(
+ 'xpack.siem.case.caseView.pushToServiceDisableByNoCaseConfigDescription',
+ {
+ defaultMessage: 'You did not configure you case system',
+ }
+);
+
+export const PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE = i18n.translate(
+ 'xpack.siem.case.caseView.pushToServiceDisableBecauseCaseClosedTitle',
+ {
+ defaultMessage: 'Case closed',
+ }
+);
+
+export const PUSH_DISABLE_BECAUSE_CASE_CLOSED_DESCRIPTION = i18n.translate(
+ 'xpack.siem.case.caseView.pushToServiceDisableBecauseCaseClosedDescription',
+ {
+ defaultMessage: 'You cannot push a case who have been closed',
+ }
+);
+
+export const PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE = i18n.translate(
+ 'xpack.siem.case.caseView.pushToServiceDisableByConfigTitle',
+ {
+ defaultMessage: 'Connector kibana config',
+ }
+);
+
+export const PUSH_DISABLE_BY_KIBANA_CONFIG_DESCRIPTION = i18n.translate(
+ 'xpack.siem.case.caseView.pushToServiceDisableByConfigDescription',
+ {
+ defaultMessage: 'ServiceNow connector have been disabled in kibana config',
+ }
+);
+
+export const PUSH_DISABLE_BY_LICENSE_TITLE = i18n.translate(
+ 'xpack.siem.case.caseView.pushToServiceDisableByLicenseTitle',
+ {
+ defaultMessage: 'Elastic Stack subscriptions',
+ }
+);
+
+export const PUSH_DISABLE_BY_LICENSE_DESCRIPTION = i18n.translate(
+ 'xpack.siem.case.caseView.pushToServiceDisableByLicenseDescription',
+ {
+ defaultMessage: 'ServiceNow is disabled because you do not have the right license',
+ }
+);
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/index.tsx
new file mode 100644
index 0000000000000..37fea6f624536
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/index.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiCallOut, EuiButton, EuiDescriptionList } from '@elastic/eui';
+import React, { memo, useCallback, useState } from 'react';
+
+import * as i18n from './translations';
+
+interface ErrorsPushServiceCallOut {
+ errors: Array<{ title: string; description: string }>;
+}
+
+const ErrorsPushServiceCallOutComponent = ({ errors }: ErrorsPushServiceCallOut) => {
+ const [showCallOut, setShowCallOut] = useState(true);
+ const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]);
+
+ return showCallOut ? (
+
+
+
+ {i18n.DISMISS_CALLOUT}
+
+
+ ) : null;
+};
+
+export const ErrorsPushServiceCallOut = memo(ErrorsPushServiceCallOutComponent);
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/translations.ts
new file mode 100644
index 0000000000000..cfd26632e6a36
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/translations.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const ERROR_PUSH_SERVICE_CALLOUT_TITLE = i18n.translate(
+ 'xpack.siem.case.errorsPushServiceCallOutTitle',
+ {
+ defaultMessage: 'You can not push to ServiceNow because of the errors below',
+ }
+);
+
+export const DISMISS_CALLOUT = i18n.translate(
+ 'xpack.siem.case.dismissErrorsPushServiceCallOutTitle',
+ {
+ defaultMessage: 'Dismiss',
+ }
+);
diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts
index 9fff630d4ff2b..a5d73acc10502 100644
--- a/x-pack/plugins/case/common/api/cases/case.ts
+++ b/x-pack/plugins/case/common/api/cases/case.ts
@@ -28,6 +28,17 @@ const CasePushBasicRt = rt.type({
external_url: rt.string,
});
+const CasePushedBasicRt = rt.union([
+ rt.intersection([
+ CasePushBasicRt,
+ rt.type({
+ at: rt.string,
+ by: UserRT,
+ }),
+ ]),
+ rt.null,
+]);
+
export const CaseAttributesRt = rt.intersection([
CaseBasicRt,
rt.type({
@@ -35,16 +46,7 @@ export const CaseAttributesRt = rt.intersection([
closed_by: rt.union([UserRT, rt.null]),
created_at: rt.string,
created_by: UserRT,
- pushed: rt.union([
- rt.intersection([
- CasePushBasicRt,
- rt.type({
- at: rt.string,
- by: UserRT,
- }),
- ]),
- rt.null,
- ]),
+ pushed: CasePushedBasicRt,
updated_at: rt.union([rt.string, rt.null]),
updated_by: rt.union([UserRT, rt.null]),
}),
@@ -98,6 +100,60 @@ export const CasePatchRequestRt = rt.intersection([
export const CasesPatchRequestRt = rt.type({ cases: rt.array(CasePatchRequestRt) });
export const CasesResponseRt = rt.array(CaseResponseRt);
+/*
+ * This type are related to this file below
+ * x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
+ * why because this schema is not share in a common folder
+ * so we redefine then so we can use/validate types
+ */
+
+const PushCaseUserParams = rt.type({
+ fullName: rt.union([rt.string, rt.null]),
+ username: rt.string,
+});
+
+export const PushCommentParamsRt = rt.type({
+ commentId: rt.string,
+ comment: rt.string,
+ createdAt: rt.string,
+ createdBy: PushCaseUserParams,
+ updatedAt: rt.union([rt.string, rt.null]),
+ updatedBy: rt.union([PushCaseUserParams, rt.null]),
+});
+
+export const PushCaseParamsRt = rt.intersection([
+ rt.type({
+ caseId: rt.string,
+ createdAt: rt.string,
+ createdBy: PushCaseUserParams,
+ incidentId: rt.union([rt.string, rt.null]),
+ title: rt.string,
+ updatedAt: rt.union([rt.string, rt.null]),
+ updatedBy: rt.union([PushCaseUserParams, rt.null]),
+ }),
+ rt.partial({
+ description: rt.string,
+ comments: rt.array(PushCommentParamsRt),
+ }),
+]);
+
+export const PushCaseResponseRt = rt.intersection([
+ rt.type({
+ number: rt.string,
+ incidentId: rt.string,
+ pushedDate: rt.string,
+ url: rt.string,
+ }),
+ rt.partial({
+ comments: rt.array(
+ rt.type({
+ commentId: rt.string,
+ pushedDate: rt.string,
+ })
+ ),
+ }),
+]);
+
export type CaseAttributes = rt.TypeOf;
export type CaseRequest = rt.TypeOf;
export type CaseResponse = rt.TypeOf;
@@ -106,3 +162,7 @@ export type CasesFindResponse = rt.TypeOf;
export type CasePatchRequest = rt.TypeOf;
export type CasesPatchRequest = rt.TypeOf;
export type CasePushRequest = rt.TypeOf;
+export type PushCaseParams = rt.TypeOf;
+export type PushCaseResponse = rt.TypeOf;
+export type CasePushedData = rt.TypeOf;
+export type PushCommentParams = rt.TypeOf;
diff --git a/x-pack/plugins/case/server/routes/api/cases/pushed_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts
similarity index 78%
rename from x-pack/plugins/case/server/routes/api/cases/pushed_case.ts
rename to x-pack/plugins/case/server/routes/api/cases/push_case.ts
index 23e6baa79c446..471e86237e564 100644
--- a/x-pack/plugins/case/server/routes/api/cases/pushed_case.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts
@@ -16,10 +16,15 @@ import { CasePushRequestRt, CaseResponseRt, throwErrors } from '../../../../comm
import { buildCaseUserActionItem } from '../../../services/user_actions/helpers';
import { RouteDeps } from '../types';
-export function initPushedCaseUserActionApi({ caseService, router, userActionService }: RouteDeps) {
+export function initPushCaseUserActionApi({
+ caseConfigureService,
+ caseService,
+ router,
+ userActionService,
+}: RouteDeps) {
router.post(
{
- path: '/api/cases/{case_id}/_pushed',
+ path: '/api/cases/{case_id}/_push',
validate: {
params: schema.object({
case_id: schema.string(),
@@ -38,20 +43,28 @@ export function initPushedCaseUserActionApi({ caseService, router, userActionSer
const pushedBy = await caseService.getUser({ request, response });
const pushedDate = new Date().toISOString();
- const myCase = await caseService.getCase({
- client: context.core.savedObjects.client,
- caseId: request.params.case_id,
- });
+ const [myCase, myCaseConfigure, totalCommentsFindByCases] = await Promise.all([
+ caseService.getCase({
+ client: context.core.savedObjects.client,
+ caseId: request.params.case_id,
+ }),
+ caseConfigureService.find({ client }),
+ caseService.getAllCaseComments({
+ client,
+ caseId,
+ options: {
+ fields: [],
+ page: 1,
+ perPage: 1,
+ },
+ }),
+ ]);
- const totalCommentsFindByCases = await caseService.getAllCaseComments({
- client,
- caseId,
- options: {
- fields: [],
- page: 1,
- perPage: 1,
- },
- });
+ if (myCase.attributes.status === 'closed') {
+ throw Boom.conflict(
+ `This case ${myCase.attributes.title} is closed. You can not pushed if the case is closed.`
+ );
+ }
const comments = await caseService.getAllCaseComments({
client,
@@ -74,6 +87,11 @@ export function initPushedCaseUserActionApi({ caseService, router, userActionSer
client,
caseId,
updatedAttributes: {
+ ...(myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing'
+ ? {
+ status: 'closed',
+ }
+ : {}),
pushed,
updated_at: pushedDate,
updated_by: pushedBy,
diff --git a/x-pack/plugins/case/server/routes/api/index.ts b/x-pack/plugins/case/server/routes/api/index.ts
index 1fe7d77a7c662..ced88fabf3160 100644
--- a/x-pack/plugins/case/server/routes/api/index.ts
+++ b/x-pack/plugins/case/server/routes/api/index.ts
@@ -9,7 +9,7 @@ import { initFindCasesApi } from '././cases/find_cases';
import { initGetCaseApi } from './cases/get_case';
import { initPatchCasesApi } from './cases/patch_cases';
import { initPostCaseApi } from './cases/post_case';
-import { initPushedCaseUserActionApi } from './cases/pushed_case';
+import { initPushCaseUserActionApi } from './cases/push_case';
import { initGetReportersApi } from './cases/reporters/get_reporters';
import { initGetCasesStatusApi } from './cases/status/get_status';
import { initGetTagsApi } from './cases/tags/get_tags';
@@ -37,7 +37,7 @@ export function initCaseApi(deps: RouteDeps) {
initGetCaseApi(deps);
initPatchCasesApi(deps);
initPostCaseApi(deps);
- initPushedCaseUserActionApi(deps);
+ initPushCaseUserActionApi(deps);
initGetAllUserActionsApi(deps);
// Comments
initDeleteCommentApi(deps);
From bb90c3566a0c8d0c9ab603a59e1a1e7d3fecad39 Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Date: Sat, 21 Mar 2020 08:09:03 -0400
Subject: [PATCH 09/17] wip to show pushed service in ui
---
.../case/use_get_case_user_actions.tsx | 7 +-
.../pages/case/components/case_view/index.tsx | 10 ++-
.../components/user_action_tree/index.tsx | 2 +-
.../user_action_tree/user_action_item.tsx | 64 ++++++++++++++++---
.../case/common/api/cases/user_actions.ts | 1 +
.../case/server/routes/api/cases/push_case.ts | 2 +-
6 files changed, 72 insertions(+), 14 deletions(-)
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx
index 0e3e5fe59d54f..b1df4aae6a60b 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx
@@ -51,7 +51,12 @@ export const useGetCaseUserActions = (caseId: string): UseGetCaseUserActions =>
// We are removing the first item because it will always the creation of the case
// and we do not want it to simplify our life
setCaseUserActionsState({
- caseUserActions: !isEmpty(response) ? response.slice(1) : [],
+ caseUserActions: !isEmpty(response)
+ ? [
+ ...response.slice(1),
+ { ...response[response.length - 1], actionId: 33, action: 'push-to-service' },
+ ]
+ : [],
isLoading: false,
isError: false,
});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
index e50853e8bc417..f7ee4b081a755 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
@@ -138,7 +138,13 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
const toggleStatusCase = useCallback(status => onUpdateField('status', status), [onUpdateField]);
const spyState = useMemo(() => ({ caseTitle: caseData.title }), [caseData.title]);
-
+ const hasDataToPush = useMemo(() => {
+ const indexPushToService = caseUserActions.findIndex(cua => cua.action === 'push-to-service');
+ if (indexPushToService === -1 || indexPushToService < caseUserActions.length - 1) {
+ return true;
+ }
+ return false;
+ }, [caseUserActions]);
const caseStatusData = useMemo(
() =>
caseData.status === 'open'
@@ -237,7 +243,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
onChange={onChangeStatus}
/>
- {pushButton}
+ {hasDataToPush && {pushButton}}
>
)}
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx
index f1a0fda12d072..9c2303a68df39 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx
@@ -15,7 +15,7 @@ import { useUpdateComment } from '../../../../containers/case/use_update_comment
import { useCurrentUser } from '../../../../lib/kibana';
import { AddComment } from '../add_comment';
import { getLabelTitle } from './helpers';
-import { UserActionItem } from './user_action_item';
+import { UserActionItem, UserActionItemContainer } from './user_action_item';
import { UserActionMarkdown } from './user_action_markdown';
export interface UserActionTreeProps {
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx
index 0b7dd5583026c..8efb6ed5e9351 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx
@@ -4,7 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiPanel } from '@elastic/eui';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLoadingSpinner,
+ EuiPanel,
+ EuiHorizontalRule,
+} from '@elastic/eui';
import React from 'react';
import styled, { css } from 'styled-components';
@@ -28,7 +34,7 @@ interface UserActionItemProps {
idToOutline?: string | null;
}
-const UserActionItemContainer = styled(EuiFlexGroup)`
+export const UserActionItemContainer = styled(EuiFlexGroup)`
${({ theme }) => css`
& {
background-image: linear-gradient(
@@ -80,6 +86,18 @@ const MyEuiPanel = styled(EuiPanel)<{ showoutline: string }>`
`
: ''}
`;
+const PushedContainer = styled(EuiFlexItem)`
+ margin-top: 8px;
+ margin-bottom: 24px;
+ hr {
+ margin: 5px;
+ height: 2px;
+ }
+`;
+
+const PushedInfoContainer = styled.div`
+ margin-left: 48px;
+`;
export const UserActionItem = ({
createdAt,
@@ -97,13 +115,41 @@ export const UserActionItem = ({
userName,
updatedAt,
}: UserActionItemProps) => (
-
-
- {fullName.length > 0 || userName.length > 0 ? (
-
- ) : (
-
- )}
+
+
+
+
+ {fullName.length > 0 || userName.length > 0 ? (
+
+ ) : (
+
+ )}
+
+
+ {isEditable && markdown}
+ {!isEditable && (
+
+ >}
+ linkId={linkId}
+ userName={userName}
+ updatedAt={updatedAt}
+ onEdit={onEdit}
+ outlineComment={outlineComment}
+ />
+ {markdown}
+
+ )}
+
+
{isEditable && markdown}
diff --git a/x-pack/plugins/case/common/api/cases/user_actions.ts b/x-pack/plugins/case/common/api/cases/user_actions.ts
index 6bc74ea04e119..2b70a698a5152 100644
--- a/x-pack/plugins/case/common/api/cases/user_actions.ts
+++ b/x-pack/plugins/case/common/api/cases/user_actions.ts
@@ -15,6 +15,7 @@ const UserActionFieldRt = rt.array(
rt.union([
rt.literal('comment'),
rt.literal('description'),
+ rt.literal('pushed'),
rt.literal('tags'),
rt.literal('title'),
rt.literal('status'),
diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts
index 471e86237e564..05d2b35c81481 100644
--- a/x-pack/plugins/case/server/routes/api/cases/push_case.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts
@@ -121,7 +121,7 @@ export function initPushCaseUserActionApi({
actionAt: pushedDate,
actionBy: pushedBy,
caseId,
- fields: [],
+ fields: ['pushed'],
newValue: JSON.stringify(pushed),
}),
],
From f557058f34861da222cd1695d2204ff72146d3cc Mon Sep 17 00:00:00 2001
From: Christos Nasikas
Date: Sat, 21 Mar 2020 13:43:32 +0200
Subject: [PATCH 10/17] Fix bugs
---
x-pack/legacy/plugins/siem/public/containers/case/api.ts | 5 +++--
x-pack/plugins/case/common/api/cases/case.ts | 2 ++
x-pack/plugins/case/server/routes/api/cases/push_case.ts | 4 ++--
3 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts
index 39ab9da145dc4..5b57159cace5f 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts
@@ -17,6 +17,7 @@ import {
CasePushRequest,
PushCaseParams,
PushCaseResponse,
+ ActionTypeExecutorResult,
} from '../../../../../../plugins/case/common/api';
import { KibanaServices } from '../../lib/kibana';
import {
@@ -206,7 +207,7 @@ export const pushToService = async (
casePushParams: PushCaseParams,
signal: AbortSignal
): Promise => {
- const response = await KibanaServices.get().http.fetch(
+ const response = await KibanaServices.get().http.fetch(
`/api/action/${connectorId}/_execute`,
{
method: 'POST',
@@ -214,7 +215,7 @@ export const pushToService = async (
signal,
}
);
- return decodePushCaseResponse(response);
+ return decodePushCaseResponse(response.data);
};
export const getActionLicense = async (signal: AbortSignal): Promise => {
diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts
index a5d73acc10502..627c3d2b04608 100644
--- a/x-pack/plugins/case/common/api/cases/case.ts
+++ b/x-pack/plugins/case/common/api/cases/case.ts
@@ -11,6 +11,8 @@ import { UserRT } from '../user';
import { CommentResponseRt } from './comment';
import { CasesStatusResponseRt } from './status';
+export { ActionTypeExecutorResult } from '../../../../actions/server/types';
+
const StatusRt = rt.union([rt.literal('open'), rt.literal('closed')]);
const CaseBasicRt = rt.type({
diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts
index 05d2b35c81481..7e6dca19445de 100644
--- a/x-pack/plugins/case/server/routes/api/cases/push_case.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts
@@ -29,7 +29,7 @@ export function initPushCaseUserActionApi({
params: schema.object({
case_id: schema.string(),
}),
- query: escapeHatch,
+ body: escapeHatch,
},
},
async (context, request, response) => {
@@ -37,7 +37,7 @@ export function initPushCaseUserActionApi({
const client = context.core.savedObjects.client;
const caseId = request.params.case_id;
const query = pipe(
- CasePushRequestRt.decode(request.query),
+ CasePushRequestRt.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
const pushedBy = await caseService.getUser({ request, response });
From 4f4ea85a6a6bfc628a81e2f2d82384b19c3c0a3d Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Date: Sat, 21 Mar 2020 23:48:16 -0400
Subject: [PATCH 11/17] finish the full flow with pushing to service now
---
.../case/use_get_case_user_actions.tsx | 39 +++++++--
.../components/case_view/__mock__/index.tsx | 18 +++++
.../case/components/case_view/index.test.tsx | 34 ++++++--
.../pages/case/components/case_view/index.tsx | 45 ++++++-----
.../components/case_view/push_to_service.tsx | 1 +
.../case/components/case_view/translations.ts | 14 ++++
.../components/user_action_tree/helpers.tsx | 80 ++++++++++++-------
.../components/user_action_tree/index.tsx | 49 ++++++++++--
.../user_action_tree/translations.ts | 21 +++++
.../user_action_tree/user_action_item.tsx | 64 ++++++++-------
.../user_action_tree/user_action_title.tsx | 26 +++---
.../plugins/siem/public/pages/case/index.tsx | 4 +-
.../siem/public/pages/case/translations.ts | 6 +-
.../routes/api/__fixtures__/mock_router.ts | 9 ++-
.../api/cases/comments/delete_all_comments.ts | 4 +-
.../api/cases/comments/delete_comment.ts | 4 +-
.../api/cases/comments/patch_comment.ts | 5 +-
.../routes/api/cases/comments/post_comment.ts | 17 ++--
.../api/cases/configure/patch_configure.ts | 3 +-
.../api/cases/configure/post_configure.ts | 3 +-
.../server/routes/api/cases/delete_cases.ts | 4 +-
.../routes/api/cases/patch_cases.test.ts | 6 +-
.../server/routes/api/cases/patch_cases.ts | 3 +-
.../case/server/routes/api/cases/post_case.ts | 8 +-
.../case/server/routes/api/cases/push_case.ts | 50 +++++++-----
.../case/server/saved_object_types/cases.ts | 3 +
x-pack/plugins/case/server/services/index.ts | 10 ++-
27 files changed, 364 insertions(+), 166 deletions(-)
create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx
index b1df4aae6a60b..cdc0f9cab5a6d 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx
@@ -14,12 +14,18 @@ import { CaseUserActions } from './types';
interface CaseUserActionsState {
caseUserActions: CaseUserActions[];
+ firstIndexPushToService: number;
+ hasDataToPush: boolean;
isLoading: boolean;
isError: boolean;
+ lastIndexPushToService: number;
}
const initialData: CaseUserActionsState = {
caseUserActions: [],
+ firstIndexPushToService: -1,
+ lastIndexPushToService: -1,
+ hasDataToPush: false,
isLoading: true,
isError: false,
};
@@ -28,6 +34,25 @@ interface UseGetCaseUserActions extends CaseUserActionsState {
fetchCaseUserActions: (caseId: string) => void;
}
+const getPushedInfo = (
+ caseUserActions: CaseUserActions[]
+): { firstIndexPushToService: number; lastIndexPushToService: number; hasDataToPush: boolean } => {
+ const firstIndexPushToService = caseUserActions.findIndex(
+ cua => cua.action === 'push-to-service'
+ );
+ const lastIndexPushToService = caseUserActions
+ .map(cua => cua.action)
+ .lastIndexOf('push-to-service');
+
+ const hasDataToPush =
+ lastIndexPushToService === -1 || lastIndexPushToService < caseUserActions.length - 1;
+ return {
+ firstIndexPushToService,
+ lastIndexPushToService,
+ hasDataToPush,
+ };
+};
+
export const useGetCaseUserActions = (caseId: string): UseGetCaseUserActions => {
const [caseUserActionsState, setCaseUserActionsState] = useState(
initialData
@@ -48,15 +73,12 @@ export const useGetCaseUserActions = (caseId: string): UseGetCaseUserActions =>
const response = await getCaseUserActions(thisCaseId, abortCtrl.signal);
if (!didCancel) {
// Attention Future developer
- // We are removing the first item because it will always the creation of the case
+ // We are removing the first item because it will always be the creation of the case
// and we do not want it to simplify our life
+ const caseUserActions = !isEmpty(response) ? response.slice(1) : [];
setCaseUserActionsState({
- caseUserActions: !isEmpty(response)
- ? [
- ...response.slice(1),
- { ...response[response.length - 1], actionId: 33, action: 'push-to-service' },
- ]
- : [],
+ caseUserActions,
+ ...getPushedInfo(caseUserActions),
isLoading: false,
isError: false,
});
@@ -70,6 +92,9 @@ export const useGetCaseUserActions = (caseId: string): UseGetCaseUserActions =>
});
setCaseUserActionsState({
caseUserActions: [],
+ firstIndexPushToService: -1,
+ lastIndexPushToService: -1,
+ hasDataToPush: false,
isLoading: false,
isError: true,
});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx
index 5eeaa46727242..fbd461242bdb5 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx
@@ -67,3 +67,21 @@ export const data: Case = {
export const dataClosed: Case = {
...caseClosedProps.initialData,
};
+
+export const caseUserActions = [
+ {
+ actionField: ['comment'],
+ action: 'create',
+ actionAt: '2020-03-20T17:10:09.814Z',
+ actionBy: {
+ fullName: 'Steph Milovic',
+ username: 'smilovic',
+ email: 'notmyrealemailfool@elastic.co',
+ },
+ newValue: 'Solve this fast!',
+ oldValue: null,
+ actionId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15',
+ caseId: '9b833a50-6acd-11ea-8fad-af86b1071bd9',
+ commentId: 'a357c6a0-5435-11ea-b427-fb51a1fcb7b8',
+ },
+];
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx
index 691df72d9f066..3c95ba8a4f0bf 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx
@@ -11,13 +11,18 @@ import { mount } from 'enzyme';
import routeData from 'react-router';
/* eslint-enable @kbn/eslint/module_migration */
import { CaseComponent } from './';
-import { caseProps, caseClosedProps, data, dataClosed } from './__mock__';
+import { caseProps, caseClosedProps, data, dataClosed, caseUserActions } from './__mock__';
import { TestProviders } from '../../../../mock';
import { useUpdateCase } from '../../../../containers/case/use_update_case';
import { useGetCaseUserActions } from '../../../../containers/case/use_get_case_user_actions';
+import { wait } from '../../../../lib/helpers';
+import { usePushToService } from './push_to_service';
jest.mock('../../../../containers/case/use_update_case');
+jest.mock('../../../../containers/case/use_get_case_user_actions');
+jest.mock('./push_to_service');
const useUpdateCaseMock = useUpdateCase as jest.Mock;
const useGetCaseUserActionsMock = useGetCaseUserActions as jest.Mock;
+const usePushToServiceMock = usePushToService as jest.Mock;
type Action = 'PUSH' | 'POP' | 'REPLACE';
const pop: Action = 'POP';
const location = {
@@ -70,10 +75,18 @@ describe('CaseView ', () => {
};
const defaultUseGetCaseUserActions = {
- caseUserActions: [],
+ caseUserActions,
+ fetchCaseUserActions,
+ firstIndexPushToService: -1,
+ hasDataToPush: false,
isLoading: false,
isError: false,
- fetchCaseUserActions,
+ lastIndexPushToService: -1,
+ };
+
+ const defaultUsePushToServiceMock = {
+ pushButton: <>{'Hello Button'}>,
+ pushCallouts: null,
};
beforeEach(() => {
@@ -81,9 +94,10 @@ describe('CaseView ', () => {
useUpdateCaseMock.mockImplementation(() => defaultUpdateCaseState);
jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation);
useGetCaseUserActionsMock.mockImplementation(() => defaultUseGetCaseUserActions);
+ usePushToServiceMock.mockImplementation(() => defaultUsePushToServiceMock);
});
- it('should render CaseComponent', () => {
+ it('should render CaseComponent', async () => {
const wrapper = mount(
@@ -91,6 +105,7 @@ describe('CaseView ', () => {
);
+ await wait();
expect(
wrapper
.find(`[data-test-subj="case-view-title"]`)
@@ -130,7 +145,7 @@ describe('CaseView ', () => {
).toEqual(data.description);
});
- it('should show closed indicators in header when case is closed', () => {
+ it('should show closed indicators in header when case is closed', async () => {
useUpdateCaseMock.mockImplementation(() => ({
...defaultUpdateCaseState,
caseData: dataClosed,
@@ -142,6 +157,7 @@ describe('CaseView ', () => {
);
+ await wait();
expect(wrapper.contains(`[data-test-subj="case-view-createdAt"]`)).toBe(false);
expect(
wrapper
@@ -157,7 +173,7 @@ describe('CaseView ', () => {
).toEqual(dataClosed.status);
});
- it('should dispatch update state when button is toggled', () => {
+ it('should dispatch update state when button is toggled', async () => {
const wrapper = mount(
@@ -165,18 +181,19 @@ describe('CaseView ', () => {
);
-
+ await wait();
wrapper
.find('input[data-test-subj="toggle-case-status"]')
.simulate('change', { target: { checked: true } });
expect(updateCaseProperty).toBeCalledWith({
+ fetchCaseUserActions,
updateKey: 'status',
updateValue: 'closed',
});
});
- it('should render comments', () => {
+ it('should render comments', async () => {
const wrapper = mount(
@@ -184,6 +201,7 @@ describe('CaseView ', () => {
);
+ await wait();
expect(
wrapper
.find(
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
index f7ee4b081a755..dd6917b2f3806 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
@@ -15,6 +15,7 @@ import {
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
+import { uniqBy } from 'lodash/fp';
import * as i18n from './translations';
import { Case } from '../../../../containers/case/types';
import { getCaseUrl } from '../../../../components/link_to';
@@ -64,8 +65,11 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
const [initLoadingData, setInitLoadingData] = useState(true);
const {
caseUserActions,
- isLoading: isLoadingUserActions,
fetchCaseUserActions,
+ firstIndexPushToService,
+ hasDataToPush,
+ isLoading: isLoadingUserActions,
+ lastIndexPushToService,
} = useGetCaseUserActions(caseId);
const { caseData, isLoading, updateKey, updateCase, updateCaseProperty } = useUpdateCase(
caseId,
@@ -119,7 +123,6 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
},
[fetchCaseUserActions, updateCaseProperty, caseData.status]
);
-
const handleUpdateCase = useCallback(
(newCase: Case) => {
updateCase(newCase);
@@ -127,6 +130,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
},
[updateCase, fetchCaseUserActions]
);
+
const { pushButton, pushCallouts } = usePushToService({
caseData,
isNew: caseUserActions.filter(cua => cua.action === 'push-to-service').length === 0,
@@ -135,16 +139,15 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]);
const onSubmitTitle = useCallback(newTitle => onUpdateField('title', newTitle), [onUpdateField]);
- const toggleStatusCase = useCallback(status => onUpdateField('status', status), [onUpdateField]);
-
+ const toggleStatusCase = useCallback(
+ e => onUpdateField('status', e.target.checked ? 'closed' : 'open'),
+ [onUpdateField]
+ );
const spyState = useMemo(() => ({ caseTitle: caseData.title }), [caseData.title]);
- const hasDataToPush = useMemo(() => {
- const indexPushToService = caseUserActions.findIndex(cua => cua.action === 'push-to-service');
- if (indexPushToService === -1 || indexPushToService < caseUserActions.length - 1) {
- return true;
- }
- return false;
- }, [caseUserActions]);
+ const participants = useMemo(
+ () => uniqBy('actionBy.username', caseUserActions).map(cau => cau.actionBy),
+ [caseUserActions]
+ );
const caseStatusData = useMemo(
() =>
caseData.status === 'open'
@@ -175,13 +178,9 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
subject: i18n.EMAIL_SUBJECT(caseData.title),
body: i18n.EMAIL_BODY(caseLink),
}),
- [caseData.title]
+ [caseLink, caseData.title]
);
- const onChangeStatus = useCallback(e => toggleStatusCase(e.target.checked ? 'closed' : 'open'), [
- toggleStatusCase,
- ]);
-
useEffect(() => {
if (initLoadingData && !isLoadingUserActions) {
setInitLoadingData(false);
@@ -210,7 +209,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
caseId={caseData.id}
caseTitle={caseData.title}
isLoading={isLoading && updateKey === 'status'}
- toggleStatusCase={onChangeStatus}
+ toggleStatusCase={toggleStatusCase}
{...caseStatusData}
/>
@@ -227,8 +226,10 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
caseUserActions={caseUserActions}
data={caseData}
fetchUserActions={fetchCaseUserActions.bind(null, caseData.id)}
+ firstIndexPushToService={firstIndexPushToService}
isLoadingDescription={isLoading && updateKey === 'description'}
isLoadingUserActions={isLoadingUserActions}
+ lastIndexPushToService={lastIndexPushToService}
onUpdateField={onUpdateField}
/>
@@ -240,7 +241,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
isSelected={caseStatusData.isSelected}
isLoading={isLoading && updateKey === 'status'}
label={caseStatusData.buttonLabel}
- onChange={onChangeStatus}
+ onChange={toggleStatusCase}
/>
{hasDataToPush && {pushButton}}
@@ -250,11 +251,17 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
+
(
0}
isLoading={isLoading}
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts
index 73aa5a47182c4..d869e962c44f6 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts
@@ -34,6 +34,20 @@ export const REMOVED_FIELD = i18n.translate('xpack.siem.case.caseView.actionLabe
defaultMessage: 'removed',
});
+export const PUSHED_NEW_INCIDENT = i18n.translate(
+ 'xpack.siem.case.caseView.actionLabel.pushedNewIncident',
+ {
+ defaultMessage: 'pushed as new incident',
+ }
+);
+
+export const UPDATE_INCIDENT = i18n.translate(
+ 'xpack.siem.case.caseView.actionLabel.updateIncident',
+ {
+ defaultMessage: 'updated incident',
+ }
+);
+
export const ADDED_DESCRIPTION = i18n.translate(
'xpack.siem.case.caseView.actionLabel.addDescription',
{
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx
index a91435869cee4..2a7197b2c635b 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx
@@ -4,42 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiLink } from '@elastic/eui';
import React from 'react';
+import { CasePushedData } from '../../../../../../../../plugins/case/common/api';
import { CaseUserActions } from '../../../../containers/case/types';
import * as i18n from '../case_view/translations';
-export const getLabelTitle = (field: string, action: CaseUserActions) => {
- if (field === 'tags' && action.action === 'add') {
- return (
-
-
- {i18n.ADDED_FIELD} {i18n.TAGS.toLowerCase()}
-
- {action.newValue != null &&
- action.newValue.split(',').map(tag => (
-
- {tag}
-
- ))}
-
- );
- }
- if (field === 'tags' && action.action === 'delete') {
- return (
-
-
- {i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()}
-
- {action.newValue != null &&
- action.newValue.split(',').map(tag => (
-
- {tag}
-
- ))}
-
- );
+interface LabelTitle {
+ action: CaseUserActions;
+ field: string;
+ firstIndexPushToService: number;
+ index: number;
+}
+
+export const getLabelTitle = ({ action, field, firstIndexPushToService, index }: LabelTitle) => {
+ if (field === 'tags') {
+ return getTagsLabelTitle(action);
} else if (field === 'title' && action.action === 'update') {
return `${i18n.CHANGED_FIELD.toLowerCase()} ${i18n.CASE_NAME.toLowerCase()} ${i18n.TO} "${
action.newValue
@@ -52,6 +33,43 @@ export const getLabelTitle = (field: string, action: CaseUserActions) => {
} ${i18n.CASE}`;
} else if (field === 'comment' && action.action === 'update') {
return `${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`;
+ } else if (field === 'pushed' && action.action === 'push-to-service' && action.newValue != null) {
+ return getPushedServiceLabelTitle(action, firstIndexPushToService, index);
}
return '';
};
+
+const getTagsLabelTitle = (action: CaseUserActions) => (
+
+
+ {action.action === 'add' && i18n.ADDED_FIELD}
+ {action.action === 'delete' && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()}
+
+ {action.newValue != null &&
+ action.newValue.split(',').map(tag => (
+
+ {tag}
+
+ ))}
+
+);
+
+const getPushedServiceLabelTitle = (
+ action: CaseUserActions,
+ firstIndexPushToService: number,
+ index: number
+) => {
+ const pushedVal = JSON.parse(action.newValue ?? '') as CasePushedData;
+ return (
+
+
+ {firstIndexPushToService === index ? i18n.PUSHED_NEW_INCIDENT : i18n.UPDATE_INCIDENT}
+
+
+
+ {pushedVal?.connector_name} {pushedVal?.external_title}
+
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx
index 9c2303a68df39..e41e14a6cc1f1 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx
@@ -5,7 +5,8 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
-import React, { useCallback, useMemo, useRef, useState } from 'react';
+import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react';
+import { useParams } from 'react-router-dom';
import styled from 'styled-components';
import * as i18n from '../case_view/translations';
@@ -15,15 +16,17 @@ import { useUpdateComment } from '../../../../containers/case/use_update_comment
import { useCurrentUser } from '../../../../lib/kibana';
import { AddComment } from '../add_comment';
import { getLabelTitle } from './helpers';
-import { UserActionItem, UserActionItemContainer } from './user_action_item';
+import { UserActionItem } from './user_action_item';
import { UserActionMarkdown } from './user_action_markdown';
export interface UserActionTreeProps {
data: Case;
caseUserActions: CaseUserActions[];
+ fetchUserActions: () => void;
+ firstIndexPushToService: number;
isLoadingDescription: boolean;
isLoadingUserActions: boolean;
- fetchUserActions: () => void;
+ lastIndexPushToService: number;
onUpdateField: (updateKey: keyof Case, updateValue: string | string[]) => void;
}
@@ -39,11 +42,15 @@ export const UserActionTree = React.memo(
data: caseData,
caseUserActions,
fetchUserActions,
+ firstIndexPushToService,
isLoadingDescription,
isLoadingUserActions,
+ lastIndexPushToService,
onUpdateField,
}: UserActionTreeProps) => {
+ const { commentId } = useParams();
const handlerTimeoutId = useRef(0);
+ const [initLoading, setInitLoading] = useState(true);
const [selectedOutlineCommentId, setSelectedOutlineCommentId] = useState('');
const { comments, isLoadingIds, updateComment, addPostedComment } = useUpdateComment(
caseData.comments
@@ -77,6 +84,15 @@ export const UserActionTree = React.memo(
const handleOutlineComment = useCallback(
(id: string) => {
+ const moveToTarget = document.getElementById(`${id}-permLink`);
+ if (moveToTarget != null) {
+ const yOffset = -60;
+ const y = moveToTarget.getBoundingClientRect().top + window.pageYOffset + yOffset;
+ window.scrollTo({
+ top: y,
+ behavior: 'smooth',
+ });
+ }
window.clearTimeout(handlerTimeoutId.current);
setSelectedOutlineCommentId(id);
handlerTimeoutId.current = window.setTimeout(() => {
@@ -123,6 +139,15 @@ export const UserActionTree = React.memo(
[caseData.id, handleUpdate]
);
+ useEffect(() => {
+ if (initLoading && !isLoadingUserActions && isLoadingIds.length === 0) {
+ setInitLoading(false);
+ if (commentId != null) {
+ handleOutlineComment(commentId);
+ }
+ }
+ }, [commentId, initLoading, isLoadingUserActions, isLoadingIds]);
+
return (
<>
- {caseUserActions.map(action => {
+
+ {caseUserActions.map((action, index) => {
if (action.commentId != null && action.action === 'create') {
const comment = comments.find(c => c.id === action.commentId);
if (comment != null) {
@@ -171,7 +197,12 @@ export const UserActionTree = React.memo(
}
if (action.actionField.length === 1) {
const myField = action.actionField[0];
- const labelTitle: string | JSX.Element = getLabelTitle(myField, action);
+ const labelTitle: string | JSX.Element = getLabelTitle({
+ action,
+ field: myField,
+ firstIndexPushToService,
+ index,
+ });
return (
);
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts
new file mode 100644
index 0000000000000..3958938e9c126
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const ALREADY_PUSHED_TO_SERVICE = i18n.translate(
+ 'xpack.siem.case.caseView.alreadyPushedToService',
+ {
+ defaultMessage: 'Already pushed to Service Now incident',
+ }
+);
+
+export const REQUIRED_UPDATE_TO_SERVICE = i18n.translate(
+ 'xpack.siem.case.caseView.requiredUpdateToService',
+ {
+ defaultMessage: 'Requires update to ServiceNow incident',
+ }
+);
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx
index 8efb6ed5e9351..dc51e6f3e83fb 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx
@@ -10,12 +10,14 @@ import {
EuiLoadingSpinner,
EuiPanel,
EuiHorizontalRule,
+ EuiText,
} from '@elastic/eui';
import React from 'react';
import styled, { css } from 'styled-components';
import { UserActionAvatar } from './user_action_avatar';
import { UserActionTitle } from './user_action_title';
+import * as i18n from './translations';
interface UserActionItemProps {
createdAt: string;
@@ -31,6 +33,8 @@ interface UserActionItemProps {
userName: string;
updatedAt?: string | null;
outlineComment?: (id: string) => void;
+ showBottomFooter?: boolean;
+ showTopFooter?: boolean;
idToOutline?: string | null;
}
@@ -86,13 +90,16 @@ const MyEuiPanel = styled(EuiPanel)<{ showoutline: string }>`
`
: ''}
`;
+
const PushedContainer = styled(EuiFlexItem)`
- margin-top: 8px;
- margin-bottom: 24px;
- hr {
- margin: 5px;
- height: 2px;
- }
+ ${({ theme }) => `
+ margin-top: ${theme.eui.euiSizeS};
+ margin-bottom: ${theme.eui.euiSizeXL};
+ hr {
+ margin: 5px;
+ height: ${theme.eui.euiBorderWidthThick};
+ }
+ `}
`;
const PushedInfoContainer = styled.div`
@@ -112,6 +119,8 @@ export const UserActionItem = ({
markdown,
onEdit,
outlineComment,
+ showBottomFooter,
+ showTopFooter,
userName,
updatedAt,
}: UserActionItemProps) => (
@@ -137,7 +146,7 @@ export const UserActionItem = ({
createdAt={createdAt}
id={id}
isLoading={isLoading}
- labelAction={labelAction}
+ labelAction={labelEditAction}
labelTitle={labelTitle ?? <>>}
linkId={linkId}
userName={userName}
@@ -151,29 +160,22 @@ export const UserActionItem = ({
-
- {isEditable && markdown}
- {!isEditable && (
-
- >}
- linkId={linkId}
- userName={userName}
- updatedAt={updatedAt}
- onEdit={onEdit}
- outlineComment={outlineComment}
- />
- {markdown}
-
- )}
-
+ {showTopFooter && (
+
+
+
+ {i18n.ALREADY_PUSHED_TO_SERVICE}
+
+
+
+ {showBottomFooter && (
+
+
+ {i18n.REQUIRED_UPDATE_TO_SERVICE}
+
+
+ )}
+
+ )}
);
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx
index 289eebae6c527..d3c94a1f265d6 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx
@@ -65,7 +65,7 @@ export const UserActionTitle = ({
}, [id, labelEditAction, onEdit]);
const handleAnchorLink = useCallback(() => {
- copy(`${window.location.origin}${window.location.pathname}#case/${caseId}/id${urlSearch}`, {
+ copy(`${window.location.origin}${window.location.pathname}#case/${caseId}/${id}${urlSearch}`, {
debug: true,
});
}, [caseId, id, urlSearch]);
@@ -74,15 +74,6 @@ export const UserActionTitle = ({
if (outlineComment != null && linkId != null) {
outlineComment(linkId);
}
- const moveToTarget = document.getElementById(`${linkId}-permLink`);
- if (moveToTarget != null) {
- const yOffset = -60;
- const y = moveToTarget.getBoundingClientRect().top + window.pageYOffset + yOffset;
- window.scrollTo({
- top: y,
- behavior: 'smooth',
- });
- }
}, [linkId, outlineComment]);
return (
@@ -128,11 +119,22 @@ export const UserActionTitle = ({
{!isEmpty(linkId) && (
-
+
)}
-
+
{propertyActions.length > 0 && (
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx
index cecb38816781b..124cefa726a8b 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx
@@ -30,10 +30,10 @@ const CaseContainerComponent: React.FC = () => (
-
+
-
+
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts
index f91e623197b2f..4bb07053a9916 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts
@@ -34,10 +34,14 @@ export const CLOSED_ON = i18n.translate('xpack.siem.case.caseView.closedOn', {
defaultMessage: 'Closed on',
});
-export const REPORTER = i18n.translate('xpack.siem.case.caseView.createdBy', {
+export const REPORTER = i18n.translate('xpack.siem.case.caseView.reporterLabel', {
defaultMessage: 'Reporter',
});
+export const PARTICIPANTS = i18n.translate('xpack.siem.case.caseView.particpantsLabel', {
+ defaultMessage: 'Participants',
+});
+
export const CREATE_BC_TITLE = i18n.translate('xpack.siem.case.caseView.breadcrumb', {
defaultMessage: 'Create',
});
diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts
index 0945ded8d57c3..eff91fff32c02 100644
--- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts
+++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts
@@ -6,7 +6,7 @@
import { IRouter } from 'kibana/server';
import { loggingServiceMock, httpServiceMock } from '../../../../../../../src/core/server/mocks';
-import { CaseService, CaseConfigureService, CaseUserActionService } from '../../../services';
+import { CaseService, CaseConfigureService } from '../../../services';
import { authenticationMock } from '../__fixtures__';
import { RouteDeps } from '../types';
@@ -22,19 +22,20 @@ export const createRoute = async (
const caseServicePlugin = new CaseService(log);
const caseConfigureServicePlugin = new CaseConfigureService(log);
- const userActionServicePlugin = new CaseUserActionService(log);
const caseService = await caseServicePlugin.setup({
authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(),
});
const caseConfigureService = await caseConfigureServicePlugin.setup();
- const userActionService = await userActionServicePlugin.setup();
api({
caseConfigureService,
caseService,
router,
- userActionService,
+ userActionService: {
+ postUserActions: jest.fn(),
+ getUserActions: jest.fn(),
+ },
});
return router[method].mock.calls[0][1];
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts
index 8f0e616ceacbb..a1279ecd8247f 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts
@@ -23,7 +23,7 @@ export function initDeleteAllCommentsApi({ caseService, router, userActionServic
async (context, request, response) => {
try {
const client = context.core.savedObjects.client;
- const deletedBy = await caseService.getUser({ request, response });
+ const { username, full_name, email } = await caseService.getUser({ request, response });
const deleteDate = new Date().toISOString();
const comments = await caseService.getAllCaseComments({
@@ -45,7 +45,7 @@ export function initDeleteAllCommentsApi({ caseService, router, userActionServic
buildCommentUserActionItem({
action: 'delete',
actionAt: deleteDate,
- actionBy: deletedBy,
+ actionBy: { username, full_name, email },
caseId: request.params.case_id,
commentId: comment.id,
fields: ['comment'],
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts
index 8587d24a85102..8d390ddb7ed17 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts
@@ -26,7 +26,7 @@ export function initDeleteCommentApi({ caseService, router, userActionService }:
async (context, request, response) => {
try {
const client = context.core.savedObjects.client;
- const deletedBy = await caseService.getUser({ request, response });
+ const { username, full_name, email } = await caseService.getUser({ request, response });
const deleteDate = new Date().toISOString();
const myComment = await caseService.getComment({
@@ -56,7 +56,7 @@ export function initDeleteCommentApi({ caseService, router, userActionService }:
buildCommentUserActionItem({
action: 'delete',
actionAt: deleteDate,
- actionBy: deletedBy,
+ actionBy: { username, full_name, email },
caseId: request.params.case_id,
commentId: request.params.comment_id,
fields: ['comment'],
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts
index ded5a8eefc03a..2299591884354 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts
@@ -57,9 +57,8 @@ export function initPatchCommentApi({ caseService, router, userActionService }:
);
}
- const updatedBy = await caseService.getUser({ request, response });
+ const { username, full_name, email } = await caseService.getUser({ request, response });
const updatedDate = new Date().toISOString();
- const { email, full_name, username } = updatedBy;
const updatedComment = await caseService.patchComment({
client: context.core.savedObjects.client,
commentId: query.id,
@@ -77,7 +76,7 @@ export function initPatchCommentApi({ caseService, router, userActionService }:
buildCommentUserActionItem({
action: 'update',
actionAt: updatedDate,
- actionBy: updatedBy,
+ actionBy: { username, full_name, email },
caseId: request.params.case_id,
commentId: updatedComment.id,
fields: ['comment'],
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts
index 4c5405a1aef5e..c0a321ab7b86a 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts
@@ -40,7 +40,12 @@ export function initPostCommentApi({ caseService, router, userActionService }: R
fold(throwErrors(Boom.badRequest), identity)
);
- const createdBy = await caseService.getUser({ request, response });
+ const myCase = await caseService.getCase({
+ client: context.core.savedObjects.client,
+ caseId: request.params.case_id,
+ });
+
+ const { username, full_name, email } = await caseService.getUser({ request, response });
const createdDate = new Date().toISOString();
const newComment = await caseService.postNewComment({
@@ -48,13 +53,15 @@ export function initPostCommentApi({ caseService, router, userActionService }: R
attributes: transformNewComment({
createdDate,
...query,
- ...createdBy,
+ username,
+ full_name,
+ email,
}),
references: [
{
type: CASE_SAVED_OBJECT,
name: `associated-${CASE_SAVED_OBJECT}`,
- id: request.params.case_id,
+ id: myCase.id,
},
],
});
@@ -65,8 +72,8 @@ export function initPostCommentApi({ caseService, router, userActionService }: R
buildCommentUserActionItem({
action: 'create',
actionAt: createdDate,
- actionBy: createdBy,
- caseId: request.params.case_id,
+ actionBy: { username, full_name, email },
+ caseId: myCase.id,
commentId: newComment.id,
fields: ['comment'],
newValue: query.comment,
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts
index 1542394fc438d..3a1b9d5059cbc 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts
@@ -48,8 +48,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout
);
}
- const updatedBy = await caseService.getUser({ request, response });
- const { email, full_name, username } = updatedBy;
+ const { username, full_name, email } = await caseService.getUser({ request, response });
const updateDate = new Date().toISOString();
const patch = await caseConfigureService.patch({
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts
index c839d36dcf4df..2a23abf0cbf21 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts
@@ -42,8 +42,7 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route
)
);
}
- const updatedBy = await caseService.getUser({ request, response });
- const { email, full_name, username } = updatedBy;
+ const { email, full_name, username } = await caseService.getUser({ request, response });
const creationDate = new Date().toISOString();
const post = await caseConfigureService.post({
diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts
index ea97122bd322c..8b0384c12edce 100644
--- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts
@@ -54,7 +54,7 @@ export function initDeleteCasesApi({ caseService, router, userActionService }: R
)
);
}
- const deletedBy = await caseService.getUser({ request, response });
+ const { username, full_name, email } = await caseService.getUser({ request, response });
const deleteDate = new Date().toISOString();
await userActionService.postUserActions({
@@ -63,7 +63,7 @@ export function initDeleteCasesApi({ caseService, router, userActionService }: R
buildCaseUserActionItem({
action: 'create',
actionAt: deleteDate,
- actionBy: deletedBy,
+ actionBy: { username, full_name, email },
caseId: id,
fields: ['comment', 'description', 'status', 'tags', 'title'],
})
diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts
index 19ff7f0734a77..3c3e111cfa901 100644
--- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts
@@ -52,15 +52,16 @@ describe('PATCH cases', () => {
{
closed_at: '2019-11-25T21:54:48.952Z',
closed_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' },
- comment_ids: ['mock-comment-1'],
comments: [],
created_at: '2019-11-25T21:54:48.952Z',
created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' },
description: 'This is a brand new case of a bad meanie defacing data',
id: 'mock-id-1',
+ pushed: null,
status: 'closed',
tags: ['defacement'],
title: 'Super Bad Security Issue',
+ totalComment: 0,
updated_at: '2019-11-25T21:54:48.952Z',
updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' },
version: 'WzE3LDFd',
@@ -94,15 +95,16 @@ describe('PATCH cases', () => {
{
closed_at: null,
closed_by: null,
- comment_ids: [],
comments: [],
created_at: '2019-11-25T22:32:17.947Z',
created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' },
description: 'Oh no, a bad meanie going LOLBins all over the place!',
id: 'mock-id-4',
+ pushed: null,
status: 'open',
tags: ['LOLBins'],
title: 'Another bad one',
+ totalComment: 0,
updated_at: '2019-11-25T21:54:48.952Z',
updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' },
version: 'WzE3LDFd',
diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts
index e9102f7916348..62c41d872e2c8 100644
--- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts
@@ -74,8 +74,7 @@ export function initPatchCasesApi({ caseService, router, userActionService }: Ro
return Object.keys(updateCaseAttributes).length > 0;
});
if (updateFilterCases.length > 0) {
- const updatedBy = await caseService.getUser({ request, response });
- const { email, full_name, username } = updatedBy;
+ const { username, full_name, email } = await caseService.getUser({ request, response });
const updatedDt = new Date().toISOString();
const updatedCases = await caseService.patchCases({
client,
diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.ts
index 0c881f1b11c87..75be68013bcd4 100644
--- a/x-pack/plugins/case/server/routes/api/cases/post_case.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/post_case.ts
@@ -31,14 +31,16 @@ export function initPostCaseApi({ caseService, router, userActionService }: Rout
fold(throwErrors(Boom.badRequest), identity)
);
- const createdBy = await caseService.getUser({ request, response });
+ const { username, full_name, email } = await caseService.getUser({ request, response });
const createdDate = new Date().toISOString();
const newCase = await caseService.postNewCase({
client,
attributes: transformNewCase({
createdDate,
newCase: query,
- ...createdBy,
+ username,
+ full_name,
+ email,
}),
});
@@ -48,7 +50,7 @@ export function initPostCaseApi({ caseService, router, userActionService }: Rout
buildCaseUserActionItem({
action: 'create',
actionAt: createdDate,
- actionBy: createdBy,
+ actionBy: { username, full_name, email },
caseId: newCase.id,
fields: ['description', 'status', 'tags', 'title'],
newValue: JSON.stringify(query),
diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts
index 7e6dca19445de..be1793e43cd44 100644
--- a/x-pack/plugins/case/server/routes/api/cases/push_case.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts
@@ -40,7 +40,7 @@ export function initPushCaseUserActionApi({
CasePushRequestRt.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
- const pushedBy = await caseService.getUser({ request, response });
+ const { username, full_name, email } = await caseService.getUser({ request, response });
const pushedDate = new Date().toISOString();
const [myCase, myCaseConfigure, totalCommentsFindByCases] = await Promise.all([
@@ -78,7 +78,7 @@ export function initPushCaseUserActionApi({
const pushed = {
at: pushedDate,
- by: pushedBy,
+ by: { username, full_name, email },
...query,
};
@@ -94,7 +94,7 @@ export function initPushCaseUserActionApi({
: {}),
pushed,
updated_at: pushedDate,
- updated_by: pushedBy,
+ updated_by: { username, full_name, email },
},
version: myCase.version,
}),
@@ -104,29 +104,41 @@ export function initPushCaseUserActionApi({
commentId: comment.id,
updatedAttributes: {
pushed_at: pushedDate,
- pushed_by: pushedBy,
+ pushed_by: { username, full_name, email },
updated_at: pushedDate,
- updated_by: pushedBy,
+ updated_by: { username, full_name, email },
},
version: comment.version,
})),
}),
+ userActionService.postUserActions({
+ client,
+ actions: [
+ ...(myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing'
+ ? [
+ buildCaseUserActionItem({
+ action: 'update',
+ actionAt: pushedDate,
+ actionBy: { username, full_name, email },
+ caseId,
+ fields: [status],
+ newValue: 'closes',
+ oldValue: myCase.attributes.status,
+ }),
+ ]
+ : []),
+ buildCaseUserActionItem({
+ action: 'push-to-service',
+ actionAt: pushedDate,
+ actionBy: { username, full_name, email },
+ caseId,
+ fields: ['pushed'],
+ newValue: JSON.stringify(pushed),
+ }),
+ ],
+ }),
]);
- await userActionService.postUserActions({
- client,
- actions: [
- buildCaseUserActionItem({
- action: 'push-to-service',
- actionAt: pushedDate,
- actionBy: pushedBy,
- caseId,
- fields: ['pushed'],
- newValue: JSON.stringify(pushed),
- }),
- ],
- });
-
return response.ok({
body: CaseResponseRt.encode(
flattenCaseSavedObject(
diff --git a/x-pack/plugins/case/server/saved_object_types/cases.ts b/x-pack/plugins/case/server/saved_object_types/cases.ts
index c445374c37adc..004558983b069 100644
--- a/x-pack/plugins/case/server/saved_object_types/cases.ts
+++ b/x-pack/plugins/case/server/saved_object_types/cases.ts
@@ -62,6 +62,9 @@ export const caseSavedObjectType: SavedObjectsType = {
full_name: {
type: 'keyword',
},
+ email: {
+ type: 'keyword',
+ },
},
},
connector_id: {
diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts
index 01d55e993ad8a..42f691d929a99 100644
--- a/x-pack/plugins/case/server/services/index.ts
+++ b/x-pack/plugins/case/server/services/index.ts
@@ -18,7 +18,13 @@ import {
} from 'kibana/server';
import { AuthenticatedUser, SecurityPluginSetup } from '../../../security/server';
-import { CaseAttributes, CommentAttributes, SavedObjectFindOptions, User } from '../../common/api';
+import {
+ CaseAttributes,
+ CommentAttributes,
+ SavedObjectFindOptions,
+ User,
+ CasePushedData,
+} from '../../common/api';
import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT } from '../saved_object_types';
import { readReporters } from './reporters/read_reporters';
import { readTags } from './tags/read_tags';
@@ -65,7 +71,7 @@ interface PostCommentArgs extends ClientArgs {
interface PatchCase {
caseId: string;
- updatedAttributes: Partial;
+ updatedAttributes: Partial;
version?: string;
}
type PatchCaseArgs = PatchCase & ClientArgs;
From 3079c9143fd59aa7a1a9f1d120e4079d5f4459e3 Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Date: Mon, 23 Mar 2020 09:51:37 -0400
Subject: [PATCH 12/17] review about client discrepency
---
.../server/routes/api/cases/comments/delete_all_comments.ts | 2 +-
.../case/server/routes/api/cases/comments/delete_comment.ts | 2 +-
.../case/server/routes/api/cases/comments/find_comments.ts | 5 +++--
.../case/server/routes/api/cases/comments/get_all_comment.ts | 3 ++-
.../case/server/routes/api/cases/comments/patch_comment.ts | 4 ++--
.../case/server/routes/api/cases/comments/post_comment.ts | 2 +-
x-pack/plugins/case/server/routes/api/cases/get_case.ts | 5 +++--
x-pack/plugins/case/server/routes/api/cases/push_case.ts | 2 +-
.../case/server/routes/api/cases/reporters/get_reporters.ts | 3 ++-
.../case/server/routes/api/cases/status/get_status.ts | 5 +++--
x-pack/plugins/case/server/routes/api/cases/tags/get_tags.ts | 3 ++-
.../routes/api/cases/user_actions/get_all_user_actions.ts | 3 ++-
12 files changed, 23 insertions(+), 16 deletions(-)
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts
index a1279ecd8247f..941ac90f2e90e 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts
@@ -27,7 +27,7 @@ export function initDeleteAllCommentsApi({ caseService, router, userActionServic
const deleteDate = new Date().toISOString();
const comments = await caseService.getAllCaseComments({
- client: context.core.savedObjects.client,
+ client,
caseId: request.params.case_id,
});
await Promise.all(
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts
index 8d390ddb7ed17..44e57fc809e04 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts
@@ -30,7 +30,7 @@ export function initDeleteCommentApi({ caseService, router, userActionService }:
const deleteDate = new Date().toISOString();
const myComment = await caseService.getComment({
- client: context.core.savedObjects.client,
+ client,
commentId: request.params.comment_id,
});
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts
index dcf70d0d9819c..92da64cebee74 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts
@@ -32,6 +32,7 @@ export function initFindCaseCommentsApi({ caseService, router }: RouteDeps) {
},
async (context, request, response) => {
try {
+ const client = context.core.savedObjects.client;
const query = pipe(
SavedObjectFindOptionsRt.decode(request.query),
fold(throwErrors(Boom.badRequest), identity)
@@ -39,7 +40,7 @@ export function initFindCaseCommentsApi({ caseService, router }: RouteDeps) {
const args = query
? {
- client: context.core.savedObjects.client,
+ client,
caseId: request.params.case_id,
options: {
...query,
@@ -47,7 +48,7 @@ export function initFindCaseCommentsApi({ caseService, router }: RouteDeps) {
},
}
: {
- client: context.core.savedObjects.client,
+ client,
caseId: request.params.case_id,
};
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts
index 65f2de7125236..1500039eb2cc2 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts
@@ -22,8 +22,9 @@ export function initGetAllCommentsApi({ caseService, router }: RouteDeps) {
},
async (context, request, response) => {
try {
+ const client = context.core.savedObjects.client;
const comments = await caseService.getAllCaseComments({
- client: context.core.savedObjects.client,
+ client,
caseId: request.params.case_id,
});
return response.ok({
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts
index 2299591884354..c67ad1bdaea71 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts
@@ -36,7 +36,7 @@ export function initPatchCommentApi({ caseService, router, userActionService }:
);
const myComment = await caseService.getComment({
- client: context.core.savedObjects.client,
+ client,
commentId: query.id,
});
@@ -60,7 +60,7 @@ export function initPatchCommentApi({ caseService, router, userActionService }:
const { username, full_name, email } = await caseService.getUser({ request, response });
const updatedDate = new Date().toISOString();
const updatedComment = await caseService.patchComment({
- client: context.core.savedObjects.client,
+ client,
commentId: query.id,
updatedAttributes: {
comment: query.comment,
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts
index c0a321ab7b86a..2410505872a3a 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts
@@ -41,7 +41,7 @@ export function initPostCommentApi({ caseService, router, userActionService }: R
);
const myCase = await caseService.getCase({
- client: context.core.savedObjects.client,
+ client,
caseId: request.params.case_id,
});
diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.ts
index 1415513bca346..e947118a39e8e 100644
--- a/x-pack/plugins/case/server/routes/api/cases/get_case.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/get_case.ts
@@ -25,10 +25,11 @@ export function initGetCaseApi({ caseService, router }: RouteDeps) {
},
async (context, request, response) => {
try {
+ const client = context.core.savedObjects.client;
const includeComments = JSON.parse(request.query.includeComments);
const theCase = await caseService.getCase({
- client: context.core.savedObjects.client,
+ client,
caseId: request.params.case_id,
});
@@ -37,7 +38,7 @@ export function initGetCaseApi({ caseService, router }: RouteDeps) {
}
const theComments = await caseService.getAllCaseComments({
- client: context.core.savedObjects.client,
+ client,
caseId: request.params.case_id,
});
diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts
index be1793e43cd44..462b7af0db336 100644
--- a/x-pack/plugins/case/server/routes/api/cases/push_case.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts
@@ -45,7 +45,7 @@ export function initPushCaseUserActionApi({
const [myCase, myCaseConfigure, totalCommentsFindByCases] = await Promise.all([
caseService.getCase({
- client: context.core.savedObjects.client,
+ client,
caseId: request.params.case_id,
}),
caseConfigureService.find({ client }),
diff --git a/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts b/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts
index 519bb198f5f9e..56862a96e0563 100644
--- a/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts
@@ -16,8 +16,9 @@ export function initGetReportersApi({ caseService, router }: RouteDeps) {
},
async (context, request, response) => {
try {
+ const client = context.core.savedObjects.client;
const reporters = await caseService.getReporters({
- client: context.core.savedObjects.client,
+ client,
});
return response.ok({ body: UsersRt.encode(reporters) });
} catch (error) {
diff --git a/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts b/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts
index b4fc90d702604..f7431729d398c 100644
--- a/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts
@@ -18,8 +18,9 @@ export function initGetCasesStatusApi({ caseService, router }: RouteDeps) {
},
async (context, request, response) => {
try {
+ const client = context.core.savedObjects.client;
const argsOpenCases = {
- client: context.core.savedObjects.client,
+ client,
options: {
fields: [],
page: 1,
@@ -29,7 +30,7 @@ export function initGetCasesStatusApi({ caseService, router }: RouteDeps) {
};
const argsClosedCases = {
- client: context.core.savedObjects.client,
+ client,
options: {
fields: [],
page: 1,
diff --git a/x-pack/plugins/case/server/routes/api/cases/tags/get_tags.ts b/x-pack/plugins/case/server/routes/api/cases/tags/get_tags.ts
index ca51f421f4f56..55e8fe2af128c 100644
--- a/x-pack/plugins/case/server/routes/api/cases/tags/get_tags.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/tags/get_tags.ts
@@ -15,8 +15,9 @@ export function initGetTagsApi({ caseService, router }: RouteDeps) {
},
async (context, request, response) => {
try {
+ const client = context.core.savedObjects.client;
const tags = await caseService.getTags({
- client: context.core.savedObjects.client,
+ client,
});
return response.ok({ body: tags });
} catch (error) {
diff --git a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts
index 911ca9ced3643..2d4f16e46d561 100644
--- a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts
@@ -23,8 +23,9 @@ export function initGetAllUserActionsApi({ userActionService, router }: RouteDep
},
async (context, request, response) => {
try {
+ const client = context.core.savedObjects.client;
const userActions = await userActionService.getUserActions({
- client: context.core.savedObjects.client,
+ client,
caseId: request.params.case_id,
});
return response.ok({
From c14f1d48eedc69cf503c45740ae1f5c7b3246274 Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Date: Mon, 23 Mar 2020 13:49:09 -0400
Subject: [PATCH 13/17] clean up + review
---
.../siem/public/containers/case/api.ts | 16 ++--
.../case/configure/use_configure.tsx | 8 +-
.../public/containers/case/translations.ts | 7 ++
.../siem/public/containers/case/types.ts | 8 +-
.../public/containers/case/use_get_case.tsx | 2 +-
.../case/use_get_case_user_actions.tsx | 11 ++-
.../case/use_post_push_to_service.tsx | 31 ++++---
.../siem/public/containers/case/utils.ts | 8 +-
.../components/all_cases/__mock__/index.tsx | 10 +--
.../components/case_view/__mock__/index.tsx | 2 +-
.../case/components/case_view/index.test.tsx | 1 +
.../pages/case/components/case_view/index.tsx | 14 ++--
.../components/case_view/push_to_service.tsx | 16 ++--
.../case/components/configure_cases/index.tsx | 2 +-
.../components/user_action_tree/helpers.tsx | 6 +-
.../components/user_action_tree/index.tsx | 26 +++---
.../user_action_tree/user_action_title.tsx | 10 ++-
x-pack/plugins/case/common/api/cases/case.ts | 42 +++++-----
.../api/__fixtures__/mock_saved_objects.ts | 8 +-
.../case/server/routes/api/cases/helpers.ts | 54 ++++++++++--
.../routes/api/cases/patch_cases.test.ts | 4 +-
.../server/routes/api/cases/patch_cases.ts | 1 +
.../case/server/routes/api/cases/push_case.ts | 20 +++--
.../plugins/case/server/routes/api/utils.ts | 2 +-
.../case/server/saved_object_types/cases.ts | 6 +-
x-pack/plugins/case/server/services/index.ts | 10 +--
.../server/services/user_actions/helpers.ts | 84 ++++++-------------
27 files changed, 218 insertions(+), 191 deletions(-)
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts
index 5b57159cace5f..16ee294224bb9 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts
@@ -14,9 +14,9 @@ import {
CommentResponse,
User,
CaseUserActionsResponse,
- CasePushRequest,
- PushCaseParams,
- PushCaseResponse,
+ CaseExternalServiceRequest,
+ ServiceConnectorCaseParams,
+ ServiceConnectorCaseResponse,
ActionTypeExecutorResult,
} from '../../../../../../plugins/case/common/api';
import { KibanaServices } from '../../lib/kibana';
@@ -42,7 +42,7 @@ import {
decodeCasesStatusResponse,
decodeCommentResponse,
decodeCaseUserActionsResponse,
- decodePushCaseResponse,
+ decodeServiceConnectorCaseResponse,
} from './utils';
export const getCase = async (caseId: string, includeComments: boolean = true): Promise => {
@@ -188,7 +188,7 @@ export const deleteCases = async (caseIds: string[]): Promise => {
export const pushCase = async (
caseId: string,
- push: CasePushRequest,
+ push: CaseExternalServiceRequest,
signal: AbortSignal
): Promise => {
const response = await KibanaServices.get().http.fetch(
@@ -204,9 +204,9 @@ export const pushCase = async (
export const pushToService = async (
connectorId: string,
- casePushParams: PushCaseParams,
+ casePushParams: ServiceConnectorCaseParams,
signal: AbortSignal
-): Promise => {
+): Promise => {
const response = await KibanaServices.get().http.fetch(
`/api/action/${connectorId}/_execute`,
{
@@ -215,7 +215,7 @@ export const pushToService = async (
signal,
}
);
- return decodePushCaseResponse(response.data);
+ return decodeServiceConnectorCaseResponse(response.data);
};
export const getActionLicense = async (signal: AbortSignal): Promise => {
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx
index 4d340ccf98605..a24f8303824c5 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx
@@ -25,12 +25,12 @@ export interface ReturnUseCaseConfigure {
}
interface UseCaseConfigure {
- setConnectorId: (newConnectorId: string, newConnectorName?: string) => void;
+ setConnector: (newConnectorId: string, newConnectorName?: string) => void;
setClosureType?: (newClosureType: ClosureType) => void;
}
export const useCaseConfigure = ({
- setConnectorId,
+ setConnector,
setClosureType,
}: UseCaseConfigure): ReturnUseCaseConfigure => {
const [, dispatchToaster] = useStateToaster();
@@ -49,7 +49,7 @@ export const useCaseConfigure = ({
if (!didCancel) {
setLoading(false);
if (res != null) {
- setConnectorId(res.connectorId, res.connectorName);
+ setConnector(res.connectorId, res.connectorName);
if (setClosureType != null) {
setClosureType(res.closureType);
}
@@ -99,7 +99,7 @@ export const useCaseConfigure = ({
);
if (!didCancel) {
setPersistLoading(false);
- setConnectorId(res.connectorId);
+ setConnector(res.connectorId);
if (setClosureType) {
setClosureType(res.closureType);
}
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/translations.ts b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts
index 0c8b896e2b426..601db373f041e 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts
@@ -16,3 +16,10 @@ export const TAG_FETCH_FAILURE = i18n.translate(
defaultMessage: 'Failed to fetch Tags',
}
);
+
+export const SUCCESS_SEND_TO_EXTERNAL_SERVICE = i18n.translate(
+ 'xpack.siem.containers.case.pushToExterService',
+ {
+ defaultMessage: 'Successfully sent to ServiceNow',
+ }
+);
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts
index d3a88ba0b7b24..bbbb13788d53a 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts
@@ -29,9 +29,9 @@ export interface CaseUserActions {
oldValue: string | null;
}
-export interface CasePush {
- at: string;
- by: string;
+export interface CaseExternalService {
+ pushedAt: string;
+ pushedBy: string;
connectorId: string;
connectorName: string;
externalId: string;
@@ -46,7 +46,7 @@ export interface Case {
createdAt: string;
createdBy: ElasticUser;
description: string;
- pushed: CasePush | null;
+ externalService: CaseExternalService | null;
status: string;
tags: string[];
title: string;
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx
index bbd4a84bccca5..02b41c9fc720f 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx
@@ -57,7 +57,7 @@ const initialData: Case = {
username: '',
},
description: '',
- pushed: null,
+ externalService: null,
status: '',
tags: [],
title: '',
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx
index cdc0f9cab5a6d..4c278bc038134 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx
@@ -4,18 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { isEmpty } from 'lodash/fp';
+import { isEmpty, uniqBy } from 'lodash/fp';
import { useCallback, useEffect, useState } from 'react';
import { errorToToaster, useStateToaster } from '../../components/toasters';
import { getCaseUserActions } from './api';
import * as i18n from './translations';
-import { CaseUserActions } from './types';
+import { CaseUserActions, ElasticUser } from './types';
interface CaseUserActionsState {
caseUserActions: CaseUserActions[];
firstIndexPushToService: number;
hasDataToPush: boolean;
+ participants: ElasticUser[];
isLoading: boolean;
isError: boolean;
lastIndexPushToService: number;
@@ -28,6 +29,7 @@ const initialData: CaseUserActionsState = {
hasDataToPush: false,
isLoading: true,
isError: false,
+ participants: [],
};
interface UseGetCaseUserActions extends CaseUserActionsState {
@@ -75,12 +77,16 @@ export const useGetCaseUserActions = (caseId: string): UseGetCaseUserActions =>
// Attention Future developer
// We are removing the first item because it will always be the creation of the case
// and we do not want it to simplify our life
+ const participants = !isEmpty(response)
+ ? uniqBy('actionBy.username', response).map(cau => cau.actionBy)
+ : [];
const caseUserActions = !isEmpty(response) ? response.slice(1) : [];
setCaseUserActionsState({
caseUserActions,
...getPushedInfo(caseUserActions),
isLoading: false,
isError: false,
+ participants,
});
}
} catch (error) {
@@ -97,6 +103,7 @@ export const useGetCaseUserActions = (caseId: string): UseGetCaseUserActions =>
hasDataToPush: false,
isLoading: false,
isError: true,
+ participants: [],
});
}
}
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx
index bfc483cc93e1f..b6fb15f4fa083 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx
@@ -6,22 +6,25 @@
import { useReducer, useCallback } from 'react';
-import { PushCaseResponse, PushCaseParams } from '../../../../../../plugins/case/common/api';
-import { errorToToaster, useStateToaster } from '../../components/toasters';
+import {
+ ServiceConnectorCaseResponse,
+ ServiceConnectorCaseParams,
+} from '../../../../../../plugins/case/common/api';
+import { errorToToaster, useStateToaster, displaySuccessToast } from '../../components/toasters';
-import { pushToService, pushCase } from './api';
+import { getCase, pushToService, pushCase } from './api';
import * as i18n from './translations';
import { Case } from './types';
interface PushToServiceState {
- serviceData: PushCaseResponse | null;
+ serviceData: ServiceConnectorCaseResponse | null;
pushedCaseData: Case | null;
isLoading: boolean;
isError: boolean;
}
type Action =
| { type: 'FETCH_INIT' }
- | { type: 'FETCH_SUCCESS_PUSH_SERVICE'; payload: PushCaseResponse | null }
+ | { type: 'FETCH_SUCCESS_PUSH_SERVICE'; payload: ServiceConnectorCaseResponse | null }
| { type: 'FETCH_SUCCESS_PUSH_CASE'; payload: Case | null }
| { type: 'FETCH_FAILURE' };
@@ -59,14 +62,14 @@ const dataFetchReducer = (state: PushToServiceState, action: Action): PushToServ
};
interface PushToServiceRequest {
+ caseId: string;
connectorId: string;
connectorName: string;
- caseToPush: Case;
updateCase: (newCase: Case) => void;
}
interface UsePostPushToService extends PushToServiceState {
- postPushToService: ({ caseToPush, connectorId, updateCase }: PushToServiceRequest) => void;
+ postPushToService: ({ caseId, connectorId, updateCase }: PushToServiceRequest) => void;
}
export const usePostPushToService = (): UsePostPushToService => {
@@ -79,18 +82,19 @@ export const usePostPushToService = (): UsePostPushToService => {
const [, dispatchToaster] = useStateToaster();
const postPushToService = useCallback(
- async ({ caseToPush, connectorId, connectorName, updateCase }: PushToServiceRequest) => {
+ async ({ caseId, connectorId, connectorName, updateCase }: PushToServiceRequest) => {
let cancel = false;
const abortCtrl = new AbortController();
try {
dispatch({ type: 'FETCH_INIT' });
+ const casePushData = await getCase(caseId);
const responseService = await pushToService(
connectorId,
- formatServiceRequestData(caseToPush),
+ formatServiceRequestData(casePushData),
abortCtrl.signal
);
const responseCase = await pushCase(
- caseToPush.id,
+ caseId,
{
connector_id: connectorId,
connector_name: connectorName,
@@ -104,6 +108,7 @@ export const usePostPushToService = (): UsePostPushToService => {
dispatch({ type: 'FETCH_SUCCESS_PUSH_SERVICE', payload: responseService });
dispatch({ type: 'FETCH_SUCCESS_PUSH_CASE', payload: responseCase });
updateCase(responseCase);
+ displaySuccessToast(i18n.SUCCESS_SEND_TO_EXTERNAL_SERVICE, dispatchToaster);
}
} catch (error) {
if (!cancel) {
@@ -126,14 +131,14 @@ export const usePostPushToService = (): UsePostPushToService => {
return { ...state, postPushToService };
};
-const formatServiceRequestData = (myCase: Case): PushCaseParams => {
+const formatServiceRequestData = (myCase: Case): ServiceConnectorCaseParams => {
const {
id: caseId,
createdAt,
createdBy,
comments,
description,
- pushed,
+ externalService,
title,
updatedAt,
updatedBy,
@@ -164,7 +169,7 @@ const formatServiceRequestData = (myCase: Case): PushCaseParams => {
: null,
})),
description,
- incidentId: pushed?.externalId ?? null,
+ incidentId: externalService?.externalId ?? null,
title,
updatedAt,
updatedBy:
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/utils.ts b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts
index 3831a7b1c6371..ce23ac6c440b6 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/utils.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts
@@ -25,8 +25,8 @@ import {
CaseConfigureResponseRt,
CaseUserActionsResponse,
CaseUserActionsResponseRt,
- PushCaseResponseRt,
- PushCaseResponse,
+ ServiceConnectorCaseResponseRt,
+ ServiceConnectorCaseResponse,
} from '../../../../../../plugins/case/common/api';
import { ToasterError } from '../../components/toasters';
import { AllCases, Case } from './types';
@@ -97,8 +97,8 @@ export const decodeCaseUserActionsResponse = (respUserActions?: CaseUserActionsR
fold(throwErrors(createToasterPlainError), identity)
);
-export const decodePushCaseResponse = (respPushCase?: PushCaseResponse) =>
+export const decodeServiceConnectorCaseResponse = (respPushCase?: ServiceConnectorCaseResponse) =>
pipe(
- PushCaseResponseRt.decode(respPushCase),
+ ServiceConnectorCaseResponseRt.decode(respPushCase),
fold(throwErrors(createToasterPlainError), identity)
);
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx
index 3ca36d2dd9632..d4ec32dfd070b 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx
@@ -20,7 +20,7 @@ export const useGetCasesMockState: UseGetCasesState = {
createdBy: { username: 'elastic' },
comments: [],
description: 'Security banana Issue',
- pushed: null,
+ externalService: null,
status: 'open',
tags: ['defacement'],
title: 'Another horrible breach',
@@ -37,7 +37,7 @@ export const useGetCasesMockState: UseGetCasesState = {
createdBy: { username: 'elastic' },
comments: [],
description: 'Security banana Issue',
- pushed: null,
+ externalService: null,
status: 'open',
tags: ['phishing'],
title: 'Bad email',
@@ -54,7 +54,7 @@ export const useGetCasesMockState: UseGetCasesState = {
createdBy: { username: 'elastic' },
comments: [],
description: 'Security banana Issue',
- pushed: null,
+ externalService: null,
status: 'open',
tags: ['phishing'],
title: 'Bad email',
@@ -71,7 +71,7 @@ export const useGetCasesMockState: UseGetCasesState = {
createdBy: { username: 'elastic' },
comments: [],
description: 'Security banana Issue',
- pushed: null,
+ externalService: null,
status: 'closed',
tags: ['phishing'],
title: 'Uh oh',
@@ -88,7 +88,7 @@ export const useGetCasesMockState: UseGetCasesState = {
createdBy: { username: 'elastic' },
comments: [],
description: 'Security banana Issue',
- pushed: null,
+ externalService: null,
status: 'open',
tags: ['phishing'],
title: 'Uh oh',
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx
index fbd461242bdb5..7aadea1a453a7 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx
@@ -35,7 +35,7 @@ export const caseProps: CaseProps = {
createdAt: '2020-02-13T19:44:23.627Z',
createdBy: { fullName: null, email: 'testemail@elastic.co', username: 'elastic' },
description: 'Security banana Issue',
- pushed: null,
+ externalService: null,
status: 'open',
tags: ['defacement'],
title: 'Another horrible breach!!',
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx
index 3c95ba8a4f0bf..18cc33d8a6d4d 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx
@@ -82,6 +82,7 @@ describe('CaseView ', () => {
isLoading: false,
isError: false,
lastIndexPushToService: -1,
+ participants: [data.createdBy],
};
const defaultUsePushToServiceMock = {
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
index dd6917b2f3806..742921cb9f69e 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
@@ -15,7 +15,6 @@ import {
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
-import { uniqBy } from 'lodash/fp';
import * as i18n from './translations';
import { Case } from '../../../../containers/case/types';
import { getCaseUrl } from '../../../../components/link_to';
@@ -70,6 +69,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
hasDataToPush,
isLoading: isLoadingUserActions,
lastIndexPushToService,
+ participants,
} = useGetCaseUserActions(caseId);
const { caseData, isLoading, updateKey, updateCase, updateCaseProperty } = useUpdateCase(
caseId,
@@ -132,7 +132,8 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
);
const { pushButton, pushCallouts } = usePushToService({
- caseData,
+ caseId: caseData.id,
+ caseStatus: caseData.status,
isNew: caseUserActions.filter(cua => cua.action === 'push-to-service').length === 0,
updateCase: handleUpdateCase,
});
@@ -144,10 +145,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
[onUpdateField]
);
const spyState = useMemo(() => ({ caseTitle: caseData.title }), [caseData.title]);
- const participants = useMemo(
- () => uniqBy('actionBy.username', caseUserActions).map(cau => cau.actionBy),
- [caseUserActions]
- );
+
const caseStatusData = useMemo(
() =>
caseData.status === 'open'
@@ -233,7 +231,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) =>
onUpdateField={onUpdateField}
/>
-
+
(({ caseId, initialData }) =>
users={[caseData.createdBy]}
/>
void;
}
@@ -32,7 +33,8 @@ interface ReturnUsePushToService {
}
export const usePushToService = ({
- caseData,
+ caseId,
+ caseStatus,
updateCase,
isNew,
}: UsePushToService): ReturnUsePushToService => {
@@ -45,7 +47,7 @@ export const usePushToService = ({
}, []);
const { loading: loadingCaseConfigure } = useCaseConfigure({
- setConnectorId: handleSetConnector,
+ setConnector: handleSetConnector,
});
const { isLoading: loadingLicense, actionLicense } = useGetActionLicense();
@@ -53,16 +55,16 @@ export const usePushToService = ({
const handlePushToService = useCallback(() => {
if (connector != null) {
postPushToService({
- caseToPush: caseData,
+ caseId,
...connector,
updateCase,
});
}
- }, [caseData, connector, postPushToService, updateCase]);
+ }, [caseId, connector, postPushToService, updateCase]);
const errorsMsg = useMemo(() => {
let errors: Array<{ title: string; description: string }> = [];
- if (caseData.status === 'closed') {
+ if (caseStatus === 'closed') {
errors = [
...errors,
{
@@ -99,7 +101,7 @@ export const usePushToService = ({
];
}
return errors;
- }, [actionLicense, caseData, connector, loadingCaseConfigure, loadingLicense]);
+ }, [actionLicense, caseStatus, connector, loadingCaseConfigure, loadingLicense]);
const pushToServiceButton = useMemo(
() => (
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx
index 8a6d8a40e560a..fb4d91492c1d4 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx
@@ -113,7 +113,7 @@ const ConfigureCasesComponent: React.FC = () => {
}, []);
const { loading: loadingCaseConfigure, persistLoading, persistCaseConfigure } = useCaseConfigure({
- setConnectorId,
+ setConnector: setConnectorId,
setClosureType,
});
const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors();
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx
index 2a7197b2c635b..008f4d7048f56 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx
@@ -7,7 +7,7 @@
import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiLink } from '@elastic/eui';
import React from 'react';
-import { CasePushedData } from '../../../../../../../../plugins/case/common/api';
+import { CaseFullExternalService } from '../../../../../../../../plugins/case/common/api';
import { CaseUserActions } from '../../../../containers/case/types';
import * as i18n from '../case_view/translations';
@@ -59,14 +59,14 @@ const getPushedServiceLabelTitle = (
firstIndexPushToService: number,
index: number
) => {
- const pushedVal = JSON.parse(action.newValue ?? '') as CasePushedData;
+ const pushedVal = JSON.parse(action.newValue ?? '') as CaseFullExternalService;
return (
{firstIndexPushToService === index ? i18n.PUSHED_NEW_INCIDENT : i18n.UPDATE_INCIDENT}
-
+
{pushedVal?.connector_name} {pushedVal?.external_title}
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx
index e41e14a6cc1f1..0a007ebf1e320 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx
@@ -34,8 +34,8 @@ const MyEuiFlexGroup = styled(EuiFlexGroup)`
margin-bottom: 8px;
`;
-const DescriptionId = 'description';
-const NewId = 'newComment';
+const DESCRIPTION_ID = 'description';
+const NEW_ID = 'newComment';
export const UserActionTree = React.memo(
({
@@ -114,12 +114,12 @@ export const UserActionTree = React.memo(
const MarkdownDescription = useMemo(
() => (
{
- handleManageMarkdownEditId(DescriptionId);
- onUpdateField(DescriptionId, content);
+ handleManageMarkdownEditId(DESCRIPTION_ID);
+ onUpdateField(DESCRIPTION_ID, content);
}}
onChangeEditable={handleManageMarkdownEditId}
/>
@@ -132,7 +132,7 @@ export const UserActionTree = React.memo(
),
@@ -152,14 +152,14 @@ export const UserActionTree = React.memo(
<>
@@ -231,7 +231,7 @@ export const UserActionTree = React.memo(
}
return null;
})}
- {(isLoadingUserActions || isLoadingIds.includes(NewId)) && (
+ {(isLoadingUserActions || isLoadingIds.includes(NEW_ID)) && (
@@ -240,9 +240,9 @@ export const UserActionTree = React.memo(
)}
{
- copy(`${window.location.origin}${window.location.pathname}#case/${caseId}/${id}${urlSearch}`, {
- debug: true,
- });
+ copy(
+ `${window.location.origin}${window.location.pathname}#${SiemPageName.case}/${caseId}/${id}${urlSearch}`,
+ {
+ debug: true,
+ }
+ );
}, [caseId, id, urlSearch]);
const handleMoveToLink = useCallback(() => {
diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts
index 627c3d2b04608..ee244dd205113 100644
--- a/x-pack/plugins/case/common/api/cases/case.ts
+++ b/x-pack/plugins/case/common/api/cases/case.ts
@@ -22,7 +22,7 @@ const CaseBasicRt = rt.type({
title: rt.string,
});
-const CasePushBasicRt = rt.type({
+const CaseExternalServiceBasicRt = rt.type({
connector_id: rt.string,
connector_name: rt.string,
external_id: rt.string,
@@ -30,12 +30,12 @@ const CasePushBasicRt = rt.type({
external_url: rt.string,
});
-const CasePushedBasicRt = rt.union([
+const CaseFullExternalServiceRt = rt.union([
rt.intersection([
- CasePushBasicRt,
+ CaseExternalServiceBasicRt,
rt.type({
- at: rt.string,
- by: UserRT,
+ pushed_at: rt.string,
+ pushed_by: UserRT,
}),
]),
rt.null,
@@ -48,7 +48,7 @@ export const CaseAttributesRt = rt.intersection([
closed_by: rt.union([UserRT, rt.null]),
created_at: rt.string,
created_by: UserRT,
- pushed: CasePushedBasicRt,
+ external_service: CaseFullExternalServiceRt,
updated_at: rt.union([rt.string, rt.null]),
updated_by: rt.union([UserRT, rt.null]),
}),
@@ -56,7 +56,7 @@ export const CaseAttributesRt = rt.intersection([
export const CaseRequestRt = CaseBasicRt;
-export const CasePushRequestRt = CasePushBasicRt;
+export const CaseExternalServiceRequestRt = CaseExternalServiceBasicRt;
export const CasesFindRequestRt = rt.partial({
tags: rt.union([rt.array(rt.string), rt.string]),
@@ -109,37 +109,37 @@ export const CasesResponseRt = rt.array(CaseResponseRt);
* so we redefine then so we can use/validate types
*/
-const PushCaseUserParams = rt.type({
+const ServiceConnectorUserParams = rt.type({
fullName: rt.union([rt.string, rt.null]),
username: rt.string,
});
-export const PushCommentParamsRt = rt.type({
+export const ServiceConnectorCommentParamsRt = rt.type({
commentId: rt.string,
comment: rt.string,
createdAt: rt.string,
- createdBy: PushCaseUserParams,
+ createdBy: ServiceConnectorUserParams,
updatedAt: rt.union([rt.string, rt.null]),
- updatedBy: rt.union([PushCaseUserParams, rt.null]),
+ updatedBy: rt.union([ServiceConnectorUserParams, rt.null]),
});
-export const PushCaseParamsRt = rt.intersection([
+export const ServiceConnectorCaseParamsRt = rt.intersection([
rt.type({
caseId: rt.string,
createdAt: rt.string,
- createdBy: PushCaseUserParams,
+ createdBy: ServiceConnectorUserParams,
incidentId: rt.union([rt.string, rt.null]),
title: rt.string,
updatedAt: rt.union([rt.string, rt.null]),
- updatedBy: rt.union([PushCaseUserParams, rt.null]),
+ updatedBy: rt.union([ServiceConnectorUserParams, rt.null]),
}),
rt.partial({
description: rt.string,
- comments: rt.array(PushCommentParamsRt),
+ comments: rt.array(ServiceConnectorCommentParamsRt),
}),
]);
-export const PushCaseResponseRt = rt.intersection([
+export const ServiceConnectorCaseResponseRt = rt.intersection([
rt.type({
number: rt.string,
incidentId: rt.string,
@@ -163,8 +163,8 @@ export type CasesResponse = rt.TypeOf;
export type CasesFindResponse = rt.TypeOf;
export type CasePatchRequest = rt.TypeOf;
export type CasesPatchRequest = rt.TypeOf;
-export type CasePushRequest = rt.TypeOf;
-export type PushCaseParams = rt.TypeOf;
-export type PushCaseResponse = rt.TypeOf;
-export type CasePushedData = rt.TypeOf;
-export type PushCommentParams = rt.TypeOf;
+export type CaseExternalServiceRequest = rt.TypeOf;
+export type ServiceConnectorCaseParams = rt.TypeOf;
+export type ServiceConnectorCaseResponse = rt.TypeOf;
+export type CaseFullExternalService = rt.TypeOf;
+export type ServiceConnectorCommentParams = rt.TypeOf;
diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts
index 3ad8e0b23b936..03da50f886fd5 100644
--- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts
+++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts
@@ -21,7 +21,7 @@ export const mockCases: Array> = [
username: 'elastic',
},
description: 'This is a brand new case of a bad meanie defacing data',
- pushed: null,
+ external_service: null,
title: 'Super Bad Security Issue',
status: 'open',
tags: ['defacement'],
@@ -49,7 +49,7 @@ export const mockCases: Array> = [
username: 'elastic',
},
description: 'Oh no, a bad meanie destroying data!',
- pushed: null,
+ external_service: null,
title: 'Damaging Data Destruction Detected',
status: 'open',
tags: ['Data Destruction'],
@@ -77,7 +77,7 @@ export const mockCases: Array> = [
username: 'elastic',
},
description: 'Oh no, a bad meanie going LOLBins all over the place!',
- pushed: null,
+ external_service: null,
title: 'Another bad one',
status: 'open',
tags: ['LOLBins'],
@@ -109,7 +109,7 @@ export const mockCases: Array> = [
username: 'elastic',
},
description: 'Oh no, a bad meanie going LOLBins all over the place!',
- pushed: null,
+ external_service: null,
status: 'closed',
title: 'Another bad one',
tags: ['LOLBins'],
diff --git a/x-pack/plugins/case/server/routes/api/cases/helpers.ts b/x-pack/plugins/case/server/routes/api/cases/helpers.ts
index 537568310a06f..747b5195da7ec 100644
--- a/x-pack/plugins/case/server/routes/api/cases/helpers.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/helpers.ts
@@ -4,16 +4,56 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { difference, get } from 'lodash';
+import { get } from 'lodash';
import { CaseAttributes, CasePatchRequest } from '../../../../common/api';
-export const isTwoArraysDifference = (origVal: unknown, updatedVal: unknown) =>
- origVal != null &&
- updatedVal != null &&
- Array.isArray(updatedVal) &&
- Array.isArray(origVal) &&
- difference(origVal, updatedVal).length !== 0;
+interface CompareArrays {
+ addedItems: string[];
+ deletedItems: string[];
+}
+export const compareArrays = ({
+ originalValue,
+ updatedValue,
+}: {
+ originalValue: string[];
+ updatedValue: string[];
+}): CompareArrays => {
+ const result: CompareArrays = {
+ addedItems: [],
+ deletedItems: [],
+ };
+ originalValue.forEach(origVal => {
+ if (!updatedValue.includes(origVal)) {
+ result.deletedItems = [...result.deletedItems, origVal];
+ }
+ });
+ updatedValue.forEach(updatedVal => {
+ if (!originalValue.includes(updatedVal)) {
+ result.addedItems = [...result.addedItems, updatedVal];
+ }
+ });
+
+ return result;
+};
+
+export const isTwoArraysDifference = (
+ originalValue: unknown,
+ updatedValue: unknown
+): CompareArrays | null => {
+ if (
+ originalValue != null &&
+ updatedValue != null &&
+ Array.isArray(updatedValue) &&
+ Array.isArray(originalValue)
+ ) {
+ const compObj = compareArrays({ originalValue, updatedValue });
+ if (compObj.addedItems.length > 0 || compObj.deletedItems.length > 0) {
+ return compObj;
+ }
+ }
+ return null;
+};
export const getCaseToUpdate = (
currentCase: CaseAttributes,
diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts
index 3c3e111cfa901..ac1e67cec52bd 100644
--- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts
@@ -57,7 +57,7 @@ describe('PATCH cases', () => {
created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' },
description: 'This is a brand new case of a bad meanie defacing data',
id: 'mock-id-1',
- pushed: null,
+ external_service: null,
status: 'closed',
tags: ['defacement'],
title: 'Super Bad Security Issue',
@@ -100,7 +100,7 @@ describe('PATCH cases', () => {
created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' },
description: 'Oh no, a bad meanie going LOLBins all over the place!',
id: 'mock-id-4',
- pushed: null,
+ external_service: null,
status: 'open',
tags: ['LOLBins'],
title: 'Another bad one',
diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts
index 62c41d872e2c8..3d0b7bc79f88b 100644
--- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts
@@ -104,6 +104,7 @@ export function initPatchCasesApi({ caseService, router, userActionService }: Ro
};
}),
});
+
const returnUpdatedCase = myCases.saved_objects
.filter(myCase =>
updatedCases.saved_objects.some(updatedCase => updatedCase.id === myCase.id)
diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts
index 462b7af0db336..6ae3df180d9e4 100644
--- a/x-pack/plugins/case/server/routes/api/cases/push_case.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts
@@ -12,7 +12,7 @@ import { identity } from 'fp-ts/lib/function';
import { flattenCaseSavedObject, wrapError, escapeHatch } from '../utils';
-import { CasePushRequestRt, CaseResponseRt, throwErrors } from '../../../../common/api';
+import { CaseExternalServiceRequestRt, CaseResponseRt, throwErrors } from '../../../../common/api';
import { buildCaseUserActionItem } from '../../../services/user_actions/helpers';
import { RouteDeps } from '../types';
@@ -37,7 +37,7 @@ export function initPushCaseUserActionApi({
const client = context.core.savedObjects.client;
const caseId = request.params.case_id;
const query = pipe(
- CasePushRequestRt.decode(request.body),
+ CaseExternalServiceRequestRt.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
const { username, full_name, email } = await caseService.getUser({ request, response });
@@ -76,9 +76,9 @@ export function initPushCaseUserActionApi({
},
});
- const pushed = {
- at: pushedDate,
- by: { username, full_name, email },
+ const externalService = {
+ pushed_at: pushedDate,
+ pushed_by: { username, full_name, email },
...query,
};
@@ -90,9 +90,11 @@ export function initPushCaseUserActionApi({
...(myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing'
? {
status: 'closed',
+ closed_at: pushedDate,
+ closed_by: { email, full_name, username },
}
: {}),
- pushed,
+ external_service: externalService,
updated_at: pushedDate,
updated_by: { username, full_name, email },
},
@@ -121,8 +123,8 @@ export function initPushCaseUserActionApi({
actionAt: pushedDate,
actionBy: { username, full_name, email },
caseId,
- fields: [status],
- newValue: 'closes',
+ fields: ['status'],
+ newValue: 'closed',
oldValue: myCase.attributes.status,
}),
]
@@ -133,7 +135,7 @@ export function initPushCaseUserActionApi({
actionBy: { username, full_name, email },
caseId,
fields: ['pushed'],
- newValue: JSON.stringify(pushed),
+ newValue: JSON.stringify(externalService),
}),
],
}),
diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts
index c026541787dca..9d90eb8ef4a6d 100644
--- a/x-pack/plugins/case/server/routes/api/utils.ts
+++ b/x-pack/plugins/case/server/routes/api/utils.ts
@@ -42,7 +42,7 @@ export const transformNewCase = ({
closed_by: null,
created_at: createdDate,
created_by: { email, full_name, username },
- pushed: null,
+ external_service: null,
updated_at: null,
updated_by: null,
...newCase,
diff --git a/x-pack/plugins/case/server/saved_object_types/cases.ts b/x-pack/plugins/case/server/saved_object_types/cases.ts
index 004558983b069..a4c5dab0feeb7 100644
--- a/x-pack/plugins/case/server/saved_object_types/cases.ts
+++ b/x-pack/plugins/case/server/saved_object_types/cases.ts
@@ -49,12 +49,12 @@ export const caseSavedObjectType: SavedObjectsType = {
description: {
type: 'text',
},
- pushed: {
+ external_service: {
properties: {
- at: {
+ pushed_at: {
type: 'date',
},
- by: {
+ pushed_by: {
properties: {
username: {
type: 'keyword',
diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts
index 42f691d929a99..09d726228d309 100644
--- a/x-pack/plugins/case/server/services/index.ts
+++ b/x-pack/plugins/case/server/services/index.ts
@@ -18,13 +18,7 @@ import {
} from 'kibana/server';
import { AuthenticatedUser, SecurityPluginSetup } from '../../../security/server';
-import {
- CaseAttributes,
- CommentAttributes,
- SavedObjectFindOptions,
- User,
- CasePushedData,
-} from '../../common/api';
+import { CaseAttributes, CommentAttributes, SavedObjectFindOptions, User } from '../../common/api';
import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT } from '../saved_object_types';
import { readReporters } from './reporters/read_reporters';
import { readTags } from './tags/read_tags';
@@ -71,7 +65,7 @@ interface PostCommentArgs extends ClientArgs {
interface PatchCase {
caseId: string;
- updatedAttributes: Partial;
+ updatedAttributes: Partial;
version?: string;
}
type PatchCaseArgs = PatchCase & ClientArgs;
diff --git a/x-pack/plugins/case/server/services/user_actions/helpers.ts b/x-pack/plugins/case/server/services/user_actions/helpers.ts
index d76d756bfa4f2..b5c0dc46facf8 100644
--- a/x-pack/plugins/case/server/services/user_actions/helpers.ts
+++ b/x-pack/plugins/case/server/services/user_actions/helpers.ts
@@ -117,34 +117,6 @@ export const buildCaseUserActionItem = ({
],
});
-interface CompareArray {
- addedItems: string[];
- deletedItems: string[];
-}
-const compareArray = ({
- originalValue,
- updatedValue,
-}: {
- originalValue: string[];
- updatedValue: string[];
-}): CompareArray => {
- const result: CompareArray = {
- addedItems: [],
- deletedItems: [],
- };
- originalValue.forEach(origVal => {
- if (!updatedValue.includes(origVal)) {
- result.deletedItems = [...result.deletedItems, origVal];
- }
- });
- updatedValue.forEach(updatedVal => {
- if (!originalValue.includes(updatedVal)) {
- result.addedItems = [...result.addedItems, updatedVal];
- }
- });
-
- return result;
-};
const userActionFieldsAllowed: UserActionField = [
'comment',
'description',
@@ -173,37 +145,31 @@ export const buildCaseUserActions = ({
if (userActionFieldsAllowed.includes(field)) {
const origValue = get(originalItem, ['attributes', field]);
const updatedValue = get(updatedItem, ['attributes', field]);
- if (isTwoArraysDifference(origValue, updatedValue)) {
- const arrayDiff = compareArray({
- originalValue: origValue as string[],
- updatedValue: updatedValue as string[],
- });
- if (arrayDiff.addedItems.length > 0) {
- userActions = [
- ...userActions,
- buildCaseUserActionItem({
- action: 'add',
- actionAt: actionDate,
- actionBy,
- caseId: updatedItem.id,
- fields: [field],
- newValue: arrayDiff.addedItems.join(', '),
- }),
- ];
- }
- if (arrayDiff.deletedItems.length > 0) {
- userActions = [
- ...userActions,
- buildCaseUserActionItem({
- action: 'delete',
- actionAt: actionDate,
- actionBy,
- caseId: updatedItem.id,
- fields: [field],
- newValue: arrayDiff.deletedItems.join(', '),
- }),
- ];
- }
+ const compareValues = isTwoArraysDifference(origValue, updatedValue);
+ if (compareValues != null && compareValues.addedItems.length > 0) {
+ userActions = [
+ ...userActions,
+ buildCaseUserActionItem({
+ action: 'add',
+ actionAt: actionDate,
+ actionBy,
+ caseId: updatedItem.id,
+ fields: [field],
+ newValue: compareValues.addedItems.join(', '),
+ }),
+ ];
+ } else if (compareValues != null && compareValues.deletedItems.length > 0) {
+ userActions = [
+ ...userActions,
+ buildCaseUserActionItem({
+ action: 'delete',
+ actionAt: actionDate,
+ actionBy,
+ caseId: updatedItem.id,
+ fields: [field],
+ newValue: compareValues.deletedItems.join(', '),
+ }),
+ ];
} else if (origValue !== updatedValue) {
userActions = [
...userActions,
From fb88c5ce2404c7a5de0ae5dfc7c8803a18a68524 Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Date: Mon, 23 Mar 2020 14:16:12 -0400
Subject: [PATCH 14/17] merge issue
---
.../public/pages/case/components/user_action_tree/index.tsx | 2 +-
.../pages/case/components/user_action_tree/user_action_item.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx
index 0a007ebf1e320..8b77186f76f77 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx
@@ -156,7 +156,7 @@ export const UserActionTree = React.memo(
isEditable={manageMarkdownEditIds.includes(DESCRIPTION_ID)}
isLoading={isLoadingDescription}
labelEditAction={i18n.EDIT_DESCRIPTION}
- labelTitle={i18n.ADDED_DESCRIPTION}
+ labelTitle={<>{i18n.ADDED_DESCRIPTION}>}
fullName={caseData.createdBy.fullName ?? caseData.createdBy.username}
markdown={MarkdownDescription}
onEdit={handleManageMarkdownEditId.bind(null, DESCRIPTION_ID)}
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx
index dc51e6f3e83fb..10a7c56e2eb2d 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx
@@ -146,7 +146,7 @@ export const UserActionItem = ({
createdAt={createdAt}
id={id}
isLoading={isLoading}
- labelAction={labelEditAction}
+ labelEditAction={labelEditAction}
labelTitle={labelTitle ?? <>>}
linkId={linkId}
userName={userName}
From e311752904b7589ab669fc50ea55ce49a650967a Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Date: Mon, 23 Mar 2020 16:00:41 -0400
Subject: [PATCH 15/17] update error msgs to info
---
.../components/link_to/redirect_to_case.tsx | 7 +-
.../siem/public/components/links/index.tsx | 28 +++++---
.../case/components/all_cases/index.test.tsx | 4 +-
.../pages/case/components/all_cases/index.tsx | 20 +++---
.../components/case_view/push_to_service.tsx | 71 +++++++++++++++----
.../case/components/case_view/translations.ts | 38 ++++------
.../errors_push_service_callout/index.tsx | 19 ++---
.../translations.ts | 2 +-
.../siem/public/pages/case/translations.ts | 2 +-
.../plugins/siem/public/pages/case/utils.ts | 4 +-
10 files changed, 121 insertions(+), 74 deletions(-)
diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_case.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_case.tsx
index 3056b166c1153..20ba0b50f5126 100644
--- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_case.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_case.tsx
@@ -31,6 +31,7 @@ export const RedirectToConfigureCasesPage = () => (
const baseCaseUrl = `#/link-to/${SiemPageName.case}`;
export const getCaseUrl = () => baseCaseUrl;
-export const getCaseDetailsUrl = (detailName: string) => `${baseCaseUrl}/${detailName}`;
-export const getCreateCaseUrl = () => `${baseCaseUrl}/create`;
-export const getConfigureCasesUrl = () => `${baseCaseUrl}/configure`;
+export const getCaseDetailsUrl = (detailName: string, search: string) =>
+ `${baseCaseUrl}/${detailName}${search}`;
+export const getCreateCaseUrl = (search: string) => `${baseCaseUrl}/create${search}`;
+export const getConfigureCasesUrl = (search: string) => `${baseCaseUrl}/configure${search}`;
diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.tsx
index 04de0b1d5d3bf..935df9ad3361f 100644
--- a/x-pack/legacy/plugins/siem/public/components/links/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/links/index.tsx
@@ -23,8 +23,10 @@ import {
import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types';
import { useUiSetting$ } from '../../lib/kibana';
import { IP_REPUTATION_LINKS_SETTING } from '../../../common/constants';
+import { navTabs } from '../../pages/home/home_navigations';
import * as i18n from '../page/network/ip_overview/translations';
import { isUrlInvalid } from '../../pages/detection_engine/rules/components/step_about_rule/helpers';
+import { useGetUrlSearch } from '../navigation/use_get_url_search';
import { ExternalLinkIcon } from '../external_link_icon';
export const DEFAULT_NUMBER_OF_LINK = 5;
@@ -89,20 +91,24 @@ export const IPDetailsLink = React.memo(IPDetailsLinkComponent);
const CaseDetailsLinkComponent: React.FC<{ children?: React.ReactNode; detailName: string }> = ({
children,
detailName,
-}) => (
-
- {children ? children : detailName}
-
-);
+}) => {
+ const urlSearch = useGetUrlSearch(navTabs.case);
+ return (
+
+ {children ? children : detailName}
+
+ );
+};
export const CaseDetailsLink = React.memo(CaseDetailsLinkComponent);
CaseDetailsLink.displayName = 'CaseDetailsLink';
-export const CreateCaseLink = React.memo<{ children: React.ReactNode }>(({ children }) => (
- {children}
-));
+export const CreateCaseLink = React.memo<{ children: React.ReactNode }>(({ children }) => {
+ const urlSearch = useGetUrlSearch(navTabs.case);
+ return {children};
+});
CreateCaseLink.displayName = 'CreateCaseLink';
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx
index 13869c79c45fd..bdcb87b483851 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx
@@ -95,7 +95,9 @@ describe('AllCases', () => {
.find(`a[data-test-subj="case-details-link"]`)
.first()
.prop('href')
- ).toEqual(`#/link-to/case/${useGetCasesMockState.data.cases[0].id}`);
+ ).toEqual(
+ `#/link-to/case/${useGetCasesMockState.data.cases[0].id}?timerange=(global:(linkTo:!(timeline),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)),timeline:(linkTo:!(global),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)))`
+ );
expect(
wrapper
.find(`a[data-test-subj="case-details-link"]`)
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx
index e7e1e624ccba2..87a2ea888831a 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx
@@ -35,7 +35,9 @@ import {
UtilityBarText,
} from '../../../../components/utility_bar';
import { getConfigureCasesUrl, getCreateCaseUrl } from '../../../../components/link_to';
-
+import { useUpdateCases } from '../../../../containers/case/use_bulk_update_case';
+import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search';
+import { navTabs } from '../../../home/home_navigations';
import { getBulkItems } from '../bulk_actions';
import { CaseHeaderPage } from '../case_header_page';
import { ConfirmDeleteCaseModal } from '../confirm_delete_case';
@@ -43,10 +45,6 @@ import { OpenClosedStats } from '../open_closed_stats';
import { getActions } from './actions';
import { CasesTableFilters } from './table_filters';
-import { useUpdateCases } from '../../../../containers/case/use_bulk_update_case';
-
-const CONFIGURE_CASES_URL = getConfigureCasesUrl();
-const CREATE_CASE_URL = getCreateCaseUrl();
const Div = styled.div`
margin-top: ${({ theme }) => theme.eui.paddingSizes.m};
@@ -78,6 +76,7 @@ const getSortField = (field: string): SortFieldCase => {
return SortFieldCase.createdAt;
};
export const AllCases = React.memo(() => {
+ const urlSearch = useGetUrlSearch(navTabs.case);
const {
countClosedCases,
countOpenCases,
@@ -276,12 +275,12 @@ export const AllCases = React.memo(() => {
/>
-
+
{i18n.CONFIGURE_CASES_BUTTON}
-
+
{i18n.CREATE_TITLE}
@@ -342,7 +341,12 @@ export const AllCases = React.memo(() => {
titleSize="xs"
body={i18n.NO_CASES_BODY}
actions={
-
+
{i18n.ADD_NEW_CASE}
}
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/push_to_service.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/push_to_service.tsx
index 1005636e4ff76..3cb4d06101731 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/push_to_service.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/push_to_service.tsx
@@ -4,16 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiButton, EuiToolTip } from '@elastic/eui';
+import { EuiButton, EuiLink, EuiToolTip } from '@elastic/eui';
import React, { useCallback, useState, useMemo } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
import { useCaseConfigure } from '../../../../containers/case/configure/use_configure';
import { Case } from '../../../../containers/case/types';
import { useGetActionLicense } from '../../../../containers/case/use_get_action_license';
import { usePostPushToService } from '../../../../containers/case/use_post_push_to_service';
-
-import * as i18n from './translations';
+import { getConfigureCasesUrl } from '../../../../components/link_to';
+import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search';
+import { navTabs } from '../../../home/home_navigations';
import { ErrorsPushServiceCallOut } from '../errors_push_service_callout';
+import * as i18n from './translations';
interface UsePushToService {
caseId: string;
@@ -38,6 +41,7 @@ export const usePushToService = ({
updateCase,
isNew,
}: UsePushToService): ReturnUsePushToService => {
+ const urlSearch = useGetUrlSearch(navTabs.case);
const [connector, setConnector] = useState(null);
const { isLoading, postPushToService } = usePostPushToService();
@@ -63,13 +67,25 @@ export const usePushToService = ({
}, [caseId, connector, postPushToService, updateCase]);
const errorsMsg = useMemo(() => {
- let errors: Array<{ title: string; description: string }> = [];
- if (caseStatus === 'closed') {
+ let errors: Array<{ title: string; description: JSX.Element }> = [];
+ if (actionLicense != null && !actionLicense.enabledInLicense) {
errors = [
...errors,
{
- title: i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE,
- description: i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_DESCRIPTION,
+ title: i18n.PUSH_DISABLE_BY_LICENSE_TITLE,
+ description: (
+
+ {i18n.LINK_CLOUD_DEPLOYMENT}
+
+ ),
+ }}
+ />
+ ),
},
];
}
@@ -78,16 +94,33 @@ export const usePushToService = ({
...errors,
{
title: i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE,
- description: i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_DESCRIPTION,
+ description: (
+
+ {i18n.LINK_CONNECTOR_CONFIGURE}
+
+ ),
+ }}
+ />
+ ),
},
];
}
- if (actionLicense != null && !actionLicense.enabledInLicense) {
+ if (caseStatus === 'closed') {
errors = [
...errors,
{
- title: i18n.PUSH_DISABLE_BY_LICENSE_TITLE,
- description: i18n.PUSH_DISABLE_BY_LICENSE_DESCRIPTION,
+ title: i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE,
+ description: (
+
+ ),
},
];
}
@@ -96,12 +129,24 @@ export const usePushToService = ({
...errors,
{
title: i18n.PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE,
- description: i18n.PUSH_DISABLE_BY_KIBANA_CONFIG_DESCRIPTION,
+ description: (
+
+ {'coming soon...'}
+
+ ),
+ }}
+ />
+ ),
},
];
}
return errors;
- }, [actionLicense, caseStatus, connector, loadingCaseConfigure, loadingLicense]);
+ }, [actionLicense, caseStatus, connector, loadingCaseConfigure, loadingLicense, urlSearch]);
const pushToServiceButton = useMemo(
() => (
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts
index d869e962c44f6..beba80ccd934c 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts
@@ -117,55 +117,41 @@ export const UPDATE_PUSH_SERVICENOW = i18n.translate(
export const PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE = i18n.translate(
'xpack.siem.case.caseView.pushToServiceDisableByNoCaseConfigTitle',
{
- defaultMessage: 'Configure case',
- }
-);
-
-export const PUSH_DISABLE_BY_NO_CASE_CONFIG_DESCRIPTION = i18n.translate(
- 'xpack.siem.case.caseView.pushToServiceDisableByNoCaseConfigDescription',
- {
- defaultMessage: 'You did not configure you case system',
+ defaultMessage: 'Configure external connector',
}
);
export const PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE = i18n.translate(
'xpack.siem.case.caseView.pushToServiceDisableBecauseCaseClosedTitle',
{
- defaultMessage: 'Case closed',
- }
-);
-
-export const PUSH_DISABLE_BECAUSE_CASE_CLOSED_DESCRIPTION = i18n.translate(
- 'xpack.siem.case.caseView.pushToServiceDisableBecauseCaseClosedDescription',
- {
- defaultMessage: 'You cannot push a case who have been closed',
+ defaultMessage: 'Reopen the case',
}
);
export const PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE = i18n.translate(
'xpack.siem.case.caseView.pushToServiceDisableByConfigTitle',
{
- defaultMessage: 'Connector kibana config',
+ defaultMessage: 'Enable ServiceNow in Kibana configuration file',
}
);
-export const PUSH_DISABLE_BY_KIBANA_CONFIG_DESCRIPTION = i18n.translate(
- 'xpack.siem.case.caseView.pushToServiceDisableByConfigDescription',
+export const PUSH_DISABLE_BY_LICENSE_TITLE = i18n.translate(
+ 'xpack.siem.case.caseView.pushToServiceDisableByLicenseTitle',
{
- defaultMessage: 'ServiceNow connector have been disabled in kibana config',
+ defaultMessage: 'Upgrade to Elastic Platinum',
}
);
-export const PUSH_DISABLE_BY_LICENSE_TITLE = i18n.translate(
- 'xpack.siem.case.caseView.pushToServiceDisableByLicenseTitle',
+export const LINK_CLOUD_DEPLOYMENT = i18n.translate(
+ 'xpack.siem.case.caseView.cloudDeploymentLink',
{
- defaultMessage: 'Elastic Stack subscriptions',
+ defaultMessage: 'cloud deployment',
}
);
-export const PUSH_DISABLE_BY_LICENSE_DESCRIPTION = i18n.translate(
- 'xpack.siem.case.caseView.pushToServiceDisableByLicenseDescription',
+export const LINK_CONNECTOR_CONFIGURE = i18n.translate(
+ 'xpack.siem.case.caseView.connectorConfigureLink',
{
- defaultMessage: 'ServiceNow is disabled because you do not have the right license',
+ defaultMessage: 'connector',
}
);
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/index.tsx
index 37fea6f624536..15b50e4b4cd8d 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/index.tsx
@@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiCallOut, EuiButton, EuiDescriptionList } from '@elastic/eui';
+import { EuiCallOut, EuiButton, EuiDescriptionList, EuiSpacer } from '@elastic/eui';
import React, { memo, useCallback, useState } from 'react';
import * as i18n from './translations';
interface ErrorsPushServiceCallOut {
- errors: Array<{ title: string; description: string }>;
+ errors: Array<{ title: string; description: JSX.Element }>;
}
const ErrorsPushServiceCallOutComponent = ({ errors }: ErrorsPushServiceCallOut) => {
@@ -18,12 +18,15 @@ const ErrorsPushServiceCallOutComponent = ({ errors }: ErrorsPushServiceCallOut)
const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]);
return showCallOut ? (
-
-
-
- {i18n.DISMISS_CALLOUT}
-
-
+ <>
+
+
+
+ {i18n.DISMISS_CALLOUT}
+
+
+
+ >
) : null;
};
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/translations.ts
index cfd26632e6a36..57712e720f6d0 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/translations.ts
@@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
export const ERROR_PUSH_SERVICE_CALLOUT_TITLE = i18n.translate(
'xpack.siem.case.errorsPushServiceCallOutTitle',
{
- defaultMessage: 'You can not push to ServiceNow because of the errors below',
+ defaultMessage: 'To send cases to external systems, you need to:',
}
);
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts
index 4bb07053a9916..8f9d2087699f8 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts
@@ -152,7 +152,7 @@ export const CONFIGURE_CASES_PAGE_TITLE = i18n.translate(
);
export const CONFIGURE_CASES_BUTTON = i18n.translate('xpack.siem.case.configureCasesButton', {
- defaultMessage: 'Edit third-party connection',
+ defaultMessage: 'Edit external connection',
});
export const ADD_COMMENT = i18n.translate('xpack.siem.case.caseView.comment.addComment', {
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/utils.ts b/x-pack/legacy/plugins/siem/public/pages/case/utils.ts
index ccb3b71a476ec..3f2964b8cdd6d 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/utils.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/case/utils.ts
@@ -21,7 +21,7 @@ export const getBreadcrumbs = (params: RouteSpyState): Breadcrumb[] => {
...breadcrumb,
{
text: i18n.CREATE_BC_TITLE,
- href: getCreateCaseUrl(),
+ href: getCreateCaseUrl(''),
},
];
} else if (params.detailName != null) {
@@ -29,7 +29,7 @@ export const getBreadcrumbs = (params: RouteSpyState): Breadcrumb[] => {
...breadcrumb,
{
text: params.state?.caseTitle ?? '',
- href: getCaseDetailsUrl(params.detailName),
+ href: getCaseDetailsUrl(params.detailName, ''),
},
];
}
From 28b7fa6946e5c94e53b1b1245fdae00012f82bff Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Date: Mon, 23 Mar 2020 16:34:00 -0400
Subject: [PATCH 16/17] add aria label + fix but on add/remove tags
---
.../user_action_tree/translations.ts | 13 +++++
.../user_action_tree/user_action_title.tsx | 8 ++-
.../server/services/user_actions/helpers.ts | 51 ++++++++++---------
3 files changed, 43 insertions(+), 29 deletions(-)
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts
index 3958938e9c126..0ca6bcff513fc 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts
@@ -6,6 +6,8 @@
import { i18n } from '@kbn/i18n';
+export * from '../case_view/translations';
+
export const ALREADY_PUSHED_TO_SERVICE = i18n.translate(
'xpack.siem.case.caseView.alreadyPushedToService',
{
@@ -19,3 +21,14 @@ export const REQUIRED_UPDATE_TO_SERVICE = i18n.translate(
defaultMessage: 'Requires update to ServiceNow incident',
}
);
+
+export const COPY_LINK_COMMENT = i18n.translate('xpack.siem.case.caseView.copyCommentLinkAria', {
+ defaultMessage: 'click to copy comment link',
+});
+
+export const MOVE_TO_ORIGINAL_COMMENT = i18n.translate(
+ 'xpack.siem.case.caseView.moveToCommentAria',
+ {
+ defaultMessage: 'click to highlight the reference comment',
+ }
+);
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx
index 5c67efb241a43..6ca81667d9712 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx
@@ -15,9 +15,9 @@ import { useParams } from 'react-router-dom';
import { LocalizedDateTooltip } from '../../../../components/localized_date_tooltip';
import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search';
import { navTabs } from '../../../home/home_navigations';
-import * as i18n from '../case_view/translations';
import { PropertyActions } from '../property_actions';
import { SiemPageName } from '../../../home/types';
+import * as i18n from './translations';
const MySpinner = styled(EuiLoadingSpinner)`
.euiLoadingSpinner {
@@ -124,8 +124,7 @@ export const UserActionTitle = ({
{!isEmpty(linkId) && (
@@ -133,8 +132,7 @@ export const UserActionTitle = ({
)}
0) {
- userActions = [
- ...userActions,
- buildCaseUserActionItem({
- action: 'add',
- actionAt: actionDate,
- actionBy,
- caseId: updatedItem.id,
- fields: [field],
- newValue: compareValues.addedItems.join(', '),
- }),
- ];
- } else if (compareValues != null && compareValues.deletedItems.length > 0) {
- userActions = [
- ...userActions,
- buildCaseUserActionItem({
- action: 'delete',
- actionAt: actionDate,
- actionBy,
- caseId: updatedItem.id,
- fields: [field],
- newValue: compareValues.deletedItems.join(', '),
- }),
- ];
+ if (compareValues != null) {
+ if (compareValues.addedItems.length > 0) {
+ userActions = [
+ ...userActions,
+ buildCaseUserActionItem({
+ action: 'add',
+ actionAt: actionDate,
+ actionBy,
+ caseId: updatedItem.id,
+ fields: [field],
+ newValue: compareValues.addedItems.join(', '),
+ }),
+ ];
+ }
+ if (compareValues.deletedItems.length > 0) {
+ userActions = [
+ ...userActions,
+ buildCaseUserActionItem({
+ action: 'delete',
+ actionAt: actionDate,
+ actionBy,
+ caseId: updatedItem.id,
+ fields: [field],
+ newValue: compareValues.deletedItems.join(', '),
+ }),
+ ];
+ }
} else if (origValue !== updatedValue) {
userActions = [
...userActions,
From 87fca1ca69ac38175a40bcf3231de7706eaac635 Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Date: Mon, 23 Mar 2020 17:42:17 -0400
Subject: [PATCH 17/17] fix i18n
---
.../public/pages/case/components/case_view/push_to_service.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/push_to_service.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/push_to_service.tsx
index 3cb4d06101731..944302c1940ee 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/push_to_service.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/push_to_service.tsx
@@ -131,7 +131,7 @@ export const usePushToService = ({
title: i18n.PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE,
description: (