[Maps] add support for Top Hits to Documents source (#38052)
* top hits * fetch top hits * trigger load if top hits configuration changes * text clean-up * add functional tests for top hits * add functional test verify configuration updates re-fetch data * show entity source tooltip message for top hits * include entities cound in results trimmed message * pass fields needed for data driven styling and joins * fix i18n problem * more i18n changes * reverse hits list so most recent events are drawn on top * review feedback * set meta to null when no sourceDataRequest
This commit is contained in:
parent
2f4dc0fa6c
commit
230959cfe1
|
@ -49,6 +49,7 @@ export const FlyoutFooter = ({ cancelLayerPanel, saveLayerEdits, removeLayer,
|
|||
<EuiButtonEmpty
|
||||
onClick={cancelLayerPanel}
|
||||
flush="left"
|
||||
data-test-subj="layerPanelCancelButton"
|
||||
>
|
||||
{cancelButtonLabel}
|
||||
</EuiButtonEmpty>
|
||||
|
|
|
@ -265,14 +265,14 @@ export class AbstractLayer {
|
|||
//no-op by default
|
||||
}
|
||||
|
||||
updateDueToExtent(source, meta = {}, dataFilters = {}) {
|
||||
updateDueToExtent(source, prevMeta = {}, nextMeta = {}) {
|
||||
const extentAware = source.isFilterByMapBounds();
|
||||
if (!extentAware) {
|
||||
return NO_SOURCE_UPDATE_REQUIRED;
|
||||
}
|
||||
|
||||
const { buffer: previousBuffer } = meta;
|
||||
const { buffer: newBuffer } = dataFilters;
|
||||
const { buffer: previousBuffer } = prevMeta;
|
||||
const { buffer: newBuffer } = nextMeta;
|
||||
|
||||
if (!previousBuffer) {
|
||||
return SOURCE_UPDATE_REQUIRED;
|
||||
|
@ -296,7 +296,7 @@ export class AbstractLayer {
|
|||
]);
|
||||
const doesPreviousBufferContainNewBuffer = turfBooleanContains(previousBufferGeometry, newBufferGeometry);
|
||||
|
||||
const isTrimmed = _.get(meta, 'areResultsTrimmed', false);
|
||||
const isTrimmed = _.get(prevMeta, 'areResultsTrimmed', false);
|
||||
return doesPreviousBufferContainNewBuffer && !isTrimmed
|
||||
? NO_SOURCE_UPDATE_REQUIRED
|
||||
: SOURCE_UPDATE_REQUIRED;
|
||||
|
|
|
@ -208,7 +208,7 @@ export class CreateSourceEditor extends Component {
|
|||
<EuiSwitch
|
||||
label={
|
||||
i18n.translate('xpack.maps.source.esSearch.extentFilterLabel', {
|
||||
defaultMessage: `Dynamically filter for data in the visible map area.`
|
||||
defaultMessage: `Dynamically filter for data in the visible map area`
|
||||
})
|
||||
|
||||
}
|
||||
|
|
|
@ -56,6 +56,10 @@ export class ESSearchSource extends AbstractESSource {
|
|||
geoField: descriptor.geoField,
|
||||
filterByMapBounds: _.get(descriptor, 'filterByMapBounds', DEFAULT_FILTER_BY_MAP_BOUNDS),
|
||||
tooltipProperties: _.get(descriptor, 'tooltipProperties', []),
|
||||
useTopHits: _.get(descriptor, 'useTopHits', false),
|
||||
topHitsSplitField: descriptor.topHitsSplitField,
|
||||
topHitsTimeField: descriptor.topHitsTimeField,
|
||||
topHitsSize: _.get(descriptor, 'topHitsSize', 1),
|
||||
}, inspectorAdapters);
|
||||
}
|
||||
|
||||
|
@ -66,6 +70,10 @@ export class ESSearchSource extends AbstractESSource {
|
|||
onChange={onChange}
|
||||
filterByMapBounds={this._descriptor.filterByMapBounds}
|
||||
tooltipProperties={this._descriptor.tooltipProperties}
|
||||
useTopHits={this._descriptor.useTopHits}
|
||||
topHitsSplitField={this._descriptor.topHitsSplitField}
|
||||
topHitsTimeField={this._descriptor.topHitsTimeField}
|
||||
topHitsSize={this._descriptor.topHitsSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -127,7 +135,65 @@ export class ESSearchSource extends AbstractESSource {
|
|||
];
|
||||
}
|
||||
|
||||
async getGeoJsonWithMeta(layerName, searchFilters) {
|
||||
async _getTopHits(layerName, searchFilters) {
|
||||
const {
|
||||
topHitsSplitField,
|
||||
topHitsTimeField,
|
||||
topHitsSize,
|
||||
} = this._descriptor;
|
||||
|
||||
const searchSource = await this._makeSearchSource(searchFilters, 0);
|
||||
searchSource.setField('aggs', {
|
||||
entitySplit: {
|
||||
terms: {
|
||||
field: topHitsSplitField,
|
||||
size: 10000
|
||||
},
|
||||
aggs: {
|
||||
entityHits: {
|
||||
top_hits: {
|
||||
sort: [
|
||||
{
|
||||
[topHitsTimeField]: {
|
||||
order: 'desc'
|
||||
}
|
||||
}
|
||||
],
|
||||
_source: {
|
||||
includes: searchFilters.fieldNames
|
||||
},
|
||||
size: topHitsSize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const resp = await this._runEsQuery(layerName, searchSource, 'Elasticsearch document top hits request');
|
||||
|
||||
let hasTrimmedResults = false;
|
||||
const allHits = [];
|
||||
const entityBuckets = _.get(resp, 'aggregations.entitySplit.buckets', []);
|
||||
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
|
||||
allHits.push(...hits.reverse());
|
||||
if (total > hits.length) {
|
||||
hasTrimmedResults = true;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
hits: allHits,
|
||||
meta: {
|
||||
areResultsTrimmed: hasTrimmedResults,
|
||||
entityCount: entityBuckets.length,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async _getSearchHits(layerName, searchFilters) {
|
||||
const searchSource = await this._makeSearchSource(searchFilters, DEFAULT_ES_DOC_LIMIT);
|
||||
// Setting "fields" instead of "source: { includes: []}"
|
||||
// because SearchSource automatically adds the following by default
|
||||
|
@ -136,7 +202,26 @@ export class ESSearchSource extends AbstractESSource {
|
|||
// By setting "fields", SearchSource removes all of defaults
|
||||
searchSource.setField('fields', searchFilters.fieldNames);
|
||||
|
||||
let featureCollection;
|
||||
const resp = await this._runEsQuery(layerName, searchSource, 'Elasticsearch document request');
|
||||
|
||||
return {
|
||||
hits: resp.hits.hits,
|
||||
meta: {
|
||||
areResultsTrimmed: resp.hits.total > resp.hits.hits.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_isTopHits() {
|
||||
const { useTopHits, topHitsSplitField, topHitsTimeField } = this._descriptor;
|
||||
return !!(useTopHits && topHitsSplitField && topHitsTimeField);
|
||||
}
|
||||
|
||||
async getGeoJsonWithMeta(layerName, searchFilters) {
|
||||
const { hits, meta } = this._isTopHits()
|
||||
? await this._getTopHits(layerName, searchFilters)
|
||||
: await this._getSearchHits(layerName, searchFilters);
|
||||
|
||||
const indexPattern = await this._getIndexPattern();
|
||||
const unusedMetaFields = indexPattern.metaFields.filter(metaField => {
|
||||
return metaField !== '_id';
|
||||
|
@ -150,10 +235,10 @@ export class ESSearchSource extends AbstractESSource {
|
|||
return properties;
|
||||
};
|
||||
|
||||
const resp = await this._runEsQuery(layerName, searchSource, 'Elasticsearch document request');
|
||||
let featureCollection;
|
||||
try {
|
||||
const geoField = await this._getGeoField();
|
||||
featureCollection = hitsToGeoJson(resp.hits.hits, flattenHit, geoField.name, geoField.type);
|
||||
featureCollection = hitsToGeoJson(hits, flattenHit, geoField.name, geoField.type);
|
||||
} catch(error) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.maps.source.esSearch.convertToGeoJsonErrorMsg', {
|
||||
|
@ -165,9 +250,7 @@ export class ESSearchSource extends AbstractESSource {
|
|||
|
||||
return {
|
||||
data: featureCollection,
|
||||
meta: {
|
||||
areResultsTrimmed: resp.hits.total > resp.hits.hits.length
|
||||
}
|
||||
meta
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -252,11 +335,31 @@ export class ESSearchSource extends AbstractESSource {
|
|||
|
||||
getSourceTooltipContent(sourceDataRequest) {
|
||||
const featureCollection = sourceDataRequest ? sourceDataRequest.getData() : null;
|
||||
const meta = sourceDataRequest ? sourceDataRequest.getMeta() : {};
|
||||
const meta = sourceDataRequest ? sourceDataRequest.getMeta() : null;
|
||||
if (!featureCollection || !meta) {
|
||||
// no tooltip content needed when there is no feature collection or meta
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this._isTopHits()) {
|
||||
const entitiesFoundMsg = i18n.translate('xpack.maps.esSearch.topHitsEntitiesCountMsg', {
|
||||
defaultMessage: `Found {entityCount} entities.`,
|
||||
values: { entityCount: meta.entityCount }
|
||||
});
|
||||
if (meta.areResultsTrimmed) {
|
||||
const trimmedMsg = i18n.translate('xpack.maps.esSearch.topHitsResultsTrimmedMsg', {
|
||||
defaultMessage: `Results limited to most recent {topHitsSize} documents per entity.`,
|
||||
values: { topHitsSize: this._descriptor.topHitsSize }
|
||||
});
|
||||
return `${entitiesFoundMsg} ${trimmedMsg}`;
|
||||
}
|
||||
|
||||
return entitiesFoundMsg;
|
||||
}
|
||||
|
||||
if (meta.areResultsTrimmed) {
|
||||
return i18n.translate('xpack.maps.esSearch.resultsTrimmedMsg', {
|
||||
defaultMessage: `Results limited to first {count} matching documents.`,
|
||||
defaultMessage: `Results limited to first {count} documents.`,
|
||||
values: { count: featureCollection.features.length }
|
||||
});
|
||||
}
|
||||
|
@ -266,4 +369,13 @@ export class ESSearchSource extends AbstractESSource {
|
|||
values: { count: featureCollection.features.length }
|
||||
});
|
||||
}
|
||||
|
||||
getSyncMeta() {
|
||||
return {
|
||||
useTopHits: this._descriptor.useTopHits,
|
||||
topHitsSplitField: this._descriptor.topHitsSplitField,
|
||||
topHitsTimeField: this._descriptor.topHitsTimeField,
|
||||
topHitsSize: this._descriptor.topHitsSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,12 @@ import {
|
|||
EuiSwitch,
|
||||
} from '@elastic/eui';
|
||||
import { MultiFieldSelect } from '../../../components/multi_field_select';
|
||||
import { SingleFieldSelect } from '../../../components/single_field_select';
|
||||
|
||||
import { indexPatternService } from '../../../../kibana_services';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getTermsFields } from '../../../utils/get_terms_fields';
|
||||
import { ValidatedRange } from '../../../components/validated_range';
|
||||
|
||||
export class UpdateSourceEditor extends Component {
|
||||
|
||||
|
@ -22,10 +25,16 @@ export class UpdateSourceEditor extends Component {
|
|||
onChange: PropTypes.func.isRequired,
|
||||
filterByMapBounds: PropTypes.bool.isRequired,
|
||||
tooltipProperties: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
useTopHits: PropTypes.bool.isRequired,
|
||||
topHitsSplitField: PropTypes.string,
|
||||
topHitsTimeField: PropTypes.string,
|
||||
topHitsSize: PropTypes.number.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
fields: null,
|
||||
tooltipFields: null,
|
||||
termFields: null,
|
||||
dateFields: null,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -59,13 +68,31 @@ export class UpdateSourceEditor extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
const dateFields = indexPattern.fields.filter(field => {
|
||||
return field.type === 'date';
|
||||
});
|
||||
|
||||
this.setState({
|
||||
fields: indexPattern.fields.filter(field => {
|
||||
dateFields,
|
||||
tooltipFields: indexPattern.fields.filter(field => {
|
||||
// Do not show multi fields as tooltip field options
|
||||
// since they do not have values in _source and exist for indexing only
|
||||
return field.subType !== 'multi';
|
||||
})
|
||||
}),
|
||||
termFields: getTermsFields(indexPattern.fields),
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTooltipPropertiesSelect = (propertyNames) => {
|
||||
|
@ -76,6 +103,94 @@ export class UpdateSourceEditor extends Component {
|
|||
this.props.onChange({ propName: 'filterByMapBounds', value: event.target.checked });
|
||||
};
|
||||
|
||||
onUseTopHitsChange = event => {
|
||||
this.props.onChange({ propName: 'useTopHits', value: event.target.checked });
|
||||
};
|
||||
|
||||
onTopHitsSplitFieldChange = topHitsSplitField => {
|
||||
this.props.onChange({ propName: 'topHitsSplitField', value: topHitsSplitField });
|
||||
};
|
||||
|
||||
onTopHitsTimeFieldChange = topHitsTimeField => {
|
||||
this.props.onChange({ propName: 'topHitsTimeField', value: topHitsTimeField });
|
||||
};
|
||||
|
||||
onTopHitsSizeChange = size => {
|
||||
this.props.onChange({ propName: 'topHitsSize', value: size });
|
||||
};
|
||||
|
||||
renderTopHitsForm() {
|
||||
if (!this.props.useTopHits) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let timeFieldSelect;
|
||||
let sizeSlider;
|
||||
if (this.props.topHitsSplitField) {
|
||||
timeFieldSelect = (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.esSearch.topHitsTimeFieldLabel', {
|
||||
defaultMessage: 'Time'
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.maps.source.esSearch.topHitsTimeFieldSelectPlaceholder', {
|
||||
defaultMessage: 'Select time field'
|
||||
})}
|
||||
value={this.props.topHitsTimeField}
|
||||
onChange={this.onTopHitsTimeFieldChange}
|
||||
fields={this.state.dateFields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
||||
sizeSlider = (
|
||||
<EuiFormRow
|
||||
label={
|
||||
i18n.translate('xpack.maps.source.esSearch.topHitsSizeLabel', {
|
||||
defaultMessage: 'Documents per entity'
|
||||
})
|
||||
}
|
||||
>
|
||||
<ValidatedRange
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
value={this.props.topHitsSize}
|
||||
onChange={this.onTopHitsSizeChange}
|
||||
showLabels
|
||||
showInput
|
||||
showRange
|
||||
data-test-subj="layerPanelTopHitsSize"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.source.esSearch.topHitsSplitFieldLabel', {
|
||||
defaultMessage: 'Entity'
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.maps.source.esSearch.topHitsSplitFieldSelectPlaceholder', {
|
||||
defaultMessage: 'Select entity field'
|
||||
})}
|
||||
value={this.props.topHitsSplitField}
|
||||
onChange={this.onTopHitsSplitFieldChange}
|
||||
fields={this.state.termFields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{timeFieldSelect}
|
||||
|
||||
{sizeSlider}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
|
@ -93,7 +208,7 @@ export class UpdateSourceEditor extends Component {
|
|||
}
|
||||
value={this.props.tooltipProperties}
|
||||
onChange={this.onTooltipPropertiesSelect}
|
||||
fields={this.state.fields}
|
||||
fields={this.state.tooltipFields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
|
@ -101,7 +216,7 @@ export class UpdateSourceEditor extends Component {
|
|||
<EuiSwitch
|
||||
label={
|
||||
i18n.translate('xpack.maps.source.esSearch.extentFilterLabel', {
|
||||
defaultMessage: `Dynamically filter for data in the visible map area.`
|
||||
defaultMessage: `Dynamically filter for data in the visible map area`
|
||||
})
|
||||
|
||||
}
|
||||
|
@ -109,6 +224,20 @@ export class UpdateSourceEditor extends Component {
|
|||
onChange={this.onFilterByMapBoundsChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow>
|
||||
<EuiSwitch
|
||||
label={
|
||||
i18n.translate('xpack.maps.source.esSearch.useTopHitsLabel', {
|
||||
defaultMessage: `Show most recent documents by entity`
|
||||
})
|
||||
}
|
||||
checked={this.props.useTopHits}
|
||||
onChange={this.onUseTopHitsChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{this.renderTopHitsForm()}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -101,6 +101,10 @@ export class AbstractSource {
|
|||
return 0;
|
||||
}
|
||||
|
||||
getSyncMeta() {
|
||||
return {};
|
||||
}
|
||||
|
||||
isJoinable() {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -190,7 +190,7 @@ export class VectorLayer extends AbstractLayer {
|
|||
return this._dataRequests.find(dataRequest => dataRequest.getDataId() === sourceDataId);
|
||||
}
|
||||
|
||||
async _canSkipSourceUpdate(source, sourceDataId, searchFilters) {
|
||||
async _canSkipSourceUpdate(source, sourceDataId, nextMeta) {
|
||||
|
||||
const timeAware = await source.isTimeAware();
|
||||
const refreshTimerAware = await source.isRefreshTimerAware();
|
||||
|
@ -215,24 +215,24 @@ export class VectorLayer extends AbstractLayer {
|
|||
if (!sourceDataRequest) {
|
||||
return false;
|
||||
}
|
||||
const meta = sourceDataRequest.getMeta();
|
||||
if (!meta) {
|
||||
const prevMeta = sourceDataRequest.getMeta();
|
||||
if (!prevMeta) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let updateDueToTime = false;
|
||||
if (timeAware) {
|
||||
updateDueToTime = !_.isEqual(meta.timeFilters, searchFilters.timeFilters);
|
||||
updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters);
|
||||
}
|
||||
|
||||
let updateDueToRefreshTimer = false;
|
||||
if (refreshTimerAware && searchFilters.refreshTimerLastTriggeredAt) {
|
||||
updateDueToRefreshTimer = !_.isEqual(meta.refreshTimerLastTriggeredAt, searchFilters.refreshTimerLastTriggeredAt);
|
||||
if (refreshTimerAware && nextMeta.refreshTimerLastTriggeredAt) {
|
||||
updateDueToRefreshTimer = !_.isEqual(prevMeta.refreshTimerLastTriggeredAt, nextMeta.refreshTimerLastTriggeredAt);
|
||||
}
|
||||
|
||||
let updateDueToFields = false;
|
||||
if (isFieldAware) {
|
||||
updateDueToFields = !_.isEqual(meta.fieldNames, searchFilters.fieldNames);
|
||||
updateDueToFields = !_.isEqual(prevMeta.fieldNames, nextMeta.fieldNames);
|
||||
}
|
||||
|
||||
let updateDueToQuery = false;
|
||||
|
@ -240,24 +240,26 @@ export class VectorLayer extends AbstractLayer {
|
|||
let updateDueToLayerQuery = false;
|
||||
let updateDueToApplyGlobalQuery = false;
|
||||
if (isQueryAware) {
|
||||
updateDueToApplyGlobalQuery = meta.applyGlobalQuery !== searchFilters.applyGlobalQuery;
|
||||
updateDueToLayerQuery = !_.isEqual(meta.layerQuery, searchFilters.layerQuery);
|
||||
if (searchFilters.applyGlobalQuery) {
|
||||
updateDueToQuery = !_.isEqual(meta.query, searchFilters.query);
|
||||
updateDueToFilters = !_.isEqual(meta.filters, searchFilters.filters);
|
||||
updateDueToApplyGlobalQuery = prevMeta.applyGlobalQuery !== nextMeta.applyGlobalQuery;
|
||||
updateDueToLayerQuery = !_.isEqual(prevMeta.layerQuery, nextMeta.layerQuery);
|
||||
if (nextMeta.applyGlobalQuery) {
|
||||
updateDueToQuery = !_.isEqual(prevMeta.query, nextMeta.query);
|
||||
updateDueToFilters = !_.isEqual(prevMeta.filters, nextMeta.filters);
|
||||
} else {
|
||||
// Global filters and query are not applied to layer search request so no re-fetch required.
|
||||
// Exception is "Refresh" query.
|
||||
updateDueToQuery = isRefreshOnlyQuery(meta.query, searchFilters.query);
|
||||
updateDueToQuery = isRefreshOnlyQuery(prevMeta.query, nextMeta.query);
|
||||
}
|
||||
}
|
||||
|
||||
let updateDueToPrecisionChange = false;
|
||||
if (isGeoGridPrecisionAware) {
|
||||
updateDueToPrecisionChange = !_.isEqual(meta.geogridPrecision, searchFilters.geogridPrecision);
|
||||
updateDueToPrecisionChange = !_.isEqual(prevMeta.geogridPrecision, nextMeta.geogridPrecision);
|
||||
}
|
||||
|
||||
const updateDueToExtentChange = this.updateDueToExtent(source, meta, searchFilters);
|
||||
const updateDueToExtentChange = this.updateDueToExtent(source, prevMeta, nextMeta);
|
||||
|
||||
const updateDueToSourceMetaChange = !_.isEqual(prevMeta.sourceMeta, nextMeta.sourceMeta);
|
||||
|
||||
return !updateDueToTime
|
||||
&& !updateDueToRefreshTimer
|
||||
|
@ -267,7 +269,8 @@ export class VectorLayer extends AbstractLayer {
|
|||
&& !updateDueToFilters
|
||||
&& !updateDueToLayerQuery
|
||||
&& !updateDueToApplyGlobalQuery
|
||||
&& !updateDueToPrecisionChange;
|
||||
&& !updateDueToPrecisionChange
|
||||
&& !updateDueToSourceMetaChange;
|
||||
}
|
||||
|
||||
async _syncJoin({ join, startLoading, stopLoading, onLoadError, dataFilters }) {
|
||||
|
@ -313,7 +316,6 @@ export class VectorLayer extends AbstractLayer {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
async _syncJoins({ startLoading, stopLoading, onLoadError, dataFilters }) {
|
||||
const joinSyncs = this.getValidJoins().map(async join => {
|
||||
return this._syncJoin({ join, startLoading, stopLoading, onLoadError, dataFilters });
|
||||
|
@ -337,6 +339,7 @@ export class VectorLayer extends AbstractLayer {
|
|||
geogridPrecision: this._source.getGeoGridPrecision(dataFilters.zoom),
|
||||
layerQuery: this.getQuery(),
|
||||
applyGlobalQuery: this.getApplyGlobalQuery(),
|
||||
sourceMeta: this._source.getSyncMeta(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
12
x-pack/test/functional/apps/maps/documents_source/index.js
Normal file
12
x-pack/test/functional/apps/maps/documents_source/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 default function ({ loadTestFile }) {
|
||||
describe('documents source', function () {
|
||||
loadTestFile(require.resolve('./search_hits'));
|
||||
loadTestFile(require.resolve('./top_hits'));
|
||||
});
|
||||
}
|
|
@ -10,7 +10,7 @@ export default function ({ getPageObjects, getService }) {
|
|||
const PageObjects = getPageObjects(['maps']);
|
||||
const inspector = getService('inspector');
|
||||
|
||||
describe('elasticsearch document layer', () => {
|
||||
describe('search hits', () => {
|
||||
before(async () => {
|
||||
await PageObjects.maps.loadSavedMap('document example');
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
const VECTOR_SOURCE_ID = 'z52lq';
|
||||
|
||||
export default function ({ getPageObjects, getService }) {
|
||||
const PageObjects = getPageObjects(['maps', 'common']);
|
||||
const inspector = getService('inspector');
|
||||
const find = getService('find');
|
||||
|
||||
describe('top hits', () => {
|
||||
before(async () => {
|
||||
await PageObjects.maps.loadSavedMap('document example top hits');
|
||||
});
|
||||
|
||||
it('should not fetch any search hits', async () => {
|
||||
await inspector.open();
|
||||
await inspector.openInspectorRequestsView();
|
||||
const requestStats = await inspector.getTableData();
|
||||
const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits');
|
||||
expect(hits).to.equal('0'); // aggregation requests do not return any documents
|
||||
});
|
||||
|
||||
it('should display top hits per entity', async () => {
|
||||
const mapboxStyle = await PageObjects.maps.getMapboxStyle();
|
||||
expect(mapboxStyle.sources[VECTOR_SOURCE_ID].data.features.length).to.equal(10);
|
||||
});
|
||||
|
||||
describe('configuration', async () => {
|
||||
before(async () => {
|
||||
await PageObjects.maps.openLayerPanel('logstash');
|
||||
// Can not use testSubjects because data-test-subj is placed range input and number input
|
||||
const sizeInput = await find.byCssSelector(`input[data-test-subj="layerPanelTopHitsSize"][type='number']`);
|
||||
await sizeInput.click();
|
||||
await sizeInput.clearValue();
|
||||
await sizeInput.type('3');
|
||||
await PageObjects.maps.waitForLayersToLoad();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await PageObjects.maps.closeLayerPanel();
|
||||
});
|
||||
|
||||
it('should update top hits when configation changes', async () => {
|
||||
const mapboxStyle = await PageObjects.maps.getMapboxStyle();
|
||||
expect(mapboxStyle.sources[VECTOR_SOURCE_ID].data.features.length).to.equal(15);
|
||||
});
|
||||
});
|
||||
|
||||
describe('query', async () => {
|
||||
before(async () => {
|
||||
await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "win 8"');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await PageObjects.maps.setAndSubmitQuery('');
|
||||
});
|
||||
|
||||
it('should apply query to top hits request', async () => {
|
||||
await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "win 8"');
|
||||
const mapboxStyle = await PageObjects.maps.getMapboxStyle();
|
||||
expect(mapboxStyle.sources[VECTOR_SOURCE_ID].data.features.length).to.equal(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -30,7 +30,7 @@ export default function ({ loadTestFile, getService }) {
|
|||
|
||||
describe('', function () {
|
||||
this.tags('ciGroup7');
|
||||
loadTestFile(require.resolve('./es_search_source'));
|
||||
loadTestFile(require.resolve('./documents_source'));
|
||||
loadTestFile(require.resolve('./saved_object_management'));
|
||||
loadTestFile(require.resolve('./sample_data'));
|
||||
loadTestFile(require.resolve('./feature_controls/maps_security'));
|
||||
|
|
|
@ -151,6 +151,61 @@
|
|||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "map:68305470-87bc-11e9-a991-3b492a7c3e09",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"map": {
|
||||
"title" : "document example top hits",
|
||||
"description" : "",
|
||||
"mapStateJSON" : "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-24T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"}}",
|
||||
"layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"topHitsTimeField\":\"@timestamp\",\"useTopHits\":true,\"topHitsSplitField\":\"machine.os.raw\",\"topHitsSize\":2,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]",
|
||||
"uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}",
|
||||
"bounds" : {
|
||||
"type" : "polygon",
|
||||
"coordinates" : [
|
||||
[
|
||||
[
|
||||
-137.80011,
|
||||
46.32557
|
||||
],
|
||||
[
|
||||
-137.80011,
|
||||
17.86223
|
||||
],
|
||||
[
|
||||
-63.42171,
|
||||
17.86223
|
||||
],
|
||||
[
|
||||
-63.42171,
|
||||
46.32557
|
||||
],
|
||||
[
|
||||
-137.80011,
|
||||
46.32557
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "map",
|
||||
"references" : [
|
||||
{
|
||||
"name" : "layer_1_source_index_pattern",
|
||||
"type" : "index-pattern",
|
||||
"id" : "c698b940-e149-11e8-a35a-370a8516603a"
|
||||
}
|
||||
],
|
||||
"migrationVersion" : {
|
||||
"map" : "7.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
|
|
|
@ -265,6 +265,11 @@ export function GisPageProvider({ getService, getPageObjects }) {
|
|||
await testSubjects.click('editLayerButton');
|
||||
}
|
||||
|
||||
async closeLayerPanel() {
|
||||
await testSubjects.click('layerPanelCancelButton');
|
||||
await this.waitForLayersToLoad();
|
||||
}
|
||||
|
||||
async getLayerTOCDetails(layerName) {
|
||||
return await testSubjects.getVisibleText(`mapLayerTOCDetails${escapeLayerName(layerName)}`);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue