diff --git a/packages/kbn-babel-preset/styled_components_files.js b/packages/kbn-babel-preset/styled_components_files.js index 536cdf71f7e8c..03b8923db655b 100644 --- a/packages/kbn-babel-preset/styled_components_files.js +++ b/packages/kbn-babel-preset/styled_components_files.js @@ -440,7 +440,6 @@ module.exports = { /x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]resolver[\/\\]view[\/\\]symbol_definitions.tsx/, /x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]sourcerer[\/\\]components[\/\\]helpers.tsx/, /x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]sourcerer[\/\\]components[\/\\]refresh_button.tsx/, - /x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]sourcerer[\/\\]components[\/\\]update_default_data_view_modal.tsx/, /x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]timelines[\/\\]components[\/\\]certificate_fingerprint[\/\\]index.tsx/, /x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]timelines[\/\\]components[\/\\]edit_data_provider[\/\\]index.tsx/, /x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]timelines[\/\\]components[\/\\]fields_browser[\/\\]create_field_button[\/\\]index.tsx/, diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 06c0f4c6afb38..8b8b30a1dc952 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -38184,8 +38184,6 @@ "xpack.securitySolution.indexPatterns.failureToastTitle": "Impossible de mettre à jour la vue de données", "xpack.securitySolution.indexPatterns.inactive": "Modèles d'indexation inactifs", "xpack.securitySolution.indexPatterns.indexPatternsLabel": "Modèles d'indexation", - "xpack.securitySolution.indexPatterns.missingPatterns": "La vue de données Security ne contient pas les modèles d'indexation suivants nécessaires pour recréer la vue de données de la chronologie précédente : {callout}", - "xpack.securitySolution.indexPatterns.missingPatterns.callout": "La vue de données Security ne contient pas les modèles d'indexation suivants : {callout}", "xpack.securitySolution.indexPatterns.missingPatterns.timeline.description": "Nous avons préservé votre chronologie en créant une vue de données temporaires. Si vous souhaitez modifier vos données, nous pouvons ajouter les modèles d'indexation manquants à la vue de données Security. Vous pouvez également sélectionner manuellement une vue de données {link}.", "xpack.securitySolution.indexPatterns.missingPatterns.timelineTemplate.description": "Nous avons conservé votre modèle de chronologie en créant une vue de données temporaires. Si vous souhaitez modifier vos données, nous pouvons ajouter les modèles d'indexation manquants à la vue de données Security. Vous pouvez également sélectionner manuellement une vue de données {link}.", "xpack.securitySolution.indexPatterns.modifiedBadgeTitle": "Modifié", @@ -38209,7 +38207,6 @@ "xpack.securitySolution.indexPatterns.timelineTemplate.toggleToNewSourcerer": "Nous avons conservé votre modèle de chronologie en créant une vue de données temporaires. Si vous souhaitez modifier vos données, nous pouvons recréer votre vue de données temporaires à l'aide du sélecteur de vue de nouvelles données. Vous pouvez également sélectionner manuellement une vue de données {link}.", "xpack.securitySolution.indexPatterns.toggleToNewSourcerer.link": "ici", "xpack.securitySolution.indexPatterns.update": "Mettre à jour et recréer la vue de données", - "xpack.securitySolution.indexPatterns.updateAvailableBadgeTitle": "Mise à jour disponible", "xpack.securitySolution.indexPatterns.updateDataView": "Souhaitez-vous ajouter ce modèle d'indexation à la vue de données Security ? Sinon, nous pouvons recréer la vue de données sans les modèles d'indexation manquants.", "xpack.securitySolution.indexPatterns.updateSecurityDataView": "Mettre à jour la vue de données Security", "xpack.securitySolution.inputCapture.ariaPlaceHolder": "Saisir une commande", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 332b951ed7991..2f5470ed60852 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -38045,8 +38045,6 @@ "xpack.securitySolution.indexPatterns.failureToastTitle": "データビューを更新できません", "xpack.securitySolution.indexPatterns.inactive": "非アクティブなインデックスパターン", "xpack.securitySolution.indexPatterns.indexPatternsLabel": "インデックスパターン", - "xpack.securitySolution.indexPatterns.missingPatterns": "以前のタイムラインのデータビューを再作成するには、セキュリティデータビューに次のインデックスパターンがありません:{callout}", - "xpack.securitySolution.indexPatterns.missingPatterns.callout": "セキュリティデータビューには次のインデックスパターンがありません:{callout}", "xpack.securitySolution.indexPatterns.missingPatterns.timeline.description": "一時データビューを作成することで、タイムラインを保持しています。データを修正する場合は、見つからないインデックスパターンをセキュリティデータビューに追加できます。手動でデータビュー{link}を選択することもできます。", "xpack.securitySolution.indexPatterns.missingPatterns.timelineTemplate.description": "一時データビューを作成することで、タイムラインテンプレートを保持しています。データを修正する場合は、見つからないインデックスパターンをセキュリティデータビューに追加できます。手動でデータビュー{link}を選択することもできます。", "xpack.securitySolution.indexPatterns.modifiedBadgeTitle": "変更済み", @@ -38070,7 +38068,6 @@ "xpack.securitySolution.indexPatterns.timelineTemplate.toggleToNewSourcerer": "一時データビューを作成することで、タイムラインテンプレートを保持しています。データを修正する場合は、新しいデータビューセレクターを使用して、一時データビューを再作成できます。手動でデータビュー{link}を選択することもできます。", "xpack.securitySolution.indexPatterns.toggleToNewSourcerer.link": "こちら", "xpack.securitySolution.indexPatterns.update": "データビューを更新して再作成", - "xpack.securitySolution.indexPatterns.updateAvailableBadgeTitle": "更新が利用可能です", "xpack.securitySolution.indexPatterns.updateDataView": "このインデックスパターンをセキュリティデータビューに追加しますか?そうでない場合は、見つからないインデックスパターンなしで、データビューを再作成できます。", "xpack.securitySolution.indexPatterns.updateSecurityDataView": "セキュリティデータビューを更新", "xpack.securitySolution.inputCapture.ariaPlaceHolder": "コマンドを入力", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index af1c963577ea3..7d2d78454312d 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -37476,8 +37476,6 @@ "xpack.securitySolution.indexPatterns.failureToastTitle": "无法更新数据视图", "xpack.securitySolution.indexPatterns.inactive": "非活动索引模式", "xpack.securitySolution.indexPatterns.indexPatternsLabel": "索引模式", - "xpack.securitySolution.indexPatterns.missingPatterns": "要重新创建上一时间线的数据视图,安全数据视图缺少以下索引模式:{callout}", - "xpack.securitySolution.indexPatterns.missingPatterns.callout": "安全数据视图缺少以下索引模式:{callout}", "xpack.securitySolution.indexPatterns.missingPatterns.timeline.description": "我们已通过创建临时数据视图来保留您的时间线。如果您要修改数据,我们可以将缺失的索引模式添加到安全数据视图。您还可以手动选择数据视图 {link}。", "xpack.securitySolution.indexPatterns.missingPatterns.timelineTemplate.description": "我们已通过创建临时数据视图来保留您的时间线模板。如果您要修改数据,我们可以将缺失的索引模式添加到安全数据视图。您还可以手动选择数据视图 {link}。", "xpack.securitySolution.indexPatterns.modifiedBadgeTitle": "已修改", @@ -37501,7 +37499,6 @@ "xpack.securitySolution.indexPatterns.timelineTemplate.toggleToNewSourcerer": "我们已通过创建临时数据视图来保留您的时间线模板。如果您要修改数据,我们可以使用新的数据视图选择器重新创建临时数据视图。您还可以手动选择数据视图 {link}。", "xpack.securitySolution.indexPatterns.toggleToNewSourcerer.link": "此处", "xpack.securitySolution.indexPatterns.update": "更新并重新创建数据视图", - "xpack.securitySolution.indexPatterns.updateAvailableBadgeTitle": "有可用更新", "xpack.securitySolution.indexPatterns.updateDataView": "是否要将此索引模式添加到安全数据视图?否则,我们可以不使用缺失的索引模式来重新创建数据视图。", "xpack.securitySolution.indexPatterns.updateSecurityDataView": "更新安全数据视图", "xpack.securitySolution.inputCapture.ariaPlaceHolder": "输入命令", diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/__mocks__/use_create_adhoc_data_view.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/__mocks__/use_create_adhoc_data_view.tsx new file mode 100644 index 0000000000000..929780f67f2e7 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/__mocks__/use_create_adhoc_data_view.tsx @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export const useCreateAdhocDataView = jest.fn(() => { + return { + createAdhocDataView: jest.fn(() => ({ + id: 'adhoc_sourcerer_mock_view', + getIndexPattern: () => 'pattern1,pattern2', + })), + }; +}); + +export const isAdhocDataView = jest.requireActual('../use_create_adhoc_data_view').isAdhocDataView; diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/alerts_sourcerer.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/alerts_sourcerer.test.tsx index 619f7e91eae82..2450c4143b32f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/alerts_sourcerer.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/alerts_sourcerer.test.tsx @@ -18,10 +18,7 @@ const mockDispatch = jest.fn(); jest.mock('../containers'); jest.mock('../containers/use_signal_helpers'); -const mockUseUpdateDataView = jest.fn().mockReturnValue(() => true); -jest.mock('./use_update_data_view', () => ({ - useUpdateDataView: () => mockUseUpdateDataView, -})); +jest.mock('./use_create_adhoc_data_view'); jest.mock('react-redux', () => { const original = jest.requireActual('react-redux'); diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/index.test.tsx index 1f39bc02c68c4..62a16f85f8d6a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/index.test.tsx @@ -24,10 +24,7 @@ const mockDispatch = jest.fn(); jest.mock('../containers'); jest.mock('../containers/use_signal_helpers'); -const mockUseUpdateDataView = jest.fn().mockReturnValue(() => true); -jest.mock('./use_update_data_view', () => ({ - useUpdateDataView: () => mockUseUpdateDataView, -})); +jest.mock('./use_create_adhoc_data_view'); jest.mock('react-redux', () => { const original = jest.requireActual('react-redux'); diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/index.tsx index ad5a939b69995..a5e2f6b29d3bd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/index.tsx @@ -17,6 +17,7 @@ import type { ChangeEventHandler } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import type { DataViewSpec } from '@kbn/data-views-plugin/common'; import * as i18n from './translations'; import type { sourcererModel } from '../store'; import { sourcererActions, sourcererSelectors } from '../store'; @@ -26,14 +27,14 @@ import type { ModifiedTypes } from './use_pick_index_patterns'; import { SourcererScopeName } from '../store/model'; import { usePickIndexPatterns } from './use_pick_index_patterns'; import { FormRow, PopoverContent, StyledButtonEmpty, StyledFormRow } from './helpers'; -import { TemporarySourcerer } from './temporary'; import { useSourcererDataView } from '../containers'; -import { useUpdateDataView } from './use_update_data_view'; import { Trigger } from './trigger'; import { AlertsCheckbox, SaveButtons, SourcererCallout } from './sub_components'; import { useSignalHelpers } from '../containers/use_signal_helpers'; import { useUpdateUrlParam } from '../../common/utils/global_query_string'; import { URL_PARAM_KEY } from '../../common/hooks/use_url_state'; +import { useDataViewFallback } from './use_data_view_fallback'; +import { isAdhocDataView } from './use_create_adhoc_data_view'; export interface SourcererComponentProps { scope: sourcererModel.SourcererScopeName; @@ -52,12 +53,7 @@ interface SourcererPopoverProps { signalIndexName: string | null; handleClosePopOver: () => void; isTimelineSourcerer: boolean; - selectedDataViewId: string | null; - sourcererMissingPatterns: string[]; - onUpdateDetectionAlertsChecked: () => void; handleOutsideClick: () => void; - setMissingPatterns: (missingPatterns: string[]) => void; - setDataViewId: (dataViewId: string | null) => void; scopeId: sourcererModel.SourcererScopeName; children: React.ReactNode; } @@ -76,11 +72,6 @@ const SourcererPopover = React.memo( signalIndexName, handleClosePopOver, isTimelineSourcerer, - selectedDataViewId, - sourcererMissingPatterns, - onUpdateDetectionAlertsChecked, - setMissingPatterns, - setDataViewId, scopeId, children, }) => { @@ -221,7 +212,6 @@ export const Sourcerer = React.memo(({ scope: scopeId } ); const [expandAdvancedOptions, setExpandAdvancedOptions] = useState(false); - const [isShowingUpdateModal, setIsShowingUpdateModal] = useState(false); const setPopoverIsOpenCb = useCallback(() => { setPopoverIsOpen((prevState) => !prevState); @@ -231,7 +221,9 @@ export const Sourcerer = React.memo(({ scope: scopeId } ( newSelectedDataView: string, newSelectedPatterns: string[], - shouldValidateSelectedPatterns?: boolean + shouldValidateSelectedPatterns?: boolean, + // Overrides data view entirely with a specified one. Only works with shouldValidateSelectedPatterns set to false. + dataView?: DataViewSpec ) => { dispatch( sourcererActions.setSelectedDataView({ @@ -239,6 +231,7 @@ export const Sourcerer = React.memo(({ scope: scopeId } selectedDataViewId: newSelectedDataView, selectedPatterns: newSelectedPatterns, shouldValidateSelectedPatterns, + dataView, }) ); @@ -291,56 +284,43 @@ export const Sourcerer = React.memo(({ scope: scopeId } sourcererMissingPatterns, ]); - // deprecated timeline index pattern handlers - const onContinueUpdateDeprecated = useCallback(() => { - setIsShowingUpdateModal(false); - const patterns = selectedPatterns.filter((pattern) => - defaultDataView.patternList.includes(pattern) - ); - dispatchChangeDataView(defaultDataView.id, patterns); - setPopoverIsOpen(false); - }, [defaultDataView.id, defaultDataView.patternList, dispatchChangeDataView, selectedPatterns]); - - const onUpdateDeprecated = useCallback(() => { - // are all the patterns in the default? - if (missingPatterns.length === 0) { - onContinueUpdateDeprecated(); - } else { - // open modal - setIsShowingUpdateModal(true); - } - }, [missingPatterns, onContinueUpdateDeprecated]); - - const [isTriggerDisabled, setIsTriggerDisabled] = useState(false); - - const onOpenAndReset = useCallback(() => { + const handleDataViewFallbackError = useCallback(() => { setPopoverIsOpen(true); resetDataSources(); }, [resetDataSources]); - const updateDataView = useUpdateDataView(onOpenAndReset); - const onUpdateDataView = useCallback(async () => { - const isUiSettingsSuccess = await updateDataView(missingPatterns); - setIsShowingUpdateModal(false); - setPopoverIsOpen(false); + const handleApplyFallbackDataView = useCallback( + (dataView: DataViewSpec) => { + if (!dataView.id) { + return; + } - if (isUiSettingsSuccess) { - dispatchChangeDataView( - defaultDataView.id, - // to be at this stage, activePatterns is defined, the ?? selectedPatterns is to make TS happy - activePatterns ?? selectedPatterns, - false - ); - setIsTriggerDisabled(true); + dispatchChangeDataView(dataView.id, dataView.title?.split(',') || [], false, dataView); + setDataViewId(dataView.id); + }, + [dispatchChangeDataView] + ); + + const modificationStatus = useMemo(() => { + if (isAdhocDataView(dataViewId)) { + return 'adhoc'; } - }, [ - activePatterns, - defaultDataView.id, + + return isModified; + }, [dataViewId, isModified]); + + // NOTE: this hook will enable the fallback data view (adhoc), + // depending on the state of this component (see "enableFallback" below) + useDataViewFallback({ + onApplyFallbackDataView: handleApplyFallbackDataView, + enableFallback: + // NOTE: there are cases where sourcerer does not know the dataViewId to display and only knows required patterns. + // In that case, we enable the fallback dataview creation that will try to create adhoc data view based on the patterns & will select it. + !loading && + ((dataViewId === null && isModified === 'deprecated') || isModified === 'missingPatterns'), missingPatterns, - dispatchChangeDataView, - selectedPatterns, - updateDataView, - ]); + onResolveErrorManually: handleDataViewFallbackError, + }); useEffect(() => { setDataViewId(selectedDataViewId); @@ -360,8 +340,8 @@ export const Sourcerer = React.memo(({ scope: scopeId } (({ scope: scopeId } selectedPatterns={selectedPatterns} signalIndexName={signalIndexName} handleClosePopOver={handleClosePopOver} - selectedDataViewId={selectedDataViewId} - sourcererMissingPatterns={sourcererMissingPatterns} - onUpdateDetectionAlertsChecked={onUpdateDetectionAlertsChecked} - setMissingPatterns={setMissingPatterns} - setDataViewId={setDataViewId} scopeId={scopeId} > @@ -387,83 +362,65 @@ export const Sourcerer = React.memo(({ scope: scopeId } title={isTimelineSourcerer ? i18n.CALL_OUT_TIMELINE_TITLE : i18n.CALL_OUT_TITLE} /> - {(dataViewId === null && isModified === 'deprecated') || - isModified === 'missingPatterns' ? ( - setIsShowingUpdateModal(false)} - onReset={resetDataSources} - onUpdateStepOne={isModified === 'deprecated' ? onUpdateDeprecated : onUpdateDataView} - onUpdateStepTwo={onUpdateDataView} - selectedPatterns={selectedPatterns} - /> - ) : ( - - <> - - {dataViewId && ( - - - - )} - - - - {i18n.INDEX_PATTERNS_ADVANCED_OPTIONS_TITLE} - - {expandAdvancedOptions && } - - + <> + + {dataViewId && ( + + - - - + )} + + + + {i18n.INDEX_PATTERNS_ADVANCED_OPTIONS_TITLE} + + {expandAdvancedOptions && } + + - - - - )} + + + + + + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/misc.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/misc.test.tsx index 1897acca1c6dd..6e978ecd7eab6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/misc.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/misc.test.tsx @@ -6,9 +6,6 @@ */ import React from 'react'; -import type { ReactWrapper } from 'enzyme'; -import { mount } from 'enzyme'; -import { cloneDeep } from 'lodash'; import { initialSourcererState, type SelectedDataView, SourcererScopeName } from '../store/model'; import { Sourcerer } from '.'; @@ -19,16 +16,13 @@ import { useSignalHelpers } from '../containers/use_signal_helpers'; import { TimelineId } from '../../../common/types/timeline'; import { type TimelineType, TimelineTypeEnum } from '../../../common/api/timeline'; import { sortWithExcludesAtEnd } from '../../../common/utils/sourcerer'; -import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import { render, fireEvent, screen, waitFor, act } from '@testing-library/react'; const mockDispatch = jest.fn(); jest.mock('../containers'); jest.mock('../containers/use_signal_helpers'); -const mockUseUpdateDataView = jest.fn().mockReturnValue(() => true); -jest.mock('./use_update_data_view', () => ({ - useUpdateDataView: () => mockUseUpdateDataView, -})); +jest.mock('./use_create_adhoc_data_view'); jest.mock('react-redux', () => { const original = jest.requireActual('react-redux'); @@ -61,11 +55,13 @@ const defaultProps = { scope: sourcererModel.SourcererScopeName.default, }; -const checkOptionsAndSelections = (wrapper: ReactWrapper, patterns: string[]) => ({ +const checkOptionsAndSelections = (patterns: string[]) => ({ availableOptionCount: - wrapper.find('List').length > 0 ? wrapper.find('List').prop('itemCount') : 0, - optionsSelected: patterns.every((pattern) => - wrapper.find(`[data-test-subj="sourcerer-combo-box"] span[title="${pattern}"]`).first().exists() + screen.queryAllByTestId('List').length > 0 ? screen.queryAllByTestId('List').length : 0, + optionsSelected: patterns.every( + (pattern) => + screen.getByTestId(`sourcerer-combo-box`).querySelectorAll(`span[title="${pattern}"]`) + .length === 1 ), }); @@ -106,36 +102,36 @@ describe('No data', () => { }); test('Hide sourcerer - default ', () => { - const wrapper = mount( + render( ); - expect(wrapper.find(`[data-test-subj="sourcerer-trigger"]`).exists()).toEqual(false); + expect(screen.queryAllByTestId('sourcerer-trigger')).toHaveLength(0); }); test('Hide sourcerer - detections ', () => { - const wrapper = mount( + render( ); - expect(wrapper.find(`[data-test-subj="sourcerer-trigger"]`).exists()).toEqual(false); + expect(screen.queryAllByTestId('sourcerer-trigger')).toHaveLength(0); }); test('Hide sourcerer - timeline ', () => { - const wrapper = mount( + render( ); - expect(wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).exists()).toEqual(true); + expect(screen.queryAllByTestId('timeline-sourcerer-trigger')).toHaveLength(1); }); }); -describe('Update available', () => { - const state2 = { +describe('Compat mode', () => { + const state = { ...mockGlobalState, sourcerer: { ...mockGlobalState.sourcerer, @@ -167,7 +163,7 @@ describe('Update available', () => { }, }, }; - let store = createMockStore(state2); + let store = createMockStore(state); const pollForSignalIndexMock = jest.fn(); beforeEach(() => { (useSignalHelpers as jest.Mock).mockReturnValue({ @@ -176,9 +172,8 @@ describe('Update available', () => { }); (useSourcererDataView as jest.Mock).mockReturnValue({ ...sourcererDataView, - activePatterns: ['myFakebeat-*'], }); - store = createMockStore(state2); + store = createMockStore(state); render( @@ -190,8 +185,8 @@ describe('Update available', () => { jest.clearAllMocks(); }); - test('Show Update available label', () => { - expect(screen.getByTestId('sourcerer-deprecated-badge')).toBeInTheDocument(); + test('Show Compat mode badge', () => { + expect(screen.getByText('Compat mode')).toBeInTheDocument(); }); test('Show correct tooltip', async () => { @@ -200,65 +195,10 @@ describe('Update available', () => { expect(screen.getByTestId('sourcerer-tooltip').textContent).toBe('myFakebeat-*'); }); }); - - test('Show UpdateDefaultDataViewModal', () => { - fireEvent.click(screen.queryAllByTestId('timeline-sourcerer-trigger')[0]); - - fireEvent.click(screen.queryAllByTestId('sourcerer-deprecated-update')[0]); - - expect(screen.getByTestId('sourcerer-update-data-view-modal')).toBeVisible(); - }); - - test('Show UpdateDefaultDataViewModal Callout', () => { - fireEvent.click(screen.queryAllByTestId('timeline-sourcerer-trigger')[0]); - - fireEvent.click(screen.queryAllByTestId('sourcerer-deprecated-update')[0]); - - expect(screen.queryAllByTestId('sourcerer-deprecated-callout')[0].textContent).toBe( - 'This timeline uses a legacy data view selector' - ); - - expect(screen.queryAllByTestId('sourcerer-current-patterns-message')[0].textContent).toBe( - 'The active index patterns in this timeline are: myFakebeat-*' - ); - - expect(screen.queryAllByTestId('sourcerer-deprecated-message')[0].textContent).toBe( - "We have preserved your timeline by creating a temporary data view. If you'd like to modify your data, we can recreate your temporary data view with the new data view selector. You can also manually select a data view here." - ); - }); - - test('Show Add index pattern in UpdateDefaultDataViewModal', () => { - fireEvent.click(screen.queryAllByTestId('timeline-sourcerer-trigger')[0]); - - fireEvent.click(screen.queryAllByTestId('sourcerer-deprecated-update')[0]); - - expect(screen.queryAllByTestId('sourcerer-update-data-view')[0].textContent).toBe( - 'Add index pattern' - ); - }); - - test('Set all the index patterns from legacy timeline to sourcerer, after clicking on "Add index pattern"', async () => { - fireEvent.click(screen.queryAllByTestId('timeline-sourcerer-trigger')[0]); - - fireEvent.click(screen.queryAllByTestId('sourcerer-deprecated-update')[0]); - - fireEvent.click(screen.queryAllByTestId('sourcerer-update-data-view')[0]); - - await waitFor(() => { - expect(mockDispatch).toHaveBeenCalledWith( - sourcererActions.setSelectedDataView({ - id: SourcererScopeName.timeline, - selectedDataViewId: 'security-solution', - selectedPatterns: ['myFakebeat-*'], - shouldValidateSelectedPatterns: false, - }) - ); - }); - }); }); -describe('Update available for timeline template', () => { - const state2 = { +describe('Compat mode for timeline template', () => { + const state = { ...mockGlobalState, timeline: { ...mockGlobalState.timeline, @@ -300,14 +240,13 @@ describe('Update available for timeline template', () => { }, }, }; - let store = createMockStore(state2); + let store = createMockStore(state); beforeEach(() => { (useSourcererDataView as jest.Mock).mockReturnValue({ ...sourcererDataView, - activePatterns: ['myFakebeat-*'], }); - store = createMockStore(state2); + store = createMockStore(state); render( @@ -319,22 +258,13 @@ describe('Update available for timeline template', () => { jest.clearAllMocks(); }); - test('Show UpdateDefaultDataViewModal CallOut', () => { - fireEvent.click(screen.getByTestId('timeline-sourcerer-trigger')); - fireEvent.click(screen.getByTestId('sourcerer-deprecated-update')); - - expect(screen.getByTestId('sourcerer-deprecated-callout')).toHaveTextContent( - 'This timeline template uses a legacy data view selector' - ); - - expect(screen.getByTestId('sourcerer-deprecated-message')).toHaveTextContent( - "We have preserved your timeline template by creating a temporary data view. If you'd like to modify your data, we can recreate your temporary data view with the new data view selector. You can also manually select a data view here." - ); + test('Show Compat mode badge', () => { + expect(screen.getByText('Compat mode')).toBeInTheDocument(); }); }); describe('Missing index patterns', () => { - const state2 = { + const state = { ...mockGlobalState, timeline: { ...mockGlobalState.timeline, @@ -376,7 +306,7 @@ describe('Missing index patterns', () => { }, }, }; - let store = createMockStore(state2); + let store = createMockStore(state); beforeEach(() => { const pollForSignalIndexMock = jest.fn(); (useSignalHelpers as jest.Mock).mockReturnValue({ @@ -389,45 +319,11 @@ describe('Missing index patterns', () => { jest.clearAllMocks(); }); - test('Show UpdateDefaultDataViewModal CallOut for timeline', async () => { - (useSourcererDataView as jest.Mock).mockReturnValue({ - ...sourcererDataView, - activePatterns: ['myFakebeat-*'], - }); - const state3 = cloneDeep(state2); - state3.timeline.timelineById[TimelineId.active].timelineType = TimelineTypeEnum.default; - store = createMockStore(state3); - - render( - - - - ); - - fireEvent.click(screen.getByTestId('timeline-sourcerer-trigger')); - - fireEvent.click(screen.getByTestId('sourcerer-deprecated-update')); - - expect(screen.getByTestId('sourcerer-deprecated-callout').textContent).toBe( - 'This timeline is out of date with the Security Data View' - ); - expect(screen.getByTestId('sourcerer-current-patterns-message').textContent).toBe( - 'The active index patterns in this timeline are: myFakebeat-*' - ); - expect(screen.queryAllByTestId('sourcerer-missing-patterns-callout')[0].textContent).toBe( - 'Security Data View is missing the following index patterns: myFakebeat-*' - ); - expect(screen.queryAllByTestId('sourcerer-missing-patterns-message')[0].textContent).toBe( - "We have preserved your timeline by creating a temporary data view. If you'd like to modify your data, we can add the missing index patterns to the Security Data View. You can also manually select a data view here." - ); - }); - - test('Show UpdateDefaultDataViewModal CallOut for timeline template', async () => { + test('Show Compat mode badge', async () => { (useSourcererDataView as jest.Mock).mockReturnValue({ ...sourcererDataView, - activePatterns: ['myFakebeat-*'], }); - store = createMockStore(state2); + store = createMockStore(state); render( @@ -435,27 +331,9 @@ describe('Missing index patterns', () => { ); - fireEvent.click(screen.getByTestId('timeline-sourcerer-trigger')); + await act(async () => {}); - fireEvent.click(screen.getByTestId('sourcerer-deprecated-update')); - - await waitFor(() => { - expect(screen.queryAllByTestId('sourcerer-deprecated-callout')[0].textContent).toBe( - 'This timeline template is out of date with the Security Data View' - ); - - expect(screen.queryAllByTestId('sourcerer-current-patterns-message')[0].textContent).toBe( - 'The active index patterns in this timeline template are: myFakebeat-*' - ); - - expect(screen.queryAllByTestId('sourcerer-missing-patterns-callout')[0].textContent).toBe( - 'Security Data View is missing the following index patterns: myFakebeat-*' - ); - - expect(screen.queryAllByTestId('sourcerer-missing-patterns-message')[0].textContent).toBe( - "We have preserved your timeline template by creating a temporary data view. If you'd like to modify your data, we can add the missing index patterns to the Security Data View. You can also manually select a data view here." - ); - }); + expect(screen.getByText('Compat mode')).toBeInTheDocument(); }); }); @@ -495,27 +373,27 @@ describe('Sourcerer integration tests', () => { (useSourcererDataView as jest.Mock).mockReturnValue({ ...sourcererDataView, - activePatterns: ['myFakebeat-*'], }); store = createMockStore(state); jest.clearAllMocks(); }); it('Selects a different index pattern', async () => { - const wrapper = mount( + render( ); - wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); - wrapper.find(`button[data-test-subj="sourcerer-select"]`).first().simulate('click'); - wrapper.find(`[data-test-subj="dataView-option-super"]`).first().simulate('click'); - expect(checkOptionsAndSelections(wrapper, ['fakebeat-*'])).toEqual({ + fireEvent.click(screen.getByTestId('sourcerer-trigger')); + fireEvent.click(screen.getByTestId('sourcerer-select')); + + fireEvent.click(screen.queryAllByTestId('dataView-option-super')[0]); + expect(checkOptionsAndSelections(['fakebeat-*'])).toEqual({ availableOptionCount: 0, optionsSelected: true, }); - wrapper.find(`button[data-test-subj="sourcerer-save"]`).first().simulate('click'); + fireEvent.click(screen.getByTestId('sourcerer-save')); expect(mockDispatch).toHaveBeenCalledWith( sourcererActions.setSelectedDataView({ diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/sourcerer_integration.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/sourcerer_integration.test.tsx index 5f21a814da363..2047ea6407beb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/sourcerer_integration.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/sourcerer_integration.test.tsx @@ -21,10 +21,7 @@ const mockDispatch = jest.fn(); jest.mock('../containers'); jest.mock('../containers/use_signal_helpers'); -const mockUseUpdateDataView = jest.fn().mockReturnValue(() => true); -jest.mock('./use_update_data_view', () => ({ - useUpdateDataView: () => mockUseUpdateDataView, -})); +jest.mock('./use_create_adhoc_data_view'); jest.mock('react-redux', () => { const original = jest.requireActual('react-redux'); diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/temporary.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/temporary.tsx deleted file mode 100644 index 66899affeb4aa..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/temporary.tsx +++ /dev/null @@ -1,231 +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 { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiCallOut, - EuiText, - EuiTextColor, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiToolTip, -} from '@elastic/eui'; -import React, { useMemo } from 'react'; -import * as i18n from './translations'; -import { Blockquote, ResetButton } from './helpers'; -import { UpdateDefaultDataViewModal } from './update_default_data_view_modal'; -import { TimelineId } from '../../../common/types'; -import { TimelineTypeEnum } from '../../../common/api/timeline'; -import { timelineSelectors } from '../../timelines/store'; -import { useDeepEqualSelector } from '../../common/hooks/use_selector'; -import { timelineDefaults } from '../../timelines/store/defaults'; -import { - BadCurrentPatternsMessage, - CurrentPatternsMessage, - DeprecatedMessage, - MissingPatternsMessage, -} from './utils'; - -interface Props { - activePatterns?: string[]; - indicesExist: boolean; - isModified: 'deprecated' | 'missingPatterns'; - missingPatterns: string[]; - onDismiss: () => void; - onReset: () => void; - onUpdate: () => void; - selectedPatterns: string[]; -} - -const translations = { - deprecated: { - title: { - [TimelineTypeEnum.default]: i18n.CALL_OUT_DEPRECATED_TITLE, - [TimelineTypeEnum.template]: i18n.CALL_OUT_DEPRECATED_TEMPLATE_TITLE, - }, - update: i18n.UPDATE_INDEX_PATTERNS, - }, - missingPatterns: { - title: { - [TimelineTypeEnum.default]: i18n.CALL_OUT_MISSING_PATTERNS_TITLE, - [TimelineTypeEnum.template]: i18n.CALL_OUT_MISSING_PATTERNS_TEMPLATE_TITLE, - }, - update: i18n.ADD_INDEX_PATTERN, - }, -}; - -export const TemporarySourcererComp = React.memo( - ({ - activePatterns, - indicesExist, - isModified, - onDismiss, - onReset, - onUpdate, - selectedPatterns, - missingPatterns, - }) => { - const trigger = useMemo( - () => ( - - {translations[isModified].update} - - ), - [indicesExist, isModified, onUpdate] - ); - const buttonWithTooltip = useMemo( - () => - !indicesExist ? ( - - {trigger} - - ) : ( - trigger - ), - [indicesExist, trigger] - ); - - const deadPatterns = - activePatterns && activePatterns.length > 0 - ? selectedPatterns.filter((p) => !activePatterns.includes(p)) - : []; - const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - - const timelineType = useDeepEqualSelector( - (state) => (getTimeline(state, TimelineId.active) ?? timelineDefaults).timelineType - ); - - return ( - <> - - - - -

- {activePatterns && activePatterns.length > 0 ? ( - - ) : ( - - )} - - {isModified === 'deprecated' && ( - - )} - {isModified === 'missingPatterns' && ( - <> - - {missingPatterns.join(', ')}, - }} - /> - - - - )} -

-
-
- - - - {i18n.INDEX_PATTERNS_CLOSE} - - - {buttonWithTooltip} - - - ); - } -); - -TemporarySourcererComp.displayName = 'TemporarySourcererComp'; - -interface TemporarySourcererProps { - activePatterns?: string[]; - indicesExist: boolean; - isModified: 'deprecated' | 'missingPatterns'; - isShowingUpdateModal: boolean; - missingPatterns: string[]; - onContinueWithoutUpdate: () => void; - onDismiss: () => void; - onDismissModal: () => void; - onReset: () => void; - onUpdateStepOne: () => void; - onUpdateStepTwo: () => void; - selectedPatterns: string[]; -} - -export const TemporarySourcerer = React.memo( - ({ - activePatterns, - indicesExist, - isModified, - missingPatterns, - onContinueWithoutUpdate, - onDismiss, - onReset, - onUpdateStepOne, - onUpdateStepTwo, - selectedPatterns, - isShowingUpdateModal, - onDismissModal, - }) => ( - <> - - - - ) -); - -TemporarySourcerer.displayName = 'TemporarySourcerer'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/timeline_sourcerer.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/timeline_sourcerer.test.tsx index 35a26856f4930..627926b103056 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/timeline_sourcerer.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/timeline_sourcerer.test.tsx @@ -19,10 +19,7 @@ const mockDispatch = jest.fn(); jest.mock('../containers'); jest.mock('../containers/use_signal_helpers'); -const mockUseUpdateDataView = jest.fn().mockReturnValue(() => true); -jest.mock('./use_update_data_view', () => ({ - useUpdateDataView: () => mockUseUpdateDataView, -})); +jest.mock('./use_create_adhoc_data_view'); jest.mock('react-redux', () => { const original = jest.requireActual('react-redux'); diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/translations.ts index 51698a9fa7547..d73c5e3632185 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/translations.ts @@ -96,10 +96,10 @@ export const ALERTS_BADGE_TITLE = i18n.translate( } ); -export const DEPRECATED_BADGE_TITLE = i18n.translate( - 'xpack.securitySolution.indexPatterns.updateAvailableBadgeTitle', +export const COMPAT_BADGE_TITLE = i18n.translate( + 'xpack.securitySolution.indexPatterns.compatBadgeTitle', { - defaultMessage: 'Update available', + defaultMessage: 'Compat mode', } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/trigger.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/trigger.tsx index 5dc7ab8522189..5a44566a8093d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/trigger.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/trigger.tsx @@ -50,18 +50,15 @@ export const TriggerComponent: FC = ({ {i18n.ALERTS_BADGE_TITLE} ); - case 'deprecated': + case 'adhoc': { return ( - - {i18n.DEPRECATED_BADGE_TITLE} + + {i18n.COMPAT_BADGE_TITLE} ); + } + case 'deprecated': case 'missingPatterns': - return ( - - {i18n.DEPRECATED_BADGE_TITLE} - - ); case '': default: return null; diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/update_default_data_view_modal.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/update_default_data_view_modal.tsx deleted file mode 100644 index c010a4687c8a3..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/update_default_data_view_modal.tsx +++ /dev/null @@ -1,93 +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 { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiModal, - EuiModalBody, - EuiModalHeader, - EuiModalHeaderTitle, - EuiText, - EuiTextColor, -} from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; -import * as i18n from './translations'; -import { Blockquote, ResetButton } from './helpers'; - -interface Props { - isShowing: boolean; - missingPatterns: string[]; - onDismissModal: () => void; - onContinue: () => void; - onUpdate: () => void; -} -const MyEuiModal = styled(EuiModal)` - .euiModal__flex { - width: 60vw; - } - .euiCodeBlock { - height: auto !important; - max-width: 718px; - } -`; - -export const UpdateDefaultDataViewModal = React.memo( - ({ isShowing, onDismissModal, onContinue, onUpdate, missingPatterns }) => - isShowing ? ( - - - {i18n.UPDATE_SECURITY_DATA_VIEW} - - - - -

- {missingPatterns.join(', ')}, - }} - /> - {i18n.UPDATE_DATA_VIEW} -

-
-
- - - - {i18n.CONTINUE_WITHOUT_ADDING} - - - - - {i18n.ADD_INDEX_PATTERN} - - - -
-
- ) : null -); - -UpdateDefaultDataViewModal.displayName = 'UpdateDefaultDataViewModal'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_create_adhoc_data_view.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_create_adhoc_data_view.test.tsx new file mode 100644 index 0000000000000..b8f1d6b9dfd02 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_create_adhoc_data_view.test.tsx @@ -0,0 +1,84 @@ +/* + * 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 { act, renderHook } from '@testing-library/react'; +import { useCreateAdhocDataView } from './use_create_adhoc_data_view'; +import { useKibana as mockUseKibana } from '../../common/lib/kibana/__mocks__'; +import * as i18n from './translations'; +const mockAddSuccess = jest.fn(); +const mockAddError = jest.fn(); +const mockedUseKibana = mockUseKibana(); +mockedUseKibana.services.dataViews = { create: jest.fn() }; +mockedUseKibana.services.uiSettings = { get: jest.fn().mockReturnValue([]) }; + +jest.mock('../../common/hooks/use_app_toasts', () => { + const original = jest.requireActual('../../common/hooks/use_app_toasts'); + + return { + ...original, + useAppToasts: () => ({ + addSuccess: mockAddSuccess, + addError: mockAddError, + }), + }; +}); +jest.mock('../../common/lib/kibana', () => { + return { + useKibana: () => mockedUseKibana, + }; +}); +jest.mock('@kbn/react-kibana-mount', () => { + const original = jest.requireActual('@kbn/react-kibana-mount'); + + return { + ...original, + toMountPoint: jest.fn(), + }; +}); + +describe('useCreateAdhocDataView', () => { + beforeEach(() => {}); + + afterEach(() => {}); + + it('should create data view successfully with given patterns', async () => { + const { result } = renderHook(() => useCreateAdhocDataView(jest.fn())); + + const mockDataViews = mockedUseKibana.services.dataViews; + + await act(async () => { + await result.current.createAdhocDataView(['pattern-1-*', 'pattern-2-*']); + }); + + expect(mockDataViews.create).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'pattern-1-*,pattern-2-*', + id: 'adhoc_sourcerer_pattern-1-*,pattern-2-*', + }) + ); + }); + + it('should add error on failure', async () => { + const onResolveErrorManually = jest.fn(); + const error = new Error('error'); + const mockDataViews = mockedUseKibana.services.dataViews; + + mockDataViews.create.mockRejectedValueOnce(error); + + const { result } = renderHook(() => useCreateAdhocDataView(onResolveErrorManually)); + + await act(async () => { + const dataView = await result.current.createAdhocDataView(['pattern-1-*']); + expect(dataView).toBeNull(); + }); + + expect(mockAddError).toHaveBeenCalledWith(error, { + title: i18n.FAILURE_TOAST_TITLE, + toastMessage: expect.anything(), + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_create_adhoc_data_view.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_create_adhoc_data_view.tsx new file mode 100644 index 0000000000000..4cd5e78ff3d44 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_create_adhoc_data_view.tsx @@ -0,0 +1,78 @@ +/* + * 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, { useCallback } from 'react'; +import { EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { useKibana } from '../../common/lib/kibana'; +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import * as i18n from './translations'; +import { useAppToasts } from '../../common/hooks/use_app_toasts'; +import { ensurePatternFormat } from '../../../common/utils/sourcerer'; + +export interface UseCreateAdhocDataViewReturnValue { + createAdhocDataView: (missingPatterns: string[]) => Promise; +} + +const ADHOC_ID_PREFIX = 'adhoc_sourcerer_' as const; + +export const isAdhocDataView = (dataViewId: string | null) => + dataViewId?.startsWith(ADHOC_ID_PREFIX) ?? false; + +export const useCreateAdhocDataView = ( + onResolveErrorManually: () => void +): UseCreateAdhocDataViewReturnValue => { + const { dataViews, uiSettings } = useKibana().services; + const { addError } = useAppToasts(); + + const createAdhocDataView = useCallback( + async (patterns: string[]): Promise => { + const createDataView = async (): Promise => { + const defaultPatterns = uiSettings.get(DEFAULT_INDEX_KEY); + const combinedPatterns = [...defaultPatterns, ...patterns]; + const validatedPatterns = ensurePatternFormat(combinedPatterns); + const patternsString = validatedPatterns.join(','); + const adHocDataView = await dataViews.create({ + id: `${ADHOC_ID_PREFIX}${patternsString}`, + title: patternsString, + }); + + if (adHocDataView.fields.getByName('@timestamp')?.type === 'date') { + adHocDataView.timeFieldName = '@timestamp'; + } + + return adHocDataView; + }; + try { + return await createDataView(); + } catch (possibleError) { + addError(possibleError !== null ? possibleError : new Error(i18n.FAILURE_TOAST_TITLE), { + title: i18n.FAILURE_TOAST_TITLE, + toastMessage: ( + + {i18n.TOGGLE_TO_NEW_SOURCERER} + + ), + }} + /> + ) as unknown as string, + }); + + return null; + } + }, + [addError, onResolveErrorManually, uiSettings, dataViews] + ); + + return { createAdhocDataView }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_data_view_fallback.test.ts b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_data_view_fallback.test.ts new file mode 100644 index 0000000000000..1ed8f4e71ad25 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_data_view_fallback.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { act, renderHook } from '@testing-library/react'; +import { useDataViewFallback } from './use_data_view_fallback'; +import { useCreateAdhocDataView } from './use_create_adhoc_data_view'; +import type { DataView } from '@kbn/data-views-plugin/common'; + +jest.mock('./use_create_adhoc_data_view'); + +const mockDataView = { + id: 'mock', + getIndexPattern: () => 'pattern1,pattern2', +} as unknown as DataView; + +describe('useDataViewFallback', () => { + let onResolveErrorManually: VoidFunction; + let onApplyFallbackDataView: VoidFunction; + + beforeEach(() => { + onResolveErrorManually = jest.fn(); + onApplyFallbackDataView = jest.fn(); + jest.mocked(useCreateAdhocDataView).mockReturnValue({ + createAdhocDataView: jest.fn(async (): Promise => mockDataView), + }); + }); + + it('should not create an adhoc data view when `enableFallback` is false', async () => { + renderHook(() => + useDataViewFallback({ + onResolveErrorManually, + missingPatterns: ['pattern1'], + enableFallback: false, + onApplyFallbackDataView, + }) + ); + + expect(useCreateAdhocDataView(() => {}).createAdhocDataView).not.toHaveBeenCalled(); + }); + + it('should create adhoc data views when `enableFallback` and `missingPatterns` are provided', async () => { + renderHook(() => + useDataViewFallback({ + onResolveErrorManually, + missingPatterns: ['pattern1'], + enableFallback: true, + onApplyFallbackDataView, + }) + ); + + expect(useCreateAdhocDataView(() => {}).createAdhocDataView).toHaveBeenCalledWith(['pattern1']); + }); + + it('should apply fallback data view once when a data view is successfully created', async () => { + const result = renderHook(() => + useDataViewFallback({ + onResolveErrorManually, + missingPatterns: ['pattern1'], + enableFallback: true, + onApplyFallbackDataView, + }) + ); + + // NOTE: test if multiple renders with the same props result with a single dataview selection call + await act(async () => result.rerender()); + await act(async () => result.rerender()); + await act(async () => result.rerender()); + + expect(onApplyFallbackDataView).toHaveBeenCalledWith('mock', ['pattern1', 'pattern2'], false); + expect(onApplyFallbackDataView).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_data_view_fallback.ts b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_data_view_fallback.ts new file mode 100644 index 0000000000000..5fdf64a673b49 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_data_view_fallback.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useMemo } from 'react'; +import type { DataViewSpec } from '@kbn/data-views-plugin/common'; +import { useCreateAdhocDataView } from './use_create_adhoc_data_view'; + +interface UseDataViewFallbackConfig { + /** + * Called when user interacts with the error notification + */ + onResolveErrorManually: VoidFunction; + + /** + * Missing patterns that need to be wrapped in an adhoc adhocDataView + */ + missingPatterns: string[]; + + /** + * Is fallback process enabled? + */ + enableFallback: boolean; + + /** + * Calls external function on dataview change + */ + onApplyFallbackDataView: (dataView: DataViewSpec) => void; +} + +export const useDataViewFallback = ({ + onResolveErrorManually, + missingPatterns, + enableFallback, + onApplyFallbackDataView, +}: UseDataViewFallbackConfig) => { + const { createAdhocDataView } = useCreateAdhocDataView(onResolveErrorManually); + + // NOTE: this exists because missingPatterns array reference is swapped very often and we only care about + // the values here + const stableMissingPatternsString = useMemo(() => missingPatterns.join(), [missingPatterns]); + + useEffect(() => { + // NOTE: this lets us prevent setting the index on every commit, + // as there is no way to pass in abort signal into the data view creation apis + let ignore = false; + + if (!enableFallback) { + return; + } + + if (!stableMissingPatternsString.length) { + return; + } + + (async () => { + const adhocDataView = await createAdhocDataView(stableMissingPatternsString.split(',')); + + if (ignore) { + return; + } + + if (!adhocDataView || !adhocDataView.id) { + return; + } + + onApplyFallbackDataView(adhocDataView.toSpec()); + })(); + + return () => { + ignore = true; + }; + }, [createAdhocDataView, onApplyFallbackDataView, enableFallback, stableMissingPatternsString]); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_pick_index_patterns.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_pick_index_patterns.tsx index 8798a84409d13..335c4fa8efd05 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_pick_index_patterns.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_pick_index_patterns.tsx @@ -29,7 +29,7 @@ interface UsePickIndexPatternsProps { signalIndexName: string | null; } -export type ModifiedTypes = 'modified' | 'alerts' | 'deprecated' | 'missingPatterns' | ''; +export type ModifiedTypes = 'modified' | 'alerts' | 'deprecated' | 'missingPatterns' | 'adhoc' | ''; interface UsePickIndexPatterns { allOptions: Array>; diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_update_data_view.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_update_data_view.test.tsx deleted file mode 100644 index bb4d935dbdc99..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_update_data_view.test.tsx +++ /dev/null @@ -1,84 +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 { renderHook } from '@testing-library/react'; -import { useUpdateDataView } from './use_update_data_view'; -import { useKibana as mockUseKibana } from '../../common/lib/kibana/__mocks__'; -import * as i18n from './translations'; -const mockAddSuccess = jest.fn(); -const mockAddError = jest.fn(); -const mockPatterns = ['packetbeat-*', 'winlogbeat-*']; -const mockedUseKibana = mockUseKibana(); -jest.mock('../../common/hooks/use_app_toasts', () => { - const original = jest.requireActual('../../common/hooks/use_app_toasts'); - - return { - ...original, - useAppToasts: () => ({ - addSuccess: mockAddSuccess, - addError: mockAddError, - }), - }; -}); -jest.mock('../../common/lib/kibana', () => { - return { - useKibana: () => mockedUseKibana, - }; -}); -jest.mock('@kbn/react-kibana-mount', () => { - const original = jest.requireActual('@kbn/react-kibana-mount'); - - return { - ...original, - toMountPoint: jest.fn(), - }; -}); -describe('use_update_data_view', () => { - const mockError = jest.fn(); - - beforeEach(() => { - mockedUseKibana.services.uiSettings.get.mockImplementation(() => mockPatterns); - mockedUseKibana.services.uiSettings.set.mockResolvedValue(true); - jest.clearAllMocks(); - }); - - test('Successful uiSettings updates with correct index pattern, and shows success toast', async () => { - const { result } = renderHook(() => useUpdateDataView(mockError)); - const updateDataView = result.current; - const isUiSettingsSuccess = await updateDataView(['missing-*']); - expect(mockedUseKibana.services.uiSettings.set.mock.calls[0][1]).toEqual( - [...mockPatterns, 'missing-*'].sort() - ); - expect(isUiSettingsSuccess).toEqual(true); - expect(mockAddSuccess).toHaveBeenCalled(); - }); - - test('Failed uiSettings update returns false and shows error toast', async () => { - mockedUseKibana.services.uiSettings.set.mockImplementation(() => false); - const { result } = renderHook(() => useUpdateDataView(mockError)); - const updateDataView = result.current; - const isUiSettingsSuccess = await updateDataView(['missing-*']); - expect(mockedUseKibana.services.uiSettings.set.mock.calls[0][1]).toEqual( - [...mockPatterns, 'missing-*'].sort() - ); - expect(isUiSettingsSuccess).toEqual(false); - expect(mockAddError).toHaveBeenCalled(); - expect(mockAddError.mock.calls[0][0]).toEqual(new Error(i18n.FAILURE_TOAST_TITLE)); - }); - - test('Failed uiSettings throws error and shows error toast', async () => { - mockedUseKibana.services.uiSettings.get.mockImplementation(() => { - throw new Error('Uh oh bad times over here'); - }); - const { result } = renderHook(() => useUpdateDataView(mockError)); - const updateDataView = result.current; - const isUiSettingsSuccess = await updateDataView(['missing-*']); - expect(isUiSettingsSuccess).toEqual(false); - expect(mockAddError).toHaveBeenCalled(); - expect(mockAddError.mock.calls[0][0]).toEqual(new Error('Uh oh bad times over here')); - }); -}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_update_data_view.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_update_data_view.tsx deleted file mode 100644 index a18d37d7c10f5..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/components/use_update_data_view.tsx +++ /dev/null @@ -1,72 +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 React, { useCallback } from 'react'; -import { EuiLink } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { toMountPoint } from '@kbn/react-kibana-mount'; -import { useKibana } from '../../common/lib/kibana'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import * as i18n from './translations'; -import { RefreshButton } from './refresh_button'; -import { useAppToasts } from '../../common/hooks/use_app_toasts'; -import { ensurePatternFormat } from '../../../common/utils/sourcerer'; - -export const useUpdateDataView = ( - onOpenAndReset: () => void -): ((missingPatterns: string[]) => Promise) => { - const { uiSettings, ...startServices } = useKibana().services; - const { addSuccess, addError } = useAppToasts(); - return useCallback( - async (missingPatterns: string[]): Promise => { - const asyncSearch = async (): Promise<[boolean, Error | null]> => { - try { - const defaultPatterns = uiSettings.get(DEFAULT_INDEX_KEY); - const uiSettingsIndexPattern = [...defaultPatterns, ...missingPatterns]; - const isSuccess = await uiSettings.set( - DEFAULT_INDEX_KEY, - ensurePatternFormat(uiSettingsIndexPattern) - ); - return [isSuccess, null]; - } catch (e) { - return [false, e]; - } - }; - const [isUiSettingsSuccess, possibleError] = await asyncSearch(); - if (isUiSettingsSuccess) { - addSuccess({ - color: 'success', - title: toMountPoint(i18n.SUCCESS_TOAST_TITLE, startServices), - text: toMountPoint(, startServices), - iconType: undefined, - toastLifeTimeMs: 600000, - }); - return true; - } - addError(possibleError !== null ? possibleError : new Error(i18n.FAILURE_TOAST_TITLE), { - title: i18n.FAILURE_TOAST_TITLE, - toastMessage: ( - <> - - {i18n.TOGGLE_TO_NEW_SOURCERER} - - ), - }} - /> - - ) as unknown as string, - }); - return false; - }, - [addError, addSuccess, onOpenAndReset, uiSettings, startServices] - ); -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/index.tsx index 2ad961fac3c42..a0c5a0b87865a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/index.tsx @@ -5,15 +5,13 @@ * 2.0. */ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; -import type { FieldSpec } from '@kbn/data-plugin/common'; import { sourcererSelectors } from '../store'; -import type { SelectedDataView, SourcererDataView, RunTimeMappings } from '../store/model'; +import type { SelectedDataView } from '../store/model'; import { SourcererScopeName } from '../store/model'; import { checkIfIndicesExist } from '../store/helpers'; import { getDataViewStateFromIndexFields } from '../../common/containers/source/use_data_view'; -import { useFetchIndex } from '../../common/containers/source'; import type { State } from '../../common/store/types'; import { sortWithExcludesAtEnd } from '../../../common/utils/sourcerer'; @@ -35,8 +33,10 @@ export const useSourcererDataView = ( const scopeSelectedPatterns = useSelector((state: State) => { return sourcererSelectors.sourcererScopeSelectedPatterns(state, scopeId); }); - const missingPatterns = useSelector((state: State) => { - return sourcererSelectors.sourcererScopeMissingPatterns(state, scopeId); + + // NOTE: currently this is only defined when an adhoc DV is created + const optionalSelectedDataViewSpec = useSelector((state: State) => { + return sourcererSelectors.sourcererScopeSelectedDataViewSpec(state, scopeId); }); const selectedPatterns = useMemo( @@ -44,47 +44,23 @@ export const useSourcererDataView = ( [scopeSelectedPatterns] ); - const [legacyPatterns, setLegacyPatterns] = useState([]); - - const [indexPatternsLoading, fetchIndexReturn] = useFetchIndex(legacyPatterns); - - const legacyDataView: Omit & { id: string | null } = useMemo( - () => ({ - ...fetchIndexReturn, - dataView: fetchIndexReturn.dataView, - runtimeMappings: (fetchIndexReturn.dataView?.runtimeFieldMap as RunTimeMappings) ?? {}, - title: fetchIndexReturn.dataView?.title ?? '', - id: fetchIndexReturn.dataView?.id ?? null, - loading: indexPatternsLoading, - patternList: fetchIndexReturn.indexes, - indexFields: fetchIndexReturn.indexPatterns.fields as FieldSpec[], - fields: fetchIndexReturn.dataView?.fields, - }), - [fetchIndexReturn, indexPatternsLoading] - ); - - useEffect(() => { - if (selectedDataView == null || missingPatterns.length > 0) { - // old way of fetching indices, legacy timeline - setLegacyPatterns(selectedPatterns); - } else { - setLegacyPatterns([]); - } - }, [missingPatterns, selectedDataView, selectedPatterns]); - const sourcererDataView = useMemo(() => { - const _dv = - selectedDataView == null || missingPatterns.length > 0 ? legacyDataView : selectedDataView; - // Make sure the title is up to date, so that the correct index patterns are used everywhere return { - ..._dv, + ...selectedDataView, dataView: { - ..._dv.dataView, + ...selectedDataView?.dataView, title: selectedPatterns.join(','), - name: selectedPatterns.join(','), + name: selectedDataView?.dataView?.name ?? selectedPatterns.join(','), + id: selectedDataViewId ?? undefined, + fields: optionalSelectedDataViewSpec?.fields ?? selectedDataView?.dataView?.fields, }, }; - }, [legacyDataView, missingPatterns.length, selectedDataView, selectedPatterns]); + }, [ + optionalSelectedDataViewSpec?.fields, + selectedDataView, + selectedDataViewId, + selectedPatterns, + ]); const indicesExist = useMemo(() => { if (loading || sourcererDataView.loading) { @@ -93,7 +69,7 @@ export const useSourcererDataView = ( return checkIfIndicesExist({ scopeId, signalIndexName, - patternList: sourcererDataView.patternList, + patternList: sourcererDataView.patternList ?? [], isDefaultDataViewSelected: sourcererDataView.id === defaultDataView.id, }); } @@ -109,7 +85,7 @@ export const useSourcererDataView = ( const browserFields = useCallback(() => { const { browserFields: dataViewBrowserFields } = getDataViewStateFromIndexFields( - sourcererDataView.patternList.join(','), + sourcererDataView?.patternList?.join(',') || '', sourcererDataView.fields ); return dataViewBrowserFields; @@ -118,9 +94,9 @@ export const useSourcererDataView = ( return useMemo( () => ({ browserFields: browserFields(), - dataViewId: sourcererDataView.id, + dataViewId: sourcererDataView.id ?? null, indicesExist, - loading: loading || sourcererDataView.loading, + loading: loading || !!sourcererDataView?.loading, // selected patterns in DATA_VIEW including filter selectedPatterns, sourcererDataView: sourcererDataView.dataView, diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/store/actions.ts b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/store/actions.ts index 54aa7dfa2fef8..fb6b0a6fdbe74 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/store/actions.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/store/actions.ts @@ -7,6 +7,7 @@ import actionCreatorFactory from 'typescript-fsa'; +import type { DataViewSpec } from '@kbn/data-views-plugin/common'; import type { SelectedDataView, SourcererDataView, SourcererScopeName } from './model'; import type { SecurityDataView } from '../containers/create_sourcerer_data_view'; @@ -35,5 +36,6 @@ export interface SelectedDataViewPayload { selectedDataViewId: SelectedDataView['dataViewId']; selectedPatterns: SelectedDataView['selectedPatterns']; shouldValidateSelectedPatterns?: boolean; + dataView?: DataViewSpec; } export const setSelectedDataView = actionCreator('SET_SELECTED_DATA_VIEW'); diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/store/helpers.ts b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/store/helpers.ts index 2fccdbdd23025..c958ca23b2be7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/store/helpers.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/store/helpers.ts @@ -52,7 +52,7 @@ export const validateSelectedPatterns = ( payload: SelectedDataViewPayload, shouldValidateSelectedPatterns: boolean ): Partial => { - const { id, ...rest } = payload; + const { id, dataView: dataViewOverride, ...rest } = payload; const dataView = state.kibanaDataViews.find((p) => p.id === rest.selectedDataViewId); // dedupe because these could come from a silly url or pre 8.0 timeline const dedupePatterns = ensurePatternFormat(rest.selectedPatterns); @@ -62,6 +62,7 @@ export const validateSelectedPatterns = ( const dedupeAllDefaultPatterns = ensurePatternFormat( (dataView ?? state.defaultDataView).title.split(',') ); + // TODO: If we having missing patterns here, just create a new dataView...don't worry about missingPatterns anymore... missingPatterns = dedupePatterns.filter( (pattern) => !dedupeAllDefaultPatterns.includes(pattern) ); @@ -89,11 +90,19 @@ export const validateSelectedPatterns = ( const signalIndexName = state.signalIndexName; selectedPatterns = getPatternListFromScope(id, selectedPatterns, signalIndexName); + let selectedDataViewId = dataView?.id ?? null; + + if (dataViewOverride) { + selectedPatterns = payload.selectedPatterns; + missingPatterns = []; + selectedDataViewId = String(dataViewOverride.id); + } + return { [id]: { ...state.sourcererScopes[id], ...rest, - selectedDataViewId: dataView?.id ?? null, + selectedDataViewId, selectedPatterns, missingPatterns, // if in timeline, allow for empty in case pattern was deleted @@ -109,6 +118,7 @@ export const validateSelectedPatterns = ( } : {}), loading: false, + dataViewSpec: dataViewOverride, }, }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/store/model.ts b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/store/model.ts index 807c74a9c3f8f..703c5d971a242 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/store/model.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/store/model.ts @@ -35,6 +35,12 @@ export interface SourcererScope { * selectedDataViewId === null OR defaultDataView.id * saved timeline has pattern that is not in the default */ missingPatterns: string[]; + + /** + * The full dataview spec that might be set, depending on the data view loading strategy. + * Currently only available in the legacy compatibility flow when adhoc dv is created. + */ + dataViewSpec?: DataViewSpec; } export type SourcererScopeById = Record; diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/store/selectors.ts b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/store/selectors.ts index 28e59276b5777..0a95ec8cdec52 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/store/selectors.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/store/selectors.ts @@ -45,6 +45,16 @@ export const sourcererScopeSelectedDataViewId = createSelector( } ); +export const sourcererScopeSelectedDataViewSpec = createSelector( + sourcererScope, + (scope) => scope.dataViewSpec, + { + memoizeOptions: { + maxSize: SOURCERER_SCOPE_MAX_SIZE, + }, + } +); + export const sourcererScopeSelectedPatterns = createSelector( sourcererScope, (scope) => scope.selectedPatterns,