diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts index 8fb6140d55e319..bf185f78941de2 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts @@ -24,9 +24,9 @@ import { syncStates, BaseStateContainer, } from '../../../../../../../plugins/kibana_utils/public'; -import { esFilters, FilterManager, Filter } from '../../../../../../../plugins/data/public'; +import { esFilters, FilterManager, Filter, Query } from '../../../../../../../plugins/data/public'; -interface AppState { +export interface AppState { /** * Columns displayed in the table, cannot be changed by UI, just in discover's main app */ @@ -47,6 +47,7 @@ interface AppState { * Number of records to be fetched after the anchor records (older records) */ successorCount: number; + query?: Query; } interface GlobalState { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js index a175a1aebebdfa..df970ab5f25843 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js @@ -24,7 +24,11 @@ import './discover_field'; import './discover_field_search_directive'; import './discover_index_pattern_directive'; import fieldChooserTemplate from './field_chooser.html'; -import { IndexPatternFieldList } from '../../../../../../../../plugins/data/public'; +import { + IndexPatternFieldList, + KBN_FIELD_TYPES, +} from '../../../../../../../../plugins/data/public'; +import { getMapsAppUrl, isFieldVisualizable, isMapsAppRegistered } from './lib/visualize_url_utils'; export function createFieldChooserDirective($location, config, $route) { return { @@ -186,8 +190,15 @@ export function createFieldChooserDirective($location, config, $route) { return ''; } + if ( + (field.type === KBN_FIELD_TYPES.GEO_POINT || field.type === KBN_FIELD_TYPES.GEO_SHAPE) && + isMapsAppRegistered() + ) { + return getMapsAppUrl(field, $scope.indexPattern, $scope.state, $scope.columns); + } + let agg = {}; - const isGeoPoint = field.type === 'geo_point'; + const isGeoPoint = field.type === KBN_FIELD_TYPES.GEO_POINT; const type = isGeoPoint ? 'tile_map' : 'histogram'; // If we're visualizing a date field, and our index is time based (and thus has a time filter), // then run a date histogram @@ -243,7 +254,7 @@ export function createFieldChooserDirective($location, config, $route) { $scope.computeDetails = function(field, recompute) { if (_.isUndefined(field.details) || recompute) { field.details = { - visualizeUrl: field.visualizable ? getVisualizeUrl(field) : null, + visualizeUrl: isFieldVisualizable(field) ? getVisualizeUrl(field) : null, ...fieldCalculator.getFieldValueCounts({ hits: $scope.hits, field: field, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/detail_views/string.html b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/detail_views/string.html index 5d134911fc91b7..333dc472e956d8 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/detail_views/string.html +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/detail_views/string.html @@ -79,7 +79,7 @@ @@ -87,7 +87,7 @@ diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts new file mode 100644 index 00000000000000..8dbf3cd79ccb16 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts @@ -0,0 +1,108 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import uuid from 'uuid/v4'; +// @ts-ignore +import rison from 'rison-node'; +import { + IFieldType, + IIndexPattern, + KBN_FIELD_TYPES, +} from '../../../../../../../../../plugins/data/public'; +import { AppState } from '../../../angular/context_state'; +import { getServices } from '../../../../kibana_services'; + +function getMapsAppBaseUrl() { + const mapsAppVisAlias = getServices() + .visualizations.types.getAliases() + .find(({ name }) => { + return name === 'maps'; + }); + return mapsAppVisAlias ? mapsAppVisAlias.aliasUrl : null; +} + +export function isMapsAppRegistered() { + return getServices() + .visualizations.types.getAliases() + .some(({ name }) => { + return name === 'maps'; + }); +} + +export function isFieldVisualizable(field: IFieldType) { + if ( + (field.type === KBN_FIELD_TYPES.GEO_POINT || field.type === KBN_FIELD_TYPES.GEO_SHAPE) && + isMapsAppRegistered() + ) { + return true; + } + return field.visualizable; +} + +export function getMapsAppUrl( + field: IFieldType, + indexPattern: IIndexPattern, + appState: AppState, + columns: string[] +) { + const mapAppParams = new URLSearchParams(); + + // Copy global state + const locationSplit = window.location.href.split('discover?'); + if (locationSplit.length > 1) { + const discoverParams = new URLSearchParams(locationSplit[1]); + const globalStateUrlValue = discoverParams.get('_g'); + if (globalStateUrlValue) { + mapAppParams.set('_g', globalStateUrlValue); + } + } + + // Copy filters and query in app state + const mapsAppState: any = { + filters: appState.filters || [], + }; + if (appState.query) { + mapsAppState.query = appState.query; + } + // @ts-ignore + mapAppParams.set('_a', rison.encode(mapsAppState)); + + // create initial layer descriptor + const hasColumns = columns && columns.length && columns[0] !== '_source'; + mapAppParams.set( + 'initialLayers', + // @ts-ignore + rison.encode_array([ + { + id: uuid(), + label: indexPattern.title, + sourceDescriptor: { + id: uuid(), + type: 'ES_SEARCH', + geoField: field.name, + tooltipProperties: hasColumns ? columns : [], + indexPatternId: indexPattern.id, + }, + visible: true, + type: 'VECTOR', + }, + ]) + ); + + return getServices().addBasePath(`${getMapsAppBaseUrl()}?${mapAppParams.toString()}`); +} diff --git a/test/functional/page_objects/discover_page.js b/test/functional/page_objects/discover_page.js index 5ccc5625849d27..080b8c8ee753fc 100644 --- a/test/functional/page_objects/discover_page.js +++ b/test/functional/page_objects/discover_page.js @@ -206,12 +206,6 @@ export function DiscoverPageProvider({ getService, getPageObjects }) { return await testSubjects.getVisibleText('discoverQueryHits'); } - async query(queryString) { - await find.setValue('input[aria-label="Search input"]', queryString); - await find.clickByCssSelector('button[aria-label="Search"]'); - await PageObjects.header.waitUntilLoadingHasFinished(); - } - async getDocHeader() { const header = await find.byCssSelector('thead > tr:nth-child(1)'); return await header.getVisibleText(); diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js b/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js index 45ee441716769e..3cae75231d28e3 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js +++ b/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js @@ -9,7 +9,7 @@ import { EMSTMSSource } from '../layers/sources/ems_tms_source'; import chrome from 'ui/chrome'; import { getKibanaTileMap } from '../meta'; -export function getInitialLayers(layerListJSON) { +export function getInitialLayers(layerListJSON, initialLayers = []) { if (layerListJSON) { return JSON.parse(layerListJSON); } @@ -19,7 +19,7 @@ export function getInitialLayers(layerListJSON) { const sourceDescriptor = KibanaTilemapSource.createDescriptor(); const source = new KibanaTilemapSource(sourceDescriptor); const layer = source.createDefaultLayer(); - return [layer.toLayerDescriptor()]; + return [layer.toLayerDescriptor(), ...initialLayers]; } const isEmsEnabled = chrome.getInjected('isEmsEnabled', true); @@ -27,8 +27,8 @@ export function getInitialLayers(layerListJSON) { const descriptor = EMSTMSSource.createDescriptor({ isAutoSelect: true }); const source = new EMSTMSSource(descriptor); const layer = source.createDefaultLayer(); - return [layer.toLayerDescriptor()]; + return [layer.toLayerDescriptor(), ...initialLayers]; } - return []; + return initialLayers; } diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 95c8ff975b1d61..a8e9ae46a3b9ad 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -6,6 +6,7 @@ import _ from 'lodash'; import chrome from 'ui/chrome'; +import rison from 'rison-node'; import 'ui/directives/listen'; import 'ui/directives/storage'; import React from 'react'; @@ -66,6 +67,32 @@ const REACT_ANCHOR_DOM_ELEMENT_ID = 'react-maps-root'; const app = uiModules.get(MAP_APP_PATH, []); +function getInitialLayersFromUrlParam() { + const locationSplit = window.location.href.split('?'); + if (locationSplit.length <= 1) { + return []; + } + const mapAppParams = new URLSearchParams(locationSplit[1]); + if (!mapAppParams.has('initialLayers')) { + return []; + } + + try { + return rison.decode_array(mapAppParams.get('initialLayers')); + } catch (e) { + toastNotifications.addWarning({ + title: i18n.translate('xpack.maps.initialLayers.unableToParseTitle', { + defaultMessage: `Inital layers not added to map`, + }), + text: i18n.translate('xpack.maps.initialLayers.unableToParseMessage', { + defaultMessage: `Unable to parse contents of 'initialLayers' parameter. Error: {errorMsg}`, + values: { errorMsg: e.message }, + }), + }); + return []; + } +} + app.controller( 'GisMapController', ($scope, $route, kbnUrl, localStorage, AppState, globalState) => { @@ -333,7 +360,7 @@ app.controller( store.dispatch(setOpenTOCDetails(_.get(uiState, 'openTOCDetails', []))); } - const layerList = getInitialLayers(savedMap.layerListJSON); + const layerList = getInitialLayers(savedMap.layerListJSON, getInitialLayersFromUrlParam()); initialLayerListConfig = copyPersistentState(layerList); store.dispatch(replaceLayerList(layerList)); store.dispatch(setRefreshConfig($scope.refreshConfig)); diff --git a/x-pack/test/functional/apps/maps/discover.js b/x-pack/test/functional/apps/maps/discover.js new file mode 100644 index 00000000000000..ce335964767551 --- /dev/null +++ b/x-pack/test/functional/apps/maps/discover.js @@ -0,0 +1,50 @@ +/* + * 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 expect from '@kbn/expect'; + +export default function({ getService, getPageObjects }) { + const queryBar = getService('queryBar'); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'maps', 'timePicker']); + + describe('discover visualize button', () => { + beforeEach(async () => { + await PageObjects.common.navigateToApp('discover'); + }); + + it('should link geo_shape fields to Maps application', async () => { + await PageObjects.discover.selectIndexPattern('geo_shapes*'); + await PageObjects.discover.clickFieldListItem('geometry'); + await PageObjects.discover.clickFieldListItemVisualize('geometry'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + const doesLayerExist = await PageObjects.maps.doesLayerExist('geo_shapes*'); + expect(doesLayerExist).to.equal(true); + const hits = await PageObjects.maps.getHits(); + expect(hits).to.equal('4'); + }); + + it('should link geo_point fields to Maps application with time and query context', async () => { + await PageObjects.discover.selectIndexPattern('logstash-*'); + await PageObjects.timePicker.setAbsoluteRange( + 'Sep 22, 2015 @ 00:00:00.000', + 'Sep 22, 2015 @ 04:00:00.000' + ); + await queryBar.setQuery('machine.os.raw : "ios"'); + await queryBar.submitQuery(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.discover.clickFieldListItem('geo.coordinates'); + await PageObjects.discover.clickFieldListItemVisualize('geo.coordinates'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + const doesLayerExist = await PageObjects.maps.doesLayerExist('logstash-*'); + expect(doesLayerExist).to.equal(true); + const hits = await PageObjects.maps.getHits(); + expect(hits).to.equal('7'); + }); + }); +} diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index 0545fcd1b6453a..e8a9d7ba54bc55 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -45,6 +45,7 @@ export default function({ loadTestFile, getService }) { loadTestFile(require.resolve('./import_geojson')); loadTestFile(require.resolve('./layer_errors')); loadTestFile(require.resolve('./embeddable')); + loadTestFile(require.resolve('./discover')); }); }); }