[Maps] convert join display from SQL syntax to plain sentence (#28533)

* [Maps] convert join display from SQL syntax to plain sentence

* text clean up

* allow singular and plural text

* review feed back
This commit is contained in:
Nathan Reese 2019-01-18 16:57:22 -07:00 committed by GitHub
parent 10f1ad6702
commit ea6651a8f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 189 additions and 287 deletions

View file

@ -1,18 +0,0 @@
/*
* 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 React from 'react';
import { EuiExpression } from '@elastic/eui';
export function FromExpression({ leftSourceName }) {
return (
<EuiExpression
description="FROM"
value={`${leftSourceName} left`}
/>
);
}

View file

@ -1,23 +0,0 @@
/*
* 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 React from 'react';
import PropTypes from 'prop-types';
import { EuiExpression } from '@elastic/eui';
export function GroupByExpression({ term }) {
return (
<EuiExpression
description="GROUP BY"
value={`right.${term}`}
/>
);
}
GroupByExpression.propTypes = {
term: PropTypes.string.isRequired,
};

View file

@ -13,11 +13,8 @@ import {
EuiButtonIcon,
} from '@elastic/eui';
import { FromExpression } from './from_expression';
import { GroupByExpression } from './group_by_expression';
import { JoinExpression } from './join_expression';
import { OnExpression } from './on_expression';
import { SelectExpression } from './select_expression';
import { MetricsExpression } from './metrics_expression';
import {
indexPatternService,
@ -27,13 +24,6 @@ const getIndexPatternId = (props) => {
return _.get(props, 'join.right.indexPatternId');
};
/*
* SELECT <metric_agg>
* FROM <left_source (can not be changed)>
* LEFT JOIN <right_source (index-pattern)>
* ON <left_field>
* GROUP BY <right_field>
*/
export class Join extends Component {
state = {
@ -169,62 +159,40 @@ export class Join extends Component {
const right = _.get(join, 'right', {});
const rightSourceName = right.indexPatternTitle ? right.indexPatternTitle : right.indexPatternId;
let onExpression;
if (leftFields && rightFields) {
onExpression = (
let metricsExpression;
if (join.leftField && right.indexPatternId && right.term) {
metricsExpression = (
<EuiFlexItem grow={false}>
<OnExpression
leftValue={join.leftField}
leftFields={leftFields}
onLeftChange={this._onLeftFieldChange}
rightValue={right.term}
rightFields={rightFields}
onRightChange={this._onRightFieldChange}
/>
</EuiFlexItem>
);
}
let groupByExpression;
if (right.indexPatternId && right.term) {
groupByExpression = (
<EuiFlexItem grow={false}>
<GroupByExpression
rightSourceName={rightSourceName}
term={right.term}
/>
</EuiFlexItem>
);
}
return (
<EuiFlexGroup className="gisJoinItem" responsive={false} wrap={true} gutterSize="s">
<EuiFlexItem grow={false}>
<SelectExpression
<MetricsExpression
metrics={right.metrics}
rightFields={rightFields}
onChange={this._onMetricsChange}
/>
</EuiFlexItem>
);
}
<EuiFlexItem grow={false}>
<FromExpression
leftSourceName={leftSourceName}
/>
</EuiFlexItem>
return (
<EuiFlexGroup className="gisJoinItem" responsive={false} wrap={true} gutterSize="s">
<EuiFlexItem grow={false}>
<JoinExpression
indexPatternId={right.indexPatternId}
leftSourceName={leftSourceName}
leftValue={join.leftField}
leftFields={leftFields}
onLeftFieldChange={this._onLeftFieldChange}
rightSourceIndexPatternId={right.indexPatternId}
rightSourceName={rightSourceName}
onChange={this._onRightSourceChange}
onRightSourceChange={this._onRightSourceChange}
rightValue={right.term}
rightFields={rightFields}
onRightFieldChange={this._onRightFieldChange}
/>
</EuiFlexItem>
{onExpression}
{groupByExpression}
{metricsExpression}
<EuiButtonIcon
className="gisJoinItem__delete"

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import _ from 'lodash';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
@ -12,9 +13,11 @@ import {
EuiPopoverTitle,
EuiExpression,
EuiFormRow,
EuiComboBox,
} from '@elastic/eui';
import { IndexPatternSelect } from 'ui/index_patterns/components/index_pattern_select';
import { SingleFieldSelect } from '../../../../shared/components/single_field_select';
import {
indexPatternService,
@ -38,11 +41,10 @@ export class JoinExpression extends Component {
});
};
_onChange = async (indexPatternId) => {
this.setState({ isPopoverOpen: false });
_onRightSourceChange = async (indexPatternId) => {
try {
const indexPattern = await indexPatternService.get(indexPatternId);
this.props.onChange({
this.props.onRightSourceChange({
indexPatternId,
indexPatternTitle: indexPattern.title,
});
@ -51,11 +53,114 @@ export class JoinExpression extends Component {
}
}
render() {
_onLeftFieldChange = (selectedFields) => {
this.props.onLeftFieldChange(_.get(selectedFields, '[0].value.name', null));
};
_renderLeftFieldSelect() {
const {
indexPatternId,
rightSourceName
leftValue,
leftFields,
} = this.props;
if (!leftFields) {
return null;
}
const options = leftFields.map(field => {
return {
value: field,
label: field.label,
};
});
let leftFieldOption;
if (leftValue) {
leftFieldOption = options.find((option) => {
const field = option.value;
return field.name === leftValue;
});
}
const selectedOptions = leftFieldOption
? [leftFieldOption]
: [];
return (
<EuiFormRow
label="Left field"
>
<EuiComboBox
placeholder="Select field"
singleSelection={true}
isClearable={false}
options={options}
selectedOptions={selectedOptions}
onChange={this._onLeftFieldChange}
/>
</EuiFormRow>
);
}
_renderRightSourceSelect() {
if (!this.props.leftValue) {
return null;
}
return (
<EuiFormRow
label="Right source"
>
<IndexPatternSelect
placeholder="Select index pattern"
indexPatternId={this.props.rightSourceIndexPatternId}
onChange={this._onRightSourceChange}
isClearable={false}
/>
</EuiFormRow>
);
}
_renderRightFieldSelect() {
if (!this.props.rightFields || !this.props.leftValue) {
return null;
}
const filterStringOrNumberFields = (field) => {
return field.type === 'string' || field.type === 'number';
};
return (
<EuiFormRow
label="Right field"
>
<SingleFieldSelect
placeholder="Select field"
value={this.props.rightValue}
onChange={this.props.onRightFieldChange}
filterField={filterStringOrNumberFields}
fields={this.props.rightFields}
isClearable={false}
/>
</EuiFormRow>
);
}
_getExpressionValue() {
const {
leftSourceName,
leftValue,
rightSourceName,
rightValue,
} = this.props;
if (leftSourceName && leftValue && rightSourceName && rightValue) {
return `${leftSourceName}:${leftValue} with ${rightSourceName}:${rightValue}`;
}
return '-- select --';
}
render() {
const { leftSourceName } = this.props;
return (
<EuiPopover
id="joinPopover"
@ -64,27 +169,31 @@ export class JoinExpression extends Component {
ownFocus
initialFocus="body" /* avoid initialFocus on Combobox */
withTitle
anchorPosition="leftCenter"
button={
<EuiExpression
onClick={this._togglePopover}
description="JOIN"
value={rightSourceName ? `${rightSourceName} right` : '-- select --'}
description="Join"
uppercase={false}
value={this._getExpressionValue()}
/>
}
>
<div style={{ width: 300 }}>
<EuiPopoverTitle>JOIN</EuiPopoverTitle>
<EuiPopoverTitle>Join</EuiPopoverTitle>
<EuiFormRow
label="Index pattern"
helpText={`Select right source`}
label="Left source"
>
<IndexPatternSelect
placeholder="Select index pattern"
indexPatternId={indexPatternId}
onChange={this._onChange}
isClearable={false}
<EuiComboBox
selectedOptions={[{ value: leftSourceName, label: leftSourceName }]}
isDisabled
/>
</EuiFormRow>
{this._renderLeftFieldSelect()}
{this._renderRightSourceSelect()}
{this._renderRightFieldSelect()}
</div>
</EuiPopover>
);
@ -92,6 +201,24 @@ export class JoinExpression extends Component {
}
JoinExpression.propTypes = {
indexPatternId: PropTypes.string,
// Left source props (static - can not change)
leftSourceName: PropTypes.string,
// Left field props
leftValue: PropTypes.string,
leftFields: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
})),
onLeftFieldChange: PropTypes.func.isRequired,
// Right source props
rightSourceIndexPatternId: PropTypes.string,
rightSourceName: PropTypes.string,
onRightSourceChange: PropTypes.func.isRequired,
// Right field props
rightValue: PropTypes.string,
rightFields: PropTypes.object, // indexPattern.fields IndexedArray object
onRightFieldChange: PropTypes.func.isRequired,
};

View file

@ -16,7 +16,7 @@ import {
import { MetricsEditor } from '../../../../shared/components/metrics_editor';
export class SelectExpression extends Component {
export class MetricsExpression extends Component {
state = {
isPopoverOpen: false,
@ -64,30 +64,32 @@ export class SelectExpression extends Component {
})
.map(({ type, field }) => {
if (type === 'count') {
return 'count(*)';
return 'count';
}
return `${type}(right.${field})`;
return `${type} ${field}`;
});
return (
<EuiPopover
id="selectPopover"
id="metricsPopover"
isOpen={this.state.isPopoverOpen}
closePopover={this._closePopover}
ownFocus
initialFocus="body" /* avoid initialFocus on Combobox */
withTitle
anchorPosition="leftCenter"
button={
<EuiExpression
onClick={this._togglePopover}
description="SELECT"
value={metricExpressions.length > 0 ? metricExpressions.join(', ') : 'count(*)'}
description={metricExpressions.length > 1 ? 'and use metrics' : 'and use metric'}
uppercase={false}
value={metricExpressions.length > 0 ? metricExpressions.join(', ') : 'count'}
/>
}
>
<div style={{ width: 400 }}>
<EuiPopoverTitle>SELECT</EuiPopoverTitle>
<EuiPopoverTitle>Metrics</EuiPopoverTitle>
{this._renderMetricsEditor()}
</div>
</EuiPopover>
@ -95,13 +97,13 @@ export class SelectExpression extends Component {
}
}
SelectExpression.propTypes = {
MetricsExpression.propTypes = {
metrics: PropTypes.array,
rightFields: PropTypes.object, // indexPattern.fields IndexedArray object
onChange: PropTypes.func.isRequired,
};
SelectExpression.defaultProps = {
MetricsExpression.defaultProps = {
metrics: [
{ type: 'count' }
]

View file

@ -1,151 +0,0 @@
/*
* 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 React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
EuiPopover,
EuiPopoverTitle,
EuiExpression,
EuiFormRow,
EuiComboBox,
} from '@elastic/eui';
import { SingleFieldSelect } from '../../../../shared/components/single_field_select';
export class OnExpression extends Component {
state = {
isPopoverOpen: false,
};
_togglePopover = () => {
this.setState((prevState) => ({
isPopoverOpen: !prevState.isPopoverOpen,
}));
};
_closePopover = () => {
this.setState({
isPopoverOpen: false,
});
};
_onLeftFieldChange = (selectedFields) => {
const selectedField = selectedFields.length > 0 ? selectedFields[0].value : null;
this.props.onLeftChange(selectedField.name);
};
_renderLeftFieldSelect() {
const {
leftValue,
leftFields,
} = this.props;
const options = leftFields.map(field => {
return {
value: field,
label: field.label,
};
});
let leftFieldOption;
if (leftValue) {
leftFieldOption = options.find((option) => {
const field = option.value;
return field.name === leftValue;
});
}
const selectedOptions = leftFieldOption
? [leftFieldOption]
: [];
return (
<EuiComboBox
placeholder="Select field"
singleSelection={true}
isClearable={false}
options={options}
selectedOptions={selectedOptions}
onChange={this._onLeftFieldChange}
/>
);
}
_renderRightFieldSelect() {
const filterStringOrNumberFields = (field) => {
return field.type === 'string' || field.type === 'number';
};
return (
<SingleFieldSelect
placeholder="Select field"
value={this.props.rightValue}
onChange={this.props.onRightChange}
filterField={filterStringOrNumberFields}
fields={this.props.rightFields}
isClearable={false}
/>
);
}
render() {
const {
leftValue,
rightValue,
} = this.props;
return (
<EuiPopover
id="onPopover"
isOpen={this.state.isPopoverOpen}
closePopover={this._closePopover}
ownFocus
withTitle
initialFocus="body" /* avoid initialFocus on Combobox */
button={
<EuiExpression
onClick={this._togglePopover}
description="ON"
value={
leftValue && rightValue
? `left.${leftValue} = right.${rightValue}`
: '-- select --'
}
/>
}
>
<div style={{ width: 300 }}>
<EuiPopoverTitle>ON</EuiPopoverTitle>
<EuiFormRow
label="left field"
>
{this._renderLeftFieldSelect()}
</EuiFormRow>
<EuiFormRow
label="right field"
>
{this._renderRightFieldSelect()}
</EuiFormRow>
</div>
</EuiPopover>
);
}
}
OnExpression.propTypes = {
leftValue: PropTypes.string,
leftFields: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
})).isRequired,
onLeftChange: PropTypes.func.isRequired,
rightValue: PropTypes.string,
rightFields: PropTypes.object.isRequired, // indexPattern.fields IndexedArray object
onRightChange: PropTypes.func.isRequired,
};

View file

@ -69,7 +69,7 @@ export function JoinEditor({ joins, layer, onChange }) {
<div>
<EuiFlexGroup responsive={false}>
<EuiFlexItem>
<EuiTitle size="xs"><h5>Joins</h5></EuiTitle>
<EuiTitle size="xs"><h5>Term joins</h5></EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon iconType="plusInCircle" onClick={addJoin} aria-label="Add join" title="Add join" />

View file

@ -158,5 +158,6 @@ MetricsEditor.propTypes = {
MetricsEditor.defaultProps = {
metrics: [
{ type: 'count' }
]
],
allowMultipleMetrics: true
};

View file

@ -176,15 +176,13 @@ export class ESJoinSource extends ASource {
getJoinDescription(leftSourceName, leftFieldName) {
const metrics = this._getValidMetrics().map(metric => {
return metric.type !== 'count' ? `${metric.type}(${metric.field})` : 'count(*)';
return metric.type !== 'count' ? `${metric.type} ${metric.field}` : 'count';
});
const joinStatement = [];
joinStatement.push(`SELECT ${metrics.join(',')}`);
joinStatement.push(`FROM ${leftSourceName} left`);
joinStatement.push(`JOIN ${this._descriptor.indexPatternTitle} right`);
joinStatement.push(`ON left.${leftFieldName} right.${this._descriptor.term}`);
joinStatement.push(`GROUP BY right.${this._descriptor.term}`);
return `Elasticsearch terms aggregation request for join: "${joinStatement.join(' ')}"`;
joinStatement.push(`Join ${leftSourceName}:${leftFieldName} with`);
joinStatement.push(`${this._descriptor.indexPatternTitle}:${this._descriptor.term}`);
joinStatement.push(`for metrics ${metrics.join(',')}`);
return `Elasticsearch terms aggregation request for ${joinStatement.join(' ')}`;
}
_getValidMetrics() {
@ -207,11 +205,11 @@ export class ESJoinSource extends ASource {
getMetricFields() {
return this._getValidMetrics().map(metric => {
const metricKey = metric.type !== 'count' ? `${metric.type}_of_${metric.field}` : metric.type;
const metricLabel = metric.type !== 'count' ? `${metric.type}(${metric.field})` : 'count(*)';
const metricLabel = metric.type !== 'count' ? `${metric.type} ${metric.field}` : 'count';
return {
...metric,
propertyKey: `__kbnjoin__${metricKey}_groupby_${this._descriptor.indexPatternTitle}.${this._descriptor.term}`,
propertyLabel: `${metricLabel} group by ${this._descriptor.indexPatternTitle}.${this._descriptor.term}`,
propertyLabel: `${metricLabel} of ${this._descriptor.indexPatternTitle}:${this._descriptor.term}`,
};
});
}
@ -249,6 +247,4 @@ export class ESJoinSource extends ASource {
async getDisplayName() {
return `es_table ${this._descriptor.indexPatternId}`;
}
}

View file

@ -44,7 +44,7 @@ describe('getMetricFields', () => {
expect(metrics[0]).toEqual({
type: 'count',
propertyKey: '__kbnjoin__count_groupby_myIndex.myTermField',
propertyLabel: 'count(*) group by myIndex.myTermField',
propertyLabel: 'count of myIndex:myTermField',
});
});
@ -60,12 +60,12 @@ describe('getMetricFields', () => {
type: 'sum',
field: sumFieldName,
propertyKey: '__kbnjoin__sum_of_myFieldGettingSummed_groupby_myIndex.myTermField',
propertyLabel: 'sum(myFieldGettingSummed) group by myIndex.myTermField',
propertyLabel: 'sum myFieldGettingSummed of myIndex:myTermField',
});
expect(metrics[1]).toEqual({
type: 'count',
propertyKey: '__kbnjoin__count_groupby_myIndex.myTermField',
propertyLabel: 'count(*) group by myIndex.myTermField',
propertyLabel: 'count of myIndex:myTermField',
});
});
});