From cc12e5e56a0ca47556e31025729b2e1eb6884237 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 12 Jan 2021 13:01:49 -0700 Subject: [PATCH 01/15] wip: create embedded map component for explorer --- x-pack/plugins/ml/kibana.json | 3 +- .../explorer_map_container/embedded_map.tsx | 213 ++++++++++++++++++ .../explorer_map_container.tsx | 60 +++++ .../explorer_map_container/index.ts | 7 + .../public/application/explorer/explorer.js | 3 + 5 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/embedded_map.tsx create mode 100644 x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/explorer_map_container.tsx create mode 100644 x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/index.ts diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 1c47512e0b3de..6fc417a90b5b7 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -17,7 +17,8 @@ "uiActions", "kibanaLegacy", "indexPatternManagement", - "discover" + "discover", + "maps" ], "optionalPlugins": [ "home", diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/embedded_map.tsx b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/embedded_map.tsx new file mode 100644 index 0000000000000..dc490f1c88c48 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/embedded_map.tsx @@ -0,0 +1,213 @@ +/* + * 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. + */ + +// /* +// * 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, { useState, useRef, useEffect } from 'react'; +// import uuid from 'uuid'; +// import { FeatureCollection } from 'geojson'; +// import { +// ErrorEmbeddable, +// ViewMode, +// isErrorEmbeddable, +// } from '../../../../../../../../../src/plugins/embeddable/public'; +// import { +// MapEmbeddable, +// MapEmbeddableInput, +// // eslint-disable-next-line @kbn/eslint/no-restricted-paths +// } from '../../../../../../maps/public/embeddable'; +// import { useMlKibana } from '../../../../contexts/kibana'; +// import { MAP_SAVED_OBJECT_TYPE, SOURCE_TYPES } from '../../../../../../../maps/common/constants'; +// import { LayerDescriptor } from '../../../../../../../maps/common/descriptor_types'; +// import { RenderTooltipContentParams } from '../../../../../../../maps/public'; +// import { MapToolTip } from './ml_embedded_map_tooltip'; +// export const getGeoPointsLayer = ( +// geoPoints: Array<{ lon: number; lat: number }>, +// pointColor: string +// ) => { +// const features: FeatureCollection = geoPoints?.map((point, idx) => ({ +// type: 'feature', +// id: `geo_points-${idx}`, +// geometry: { +// type: 'Point', +// coordinates: [+point.lon, +point.lat], +// }, +// })); +// 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', +// }; +// }; + +// export function EmbeddedMapComponent({ config }) { +// const [embeddable, setEmbeddable] = useState(); + +// const embeddableRoot: React.RefObject = useRef(null); +// const layerList = useRef([]); + +// const { +// services: { embeddable: embeddablePlugin, maps: mapsPlugin }, +// } = useMlKibana(); + +// if (!embeddablePlugin) { +// throw new Error('Embeddable start plugin not found'); +// } +// if (!mapsPlugin) { +// throw new Error('Maps start plugin not found'); +// } + +// const factory: any = embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); + +// const input: MapEmbeddableInput = { +// id: uuid.v4(), +// 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 wil also omit Greenland/Antarctica etc +// mapCenter: { +// lon: 11, +// lat: 20, +// zoom: 0, +// }, +// disableInteractive: false, +// hideToolbarOverlay: true, +// hideLayerControl: true, +// hideViewControl: false, +// }; + +// const renderTooltipContent = ({ +// closeTooltip, +// features, +// loadFeatureProperties, +// }): RenderTooltipContentParams => { +// return ( +// +// ); +// }; + +// // Update the layer list with updated geo points upon refresh +// useEffect(() => { +// if (embeddable && !isErrorEmbeddable(embeddable) && Array.isArray(config?.stats?.examples)) { +// layerList.current = [...layerList.current, getGeoPointsLayer(config.stats.examples!, 'red')]; +// embeddable.setLayerList(layerList.current); +// } +// }, [embeddable, config]); + +// useEffect(() => { +// async function setupEmbeddable() { +// if (!factory) { +// throw new Error('Map embeddable not found.'); +// } +// const embeddableObject: any = await factory.create({ +// ...input, +// title: 'Data visualizer map', +// }); + +// if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { +// embeddableObject.setRenderTooltipContent(renderTooltipContent); +// const basemapLayerDescriptor = mapsPlugin +// ? await mapsPlugin.createLayerDescriptors.createBasemapLayerDescriptor() +// : null; + +// if (basemapLayerDescriptor) { +// layerList.current = [basemapLayerDescriptor]; +// await embeddableObject.setLayerList(layerList.current); +// } +// } + +// setEmbeddable(embeddableObject); +// } + +// setupEmbeddable(); + +// // we want this effect to execute exactly once after the component mounts +// }, []); + +// // We can only render after embeddable has already initialized +// useEffect(() => { +// if (embeddableRoot.current && embeddable) { +// embeddable.render(embeddableRoot.current); +// } +// }, [embeddable, embeddableRoot]); + +// return ( +//
+//
+//
+// ); +// } diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/explorer_map_container.tsx b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/explorer_map_container.tsx new file mode 100644 index 0000000000000..c84e7db3a2211 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/explorer_map_container.tsx @@ -0,0 +1,60 @@ +/* + * 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, { FC } from 'react'; +// import { i18n } from '@kbn/i18n'; +// import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPanel, EuiSpacer } from '@elastic/eui'; + +interface Props { + tableData: any; // TODO update types +} + +export const ExplorerMapContainer: FC = ({ tableData }) => { + // console.log('-- TABLE DATA ----', JSON.stringify(tableData, null, 2)); // remove + return ( + <> + +
hello
+ {/* +

+ + + + ), + }} + /> +

+ + } + > + <> + + +
*/} +
+ + + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/index.ts b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/index.ts new file mode 100644 index 0000000000000..599746d55d256 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/index.ts @@ -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 { ExplorerMapContainer } from './explorer_map_container'; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index cea1159ebc14a..9c01a15fca0bd 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -45,6 +45,7 @@ import { CheckboxShowCharts } from '../components/controls/checkbox_showcharts'; import { JobSelector } from '../components/job_selector'; import { SelectInterval } from '../components/controls/select_interval/select_interval'; import { SelectSeverity } from '../components/controls/select_severity/select_severity'; +import { ExplorerMapContainer } from './components/explorer_map_container'; import { ExplorerQueryBar, getKqlQueryValues, @@ -405,6 +406,8 @@ export class Explorer extends React.Component { )} + {/* TODO: only for jobs with geo data and if there are anomalies present */} + {selectedCells !== undefined && } {loading === false && ( From 0e9b28199553ac41768a949c2ec6f42b84267daf Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 12 Jan 2021 16:23:52 -0700 Subject: [PATCH 02/15] add embeddedMap component to explorer --- .../explorer_map_container/embedded_map.tsx | 346 +++++++----------- .../explorer_map_container.tsx | 62 ++-- .../explorer_map_container/map_config.ts | 123 +++++++ .../public/application/explorer/explorer.js | 9 +- 4 files changed, 290 insertions(+), 250 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/map_config.ts diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/embedded_map.tsx b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/embedded_map.tsx index dc490f1c88c48..b501da1ba1a85 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/embedded_map.tsx +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/embedded_map.tsx @@ -4,210 +4,142 @@ * you may not use this file except in compliance with the Elastic License. */ -// /* -// * 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, { useState, useRef, useEffect } from 'react'; -// import uuid from 'uuid'; -// import { FeatureCollection } from 'geojson'; -// import { -// ErrorEmbeddable, -// ViewMode, -// isErrorEmbeddable, -// } from '../../../../../../../../../src/plugins/embeddable/public'; -// import { -// MapEmbeddable, -// MapEmbeddableInput, -// // eslint-disable-next-line @kbn/eslint/no-restricted-paths -// } from '../../../../../../maps/public/embeddable'; -// import { useMlKibana } from '../../../../contexts/kibana'; -// import { MAP_SAVED_OBJECT_TYPE, SOURCE_TYPES } from '../../../../../../../maps/common/constants'; -// import { LayerDescriptor } from '../../../../../../../maps/common/descriptor_types'; -// import { RenderTooltipContentParams } from '../../../../../../../maps/public'; -// import { MapToolTip } from './ml_embedded_map_tooltip'; -// export const getGeoPointsLayer = ( -// geoPoints: Array<{ lon: number; lat: number }>, -// pointColor: string -// ) => { -// const features: FeatureCollection = geoPoints?.map((point, idx) => ({ -// type: 'feature', -// id: `geo_points-${idx}`, -// geometry: { -// type: 'Point', -// coordinates: [+point.lon, +point.lat], -// }, -// })); -// 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', -// }; -// }; - -// export function EmbeddedMapComponent({ config }) { -// const [embeddable, setEmbeddable] = useState(); - -// const embeddableRoot: React.RefObject = useRef(null); -// const layerList = useRef([]); - -// const { -// services: { embeddable: embeddablePlugin, maps: mapsPlugin }, -// } = useMlKibana(); - -// if (!embeddablePlugin) { -// throw new Error('Embeddable start plugin not found'); -// } -// if (!mapsPlugin) { -// throw new Error('Maps start plugin not found'); -// } - -// const factory: any = embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); - -// const input: MapEmbeddableInput = { -// id: uuid.v4(), -// 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 wil also omit Greenland/Antarctica etc -// mapCenter: { -// lon: 11, -// lat: 20, -// zoom: 0, -// }, -// disableInteractive: false, -// hideToolbarOverlay: true, -// hideLayerControl: true, -// hideViewControl: false, -// }; - -// const renderTooltipContent = ({ -// closeTooltip, -// features, -// loadFeatureProperties, -// }): RenderTooltipContentParams => { -// return ( -// -// ); -// }; - -// // Update the layer list with updated geo points upon refresh -// useEffect(() => { -// if (embeddable && !isErrorEmbeddable(embeddable) && Array.isArray(config?.stats?.examples)) { -// layerList.current = [...layerList.current, getGeoPointsLayer(config.stats.examples!, 'red')]; -// embeddable.setLayerList(layerList.current); -// } -// }, [embeddable, config]); - -// useEffect(() => { -// async function setupEmbeddable() { -// if (!factory) { -// throw new Error('Map embeddable not found.'); -// } -// const embeddableObject: any = await factory.create({ -// ...input, -// title: 'Data visualizer map', -// }); - -// if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { -// embeddableObject.setRenderTooltipContent(renderTooltipContent); -// const basemapLayerDescriptor = mapsPlugin -// ? await mapsPlugin.createLayerDescriptors.createBasemapLayerDescriptor() -// : null; - -// if (basemapLayerDescriptor) { -// layerList.current = [basemapLayerDescriptor]; -// await embeddableObject.setLayerList(layerList.current); -// } -// } - -// setEmbeddable(embeddableObject); -// } - -// setupEmbeddable(); - -// // we want this effect to execute exactly once after the component mounts -// }, []); - -// // We can only render after embeddable has already initialized -// useEffect(() => { -// if (embeddableRoot.current && embeddable) { -// embeddable.render(embeddableRoot.current); -// } -// }, [embeddable, embeddableRoot]); - -// return ( -//
-//
-//
-// ); -// } +import React, { useState, useRef, useEffect } from 'react'; +import uuid from 'uuid'; +import { + ErrorEmbeddable, + ViewMode, + isErrorEmbeddable, +} from '../../../../../../../../src/plugins/embeddable/public'; +import { + MapEmbeddable, + MapEmbeddableInput, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../maps/public/embeddable'; +import { useMlKibana } from '../../../contexts/kibana'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../maps/common/constants'; +import { LayerDescriptor } from '../../../../../../maps/common/descriptor_types'; +import { getMLAnomaliesActualLayer, getMLAnomaliesTypicalLayer } from './map_config'; +import { AnomalyRecordDoc } from '../../../../../common/types/anomalies'; + +export function EmbeddedMapComponent({ anomalies }: { anomalies: AnomalyRecordDoc[] }) { + const [embeddable, setEmbeddable] = useState(); + + const embeddableRoot: React.RefObject = useRef(null); + const layerList = useRef([]); + + const { + services: { embeddable: embeddablePlugin, maps: mapsPlugin }, + } = useMlKibana(); + + if (!embeddablePlugin) { + throw new Error('Embeddable start plugin not found'); + } + if (!mapsPlugin) { + throw new Error('Maps start plugin not found'); + } + + const factory: any = embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); + + const input: MapEmbeddableInput = { + id: uuid.v4(), + 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: true, + 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 + }, + }; + + // Update the layer list with updated geo points upon refresh + useEffect(() => { + if (embeddable && !isErrorEmbeddable(embeddable) && anomalies && anomalies.length > 0) { + layerList.current = [ + layerList.current[0], + getMLAnomaliesActualLayer(anomalies), + getMLAnomaliesTypicalLayer(anomalies), + ]; + embeddable.setLayerList(layerList.current); + } + }, [embeddable, anomalies]); + + useEffect(() => { + async function setupEmbeddable() { + if (!factory) { + throw new Error('Map embeddable not found.'); + } + const embeddableObject: any = await factory.create({ + ...input, + title: 'Explorer map', + }); + + if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { + const basemapLayerDescriptor = mapsPlugin + ? await mapsPlugin.createLayerDescriptors.createBasemapLayerDescriptor() + : null; + + if (basemapLayerDescriptor) { + layerList.current = [basemapLayerDescriptor]; + await embeddableObject.setLayerList(layerList.current); + } + } + + setEmbeddable(embeddableObject); + } + + setupEmbeddable(); + // we want this effect to execute exactly once after the component mounts + }, []); + + // We can only render after embeddable has already initialized + useEffect(() => { + if (embeddableRoot?.current && embeddable) { + embeddable.render(embeddableRoot.current); + } + }, [embeddable, embeddableRoot, anomalies]); + + return ( +
+
+
+ ); +} diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/explorer_map_container.tsx b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/explorer_map_container.tsx index c84e7db3a2211..e90de8e118e57 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/explorer_map_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/explorer_map_container.tsx @@ -5,54 +5,34 @@ */ import React, { FC } from 'react'; -// import { i18n } from '@kbn/i18n'; -// import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPanel, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiAccordion, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EmbeddedMapComponent } from './embedded_map'; +import { AnomalyRecordDoc } from '../../../../../common/types/anomalies'; interface Props { - tableData: any; // TODO update types + anomalies: AnomalyRecordDoc[]; } -export const ExplorerMapContainer: FC = ({ tableData }) => { - // console.log('-- TABLE DATA ----', JSON.stringify(tableData, null, 2)); // remove +export const ExplorerMapContainer: FC = ({ anomalies }) => { return ( <> -
hello
- {/* -

- - - - ), - }} - /> -

- - } - > - <> - - -
*/} + +

+ +

+ + } + > + <> + + +
diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/map_config.ts b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/map_config.ts new file mode 100644 index 0000000000000..ab3d1d7ae88fc --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/map_config.ts @@ -0,0 +1,123 @@ +/* + * 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 { AnomalyRecordDoc } from '../../../../../common/types/anomalies'; + +const FEATURE = 'Feature'; +const POINT = 'Point'; + +function getAnomalyFeatures(anomalies: AnomalyRecordDoc[], type: 'actual' | 'typical') { + return anomalies.map((anomaly) => { + // Must reverse coordinates here. Map expects [lon, lat] - anomalies are stored as [lat, lon] for lat_lon jobs + const coordinates = anomaly[type]?.reverse(); + + return { + type: FEATURE, + geometry: { + type: POINT, + coordinates, + }, + properties: { + ...(anomaly.entityName ? { [anomaly.entityName]: anomaly.entityValue } : {}), + severity: anomaly.severity, + [type]: coordinates, + }, + }; + }); +} + +export const getMLAnomaliesTypicalLayer = (anomalies: any) => { + return { + id: 'anomalies_typical_layer', + label: 'Typical', + sourceDescriptor: { + id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854e', + type: 'GEOJSON_FILE', + __featureCollection: { + features: getAnomalyFeatures(anomalies, 'typical'), + type: 'FeatureCollection', + }, + }, + visible: true, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { + color: '#98A2B2', + }, + }, + lineColor: { + type: 'STATIC', + options: { + color: '#fff', + }, + }, + lineWidth: { + type: 'STATIC', + options: { + size: 2, + }, + }, + iconSize: { + type: 'STATIC', + options: { + size: 6, + }, + }, + }, + }, + type: 'VECTOR', + }; +}; + +// GEOJSON_FILE type layer does not support source-type to inject custom data for styling +export const getMLAnomaliesActualLayer = (anomalies: any) => { + return { + id: 'anomalies_actual_layer', + label: 'Actual', + sourceDescriptor: { + id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854d', + type: 'GEOJSON_FILE', + __featureCollection: { + features: getAnomalyFeatures(anomalies, 'actual'), + type: 'FeatureCollection', + }, + }, + visible: true, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { + color: '#FF0000', + }, + }, + lineColor: { + type: 'STATIC', + options: { + color: '#fff', + }, + }, + lineWidth: { + type: 'STATIC', + options: { + size: 2, + }, + }, + iconSize: { + type: 'STATIC', + options: { + size: 6, + }, + }, + }, + }, + type: 'VECTOR', + }; +}; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 9c01a15fca0bd..6a3b926769934 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -270,6 +270,12 @@ export class Explorer extends React.Component { const timefilter = getTimefilter(); const bounds = timefilter.getActiveBounds(); const selectedJobIds = Array.isArray(selectedJobs) ? selectedJobs.map((job) => job.id) : []; + const showAnomaliesMap = + selectedCells !== undefined && + tableData.anomalies?.length && + typeof tableData.anomalies[0].detector === 'string' && + tableData.anomalies[0].detector.includes('lat_long'); + return ( )} - {/* TODO: only for jobs with geo data and if there are anomalies present */} - {selectedCells !== undefined && } + {showAnomaliesMap && } {loading === false && ( From 052014c6aeb4ad1612e6c1ceaee1f2b3ba87230d Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 14 Jan 2021 15:29:16 -0700 Subject: [PATCH 03/15] use geo_results --- .../components/explorer_map_container/map_config.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/map_config.ts b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/map_config.ts index ab3d1d7ae88fc..0a190d6e349bd 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/map_config.ts +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/map_config.ts @@ -9,10 +9,14 @@ import { AnomalyRecordDoc } from '../../../../../common/types/anomalies'; const FEATURE = 'Feature'; const POINT = 'Point'; -function getAnomalyFeatures(anomalies: AnomalyRecordDoc[], type: 'actual' | 'typical') { +function getAnomalyFeatures(anomalies: AnomalyRecordDoc[], type: 'actual_point' | 'typical_point') { return anomalies.map((anomaly) => { // Must reverse coordinates here. Map expects [lon, lat] - anomalies are stored as [lat, lon] for lat_lon jobs - const coordinates = anomaly[type]?.reverse(); + const geoResults = anomaly.source.geo_results || anomaly.source.causes[0].geo_results; + const coordinates = geoResults[type] + .split(',') + .map((point: string) => Number(point)) + .reverse(); return { type: FEATURE, @@ -37,7 +41,7 @@ export const getMLAnomaliesTypicalLayer = (anomalies: any) => { id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854e', type: 'GEOJSON_FILE', __featureCollection: { - features: getAnomalyFeatures(anomalies, 'typical'), + features: getAnomalyFeatures(anomalies, 'typical_point'), type: 'FeatureCollection', }, }, @@ -84,7 +88,7 @@ export const getMLAnomaliesActualLayer = (anomalies: any) => { id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854d', type: 'GEOJSON_FILE', __featureCollection: { - features: getAnomalyFeatures(anomalies, 'actual'), + features: getAnomalyFeatures(anomalies, 'actual_point'), type: 'FeatureCollection', }, }, From 8d0cf05d5d45f1726c2b32ed2a4aa7b44e0e6bef Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 14 Jan 2021 15:39:00 -0700 Subject: [PATCH 04/15] remove charts callout when map is shown --- .../plugins/ml/public/application/explorer/explorer.js | 4 +++- .../explorer_charts/explorer_charts_container.js | 6 +++++- .../explorer_charts/explorer_charts_error_callouts.tsx | 9 ++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 6a3b926769934..7665d4cf7850b 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -460,7 +460,9 @@ export class Explorer extends React.Component {
- {showCharts && } + {showCharts && ( + + )}
isLabelLengthAboveThreshold(series)); return ( <> - + {seriesToPlot.length > 0 && seriesToPlot.map((series) => ( diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_error_callouts.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_error_callouts.tsx index 3c840dfc3aba1..88c16738cc7af 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_error_callouts.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_error_callouts.tsx @@ -11,12 +11,19 @@ import { ExplorerChartSeriesErrorMessages } from './explorer_charts_container_se interface ExplorerChartsErrorCalloutsProps { errorMessagesByType: ExplorerChartSeriesErrorMessages; + showAnomaliesMap: boolean; } export const ExplorerChartsErrorCallOuts: FC = ({ errorMessagesByType, + showAnomaliesMap, }) => { - if (!errorMessagesByType || Object.keys(errorMessagesByType).length === 0) return null; + if ( + !errorMessagesByType || + Object.keys(errorMessagesByType).length === 0 || + showAnomaliesMap === true + ) + return null; const content = Object.keys(errorMessagesByType).map((errorType) => ( Date: Mon, 25 Jan 2021 10:21:10 -0700 Subject: [PATCH 05/15] add translation, round geo coordinates --- .../explorer_map_container/explorer_map_container.tsx | 8 +++++++- .../components/explorer_map_container/map_config.ts | 4 ++-- .../components/advanced_detector_modal/descriptions.tsx | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/explorer_map_container.tsx b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/explorer_map_container.tsx index e90de8e118e57..867666362020f 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/explorer_map_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/explorer_map_container.tsx @@ -5,6 +5,7 @@ */ import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiAccordion, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { EmbeddedMapComponent } from './embedded_map'; @@ -24,7 +25,12 @@ export const ExplorerMapContainer: FC = ({ anomalies }) => { buttonContent={

- +

} diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/map_config.ts b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/map_config.ts index 0a190d6e349bd..68cadba92d7c9 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/map_config.ts +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/map_config.ts @@ -26,8 +26,8 @@ function getAnomalyFeatures(anomalies: AnomalyRecordDoc[], type: 'actual_point' }, properties: { ...(anomaly.entityName ? { [anomaly.entityName]: anomaly.entityValue } : {}), - severity: anomaly.severity, - [type]: coordinates, + severity: Math.floor(anomaly.severity), + [type]: coordinates.map((point: number) => point.toFixed(2)), }, }; }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx index 280ac85a5a2bc..470fe11759d27 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx @@ -46,7 +46,7 @@ export const FieldDescription: FC = memo(({ children }) => { description={ } > From ebce0ac5c029dfbd52b47fa60f783d8f5392e174 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 25 Jan 2021 16:07:45 -0700 Subject: [PATCH 06/15] create GEO_MAP chart type and move embedded map to charts area --- x-pack/plugins/ml/common/util/job_utils.ts | 2 +- .../explorer_chart_embedded_map.tsx | 155 ++++++++++++++++++ .../explorer_charts_container.js | 18 +- .../explorer_charts_container_service.js | 99 +++++++---- .../explorer/explorer_charts/map_config.ts | 130 +++++++++++++++ .../explorer/explorer_constants.ts | 1 + .../results_service/result_service_rx.ts | 3 +- .../ml/public/application/util/chart_utils.js | 2 + 8 files changed, 371 insertions(+), 39 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx create mode 100644 x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts index 4f4d9851c4957..1768710309449 100644 --- a/x-pack/plugins/ml/common/util/job_utils.ts +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -93,7 +93,7 @@ export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex // Note that the 'function' field in a record contains what the user entered e.g. 'high_count', // whereas the 'function_description' field holds an ML-built display hint for function e.g. 'count'. isSourceDataChartable = - mlFunctionToESAggregation(functionName) !== null && + (mlFunctionToESAggregation(functionName) !== null || functionName === 'lat_long') && dtr.by_field_name !== MLCATEGORY && dtr.partition_field_name !== MLCATEGORY && dtr.over_field_name !== MLCATEGORY; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx new file mode 100644 index 0000000000000..5f6822f1fce04 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx @@ -0,0 +1,155 @@ +/* + * 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, { useState, useRef, useEffect } from 'react'; +import uuid from 'uuid'; +import { + ErrorEmbeddable, + ViewMode, + isErrorEmbeddable, +} from '../../../../../../../src/plugins/embeddable/public'; +import { + MapEmbeddable, + MapEmbeddableInput, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../maps/public/embeddable'; +import { useMlKibana } from '../../contexts/kibana'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../../../maps/common/constants'; +import { LayerDescriptor } from '../../../../../maps/common/descriptor_types'; +import { getMLAnomaliesActualLayer, getMLAnomaliesTypicalLayer } from './map_config'; +// import { AnomalyRecordDoc } from '../../../../common/types/seriesConfig.chartData'; + +interface Props { + seriesConfig?: any; // object; + severity: number; + tooltipService: any; // object; +} + +export function EmbeddedMapComponent({ seriesConfig, severity, tooltipService }: Props) { + const [embeddable, setEmbeddable] = useState(); + + const embeddableRoot: React.RefObject = useRef(null); + const layerList = useRef([]); + const { + services: { embeddable: embeddablePlugin, maps: mapsPlugin }, + } = useMlKibana(); + + if (!embeddablePlugin) { + throw new Error('Embeddable start plugin not found'); + } + if (!mapsPlugin) { + throw new Error('Maps start plugin not found'); + } + + const factory: any = embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); + + const input: MapEmbeddableInput = { + id: uuid.v4(), + 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 chart data + mapSettings: { + disableInteractive: false, + hideToolbarOverlay: true, + 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 + }, + }; + + // Update the layer list with updated geo points upon refresh + useEffect(() => { + if ( + embeddable && + !isErrorEmbeddable(embeddable) && + seriesConfig.chartData && + seriesConfig.chartData.length > 0 + ) { + layerList.current = [ + layerList.current[0], + getMLAnomaliesActualLayer(seriesConfig.chartData), + getMLAnomaliesTypicalLayer(seriesConfig.chartData), + ]; + embeddable.setLayerList(layerList.current); + } + }, [embeddable, seriesConfig.chartData]); + + useEffect(() => { + async function setupEmbeddable() { + if (!factory) { + throw new Error('Map embeddable not found.'); + } + const embeddableObject: any = await factory.create({ + ...input, + title: 'Explorer map', + }); + + if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { + const basemapLayerDescriptor = mapsPlugin + ? await mapsPlugin.createLayerDescriptors.createBasemapLayerDescriptor() + : null; + + if (basemapLayerDescriptor) { + layerList.current = [basemapLayerDescriptor]; + await embeddableObject.setLayerList(layerList.current); + } + } + + setEmbeddable(embeddableObject); + } + + setupEmbeddable(); + // we want this effect to execute exactly once after the component mounts + }, []); + + // We can only render after embeddable has already initialized + useEffect(() => { + if (embeddableRoot?.current && embeddable) { + embeddable.render(embeddableRoot.current); + } + }, [embeddable, embeddableRoot, seriesConfig.chartData]); + + return ( +
+
+
+ ); +} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index b2b8c20a36ba8..5395861ba33e5 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -22,6 +22,7 @@ import { } from '../../util/chart_utils'; import { ExplorerChartDistribution } from './explorer_chart_distribution'; import { ExplorerChartSingleMetric } from './explorer_chart_single_metric'; +import { EmbeddedMapComponent } from './explorer_chart_embedded_map'; import { ExplorerChartLabel } from './components/explorer_chart_label'; import { CHART_TYPE } from '../explorer_constants'; @@ -57,6 +58,7 @@ function getChartId(series) { function ExplorerChartContainer({ series, severity, + showSingleMetricViewerLink, tooManyBuckets, wrapLabel, mlUrlGenerator, @@ -68,7 +70,7 @@ function ExplorerChartContainer({ let isCancelled = false; const generateLink = async () => { const singleMetricViewerLink = await getExploreSeriesLink(mlUrlGenerator, series); - if (!isCancelled) { + if (!isCancelled && showSingleMetricViewerLink === true) { setExplorerSeriesLink(singleMetricViewerLink); } }; @@ -150,6 +152,20 @@ function ExplorerChartContainer({ {(() => { + if (chartType === CHART_TYPE.GEO_MAP) { + return ( + + {(tooltipService) => ( + + )} + + ); + } if ( chartType === CHART_TYPE.EVENT_DISTRIBUTION || chartType === CHART_TYPE.POPULATION_DISTRIBUTION diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index 3dc1c0234584d..8703501398ea8 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -36,6 +36,7 @@ export function getDefaultChartsData() { chartsPerRow: 1, errorMessages: undefined, seriesToPlot: [], + showSingleMetricViewerLink: true, // default values, will update on every re-render tooManyBuckets: false, timeFieldName: 'timestamp', @@ -77,8 +78,37 @@ export const anomalyDataChange = function ( // For now just take first 6 (or 8 if 4 charts per row). const maxSeriesToPlot = Math.max(chartsPerRow * 2, 6); const recordsToPlot = allSeriesRecords.slice(0, maxSeriesToPlot); + const isGeoMap = + (recordsToPlot[0]?.function_description || recordsToPlot[0]?.function) === 'lat_long'; const seriesConfigs = recordsToPlot.map(buildConfig); + // initialize the charts with loading indicators + data.seriesToPlot = seriesConfigs.map((config) => ({ + ...config, + loading: true, + chartData: null, + })); + + if (isGeoMap === true) { + data.seriesToPlot = seriesConfigs.map((config) => { + const chartData = config.entityFields.length + ? [ + recordsToPlot.find((record) => { + const entityFieldName = config.entityFields[0].fieldName; + const entityFieldValue = config.entityFields[0].fieldValue; + return record[entityFieldName][0] === entityFieldValue; + }), + ] + : recordsToPlot; + return { + ...config, + loading: false, + chartData, + }; + }); + data.showSingleMetricViewerLink = false; + } + // Calculate the time range of the charts, which is a function of the chart width and max job bucket span. data.tooManyBuckets = false; const chartWidth = Math.floor(containerWith / chartsPerRow); @@ -92,13 +122,6 @@ export const anomalyDataChange = function ( ); data.tooManyBuckets = tooManyBuckets; - // initialize the charts with loading indicators - data.seriesToPlot = seriesConfigs.map((config) => ({ - ...config, - loading: true, - chartData: null, - })); - data.errorMessages = errorMessages; explorerService.setCharts({ ...data }); @@ -395,35 +418,39 @@ export const anomalyDataChange = function ( return chartData.find((point) => point.date === time); } - Promise.all(seriesPromises) - .then((response) => { - // calculate an overall min/max for all series - const processedData = response.map(processChartData); - const allDataPoints = reduce( - processedData, - (datapoints, series) => { - each(series, (d) => datapoints.push(d)); - return datapoints; - }, - [] - ); - const overallChartLimits = chartLimits(allDataPoints); - - data.seriesToPlot = response.map((d, i) => ({ - ...seriesConfigs[i], - loading: false, - chartData: processedData[i], - plotEarliest: chartRange.min, - plotLatest: chartRange.max, - selectedEarliest: selectedEarliestMs, - selectedLatest: selectedLatestMs, - chartLimits: USE_OVERALL_CHART_LIMITS ? overallChartLimits : chartLimits(processedData[i]), - })); - explorerService.setCharts({ ...data }); - }) - .catch((error) => { - console.error(error); - }); + if (!isGeoMap) { + Promise.all(seriesPromises) + .then((response) => { + // calculate an overall min/max for all series + const processedData = response.map(processChartData); + const allDataPoints = reduce( + processedData, + (datapoints, series) => { + each(series, (d) => datapoints.push(d)); + return datapoints; + }, + [] + ); + const overallChartLimits = chartLimits(allDataPoints); + + data.seriesToPlot = response.map((d, i) => ({ + ...seriesConfigs[i], + loading: false, + chartData: processedData[i], + plotEarliest: chartRange.min, + plotLatest: chartRange.max, + selectedEarliest: selectedEarliestMs, + selectedLatest: selectedLatestMs, + chartLimits: USE_OVERALL_CHART_LIMITS + ? overallChartLimits + : chartLimits(processedData[i]), + })); + explorerService.setCharts({ ...data }); + }) + .catch((error) => { + console.error(error); + }); + } }; function processRecordsForDisplay(anomalyRecords) { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts b/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts new file mode 100644 index 0000000000000..4863b93cd4931 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts @@ -0,0 +1,130 @@ +/* + * 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. + */ + +const FEATURE = 'Feature'; +const POINT = 'Point'; + +function getAnomalyFeatures(anomalies: any[], type: 'actual_point' | 'typical_point') { + const anomalyFeatures = []; + for (let i = 0; i < anomalies.length; i++) { + const anomaly = anomalies[i]; + const geoResults = anomaly.geo_results || anomaly?.causes[0].geo_results; + const coordinateStr = geoResults[type]; + if (coordinateStr !== undefined) { + // Must reverse coordinates here. Map expects [lon, lat] - anomalies are stored as [lat, lon] for lat_lon jobs + const coordinates = coordinateStr + .split(',') + .map((point: string) => Number(point)) + .reverse(); + + anomalyFeatures.push({ + type: FEATURE, + geometry: { + type: POINT, + coordinates, + }, + properties: { + record_score: Math.floor(anomaly.record_score), + [type]: coordinates.map((point: number) => point.toFixed(2)), + }, + }); + } + } + return anomalyFeatures; +} + +export const getMLAnomaliesTypicalLayer = (anomalies: any) => { + return { + id: 'anomalies_typical_layer', + label: 'Typical', + sourceDescriptor: { + id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854e', + type: 'GEOJSON_FILE', + __featureCollection: { + features: getAnomalyFeatures(anomalies, 'typical_point'), + type: 'FeatureCollection', + }, + }, + visible: true, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { + color: '#98A2B2', + }, + }, + lineColor: { + type: 'STATIC', + options: { + color: '#fff', + }, + }, + lineWidth: { + type: 'STATIC', + options: { + size: 2, + }, + }, + iconSize: { + type: 'STATIC', + options: { + size: 6, + }, + }, + }, + }, + type: 'VECTOR', + }; +}; + +// GEOJSON_FILE type layer does not support source-type to inject custom data for styling +export const getMLAnomaliesActualLayer = (anomalies: any) => { + return { + id: 'anomalies_actual_layer', + label: 'Actual', + sourceDescriptor: { + id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854d', + type: 'GEOJSON_FILE', + __featureCollection: { + features: getAnomalyFeatures(anomalies, 'actual_point'), + type: 'FeatureCollection', + }, + }, + visible: true, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { + color: '#FF0000', + }, + }, + lineColor: { + type: 'STATIC', + options: { + color: '#fff', + }, + }, + lineWidth: { + type: 'STATIC', + options: { + size: 2, + }, + }, + iconSize: { + type: 'STATIC', + options: { + size: 6, + }, + }, + }, + }, + type: 'VECTOR', + }; +}; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts index 3f5f016fc365a..2178c837458e9 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts @@ -48,6 +48,7 @@ export const CHART_TYPE = { EVENT_DISTRIBUTION: 'event_distribution', POPULATION_DISTRIBUTION: 'population_distribution', SINGLE_METRIC: 'single_metric', + GEO_MAP: 'geo_map', }; export const MAX_CATEGORY_EXAMPLES = 10; diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts index 514449385bf0b..3747e84f43765 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -156,7 +156,8 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { } body.aggs.byTime.aggs = {}; - if (metricFieldName !== undefined && metricFieldName !== '') { + + if (metricFieldName !== undefined && metricFieldName !== '' && metricFunction) { const metricAgg: any = { [metricFunction]: {}, }; diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.js b/x-pack/plugins/ml/public/application/util/chart_utils.js index 402c922a0034f..25e7adf49e969 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.js @@ -189,6 +189,8 @@ export function getChartType(config) { config.metricFunction !== null // Event distribution chart relies on the ML function mapping to an ES aggregation ) { chartType = CHART_TYPE.POPULATION_DISTRIBUTION; + } else if (config.functionDescription === 'lat_long') { + chartType = CHART_TYPE.GEO_MAP; } if ( From ffb319fb26649ac56a95f548c113a713736164ee Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 25 Jan 2021 16:16:23 -0700 Subject: [PATCH 07/15] remove embedded map that is no longer used --- .../explorer_map_container/embedded_map.tsx | 145 ------------------ .../explorer_map_container.tsx | 46 ------ .../explorer_map_container/index.ts | 7 - .../explorer_map_container/map_config.ts | 127 --------------- .../public/application/explorer/explorer.js | 12 +- .../explorer_charts_container.js | 6 +- .../explorer_charts_error_callouts.tsx | 9 +- 7 files changed, 3 insertions(+), 349 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/embedded_map.tsx delete mode 100644 x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/explorer_map_container.tsx delete mode 100644 x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/index.ts delete mode 100644 x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/map_config.ts diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/embedded_map.tsx b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/embedded_map.tsx deleted file mode 100644 index b501da1ba1a85..0000000000000 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/embedded_map.tsx +++ /dev/null @@ -1,145 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useState, useRef, useEffect } from 'react'; -import uuid from 'uuid'; -import { - ErrorEmbeddable, - ViewMode, - isErrorEmbeddable, -} from '../../../../../../../../src/plugins/embeddable/public'; -import { - MapEmbeddable, - MapEmbeddableInput, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../maps/public/embeddable'; -import { useMlKibana } from '../../../contexts/kibana'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../maps/common/constants'; -import { LayerDescriptor } from '../../../../../../maps/common/descriptor_types'; -import { getMLAnomaliesActualLayer, getMLAnomaliesTypicalLayer } from './map_config'; -import { AnomalyRecordDoc } from '../../../../../common/types/anomalies'; - -export function EmbeddedMapComponent({ anomalies }: { anomalies: AnomalyRecordDoc[] }) { - const [embeddable, setEmbeddable] = useState(); - - const embeddableRoot: React.RefObject = useRef(null); - const layerList = useRef([]); - - const { - services: { embeddable: embeddablePlugin, maps: mapsPlugin }, - } = useMlKibana(); - - if (!embeddablePlugin) { - throw new Error('Embeddable start plugin not found'); - } - if (!mapsPlugin) { - throw new Error('Maps start plugin not found'); - } - - const factory: any = embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); - - const input: MapEmbeddableInput = { - id: uuid.v4(), - 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: true, - 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 - }, - }; - - // Update the layer list with updated geo points upon refresh - useEffect(() => { - if (embeddable && !isErrorEmbeddable(embeddable) && anomalies && anomalies.length > 0) { - layerList.current = [ - layerList.current[0], - getMLAnomaliesActualLayer(anomalies), - getMLAnomaliesTypicalLayer(anomalies), - ]; - embeddable.setLayerList(layerList.current); - } - }, [embeddable, anomalies]); - - useEffect(() => { - async function setupEmbeddable() { - if (!factory) { - throw new Error('Map embeddable not found.'); - } - const embeddableObject: any = await factory.create({ - ...input, - title: 'Explorer map', - }); - - if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { - const basemapLayerDescriptor = mapsPlugin - ? await mapsPlugin.createLayerDescriptors.createBasemapLayerDescriptor() - : null; - - if (basemapLayerDescriptor) { - layerList.current = [basemapLayerDescriptor]; - await embeddableObject.setLayerList(layerList.current); - } - } - - setEmbeddable(embeddableObject); - } - - setupEmbeddable(); - // we want this effect to execute exactly once after the component mounts - }, []); - - // We can only render after embeddable has already initialized - useEffect(() => { - if (embeddableRoot?.current && embeddable) { - embeddable.render(embeddableRoot.current); - } - }, [embeddable, embeddableRoot, anomalies]); - - return ( -
-
-
- ); -} diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/explorer_map_container.tsx b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/explorer_map_container.tsx deleted file mode 100644 index 867666362020f..0000000000000 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/explorer_map_container.tsx +++ /dev/null @@ -1,46 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiAccordion, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { EmbeddedMapComponent } from './embedded_map'; -import { AnomalyRecordDoc } from '../../../../../common/types/anomalies'; - -interface Props { - anomalies: AnomalyRecordDoc[]; -} - -export const ExplorerMapContainer: FC = ({ anomalies }) => { - return ( - <> - - -

- -

- - } - > - <> - - -
-
- - - ); -}; diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/index.ts b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/index.ts deleted file mode 100644 index 599746d55d256..0000000000000 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/index.ts +++ /dev/null @@ -1,7 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export { ExplorerMapContainer } from './explorer_map_container'; diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/map_config.ts b/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/map_config.ts deleted file mode 100644 index 68cadba92d7c9..0000000000000 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_map_container/map_config.ts +++ /dev/null @@ -1,127 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { AnomalyRecordDoc } from '../../../../../common/types/anomalies'; - -const FEATURE = 'Feature'; -const POINT = 'Point'; - -function getAnomalyFeatures(anomalies: AnomalyRecordDoc[], type: 'actual_point' | 'typical_point') { - return anomalies.map((anomaly) => { - // Must reverse coordinates here. Map expects [lon, lat] - anomalies are stored as [lat, lon] for lat_lon jobs - const geoResults = anomaly.source.geo_results || anomaly.source.causes[0].geo_results; - const coordinates = geoResults[type] - .split(',') - .map((point: string) => Number(point)) - .reverse(); - - return { - type: FEATURE, - geometry: { - type: POINT, - coordinates, - }, - properties: { - ...(anomaly.entityName ? { [anomaly.entityName]: anomaly.entityValue } : {}), - severity: Math.floor(anomaly.severity), - [type]: coordinates.map((point: number) => point.toFixed(2)), - }, - }; - }); -} - -export const getMLAnomaliesTypicalLayer = (anomalies: any) => { - return { - id: 'anomalies_typical_layer', - label: 'Typical', - sourceDescriptor: { - id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854e', - type: 'GEOJSON_FILE', - __featureCollection: { - features: getAnomalyFeatures(anomalies, 'typical_point'), - type: 'FeatureCollection', - }, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#98A2B2', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 2, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', - }; -}; - -// GEOJSON_FILE type layer does not support source-type to inject custom data for styling -export const getMLAnomaliesActualLayer = (anomalies: any) => { - return { - id: 'anomalies_actual_layer', - label: 'Actual', - sourceDescriptor: { - id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854d', - type: 'GEOJSON_FILE', - __featureCollection: { - features: getAnomalyFeatures(anomalies, 'actual_point'), - type: 'FeatureCollection', - }, - }, - visible: true, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#FF0000', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 2, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }, - type: 'VECTOR', - }; -}; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 7665d4cf7850b..ee05387633940 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -45,7 +45,6 @@ import { CheckboxShowCharts } from '../components/controls/checkbox_showcharts'; import { JobSelector } from '../components/job_selector'; import { SelectInterval } from '../components/controls/select_interval/select_interval'; import { SelectSeverity } from '../components/controls/select_severity/select_severity'; -import { ExplorerMapContainer } from './components/explorer_map_container'; import { ExplorerQueryBar, getKqlQueryValues, @@ -270,11 +269,6 @@ export class Explorer extends React.Component { const timefilter = getTimefilter(); const bounds = timefilter.getActiveBounds(); const selectedJobIds = Array.isArray(selectedJobs) ? selectedJobs.map((job) => job.id) : []; - const showAnomaliesMap = - selectedCells !== undefined && - tableData.anomalies?.length && - typeof tableData.anomalies[0].detector === 'string' && - tableData.anomalies[0].detector.includes('lat_long'); return ( )} - )} - {showAnomaliesMap && } {loading === false && ( @@ -460,9 +452,7 @@ export class Explorer extends React.Component {
- {showCharts && ( - - )} + {showCharts && }
isLabelLengthAboveThreshold(series)); return ( <> - + {seriesToPlot.length > 0 && seriesToPlot.map((series) => ( diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_error_callouts.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_error_callouts.tsx index 88c16738cc7af..3c840dfc3aba1 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_error_callouts.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_error_callouts.tsx @@ -11,19 +11,12 @@ import { ExplorerChartSeriesErrorMessages } from './explorer_charts_container_se interface ExplorerChartsErrorCalloutsProps { errorMessagesByType: ExplorerChartSeriesErrorMessages; - showAnomaliesMap: boolean; } export const ExplorerChartsErrorCallOuts: FC = ({ errorMessagesByType, - showAnomaliesMap, }) => { - if ( - !errorMessagesByType || - Object.keys(errorMessagesByType).length === 0 || - showAnomaliesMap === true - ) - return null; + if (!errorMessagesByType || Object.keys(errorMessagesByType).length === 0) return null; const content = Object.keys(errorMessagesByType).map((errorType) => ( Date: Tue, 26 Jan 2021 12:44:02 -0700 Subject: [PATCH 08/15] fix type and fail silently if plugin not available --- x-pack/plugins/ml/kibana.json | 3 +- .../explorer_chart_embedded_map.tsx | 34 ++++++++++--------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 6fc417a90b5b7..1c47512e0b3de 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -17,8 +17,7 @@ "uiActions", "kibanaLegacy", "indexPatternManagement", - "discover", - "maps" + "discover" ], "optionalPlugins": [ "home", diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx index 5f6822f1fce04..427bec5bb1c1d 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx @@ -16,19 +16,17 @@ import { MapEmbeddableInput, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../maps/public/embeddable'; +import { Dictionary } from '../../../../common/types/common'; import { useMlKibana } from '../../contexts/kibana'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../../maps/common/constants'; import { LayerDescriptor } from '../../../../../maps/common/descriptor_types'; import { getMLAnomaliesActualLayer, getMLAnomaliesTypicalLayer } from './map_config'; -// import { AnomalyRecordDoc } from '../../../../common/types/seriesConfig.chartData'; - interface Props { - seriesConfig?: any; // object; + seriesConfig: Dictionary; severity: number; - tooltipService: any; // object; } -export function EmbeddedMapComponent({ seriesConfig, severity, tooltipService }: Props) { +export function EmbeddedMapComponent({ seriesConfig, severity }: Props) { const [embeddable, setEmbeddable] = useState(); const embeddableRoot: React.RefObject = useRef(null); @@ -37,14 +35,7 @@ export function EmbeddedMapComponent({ seriesConfig, severity, tooltipService }: services: { embeddable: embeddablePlugin, maps: mapsPlugin }, } = useMlKibana(); - if (!embeddablePlugin) { - throw new Error('Embeddable start plugin not found'); - } - if (!mapsPlugin) { - throw new Error('Maps start plugin not found'); - } - - const factory: any = embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); + const factory = embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); const input: MapEmbeddableInput = { id: uuid.v4(), @@ -97,7 +88,9 @@ export function EmbeddedMapComponent({ seriesConfig, severity, tooltipService }: useEffect(() => { async function setupEmbeddable() { if (!factory) { - throw new Error('Map embeddable not found.'); + // eslint-disable-next-line no-console + console.error('Map embeddable not found.'); + return; } const embeddableObject: any = await factory.create({ ...input, @@ -129,9 +122,19 @@ export function EmbeddedMapComponent({ seriesConfig, severity, tooltipService }: } }, [embeddable, embeddableRoot, seriesConfig.chartData]); + 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 (
Date: Tue, 26 Jan 2021 16:26:28 -0700 Subject: [PATCH 09/15] fix multiple type of jobs charts view --- x-pack/plugins/ml/common/util/job_utils.ts | 3 +- .../explorer_chart_embedded_map.tsx | 27 ++-- .../explorer_charts_container.js | 26 ++-- .../explorer_charts_container_service.js | 140 +++++++++++------- .../explorer/explorer_charts/map_config.ts | 4 +- 5 files changed, 111 insertions(+), 89 deletions(-) diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts index 1768710309449..39aee735d4ba2 100644 --- a/x-pack/plugins/ml/common/util/job_utils.ts +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -93,7 +93,8 @@ export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex // Note that the 'function' field in a record contains what the user entered e.g. 'high_count', // whereas the 'function_description' field holds an ML-built display hint for function e.g. 'count'. isSourceDataChartable = - (mlFunctionToESAggregation(functionName) !== null || functionName === 'lat_long') && + (mlFunctionToESAggregation(functionName) !== null || + functionName === ML_JOB_AGGREGATION.LAT_LONG) && dtr.by_field_name !== MLCATEGORY && dtr.partition_field_name !== MLCATEGORY && dtr.over_field_name !== MLCATEGORY; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx index 427bec5bb1c1d..64743ffd3192b 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx @@ -18,7 +18,7 @@ import { } from '../../../../../maps/public/embeddable'; import { Dictionary } from '../../../../common/types/common'; import { useMlKibana } from '../../contexts/kibana'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../../../maps/common/constants'; +import { MAP_SAVED_OBJECT_TYPE, INITIAL_LOCATION } from '../../../../../maps/common/constants'; import { LayerDescriptor } from '../../../../../maps/common/descriptor_types'; import { getMLAnomaliesActualLayer, getMLAnomaliesTypicalLayer } from './map_config'; interface Props { @@ -49,22 +49,13 @@ export function EmbeddedMapComponent({ seriesConfig, severity }: Props) { 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 chart data mapSettings: { disableInteractive: false, hideToolbarOverlay: true, 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 + 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 }, }; @@ -73,17 +64,17 @@ export function EmbeddedMapComponent({ seriesConfig, severity }: Props) { if ( embeddable && !isErrorEmbeddable(embeddable) && - seriesConfig.chartData && - seriesConfig.chartData.length > 0 + seriesConfig.mapData && + seriesConfig.mapData.length > 0 ) { layerList.current = [ layerList.current[0], - getMLAnomaliesActualLayer(seriesConfig.chartData), - getMLAnomaliesTypicalLayer(seriesConfig.chartData), + getMLAnomaliesActualLayer(seriesConfig.mapData), + getMLAnomaliesTypicalLayer(seriesConfig.mapData), ]; embeddable.setLayerList(layerList.current); } - }, [embeddable, seriesConfig.chartData]); + }, [embeddable, seriesConfig.mapData]); useEffect(() => { async function setupEmbeddable() { @@ -120,7 +111,7 @@ export function EmbeddedMapComponent({ seriesConfig, severity }: Props) { if (embeddableRoot?.current && embeddable) { embeddable.render(embeddableRoot.current); } - }, [embeddable, embeddableRoot, seriesConfig.chartData]); + }, [embeddable, embeddableRoot, seriesConfig.mapData]); if (!embeddablePlugin) { // eslint-disable-next-line no-console diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index 1e63e9e020a56..103aad865d85b 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -183,18 +183,20 @@ function ExplorerChartContainer({ ); } - return ( - - {(tooltipService) => ( - - )} - - ); + if (chartType === CHART_TYPE.SINGLE_METRIC) { + return ( + + {(tooltipService) => ( + + )} + + ); + } })()} ); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index 8703501398ea8..29f740bc59f27 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -28,6 +28,7 @@ import { mlJobService } from '../../services/job_service'; import { explorerService } from '../explorer_dashboard_service'; import { CHART_TYPE } from '../explorer_constants'; +import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types'; import { i18n } from '@kbn/i18n'; import { SWIM_LANE_LABEL_WIDTH } from '../swimlane_container'; @@ -78,8 +79,11 @@ export const anomalyDataChange = function ( // For now just take first 6 (or 8 if 4 charts per row). const maxSeriesToPlot = Math.max(chartsPerRow * 2, 6); const recordsToPlot = allSeriesRecords.slice(0, maxSeriesToPlot); - const isGeoMap = - (recordsToPlot[0]?.function_description || recordsToPlot[0]?.function) === 'lat_long'; + const hasGeoData = recordsToPlot.find( + (record) => + (record.function_description || recordsToPlot.function) === ML_JOB_AGGREGATION.LAT_LONG + ); + const seriesConfigs = recordsToPlot.map(buildConfig); // initialize the charts with loading indicators @@ -89,24 +93,34 @@ export const anomalyDataChange = function ( chartData: null, })); - if (isGeoMap === true) { - data.seriesToPlot = seriesConfigs.map((config) => { - const chartData = config.entityFields.length - ? [ + const mapData = []; + + if (hasGeoData !== undefined) { + for (let i = 0; i < seriesConfigs.length; i++) { + const config = seriesConfigs[i]; + let records; + if (config.detectorLabel.includes(ML_JOB_AGGREGATION.LAT_LONG)) { + if (config.entityFields.length) { + records = [ recordsToPlot.find((record) => { const entityFieldName = config.entityFields[0].fieldName; const entityFieldValue = config.entityFields[0].fieldValue; - return record[entityFieldName][0] === entityFieldValue; + return (record[entityFieldName] && record[entityFieldName][0]) === entityFieldValue; }), - ] - : recordsToPlot; - return { - ...config, - loading: false, - chartData, - }; - }); - data.showSingleMetricViewerLink = false; + ]; + } else { + records = recordsToPlot; + } + + mapData.push({ + ...config, + loading: false, + mapData: records, + }); + } + } + + data.seriesToPlot = mapData; } // Calculate the time range of the charts, which is a function of the chart width and max job bucket span. @@ -292,14 +306,19 @@ export const anomalyDataChange = function ( // only after that trigger data processing and page render. // TODO - if query returns no results e.g. source data has been deleted, // display a message saying 'No data between earliest/latest'. - const seriesPromises = seriesConfigs.map((seriesConfig) => - Promise.all([ - getMetricData(seriesConfig, chartRange), - getRecordsForCriteria(seriesConfig, chartRange), - getScheduledEvents(seriesConfig, chartRange), - getEventDistribution(seriesConfig, chartRange), - ]) - ); + const seriesPromises = []; + seriesConfigs.forEach((seriesConfig) => { + if (!seriesConfig.detectorLabel.includes(ML_JOB_AGGREGATION.LAT_LONG)) { + seriesPromises.push( + Promise.all([ + getMetricData(seriesConfig, chartRange), + getRecordsForCriteria(seriesConfig, chartRange), + getScheduledEvents(seriesConfig, chartRange), + getEventDistribution(seriesConfig, chartRange), + ]) + ); + } + }); function processChartData(response, seriesIndex) { const metricData = response[0].results; @@ -418,39 +437,48 @@ export const anomalyDataChange = function ( return chartData.find((point) => point.date === time); } - if (!isGeoMap) { - Promise.all(seriesPromises) - .then((response) => { - // calculate an overall min/max for all series - const processedData = response.map(processChartData); - const allDataPoints = reduce( - processedData, - (datapoints, series) => { - each(series, (d) => datapoints.push(d)); - return datapoints; - }, - [] - ); - const overallChartLimits = chartLimits(allDataPoints); + Promise.all(seriesPromises) + .then((response) => { + // calculate an overall min/max for all series + const processedData = response.map(processChartData); + const allDataPoints = reduce( + processedData, + (datapoints, series) => { + each(series, (d) => datapoints.push(d)); + return datapoints; + }, + [] + ); + const overallChartLimits = chartLimits(allDataPoints); + + data.seriesToPlot = response + .map((d, i) => { + if (!seriesConfigs[i].detectorLabel.includes(ML_JOB_AGGREGATION.LAT_LONG)) { + return { + ...seriesConfigs[i], + loading: false, + chartData: processedData[i], + plotEarliest: chartRange.min, + plotLatest: chartRange.max, + selectedEarliest: selectedEarliestMs, + selectedLatest: selectedLatestMs, + chartLimits: USE_OVERALL_CHART_LIMITS + ? overallChartLimits + : chartLimits(processedData[i]), + }; + } + }) + .filter((value) => value !== undefined); - data.seriesToPlot = response.map((d, i) => ({ - ...seriesConfigs[i], - loading: false, - chartData: processedData[i], - plotEarliest: chartRange.min, - plotLatest: chartRange.max, - selectedEarliest: selectedEarliestMs, - selectedLatest: selectedLatestMs, - chartLimits: USE_OVERALL_CHART_LIMITS - ? overallChartLimits - : chartLimits(processedData[i]), - })); - explorerService.setCharts({ ...data }); - }) - .catch((error) => { - console.error(error); - }); - } + if (mapData.length) { + // push map data in if it's available + data.seriesToPlot.push(...mapData); + } + explorerService.setCharts({ ...data }); + }) + .catch((error) => { + console.error(error); + }); }; function processRecordsForDisplay(anomalyRecords) { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts b/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts index 4863b93cd4931..e1fda4256bff6 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts @@ -11,8 +11,8 @@ function getAnomalyFeatures(anomalies: any[], type: 'actual_point' | 'typical_po const anomalyFeatures = []; for (let i = 0; i < anomalies.length; i++) { const anomaly = anomalies[i]; - const geoResults = anomaly.geo_results || anomaly?.causes[0].geo_results; - const coordinateStr = geoResults[type]; + const geoResults = anomaly.geo_results || (anomaly?.causes && anomaly?.causes[0]?.geo_results); + const coordinateStr = geoResults && geoResults[type]; if (coordinateStr !== undefined) { // Must reverse coordinates here. Map expects [lon, lat] - anomalies are stored as [lat, lon] for lat_lon jobs const coordinates = coordinateStr From bff94a7395fd798c75356a475a18ec4316160e4f Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 26 Jan 2021 19:43:12 -0700 Subject: [PATCH 10/15] fix tooltip function and remove single viewer link for latlong --- .../public/application/explorer/explorer.js | 2 +- .../explorer_chart_embedded_map.tsx | 19 +++++++++++++------ .../explorer_charts_container.js | 6 +++--- .../explorer_charts_container_service.js | 1 - .../application/explorer/explorer_utils.js | 5 ++++- .../application/util/chart_config_builder.js | 5 ++++- 6 files changed, 25 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index ee05387633940..cea1159ebc14a 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -269,7 +269,6 @@ export class Explorer extends React.Component { const timefilter = getTimefilter(); const bounds = timefilter.getActiveBounds(); const selectedJobIds = Array.isArray(selectedJobs) ? selectedJobs.map((job) => job.id) : []; - return ( )} + (); + const id = useMemo(() => htmlIdGenerator()(), []); const embeddableRoot: React.RefObject = useRef(null); const layerList = useRef([]); @@ -35,10 +38,14 @@ export function EmbeddedMapComponent({ seriesConfig, severity }: Props) { services: { embeddable: embeddablePlugin, maps: mapsPlugin }, } = useMlKibana(); - const factory = embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); + const factory: + | EmbeddableFactory + | undefined = embeddablePlugin + ? embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE) + : undefined; const input: MapEmbeddableInput = { - id: uuid.v4(), + id, attributes: { title: '' }, filters: [], hidePanelTitles: true, @@ -83,7 +90,7 @@ export function EmbeddedMapComponent({ seriesConfig, severity }: Props) { console.error('Map embeddable not found.'); return; } - const embeddableObject: any = await factory.create({ + const embeddableObject = await factory.create({ ...input, title: 'Explorer map', }); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index 103aad865d85b..ca65e2249cf6e 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -31,6 +31,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { MlTooltipComponent } from '../../components/chart_tooltip'; import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ML_APP_URL_GENERATOR } from '../../../../common/constants/ml_url_generator'; +import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types'; import { addItemToRecentlyAccessed } from '../../util/recently_accessed'; import { ExplorerChartsErrorCallOuts } from './explorer_charts_error_callouts'; @@ -58,7 +59,6 @@ function getChartId(series) { function ExplorerChartContainer({ series, severity, - showSingleMetricViewerLink, tooManyBuckets, wrapLabel, mlUrlGenerator, @@ -69,8 +69,8 @@ function ExplorerChartContainer({ useEffect(() => { let isCancelled = false; const generateLink = async () => { - const singleMetricViewerLink = await getExploreSeriesLink(mlUrlGenerator, series); - if (!isCancelled && showSingleMetricViewerLink === true) { + if (!isCancelled && series.functionDescription !== ML_JOB_AGGREGATION.LAT_LONG) { + const singleMetricViewerLink = await getExploreSeriesLink(mlUrlGenerator, series); setExplorerSeriesLink(singleMetricViewerLink); } }; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index 29f740bc59f27..97b19d40de477 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -37,7 +37,6 @@ export function getDefaultChartsData() { chartsPerRow: 1, errorMessages: undefined, seriesToPlot: [], - showSingleMetricViewerLink: true, // default values, will update on every re-render tooManyBuckets: false, timeFieldName: 'timestamp', diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js index f6889c9a6f24c..e5d400599036d 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js @@ -17,6 +17,7 @@ import { } from '../../../common/constants/search'; import { getEntityFieldList } from '../../../common/util/anomaly_utils'; import { extractErrorMessage } from '../../../common/util/errors'; +import { ML_JOB_AGGREGATION } from '../../../common/constants/aggregation_types'; import { isSourceDataChartableForDetector, isModelPlotChartableForDetector, @@ -511,7 +512,9 @@ export async function loadAnomaliesTableData( const entityFields = getEntityFieldList(anomaly.source); isChartable = isModelPlotEnabled(job, anomaly.detectorIndex, entityFields); } - anomaly.isTimeSeriesViewRecord = isChartable; + + anomaly.isTimeSeriesViewRecord = + isChartable && anomaly.source?.function !== ML_JOB_AGGREGATION.LAT_LONG; if (mlJobService.customUrlsByJob[jobId] !== undefined) { anomaly.customUrls = mlJobService.customUrlsByJob[jobId]; diff --git a/x-pack/plugins/ml/public/application/util/chart_config_builder.js b/x-pack/plugins/ml/public/application/util/chart_config_builder.js index a30280f1220c0..a306211defc87 100644 --- a/x-pack/plugins/ml/public/application/util/chart_config_builder.js +++ b/x-pack/plugins/ml/public/application/util/chart_config_builder.js @@ -24,7 +24,10 @@ export function buildConfigFromDetector(job, detectorIndex) { const config = { jobId: job.job_id, detectorIndex: detectorIndex, - metricFunction: mlFunctionToESAggregation(detector.function), + metricFunction: + detector.function === ML_JOB_AGGREGATION.LAT_LONG + ? ML_JOB_AGGREGATION.LAT_LONG + : mlFunctionToESAggregation(detector.function), timeField: job.data_description.time_field, interval: job.analysis_config.bucket_span, datafeedConfig: job.datafeed_config, From ea9414af7fd140367c94b8fd9b97ca715d2e114d Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 27 Jan 2021 12:29:44 -0700 Subject: [PATCH 11/15] ensure diff types of jobs show correct charts. fix jest test --- .../plugins/ml/common/util/job_utils.test.ts | 2 +- .../explorer_charts_container_service.js | 63 +++++++++---------- .../explorer/explorer_charts/map_config.ts | 35 ++++++++++- .../ml/public/application/util/chart_utils.js | 7 ++- 4 files changed, 69 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/ml/common/util/job_utils.test.ts b/x-pack/plugins/ml/common/util/job_utils.test.ts index 1ea70c0c19b4e..b0d4d583c619b 100644 --- a/x-pack/plugins/ml/common/util/job_utils.test.ts +++ b/x-pack/plugins/ml/common/util/job_utils.test.ts @@ -281,6 +281,7 @@ describe('ML - job utils', () => { expect(isSourceDataChartableForDetector(job, 22)).toBe(true); expect(isSourceDataChartableForDetector(job, 23)).toBe(true); expect(isSourceDataChartableForDetector(job, 24)).toBe(true); + expect(isSourceDataChartableForDetector(job, 36)).toBe(true); expect(isSourceDataChartableForDetector(job, 37)).toBe(true); }); @@ -296,7 +297,6 @@ describe('ML - job utils', () => { expect(isSourceDataChartableForDetector(job, 33)).toBe(false); expect(isSourceDataChartableForDetector(job, 34)).toBe(false); expect(isSourceDataChartableForDetector(job, 35)).toBe(false); - expect(isSourceDataChartableForDetector(job, 36)).toBe(false); }); }); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index 97b19d40de477..f9d8fa6726d72 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -84,6 +84,7 @@ export const anomalyDataChange = function ( ); const seriesConfigs = recordsToPlot.map(buildConfig); + const seriesConfigsNoGeoData = []; // initialize the charts with loading indicators data.seriesToPlot = seriesConfigs.map((config) => ({ @@ -116,10 +117,10 @@ export const anomalyDataChange = function ( loading: false, mapData: records, }); + } else { + seriesConfigsNoGeoData.push(config); } } - - data.seriesToPlot = mapData; } // Calculate the time range of the charts, which is a function of the chart width and max job bucket span. @@ -306,26 +307,26 @@ export const anomalyDataChange = function ( // TODO - if query returns no results e.g. source data has been deleted, // display a message saying 'No data between earliest/latest'. const seriesPromises = []; - seriesConfigs.forEach((seriesConfig) => { - if (!seriesConfig.detectorLabel.includes(ML_JOB_AGGREGATION.LAT_LONG)) { - seriesPromises.push( - Promise.all([ - getMetricData(seriesConfig, chartRange), - getRecordsForCriteria(seriesConfig, chartRange), - getScheduledEvents(seriesConfig, chartRange), - getEventDistribution(seriesConfig, chartRange), - ]) - ); - } + // Use seriesConfigs list without geo data config so indices match up after seriesPromises are resolved and we map through the responses + const seriesCongifsForPromises = hasGeoData ? seriesConfigsNoGeoData : seriesConfigs; + seriesCongifsForPromises.forEach((seriesConfig) => { + seriesPromises.push( + Promise.all([ + getMetricData(seriesConfig, chartRange), + getRecordsForCriteria(seriesConfig, chartRange), + getScheduledEvents(seriesConfig, chartRange), + getEventDistribution(seriesConfig, chartRange), + ]) + ); }); function processChartData(response, seriesIndex) { const metricData = response[0].results; const records = response[1].records; - const jobId = seriesConfigs[seriesIndex].jobId; + const jobId = seriesCongifsForPromises[seriesIndex].jobId; const scheduledEvents = response[2].events[jobId]; const eventDistribution = response[3]; - const chartType = getChartType(seriesConfigs[seriesIndex]); + const chartType = getChartType(seriesCongifsForPromises[seriesIndex]); // Sort records in ascending time order matching up with chart data records.sort((recordA, recordB) => { @@ -450,24 +451,20 @@ export const anomalyDataChange = function ( ); const overallChartLimits = chartLimits(allDataPoints); - data.seriesToPlot = response - .map((d, i) => { - if (!seriesConfigs[i].detectorLabel.includes(ML_JOB_AGGREGATION.LAT_LONG)) { - return { - ...seriesConfigs[i], - loading: false, - chartData: processedData[i], - plotEarliest: chartRange.min, - plotLatest: chartRange.max, - selectedEarliest: selectedEarliestMs, - selectedLatest: selectedLatestMs, - chartLimits: USE_OVERALL_CHART_LIMITS - ? overallChartLimits - : chartLimits(processedData[i]), - }; - } - }) - .filter((value) => value !== undefined); + data.seriesToPlot = response.map((d, i) => { + return { + ...seriesCongifsForPromises[i], + loading: false, + chartData: processedData[i], + plotEarliest: chartRange.min, + plotLatest: chartRange.max, + selectedEarliest: selectedEarliestMs, + selectedLatest: selectedLatestMs, + chartLimits: USE_OVERALL_CHART_LIMITS + ? overallChartLimits + : chartLimits(processedData[i]), + }; + }); if (mapData.length) { // push map data in if it's available diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts b/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts index e1fda4256bff6..a9ca873d84a56 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts @@ -4,8 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ +import { FIELD_ORIGIN, STYLE_TYPE } from '../../../../../maps/common/constants'; + const FEATURE = 'Feature'; const POINT = 'Point'; +const SEVERITY_COLOR_RAMP = [ + { + stop: 0, + color: '#8BC8FB', + }, + { + stop: 25, + color: '#FDEC25', + }, + { + stop: 50, + color: '#FBA740', + }, + { + stop: 75, + color: '#FE5050', + }, +]; function getAnomalyFeatures(anomalies: any[], type: 'actual_point' | 'typical_point') { const anomalyFeatures = []; @@ -90,6 +110,12 @@ export const getMLAnomaliesActualLayer = (anomalies: any) => { sourceDescriptor: { id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854d', type: 'GEOJSON_FILE', + __fields: [ + { + name: 'record_score', + type: 'number', + }, + ], __featureCollection: { features: getAnomalyFeatures(anomalies, 'actual_point'), type: 'FeatureCollection', @@ -100,9 +126,14 @@ export const getMLAnomaliesActualLayer = (anomalies: any) => { type: 'VECTOR', properties: { fillColor: { - type: 'STATIC', + type: STYLE_TYPE.DYNAMIC, options: { - color: '#FF0000', + customColorRamp: SEVERITY_COLOR_RAMP, + field: { + name: 'record_score', + origin: FIELD_ORIGIN.SOURCE, + }, + useCustomColorRamp: true, }, }, lineColor: { diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.js b/x-pack/plugins/ml/public/application/util/chart_utils.js index 25e7adf49e969..799187cc37dfd 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.js @@ -176,6 +176,11 @@ const POPULATION_DISTRIBUTION_ENABLED = true; // get the chart type based on its configuration export function getChartType(config) { let chartType = CHART_TYPE.SINGLE_METRIC; + + if (config.functionDescription === 'lat_long' || config.mapData !== undefined) { + return CHART_TYPE.GEO_MAP; + } + if ( EVENT_DISTRIBUTION_ENABLED && config.functionDescription === 'rare' && @@ -189,8 +194,6 @@ export function getChartType(config) { config.metricFunction !== null // Event distribution chart relies on the ML function mapping to an ES aggregation ) { chartType = CHART_TYPE.POPULATION_DISTRIBUTION; - } else if (config.functionDescription === 'lat_long') { - chartType = CHART_TYPE.GEO_MAP; } if ( From 1912759d3e75dd824442fbb2121d53c25f37c7d2 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 27 Jan 2021 16:15:22 -0700 Subject: [PATCH 12/15] show errorCallout if maps not enabled and is lat_long job --- .../explorer_charts_container.js | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index ca65e2249cf6e..8a369dd978c68 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -45,6 +45,9 @@ const textViewButton = i18n.translate( defaultMessage: 'Open in Single Metric Viewer', } ); +const mapsPluginMessage = i18n.translate('xpack.ml.explorer.charts.mapsPluginMissingMessage', { + defaultMessage: 'maps or embeddable start plugin not found', +}); // create a somewhat unique ID // from charts metadata for React's key attribute @@ -217,8 +220,31 @@ export const ExplorerChartsContainerUI = ({ share: { urlGenerators: { getUrlGenerator }, }, + embeddable: embeddablePlugin, + maps: mapsPlugin, }, } = kibana; + + let seriesToPlotFiltered; + + if (!embeddablePlugin || !mapsPlugin) { + seriesToPlotFiltered = []; + // Show missing plugin callout + seriesToPlot.forEach((series) => { + if (series.functionDescription === 'lat_long') { + if (errorMessages[mapsPluginMessage] === undefined) { + errorMessages[mapsPluginMessage] = new Set([series.jobId]); + } else { + errorMessages[mapsPluginMessage].add(series.jobId); + } + } else { + seriesToPlotFiltered.push(series); + } + }); + } + + const seriesToUse = seriesToPlotFiltered !== undefined ? seriesToPlotFiltered : seriesToPlot; + const mlUrlGenerator = useMemo(() => getUrlGenerator(ML_APP_URL_GENERATOR), [getUrlGenerator]); // doesn't allow a setting of `columns={1}` when chartsPerRow would be 1. @@ -226,13 +252,13 @@ export const ExplorerChartsContainerUI = ({ const chartsWidth = chartsPerRow === 1 ? 'calc(100% - 20px)' : 'auto'; const chartsColumns = chartsPerRow === 1 ? 0 : chartsPerRow; - const wrapLabel = seriesToPlot.some((series) => isLabelLengthAboveThreshold(series)); + const wrapLabel = seriesToUse.some((series) => isLabelLengthAboveThreshold(series)); return ( <> - {seriesToPlot.length > 0 && - seriesToPlot.map((series) => ( + {seriesToUse.length > 0 && + seriesToUse.map((series) => ( Date: Wed, 27 Jan 2021 16:56:40 -0700 Subject: [PATCH 13/15] use shared MlEmbeddedMapComponent in explorer --- .../ml_embedded_map/ml_embedded_map.tsx | 11 +- .../explorer_chart_embedded_map.tsx | 142 ++---------------- .../explorer_charts_container.js | 6 +- 3 files changed, 14 insertions(+), 145 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/ml_embedded_map/ml_embedded_map.tsx b/x-pack/plugins/ml/public/application/components/ml_embedded_map/ml_embedded_map.tsx index d5fdc9d52a102..12c7d6ac69bb1 100644 --- a/x-pack/plugins/ml/public/application/components/ml_embedded_map/ml_embedded_map.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_embedded_map/ml_embedded_map.tsx @@ -8,6 +8,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { htmlIdGenerator } from '@elastic/eui'; import { LayerDescriptor } from '../../../../../maps/common/descriptor_types'; +import { INITIAL_LOCATION } from '../../../../../maps/common/constants'; import { MapEmbeddable, MapEmbeddableInput, @@ -81,21 +82,13 @@ export function MlEmbeddedMapComponent({ 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 + 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 }, }; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx index 8d84cd3b47ffb..fc1621e962f36 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_embedded_map.tsx @@ -4,152 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useState, useRef, useEffect } from 'react'; -import { htmlIdGenerator } from '@elastic/eui'; -import { - EmbeddableFactory, - ErrorEmbeddable, - isErrorEmbeddable, - ViewMode, -} from '../../../../../../../src/plugins/embeddable/public'; -import { - MapEmbeddable, - MapEmbeddableInput, - MapEmbeddableOutput, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../maps/public/embeddable'; +import React, { useState, useEffect } from 'react'; import { Dictionary } from '../../../../common/types/common'; -import { useMlKibana } from '../../contexts/kibana'; -import { MAP_SAVED_OBJECT_TYPE, INITIAL_LOCATION } from '../../../../../maps/common/constants'; import { LayerDescriptor } from '../../../../../maps/common/descriptor_types'; import { getMLAnomaliesActualLayer, getMLAnomaliesTypicalLayer } from './map_config'; +import { MlEmbeddedMapComponent } from '../../components/ml_embedded_map'; interface Props { seriesConfig: Dictionary; - severity: number; } -export function EmbeddedMapComponent({ seriesConfig, severity }: Props) { - const [embeddable, setEmbeddable] = useState(); - const id = useMemo(() => htmlIdGenerator()(), []); +export function EmbeddedMapComponentWrapper({ seriesConfig }: Props) { + const [layerList, setLayerList] = useState([]); - const embeddableRoot: React.RefObject = useRef(null); - const layerList = useRef([]); - const { - services: { embeddable: embeddablePlugin, maps: mapsPlugin }, - } = useMlKibana(); - - const factory: - | EmbeddableFactory - | undefined = embeddablePlugin - ? embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE) - : undefined; - - const input: MapEmbeddableInput = { - id, - attributes: { title: '' }, - filters: [], - hidePanelTitles: true, - refreshConfig: { - value: 0, - pause: false, - }, - viewMode: ViewMode.VIEW, - isLayerTOCOpen: false, - hideFilterActions: true, - mapSettings: { - disableInteractive: false, - hideToolbarOverlay: true, - 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 - }, - }; - - // Update the layer list with updated geo points upon refresh useEffect(() => { - if ( - embeddable && - !isErrorEmbeddable(embeddable) && - seriesConfig.mapData && - seriesConfig.mapData.length > 0 - ) { - layerList.current = [ - layerList.current[0], + if (seriesConfig.mapData && seriesConfig.mapData.length > 0) { + setLayerList([ getMLAnomaliesActualLayer(seriesConfig.mapData), getMLAnomaliesTypicalLayer(seriesConfig.mapData), - ]; - embeddable.setLayerList(layerList.current); - } - }, [embeddable, seriesConfig.mapData]); - - useEffect(() => { - async function setupEmbeddable() { - if (!factory) { - // eslint-disable-next-line no-console - console.error('Map embeddable not found.'); - return; - } - const embeddableObject = await factory.create({ - ...input, - title: 'Explorer map', - }); - - if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { - const basemapLayerDescriptor = mapsPlugin - ? await mapsPlugin.createLayerDescriptors.createBasemapLayerDescriptor() - : null; - - if (basemapLayerDescriptor) { - layerList.current = [basemapLayerDescriptor]; - await embeddableObject.setLayerList(layerList.current); - } - } - - setEmbeddable(embeddableObject); + ]); } - - setupEmbeddable(); - // we want this effect to execute exactly once after the component mounts - }, []); - - // We can only render after embeddable has already initialized - useEffect(() => { - if (embeddableRoot?.current && embeddable) { - embeddable.render(embeddableRoot.current); - } - }, [embeddable, embeddableRoot, seriesConfig.mapData]); - - 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; - } + }, [seriesConfig]); return ( -
-
+
+
); } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index 8a369dd978c68..9921b5f991844 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -22,7 +22,7 @@ import { } from '../../util/chart_utils'; import { ExplorerChartDistribution } from './explorer_chart_distribution'; import { ExplorerChartSingleMetric } from './explorer_chart_single_metric'; -import { EmbeddedMapComponent } from './explorer_chart_embedded_map'; +import { EmbeddedMapComponentWrapper } from './explorer_chart_embedded_map'; import { ExplorerChartLabel } from './components/explorer_chart_label'; import { CHART_TYPE } from '../explorer_constants'; @@ -159,10 +159,8 @@ function ExplorerChartContainer({ return ( {(tooltipService) => ( - )} From 4b8124d39dbc739448e0210a7ea5cb0ced8c6324 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 28 Jan 2021 09:54:56 -0700 Subject: [PATCH 14/15] ensure latLong jobs not viewable in single metric viewer --- x-pack/plugins/ml/common/util/job_utils.ts | 15 +++++++++++++-- .../explorer_charts_container_service.js | 6 +++++- .../explorer/explorer_charts/map_config.ts | 18 +++++++++--------- .../application/explorer/explorer_utils.js | 4 +--- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts index 39aee735d4ba2..d20ad4a368948 100644 --- a/x-pack/plugins/ml/common/util/job_utils.ts +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -78,6 +78,18 @@ export function isTimeSeriesViewDetector(job: CombinedJob, detectorIndex: number ); } +// Returns a flag to indicate whether the specified job is suitable for embedded map viewing. +export function isMappableJob(job: CombinedJob, detectorIndex: number): boolean { + let isMappable = false; + const { detectors } = job.analysis_config; + if (detectorIndex >= 0 && detectorIndex < detectors.length) { + const dtr = detectors[detectorIndex]; + const functionName = dtr.function; + isMappable = functionName === ML_JOB_AGGREGATION.LAT_LONG; + } + return isMappable; +} + // Returns a flag to indicate whether the source data can be plotted in a time // series chart for the specified detector. export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex: number): boolean { @@ -93,8 +105,7 @@ export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex // Note that the 'function' field in a record contains what the user entered e.g. 'high_count', // whereas the 'function_description' field holds an ML-built display hint for function e.g. 'count'. isSourceDataChartable = - (mlFunctionToESAggregation(functionName) !== null || - functionName === ML_JOB_AGGREGATION.LAT_LONG) && + mlFunctionToESAggregation(functionName) !== null && dtr.by_field_name !== MLCATEGORY && dtr.partition_field_name !== MLCATEGORY && dtr.over_field_name !== MLCATEGORY; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index f9d8fa6726d72..077e60db4760a 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -22,6 +22,7 @@ import { isSourceDataChartableForDetector, isModelPlotChartableForDetector, isModelPlotEnabled, + isMappableJob, } from '../../../../common/util/job_utils'; import { mlResultsService } from '../../services/results_service'; import { mlJobService } from '../../services/job_service'; @@ -498,7 +499,10 @@ function processRecordsForDisplay(anomalyRecords) { return; } - let isChartable = isSourceDataChartableForDetector(job, record.detector_index); + let isChartable = + isSourceDataChartableForDetector(job, record.detector_index) || + isMappableJob(job, record.detector_index); + if (isChartable === false) { if (isModelPlotChartableForDetector(job, record.detector_index)) { // Check if model plot is enabled for this job. diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts b/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts index a9ca873d84a56..451fa602315d7 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts @@ -5,25 +5,26 @@ */ import { FIELD_ORIGIN, STYLE_TYPE } from '../../../../../maps/common/constants'; +import { ANOMALY_THRESHOLD, SEVERITY_COLORS } from '../../../../common'; const FEATURE = 'Feature'; const POINT = 'Point'; const SEVERITY_COLOR_RAMP = [ { - stop: 0, - color: '#8BC8FB', + stop: ANOMALY_THRESHOLD.LOW, + color: SEVERITY_COLORS.WARNING, }, { - stop: 25, - color: '#FDEC25', + stop: ANOMALY_THRESHOLD.MINOR, + color: SEVERITY_COLORS.MINOR, }, { - stop: 50, - color: '#FBA740', + stop: ANOMALY_THRESHOLD.MAJOR, + color: SEVERITY_COLORS.MAJOR, }, { - stop: 75, - color: '#FE5050', + stop: ANOMALY_THRESHOLD.CRITICAL, + color: SEVERITY_COLORS.CRITICAL, }, ]; @@ -102,7 +103,6 @@ export const getMLAnomaliesTypicalLayer = (anomalies: any) => { }; }; -// GEOJSON_FILE type layer does not support source-type to inject custom data for styling export const getMLAnomaliesActualLayer = (anomalies: any) => { return { id: 'anomalies_actual_layer', diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js index e5d400599036d..4ba9d4ea14f10 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js @@ -17,7 +17,6 @@ import { } from '../../../common/constants/search'; import { getEntityFieldList } from '../../../common/util/anomaly_utils'; import { extractErrorMessage } from '../../../common/util/errors'; -import { ML_JOB_AGGREGATION } from '../../../common/constants/aggregation_types'; import { isSourceDataChartableForDetector, isModelPlotChartableForDetector, @@ -513,8 +512,7 @@ export async function loadAnomaliesTableData( isChartable = isModelPlotEnabled(job, anomaly.detectorIndex, entityFields); } - anomaly.isTimeSeriesViewRecord = - isChartable && anomaly.source?.function !== ML_JOB_AGGREGATION.LAT_LONG; + anomaly.isTimeSeriesViewRecord = isChartable; if (mlJobService.customUrlsByJob[jobId] !== undefined) { anomaly.customUrls = mlJobService.customUrlsByJob[jobId]; From 4fed952801156fdce8cd382e21fe53145bfa161f Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 28 Jan 2021 09:59:26 -0700 Subject: [PATCH 15/15] update jest test --- x-pack/plugins/ml/common/util/job_utils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/common/util/job_utils.test.ts b/x-pack/plugins/ml/common/util/job_utils.test.ts index b0d4d583c619b..1ea70c0c19b4e 100644 --- a/x-pack/plugins/ml/common/util/job_utils.test.ts +++ b/x-pack/plugins/ml/common/util/job_utils.test.ts @@ -281,7 +281,6 @@ describe('ML - job utils', () => { expect(isSourceDataChartableForDetector(job, 22)).toBe(true); expect(isSourceDataChartableForDetector(job, 23)).toBe(true); expect(isSourceDataChartableForDetector(job, 24)).toBe(true); - expect(isSourceDataChartableForDetector(job, 36)).toBe(true); expect(isSourceDataChartableForDetector(job, 37)).toBe(true); }); @@ -297,6 +296,7 @@ describe('ML - job utils', () => { expect(isSourceDataChartableForDetector(job, 33)).toBe(false); expect(isSourceDataChartableForDetector(job, 34)).toBe(false); expect(isSourceDataChartableForDetector(job, 35)).toBe(false); + expect(isSourceDataChartableForDetector(job, 36)).toBe(false); }); });