[Security Solution][Case] Fix subcases bugs on detections and case view (#91836)
Co-authored-by: Jonathan Buttner <jonathan.buttner@elastic.co>
This commit is contained in:
parent
a1d2d870d3
commit
c2877a6d96
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
import { CaseAttributesRt } from './case';
|
||||
import { CommentResponseRt } from './comment';
|
||||
import { SubCaseAttributesRt, SubCaseResponseRt } from './sub_case';
|
||||
|
||||
export const CollectionSubCaseAttributesRt = rt.intersection([
|
||||
rt.partial({ subCase: SubCaseAttributesRt }),
|
||||
rt.type({
|
||||
case: CaseAttributesRt,
|
||||
}),
|
||||
]);
|
||||
|
||||
export const CollectWithSubCaseResponseRt = rt.intersection([
|
||||
CaseAttributesRt,
|
||||
rt.type({
|
||||
id: rt.string,
|
||||
totalComment: rt.number,
|
||||
version: rt.string,
|
||||
}),
|
||||
rt.partial({
|
||||
subCase: SubCaseResponseRt,
|
||||
totalAlerts: rt.number,
|
||||
comments: rt.array(CommentResponseRt),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type CollectionWithSubCaseResponse = rt.TypeOf<typeof CollectWithSubCaseResponseRt>;
|
||||
export type CollectionWithSubCaseAttributes = rt.TypeOf<typeof CollectionSubCaseAttributesRt>;
|
|
@ -11,4 +11,3 @@ export * from './comment';
|
|||
export * from './status';
|
||||
export * from './user_actions';
|
||||
export * from './sub_case';
|
||||
export * from './commentable_case';
|
||||
|
|
|
@ -24,8 +24,8 @@ export const getSubCasesUrl = (caseID: string): string => {
|
|||
return SUB_CASES_URL.replace('{case_id}', caseID);
|
||||
};
|
||||
|
||||
export const getSubCaseDetailsUrl = (caseID: string, subCaseID: string): string => {
|
||||
return SUB_CASE_DETAILS_URL.replace('{case_id}', caseID).replace('{sub_case_id}', subCaseID);
|
||||
export const getSubCaseDetailsUrl = (caseID: string, subCaseId: string): string => {
|
||||
return SUB_CASE_DETAILS_URL.replace('{case_id}', caseID).replace('{sub_case_id}', subCaseId);
|
||||
};
|
||||
|
||||
export const getCaseCommentsUrl = (id: string): string => {
|
||||
|
@ -40,8 +40,8 @@ export const getCaseUserActionUrl = (id: string): string => {
|
|||
return CASE_USER_ACTIONS_URL.replace('{case_id}', id);
|
||||
};
|
||||
|
||||
export const getSubCaseUserActionUrl = (caseID: string, subCaseID: string): string => {
|
||||
return SUB_CASE_USER_ACTIONS_URL.replace('{case_id}', caseID).replace('{sub_case_id}', subCaseID);
|
||||
export const getSubCaseUserActionUrl = (caseID: string, subCaseId: string): string => {
|
||||
return SUB_CASE_USER_ACTIONS_URL.replace('{case_id}', caseID).replace('{sub_case_id}', subCaseId);
|
||||
};
|
||||
|
||||
export const getCasePushUrl = (caseId: string, connectorId: string): string => {
|
||||
|
|
|
@ -9,14 +9,37 @@ import { either, fold } from 'fp-ts/lib/Either';
|
|||
import { identity } from 'fp-ts/lib/function';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import * as rt from 'io-ts';
|
||||
import { failure } from 'io-ts/lib/PathReporter';
|
||||
import { isObject } from 'lodash/fp';
|
||||
|
||||
type ErrorFactory = (message: string) => Error;
|
||||
|
||||
export const formatErrors = (errors: rt.Errors): string[] => {
|
||||
const err = errors.map((error) => {
|
||||
if (error.message != null) {
|
||||
return error.message;
|
||||
} else {
|
||||
const keyContext = error.context
|
||||
.filter(
|
||||
(entry) => entry.key != null && !Number.isInteger(+entry.key) && entry.key.trim() !== ''
|
||||
)
|
||||
.map((entry) => entry.key)
|
||||
.join(',');
|
||||
|
||||
const nameContext = error.context.find((entry) => entry.type?.name?.length > 0);
|
||||
const suppliedValue =
|
||||
keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : '';
|
||||
const value = isObject(error.value) ? JSON.stringify(error.value) : error.value;
|
||||
return `Invalid value "${value}" supplied to "${suppliedValue}"`;
|
||||
}
|
||||
});
|
||||
|
||||
return [...new Set(err)];
|
||||
};
|
||||
|
||||
export const createPlainError = (message: string) => new Error(message);
|
||||
|
||||
export const throwErrors = (createError: ErrorFactory) => (errors: rt.Errors) => {
|
||||
throw createError(failure(errors).join('\n'));
|
||||
throw createError(formatErrors(errors).join());
|
||||
};
|
||||
|
||||
export const decodeOrThrow = <A, O, I>(
|
||||
|
|
|
@ -72,7 +72,7 @@ export interface TransformFieldsArgs<P, S> {
|
|||
|
||||
export interface ExternalServiceComment {
|
||||
comment: string;
|
||||
commentId?: string;
|
||||
commentId: string;
|
||||
}
|
||||
|
||||
export interface MapIncident {
|
||||
|
|
|
@ -540,6 +540,7 @@ describe('utils', () => {
|
|||
},
|
||||
{
|
||||
comment: 'Elastic Security Alerts attached to the case: 3',
|
||||
commentId: 'mock-id-1-total-alerts',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -569,6 +570,7 @@ describe('utils', () => {
|
|||
},
|
||||
{
|
||||
comment: 'Elastic Security Alerts attached to the case: 4',
|
||||
commentId: 'mock-id-1-total-alerts',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -185,6 +185,7 @@ export const createIncident = async ({
|
|||
if (totalAlerts > 0) {
|
||||
comments.push({
|
||||
comment: `Elastic Security Alerts attached to the case: ${totalAlerts}`,
|
||||
commentId: `${theCase.id}-total-alerts`,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
CaseType,
|
||||
SubCaseAttributes,
|
||||
CommentRequest,
|
||||
CollectionWithSubCaseResponse,
|
||||
CaseResponse,
|
||||
User,
|
||||
CommentRequestAlertType,
|
||||
AlertCommentRequestRt,
|
||||
|
@ -113,7 +113,7 @@ const addGeneratedAlerts = async ({
|
|||
caseClient,
|
||||
caseId,
|
||||
comment,
|
||||
}: AddCommentFromRuleArgs): Promise<CollectionWithSubCaseResponse> => {
|
||||
}: AddCommentFromRuleArgs): Promise<CaseResponse> => {
|
||||
const query = pipe(
|
||||
AlertCommentRequestRt.decode(comment),
|
||||
fold(throwErrors(Boom.badRequest), identity)
|
||||
|
@ -260,7 +260,7 @@ export const addComment = async ({
|
|||
caseId,
|
||||
comment,
|
||||
user,
|
||||
}: AddCommentArgs): Promise<CollectionWithSubCaseResponse> => {
|
||||
}: AddCommentArgs): Promise<CaseResponse> => {
|
||||
const query = pipe(
|
||||
CommentRequestRt.decode(comment),
|
||||
fold(throwErrors(Boom.badRequest), identity)
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
CasesPatchRequest,
|
||||
CasesResponse,
|
||||
CaseStatuses,
|
||||
CollectionWithSubCaseResponse,
|
||||
CommentRequest,
|
||||
ConnectorMappingsAttributes,
|
||||
GetFieldsResponse,
|
||||
|
@ -89,7 +88,7 @@ export interface ConfigureFields {
|
|||
* This represents the interface that other plugins can access.
|
||||
*/
|
||||
export interface CaseClient {
|
||||
addComment(args: CaseClientAddComment): Promise<CollectionWithSubCaseResponse>;
|
||||
addComment(args: CaseClientAddComment): Promise<CaseResponse>;
|
||||
create(theCase: CasePostRequest): Promise<CaseResponse>;
|
||||
get(args: CaseClientGet): Promise<CaseResponse>;
|
||||
getAlerts(args: CaseClientGetAlerts): Promise<CaseClientGetAlertsResponse>;
|
||||
|
|
|
@ -17,8 +17,8 @@ import {
|
|||
CaseSettings,
|
||||
CaseStatuses,
|
||||
CaseType,
|
||||
CollectionWithSubCaseResponse,
|
||||
CollectWithSubCaseResponseRt,
|
||||
CaseResponse,
|
||||
CaseResponseRt,
|
||||
CommentAttributes,
|
||||
CommentPatchRequest,
|
||||
CommentRequest,
|
||||
|
@ -254,7 +254,7 @@ export class CommentableCase {
|
|||
};
|
||||
}
|
||||
|
||||
public async encode(): Promise<CollectionWithSubCaseResponse> {
|
||||
public async encode(): Promise<CaseResponse> {
|
||||
const collectionCommentStats = await this.service.getAllCaseComments({
|
||||
client: this.soClient,
|
||||
id: this.collection.id,
|
||||
|
@ -265,22 +265,6 @@ export class CommentableCase {
|
|||
},
|
||||
});
|
||||
|
||||
if (this.subCase) {
|
||||
const subCaseComments = await this.service.getAllSubCaseComments({
|
||||
client: this.soClient,
|
||||
id: this.subCase.id,
|
||||
});
|
||||
|
||||
return CollectWithSubCaseResponseRt.encode({
|
||||
subCase: flattenSubCaseSavedObject({
|
||||
savedObject: this.subCase,
|
||||
comments: subCaseComments.saved_objects,
|
||||
totalAlerts: countAlertsForID({ comments: subCaseComments, id: this.subCase.id }),
|
||||
}),
|
||||
...this.formatCollectionForEncoding(collectionCommentStats.total),
|
||||
});
|
||||
}
|
||||
|
||||
const collectionComments = await this.service.getAllCaseComments({
|
||||
client: this.soClient,
|
||||
id: this.collection.id,
|
||||
|
@ -291,10 +275,45 @@ export class CommentableCase {
|
|||
},
|
||||
});
|
||||
|
||||
return CollectWithSubCaseResponseRt.encode({
|
||||
const collectionTotalAlerts =
|
||||
countAlertsForID({ comments: collectionComments, id: this.collection.id }) ?? 0;
|
||||
|
||||
const caseResponse = {
|
||||
comments: flattenCommentSavedObjects(collectionComments.saved_objects),
|
||||
totalAlerts: countAlertsForID({ comments: collectionComments, id: this.collection.id }),
|
||||
totalAlerts: collectionTotalAlerts,
|
||||
...this.formatCollectionForEncoding(collectionCommentStats.total),
|
||||
});
|
||||
};
|
||||
|
||||
if (this.subCase) {
|
||||
const subCaseComments = await this.service.getAllSubCaseComments({
|
||||
client: this.soClient,
|
||||
id: this.subCase.id,
|
||||
});
|
||||
const totalAlerts = countAlertsForID({ comments: subCaseComments, id: this.subCase.id }) ?? 0;
|
||||
|
||||
return CaseResponseRt.encode({
|
||||
...caseResponse,
|
||||
/**
|
||||
* For now we need the sub case comments and totals to be exposed on the top level of the response so that the UI
|
||||
* functionality can stay the same. Ideally in the future we can refactor this so that the UI will look for the
|
||||
* comments either in the top level for a case or a collection or in the subCases field if it is a sub case.
|
||||
*
|
||||
* If we ever need to return both the collection's comments and the sub case comments we'll need to refactor it then
|
||||
* as well.
|
||||
*/
|
||||
comments: flattenCommentSavedObjects(subCaseComments.saved_objects),
|
||||
totalComment: subCaseComments.saved_objects.length,
|
||||
totalAlerts,
|
||||
subCases: [
|
||||
flattenSubCaseSavedObject({
|
||||
savedObject: this.subCase,
|
||||
totalComment: subCaseComments.saved_objects.length,
|
||||
totalAlerts,
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return CaseResponseRt.encode(caseResponse);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
AssociationType,
|
||||
CaseResponse,
|
||||
CasesResponse,
|
||||
CollectionWithSubCaseResponse,
|
||||
} from '../../../common/api';
|
||||
import {
|
||||
connectorMappingsServiceMock,
|
||||
|
@ -1018,9 +1017,10 @@ describe('case connector', () => {
|
|||
|
||||
describe('addComment', () => {
|
||||
it('executes correctly', async () => {
|
||||
const commentReturn: CollectionWithSubCaseResponse = {
|
||||
const commentReturn: CaseResponse = {
|
||||
id: 'mock-it',
|
||||
totalComment: 0,
|
||||
totalAlerts: 0,
|
||||
version: 'WzksMV0=',
|
||||
|
||||
closed_at: null,
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
ConnectorSchema,
|
||||
CommentSchema,
|
||||
} from './schema';
|
||||
import { CaseResponse, CasesResponse, CollectionWithSubCaseResponse } from '../../../common/api';
|
||||
import { CaseResponse, CasesResponse } from '../../../common/api';
|
||||
|
||||
export type CaseConfiguration = TypeOf<typeof CaseConfigurationSchema>;
|
||||
export type Connector = TypeOf<typeof ConnectorSchema>;
|
||||
|
@ -29,7 +29,7 @@ export type ExecutorSubActionAddCommentParams = TypeOf<
|
|||
>;
|
||||
|
||||
export type CaseExecutorParams = TypeOf<typeof CaseExecutorParamsSchema>;
|
||||
export type CaseExecutorResponse = CaseResponse | CasesResponse | CollectionWithSubCaseResponse;
|
||||
export type CaseExecutorResponse = CaseResponse | CasesResponse;
|
||||
|
||||
export type CaseActionType = ActionType<
|
||||
CaseConfiguration,
|
||||
|
|
|
@ -23,7 +23,7 @@ export function initDeleteAllCommentsApi({ caseService, router, userActionServic
|
|||
}),
|
||||
query: schema.maybe(
|
||||
schema.object({
|
||||
subCaseID: schema.maybe(schema.string()),
|
||||
subCaseId: schema.maybe(schema.string()),
|
||||
})
|
||||
),
|
||||
},
|
||||
|
@ -35,11 +35,11 @@ export function initDeleteAllCommentsApi({ caseService, router, userActionServic
|
|||
const { username, full_name, email } = await caseService.getUser({ request });
|
||||
const deleteDate = new Date().toISOString();
|
||||
|
||||
const id = request.query?.subCaseID ?? request.params.case_id;
|
||||
const id = request.query?.subCaseId ?? request.params.case_id;
|
||||
const comments = await caseService.getCommentsByAssociation({
|
||||
client,
|
||||
id,
|
||||
associationType: request.query?.subCaseID
|
||||
associationType: request.query?.subCaseId
|
||||
? AssociationType.subCase
|
||||
: AssociationType.case,
|
||||
});
|
||||
|
@ -61,7 +61,7 @@ export function initDeleteAllCommentsApi({ caseService, router, userActionServic
|
|||
actionAt: deleteDate,
|
||||
actionBy: { username, full_name, email },
|
||||
caseId: request.params.case_id,
|
||||
subCaseId: request.query?.subCaseID,
|
||||
subCaseId: request.query?.subCaseId,
|
||||
commentId: comment.id,
|
||||
fields: ['comment'],
|
||||
})
|
||||
|
|
|
@ -25,7 +25,7 @@ export function initDeleteCommentApi({ caseService, router, userActionService }:
|
|||
}),
|
||||
query: schema.maybe(
|
||||
schema.object({
|
||||
subCaseID: schema.maybe(schema.string()),
|
||||
subCaseId: schema.maybe(schema.string()),
|
||||
})
|
||||
),
|
||||
},
|
||||
|
@ -46,8 +46,8 @@ export function initDeleteCommentApi({ caseService, router, userActionService }:
|
|||
throw Boom.notFound(`This comment ${request.params.comment_id} does not exist anymore.`);
|
||||
}
|
||||
|
||||
const type = request.query?.subCaseID ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT;
|
||||
const id = request.query?.subCaseID ?? request.params.case_id;
|
||||
const type = request.query?.subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT;
|
||||
const id = request.query?.subCaseId ?? request.params.case_id;
|
||||
|
||||
const caseRef = myComment.references.find((c) => c.type === type);
|
||||
if (caseRef == null || (caseRef != null && caseRef.id !== id)) {
|
||||
|
@ -69,7 +69,7 @@ export function initDeleteCommentApi({ caseService, router, userActionService }:
|
|||
actionAt: deleteDate,
|
||||
actionBy: { username, full_name, email },
|
||||
caseId: id,
|
||||
subCaseId: request.query?.subCaseID,
|
||||
subCaseId: request.query?.subCaseId,
|
||||
commentId: request.params.comment_id,
|
||||
fields: ['comment'],
|
||||
}),
|
||||
|
|
|
@ -27,7 +27,7 @@ import { defaultPage, defaultPerPage } from '../..';
|
|||
|
||||
const FindQueryParamsRt = rt.partial({
|
||||
...SavedObjectFindOptionsRt.props,
|
||||
subCaseID: rt.string,
|
||||
subCaseId: rt.string,
|
||||
});
|
||||
|
||||
export function initFindCaseCommentsApi({ caseService, router }: RouteDeps) {
|
||||
|
@ -49,8 +49,8 @@ export function initFindCaseCommentsApi({ caseService, router }: RouteDeps) {
|
|||
fold(throwErrors(Boom.badRequest), identity)
|
||||
);
|
||||
|
||||
const id = query.subCaseID ?? request.params.case_id;
|
||||
const associationType = query.subCaseID ? AssociationType.subCase : AssociationType.case;
|
||||
const id = query.subCaseId ?? request.params.case_id;
|
||||
const associationType = query.subCaseId ? AssociationType.subCase : AssociationType.case;
|
||||
const args = query
|
||||
? {
|
||||
caseService,
|
||||
|
|
|
@ -25,7 +25,7 @@ export function initGetAllCommentsApi({ caseService, router }: RouteDeps) {
|
|||
query: schema.maybe(
|
||||
schema.object({
|
||||
includeSubCaseComments: schema.maybe(schema.boolean()),
|
||||
subCaseID: schema.maybe(schema.string()),
|
||||
subCaseId: schema.maybe(schema.string()),
|
||||
})
|
||||
),
|
||||
},
|
||||
|
@ -35,10 +35,10 @@ export function initGetAllCommentsApi({ caseService, router }: RouteDeps) {
|
|||
const client = context.core.savedObjects.client;
|
||||
let comments: SavedObjectsFindResponse<CommentAttributes>;
|
||||
|
||||
if (request.query?.subCaseID) {
|
||||
if (request.query?.subCaseId) {
|
||||
comments = await caseService.getAllSubCaseComments({
|
||||
client,
|
||||
id: request.query.subCaseID,
|
||||
id: request.query.subCaseId,
|
||||
options: {
|
||||
sortField: defaultSortField,
|
||||
},
|
||||
|
|
|
@ -26,11 +26,11 @@ interface CombinedCaseParams {
|
|||
service: CaseServiceSetup;
|
||||
client: SavedObjectsClientContract;
|
||||
caseID: string;
|
||||
subCaseID?: string;
|
||||
subCaseId?: string;
|
||||
}
|
||||
|
||||
async function getCommentableCase({ service, client, caseID, subCaseID }: CombinedCaseParams) {
|
||||
if (subCaseID) {
|
||||
async function getCommentableCase({ service, client, caseID, subCaseId }: CombinedCaseParams) {
|
||||
if (subCaseId) {
|
||||
const [caseInfo, subCase] = await Promise.all([
|
||||
service.getCase({
|
||||
client,
|
||||
|
@ -38,7 +38,7 @@ async function getCommentableCase({ service, client, caseID, subCaseID }: Combin
|
|||
}),
|
||||
service.getSubCase({
|
||||
client,
|
||||
id: subCaseID,
|
||||
id: subCaseId,
|
||||
}),
|
||||
]);
|
||||
return new CommentableCase({ collection: caseInfo, service, subCase, soClient: client });
|
||||
|
@ -66,7 +66,7 @@ export function initPatchCommentApi({
|
|||
}),
|
||||
query: schema.maybe(
|
||||
schema.object({
|
||||
subCaseID: schema.maybe(schema.string()),
|
||||
subCaseId: schema.maybe(schema.string()),
|
||||
})
|
||||
),
|
||||
body: escapeHatch,
|
||||
|
@ -87,7 +87,7 @@ export function initPatchCommentApi({
|
|||
service: caseService,
|
||||
client,
|
||||
caseID: request.params.case_id,
|
||||
subCaseID: request.query?.subCaseID,
|
||||
subCaseId: request.query?.subCaseId,
|
||||
});
|
||||
|
||||
const myComment = await caseService.getComment({
|
||||
|
@ -103,7 +103,7 @@ export function initPatchCommentApi({
|
|||
throw Boom.badRequest(`You cannot change the type of the comment.`);
|
||||
}
|
||||
|
||||
const saveObjType = request.query?.subCaseID ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT;
|
||||
const saveObjType = request.query?.subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT;
|
||||
|
||||
const caseRef = myComment.references.find((c) => c.type === saveObjType);
|
||||
if (caseRef == null || (caseRef != null && caseRef.id !== commentableCase.id)) {
|
||||
|
@ -144,7 +144,7 @@ export function initPatchCommentApi({
|
|||
actionAt: updatedDate,
|
||||
actionBy: { username, full_name, email },
|
||||
caseId: request.params.case_id,
|
||||
subCaseId: request.query?.subCaseID,
|
||||
subCaseId: request.query?.subCaseId,
|
||||
commentId: updatedComment.id,
|
||||
fields: ['comment'],
|
||||
newValue: JSON.stringify(queryRestAttributes),
|
||||
|
|
|
@ -21,7 +21,7 @@ export function initPostCommentApi({ router }: RouteDeps) {
|
|||
}),
|
||||
query: schema.maybe(
|
||||
schema.object({
|
||||
subCaseID: schema.maybe(schema.string()),
|
||||
subCaseId: schema.maybe(schema.string()),
|
||||
})
|
||||
),
|
||||
body: escapeHatch,
|
||||
|
@ -33,7 +33,7 @@ export function initPostCommentApi({ router }: RouteDeps) {
|
|||
}
|
||||
|
||||
const caseClient = context.case.getCaseClient();
|
||||
const caseId = request.query?.subCaseID ?? request.params.case_id;
|
||||
const caseId = request.query?.subCaseId ?? request.params.case_id;
|
||||
const comment = request.body as CommentRequest;
|
||||
|
||||
try {
|
||||
|
|
|
@ -153,8 +153,8 @@ async function getParentCases({
|
|||
|
||||
return parentCases.saved_objects.reduce((acc, so) => {
|
||||
const subCaseIDsWithParent = parentIDInfo.parentIDToSubID.get(so.id);
|
||||
subCaseIDsWithParent?.forEach((subCaseID) => {
|
||||
acc.set(subCaseID, so);
|
||||
subCaseIDsWithParent?.forEach((subCaseId) => {
|
||||
acc.set(subCaseId, so);
|
||||
});
|
||||
return acc;
|
||||
}, new Map<string, SavedObject<ESCaseAttributes>>());
|
||||
|
|
|
@ -8,12 +8,7 @@
|
|||
import yargs from 'yargs';
|
||||
import { ToolingLog } from '@kbn/dev-utils';
|
||||
import { KbnClient } from '@kbn/test';
|
||||
import {
|
||||
CaseResponse,
|
||||
CaseType,
|
||||
CollectionWithSubCaseResponse,
|
||||
ConnectorTypes,
|
||||
} from '../../../common/api';
|
||||
import { CaseResponse, CaseType, ConnectorTypes } from '../../../common/api';
|
||||
import { CommentType } from '../../../common/api/cases/comment';
|
||||
import { CASES_URL } from '../../../common/constants';
|
||||
import { ActionResult, ActionTypeExecutorResult } from '../../../../actions/common';
|
||||
|
@ -119,9 +114,7 @@ async function handleGenGroupAlerts(argv: any) {
|
|||
),
|
||||
};
|
||||
|
||||
const executeResp = await client.request<
|
||||
ActionTypeExecutorResult<CollectionWithSubCaseResponse>
|
||||
>({
|
||||
const executeResp = await client.request<ActionTypeExecutorResult<CaseResponse>>({
|
||||
path: `/api/actions/action/${createdAction.data.id}/_execute`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import { EuiBasicTable as _EuiBasicTable } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { Case } from '../../containers/types';
|
||||
import { Case, SubCase } from '../../containers/types';
|
||||
import { CasesColumns } from './columns';
|
||||
import { AssociationType } from '../../../../../case/common/api';
|
||||
|
||||
|
@ -34,14 +34,25 @@ BasicTable.displayName = 'BasicTable';
|
|||
export const getExpandedRowMap = ({
|
||||
data,
|
||||
columns,
|
||||
isModal,
|
||||
onSubCaseClick,
|
||||
}: {
|
||||
data: Case[] | null;
|
||||
columns: CasesColumns[];
|
||||
isModal: boolean;
|
||||
onSubCaseClick?: (theSubCase: SubCase) => void;
|
||||
}): ExpandedRowMap => {
|
||||
if (data == null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const rowProps = (theSubCase: SubCase) => {
|
||||
return {
|
||||
...(isModal && onSubCaseClick ? { onClick: () => onSubCaseClick(theSubCase) } : {}),
|
||||
className: 'subCase',
|
||||
};
|
||||
};
|
||||
|
||||
return data.reduce((acc, curr) => {
|
||||
if (curr.subCases != null) {
|
||||
const subCases = curr.subCases.map((subCase, index) => ({
|
||||
|
@ -58,6 +69,7 @@ export const getExpandedRowMap = ({
|
|||
data-test-subj={`sub-cases-table-${curr.id}`}
|
||||
itemId="id"
|
||||
items={subCases}
|
||||
rowProps={rowProps}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
|
|
@ -19,11 +19,12 @@ import {
|
|||
import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types';
|
||||
import { isEmpty, memoize } from 'lodash/fp';
|
||||
import styled, { css } from 'styled-components';
|
||||
import * as i18n from './translations';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import * as i18n from './translations';
|
||||
import { CaseStatuses, CaseType } from '../../../../../case/common/api';
|
||||
import { getCasesColumns } from './columns';
|
||||
import { Case, DeleteCase, FilterOptions, SortFieldCase } from '../../containers/types';
|
||||
import { Case, DeleteCase, FilterOptions, SortFieldCase, SubCase } from '../../containers/types';
|
||||
import { useGetCases, UpdateCase } from '../../containers/use_get_cases';
|
||||
import { useGetCasesStatus } from '../../containers/use_get_cases_status';
|
||||
import { useDeleteCases } from '../../containers/use_delete_cases';
|
||||
|
@ -58,6 +59,7 @@ import { getExpandedRowMap } from './expanded_row';
|
|||
const Div = styled.div`
|
||||
margin-top: ${({ theme }) => theme.eui.paddingSizes.m};
|
||||
`;
|
||||
|
||||
const FlexItemDivider = styled(EuiFlexItem)`
|
||||
${({ theme }) => css`
|
||||
.euiFlexGroup--gutterMedium > &.euiFlexItem {
|
||||
|
@ -75,6 +77,7 @@ const ProgressLoader = styled(EuiProgress)`
|
|||
z-index: ${theme.eui.euiZHeader};
|
||||
`}
|
||||
`;
|
||||
|
||||
const getSortField = (field: string): SortFieldCase => {
|
||||
if (field === SortFieldCase.createdAt) {
|
||||
return SortFieldCase.createdAt;
|
||||
|
@ -86,19 +89,39 @@ const getSortField = (field: string): SortFieldCase => {
|
|||
|
||||
const EuiBasicTable: any = _EuiBasicTable; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
const BasicTable = styled(EuiBasicTable)`
|
||||
.euiTableRow-isExpandedRow.euiTableRow-isSelectable .euiTableCellContent {
|
||||
padding: 8px 0 8px 32px;
|
||||
}
|
||||
${({ theme }) => `
|
||||
.euiTableRow-isExpandedRow.euiTableRow-isSelectable .euiTableCellContent {
|
||||
padding: 8px 0 8px 32px;
|
||||
}
|
||||
|
||||
&.isModal .euiTableRow.isDisabled {
|
||||
cursor: not-allowed;
|
||||
background-color: ${theme.eui.euiTableHoverClickableColor};
|
||||
}
|
||||
|
||||
&.isModal .euiTableRow.euiTableRow-isExpandedRow .euiTableRowCell,
|
||||
&.isModal .euiTableRow.euiTableRow-isExpandedRow:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&.isModal .euiTableRow.euiTableRow-isExpandedRow {
|
||||
.subCase:hover {
|
||||
background-color: ${theme.eui.euiTableHoverClickableColor};
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
BasicTable.displayName = 'BasicTable';
|
||||
|
||||
interface AllCasesProps {
|
||||
onRowClick?: (theCase?: Case) => void;
|
||||
onRowClick?: (theCase?: Case | SubCase) => void;
|
||||
isModal?: boolean;
|
||||
userCanCrud: boolean;
|
||||
disabledStatuses?: CaseStatuses[];
|
||||
disabledCases?: CaseType[];
|
||||
}
|
||||
export const AllCases = React.memo<AllCasesProps>(
|
||||
({ onRowClick, isModal = false, userCanCrud }) => {
|
||||
({ onRowClick, isModal = false, userCanCrud, disabledStatuses, disabledCases = [] }) => {
|
||||
const { navigateToApp } = useKibana().services.application;
|
||||
const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.case);
|
||||
const { actionLicense } = useGetActionLicense();
|
||||
|
@ -334,8 +357,10 @@ export const AllCases = React.memo<AllCasesProps>(
|
|||
getExpandedRowMap({
|
||||
columns: memoizedGetCasesColumns,
|
||||
data: data.cases,
|
||||
isModal,
|
||||
onSubCaseClick: onRowClick,
|
||||
}),
|
||||
[data.cases, memoizedGetCasesColumns]
|
||||
[data.cases, isModal, memoizedGetCasesColumns, onRowClick]
|
||||
);
|
||||
|
||||
const memoizedPagination = useMemo(
|
||||
|
@ -356,6 +381,7 @@ export const AllCases = React.memo<AllCasesProps>(
|
|||
() => ({
|
||||
selectable: (theCase) => isEmpty(theCase.subCases),
|
||||
onSelectionChange: setSelectedCases,
|
||||
selectableMessage: (selectable) => (!selectable ? i18n.SELECTABLE_MESSAGE_COLLECTIONS : ''),
|
||||
}),
|
||||
[setSelectedCases]
|
||||
);
|
||||
|
@ -377,7 +403,8 @@ export const AllCases = React.memo<AllCasesProps>(
|
|||
|
||||
return {
|
||||
'data-test-subj': `cases-table-row-${theCase.id}`,
|
||||
...(isModal ? { onClick: onTableRowClick } : {}),
|
||||
className: classnames({ isDisabled: theCase.type === CaseType.collection }),
|
||||
...(isModal && theCase.type !== CaseType.collection ? { onClick: onTableRowClick } : {}),
|
||||
};
|
||||
},
|
||||
[isModal, onRowClick]
|
||||
|
@ -462,6 +489,7 @@ export const AllCases = React.memo<AllCasesProps>(
|
|||
status: filterOptions.status,
|
||||
}}
|
||||
setFilterRefetch={setFilterRefetch}
|
||||
disabledStatuses={disabledStatuses}
|
||||
/>
|
||||
{isCasesLoading && isDataEmpty ? (
|
||||
<Div>
|
||||
|
@ -530,6 +558,7 @@ export const AllCases = React.memo<AllCasesProps>(
|
|||
rowProps={tableRowProps}
|
||||
selection={userCanCrud && !isModal ? euiBasicTableSelectionProps : undefined}
|
||||
sorting={sorting}
|
||||
className={classnames({ isModal })}
|
||||
/>
|
||||
</Div>
|
||||
)}
|
||||
|
|
|
@ -61,4 +61,24 @@ describe('StatusFilter', () => {
|
|||
expect(onStatusChanged).toBeCalledWith('closed');
|
||||
});
|
||||
});
|
||||
|
||||
it('should disabled selected statuses', () => {
|
||||
const wrapper = mount(
|
||||
<StatusFilter {...defaultProps} disabledStatuses={[CaseStatuses.closed]} />
|
||||
);
|
||||
|
||||
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
|
||||
|
||||
expect(
|
||||
wrapper.find('button[data-test-subj="case-status-filter-open"]').prop('disabled')
|
||||
).toBeFalsy();
|
||||
|
||||
expect(
|
||||
wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').prop('disabled')
|
||||
).toBeFalsy();
|
||||
|
||||
expect(
|
||||
wrapper.find('button[data-test-subj="case-status-filter-closed"]').prop('disabled')
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,9 +14,15 @@ interface Props {
|
|||
stats: Record<CaseStatuses, number>;
|
||||
selectedStatus: CaseStatuses;
|
||||
onStatusChanged: (status: CaseStatuses) => void;
|
||||
disabledStatuses?: CaseStatuses[];
|
||||
}
|
||||
|
||||
const StatusFilterComponent: React.FC<Props> = ({ stats, selectedStatus, onStatusChanged }) => {
|
||||
const StatusFilterComponent: React.FC<Props> = ({
|
||||
stats,
|
||||
selectedStatus,
|
||||
onStatusChanged,
|
||||
disabledStatuses = [],
|
||||
}) => {
|
||||
const caseStatuses = Object.keys(statuses) as CaseStatuses[];
|
||||
const options: Array<EuiSuperSelectOption<CaseStatuses>> = caseStatuses.map((status) => ({
|
||||
value: status,
|
||||
|
@ -28,6 +34,7 @@ const StatusFilterComponent: React.FC<Props> = ({ stats, selectedStatus, onStatu
|
|||
<EuiFlexItem grow={false}>{` (${stats[status]})`}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
disabled: disabledStatuses.includes(status),
|
||||
'data-test-subj': `case-status-filter-${status}`,
|
||||
}));
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ interface CasesTableFiltersProps {
|
|||
onFilterChanged: (filterOptions: Partial<FilterOptions>) => void;
|
||||
initial: FilterOptions;
|
||||
setFilterRefetch: (val: () => void) => void;
|
||||
disabledStatuses?: CaseStatuses[];
|
||||
}
|
||||
|
||||
// Fix the width of the status dropdown to prevent hiding long text items
|
||||
|
@ -50,6 +51,7 @@ const CasesTableFiltersComponent = ({
|
|||
onFilterChanged,
|
||||
initial = defaultInitial,
|
||||
setFilterRefetch,
|
||||
disabledStatuses,
|
||||
}: CasesTableFiltersProps) => {
|
||||
const [selectedReporters, setSelectedReporters] = useState(
|
||||
initial.reporters.map((r) => r.full_name ?? r.username ?? '')
|
||||
|
@ -158,6 +160,7 @@ const CasesTableFiltersComponent = ({
|
|||
selectedStatus={initial.status}
|
||||
onStatusChanged={onStatusChanged}
|
||||
stats={stats}
|
||||
disabledStatuses={disabledStatuses}
|
||||
/>
|
||||
</StatusFilterWrapper>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiIconTip,
|
||||
} from '@elastic/eui';
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import { CaseStatuses, CaseType } from '../../../../../case/common/api';
|
||||
import * as i18n from '../case_view/translations';
|
||||
import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date';
|
||||
import { Actions } from './actions';
|
||||
|
@ -73,19 +73,21 @@ const CaseActionBarComponent: React.FC<CaseActionBarProps> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="l" justifyContent="flexEnd">
|
||||
<EuiFlexGroup gutterSize="l" justifyContent="flexEnd" data-test-subj="case-action-bar-wrapper">
|
||||
<EuiFlexItem grow={false}>
|
||||
<MyDescriptionList compressed>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false} data-test-subj="case-view-status">
|
||||
<EuiDescriptionListTitle>{i18n.STATUS}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<StatusContextMenu
|
||||
currentStatus={caseData.status}
|
||||
onStatusChanged={onStatusChanged}
|
||||
/>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
{caseData.type !== CaseType.collection && (
|
||||
<EuiFlexItem grow={false} data-test-subj="case-view-status">
|
||||
<EuiDescriptionListTitle>{i18n.STATUS}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<StatusContextMenu
|
||||
currentStatus={caseData.status}
|
||||
onStatusChanged={onStatusChanged}
|
||||
/>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>{title}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
|
|
|
@ -29,6 +29,7 @@ import { connectorsMock } from '../../containers/configure/mock';
|
|||
import { usePostPushToService } from '../../containers/use_post_push_to_service';
|
||||
import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query';
|
||||
import { ConnectorTypes } from '../../../../../case/common/api/connectors';
|
||||
import { CaseType } from '../../../../../case/common/api';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
|
@ -201,6 +202,10 @@ describe('CaseView ', () => {
|
|||
.first()
|
||||
.text()
|
||||
).toBe(data.description);
|
||||
|
||||
expect(
|
||||
wrapper.find('button[data-test-subj="case-view-status-action-button"]').first().text()
|
||||
).toBe('Mark in progress');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -464,7 +469,7 @@ describe('CaseView ', () => {
|
|||
);
|
||||
await waitFor(() => {
|
||||
wrapper.find('[data-test-subj="case-refresh"]').first().simulate('click');
|
||||
expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id);
|
||||
expect(fetchCaseUserActions).toBeCalledWith('1234', undefined);
|
||||
expect(fetchCase).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
@ -547,8 +552,7 @@ describe('CaseView ', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// TO DO fix when the useEffects in edit_connector are cleaned up
|
||||
it.skip('should update connector', async () => {
|
||||
it('should update connector', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
|
@ -752,4 +756,74 @@ describe('CaseView ', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Collections', () => {
|
||||
it('it does not allow the user to update the status', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<CaseComponent
|
||||
{...caseProps}
|
||||
caseData={{ ...caseProps.caseData, type: CaseType.collection }}
|
||||
/>
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="case-action-bar-wrapper"]').exists()).toBe(true);
|
||||
expect(wrapper.find('button[data-test-subj="case-view-status"]').exists()).toBe(false);
|
||||
expect(wrapper.find('[data-test-subj="user-actions"]').exists()).toBe(true);
|
||||
expect(
|
||||
wrapper.find('button[data-test-subj="case-view-status-action-button"]').exists()
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('it shows the push button when has data to push', async () => {
|
||||
useGetCaseUserActionsMock.mockImplementation(() => ({
|
||||
...defaultUseGetCaseUserActions,
|
||||
hasDataToPush: true,
|
||||
}));
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<CaseComponent
|
||||
{...caseProps}
|
||||
caseData={{ ...caseProps.caseData, type: CaseType.collection }}
|
||||
/>
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="has-data-to-push-button"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('it does not show the horizontal rule when does NOT has data to push', async () => {
|
||||
useGetCaseUserActionsMock.mockImplementation(() => ({
|
||||
...defaultUseGetCaseUserActions,
|
||||
hasDataToPush: false,
|
||||
}));
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<CaseComponent
|
||||
{...caseProps}
|
||||
caseData={{ ...caseProps.caseData, type: CaseType.collection }}
|
||||
/>
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="case-view-bottom-actions-horizontal-rule"]').exists()
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
EuiHorizontalRule,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CaseStatuses, CaseAttributes } from '../../../../../case/common/api';
|
||||
import { CaseStatuses, CaseAttributes, CaseType } from '../../../../../case/common/api';
|
||||
import { Case, CaseConnector } from '../../containers/types';
|
||||
import { getCaseDetailsUrl, getCaseUrl, useFormatUrl } from '../../../common/components/link_to';
|
||||
import { gutterTimeline } from '../../../common/lib/helpers';
|
||||
|
@ -213,9 +213,9 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
const handleUpdateCase = useCallback(
|
||||
(newCase: Case) => {
|
||||
updateCase(newCase);
|
||||
fetchCaseUserActions(newCase.id);
|
||||
fetchCaseUserActions(caseId, subCaseId);
|
||||
},
|
||||
[updateCase, fetchCaseUserActions]
|
||||
[updateCase, fetchCaseUserActions, caseId, subCaseId]
|
||||
);
|
||||
|
||||
const { loading: isLoadingConnectors, connectors } = useConnectors();
|
||||
|
@ -283,9 +283,9 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
fetchCaseUserActions(caseData.id);
|
||||
fetchCaseUserActions(caseId, subCaseId);
|
||||
fetchCase();
|
||||
}, [caseData.id, fetchCase, fetchCaseUserActions]);
|
||||
}, [caseId, fetchCase, fetchCaseUserActions, subCaseId]);
|
||||
|
||||
const spyState = useMemo(() => ({ caseTitle: caseData.title }), [caseData.title]);
|
||||
|
||||
|
@ -345,6 +345,7 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
);
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderWrapper>
|
||||
|
@ -387,7 +388,7 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
caseUserActions={caseUserActions}
|
||||
connectors={connectors}
|
||||
data={caseData}
|
||||
fetchUserActions={fetchCaseUserActions.bind(null, caseData.id)}
|
||||
fetchUserActions={fetchCaseUserActions.bind(null, caseId, subCaseId)}
|
||||
isLoadingDescription={isLoading && updateKey === 'description'}
|
||||
isLoadingUserActions={isLoadingUserActions}
|
||||
onShowAlertDetails={showAlert}
|
||||
|
@ -395,22 +396,29 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
updateCase={updateCase}
|
||||
userCanCrud={userCanCrud}
|
||||
/>
|
||||
<MyEuiHorizontalRule margin="s" />
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<StatusActionButton
|
||||
status={caseData.status}
|
||||
onStatusChanged={changeStatus}
|
||||
disabled={!userCanCrud}
|
||||
isLoading={isLoading && updateKey === 'status'}
|
||||
{(caseData.type !== CaseType.collection || hasDataToPush) && (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" justifyContent="flexEnd">
|
||||
<MyEuiHorizontalRule
|
||||
margin="s"
|
||||
data-test-subj="case-view-bottom-actions-horizontal-rule"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{hasDataToPush && (
|
||||
<EuiFlexItem data-test-subj="has-data-to-push-button" grow={false}>
|
||||
{pushButton}
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
{caseData.type !== CaseType.collection && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<StatusActionButton
|
||||
status={caseData.status}
|
||||
onStatusChanged={changeStatus}
|
||||
disabled={!userCanCrud}
|
||||
isLoading={isLoading && updateKey === 'status'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{hasDataToPush && (
|
||||
<EuiFlexItem data-test-subj="has-data-to-push-button" grow={false}>
|
||||
{pushButton}
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
|
@ -465,6 +473,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
|
|||
if (isError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<MyEuiFlexGroup gutterSize="none" justifyContent="center" alignItems="center">
|
||||
|
@ -476,14 +485,16 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
|
|||
}
|
||||
|
||||
return (
|
||||
<CaseComponent
|
||||
caseId={caseId}
|
||||
subCaseId={subCaseId}
|
||||
fetchCase={fetchCase}
|
||||
caseData={data}
|
||||
updateCase={updateCase}
|
||||
userCanCrud={userCanCrud}
|
||||
/>
|
||||
data && (
|
||||
<CaseComponent
|
||||
caseId={caseId}
|
||||
subCaseId={subCaseId}
|
||||
fetchCase={fetchCase}
|
||||
caseData={data}
|
||||
updateCase={updateCase}
|
||||
userCanCrud={userCanCrud}
|
||||
/>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -47,7 +47,9 @@ const CaseParamsFields: React.FunctionComponent<ActionParamsProps<CaseActionPara
|
|||
const newProps = { ...actionParams.subActionParams, [key]: value };
|
||||
editAction('subActionParams', newProps, index);
|
||||
},
|
||||
[actionParams.subActionParams, editAction, index]
|
||||
// edit action causes re-renders
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[actionParams.subActionParams, index]
|
||||
);
|
||||
|
||||
const onCaseChanged = useCallback(
|
||||
|
|
|
@ -57,14 +57,18 @@ const ExistingCaseComponent: React.FC<ExistingCaseProps> = ({ onCaseChanged, sel
|
|||
onCaseChanged('');
|
||||
dispatchResetIsDeleted();
|
||||
}
|
||||
}, [isDeleted, dispatchResetIsDeleted, onCaseChanged]);
|
||||
// onCaseChanged and/or dispatchResetIsDeleted causes re-renders
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isDeleted]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isError && data != null) {
|
||||
setCreatedCase(data);
|
||||
onCaseChanged(data.id);
|
||||
}
|
||||
}, [data, isLoading, isError, onCaseChanged]);
|
||||
// onCaseChanged causes re-renders
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data, isLoading, isError]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -20,14 +20,16 @@ jest.mock('../create/form_context', () => {
|
|||
onSuccess,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
onSuccess: ({ id }: { id: string }) => void;
|
||||
onSuccess: ({ id }: { id: string }) => Promise<void>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="form-context-on-success"
|
||||
onClick={() => onSuccess({ id: 'case-id' })}
|
||||
onClick={async () => {
|
||||
await onSuccess({ id: 'case-id' });
|
||||
}}
|
||||
>
|
||||
{'submit'}
|
||||
</button>
|
||||
|
@ -55,10 +57,10 @@ jest.mock('../create/submit_button', () => {
|
|||
});
|
||||
|
||||
const onCloseFlyout = jest.fn();
|
||||
const onCaseCreated = jest.fn();
|
||||
const onSuccess = jest.fn();
|
||||
const defaultProps = {
|
||||
onCloseFlyout,
|
||||
onCaseCreated,
|
||||
onSuccess,
|
||||
};
|
||||
|
||||
describe('CreateCaseFlyout', () => {
|
||||
|
@ -97,7 +99,7 @@ describe('CreateCaseFlyout', () => {
|
|||
const props = wrapper.find('FormContext').props();
|
||||
expect(props).toEqual(
|
||||
expect.objectContaining({
|
||||
onSuccess: onCaseCreated,
|
||||
onSuccess,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -110,6 +112,6 @@ describe('CreateCaseFlyout', () => {
|
|||
);
|
||||
|
||||
wrapper.find(`[data-test-subj='form-context-on-success']`).first().simulate('click');
|
||||
expect(onCaseCreated).toHaveBeenCalledWith({ id: 'case-id' });
|
||||
expect(onSuccess).toHaveBeenCalledWith({ id: 'case-id' });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,7 +17,8 @@ import * as i18n from '../../translations';
|
|||
|
||||
export interface CreateCaseModalProps {
|
||||
onCloseFlyout: () => void;
|
||||
onCaseCreated: (theCase: Case) => void;
|
||||
onSuccess: (theCase: Case) => Promise<void>;
|
||||
afterCaseCreated?: (theCase: Case) => Promise<void>;
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
|
@ -40,7 +41,8 @@ const FormWrapper = styled.div`
|
|||
`;
|
||||
|
||||
const CreateCaseFlyoutComponent: React.FC<CreateCaseModalProps> = ({
|
||||
onCaseCreated,
|
||||
onSuccess,
|
||||
afterCaseCreated,
|
||||
onCloseFlyout,
|
||||
}) => {
|
||||
return (
|
||||
|
@ -52,7 +54,7 @@ const CreateCaseFlyoutComponent: React.FC<CreateCaseModalProps> = ({
|
|||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<FormWrapper>
|
||||
<FormContext onSuccess={onCaseCreated}>
|
||||
<FormContext onSuccess={onSuccess} afterCaseCreated={afterCaseCreated}>
|
||||
<CreateCaseForm withSteps={false} />
|
||||
<Container>
|
||||
<SubmitCaseButton />
|
||||
|
|
|
@ -98,6 +98,7 @@ const fillForm = (wrapper: ReactWrapper) => {
|
|||
describe('Create case', () => {
|
||||
const fetchTags = jest.fn();
|
||||
const onFormSubmitSuccess = jest.fn();
|
||||
const afterCaseCreated = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
@ -593,4 +594,89 @@ describe('Create case', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(`it should call afterCaseCreated`, async () => {
|
||||
useConnectorsMock.mockReturnValue({
|
||||
...sampleConnectorData,
|
||||
connectors: connectorsMock,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess} afterCaseCreated={afterCaseCreated}>
|
||||
<CreateCaseForm />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fillForm(wrapper);
|
||||
expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeFalsy();
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(afterCaseCreated).toHaveBeenCalledWith({
|
||||
id: sampleId,
|
||||
...sampleData,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(`it should call callbacks in correct order`, async () => {
|
||||
useConnectorsMock.mockReturnValue({
|
||||
...sampleConnectorData,
|
||||
connectors: connectorsMock,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess} afterCaseCreated={afterCaseCreated}>
|
||||
<CreateCaseForm />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fillForm(wrapper);
|
||||
expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeFalsy();
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(postCase).toHaveBeenCalled();
|
||||
expect(afterCaseCreated).toHaveBeenCalled();
|
||||
expect(pushCaseToExternalService).toHaveBeenCalled();
|
||||
expect(onFormSubmitSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const postCaseOrder = postCase.mock.invocationCallOrder[0];
|
||||
const afterCaseOrder = afterCaseCreated.mock.invocationCallOrder[0];
|
||||
const pushCaseToExternalServiceOrder = pushCaseToExternalService.mock.invocationCallOrder[0];
|
||||
const onFormSubmitSuccessOrder = onFormSubmitSuccess.mock.invocationCallOrder[0];
|
||||
|
||||
expect(
|
||||
postCaseOrder < afterCaseOrder &&
|
||||
postCaseOrder < pushCaseToExternalServiceOrder &&
|
||||
postCaseOrder < onFormSubmitSuccessOrder
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
afterCaseOrder < pushCaseToExternalServiceOrder && afterCaseOrder < onFormSubmitSuccessOrder
|
||||
).toBe(true);
|
||||
|
||||
expect(pushCaseToExternalServiceOrder < onFormSubmitSuccessOrder).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,13 +32,15 @@ const initialCaseValue: FormProps = {
|
|||
|
||||
interface Props {
|
||||
caseType?: CaseType;
|
||||
onSuccess?: (theCase: Case) => void;
|
||||
onSuccess?: (theCase: Case) => Promise<void>;
|
||||
afterCaseCreated?: (theCase: Case) => Promise<void>;
|
||||
}
|
||||
|
||||
export const FormContext: React.FC<Props> = ({
|
||||
caseType = CaseType.individual,
|
||||
children,
|
||||
onSuccess,
|
||||
afterCaseCreated,
|
||||
}) => {
|
||||
const { connectors } = useConnectors();
|
||||
const { connector: configurationConnector } = useCaseConfigure();
|
||||
|
@ -72,6 +74,10 @@ export const FormContext: React.FC<Props> = ({
|
|||
settings: { syncAlerts },
|
||||
});
|
||||
|
||||
if (afterCaseCreated && updatedCase) {
|
||||
await afterCaseCreated(updatedCase);
|
||||
}
|
||||
|
||||
if (updatedCase?.id && dataConnectorId !== 'none') {
|
||||
await pushCaseToExternalService({
|
||||
caseId: updatedCase.id,
|
||||
|
@ -80,11 +86,11 @@ export const FormContext: React.FC<Props> = ({
|
|||
}
|
||||
|
||||
if (onSuccess && updatedCase) {
|
||||
onSuccess(updatedCase);
|
||||
await onSuccess(updatedCase);
|
||||
}
|
||||
}
|
||||
},
|
||||
[caseType, connectors, postCase, onSuccess, pushCaseToExternalService]
|
||||
[caseType, connectors, postCase, onSuccess, pushCaseToExternalService, afterCaseCreated]
|
||||
);
|
||||
|
||||
const { form } = useForm<FormProps>({
|
||||
|
|
|
@ -41,7 +41,7 @@ const InsertTimeline = () => {
|
|||
export const Create = React.memo(() => {
|
||||
const history = useHistory();
|
||||
const onSuccess = useCallback(
|
||||
({ id }) => {
|
||||
async ({ id }) => {
|
||||
history.push(getCaseDetailsUrl({ id }));
|
||||
},
|
||||
[history]
|
||||
|
|
|
@ -42,7 +42,7 @@ describe('StatusActionButton', () => {
|
|||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="case-view-status-action-button"]`).first().prop('iconType')
|
||||
).toBe('folderClosed');
|
||||
).toBe('folderCheck');
|
||||
});
|
||||
|
||||
it('it renders the correct button icon: status closed', () => {
|
||||
|
|
|
@ -83,7 +83,7 @@ export const statuses: Statuses = {
|
|||
[CaseStatuses.closed]: {
|
||||
color: 'default',
|
||||
label: i18n.CLOSED,
|
||||
icon: 'folderClosed' as const,
|
||||
icon: 'folderCheck' as const,
|
||||
actions: {
|
||||
bulk: {
|
||||
title: i18n.BULK_ACTION_CLOSE_SELECTED,
|
||||
|
|
|
@ -30,12 +30,18 @@ jest.mock('../../../common/components/toasters', () => {
|
|||
|
||||
jest.mock('../all_cases', () => {
|
||||
return {
|
||||
AllCases: ({ onRowClick }: { onRowClick: ({ id }: { id: string }) => void }) => {
|
||||
AllCases: ({ onRowClick }: { onRowClick: (theCase: Partial<Case>) => void }) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="all-cases-modal-button"
|
||||
onClick={() => onRowClick({ id: 'selected-case' })}
|
||||
onClick={() =>
|
||||
onRowClick({
|
||||
id: 'selected-case',
|
||||
title: 'the selected case',
|
||||
settings: { syncAlerts: true },
|
||||
})
|
||||
}
|
||||
>
|
||||
{'case-row'}
|
||||
</button>
|
||||
|
@ -49,18 +55,25 @@ jest.mock('../create/form_context', () => {
|
|||
FormContext: ({
|
||||
children,
|
||||
onSuccess,
|
||||
afterCaseCreated,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
onSuccess: (theCase: Partial<Case>) => void;
|
||||
onSuccess: (theCase: Partial<Case>) => Promise<void>;
|
||||
afterCaseCreated: (theCase: Partial<Case>) => Promise<void>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="form-context-on-success"
|
||||
onClick={() =>
|
||||
onSuccess({ id: 'new-case', title: 'the new case', settings: { syncAlerts: true } })
|
||||
}
|
||||
onClick={() => {
|
||||
afterCaseCreated({
|
||||
id: 'new-case',
|
||||
title: 'the new case',
|
||||
settings: { syncAlerts: true },
|
||||
});
|
||||
onSuccess({ id: 'new-case', title: 'the new case', settings: { syncAlerts: true } });
|
||||
}}
|
||||
>
|
||||
{'submit'}
|
||||
</button>
|
||||
|
@ -212,14 +225,7 @@ describe('AddToCaseAction', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('navigates to case view', async () => {
|
||||
usePostCommentMock.mockImplementation(() => {
|
||||
return {
|
||||
...defaultPostComment,
|
||||
postComment: jest.fn().mockImplementation(({ caseId, data, updateCase }) => updateCase()),
|
||||
};
|
||||
});
|
||||
|
||||
it('navigates to case view when attach to a new case', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AddToCaseAction {...props} />
|
||||
|
@ -244,4 +250,45 @@ describe('AddToCaseAction', () => {
|
|||
|
||||
expect(mockNavigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/new-case' });
|
||||
});
|
||||
|
||||
it('navigates to case view when attach to an existing case', async () => {
|
||||
usePostCommentMock.mockImplementation(() => {
|
||||
return {
|
||||
...defaultPostComment,
|
||||
postComment: jest.fn().mockImplementation(({ caseId, data, updateCase }) => {
|
||||
updateCase({
|
||||
id: 'selected-case',
|
||||
title: 'the selected case',
|
||||
settings: { syncAlerts: true },
|
||||
});
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AddToCaseAction {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click');
|
||||
wrapper.find(`[data-test-subj="add-existing-case-menu-item"]`).first().simulate('click');
|
||||
wrapper.find(`[data-test-subj="all-cases-modal-button"]`).first().simulate('click');
|
||||
|
||||
expect(mockDispatchToaster).toHaveBeenCalled();
|
||||
const toast = mockDispatchToaster.mock.calls[0][0].toast;
|
||||
|
||||
const toastWrapper = mount(
|
||||
<EuiGlobalToastList toasts={[toast]} toastLifeTimeMs={6000} dismissToast={() => {}} />
|
||||
);
|
||||
|
||||
toastWrapper
|
||||
.find('[data-test-subj="toaster-content-case-view-link"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
expect(mockNavigateToApp).toHaveBeenCalledWith('securitySolution:case', {
|
||||
path: '/selected-case',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CommentType } from '../../../../../case/common/api';
|
||||
import { CommentType, CaseStatuses } from '../../../../../case/common/api';
|
||||
import { Ecs } from '../../../../common/ecs';
|
||||
import { ActionIconItem } from '../../../timelines/components/timeline/body/actions/action_icon_item';
|
||||
import { usePostComment } from '../../containers/use_post_comment';
|
||||
|
@ -70,9 +70,9 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
|
|||
} = useControl();
|
||||
|
||||
const attachAlertToCase = useCallback(
|
||||
(theCase: Case) => {
|
||||
async (theCase: Case, updateCase?: (newCase: Case) => void) => {
|
||||
closeCaseFlyoutOpen();
|
||||
postComment({
|
||||
await postComment({
|
||||
caseId: theCase.id,
|
||||
data: {
|
||||
type: CommentType.alert,
|
||||
|
@ -83,14 +83,19 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
|
|||
name: rule?.name != null ? rule.name[0] : null,
|
||||
},
|
||||
},
|
||||
updateCase: () =>
|
||||
dispatchToaster({
|
||||
type: 'addToaster',
|
||||
toast: createUpdateSuccessToaster(theCase, onViewCaseClick),
|
||||
}),
|
||||
updateCase,
|
||||
});
|
||||
},
|
||||
[closeCaseFlyoutOpen, postComment, eventId, eventIndex, rule, dispatchToaster, onViewCaseClick]
|
||||
[closeCaseFlyoutOpen, postComment, eventId, eventIndex, rule]
|
||||
);
|
||||
|
||||
const onCaseSuccess = useCallback(
|
||||
async (theCase: Case) =>
|
||||
dispatchToaster({
|
||||
type: 'addToaster',
|
||||
toast: createUpdateSuccessToaster(theCase, onViewCaseClick),
|
||||
}),
|
||||
[dispatchToaster, onViewCaseClick]
|
||||
);
|
||||
|
||||
const onCaseClicked = useCallback(
|
||||
|
@ -105,12 +110,13 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
attachAlertToCase(theCase);
|
||||
attachAlertToCase(theCase, onCaseSuccess);
|
||||
},
|
||||
[attachAlertToCase, openCaseFlyoutOpen]
|
||||
[attachAlertToCase, onCaseSuccess, openCaseFlyoutOpen]
|
||||
);
|
||||
|
||||
const { modal: allCasesModal, openModal: openAllCaseModal } = useAllCasesModal({
|
||||
disabledStatuses: [CaseStatuses.closed],
|
||||
onRowClick: onCaseClicked,
|
||||
});
|
||||
|
||||
|
@ -183,7 +189,11 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
|
|||
</EuiPopover>
|
||||
</ActionIconItem>
|
||||
{isCreateCaseFlyoutOpen && (
|
||||
<CreateCaseFlyout onCloseFlyout={closeCaseFlyoutOpen} onCaseCreated={attachAlertToCase} />
|
||||
<CreateCaseFlyout
|
||||
onCloseFlyout={closeCaseFlyoutOpen}
|
||||
afterCaseCreated={attachAlertToCase}
|
||||
onSuccess={onCaseSuccess}
|
||||
/>
|
||||
)}
|
||||
{allCasesModal}
|
||||
</>
|
||||
|
|
|
@ -6,36 +6,52 @@
|
|||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui';
|
||||
|
||||
import { useGetUserSavedObjectPermissions } from '../../../common/lib/kibana';
|
||||
import { Case } from '../../containers/types';
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import { Case, SubCase } from '../../containers/types';
|
||||
import { AllCases } from '../all_cases';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface AllCasesModalProps {
|
||||
isModalOpen: boolean;
|
||||
onCloseCaseModal: () => void;
|
||||
onRowClick: (theCase?: Case) => void;
|
||||
onRowClick: (theCase?: Case | SubCase) => void;
|
||||
disabledStatuses?: CaseStatuses[];
|
||||
}
|
||||
|
||||
const Modal = styled(EuiModal)`
|
||||
${({ theme }) => `
|
||||
width: ${theme.eui.euiBreakpoints.l};
|
||||
max-width: ${theme.eui.euiBreakpoints.l};
|
||||
`}
|
||||
`;
|
||||
|
||||
const AllCasesModalComponent: React.FC<AllCasesModalProps> = ({
|
||||
isModalOpen,
|
||||
onCloseCaseModal,
|
||||
onRowClick,
|
||||
disabledStatuses,
|
||||
}) => {
|
||||
const userPermissions = useGetUserSavedObjectPermissions();
|
||||
const userCanCrud = userPermissions?.crud ?? false;
|
||||
|
||||
return isModalOpen ? (
|
||||
<EuiModal onClose={onCloseCaseModal} data-test-subj="all-cases-modal">
|
||||
<Modal onClose={onCloseCaseModal} data-test-subj="all-cases-modal">
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{i18n.SELECT_CASE_TITLE}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<AllCases onRowClick={onRowClick} userCanCrud={userCanCrud} isModal />
|
||||
<AllCases
|
||||
onRowClick={onRowClick}
|
||||
userCanCrud={userCanCrud}
|
||||
isModal
|
||||
disabledStatuses={disabledStatuses}
|
||||
/>
|
||||
</EuiModalBody>
|
||||
</EuiModal>
|
||||
</Modal>
|
||||
) : null;
|
||||
};
|
||||
|
||||
|
|
|
@ -123,7 +123,7 @@ describe('useAllCasesModal', () => {
|
|||
});
|
||||
|
||||
const modal = result.current.modal;
|
||||
render(<>{modal}</>);
|
||||
render(<TestProviders>{modal}</TestProviders>);
|
||||
|
||||
act(() => {
|
||||
userEvent.click(screen.getByText('case-row'));
|
||||
|
|
|
@ -6,11 +6,13 @@
|
|||
*/
|
||||
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { Case } from '../../containers/types';
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import { Case, SubCase } from '../../containers/types';
|
||||
import { AllCasesModal } from './all_cases_modal';
|
||||
|
||||
export interface UseAllCasesModalProps {
|
||||
onRowClick: (theCase?: Case) => void;
|
||||
onRowClick: (theCase?: Case | SubCase) => void;
|
||||
disabledStatuses?: CaseStatuses[];
|
||||
}
|
||||
|
||||
export interface UseAllCasesModalReturnedValues {
|
||||
|
@ -22,12 +24,13 @@ export interface UseAllCasesModalReturnedValues {
|
|||
|
||||
export const useAllCasesModal = ({
|
||||
onRowClick,
|
||||
disabledStatuses,
|
||||
}: UseAllCasesModalProps): UseAllCasesModalReturnedValues => {
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
const closeModal = useCallback(() => setIsModalOpen(false), []);
|
||||
const openModal = useCallback(() => setIsModalOpen(true), []);
|
||||
const onClick = useCallback(
|
||||
(theCase?: Case) => {
|
||||
(theCase?: Case | SubCase) => {
|
||||
closeModal();
|
||||
onRowClick(theCase);
|
||||
},
|
||||
|
@ -41,6 +44,7 @@ export const useAllCasesModal = ({
|
|||
isModalOpen={isModalOpen}
|
||||
onCloseCaseModal={closeModal}
|
||||
onRowClick={onClick}
|
||||
disabledStatuses={disabledStatuses}
|
||||
/>
|
||||
),
|
||||
isModalOpen,
|
||||
|
@ -48,7 +52,7 @@ export const useAllCasesModal = ({
|
|||
openModal,
|
||||
onRowClick,
|
||||
}),
|
||||
[isModalOpen, closeModal, onClick, openModal, onRowClick]
|
||||
[isModalOpen, closeModal, onClick, disabledStatuses, openModal, onRowClick]
|
||||
);
|
||||
|
||||
return state;
|
||||
|
|
|
@ -20,14 +20,16 @@ jest.mock('../create/form_context', () => {
|
|||
onSuccess,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
onSuccess: ({ id }: { id: string }) => void;
|
||||
onSuccess: ({ id }: { id: string }) => Promise<void>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="form-context-on-success"
|
||||
onClick={() => onSuccess({ id: 'case-id' })}
|
||||
onClick={async () => {
|
||||
await onSuccess({ id: 'case-id' });
|
||||
}}
|
||||
>
|
||||
{'submit'}
|
||||
</button>
|
||||
|
|
|
@ -19,7 +19,7 @@ import { CaseType } from '../../../../../case/common/api';
|
|||
export interface CreateCaseModalProps {
|
||||
isModalOpen: boolean;
|
||||
onCloseCaseModal: () => void;
|
||||
onSuccess: (theCase: Case) => void;
|
||||
onSuccess: (theCase: Case) => Promise<void>;
|
||||
caseType?: CaseType;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,14 +25,16 @@ jest.mock('../create/form_context', () => {
|
|||
onSuccess,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
onSuccess: ({ id }: { id: string }) => void;
|
||||
onSuccess: ({ id }: { id: string }) => Promise<void>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="form-context-on-success"
|
||||
onClick={() => onSuccess({ id: 'case-id' })}
|
||||
onClick={async () => {
|
||||
await onSuccess({ id: 'case-id' });
|
||||
}}
|
||||
>
|
||||
{'Form submit'}
|
||||
</button>
|
||||
|
|
|
@ -29,7 +29,7 @@ export const useCreateCaseModal = ({
|
|||
const closeModal = useCallback(() => setIsModalOpen(false), []);
|
||||
const openModal = useCallback(() => setIsModalOpen(true), []);
|
||||
const onSuccess = useCallback(
|
||||
(theCase) => {
|
||||
async (theCase) => {
|
||||
onCaseCreated(theCase);
|
||||
closeModal();
|
||||
},
|
||||
|
|
|
@ -55,6 +55,9 @@ describe('UserActionTree ', () => {
|
|||
useFormMock.mockImplementation(() => ({ form: formHookMock }));
|
||||
useFormDataMock.mockImplementation(() => [{ content: sampleData.content, comment: '' }]);
|
||||
jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation);
|
||||
jest
|
||||
.spyOn(routeData, 'useParams')
|
||||
.mockReturnValue({ detailName: 'case-id', subCaseId: 'sub-case-id' });
|
||||
});
|
||||
|
||||
it('Loading spinner when user actions loading and displays fullName/username', () => {
|
||||
|
@ -289,7 +292,8 @@ describe('UserActionTree ', () => {
|
|||
).toEqual(false);
|
||||
expect(patchComment).toBeCalledWith({
|
||||
commentUpdate: sampleData.content,
|
||||
caseId: props.data.id,
|
||||
caseId: 'case-id',
|
||||
subCaseId: 'sub-case-id',
|
||||
commentId: props.data.comments[0].id,
|
||||
fetchUserActions,
|
||||
updateCase,
|
||||
|
|
|
@ -122,7 +122,11 @@ export const UserActionTree = React.memo(
|
|||
userCanCrud,
|
||||
onShowAlertDetails,
|
||||
}: UserActionTreeProps) => {
|
||||
const { commentId, subCaseId } = useParams<{ commentId?: string; subCaseId?: string }>();
|
||||
const { detailName: caseId, commentId, subCaseId } = useParams<{
|
||||
detailName: string;
|
||||
commentId?: string;
|
||||
subCaseId?: string;
|
||||
}>();
|
||||
const handlerTimeoutId = useRef(0);
|
||||
const addCommentRef = useRef<AddCommentRefObject>(null);
|
||||
const [initLoading, setInitLoading] = useState(true);
|
||||
|
@ -149,15 +153,16 @@ export const UserActionTree = React.memo(
|
|||
const handleSaveComment = useCallback(
|
||||
({ id, version }: { id: string; version: string }, content: string) => {
|
||||
patchComment({
|
||||
caseId: caseData.id,
|
||||
caseId,
|
||||
commentId: id,
|
||||
commentUpdate: content,
|
||||
fetchUserActions,
|
||||
version,
|
||||
updateCase,
|
||||
subCaseId,
|
||||
});
|
||||
},
|
||||
[caseData.id, fetchUserActions, patchComment, updateCase]
|
||||
[caseId, fetchUserActions, patchComment, subCaseId, updateCase]
|
||||
);
|
||||
|
||||
const handleOutlineComment = useCallback(
|
||||
|
@ -223,7 +228,7 @@ export const UserActionTree = React.memo(
|
|||
const MarkdownNewComment = useMemo(
|
||||
() => (
|
||||
<AddComment
|
||||
caseId={caseData.id}
|
||||
caseId={caseId}
|
||||
disabled={!userCanCrud}
|
||||
ref={addCommentRef}
|
||||
onCommentPosted={handleUpdate}
|
||||
|
@ -232,7 +237,7 @@ export const UserActionTree = React.memo(
|
|||
subCaseId={subCaseId}
|
||||
/>
|
||||
),
|
||||
[caseData.id, handleUpdate, userCanCrud, handleManageMarkdownEditId, subCaseId]
|
||||
[caseId, userCanCrud, handleUpdate, handleManageMarkdownEditId, subCaseId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { initialData, useGetCase, UseGetCase } from './use_get_case';
|
||||
import { useGetCase, UseGetCase } from './use_get_case';
|
||||
import { basicCase } from './mock';
|
||||
import * as api from './api';
|
||||
|
||||
|
@ -26,8 +26,8 @@ describe('useGetCase', () => {
|
|||
);
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
data: initialData,
|
||||
isLoading: true,
|
||||
data: null,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
fetchCase: result.current.fetchCase,
|
||||
updateCase: result.current.updateCase,
|
||||
|
@ -102,7 +102,7 @@ describe('useGetCase', () => {
|
|||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual({
|
||||
data: initialData,
|
||||
data: null,
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
fetchCase: result.current.fetchCase,
|
||||
|
|
|
@ -6,16 +6,14 @@
|
|||
*/
|
||||
|
||||
import { useEffect, useReducer, useCallback, useRef } from 'react';
|
||||
import { CaseStatuses, CaseType } from '../../../../case/common/api';
|
||||
|
||||
import { Case } from './types';
|
||||
import * as i18n from './translations';
|
||||
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
|
||||
import { getCase, getSubCase } from './api';
|
||||
import { getNoneConnector } from '../components/configure_cases/utils';
|
||||
|
||||
interface CaseState {
|
||||
data: Case;
|
||||
data: Case | null;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
}
|
||||
|
@ -56,32 +54,6 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => {
|
|||
return state;
|
||||
}
|
||||
};
|
||||
export const initialData: Case = {
|
||||
id: '',
|
||||
closedAt: null,
|
||||
closedBy: null,
|
||||
createdAt: '',
|
||||
comments: [],
|
||||
connector: { ...getNoneConnector(), fields: null },
|
||||
createdBy: {
|
||||
username: '',
|
||||
},
|
||||
description: '',
|
||||
externalService: null,
|
||||
status: CaseStatuses.open,
|
||||
tags: [],
|
||||
title: '',
|
||||
totalAlerts: 0,
|
||||
totalComment: 0,
|
||||
type: CaseType.individual,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
version: '',
|
||||
subCaseIds: [],
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
};
|
||||
|
||||
export interface UseGetCase extends CaseState {
|
||||
fetchCase: () => void;
|
||||
|
@ -90,9 +62,9 @@ export interface UseGetCase extends CaseState {
|
|||
|
||||
export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => {
|
||||
const [state, dispatch] = useReducer(dataFetchReducer, {
|
||||
isLoading: true,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: initialData,
|
||||
data: null,
|
||||
});
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
const isCancelledRef = useRef(false);
|
||||
|
|
|
@ -48,7 +48,7 @@ interface PostComment {
|
|||
subCaseId?: string;
|
||||
}
|
||||
export interface UsePostComment extends NewCommentState {
|
||||
postComment: (args: PostComment) => void;
|
||||
postComment: (args: PostComment) => Promise<void>;
|
||||
}
|
||||
|
||||
export const usePostComment = (): UsePostComment => {
|
||||
|
|
|
@ -294,3 +294,10 @@ export const ALERT_ADDED_TO_CASE = i18n.translate(
|
|||
defaultMessage: 'added to case',
|
||||
}
|
||||
);
|
||||
|
||||
export const SELECTABLE_MESSAGE_COLLECTIONS = i18n.translate(
|
||||
'xpack.securitySolution.common.allCases.table.selectableMessageCollections',
|
||||
{
|
||||
defaultMessage: 'Cases with sub-cases cannot be selected',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -25,7 +25,7 @@ describe('useQueryAlerts', () => {
|
|||
>(() => useQueryAlerts<unknown, unknown>(mockAlertsQuery, indexName));
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
loading: true,
|
||||
loading: false,
|
||||
data: null,
|
||||
response: '',
|
||||
request: '',
|
||||
|
|
|
@ -42,7 +42,7 @@ export const useQueryAlerts = <Hit, Aggs>(
|
|||
setQuery,
|
||||
refetch: null,
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
|
|
|
@ -20,7 +20,7 @@ import { TimelineStatus, TimelineId, TimelineType } from '../../../../../common/
|
|||
import { getCreateCaseUrl, getCaseDetailsUrl } from '../../../../common/components/link_to';
|
||||
import { SecurityPageName } from '../../../../app/types';
|
||||
import { timelineDefaults } from '../../../../timelines/store/timeline/defaults';
|
||||
import { Case } from '../../../../cases/containers/types';
|
||||
import { Case, SubCase } from '../../../../cases/containers/types';
|
||||
import * as i18n from '../../timeline/properties/translations';
|
||||
|
||||
interface Props {
|
||||
|
@ -46,7 +46,7 @@ const AddToCaseButtonComponent: React.FC<Props> = ({ timelineId }) => {
|
|||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
|
||||
const onRowClick = useCallback(
|
||||
async (theCase?: Case) => {
|
||||
async (theCase?: Case | SubCase) => {
|
||||
await navigateToApp(`${APP_ID}:${SecurityPageName.case}`, {
|
||||
path: theCase != null ? getCaseDetailsUrl({ id: theCase.id }) : getCreateCaseUrl(),
|
||||
});
|
||||
|
|
|
@ -101,15 +101,15 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID });
|
||||
await supertest
|
||||
.delete(
|
||||
`${CASES_URL}/${caseInfo.id}/comments/${caseInfo.subCase!.comments![0].id}?subCaseID=${
|
||||
caseInfo.subCase!.id
|
||||
`${CASES_URL}/${caseInfo.id}/comments/${caseInfo.comments![0].id}?subCaseId=${
|
||||
caseInfo.subCases![0].id
|
||||
}`
|
||||
)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(204);
|
||||
const { body } = await supertest.get(
|
||||
`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`
|
||||
`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`
|
||||
);
|
||||
expect(body.length).to.eql(0);
|
||||
});
|
||||
|
@ -117,24 +117,24 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
it('deletes all comments from a sub case', async () => {
|
||||
const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID });
|
||||
await supertest
|
||||
.post(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`)
|
||||
.post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(postCommentUserReq)
|
||||
.expect(200);
|
||||
|
||||
let { body: allComments } = await supertest.get(
|
||||
`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`
|
||||
`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`
|
||||
);
|
||||
expect(allComments.length).to.eql(2);
|
||||
|
||||
await supertest
|
||||
.delete(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`)
|
||||
.delete(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(204);
|
||||
|
||||
({ body: allComments } = await supertest.get(
|
||||
`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`
|
||||
`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`
|
||||
));
|
||||
|
||||
// no comments for the sub case
|
||||
|
|
|
@ -126,13 +126,13 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
it('finds comments for a sub case', async () => {
|
||||
const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID });
|
||||
await supertest
|
||||
.post(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`)
|
||||
.post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(postCommentUserReq)
|
||||
.expect(200);
|
||||
|
||||
const { body: subCaseComments }: { body: CommentsResponse } = await supertest
|
||||
.get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseID=${caseInfo.subCase!.id}`)
|
||||
.get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseId=${caseInfo.subCases![0].id}`)
|
||||
.send()
|
||||
.expect(200);
|
||||
expect(subCaseComments.total).to.be(2);
|
||||
|
|
|
@ -83,13 +83,13 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
it('should get comments from a sub cases', async () => {
|
||||
const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID });
|
||||
await supertest
|
||||
.post(`${CASES_URL}/${caseInfo.subCase!.id}/comments`)
|
||||
.post(`${CASES_URL}/${caseInfo.subCases![0].id}/comments`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(postCommentUserReq)
|
||||
.expect(200);
|
||||
|
||||
const { body: comments } = await supertest
|
||||
.get(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`)
|
||||
.get(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`)
|
||||
.expect(200);
|
||||
|
||||
expect(comments.length).to.eql(2);
|
||||
|
|
|
@ -60,7 +60,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
it('should get a sub case comment', async () => {
|
||||
const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID });
|
||||
const { body: comment }: { body: CommentResponse } = await supertest
|
||||
.get(`${CASES_URL}/${caseInfo.id}/comments/${caseInfo.subCase!.comments![0].id}`)
|
||||
.get(`${CASES_URL}/${caseInfo.id}/comments/${caseInfo.comments![0].id}`)
|
||||
.expect(200);
|
||||
expect(comment.type).to.be(CommentType.generatedAlert);
|
||||
});
|
||||
|
|
|
@ -10,10 +10,7 @@ import expect from '@kbn/expect';
|
|||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
import { CASES_URL } from '../../../../../../plugins/case/common/constants';
|
||||
import {
|
||||
CollectionWithSubCaseResponse,
|
||||
CommentType,
|
||||
} from '../../../../../../plugins/case/common/api';
|
||||
import { CaseResponse, CommentType } from '../../../../../../plugins/case/common/api';
|
||||
import {
|
||||
defaultUser,
|
||||
postCaseReq,
|
||||
|
@ -56,42 +53,38 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
|
||||
it('patches a comment for a sub case', async () => {
|
||||
const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID });
|
||||
const {
|
||||
body: patchedSubCase,
|
||||
}: { body: CollectionWithSubCaseResponse } = await supertest
|
||||
.post(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`)
|
||||
const { body: patchedSubCase }: { body: CaseResponse } = await supertest
|
||||
.post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(postCommentUserReq)
|
||||
.expect(200);
|
||||
|
||||
const newComment = 'Well I decided to update my comment. So what? Deal with it.';
|
||||
const { body: patchedSubCaseUpdatedComment } = await supertest
|
||||
.patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`)
|
||||
.patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
id: patchedSubCase.subCase!.comments![1].id,
|
||||
version: patchedSubCase.subCase!.comments![1].version,
|
||||
id: patchedSubCase.comments![1].id,
|
||||
version: patchedSubCase.comments![1].version,
|
||||
comment: newComment,
|
||||
type: CommentType.user,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(patchedSubCaseUpdatedComment.subCase.comments.length).to.be(2);
|
||||
expect(patchedSubCaseUpdatedComment.subCase.comments[0].type).to.be(
|
||||
CommentType.generatedAlert
|
||||
);
|
||||
expect(patchedSubCaseUpdatedComment.subCase.comments[1].type).to.be(CommentType.user);
|
||||
expect(patchedSubCaseUpdatedComment.subCase.comments[1].comment).to.be(newComment);
|
||||
expect(patchedSubCaseUpdatedComment.comments.length).to.be(2);
|
||||
expect(patchedSubCaseUpdatedComment.comments[0].type).to.be(CommentType.generatedAlert);
|
||||
expect(patchedSubCaseUpdatedComment.comments[1].type).to.be(CommentType.user);
|
||||
expect(patchedSubCaseUpdatedComment.comments[1].comment).to.be(newComment);
|
||||
});
|
||||
|
||||
it('fails to update the generated alert comment type', async () => {
|
||||
const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID });
|
||||
await supertest
|
||||
.patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`)
|
||||
.patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
id: caseInfo.subCase!.comments![0].id,
|
||||
version: caseInfo.subCase!.comments![0].version,
|
||||
id: caseInfo.comments![0].id,
|
||||
version: caseInfo.comments![0].version,
|
||||
type: CommentType.alert,
|
||||
alertId: 'test-id',
|
||||
index: 'test-index',
|
||||
|
@ -106,11 +99,11 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
it('fails to update the generated alert comment by using another generated alert comment', async () => {
|
||||
const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID });
|
||||
await supertest
|
||||
.patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`)
|
||||
.patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
id: caseInfo.subCase!.comments![0].id,
|
||||
version: caseInfo.subCase!.comments![0].version,
|
||||
id: caseInfo.comments![0].id,
|
||||
version: caseInfo.comments![0].version,
|
||||
type: CommentType.generatedAlert,
|
||||
alerts: [{ _id: 'id1' }],
|
||||
index: 'test-index',
|
||||
|
|
|
@ -393,13 +393,13 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
// create another sub case just to make sure we get the right comments
|
||||
await createSubCase({ supertest, actionID });
|
||||
await supertest
|
||||
.post(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`)
|
||||
.post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(postCommentUserReq)
|
||||
.expect(200);
|
||||
|
||||
const { body: subCaseComments }: { body: CommentsResponse } = await supertest
|
||||
.get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseID=${caseInfo.subCase!.id}`)
|
||||
.get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseId=${caseInfo.subCases![0].id}`)
|
||||
.send()
|
||||
.expect(200);
|
||||
expect(subCaseComments.total).to.be(2);
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
deleteComments,
|
||||
} from '../../../common/lib/utils';
|
||||
import { getSubCaseDetailsUrl } from '../../../../../plugins/case/common/api/helpers';
|
||||
import { CollectionWithSubCaseResponse } from '../../../../../plugins/case/common/api';
|
||||
import { CaseResponse } from '../../../../../plugins/case/common/api';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
|
@ -104,7 +104,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
|
||||
it('should delete the sub cases when deleting a collection', async () => {
|
||||
const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID });
|
||||
expect(caseInfo.subCase?.id).to.not.eql(undefined);
|
||||
expect(caseInfo.subCases![0].id).to.not.eql(undefined);
|
||||
|
||||
const { body } = await supertest
|
||||
.delete(`${CASES_URL}?ids=["${caseInfo.id}"]`)
|
||||
|
@ -114,27 +114,25 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
|
||||
expect(body).to.eql({});
|
||||
await supertest
|
||||
.get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id))
|
||||
.get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id))
|
||||
.send()
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it(`should delete a sub case's comments when that case gets deleted`, async () => {
|
||||
const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID });
|
||||
expect(caseInfo.subCase?.id).to.not.eql(undefined);
|
||||
expect(caseInfo.subCases![0].id).to.not.eql(undefined);
|
||||
|
||||
// there should be two comments on the sub case now
|
||||
const {
|
||||
body: patchedCaseWithSubCase,
|
||||
}: { body: CollectionWithSubCaseResponse } = await supertest
|
||||
const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest
|
||||
.post(`${CASES_URL}/${caseInfo.id}/comments`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query({ subCaseID: caseInfo.subCase!.id })
|
||||
.query({ subCaseId: caseInfo.subCases![0].id })
|
||||
.send(postCommentUserReq)
|
||||
.expect(200);
|
||||
|
||||
const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${
|
||||
patchedCaseWithSubCase.subCase!.comments![1].id
|
||||
patchedCaseWithSubCase.comments![1].id
|
||||
}`;
|
||||
// make sure we can get the second comment
|
||||
await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200);
|
||||
|
|
|
@ -265,8 +265,8 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
supertest,
|
||||
cases: [
|
||||
{
|
||||
id: collection.newSubCaseInfo.subCase!.id,
|
||||
version: collection.newSubCaseInfo.subCase!.version,
|
||||
id: collection.newSubCaseInfo.subCases![0].id,
|
||||
version: collection.newSubCaseInfo.subCases![0].version,
|
||||
status: CaseStatuses['in-progress'],
|
||||
},
|
||||
],
|
||||
|
@ -356,7 +356,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
it('correctly counts stats including a collection without sub cases', async () => {
|
||||
// delete the sub case on the collection so that it doesn't have any sub cases
|
||||
await supertest
|
||||
.delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCase!.id}"]`)
|
||||
.delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCases![0].id}"]`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(204);
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
deleteCaseAction,
|
||||
} from '../../../../common/lib/utils';
|
||||
import { getSubCaseDetailsUrl } from '../../../../../../plugins/case/common/api/helpers';
|
||||
import { CollectionWithSubCaseResponse } from '../../../../../../plugins/case/common/api';
|
||||
import { CaseResponse } from '../../../../../../plugins/case/common/api';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
|
@ -40,10 +40,10 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
it('should delete a sub case', async () => {
|
||||
const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID });
|
||||
expect(caseInfo.subCase?.id).to.not.eql(undefined);
|
||||
expect(caseInfo.subCases![0].id).to.not.eql(undefined);
|
||||
|
||||
const { body: subCase } = await supertest
|
||||
.get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id))
|
||||
.get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id))
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
|
@ -57,33 +57,31 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(body).to.eql({});
|
||||
await supertest
|
||||
.get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id))
|
||||
.get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id))
|
||||
.send()
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it(`should delete a sub case's comments when that case gets deleted`, async () => {
|
||||
const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID });
|
||||
expect(caseInfo.subCase?.id).to.not.eql(undefined);
|
||||
expect(caseInfo.subCases![0].id).to.not.eql(undefined);
|
||||
|
||||
// there should be two comments on the sub case now
|
||||
const {
|
||||
body: patchedCaseWithSubCase,
|
||||
}: { body: CollectionWithSubCaseResponse } = await supertest
|
||||
const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest
|
||||
.post(`${CASES_URL}/${caseInfo.id}/comments`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query({ subCaseID: caseInfo.subCase!.id })
|
||||
.query({ subCaseId: caseInfo.subCases![0].id })
|
||||
.send(postCommentUserReq)
|
||||
.expect(200);
|
||||
|
||||
const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${
|
||||
patchedCaseWithSubCase.subCase!.comments![1].id
|
||||
patchedCaseWithSubCase.comments![1].id
|
||||
}`;
|
||||
// make sure we can get the second comment
|
||||
await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200);
|
||||
|
||||
await supertest
|
||||
.delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${patchedCaseWithSubCase.subCase!.id}"]`)
|
||||
.delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${patchedCaseWithSubCase.subCases![0].id}"]`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(204);
|
||||
|
|
|
@ -74,7 +74,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
...findSubCasesResp,
|
||||
total: 1,
|
||||
// find should not return the comments themselves only the stats
|
||||
subCases: [{ ...caseInfo.subCase!, comments: [], totalComment: 1, totalAlerts: 2 }],
|
||||
subCases: [{ ...caseInfo.subCases![0], comments: [], totalComment: 1, totalAlerts: 2 }],
|
||||
count_open_cases: 1,
|
||||
});
|
||||
});
|
||||
|
@ -101,7 +101,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
status: CaseStatuses.closed,
|
||||
},
|
||||
{
|
||||
...subCase2Resp.newSubCaseInfo.subCase,
|
||||
...subCase2Resp.newSubCaseInfo.subCases![0],
|
||||
comments: [],
|
||||
totalComment: 1,
|
||||
totalAlerts: 2,
|
||||
|
@ -157,8 +157,8 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
supertest,
|
||||
cases: [
|
||||
{
|
||||
id: secondSub.subCase!.id,
|
||||
version: secondSub.subCase!.version,
|
||||
id: secondSub.subCases![0].id,
|
||||
version: secondSub.subCases![0].version,
|
||||
status: CaseStatuses['in-progress'],
|
||||
},
|
||||
],
|
||||
|
@ -231,8 +231,8 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
supertest,
|
||||
cases: [
|
||||
{
|
||||
id: secondSub.subCase!.id,
|
||||
version: secondSub.subCase!.version,
|
||||
id: secondSub.subCases![0].id,
|
||||
version: secondSub.subCases![0].version,
|
||||
status: CaseStatuses['in-progress'],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
} from '../../../../../../plugins/case/common/api/helpers';
|
||||
import {
|
||||
AssociationType,
|
||||
CollectionWithSubCaseResponse,
|
||||
CaseResponse,
|
||||
SubCaseResponse,
|
||||
} from '../../../../../../plugins/case/common/api';
|
||||
|
||||
|
@ -53,14 +53,14 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID });
|
||||
|
||||
const { body }: { body: SubCaseResponse } = await supertest
|
||||
.get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id))
|
||||
.get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id))
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql(
|
||||
commentsResp({
|
||||
comments: [{ comment: defaultCreateSubComment, id: caseInfo.subCase!.comments![0].id }],
|
||||
comments: [{ comment: defaultCreateSubComment, id: caseInfo.comments![0].id }],
|
||||
associationType: AssociationType.subCase,
|
||||
})
|
||||
);
|
||||
|
@ -73,15 +73,15 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
it('should return the correct number of alerts with multiple types of alerts', async () => {
|
||||
const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID });
|
||||
|
||||
const { body: singleAlert }: { body: CollectionWithSubCaseResponse } = await supertest
|
||||
const { body: singleAlert }: { body: CaseResponse } = await supertest
|
||||
.post(getCaseCommentsUrl(caseInfo.id))
|
||||
.query({ subCaseID: caseInfo.subCase!.id })
|
||||
.query({ subCaseId: caseInfo.subCases![0].id })
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(postCommentAlertReq)
|
||||
.expect(200);
|
||||
|
||||
const { body }: { body: SubCaseResponse } = await supertest
|
||||
.get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id))
|
||||
.get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id))
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
@ -89,10 +89,10 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql(
|
||||
commentsResp({
|
||||
comments: [
|
||||
{ comment: defaultCreateSubComment, id: caseInfo.subCase!.comments![0].id },
|
||||
{ comment: defaultCreateSubComment, id: caseInfo.comments![0].id },
|
||||
{
|
||||
comment: postCommentAlertReq,
|
||||
id: singleAlert.subCase!.comments![1].id,
|
||||
id: singleAlert.comments![1].id,
|
||||
},
|
||||
],
|
||||
associationType: AssociationType.subCase,
|
||||
|
|
|
@ -59,15 +59,15 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
supertest,
|
||||
cases: [
|
||||
{
|
||||
id: caseInfo.subCase!.id,
|
||||
version: caseInfo.subCase!.version,
|
||||
id: caseInfo.subCases![0].id,
|
||||
version: caseInfo.subCases![0].version,
|
||||
status: CaseStatuses['in-progress'],
|
||||
},
|
||||
],
|
||||
type: 'sub_case',
|
||||
});
|
||||
const { body: subCase }: { body: SubCaseResponse } = await supertest
|
||||
.get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id))
|
||||
.get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id))
|
||||
.expect(200);
|
||||
|
||||
expect(subCase.status).to.eql(CaseStatuses['in-progress']);
|
||||
|
@ -102,8 +102,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
supertest,
|
||||
cases: [
|
||||
{
|
||||
id: caseInfo.subCase!.id,
|
||||
version: caseInfo.subCase!.version,
|
||||
id: caseInfo.subCases![0].id,
|
||||
version: caseInfo.subCases![0].version,
|
||||
status: CaseStatuses['in-progress'],
|
||||
},
|
||||
],
|
||||
|
@ -159,8 +159,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
supertest,
|
||||
cases: [
|
||||
{
|
||||
id: caseInfo.subCase!.id,
|
||||
version: caseInfo.subCase!.version,
|
||||
id: caseInfo.subCases![0].id,
|
||||
version: caseInfo.subCases![0].version,
|
||||
status: CaseStatuses['in-progress'],
|
||||
},
|
||||
],
|
||||
|
@ -239,8 +239,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
supertest,
|
||||
cases: [
|
||||
{
|
||||
id: collectionWithSecondSub.subCase!.id,
|
||||
version: collectionWithSecondSub.subCase!.version,
|
||||
id: collectionWithSecondSub.subCases![0].id,
|
||||
version: collectionWithSecondSub.subCases![0].version,
|
||||
status: CaseStatuses['in-progress'],
|
||||
},
|
||||
],
|
||||
|
@ -349,8 +349,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
supertest,
|
||||
cases: [
|
||||
{
|
||||
id: caseInfo.subCase!.id,
|
||||
version: caseInfo.subCase!.version,
|
||||
id: caseInfo.subCases![0].id,
|
||||
version: caseInfo.subCases![0].version,
|
||||
status: CaseStatuses['in-progress'],
|
||||
},
|
||||
],
|
||||
|
@ -450,8 +450,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.send({
|
||||
subCases: [
|
||||
{
|
||||
id: caseInfo.subCase!.id,
|
||||
version: caseInfo.subCase!.version,
|
||||
id: caseInfo.subCases![0].id,
|
||||
version: caseInfo.subCases![0].version,
|
||||
type: 'blah',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -26,7 +26,6 @@ import {
|
|||
CaseClientPostRequest,
|
||||
SubCaseResponse,
|
||||
AssociationType,
|
||||
CollectionWithSubCaseResponse,
|
||||
SubCasesFindResponse,
|
||||
CommentRequest,
|
||||
} from '../../../../plugins/case/common/api';
|
||||
|
@ -159,18 +158,17 @@ export const subCaseResp = ({
|
|||
|
||||
interface FormattedCollectionResponse {
|
||||
caseInfo: Partial<CaseResponse>;
|
||||
subCase?: Partial<SubCaseResponse>;
|
||||
subCases?: Array<Partial<SubCaseResponse>>;
|
||||
comments?: Array<Partial<CommentResponse>>;
|
||||
}
|
||||
|
||||
export const formatCollectionResponse = (
|
||||
caseInfo: CollectionWithSubCaseResponse
|
||||
): FormattedCollectionResponse => {
|
||||
export const formatCollectionResponse = (caseInfo: CaseResponse): FormattedCollectionResponse => {
|
||||
const subCase = removeServerGeneratedPropertiesFromSubCase(caseInfo.subCases?.[0]);
|
||||
return {
|
||||
caseInfo: removeServerGeneratedPropertiesFromCaseCollection(caseInfo),
|
||||
subCase: removeServerGeneratedPropertiesFromSubCase(caseInfo.subCase),
|
||||
subCases: subCase ? [subCase] : undefined,
|
||||
comments: removeServerGeneratedPropertiesFromComments(
|
||||
caseInfo.subCase?.comments ?? caseInfo.comments
|
||||
caseInfo.subCases?.[0].comments ?? caseInfo.comments
|
||||
),
|
||||
};
|
||||
};
|
||||
|
@ -187,10 +185,10 @@ export const removeServerGeneratedPropertiesFromSubCase = (
|
|||
};
|
||||
|
||||
export const removeServerGeneratedPropertiesFromCaseCollection = (
|
||||
config: Partial<CollectionWithSubCaseResponse>
|
||||
): Partial<CollectionWithSubCaseResponse> => {
|
||||
config: Partial<CaseResponse>
|
||||
): Partial<CaseResponse> => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { closed_at, created_at, updated_at, version, subCase, ...rest } = config;
|
||||
const { closed_at, created_at, updated_at, version, subCases, ...rest } = config;
|
||||
return rest;
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
CaseConnector,
|
||||
ConnectorTypes,
|
||||
CasePostRequest,
|
||||
CollectionWithSubCaseResponse,
|
||||
CaseResponse,
|
||||
SubCasesFindResponse,
|
||||
CaseStatuses,
|
||||
SubCasesResponse,
|
||||
|
@ -120,7 +120,7 @@ export const defaultCreateSubPost = postCollectionReq;
|
|||
* Response structure for the createSubCase and createSubCaseComment functions.
|
||||
*/
|
||||
export interface CreateSubCaseResp {
|
||||
newSubCaseInfo: CollectionWithSubCaseResponse;
|
||||
newSubCaseInfo: CaseResponse;
|
||||
modifiedSubCases?: SubCasesResponse;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue