mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Maps] Refactor draw filter UI to be similar to create filter from feature geometry UI (#44014) (#44200)
* refactor draw UI to use same UI as create geometry filter from features * update FeatureGeometryFilterForm to use GeometryFilterForm * add jest test for GeomemtryFilterForm component * fix i18n translations * update ToolsControl jest snapshots * use __ in css class names
This commit is contained in:
parent
79be2e8141
commit
07c28b96f2
13 changed files with 764 additions and 359 deletions
282
x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap
generated
Normal file
282
x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap
generated
Normal file
|
@ -0,0 +1,282 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should not render relation select when geo field is geo_point 1`] = `
|
||||
<EuiForm>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiFormRow
|
||||
compressed={true}
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Geometry label"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed={false}
|
||||
fullWidth={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
value="My shape"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
className="mapGeometryFilter__geoFieldSuperSelectWrapper"
|
||||
compressed={true}
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Filtered field"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
className="mapGeometryFilter__geoFieldSuperSelect"
|
||||
compressed={true}
|
||||
fullWidth={true}
|
||||
hasDividers={true}
|
||||
isInvalid={false}
|
||||
itemClassName="mapGeometryFilter__geoFieldItem"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"inputDisplay": <EuiText>
|
||||
<EuiTextColor
|
||||
color="subdued"
|
||||
>
|
||||
<small>
|
||||
My index
|
||||
</small>
|
||||
</EuiTextColor>
|
||||
<br />
|
||||
my geo field
|
||||
</EuiText>,
|
||||
"value": "My index/my geo field",
|
||||
},
|
||||
]
|
||||
}
|
||||
valueOfSelected="My index/my geo field"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill={false}
|
||||
iconSide="left"
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
type="button"
|
||||
>
|
||||
Create filter
|
||||
</EuiButton>
|
||||
</EuiForm>
|
||||
`;
|
||||
|
||||
exports[`should not show "within" relation when filter geometry is not closed 1`] = `
|
||||
<EuiForm>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiFormRow
|
||||
compressed={true}
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Geometry label"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed={false}
|
||||
fullWidth={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
value="My shape"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
className="mapGeometryFilter__geoFieldSuperSelectWrapper"
|
||||
compressed={true}
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Filtered field"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
className="mapGeometryFilter__geoFieldSuperSelect"
|
||||
compressed={true}
|
||||
fullWidth={true}
|
||||
hasDividers={true}
|
||||
isInvalid={false}
|
||||
itemClassName="mapGeometryFilter__geoFieldItem"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"inputDisplay": <EuiText>
|
||||
<EuiTextColor
|
||||
color="subdued"
|
||||
>
|
||||
<small>
|
||||
My index
|
||||
</small>
|
||||
</EuiTextColor>
|
||||
<br />
|
||||
my geo field
|
||||
</EuiText>,
|
||||
"value": "My index/my geo field",
|
||||
},
|
||||
]
|
||||
}
|
||||
valueOfSelected="My index/my geo field"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
compressed={true}
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Spatial relation"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiSelect
|
||||
compressed={false}
|
||||
fullWidth={false}
|
||||
hasNoInitialSelection={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"text": "intersects",
|
||||
"value": "INTERSECTS",
|
||||
},
|
||||
Object {
|
||||
"text": "disjoint",
|
||||
"value": "DISJOINT",
|
||||
},
|
||||
]
|
||||
}
|
||||
value="INTERSECTS"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill={false}
|
||||
iconSide="left"
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
type="button"
|
||||
>
|
||||
Create filter
|
||||
</EuiButton>
|
||||
</EuiForm>
|
||||
`;
|
||||
|
||||
exports[`should render relation select when geo field is geo_shape 1`] = `
|
||||
<EuiForm>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiFormRow
|
||||
compressed={true}
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Geometry label"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed={false}
|
||||
fullWidth={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
value="My shape"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
className="mapGeometryFilter__geoFieldSuperSelectWrapper"
|
||||
compressed={true}
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Filtered field"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
className="mapGeometryFilter__geoFieldSuperSelect"
|
||||
compressed={true}
|
||||
fullWidth={true}
|
||||
hasDividers={true}
|
||||
isInvalid={false}
|
||||
itemClassName="mapGeometryFilter__geoFieldItem"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"inputDisplay": <EuiText>
|
||||
<EuiTextColor
|
||||
color="subdued"
|
||||
>
|
||||
<small>
|
||||
My index
|
||||
</small>
|
||||
</EuiTextColor>
|
||||
<br />
|
||||
my geo field
|
||||
</EuiText>,
|
||||
"value": "My index/my geo field",
|
||||
},
|
||||
]
|
||||
}
|
||||
valueOfSelected="My index/my geo field"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
compressed={true}
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Spatial relation"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiSelect
|
||||
compressed={false}
|
||||
fullWidth={false}
|
||||
hasNoInitialSelection={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"text": "intersects",
|
||||
"value": "INTERSECTS",
|
||||
},
|
||||
Object {
|
||||
"text": "disjoint",
|
||||
"value": "DISJOINT",
|
||||
},
|
||||
Object {
|
||||
"text": "within",
|
||||
"value": "WITHIN",
|
||||
},
|
||||
]
|
||||
}
|
||||
value="INTERSECTS"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill={false}
|
||||
iconSide="left"
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
type="button"
|
||||
>
|
||||
Create filter
|
||||
</EuiButton>
|
||||
</EuiForm>
|
||||
`;
|
|
@ -1,3 +1,15 @@
|
|||
.mapMetricEditorPanel {
|
||||
margin-bottom: $euiSizeS;
|
||||
}
|
||||
|
||||
.mapGeometryFilter__geoFieldSuperSelect {
|
||||
height: $euiSizeXL * 2;
|
||||
}
|
||||
|
||||
.mapGeometryFilter__geoFieldSuperSelectWrapper {
|
||||
height: $euiSizeXL * 3;
|
||||
}
|
||||
|
||||
.mapGeometryFilter__geoFieldItem {
|
||||
padding: $euiSizeXS;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiSuperSelect,
|
||||
EuiTextColor,
|
||||
EuiText,
|
||||
EuiFieldText,
|
||||
EuiButton,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
ES_GEO_FIELD_TYPE,
|
||||
ES_SPATIAL_RELATIONS,
|
||||
} from '../../common/constants';
|
||||
import { getEsSpatialRelationLabel } from '../../common/i18n_getters';
|
||||
|
||||
const GEO_FIELD_VALUE_DELIMITER = '/'; // `/` is not allowed in index pattern name so should not have collisions
|
||||
|
||||
function createIndexGeoFieldName({ indexPatternTitle, geoFieldName }) {
|
||||
return `${indexPatternTitle}${GEO_FIELD_VALUE_DELIMITER}${geoFieldName}`;
|
||||
}
|
||||
|
||||
function splitIndexGeoFieldName(value) {
|
||||
const split = value.split(GEO_FIELD_VALUE_DELIMITER);
|
||||
return {
|
||||
indexPatternTitle: split[0],
|
||||
geoFieldName: split[1]
|
||||
};
|
||||
}
|
||||
|
||||
export class GeometryFilterForm extends Component {
|
||||
|
||||
static propTypes = {
|
||||
buttonLabel: PropTypes.string.isRequired,
|
||||
geoFields: PropTypes.array.isRequired,
|
||||
intitialGeometryLabel: PropTypes.string.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
isFilterGeometryClosed: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isFilterGeometryClosed: true,
|
||||
};
|
||||
|
||||
state = {
|
||||
geoFieldTag: this.props.geoFields.length ? createIndexGeoFieldName(this.props.geoFields[0]) : '',
|
||||
geometryLabel: this.props.intitialGeometryLabel,
|
||||
relation: ES_SPATIAL_RELATIONS.INTERSECTS,
|
||||
};
|
||||
|
||||
_getSelectedGeoField = () => {
|
||||
if (!this.state.geoFieldTag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
indexPatternTitle,
|
||||
geoFieldName
|
||||
} = splitIndexGeoFieldName(this.state.geoFieldTag);
|
||||
|
||||
return this.props.geoFields.find(option => {
|
||||
return option.indexPatternTitle === indexPatternTitle
|
||||
&& option.geoFieldName === geoFieldName;
|
||||
});
|
||||
}
|
||||
|
||||
_onGeoFieldChange = selectedValue => {
|
||||
this.setState({ geoFieldTag: selectedValue });
|
||||
}
|
||||
|
||||
_onGeometryLabelChange = e => {
|
||||
this.setState({
|
||||
geometryLabel: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
_onRelationChange = e => {
|
||||
this.setState({
|
||||
relation: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
_onSubmit = () => {
|
||||
const geoField = this._getSelectedGeoField();
|
||||
this.props.onSubmit({
|
||||
geometryLabel: this.state.geometryLabel,
|
||||
indexPatternId: geoField.indexPatternId,
|
||||
geoFieldName: geoField.geoFieldName,
|
||||
geoFieldType: geoField.geoFieldType,
|
||||
relation: this.state.relation,
|
||||
});
|
||||
}
|
||||
|
||||
_renderRelationInput() {
|
||||
if (!this.state.geoFieldTag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { geoFieldType } = this._getSelectedGeoField();
|
||||
|
||||
// relationship only used when filtering geo_shape fields
|
||||
if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const spatialRelations = this.props.isFilterGeometryClosed
|
||||
? Object.values(ES_SPATIAL_RELATIONS)
|
||||
: Object.values(ES_SPATIAL_RELATIONS).filter(relation => {
|
||||
// can not filter by within relation when filtering geometry is not closed
|
||||
return relation !== ES_SPATIAL_RELATIONS.WITHIN;
|
||||
});
|
||||
const options = spatialRelations
|
||||
.map(relation => {
|
||||
return {
|
||||
value: relation,
|
||||
text: getEsSpatialRelationLabel(relation)
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.geometryFilterForm.relationLabel', {
|
||||
defaultMessage: 'Spatial relation'
|
||||
})}
|
||||
compressed
|
||||
>
|
||||
<EuiSelect
|
||||
options={options}
|
||||
value={this.state.relation}
|
||||
onChange={this._onRelationChange}
|
||||
/>
|
||||
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = this.props.geoFields.map(({ indexPatternTitle, geoFieldName }) => {
|
||||
return {
|
||||
inputDisplay: (
|
||||
<EuiText>
|
||||
<EuiTextColor color="subdued">
|
||||
<small>{indexPatternTitle}</small>
|
||||
</EuiTextColor>
|
||||
<br />
|
||||
{geoFieldName}
|
||||
</EuiText>
|
||||
),
|
||||
value: createIndexGeoFieldName({ indexPatternTitle, geoFieldName })
|
||||
};
|
||||
});
|
||||
return (
|
||||
<EuiForm className={this.props.className}>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.geometryFilterForm.geometryLabelLabel', {
|
||||
defaultMessage: 'Geometry label'
|
||||
})}
|
||||
compressed
|
||||
>
|
||||
<EuiFieldText
|
||||
value={this.state.geometryLabel}
|
||||
onChange={this._onGeometryLabelChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
className="mapGeometryFilter__geoFieldSuperSelectWrapper"
|
||||
label={i18n.translate('xpack.maps.geometryFilterForm.geoFieldLabel', {
|
||||
defaultMessage: 'Filtered field'
|
||||
})}
|
||||
compressed
|
||||
>
|
||||
<EuiSuperSelect
|
||||
className="mapGeometryFilter__geoFieldSuperSelect"
|
||||
options={options}
|
||||
valueOfSelected={this.state.geoFieldTag}
|
||||
onChange={this._onGeoFieldChange}
|
||||
hasDividers={true}
|
||||
fullWidth={true}
|
||||
compressed={true}
|
||||
itemClassName="mapGeometryFilter__geoFieldItem"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{this._renderRelationInput()}
|
||||
|
||||
<EuiButton
|
||||
size="s"
|
||||
onClick={this._onSubmit}
|
||||
isDisabled={!this.state.geometryLabel || !this.state.geoFieldTag}
|
||||
>
|
||||
{this.props.buttonLabel}
|
||||
</EuiButton>
|
||||
</EuiForm>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { GeometryFilterForm } from './geometry_filter_form';
|
||||
|
||||
const defaultProps = {
|
||||
buttonLabel: 'Create filter',
|
||||
intitialGeometryLabel: 'My shape',
|
||||
onSubmit: () => {},
|
||||
};
|
||||
|
||||
test('should not render relation select when geo field is geo_point', async () => {
|
||||
const component = shallow(
|
||||
<GeometryFilterForm
|
||||
{...defaultProps}
|
||||
geoFields={[
|
||||
{
|
||||
geoFieldName: 'my geo field',
|
||||
geoFieldType: 'geo_point',
|
||||
indexPatternTitle: 'My index',
|
||||
indexPatternId: 1
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render relation select when geo field is geo_shape', async () => {
|
||||
const component = shallow(
|
||||
<GeometryFilterForm
|
||||
{...defaultProps}
|
||||
geoFields={[
|
||||
{
|
||||
geoFieldName: 'my geo field',
|
||||
geoFieldType: 'geo_shape',
|
||||
indexPatternTitle: 'My index',
|
||||
indexPatternId: 1
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should not show "within" relation when filter geometry is not closed', async () => {
|
||||
const component = shallow(
|
||||
<GeometryFilterForm
|
||||
{...defaultProps}
|
||||
geoFields={[
|
||||
{
|
||||
geoFieldName: 'my geo field',
|
||||
geoFieldType: 'geo_shape',
|
||||
indexPatternTitle: 'My index',
|
||||
indexPatternId: 1
|
||||
}
|
||||
]}
|
||||
isFilterGeometryClosed={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
|
@ -21,15 +21,3 @@
|
|||
.mapFeatureTooltip__propertyValue {
|
||||
max-width: $euiSizeXL * 5;
|
||||
}
|
||||
|
||||
.mapFeatureTooltip__geoFieldItem {
|
||||
padding: $euiSizeXS;
|
||||
}
|
||||
|
||||
.mapFeatureTooltip_geoFieldSuperSelect {
|
||||
height: $euiSizeXL * 2;
|
||||
}
|
||||
|
||||
.mapFeatureTooltip_geoFieldSuperSelectWrapper {
|
||||
height: $euiSizeXL * 3;
|
||||
}
|
||||
|
|
|
@ -5,50 +5,17 @@
|
|||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import {
|
||||
EuiIcon,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiSuperSelect,
|
||||
EuiTextColor,
|
||||
EuiText,
|
||||
EuiFieldText,
|
||||
EuiButton,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { createSpatialFilterWithGeometry } from '../../elasticsearch_geo_utils';
|
||||
import {
|
||||
ES_GEO_FIELD_TYPE,
|
||||
ES_SPATIAL_RELATIONS,
|
||||
GEO_JSON_TYPE,
|
||||
} from '../../../common/constants';
|
||||
import { getEsSpatialRelationLabel } from '../../../common/i18n_getters';
|
||||
|
||||
const GEO_FIELD_VALUE_DELIMITER = '/'; // `/` is not allowed in index pattern name so should not have collisions
|
||||
|
||||
function createIndexGeoFieldName({ indexPatternTitle, geoFieldName }) {
|
||||
return `${indexPatternTitle}${GEO_FIELD_VALUE_DELIMITER}${geoFieldName}`;
|
||||
}
|
||||
|
||||
function splitIndexGeoFieldName(value) {
|
||||
const split = value.split(GEO_FIELD_VALUE_DELIMITER);
|
||||
return {
|
||||
indexPatternTitle: split[0],
|
||||
geoFieldName: split[1]
|
||||
};
|
||||
}
|
||||
import { GeometryFilterForm } from '../../components/geometry_filter_form';
|
||||
|
||||
export class FeatureGeometryFilterForm extends Component {
|
||||
|
||||
state = {
|
||||
geoFieldTag: createIndexGeoFieldName(this.props.geoFields[0]),
|
||||
geometryLabel: this.props.feature.geometry.type.toLowerCase(),
|
||||
relation: ES_SPATIAL_RELATIONS.INTERSECTS,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.reevaluateTooltipPosition();
|
||||
}
|
||||
|
@ -57,47 +24,14 @@ export class FeatureGeometryFilterForm extends Component {
|
|||
this.props.reevaluateTooltipPosition();
|
||||
}
|
||||
|
||||
_getSelectedGeoField = () => {
|
||||
if (!this.state.geoFieldTag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
indexPatternTitle,
|
||||
geoFieldName
|
||||
} = splitIndexGeoFieldName(this.state.geoFieldTag);
|
||||
|
||||
return this.props.geoFields.find(option => {
|
||||
return option.indexPatternTitle === indexPatternTitle
|
||||
&& option.geoFieldName === geoFieldName;
|
||||
});
|
||||
}
|
||||
|
||||
_onGeoFieldChange = selectedValue => {
|
||||
this.setState({ geoFieldTag: selectedValue });
|
||||
}
|
||||
|
||||
_onGeometryLabelChange = e => {
|
||||
this.setState({
|
||||
geometryLabel: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
_onRelationChange = e => {
|
||||
this.setState({
|
||||
relation: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
_createFilter = () => {
|
||||
const geoField = this._getSelectedGeoField();
|
||||
_createFilter = ({ geometryLabel, indexPatternId, geoFieldName, geoFieldType, relation }) => {
|
||||
const filter = createSpatialFilterWithGeometry({
|
||||
geometry: this.props.feature.geometry,
|
||||
geometryLabel: this.state.geometryLabel,
|
||||
indexPatternId: geoField.indexPatternId,
|
||||
geoFieldName: geoField.geoFieldName,
|
||||
geoFieldType: geoField.geoFieldType,
|
||||
relation: this.state.relation,
|
||||
geometryLabel,
|
||||
indexPatternId,
|
||||
geoFieldName,
|
||||
geoFieldType,
|
||||
relation,
|
||||
});
|
||||
this.props.addFilters([filter]);
|
||||
this.props.onClose();
|
||||
|
@ -119,8 +53,8 @@ export class FeatureGeometryFilterForm extends Component {
|
|||
|
||||
<span className="euiContextMenu__text">
|
||||
<FormattedMessage
|
||||
id="xpack.maps.tooltip.geometryFilterForm.viewProperties"
|
||||
defaultMessage="View properties"
|
||||
id="xpack.maps.tooltip.showGeometryFilterViewLinkLabel"
|
||||
defaultMessage="Filter by geometry"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
|
@ -128,115 +62,18 @@ export class FeatureGeometryFilterForm extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
_renderRelationInput() {
|
||||
if (!this.state.geoFieldTag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { geoFieldType } = this._getSelectedGeoField();
|
||||
|
||||
// relationship only used when filtering geo_shape fields
|
||||
if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = Object.values(ES_SPATIAL_RELATIONS)
|
||||
.filter(relation => {
|
||||
// line geometries can not filter by within relation since there is no closed shape
|
||||
if (this.props.feature.geometry.type === GEO_JSON_TYPE.LINE_STRING
|
||||
|| this.props.feature.geometry.type === GEO_JSON_TYPE.MULTI_LINE_STRING) {
|
||||
return relation !== ES_SPATIAL_RELATIONS.WITHIN;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map(relation => {
|
||||
return {
|
||||
value: relation,
|
||||
text: getEsSpatialRelationLabel(relation)
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.tooltip.geometryFilterForm.relationLabel', {
|
||||
defaultMessage: 'Spatial relation'
|
||||
})}
|
||||
compressed
|
||||
>
|
||||
<EuiSelect
|
||||
options={options}
|
||||
value={this.state.relation}
|
||||
onChange={this._onRelationChange}
|
||||
/>
|
||||
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
_renderForm() {
|
||||
const options = this.props.geoFields.map(({ indexPatternTitle, geoFieldName }) => {
|
||||
return {
|
||||
inputDisplay: (
|
||||
<EuiText>
|
||||
<EuiTextColor color="subdued">
|
||||
<small>{indexPatternTitle}</small>
|
||||
</EuiTextColor>
|
||||
<br />
|
||||
{geoFieldName}
|
||||
</EuiText>
|
||||
),
|
||||
value: createIndexGeoFieldName({ indexPatternTitle, geoFieldName })
|
||||
};
|
||||
});
|
||||
return (
|
||||
<EuiForm>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.tooltip.geometryFilterForm.geometryLabelLabel', {
|
||||
defaultMessage: 'Geometry label'
|
||||
})}
|
||||
compressed
|
||||
>
|
||||
<EuiFieldText
|
||||
value={this.state.geometryLabel}
|
||||
onChange={this._onGeometryLabelChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
className="mapFeatureTooltip_geoFieldSuperSelectWrapper"
|
||||
label={i18n.translate('xpack.maps.tooltip.geometryFilterForm.geoFieldLabel', {
|
||||
defaultMessage: 'Filtered field'
|
||||
})}
|
||||
compressed
|
||||
>
|
||||
<EuiSuperSelect
|
||||
className="mapFeatureTooltip_geoFieldSuperSelect"
|
||||
options={options}
|
||||
valueOfSelected={this.state.geoFieldTag}
|
||||
onChange={this._onGeoFieldChange}
|
||||
hasDividers={true}
|
||||
fullWidth={true}
|
||||
compressed={true}
|
||||
itemClassName="mapFeatureTooltip__geoFieldItem"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{this._renderRelationInput()}
|
||||
|
||||
<EuiButton
|
||||
size="s"
|
||||
onClick={this._createFilter}
|
||||
isDisabled={!this.state.geometryLabel || !this.state.geoFieldTag}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.tooltip.geometryFilterForm.createFilterButtonLabel"
|
||||
defaultMessage="Create filter"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiForm>
|
||||
<GeometryFilterForm
|
||||
buttonLabel={i18n.translate('xpack.maps.tooltip.geometryFilterForm.createFilterButtonLabel', {
|
||||
defaultMessage: 'Create filter'
|
||||
})}
|
||||
geoFields={this.props.geoFields}
|
||||
intitialGeometryLabel={this.props.feature.geometry.type.toLowerCase()}
|
||||
onSubmit={this._createFilter}
|
||||
isFilterGeometryClosed={this.props.feature.geometry.type !== GEO_JSON_TYPE.LINE_STRING
|
||||
&& this.props.feature.geometry.type !== GEO_JSON_TYPE.MULTI_LINE_STRING}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -248,6 +85,4 @@ export class FeatureGeometryFilterForm extends Component {
|
|||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,6 @@ import chrome from 'ui/chrome';
|
|||
import { spritesheet } from '@elastic/maki';
|
||||
import sprites1 from '@elastic/maki/dist/sprite@1.png';
|
||||
import sprites2 from '@elastic/maki/dist/sprite@2.png';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DrawTooltip } from './draw_tooltip';
|
||||
|
||||
const mbDrawModes = MapboxDraw.modes;
|
||||
|
@ -104,20 +103,16 @@ export class MBMapContainer extends React.Component {
|
|||
indexPatternId: this.props.drawState.indexPatternId,
|
||||
geoFieldName: this.props.drawState.geoFieldName,
|
||||
geoFieldType: this.props.drawState.geoFieldType,
|
||||
geometryLabel: this.props.drawState.geometryLabel,
|
||||
relation: this.props.drawState.relation,
|
||||
};
|
||||
const filter = isBoundingBox
|
||||
? createSpatialFilterWithBoundingBox({
|
||||
...options,
|
||||
geometryLabel: i18n.translate('xpack.maps.drawControl.defaultEnvelopeLabel', {
|
||||
defaultMessage: 'extent'
|
||||
}),
|
||||
geometry: getBoundingBoxGeometry(geometry)
|
||||
})
|
||||
: createSpatialFilterWithGeometry({
|
||||
...options,
|
||||
geometryLabel: i18n.translate('xpack.maps.drawControl.defaultShapeLabel', {
|
||||
defaultMessage: 'shape'
|
||||
}),
|
||||
geometry
|
||||
});
|
||||
this.props.addFilters([filter]);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import './tools_control/index';
|
||||
|
||||
.mapToolbarOverlay {
|
||||
position: absolute;
|
||||
top: ($euiSizeM + $euiSizeS) + ($euiSizeXL * 2); // Position and height of mapbox controls plus margin
|
||||
|
|
|
@ -37,17 +37,57 @@ exports[`Should render cancel button when drawing 1`] = `
|
|||
"items": Array [
|
||||
Object {
|
||||
"name": "Draw shape to filter data",
|
||||
"onClick": [Function],
|
||||
"panel": undefined,
|
||||
"panel": 1,
|
||||
},
|
||||
Object {
|
||||
"name": "Draw bounds to filter data",
|
||||
"onClick": [Function],
|
||||
"panel": undefined,
|
||||
"panel": 2,
|
||||
},
|
||||
],
|
||||
"title": "Tools",
|
||||
},
|
||||
Object {
|
||||
"content": <GeometryFilterForm
|
||||
buttonLabel="Draw shape"
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
geoFields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "location",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": "1",
|
||||
"indexPatternTitle": "my_index",
|
||||
},
|
||||
]
|
||||
}
|
||||
intitialGeometryLabel="shape"
|
||||
isFilterGeometryClosed={true}
|
||||
onSubmit={[Function]}
|
||||
/>,
|
||||
"id": 1,
|
||||
"title": "Draw shape to filter data",
|
||||
},
|
||||
Object {
|
||||
"content": <GeometryFilterForm
|
||||
buttonLabel="Draw bounds"
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
geoFields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "location",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": "1",
|
||||
"indexPatternTitle": "my_index",
|
||||
},
|
||||
]
|
||||
}
|
||||
intitialGeometryLabel="bounds"
|
||||
isFilterGeometryClosed={true}
|
||||
onSubmit={[Function]}
|
||||
/>,
|
||||
"id": 2,
|
||||
"title": "Draw bounds to filter data",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
|
@ -105,17 +145,57 @@ exports[`renders 1`] = `
|
|||
"items": Array [
|
||||
Object {
|
||||
"name": "Draw shape to filter data",
|
||||
"onClick": [Function],
|
||||
"panel": undefined,
|
||||
"panel": 1,
|
||||
},
|
||||
Object {
|
||||
"name": "Draw bounds to filter data",
|
||||
"onClick": [Function],
|
||||
"panel": undefined,
|
||||
"panel": 2,
|
||||
},
|
||||
],
|
||||
"title": "Tools",
|
||||
},
|
||||
Object {
|
||||
"content": <GeometryFilterForm
|
||||
buttonLabel="Draw shape"
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
geoFields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "location",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": "1",
|
||||
"indexPatternTitle": "my_index",
|
||||
},
|
||||
]
|
||||
}
|
||||
intitialGeometryLabel="shape"
|
||||
isFilterGeometryClosed={true}
|
||||
onSubmit={[Function]}
|
||||
/>,
|
||||
"id": 1,
|
||||
"title": "Draw shape to filter data",
|
||||
},
|
||||
Object {
|
||||
"content": <GeometryFilterForm
|
||||
buttonLabel="Draw bounds"
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
geoFields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "location",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": "1",
|
||||
"indexPatternTitle": "my_index",
|
||||
},
|
||||
]
|
||||
}
|
||||
intitialGeometryLabel="bounds"
|
||||
isFilterGeometryClosed={true}
|
||||
onSubmit={[Function]}
|
||||
/>,
|
||||
"id": 2,
|
||||
"title": "Draw bounds to filter data",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.mapDrawControl__geometryFilterForm {
|
||||
padding: $euiSizeS;
|
||||
}
|
|
@ -4,14 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiPopover,
|
||||
EuiContextMenu,
|
||||
EuiSelectable,
|
||||
EuiHighlight,
|
||||
EuiTextColor,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButton,
|
||||
|
@ -19,152 +16,101 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { DRAW_TYPE } from '../../../../common/constants';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { GeometryFilterForm } from '../../../components/geometry_filter_form';
|
||||
|
||||
const RESET_STATE = {
|
||||
isPopoverOpen: false,
|
||||
drawType: null
|
||||
};
|
||||
const DRAW_SHAPE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', {
|
||||
defaultMessage: 'Draw shape to filter data',
|
||||
});
|
||||
|
||||
const DRAW_BOUNDS_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawBoundsLabel', {
|
||||
defaultMessage: 'Draw bounds to filter data',
|
||||
});
|
||||
|
||||
export class ToolsControl extends Component {
|
||||
|
||||
state = {
|
||||
...RESET_STATE
|
||||
isPopoverOpen: false
|
||||
};
|
||||
|
||||
_togglePopover = () => {
|
||||
this.setState(prevState => ({
|
||||
...RESET_STATE,
|
||||
isPopoverOpen: !prevState.isPopoverOpen,
|
||||
}));
|
||||
};
|
||||
|
||||
_closePopover = () => {
|
||||
this.setState(RESET_STATE);
|
||||
this.setState({ isPopoverOpen: false });
|
||||
};
|
||||
|
||||
_onIndexPatternSelection = (options) => {
|
||||
const selection = options.find((option) => option.checked);
|
||||
this._initiateDraw(
|
||||
this.state.drawType,
|
||||
selection.value
|
||||
);
|
||||
};
|
||||
|
||||
_initiateDraw = (drawType, indexContext) => {
|
||||
_initiateShapeDraw = (options) => {
|
||||
this.props.initiateDraw({
|
||||
drawType,
|
||||
...indexContext
|
||||
drawType: DRAW_TYPE.POLYGON,
|
||||
...options
|
||||
});
|
||||
this._closePopover();
|
||||
}
|
||||
|
||||
_initiateBoundsDraw = (options) => {
|
||||
this.props.initiateDraw({
|
||||
drawType: DRAW_TYPE.BOUNDS,
|
||||
...options
|
||||
});
|
||||
this._closePopover();
|
||||
};
|
||||
|
||||
_selectPolygonDrawType = () => {
|
||||
this.setState({ drawType: DRAW_TYPE.POLYGON });
|
||||
}
|
||||
|
||||
_selectBoundsDrawType = () => {
|
||||
this.setState({ drawType: DRAW_TYPE.BOUNDS });
|
||||
}
|
||||
|
||||
_getDrawPanels() {
|
||||
|
||||
const needsIndexPatternSelectionPanel = this.props.geoFields.length > 1;
|
||||
|
||||
const drawPolygonAction = {
|
||||
name: i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', {
|
||||
defaultMessage: 'Draw shape to filter data',
|
||||
}),
|
||||
onClick: needsIndexPatternSelectionPanel
|
||||
? this._selectPolygonDrawType
|
||||
: () => {
|
||||
this._initiateDraw(DRAW_TYPE.POLYGON, this.props.geoFields[0]);
|
||||
},
|
||||
panel: needsIndexPatternSelectionPanel
|
||||
? this._getIndexPatternSelectionPanel(1)
|
||||
: undefined
|
||||
};
|
||||
|
||||
const drawBoundsAction = {
|
||||
name: i18n.translate('xpack.maps.toolbarOverlay.drawBoundsLabel', {
|
||||
defaultMessage: 'Draw bounds to filter data',
|
||||
}),
|
||||
onClick: needsIndexPatternSelectionPanel
|
||||
? this._selectBoundsDrawType
|
||||
: () => {
|
||||
this._initiateDraw(DRAW_TYPE.BOUNDS, this.props.geoFields[0]);
|
||||
},
|
||||
panel: needsIndexPatternSelectionPanel
|
||||
? this._getIndexPatternSelectionPanel(2)
|
||||
: undefined
|
||||
};
|
||||
|
||||
return flattenPanelTree({
|
||||
id: 0,
|
||||
title: i18n.translate('xpack.maps.toolbarOverlay.tools.toolbarTitle', {
|
||||
defaultMessage: 'Tools',
|
||||
}),
|
||||
items: [drawPolygonAction, drawBoundsAction]
|
||||
});
|
||||
}
|
||||
|
||||
_getIndexPatternSelectionPanel(id) {
|
||||
const options = this.props.geoFields.map((geoField) => {
|
||||
return {
|
||||
label: `${geoField.indexPatternTitle} : ${geoField.geoFieldName}`,
|
||||
value: geoField
|
||||
};
|
||||
});
|
||||
|
||||
const renderGeoField = (option, searchValue) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTextColor color="subdued">
|
||||
<small>
|
||||
<EuiHighlight search={searchValue}>{option.value.indexPatternTitle}</EuiHighlight>
|
||||
</small>
|
||||
</EuiTextColor>
|
||||
<br />
|
||||
<EuiHighlight search={searchValue}>
|
||||
{option.value.geoFieldName}
|
||||
</EuiHighlight>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const indexPatternSelection = (
|
||||
<EuiSelectable
|
||||
searchable
|
||||
searchProps={{
|
||||
placeholder: i18n.translate('xpack.maps.toolbarOverlay.indexPattern.filterListTitle', {
|
||||
defaultMessage: 'Filter list',
|
||||
}),
|
||||
compressed: true,
|
||||
}}
|
||||
options={options}
|
||||
|
||||
onChange={this._onIndexPatternSelection}
|
||||
renderOption={renderGeoField}
|
||||
listProps={{
|
||||
rowHeight: 50,
|
||||
showIcons: false,
|
||||
}}
|
||||
>
|
||||
{(list, search) => (
|
||||
<div>
|
||||
{search}
|
||||
{list}
|
||||
</div>
|
||||
)}
|
||||
</EuiSelectable>
|
||||
);
|
||||
|
||||
return {
|
||||
id: id,
|
||||
title: i18n.translate('xpack.maps.toolbarOverlay.geofield.toolbarTitle', {
|
||||
defaultMessage: 'Select geo field',
|
||||
}),
|
||||
content: indexPatternSelection
|
||||
};
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
title: i18n.translate('xpack.maps.toolbarOverlay.tools.toolbarTitle', {
|
||||
defaultMessage: 'Tools',
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
name: DRAW_SHAPE_LABEL,
|
||||
panel: 1
|
||||
},
|
||||
{
|
||||
name: DRAW_BOUNDS_LABEL,
|
||||
panel: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: DRAW_SHAPE_LABEL,
|
||||
content: (
|
||||
<GeometryFilterForm
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
buttonLabel={i18n.translate('xpack.maps.toolbarOverlay.drawShape.onSubmitButtonLabel', {
|
||||
defaultMessage: 'Draw shape',
|
||||
})}
|
||||
geoFields={this.props.geoFields}
|
||||
intitialGeometryLabel={i18n.translate('xpack.maps.toolbarOverlay.drawShape.initialGeometryLabel', {
|
||||
defaultMessage: 'shape',
|
||||
})}
|
||||
onSubmit={this._initiateShapeDraw}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: DRAW_BOUNDS_LABEL,
|
||||
content: (
|
||||
<GeometryFilterForm
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
buttonLabel={i18n.translate('xpack.maps.toolbarOverlay.drawBounds.onSubmitButtonLabel', {
|
||||
defaultMessage: 'Draw bounds',
|
||||
})}
|
||||
geoFields={this.props.geoFields}
|
||||
intitialGeometryLabel={i18n.translate('xpack.maps.toolbarOverlay.drawBounds.initialGeometryLabel', {
|
||||
defaultMessage: 'bounds',
|
||||
})}
|
||||
onSubmit={this._initiateBoundsDraw}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
_renderToolsButton() {
|
||||
|
@ -227,18 +173,3 @@ export class ToolsControl extends Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
function flattenPanelTree(tree, array = []) {
|
||||
array.push(tree);
|
||||
|
||||
if (tree.items) {
|
||||
tree.items.forEach(item => {
|
||||
if (item.panel) {
|
||||
flattenPanelTree(item.panel, array);
|
||||
item.panel = item.panel.id;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
|
|
@ -5668,8 +5668,6 @@
|
|||
"xpack.maps.styles.vector.symbolSizeLabel": "シンボルのサイズ",
|
||||
"xpack.maps.toolbarOverlay.drawBoundsLabel": "境界を描いてデータをフィルタリング",
|
||||
"xpack.maps.toolbarOverlay.drawShapeLabel": "シェイプを描いてデータをフィルタリング",
|
||||
"xpack.maps.toolbarOverlay.geofield.toolbarTitle": "ジオフィールドを選択",
|
||||
"xpack.maps.toolbarOverlay.indexPattern.filterListTitle": "フィルターリスト",
|
||||
"xpack.maps.toolbarOverlay.tools.toolbarTitle": "ツール",
|
||||
"xpack.maps.tooltip.closeAriaLabel": "ツールヒントを閉じる",
|
||||
"xpack.maps.tooltip.filterOnPropertyAriaLabel": "プロパティのフィルター",
|
||||
|
|
|
@ -5812,8 +5812,6 @@
|
|||
"xpack.maps.styles.vector.symbolSizeLabel": "符号大小",
|
||||
"xpack.maps.toolbarOverlay.drawBoundsLabel": "绘制边界以筛选数据",
|
||||
"xpack.maps.toolbarOverlay.drawShapeLabel": "绘制形状以筛选数据",
|
||||
"xpack.maps.toolbarOverlay.geofield.toolbarTitle": "选择地理字段",
|
||||
"xpack.maps.toolbarOverlay.indexPattern.filterListTitle": "筛选列表",
|
||||
"xpack.maps.toolbarOverlay.tools.toolbarTitle": "工具",
|
||||
"xpack.maps.tooltip.closeAriaLabel": "关闭工具提示",
|
||||
"xpack.maps.tooltip.filterOnPropertyAriaLabel": "基于属性筛选",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue