diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md index c57216603665d..07bad42a3bfa3 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -15,6 +15,7 @@ Table of Contents - [Usage](#usage) - [Alerts API keys](#alerts-api-keys) - [Limitations](#limitations) + - [Plugin status](#plugin-status) - [Alert types](#alert-types) - [Methods](#methods) - [Executor](#executor) @@ -79,6 +80,27 @@ Note that the `manage_own_api_key` cluster privilege is not enough - it can be u is unauthorized for user [user-name-here] ``` +## Plugin status + +The plugin status of an alert is customized by including information about checking failures for the framework decryption: +``` +core.status.set( + combineLatest([ + core.status.derivedStatus$, + getHealthStatusStream(startPlugins.taskManager), + ]).pipe( + map(([derivedStatus, healthStatus]) => { + if (healthStatus.level > derivedStatus.level) { + return healthStatus as ServiceStatus; + } else { + return derivedStatus; + } + }) + ) + ); +``` +To check for framework decryption failures, we use the task `alerting_health_check`, which runs every 60 minutes by default. To change the default schedule, use the kibana.yml configuration option `xpack.alerts.healthCheck.interval`. + ## Alert types ### Methods diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx index 5c9d79f37cc57..b86f0d40de137 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx @@ -37,6 +37,7 @@ interface Tab { key: string; href: string; text: ReactNode; + hidden?: boolean; render: () => ReactNode; } @@ -126,6 +127,7 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { const profilingTab = { key: 'profiling', href: useServiceProfilingHref({ serviceName }), + hidden: !config.profilingEnabled, text: ( @@ -167,22 +169,20 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { tabs.push(metricsTab); } - tabs.push(serviceMapTab); - - if (config.profilingEnabled) { - tabs.push(profilingTab); - } + tabs.push(serviceMapTab, profilingTab); const selectedTab = tabs.find((serviceTab) => serviceTab.key === tab); return ( <> - {tabs.map(({ href, key, text }) => ( - - {text} - - ))} + {tabs + .filter((t) => !t.hidden) + .map(({ href, key, text }) => ( + + {text} + + ))}
diff --git a/x-pack/plugins/case/common/api/cases/commentable_case.ts b/x-pack/plugins/case/common/api/cases/commentable_case.ts deleted file mode 100644 index 023229a90d352..0000000000000 --- a/x-pack/plugins/case/common/api/cases/commentable_case.ts +++ /dev/null @@ -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; -export type CollectionWithSubCaseAttributes = rt.TypeOf; diff --git a/x-pack/plugins/case/common/api/cases/index.ts b/x-pack/plugins/case/common/api/cases/index.ts index 4d1fc68109ddb..6e7fb818cb2b5 100644 --- a/x-pack/plugins/case/common/api/cases/index.ts +++ b/x-pack/plugins/case/common/api/cases/index.ts @@ -11,4 +11,3 @@ export * from './comment'; export * from './status'; export * from './user_actions'; export * from './sub_case'; -export * from './commentable_case'; diff --git a/x-pack/plugins/case/common/api/helpers.ts b/x-pack/plugins/case/common/api/helpers.ts index 00c8ff402c802..43e292b91db4b 100644 --- a/x-pack/plugins/case/common/api/helpers.ts +++ b/x-pack/plugins/case/common/api/helpers.ts @@ -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 => { diff --git a/x-pack/plugins/case/common/api/runtime_types.ts b/x-pack/plugins/case/common/api/runtime_types.ts index 43e3be04d10e5..b2ff763838287 100644 --- a/x-pack/plugins/case/common/api/runtime_types.ts +++ b/x-pack/plugins/case/common/api/runtime_types.ts @@ -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 = ( diff --git a/x-pack/plugins/case/server/client/cases/types.ts b/x-pack/plugins/case/server/client/cases/types.ts index 2dd2caf9fe73a..f1d56e7132bd1 100644 --- a/x-pack/plugins/case/server/client/cases/types.ts +++ b/x-pack/plugins/case/server/client/cases/types.ts @@ -72,7 +72,7 @@ export interface TransformFieldsArgs { export interface ExternalServiceComment { comment: string; - commentId?: string; + commentId: string; } export interface MapIncident { diff --git a/x-pack/plugins/case/server/client/cases/utils.test.ts b/x-pack/plugins/case/server/client/cases/utils.test.ts index 44e7a682aa7ed..859114a5e8fb0 100644 --- a/x-pack/plugins/case/server/client/cases/utils.test.ts +++ b/x-pack/plugins/case/server/client/cases/utils.test.ts @@ -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', }, ]); }); diff --git a/x-pack/plugins/case/server/client/cases/utils.ts b/x-pack/plugins/case/server/client/cases/utils.ts index a5013d9b93982..67d5ef55f83c3 100644 --- a/x-pack/plugins/case/server/client/cases/utils.ts +++ b/x-pack/plugins/case/server/client/cases/utils.ts @@ -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`, }); } diff --git a/x-pack/plugins/case/server/client/comments/add.ts b/x-pack/plugins/case/server/client/comments/add.ts index 0a86c1825fedc..4c1cc59a95750 100644 --- a/x-pack/plugins/case/server/client/comments/add.ts +++ b/x-pack/plugins/case/server/client/comments/add.ts @@ -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 => { +}: AddCommentFromRuleArgs): Promise => { const query = pipe( AlertCommentRequestRt.decode(comment), fold(throwErrors(Boom.badRequest), identity) @@ -260,7 +260,7 @@ export const addComment = async ({ caseId, comment, user, -}: AddCommentArgs): Promise => { +}: AddCommentArgs): Promise => { const query = pipe( CommentRequestRt.decode(comment), fold(throwErrors(Boom.badRequest), identity) diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts index ba5677426c222..d6a8f6b5d706c 100644 --- a/x-pack/plugins/case/server/client/types.ts +++ b/x-pack/plugins/case/server/client/types.ts @@ -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; + addComment(args: CaseClientAddComment): Promise; create(theCase: CasePostRequest): Promise; get(args: CaseClientGet): Promise; getAlerts(args: CaseClientGetAlerts): Promise; diff --git a/x-pack/plugins/case/server/common/models/commentable_case.ts b/x-pack/plugins/case/server/common/models/commentable_case.ts index 9827118ee8e29..3ae225999db4e 100644 --- a/x-pack/plugins/case/server/common/models/commentable_case.ts +++ b/x-pack/plugins/case/server/common/models/commentable_case.ts @@ -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 { + public async encode(): Promise { 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); } } diff --git a/x-pack/plugins/case/server/connectors/case/index.test.ts b/x-pack/plugins/case/server/connectors/case/index.test.ts index 4be519858db18..e4c29bb099f0e 100644 --- a/x-pack/plugins/case/server/connectors/case/index.test.ts +++ b/x-pack/plugins/case/server/connectors/case/index.test.ts @@ -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, diff --git a/x-pack/plugins/case/server/connectors/case/types.ts b/x-pack/plugins/case/server/connectors/case/types.ts index 50ff104d7bad0..6a7dfd9c2e687 100644 --- a/x-pack/plugins/case/server/connectors/case/types.ts +++ b/x-pack/plugins/case/server/connectors/case/types.ts @@ -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; export type Connector = TypeOf; @@ -29,7 +29,7 @@ export type ExecutorSubActionAddCommentParams = TypeOf< >; export type CaseExecutorParams = TypeOf; -export type CaseExecutorResponse = CaseResponse | CasesResponse | CollectionWithSubCaseResponse; +export type CaseExecutorResponse = CaseResponse | CasesResponse; export type CaseActionType = ActionType< CaseConfiguration, diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts index bcbf1828e1fde..e0b3a4420f4b5 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts @@ -23,7 +23,7 @@ export function initDeleteAllCommentsApi({ caseService, router, userActionServic }), 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'], }) diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts index 73307753a550d..cae0809ea5f0b 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts @@ -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'], }), diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts index 3431c340c791e..0ec0f1871c7ad 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts @@ -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, diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts index 730b1b92a8a07..8bf49ec3e27a1 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts @@ -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; - if (request.query?.subCaseID) { + if (request.query?.subCaseId) { comments = await caseService.getAllSubCaseComments({ client, - id: request.query.subCaseID, + id: request.query.subCaseId, options: { sortField: defaultSortField, }, diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts index e8b6f7bc957eb..01b0e17464053 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts @@ -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), diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts index 95b611950bd41..607f3f381f067 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts @@ -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 { diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts index ca5cd657a39f3..4b8e4920852c2 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts @@ -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>()); diff --git a/x-pack/plugins/case/server/scripts/sub_cases/index.ts b/x-pack/plugins/case/server/scripts/sub_cases/index.ts index f6ec1f44076e4..ba3bcaa65091c 100644 --- a/x-pack/plugins/case/server/scripts/sub_cases/index.ts +++ b/x-pack/plugins/case/server/scripts/sub_cases/index.ts @@ -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 - >({ + const executeResp = await client.request>({ path: `/api/actions/action/${createdAction.data.id}/_execute`, method: 'POST', body: { diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx index e1427bc96e7e0..c69c7ebff2b9e 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx @@ -66,7 +66,7 @@ export class LogStreamEmbeddable extends Embeddable { } const startTimestamp = datemathToEpochMillis(this.input.timeRange.from); - const endTimestamp = datemathToEpochMillis(this.input.timeRange.to); + const endTimestamp = datemathToEpochMillis(this.input.timeRange.to, 'up'); if (!startTimestamp || !endTimestamp) { return; diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js index 02342c895f077..323d267899bdf 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js @@ -7,8 +7,6 @@ import { cloneDeep, get, pick } from 'lodash'; -import { WEEK } from '../../../../../../../../src/plugins/es_ui_shared/public'; - import { validateId } from './validate_id'; import { validateIndexPattern } from './validate_index_pattern'; import { validateRollupIndex } from './validate_rollup_index'; @@ -66,7 +64,7 @@ export const stepIdToStepConfigMap = { // a few hours as they're being restarted. A delay of 1d would allow them that period to reboot // and the "expense" is pretty negligible in most cases: 1 day of extra non-rolled-up data. rollupDelay: '1d', - cronFrequency: WEEK, + cronFrequency: 'WEEK', fieldToPreferredValueMap: {}, }; diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js index e4371d1ab07cd..b7341ea21e3b1 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js @@ -181,6 +181,11 @@ describe('Create Rollup Job, step 1: Logistics', () => { expect(options).toEqual(['minute', 'hour', 'day', 'week', 'month', 'year']); }); + it('should default to "WEEK"', () => { + const frequencySelect = find('cronFrequencySelect'); + expect(frequencySelect.props().value).toBe('WEEK'); + }); + describe('every minute', () => { it('should not have any additional configuration', () => { changeFrequency('MINUTE'); diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index 725a2eb9fea7b..79b912e082fdb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -5,9 +5,19 @@ * 2.0. */ -import { EntriesArray } from '../shared_imports'; +import { + CreateExceptionListItemSchema, + EntriesArray, + ExceptionListItemSchema, +} from '../shared_imports'; import { Type } from './schemas/common/schemas'; +export const hasLargeValueItem = ( + exceptionItems: Array +) => { + return exceptionItems.some((exceptionItem) => hasLargeValueList(exceptionItem.entries)); +}; + export const hasLargeValueList = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'list'); return found.length > 0; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx index bb4bd0f98949d..1e1e925a20ada 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx @@ -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} /> ), }; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx index ce0fea07bf473..56dcf3bc28757 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx @@ -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( - ({ 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( 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( () => ({ selectable: (theCase) => isEmpty(theCase.subCases), onSelectionChange: setSelectedCases, + selectableMessage: (selectable) => (!selectable ? i18n.SELECTABLE_MESSAGE_COLLECTIONS : ''), }), [setSelectedCases] ); @@ -377,7 +403,8 @@ export const AllCases = React.memo( 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( status: filterOptions.status, }} setFilterRefetch={setFilterRefetch} + disabledStatuses={disabledStatuses} /> {isCasesLoading && isDataEmpty ? (
@@ -530,6 +558,7 @@ export const AllCases = React.memo( rowProps={tableRowProps} selection={userCanCrud && !isModal ? euiBasicTableSelectionProps : undefined} sorting={sorting} + className={classnames({ isModal })} />
)} diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx index 785d4447c0acf..11d53b6609e74 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx @@ -61,4 +61,24 @@ describe('StatusFilter', () => { expect(onStatusChanged).toBeCalledWith('closed'); }); }); + + it('should disabled selected statuses', () => { + const wrapper = mount( + + ); + + 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(); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.tsx index 7fa0625229b48..41997d6f38421 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.tsx @@ -14,9 +14,15 @@ interface Props { stats: Record; selectedStatus: CaseStatuses; onStatusChanged: (status: CaseStatuses) => void; + disabledStatuses?: CaseStatuses[]; } -const StatusFilterComponent: React.FC = ({ stats, selectedStatus, onStatusChanged }) => { +const StatusFilterComponent: React.FC = ({ + stats, + selectedStatus, + onStatusChanged, + disabledStatuses = [], +}) => { const caseStatuses = Object.keys(statuses) as CaseStatuses[]; const options: Array> = caseStatuses.map((status) => ({ value: status, @@ -28,6 +34,7 @@ const StatusFilterComponent: React.FC = ({ stats, selectedStatus, onStatu {` (${stats[status]})`}
), + disabled: disabledStatuses.includes(status), 'data-test-subj': `case-status-filter-${status}`, })); diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx index 1f7f1d1e0d487..61bbbac5a1e84 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx @@ -25,6 +25,7 @@ interface CasesTableFiltersProps { onFilterChanged: (filterOptions: Partial) => 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} /> diff --git a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx index 5e33736ce9c3a..95c534f7c1ede 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx @@ -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 = ({ ); return ( - + - - {i18n.STATUS} - - - - + {caseData.type !== CaseType.collection && ( + + {i18n.STATUS} + + + + + )} {title} diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx index 7a5f6647a8dcf..5f9fb5b63d6eb 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx @@ -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( @@ -752,4 +756,74 @@ describe('CaseView ', () => { }); }); }); + + describe('Collections', () => { + it('it does not allow the user to update the status', async () => { + const wrapper = mount( + + + + + + ); + + 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( + + + + + + ); + + 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( + + + + + + ); + + await waitFor(() => { + expect( + wrapper.find('[data-test-subj="case-view-bottom-actions-horizontal-rule"]').exists() + ).toBe(false); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index e42431e55ee29..d0b7c34ab84fd 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -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( 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( ); 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( ); } }, [dispatch]); + return ( <> @@ -387,7 +388,7 @@ export const CaseComponent = React.memo( 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( updateCase={updateCase} userCanCrud={userCanCrud} /> - - - - + - - {hasDataToPush && ( - - {pushButton} - - )} - + {caseData.type !== CaseType.collection && ( + + + + )} + {hasDataToPush && ( + + {pushButton} + + )} + + )} )} @@ -465,6 +473,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = if (isError) { return null; } + if (isLoading) { return ( @@ -476,14 +485,16 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = } return ( - + data && ( + + ) ); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx index d5c90bd09a6db..b7fbaff288a2a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx @@ -47,7 +47,9 @@ const CaseParamsFields: React.FunctionComponent = ({ 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 ( <> diff --git a/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx index 842fe9e00ab39..d5883b7b88cd0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx @@ -20,14 +20,16 @@ jest.mock('../create/form_context', () => { onSuccess, }: { children: ReactNode; - onSuccess: ({ id }: { id: string }) => void; + onSuccess: ({ id }: { id: string }) => Promise; }) => { return ( <> @@ -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' }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx b/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx index cb3436f6ba3bc..e7bb0b25f391f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx @@ -17,7 +17,8 @@ import * as i18n from '../../translations'; export interface CreateCaseModalProps { onCloseFlyout: () => void; - onCaseCreated: (theCase: Case) => void; + onSuccess: (theCase: Case) => Promise; + afterCaseCreated?: (theCase: Case) => Promise; } const Container = styled.div` @@ -40,7 +41,8 @@ const FormWrapper = styled.div` `; const CreateCaseFlyoutComponent: React.FC = ({ - onCaseCreated, + onSuccess, + afterCaseCreated, onCloseFlyout, }) => { return ( @@ -52,7 +54,7 @@ const CreateCaseFlyoutComponent: React.FC = ({ - + diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx index 8236ab7b19d27..1e512ef5ffabd 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx @@ -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( + + + + + + + ); + + 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( + + + + + + + ); + + 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); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx index 83b8870ab597d..26203d7268fd3 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx @@ -32,13 +32,15 @@ const initialCaseValue: FormProps = { interface Props { caseType?: CaseType; - onSuccess?: (theCase: Case) => void; + onSuccess?: (theCase: Case) => Promise; + afterCaseCreated?: (theCase: Case) => Promise; } export const FormContext: React.FC = ({ caseType = CaseType.individual, children, onSuccess, + afterCaseCreated, }) => { const { connectors } = useConnectors(); const { connector: configurationConnector } = useCaseConfigure(); @@ -72,6 +74,10 @@ export const FormContext: React.FC = ({ 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 = ({ } if (onSuccess && updatedCase) { - onSuccess(updatedCase); + await onSuccess(updatedCase); } } }, - [caseType, connectors, postCase, onSuccess, pushCaseToExternalService] + [caseType, connectors, postCase, onSuccess, pushCaseToExternalService, afterCaseCreated] ); const { form } = useForm({ diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx index b7d162bd92761..9f904350b772e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx @@ -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] diff --git a/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx b/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx index ab30fe2979b9e..22d72429836de 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx @@ -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', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/status/config.ts b/x-pack/plugins/security_solution/public/cases/components/status/config.ts index d811db43df814..0eebef39859c7 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/config.ts +++ b/x-pack/plugins/security_solution/public/cases/components/status/config.ts @@ -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, diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx index aa1305f1f655c..b3302a05cfcb2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx @@ -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) => void }) => { return ( @@ -49,18 +55,25 @@ jest.mock('../create/form_context', () => { FormContext: ({ children, onSuccess, + afterCaseCreated, }: { children: ReactNode; - onSuccess: (theCase: Partial) => void; + onSuccess: (theCase: Partial) => Promise; + afterCaseCreated: (theCase: Partial) => Promise; }) => { return ( <> @@ -212,11 +225,43 @@ describe('AddToCaseAction', () => { }); }); - it('navigates to case view', async () => { + it('navigates to case view when attach to a new case', async () => { + const wrapper = mount( + + + + ); + + wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="add-new-case-item"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="form-context-on-success"]`).first().simulate('click'); + + expect(mockDispatchToaster).toHaveBeenCalled(); + const toast = mockDispatchToaster.mock.calls[0][0].toast; + + const toastWrapper = mount( + {}} /> + ); + + toastWrapper + .find('[data-test-subj="toaster-content-case-view-link"]') + .first() + .simulate('click'); + + 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()), + postComment: jest.fn().mockImplementation(({ caseId, data, updateCase }) => { + updateCase({ + id: 'selected-case', + title: 'the selected case', + settings: { syncAlerts: true }, + }); + }), }; }); @@ -227,8 +272,8 @@ describe('AddToCaseAction', () => { ); wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click'); - wrapper.find(`[data-test-subj="add-new-case-item"]`).first().simulate('click'); - wrapper.find(`[data-test-subj="form-context-on-success"]`).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; @@ -242,6 +287,8 @@ describe('AddToCaseAction', () => { .first() .simulate('click'); - expect(mockNavigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/new-case' }); + expect(mockNavigateToApp).toHaveBeenCalledWith('securitySolution:case', { + path: '/selected-case', + }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index aa9cec2d6b5b1..3000551dd3c07 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -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 = ({ } = 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 = ({ 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 = ({ 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 = ({ {isCreateCaseFlyoutOpen && ( - + )} {allCasesModal} diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx index eda8ed8cdfbcd..e1d6baa6e630a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx @@ -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 = ({ isModalOpen, onCloseCaseModal, onRowClick, + disabledStatuses, }) => { const userPermissions = useGetUserSavedObjectPermissions(); const userCanCrud = userPermissions?.crud ?? false; return isModalOpen ? ( - + {i18n.SELECT_CASE_TITLE} - + - + ) : null; }; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx index 576f942a36a8f..57bb39a1ab50f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx @@ -123,7 +123,7 @@ describe('useAllCasesModal', () => { }); const modal = result.current.modal; - render(<>{modal}); + render({modal}); act(() => { userEvent.click(screen.getByText('case-row')); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx index 79b490c1962da..52b8ebe0210c0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx @@ -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(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; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.test.tsx index 0e04acb013b2d..08fca0cc6e009 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.test.tsx @@ -20,14 +20,16 @@ jest.mock('../create/form_context', () => { onSuccess, }: { children: ReactNode; - onSuccess: ({ id }: { id: string }) => void; + onSuccess: ({ id }: { id: string }) => Promise; }) => { return ( <> diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx index 2806e358fceee..3e11ee526839c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx @@ -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; caseType?: CaseType; } diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.test.tsx index 9966cf75351dd..5174c03e56e0b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.test.tsx @@ -25,14 +25,16 @@ jest.mock('../create/form_context', () => { onSuccess, }: { children: ReactNode; - onSuccess: ({ id }: { id: string }) => void; + onSuccess: ({ id }: { id: string }) => Promise; }) => { return ( <> diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx index 3dc852a19e73f..1cef63ae9cfbf 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx @@ -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(); }, diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx index c5d3ef1893ad7..056add32add82 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx @@ -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, diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx index 2a9f99465251b..cf68d07859ced 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx @@ -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(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( () => ( ), - [caseData.id, handleUpdate, userCanCrud, handleManageMarkdownEditId, subCaseId] + [caseId, userCanCrud, handleUpdate, handleManageMarkdownEditId, subCaseId] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.test.tsx index a157be2dc1353..a3d64a17727e5 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.test.tsx @@ -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, diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx index fb8da8d0663ee..70e202b5d6bdf 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx @@ -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); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx index 5eb875287ba88..75d3047bc828e 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx @@ -48,7 +48,7 @@ interface PostComment { subCaseId?: string; } export interface UsePostComment extends NewCommentState { - postComment: (args: PostComment) => void; + postComment: (args: PostComment) => Promise; } export const usePostComment = (): UsePostComment => { diff --git a/x-pack/plugins/security_solution/public/cases/translations.ts b/x-pack/plugins/security_solution/public/cases/translations.ts index caaa1f6e248ea..b7cfe11aafda0 100644 --- a/x-pack/plugins/security_solution/public/cases/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/translations.ts @@ -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', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx index ff358b6ab0e1d..f7466b183e18a 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx @@ -25,7 +25,7 @@ describe('useQueryAlerts', () => { >(() => useQueryAlerts(mockAlertsQuery, indexName)); await waitForNextUpdate(); expect(result.current).toEqual({ - loading: true, + loading: false, data: null, response: '', request: '', diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx index 8557e1082c1cb..3736c8593daa9 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx @@ -42,7 +42,7 @@ export const useQueryAlerts = ( setQuery, refetch: null, }); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); useEffect(() => { let isSubscribed = true; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx index 487d42aac4840..5cba64299ee9d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx @@ -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 = ({ 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(), }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index cf6ea572aa856..649ce9ed64365 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -440,6 +440,35 @@ export const getMlResult = (): RuleAlertType => { }; }; +export const getThresholdResult = (): RuleAlertType => { + const result = getResult(); + + return { + ...result, + params: { + ...result.params, + type: 'threshold', + threshold: { + field: 'host.ip', + value: 5, + }, + }, + }; +}; + +export const getEqlResult = (): RuleAlertType => { + const result = getResult(); + + return { + ...result, + params: { + ...result.params, + type: 'eql', + query: 'process where true', + }, + }; +}; + export const updateActionResult = (): ActionResult => ({ id: 'result-1', actionTypeId: 'action-id-1', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index cadc6d0c5b7c0..d3d82682cbb4a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -7,7 +7,12 @@ import moment from 'moment'; import { loggingSystemMock } from 'src/core/server/mocks'; -import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; +import { + getResult, + getMlResult, + getThresholdResult, + getEqlResult, +} from '../routes/__mocks__/request_responses'; import { signalRulesAlertType } from './signal_rule_alert_type'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import { ruleStatusServiceFactory } from './rule_status_service'; @@ -24,6 +29,7 @@ import { getListClientMock } from '../../../../../lists/server/services/lists/li import { getExceptionListClientMock } from '../../../../../lists/server/services/exception_lists/exception_list_client.mock'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; +import { getEntryListMock } from '../../../../../lists/common/schemas/types/entry_list.mock'; jest.mock('./rule_status_saved_objects_client'); jest.mock('./rule_status_service'); @@ -211,6 +217,30 @@ describe('rules_notification_alert_type', () => { ); }); + it('should set a warning when exception list for threshold rule contains value list exceptions', async () => { + (getExceptions as jest.Mock).mockReturnValue([ + getExceptionListItemSchemaMock({ entries: [getEntryListMock()] }), + ]); + payload = getPayload(getThresholdResult(), alertServices); + await alert.executor(payload); + expect(ruleStatusService.warning).toHaveBeenCalled(); + expect(ruleStatusService.warning.mock.calls[0][0]).toContain( + 'Exceptions that use "is in list" or "is not in list" operators are not applied to Threshold rules' + ); + }); + + it('should set a warning when exception list for EQL rule contains value list exceptions', async () => { + (getExceptions as jest.Mock).mockReturnValue([ + getExceptionListItemSchemaMock({ entries: [getEntryListMock()] }), + ]); + payload = getPayload(getEqlResult(), alertServices); + await alert.executor(payload); + expect(ruleStatusService.warning).toHaveBeenCalled(); + expect(ruleStatusService.warning.mock.calls[0][0]).toContain( + 'Exceptions that use "is in list" or "is not in list" operators are not applied to EQL rules' + ); + }); + it('should set a failure status for when rules cannot read ANY provided indices', async () => { (checkPrivileges as jest.Mock).mockResolvedValueOnce({ username: 'elastic', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 2025ba512cb65..14a65bc1eeb7b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -24,6 +24,7 @@ import { isThresholdRule, isEqlRule, isThreatMatchRule, + hasLargeValueItem, } from '../../../../common/detection_engine/utils'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { SetupPlugins } from '../../../plugin'; @@ -365,6 +366,12 @@ export const signalRulesAlertType = ({ }), ]); } else if (isThresholdRule(type) && threshold) { + if (hasLargeValueItem(exceptionItems ?? [])) { + await ruleStatusService.warning( + 'Exceptions that use "is in list" or "is not in list" operators are not applied to Threshold rules' + ); + wroteWarningStatus = true; + } const inputIndex = await getInputIndex(services, version, index); const thresholdFields = Array.isArray(threshold.field) @@ -552,6 +559,12 @@ export const signalRulesAlertType = ({ if (query === undefined) { throw new Error('EQL query rule must have a query defined'); } + if (hasLargeValueItem(exceptionItems ?? [])) { + await ruleStatusService.warning( + 'Exceptions that use "is in list" or "is not in list" operators are not applied to EQL rules' + ); + wroteWarningStatus = true; + } try { const signalIndexVersion = await getIndexVersion(services.callCluster, outputIndex); if (isOutdated({ current: signalIndexVersion, target: MIN_EQL_RULE_INDEX_VERSION })) { diff --git a/x-pack/test/accessibility/apps/search_profiler.ts b/x-pack/test/accessibility/apps/search_profiler.ts index 7fba45175c831..6559d58be6298 100644 --- a/x-pack/test/accessibility/apps/search_profiler.ts +++ b/x-pack/test/accessibility/apps/search_profiler.ts @@ -15,7 +15,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const flyout = getService('flyout'); - describe('Accessibility Search Profiler Editor', () => { + // FLAKY: https://github.com/elastic/kibana/issues/91939 + describe.skip('Accessibility Search Profiler Editor', () => { before(async () => { await PageObjects.common.navigateToApp('searchProfiler'); await a11y.testAppSnapshot(); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts index f908a369b46d7..c58ca0242a5b5 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts @@ -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 diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts index 585333291111e..2d8e4c44e023e 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts @@ -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); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts index 1af16f9e54563..264103a2052e5 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts @@ -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); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts index 389ec3f088f95..bf63c55938dfe 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts @@ -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); }); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts index 86b1c3031cbef..6d9962e938249 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts @@ -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', diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts index fb095c117cdfb..9447f7ad3613c 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts @@ -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); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts index 8edc3b0d08113..5e761e4d7e33a 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts @@ -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); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts index a2bc0acbcf17c..7514044d376ca 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts @@ -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); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts index 537afbe825068..1d8216ded8b7c 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts @@ -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); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts index 3463b37250980..4fd4cd6ec7542 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts @@ -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'], }, ], diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts index cd5a1ed85742f..dff462d78ba82 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts @@ -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, diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts index 49b3c0b1f465b..5a1da194a721f 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts @@ -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', }, ], diff --git a/x-pack/test/case_api_integration/common/lib/mock.ts b/x-pack/test/case_api_integration/common/lib/mock.ts index f6fd2b1a6b3be..c3c37bd20f140 100644 --- a/x-pack/test/case_api_integration/common/lib/mock.ts +++ b/x-pack/test/case_api_integration/common/lib/mock.ts @@ -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; - subCase?: Partial; + subCases?: Array>; comments?: Array>; } -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 -): Partial => { + config: Partial +): Partial => { // 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; }; diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 7aee6170c3d5a..3ade7ef96f9dd 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -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; } diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index 6c9eb24070d8f..b9c1767e4a8cf 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -15,7 +15,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); - describe('overview page', function () { + // FLAKY: https://github.com/elastic/kibana/issues/89072 + describe.skip('overview page', function () { const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078';