diff --git a/docs/maps/maps-aggregations.asciidoc b/docs/maps/maps-aggregations.asciidoc
index b953db8858c90..923b6cc1d6649 100644
--- a/docs/maps/maps-aggregations.asciidoc
+++ b/docs/maps/maps-aggregations.asciidoc
@@ -36,8 +36,6 @@ To enable most recent entities, click "Show most recent documents by entity" and
. Set *Entity* to the field that identifies entities in your documents.
This field will be used in the terms aggregation to group your documents into entity buckets.
-. Set *Time* to the date field that puts your documents in chronological order.
-This field will be used to sort your documents in the top hits aggregation.
. Set *Documents per entity* to configure the maximum number of documents accumulated per entity.
[role="xpack"]
diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.js
index 5af3f8cfc0714..33ad39adc5d1f 100644
--- a/x-pack/legacy/plugins/maps/common/constants.js
+++ b/x-pack/legacy/plugins/maps/common/constants.js
@@ -39,6 +39,11 @@ export const LAYER_TYPE = {
HEATMAP: 'HEATMAP'
};
+export const SORT_ORDER = {
+ ASC: 'asc',
+ DESC: 'desc',
+};
+
export const EMS_TMS = 'EMS_TMS';
export const EMS_FILE = 'EMS_FILE';
export const ES_GEO_GRID = 'ES_GEO_GRID';
diff --git a/x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.js b/x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.js
new file mode 100644
index 0000000000000..7a72533005f47
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.js
@@ -0,0 +1,35 @@
+/*
+ * 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 { ES_SEARCH, SORT_ORDER } from '../constants';
+
+function isEsDocumentSource(layerDescriptor) {
+ const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type');
+ return sourceType === ES_SEARCH;
+}
+
+export function topHitsTimeToSort({ attributes }) {
+ if (!attributes.layerListJSON) {
+ return attributes;
+ }
+
+ const layerList = JSON.parse(attributes.layerListJSON);
+ layerList.forEach((layerDescriptor) => {
+ if (isEsDocumentSource(layerDescriptor)) {
+ if (_.has(layerDescriptor, 'sourceDescriptor.topHitsTimeField')) {
+ layerDescriptor.sourceDescriptor.sortField = layerDescriptor.sourceDescriptor.topHitsTimeField;
+ layerDescriptor.sourceDescriptor.sortOrder = SORT_ORDER.DESC;
+ delete layerDescriptor.sourceDescriptor.topHitsTimeField;
+ }
+ }
+ });
+
+ return {
+ ...attributes,
+ layerListJSON: JSON.stringify(layerList),
+ };
+}
diff --git a/x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.test.js b/x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.test.js
new file mode 100644
index 0000000000000..09913e83cbb43
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.test.js
@@ -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.
+ */
+
+/* eslint max-len: 0 */
+
+import { topHitsTimeToSort } from './top_hits_time_to_sort';
+
+describe('topHitsTimeToSort', () => {
+
+ test('Should handle missing layerListJSON attribute', () => {
+ const attributes = {
+ title: 'my map',
+ };
+ expect(topHitsTimeToSort({ attributes })).toEqual({
+ title: 'my map',
+ });
+ });
+
+ test('Should move topHitsTimeField to sortField for ES documents sources', () => {
+ const layerListJSON = JSON.stringify([
+ {
+ sourceDescriptor: {
+ type: 'ES_SEARCH',
+ topHitsSplitField: 'gpsId',
+ topHitsTimeField: '@timestamp',
+ }
+ }
+ ]);
+ const attributes = {
+ title: 'my map',
+ layerListJSON
+ };
+ expect(topHitsTimeToSort({ attributes })).toEqual({
+ title: 'my map',
+ layerListJSON: '[{\"sourceDescriptor\":{\"type\":\"ES_SEARCH\",\"topHitsSplitField\":\"gpsId\",\"sortField\":\"@timestamp\",\"sortOrder\":\"desc\"}}]',
+ });
+ });
+
+ test('Should handle ES documents sources without topHitsTimeField', () => {
+ const layerListJSON = JSON.stringify([
+ {
+ sourceDescriptor: {
+ type: 'ES_SEARCH',
+ topHitsSplitField: 'gpsId',
+ }
+ }
+ ]);
+ const attributes = {
+ title: 'my map',
+ layerListJSON
+ };
+ expect(topHitsTimeToSort({ attributes })).toEqual({
+ title: 'my map',
+ layerListJSON,
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/maps/migrations.js b/x-pack/legacy/plugins/maps/migrations.js
index 6a97636e679c4..cdec1f25ce247 100644
--- a/x-pack/legacy/plugins/maps/migrations.js
+++ b/x-pack/legacy/plugins/maps/migrations.js
@@ -6,6 +6,7 @@
import { extractReferences } from './common/migrations/references';
import { emsRasterTileToEmsVectorTile } from './common/migrations/ems_raster_tile_to_ems_vector_tile';
+import { topHitsTimeToSort } from './common/migrations/top_hits_time_to_sort';
export const migrations = {
'map': {
@@ -21,6 +22,14 @@ export const migrations = {
'7.4.0': (doc) => {
const attributes = emsRasterTileToEmsVectorTile(doc);
+ return {
+ ...doc,
+ attributes,
+ };
+ },
+ '7.5.0': (doc) => {
+ const attributes = topHitsTimeToSort(doc);
+
return {
...doc,
attributes,
diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap
new file mode 100644
index 0000000000000..c0736a54e55d2
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap
@@ -0,0 +1,292 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should enable sort order select when sort field provided 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`should render top hits form when useTopHits is true 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`should render update source editor 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js
index 071e231bb6e2d..53762a1f27d27 100644
--- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js
+++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js
@@ -14,7 +14,7 @@ import { SearchSource } from '../../../kibana_services';
import { hitsToGeoJson } from '../../../elasticsearch_geo_utils';
import { CreateSourceEditor } from './create_source_editor';
import { UpdateSourceEditor } from './update_source_editor';
-import { ES_SEARCH, ES_GEO_FIELD_TYPE, ES_SIZE_LIMIT } from '../../../../common/constants';
+import { ES_SEARCH, ES_GEO_FIELD_TYPE, ES_SIZE_LIMIT, SORT_ORDER } from '../../../../common/constants';
import { i18n } from '@kbn/i18n';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { ESTooltipProperty } from '../../tooltips/es_tooltip_property';
@@ -56,9 +56,10 @@ export class ESSearchSource extends AbstractESSource {
geoField: descriptor.geoField,
filterByMapBounds: _.get(descriptor, 'filterByMapBounds', DEFAULT_FILTER_BY_MAP_BOUNDS),
tooltipProperties: _.get(descriptor, 'tooltipProperties', []),
+ sortField: _.get(descriptor, 'sortField', ''),
+ sortOrder: _.get(descriptor, 'sortOrder', SORT_ORDER.DESC),
useTopHits: _.get(descriptor, 'useTopHits', false),
topHitsSplitField: descriptor.topHitsSplitField,
- topHitsTimeField: descriptor.topHitsTimeField,
topHitsSize: _.get(descriptor, 'topHitsSize', 1),
}, inspectorAdapters);
}
@@ -70,9 +71,10 @@ export class ESSearchSource extends AbstractESSource {
onChange={onChange}
filterByMapBounds={this._descriptor.filterByMapBounds}
tooltipProperties={this._descriptor.tooltipProperties}
+ sortField={this._descriptor.sortField}
+ sortOrder={this._descriptor.sortOrder}
useTopHits={this._descriptor.useTopHits}
topHitsSplitField={this._descriptor.topHitsSplitField}
- topHitsTimeField={this._descriptor.topHitsTimeField}
topHitsSize={this._descriptor.topHitsSize}
/>
);
@@ -135,10 +137,24 @@ export class ESSearchSource extends AbstractESSource {
];
}
+ // Returns sort content for an Elasticsearch search body
+ _buildEsSort() {
+ const {
+ sortField,
+ sortOrder,
+ } = this._descriptor;
+ return [
+ {
+ [sortField]: {
+ order: sortOrder
+ }
+ }
+ ];
+ }
+
async _getTopHits(layerName, searchFilters, registerCancelCallback) {
const {
topHitsSplitField,
- topHitsTimeField,
topHitsSize,
} = this._descriptor;
@@ -158,7 +174,7 @@ export class ESSearchSource extends AbstractESSource {
});
const searchSource = await this._makeSearchSource(searchFilters, 0);
- searchSource.setField('aggs', {
+ const aggs = {
entitySplit: {
terms: {
field: topHitsSplitField,
@@ -167,13 +183,6 @@ export class ESSearchSource extends AbstractESSource {
aggs: {
entityHits: {
top_hits: {
- sort: [
- {
- [topHitsTimeField]: {
- order: 'desc'
- }
- }
- ],
_source: {
includes: searchFilters.fieldNames
},
@@ -183,7 +192,11 @@ export class ESSearchSource extends AbstractESSource {
}
}
}
- });
+ };
+ if (this._hasSort()) {
+ aggs.entitySplit.aggs.entityHits.top_hits.sort = this._buildEsSort();
+ }
+ searchSource.setField('aggs', aggs);
const resp = await this._runEsQuery(layerName, searchSource, registerCancelCallback, 'Elasticsearch document top hits request');
@@ -193,7 +206,7 @@ export class ESSearchSource extends AbstractESSource {
entityBuckets.forEach(entityBucket => {
const total = _.get(entityBucket, 'entityHits.hits.total', 0);
const hits = _.get(entityBucket, 'entityHits.hits.hits', []);
- // Reverse hits list so they are drawn from oldest to newest (per entity) so newest events are on top
+ // Reverse hits list so top documents by sort are drawn on top
allHits.push(...hits.reverse());
if (total > hits.length) {
hasTrimmedResults = true;
@@ -218,10 +231,14 @@ export class ESSearchSource extends AbstractESSource {
// By setting "fields", SearchSource removes all of defaults
searchSource.setField('fields', searchFilters.fieldNames);
+ if (this._hasSort()) {
+ searchSource.setField('sort', this._buildEsSort());
+ }
+
const resp = await this._runEsQuery(layerName, searchSource, registerCancelCallback, 'Elasticsearch document request');
return {
- hits: resp.hits.hits,
+ hits: resp.hits.hits.reverse(), // Reverse hits so top documents by sort are drawn on top
meta: {
areResultsTrimmed: resp.hits.total > resp.hits.hits.length
}
@@ -229,8 +246,13 @@ export class ESSearchSource extends AbstractESSource {
}
_isTopHits() {
- const { useTopHits, topHitsSplitField, topHitsTimeField } = this._descriptor;
- return !!(useTopHits && topHitsSplitField && topHitsTimeField);
+ const { useTopHits, topHitsSplitField } = this._descriptor;
+ return !!(useTopHits && topHitsSplitField);
+ }
+
+ _hasSort() {
+ const { sortField, sortOrder } = this._descriptor;
+ return !!sortField && !!sortOrder;
}
async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback) {
@@ -406,9 +428,10 @@ export class ESSearchSource extends AbstractESSource {
getSyncMeta() {
return {
+ sortField: this._descriptor.sortField,
+ sortOrder: this._descriptor.sortOrder,
useTopHits: this._descriptor.useTopHits,
topHitsSplitField: this._descriptor.topHitsSplitField,
- topHitsTimeField: this._descriptor.topHitsTimeField,
topHitsSize: this._descriptor.topHitsSize,
};
}
diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js
index ff319fd3d7e97..37c955c982168 100644
--- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js
+++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js
@@ -6,7 +6,13 @@
import React, { Fragment, Component } from 'react';
import PropTypes from 'prop-types';
-import { EuiFormRow, EuiSwitch } from '@elastic/eui';
+import {
+ EuiFormRow,
+ EuiSwitch,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSelect,
+} from '@elastic/eui';
import { SingleFieldSelect } from '../../../components/single_field_select';
import { TooltipSelector } from '../../../components/tooltip_selector';
@@ -14,6 +20,7 @@ import { indexPatternService } from '../../../kibana_services';
import { i18n } from '@kbn/i18n';
import { getTermsFields, getSourceFields } from '../../../index_pattern_util';
import { ValidatedRange } from '../../../components/validated_range';
+import { SORT_ORDER } from '../../../../common/constants';
export class UpdateSourceEditor extends Component {
static propTypes = {
@@ -21,16 +28,17 @@ export class UpdateSourceEditor extends Component {
onChange: PropTypes.func.isRequired,
filterByMapBounds: PropTypes.bool.isRequired,
tooltipProperties: PropTypes.arrayOf(PropTypes.string).isRequired,
+ sortField: PropTypes.string,
+ sortOrder: PropTypes.string.isRequired,
useTopHits: PropTypes.bool.isRequired,
topHitsSplitField: PropTypes.string,
- topHitsTimeField: PropTypes.string,
topHitsSize: PropTypes.number.isRequired,
};
state = {
tooltipFields: null,
termFields: null,
- dateFields: null,
+ sortFields: null,
};
componentDidMount() {
@@ -64,27 +72,11 @@ export class UpdateSourceEditor extends Component {
return;
}
- const dateFields = indexPattern.fields.filter(field => {
- return field.type === 'date';
- });
-
this.setState({
- dateFields,
tooltipFields: getSourceFields(indexPattern.fields),
termFields: getTermsFields(indexPattern.fields),
+ sortFields: indexPattern.fields.filter(field => field.sortable),
});
-
- if (!this.props.topHitsTimeField) {
- // prefer default time field
- if (indexPattern.timeFieldName) {
- this.onTopHitsTimeFieldChange(indexPattern.timeFieldName);
- } else {
- // fall back to first date field in index
- if (dateFields.length > 0) {
- this.onTopHitsTimeFieldChange(dateFields[0].name);
- }
- }
- }
}
_onTooltipPropertiesChange = propertyNames => {
this.props.onChange({ propName: 'tooltipProperties', value: propertyNames });
@@ -102,10 +94,14 @@ export class UpdateSourceEditor extends Component {
this.props.onChange({ propName: 'topHitsSplitField', value: topHitsSplitField });
};
- onTopHitsTimeFieldChange = topHitsTimeField => {
- this.props.onChange({ propName: 'topHitsTimeField', value: topHitsTimeField });
+ onSortFieldChange = sortField => {
+ this.props.onChange({ propName: 'sortField', value: sortField });
};
+ onSortOrderChange = e => {
+ this.props.onChange({ propName: 'sortOrder', value: e.target.value });
+ }
+
onTopHitsSizeChange = size => {
this.props.onChange({ propName: 'topHitsSize', value: size });
};
@@ -115,31 +111,8 @@ export class UpdateSourceEditor extends Component {
return null;
}
- let timeFieldSelect;
let sizeSlider;
if (this.props.topHitsSplitField) {
- timeFieldSelect = (
-
-
-
- );
-
sizeSlider = (
- {timeFieldSelect}
-
{sizeSlider}
);
}
render() {
- let topHitsCheckbox;
- if (this.state.dateFields && this.state.dateFields.length) {
- topHitsCheckbox = (
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- );
- }
- return (
-
-
+ {this.renderTopHitsForm()}
- {topHitsCheckbox}
-
- {this.renderTopHitsForm()}
);
}
diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js
new file mode 100644
index 0000000000000..9a3a74e0ed680
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+jest.mock('../../../kibana_services', () => ({}));
+
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { UpdateSourceEditor } from './update_source_editor';
+
+const defaultProps = {
+ indexPatternId: 'indexPattern1',
+ onChange: () => {},
+ filterByMapBounds: true,
+ tooltipProperties: [],
+ sortOrder: 'DESC',
+ useTopHits: false,
+ topHitsSplitField: 'trackId',
+ topHitsSize: 1,
+};
+
+test('should render update source editor', async () => {
+ const component = shallow(
+
+ );
+
+ expect(component).toMatchSnapshot();
+});
+
+test('should enable sort order select when sort field provided', async () => {
+ const component = shallow(
+
+ );
+
+ expect(component).toMatchSnapshot();
+});
+
+test('should render top hits form when useTopHits is true', async () => {
+ const component = shallow(
+
+ );
+
+ expect(component).toMatchSnapshot();
+});
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index b7a6b24242924..46ca0417d5d72 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -5761,8 +5761,6 @@
"xpack.maps.source.esSearch.topHitsSizeLabel": "エンティティごとのドキュメント数",
"xpack.maps.source.esSearch.topHitsSplitFieldLabel": "エンティティ",
"xpack.maps.source.esSearch.topHitsSplitFieldSelectPlaceholder": "エンティティフィールドを選択",
- "xpack.maps.source.esSearch.topHitsTimeFieldLabel": "時間",
- "xpack.maps.source.esSearch.topHitsTimeFieldSelectPlaceholder": "タイムフィールドを選択",
"xpack.maps.source.esSearch.useTopHitsLabel": "エンティティによる最も最近のドキュメントを表示",
"xpack.maps.source.esSearchDescription": "Kibana インデックスパターンの地理空間データ",
"xpack.maps.source.esSearchTitle": "ドキュメント",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 79cba3ee696be..354db2b217ac6 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -5764,8 +5764,6 @@
"xpack.maps.source.esSearch.topHitsSizeLabel": "每个实体的文档",
"xpack.maps.source.esSearch.topHitsSplitFieldLabel": "实体",
"xpack.maps.source.esSearch.topHitsSplitFieldSelectPlaceholder": "选择实体字段",
- "xpack.maps.source.esSearch.topHitsTimeFieldLabel": "时间",
- "xpack.maps.source.esSearch.topHitsTimeFieldSelectPlaceholder": "选择时间字段",
"xpack.maps.source.esSearch.useTopHitsLabel": "按实体显示最近的文档",
"xpack.maps.source.esSearchDescription": "Kibana 索引模式的地理空间数据",
"xpack.maps.source.esSearchTitle": "文档",
diff --git a/x-pack/test/api_integration/apis/maps/migrations.js b/x-pack/test/api_integration/apis/maps/migrations.js
index d8bdc5e61cb11..e4ef5e5784249 100644
--- a/x-pack/test/api_integration/apis/maps/migrations.js
+++ b/x-pack/test/api_integration/apis/maps/migrations.js
@@ -42,7 +42,7 @@ export default function ({ getService }) {
type: 'index-pattern'
}
]);
- expect(resp.body.migrationVersion).to.eql({ map: '7.4.0' });
+ expect(resp.body.migrationVersion).to.eql({ map: '7.5.0' });
expect(resp.body.attributes.layerListJSON.includes('indexPatternRefName')).to.be(true);
});
});
diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js
index 6e7541cc20db8..41c777ad67c77 100644
--- a/x-pack/test/functional/apps/maps/joins.js
+++ b/x-pack/test/functional/apps/maps/joins.js
@@ -105,7 +105,7 @@ export default function ({ getPageObjects, getService }) {
return feature.properties.__kbn__isvisible__;
});
- expect(visibilitiesOfFeatures).to.eql([true, true, true, false]);
+ expect(visibilitiesOfFeatures).to.eql([false, true, true, true]);
});
@@ -182,7 +182,7 @@ export default function ({ getPageObjects, getService }) {
return feature.properties.__kbn__isvisible__;
});
- expect(visibilitiesOfFeatures).to.eql([false, false, true, false]);
+ expect(visibilitiesOfFeatures).to.eql([false, true, false, false]);
});