From 6e1cac8dadac58c7d93625b678db1fda342acacb Mon Sep 17 00:00:00 2001 From: Ievgen Sorokopud Date: Thu, 4 Aug 2022 10:26:44 +0200 Subject: [PATCH 1/5] [Security Solution] ML rule preview results button is disabled when ML job is running (#137878) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../detections/components/rules/rule_preview/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx index 9744e3fcf720e..dc06ad7c8e840 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx @@ -140,9 +140,10 @@ const RulePreviewComponent: React.FC = ({ return true; // Don't do the expensive logic if we don't need it } if (isMlLoading) { - const selectedJobs = jobs.filter(({ id }) => machineLearningJobId.includes(id)); - return selectedJobs.every((job) => isJobStarted(job.jobState, job.datafeedState)); + return false; } + const selectedJobs = jobs.filter(({ id }) => machineLearningJobId.includes(id)); + return selectedJobs.every((job) => isJobStarted(job.jobState, job.datafeedState)); }, [jobs, machineLearningJobId, ruleType, isMlLoading]); const [queryPreviewIdSelected, setQueryPreviewRadioIdSelected] = useState(QUICK_QUERY_SELECT_ID); From fc4a49532fab5c9ec92a76a1c8b97d550aa36ca4 Mon Sep 17 00:00:00 2001 From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> Date: Thu, 4 Aug 2022 09:27:26 +0100 Subject: [PATCH 2/5] [Security Solution][Detections] fixes timeline setting to None functional test for bulk edit (#137980) ## Summary Improves timeline removal functional test for bulk edit, by setting default timeline properties for rule, that being tested ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../group1/perform_bulk_action.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action.ts index 079e378533d5a..2761db1abbb85 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action.ts @@ -634,8 +634,18 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should correctly remove timeline', async () => { + const timelineId = 'test-id'; + const timelineTitle = 'Test timeline template'; const ruleId = 'ruleId'; - await createRule(supertest, log, getSimpleRule(ruleId)); + const createdRule = await createRule(supertest, log, { + ...getSimpleRule(ruleId), + timeline_id: 'test-id', + timeline_title: 'Test timeline template', + }); + + // ensure rule has been created with timeline properties + expect(createdRule.timeline_id).to.be(timelineId); + expect(createdRule.timeline_title).to.be(timelineTitle); const { body } = await postBulkAction() .send({ From 15a099dfff6ac6813db501e9ebdec85cba057703 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 4 Aug 2022 10:29:52 +0200 Subject: [PATCH 3/5] [Discover] Add unit test to discover_main_route.test.tsx (#137646) --- .../main/discover_main_route.test.tsx | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 src/plugins/discover/public/application/main/discover_main_route.test.tsx diff --git a/src/plugins/discover/public/application/main/discover_main_route.test.tsx b/src/plugins/discover/public/application/main/discover_main_route.test.tsx new file mode 100644 index 0000000000000..6734864210ee6 --- /dev/null +++ b/src/plugins/discover/public/application/main/discover_main_route.test.tsx @@ -0,0 +1,105 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { waitFor } from '@testing-library/react'; +import { setHeaderActionMenuMounter } from '../../kibana_services'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { discoverServiceMock } from '../../__mocks__/services'; +import { DiscoverMainRoute } from './discover_main_route'; +import { MemoryRouter } from 'react-router-dom'; +import { dataViewMock } from '../../__mocks__/data_view'; +import { SavedObject } from '@kbn/core/public'; +import { DataViewSavedObjectAttrs } from '@kbn/data-views-plugin/common'; +import { DiscoverMainApp } from './discover_main_app'; +import { SearchSource } from '@kbn/data-plugin/common'; +import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; +import { findTestSubject } from '@elastic/eui/lib/test'; +jest.mock('./discover_main_app', () => { + return { + DiscoverMainApp: jest.fn(), + }; +}); + +describe('DiscoverMainRoute', () => { + test('renders the main app when hasESData=true & hasUserDataView=true ', async () => { + const component = mountComponent(true, true); + + await waitFor(() => { + component.update(); + expect(component.find(DiscoverMainApp).exists()).toBe(true); + }); + }); + + test('renders no data page when hasESData=false & hasUserDataView=false', async () => { + const component = mountComponent(false, false); + + await waitFor(() => { + component.update(); + expect(findTestSubject(component, 'kbnNoDataPage').length).toBe(1); + }); + }); + test('renders no data view when hasESData=true & hasUserDataView=false', async () => { + const component = mountComponent(true, false); + + await waitFor(() => { + component.update(); + expect(findTestSubject(component, 'noDataViewsPrompt').length).toBe(1); + }); + }); + // skipped because this is the case that never ever should happen, it happened once and was fixed in + // https://github.com/elastic/kibana/pull/137824 + test.skip('renders no data page when hasESData=false & hasUserDataView=true', async () => { + const component = mountComponent(false, true); + + await waitFor(() => { + component.update(); + expect(findTestSubject(component, 'kbnNoDataPage').length).toBe(1); + }); + }); +}); +const mountComponent = (hasESData = true, hasUserDataView = true) => { + const props = { + isDev: false, + }; + + return mountWithIntl( + + + + + + ); +}; +function getServicesMock(hasESData = true, hasUserDataView = true) { + const dataViewsMock = discoverServiceMock.data.dataViews; + dataViewsMock.getCache = jest.fn(() => { + return Promise.resolve([dataViewMock as unknown as SavedObject]); + }); + dataViewsMock.get = jest.fn(() => Promise.resolve(dataViewMock)); + dataViewsMock.getDefaultDataView = jest.fn(() => Promise.resolve(dataViewMock)); + dataViewsMock.hasData = { + hasESData: jest.fn(() => Promise.resolve(hasESData)), + hasUserDataView: jest.fn(() => Promise.resolve(hasUserDataView)), + hasDataView: jest.fn(() => Promise.resolve(true)), + }; + dataViewsMock.refreshFields = jest.fn(); + + discoverServiceMock.data.search.searchSource.createEmpty = jest.fn(() => { + const fields: Record = {}; + const empty = { + ...searchSourceInstanceMock, + setField: (key: string, value: unknown) => (fields[key] = value), + getField: (key: string) => fields[key], + }; + return empty as unknown as SearchSource; + }); + return discoverServiceMock; +} + +setHeaderActionMenuMounter(jest.fn()); From 027b3a6839c5a5d2423cdaed02fe424ee3240383 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 4 Aug 2022 11:40:24 +0300 Subject: [PATCH 4/5] [Cases] Fix attachment owner when selecting solution (#137500) * Make icons bigger * Get the title of the app if there is no category * Get default owners from constant * Render fixture app * Set attachment owner from case * Fix bug * Fix tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../client/helpers/group_alerts_by_rule.ts | 46 +++--- .../cases/public/common/use_cases_toast.tsx | 10 +- .../components/add_comment/index.test.tsx | 9 +- .../public/components/add_comment/index.tsx | 3 +- .../components/all_cases/all_cases_list.tsx | 2 +- .../public/components/all_cases/columns.tsx | 7 +- .../use_cases_add_to_existing_case_modal.tsx | 7 +- .../all_cases/table_filters.test.tsx | 70 ++++++--- .../components/all_cases/table_filters.tsx | 2 +- .../cases_context/use_application.tsx | 4 +- .../create/flyout/create_case_flyout.tsx | 4 +- .../use_cases_add_to_new_case_flyout.tsx | 4 +- .../cases/public/components/create/form.tsx | 4 +- .../components/create/form_context.test.tsx | 6 +- .../public/components/create/form_context.tsx | 5 +- .../components/create/owner_selector.tsx | 4 +- .../use_create_attachments.test.tsx | 47 ++++-- .../containers/use_create_attachments.tsx | 10 +- x-pack/plugins/cases/public/index.tsx | 6 +- x-pack/plugins/cases/public/types.ts | 3 + .../hooks/use_alert_bulk_case_actions.ts | 9 +- .../components/observability_actions.tsx | 6 +- .../use_add_to_existing_case.tsx | 4 - .../use_add_to_new_case.tsx | 4 - .../use_add_to_case_actions.tsx | 6 +- .../use_bulk_add_to_case_actions.tsx | 5 +- .../test/functional/services/cases/common.ts | 7 + .../test/functional/services/cases/create.ts | 34 +++-- x-pack/test/functional/services/cases/list.ts | 9 ++ .../apps/cases/attachment_framework.ts | 143 ++++++++++++++++++ .../apps/cases/create_case_form.ts | 7 +- .../fixtures/plugins/cases/kibana.json | 2 +- .../plugins/cases/public/application.tsx | 129 ++++++++++++++++ .../fixtures/plugins/cases/public/plugin.ts | 15 +- 34 files changed, 503 insertions(+), 130 deletions(-) create mode 100644 x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/public/application.tsx diff --git a/x-pack/plugins/cases/public/client/helpers/group_alerts_by_rule.ts b/x-pack/plugins/cases/public/client/helpers/group_alerts_by_rule.ts index 0c1d73ba03b71..09e334cb3c848 100644 --- a/x-pack/plugins/cases/public/client/helpers/group_alerts_by_rule.ts +++ b/x-pack/plugins/cases/public/client/helpers/group_alerts_by_rule.ts @@ -8,7 +8,7 @@ import { CommentRequestAlertType } from '../../../common/api'; import { CommentType, Ecs } from '../../../common'; import { getRuleIdFromEvent } from './get_rule_id_from_event'; -import { CaseAttachments } from '../../types'; +import { CaseAttachmentsWithoutOwner } from '../../types'; type Maybe = T | null; interface Event { @@ -20,25 +20,29 @@ interface EventNonEcsData { value?: Maybe; } -export const groupAlertsByRule = (items: Event[], owner: string): CaseAttachments => { - const attachmentsByRule = items.reduce>((acc, item) => { - const rule = getRuleIdFromEvent(item); - if (!acc[rule.id]) { - acc[rule.id] = { - alertId: [], - index: [], - owner, - type: CommentType.alert as const, - rule, - }; - } - const alerts = acc[rule.id].alertId; - const indexes = acc[rule.id].index; - if (Array.isArray(alerts) && Array.isArray(indexes)) { - alerts.push(item.ecs._id ?? ''); - indexes.push(item.ecs._index ?? ''); - } - return acc; - }, {}); +type CommentRequestAlertTypeWithoutOwner = Omit; + +export const groupAlertsByRule = (items: Event[]): CaseAttachmentsWithoutOwner => { + const attachmentsByRule = items.reduce>( + (acc, item) => { + const rule = getRuleIdFromEvent(item); + if (!acc[rule.id]) { + acc[rule.id] = { + alertId: [], + index: [], + type: CommentType.alert as const, + rule, + }; + } + const alerts = acc[rule.id].alertId; + const indexes = acc[rule.id].index; + if (Array.isArray(alerts) && Array.isArray(indexes)) { + alerts.push(item.ecs._id ?? ''); + indexes.push(item.ecs._index ?? ''); + } + return acc; + }, + {} + ); return Object.values(attachmentsByRule); }; diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.tsx index 36ca8340036a5..5e88831144b6b 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.tsx @@ -12,7 +12,7 @@ import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { Case, CommentType } from '../../common'; import { useToasts } from './lib/kibana'; import { useCaseViewNavigation } from './navigation'; -import { CaseAttachments } from '../types'; +import { CaseAttachmentsWithoutOwner } from '../types'; import { CASE_ALERT_SUCCESS_SYNC_TEXT, CASE_ALERT_SUCCESS_TOAST, @@ -34,7 +34,7 @@ const EuiTextStyled = styled(EuiText)` `} `; -function getAlertsCount(attachments: CaseAttachments): number { +function getAlertsCount(attachments: CaseAttachmentsWithoutOwner): number { let alertsCount = 0; for (const attachment of attachments) { if (attachment.type === CommentType.alert) { @@ -57,7 +57,7 @@ function getToastTitle({ }: { theCase: Case; title?: string; - attachments?: CaseAttachments; + attachments?: CaseAttachmentsWithoutOwner; }): string { if (title !== undefined) { return title; @@ -78,7 +78,7 @@ function getToastContent({ }: { theCase: Case; content?: string; - attachments?: CaseAttachments; + attachments?: CaseAttachmentsWithoutOwner; }): string | undefined { if (content !== undefined) { return content; @@ -106,7 +106,7 @@ export const useCasesToast = () => { content, }: { theCase: Case; - attachments?: CaseAttachments; + attachments?: CaseAttachmentsWithoutOwner; title?: string; content?: string; }) => { diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index 8def93fef325d..d8bb10fce78fc 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -12,12 +12,13 @@ import { noop } from 'lodash/fp'; import { noCreateCasesPermissions, TestProviders } from '../../common/mock'; -import { CommentRequest, CommentType } from '../../../common/api'; +import { CommentType } from '../../../common/api'; import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { useCreateAttachments } from '../../containers/use_create_attachments'; import { AddComment, AddCommentProps, AddCommentRefObject } from '.'; import { CasesTimelineIntegrationProvider } from '../timeline_context'; import { timelineIntegrationMock } from '../__mock__/timeline'; +import { CaseAttachmentWithoutOwner } from '../../types'; jest.mock('../../containers/use_create_attachments'); @@ -41,10 +42,9 @@ const defaultResponse = { createAttachments, }; -const sampleData: CommentRequest = { +const sampleData: CaseAttachmentWithoutOwner = { comment: 'what a cool comment', - type: CommentType.user, - owner: SECURITY_SOLUTION_OWNER, + type: CommentType.user as const, }; describe('AddComment ', () => { @@ -73,6 +73,7 @@ describe('AddComment ', () => { expect(onCommentSaving).toBeCalled(); expect(createAttachments).toBeCalledWith({ caseId: addCommentProps.caseId, + caseOwner: SECURITY_SOLUTION_OWNER, data: [sampleData], updateCase: onCommentPosted, }); diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index 98e505b113ecd..6710a4a4471e0 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -105,7 +105,8 @@ export const AddComment = React.memo( } createAttachments({ caseId, - data: [{ ...data, type: CommentType.user, owner: owner[0] }], + caseOwner: owner[0], + data: [{ ...data, type: CommentType.user }], updateCase: onCommentPosted, }); reset(); diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx index 46ff28e96160e..3c056ccf996dd 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx @@ -70,7 +70,7 @@ export const AllCasesList = React.memo( const firstAvailableStatus = head(difference(caseStatuses, hiddenStatuses)); const initialFilterOptions = { ...(!isEmpty(hiddenStatuses) && firstAvailableStatus && { status: firstAvailableStatus }), - owner: hasOwner ? owner : availableSolutions, + owner: hasOwner ? owner : [], }; const [filterOptions, setFilterOptions] = useState({ ...DEFAULT_FILTER_OPTIONS, diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index e4a6927c2e840..0929f8971cf06 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -250,7 +250,12 @@ export const useCasesColumns = ({ render: (caseOwner: CasesOwners) => { const ownerInfo = OWNER_INFO[caseOwner]; return ownerInfo ? ( - + ) : ( getEmptyTagValue() ); diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx index 2e7a12ae6d68e..8152986e6c75b 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx @@ -13,7 +13,7 @@ import { Case } from '../../../containers/types'; import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer'; import { useCasesContext } from '../../cases_context/use_cases_context'; import { useCasesAddToNewCaseFlyout } from '../../create/flyout/use_cases_add_to_new_case_flyout'; -import { CaseAttachments } from '../../../types'; +import { CaseAttachmentsWithoutOwner } from '../../../types'; import { useCreateAttachments } from '../../../containers/use_create_attachments'; type AddToExistingFlyoutProps = AllCasesSelectorModalProps & { @@ -51,7 +51,7 @@ export const useCasesAddToExistingCaseModal = (props: AddToExistingFlyoutProps = }, [dispatch]); const handleOnRowClick = useCallback( - async (theCase: Case | undefined, attachments: CaseAttachments) => { + async (theCase: Case | undefined, attachments: CaseAttachmentsWithoutOwner) => { // when the case is undefined in the modal // the user clicked "create new case" if (theCase === undefined) { @@ -65,6 +65,7 @@ export const useCasesAddToExistingCaseModal = (props: AddToExistingFlyoutProps = if (attachments !== undefined && attachments.length > 0) { await createAttachments({ caseId: theCase.id, + caseOwner: theCase.owner, data: attachments, throwOnError: true, }); @@ -89,7 +90,7 @@ export const useCasesAddToExistingCaseModal = (props: AddToExistingFlyoutProps = ); const openModal = useCallback( - ({ attachments }: { attachments?: CaseAttachments } = {}) => { + ({ attachments }: { attachments?: CaseAttachmentsWithoutOwner } = {}) => { dispatch({ type: CasesContextStoreActionsList.OPEN_ADD_TO_CASE_MODAL, payload: { diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx index 63c5b55099cf3..9df5757525202 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx @@ -190,7 +190,7 @@ describe('CasesTableFilters ', () => { ); }); - describe('dynamic Solution filter', () => { + describe('Solution filter', () => { it('shows Solution filter when provided more than 1 availableSolutions', () => { const wrapper = mount( @@ -215,28 +215,58 @@ describe('CasesTableFilters ', () => { wrapper.find(`[data-test-subj="options-filter-popover-button-Solution"]`).exists() ).toBeFalsy(); }); - }); - it('should call onFilterChange when selected solution changes', () => { - const wrapper = mount( - - - - ); - wrapper - .find(`[data-test-subj="options-filter-popover-button-Solution"]`) - .last() - .simulate('click'); + it('should call onFilterChange when selected solution changes', () => { + const wrapper = mount( + + + + ); + wrapper + .find(`[data-test-subj="options-filter-popover-button-Solution"]`) + .last() + .simulate('click'); - wrapper - .find(`[data-test-subj="options-filter-popover-item-${SECURITY_SOLUTION_OWNER}"]`) - .last() - .simulate('click'); + wrapper + .find(`[data-test-subj="options-filter-popover-item-${SECURITY_SOLUTION_OWNER}"]`) + .last() + .simulate('click'); + + expect(onFilterChanged).toBeCalledWith({ owner: [SECURITY_SOLUTION_OWNER] }); + }); - expect(onFilterChanged).toBeCalledWith({ owner: [SECURITY_SOLUTION_OWNER] }); + it('should deselect all solutions', () => { + const wrapper = mount( + + + + ); + + wrapper + .find(`[data-test-subj="options-filter-popover-button-Solution"]`) + .last() + .simulate('click'); + + wrapper + .find(`[data-test-subj="options-filter-popover-item-${SECURITY_SOLUTION_OWNER}"]`) + .last() + .simulate('click'); + + expect(onFilterChanged).toBeCalledWith({ owner: [SECURITY_SOLUTION_OWNER] }); + + wrapper + .find(`[data-test-subj="options-filter-popover-item-${SECURITY_SOLUTION_OWNER}"]`) + .last() + .simulate('click'); + + expect(onFilterChanged).toBeCalledWith({ owner: [] }); + }); }); describe('create case button', () => { diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx index 5d0d6ca0017a6..cedd7c9b64718 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx @@ -112,7 +112,7 @@ const CasesTableFiltersComponent = ({ const handleSelectedSolution = useCallback( (newOwner) => { - if (!isEqual(newOwner, selectedOwner) && newOwner.length) { + if (!isEqual(newOwner, selectedOwner)) { setSelectedOwner(newOwner); onFilterChanged({ owner: newOwner }); } diff --git a/x-pack/plugins/cases/public/components/cases_context/use_application.tsx b/x-pack/plugins/cases/public/components/cases_context/use_application.tsx index 86cfded0bc9d0..f21c40b6ee603 100644 --- a/x-pack/plugins/cases/public/components/cases_context/use_application.tsx +++ b/x-pack/plugins/cases/public/components/cases_context/use_application.tsx @@ -18,7 +18,9 @@ export const useApplication = (): UseApplicationReturn => { const appId = useObservable(currentAppId$); const applications = useObservable(applications$); - const appTitle = appId ? applications?.get(appId)?.category?.label : undefined; + const appTitle = appId + ? applications?.get(appId)?.category?.label ?? applications?.get(appId)?.title + : undefined; return { appId, appTitle }; }; diff --git a/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.tsx b/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.tsx index 94e72c43f6ad9..be758e1718451 100644 --- a/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.tsx +++ b/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.tsx @@ -14,7 +14,7 @@ import * as i18n from '../translations'; import { Case } from '../../../../common/ui/types'; import { CreateCaseForm } from '../form'; import { UseCreateAttachments } from '../../../containers/use_create_attachments'; -import { CaseAttachments } from '../../../types'; +import { CaseAttachmentsWithoutOwner } from '../../../types'; import { casesQueryClient } from '../../cases_context/query_client'; export interface CreateCaseFlyoutProps { @@ -24,7 +24,7 @@ export interface CreateCaseFlyoutProps { ) => Promise; onClose?: () => void; onSuccess?: (theCase: Case) => Promise; - attachments?: CaseAttachments; + attachments?: CaseAttachmentsWithoutOwner; } const StyledFlyout = styled(EuiFlyout)` diff --git a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx index 8dc49758695e5..8b2d2b02cce79 100644 --- a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx +++ b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx @@ -6,7 +6,7 @@ */ import { useCallback } from 'react'; -import { CaseAttachments } from '../../../types'; +import { CaseAttachmentsWithoutOwner } from '../../../types'; import { useCasesToast } from '../../../common/use_cases_toast'; import { Case } from '../../../containers/types'; import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer'; @@ -29,7 +29,7 @@ export const useCasesAddToNewCaseFlyout = (props: AddToNewCaseFlyoutProps = {}) }, [dispatch]); const openFlyout = useCallback( - ({ attachments }: { attachments?: CaseAttachments } = {}) => { + ({ attachments }: { attachments?: CaseAttachmentsWithoutOwner } = {}) => { dispatch({ type: CasesContextStoreActionsList.OPEN_CREATE_CASE_FLYOUT, payload: { diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx index 50a3c69f2073e..78f5a4e9d5c54 100644 --- a/x-pack/plugins/cases/public/components/create/form.tsx +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -34,7 +34,7 @@ import { useCasesFeatures } from '../cases_context/use_cases_features'; import { CreateCaseOwnerSelector } from './owner_selector'; import { useCasesContext } from '../cases_context/use_cases_context'; import { useAvailableCasesOwners } from '../app/use_available_owners'; -import { CaseAttachments } from '../../types'; +import { CaseAttachmentsWithoutOwner } from '../../types'; import { Severity } from './severity'; interface ContainerProps { @@ -67,7 +67,7 @@ export interface CreateCaseFormProps extends Pick Promise; timelineIntegration?: CasesTimelineIntegration; - attachments?: CaseAttachments; + attachments?: CaseAttachmentsWithoutOwner; } const empty: ActionConnector[] = []; diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index bdc6f68ddf077..57bc23d9aaa02 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -822,7 +822,11 @@ describe('Create case', () => { }); expect(createAttachments).toHaveBeenCalledTimes(1); - expect(createAttachments).toHaveBeenCalledWith({ caseId: 'case-id', data: attachments }); + expect(createAttachments).toHaveBeenCalledWith({ + caseId: 'case-id', + data: attachments, + caseOwner: 'securitySolution', + }); }); it('should NOT call createAttachments if the attachments are an empty array', async () => { diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx index 70b4fb4ec9ab0..25365050e54d8 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -21,7 +21,7 @@ import { import { useCasesContext } from '../cases_context/use_cases_context'; import { useCasesFeatures } from '../cases_context/use_cases_features'; import { getConnectorById } from '../utils'; -import { CaseAttachments } from '../../types'; +import { CaseAttachmentsWithoutOwner } from '../../types'; import { useGetConnectors } from '../../containers/configure/use_connectors'; const initialCaseValue: FormProps = { @@ -42,7 +42,7 @@ interface Props { ) => Promise; children?: JSX.Element | JSX.Element[]; onSuccess?: (theCase: Case) => Promise; - attachments?: CaseAttachments; + attachments?: CaseAttachmentsWithoutOwner; } export const FormContext: React.FC = ({ @@ -87,6 +87,7 @@ export const FormContext: React.FC = ({ if (updatedCase && Array.isArray(attachments) && attachments.length > 0) { await createAttachments({ caseId: updatedCase.id, + caseOwner: updatedCase.owner, data: attachments, }); } diff --git a/x-pack/plugins/cases/public/components/create/owner_selector.tsx b/x-pack/plugins/cases/public/components/create/owner_selector.tsx index 34bef0b98ff78..bcc8709ee5c5d 100644 --- a/x-pack/plugins/cases/public/components/create/owner_selector.tsx +++ b/x-pack/plugins/cases/public/components/create/owner_selector.tsx @@ -19,7 +19,7 @@ import { import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { SECURITY_SOLUTION_OWNER } from '../../../common'; -import { OBSERVABILITY_OWNER, OWNER_INFO } from '../../../common/constants'; +import { OWNER_INFO } from '../../../common/constants'; import { FieldHook, getFieldValidityAndErrorMessage, UseField } from '../../common/shared_imports'; import * as i18n from './translations'; @@ -35,7 +35,7 @@ interface Props { isLoading: boolean; } -const DEFAULT_SELECTABLE_OWNERS = [SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER] as const; +const DEFAULT_SELECTABLE_OWNERS = Object.keys(OWNER_INFO) as Array; const FIELD_NAME = 'selectedOwner'; diff --git a/x-pack/plugins/cases/public/containers/use_create_attachments.test.tsx b/x-pack/plugins/cases/public/containers/use_create_attachments.test.tsx index 428b28e02dcc5..7a4b1c7f6523a 100644 --- a/x-pack/plugins/cases/public/containers/use_create_attachments.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_create_attachments.test.tsx @@ -26,13 +26,18 @@ describe('useCreateAttachments', () => { }); const abortCtrl = new AbortController(); - const samplePost = [ + const attachmentsWithoutOwner = [ { comment: 'a comment', type: CommentType.user as const, - owner: SECURITY_SOLUTION_OWNER, }, ]; + + const attachmentsWithOwner = attachmentsWithoutOwner.map((attachment) => ({ + ...attachment, + owner: SECURITY_SOLUTION_OWNER, + })); + const updateCaseCallback = jest.fn(); beforeEach(() => { jest.clearAllMocks(); @@ -64,11 +69,17 @@ describe('useCreateAttachments', () => { result.current.createAttachments({ caseId: basicCaseId, - data: samplePost, + caseOwner: SECURITY_SOLUTION_OWNER, + data: attachmentsWithoutOwner, updateCase: updateCaseCallback, }); + await waitForNextUpdate(); - expect(spyOnBulkCreateAttachments).toBeCalledWith(samplePost, basicCaseId, abortCtrl.signal); + expect(spyOnBulkCreateAttachments).toBeCalledWith( + attachmentsWithOwner, + basicCaseId, + abortCtrl.signal + ); expect(toastErrorMock).not.toHaveBeenCalled(); }); }); @@ -84,11 +95,17 @@ describe('useCreateAttachments', () => { result.current.createAttachments({ caseId: basicCaseId, - data: samplePost, + caseOwner: SECURITY_SOLUTION_OWNER, + data: attachmentsWithoutOwner, updateCase: updateCaseCallback, }); + await waitForNextUpdate(); - expect(spyOnBulkCreateAttachments).toBeCalledWith(samplePost, basicCaseId, abortCtrl.signal); + expect(spyOnBulkCreateAttachments).toBeCalledWith( + attachmentsWithOwner, + basicCaseId, + abortCtrl.signal + ); expect(toastErrorMock).not.toHaveBeenCalled(); }); }); @@ -98,12 +115,15 @@ describe('useCreateAttachments', () => { const { result, waitForNextUpdate } = renderHook(() => useCreateAttachments() ); + await waitForNextUpdate(); result.current.createAttachments({ caseId: basicCaseId, - data: samplePost, + caseOwner: SECURITY_SOLUTION_OWNER, + data: attachmentsWithoutOwner, updateCase: updateCaseCallback, }); + await waitForNextUpdate(); expect(result.current).toEqual({ isLoading: false, @@ -118,10 +138,12 @@ describe('useCreateAttachments', () => { const { result, waitForNextUpdate } = renderHook(() => useCreateAttachments() ); + await waitForNextUpdate(); result.current.createAttachments({ caseId: basicCaseId, - data: samplePost, + caseOwner: SECURITY_SOLUTION_OWNER, + data: attachmentsWithoutOwner, updateCase: updateCaseCallback, }); @@ -139,10 +161,12 @@ describe('useCreateAttachments', () => { const { result, waitForNextUpdate } = renderHook(() => useCreateAttachments() ); + await waitForNextUpdate(); result.current.createAttachments({ caseId: basicCaseId, - data: samplePost, + caseOwner: SECURITY_SOLUTION_OWNER, + data: attachmentsWithoutOwner, updateCase: updateCaseCallback, }); @@ -168,11 +192,14 @@ describe('useCreateAttachments', () => { const { result, waitForNextUpdate } = renderHook(() => useCreateAttachments() ); + await waitForNextUpdate(); + async function test() { await result.current.createAttachments({ caseId: basicCaseId, - data: samplePost, + caseOwner: SECURITY_SOLUTION_OWNER, + data: attachmentsWithoutOwner, updateCase: updateCaseCallback, throwOnError: true, }); diff --git a/x-pack/plugins/cases/public/containers/use_create_attachments.tsx b/x-pack/plugins/cases/public/containers/use_create_attachments.tsx index e6de525f36e6a..779677cd89cc6 100644 --- a/x-pack/plugins/cases/public/containers/use_create_attachments.tsx +++ b/x-pack/plugins/cases/public/containers/use_create_attachments.tsx @@ -6,12 +6,12 @@ */ import { useReducer, useCallback, useRef, useEffect } from 'react'; -import { BulkCreateCommentRequest } from '../../common/api'; import { createAttachments } from './api'; import * as i18n from './translations'; import { Case } from './types'; import { useToasts } from '../common/lib/kibana'; +import { CaseAttachmentsWithoutOwner } from '../types'; interface NewCommentState { isLoading: boolean; @@ -43,7 +43,8 @@ const dataFetchReducer = (state: NewCommentState, action: Action): NewCommentSta export interface PostComment { caseId: string; - data: BulkCreateCommentRequest; + caseOwner: string; + data: CaseAttachmentsWithoutOwner; updateCase?: (newCase: Case) => void; throwOnError?: boolean; } @@ -61,14 +62,15 @@ export const useCreateAttachments = (): UseCreateAttachments => { const abortCtrlRef = useRef(new AbortController()); const fetch = useCallback( - async ({ caseId, data, updateCase, throwOnError }: PostComment) => { + async ({ caseId, caseOwner, data, updateCase, throwOnError }: PostComment) => { try { isCancelledRef.current = false; abortCtrlRef.current.abort(); abortCtrlRef.current = new AbortController(); dispatch({ type: 'FETCH_INIT' }); - const response = await createAttachments(data, caseId, abortCtrlRef.current.signal); + const attachments = data.map((attachment) => ({ ...attachment, owner: caseOwner })); + const response = await createAttachments(attachments, caseId, abortCtrlRef.current.signal); if (!isCancelledRef.current) { dispatch({ type: 'FETCH_SUCCESS' }); diff --git a/x-pack/plugins/cases/public/index.tsx b/x-pack/plugins/cases/public/index.tsx index e267960d91a80..554fa8bad2688 100644 --- a/x-pack/plugins/cases/public/index.tsx +++ b/x-pack/plugins/cases/public/index.tsx @@ -21,7 +21,11 @@ export type { GetCreateCaseFlyoutProps } from './client/ui/get_create_case_flyou export type { GetAllCasesSelectorModalProps } from './client/ui/get_all_cases_selector_modal'; export type { GetRecentCasesProps } from './client/ui/get_recent_cases'; -export type { CaseAttachments, SupportedCaseAttachment } from './types'; +export type { + CaseAttachments, + SupportedCaseAttachment, + CaseAttachmentsWithoutOwner, +} from './types'; export type { ICasesDeepLinkId } from './common/navigation'; export { diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts index f07dd8cd9fb04..90d4179553771 100644 --- a/x-pack/plugins/cases/public/types.ts +++ b/x-pack/plugins/cases/public/types.ts @@ -18,6 +18,7 @@ import type { LensPublicStart } from '@kbn/lens-plugin/public'; import type { SecurityPluginSetup } from '@kbn/security-plugin/public'; import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import type { TriggersAndActionsUIPublicPluginStart as TriggersActionsStart } from '@kbn/triggers-actions-ui-plugin/public'; +import type { DistributiveOmit } from '@elastic/eui'; import type { CasesByAlertId, CasesByAlertIDRequest, @@ -147,5 +148,7 @@ export interface CasesUiStart { export type SupportedCaseAttachment = CommentRequestAlertType | CommentRequestUserType; export type CaseAttachments = SupportedCaseAttachment[]; +export type CaseAttachmentWithoutOwner = DistributiveOmit; +export type CaseAttachmentsWithoutOwner = CaseAttachmentWithoutOwner[]; export type ServerError = IHttpFetchError; diff --git a/x-pack/plugins/observability/public/hooks/use_alert_bulk_case_actions.ts b/x-pack/plugins/observability/public/hooks/use_alert_bulk_case_actions.ts index 0ca4f5a7187ba..40219feb09c21 100644 --- a/x-pack/plugins/observability/public/hooks/use_alert_bulk_case_actions.ts +++ b/x-pack/plugins/observability/public/hooks/use_alert_bulk_case_actions.ts @@ -15,7 +15,6 @@ import { } from '../pages/alerts/containers/alerts_table_t_grid/translations'; import { useGetUserCasesPermissions } from './use_get_user_cases_permissions'; import { ObservabilityAppServices } from '../application/types'; -import { observabilityFeatureId } from '../../common'; export interface UseAddToCaseActions { onClose?: () => void; @@ -46,9 +45,7 @@ export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActi disableOnQuery: true, disabledLabel: ADD_TO_CASE_DISABLED, onClick: (items?: TimelineItem[]) => { - const caseAttachments = items - ? casesUi.helpers.groupAlertsByRule(items, observabilityFeatureId) - : []; + const caseAttachments = items ? casesUi.helpers.groupAlertsByRule(items) : []; createCaseFlyout.open({ attachments: caseAttachments }); }, }, @@ -59,9 +56,7 @@ export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActi disabledLabel: ADD_TO_CASE_DISABLED, 'data-test-subj': 'attach-existing-case', onClick: (items?: TimelineItem[]) => { - const caseAttachments = items - ? casesUi.helpers.groupAlertsByRule(items, observabilityFeatureId) - : []; + const caseAttachments = items ? casesUi.helpers.groupAlertsByRule(items) : []; selectCaseModal.open({ attachments: caseAttachments }); }, }, diff --git a/x-pack/plugins/observability/public/pages/alerts/components/observability_actions.tsx b/x-pack/plugins/observability/public/pages/alerts/components/observability_actions.tsx index f30a815337724..ab66d058301f5 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/observability_actions.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/observability_actions.tsx @@ -16,11 +16,10 @@ import { import React, { useMemo, useState, useCallback } from 'react'; -import { CaseAttachments } from '@kbn/cases-plugin/public'; +import { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public'; import { CommentType } from '@kbn/cases-plugin/common'; import type { ActionProps } from '@kbn/timelines-plugin/common'; import { useKibana } from '../../../utils/kibana_react'; -import { observabilityFeatureId } from '../../../../common'; import { useGetUserCasesPermissions } from '../../../hooks/use_get_user_cases_permissions'; import { parseAlert } from './parse_alert'; import { translations, paths } from '../../../config'; @@ -75,13 +74,12 @@ export function ObservabilityActions({ pageId !== RULE_DETAILS_PAGE_ID && ruleId ? http.basePath.prepend(paths.observability.ruleDetails(ruleId)) : null; - const caseAttachments: CaseAttachments = useMemo(() => { + const caseAttachments: CaseAttachmentsWithoutOwner = useMemo(() => { return ecsData?._id ? [ { alertId: ecsData?._id ?? '', index: ecsData?._index ?? '', - owner: observabilityFeatureId, type: CommentType.alert, rule: cases.helpers.getRuleIdFromEvent({ ecs: ecsData, data: data ?? [] }), }, diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx index 9aa85975a1b46..059b1cfd660c6 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx @@ -7,14 +7,11 @@ import { useCallback, useMemo } from 'react'; import { CommentType } from '@kbn/cases-plugin/common'; -import { APP_ID } from '../../../../common/constants'; import { useKibana, useGetUserCasesPermissions } from '../../lib/kibana'; import { ADD_TO_CASE_SUCCESS } from './translations'; import type { LensAttributes } from './types'; -const owner = APP_ID; - export const useAddToExistingCase = ({ onAddToCaseClicked, lensAttributes, @@ -33,7 +30,6 @@ export const useAddToExistingCase = ({ timeRange, attributes: lensAttributes, })}}`, - owner, type: CommentType.user as const, }, ]; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx index a0b367738ad9b..36efbd55228f7 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx @@ -8,7 +8,6 @@ import { useCallback, useMemo } from 'react'; import { CommentType } from '@kbn/cases-plugin/common'; -import { APP_ID } from '../../../../common/constants'; import { useKibana, useGetUserCasesPermissions } from '../../lib/kibana'; import { ADD_TO_CASE_SUCCESS } from './translations'; @@ -20,8 +19,6 @@ export interface UseAddToNewCaseProps { lensAttributes: LensAttributes | null; } -const owner = APP_ID; - export const useAddToNewCase = ({ onClick, timeRange, lensAttributes }: UseAddToNewCaseProps) => { const userCasesPermissions = useGetUserCasesPermissions(); const { cases } = useKibana().services; @@ -32,7 +29,6 @@ export const useAddToNewCase = ({ onClick, timeRange, lensAttributes }: UseAddTo timeRange, attributes: lensAttributes, })}}`, - owner, type: CommentType.user as const, }, ]; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx index c8ac06cee92a0..17a5a8448a702 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx @@ -8,11 +8,10 @@ import React, { useCallback, useMemo } from 'react'; import { EuiContextMenuItem } from '@elastic/eui'; import { CommentType } from '@kbn/cases-plugin/common'; -import type { CaseAttachments } from '@kbn/cases-plugin/public'; +import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public'; import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana'; import type { TimelineNonEcsData } from '../../../../../common/search_strategy'; import { TimelineId } from '../../../../../common/types'; -import { APP_ID } from '../../../../../common/constants'; import type { Ecs } from '../../../../../common/ecs'; import { ADD_TO_EXISTING_CASE, ADD_TO_NEW_CASE } from '../translations'; @@ -40,13 +39,12 @@ export const useAddToCaseActions = ({ return ecsData?.event?.kind?.includes('signal'); }, [ecsData]); - const caseAttachments: CaseAttachments = useMemo(() => { + const caseAttachments: CaseAttachmentsWithoutOwner = useMemo(() => { return ecsData?._id ? [ { alertId: ecsData?._id ?? '', index: ecsData?._index ?? '', - owner: APP_ID, type: CommentType.alert, rule: casesUi.helpers.getRuleIdFromEvent({ ecs: ecsData, data: nonEcsData ?? [] }), }, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx index 0251c130981f6..fa397e5389725 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx @@ -6,7 +6,6 @@ */ import { useMemo } from 'react'; -import { APP_ID } from '../../../../../common/constants'; import type { TimelineItem } from '../../../../../common/search_strategy'; import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana'; import { ADD_TO_CASE_DISABLED, ADD_TO_EXISTING_CASE, ADD_TO_NEW_CASE } from '../translations'; @@ -40,7 +39,7 @@ export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActi disableOnQuery: true, disabledLabel: ADD_TO_CASE_DISABLED, onClick: (items?: TimelineItem[]) => { - const caseAttachments = items ? casesUi.helpers.groupAlertsByRule(items, APP_ID) : []; + const caseAttachments = items ? casesUi.helpers.groupAlertsByRule(items) : []; createCaseFlyout.open({ attachments: caseAttachments }); }, }, @@ -51,7 +50,7 @@ export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActi disabledLabel: ADD_TO_CASE_DISABLED, 'data-test-subj': 'attach-existing-case', onClick: (items?: TimelineItem[]) => { - const caseAttachments = items ? casesUi.helpers.groupAlertsByRule(items, APP_ID) : []; + const caseAttachments = items ? casesUi.helpers.groupAlertsByRule(items) : []; selectCaseModal.open({ attachments: caseAttachments }); }, }, diff --git a/x-pack/test/functional/services/cases/common.ts b/x-pack/test/functional/services/cases/common.ts index 5ba0c4dbbaa57..c0e2ef1068958 100644 --- a/x-pack/test/functional/services/cases/common.ts +++ b/x-pack/test/functional/services/cases/common.ts @@ -15,6 +15,7 @@ export function CasesCommonServiceProvider({ getService, getPageObject }: FtrPro const find = getService('find'); const header = getPageObject('header'); const common = getPageObject('common'); + const toasts = getService('toasts'); return { /** @@ -68,5 +69,11 @@ export function CasesCommonServiceProvider({ getService, getPageObject }: FtrPro ); await testSubjects.click(`case-severity-selection-${severity}`); }, + + async expectToasterToContain(content: string) { + const toast = await toasts.getToastElement(1); + expect(await toast.getVisibleText()).to.contain(content); + await toasts.dismissAllToasts(); + }, }; } diff --git a/x-pack/test/functional/services/cases/create.ts b/x-pack/test/functional/services/cases/create.ts index 536badeee56a6..579247161016a 100644 --- a/x-pack/test/functional/services/cases/create.ts +++ b/x-pack/test/functional/services/cases/create.ts @@ -14,7 +14,6 @@ export function CasesCreateViewServiceProvider({ getService, getPageObject }: Ft const testSubjects = getService('testSubjects'); const find = getService('find'); const comboBox = getService('comboBox'); - const config = getService('config'); return { /** @@ -35,18 +34,19 @@ export function CasesCreateViewServiceProvider({ getService, getPageObject }: Ft * and leaves the navigation in the case view page * * Doesn't do navigation. Only works if you are already inside a cases app page. - * Does not work with the cases flyout. */ - async createCaseFromCreateCasePage({ + async createCase({ title = 'test-' + uuid.v4(), description = 'desc' + uuid.v4(), tag = 'tagme', severity = CaseSeverity.LOW, + owner, }: { - title: string; - description: string; - tag: string; - severity: CaseSeverity; + title?: string; + description?: string; + tag?: string; + severity?: CaseSeverity; + owner?: string; }) { // case name await testSubjects.setValue('input', title); @@ -58,18 +58,20 @@ export function CasesCreateViewServiceProvider({ getService, getPageObject }: Ft const descriptionArea = await find.byCssSelector('textarea.euiMarkdownEditorTextArea'); await descriptionArea.focus(); await descriptionArea.type(description); - await common.clickAndValidate( - 'case-severity-selection', - `case-severity-selection-${severity}` - ); - await testSubjects.click(`case-severity-selection-${severity}`); + + if (severity !== CaseSeverity.LOW) { + await common.clickAndValidate( + 'case-severity-selection', + `case-severity-selection-${severity}` + ); + } + + if (owner) { + await testSubjects.click(`${owner}RadioButton`); + } // save await testSubjects.click('create-case-submit'); - - await testSubjects.existOrFail('case-view-title', { - timeout: config.get('timeouts.waitFor'), - }); }, }; } diff --git a/x-pack/test/functional/services/cases/list.ts b/x-pack/test/functional/services/cases/list.ts index f4d7103db0a61..95b0a746db8ca 100644 --- a/x-pack/test/functional/services/cases/list.ts +++ b/x-pack/test/functional/services/cases/list.ts @@ -141,6 +141,15 @@ export function CasesTableServiceProvider({ getService, getPageObject }: FtrProv await testSubjects.click(`options-filter-popover-item-${reporter}`); }, + async filterByOwner(owner: string) { + await common.clickAndValidate( + 'options-filter-popover-button-Solution', + `options-filter-popover-item-${owner}` + ); + + await testSubjects.click(`options-filter-popover-item-${owner}`); + }, + async refreshTable() { await testSubjects.click('all-cases-refresh'); }, diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/attachment_framework.ts b/x-pack/test/functional_with_es_ssl/apps/cases/attachment_framework.ts index 5a5a48af9b4b0..72ce196c1ae62 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/attachment_framework.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/attachment_framework.ts @@ -13,6 +13,11 @@ import { CommentRequest, } from '@kbn/cases-plugin/common/api'; import { expect } from 'expect'; +import { + deleteAllCaseItems, + findCases, + getCase, +} from '../../../cases_api_integration/common/lib/utils'; import { FtrProviderContext } from '../../ftr_provider_context'; const createLogStashDataView = async ( @@ -46,6 +51,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); const cases = getService('cases'); const find = getService('find'); + const es = getService('es'); + const common = getPageObject('common'); const createAttachmentAndNavigate = async (attachment: CommentRequest) => { const caseData = await cases.api.createCase({ @@ -200,5 +207,141 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { expect(await find.existsByCssSelector('.lnsExpressionRenderer')).toBe(true); }); }); + + /** + * The UI of the cases fixture plugin is in x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/public/application.tsx + */ + describe('Attachment hooks', () => { + const TOTAL_OWNERS = ['cases', 'securitySolution', 'observability']; + + const ensureFirstCommentOwner = async (caseId: string, owner: string) => { + const theCase = await getCase({ + supertest, + caseId, + includeComments: true, + }); + + const comment = theCase.comments![0].owner; + expect(comment).toBe(owner); + }; + + before(async () => { + await common.navigateToApp('cases_fixture'); + }); + + describe('Flyout', () => { + const openFlyout = async () => { + await common.clickAndValidate('case-fixture-attach-to-new-case', 'create-case-flyout'); + }; + + const closeFlyout = async () => { + await testSubjects.click('euiFlyoutCloseButton'); + }; + + after(async () => { + await deleteAllCaseItems(es); + }); + + it('renders solutions selection', async () => { + await openFlyout(); + + for (const owner of TOTAL_OWNERS) { + await testSubjects.existOrFail(`${owner}RadioButton`); + } + + await closeFlyout(); + }); + + it('attaches correctly', async () => { + for (const owner of TOTAL_OWNERS) { + await openFlyout(); + + /** + * The flyout close automatically after submitting a case + */ + await cases.create.createCase({ owner }); + await cases.common.expectToasterToContain('has been updated'); + } + + const casesCreatedFromFlyout = await findCases({ supertest }); + + for (const owner of TOTAL_OWNERS) { + const theCase = casesCreatedFromFlyout.cases.find((c) => c.owner === owner)!; + await ensureFirstCommentOwner(theCase.id, owner); + } + }); + }); + + describe('Modal', () => { + const createdCases = new Map(); + + const openModal = async () => { + await common.clickAndValidate('case-fixture-attach-to-existing-case', 'all-cases-modal'); + await cases.casesTable.waitForTableToFinishLoading(); + }; + + const closeModal = async () => { + await find.clickByCssSelector('[data-test-subj="all-cases-modal"] > button'); + }; + + before(async () => { + for (const owner of TOTAL_OWNERS) { + const theCase = await cases.api.createCase({ owner }); + createdCases.set(owner, theCase.id); + } + }); + + after(async () => { + await deleteAllCaseItems(es); + }); + + it('renders different solutions', async () => { + await openModal(); + + await testSubjects.existOrFail('options-filter-popover-button-Solution'); + + for (const [owner, caseId] of createdCases.entries()) { + await testSubjects.existOrFail(`cases-table-row-${caseId}`); + await testSubjects.existOrFail(`case-table-column-owner-icon-${owner}`); + } + + await closeModal(); + }); + + it('filters correctly', async () => { + for (const [owner, currentCaseId] of createdCases.entries()) { + await openModal(); + + await cases.casesTable.filterByOwner(owner); + await cases.casesTable.waitForTableToFinishLoading(); + await testSubjects.existOrFail(`cases-table-row-${currentCaseId}`); + + /** + * We ensure that the other cases are not shown + */ + for (const otherCaseId of createdCases.values()) { + if (otherCaseId !== currentCaseId) { + await testSubjects.missingOrFail(`cases-table-row-${otherCaseId}`); + } + } + + await closeModal(); + } + }); + + it('attaches correctly', async () => { + for (const [owner, currentCaseId] of createdCases.entries()) { + await openModal(); + + await cases.casesTable.waitForTableToFinishLoading(); + await testSubjects.existOrFail(`cases-table-row-${currentCaseId}`); + await testSubjects.click(`cases-table-row-select-${currentCaseId}`); + + await cases.common.expectToasterToContain('has been updated'); + await ensureFirstCommentOwner(currentCaseId, owner); + } + }); + }); + }); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/create_case_form.ts b/x-pack/test/functional_with_es_ssl/apps/cases/create_case_form.ts index c4a7fad8224ea..42ed68a9f36da 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/create_case_form.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/create_case_form.ts @@ -15,6 +15,7 @@ export default ({ getService }: FtrProviderContext) => { const find = getService('find'); const cases = getService('cases'); const testSubjects = getService('testSubjects'); + const config = getService('config'); before(async () => { await cases.navigation.navigateToApp(); @@ -27,13 +28,17 @@ export default ({ getService }: FtrProviderContext) => { it('creates a case from the stack management page', async () => { const caseTitle = 'test-' + uuid.v4(); await cases.create.openCreateCasePage(); - await cases.create.createCaseFromCreateCasePage({ + await cases.create.createCase({ title: caseTitle, description: 'test description', tag: 'tagme', severity: CaseSeverity.HIGH, }); + await testSubjects.existOrFail('case-view-title', { + timeout: config.get('timeouts.waitFor'), + }); + // validate title const title = await find.byCssSelector('[data-test-subj="header-page-title"]'); expect(await title.getVisibleText()).equal(caseTitle); diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/kibana.json b/x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/kibana.json index 86f25f410fee7..56e1f9fd62a08 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/kibana.json +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/kibana.json @@ -3,7 +3,7 @@ "owner": { "name": "Response Ops", "githubTeam": "response-ops" }, "version": "1.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["cases", "embeddable", "lens"], + "requiredPlugins": ["cases", "embeddable", "lens", "kibanaReact", "esUiShared"], "server": true, "ui": true } diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/public/application.tsx b/x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/public/application.tsx new file mode 100644 index 0000000000000..d4d893a43937c --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/public/application.tsx @@ -0,0 +1,129 @@ +/* + * 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 React from 'react'; +import ReactDOM from 'react-dom'; +import { + EuiPageTemplate, + EuiFlexGrid, + EuiFlexItem, + EuiPanel, + EuiTitle, + EuiButton, + EuiFlexGroup, +} from '@elastic/eui'; +import { AppMountParameters, CoreStart } from '@kbn/core/public'; +import { CasesUiStart } from '@kbn/cases-plugin/public'; +import { CommentType } from '@kbn/cases-plugin/common'; +import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { EuiThemeProvider as StyledComponentsThemeProvider } from '@kbn/kibana-react-plugin/common'; +import { EuiErrorBoundary } from '@elastic/eui'; +import { I18nProvider } from '@kbn/i18n-react'; + +export interface RenderAppProps { + mountParams: AppMountParameters; + coreStart: CoreStart; + pluginsStart: { cases: CasesUiStart }; +} + +interface CasesFixtureAppDeps { + cases: CasesUiStart; +} + +const permissions = { + all: true, + create: true, + read: true, + update: true, + delete: true, + push: true, +}; + +const attachments = [{ type: CommentType.user as const, comment: 'test' }]; + +const CasesFixtureAppWithContext: React.FC = (props) => { + const { cases } = props; + + const createCaseFlyout = cases.hooks.getUseCasesAddToNewCaseFlyout(); + const selectCaseModal = cases.hooks.getUseCasesAddToExistingCaseModal(); + + return ( + + + + + +

Cases attachment hooks

+
+ + + createCaseFlyout.open({ attachments })} + data-test-subj="case-fixture-attach-to-new-case" + > + {'Attach to a new case'} + + + + selectCaseModal.open({ attachments })} + data-test-subj="case-fixture-attach-to-existing-case" + > + {'Attach to an existing case'} + + + +
+
+
+
+ ); +}; + +const CasesFixtureApp: React.FC<{ deps: RenderAppProps }> = ({ deps }) => { + const { mountParams, coreStart, pluginsStart } = deps; + const { theme$ } = mountParams; + const { cases } = pluginsStart; + + const CasesContext = cases.ui.getCasesContext(); + + return ( + + + + + + + + + + + + + + ); +}; + +export const renderApp = (deps: RenderAppProps) => { + const { mountParams } = deps; + const { element } = mountParams; + + ReactDOM.render(, element); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/public/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/public/plugin.ts index a1f3a2e11e356..2df834abf1f22 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/public/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/public/plugin.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { Plugin, CoreSetup, CoreStart } from '@kbn/core/public'; -import { CasesUiSetup } from '@kbn/cases-plugin/public/types'; +import { Plugin, CoreSetup, CoreStart, AppMountParameters } from '@kbn/core/public'; +import { CasesUiSetup, CasesUiStart } from '@kbn/cases-plugin/public/types'; import { LensPublicStart } from '@kbn/lens-plugin/public'; import { getExternalReferenceAttachmentRegular } from './attachments/external_reference'; import { getPersistableStateAttachmentRegular } from './attachments/persistable_state'; @@ -20,6 +20,7 @@ export interface CasesExamplePublicSetupDeps { export interface CasesExamplePublicStartDeps { lens: LensPublicStart; + cases: CasesUiStart; } export class CasesFixturePlugin @@ -35,6 +36,16 @@ export class CasesFixturePlugin getPersistableStateAttachmentRegular(depsStart.lens.EmbeddableComponent) ); }); + + core.application.register({ + id: 'cases_fixture', + title: 'Cases Fixture App', + async mount(params: AppMountParameters) { + const [coreStart, pluginsStart] = await core.getStartServices(); + const { renderApp } = await import('./application'); + return renderApp({ coreStart, pluginsStart, mountParams: params }); + }, + }); } public start(core: CoreStart, plugins: CasesExamplePublicStartDeps) {} From bd2361d2a40a6329b8c024cef72068cbeac07078 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 4 Aug 2022 11:02:02 +0200 Subject: [PATCH 5/5] [ML] Fix the Dashboard saving indicator with Anomaly Swim Lane embeddable (#137989) --- .../anomaly_swimlane_embeddable.tsx | 4 +++ .../anomaly_swimlane_initializer.tsx | 30 +++++++++---------- .../anomaly_swimlane_setup_flyout.tsx | 7 ++--- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx index bb239868aed77..639136b169471 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx @@ -53,6 +53,10 @@ export class AnomalySwimlaneEmbeddable extends Embeddable< ); } + public reportsEmbeddableLoad() { + return true; + } + public onLoading() { this.renderComplete.dispatchInProgress(); this.updateOutput({ loading: true, error: undefined }); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx index 4fc43fc640b98..e5e2c2fd3b6be 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx @@ -25,17 +25,19 @@ import { i18n } from '@kbn/i18n'; import { SWIMLANE_TYPE, SwimlaneType } from '../../application/explorer/explorer_constants'; import { AnomalySwimlaneEmbeddableInput } from '..'; +interface ExplicitInput { + panelTitle: string; + swimlaneType: SwimlaneType; + viewBy?: string; +} + export interface AnomalySwimlaneInitializerProps { defaultTitle: string; influencers: string[]; initialInput?: Partial< Pick >; - onCreate: (swimlaneProps: { - panelTitle: string; - swimlaneType: SwimlaneType; - viewBy?: string; - }) => void; + onCreate: (swimlaneProps: ExplicitInput) => void; onCancel: () => void; } @@ -47,7 +49,7 @@ export const AnomalySwimlaneInitializer: FC = ( initialInput, }) => { const [panelTitle, setPanelTitle] = useState(defaultTitle); - const [swimlaneType, setSwimlaneType] = useState( + const [swimlaneType, setSwimlaneType] = useState( initialInput?.swimlaneType ?? SWIMLANE_TYPE.OVERALL ); const [viewBySwimlaneFieldName, setViewBySwimlaneFieldName] = useState(initialInput?.viewBy); @@ -81,6 +83,12 @@ export const AnomalySwimlaneInitializer: FC = ( (swimlaneType === SWIMLANE_TYPE.OVERALL || (swimlaneType === SWIMLANE_TYPE.VIEW_BY && !!viewBySwimlaneFieldName)); + const resultInput = { + panelTitle, + swimlaneType, + ...(viewBySwimlaneFieldName ? { viewBy: viewBySwimlaneFieldName } : {}), + }; + return ( @@ -162,15 +170,7 @@ export const AnomalySwimlaneInitializer: FC = ( /> - + { + onCreate={(explicitInput) => { modalSession.close(); resolve({ jobIds, - title: panelTitle, - swimlaneType, - viewBy, + title: explicitInput.panelTitle, + ...explicitInput, }); }} onCancel={() => {