[ML] Decouple data_visualizer from MapEmbeddable (#181928)

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

### test instructions
1. install web logs sample data
2. Open discover
3. In table, click "Field statistics"
4. Expand `geo.dest` row. Verify choropleth map is displayed.
5. Expand `geo.coordinates` row. Verify vector map is displayed.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2024-05-01 11:42:15 -06:00 committed by GitHub
parent 5ba6a399f2
commit 1e8d51ae51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 294 additions and 366 deletions

View file

@ -1,8 +0,0 @@
.embeddedMap__content {
width: 100%;
height: 100%;
display: flex;
flex: 1 1 100%;
z-index: 1;
min-height: 0; // Absolute must for Firefox to scroll contents
}

View file

@ -1,148 +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 } from '@kbn/maps-plugin/common';
import type {
MapEmbeddable,
MapEmbeddableInput,
MapEmbeddableOutput,
} from '@kbn/maps-plugin/public/embeddable';
import type { RenderTooltipContentParams } from '@kbn/maps-plugin/public';
import { MAP_SAVED_OBJECT_TYPE } from '@kbn/maps-plugin/public';
import type { EmbeddableFactory, ErrorEmbeddable } from '@kbn/embeddable-plugin/public';
import { isErrorEmbeddable, ViewMode } from '@kbn/embeddable-plugin/public';
import { useDataVisualizerKibana } from '../../../kibana_context';
import './_embedded_map.scss';
export function EmbeddedMapComponent({
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, data },
} = useDataVisualizerKibana();
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: data.query.filterManager.getFilters() ?? [],
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="dataVisualizerEmbeddedMapContent"
className="embeddedMap__content"
ref={embeddableRoot}
/>
);
}

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 { EmbeddedMapComponent } from './embedded_map';

View file

@ -10,15 +10,19 @@ import React, { useMemo } from 'react';
import type { Feature, Point } from 'geojson';
import type { FieldDataRowProps } from '../../stats_table/types/field_data_row';
import { DocumentStatsTable } from '../../stats_table/components/field_data_expanded_row/document_stats';
import { EmbeddedMapComponent } from '../../embedded_map';
import { convertWKTGeoToLonLat, getGeoPointsLayer } from './format_utils';
import { ExpandedRowContent } from '../../stats_table/components/field_data_expanded_row/expanded_row_content';
import { ExamplesList } from '../../examples_list';
import { ExpandedRowPanel } from '../../stats_table/components/field_data_expanded_row/expanded_row_panel';
import { useDataVisualizerKibana } from '../../../../kibana_context';
export const DEFAULT_GEO_REGEX = RegExp('(?<lat>.+) (?<lon>.+)');
export const GeoPointContent: FC<FieldDataRowProps> = ({ config }) => {
const {
services: { maps: mapsService },
} = useDataVisualizerKibana();
const formattedResults = useMemo(() => {
const { stats } = config;
@ -54,7 +58,7 @@ export const GeoPointContent: FC<FieldDataRowProps> = ({ config }) => {
if (geoPointsFeatures.length > 0) {
return {
examples: formattedExamples,
layerList: [getGeoPointsLayer(geoPointsFeatures)],
pointsLayer: getGeoPointsLayer(geoPointsFeatures),
};
}
}
@ -62,12 +66,10 @@ export const GeoPointContent: FC<FieldDataRowProps> = ({ config }) => {
return (
<ExpandedRowContent dataTestSubj={'dataVisualizerGeoPointContent'}>
<DocumentStatsTable config={config} />
{formattedResults && Array.isArray(formattedResults.examples) && (
<ExamplesList examples={formattedResults.examples} />
)}
{formattedResults && Array.isArray(formattedResults.layerList) && (
{formattedResults?.examples && <ExamplesList examples={formattedResults.examples} />}
{mapsService && formattedResults?.pointsLayer && (
<ExpandedRowPanel className={'dvPanel__wrapper dvMap__wrapper'} grow={true}>
<EmbeddedMapComponent layerList={formattedResults.layerList} />
<mapsService.PassiveMap passiveLayer={formattedResults.pointsLayer} />
</ExpandedRowPanel>
)}
</ExpandedRowContent>

View file

@ -5,9 +5,11 @@
* 2.0.
*/
import type { FC } from 'react';
import { useMemo } from 'react';
import React, { useEffect, useState } from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { ES_GEO_FIELD_TYPE, LayerDescriptor } from '@kbn/maps-plugin/common';
import { INITIAL_LOCATION } from '@kbn/maps-plugin/common';
import type { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query';
import { ExpandedRowContent } from '../../stats_table/components/field_data_expanded_row/expanded_row_content';
import { DocumentStatsTable } from '../../stats_table/components/field_data_expanded_row/document_stats';
@ -15,7 +17,6 @@ import { ExamplesList } from '../../examples_list';
import type { FieldVisConfig } from '../../stats_table/types';
import { useDataVisualizerKibana } from '../../../../kibana_context';
import { SUPPORTED_FIELD_TYPES } from '../../../../../../common/constants';
import { EmbeddedMapComponent } from '../../embedded_map';
import { ExpandedRowPanel } from '../../stats_table/components/field_data_expanded_row/expanded_row_panel';
export const GeoPointContentWithMap: FC<{
@ -31,84 +32,106 @@ export const GeoPointContentWithMap: FC<{
services: { maps: mapsPlugin, data },
} = useDataVisualizerKibana();
// Update the layer list with updated geo points upon refresh
useEffect(() => {
async function updateIndexPatternSearchLayer() {
if (
dataView?.id !== undefined &&
config !== undefined &&
config.fieldName !== undefined &&
(config.type === SUPPORTED_FIELD_TYPES.GEO_POINT ||
config.type === SUPPORTED_FIELD_TYPES.GEO_SHAPE)
) {
const params = {
indexPatternId: dataView.id,
geoFieldName: config.fieldName,
geoFieldType: config.type as ES_GEO_FIELD_TYPE,
filters: data.query.filterManager.getFilters() ?? [],
...(typeof esql === 'string' ? { esql, type: 'ESQL' } : {}),
...(combinedQuery
? {
query: {
query: combinedQuery.searchString,
language: combinedQuery.searchQueryLanguage,
},
}
: {}),
};
const searchLayerDescriptor = mapsPlugin
? await mapsPlugin.createLayerDescriptors.createESSearchSourceLayerDescriptor(params)
: null;
if (searchLayerDescriptor?.sourceDescriptor) {
if (esql !== undefined) {
// Currently, createESSearchSourceLayerDescriptor doesn't support ES|QL yet
// but we can manually override the source descriptor with the ES|QL ESQLSourceDescriptor
const esqlSourceDescriptor = {
columns: [
{
name: config.fieldName,
type: config.type,
},
],
dataViewId: dataView.id,
dateField: dataView.timeFieldName ?? timeFieldName,
geoField: config.fieldName,
esql,
narrowByGlobalSearch: true,
narrowByGlobalTime: true,
narrowByMapBounds: true,
id: searchLayerDescriptor.sourceDescriptor.id,
type: 'ESQL',
applyForceRefresh: true,
};
setLayerList([
...layerList,
{
...searchLayerDescriptor,
sourceDescriptor: esqlSourceDescriptor,
},
]);
} else {
setLayerList([...layerList, searchLayerDescriptor]);
}
const query = useMemo(() => {
return combinedQuery
? {
query: combinedQuery.searchString,
language: combinedQuery.searchQueryLanguage,
}
}
: undefined;
}, [combinedQuery]);
useEffect(() => {
if (!mapsPlugin) {
return;
}
updateIndexPatternSearchLayer();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataView, combinedQuery, esql, config, mapsPlugin, data.query]);
if (
!dataView?.id ||
!config?.fieldName ||
!(
config.type === SUPPORTED_FIELD_TYPES.GEO_POINT ||
config.type === SUPPORTED_FIELD_TYPES.GEO_SHAPE
)
) {
setLayerList([]);
return;
}
let ignore = false;
mapsPlugin.createLayerDescriptors
.createESSearchSourceLayerDescriptor({
indexPatternId: dataView.id,
geoFieldName: config.fieldName,
geoFieldType: config.type as ES_GEO_FIELD_TYPE,
})
.then((searchLayerDescriptor) => {
if (ignore) {
return;
}
if (esql !== undefined) {
// Currently, createESSearchSourceLayerDescriptor doesn't support ES|QL yet
// but we can manually override the source descriptor with the ES|QL ESQLSourceDescriptor
const esqlSourceDescriptor = {
columns: [
{
name: config.fieldName,
type: config.type,
},
],
dataViewId: dataView.id,
dateField: dataView.timeFieldName ?? timeFieldName,
geoField: config.fieldName,
esql,
narrowByGlobalSearch: true,
narrowByGlobalTime: true,
narrowByMapBounds: true,
id: searchLayerDescriptor.sourceDescriptor!.id,
type: 'ESQL',
applyForceRefresh: true,
};
setLayerList([
{
...searchLayerDescriptor,
sourceDescriptor: esqlSourceDescriptor,
},
]);
} else {
setLayerList([searchLayerDescriptor]);
}
})
.catch(() => {
if (!ignore) {
setLayerList([]);
}
});
return () => {
ignore = true;
};
}, [dataView, combinedQuery, esql, config, mapsPlugin, timeFieldName]);
if (stats?.examples === undefined) return null;
return (
<ExpandedRowContent dataTestSubj={'dataVisualizerIndexBasedMapContent'}>
<DocumentStatsTable config={config} />
<ExamplesList examples={stats?.examples} />
<ExpandedRowPanel className={'dvPanel__wrapper dvMap__wrapper'} grow={true}>
<EmbeddedMapComponent layerList={layerList} />
</ExpandedRowPanel>
{mapsPlugin && (
<ExpandedRowPanel className={'dvPanel__wrapper dvMap__wrapper'} grow={true}>
<mapsPlugin.Map
layerList={layerList}
hideFilterActions={true}
mapSettings={{
initialLocation: INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS,
autoFitToDataBounds: true,
}}
filters={data.query.filterManager.getFilters()}
query={query}
timeRange={data.query.timefilter.timefilter.getTime()}
/>
</ExpandedRowPanel>
)}
</ExpandedRowContent>
);
};

View file

@ -7,7 +7,7 @@
import type { FC } from 'react';
import React, { useMemo } from 'react';
import { EuiText, htmlIdGenerator } from '@elastic/eui';
import { EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { VectorLayerDescriptor } from '@kbn/maps-plugin/common';
@ -21,7 +21,6 @@ import {
import type { EMSTermJoinConfig } from '@kbn/maps-plugin/public';
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types';
import { useDataVisualizerKibana } from '../../../../../kibana_context';
import { EmbeddedMapComponent } from '../../../embedded_map';
import type { FieldVisStats } from '../../../../../../../common/types';
import { ExpandedRowPanel } from './expanded_row_panel';
@ -31,7 +30,7 @@ export const getChoroplethTopValuesLayer = (
{ layerId, field }: EMSTermJoinConfig
): VectorLayerDescriptor => {
return {
id: htmlIdGenerator()(),
id: 'choroplethLayer',
label: i18n.translate('xpack.dataVisualizer.choroplethMap.topValuesCount', {
defaultMessage: 'Top values count for {fieldName}',
values: { fieldName },
@ -41,7 +40,7 @@ export const getChoroplethTopValuesLayer = (
// Left join is the id from the type of field (e.g. world_countries)
leftField: field,
right: {
id: 'anomaly_count',
id: 'doc_count',
type: SOURCE_TYPES.TABLE_SOURCE,
__rows: topValues,
__columns: [
@ -103,13 +102,14 @@ export const ChoroplethMap: FC<Props> = ({ stats, suggestion }) => {
const {
services: {
data: { fieldFormats },
maps: mapsService,
},
} = useDataVisualizerKibana();
const { fieldName, isTopValuesSampled, topValues, sampleCount } = stats;
const layerList: VectorLayerDescriptor[] = useMemo(
() => [getChoroplethTopValuesLayer(fieldName || '', topValues || [], suggestion)],
const choroplethLayer: VectorLayerDescriptor = useMemo(
() => getChoroplethTopValuesLayer(fieldName || '', topValues || [], suggestion),
[suggestion, fieldName, topValues]
);
@ -157,9 +157,11 @@ export const ChoroplethMap: FC<Props> = ({ stats, suggestion }) => {
className={'dvPanel__wrapper'}
grow={true}
>
<div className={'dvMap__wrapper'}>
<EmbeddedMapComponent layerList={layerList} />
</div>
{mapsService && (
<div className={'dvMap__wrapper'}>
<mapsService.PassiveMap passiveLayer={choroplethLayer} />
</div>
)}
{countsElement}
</ExpandedRowPanel>

View file

@ -8,6 +8,8 @@
import type { LayerDescriptor } from '../../common/descriptor_types';
import type { CreateLayerDescriptorParams } from '../classes/sources/es_search_source';
import type { SampleValuesConfig, EMSTermJoinConfig } from '../ems_autosuggest';
import type { Props as PassiveMapProps } from '../lens/passive_map';
import type { Props as MapProps } from '../embeddable/map_component';
export interface MapsStartApi {
createLayerDescriptors: {
@ -20,5 +22,7 @@ export interface MapsStartApi {
params: CreateLayerDescriptorParams
) => Promise<LayerDescriptor>;
};
Map: React.FC<MapProps>;
PassiveMap: React.FC<PassiveMapProps>;
suggestEMSTermJoinConfig(config: SampleValuesConfig): Promise<EMSTermJoinConfig | null>;
}

View file

@ -8,19 +8,21 @@
import React, { Component, RefObject } from 'react';
import { first } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
import type { Filter } from '@kbn/es-query';
import type { Query, TimeRange } from '@kbn/es-query';
import type { LayerDescriptor, MapCenterAndZoom } from '../../common/descriptor_types';
import type { MapEmbeddableType } from './types';
import type { Filter, Query, TimeRange } from '@kbn/es-query';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import type { LayerDescriptor, MapCenterAndZoom, MapSettings } from '../../common/descriptor_types';
import { MapEmbeddable } from './map_embeddable';
import { createBasemapLayerDescriptor } from '../classes/layers/create_basemap_layer_descriptor';
interface Props {
title: string;
export interface Props {
title?: string;
filters?: Filter[];
query?: Query;
timeRange?: TimeRange;
getLayerDescriptors: () => LayerDescriptor[];
layerList: LayerDescriptor[];
mapSettings?: Partial<MapSettings>;
hideFilterActions?: boolean;
isLayerTOCOpen?: boolean;
mapCenter?: MapCenterAndZoom;
onInitialRenderComplete?: () => void;
/*
@ -30,11 +32,13 @@ interface Props {
}
export class MapComponent extends Component<Props> {
private _mapEmbeddable: MapEmbeddableType;
private _prevLayerList: LayerDescriptor[];
private _mapEmbeddable: MapEmbeddable;
private readonly _embeddableRef: RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>();
constructor(props: Props) {
super(props);
this._prevLayerList = this.props.layerList;
this._mapEmbeddable = new MapEmbeddable(
{
editable: false,
@ -42,15 +46,24 @@ export class MapComponent extends Component<Props> {
{
id: uuidv4(),
attributes: {
title: this.props.title,
layerListJSON: JSON.stringify([
createBasemapLayerDescriptor(),
...this.props.getLayerDescriptors(),
]),
title: this.props.title ?? '',
layerListJSON: JSON.stringify(this.getLayerList()),
},
hidePanelTitles: !Boolean(this.props.title),
viewMode: ViewMode.VIEW,
isLayerTOCOpen:
typeof this.props.isLayerTOCOpen === 'boolean' ? this.props.isLayerTOCOpen : false,
hideFilterActions:
typeof this.props.hideFilterActions === 'boolean' ? this.props.hideFilterActions : false,
mapCenter: this.props.mapCenter,
mapSettings: this.props.mapSettings ?? {},
}
);
this._mapEmbeddable.updateInput({
filters: this.props.filters,
query: this.props.query,
timeRange: this.props.timeRange,
});
if (this.props.onInitialRenderComplete) {
this._mapEmbeddable
@ -84,6 +97,16 @@ export class MapComponent extends Component<Props> {
query: this.props.query,
timeRange: this.props.timeRange,
});
if (this._prevLayerList !== this.props.layerList) {
this._mapEmbeddable.setLayerList(this.getLayerList());
this._prevLayerList = this.props.layerList;
}
}
getLayerList(): LayerDescriptor[] {
const basemapLayer = createBasemapLayerDescriptor();
return basemapLayer ? [basemapLayer, ...this.props.layerList] : this.props.layerList;
}
render() {

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { dynamic } from '@kbn/shared-ux-utility';
import type { Props } from './map_component';
const Component = dynamic(async () => {
const { MapComponent } = await import('./map_component');
return {
default: MapComponent,
};
});
export function MapComponentLazy(props: Props) {
return <Component {...props} />;
}

View file

@ -5,16 +5,19 @@
* 2.0.
*/
import React, { lazy } from 'react';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import type { ExpressionRenderDefinition } from '@kbn/expressions-plugin/common';
import { dynamic } from '@kbn/shared-ux-utility';
import type { RegionMapVisRenderValue } from './region_map_fn';
import { LazyWrapper } from '../../lazy_wrapper';
import { REGION_MAP_RENDER } from './types';
const getLazyComponent = () => {
return lazy(() => import('./region_map_visualization'));
};
const Component = dynamic(async () => {
const { RegionMapVisualization } = await import('./region_map_visualization');
return {
default: RegionMapVisualization,
};
});
export const regionMapRenderer = {
name: REGION_MAP_RENDER,
@ -34,6 +37,6 @@ export const regionMapRenderer = {
visConfig,
};
render(<LazyWrapper getLazyComponent={getLazyComponent} lazyComponentProps={props} />, domNode);
render(<Component {...props} />, domNode);
},
} as ExpressionRenderDefinition<RegionMapVisRenderValue>;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React from 'react';
import React, { useMemo } from 'react';
import type { Filter } from '@kbn/es-query';
import type { Query, TimeRange } from '@kbn/es-query';
import { RegionMapVisConfig } from './types';
@ -20,30 +20,33 @@ interface Props {
onInitialRenderComplete: () => void;
}
function RegionMapVisualization(props: Props) {
const mapCenter = {
lat: props.visConfig.mapCenter[0],
lon: props.visConfig.mapCenter[1],
zoom: props.visConfig.mapZoom,
};
function getLayerDescriptors() {
export function RegionMapVisualization(props: Props) {
const initialMapCenter = useMemo(() => {
return {
lat: props.visConfig.mapCenter[0],
lon: props.visConfig.mapCenter[1],
zoom: props.visConfig.mapZoom,
};
// props.visConfig reference changes each render but values are the same
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const initialLayerList = useMemo(() => {
const layerDescriptor = createRegionMapLayerDescriptor(props.visConfig.layerDescriptorParams);
return layerDescriptor ? [layerDescriptor] : [];
}
// props.visConfig reference changes each render but values are the same
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<MapComponent
title={props.visConfig.layerDescriptorParams.label}
filters={props.filters}
query={props.query}
timeRange={props.timeRange}
mapCenter={mapCenter}
getLayerDescriptors={getLayerDescriptors}
mapCenter={initialMapCenter}
isLayerTOCOpen={true}
layerList={initialLayerList}
onInitialRenderComplete={props.onInitialRenderComplete}
isSharable={false}
/>
);
}
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export default RegionMapVisualization;

View file

@ -5,16 +5,19 @@
* 2.0.
*/
import React, { lazy } from 'react';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import type { ExpressionRenderDefinition } from '@kbn/expressions-plugin/common';
import { dynamic } from '@kbn/shared-ux-utility';
import type { TileMapVisRenderValue } from './tile_map_fn';
import { LazyWrapper } from '../../lazy_wrapper';
import { TILE_MAP_RENDER } from './types';
const getLazyComponent = () => {
return lazy(() => import('./tile_map_visualization'));
};
const Component = dynamic(async () => {
const { TileMapVisualization } = await import('./tile_map_visualization');
return {
default: TileMapVisualization,
};
});
export const tileMapRenderer = {
name: TILE_MAP_RENDER,
@ -34,6 +37,6 @@ export const tileMapRenderer = {
visConfig,
};
render(<LazyWrapper getLazyComponent={getLazyComponent} lazyComponentProps={props} />, domNode);
render(<Component {...props} />, domNode);
},
} as ExpressionRenderDefinition<TileMapVisRenderValue>;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React from 'react';
import React, { useMemo } from 'react';
import type { Filter } from '@kbn/es-query';
import type { Query, TimeRange } from '@kbn/es-query';
import type { TileMapVisConfig } from './types';
@ -20,30 +20,33 @@ interface Props {
onInitialRenderComplete: () => void;
}
function TileMapVisualization(props: Props) {
const mapCenter = {
lat: props.visConfig.mapCenter[0],
lon: props.visConfig.mapCenter[1],
zoom: props.visConfig.mapZoom,
};
function getLayerDescriptors() {
export function TileMapVisualization(props: Props) {
const initialMapCenter = useMemo(() => {
return {
lat: props.visConfig.mapCenter[0],
lon: props.visConfig.mapCenter[1],
zoom: props.visConfig.mapZoom,
};
// props.visConfig reference changes each render but values are the same
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const initialLayerList = useMemo(() => {
const layerDescriptor = createTileMapLayerDescriptor(props.visConfig.layerDescriptorParams);
return layerDescriptor ? [layerDescriptor] : [];
}
// props.visConfig reference changes each render but values are the same
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<MapComponent
title={props.visConfig.layerDescriptorParams.label}
filters={props.filters}
query={props.query}
timeRange={props.timeRange}
mapCenter={mapCenter}
getLayerDescriptors={getLayerDescriptors}
mapCenter={initialMapCenter}
isLayerTOCOpen={true}
layerList={initialLayerList}
onInitialRenderComplete={props.onInitialRenderComplete}
isSharable={false}
/>
);
}
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export default TileMapVisualization;

View file

@ -9,7 +9,6 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import type { FileLayer } from '@elastic/ems-client';
import { IUiSettingsClient } from '@kbn/core/public';
import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
import type { Datatable } from '@kbn/expressions-plugin/public';
import type { FormatFactory } from '@kbn/field-formats-plugin/common';
import {
@ -24,13 +23,11 @@ import { emsWorldLayerId } from '../../../common/constants';
import { ChoroplethChartProps } from './types';
import { getEmsSuggestion } from './get_ems_suggestion';
import { PassiveMap } from '../passive_map';
import type { MapEmbeddableInput, MapEmbeddableOutput } from '../../embeddable';
interface Props extends ChoroplethChartProps {
formatFactory: FormatFactory;
uiSettings: IUiSettingsClient;
emsFileLayers: FileLayer[];
mapEmbeddableFactory: EmbeddableFactory<MapEmbeddableInput, MapEmbeddableOutput>;
onRenderComplete: () => void;
}
@ -40,7 +37,6 @@ export function ChoroplethChart({
formatFactory,
uiSettings,
emsFileLayers,
mapEmbeddableFactory,
onRenderComplete,
}: Props) {
if (!args.regionAccessor || !args.valueAccessor) {
@ -130,13 +126,7 @@ export function ChoroplethChart({
type: LAYER_TYPE.GEOJSON_VECTOR,
};
return (
<PassiveMap
passiveLayer={choroplethLayer}
factory={mapEmbeddableFactory}
onRenderComplete={onRenderComplete}
/>
);
return <PassiveMap passiveLayer={choroplethLayer} onRenderComplete={onRenderComplete} />;
}
function getAccessorLabel(table: Datatable, accessor: string) {

View file

@ -8,7 +8,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import type { IInterpreterRenderHandlers } from '@kbn/expressions-plugin/public';
import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { METRIC_TYPE } from '@kbn/analytics';
import type { CoreSetup, CoreStart } from '@kbn/core/public';
import type { FileLayer } from '@elastic/ems-client';
@ -16,7 +15,6 @@ import type { KibanaExecutionContext } from '@kbn/core-execution-context-common'
import { ChartSizeEvent } from '@kbn/chart-expressions-common';
import type { MapsPluginStartDependencies } from '../../plugin';
import type { ChoroplethChartProps } from './types';
import type { MapEmbeddableInput, MapEmbeddableOutput } from '../../embeddable';
export const RENDERER_ID = 'lens_choropleth_chart_renderer';
@ -65,13 +63,6 @@ export function getExpressionRenderer(coreSetup: CoreSetup<MapsPluginStartDepend
const { ChoroplethChart } = await import('./choropleth_chart');
const { getEmsFileLayers } = await import('../../util');
const mapEmbeddableFactory = plugins.embeddable.getEmbeddableFactory(
'map'
) as EmbeddableFactory<MapEmbeddableInput, MapEmbeddableOutput>;
if (!mapEmbeddableFactory) {
return;
}
let emsFileLayers: FileLayer[] = [];
try {
emsFileLayers = await getEmsFileLayers();
@ -111,7 +102,6 @@ export function getExpressionRenderer(coreSetup: CoreSetup<MapsPluginStartDepend
formatFactory={plugins.fieldFormats.deserialize}
uiSettings={coreStart.uiSettings}
emsFileLayers={emsFileLayers}
mapEmbeddableFactory={mapEmbeddableFactory}
onRenderComplete={renderComplete}
/>,
domNode

View file

@ -6,3 +6,4 @@
*/
export { setupLensChoroplethChart } from './choropleth_chart';
export { PassiveMapLazy } from './passive_map_lazy';

View file

@ -9,16 +9,15 @@ import React, { Component, RefObject } from 'react';
import { Subscription } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
import { EuiLoadingChart } from '@elastic/eui';
import { EmbeddableFactory, ViewMode } from '@kbn/embeddable-plugin/public';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import type { LayerDescriptor } from '../../common/descriptor_types';
import { INITIAL_LOCATION } from '../../common';
import { MapEmbeddable, MapEmbeddableInput, MapEmbeddableOutput } from '../embeddable';
import { MapEmbeddable } from '../embeddable';
import { createBasemapLayerDescriptor } from '../classes/layers/create_basemap_layer_descriptor';
interface Props {
factory: EmbeddableFactory<MapEmbeddableInput, MapEmbeddableOutput>;
export interface Props {
passiveLayer: LayerDescriptor;
onRenderComplete: () => void;
onRenderComplete?: () => void;
}
interface State {
@ -65,36 +64,39 @@ export class PassiveMap extends Component<Props, State> {
async _setupEmbeddable() {
const basemapLayerDescriptor = createBasemapLayerDescriptor();
const intialLayers = basemapLayerDescriptor ? [basemapLayerDescriptor] : [];
const mapEmbeddable = (await this.props.factory.create({
id: uuidv4(),
attributes: {
title: '',
layerListJSON: JSON.stringify([...intialLayers, this.props.passiveLayer]),
const mapEmbeddable = new MapEmbeddable(
{
editable: false,
},
filters: [],
hidePanelTitles: true,
viewMode: ViewMode.VIEW,
isLayerTOCOpen: false,
hideFilterActions: true,
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
},
})) as MapEmbeddable | undefined;
if (!mapEmbeddable) {
return;
}
this._onRenderSubscription = mapEmbeddable.getOnRenderComplete$().subscribe(() => {
if (this._isMounted) {
this.props.onRenderComplete();
{
id: uuidv4(),
attributes: {
title: '',
layerListJSON: JSON.stringify([...intialLayers, this.props.passiveLayer]),
},
filters: [],
hidePanelTitles: true,
viewMode: ViewMode.VIEW,
isLayerTOCOpen: false,
hideFilterActions: true,
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
},
}
});
);
if (this.props.onRenderComplete) {
this._onRenderSubscription = mapEmbeddable.getOnRenderComplete$().subscribe(() => {
if (this._isMounted && this.props.onRenderComplete) {
this.props.onRenderComplete();
}
});
}
if (this._isMounted) {
mapEmbeddable.setIsSharable(false);

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { dynamic } from '@kbn/shared-ux-utility';
import type { Props } from './passive_map';
const Component = dynamic(async () => {
const { PassiveMap } = await import('./passive_map');
return {
default: PassiveMap,
};
});
export function PassiveMapLazy(props: Props) {
return <Component {...props} />;
}

View file

@ -90,10 +90,11 @@ import {
import { MapInspectorView } from './inspector/map_adapter/map_inspector_view';
import { VectorTileInspectorView } from './inspector/vector_tile_adapter/vector_tile_inspector_view';
import { setupLensChoroplethChart } from './lens';
import { PassiveMapLazy, setupLensChoroplethChart } from './lens';
import { CONTENT_ID, LATEST_VERSION, MapAttributes } from '../common/content_management';
import { savedObjectToEmbeddableAttributes } from './map_attribute_service';
import { MapByValueInput } from './embeddable';
import { MapComponentLazy } from './embeddable/map_component_lazy';
export interface MapsPluginSetupDependencies {
cloud?: CloudSetup;
@ -275,6 +276,8 @@ export class MapsPlugin
return {
createLayerDescriptors,
suggestEMSTermJoinConfig,
Map: MapComponentLazy,
PassiveMap: PassiveMapLazy,
};
}
}

View file

@ -87,7 +87,8 @@
"@kbn/presentation-publishing",
"@kbn/saved-objects-finder-plugin",
"@kbn/esql-utils",
"@kbn/apm-data-view"
"@kbn/apm-data-view",
"@kbn/shared-ux-utility"
],
"exclude": [
"target/**/*",

View file

@ -497,9 +497,7 @@ export function MachineLearningDataVisualizerTableProvider(
await this.assertExamplesList(fieldName, expectedExamplesCount);
await testSubjects.existOrFail(
this.detailsSelector(fieldName, 'dataVisualizerEmbeddedMapContent')
);
await testSubjects.existOrFail(this.detailsSelector(fieldName, 'mapContainer'));
await this.ensureDetailsClosed(fieldName);
}