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 2029211c98970c..678a74c83df4fa 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 @@ -15,6 +15,7 @@ import { EuiFlexItem, EuiIconTip, EuiToolTip, + htmlIdGenerator } from '@elastic/eui'; import { @@ -31,7 +32,11 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { MlTooltipComponent } from '../../components/chart_tooltip'; import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { useMlKibana } from '../../contexts/kibana'; import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types'; +import { AnomalySource } from '../../../maps/anomaly_source'; +import { CUSTOM_COLOR_RAMP } from '../../../maps/anomaly_layer_wizard_factory'; +import { LAYER_TYPE } from '../../../../../maps/common'; import { ExplorerChartsErrorCallOuts } from './explorer_charts_error_callouts'; import { addItemToRecentlyAccessed } from '../../util/recently_accessed'; import { EmbeddedMapComponentWrapper } from './explorer_chart_embedded_map'; @@ -53,6 +58,10 @@ const textViewButton = i18n.translate( const mapsPluginMessage = i18n.translate('xpack.ml.explorer.charts.mapsPluginMissingMessage', { defaultMessage: 'maps or embeddable start plugin not found', }); +const openInMapsPluginMessage = i18n.translate('xpack.ml.explorer.charts.openInMapsPluginMessage', { + defaultMessage: 'Open in Maps', +}); + // create a somewhat unique ID // from charts metadata for React's key attribute @@ -79,6 +88,72 @@ function ExplorerChartContainer({ chartsService, }) { const [explorerSeriesLink, setExplorerSeriesLink] = useState(''); + const [mapsLink, setMapsLink] = useState(''); + + const { + services: { data, share, + application: { navigateToApp }, }, + } = useMlKibana(); + + // TODO: pull in layer name constant and wrap in useCallback if poss + const getMapsLink = async () => { + const initialLayers = [{ + id: htmlIdGenerator()(), + type: LAYER_TYPE.GEOJSON_VECTOR, + sourceDescriptor: AnomalySource.createDescriptor({ + jobId: series.jobId, + typicalActual: 'actual', + }), + style: { + type: 'VECTOR', + properties: { + fillColor: CUSTOM_COLOR_RAMP, + lineColor: CUSTOM_COLOR_RAMP, + }, + isTimeAware: false, + }, + }, + { + id: htmlIdGenerator()(), + type: LAYER_TYPE.GEOJSON_VECTOR, + sourceDescriptor: AnomalySource.createDescriptor({ + jobId: series.jobId, + typicalActual: 'typical', + }), + style: { + type: 'VECTOR', + properties: { + fillColor: CUSTOM_COLOR_RAMP, + lineColor: CUSTOM_COLOR_RAMP, + }, + isTimeAware: false, + }, + }, + { + id: htmlIdGenerator()(), + type: LAYER_TYPE.GEOJSON_VECTOR, + sourceDescriptor: AnomalySource.createDescriptor({ + jobId: series.jobId, + typicalActual: 'typical to actual', + }), + style: { + type: 'VECTOR', + properties: { + fillColor: CUSTOM_COLOR_RAMP, + lineColor: CUSTOM_COLOR_RAMP, + }, + isTimeAware: false, + }, + }]; + + const locator = share.url.locators.get('MAPS_APP_LOCATOR'); + const location = await locator.getLocation({ + initialLayers: initialLayers, + timeRange: data.query.timefilter.timefilter.getTime(), + }); + + return location; + }; useEffect(() => { let isCancelled = false; @@ -98,6 +173,26 @@ function ExplorerChartContainer({ }; }, [mlLocator, series]); + useEffect(function getMapsPluginLink() { + if (!series) return; + let isCancelled = false; + const generateLink = async () => { + if (!isCancelled) { + try { + const mapsLink = await getMapsLink(); + setMapsLink(mapsLink?.path); + } catch (error) { + console.error(error); + setMapsLink(''); + } + } + }; + generateLink().catch(console.error);; + return () => { + isCancelled = true; + }; + }, [series]); + const chartRef = useRef(null); const chartTheme = chartsService.theme.useChartsTheme(); @@ -191,6 +286,20 @@ function ExplorerChartContainer({ )} + {chartType === CHART_TYPE.GEO_MAP && mapsLink ? ( + + { + await navigateToApp('maps', { path: mapsLink }); + }} + > + + + + ) : null} diff --git a/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx b/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx index 260d058b78e789..e4309247a272ae 100644 --- a/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx +++ b/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx @@ -26,7 +26,7 @@ import type { MlPluginStart, MlStartDependencies } from '../plugin'; import type { MlApiServices } from '../application/services/ml_api_service'; export const ML_ANOMALY = 'ML_ANOMALIES'; -const CUSTOM_COLOR_RAMP = { +export const CUSTOM_COLOR_RAMP = { type: STYLE_TYPE.DYNAMIC, options: { customColorRamp: SEVERITY_COLOR_RAMP, diff --git a/x-pack/plugins/ml/public/maps/anomaly_source.tsx b/x-pack/plugins/ml/public/maps/anomaly_source.tsx index e2d92a730d95af..72da52ce501c53 100644 --- a/x-pack/plugins/ml/public/maps/anomaly_source.tsx +++ b/x-pack/plugins/ml/public/maps/anomaly_source.tsx @@ -59,7 +59,7 @@ export class AnomalySource implements IVectorSource { constructor(sourceDescriptor: Partial, adapters?: Adapters) { this._descriptor = AnomalySource.createDescriptor(sourceDescriptor); } - // TODO: implement query awareness + async getGeoJsonWithMeta( layerName: string, searchFilters: VectorSourceRequestMeta, @@ -77,7 +77,7 @@ export class AnomalySource implements IVectorSource { data: results, meta: { // Set this to true if data is incomplete (e.g. capping number of results to first 1k) - areResultsTrimmed: false, + areResultsTrimmed: results.features.length === 1000, }, }; } @@ -147,8 +147,18 @@ export class AnomalySource implements IVectorSource { return null; } - getSourceStatus() { - return { tooltipContent: null, areResultsTrimmed: true }; + getSourceStatus(sourceDataRequest?: DataRequest): SourceStatus { + const featureCollection = sourceDataRequest ? sourceDataRequest.getData() : null; + const meta = sourceDataRequest + ? (sourceDataRequest.getMeta()) + : null; + if (!featureCollection || !meta) { + return { + tooltipContent: null, + areResultsTrimmed: false, + }; + } + return { tooltipContent: null, areResultsTrimmed: meta?.areEntitiesTrimmed ?? false }; } getType(): string { @@ -222,12 +232,15 @@ export class AnomalySource implements IVectorSource { } getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceStatus { + const meta = sourceDataRequest + ? (sourceDataRequest.getMeta()) + : null; return { tooltipContent: i18n.translate('xpack.ml.maps.sourceTooltip', { defaultMessage: 'Shows anomalies', }), // set to true if data is incomplete (we limit to first 1000 results) - areResultsTrimmed: true, + areResultsTrimmed: meta?.areResultsTrimmed ?? false, }; }