[ML] decouple ML plugin from Map embeddable (#182409)

Part of https://github.com/elastic/kibana/issues/182020

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2024-05-07 14:18:50 -06:00 committed by GitHub
parent 9ffe1df25e
commit 8c20433bd0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 53 additions and 185 deletions

View file

@ -26,6 +26,7 @@ export type {
} from './classes/tooltips/tooltip_property';
export type { MapsSetupApi, MapsStartApi } from './api';
export type { CreateLayerDescriptorParams } from './classes/sources/es_search_source/create_layer_descriptor';
export type { MapEmbeddable, MapEmbeddableInput, MapEmbeddableOutput } from './embeddable';
export { type MapApi, isMapApi } from './embeddable/map_api';

View file

@ -1,8 +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.
*/
export { MlEmbeddedMapComponent } from './ml_embedded_map';

View file

@ -1,154 +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, { useEffect, useRef, useState } from 'react';
import { htmlIdGenerator } from '@elastic/eui';
import type { LayerDescriptor } from '@kbn/maps-plugin/common';
import { INITIAL_LOCATION, MAP_SAVED_OBJECT_TYPE } from '@kbn/maps-plugin/common';
import type {
MapEmbeddable,
MapEmbeddableInput,
MapEmbeddableOutput,
RenderTooltipContentParams,
} from '@kbn/maps-plugin/public';
import type { EmbeddableFactory, ErrorEmbeddable } from '@kbn/embeddable-plugin/public';
import { isErrorEmbeddable, ViewMode } from '@kbn/embeddable-plugin/public';
import { useMlKibana } from '../../contexts/kibana';
export function MlEmbeddedMapComponent({
layerList,
mapEmbeddableInput,
renderTooltipContent,
}: {
layerList: LayerDescriptor[];
mapEmbeddableInput?: MapEmbeddableInput;
renderTooltipContent?: (params: RenderTooltipContentParams) => JSX.Element;
}) {
const [embeddable, setEmbeddable] = useState<ErrorEmbeddable | MapEmbeddable | undefined>();
const embeddableRoot: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
const baseLayers = useRef<LayerDescriptor[]>();
const {
services: { embeddable: embeddablePlugin, maps: mapsPlugin },
} = useMlKibana();
const factory:
| EmbeddableFactory<MapEmbeddableInput, MapEmbeddableOutput, MapEmbeddable>
| undefined = embeddablePlugin
? embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE)
: undefined;
// Update the layer list with updated geo points upon refresh
useEffect(() => {
async function updateIndexPatternSearchLayer() {
if (
embeddable &&
!isErrorEmbeddable(embeddable) &&
Array.isArray(layerList) &&
Array.isArray(baseLayers.current)
) {
embeddable.setLayerList([...baseLayers.current, ...layerList]);
}
}
updateIndexPatternSearchLayer();
}, [embeddable, layerList]);
useEffect(() => {
async function setupEmbeddable() {
if (!factory) {
// eslint-disable-next-line no-console
console.error('Map embeddable not found.');
return;
}
const input: MapEmbeddableInput = {
id: htmlIdGenerator()(),
attributes: { title: '' },
filters: [],
hidePanelTitles: true,
viewMode: ViewMode.VIEW,
isLayerTOCOpen: false,
hideFilterActions: true,
// can use mapSettings to center map on anomalies
mapSettings: {
disableInteractive: false,
hideToolbarOverlay: false,
hideLayerControl: false,
hideViewControl: false,
initialLocation: INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS, // this will startup based on data-extent
autoFitToDataBounds: true, // this will auto-fit when there are changes to the filter and/or query
},
};
const embeddableObject = await factory.create(input);
if (embeddableObject && !isErrorEmbeddable(embeddableObject)) {
const basemapLayerDescriptor = mapsPlugin
? await mapsPlugin.createLayerDescriptors.createBasemapLayerDescriptor()
: null;
if (basemapLayerDescriptor) {
baseLayers.current = [basemapLayerDescriptor];
await embeddableObject.setLayerList(baseLayers.current);
}
}
setEmbeddable(embeddableObject);
}
setupEmbeddable();
// we want this effect to execute exactly once after the component mounts
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (embeddable && !isErrorEmbeddable(embeddable) && mapEmbeddableInput !== undefined) {
embeddable.updateInput(mapEmbeddableInput);
}
}, [embeddable, mapEmbeddableInput]);
useEffect(() => {
if (embeddable && !isErrorEmbeddable(embeddable) && renderTooltipContent !== undefined) {
embeddable.setRenderTooltipContent(renderTooltipContent);
}
}, [embeddable, renderTooltipContent]);
// We can only render after embeddable has already initialized
useEffect(() => {
if (embeddableRoot.current && embeddable) {
embeddable.render(embeddableRoot.current);
}
}, [embeddable, embeddableRoot]);
if (!embeddablePlugin) {
// eslint-disable-next-line no-console
console.error('Embeddable start plugin not found');
return null;
}
if (!mapsPlugin) {
// eslint-disable-next-line no-console
console.error('Maps start plugin not found');
return null;
}
return (
<div
data-test-subj="mlEmbeddedMapContent"
css={{
width: '100%',
height: '100%',
display: 'flex',
flex: '1 1 100%',
zIndex: 1,
minHeight: 0, // Absolute must for Firefox to scroll contents
}}
ref={embeddableRoot}
/>
);
}

