diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.action_global_apply_filter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.action_global_apply_filter.md new file mode 100644 index 0000000000000..14075ba1beba0 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.action_global_apply_filter.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ACTION\_GLOBAL\_APPLY\_FILTER](./kibana-plugin-plugins-data-public.action_global_apply_filter.md) + +## ACTION\_GLOBAL\_APPLY\_FILTER variable + +Signature: + +```typescript +ACTION_GLOBAL_APPLY_FILTER = "ACTION_GLOBAL_APPLY_FILTER" +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index a5453c7c51d5b..09702df4fdb54 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -89,6 +89,7 @@ | Variable | Description | | --- | --- | +| [ACTION\_GLOBAL\_APPLY\_FILTER](./kibana-plugin-plugins-data-public.action_global_apply_filter.md) | | | [AggGroupLabels](./kibana-plugin-plugins-data-public.agggrouplabels.md) | | | [AggGroupNames](./kibana-plugin-plugins-data-public.agggroupnames.md) | | | [baseFormattersPublic](./kibana-plugin-plugins-data-public.baseformatterspublic.md) | | diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index eb5703f1c63c1..27b16c57ffecf 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -440,7 +440,7 @@ export { export { isTimeRange, isQuery, isFilter, isFilters } from '../common'; -export { ApplyGlobalFilterActionContext } from './actions'; +export { ACTION_GLOBAL_APPLY_FILTER, ApplyGlobalFilterActionContext } from './actions'; export * from '../common/field_mapping'; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 9a2a82e8ed206..de4ec58dfdab3 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -78,6 +78,11 @@ import { UnregisterCallback } from 'history'; import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; import { UserProvidedValues } from 'src/core/server/types'; +// Warning: (ae-missing-release-tag) "ACTION_GLOBAL_APPLY_FILTER" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const ACTION_GLOBAL_APPLY_FILTER = "ACTION_GLOBAL_APPLY_FILTER"; + // Warning: (ae-forgotten-export) The symbol "AggConfigSerialized" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "AggConfigOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts index ed0656a2fc265..d064dfb1c4a37 100644 --- a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts +++ b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts @@ -53,6 +53,7 @@ export type TooltipState = { }; export type DrawState = { + actionId: string; drawType: DRAW_TYPE; filterLabel?: string; // point radius filter alias geoFieldName?: string; diff --git a/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap b/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap index 85a073c8d9ace..2d39a52dfe974 100644 --- a/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap +++ b/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap @@ -38,6 +38,10 @@ exports[`should not render relation select when geo field is geo_point 1`] = ` } } /> + @@ -121,6 +125,10 @@ exports[`should not show "within" relation when filter geometry is not closed 1` value="INTERSECTS" /> + @@ -177,6 +185,10 @@ exports[`should render error message 1`] = ` } } /> + @@ -267,6 +279,10 @@ exports[`should render relation select when geo field is geo_shape 1`] = ` value="INTERSECTS" /> + diff --git a/x-pack/plugins/maps/public/components/_action_select.scss b/x-pack/plugins/maps/public/components/_action_select.scss new file mode 100644 index 0000000000000..be903ad7d6962 --- /dev/null +++ b/x-pack/plugins/maps/public/components/_action_select.scss @@ -0,0 +1,3 @@ +.mapActionSelectIcon { + margin-right: $euiSizeS; +} diff --git a/x-pack/plugins/maps/public/components/_index.scss b/x-pack/plugins/maps/public/components/_index.scss index 76e27338bdcd4..76ce9f1bc79e3 100644 --- a/x-pack/plugins/maps/public/components/_index.scss +++ b/x-pack/plugins/maps/public/components/_index.scss @@ -1,3 +1,4 @@ +@import 'action_select'; @import 'metric_editors'; @import './geometry_filter'; @import 'tooltip_selector/tooltip_selector'; diff --git a/x-pack/plugins/maps/public/components/action_select.tsx b/x-pack/plugins/maps/public/components/action_select.tsx new file mode 100644 index 0000000000000..ad61a6a129974 --- /dev/null +++ b/x-pack/plugins/maps/public/components/action_select.tsx @@ -0,0 +1,87 @@ +/* + * 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 { EuiFormRow, EuiSuperSelect, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; + +interface Props { + value?: string; + onChange: (value: string) => void; + getFilterActions?: () => Promise; + getActionContext?: () => ActionExecutionContext; +} + +interface State { + actions: Action[]; +} + +export class ActionSelect extends Component { + private _isMounted = false; + state: State = { + actions: [], + }; + + componentDidMount() { + this._isMounted = true; + this._loadActions(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + async _loadActions() { + if (!this.props.getFilterActions || !this.props.getActionContext) { + return; + } + const actions = await this.props.getFilterActions(); + if (this._isMounted) { + this.setState({ actions }); + } + } + + render() { + if (this.state.actions.length === 0 || !this.props.getActionContext) { + return null; + } + + if (this.state.actions.length === 1 && this.props.value === this.state.actions[0].id) { + return null; + } + + const actionContext = this.props.getActionContext(); + const options = this.state.actions.map((action) => { + const iconType = action.getIconType(actionContext); + return { + value: action.id, + inputDisplay: ( +
+ {iconType ? : null} + {action.getDisplayName(actionContext)} +
+ ), + }; + }); + + return ( + + + + ); + } +} diff --git a/x-pack/plugins/maps/public/components/distance_filter_form.tsx b/x-pack/plugins/maps/public/components/distance_filter_form.tsx index 768be1558bd69..24d9aec5b77b4 100644 --- a/x-pack/plugins/maps/public/components/distance_filter_form.tsx +++ b/x-pack/plugins/maps/public/components/distance_filter_form.tsx @@ -14,18 +14,25 @@ import { EuiTextAlign, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select'; import { GeoFieldWithIndex } from './geo_field_with_index'; +import { ActionSelect } from './action_select'; +import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public'; interface Props { className?: string; buttonLabel: string; geoFields: GeoFieldWithIndex[]; + getFilterActions?: () => Promise; + getActionContext?: () => ActionExecutionContext; onSubmit: ({ + actionId, filterLabel, indexPatternId, geoFieldName, }: { + actionId: string; filterLabel: string; indexPatternId: string; geoFieldName: string; @@ -33,12 +40,14 @@ interface Props { } interface State { + actionId: string; selectedField: GeoFieldWithIndex | undefined; filterLabel: string; } export class DistanceFilterForm extends Component { - state = { + state: State = { + actionId: ACTION_GLOBAL_APPLY_FILTER, selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined, filterLabel: '', }; @@ -53,11 +62,16 @@ export class DistanceFilterForm extends Component { }); }; + _onActionIdChange = (value: string) => { + this.setState({ actionId: value }); + }; + _onSubmit = () => { if (!this.state.selectedField) { return; } this.props.onSubmit({ + actionId: this.state.actionId, filterLabel: this.state.filterLabel, indexPatternId: this.state.selectedField.indexPatternId, geoFieldName: this.state.selectedField.geoFieldName, @@ -86,6 +100,13 @@ export class DistanceFilterForm extends Component { onChange={this._onGeoFieldChange} /> + + diff --git a/x-pack/plugins/maps/public/components/geometry_filter_form.js b/x-pack/plugins/maps/public/components/geometry_filter_form.js index d5cdda3c1c324..fde07e8c16bc5 100644 --- a/x-pack/plugins/maps/public/components/geometry_filter_form.js +++ b/x-pack/plugins/maps/public/components/geometry_filter_form.js @@ -20,11 +20,15 @@ import { i18n } from '@kbn/i18n'; import { ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../common/constants'; import { getEsSpatialRelationLabel } from '../../common/i18n_getters'; import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select'; +import { ActionSelect } from './action_select'; +import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public'; export class GeometryFilterForm extends Component { static propTypes = { buttonLabel: PropTypes.string.isRequired, geoFields: PropTypes.array.isRequired, + getFilterActions: PropTypes.func, + getActionContext: PropTypes.func, intitialGeometryLabel: PropTypes.string.isRequired, onSubmit: PropTypes.func.isRequired, isFilterGeometryClosed: PropTypes.bool, @@ -36,6 +40,7 @@ export class GeometryFilterForm extends Component { }; state = { + actionId: ACTION_GLOBAL_APPLY_FILTER, selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined, geometryLabel: this.props.intitialGeometryLabel, relation: ES_SPATIAL_RELATIONS.INTERSECTS, @@ -57,8 +62,13 @@ export class GeometryFilterForm extends Component { }); }; + _onActionIdChange = (value) => { + this.setState({ actionId: value }); + }; + _onSubmit = () => { this.props.onSubmit({ + actionId: this.state.actionId, geometryLabel: this.state.geometryLabel, indexPatternId: this.state.selectedField.indexPatternId, geoFieldName: this.state.selectedField.geoFieldName, @@ -134,6 +144,13 @@ export class GeometryFilterForm extends Component { {this._renderRelationInput()} + + {error} diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap index 3b3d82c92fbb7..29df06a64a3f2 100644 --- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap @@ -1,11 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FeatureProperties should not show filter button 1`] = ` +exports[`FeatureProperties should render 1`] = `
+
+
@@ -56,12 +60,13 @@ exports[`FeatureProperties should show error message if unable to load tooltip c `; -exports[`FeatureProperties should show only filter button for filterable properties 1`] = ` +exports[`FeatureProperties should show filter button for filterable properties 1`] = ` + +
- + > + +
+ +
+`; + +exports[`FeatureProperties should show view actions button when there are available actions 1`] = ` + + + + + + + + +
+ prop1 + + + + + + + + + + +
+ prop2 + +
diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/_index.scss b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/_index.scss index fb75cc1e2db69..abd747c8fa47a 100644 --- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/_index.scss +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/_index.scss @@ -1,6 +1,5 @@ .mapFeatureTooltip_table { width: 100%; - display: block; max-height: calc(49vh - #{$euiSizeXL * 2}); td { @@ -8,6 +7,10 @@ } } +.mapFeatureTooltip_row { + border-bottom: 1px solid $euiColorLightestShade; +} + .mapFeatureTooltip_actionLinks { padding: $euiSizeXS; } @@ -20,3 +23,10 @@ max-width: $euiSizeXL * 4; font-weight: $euiFontWeightSemiBold; } + +.mapFeatureTooltip_actionsRow { + > span { + display: flex; + justify-content: flex-end; + } +} diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js index b0ce52b4db7ab..98267965fd30f 100644 --- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component, Fragment } from 'react'; -import { EuiIcon } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import { URL_MAX_LENGTH } from '../../../../../../../src/core/public'; @@ -95,28 +93,7 @@ export class FeatureGeometryFilterForm extends Component { this.props.onClose(); }; - _renderHeader() { - return ( - - ); - } - - _renderForm() { + render() { return ( ); } - - render() { - return ( - - {this._renderHeader()} - {this._renderForm()} - - ); - } } diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js index 5e2a153b2ccbf..edd501f266690 100644 --- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js @@ -5,12 +5,21 @@ */ import React from 'react'; -import { EuiCallOut, EuiLoadingSpinner, EuiTextAlign, EuiButtonIcon } from '@elastic/eui'; +import { + EuiCallOut, + EuiLoadingSpinner, + EuiTextAlign, + EuiButtonEmpty, + EuiIcon, + EuiContextMenu, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../../../src/plugins/data/public'; export class FeatureProperties extends React.Component { state = { properties: null, + actions: [], loadPropertiesErrorMsg: null, prevWidth: null, prevHeight: null, @@ -21,6 +30,7 @@ export class FeatureProperties extends React.Component { this.prevLayerId = undefined; this.prevFeatureId = undefined; this._loadProperties(); + this._loadActions(); } componentDidUpdate() { @@ -31,6 +41,16 @@ export class FeatureProperties extends React.Component { this._isMounted = false; } + async _loadActions() { + if (!this.props.getFilterActions) { + return; + } + const actions = await this.props.getFilterActions(); + if (this._isMounted) { + this.setState({ actions }); + } + } + _loadProperties = async () => { this._fetchProperties({ nextFeatureId: this.props.featureId, @@ -39,6 +59,10 @@ export class FeatureProperties extends React.Component { }); }; + _showFilterActions = (tooltipProperty) => { + this.props.showFilterActions(this._renderFilterActions(tooltipProperty)); + }; + _fetchProperties = async ({ nextLayerId, nextFeatureId, mbProperties }) => { if (this.prevLayerId === nextLayerId && this.prevFeatureId === nextFeatureId) { // do not reload same feature properties @@ -83,35 +107,108 @@ export class FeatureProperties extends React.Component { } if (this._isMounted) { - this.setState({ - properties, - }); + this.setState({ properties }); } }; + _renderFilterActions(tooltipProperty) { + const panel = { + id: 0, + items: this.state.actions.map((action) => { + const actionContext = this.props.getActionContext(); + const iconType = action.getIconType(actionContext); + const name = action.getDisplayName(actionContext); + return { + name, + icon: iconType ? : null, + onClick: async () => { + this.props.onCloseTooltip(); + const filters = await tooltipProperty.getESFilters(); + this.props.addFilters(filters, action.id); + }, + ['data-test-subj']: `mapFilterActionButton__${name}`, + }; + }), + }; + + return ( +
+ (this._node = node)} + > + + + + + +
+ {tooltipProperty.getPropertyName()} + +
+ +
+ ); + } + _renderFilterCell(tooltipProperty) { if (!this.props.showFilterButtons || !tooltipProperty.isFilterable()) { - return null; + return ; } - return ( - - { - this.props.onCloseTooltip(); - const filters = await tooltipProperty.getESFilters(); - this.props.addFilters(filters); - }} - aria-label={i18n.translate('xpack.maps.tooltip.filterOnPropertyAriaLabel', { - defaultMessage: 'Filter on property', - })} - data-test-subj="mapTooltipCreateFilterButton" - /> + const applyFilterButton = ( + { + this.props.onCloseTooltip(); + const filters = await tooltipProperty.getESFilters(); + this.props.addFilters(filters); + }} + aria-label={i18n.translate('xpack.maps.tooltip.filterOnPropertyAriaLabel', { + defaultMessage: 'Filter on property', + })} + data-test-subj="mapTooltipCreateFilterButton" + > + + + ); + + return this.state.actions.length === 0 || + (this.state.actions.length === 1 && + this.state.actions[0].id === ACTION_GLOBAL_APPLY_FILTER) ? ( + {applyFilterButton} + ) : ( + + + {applyFilterButton} + { + this._showFilterActions(tooltipProperty); + }} + aria-label={i18n.translate('xpack.maps.tooltip.viewActionsTitle', { + defaultMessage: 'View filter actions', + })} + data-test-subj="mapTooltipMoreActionsButton" + > + + + ); } @@ -154,7 +251,7 @@ export class FeatureProperties extends React.Component { const rows = this.state.properties.map((tooltipProperty) => { const label = tooltipProperty.getPropertyName(); return ( - + {label} {}, showFilterButtons: false, + getFilterActions: () => { + return [{ id: ACTION_GLOBAL_APPLY_FILTER }]; + }, }; const mockTooltipProperties = [ @@ -44,10 +48,29 @@ const mockTooltipProperties = [ ]; describe('FeatureProperties', () => { - test('should not show filter button', async () => { + test('should render', async () => { + const component = shallow( + { + return mockTooltipProperties; + }} + /> + ); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); + + test('should show filter button for filterable properties', async () => { const component = shallow( { return mockTooltipProperties; }} @@ -62,7 +85,7 @@ describe('FeatureProperties', () => { expect(component).toMatchSnapshot(); }); - test('should show only filter button for filterable properties', async () => { + test('should show view actions button when there are available actions', async () => { const component = shallow( { loadFeatureProperties={() => { return mockTooltipProperties; }} + getFilterActions={() => { + return [{ id: 'drilldown1' }]; + }} /> ); diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js index d91bc8e803ab9..8547219b42e30 100644 --- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js @@ -4,20 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; -import { EuiLink } from '@elastic/eui'; +import React, { Component, Fragment } from 'react'; +import { EuiIcon, EuiLink } from '@elastic/eui'; import { FeatureProperties } from './feature_properties'; -import { FormattedMessage } from '@kbn/i18n/react'; import { GEO_JSON_TYPE, ES_GEO_FIELD_TYPE } from '../../../../common/constants'; import { FeatureGeometryFilterForm } from './feature_geometry_filter_form'; import { TooltipHeader } from './tooltip_header'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; const VIEWS = { PROPERTIES_VIEW: 'PROPERTIES_VIEW', GEOMETRY_FILTER_VIEW: 'GEOMETRY_FILTER_VIEW', + FILTER_ACTIONS_VIEW: 'FILTER_ACTIONS_VIEW', }; -export class FeaturesTooltip extends React.Component { +export class FeaturesTooltip extends Component { state = {}; static getDerivedStateFromProps(nextProps, prevState) { @@ -41,7 +43,11 @@ export class FeaturesTooltip extends React.Component { }; _showPropertiesView = () => { - this.setState({ view: VIEWS.PROPERTIES_VIEW }); + this.setState({ view: VIEWS.PROPERTIES_VIEW, filterView: null }); + }; + + _showFilterActionsView = (filterView) => { + this.setState({ view: VIEWS.FILTER_ACTIONS_VIEW, filterView }); }; _renderActions(geoFields) { @@ -96,6 +102,22 @@ export class FeaturesTooltip extends React.Component { }); }; + _renderBackButton(label) { + return ( + + ); + } + render() { if (!this.state.currentFeature) { return null; @@ -109,14 +131,36 @@ export class FeaturesTooltip extends React.Component { if (this.state.view === VIEWS.GEOMETRY_FILTER_VIEW && currentFeatureGeometry) { return ( - + + {this._renderBackButton( + i18n.translate('xpack.maps.tooltip.showGeometryFilterViewLinkLabel', { + defaultMessage: 'Filter by geometry', + }) + )} + + + ); + } + + if (this.state.view === VIEWS.FILTER_ACTIONS_VIEW) { + return ( + + {this._renderBackButton( + i18n.translate('xpack.maps.tooltip.showAddFilterActionsViewLabel', { + defaultMessage: 'Filter actions', + }) + )} + {this.state.filterView} + ); } @@ -137,6 +181,9 @@ export class FeaturesTooltip extends React.Component { showFilterButtons={!!this.props.addFilters && this.props.isLocked} onCloseTooltip={this.props.closeTooltip} addFilters={this.props.addFilters} + getFilterActions={this.props.getFilterActions} + getActionContext={this.props.getActionContext} + showFilterActions={this._showFilterActionsView} /> {this._renderActions(geoFields)} diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js index 6de936fa4a8f1..49675ac6a3924 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js @@ -62,11 +62,12 @@ export class DrawControl extends React.Component { } }, 0); - _onDraw = (e) => { + _onDraw = async (e) => { if (!e.features.length) { return; } + let filter; if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { const circle = e.features[0]; const distanceKm = _.round( @@ -82,7 +83,7 @@ export class DrawControl extends React.Component { } else if (distanceKm <= 100) { precision = 3; } - const filter = createDistanceFilterWithMeta({ + filter = createDistanceFilterWithMeta({ alias: this.props.drawState.filterLabel, distanceKm, geoFieldName: this.props.drawState.geoFieldName, @@ -92,17 +93,12 @@ export class DrawControl extends React.Component { _.round(circle.properties.center[1], precision), ], }); - 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); + } else { + const geometry = e.features[0].geometry; + // MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number + roundCoordinates(geometry.coordinates); - try { - const filter = createSpatialFilterWithGeometry({ + filter = createSpatialFilterWithGeometry({ geometry: this.props.drawState.drawType === DRAW_TYPE.BOUNDS ? getBoundingBoxGeometry(geometry) @@ -113,7 +109,10 @@ export class DrawControl extends React.Component { geometryLabel: this.props.drawState.geometryLabel, relation: this.props.drawState.relation, }); - this.props.addFilters([filter]); + } + + try { + await this.props.addFilters([filter], this.props.drawState.actionId); } catch (error) { // TODO notify user why filter was not created console.error(error); diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js index 84a29db852539..87d6f8e1d8e71 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js @@ -195,6 +195,8 @@ export class TooltipControl extends React.Component { mbMap={this.props.mbMap} layerList={this.props.layerList} addFilters={this.props.addFilters} + getFilterActions={this.props.getFilterActions} + getActionContext={this.props.getActionContext} renderTooltipContent={this.props.renderTooltipContent} geoFields={this.props.geoFields} features={features} diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js index 6c42057680408..4cfddf0034039 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js @@ -117,6 +117,8 @@ export class TooltipPopover extends Component { _renderTooltipContent = () => { const publicProps = { addFilters: this.props.addFilters, + getFilterActions: this.props.getFilterActions, + getActionContext: this.props.getActionContext, closeTooltip: this.props.closeTooltip, features: this.props.features, isLocked: this.props.isLocked, diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/plugins/maps/public/connected_components/map/mb/view.js index 5a38f6039ae4b..22c374aceedd5 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/view.js @@ -309,6 +309,8 @@ export class MBMap extends React.Component { diff --git a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx index beb1eb0947c50..bf75c86ac249d 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx @@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { Filter } from 'src/plugins/data/public'; +import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; // @ts-expect-error import { MBMap } from '../map/mb'; // @ts-expect-error @@ -35,7 +36,9 @@ import 'mapbox-gl/dist/mapbox-gl.css'; const RENDER_COMPLETE_EVENT = 'renderComplete'; interface Props { - addFilters: ((filters: Filter[]) => void) | null; + addFilters: ((filters: Filter[]) => Promise) | null; + getFilterActions?: () => Promise; + getActionContext?: () => ActionExecutionContext; areLayersLoaded: boolean; cancelAllInFlightRequests: () => void; exitFullScreen: () => void; @@ -183,6 +186,8 @@ export class MapContainer extends Component { render() { const { addFilters, + getFilterActions, + getActionContext, flyoutDisplay, isFullScreen, exitFullScreen, @@ -230,11 +235,18 @@ export class MapContainer extends Component { {!this.props.hideToolbarOverlay && ( - + )} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js index a4f85163512f7..a9dc3f822060c 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js @@ -12,14 +12,18 @@ import { FitToData } from './fit_to_data'; export class ToolbarOverlay extends React.Component { _renderToolsControl() { - const { addFilters, geoFields } = this.props; + const { addFilters, geoFields, getFilterActions, getActionContext } = this.props; if (!addFilters || !geoFields.length) { return null; } return ( - + ); } diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js index a06def086b861..017f0369e0b73 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js @@ -123,6 +123,8 @@ export class ToolsControl extends Component { className="mapDrawControl__geometryFilterForm" buttonLabel={DRAW_SHAPE_LABEL_SHORT} geoFields={this.props.geoFields} + getFilterActions={this.props.getFilterActions} + getActionContext={this.props.getActionContext} intitialGeometryLabel={i18n.translate( 'xpack.maps.toolbarOverlay.drawShape.initialGeometryLabel', { @@ -141,6 +143,8 @@ export class ToolsControl extends Component { className="mapDrawControl__geometryFilterForm" buttonLabel={DRAW_BOUNDS_LABEL_SHORT} geoFields={this.props.geoFields} + getFilterActions={this.props.getFilterActions} + getActionContext={this.props.getActionContext} intitialGeometryLabel={i18n.translate( 'xpack.maps.toolbarOverlay.drawBounds.initialGeometryLabel', { @@ -161,6 +165,8 @@ export class ToolsControl extends Component { geoFields={this.props.geoFields.filter(({ geoFieldType }) => { return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT; })} + getFilterActions={this.props.getFilterActions} + getActionContext={this.props.getActionContext} onSubmit={this._initiateDistanceDraw} /> ), diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 43ff274b1353f..1cb393bede956 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -11,7 +11,12 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { Subscription } from 'rxjs'; import { Unsubscribe } from 'redux'; import { Embeddable, IContainer } from '../../../../../src/plugins/embeddable/public'; -import { APPLY_FILTER_TRIGGER } from '../../../../../src/plugins/ui_actions/public'; +import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public'; +import { + APPLY_FILTER_TRIGGER, + ActionExecutionContext, + TriggerContextMapping, +} from '../../../../../src/plugins/ui_actions/public'; import { esFilters, TimeRange, @@ -99,6 +104,10 @@ export class MapEmbeddable extends Embeddable this.onContainerStateChanged(input)); } + supportedTriggers(): Array { + return [APPLY_FILTER_TRIGGER]; + } + setRenderTooltipContent = (renderTooltipContent: RenderToolTipContent) => { this._renderTooltipContent = renderTooltipContent; }; @@ -226,6 +235,8 @@ export class MapEmbeddable extends Embeddable @@ -243,13 +254,36 @@ export class MapEmbeddable extends Embeddable(replaceLayerList(this._layerList)); } - addFilters = (filters: Filter[]) => { - getUiActions().executeTriggerActions(APPLY_FILTER_TRIGGER, { - embeddable: this, + addFilters = async (filters: Filter[], actionId: string = ACTION_GLOBAL_APPLY_FILTER) => { + const executeContext = { + ...this.getActionContext(), filters, + }; + const action = getUiActions().getAction(actionId); + if (!action) { + throw new Error('Unable to apply filter, could not locate action'); + } + action.execute(executeContext); + }; + + getFilterActions = async () => { + return await getUiActions().getTriggerCompatibleActions(APPLY_FILTER_TRIGGER, { + embeddable: this, + filters: [], }); }; + getActionContext = () => { + const trigger = getUiActions().getTrigger(APPLY_FILTER_TRIGGER); + if (!trigger) { + throw new Error('Unable to get context, could not locate trigger'); + } + return { + embeddable: this, + trigger, + } as ActionExecutionContext; + }; + destroy() { super.destroy(); if (this._unsubscribeFromStore) { diff --git a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js index a996910d4787a..10754d20118e9 100644 --- a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js +++ b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js @@ -13,31 +13,62 @@ export default function ({ getPageObjects, getService }) { const filterBar = getService('filterBar'); describe('tooltip filter actions', () => { - before(async () => { + async function loadDashboardAndOpenTooltip() { await kibanaServer.uiSettings.replace({ defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', }); await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.dashboard.loadSavedDashboard('dash for tooltip filter action test'); await PageObjects.maps.lockTooltipAtPosition(200, -200); - }); + } + + describe('apply filter to current view', () => { + before(async () => { + await loadDashboardAndOpenTooltip(); + }); + + it('should display create filter button when tooltip is locked', async () => { + const exists = await testSubjects.exists('mapTooltipCreateFilterButton'); + expect(exists).to.be(true); + }); + + it('should create filters when create filter button is clicked', async () => { + await testSubjects.click('mapTooltipCreateFilterButton'); + await testSubjects.click('applyFiltersPopoverButton'); + + // TODO: Fix me #64861 + // const hasSourceFilter = await filterBar.hasFilter('name', 'charlie'); + // expect(hasSourceFilter).to.be(true); - it('should display create filter button when tooltip is locked', async () => { - const exists = await testSubjects.exists('mapTooltipCreateFilterButton'); - expect(exists).to.be(true); + const hasJoinFilter = await filterBar.hasFilter('shape_name', 'charlie'); + expect(hasJoinFilter).to.be(true); + }); }); - it('should create filters when create filter button is clicked', async () => { - await testSubjects.click('mapTooltipCreateFilterButton'); - await testSubjects.click('applyFiltersPopoverButton'); + describe('panel actions', () => { + before(async () => { + await loadDashboardAndOpenTooltip(); + }); + + it('should display more actions button when tooltip is locked', async () => { + const exists = await testSubjects.exists('mapTooltipMoreActionsButton'); + expect(exists).to.be(true); + }); + + it('should trigger drilldown action when clicked', async () => { + await testSubjects.click('mapTooltipMoreActionsButton'); + await testSubjects.click('mapFilterActionButton__drilldown1'); - // TODO: Fix me #64861 - // const hasSourceFilter = await filterBar.hasFilter('name', 'charlie'); - // expect(hasSourceFilter).to.be(true); + // Assert on new dashboard with filter from action + await PageObjects.dashboard.waitForRenderComplete(); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.equal(2); - const hasJoinFilter = await filterBar.hasFilter('shape_name', 'charlie'); - expect(hasJoinFilter).to.be(true); + const hasJoinFilter = await filterBar.hasFilter('shape_name', 'charlie'); + expect(hasJoinFilter).to.be(true); + }); }); }); } diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index 198174bccb286..0f1fd3c09d706 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -1048,7 +1048,7 @@ "title" : "dash for tooltip filter action test", "hits" : 0, "description" : "Zoomed in so entire screen is covered by filter so click to open tooltip can not miss.", - "panelsJSON" : "[{\"gridData\":{\"x\":0,\"y\":0,\"w\":48,\"h\":26,\"i\":\"1\"},\"version\":\"8.0.0\",\"panelIndex\":\"1\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":-1.31919,\"lon\":59.53306,\"zoom\":9.67},\"isLayerTOCOpen\":false,\"openTOCDetails\":[\"n1t6f\"]},\"panelRefName\":\"panel_0\"}]", + "panelsJSON" : "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":48,\"h\":26,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":-1.31919,\"lon\":59.53306,\"zoom\":9.67},\"isLayerTOCOpen\":false,\"openTOCDetails\":[\"n1t6f\"],\"hiddenLayers\":[],\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"669a3521-1215-4228-9ced-77e2edf5ad17\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"drilldown1\",\"config\":{\"dashboardId\":\"19906970-2e40-11e9-85cb-6965aae20f13\",\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}}},\"panelRefName\":\"panel_0\"}]", "optionsJSON" : "{\"useMargins\":true,\"hidePanelTitles\":false}", "version" : 1, "timeRestore" : true, @@ -1071,9 +1071,9 @@ } ], "migrationVersion" : { - "dashboard" : "7.0.0" + "dashboard" : "7.3.0" }, - "updated_at" : "2019-06-14T14:09:25.039Z" + "updated_at" : "2020-08-26T14:32:27.854Z" } } }