diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/embedded_map.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/embedded_map.test.tsx.snap index 456e07cf9cd15..4c3cbecc7593d 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/embedded_map.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/embedded_map.test.tsx.snap @@ -1,34 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EmbeddedMapComponent renders correctly against snapshot 1`] = ` - - - - - Map configuration help - - - - } - > - - - - - - + `; diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx index ae0d3c2256e07..219409b10be6c 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx @@ -4,36 +4,169 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; +import { mount, ReactWrapper, shallow } from 'enzyme'; import React from 'react'; +import * as redux from 'react-redux'; +import { act } from 'react-dom/test-utils'; import '../../../common/mock/match_media'; import { useIndexPatterns } from '../../../common/hooks/use_index_patterns'; +import { TestProviders } from '../../../common/mock'; + import { EmbeddedMapComponent } from './embedded_map'; +import { createEmbeddable } from './embedded_map_helpers'; const mockUseIndexPatterns = useIndexPatterns as jest.Mock; jest.mock('../../../common/hooks/use_index_patterns'); mockUseIndexPatterns.mockImplementation(() => [true, []]); jest.mock('../../../common/lib/kibana'); +jest.mock('./embedded_map_helpers', () => ({ + createEmbeddable: jest.fn(), +})); +jest.mock('../../../common/lib/kibana', () => { + return { + useKibana: jest.fn().mockReturnValue({ + services: { + embeddable: { + EmbeddablePanel: jest.fn(() =>
), + }, + docLinks: { ELASTIC_WEBSITE_URL: 'ELASTIC_WEBSITE_URL' }, + }, + }), + }; +}); + +jest.mock('./index_patterns_missing_prompt', () => { + return { + IndexPatternsMissingPrompt: jest.fn(() =>
), + }; +}); describe('EmbeddedMapComponent', () => { - let setQuery: jest.Mock; + const setQuery: jest.Mock = jest.fn(); + const mockSelector = { + kibanaIndexPatterns: [ + { id: '6f1eeb50-023d-11eb-bcb6-6ba0578012a9', title: 'filebeat-*' }, + { id: '28995490-023d-11eb-bcb6-6ba0578012a9', title: 'auditbeat-*' }, + ], + sourcererScope: { selectedPatterns: ['filebeat-*', 'packetbeat-*'] }, + }; + const mockCreateEmbeddable = { + destroyed: false, + enhancements: { dynamicActions: {} }, + getActionContext: jest.fn(), + getFilterActions: jest.fn(), + id: '70969ddc-4d01-4048-8073-4ea63d595638', + input: { + viewMode: 'view', + title: 'Source -> Destination Point-to-Point Map', + id: '70969ddc-4d01-4048-8073-4ea63d595638', + filters: Array(0), + hidePanelTitles: true, + }, + input$: {}, + isContainer: false, + output: {}, + output$: {}, + parent: undefined, + parentSubscription: undefined, + renderComplete: {}, + runtimeId: 1, + reload: jest.fn(), + setLayerList: jest.fn(), + setEventHandlers: jest.fn(), + setRenderTooltipContent: jest.fn(), + type: 'map', + updateInput: jest.fn(), + }; + const testProps = { + endDate: '2019-08-28T05:50:57.877Z', + filters: [], + query: { query: '', language: 'kuery' }, + setQuery, + startDate: '2019-08-28T05:50:47.877Z', + }; beforeEach(() => { - setQuery = jest.fn(); + setQuery.mockClear(); }); test('renders correctly against snapshot', () => { const wrapper = shallow( - + + + ); - expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('EmbeddedMapComponent')).toMatchSnapshot(); + }); + + test('renders services.embeddable.EmbeddablePanel', async () => { + const spy = jest.spyOn(redux, 'useSelector'); + spy.mockReturnValue(mockSelector); + + (createEmbeddable as jest.Mock).mockResolvedValue(mockCreateEmbeddable); + + let wrapper: ReactWrapper; + await act(async () => { + wrapper = mount( + + + + ); + }); + + wrapper!.update(); + + expect(wrapper!.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(true); + expect(wrapper!.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(false); + expect(wrapper!.find('[data-test-subj="loading-panel"]').exists()).toEqual(false); + }); + + test('renders IndexPatternsMissingPrompt', async () => { + const spy = jest.spyOn(redux, 'useSelector'); + spy.mockReturnValue({ + ...mockSelector, + kibanaIndexPatterns: [], + }); + + (createEmbeddable as jest.Mock).mockResolvedValue(mockCreateEmbeddable); + + let wrapper: ReactWrapper; + await act(async () => { + wrapper = mount( + + + + ); + }); + + wrapper!.update(); + + expect(wrapper!.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(false); + expect(wrapper!.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(true); + expect(wrapper!.find('[data-test-subj="loading-panel"]').exists()).toEqual(false); + }); + + test('renders Loader', async () => { + const spy = jest.spyOn(redux, 'useSelector'); + spy.mockReturnValue(mockSelector); + + (createEmbeddable as jest.Mock).mockResolvedValue(null); + + let wrapper: ReactWrapper; + await act(async () => { + wrapper = mount( + + + + ); + }); + + wrapper!.update(); + + expect(wrapper!.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(false); + expect(wrapper!.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(false); + expect(wrapper!.find('[data-test-subj="loading-panel"]').exists()).toEqual(true); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx index 4d96c213818aa..7ae8aecdab606 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx @@ -5,27 +5,31 @@ */ import { EuiLink, EuiText } from '@elastic/eui'; -import React, { useEffect, useState } from 'react'; +import deepEqual from 'fast-deep-equal'; +import React, { useEffect, useState, useMemo } from 'react'; import { createPortalNode, InPortal } from 'react-reverse-portal'; import styled, { css } from 'styled-components'; -import { ErrorEmbeddable } from '../../../../../../../src/plugins/embeddable/public'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { getIndexPatternTitleIdMapping } from '../../../common/hooks/api/helpers'; -import { useIndexPatterns } from '../../../common/hooks/use_index_patterns'; +import { useSelector } from 'react-redux'; +import { + ErrorEmbeddable, + isErrorEmbeddable, +} from '../../../../../../../src/plugins/embeddable/public'; import { Loader } from '../../../common/components/loader'; import { displayErrorToast, useStateToaster } from '../../../common/components/toasters'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { Embeddable } from './embeddable'; import { EmbeddableHeader } from './embeddable_header'; -import { createEmbeddable, findMatchingIndexPatterns } from './embedded_map_helpers'; +import { createEmbeddable } from './embedded_map_helpers'; import { IndexPatternsMissingPrompt } from './index_patterns_missing_prompt'; import { MapToolTip } from './map_tool_tip/map_tool_tip'; import * as i18n from './translations'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { MapEmbeddable } from '../../../../../../plugins/maps/public/embeddable'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; -import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; +import { useKibana } from '../../../common/lib/kibana'; +import { getDefaultSourcererSelector } from './selector'; +import { getLayerList } from './map_config'; interface EmbeddableMapProps { maintainRatio?: boolean; @@ -86,13 +90,19 @@ export const EmbeddedMapComponent = ({ const [embeddable, setEmbeddable] = React.useState( undefined ); - const [isLoading, setIsLoading] = useState(true); const [isError, setIsError] = useState(false); const [isIndexError, setIsIndexError] = useState(false); const [, dispatchToaster] = useStateToaster(); - const [loadingKibanaIndexPatterns, kibanaIndexPatterns] = useIndexPatterns(); - const [siemDefaultIndices] = useUiSetting$(DEFAULT_INDEX_KEY); + const defaultSourcererScopeSelector = useMemo(getDefaultSourcererSelector, []); + const { kibanaIndexPatterns, sourcererScope } = useSelector( + defaultSourcererScopeSelector, + deepEqual + ); + + const [mapIndexPatterns, setMapIndexPatterns] = useState( + kibanaIndexPatterns.filter((kip) => sourcererScope.selectedPatterns.includes(kip.title)) + ); // This portalNode provided by react-reverse-portal allows us re-parent the MapToolTip within our // own component tree instead of the embeddables (default). This is necessary to have access to @@ -102,27 +112,30 @@ export const EmbeddedMapComponent = ({ const { services } = useKibana(); + useEffect(() => { + setMapIndexPatterns((prevMapIndexPatterns) => { + const newIndexPatterns = kibanaIndexPatterns.filter((kip) => + sourcererScope.selectedPatterns.includes(kip.title) + ); + if (!deepEqual(newIndexPatterns, prevMapIndexPatterns)) { + if (newIndexPatterns.length === 0) { + setIsError(true); + } + return newIndexPatterns; + } + return prevMapIndexPatterns; + }); + }, [kibanaIndexPatterns, sourcererScope.selectedPatterns]); + // Initial Load useEffect useEffect(() => { let isSubscribed = true; async function setupEmbeddable() { - // Ensure at least one `securitySolution:defaultIndex` kibana index pattern exists before creating embeddable - const matchingIndexPatterns = findMatchingIndexPatterns({ - kibanaIndexPatterns, - siemDefaultIndices, - }); - - if (matchingIndexPatterns.length === 0 && isSubscribed) { - setIsLoading(false); - setIsIndexError(true); - return; - } - // Create & set Embeddable try { const embeddableObject = await createEmbeddable( filters, - getIndexPatternTitleIdMapping(matchingIndexPatterns), + mapIndexPatterns, query, startDate, endDate, @@ -131,7 +144,12 @@ export const EmbeddedMapComponent = ({ services.embeddable ); if (isSubscribed) { - setEmbeddable(embeddableObject); + if (mapIndexPatterns.length === 0) { + setIsIndexError(true); + } else { + setEmbeddable(embeddableObject); + setIsIndexError(false); + } } } catch (e) { if (isSubscribed) { @@ -139,19 +157,41 @@ export const EmbeddedMapComponent = ({ setIsError(true); } } - if (isSubscribed) { - setIsLoading(false); - } } - - if (!loadingKibanaIndexPatterns) { + if (embeddable == null && sourcererScope.selectedPatterns.length > 0) { setupEmbeddable(); } + return () => { isSubscribed = false; }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loadingKibanaIndexPatterns, kibanaIndexPatterns]); + }, [ + dispatchToaster, + endDate, + embeddable, + filters, + mapIndexPatterns, + query, + portalNode, + services.embeddable, + sourcererScope.selectedPatterns, + setQuery, + startDate, + ]); + + // update layer with new index patterns + useEffect(() => { + const setLayerList = async () => { + if (embeddable != null) { + // @ts-expect-error + await embeddable.setLayerList(getLayerList(mapIndexPatterns)); + embeddable.reload(); + } + }; + if (embeddable != null && !isErrorEmbeddable(embeddable)) { + setLayerList(); + } + }, [embeddable, mapIndexPatterns]); // queryExpression updated useEffect useEffect(() => { @@ -198,10 +238,10 @@ export const EmbeddedMapComponent = ({ - {embeddable != null ? ( - - ) : !isLoading && isIndexError ? ( + {isIndexError ? ( + ) : embeddable != null ? ( + ) : ( )} diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/selector.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/selector.test.tsx new file mode 100644 index 0000000000000..d5b105dd32798 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/selector.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { State } from '../../../common/store'; + +import { getDefaultSourcererSelector } from './selector'; + +jest.mock('../../../common/store/sourcerer', () => ({ + sourcererSelectors: { + kibanaIndexPatternsSelector: jest.fn().mockReturnValue(jest.fn()), + scopesSelector: jest.fn().mockReturnValue(jest.fn().mockReturnValue({ default: '' })), + }, +})); + +describe('getDefaultSourcererSelector', () => { + test('Returns correct format', () => { + const mockMapStateToProps = getDefaultSourcererSelector(); + const result = mockMapStateToProps({} as State); + expect(result).toHaveProperty('kibanaIndexPatterns'); + expect(result).toHaveProperty('sourcererScope'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/selector.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/selector.tsx new file mode 100644 index 0000000000000..2d0bc970f0a51 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/selector.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { State } from '../../../common/store'; +import { sourcererSelectors } from '../../../common/store/sourcerer'; +import { + KibanaIndexPatterns, + ManageScope, + SourcererScopeName, +} from '../../../common/store/sourcerer/model'; + +export interface DefaultSourcererSelector { + kibanaIndexPatterns: KibanaIndexPatterns; + sourcererScope: ManageScope; +} + +export const getDefaultSourcererSelector = () => { + const getKibanaIndexPatternsSelector = sourcererSelectors.kibanaIndexPatternsSelector(); + const getScopesSelector = sourcererSelectors.scopesSelector(); + + const mapStateToProps = (state: State): DefaultSourcererSelector => { + const kibanaIndexPatterns = getKibanaIndexPatternsSelector(state); + const scope = getScopesSelector(state)[SourcererScopeName.default]; + + return { + kibanaIndexPatterns, + sourcererScope: scope, + }; + }; + + return mapStateToProps; +};