Skip to content

Commit

Permalink
[ML] Add embedded map to geo_point fields for Data Visualizer (#88880)
Browse files Browse the repository at this point in the history
  • Loading branch information
qn895 authored Jan 27, 2021
1 parent 723dd32 commit da9ad2a
Show file tree
Hide file tree
Showing 46 changed files with 842 additions and 223 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ import {
MapEmbeddableInput,
MapEmbeddableOutput,
} from './types';
export { MapEmbeddableInput };
export { MapEmbeddableInput, MapEmbeddableOutput };

export class MapEmbeddable
extends Embeddable<MapEmbeddableInput, MapEmbeddableOutput>
Expand Down
6 changes: 4 additions & 2 deletions x-pack/plugins/ml/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"security",
"spaces",
"management",
"licenseManagement"
"licenseManagement",
"maps"
],
"server": true,
"ui": true,
Expand All @@ -35,7 +36,8 @@
"dashboard",
"savedObjects",
"home",
"spaces"
"spaces",
"maps"
],
"extraPublicDirs": [
"common"
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/ml/public/application/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
@import 'components/navigation_menu/index';
@import 'components/rule_editor/index'; // SASSTODO: This file overwrites EUI directly
@import 'components/stats_bar/index';
@import 'components/ml_embedded_map/index';

// Hacks are last so they can overwrite anything above if needed
@import 'hacks';
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/ml/public/application/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
security: deps.security,
licenseManagement: deps.licenseManagement,
storage: localStorage,
embeddable: deps.embeddable,
maps: deps.maps,
...coreStart,
};

Expand Down Expand Up @@ -118,6 +120,7 @@ export const renderApp = (
http: coreStart.http,
security: deps.security,
urlGenerators: deps.share.urlGenerators,
maps: deps.maps,
});

appMountParams.onAppLeave((actions) => actions.default());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import 'ml_embedded_map';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.mlEmbeddedMapContent {
width: 100%;
height: 100%;
display: flex;
flex: 1 1 100%;
z-index: 1;
min-height: 0; // Absolute must for Firefox to scroll contents
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* 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.
*/

export { MlEmbeddedMapComponent } from './ml_embedded_map';
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* 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 React, { useEffect, useRef, useState } from 'react';

import { htmlIdGenerator } from '@elastic/eui';
import { LayerDescriptor } from '../../../../../maps/common/descriptor_types';
import {
MapEmbeddable,
MapEmbeddableInput,
MapEmbeddableOutput,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../maps/public/embeddable';
import { MAP_SAVED_OBJECT_TYPE, RenderTooltipContentParams } from '../../../../../maps/public';
import {
EmbeddableFactory,
ErrorEmbeddable,
isErrorEmbeddable,
ViewMode,
} from '../../../../../../../src/plugins/embeddable/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,
refreshConfig: {
value: 0,
pause: false,
},
viewMode: ViewMode.VIEW,
isLayerTOCOpen: false,
hideFilterActions: true,
// Zoom Lat/Lon values are set to make sure map is in center in the panel
// It will also omit Greenland/Antarctica etc. NOTE: Can be removed when initialLocation is set
mapCenter: {
lon: 11,
lat: 20,
zoom: 1,
},
// can use mapSettings to center map on anomalies
mapSettings: {
disableInteractive: false,
hideToolbarOverlay: false,
hideLayerControl: false,
hideViewControl: false,
// Doesn't currently work with GEO_JSON. Will uncomment when https://github.com/elastic/kibana/pull/88294 is in
// 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
}, []);

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"
className="mlEmbeddedMapContent"
ref={embeddableRoot}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ import { LicenseManagementUIPluginSetup } from '../../../../../license_managemen
import { SharePluginStart } from '../../../../../../../src/plugins/share/public';
import { MlServicesContext } from '../../app';
import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public';
import type { EmbeddableStart } from '../../../../../../../src/plugins/embeddable/public';
import { MapsStartApi } from '../../../../../maps/public';

interface StartPlugins {
data: DataPublicPluginStart;
security?: SecurityPluginSetup;
licenseManagement?: LicenseManagementUIPluginSetup;
share: SharePluginStart;
embeddable: EmbeddableStart;
maps?: MapsStartApi;
}
export type StartServices = CoreStart &
StartPlugins & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import React from 'react';
import {
BooleanContent,
DateContent,
GeoPointContent,
IpContent,
KeywordContent,
OtherContent,
TextContent,
NumberContent,
} from '../../../stats_table/components/field_data_expanded_row';
import { GeoPointContent } from './geo_point_content/geo_point_content';
import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types';
import type { FileBasedFieldVisConfig } from '../../../stats_table/types/field_vis_config';

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
* you may not use this file except in compliance with the Elastic License.
*/

import { Feature, Point } from 'geojson';
import { euiPaletteColorBlind } from '@elastic/eui';
import { DEFAULT_GEO_REGEX } from './geo_point_content';
import { SOURCE_TYPES } from '../../../../../../../../maps/common/constants';

export const convertWKTGeoToLonLat = (
value: string | number
): { lat: number; lon: number } | undefined => {
if (typeof value === 'string') {
const trimmedValue = value.trim().replace('POINT (', '').replace(')', '');
const regExpSerializer = DEFAULT_GEO_REGEX;
const parsed = regExpSerializer.exec(trimmedValue.trim());

if (parsed?.groups?.lat != null && parsed?.groups?.lon != null) {
return {
lat: parseFloat(parsed.groups.lat.trim()),
lon: parseFloat(parsed.groups.lon.trim()),
};
}
}
};

export const DEFAULT_POINT_COLOR = euiPaletteColorBlind()[0];
export const getGeoPointsLayer = (
features: Array<Feature<Point>>,
pointColor: string = DEFAULT_POINT_COLOR
) => {
return {
id: 'geo_points',
label: 'Geo points',
sourceDescriptor: {
type: SOURCE_TYPES.GEOJSON_FILE,
__featureCollection: {
features,
type: 'FeatureCollection',
},
},
visible: true,
style: {
type: 'VECTOR',
properties: {
fillColor: {
type: 'STATIC',
options: {
color: pointColor,
},
},
lineColor: {
type: 'STATIC',
options: {
color: '#fff',
},
},
lineWidth: {
type: 'STATIC',
options: {
size: 2,
},
},
iconSize: {
type: 'STATIC',
options: {
size: 6,
},
},
},
},
type: 'VECTOR',
};
};
Loading

0 comments on commit da9ad2a

Please sign in to comment.