mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security solution] Add field validation to data views on Network map (#147899)
This commit is contained in:
parent
52d235ae49
commit
0076fa5641
7 changed files with 303 additions and 183 deletions
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { useKibana } from '../../../common/lib/kibana';
|
||||
import { useIsFieldInIndexPattern } from '.';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
const mockUseKibana = useKibana as jest.Mock;
|
||||
describe('useIsFieldInIndexPattern', () => {
|
||||
beforeAll(() => {
|
||||
mockUseKibana.mockReturnValue({
|
||||
services: {
|
||||
data: {
|
||||
dataViews: {
|
||||
getFieldsForWildcard: () => [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('returns false when no fields in field list exist in the index pattern', async () => {
|
||||
const isFieldInIndexPattern = useIsFieldInIndexPattern();
|
||||
const res = await isFieldInIndexPattern('index-pattern-*', ['fields.list']);
|
||||
expect(res).toEqual(false);
|
||||
});
|
||||
it('returns false when some but not all fields in field list exist in the index pattern', async () => {
|
||||
mockUseKibana.mockReturnValue({
|
||||
services: {
|
||||
http: {},
|
||||
data: {
|
||||
dataViews: {
|
||||
getFieldsForWildcard: () => [{ name: 'fields.list' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const isFieldInIndexPattern = useIsFieldInIndexPattern();
|
||||
const res = await isFieldInIndexPattern('index-pattern-*', ['fields.list', 'another']);
|
||||
expect(res).toEqual(false);
|
||||
});
|
||||
it('returns true when all fields in field list exist in the index pattern', async () => {
|
||||
mockUseKibana.mockReturnValue({
|
||||
services: {
|
||||
http: {},
|
||||
data: {
|
||||
dataViews: {
|
||||
getFieldsForWildcard: () => [{ name: 'fields.list' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const isFieldInIndexPattern = useIsFieldInIndexPattern();
|
||||
const res = await isFieldInIndexPattern('index-pattern-*', ['fields.list']);
|
||||
expect(res).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { useKibana } from '../../../common/lib/kibana';
|
||||
|
||||
type FieldValidationCheck = (pattern: string, fieldsList: string[]) => Promise<boolean>;
|
||||
|
||||
export const useIsFieldInIndexPattern = (): FieldValidationCheck => {
|
||||
const { dataViews } = useKibana().services.data;
|
||||
return async (pattern: string, fieldsList: string[]) => {
|
||||
const fields = await dataViews.getFieldsForWildcard({
|
||||
pattern,
|
||||
fields: fieldsList,
|
||||
});
|
||||
const fieldNames = fields.map((f) => f.name);
|
||||
return fieldsList.every((field) => fieldNames.includes(field));
|
||||
};
|
||||
};
|
|
@ -1,16 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EmbeddedMapComponent renders correctly against snapshot 1`] = `
|
||||
<EmbeddedMapComponent
|
||||
endDate="2019-08-28T05:50:57.877Z"
|
||||
filters={Array []}
|
||||
query={
|
||||
Object {
|
||||
"language": "kuery",
|
||||
"query": "",
|
||||
}
|
||||
}
|
||||
setQuery={[MockFunction]}
|
||||
startDate="2019-08-28T05:50:47.877Z"
|
||||
/>
|
||||
`;
|
|
@ -5,221 +5,237 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ReactWrapper } from 'enzyme';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import * as redux from 'react-redux';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import '../../../../common/mock/match_media';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
|
||||
import { EmbeddedMapComponent } from './embedded_map';
|
||||
import { createEmbeddable } from './create_embeddable';
|
||||
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
|
||||
import { getLayerList } from './map_config';
|
||||
import { useIsFieldInIndexPattern } from '../../../containers/fields';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('./create_embeddable', () => ({
|
||||
createEmbeddable: jest.fn(),
|
||||
jest.mock('./create_embeddable');
|
||||
jest.mock('./map_config');
|
||||
jest.mock('../../../../common/containers/sourcerer');
|
||||
jest.mock('../../../containers/fields');
|
||||
jest.mock('./index_patterns_missing_prompt', () => ({
|
||||
IndexPatternsMissingPrompt: jest.fn(() => <div data-test-subj="IndexPatternsMissingPrompt" />),
|
||||
}));
|
||||
|
||||
const mockGetStorage = jest.fn();
|
||||
const mockSetStorage = jest.fn();
|
||||
|
||||
jest.mock('../../../../common/lib/kibana', () => {
|
||||
return {
|
||||
useKibana: () => ({
|
||||
services: {
|
||||
embeddable: {
|
||||
EmbeddablePanel: jest.fn(() => <div data-test-subj="EmbeddablePanel" />),
|
||||
},
|
||||
docLinks: {
|
||||
ELASTIC_WEBSITE_URL: 'ELASTIC_WEBSITE_URL',
|
||||
links: {
|
||||
siem: { networkMap: '' },
|
||||
},
|
||||
},
|
||||
storage: {
|
||||
get: mockGetStorage,
|
||||
set: mockSetStorage,
|
||||
jest.mock('../../../../common/lib/kibana', () => ({
|
||||
useKibana: () => ({
|
||||
services: {
|
||||
embeddable: {
|
||||
EmbeddablePanel: jest.fn(() => <div data-test-subj="EmbeddablePanel" />),
|
||||
},
|
||||
docLinks: {
|
||||
ELASTIC_WEBSITE_URL: 'ELASTIC_WEBSITE_URL',
|
||||
links: {
|
||||
siem: { networkMap: '' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
jest.mock('../../../../common/containers/sourcerer', () => {
|
||||
return {
|
||||
useSourcererDataView: () => ({
|
||||
selectedPatterns: ['filebeat-*', 'packetbeat-*'],
|
||||
}),
|
||||
};
|
||||
});
|
||||
jest.mock('./index_patterns_missing_prompt', () => {
|
||||
return {
|
||||
IndexPatternsMissingPrompt: jest.fn(() => <div data-test-subj="IndexPatternsMissingPrompt" />),
|
||||
};
|
||||
});
|
||||
|
||||
describe('EmbeddedMapComponent', () => {
|
||||
const setQuery: jest.Mock = jest.fn();
|
||||
const mockSelector = {
|
||||
kibanaDataViews: [
|
||||
{ id: '6f1eeb50-023d-11eb-bcb6-6ba0578012a9', title: 'filebeat-*' },
|
||||
{ id: '28995490-023d-11eb-bcb6-6ba0578012a9', title: 'auditbeat-*' },
|
||||
],
|
||||
};
|
||||
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,
|
||||
storage: {
|
||||
get: mockGetStorage,
|
||||
set: mockSetStorage,
|
||||
},
|
||||
},
|
||||
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',
|
||||
};
|
||||
}),
|
||||
useToasts: jest.fn().mockReturnValue({
|
||||
addError: jest.fn(),
|
||||
addSuccess: jest.fn(),
|
||||
addWarning: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockUseSourcererDataView = useSourcererDataView as jest.Mock;
|
||||
const mockCreateEmbeddable = createEmbeddable as jest.Mock;
|
||||
const mockUseIsFieldInIndexPattern = useIsFieldInIndexPattern as jest.Mock;
|
||||
const mockGetStorage = jest.fn();
|
||||
const mockSetStorage = jest.fn();
|
||||
const setQuery: jest.Mock = jest.fn();
|
||||
const filebeatDataView = { id: '6f1eeb50-023d-11eb-bcb6-6ba0578012a9', title: 'filebeat-*' };
|
||||
const auditbeatDataView = { id: '28995490-023d-11eb-bcb6-6ba0578012a9', title: 'auditbeat-*' };
|
||||
const mockSelector = {
|
||||
kibanaDataViews: [filebeatDataView, auditbeatDataView],
|
||||
};
|
||||
const embeddableValue = {
|
||||
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',
|
||||
};
|
||||
describe('EmbeddedMapComponent', () => {
|
||||
beforeEach(() => {
|
||||
setQuery.mockClear();
|
||||
mockGetStorage.mockReturnValue(true);
|
||||
jest.spyOn(redux, 'useSelector').mockReturnValue(mockSelector);
|
||||
mockUseSourcererDataView.mockReturnValue({ selectedPatterns: ['filebeat-*', 'packetbeat-*'] });
|
||||
mockCreateEmbeddable.mockResolvedValue(embeddableValue);
|
||||
mockUseIsFieldInIndexPattern.mockReturnValue(() => [true, true]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
test('renders', async () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<EmbeddedMapComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('EmbeddedMapComponent')).toMatchSnapshot();
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('EmbeddedMapComponent')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('renders services.embeddable.EmbeddablePanel', async () => {
|
||||
const spy = jest.spyOn(redux, 'useSelector');
|
||||
spy.mockReturnValue(mockSelector);
|
||||
|
||||
(createEmbeddable as jest.Mock).mockResolvedValue(mockCreateEmbeddable);
|
||||
|
||||
const wrapper: ReactWrapper = mount(
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<EmbeddedMapComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
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);
|
||||
expect(getByTestId('EmbeddablePanel')).toBeInTheDocument();
|
||||
expect(queryByTestId('IndexPatternsMissingPrompt')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('loading-spinner')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('renders IndexPatternsMissingPrompt', async () => {
|
||||
const spy = jest.spyOn(redux, 'useSelector');
|
||||
spy.mockReturnValue({
|
||||
jest.spyOn(redux, 'useSelector').mockReturnValue({
|
||||
...mockSelector,
|
||||
kibanaDataViews: [],
|
||||
});
|
||||
|
||||
(createEmbeddable as jest.Mock).mockResolvedValue(mockCreateEmbeddable);
|
||||
|
||||
const wrapper: ReactWrapper = mount(
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<EmbeddedMapComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
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);
|
||||
expect(queryByTestId('EmbeddablePanel')).not.toBeInTheDocument();
|
||||
expect(getByTestId('IndexPatternsMissingPrompt')).toBeInTheDocument();
|
||||
expect(queryByTestId('loading-spinner')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('renders Loader', async () => {
|
||||
const spy = jest.spyOn(redux, 'useSelector');
|
||||
spy.mockReturnValue(mockSelector);
|
||||
mockCreateEmbeddable.mockResolvedValue(null);
|
||||
|
||||
(createEmbeddable as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
const wrapper: ReactWrapper = mount(
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<EmbeddedMapComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
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);
|
||||
expect(queryByTestId('EmbeddablePanel')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('IndexPatternsMissingPrompt')).not.toBeInTheDocument();
|
||||
expect(getByTestId('loading-spinner')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('map hidden on close', async () => {
|
||||
mockGetStorage.mockReturnValue(false);
|
||||
const wrapper = mount(
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<EmbeddedMapComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="siemEmbeddable"]').first().exists()).toEqual(false);
|
||||
|
||||
const container = wrapper.find('[data-test-subj="false-toggle-network-map"]').last();
|
||||
container.simulate('click');
|
||||
expect(queryByTestId('siemEmbeddable')).not.toBeInTheDocument();
|
||||
getByTestId('false-toggle-network-map').click();
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(mockSetStorage).toHaveBeenNthCalledWith(1, 'network_map_visbile', true);
|
||||
expect(wrapper.find('[data-test-subj="siemEmbeddable"]').first().exists()).toEqual(true);
|
||||
expect(getByTestId('siemEmbeddable')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('map visible on open', async () => {
|
||||
const wrapper = mount(
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<EmbeddedMapComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="siemEmbeddable"]').first().exists()).toEqual(true);
|
||||
const container = wrapper.find('[data-test-subj="true-toggle-network-map"]').last();
|
||||
container.simulate('click');
|
||||
expect(getByTestId('siemEmbeddable')).toBeInTheDocument();
|
||||
getByTestId('true-toggle-network-map').click();
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(mockSetStorage).toHaveBeenNthCalledWith(1, 'network_map_visbile', false);
|
||||
expect(wrapper.find('[data-test-subj="siemEmbeddable"]').first().exists()).toEqual(false);
|
||||
expect(queryByTestId('siemEmbeddable')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('On mount, selects existing Kibana data views that match any selected index pattern', async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<EmbeddedMapComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
const dataViewArg = (getLayerList as jest.Mock).mock.calls[0][0];
|
||||
expect(dataViewArg).toEqual([filebeatDataView]);
|
||||
});
|
||||
});
|
||||
|
||||
test('On rerender with new selected patterns, selects existing Kibana data views that match any selected index pattern', async () => {
|
||||
mockUseSourcererDataView
|
||||
.mockReturnValueOnce({ selectedPatterns: ['filebeat-*', 'packetbeat-*'] })
|
||||
.mockReturnValue({ selectedPatterns: ['filebeat-*', 'auditbeat-*'] });
|
||||
const { rerender } = render(
|
||||
<TestProviders>
|
||||
<EmbeddedMapComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
const dataViewArg = (getLayerList as jest.Mock).mock.calls[0][0];
|
||||
expect(dataViewArg).toEqual([filebeatDataView]);
|
||||
});
|
||||
rerender(
|
||||
<TestProviders>
|
||||
<EmbeddedMapComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
// data view is updated with the returned embeddable.setLayerList callback, which is passesd getLayerList(dataViews)
|
||||
const dataViewArg = (getLayerList as jest.Mock).mock.calls[1][0];
|
||||
expect(dataViewArg).toEqual([filebeatDataView, auditbeatDataView]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import { EuiAccordion, EuiLink, EuiText } from '@elastic/eui';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { createHtmlPortalNode, InPortal } from 'react-reverse-portal';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
@ -15,8 +14,9 @@ import type { Filter, Query } from '@kbn/es-query';
|
|||
import type { ErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
import type { MapEmbeddable } from '@kbn/maps-plugin/public/embeddable';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import { useIsFieldInIndexPattern } from '../../../containers/fields';
|
||||
import { Loader } from '../../../../common/components/loader';
|
||||
import { displayErrorToast, useStateToaster } from '../../../../common/components/toasters';
|
||||
import type { GlobalTimeArgs } from '../../../../common/containers/use_global_time';
|
||||
import { Embeddable } from './embeddable';
|
||||
import { createEmbeddable } from './create_embeddable';
|
||||
|
@ -24,8 +24,9 @@ import { IndexPatternsMissingPrompt } from './index_patterns_missing_prompt';
|
|||
import { MapToolTip } from './map_tool_tip/map_tool_tip';
|
||||
import * as i18n from './translations';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { getLayerList } from './map_config';
|
||||
import { getLayerList, getRequiredMapsFields } from './map_config';
|
||||
import { sourcererSelectors } from '../../../../common/store/sourcerer';
|
||||
import type { SourcererDataView } from '../../../../common/store/sourcerer/model';
|
||||
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
|
||||
|
@ -110,7 +111,7 @@ export const EmbeddedMapComponent = ({
|
|||
const [isIndexError, setIsIndexError] = useState(false);
|
||||
const [storageValue, setStorageValue] = useState(storage.get(NETWORK_MAP_VISIBLE) ?? true);
|
||||
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
const { addError } = useAppToasts();
|
||||
|
||||
const getDataViewsSelector = useMemo(
|
||||
() => sourcererSelectors.getSourcererDataViewsSelector(),
|
||||
|
@ -119,9 +120,50 @@ export const EmbeddedMapComponent = ({
|
|||
const { kibanaDataViews } = useDeepEqualSelector((state) => getDataViewsSelector(state));
|
||||
const { selectedPatterns } = useSourcererDataView(SourcererScopeName.default);
|
||||
|
||||
const [mapIndexPatterns, setMapIndexPatterns] = useState(
|
||||
kibanaDataViews.filter((dataView) => selectedPatterns.includes(dataView.title))
|
||||
);
|
||||
const isFieldInIndexPattern = useIsFieldInIndexPattern();
|
||||
|
||||
const [mapDataViews, setMapDataViews] = useState<SourcererDataView[]>([]);
|
||||
|
||||
const availableDataViews = useMemo(() => {
|
||||
const dataViews = kibanaDataViews.filter((dataView) =>
|
||||
selectedPatterns.includes(dataView.title)
|
||||
);
|
||||
if (selectedPatterns.length > 0 && dataViews.length === 0) {
|
||||
setIsIndexError(true);
|
||||
}
|
||||
return dataViews;
|
||||
}, [kibanaDataViews, selectedPatterns]);
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const apiResponse = await Promise.all(
|
||||
availableDataViews.map(async ({ title }) =>
|
||||
isFieldInIndexPattern(title, getRequiredMapsFields(title))
|
||||
)
|
||||
);
|
||||
// ensures only index patterns with maps fields are passed
|
||||
const goodDataViews = availableDataViews.filter((_, i) => apiResponse[i] ?? false);
|
||||
if (!canceled) {
|
||||
setMapDataViews(goodDataViews);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!canceled) {
|
||||
setMapDataViews([]);
|
||||
addError(e, { title: i18n.ERROR_CREATING_EMBEDDABLE });
|
||||
setIsError(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (availableDataViews.length) {
|
||||
fetchData();
|
||||
}
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [addError, availableDataViews, isFieldInIndexPattern]);
|
||||
|
||||
// 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
|
||||
|
@ -129,21 +171,6 @@ export const EmbeddedMapComponent = ({
|
|||
// Search InPortal/OutPortal for implementation touch points
|
||||
const portalNode = React.useMemo(() => createHtmlPortalNode(), []);
|
||||
|
||||
useEffect(() => {
|
||||
setMapIndexPatterns((prevMapIndexPatterns) => {
|
||||
const newIndexPatterns = kibanaDataViews.filter((dataView) =>
|
||||
selectedPatterns.includes(dataView.title)
|
||||
);
|
||||
if (!deepEqual(newIndexPatterns, prevMapIndexPatterns)) {
|
||||
if (newIndexPatterns.length === 0) {
|
||||
setIsError(true);
|
||||
}
|
||||
return newIndexPatterns;
|
||||
}
|
||||
return prevMapIndexPatterns;
|
||||
});
|
||||
}, [kibanaDataViews, selectedPatterns]);
|
||||
|
||||
// Initial Load useEffect
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
|
@ -152,7 +179,7 @@ export const EmbeddedMapComponent = ({
|
|||
try {
|
||||
const embeddableObject = await createEmbeddable(
|
||||
filters,
|
||||
mapIndexPatterns,
|
||||
mapDataViews,
|
||||
query,
|
||||
startDate,
|
||||
endDate,
|
||||
|
@ -161,21 +188,17 @@ export const EmbeddedMapComponent = ({
|
|||
services.embeddable
|
||||
);
|
||||
if (isSubscribed) {
|
||||
if (mapIndexPatterns.length === 0) {
|
||||
setIsIndexError(true);
|
||||
} else {
|
||||
setEmbeddable(embeddableObject);
|
||||
setIsIndexError(false);
|
||||
}
|
||||
setEmbeddable(embeddableObject);
|
||||
}
|
||||
} catch (e) {
|
||||
if (isSubscribed) {
|
||||
displayErrorToast(i18n.ERROR_CREATING_EMBEDDABLE, [e.message], dispatchToaster);
|
||||
addError(e, { title: i18n.ERROR_CREATING_EMBEDDABLE });
|
||||
setIsError(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (embeddable == null && selectedPatterns.length > 0) {
|
||||
|
||||
if (embeddable == null && selectedPatterns.length > 0 && !isIndexError) {
|
||||
setupEmbeddable();
|
||||
}
|
||||
|
||||
|
@ -183,32 +206,33 @@ export const EmbeddedMapComponent = ({
|
|||
isSubscribed = false;
|
||||
};
|
||||
}, [
|
||||
dispatchToaster,
|
||||
addError,
|
||||
endDate,
|
||||
embeddable,
|
||||
filters,
|
||||
mapIndexPatterns,
|
||||
mapDataViews,
|
||||
query,
|
||||
portalNode,
|
||||
services.embeddable,
|
||||
selectedPatterns,
|
||||
setQuery,
|
||||
startDate,
|
||||
isIndexError,
|
||||
]);
|
||||
|
||||
// update layer with new index patterns
|
||||
useEffect(() => {
|
||||
const setLayerList = async () => {
|
||||
if (embeddable != null) {
|
||||
if (embeddable != null && mapDataViews.length) {
|
||||
// @ts-expect-error
|
||||
await embeddable.setLayerList(getLayerList(mapIndexPatterns));
|
||||
await embeddable.setLayerList(getLayerList(mapDataViews));
|
||||
embeddable.reload();
|
||||
}
|
||||
};
|
||||
if (embeddable != null && !isErrorEmbeddable(embeddable)) {
|
||||
setLayerList();
|
||||
}
|
||||
}, [embeddable, mapIndexPatterns]);
|
||||
}, [embeddable, mapDataViews]);
|
||||
|
||||
// queryExpression updated useEffect
|
||||
useEffect(() => {
|
||||
|
@ -267,6 +291,7 @@ export const EmbeddedMapComponent = ({
|
|||
|
||||
return isError ? null : (
|
||||
<StyledEuiAccordion
|
||||
data-test-subj="EmbeddedMapComponent"
|
||||
onToggle={setDefaultMapVisibility}
|
||||
id={'network-map'}
|
||||
arrowDisplay="right"
|
||||
|
|
|
@ -97,6 +97,16 @@ export const lmc: LayerMappingCollection = {
|
|||
'traces-apm*,logs-apm*,metrics-apm*,apm-*': APM_LAYER_FIELD_MAPPING,
|
||||
};
|
||||
|
||||
export const getRequiredMapsFields = (title: string): string[] => {
|
||||
const fieldMappings = lmc[title] ?? lmc.default;
|
||||
return [
|
||||
fieldMappings.source.metricField,
|
||||
fieldMappings.source.geoField,
|
||||
fieldMappings.destination.metricField,
|
||||
fieldMappings.destination.geoField,
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns `Source/Destination Point-to-point` Map LayerList configuration, with a source,
|
||||
* destination, and line layer for each of the provided indexPatterns
|
||||
|
|
|
@ -5,4 +5,5 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
require('../../src/setup_node_env');
|
||||
require('@kbn/test').runJest();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue