diff --git a/superset/assets/spec/javascripts/dashboard/components/FilterIndicatorsContainer_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/FilterIndicatorsContainer_spec.jsx index 0fa06abbdf5f..0e5392fca4ef 100644 --- a/superset/assets/spec/javascripts/dashboard/components/FilterIndicatorsContainer_spec.jsx +++ b/superset/assets/spec/javascripts/dashboard/components/FilterIndicatorsContainer_spec.jsx @@ -34,6 +34,7 @@ describe('FilterIndicatorsContainer', () => { filterImmuneSlices: [], filterImmuneSliceFields: {}, setDirectPathToChild: () => {}, + filterFieldOnFocus: {}, }; colorMap.getFilterColorKey = jest.fn(() => 'id_column'); diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js index 22d7bfc60a9a..5b748f8f3197 100644 --- a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js +++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js @@ -29,4 +29,5 @@ export default { isStarred: true, isPublished: true, css: '', + focusedFilterField: [], }; diff --git a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js index e6117819fd6c..cb5befc8cc81 100644 --- a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js +++ b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js @@ -22,6 +22,7 @@ import { ON_SAVE, REMOVE_SLICE, SET_EDIT_MODE, + SET_FOCUSED_FILTER_FIELD, SET_MAX_UNDO_HISTORY_EXCEEDED, SET_UNSAVED_CHANGES, TOGGLE_EXPAND_SLICE, @@ -131,4 +132,38 @@ describe('dashboardState reducer', () => { updatedColorScheme: false, }); }); + + it('should clear focused filter field', () => { + // dashboard only has 1 focused filter field at a time, + // but when user switch different filter boxes, + // browser didn't always fire onBlur and onFocus events in order. + // so in redux state focusedFilterField prop is a queue, + // we always shift first element in the queue + + // init state: has 1 focus field + const initState = { + focusedFilterField: [ + { + chartId: 1, + column: 'column_1', + }, + ], + }; + // when user switching filter, + // browser focus on new filter first, + // then blur current filter + const step1 = dashboardStateReducer(initState, { + type: SET_FOCUSED_FILTER_FIELD, + chartId: 2, + column: 'column_2', + }); + const step2 = dashboardStateReducer(step1, { + type: SET_FOCUSED_FILTER_FIELD, + }); + + expect(step2.focusedFilterField.slice(-1).pop()).toEqual({ + chartId: 2, + column: 'column_2', + }); + }); }); diff --git a/superset/assets/spec/javascripts/dashboard/util/getChartAndLabelComponentIdFromPath_spec.js b/superset/assets/spec/javascripts/dashboard/util/getChartAndLabelComponentIdFromPath_spec.js new file mode 100644 index 000000000000..42169356ca77 --- /dev/null +++ b/superset/assets/spec/javascripts/dashboard/util/getChartAndLabelComponentIdFromPath_spec.js @@ -0,0 +1,38 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import getChartAndLabelComponentIdFromPath from '../../../../src/dashboard/util/getChartAndLabelComponentIdFromPath'; + +describe('getChartAndLabelComponentIdFromPath', () => { + it('should return label and component id', () => { + const directPathToChild = [ + 'ROOT_ID', + 'TABS-aX1uNK-ryo', + 'TAB-ZRgxfD2ktj', + 'ROW-46632bc2', + 'COLUMN-XjlxaS-flc', + 'CHART-x-RMdAtlDb', + 'LABEL-region', + ]; + + expect(getChartAndLabelComponentIdFromPath(directPathToChild)).toEqual({ + label: 'LABEL-region', + chart: 'CHART-x-RMdAtlDb', + }); + }); +}); diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx index b44367bc4ae3..b2791cf8bf7b 100644 --- a/superset/assets/src/chart/Chart.jsx +++ b/superset/assets/src/chart/Chart.jsx @@ -58,12 +58,16 @@ const propTypes = { // dashboard callbacks addFilter: PropTypes.func, onQuery: PropTypes.func, + onFilterMenuOpen: PropTypes.func, + onFilterMenuClose: PropTypes.func, }; const BLANK = {}; const defaultProps = { addFilter: () => BLANK, + onFilterMenuOpen: () => BLANK, + onFilterMenuClose: () => BLANK, initialValues: BLANK, setControlValue() {}, triggerRender: false, diff --git a/superset/assets/src/chart/ChartRenderer.jsx b/superset/assets/src/chart/ChartRenderer.jsx index 49eef8f683be..2409c66ecd7f 100644 --- a/superset/assets/src/chart/ChartRenderer.jsx +++ b/superset/assets/src/chart/ChartRenderer.jsx @@ -44,12 +44,16 @@ const propTypes = { refreshOverlayVisible: PropTypes.bool, // dashboard callbacks addFilter: PropTypes.func, + onFilterMenuOpen: PropTypes.func, + onFilterMenuClose: PropTypes.func, }; const BLANK = {}; const defaultProps = { addFilter: () => BLANK, + onFilterMenuOpen: () => BLANK, + onFilterMenuClose: () => BLANK, initialValues: BLANK, setControlValue() {}, triggerRender: false, @@ -73,6 +77,8 @@ class ChartRenderer extends React.Component { onError: this.handleRenderFailure, setControlValue: this.handleSetControlValue, setTooltip: this.setTooltip, + onFilterMenuOpen: this.props.onFilterMenuOpen, + onFilterMenuClose: this.props.onFilterMenuClose, }; } diff --git a/superset/assets/src/components/FilterBadgeIcon.css b/superset/assets/src/components/FilterBadgeIcon.css index 4cb6a713e56c..2c13dc8a146e 100644 --- a/superset/assets/src/components/FilterBadgeIcon.css +++ b/superset/assets/src/components/FilterBadgeIcon.css @@ -27,3 +27,8 @@ cursor: pointer; background-color: #9e9e9e; } + +.color-bar.badge-group, +.filter-badge.badge-group { + background-color: rgb(72, 72, 72); +} diff --git a/superset/assets/src/dashboard/actions/dashboardState.js b/superset/assets/src/dashboard/actions/dashboardState.js index 1b8ea83f4b5f..b1d850fb89d1 100644 --- a/superset/assets/src/dashboard/actions/dashboardState.js +++ b/superset/assets/src/dashboard/actions/dashboardState.js @@ -331,6 +331,16 @@ export function setDirectPathToChild(path) { return { type: SET_DIRECT_PATH, path }; } +export const SET_FOCUSED_FILTER_FIELD = 'SET_FOCUSED_FILTER_FIELD'; +export function setFocusedFilterField(chartId, column) { + return { type: SET_FOCUSED_FILTER_FIELD, chartId, column }; +} + +export function unsetFocusedFilterField() { + // same ACTION as setFocusedFilterField, without arguments + return { type: SET_FOCUSED_FILTER_FIELD }; +} + // Undo history --------------------------------------------------------------- export const SET_MAX_UNDO_HISTORY_EXCEEDED = 'SET_MAX_UNDO_HISTORY_EXCEEDED'; export function setMaxUndoHistoryExceeded(maxUndoHistoryExceeded = true) { diff --git a/superset/assets/src/dashboard/components/FilterIndicator.jsx b/superset/assets/src/dashboard/components/FilterIndicator.jsx index 5fd238d867b7..a038f39d3a2f 100644 --- a/superset/assets/src/dashboard/components/FilterIndicator.jsx +++ b/superset/assets/src/dashboard/components/FilterIndicator.jsx @@ -19,6 +19,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { t } from '@superset-ui/translation'; +import { isEmpty } from 'lodash'; import { filterIndicatorPropShape } from '../util/propShapes'; import FilterBadgeIcon from '../../components/FilterBadgeIcon'; @@ -43,7 +44,12 @@ class FilterIndicator extends React.PureComponent { } render() { - const { colorCode, label, values } = this.props.indicator; + const { + colorCode, + label, + values, + isFilterFieldActive, + } = this.props.indicator; const filterTooltip = (
- +
); diff --git a/superset/assets/src/dashboard/components/FilterIndicatorGroup.jsx b/superset/assets/src/dashboard/components/FilterIndicatorGroup.jsx index bc7a22c862b9..a9385db36edf 100644 --- a/superset/assets/src/dashboard/components/FilterIndicatorGroup.jsx +++ b/superset/assets/src/dashboard/components/FilterIndicatorGroup.jsx @@ -19,6 +19,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { t } from '@superset-ui/translation'; +import { isEmpty } from 'lodash'; import FilterBadgeIcon from '../../components/FilterBadgeIcon'; import FilterIndicatorTooltip from './FilterIndicatorTooltip'; @@ -42,6 +43,13 @@ class FilterIndicatorGroup extends React.PureComponent { render() { const { indicators } = this.props; + const hasFilterFieldActive = indicators.some( + indicator => indicator.isFilterFieldActive, + ); + const hasFilterApplied = indicators.some( + indicator => !isEmpty(indicator.values), + ); + return ( } > -
+
- +
); diff --git a/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx b/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx index 09a74780f4c9..6afb3fcb1778 100644 --- a/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx +++ b/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx @@ -38,6 +38,7 @@ const propTypes = { filterImmuneSlices: PropTypes.arrayOf(PropTypes.number).isRequired, filterImmuneSliceFields: PropTypes.object.isRequired, setDirectPathToChild: PropTypes.func.isRequired, + filterFieldOnFocus: PropTypes.object.isRequired, }; const defaultProps = { @@ -62,6 +63,7 @@ export default class FilterIndicatorsContainer extends React.PureComponent { chartId: currentChartId, filterImmuneSlices, filterImmuneSliceFields, + filterFieldOnFocus, } = this.props; if (Object.keys(dashboardFilters).length === 0) { @@ -108,6 +110,9 @@ export default class FilterIndicatorsContainer extends React.PureComponent { (isDateFilter && columns[name] === 'No filter') ? [] : [].concat(columns[name]), + isFilterFieldActive: + chartId === filterFieldOnFocus.chartId && + name === filterFieldOnFocus.column, }; // do not apply filter on fields in the filterImmuneSliceFields map diff --git a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx index 330910bd2ce6..4d34cc64b7c7 100644 --- a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx @@ -51,6 +51,8 @@ const propTypes = { logEvent: PropTypes.func.isRequired, toggleExpandSlice: PropTypes.func.isRequired, changeFilter: PropTypes.func.isRequired, + setFocusedFilterField: PropTypes.func.isRequired, + unsetFocusedFilterField: PropTypes.func.isRequired, editMode: PropTypes.bool.isRequired, isExpanded: PropTypes.bool.isRequired, isCached: PropTypes.bool, @@ -83,6 +85,8 @@ class Chart extends React.Component { }; this.changeFilter = this.changeFilter.bind(this); + this.handleFilterMenuOpen = this.handleFilterMenuOpen.bind(this); + this.handleFilterMenuClose = this.handleFilterMenuClose.bind(this); this.exploreChart = this.exploreChart.bind(this); this.exportCSV = this.exportCSV.bind(this); this.forceRefresh = this.forceRefresh.bind(this); @@ -169,6 +173,14 @@ class Chart extends React.Component { this.props.changeFilter(this.props.chart.id, newSelectedValues); } + handleFilterMenuOpen(chartId, column) { + this.props.setFocusedFilterField(chartId, column); + } + + handleFilterMenuClose() { + this.props.unsetFocusedFilterField(); + } + exploreChart() { this.props.logEvent(LOG_ACTIONS_EXPLORE_DASHBOARD_CHART, { slice_id: this.props.slice.slice_id, @@ -278,6 +290,8 @@ class Chart extends React.Component { width={width} height={this.getChartHeight()} addFilter={this.changeFilter} + onFilterMenuOpen={this.handleFilterMenuOpen} + onFilterMenuClose={this.handleFilterMenuClose} annotationData={chart.annotationData} chartAlert={chart.chartAlert} chartId={id} diff --git a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx index dd8c0b7d988c..a3cf5c1ce3a2 100644 --- a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx +++ b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx @@ -26,6 +26,7 @@ import DeleteComponentButton from '../DeleteComponentButton'; import DragDroppable from '../dnd/DragDroppable'; import HoverMenu from '../menu/HoverMenu'; import ResizableContainer from '../resizable/ResizableContainer'; +import getChartAndLabelComponentIdFromPath from '../../util/getChartAndLabelComponentIdFromPath'; import { componentShape } from '../../util/propShapes'; import { ROW_TYPE, COLUMN_TYPE } from '../../util/componentTypes'; @@ -34,7 +35,6 @@ import { GRID_MIN_ROW_UNITS, GRID_BASE_UNIT, GRID_GUTTER_SIZE, - IN_COMPONENT_ELEMENT_TYPES, } from '../../util/constants'; const CHART_MARGIN = 32; @@ -48,6 +48,7 @@ const propTypes = { depth: PropTypes.number.isRequired, editMode: PropTypes.bool.isRequired, directPathToChild: PropTypes.arrayOf(PropTypes.string), + directPathLastUpdated: PropTypes.number, // grid related availableColumnCount: PropTypes.number.isRequired, @@ -64,23 +65,48 @@ const propTypes = { const defaultProps = { directPathToChild: [], + directPathLastUpdated: 0, }; class ChartHolder extends React.Component { - static renderInFocusCSS(labelName) { + static renderInFocusCSS(columnName) { return ( ); } + static getDerivedStateFromProps(props, state) { + const { component, directPathToChild, directPathLastUpdated } = props; + const { + label: columnName, + chart: chartComponentId, + } = getChartAndLabelComponentIdFromPath(directPathToChild); + + if ( + directPathLastUpdated !== state.directPathLastUpdated && + component.id === chartComponentId + ) { + return { + outlinedComponentId: component.id, + outlinedColumnName: columnName, + directPathLastUpdated, + }; + } + return null; + } + constructor(props) { super(props); this.state = { isFocused: false, + outlinedComponentId: null, + outlinedColumnName: null, + directPathLastUpdated: 0, }; this.handleChangeFocus = this.handleChangeFocus.bind(this); @@ -88,25 +114,27 @@ class ChartHolder extends React.Component { this.handleUpdateSliceName = this.handleUpdateSliceName.bind(this); } - getChartAndLabelComponentIdFromPath() { - const { directPathToChild = [] } = this.props; - const result = {}; + componentDidMount() { + this.hideOutline({}, this.state); + } - if (directPathToChild.length > 0) { - const currentPath = directPathToChild.slice(); + componentDidUpdate(prevProps, prevState) { + this.hideOutline(prevState, this.state); + } - while (currentPath.length) { - const componentId = currentPath.pop(); - const componentType = componentId.split('-')[0]; + hideOutline(prevState, state) { + const { outlinedComponentId: timerKey } = state; + const { outlinedComponentId: prevTimerKey } = prevState; - result[componentType.toLowerCase()] = componentId; - if (!IN_COMPONENT_ELEMENT_TYPES.includes(componentType)) { - break; - } - } + // because of timeout, there might be multiple charts showing outline + if (!!timerKey && !prevTimerKey) { + setTimeout(() => { + this.setState(() => ({ + outlinedComponentId: null, + outlinedColumnName: null, + })); + }, 2000); } - - return result; } handleChangeFocus(nextFocus) { @@ -155,12 +183,6 @@ class ChartHolder extends React.Component { ? parentComponent.meta.width || GRID_MIN_COLUMN_COUNT : component.meta.width || GRID_MIN_COLUMN_COUNT; - const { - label: labelName, - chart: chartComponentId, - } = this.getChartAndLabelComponentIdFromPath(); - const inFocus = chartComponentId === component.id; - return ( {!editMode && ( - + )} - {inFocus && ChartHolder.renderInFocusCSS(labelName)} + {!!this.state.outlinedComponentId && + ChartHolder.renderInFocusCSS(this.state.outlinedColumnName)} 0) { .filter-badge.badge-@{i}, + .active .color-bar.badge-@{i}, + .dashboard-filter-indicators-container:hover .color-bar.badge-@{i}, .dashboard-component-chart-holder:hover .color-bar.badge-@{i} { @value: extract(@badge-colors, @i); background-color: @value; diff --git a/superset/assets/src/dashboard/util/getChartAndLabelComponentIdFromPath.js b/superset/assets/src/dashboard/util/getChartAndLabelComponentIdFromPath.js new file mode 100644 index 000000000000..7949612eb3c9 --- /dev/null +++ b/superset/assets/src/dashboard/util/getChartAndLabelComponentIdFromPath.js @@ -0,0 +1,39 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { IN_COMPONENT_ELEMENT_TYPES } from './constants'; + +export default function getChartAndLabelComponentIdFromPath(directPathToChild) { + const result = {}; + + if (directPathToChild.length > 0) { + const currentPath = directPathToChild.slice(); + + while (currentPath.length) { + const componentId = currentPath.pop(); + const componentType = componentId.split('-')[0]; + + result[componentType.toLowerCase()] = componentId; + if (!IN_COMPONENT_ELEMENT_TYPES.includes(componentType)) { + break; + } + } + } + + return result; +} diff --git a/superset/assets/src/dashboard/util/propShapes.jsx b/superset/assets/src/dashboard/util/propShapes.jsx index 7bcd8f35aea4..d4fb6ddd623f 100644 --- a/superset/assets/src/dashboard/util/propShapes.jsx +++ b/superset/assets/src/dashboard/util/propShapes.jsx @@ -72,9 +72,10 @@ export const filterIndicatorPropShape = PropTypes.shape({ componentId: PropTypes.string.isRequired, directPathToFilter: PropTypes.arrayOf(PropTypes.string).isRequired, isDateFilter: PropTypes.bool.isRequired, + isFilterFieldActive: PropTypes.bool.isRequired, isInstantFilter: PropTypes.bool.isRequired, - name: PropTypes.string.isRequired, label: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, scope: PropTypes.string.isRequired, values: PropTypes.array.isRequired, }); diff --git a/superset/assets/src/explore/components/controls/DateFilterControl.jsx b/superset/assets/src/explore/components/controls/DateFilterControl.jsx index 0a85796209b5..1f5fbf9822c3 100644 --- a/superset/assets/src/explore/components/controls/DateFilterControl.jsx +++ b/superset/assets/src/explore/components/controls/DateFilterControl.jsx @@ -75,6 +75,8 @@ const FREEFORM_TOOLTIP = t( '`last october` can be used.', ); +const DATE_FILTER_POPOVER_STYLE = { width: '250px' }; + const propTypes = { animation: PropTypes.bool, name: PropTypes.string.isRequired, @@ -83,12 +85,16 @@ const propTypes = { onChange: PropTypes.func, value: PropTypes.string, height: PropTypes.number, + onOpenDateFilterControl: PropTypes.func, + onCloseDateFilterControl: PropTypes.func, }; const defaultProps = { animation: true, onChange: () => {}, value: 'Last week', + onOpenDateFilterControl: () => {}, + onCloseDateFilterControl: () => {}, }; function isValidMoment(s) { @@ -182,6 +188,7 @@ export default class DateFilterControl extends React.Component { this.close = this.close.bind(this); this.handleClick = this.handleClick.bind(this); + this.handleClickTrigger = this.handleClickTrigger.bind(this); this.isValidSince = this.isValidSince.bind(this); this.isValidUntil = this.isValidUntil.bind(this); this.onEnter = this.onEnter.bind(this); @@ -242,11 +249,35 @@ export default class DateFilterControl extends React.Component { } handleClick(e) { + const target = e.target; // switch to `TYPES.CUSTOM_START_END` when the calendar is clicked - if (this.startEndSectionRef && this.startEndSectionRef.contains(e.target)) { + if (this.startEndSectionRef && this.startEndSectionRef.contains(target)) { this.setTypeCustomStartEnd(); } + + // if user click outside popover, popover will hide and we will call onCloseDateFilterControl, + // but need to exclude OverlayTrigger component to avoid handle click events twice. + if (target.getAttribute('name') !== 'popover-trigger') { + if ( + this.popoverContainer && + !this.popoverContainer.contains(target) + ) { + this.props.onCloseDateFilterControl(); + } + } } + + handleClickTrigger() { + // when user clicks OverlayTrigger, + // popoverContainer component will be created after handleClickTrigger + // and before handleClick handler + if (!this.popoverContainer) { + this.props.onOpenDateFilterControl(); + } else { + this.props.onCloseDateFilterControl(); + } + } + close() { let val; if (this.state.type === TYPES.DEFAULTS || this.state.tab === TABS.DEFAULTS) { @@ -256,6 +287,7 @@ export default class DateFilterControl extends React.Component { } else { val = [this.state.since, this.state.until].join(SEPARATOR); } + this.props.onCloseDateFilterControl(); this.props.onChange(val); this.refs.trigger.hide(); this.setState({ showSinceCalendar: false, showUntilCalendar: false }); @@ -338,7 +370,7 @@ export default class DateFilterControl extends React.Component { }); return ( -
+
{ this.popoverContainer = ref; }}> - +
); diff --git a/superset/assets/src/visualizations/FilterBox/FilterBox.css b/superset/assets/src/visualizations/FilterBox/FilterBox.css index 97b4ed999de0..f546c9c463dc 100644 --- a/superset/assets/src/visualizations/FilterBox/FilterBox.css +++ b/superset/assets/src/visualizations/FilterBox/FilterBox.css @@ -64,6 +64,7 @@ ul.select2-results div.filter_box{ } .filter-container .filter-badge-container { width: 30px; + padding-right: 10px; } .filter-container .filter-badge-container + div { width: 100%; diff --git a/superset/assets/src/visualizations/FilterBox/FilterBox.jsx b/superset/assets/src/visualizations/FilterBox/FilterBox.jsx index 28983c1e65dc..a2b9cc84336a 100644 --- a/superset/assets/src/visualizations/FilterBox/FilterBox.jsx +++ b/superset/assets/src/visualizations/FilterBox/FilterBox.jsx @@ -64,6 +64,8 @@ const propTypes = { metric: PropTypes.number, }))), onChange: PropTypes.func, + onFilterMenuOpen: PropTypes.func, + onFilterMenuClose: PropTypes.func, showDateFilter: PropTypes.bool, showSqlaTimeGrain: PropTypes.bool, showSqlaTimeColumn: PropTypes.bool, @@ -73,6 +75,8 @@ const propTypes = { const defaultProps = { origSelectedValues: {}, onChange: () => {}, + onFilterMenuOpen: () => {}, + onFilterMenuClose: () => {}, showDateFilter: false, showSqlaTimeGrain: false, showSqlaTimeColumn: false, @@ -90,6 +94,19 @@ class FilterBox extends React.Component { hasChanged: false, }; this.changeFilter = this.changeFilter.bind(this); + this.onFilterMenuOpen = this.onFilterMenuOpen.bind(this, props.chartId); + this.onFilterMenuClose = this.onFilterMenuClose.bind(this); + this.onFocus = this.onFilterMenuOpen; + this.onBlur = this.onFilterMenuClose; + this.onOpenDateFilterControl = this.onFilterMenuOpen.bind(props.chartId, TIME_RANGE); + } + + onFilterMenuOpen(chartId, column) { + this.props.onFilterMenuOpen(chartId, column); + } + + onFilterMenuClose() { + this.props.onFilterMenuClose(); } getControlData(controlName) { @@ -150,6 +167,8 @@ class FilterBox extends React.Component { label={label} description={t('Select start and end date')} onChange={(...args) => { this.changeFilter(TIME_RANGE, ...args); }} + onOpenDateFilterControl={this.onOpenDateFilterControl} + onCloseDateFilterControl={this.onFilterMenuClose} value={this.state.selectedValues[TIME_RANGE] || 'No filter'} />
@@ -256,6 +275,10 @@ class FilterBox extends React.Component { return { value: opt.id, label: opt.id, style }; })} onChange={(...args) => { this.changeFilter(key, ...args); }} + onFocus={this.onFocus} + onBlur={this.onBlur} + onOpen={(...args) => { this.onFilterMenuOpen(key, ...args); }} + onClose={this.onFilterMenuClose} selectComponent={Creatable} selectWrap={VirtualizedSelect} optionRenderer={VirtualizedRendererWrap(opt => opt.label)} diff --git a/superset/assets/src/visualizations/FilterBox/transformProps.js b/superset/assets/src/visualizations/FilterBox/transformProps.js index 4ccf1cd9a222..bd39404c47d0 100644 --- a/superset/assets/src/visualizations/FilterBox/transformProps.js +++ b/superset/assets/src/visualizations/FilterBox/transformProps.js @@ -27,7 +27,11 @@ export default function transformProps(chartProps) { queryData, rawDatasource, } = chartProps; - const { onAddFilter = NOOP } = hooks; + const { + onAddFilter = NOOP, + onFilterMenuOpen = NOOP, + onFilterMenuClose = NOOP, + } = hooks; const { sliceId, dateFilter, @@ -53,6 +57,8 @@ export default function transformProps(chartProps) { filtersFields, instantFiltering, onChange: onAddFilter, + onFilterMenuOpen, + onFilterMenuClose, origSelectedValues: initialValues || {}, showDateFilter: dateFilter, showDruidTimeGrain: showDruidTimeGranularity,