View file

@ -18,6 +18,7 @@ import {
htmlIdGenerator,
} from '@elastic/eui';
import type { VectorLayerDescriptor } from '@kbn/maps-plugin/common';
import { INITIAL_LOCATION } from '@kbn/maps-plugin/common';
import {
FIELD_ORIGIN,
LAYER_TYPE,
@ -29,7 +30,6 @@ import type { EMSTermJoinConfig } from '@kbn/maps-plugin/public';
import { isDefined } from '@kbn/ml-is-defined';
import type { MlAnomaliesTableRecord } from '@kbn/ml-anomaly-utils';
import { useMlKibana } from '../contexts/kibana';
import { MlEmbeddedMapComponent } from '../components/ml_embedded_map';
const MAX_ENTITY_VALUES = 3;
@ -253,7 +253,16 @@ export const AnomaliesMap: FC<Props> = ({ anomalies, jobIds }) => {
data-test-subj="mlAnomalyExplorerAnomaliesMap"
style={{ width: '100%', height: 300 }}
>
<MlEmbeddedMapComponent layerList={layerList} />
{mapsPlugin && (
<mapsPlugin.Map
layerList={layerList}
hideFilterActions={true}
mapSettings={{
initialLocation: INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS,
autoFitToDataBounds: true,
}}
/>
)}
</div>
</EuiAccordion>
</EuiPanel>

View file

@ -7,14 +7,19 @@
import React, { useState, useEffect } from 'react';
import type { LayerDescriptor } from '@kbn/maps-plugin/common';
import { INITIAL_LOCATION } from '@kbn/maps-plugin/common';
import type { Dictionary } from '../../../../common/types/common';
import { getMLAnomaliesActualLayer, getMLAnomaliesTypicalLayer } from './map_config';
import { MlEmbeddedMapComponent } from '../../components/ml_embedded_map';
import { useMlKibana } from '../../contexts/kibana';
interface Props {
seriesConfig: Dictionary<any>;
}
export function EmbeddedMapComponentWrapper({ seriesConfig }: Props) {
const {
services: { maps: mapsPlugin },
} = useMlKibana();
const [layerList, setLayerList] = useState<LayerDescriptor[]>([]);
useEffect(() => {
@ -26,9 +31,16 @@ export function EmbeddedMapComponentWrapper({ seriesConfig }: Props) {
}
}, [seriesConfig]);
return (
return mapsPlugin ? (
<div data-test-subj="xpack.ml.explorer.embeddedMap" style={{ width: '100%', height: 300 }}>
<MlEmbeddedMapComponent layerList={layerList} />
<mapsPlugin.Map
layerList={layerList}
hideFilterActions={true}
mapSettings={{
initialLocation: INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS,
autoFitToDataBounds: true,
}}
/>
</div>
);
) : null;
}

View file

@ -9,7 +9,7 @@ import memoizeOne from 'memoize-one';
import { isEqual } from 'lodash';
import type { DataView } from '@kbn/data-views-plugin/common';
import type { ES_GEO_FIELD_TYPE, LayerDescriptor } from '@kbn/maps-plugin/common';
import type { MapsStartApi } from '@kbn/maps-plugin/public';
import type { CreateLayerDescriptorParams, MapsStartApi } from '@kbn/maps-plugin/public';
import type { Query } from '@kbn/es-query';
import type { Field, SplitField } from '@kbn/ml-anomaly-utils';
import { ChartLoader } from '../chart_loader';
@ -30,7 +30,6 @@ export class MapLoader extends ChartLoader {
geoField: Field,
splitField: SplitField,
fieldValues: string[],
filters?: any[],
savedSearchQuery?: Query
) {
const layerList: LayerDescriptor[] = [];
@ -41,11 +40,10 @@ export class MapLoader extends ChartLoader {
? `${splitField.name}:${fieldValues[0]} ${query ? `and ${query}` : ''}`
: `${query ? query : ''}`;
const params: any = {
const params: CreateLayerDescriptorParams = {
indexPatternId: this._dataView.id,
geoFieldName: geoField.name,
geoFieldType: geoField.type as unknown as ES_GEO_FIELD_TYPE,
filters: filters ?? [],
query: { query: queryString, language: 'kuery' },
};

View file

@ -9,9 +9,10 @@ import type { FC } from 'react';
import React from 'react';
import { EuiFlexGrid, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import type { LayerDescriptor } from '@kbn/maps-plugin/common';
import { INITIAL_LOCATION } from '@kbn/maps-plugin/common';
import type { Aggregation, Field, SplitField } from '@kbn/ml-anomaly-utils';
import { SplitCards, useAnimateSplit } from '../split_cards';
import { MlEmbeddedMapComponent } from '../../../../../../../components/ml_embedded_map';
import { useMlKibana } from '../../../../../../../contexts/kibana';
import { JOB_TYPE } from '../../../../../../../../../common/constants/new_job';
import { DetectorTitle } from '../detector_title';
@ -31,6 +32,10 @@ export const GeoMapExamples: FC<Props> = ({
geoAgg,
layerList,
}) => {
const {
services: { maps: mapsPlugin },
} = useMlKibana();
const animateSplit = useAnimateSplit();
return (
@ -46,9 +51,18 @@ export const GeoMapExamples: FC<Props> = ({
<>
{geoAgg && geoField ? <DetectorTitle index={0} agg={geoAgg} field={geoField} /> : null}
<EuiSpacer size="s" />
<span data-test-subj="mlGeoJobWizardMap" style={{ width: '100%', height: 400 }}>
<MlEmbeddedMapComponent layerList={layerList} />
</span>
{mapsPlugin && (
<span data-test-subj="mlGeoJobWizardMap" style={{ width: '100%', height: 400 }}>
<mapsPlugin.Map
layerList={layerList}
hideFilterActions={true}
mapSettings={{
initialLocation: INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS,
autoFitToDataBounds: true,
}}
/>
</span>
)}
</>
</EuiFlexItem>
</EuiFlexGrid>

View file

@ -29,7 +29,7 @@ export const GeoDetector: FC<Props> = ({ setIsValid }) => {
const [layerList, setLayerList] = useState<LayerDescriptor[]>([]);
const {
services: { data, notifications: toasts },
services: { notifications: toasts },
} = useMlKibana();
const { mapLoader } = useContext(JobCreatorContext);
@ -72,14 +72,12 @@ export const GeoDetector: FC<Props> = ({ setIsValid }) => {
useEffect(() => {
async function getMapLayersForGeoJob() {
if (jobCreator.geoField) {
const { filter, query } = jobCreator.savedSearchQuery ?? {};
const filters = [...data.query.filterManager.getFilters(), ...(filter ?? [])];
const { query } = jobCreator.savedSearchQuery ?? {};
const layers = await mapLoader.getMapLayersForGeoJob(
jobCreator.geoField,
jobCreator.splitField,
fieldValues,
filters,
query
);
setLayerList(layers);

View file

@ -24,7 +24,7 @@ export const GeoDetectorsSummary: FC = () => {
const splitField = jobCreator.splitField;
const {
services: { data, notifications },
services: { notifications },
} = useMlKibana();
// Load example field values for summary view
@ -56,14 +56,12 @@ export const GeoDetectorsSummary: FC = () => {
useEffect(() => {
async function getMapLayersForGeoJob() {
if (geoField) {
const { filter, query } = jobCreator.savedSearchQuery ?? {};
const filters = [...data.query.filterManager.getFilters(), ...(filter ?? [])];
const { query } = jobCreator.savedSearchQuery ?? {};
const layers = await mapLoader.getMapLayersForGeoJob(
geoField,
splitField,
fieldValues,
filters,
query
);
setLayerList(layers);

View file

@ -46,7 +46,7 @@ export function MachineLearningJobWizardGeoProvider({ getService }: FtrProviderC
);
await testSubjects.existOrFail('mlGeoJobWizardMap');
await testSubjects.existOrFail('mlEmbeddedMapContent');
await testSubjects.existOrFail('mapContainer');
},
};
}