mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Maps] Move set view to toolbar control, show zoom level in view control (#38925)
* break out ToolsControl from ToolbarOverlay component * move set view control to toolbar overlay component * fix i18n * use cchaos approved top calculation in scss * fix functional tests by updating lat,lon, and zoom state when props change * do not set state from props in constructor, only set state from props in getDerivedStateFromProps * provide default value for isSetViewOpen in store * just pass needed params to connect
This commit is contained in:
parent
1ca0953832
commit
7b5b6e4e02
17 changed files with 505 additions and 458 deletions
|
@ -1,6 +1,6 @@
|
|||
.mapToolbarOverlay {
|
||||
position: absolute;
|
||||
top: ($euiSizeM * 2) + ($euiSizeXL * 2); // Position and height of mapbox controls plus margin
|
||||
top: ($euiSizeM + $euiSizeS) + ($euiSizeXL * 2); // Position and height of mapbox controls plus margin
|
||||
left: $euiSizeM;
|
||||
z-index: 2; // Sit on top of mapbox controls shadow
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
|||
.mapToolbarOverlay__button {
|
||||
@include size($euiSizeXL);
|
||||
background-color: $euiColorEmptyShade !important;
|
||||
pointer-events: all;
|
||||
|
||||
&:enabled,
|
||||
&:enabled:hover,
|
||||
|
|
|
@ -5,27 +5,16 @@
|
|||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { ToolbarOverlay } from './view';
|
||||
import { getDrawState, getUniqueIndexPatternIds } from '../../selectors/map_selectors';
|
||||
import { ToolbarOverlay } from './toolbar_overlay';
|
||||
import { getUniqueIndexPatternIds } from '../../selectors/map_selectors';
|
||||
import { getIsFilterable } from '../../store/ui';
|
||||
import { updateDrawState } from '../../actions/store_actions';
|
||||
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
isFilterable: getIsFilterable(state),
|
||||
drawState: getDrawState(state),
|
||||
uniqueIndexPatternIds: getUniqueIndexPatternIds(state)
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
initiateDraw: (options) => {
|
||||
dispatch(updateDrawState(options));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const connectedToolbarOverlay = connect(mapStateToProps, mapDispatchToProps)(ToolbarOverlay);
|
||||
const connectedToolbarOverlay = connect(mapStateToProps, null)(ToolbarOverlay);
|
||||
export { connectedToolbarOverlay as ToolbarOverlay };
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { connect } from 'react-redux';
|
||||
import { SetViewControl } from './set_view_control';
|
||||
import { setGotoWithCenter } from '../../../actions/store_actions';
|
||||
import { getMapZoom, getMapCenter } from '../../../selectors/map_selectors';
|
||||
import {
|
||||
getIsSetViewOpen,
|
||||
closeSetView,
|
||||
openSetView,
|
||||
} from '../../../store/ui';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
isSetViewOpen: getIsSetViewOpen(state),
|
||||
zoom: getMapZoom(state),
|
||||
center: getMapCenter(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onSubmit: ({ lat, lon, zoom }) => {
|
||||
dispatch(closeSetView());
|
||||
dispatch(setGotoWithCenter({ lat, lon, zoom }));
|
||||
},
|
||||
closeSetView: () => {
|
||||
dispatch(closeSetView());
|
||||
},
|
||||
openSetView: () => {
|
||||
dispatch(openSetView());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const connectedSetViewControl = connect(mapStateToProps, mapDispatchToProps)(SetViewControl);
|
||||
export { connectedSetViewControl as SetViewControl };
|
|
@ -4,23 +4,49 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiButton,
|
||||
EuiFieldNumber,
|
||||
EuiButtonIcon,
|
||||
EuiPopover,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export class SetView extends React.Component {
|
||||
function getViewString(lat, lon, zoom) {
|
||||
return `${lat},${lon},${zoom}`;
|
||||
}
|
||||
|
||||
state = {
|
||||
lat: this.props.center.lat,
|
||||
lon: this.props.center.lon,
|
||||
zoom: this.props.zoom,
|
||||
export class SetViewControl extends Component {
|
||||
|
||||
state = {}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
const nextView = getViewString(nextProps.center.lat, nextProps.center.lon, nextProps.zoom);
|
||||
if (nextView !== prevState.prevView) {
|
||||
return {
|
||||
lat: nextProps.center.lat,
|
||||
lon: nextProps.center.lon,
|
||||
zoom: nextProps.zoom,
|
||||
prevView: nextView,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_togglePopover = () => {
|
||||
if (this.props.isSetViewOpen) {
|
||||
this.props.closeSetView();
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.openSetView();
|
||||
};
|
||||
|
||||
_onLatChange = evt => {
|
||||
this._onChange('lat', evt);
|
||||
};
|
||||
|
@ -63,7 +89,7 @@ export class SetView extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
_onSubmit = () => {
|
||||
const {
|
||||
lat,
|
||||
lon,
|
||||
|
@ -72,7 +98,7 @@ export class SetView extends React.Component {
|
|||
this.props.onSubmit({ lat, lon, zoom });
|
||||
}
|
||||
|
||||
render() {
|
||||
_renderSetViewForm() {
|
||||
const { isInvalid: isLatInvalid, component: latFormRow } = this._renderNumberFormRow({
|
||||
value: this.state.lat,
|
||||
min: -90,
|
||||
|
@ -113,7 +139,7 @@ export class SetView extends React.Component {
|
|||
<EuiButton
|
||||
size="s"
|
||||
disabled={isLatInvalid || isLonInvalid || isZoomInvalid}
|
||||
onClick={this.onSubmit}
|
||||
onClick={this._onSubmit}
|
||||
data-test-subj="submitViewButton"
|
||||
>
|
||||
Go
|
||||
|
@ -123,13 +149,43 @@ export class SetView extends React.Component {
|
|||
</EuiForm>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EuiPopover
|
||||
anchorPosition="leftUp"
|
||||
button={(
|
||||
<EuiButtonIcon
|
||||
className="mapToolbarOverlay__button"
|
||||
onClick={this._togglePopover}
|
||||
data-test-subj="toggleSetViewVisibilityButton"
|
||||
iconType="crosshairs"
|
||||
color="text"
|
||||
aria-label={i18n.translate('xpack.maps.viewControl.goToButtonLabel', {
|
||||
defaultMessage: 'Go to'
|
||||
})}
|
||||
title={i18n.translate('xpack.maps.viewControl.goToButtonLabel', {
|
||||
defaultMessage: 'Go to'
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
isOpen={this.props.isSetViewOpen}
|
||||
closePopover={this.props.closeSetView}
|
||||
>
|
||||
{this._renderSetViewForm()}
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SetView.propTypes = {
|
||||
SetViewControl.propTypes = {
|
||||
isSetViewOpen: PropTypes.bool.isRequired,
|
||||
zoom: PropTypes.number.isRequired,
|
||||
center: PropTypes.shape({
|
||||
lat: PropTypes.number.isRequired,
|
||||
lon: PropTypes.number.isRequired
|
||||
}),
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
closeSetView: PropTypes.func.isRequired,
|
||||
openSetView: PropTypes.func.isRequired,
|
||||
};
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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 _ from 'lodash';
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { getIndexPatternsFromIds } from '../../index_pattern_util';
|
||||
import { ES_GEO_FIELD_TYPE } from '../../../common/constants';
|
||||
import { SetViewControl } from './set_view_control';
|
||||
import { ToolsControl } from './tools_control';
|
||||
|
||||
export class ToolbarOverlay extends React.Component {
|
||||
|
||||
state = {
|
||||
prevUniqueIndexPatternIds: [],
|
||||
uniqueIndexPatternsAndGeoFields: [],
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.isFilterable) {
|
||||
const nextUniqueIndexPatternIds = _.get(this.props, 'uniqueIndexPatternIds', []);
|
||||
this._loadUniqueIndexPatternAndFieldCombos(nextUniqueIndexPatternIds);
|
||||
}
|
||||
}
|
||||
|
||||
_loadUniqueIndexPatternAndFieldCombos = async (nextUniqueIndexPatternIds) => {
|
||||
if (_.isEqual(nextUniqueIndexPatternIds, this.state.prevUniqueIndexPatternIds)) {
|
||||
// all ready loaded index pattern ids
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
prevUniqueIndexPatternIds: nextUniqueIndexPatternIds,
|
||||
});
|
||||
|
||||
const uniqueIndexPatternsAndGeoFields = [];
|
||||
try {
|
||||
const indexPatterns = await getIndexPatternsFromIds(nextUniqueIndexPatternIds);
|
||||
indexPatterns.forEach((indexPattern) => {
|
||||
indexPattern.fields.forEach(field => {
|
||||
if (field.type === ES_GEO_FIELD_TYPE.GEO_POINT || field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE) {
|
||||
uniqueIndexPatternsAndGeoFields.push({
|
||||
geoField: field.name,
|
||||
geoFieldType: field.type,
|
||||
indexPatternTitle: indexPattern.title,
|
||||
indexPatternId: indexPattern.id
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch(e) {
|
||||
// swallow errors.
|
||||
// the Layer-TOC will indicate which layers are disfunctional on a per-layer basis
|
||||
}
|
||||
|
||||
if (!this._isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ uniqueIndexPatternsAndGeoFields });
|
||||
}
|
||||
|
||||
_renderToolsControl() {
|
||||
const { uniqueIndexPatternsAndGeoFields } = this.state;
|
||||
if (
|
||||
!this.props.isFilterable ||
|
||||
!uniqueIndexPatternsAndGeoFields.length
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<ToolsControl
|
||||
uniqueIndexPatternsAndGeoFields={uniqueIndexPatternsAndGeoFields}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
className="mapToolbarOverlay"
|
||||
responsive={false}
|
||||
direction="column"
|
||||
alignItems="flexEnd"
|
||||
gutterSize="s"
|
||||
>
|
||||
|
||||
<EuiFlexItem>
|
||||
<SetViewControl />
|
||||
</EuiFlexItem>
|
||||
|
||||
{this._renderToolsControl()}
|
||||
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { connect } from 'react-redux';
|
||||
import { ToolsControl } from './tools_control';
|
||||
import { getDrawState } from '../../../selectors/map_selectors';
|
||||
import { updateDrawState } from '../../../actions/store_actions';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
drawState: getDrawState(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
initiateDraw: (options) => {
|
||||
dispatch(updateDrawState(options));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const connectedToolsControl = connect(mapStateToProps, mapDispatchToProps)(ToolsControl);
|
||||
export { connectedToolsControl as ToolsControl };
|
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
* 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, Fragment } from 'react';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiPopover,
|
||||
EuiContextMenu,
|
||||
EuiSelectable,
|
||||
EuiHighlight,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DRAW_TYPE } from '../../../actions/store_actions';
|
||||
|
||||
const RESET_STATE = {
|
||||
isPopoverOpen: false,
|
||||
drawType: null
|
||||
};
|
||||
|
||||
export class ToolsControl extends Component {
|
||||
|
||||
state = {
|
||||
...RESET_STATE
|
||||
};
|
||||
|
||||
_togglePopover = () => {
|
||||
this.setState(prevState => ({
|
||||
...RESET_STATE,
|
||||
isPopoverOpen: !prevState.isPopoverOpen,
|
||||
}));
|
||||
};
|
||||
|
||||
_closePopover = () => {
|
||||
this.setState(RESET_STATE);
|
||||
};
|
||||
|
||||
_onIndexPatternSelection = (options) => {
|
||||
const selection = options.find((option) => option.checked);
|
||||
this._initiateDraw(
|
||||
this.state.drawType,
|
||||
selection.value
|
||||
);
|
||||
};
|
||||
|
||||
_initiateDraw = (drawType, indexContext) => {
|
||||
this.props.initiateDraw({
|
||||
drawType,
|
||||
...indexContext
|
||||
});
|
||||
this._closePopover();
|
||||
};
|
||||
|
||||
_selectPolygonDrawType = () => {
|
||||
this.setState({ drawType: DRAW_TYPE.POLYGON });
|
||||
}
|
||||
|
||||
_selectBoundsDrawType = () => {
|
||||
this.setState({ drawType: DRAW_TYPE.BOUNDS });
|
||||
}
|
||||
|
||||
_getDrawPanels() {
|
||||
|
||||
const needsIndexPatternSelectionPanel = this.props.uniqueIndexPatternsAndGeoFields.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.uniqueIndexPatternsAndGeoFields[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.uniqueIndexPatternsAndGeoFields[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.uniqueIndexPatternsAndGeoFields.map((indexPatternAndGeoField) => {
|
||||
return {
|
||||
label: `${indexPatternAndGeoField.indexPatternTitle} : ${indexPatternAndGeoField.geoField}`,
|
||||
value: indexPatternAndGeoField
|
||||
};
|
||||
});
|
||||
|
||||
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.geoField}
|
||||
</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
|
||||
};
|
||||
}
|
||||
|
||||
_renderToolsButton() {
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
className="mapToolbarOverlay__button"
|
||||
color="text"
|
||||
iconType="wrench"
|
||||
onClick={this._togglePopover}
|
||||
aria-label={i18n.translate('xpack.maps.toolbarOverlay.toolsControlTitle', {
|
||||
defaultMessage: 'Tools',
|
||||
})}
|
||||
title={i18n.translate('xpack.maps.toolbarOverlay.toolsControlTitle', {
|
||||
defaultMessage: 'Tools',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EuiPopover
|
||||
id="contextMenu"
|
||||
button={this._renderToolsButton()}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this._closePopover}
|
||||
panelPaddingSize="none"
|
||||
withTitle
|
||||
anchorPosition="leftUp"
|
||||
>
|
||||
<EuiContextMenu
|
||||
initialPanelId={0}
|
||||
panels={this._getDrawPanels()}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
|
@ -1,282 +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, { Fragment } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonIcon,
|
||||
EuiPopover,
|
||||
EuiContextMenu,
|
||||
EuiSelectable,
|
||||
EuiHighlight,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getIndexPatternsFromIds } from '../../index_pattern_util';
|
||||
import _ from 'lodash';
|
||||
import { DRAW_TYPE } from '../../actions/store_actions';
|
||||
import { ES_GEO_FIELD_TYPE } from '../../../common/constants';
|
||||
|
||||
const RESET_STATE = {
|
||||
isPopoverOpen: false,
|
||||
drawType: null
|
||||
};
|
||||
|
||||
export class ToolbarOverlay extends React.Component {
|
||||
|
||||
|
||||
state = {
|
||||
isPopoverOpen: false,
|
||||
uniqueIndexPatternsAndGeoFields: [],
|
||||
drawType: null
|
||||
};
|
||||
|
||||
_toggleToolbar = () => {
|
||||
if (!this._isMounted) {
|
||||
return;
|
||||
}
|
||||
this.setState(prevState => ({
|
||||
isPopoverOpen: !prevState.isPopoverOpen,
|
||||
drawType: null
|
||||
}));
|
||||
};
|
||||
|
||||
_closePopover = () => {
|
||||
if (!this._isMounted) {
|
||||
return;
|
||||
}
|
||||
this.setState(RESET_STATE);
|
||||
};
|
||||
|
||||
_onIndexPatternSelection = (options) => {
|
||||
if (!this._isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = options.find((option) => option.checked);
|
||||
const drawType = this.state.drawType;
|
||||
this.setState(RESET_STATE, () => {
|
||||
if (drawType) {
|
||||
this.props.initiateDraw({ drawType: drawType, ...selection.value });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
_activateDrawForFirstIndexPattern = (drawType) => {
|
||||
if (!this._isMounted) {
|
||||
return;
|
||||
}
|
||||
const indexPatternAndGeofield = this.state.uniqueIndexPatternsAndGeoFields[0];
|
||||
this.setState(RESET_STATE, () => {
|
||||
this.props.initiateDraw({ drawType: drawType, ...indexPatternAndGeofield });
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
async _getuniqueIndexPatternAndFieldCombos() {
|
||||
try {
|
||||
const indexPatterns = await getIndexPatternsFromIds(this.props.uniqueIndexPatternIds);
|
||||
const uniqueIndexPatternsAndGeofields = [];
|
||||
indexPatterns.forEach((indexPattern) => {
|
||||
indexPattern.fields.forEach(field => {
|
||||
if (field.type === ES_GEO_FIELD_TYPE.GEO_POINT || field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE) {
|
||||
uniqueIndexPatternsAndGeofields.push({
|
||||
geoField: field.name,
|
||||
geoFieldType: field.type,
|
||||
indexPatternTitle: indexPattern.title,
|
||||
indexPatternId: indexPattern.id
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
if (this._isMounted && !_.isEqual(this.state.uniqueIndexPatternsAndGeoFields, uniqueIndexPatternsAndGeofields)) {
|
||||
this.setState({
|
||||
uniqueIndexPatternsAndGeoFields: uniqueIndexPatternsAndGeofields
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
// swallow errors.
|
||||
// the Layer-TOC will indicate which layers are disfunctional on a per-layer basis
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._getuniqueIndexPatternAndFieldCombos();
|
||||
}
|
||||
|
||||
_getDrawActionsPanel() {
|
||||
|
||||
const drawPolygonAction = {
|
||||
name: i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', {
|
||||
defaultMessage: 'Draw shape to filter data',
|
||||
}),
|
||||
};
|
||||
|
||||
const drawBoundsAction = {
|
||||
name: i18n.translate('xpack.maps.toolbarOverlay.drawBoundsLabel', {
|
||||
defaultMessage: 'Draw bounds to filter data',
|
||||
}),
|
||||
};
|
||||
|
||||
if (this.state.uniqueIndexPatternsAndGeoFields.length === 1) {
|
||||
drawPolygonAction.onClick = () => this._activateDrawForFirstIndexPattern(DRAW_TYPE.POLYGON);
|
||||
drawBoundsAction.onClick = () => this._activateDrawForFirstIndexPattern(DRAW_TYPE.BOUNDS);
|
||||
} else {
|
||||
drawPolygonAction.panel = this._getIndexPatternSelectionPanel(1);
|
||||
drawPolygonAction.onClick = () => {
|
||||
this.setState({ drawType: DRAW_TYPE.POLYGON });
|
||||
};
|
||||
drawBoundsAction.panel = this._getIndexPatternSelectionPanel(2);
|
||||
drawBoundsAction.onClick = () => {
|
||||
this.setState({ drawType: DRAW_TYPE.BOUNDS });
|
||||
};
|
||||
}
|
||||
|
||||
return flattenPanelTree({
|
||||
id: 0,
|
||||
title: i18n.translate('xpack.maps.toolbarOverlay.tools.toolbarTitle', {
|
||||
defaultMessage: 'Tools',
|
||||
}),
|
||||
items: [drawPolygonAction, drawBoundsAction]
|
||||
});
|
||||
}
|
||||
|
||||
_getIndexPatternSelectionPanel(id) {
|
||||
const options = this.state.uniqueIndexPatternsAndGeoFields.map((indexPatternAndGeoField) => {
|
||||
return {
|
||||
label: `${indexPatternAndGeoField.indexPatternTitle} : ${indexPatternAndGeoField.geoField}`,
|
||||
value: indexPatternAndGeoField
|
||||
};
|
||||
});
|
||||
|
||||
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.geoField}
|
||||
</EuiHighlight>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const indexPatternSelection = (
|
||||
<EuiSelectable
|
||||
searchable
|
||||
searchProps={{
|
||||
placeholder: i18n.translate('xpack.maps.toolbarOverlay.indexPattern.filterListTitle', {
|
||||
defaultMessage: 'Filter list',
|
||||
}),
|
||||
compressed: true,
|
||||
}}
|
||||
options={options}
|
||||
/**
|
||||
* *TODO*: FIX this handler as EuiSelectable passes back the full options
|
||||
* list with the selected option set with `checked: 'on'`
|
||||
*/
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
_renderToolbarButton() {
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
className="mapToolbarOverlay__button"
|
||||
color="text"
|
||||
iconType="wrench"
|
||||
onClick={this._toggleToolbar}
|
||||
aria-label={i18n.translate('xpack.maps.toolbarOverlay.toolbarIconTitle', {
|
||||
defaultMessage: 'Tools',
|
||||
})}
|
||||
title={i18n.translate('xpack.maps.toolbarOverlay.toolbarIconTitle', {
|
||||
defaultMessage: 'Tools',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
if (
|
||||
!this.props.isFilterable ||
|
||||
!this.state.uniqueIndexPatternsAndGeoFields.length
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup className="mapToolbarOverlay" responsive={false} direction="row" alignItems="flexEnd" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiPopover
|
||||
id="contextMenu"
|
||||
button={this._renderToolbarButton()}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this._closePopover}
|
||||
panelPaddingSize="none"
|
||||
withTitle
|
||||
anchorPosition="leftUp"
|
||||
>
|
||||
<EuiContextMenu
|
||||
initialPanelId={0}
|
||||
panels={this._getDrawActionsPanel()}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
|
@ -11,8 +11,7 @@
|
|||
}
|
||||
|
||||
.mapLayerControl__addLayerButton,
|
||||
.mapLayerControl__openLayerTOCButton,
|
||||
.mapViewControl__gotoButton {
|
||||
.mapLayerControl__openLayerTOCButton {
|
||||
pointer-events: all;
|
||||
|
||||
&:enabled,
|
||||
|
@ -23,8 +22,7 @@
|
|||
}
|
||||
|
||||
.mapLayerControl__openLayerTOCButton,
|
||||
.mapLayerControl__closeLayerTOCButton,
|
||||
.mapViewControl__gotoButton {
|
||||
.mapLayerControl__closeLayerTOCButton {
|
||||
@include size($euiSizeXL);
|
||||
background-color: $euiColorEmptyShade !important; // During all states
|
||||
}
|
||||
|
|
|
@ -1,16 +1,5 @@
|
|||
/**
|
||||
* 1. The overlay captures mouse events even if it's empty space. To counter-act this,
|
||||
* we remove all pointer events from the overlay then add them back on the
|
||||
* individual widgets.
|
||||
*/
|
||||
|
||||
.mapViewControl__coordinates {
|
||||
@include mapOverlayIsTextOnly;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mapViewControl__gotoButton {
|
||||
min-width: 0;
|
||||
pointer-events: all; /* 1 */
|
||||
}
|
||||
|
|
|
@ -6,30 +6,14 @@
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
import { ViewControl } from './view_control';
|
||||
import { getMouseCoordinates } from '../../../selectors/map_selectors';
|
||||
import {
|
||||
getIsSetViewOpen,
|
||||
closeSetView,
|
||||
openSetView,
|
||||
} from '../../../store/ui';
|
||||
import { getMouseCoordinates, getMapZoom } from '../../../selectors/map_selectors';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
isSetViewOpen: getIsSetViewOpen(state),
|
||||
mouseCoordinates: getMouseCoordinates(state),
|
||||
zoom: getMapZoom(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
closeSetView: () => {
|
||||
dispatch(closeSetView());
|
||||
},
|
||||
openSetView: () => {
|
||||
dispatch(openSetView());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const connectedViewControl = connect(mapStateToProps, mapDispatchToProps)(ViewControl);
|
||||
const connectedViewControl = connect(mapStateToProps, null)(ViewControl);
|
||||
export { connectedViewControl as ViewControl };
|
||||
|
|
|
@ -1,30 +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 { connect } from 'react-redux';
|
||||
import { SetView } from './set_view';
|
||||
import { setGotoWithCenter } from '../../../../actions/store_actions';
|
||||
import { getMapZoom, getMapCenter } from '../../../../selectors/map_selectors';
|
||||
import { closeSetView } from '../../../../store/ui';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
zoom: getMapZoom(state),
|
||||
center: getMapCenter(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onSubmit: ({ lat, lon, zoom }) => {
|
||||
dispatch(closeSetView());
|
||||
dispatch(setGotoWithCenter({ lat, lon, zoom }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const connectedSetView = connect(mapStateToProps, mapDispatchToProps, null, { withRef: true })(SetView);
|
||||
export { connectedSetView as SetView };
|
|
@ -6,95 +6,39 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonIcon,
|
||||
EuiPopover,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { SetView } from './set_view';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { DECIMAL_DEGREES_PRECISION } from '../../../../common/constants';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export function ViewControl({ isSetViewOpen, closeSetView, openSetView, mouseCoordinates }) {
|
||||
const toggleSetViewVisibility = () => {
|
||||
if (isSetViewOpen) {
|
||||
closeSetView();
|
||||
return;
|
||||
}
|
||||
|
||||
openSetView();
|
||||
};
|
||||
const setView = (
|
||||
<EuiPopover
|
||||
anchorPosition="upRight"
|
||||
button={(
|
||||
<EuiButtonIcon
|
||||
className="mapViewControl__gotoButton"
|
||||
onClick={toggleSetViewVisibility}
|
||||
data-test-subj="toggleSetViewVisibilityButton"
|
||||
iconType="crosshairs"
|
||||
color="text"
|
||||
aria-label={i18n.translate('xpack.maps.viewControl.goToButtonLabel', {
|
||||
defaultMessage: 'Go to'
|
||||
})}
|
||||
title={i18n.translate('xpack.maps.viewControl.goToButtonLabel', {
|
||||
defaultMessage: 'Go to'
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
isOpen={isSetViewOpen}
|
||||
closePopover={closeSetView}
|
||||
>
|
||||
<SetView />
|
||||
</EuiPopover>
|
||||
);
|
||||
|
||||
function renderMouseCoordinates() {
|
||||
const lat = mouseCoordinates
|
||||
? _.round(mouseCoordinates.lat, DECIMAL_DEGREES_PRECISION)
|
||||
: '';
|
||||
const lon = mouseCoordinates
|
||||
? _.round(mouseCoordinates.lon, DECIMAL_DEGREES_PRECISION)
|
||||
: '';
|
||||
return (
|
||||
<div className="mapViewControl__coordinates">
|
||||
<EuiText size="xs">
|
||||
<small>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.viewControl.latLabel"
|
||||
defaultMessage="lat:"
|
||||
/>
|
||||
</strong> {lat},{' '}
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.viewControl.lonLabel"
|
||||
defaultMessage="lon:"
|
||||
/>
|
||||
</strong> {lon}
|
||||
</small>
|
||||
</EuiText>
|
||||
</div>
|
||||
);
|
||||
export function ViewControl({ mouseCoordinates, zoom }) {
|
||||
if (!mouseCoordinates) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexEnd"
|
||||
alignItems="flexEnd"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
{mouseCoordinates && renderMouseCoordinates()}
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
{setView}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<div className="mapViewControl__coordinates">
|
||||
<EuiText size="xs">
|
||||
<small>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.viewControl.latLabel"
|
||||
defaultMessage="lat:"
|
||||
/>
|
||||
</strong> {_.round(mouseCoordinates.lat, DECIMAL_DEGREES_PRECISION)},{' '}
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.viewControl.lonLabel"
|
||||
defaultMessage="lon:"
|
||||
/>
|
||||
</strong> {_.round(mouseCoordinates.lon, DECIMAL_DEGREES_PRECISION)},{' '}
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.viewControl.zoomLabel"
|
||||
defaultMessage="zoom:"
|
||||
/>
|
||||
</strong> {zoom}
|
||||
</small>
|
||||
</EuiText>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { indexPatternService } from './kibana_services';
|
||||
|
||||
export async function getIndexPatternsFromIds(indexPatternIds) {
|
||||
export async function getIndexPatternsFromIds(indexPatternIds = []) {
|
||||
|
||||
const promises = [];
|
||||
indexPatternIds.forEach((id) => {
|
||||
|
|
|
@ -187,7 +187,7 @@ export const getUniqueIndexPatternIds = createSelector(
|
|||
layerList.forEach(layer => {
|
||||
indexPatternIds.push(...layer.getIndexPatternIds());
|
||||
});
|
||||
return _.uniq(indexPatternIds);
|
||||
return _.uniq(indexPatternIds).sort();
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ const INITIAL_STATE = {
|
|||
isReadOnly: false,
|
||||
isLayerTOCOpen: DEFAULT_IS_LAYER_TOC_OPEN,
|
||||
isFilterable: false,
|
||||
isSetViewOpen: false,
|
||||
// storing TOC detail visibility outside of map.layerList because its UI state and not map rendering state.
|
||||
// This also makes for easy read/write access for embeddables.
|
||||
openTOCDetails: [],
|
||||
|
|
|
@ -5452,7 +5452,6 @@
|
|||
"xpack.maps.toolbarOverlay.drawShapeLabel": "シェイプを描いてデータをフィルタリング",
|
||||
"xpack.maps.toolbarOverlay.geofield.toolbarTitle": "ジオフィールドを選択",
|
||||
"xpack.maps.toolbarOverlay.indexPattern.filterListTitle": "フィルターリスト",
|
||||
"xpack.maps.toolbarOverlay.toolbarIconTitle": "ツール",
|
||||
"xpack.maps.toolbarOverlay.tools.toolbarTitle": "ツール",
|
||||
"xpack.maps.tooltip.closeAriaLabel": "ツールヒントを閉じる",
|
||||
"xpack.maps.tooltip.filterOnPropertyAriaLabel": "プロパティのフィルター",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue