mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Maps] add Where clause to terms joins (#39593)
* [Maps] add Where clause to terms joins * add functional test * rename layerQuery to sourceQuery
This commit is contained in:
parent
ebd656cb23
commit
51ae0f45af
10 changed files with 194 additions and 14 deletions
|
@ -28,6 +28,7 @@ image::maps/images/global_search_bar.png[]
|
|||
You can apply a search request to individual layers by setting `Filters` in the layer details panel.
|
||||
Click the *Add filter* button to add a filter to a layer.
|
||||
|
||||
NOTE: Layer filters are not applied to *term joins*. You can apply a search request to *term joins* by setting the *where* clause in the join definition.
|
||||
|
||||
[role="screenshot"]
|
||||
image::maps/images/layer_search.png[]
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { JoinExpression } from './join_expression';
|
||||
import { MetricsExpression } from './metrics_expression';
|
||||
import { WhereExpression } from './where_expression';
|
||||
|
||||
import {
|
||||
indexPatternService,
|
||||
|
@ -29,6 +30,7 @@ export class Join extends Component {
|
|||
leftFields: null,
|
||||
leftSourceName: '',
|
||||
rightFields: undefined,
|
||||
indexPattern: undefined,
|
||||
loadError: undefined,
|
||||
prevIndexPatternId: getIndexPatternId(this.props),
|
||||
};
|
||||
|
@ -92,7 +94,8 @@ export class Join extends Component {
|
|||
}
|
||||
|
||||
this.setState({
|
||||
rightFields: indexPattern.fields
|
||||
rightFields: indexPattern.fields,
|
||||
indexPattern,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -155,6 +158,16 @@ export class Join extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
_onWhereQueryChange = (whereQuery) => {
|
||||
this.props.onChange({
|
||||
leftField: this.props.join.leftField,
|
||||
right: {
|
||||
...this.props.join.right,
|
||||
whereQuery,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
join,
|
||||
|
@ -164,12 +177,14 @@ export class Join extends Component {
|
|||
leftSourceName,
|
||||
leftFields,
|
||||
rightFields,
|
||||
indexPattern,
|
||||
} = this.state;
|
||||
const right = _.get(join, 'right', {});
|
||||
const rightSourceName = right.indexPatternTitle ? right.indexPatternTitle : right.indexPatternId;
|
||||
const isJoinConfigComplete = join.leftField && right.indexPatternId && right.term;
|
||||
|
||||
let metricsExpression;
|
||||
if (join.leftField && right.indexPatternId && right.term) {
|
||||
if (isJoinConfigComplete) {
|
||||
metricsExpression = (
|
||||
<EuiFlexItem grow={false}>
|
||||
<MetricsExpression
|
||||
|
@ -181,6 +196,19 @@ export class Join extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
let whereExpression;
|
||||
if (indexPattern && isJoinConfigComplete) {
|
||||
whereExpression = (
|
||||
<EuiFlexItem grow={false}>
|
||||
<WhereExpression
|
||||
indexPattern={indexPattern}
|
||||
whereQuery={join.right.whereQuery}
|
||||
onChange={this._onWhereQueryChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mapJoinItem">
|
||||
<EuiFlexGroup className="mapJoinItem__inner" responsive={false} wrap={true} gutterSize="s">
|
||||
|
@ -204,6 +232,8 @@ export class Join extends Component {
|
|||
|
||||
{metricsExpression}
|
||||
|
||||
{whereExpression}
|
||||
|
||||
<EuiButtonIcon
|
||||
className="mapJoinItem__delete"
|
||||
iconType="trash"
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 chrome from 'ui/chrome';
|
||||
import React, { Component } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiPopover,
|
||||
EuiExpression,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { data } from 'plugins/data/setup';
|
||||
const { QueryBar } = data.query.ui;
|
||||
import { Storage } from 'ui/storage';
|
||||
|
||||
const settings = chrome.getUiSettingsClient();
|
||||
const localStorage = new Storage(window.localStorage);
|
||||
|
||||
export class WhereExpression extends Component {
|
||||
|
||||
state = {
|
||||
isPopoverOpen: false,
|
||||
};
|
||||
|
||||
_togglePopover = () => {
|
||||
this.setState((prevState) => ({
|
||||
isPopoverOpen: !prevState.isPopoverOpen,
|
||||
}));
|
||||
}
|
||||
|
||||
_closePopover = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
}
|
||||
|
||||
_onQueryChange = ({ query }) => {
|
||||
this.props.onChange(query);
|
||||
this._closePopover();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { whereQuery, indexPattern } = this.props;
|
||||
const expressionValue = whereQuery && whereQuery.query
|
||||
? whereQuery.query
|
||||
: i18n.translate('xpack.maps.layerPanel.whereExpression.expressionValuePlaceholder', {
|
||||
defaultMessage: '-- add filter --'
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="whereClausePopover"
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this._closePopover}
|
||||
ownFocus
|
||||
withTitle
|
||||
anchorPosition="leftCenter"
|
||||
button={
|
||||
<EuiExpression
|
||||
onClick={this._togglePopover}
|
||||
description={i18n.translate('xpack.maps.layerPanel.whereExpression.expressionDescription', {
|
||||
defaultMessage: 'where'
|
||||
})}
|
||||
uppercase={false}
|
||||
value={expressionValue}
|
||||
data-test-subj="mapJoinWhereExpressionButton"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="mapFilterEditor" data-test-subj="mapJoinWhereFilterEditor">
|
||||
<QueryBar
|
||||
query={whereQuery ? whereQuery : { language: settings.get('search:queryLanguage'), query: '' }}
|
||||
onSubmit={this._onQueryChange}
|
||||
appName="maps"
|
||||
showDatePicker={false}
|
||||
indexPatterns={[indexPattern]}
|
||||
store={localStorage}
|
||||
customSubmitButton={
|
||||
<EuiButton
|
||||
fill
|
||||
data-test-subj="mapWhereFilterEditorSubmitButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.layerPanel.whereExpression.queryBarSubmitButtonLabel"
|
||||
defaultMessage="Set filter"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -140,8 +140,8 @@ export class HeatmapLayer extends AbstractLayer {
|
|||
// Exception is "Refresh" query.
|
||||
updateDueToQuery = isRefreshOnlyQuery(meta.query, searchFilters.query);
|
||||
}
|
||||
const updateDueToLayerQuery = searchFilters.layerQuery
|
||||
&& !_.isEqual(meta.layerQuery, searchFilters.layerQuery);
|
||||
const updateDueToSourceQuery = searchFilters.sourceQuery
|
||||
&& !_.isEqual(meta.sourceQuery, searchFilters.sourceQuery);
|
||||
const updateDueToApplyGlobalQuery = meta.applyGlobalQuery !== searchFilters.applyGlobalQuery;
|
||||
|
||||
const updateDueToMetricChange = !_.isEqual(meta.metric, searchFilters.metric);
|
||||
|
@ -151,7 +151,7 @@ export class HeatmapLayer extends AbstractLayer {
|
|||
&& !updateDueToExtent
|
||||
&& !updateDueToRefreshTimer
|
||||
&& !updateDueToQuery
|
||||
&& !updateDueToLayerQuery
|
||||
&& !updateDueToSourceQuery
|
||||
&& !updateDueToApplyGlobalQuery
|
||||
&& !updateDueToFilters
|
||||
&& !updateDueToMetricChange
|
||||
|
@ -165,7 +165,7 @@ export class HeatmapLayer extends AbstractLayer {
|
|||
_getSearchFilters(dataFilters) {
|
||||
return {
|
||||
...dataFilters,
|
||||
layerQuery: this.getQuery(),
|
||||
sourceQuery: this.getQuery(),
|
||||
applyGlobalQuery: this.getApplyGlobalQuery(),
|
||||
geogridPrecision: this._source.getGeoGridPrecision(dataFilters.zoom),
|
||||
metric: this._getPropKeyOfSelectedMetric()
|
||||
|
|
|
@ -82,5 +82,8 @@ export class LeftInnerJoin {
|
|||
return this._rightSource.getIndexPatternIds();
|
||||
}
|
||||
|
||||
getWhereQuery() {
|
||||
return this._rightSource.getWhereQuery();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -79,6 +79,10 @@ export class ESJoinSource extends AbstractESSource {
|
|||
return this._descriptor.term;
|
||||
}
|
||||
|
||||
getWhereQuery() {
|
||||
return this._descriptor.whereQuery;
|
||||
}
|
||||
|
||||
_formatMetricKey(metric) {
|
||||
const metricKey = metric.type !== 'count' ? `${metric.type}_of_${metric.field}` : metric.type;
|
||||
return `__kbnjoin__${metricKey}_groupby_${this._descriptor.indexPatternTitle}.${this._descriptor.term}`;
|
||||
|
|
|
@ -162,19 +162,19 @@ export class AbstractESSource extends AbstractVectorSource {
|
|||
searchSource.setField('query', searchFilters.query);
|
||||
}
|
||||
|
||||
if (searchFilters.layerQuery) {
|
||||
if (searchFilters.sourceQuery) {
|
||||
const layerSearchSource = new SearchSource();
|
||||
layerSearchSource.setField('index', indexPattern);
|
||||
layerSearchSource.setField('query', searchFilters.layerQuery);
|
||||
layerSearchSource.setField('query', searchFilters.sourceQuery);
|
||||
searchSource.setParent(layerSearchSource);
|
||||
}
|
||||
|
||||
return searchSource;
|
||||
}
|
||||
|
||||
async getBoundsForFilters({ layerQuery, query, timeFilters, filters, applyGlobalQuery }) {
|
||||
async getBoundsForFilters({ sourceQuery, query, timeFilters, filters, applyGlobalQuery }) {
|
||||
|
||||
const searchSource = await this._makeSearchSource({ layerQuery, query, timeFilters, filters, applyGlobalQuery }, 0);
|
||||
const searchSource = await this._makeSearchSource({ sourceQuery, query, timeFilters, filters, applyGlobalQuery }, 0);
|
||||
const geoField = await this._getGeoField();
|
||||
const indexPattern = await this._getIndexPattern();
|
||||
|
||||
|
|
|
@ -243,11 +243,11 @@ export class VectorLayer extends AbstractLayer {
|
|||
|
||||
let updateDueToQuery = false;
|
||||
let updateDueToFilters = false;
|
||||
let updateDueToLayerQuery = false;
|
||||
let updateDueToSourceQuery = false;
|
||||
let updateDueToApplyGlobalQuery = false;
|
||||
if (isQueryAware) {
|
||||
updateDueToApplyGlobalQuery = prevMeta.applyGlobalQuery !== nextMeta.applyGlobalQuery;
|
||||
updateDueToLayerQuery = !_.isEqual(prevMeta.layerQuery, nextMeta.layerQuery);
|
||||
updateDueToSourceQuery = !_.isEqual(prevMeta.sourceQuery, nextMeta.sourceQuery);
|
||||
if (nextMeta.applyGlobalQuery) {
|
||||
updateDueToQuery = !_.isEqual(prevMeta.query, nextMeta.query);
|
||||
updateDueToFilters = !_.isEqual(prevMeta.filters, nextMeta.filters);
|
||||
|
@ -273,7 +273,7 @@ export class VectorLayer extends AbstractLayer {
|
|||
&& !updateDueToFields
|
||||
&& !updateDueToQuery
|
||||
&& !updateDueToFilters
|
||||
&& !updateDueToLayerQuery
|
||||
&& !updateDueToSourceQuery
|
||||
&& !updateDueToApplyGlobalQuery
|
||||
&& !updateDueToPrecisionChange
|
||||
&& !updateDueToSourceMetaChange;
|
||||
|
@ -287,6 +287,7 @@ export class VectorLayer extends AbstractLayer {
|
|||
|
||||
const searchFilters = {
|
||||
...dataFilters,
|
||||
sourceQuery: joinSource.getWhereQuery(),
|
||||
applyGlobalQuery: this.getApplyGlobalQuery(),
|
||||
};
|
||||
const canSkip = await this._canSkipSourceUpdate(joinSource, sourceDataId, searchFilters);
|
||||
|
@ -343,7 +344,7 @@ export class VectorLayer extends AbstractLayer {
|
|||
...dataFilters,
|
||||
fieldNames: _.uniq(fieldNames).sort(),
|
||||
geogridPrecision: this._source.getGeoGridPrecision(dataFilters.zoom),
|
||||
layerQuery: this.getQuery(),
|
||||
sourceQuery: this.getQuery(),
|
||||
applyGlobalQuery: this.getApplyGlobalQuery(),
|
||||
sourceMeta: this._source.getSyncMeta(),
|
||||
};
|
||||
|
|
|
@ -127,6 +127,36 @@ export default function ({ getPageObjects, getService }) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('where clause', () => {
|
||||
before(async () => {
|
||||
await PageObjects.maps.setJoinWhereQuery('geo_shapes*', 'prop1 >= 11');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await PageObjects.maps.closeLayerPanel();
|
||||
});
|
||||
|
||||
it('should apply query to join request', async () => {
|
||||
await PageObjects.maps.openInspectorRequest('meta_for_geo_shapes*.shape_name');
|
||||
const requestStats = await inspector.getTableData();
|
||||
const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)');
|
||||
expect(totalHits).to.equal('2');
|
||||
const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits');
|
||||
expect(hits).to.equal('0'); // aggregation requests do not return any documents
|
||||
await inspector.close();
|
||||
});
|
||||
|
||||
it('should update dynamic data range in legend with new results', async () => {
|
||||
const layerTOCDetails = await PageObjects.maps.getLayerTOCDetails('geo_shapes*');
|
||||
const split = layerTOCDetails.trim().split('\n');
|
||||
|
||||
const min = split[0];
|
||||
expect(min).to.equal('12');
|
||||
|
||||
const max = split[2];
|
||||
expect(max).to.equal('12');
|
||||
});
|
||||
});
|
||||
|
||||
describe('inspector', () => {
|
||||
afterEach(async () => {
|
||||
|
|
|
@ -353,6 +353,19 @@ export function GisPageProvider({ getService, getPageObjects }) {
|
|||
await this.waitForLayersToLoad();
|
||||
}
|
||||
|
||||
async setJoinWhereQuery(layerName, query) {
|
||||
await this.openLayerPanel(layerName);
|
||||
await testSubjects.click('mapJoinWhereExpressionButton');
|
||||
const filterEditorContainer = await testSubjects.find('mapJoinWhereFilterEditor');
|
||||
const queryBarInFilterEditor = await testSubjects.findDescendant('queryInput', filterEditorContainer);
|
||||
await queryBarInFilterEditor.click();
|
||||
const input = await find.activeElement();
|
||||
await input.clearValue();
|
||||
await input.type(query);
|
||||
await testSubjects.click('mapWhereFilterEditorSubmitButton');
|
||||
await this.waitForLayersToLoad();
|
||||
}
|
||||
|
||||
async selectVectorSource() {
|
||||
log.debug(`Select vector source`);
|
||||
await testSubjects.click('vectorShapes');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue