mirror of
https://github.com/apache/superset.git
synced 2026-04-18 15:44:57 +00:00
[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:
@@ -34,6 +34,7 @@ describe('FilterIndicatorsContainer', () => {
|
||||
filterImmuneSlices: [],
|
||||
filterImmuneSliceFields: {},
|
||||
setDirectPathToChild: () => {},
|
||||
filterFieldOnFocus: {},
|
||||
};
|
||||
|
||||
colorMap.getFilterColorKey = jest.fn(() => 'id_column');
|
||||
|
||||
@@ -29,4 +29,5 @@ export default {
|
||||
isStarred: true,
|
||||
isPublished: true,
|
||||
css: '',
|
||||
focusedFilterField: [],
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -27,3 +27,8 @@
|
||||
cursor: pointer;
|
||||
background-color: #9e9e9e;
|
||||
}
|
||||
|
||||
.color-bar.badge-group,
|
||||
.filter-badge.badge-group {
|
||||
background-color: rgb(72, 72, 72);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 || '',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user