Skip to content

Commit

Permalink
[Maps] pew pew source (#41504)
Browse files Browse the repository at this point in the history
* [Maps][POC] pew pew source

* refetch data on zoom level change

* add metric aggs to request

* fix bug where initial draw did not have styles set up

* make tooltips work

* fix import broken with merging master

* use custom labels in tooltips

* remove duplicate keys and clean up title and desc wording

* update source description

* update references migration to extract references for pew pew source

* add percy visual test to ensure pew pew map data is fetch, processed, and visualized as expected

* update title and description

* use GEO_FIELD_TYPES in filterGeoField function

* remove unneeded Fragment wrapper

* fix typo

* update inspector description id and message
  • Loading branch information
nreese authored Aug 28, 2019
1 parent ce2ba6d commit e42cfac
Show file tree
Hide file tree
Showing 12 changed files with 832 additions and 5 deletions.
1 change: 1 addition & 0 deletions x-pack/legacy/plugins/maps/common/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const EMS_TMS = 'EMS_TMS';
export const EMS_FILE = 'EMS_FILE';
export const ES_GEO_GRID = 'ES_GEO_GRID';
export const ES_SEARCH = 'ES_SEARCH';
export const ES_PEW_PEW = 'ES_PEW_PEW';
export const SOURCE_DATA_ID_ORIGIN = 'source';

export const GEOJSON_FILE = 'GEOJSON_FILE';
Expand Down
4 changes: 2 additions & 2 deletions x-pack/legacy/plugins/maps/common/migrations/references.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
// Can not use public Layer classes to extract references since this logic must run in both client and server.

import _ from 'lodash';
import { ES_GEO_GRID, ES_SEARCH } from '../constants';
import { ES_GEO_GRID, ES_SEARCH, ES_PEW_PEW } from '../constants';

function doesSourceUseIndexPattern(layerDescriptor) {
const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type');
return sourceType === ES_GEO_GRID || sourceType === ES_SEARCH;
return sourceType === ES_GEO_GRID || sourceType === ES_SEARCH || sourceType === ES_PEW_PEW;
}

export function extractReferences({ attributes, references = [] }) {
Expand Down
46 changes: 45 additions & 1 deletion x-pack/legacy/plugins/maps/common/migrations/references.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
/* eslint max-len: 0 */

import { extractReferences, injectReferences } from './references';
import { ES_GEO_GRID, ES_SEARCH } from '../constants';
import { ES_GEO_GRID, ES_SEARCH, ES_PEW_PEW } from '../constants';

const layerListJSON = {
esSearchSource: {
Expand All @@ -21,6 +21,10 @@ const layerListJSON = {
join: {
withIndexPatternId: '[{\"joins\":[{\"right\":{\"indexPatternId\":\"e20b2a30-f735-11e8-8ce0-9723965e01e3\"}}]}]',
withIndexPatternRef: '[{\"joins\":[{\"right\":{\"indexPatternRefName\":\"layer_0_join_0_index_pattern\"}}]}]',
},
pewPewSource: {
withIndexPatternId: `[{\"sourceDescriptor\":{\"type\":\"${ES_PEW_PEW}\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\"}}]`,
withIndexPatternRef: `[{\"sourceDescriptor\":{\"type\":\"${ES_PEW_PEW}\",\"indexPatternRefName\":\"layer_0_source_index_pattern\"}}]`,
}
};

Expand Down Expand Up @@ -78,6 +82,26 @@ describe('extractReferences', () => {
});
});

test('Should extract index-pattern reference from ES pew pew source descriptor', () => {
const attributes = {
title: 'my map',
layerListJSON: layerListJSON.pewPewSource.withIndexPatternId,
};
expect(extractReferences({ attributes })).toEqual({
attributes: {
title: 'my map',
layerListJSON: layerListJSON.pewPewSource.withIndexPatternRef,
},
references: [
{
id: 'c698b940-e149-11e8-a35a-370a8516603a',
name: 'layer_0_source_index_pattern',
type: 'index-pattern',
}
],
});
});

test('Should extract index-pattern reference from joins', () => {
const attributes = {
title: 'my map',
Expand Down Expand Up @@ -151,6 +175,26 @@ describe('injectReferences', () => {
});
});

test('Should inject index-pattern reference into ES pew pew source descriptor', () => {
const attributes = {
title: 'my map',
layerListJSON: layerListJSON.pewPewSource.withIndexPatternRef,
};
const references = [
{
id: 'c698b940-e149-11e8-a35a-370a8516603a',
name: 'layer_0_source_index_pattern',
type: 'index-pattern',
}
];
expect(injectReferences({ attributes, references })).toEqual({
attributes: {
title: 'my map',
layerListJSON: layerListJSON.pewPewSource.withIndexPatternId,
}
});
});

test('Should inject index-pattern reference into joins', () => {
const attributes = {
title: 'my map',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/


import { EMSFileSource } from './ems_file_source';
import { GeojsonFileSource } from './client_file_source';
import { KibanaRegionmapSource } from './kibana_regionmap_source';
Expand All @@ -14,12 +13,13 @@ import { WMSSource } from './wms_source';
import { KibanaTilemapSource } from './kibana_tilemap_source';
import { ESGeoGridSource } from './es_geo_grid_source';
import { ESSearchSource } from './es_search_source';

import { ESPewPewSource } from './es_pew_pew_source/es_pew_pew_source';

export const ALL_SOURCES = [
GeojsonFileSource,
ESSearchSource,
ESGeoGridSource,
ESPewPewSource,
EMSFileSource,
EMSTMSSource,
KibanaRegionmapSource,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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 _ from 'lodash';

const LAT_INDEX = 0;
const LON_INDEX = 1;

function parsePointFromKey(key) {
const split = key.split(',');
const lat = parseFloat(split[LAT_INDEX]);
const lon = parseFloat(split[LON_INDEX]);
return [lon, lat];
}

export function convertToLines(esResponse) {

const lineFeatures = [];

const destBuckets = _.get(esResponse, 'aggregations.destSplit.buckets', []);
for (let i = 0; i < destBuckets.length; i++) {
const destBucket = destBuckets[i];
const dest = parsePointFromKey(destBucket.key);
const sourceBuckets = _.get(destBucket, 'sourceGrid.buckets', []);
for (let j = 0; j < sourceBuckets.length; j++) {
const {
key, // eslint-disable-line no-unused-vars
sourceCentroid,
...rest
} = sourceBuckets[j];

// flatten metrics
Object.keys(rest).forEach(key => {
if (_.has(rest[key], 'value')) {
rest[key] = rest[key].value;
}
});

lineFeatures.push({
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: [[sourceCentroid.location.lon, sourceCentroid.location.lat], dest]
},
properties: {
...rest
}
});
}
}

return {
featureCollection: {
type: 'FeatureCollection',
features: lineFeatures
}
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
* 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 _ from 'lodash';
import React, { Fragment, Component } from 'react';
import PropTypes from 'prop-types';

import { IndexPatternSelect } from 'ui/index_patterns';
import { SingleFieldSelect } from '../../../components/single_field_select';
import { indexPatternService } from '../../../kibana_services';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';

import {
EuiFormRow,
EuiCallOut,
} from '@elastic/eui';
import { ES_GEO_FIELD_TYPE } from '../../../../common/constants';

const GEO_FIELD_TYPES = [ES_GEO_FIELD_TYPE.GEO_POINT];

function filterGeoField({ type }) {
return GEO_FIELD_TYPES.includes(type);
}

export class CreateSourceEditor extends Component {

static propTypes = {
onSourceConfigChange: PropTypes.func.isRequired,
};

state = {
isLoadingIndexPattern: false,
indexPattern: undefined,
indexPatternId: undefined,
sourceGeoField: undefined,
destGeoField: undefined,
indexPatternHasMultipleGeoFields: false,
}

componentWillUnmount() {
this._isMounted = false;
}

componentDidMount() {
this._isMounted = true;
}

onIndexPatternSelect = (indexPatternId) => {
this.setState({
indexPatternId,
}, this.loadIndexPattern.bind(null, indexPatternId));
};

loadIndexPattern = (indexPatternId) => {
this.setState({
isLoadingIndexPattern: true,
indexPattern: undefined,
sourceGeoField: undefined,
destGeoField: undefined,
indexPatternHasMultipleGeoFields: false,
}, this.debouncedLoad.bind(null, indexPatternId));
};

debouncedLoad = _.debounce(async (indexPatternId) => {
if (!indexPatternId || indexPatternId.length === 0) {
return;
}

let indexPattern;
try {
indexPattern = await indexPatternService.get(indexPatternId);
} catch (err) {
// index pattern no longer exists
return;
}

if (!this._isMounted) {
return;
}

// props.indexPatternId may be updated before getIndexPattern returns
// ignore response when fetched index pattern does not match active index pattern
if (this.state.indexPatternId !== indexPatternId) {
return;
}

const geoFields = indexPattern.fields.filter(filterGeoField);

this.setState({
isLoadingIndexPattern: false,
indexPattern: indexPattern,
indexPatternHasMultipleGeoFields: geoFields.length >= 2,
});
}, 300);

_onSourceGeoSelect = (sourceGeoField) => {
this.setState({
sourceGeoField
}, this.previewLayer);
};

_onDestGeoSelect = (destGeoField) => {
this.setState({
destGeoField
}, this.previewLayer);
};

previewLayer = () => {
const {
indexPatternId,
sourceGeoField,
destGeoField,
} = this.state;

const sourceConfig = (indexPatternId && sourceGeoField && destGeoField)
? { indexPatternId, sourceGeoField, destGeoField }
: null;
this.props.onSourceConfigChange(sourceConfig);
};

_renderGeoSelects() {
if (!this.state.indexPattern || !this.state.indexPatternHasMultipleGeoFields) {
return null;
}

return (
<Fragment>
<EuiFormRow label={i18n.translate('xpack.maps.source.pewPew.sourceGeoFieldLabel', {
defaultMessage: 'Source'
})}
>
<SingleFieldSelect
placeholder={i18n.translate('xpack.maps.source.pewPew.sourceGeoFieldPlaceholder', {
defaultMessage: 'Select source geo field'
})}
value={this.state.sourceGeoField}
onChange={this._onSourceGeoSelect}
filterField={filterGeoField}
fields={this.state.indexPattern ? this.state.indexPattern.fields : undefined}
/>
</EuiFormRow>

<EuiFormRow label={i18n.translate('xpack.maps.source.pewPew.destGeoFieldLabel', {
defaultMessage: 'Destination'
})}
>
<SingleFieldSelect
placeholder={i18n.translate('xpack.maps.source.pewPew.destGeoFieldPlaceholder', {
defaultMessage: 'Select destination geo field'
})}
value={this.state.destGeoField}
onChange={this._onDestGeoSelect}
filterField={filterGeoField}
fields={this.state.indexPattern ? this.state.indexPattern.fields : undefined}
/>
</EuiFormRow>
</Fragment>
);
}

_renderIndexPatternSelect() {
return (
<EuiFormRow label={i18n.translate('xpack.maps.source.pewPew.indexPatternLabel', {
defaultMessage: 'Index pattern'
})}
>
<IndexPatternSelect
indexPatternId={this.state.indexPatternId}
onChange={this.onIndexPatternSelect}
placeholder={i18n.translate('xpack.maps.source.pewPew.indexPatternPlaceholder', {
defaultMessage: 'Select index pattern'
})}
fieldTypes={GEO_FIELD_TYPES}
/>
</EuiFormRow>
);
}

render() {
let callout;
if (this.state.indexPattern && !this.state.indexPatternHasMultipleGeoFields) {
callout = (
<EuiCallOut
color="warning"
>
<p>
<FormattedMessage
id="xpack.maps.source.pewPew.noSourceAndDestDetails"
defaultMessage="Selected index pattern does not contain source and destination fields."
/>
</p>
</EuiCallOut>
);
}

return (
<Fragment>
{callout}
{this._renderIndexPatternSelect()}
{this._renderGeoSelects()}
</Fragment>
);
}
}
Loading

0 comments on commit e42cfac

Please sign in to comment.