mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Maps] add draw control to create distance filter (#58163)
* [Maps] add distance filter to draw controls * create distance filter * update jest snapshot * remove duplicated code * reset circle draw when user hits escape * i18n cleanup * ts MultiIndexGeoFieldSelect * ts DistanceFilterForm * remove unused prop * make interface a type * move geo_field_with_index to components folder * convert draw_circle to TS Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
6cd888f75f
commit
6cbfa274cf
16 changed files with 636 additions and 282 deletions
|
@ -17,49 +17,27 @@ exports[`should not render relation select when geo field is geo_point 1`] = `
|
|||
value="My shape"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
className="mapGeometryFilter__geoFieldSuperSelectWrapper"
|
||||
describedByIds={Array []}
|
||||
display="rowCompressed"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Filtered field"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
className="mapGeometryFilter__geoFieldSuperSelect"
|
||||
compressed={true}
|
||||
fullWidth={true}
|
||||
hasDividers={true}
|
||||
isInvalid={false}
|
||||
isLoading={false}
|
||||
itemClassName="mapGeometryFilter__geoFieldItem"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"inputDisplay": <EuiText
|
||||
component="span"
|
||||
size="s"
|
||||
>
|
||||
<EuiTextColor
|
||||
color="subdued"
|
||||
>
|
||||
<small>
|
||||
My index
|
||||
</small>
|
||||
</EuiTextColor>
|
||||
<br />
|
||||
my geo field
|
||||
</EuiText>,
|
||||
"value": "My index/my geo field",
|
||||
},
|
||||
]
|
||||
<MultiIndexGeoFieldSelect
|
||||
fields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "my geo field",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": 1,
|
||||
"indexPatternTitle": "My index",
|
||||
},
|
||||
]
|
||||
}
|
||||
onChange={[Function]}
|
||||
selectedField={
|
||||
Object {
|
||||
"geoFieldName": "my geo field",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": 1,
|
||||
"indexPatternTitle": "My index",
|
||||
}
|
||||
valueOfSelected="My index/my geo field"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
}
|
||||
/>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
|
@ -95,49 +73,27 @@ exports[`should not show "within" relation when filter geometry is not closed 1`
|
|||
value="My shape"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
className="mapGeometryFilter__geoFieldSuperSelectWrapper"
|
||||
describedByIds={Array []}
|
||||
display="rowCompressed"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Filtered field"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
className="mapGeometryFilter__geoFieldSuperSelect"
|
||||
compressed={true}
|
||||
fullWidth={true}
|
||||
hasDividers={true}
|
||||
isInvalid={false}
|
||||
isLoading={false}
|
||||
itemClassName="mapGeometryFilter__geoFieldItem"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"inputDisplay": <EuiText
|
||||
component="span"
|
||||
size="s"
|
||||
>
|
||||
<EuiTextColor
|
||||
color="subdued"
|
||||
>
|
||||
<small>
|
||||
My index
|
||||
</small>
|
||||
</EuiTextColor>
|
||||
<br />
|
||||
my geo field
|
||||
</EuiText>,
|
||||
"value": "My index/my geo field",
|
||||
},
|
||||
]
|
||||
<MultiIndexGeoFieldSelect
|
||||
fields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "my geo field",
|
||||
"geoFieldType": "geo_shape",
|
||||
"indexPatternId": 1,
|
||||
"indexPatternTitle": "My index",
|
||||
},
|
||||
]
|
||||
}
|
||||
onChange={[Function]}
|
||||
selectedField={
|
||||
Object {
|
||||
"geoFieldName": "my geo field",
|
||||
"geoFieldType": "geo_shape",
|
||||
"indexPatternId": 1,
|
||||
"indexPatternTitle": "My index",
|
||||
}
|
||||
valueOfSelected="My index/my geo field"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
}
|
||||
/>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="rowCompressed"
|
||||
|
@ -200,49 +156,27 @@ exports[`should render error message 1`] = `
|
|||
value="My shape"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
className="mapGeometryFilter__geoFieldSuperSelectWrapper"
|
||||
describedByIds={Array []}
|
||||
display="rowCompressed"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Filtered field"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
className="mapGeometryFilter__geoFieldSuperSelect"
|
||||
compressed={true}
|
||||
fullWidth={true}
|
||||
hasDividers={true}
|
||||
isInvalid={false}
|
||||
isLoading={false}
|
||||
itemClassName="mapGeometryFilter__geoFieldItem"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"inputDisplay": <EuiText
|
||||
component="span"
|
||||
size="s"
|
||||
>
|
||||
<EuiTextColor
|
||||
color="subdued"
|
||||
>
|
||||
<small>
|
||||
My index
|
||||
</small>
|
||||
</EuiTextColor>
|
||||
<br />
|
||||
my geo field
|
||||
</EuiText>,
|
||||
"value": "My index/my geo field",
|
||||
},
|
||||
]
|
||||
<MultiIndexGeoFieldSelect
|
||||
fields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "my geo field",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": 1,
|
||||
"indexPatternTitle": "My index",
|
||||
},
|
||||
]
|
||||
}
|
||||
onChange={[Function]}
|
||||
selectedField={
|
||||
Object {
|
||||
"geoFieldName": "my geo field",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": 1,
|
||||
"indexPatternTitle": "My index",
|
||||
}
|
||||
valueOfSelected="My index/my geo field"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
}
|
||||
/>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
|
@ -281,49 +215,27 @@ exports[`should render relation select when geo field is geo_shape 1`] = `
|
|||
value="My shape"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
className="mapGeometryFilter__geoFieldSuperSelectWrapper"
|
||||
describedByIds={Array []}
|
||||
display="rowCompressed"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Filtered field"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
className="mapGeometryFilter__geoFieldSuperSelect"
|
||||
compressed={true}
|
||||
fullWidth={true}
|
||||
hasDividers={true}
|
||||
isInvalid={false}
|
||||
isLoading={false}
|
||||
itemClassName="mapGeometryFilter__geoFieldItem"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"inputDisplay": <EuiText
|
||||
component="span"
|
||||
size="s"
|
||||
>
|
||||
<EuiTextColor
|
||||
color="subdued"
|
||||
>
|
||||
<small>
|
||||
My index
|
||||
</small>
|
||||
</EuiTextColor>
|
||||
<br />
|
||||
my geo field
|
||||
</EuiText>,
|
||||
"value": "My index/my geo field",
|
||||
},
|
||||
]
|
||||
<MultiIndexGeoFieldSelect
|
||||
fields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "my geo field",
|
||||
"geoFieldType": "geo_shape",
|
||||
"indexPatternId": 1,
|
||||
"indexPatternTitle": "My index",
|
||||
},
|
||||
]
|
||||
}
|
||||
onChange={[Function]}
|
||||
selectedField={
|
||||
Object {
|
||||
"geoFieldName": "my geo field",
|
||||
"geoFieldType": "geo_shape",
|
||||
"indexPatternId": 1,
|
||||
"indexPatternTitle": "My index",
|
||||
}
|
||||
valueOfSelected="My index/my geo field"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
}
|
||||
/>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="rowCompressed"
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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, ChangeEvent } from 'react';
|
||||
import {
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiFieldText,
|
||||
EuiButton,
|
||||
EuiSpacer,
|
||||
EuiTextAlign,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select';
|
||||
import { GeoFieldWithIndex } from './geo_field_with_index';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
buttonLabel: string;
|
||||
geoFields: GeoFieldWithIndex[];
|
||||
onSubmit: ({
|
||||
filterLabel,
|
||||
indexPatternId,
|
||||
geoFieldName,
|
||||
}: {
|
||||
filterLabel: string;
|
||||
indexPatternId: string;
|
||||
geoFieldName: string;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
selectedField: GeoFieldWithIndex | undefined;
|
||||
filterLabel: string;
|
||||
}
|
||||
|
||||
export class DistanceFilterForm extends Component<Props, State> {
|
||||
state = {
|
||||
selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined,
|
||||
filterLabel: '',
|
||||
};
|
||||
|
||||
_onGeoFieldChange = (selectedField: GeoFieldWithIndex | undefined) => {
|
||||
this.setState({ selectedField });
|
||||
};
|
||||
|
||||
_onFilterLabelChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
filterLabel: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onSubmit = () => {
|
||||
if (!this.state.selectedField) {
|
||||
return;
|
||||
}
|
||||
this.props.onSubmit({
|
||||
filterLabel: this.state.filterLabel,
|
||||
indexPatternId: this.state.selectedField.indexPatternId,
|
||||
geoFieldName: this.state.selectedField.geoFieldName,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EuiForm className={this.props.className}>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.distanceFilterForm.filterLabelLabel', {
|
||||
defaultMessage: 'Filter label',
|
||||
})}
|
||||
display="rowCompressed"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed
|
||||
value={this.state.filterLabel}
|
||||
onChange={this._onFilterLabelChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<MultiIndexGeoFieldSelect
|
||||
selectedField={this.state.selectedField}
|
||||
fields={this.props.geoFields}
|
||||
onChange={this._onGeoFieldChange}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiTextAlign textAlign="right">
|
||||
<EuiButton size="s" fill onClick={this._onSubmit} isDisabled={!this.state.selectedField}>
|
||||
{this.props.buttonLabel}
|
||||
</EuiButton>
|
||||
</EuiTextAlign>
|
||||
</EuiForm>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
||||
|
||||
// Maps can contain geo fields from multiple index patterns. GeoFieldWithIndex is used to:
|
||||
// 1) Combine the geo field along with associated index pattern state.
|
||||
// 2) Package asynchronously looked up state via indexPatternService to avoid
|
||||
// PITA of looking up async state in downstream react consumers.
|
||||
export type GeoFieldWithIndex = {
|
||||
geoFieldName: string;
|
||||
geoFieldType: string;
|
||||
indexPatternTitle: string;
|
||||
indexPatternId: string;
|
||||
};
|
|
@ -9,9 +9,6 @@ import PropTypes from 'prop-types';
|
|||
import {
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiSuperSelect,
|
||||
EuiTextColor,
|
||||
EuiText,
|
||||
EuiFieldText,
|
||||
EuiButton,
|
||||
EuiSelect,
|
||||
|
@ -22,20 +19,7 @@ import {
|
|||
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],
|
||||
};
|
||||
}
|
||||
import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select';
|
||||
|
||||
export class GeometryFilterForm extends Component {
|
||||
static propTypes = {
|
||||
|
@ -52,27 +36,13 @@ export class GeometryFilterForm extends Component {
|
|||
};
|
||||
|
||||
state = {
|
||||
geoFieldTag: this.props.geoFields.length
|
||||
? createIndexGeoFieldName(this.props.geoFields[0])
|
||||
: '',
|
||||
selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined,
|
||||
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 });
|
||||
_onGeoFieldChange = selectedField => {
|
||||
this.setState({ selectedField });
|
||||
};
|
||||
|
||||
_onGeometryLabelChange = e => {
|
||||
|
@ -88,25 +58,21 @@ export class GeometryFilterForm extends Component {
|
|||
};
|
||||
|
||||
_onSubmit = () => {
|
||||
const geoField = this._getSelectedGeoField();
|
||||
this.props.onSubmit({
|
||||
geometryLabel: this.state.geometryLabel,
|
||||
indexPatternId: geoField.indexPatternId,
|
||||
geoFieldName: geoField.geoFieldName,
|
||||
geoFieldType: geoField.geoFieldType,
|
||||
indexPatternId: this.state.selectedField.indexPatternId,
|
||||
geoFieldName: this.state.selectedField.geoFieldName,
|
||||
geoFieldType: this.state.selectedField.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) {
|
||||
if (
|
||||
!this.state.selectedField ||
|
||||
this.state.selectedField.geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -141,20 +107,6 @@ export class GeometryFilterForm extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const options = this.props.geoFields.map(({ indexPatternTitle, geoFieldName }) => {
|
||||
return {
|
||||
inputDisplay: (
|
||||
<EuiText size="s" component="span">
|
||||
<EuiTextColor color="subdued">
|
||||
<small>{indexPatternTitle}</small>
|
||||
</EuiTextColor>
|
||||
<br />
|
||||
{geoFieldName}
|
||||
</EuiText>
|
||||
),
|
||||
value: createIndexGeoFieldName({ indexPatternTitle, geoFieldName }),
|
||||
};
|
||||
});
|
||||
let error;
|
||||
if (this.props.errorMsg) {
|
||||
error = <EuiFormErrorText>{this.props.errorMsg}</EuiFormErrorText>;
|
||||
|
@ -174,24 +126,11 @@ export class GeometryFilterForm extends Component {
|
|||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
className="mapGeometryFilter__geoFieldSuperSelectWrapper"
|
||||
label={i18n.translate('xpack.maps.geometryFilterForm.geoFieldLabel', {
|
||||
defaultMessage: 'Filtered field',
|
||||
})}
|
||||
display="rowCompressed"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
className="mapGeometryFilter__geoFieldSuperSelect"
|
||||
options={options}
|
||||
valueOfSelected={this.state.geoFieldTag}
|
||||
onChange={this._onGeoFieldChange}
|
||||
hasDividers={true}
|
||||
fullWidth={true}
|
||||
compressed={true}
|
||||
itemClassName="mapGeometryFilter__geoFieldItem"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<MultiIndexGeoFieldSelect
|
||||
selectedField={this.state.selectedField}
|
||||
fields={this.props.geoFields}
|
||||
onChange={this._onGeoFieldChange}
|
||||
/>
|
||||
|
||||
{this._renderRelationInput()}
|
||||
|
||||
|
@ -204,7 +143,7 @@ export class GeometryFilterForm extends Component {
|
|||
size="s"
|
||||
fill
|
||||
onClick={this._onSubmit}
|
||||
isDisabled={!this.state.geometryLabel || !this.state.geoFieldTag}
|
||||
isDisabled={!this.state.geometryLabel || !this.state.selectedField}
|
||||
isLoading={this.props.isLoading}
|
||||
>
|
||||
{this.props.buttonLabel}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 { EuiFormRow, EuiSuperSelect, EuiTextColor, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { GeoFieldWithIndex } from './geo_field_with_index';
|
||||
|
||||
const OPTION_ID_DELIMITER = '/';
|
||||
|
||||
function createOptionId(geoField: GeoFieldWithIndex): string {
|
||||
// Namespace field with indexPatterId to avoid collisions between field names
|
||||
return `${geoField.indexPatternId}${OPTION_ID_DELIMITER}${geoField.geoFieldName}`;
|
||||
}
|
||||
|
||||
function splitOptionId(optionId: string) {
|
||||
const split = optionId.split(OPTION_ID_DELIMITER);
|
||||
return {
|
||||
indexPatternId: split[0],
|
||||
geoFieldName: split[1],
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
fields: GeoFieldWithIndex[];
|
||||
onChange: (newSelectedField: GeoFieldWithIndex | undefined) => void;
|
||||
selectedField: GeoFieldWithIndex | undefined;
|
||||
}
|
||||
|
||||
export function MultiIndexGeoFieldSelect({ fields, onChange, selectedField }: Props) {
|
||||
function onFieldSelect(selectedOptionId: string) {
|
||||
const { indexPatternId, geoFieldName } = splitOptionId(selectedOptionId);
|
||||
|
||||
const newSelectedField = fields.find(field => {
|
||||
return field.indexPatternId === indexPatternId && field.geoFieldName === geoFieldName;
|
||||
});
|
||||
onChange(newSelectedField);
|
||||
}
|
||||
|
||||
const options = fields.map((geoField: GeoFieldWithIndex) => {
|
||||
return {
|
||||
inputDisplay: (
|
||||
<EuiText size="s">
|
||||
<EuiTextColor color="subdued">
|
||||
<small>{geoField.indexPatternTitle}</small>
|
||||
</EuiTextColor>
|
||||
<br />
|
||||
{geoField.geoFieldName}
|
||||
</EuiText>
|
||||
),
|
||||
value: createOptionId(geoField),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
className="mapGeometryFilter__geoFieldSuperSelectWrapper"
|
||||
label={i18n.translate('xpack.maps.multiIndexFieldSelect.fieldLabel', {
|
||||
defaultMessage: 'Filtering field',
|
||||
})}
|
||||
display="rowCompressed"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
className="mapGeometryFilter__geoFieldSuperSelect"
|
||||
options={options}
|
||||
valueOfSelected={selectedField ? createOptionId(selectedField) : ''}
|
||||
onChange={onFieldSelect}
|
||||
hasDividers={true}
|
||||
fullWidth={true}
|
||||
compressed={true}
|
||||
itemClassName="mapGeometryFilter__geoFieldItem"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
||||
|
||||
// @ts-ignore
|
||||
import turf from 'turf';
|
||||
// @ts-ignore
|
||||
import turfCircle from '@turf/circle';
|
||||
|
||||
type DrawCircleState = {
|
||||
circle: {
|
||||
properties: {
|
||||
center: {} | null;
|
||||
radiusKm: number;
|
||||
};
|
||||
id: string | number;
|
||||
incomingCoords: (coords: unknown[]) => void;
|
||||
toGeoJSON: () => unknown;
|
||||
};
|
||||
};
|
||||
|
||||
type MouseEvent = {
|
||||
lngLat: {
|
||||
lng: number;
|
||||
lat: number;
|
||||
};
|
||||
};
|
||||
|
||||
export const DrawCircle = {
|
||||
onSetup() {
|
||||
// @ts-ignore
|
||||
const circle: unknown = this.newFeature({
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
center: null,
|
||||
radiusKm: 0,
|
||||
},
|
||||
geometry: {
|
||||
type: 'Polygon',
|
||||
coordinates: [[]],
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
this.addFeature(circle);
|
||||
// @ts-ignore
|
||||
this.clearSelectedFeatures();
|
||||
// @ts-ignore
|
||||
this.updateUIClasses({ mouse: 'add' });
|
||||
// @ts-ignore
|
||||
this.setActionableState({
|
||||
trash: true,
|
||||
});
|
||||
return {
|
||||
circle,
|
||||
};
|
||||
},
|
||||
onKeyUp(state: DrawCircleState, e: { keyCode: number }) {
|
||||
if (e.keyCode === 27) {
|
||||
// clear point when user hits escape
|
||||
state.circle.properties.center = null;
|
||||
state.circle.properties.radiusKm = 0;
|
||||
state.circle.incomingCoords([[]]);
|
||||
}
|
||||
},
|
||||
onClick(state: DrawCircleState, e: MouseEvent) {
|
||||
if (!state.circle.properties.center) {
|
||||
// first click, start circle
|
||||
state.circle.properties.center = [e.lngLat.lng, e.lngLat.lat];
|
||||
} else {
|
||||
// second click, finish draw
|
||||
// @ts-ignore
|
||||
this.updateUIClasses({ mouse: 'pointer' });
|
||||
state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, [
|
||||
e.lngLat.lng,
|
||||
e.lngLat.lat,
|
||||
]);
|
||||
// @ts-ignore
|
||||
this.changeMode('simple_select', { featuresId: state.circle.id });
|
||||
}
|
||||
},
|
||||
onMouseMove(state: DrawCircleState, e: MouseEvent) {
|
||||
if (!state.circle.properties.center) {
|
||||
// circle not started, nothing to update
|
||||
return;
|
||||
}
|
||||
|
||||
const mouseLocation = [e.lngLat.lng, e.lngLat.lat];
|
||||
state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, mouseLocation);
|
||||
const newCircleFeature = turfCircle(
|
||||
state.circle.properties.center,
|
||||
state.circle.properties.radiusKm
|
||||
);
|
||||
state.circle.incomingCoords(newCircleFeature.geometry.coordinates);
|
||||
},
|
||||
onStop(state: DrawCircleState) {
|
||||
// @ts-ignore
|
||||
this.updateUIClasses({ mouse: 'none' });
|
||||
// @ts-ignore
|
||||
this.activateUIButton();
|
||||
|
||||
// @ts-ignore
|
||||
if (this.getFeature(state.circle.id) === undefined) return;
|
||||
|
||||
if (state.circle.properties.center && state.circle.properties.radiusKm > 0) {
|
||||
// @ts-ignore
|
||||
this.map.fire('draw.create', {
|
||||
features: [state.circle.toGeoJSON()],
|
||||
});
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this.deleteFeature([state.circle.id], { silent: true });
|
||||
// @ts-ignore
|
||||
this.changeMode('simple_select', {}, { silent: true });
|
||||
}
|
||||
},
|
||||
toDisplayFeatures(
|
||||
state: DrawCircleState,
|
||||
geojson: { properties: { active: string } },
|
||||
display: (geojson: unknown) => unknown
|
||||
) {
|
||||
if (state.circle.properties.center) {
|
||||
geojson.properties.active = 'true';
|
||||
return display(geojson);
|
||||
}
|
||||
},
|
||||
onTrash(state: DrawCircleState) {
|
||||
// @ts-ignore
|
||||
this.deleteFeature([state.circle.id], { silent: true });
|
||||
// @ts-ignore
|
||||
this.changeMode('simple_select');
|
||||
},
|
||||
};
|
|
@ -9,7 +9,9 @@ import React from 'react';
|
|||
import { DRAW_TYPE } from '../../../../../common/constants';
|
||||
import MapboxDraw from '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw-unminified';
|
||||
import DrawRectangle from 'mapbox-gl-draw-rectangle-mode';
|
||||
import { DrawCircle } from './draw_circle';
|
||||
import {
|
||||
createDistanceFilterWithMeta,
|
||||
createSpatialFilterWithBoundingBox,
|
||||
createSpatialFilterWithGeometry,
|
||||
getBoundingBoxGeometry,
|
||||
|
@ -19,6 +21,7 @@ import { DrawTooltip } from './draw_tooltip';
|
|||
|
||||
const mbDrawModes = MapboxDraw.modes;
|
||||
mbDrawModes.draw_rectangle = DrawRectangle;
|
||||
mbDrawModes.draw_circle = DrawCircle;
|
||||
|
||||
export class DrawControl extends React.Component {
|
||||
constructor() {
|
||||
|
@ -60,7 +63,21 @@ export class DrawControl extends React.Component {
|
|||
return;
|
||||
}
|
||||
|
||||
const isBoundingBox = this.props.drawState.drawType === DRAW_TYPE.BOUNDS;
|
||||
if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) {
|
||||
const circle = e.features[0];
|
||||
roundCoordinates(circle.properties.center);
|
||||
const filter = createDistanceFilterWithMeta({
|
||||
alias: this.props.drawState.filterLabel,
|
||||
distanceKm: _.round(circle.properties.radiusKm, circle.properties.radiusKm > 10 ? 0 : 2),
|
||||
geoFieldName: this.props.drawState.geoFieldName,
|
||||
indexPatternId: this.props.drawState.indexPatternId,
|
||||
point: circle.properties.center,
|
||||
});
|
||||
this.props.addFilters([filter]);
|
||||
this.props.disableDrawState();
|
||||
return;
|
||||
}
|
||||
|
||||
const geometry = e.features[0].geometry;
|
||||
// MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number
|
||||
roundCoordinates(geometry.coordinates);
|
||||
|
@ -73,15 +90,16 @@ export class DrawControl extends React.Component {
|
|||
geometryLabel: this.props.drawState.geometryLabel,
|
||||
relation: this.props.drawState.relation,
|
||||
};
|
||||
const filter = isBoundingBox
|
||||
? createSpatialFilterWithBoundingBox({
|
||||
...options,
|
||||
geometry: getBoundingBoxGeometry(geometry),
|
||||
})
|
||||
: createSpatialFilterWithGeometry({
|
||||
...options,
|
||||
geometry,
|
||||
});
|
||||
const filter =
|
||||
this.props.drawState.drawType === DRAW_TYPE.BOUNDS
|
||||
? createSpatialFilterWithBoundingBox({
|
||||
...options,
|
||||
geometry: getBoundingBoxGeometry(geometry),
|
||||
})
|
||||
: createSpatialFilterWithGeometry({
|
||||
...options,
|
||||
geometry,
|
||||
});
|
||||
this.props.addFilters([filter]);
|
||||
} catch (error) {
|
||||
// TODO notify user why filter was not created
|
||||
|
@ -109,11 +127,14 @@ export class DrawControl extends React.Component {
|
|||
this.props.mbMap.getCanvas().style.cursor = 'crosshair';
|
||||
this.props.mbMap.on('draw.create', this._onDraw);
|
||||
}
|
||||
const mbDrawMode =
|
||||
this.props.drawState.drawType === DRAW_TYPE.POLYGON
|
||||
? this._mbDrawControl.modes.DRAW_POLYGON
|
||||
: 'draw_rectangle';
|
||||
this._mbDrawControl.changeMode(mbDrawMode);
|
||||
|
||||
if (this.props.drawState.drawType === DRAW_TYPE.BOUNDS) {
|
||||
this._mbDrawControl.changeMode('draw_rectangle');
|
||||
} else if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) {
|
||||
this._mbDrawControl.changeMode('draw_circle');
|
||||
} else if (this.props.drawState.drawType === DRAW_TYPE.POLYGON) {
|
||||
this._mbDrawControl.changeMode(this._mbDrawControl.modes.DRAW_POLYGON);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -42,14 +42,24 @@ export class DrawTooltip extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const instructions =
|
||||
this.props.drawState.drawType === DRAW_TYPE.BOUNDS
|
||||
? i18n.translate('xpack.maps.drawTooltip.boundsInstructions', {
|
||||
defaultMessage: 'Click to start rectangle. Click again to finish.',
|
||||
})
|
||||
: i18n.translate('xpack.maps.drawTooltip.polygonInstructions', {
|
||||
defaultMessage: 'Click to add vertex. Double click to finish.',
|
||||
});
|
||||
let instructions;
|
||||
if (this.props.drawState.drawType === DRAW_TYPE.BOUNDS) {
|
||||
instructions = i18n.translate('xpack.maps.drawTooltip.boundsInstructions', {
|
||||
defaultMessage:
|
||||
'Click to start rectangle. Move mouse to adjust rectangle size. Click again to finish.',
|
||||
});
|
||||
} else if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) {
|
||||
instructions = i18n.translate('xpack.maps.drawTooltip.distanceInstructions', {
|
||||
defaultMessage: 'Click to set point. Move mouse to adjust distance. Click to finish.',
|
||||
});
|
||||
} else if (this.props.drawState.drawType === DRAW_TYPE.POLYGON) {
|
||||
instructions = i18n.translate('xpack.maps.drawTooltip.polygonInstructions', {
|
||||
defaultMessage: 'Click to start shape. Click to add vertex. Double click to finish.',
|
||||
});
|
||||
} else {
|
||||
// unknown draw type, tooltip not needed
|
||||
return null;
|
||||
}
|
||||
|
||||
const tooltipAnchor = (
|
||||
<div style={{ height: '26px', width: '26px', background: 'transparent' }} />
|
||||
|
|
|
@ -41,6 +41,10 @@ exports[`Should render cancel button when drawing 1`] = `
|
|||
"name": "Draw bounds to filter data",
|
||||
"panel": 2,
|
||||
},
|
||||
Object {
|
||||
"name": "Draw distance to filter data",
|
||||
"panel": 3,
|
||||
},
|
||||
],
|
||||
"title": "Tools",
|
||||
},
|
||||
|
@ -86,6 +90,25 @@ exports[`Should render cancel button when drawing 1`] = `
|
|||
"id": 2,
|
||||
"title": "Draw bounds",
|
||||
},
|
||||
Object {
|
||||
"content": <DistanceFilterForm
|
||||
buttonLabel="Draw distance"
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
geoFields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "location",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": "1",
|
||||
"indexPatternTitle": "my_index",
|
||||
},
|
||||
]
|
||||
}
|
||||
onSubmit={[Function]}
|
||||
/>,
|
||||
"id": 3,
|
||||
"title": "Draw distance",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
|
@ -144,6 +167,10 @@ exports[`renders 1`] = `
|
|||
"name": "Draw bounds to filter data",
|
||||
"panel": 2,
|
||||
},
|
||||
Object {
|
||||
"name": "Draw distance to filter data",
|
||||
"panel": 3,
|
||||
},
|
||||
],
|
||||
"title": "Tools",
|
||||
},
|
||||
|
@ -189,6 +216,25 @@ exports[`renders 1`] = `
|
|||
"id": 2,
|
||||
"title": "Draw bounds",
|
||||
},
|
||||
Object {
|
||||
"content": <DistanceFilterForm
|
||||
buttonLabel="Draw distance"
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
geoFields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "location",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": "1",
|
||||
"indexPatternTitle": "my_index",
|
||||
},
|
||||
]
|
||||
}
|
||||
onSubmit={[Function]}
|
||||
/>,
|
||||
"id": 3,
|
||||
"title": "Draw distance",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -14,9 +14,10 @@ import {
|
|||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DRAW_TYPE } from '../../../../common/constants';
|
||||
import { DRAW_TYPE, ES_GEO_FIELD_TYPE } from '../../../../common/constants';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { GeometryFilterForm } from '../../../components/geometry_filter_form';
|
||||
import { DistanceFilterForm } from '../../../components/distance_filter_form';
|
||||
|
||||
const DRAW_SHAPE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', {
|
||||
defaultMessage: 'Draw shape to filter data',
|
||||
|
@ -26,6 +27,10 @@ const DRAW_BOUNDS_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawBoundsLa
|
|||
defaultMessage: 'Draw bounds to filter data',
|
||||
});
|
||||
|
||||
const DRAW_DISTANCE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawDistanceLabel', {
|
||||
defaultMessage: 'Draw distance to filter data',
|
||||
});
|
||||
|
||||
const DRAW_SHAPE_LABEL_SHORT = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabelShort', {
|
||||
defaultMessage: 'Draw shape',
|
||||
});
|
||||
|
@ -34,6 +39,13 @@ const DRAW_BOUNDS_LABEL_SHORT = i18n.translate('xpack.maps.toolbarOverlay.drawBo
|
|||
defaultMessage: 'Draw bounds',
|
||||
});
|
||||
|
||||
const DRAW_DISTANCE_LABEL_SHORT = i18n.translate(
|
||||
'xpack.maps.toolbarOverlay.drawDistanceLabelShort',
|
||||
{
|
||||
defaultMessage: 'Draw distance',
|
||||
}
|
||||
);
|
||||
|
||||
export class ToolsControl extends Component {
|
||||
state = {
|
||||
isPopoverOpen: false,
|
||||
|
@ -65,23 +77,43 @@ export class ToolsControl extends Component {
|
|||
this._closePopover();
|
||||
};
|
||||
|
||||
_initiateDistanceDraw = options => {
|
||||
this.props.initiateDraw({
|
||||
drawType: DRAW_TYPE.DISTANCE,
|
||||
...options,
|
||||
});
|
||||
this._closePopover();
|
||||
};
|
||||
|
||||
_getDrawPanels() {
|
||||
const tools = [
|
||||
{
|
||||
name: DRAW_SHAPE_LABEL,
|
||||
panel: 1,
|
||||
},
|
||||
{
|
||||
name: DRAW_BOUNDS_LABEL,
|
||||
panel: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const hasGeoPoints = this.props.geoFields.some(({ geoFieldType }) => {
|
||||
return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT;
|
||||
});
|
||||
if (hasGeoPoints) {
|
||||
tools.push({
|
||||
name: DRAW_DISTANCE_LABEL,
|
||||
panel: 3,
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
items: tools,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
|
@ -119,6 +151,20 @@ export class ToolsControl extends Component {
|
|||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: DRAW_DISTANCE_LABEL_SHORT,
|
||||
content: (
|
||||
<DistanceFilterForm
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
buttonLabel={DRAW_DISTANCE_LABEL_SHORT}
|
||||
geoFields={this.props.geoFields.filter(({ geoFieldType }) => {
|
||||
return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT;
|
||||
})}
|
||||
onSubmit={this._initiateDistanceDraw}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -344,6 +344,39 @@ function createGeometryFilterWithMeta({
|
|||
return createGeoPolygonFilter(geometry.coordinates, geoFieldName, { meta });
|
||||
}
|
||||
|
||||
export function createDistanceFilterWithMeta({
|
||||
alias,
|
||||
distanceKm,
|
||||
geoFieldName,
|
||||
indexPatternId,
|
||||
point,
|
||||
}) {
|
||||
const meta = {
|
||||
type: SPATIAL_FILTER_TYPE,
|
||||
negate: false,
|
||||
index: indexPatternId,
|
||||
key: geoFieldName,
|
||||
alias: alias
|
||||
? alias
|
||||
: i18n.translate('xpack.maps.es_geo_utils.distanceFilterAlias', {
|
||||
defaultMessage: '{geoFieldName} within {distanceKm}km of {pointLabel}',
|
||||
values: {
|
||||
distanceKm,
|
||||
geoFieldName,
|
||||
pointLabel: point.join(','),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
return {
|
||||
geo_distance: {
|
||||
distance: `${distanceKm}km`,
|
||||
[geoFieldName]: point,
|
||||
},
|
||||
meta,
|
||||
};
|
||||
}
|
||||
|
||||
export function roundCoordinates(coordinates) {
|
||||
for (let i = 0; i < coordinates.length; i++) {
|
||||
const value = coordinates[i];
|
||||
|
|
|
@ -196,6 +196,7 @@
|
|||
"@scant/router": "^0.1.0",
|
||||
"@slack/webhook": "^5.0.0",
|
||||
"@turf/boolean-contains": "6.0.1",
|
||||
"@turf/circle": "6.0.1",
|
||||
"angular": "^1.7.9",
|
||||
"angular-resource": "1.7.9",
|
||||
"angular-sanitize": "1.7.9",
|
||||
|
|
|
@ -120,6 +120,7 @@ export const EMPTY_FEATURE_COLLECTION = {
|
|||
|
||||
export const DRAW_TYPE = {
|
||||
BOUNDS: 'BOUNDS',
|
||||
DISTANCE: 'DISTANCE',
|
||||
POLYGON: 'POLYGON',
|
||||
};
|
||||
|
||||
|
|
|
@ -7073,7 +7073,6 @@
|
|||
"xpack.maps.feature.appDescription": "Elasticsearch と Elastic Maps Service の地理空間データを閲覧します",
|
||||
"xpack.maps.featureRegistry.mapsFeatureName": "マップ",
|
||||
"xpack.maps.geoGrid.resolutionLabel": "グリッド解像度",
|
||||
"xpack.maps.geometryFilterForm.geoFieldLabel": "フィルタリングされたフィールド",
|
||||
"xpack.maps.geometryFilterForm.geometryLabelLabel": "ジオメトリラベル",
|
||||
"xpack.maps.geometryFilterForm.relationLabel": "空間関係",
|
||||
"xpack.maps.heatmap.colorRampLabel": "色の範囲",
|
||||
|
|
|
@ -7073,7 +7073,6 @@
|
|||
"xpack.maps.feature.appDescription": "从 Elasticsearch 和 Elastic 地图服务浏览地理空间数据",
|
||||
"xpack.maps.featureRegistry.mapsFeatureName": "Maps",
|
||||
"xpack.maps.geoGrid.resolutionLabel": "网格分辨率",
|
||||
"xpack.maps.geometryFilterForm.geoFieldLabel": "已筛选字段",
|
||||
"xpack.maps.geometryFilterForm.geometryLabelLabel": "几何标签",
|
||||
"xpack.maps.geometryFilterForm.relationLabel": "空间关系",
|
||||
"xpack.maps.heatmap.colorRampLabel": "颜色范围",
|
||||
|
|
16
yarn.lock
16
yarn.lock
|
@ -4086,6 +4086,22 @@
|
|||
"@turf/helpers" "6.x"
|
||||
"@turf/invariant" "6.x"
|
||||
|
||||
"@turf/circle@6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@turf/circle/-/circle-6.0.1.tgz#0ab72083373ae3c76b700c17a504ab1b5c0910b9"
|
||||
integrity sha512-pF9XsYtCvY9ZyNqJ3hFYem9VaiGdVNQb0SFq/zzDMwH3iWZPPJQHnnDB/3e8RD1VDtBBov9p5uO2k7otsfezjw==
|
||||
dependencies:
|
||||
"@turf/destination" "6.x"
|
||||
"@turf/helpers" "6.x"
|
||||
|
||||
"@turf/destination@6.x":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@turf/destination/-/destination-6.0.1.tgz#5275887fa96ec463f44864a2c17f0b712361794a"
|
||||
integrity sha512-MroK4nRdp7as174miCAugp8Uvorhe6rZ7MJiC9Hb4+hZR7gNFJyVKmkdDDXIoCYs6MJQsx0buI+gsCpKwgww0Q==
|
||||
dependencies:
|
||||
"@turf/helpers" "6.x"
|
||||
"@turf/invariant" "6.x"
|
||||
|
||||
"@turf/helpers@6.x":
|
||||
version "6.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.1.4.tgz#d6fd7ebe6782dd9c87dca5559bda5c48ae4c3836"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue