[feature][dashboard] Show/hide filter indicator on the applicable charts when filter options are open/close (#8166)

* show ellipsis for long filter name in tooltip

* show filter indicator color bar when filter is applied

* show/hide filter indicator when filter is showing/hiding options

* use component local state to hide/show chart outline

* fix review comments + unit tests
This commit is contained in:
Grace Guo
2019-09-10 15:29:13 -07:00
committed by GitHub
parent 296f3e81a7
commit b4a1234670
28 changed files with 375 additions and 54 deletions

View File

@@ -34,6 +34,7 @@ describe('FilterIndicatorsContainer', () => {
filterImmuneSlices: [],
filterImmuneSliceFields: {},
setDirectPathToChild: () => {},
filterFieldOnFocus: {},
};
colorMap.getFilterColorKey = jest.fn(() => 'id_column');

View File

@@ -29,4 +29,5 @@ export default {
isStarred: true,
isPublished: true,
css: '',
focusedFilterField: [],
};

View File

@@ -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',
});
});
});

View File

@@ -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',
});
});
});

View File

@@ -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,

View File

@@ -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,
};
}

View File

@@ -27,3 +27,8 @@
cursor: pointer;
background-color: #9e9e9e;
}
.color-bar.badge-group,
.filter-badge.badge-group {
background-color: rgb(72, 72, 72);
}

View File

@@ -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) {

View File

@@ -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 = (
<FilterIndicatorTooltip
@@ -56,12 +62,12 @@ class FilterIndicator extends React.PureComponent {
return (
<FilterTooltipWrapper tooltip={filterTooltip}>
<div
className="filter-indicator"
className={`filter-indicator ${isFilterFieldActive ? 'active' : ''}`}
onClick={this.focusToFilterComponent}
role="none"
>
<div className={`color-bar ${colorCode}`} />
<FilterBadgeIcon />
<FilterBadgeIcon colorCode={isEmpty(values) ? '' : colorCode} />
</div>
</FilterTooltipWrapper>
);

View File

@@ -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 (
<FilterTooltipWrapper
tooltip={
@@ -63,9 +71,13 @@ class FilterIndicatorGroup extends React.PureComponent {
</React.Fragment>
}
>
<div className="filter-indicator-group">
<div
className={`filter-indicator-group ${
hasFilterFieldActive ? 'active' : ''
}`}
>
<div className="color-bar badge-group" />
<FilterBadgeIcon />
<FilterBadgeIcon colorCode={hasFilterApplied ? 'badge-group' : ''} />
</div>
</FilterTooltipWrapper>
);

View File

@@ -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

View File

@@ -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}

View File

@@ -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 (
<style>
{`.inFocus label[for=${labelName}] + .Select .Select-control {
border: 2px solid #00736a;
{`label[for=${columnName}] + .Select .Select-control {
border-color: #00736a;
transition: border-color 1s ease-in-out;
}`}
</style>
);
}
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 (
<DragDroppable
component={component}
@@ -192,13 +214,17 @@ class ChartHolder extends React.Component {
<div
ref={dragSourceRef}
className={`dashboard-component dashboard-component-chart-holder ${
inFocus ? 'inFocus' : ''
this.state.outlinedComponentId ? 'fade-in' : 'fade-out'
}`}
>
{!editMode && (
<AnchorLink anchorLinkId={component.id} inFocus={inFocus} />
<AnchorLink
anchorLinkId={component.id}
inFocus={!!this.state.outlinedComponentId}
/>
)}
{inFocus && ChartHolder.renderInFocusCSS(labelName)}
{!!this.state.outlinedComponentId &&
ChartHolder.renderInFocusCSS(this.state.outlinedColumnName)}
<Chart
componentId={component.id}
id={component.meta.chartId}

View File

@@ -19,7 +19,11 @@
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { toggleExpandSlice } from '../actions/dashboardState';
import {
toggleExpandSlice,
setFocusedFilterField,
unsetFocusedFilterField,
} from '../actions/dashboardState';
import { updateComponents } from '../actions/dashboardLayout';
import { changeFilter } from '../actions/dashboardFilters';
import { addDangerToast } from '../../messageToasts/actions';
@@ -78,6 +82,8 @@ function mapDispatchToProps(dispatch) {
addDangerToast,
toggleExpandSlice,
changeFilter,
setFocusedFilterField,
unsetFocusedFilterField,
refreshChart,
logEvent,
},

View File

@@ -45,10 +45,12 @@ const propTypes = {
handleComponentDrop: PropTypes.func.isRequired,
logEvent: PropTypes.func.isRequired,
directPathToChild: PropTypes.arrayOf(PropTypes.string),
directPathLastUpdated: PropTypes.number,
};
const defaultProps = {
directPathToChild: [],
directPathLastUpdated: 0,
isComponentVisible: true,
};
@@ -65,6 +67,11 @@ function mapStateToProps(
editMode: dashboardState.editMode,
filters: getActiveFilters(),
directPathToChild: dashboardState.directPathToChild,
directPathLastUpdated: dashboardState.directPathLastUpdated,
filterFieldOnFocus:
dashboardState.focusedFilterField.length === 0
? {}
: dashboardState.focusedFilterField.slice(-1).pop(),
};
// rows and columns need more data about their child dimensions

View File

@@ -23,11 +23,11 @@ import FilterIndicatorsContainer from '../components/FilterIndicatorsContainer';
import { setDirectPathToChild } from '../actions/dashboardState';
function mapStateToProps(
{ dashboardFilters, dashboardInfo, charts },
{ dashboardState, dashboardFilters, dashboardInfo, charts },
ownProps,
) {
const chartId = ownProps.chartId;
const chartStatus = charts[chartId].chartStatus;
const chartStatus = (charts[chartId] || {}).chartStatus;
return {
dashboardFilters,
@@ -36,6 +36,10 @@ function mapStateToProps(
filterImmuneSlices: dashboardInfo.metadata.filterImmuneSlices || [],
filterImmuneSliceFields:
dashboardInfo.metadata.filterImmuneSliceFields || {},
filterFieldOnFocus:
dashboardState.focusedFilterField.length === 0
? {}
: dashboardState.focusedFilterField.slice(-1).pop(),
};
}

View File

@@ -33,6 +33,7 @@ import {
UPDATE_CSS,
SET_REFRESH_FREQUENCY,
SET_DIRECT_PATH,
SET_FOCUSED_FILTER_FIELD,
} from '../actions/dashboardState';
import { BUILDER_PANE_TYPE } from '../util/constants';
@@ -126,6 +127,22 @@ export default function dashboardStateReducer(state = {}, action) {
return {
...state,
directPathToChild: action.path,
directPathLastUpdated: Date.now(),
};
},
[SET_FOCUSED_FILTER_FIELD]() {
const { focusedFilterField } = state;
if (action.chartId && action.column) {
focusedFilterField.push({
chartId: action.chartId,
column: action.column,
});
} else {
focusedFilterField.shift();
}
return {
...state,
focusedFilterField,
};
},
};

View File

@@ -253,6 +253,13 @@ export default function(bootstrapData) {
dashboardState: {
sliceIds: Array.from(sliceIds),
directPathToChild,
directPathLastUpdated: Date.now(),
// 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,
// but component use focusedFilterField prop as single object.
focusedFilterField: [],
expandedSlices: dashboard.metadata.expanded_slices || {},
refreshFrequency: dashboard.metadata.refresh_frequency || 0,
css: dashboard.css || '',

View File

@@ -39,6 +39,18 @@
height: 40%;
}
}
&.fade-in {
border-radius: 4px;
box-shadow: inset 0 0 0 2px rgba(0, 166, 153, 1), 0 0 0 3px rgba(0, 166, 153, .15);
transition: box-shadow 1s ease-in-out;
}
&.fade-out {
border-radius: 4px;
box-shadow: none;
transition: box-shadow 1s ease-in-out;
}
}
.dashboard-chart {

View File

@@ -43,10 +43,16 @@
line-height: 22px;
margin-right: 22px;
text-align: left;
display: flex;
flex-flow: row wrap;
align-items: stretch;
label {
font-weight: 700;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
margin-right: 4px;
}
}

View File

@@ -58,23 +58,21 @@
}
}
.inFocus .filter-indicator-group {
.show-outline .filter-indicator-group {
box-shadow: rgba(0, 166, 153, 1) -2px 0 0 0, rgb(255, 255, 255) -4px 0 0 0, rgb(219, 219, 219) -6px 0 0 0;
}
.dashboard-component-chart-holder:hover,
.dashboard-filter-indicators-container:hover {
.filter-indicator,
.filter-indicator-group {
.dashboard-component-chart-holder,
.dashboard-filter-indicators-container {
.active.filter-indicator,
.active.filter-indicator-group,
&:hover .filter-indicator,
&:hover .filter-indicator-group {
width: 20px;
background-color: transparent;
.color-bar {
width: 2px;
&.badge-group {
background-color: rgb(72, 72, 72);
}
}
.filter-badge {
@@ -88,7 +86,3 @@
}
}
.inFocus {
border-radius: 4px;
box-shadow: inset 0 0 0 2px rgba(0, 166, 153, 1), 0 0 0 3px rgba(0, 166, 153, .15);
}

View File

@@ -66,6 +66,8 @@
@iterations: length(@badge-colors);
.badge-loop (@i) when (@i > 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;

View File

@@ -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;
}

View File

@@ -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,
});

View File

@@ -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 (
<Popover id="filter-popover" placement="top" positionTop={0}>
<div style={{ width: '250px' }}>
<div style={DATE_FILTER_POPOVER_STYLE} ref={(ref) => { this.popoverContainer = ref; }}>
<Tabs
defaultActiveKey={this.state.tab === TABS.DEFAULTS ? 1 : 2}
id="type"
@@ -474,8 +506,9 @@ export default class DateFilterControl extends React.Component {
ref="trigger"
placement="right"
overlay={this.renderPopover()}
onClick={this.handleClickTrigger}
>
<Label style={{ cursor: 'pointer' }}>{value}</Label>
<Label name="popover-trigger" style={{ cursor: 'pointer' }}>{value}</Label>
</OverlayTrigger>
</div>
);

View File

@@ -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%;

View File

@@ -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'}
/>
</div>
@@ -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)}

View File

@@ -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,