+
{({ width }) => (
@@ -277,7 +305,7 @@ class DashboardBuilder extends React.Component {
colorScheme={colorScheme}
/>
)}
-
+
);
diff --git a/superset-frontend/src/dashboard/components/FilterIndicator.jsx b/superset-frontend/src/dashboard/components/FilterIndicator.jsx
deleted file mode 100644
index f4daa70c33..0000000000
--- a/superset-frontend/src/dashboard/components/FilterIndicator.jsx
+++ /dev/null
@@ -1,79 +0,0 @@
-/**
- * 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 React from 'react';
-import PropTypes from 'prop-types';
-import { t } from '@superset-ui/core';
-import { isEmpty } from 'lodash';
-
-import { filterIndicatorPropShape } from '../util/propShapes';
-import FilterBadgeIcon from '../../components/FilterBadgeIcon';
-import FilterIndicatorTooltip from './FilterIndicatorTooltip';
-import FilterTooltipWrapper from './FilterTooltipWrapper';
-
-const propTypes = {
- indicator: filterIndicatorPropShape.isRequired,
- setDirectPathToChild: PropTypes.func.isRequired,
-};
-
-class FilterIndicator extends React.PureComponent {
- constructor(props) {
- super(props);
-
- const { indicator, setDirectPathToChild } = props;
- const { directPathToFilter } = indicator;
- this.focusToFilterComponent = setDirectPathToChild.bind(
- this,
- directPathToFilter,
- );
- }
-
- render() {
- const {
- colorCode,
- label,
- values,
- isFilterFieldActive,
- } = this.props.indicator;
-
- const filterTooltip = (
-
- );
-
- return (
-
-
-
- );
- }
-}
-
-FilterIndicator.propTypes = propTypes;
-
-export default FilterIndicator;
diff --git a/superset-frontend/src/dashboard/components/FilterIndicatorGroup.jsx b/superset-frontend/src/dashboard/components/FilterIndicatorGroup.jsx
deleted file mode 100644
index 8fa547cb5f..0000000000
--- a/superset-frontend/src/dashboard/components/FilterIndicatorGroup.jsx
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * 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 React from 'react';
-import PropTypes from 'prop-types';
-import { t } from '@superset-ui/core';
-import { isEmpty } from 'lodash';
-
-import FilterBadgeIcon from '../../components/FilterBadgeIcon';
-import FilterIndicatorTooltip from './FilterIndicatorTooltip';
-import FilterTooltipWrapper from './FilterTooltipWrapper';
-import { filterIndicatorPropShape } from '../util/propShapes';
-
-const propTypes = {
- indicators: PropTypes.arrayOf(filterIndicatorPropShape).isRequired,
- setDirectPathToChild: PropTypes.func.isRequired,
-};
-
-class FilterIndicatorGroup extends React.PureComponent {
- constructor(props) {
- super(props);
-
- const { indicators, setDirectPathToChild } = this.props;
- this.onClickIcons = indicators.map(indicator =>
- setDirectPathToChild.bind(this, indicator.directPathToFilter),
- );
- }
-
- render() {
- const { indicators } = this.props;
- const hasFilterFieldActive = indicators.some(
- indicator => indicator.isFilterFieldActive,
- );
- const hasFilterApplied = indicators.some(
- indicator => !isEmpty(indicator.values),
- );
-
- return (
-
-
- {t('%s filters', indicators.length)}
-
-
- {indicators.map((indicator, index) => (
- -
-
-
- ))}
-
- >
- }
- >
-
-
- );
- }
-}
-
-FilterIndicatorGroup.propTypes = propTypes;
-
-export default FilterIndicatorGroup;
diff --git a/superset-frontend/src/dashboard/components/FilterIndicatorTooltip.jsx b/superset-frontend/src/dashboard/components/FilterIndicatorTooltip.jsx
deleted file mode 100644
index 87215dd25c..0000000000
--- a/superset-frontend/src/dashboard/components/FilterIndicatorTooltip.jsx
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * 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 React from 'react';
-import PropTypes from 'prop-types';
-import { t } from '@superset-ui/core';
-import { isEmpty } from 'lodash';
-import FormLabel from 'src/components/FormLabel';
-
-const propTypes = {
- label: PropTypes.string.isRequired,
- values: PropTypes.array.isRequired,
- clickIconHandler: PropTypes.func,
-};
-
-const defaultProps = {
- clickIconHandler: undefined,
-};
-
-export default function FilterIndicatorTooltip({
- label,
- values,
- clickIconHandler,
-}) {
- const displayValue = isEmpty(values) ? t('Not filtered') : values.join(', ');
-
- return (
-
-
- {label}:
- {displayValue}
-
-
- {clickIconHandler && (
-
- )}
-
- );
-}
-
-FilterIndicatorTooltip.propTypes = propTypes;
-FilterIndicatorTooltip.defaultProps = defaultProps;
diff --git a/superset-frontend/src/dashboard/components/FilterIndicatorsContainer.jsx b/superset-frontend/src/dashboard/components/FilterIndicatorsContainer.jsx
deleted file mode 100644
index 89a6f0e7c6..0000000000
--- a/superset-frontend/src/dashboard/components/FilterIndicatorsContainer.jsx
+++ /dev/null
@@ -1,203 +0,0 @@
-/**
- * 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 React from 'react';
-import PropTypes from 'prop-types';
-import { isEmpty, isNil } from 'lodash';
-
-import FilterIndicator from './FilterIndicator';
-import FilterIndicatorGroup from './FilterIndicatorGroup';
-import { FILTER_INDICATORS_DISPLAY_LENGTH } from '../util/constants';
-import { getChartIdsInFilterScope } from '../util/activeDashboardFilters';
-import { getDashboardFilterKey } from '../util/getDashboardFilterKey';
-import { getFilterColorMap } from '../util/dashboardFiltersColorMap';
-import { TIME_FILTER_MAP } from '../../visualizations/FilterBox/FilterBox';
-
-const propTypes = {
- // from props
- dashboardFilters: PropTypes.object.isRequired,
- chartId: PropTypes.number.isRequired,
- chartStatus: PropTypes.string,
-
- // from redux
- datasources: PropTypes.object.isRequired,
- setDirectPathToChild: PropTypes.func.isRequired,
- filterFieldOnFocus: PropTypes.object.isRequired,
-};
-
-const defaultProps = {
- chartStatus: 'loading',
-};
-
-const TIME_GRANULARITY_FIELDS = [
- TIME_FILTER_MAP.granularity,
- TIME_FILTER_MAP.time_grain_sqla,
-];
-
-function sortByIndicatorLabel(indicator1, indicator2) {
- const s1 = (indicator1.label || indicator1.name).toLowerCase();
- const s2 = (indicator2.label || indicator2.name).toLowerCase();
- if (s1 < s2) {
- return -1;
- }
- if (s1 > s2) {
- return 1;
- }
- return 0;
-}
-
-export default class FilterIndicatorsContainer extends React.PureComponent {
- getFilterIndicators() {
- const {
- datasources = {},
- dashboardFilters,
- chartId: currentChartId,
- filterFieldOnFocus,
- } = this.props;
-
- if (Object.keys(dashboardFilters).length === 0) {
- return [];
- }
-
- const dashboardFiltersColorMap = getFilterColorMap();
- const sortIndicatorsByEmptiness = Object.values(dashboardFilters).reduce(
- (indicators, dashboardFilter) => {
- const {
- chartId,
- componentId,
- datasourceId,
- directPathToFilter,
- isDateFilter,
- isInstantFilter,
- columns,
- labels,
- scopes,
- } = dashboardFilter;
- const datasource = datasources[datasourceId] || {};
-
- if (currentChartId !== chartId) {
- Object.keys(columns)
- .filter(name =>
- getChartIdsInFilterScope({ filterScope: scopes[name] }).includes(
- currentChartId,
- ),
- )
- .forEach(name => {
- const colorMapKey = getDashboardFilterKey({
- chartId,
- column: name,
- });
-
- // filter values could be single value or array of values
- const values =
- isNil(columns[name]) ||
- (isDateFilter && columns[name] === 'No filter') ||
- (Array.isArray(columns[name]) && columns[name].length === 0)
- ? []
- : [].concat(columns[name]);
-
- const indicator = {
- chartId,
- colorCode: dashboardFiltersColorMap[colorMapKey],
- componentId,
- directPathToFilter: directPathToFilter.concat(`LABEL-${name}`),
- isDateFilter,
- isInstantFilter,
- name,
- label: labels[name] || name,
- values,
- isFilterFieldActive:
- chartId === filterFieldOnFocus.chartId &&
- name === filterFieldOnFocus.column,
- };
-
- // map time granularity value to datasource configure
- if (isDateFilter && TIME_GRANULARITY_FIELDS.includes(name)) {
- const timeGranularityConfig =
- (name === TIME_FILTER_MAP.time_grain_sqla
- ? datasource.time_grain_sqla
- : datasource.granularity) || [];
- const timeGranularityDisplayMapping = timeGranularityConfig.reduce(
- (map, [key, value]) => ({
- ...map,
- [key]: value,
- }),
- {},
- );
-
- indicator.values = indicator.values.map(
- value => timeGranularityDisplayMapping[value] || value,
- );
- }
-
- if (isEmpty(indicator.values)) {
- indicators[1].push(indicator);
- } else {
- indicators[0].push(indicator);
- }
- });
- }
-
- return indicators;
- },
- [[], []],
- );
-
- // cypress' electron don't support [].flat():
- return [
- ...sortIndicatorsByEmptiness[0].sort(sortByIndicatorLabel),
- ...sortIndicatorsByEmptiness[1].sort(sortByIndicatorLabel),
- ];
- }
-
- render() {
- const { chartStatus, setDirectPathToChild } = this.props;
- if (chartStatus === 'loading') {
- return null;
- }
-
- const indicators = this.getFilterIndicators();
- // if total indicators <= FILTER_INDICATORS_DISPLAY_LENGTH,
- // show indicator for each filter field.
- // else: show single group indicator.
- const showIndicatorsInGroup =
- indicators.length > FILTER_INDICATORS_DISPLAY_LENGTH;
-
- return (
-
- {!showIndicatorsInGroup &&
- indicators.map(indicator => (
-
- ))}
- {showIndicatorsInGroup && (
-
- )}
-
- );
- }
-}
-
-FilterIndicatorsContainer.propTypes = propTypes;
-FilterIndicatorsContainer.defaultProps = defaultProps;
diff --git a/superset-frontend/src/dashboard/components/FilterTooltipWrapper.jsx b/superset-frontend/src/dashboard/components/FilterTooltipWrapper.jsx
deleted file mode 100644
index 990129e373..0000000000
--- a/superset-frontend/src/dashboard/components/FilterTooltipWrapper.jsx
+++ /dev/null
@@ -1,82 +0,0 @@
-/**
- * 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 React from 'react';
-import PropTypes from 'prop-types';
-import { Overlay, Tooltip } from 'react-bootstrap';
-
-const propTypes = {
- tooltip: PropTypes.node.isRequired,
- children: PropTypes.node.isRequired,
-};
-
-class FilterTooltipWrapper extends React.Component {
- constructor(props) {
- super(props);
-
- // internal instance variable to make tooltip show/hide have delay
- this.isHover = false;
- this.state = {
- show: false,
- };
-
- this.showTooltip = this.showTooltip.bind(this);
- this.hideTooltip = this.hideTooltip.bind(this);
- this.attachRef = target => this.setState({ target });
- }
-
- showTooltip() {
- this.isHover = true;
-
- setTimeout(() => this.isHover && this.setState({ show: true }), 100);
- }
-
- hideTooltip() {
- this.isHover = false;
-
- setTimeout(() => !this.isHover && this.setState({ show: false }), 300);
- }
-
- render() {
- const { show, target } = this.state;
- return (
- <>
-
-
-
- {this.props.tooltip}
-
-
-
-
-
- {this.props.children}
-
- >
- );
- }
-}
-
-FilterTooltipWrapper.propTypes = propTypes;
-
-export default FilterTooltipWrapper;
diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel.tsx
new file mode 100644
index 0000000000..9246a2cae2
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel.tsx
@@ -0,0 +1,193 @@
+/**
+ * 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 React, { useState } from 'react';
+import { t, tn, useTheme } from '@superset-ui/core';
+import {
+ SearchOutlined,
+ MinusCircleFilled,
+ CheckCircleFilled,
+ ExclamationCircleFilled,
+} from '@ant-design/icons';
+import { Collapse, Popover } from 'src/common/components/index';
+import { Indent, Item, ItemIcon, Panel, Reset, Title, Summary } from './Styles';
+import { Indicator } from './selectors';
+
+export interface IndicatorProps {
+ indicator: Indicator;
+ onClick: (path: string[]) => void;
+}
+
+const Indicator = ({
+ indicator: { column, name, value = [], path },
+ onClick,
+}: IndicatorProps) => {
+ return (
+
- onClick([...path, `LABEL-${column}`])}>
+
+
+
+ {name.toUpperCase()}
+ {value.length ? `: ${value.join(', ')}` : ''}
+
+ );
+};
+
+export interface DetailsPanelProps {
+ appliedIndicators: Indicator[];
+ incompatibleIndicators: Indicator[];
+ unsetIndicators: Indicator[];
+ onHighlightFilterSource: (path: string[]) => void;
+ children: JSX.Element;
+}
+
+const DetailsPanelPopover = ({
+ appliedIndicators = [],
+ incompatibleIndicators = [],
+ unsetIndicators = [],
+ onHighlightFilterSource,
+ children,
+}: DetailsPanelProps) => {
+ const theme = useTheme();
+
+ function defaultActivePanel() {
+ if (incompatibleIndicators.length) return 'incompatible';
+ if (appliedIndicators.length) return 'applied';
+ return 'unset';
+ }
+
+ const [activePanels, setActivePanels] = useState
(() => [
+ defaultActivePanel(),
+ ]);
+
+ function handlePopoverStatus(isOpen: boolean) {
+ // every time the popover opens, make sure the most relevant panel is active
+ if (isOpen) {
+ if (!activePanels.includes(defaultActivePanel())) {
+ setActivePanels([...activePanels, defaultActivePanel()]);
+ }
+ }
+ }
+
+ function handleActivePanelChange(panels: string | string[]) {
+ // need to convert to an array so that handlePopoverStatus will work
+ if (typeof panels === 'string') {
+ setActivePanels([panels]);
+ } else {
+ setActivePanels(panels);
+ }
+ }
+
+ const total =
+ appliedIndicators.length +
+ incompatibleIndicators.length +
+ unsetIndicators.length;
+
+ const content = (
+
+
+ {tn('%d Scoped Filter', '%d Scoped Filters', total, total)}
+
+
+
+ {appliedIndicators.length ? (
+
+ {' '}
+ {t('Applied (%d)', appliedIndicators.length)}
+
+ }
+ >
+
+ {appliedIndicators.map(indicator => (
+
+ ))}
+
+
+ ) : null}
+ {incompatibleIndicators.length ? (
+
+ {' '}
+ {t('Incompatible (%d)', incompatibleIndicators.length)}
+
+ }
+ >
+
+ {incompatibleIndicators.map(indicator => (
+
+ ))}
+
+
+ ) : null}
+ {unsetIndicators.length ? (
+
+ {' '}
+ {t('Unset (%d)', unsetIndicators.length)}
+
+ }
+ disabled={!unsetIndicators.length}
+ >
+
+ {unsetIndicators.map(indicator => (
+
+ ))}
+
+
+ ) : null}
+
+
+
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default DetailsPanelPopover;
diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx
new file mode 100644
index 0000000000..ba6e358e61
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx
@@ -0,0 +1,131 @@
+/**
+ * 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 { styled } from '@superset-ui/core';
+
+export const Pill = styled.div`
+ display: inline-block;
+ background: ${({ theme }) => theme.colors.grayscale.base};
+ color: ${({ theme }) => theme.colors.grayscale.light5};
+ border-radius: 1em;
+ vertical-align: text-top;
+ padding: ${({ theme }) => `${theme.gridUnit}px ${theme.gridUnit * 2}px`};
+ font-size: ${({ theme }) => theme.typography.sizes.m}px;
+ font-weight: bold;
+ min-width: 1em;
+ min-height: 1em;
+ line-height: 1em;
+ vertical-align: middle;
+ white-space: nowrap;
+
+ svg {
+ position: relative;
+ top: -1px;
+ }
+
+ &:hover {
+ cursor: pointer;
+ background: ${({ theme }) => theme.colors.grayscale.dark1};
+ }
+
+ &.has-incompatible-filters {
+ color: ${({ theme }) => theme.colors.grayscale.dark2};
+ background: ${({ theme }) => theme.colors.alert.base};
+ &:hover {
+ background: ${({ theme }) => theme.colors.alert.dark1};
+ }
+ }
+
+ &.filters-inactive {
+ color: ${({ theme }) => theme.colors.grayscale.light5};
+ background: ${({ theme }) => theme.colors.grayscale.light1};
+ padding: ${({ theme }) => theme.gridUnit}px;
+ text-align: center;
+ height: 22px;
+ width: 22px;
+
+ &:hover {
+ background: ${({ theme }) => theme.colors.grayscale.base};
+ }
+ }
+`;
+
+export const WarningPill = styled(Pill)`
+ background: ${({ theme }) => theme.colors.alert.base};
+ color: ${({ theme }) => theme.colors.grayscale.dark1};
+`;
+
+export const UnsetPill = styled(Pill)`
+ background: ${({ theme }) => theme.colors.grayscale.light1};
+`;
+
+export interface TitleProps {
+ bold?: boolean;
+}
+
+export const Title = styled.span`
+ font-weight: ${({ bold, theme }) => {
+ return bold ? theme.typography.weights.bold : 'auto';
+ }};
+`;
+
+export const Summary = styled.div`
+ font-weight: ${({ theme }) => theme.typography.weights.bold};
+`;
+
+export const ItemIcon = styled.i`
+ display: none;
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ left: -${({ theme }) => theme.gridUnit * 5}px;
+`;
+
+export const Item = styled.button`
+ cursor: pointer;
+ display: block;
+ padding: 0;
+ border: none;
+ background: none;
+ white-space: nowrap;
+ position: relative;
+ outline: none;
+
+ &::-moz-focus-inner {
+ border: 0;
+ }
+
+ &:hover > i {
+ display: block;
+ }
+`;
+
+export const Reset = styled.div`
+ margin: 0 -${({ theme }) => theme.gridUnit * 4}px;
+`;
+
+export const Indent = styled.div`
+ padding-left: ${({ theme }) => theme.gridUnit * 6}px;
+ margin: -${({ theme }) => theme.gridUnit * 3}px 0;
+`;
+
+export const Panel = styled.div`
+ min-width: 200px;
+ max-width: 400px;
+ overflow-x: hidden;
+`;
diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx
new file mode 100644
index 0000000000..92d0ab088c
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx
@@ -0,0 +1,84 @@
+/**
+ * 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 React from 'react';
+import cx from 'classnames';
+import Icon from 'src/components/Icon';
+import DetailsPanelPopover from './DetailsPanel';
+import { Pill } from './Styles';
+import { Indicator } from './selectors';
+
+export interface FiltersBadgeProps {
+ appliedIndicators: Indicator[];
+ unsetIndicators: Indicator[];
+ incompatibleIndicators: Indicator[];
+ onHighlightFilterSource: (path: string[]) => void;
+}
+
+const FiltersBadge = ({
+ appliedIndicators,
+ unsetIndicators,
+ incompatibleIndicators,
+ onHighlightFilterSource,
+}: FiltersBadgeProps) => {
+ if (
+ !appliedIndicators.length &&
+ !incompatibleIndicators.length &&
+ !unsetIndicators.length
+ ) {
+ return null;
+ }
+
+ const isInactive =
+ !appliedIndicators.length && !incompatibleIndicators.length;
+
+ return (
+
+
+
+ {!isInactive && (
+
+ {appliedIndicators.length}
+
+ )}
+ {incompatibleIndicators.length ? (
+ <>
+ {' '}
+
+
+ {incompatibleIndicators.length}
+
+ >
+ ) : null}
+
+
+ );
+};
+
+export default FiltersBadge;
diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts
new file mode 100644
index 0000000000..9cdd43a22c
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts
@@ -0,0 +1,161 @@
+/**
+ * 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 { getChartIdsInFilterScope } from '../../util/activeDashboardFilters';
+import { TIME_FILTER_MAP } from '../../../visualizations/FilterBox/FilterBox';
+
+export enum IndicatorStatus {
+ Unset = 'UNSET',
+ Applied = 'APPLIED',
+ Incompatible = 'INCOMPATIBLE',
+}
+
+const TIME_GRANULARITY_FIELDS = new Set(Object.values(TIME_FILTER_MAP));
+
+// As of 2020-09-28, the DatasourceMeta type in superset-ui is incorrect.
+// Should patch it here until the DatasourceMeta type is updated.
+type Datasource = {
+ time_grain_sqla?: [string, string][];
+ granularity?: [string, string][];
+};
+
+type Filter = {
+ chartId: number;
+ columns: { [key: string]: string | string[] };
+ scopes: { [key: string]: any };
+ labels: { [key: string]: string };
+ isDateFilter: boolean;
+ directPathToFilter: string[];
+ datasourceId: string;
+};
+
+const selectIndicatorValue = (
+ columnKey: string,
+ filter: Filter,
+ datasource: Datasource,
+): string[] => {
+ const values = filter.columns[columnKey];
+ const arrValues = Array.isArray(values) ? values : [values];
+
+ if (
+ values == null ||
+ (filter.isDateFilter && values === 'No filter') ||
+ arrValues.length === 0
+ ) {
+ return [];
+ }
+
+ if (filter.isDateFilter && TIME_GRANULARITY_FIELDS.has(columnKey)) {
+ const timeGranularityMap = (
+ (columnKey === TIME_FILTER_MAP.time_grain_sqla
+ ? datasource.time_grain_sqla
+ : datasource.granularity) || []
+ ).reduce(
+ (map, [key, value]) => ({
+ ...map,
+ [key]: value,
+ }),
+ {},
+ );
+
+ return arrValues.map(value => timeGranularityMap[value] || value);
+ }
+
+ return arrValues;
+};
+
+const selectIndicatorsForChartFromFilter = (
+ chartId: number,
+ filter: Filter,
+ filterDataSource: Datasource,
+ appliedColumns: Set,
+ rejectedColumns: Set,
+): Indicator[] => {
+ // filters can be applied (if the filter is compatible with the datasource)
+ // or rejected (if the filter is incompatible)
+ // or the status can be unknown (if the filter has calculated parameters that we can't analyze)
+ const getStatus = (column: string) => {
+ if (appliedColumns.has(column)) return IndicatorStatus.Applied;
+ if (rejectedColumns.has(column)) return IndicatorStatus.Incompatible;
+ return IndicatorStatus.Unset;
+ };
+
+ return Object.keys(filter.columns)
+ .filter(column =>
+ getChartIdsInFilterScope({
+ filterScope: filter.scopes[column],
+ }).includes(chartId),
+ )
+ .map(column => ({
+ column,
+ name: filter.labels[column] || column,
+ value: selectIndicatorValue(column, filter, filterDataSource),
+ status: getStatus(column),
+ path: filter.directPathToFilter,
+ }));
+};
+
+export type Indicator = {
+ column: string;
+ name: string;
+ value: string[];
+ status: IndicatorStatus;
+ path: string[];
+};
+
+// inspects redux state to find what the filter indicators should be shown for a given chart
+export const selectIndicatorsForChart = (
+ chartId: number,
+ filters: { [key: number]: Filter },
+ datasources: { [key: string]: Datasource },
+ charts: any,
+): Indicator[] => {
+ const chart = charts[chartId];
+ // no indicators if chart is loading
+ if (chart.chartStatus === 'loading') return [];
+
+ // for now we only need to know which columns are compatible/incompatible,
+ // so grab the columns from the applied/rejected filters
+ const appliedColumns: Set = new Set(
+ (chart?.queryResponse?.applied_filters || []).map(
+ (filter: any) => filter.column,
+ ),
+ );
+ const rejectedColumns: Set = new Set(
+ (chart?.queryResponse?.rejected_filters || []).map(
+ (filter: any) => filter.column,
+ ),
+ );
+ const indicators = Object.values(filters)
+ .filter(filter => filter.chartId !== chartId)
+ .reduce(
+ (acc, filter) =>
+ acc.concat(
+ selectIndicatorsForChartFromFilter(
+ chartId,
+ filter,
+ datasources[filter.datasourceId] || {},
+ appliedColumns,
+ rejectedColumns,
+ ),
+ ),
+ [] as Indicator[],
+ );
+ indicators.sort((a, b) => a.name.localeCompare(b.name));
+ return indicators;
+};
diff --git a/superset-frontend/src/dashboard/components/Header.jsx b/superset-frontend/src/dashboard/components/Header.jsx
index 42c9c507f0..4392d00c98 100644
--- a/superset-frontend/src/dashboard/components/Header.jsx
+++ b/superset-frontend/src/dashboard/components/Header.jsx
@@ -106,6 +106,14 @@ const StyledDashboardHeader = styled.div`
.fave-unfave-icon {
margin-left: ${({ theme }) => theme.gridUnit * 2}px;
}
+ .button-container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ .action-button {
+ font-size: ${({ theme }) => theme.typography.sizes.xl}px;
+ }
+ }
`;
class Header extends React.PureComponent {
diff --git a/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx b/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx
index ea666da377..35f9a3085c 100644
--- a/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx
+++ b/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx
@@ -151,7 +151,7 @@ class HeaderActionsDropdown extends React.PureComponent {
title={}
noCaret
id="save-dash-split-button"
- bsSize="small"
+ bsSize="large"
style={{ border: 'none', padding: 0, marginLeft: '4px' }}
pullRight
>
diff --git a/superset-frontend/src/dashboard/components/SliceHeader.jsx b/superset-frontend/src/dashboard/components/SliceHeader.jsx
index 9733511a50..1f62d839b2 100644
--- a/superset-frontend/src/dashboard/components/SliceHeader.jsx
+++ b/superset-frontend/src/dashboard/components/SliceHeader.jsx
@@ -23,6 +23,7 @@ import { t } from '@superset-ui/core';
import EditableTitle from '../../components/EditableTitle';
import TooltipWrapper from '../../components/TooltipWrapper';
import SliceHeaderControls from './SliceHeaderControls';
+import FiltersBadge from '../containers/FiltersBadge';
const propTypes = {
innerRef: PropTypes.func,
@@ -105,7 +106,7 @@ class SliceHeader extends React.PureComponent {
return (
-
+
)}
+
+
{!editMode && (
-
+ <>
+
+
+ >
)}
diff --git a/superset-frontend/src/dashboard/components/filterscope/FilterFieldItem.jsx b/superset-frontend/src/dashboard/components/filterscope/FilterFieldItem.jsx
index 693546202d..3f1d88d475 100644
--- a/superset-frontend/src/dashboard/components/filterscope/FilterFieldItem.jsx
+++ b/superset-frontend/src/dashboard/components/filterscope/FilterFieldItem.jsx
@@ -21,22 +21,19 @@ import PropTypes from 'prop-types';
import cx from 'classnames';
import FormLabel from 'src/components/FormLabel';
-import FilterBadgeIcon from 'src/components/FilterBadgeIcon';
const propTypes = {
label: PropTypes.string.isRequired,
- colorCode: PropTypes.string.isRequired,
isSelected: PropTypes.bool.isRequired,
};
-export default function FilterFieldItem({ label, colorCode, isSelected }) {
+export default function FilterFieldItem({ label, isSelected }) {
return (
-
{label}
);
diff --git a/superset-frontend/src/dashboard/components/filterscope/renderFilterFieldTreeNodes.jsx b/superset-frontend/src/dashboard/components/filterscope/renderFilterFieldTreeNodes.jsx
index 5a3264cf09..55d1bae910 100644
--- a/superset-frontend/src/dashboard/components/filterscope/renderFilterFieldTreeNodes.jsx
+++ b/superset-frontend/src/dashboard/components/filterscope/renderFilterFieldTreeNodes.jsx
@@ -19,7 +19,6 @@
import React from 'react';
import FilterFieldItem from './FilterFieldItem';
-import { getFilterColorMap } from '../../util/dashboardFiltersColorMap';
export default function renderFilterFieldTreeNodes({ nodes, activeKey }) {
if (!nodes) {
@@ -32,15 +31,10 @@ export default function renderFilterFieldTreeNodes({ nodes, activeKey }) {
...node,
children: node.children.map(child => {
const { label, value } = child;
- const colorCode = getFilterColorMap()[value];
return {
...child,
label: (
-
+
),
};
}),
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx
index aecdbb700a..94af05acd6 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx
@@ -196,8 +196,8 @@ class Chart extends React.Component {
this.props.setFocusedFilterField(chartId, column);
}
- handleFilterMenuClose() {
- this.props.unsetFocusedFilterField();
+ handleFilterMenuClose(chartId, column) {
+ this.props.unsetFocusedFilterField(chartId, column);
}
exploreChart() {
diff --git a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx
index f1f4f9103c..61a0cf2e45 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx
@@ -18,8 +18,10 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
+import cx from 'classnames';
+import { useTheme } from '@superset-ui/core';
-import FilterIndicators from '../../containers/FilterIndicators';
+import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters';
import Chart from '../../containers/Chart';
import AnchorLink from '../../../components/AnchorLink';
import DeleteComponentButton from '../DeleteComponentButton';
@@ -50,6 +52,7 @@ const propTypes = {
editMode: PropTypes.bool.isRequired,
directPathToChild: PropTypes.arrayOf(PropTypes.string),
directPathLastUpdated: PropTypes.number,
+ focusedFilterScope: PropTypes.object,
// grid related
availableColumnCount: PropTypes.number.isRequired,
@@ -69,6 +72,53 @@ const defaultProps = {
directPathLastUpdated: 0,
};
+/**
+ * Renders any styles necessary to highlight the chart's relationship to the focused filter.
+ *
+ * If there is no focused filter scope (i.e. most of the time), this will be just a pass-through.
+ *
+ * If the chart is outside the scope of the focused filter, dims the chart.
+ *
+ * If the chart is in the scope of the focused filter,
+ * renders a highlight around the chart.
+ *
+ * If ChartHolder were a function component, this could be implemented as a hook instead.
+ */
+const FilterFocusHighlight = React.forwardRef(
+ ({ chartId, focusedFilterScope, ...otherProps }, ref) => {
+ const theme = useTheme();
+ if (!focusedFilterScope) return
;
+
+ // we use local styles here instead of a conditionally-applied class,
+ // because adding any conditional class to this container
+ // causes performance issues in Chrome.
+
+ // default to the "de-emphasized" state
+ let styles = { opacity: 0.3, pointerEvents: 'none' };
+
+ if (
+ chartId === focusedFilterScope.chartId ||
+ getChartIdsInFilterScope({
+ filterScope: focusedFilterScope.scope,
+ }).includes(chartId)
+ ) {
+ // apply the "highlighted" state if this chart
+ // contains a filter being focused, or is in scope of a focused filter.
+ styles = {
+ borderColor: theme.colors.primary.light2,
+ opacity: 1,
+ boxShadow: `0px 0px ${({ theme }) => theme.gridUnit * 2}px ${
+ theme.colors.primary.light2
+ }`,
+ pointerEvents: 'auto',
+ };
+ }
+
+ // inline styles are used here due to a performance issue when adding/changing a class, which causes a reflow
+ return
;
+ },
+);
+
class ChartHolder extends React.Component {
static renderInFocusCSS(columnName) {
return (
@@ -182,6 +232,7 @@ class ChartHolder extends React.Component {
editMode,
isComponentVisible,
dashboardId,
+ focusedFilterScope,
} = this.props;
// inherit the size of parent columns
@@ -207,6 +258,8 @@ class ChartHolder extends React.Component {
);
}
+ const { chartId } = component.meta;
+
return (
-
{!editMode && (
- {!editMode && (
-
- )}
{editMode && (
@@ -278,7 +333,7 @@ class ChartHolder extends React.Component {
)}
-
+
{dropIndicatorProps && }
diff --git a/superset-frontend/src/dashboard/components/gridComponents/index.js b/superset-frontend/src/dashboard/components/gridComponents/index.js
index 44086bbd4b..27f419c9dd 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/index.js
+++ b/superset-frontend/src/dashboard/components/gridComponents/index.js
@@ -45,7 +45,7 @@ export { default as Row } from './Row';
export { default as Tab } from './Tab';
export { default as Tabs } from './Tabs';
-export default {
+export const componentLookup = {
[CHART_TYPE]: ChartHolder,
[MARKDOWN_TYPE]: Markdown,
[COLUMN_TYPE]: Column,
diff --git a/superset-frontend/src/dashboard/components/resizable/ResizableContainer.jsx b/superset-frontend/src/dashboard/components/resizable/ResizableContainer.jsx
index 26c5f66574..ff576101f4 100644
--- a/superset-frontend/src/dashboard/components/resizable/ResizableContainer.jsx
+++ b/superset-frontend/src/dashboard/components/resizable/ResizableContainer.jsx
@@ -201,13 +201,19 @@ class ResizableContainer extends React.PureComponent {
adjustableWidth
? Math.max(
size.width,
- maxWidthMultiple * (widthStep + gutterWidth) - gutterWidth,
+ Math.min(
+ proxyToInfinity,
+ maxWidthMultiple * (widthStep + gutterWidth) - gutterWidth,
+ ),
)
: undefined
}
maxHeight={
adjustableHeight
- ? Math.max(size.height, maxHeightMultiple * heightStep)
+ ? Math.max(
+ size.height,
+ Math.min(proxyToInfinity, maxHeightMultiple * heightStep),
+ )
: undefined
}
size={size}
diff --git a/superset-frontend/src/dashboard/containers/DashboardBuilder.jsx b/superset-frontend/src/dashboard/containers/DashboardBuilder.jsx
index dacdb7b810..3a9c8cc67d 100644
--- a/superset-frontend/src/dashboard/containers/DashboardBuilder.jsx
+++ b/superset-frontend/src/dashboard/containers/DashboardBuilder.jsx
@@ -38,6 +38,7 @@ function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState }) {
showBuilderPane: dashboardState.showBuilderPane,
directPathToChild: dashboardState.directPathToChild,
colorScheme: dashboardState.colorScheme,
+ focusedFilterField: dashboardState.focusedFilterField,
};
}
diff --git a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx
index b7ac2942e0..6920b8b011 100644
--- a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx
+++ b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx
@@ -23,7 +23,7 @@ import { connect } from 'react-redux';
import { logEvent } from 'src/logger/actions';
import { addDangerToast } from 'src/messageToasts/actions';
-import ComponentLookup from '../components/gridComponents';
+import { componentLookup } from '../components/gridComponents';
import getDetailedComponentWidth from '../util/getDetailedComponentWidth';
import { getActiveFilters } from '../util/activeDashboardFilters';
import { componentShape } from '../util/propShapes';
@@ -57,8 +57,28 @@ const defaultProps = {
isComponentVisible: true,
};
+/**
+ * Selects the chart scope of the filter input that has focus.
+ *
+ * @returns {{chartId: number, scope: { scope: string[], immune: string[] }} | null }
+ * the scope of the currently focused filter, if any
+ */
+function selectFocusedFilterScope(dashboardState, dashboardFilters) {
+ if (!dashboardState.focusedFilterField) return null;
+ const { chartId, column } = dashboardState.focusedFilterField;
+ return {
+ chartId,
+ scope: dashboardFilters[chartId].scopes[column],
+ };
+}
+
function mapStateToProps(
- { dashboardLayout: undoableLayout, dashboardState, dashboardInfo },
+ {
+ dashboardLayout: undoableLayout,
+ dashboardState,
+ dashboardInfo,
+ dashboardFilters,
+ },
ownProps,
) {
const dashboardLayout = undoableLayout.present;
@@ -74,10 +94,10 @@ function mapStateToProps(
directPathToChild: dashboardState.directPathToChild,
directPathLastUpdated: dashboardState.directPathLastUpdated,
dashboardId: dashboardInfo.id,
- filterFieldOnFocus:
- dashboardState.focusedFilterField.length === 0
- ? {}
- : dashboardState.focusedFilterField.slice(-1).pop(),
+ focusedFilterScope: selectFocusedFilterScope(
+ dashboardState,
+ dashboardFilters,
+ ),
};
// rows and columns need more data about their child dimensions
@@ -117,7 +137,7 @@ function mapDispatchToProps(dispatch) {
class DashboardComponent extends React.PureComponent {
render() {
const { component } = this.props;
- const Component = component ? ComponentLookup[component.type] : null;
+ const Component = component ? componentLookup[component.type] : null;
return Component ? : null;
}
}
diff --git a/superset-frontend/src/dashboard/containers/FilterIndicators.jsx b/superset-frontend/src/dashboard/containers/FilterIndicators.jsx
deleted file mode 100644
index d5ec3a9cac..0000000000
--- a/superset-frontend/src/dashboard/containers/FilterIndicators.jsx
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * 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 { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
-
-import FilterIndicatorsContainer from '../components/FilterIndicatorsContainer';
-import { setDirectPathToChild } from '../actions/dashboardState';
-
-function mapStateToProps(
- { datasources, dashboardState, dashboardFilters, dashboardLayout, charts },
- ownProps,
-) {
- const { chartId } = ownProps;
- const { chartStatus } = charts[chartId] || {};
-
- return {
- datasources,
- dashboardFilters,
- chartId,
- chartStatus,
- layout: dashboardLayout.present,
- filterFieldOnFocus:
- dashboardState.focusedFilterField.length === 0
- ? {}
- : dashboardState.focusedFilterField.slice(-1).pop(),
- };
-}
-
-function mapDispatchToProps(dispatch) {
- return bindActionCreators(
- {
- setDirectPathToChild,
- },
- dispatch,
- );
-}
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps,
-)(FilterIndicatorsContainer);
diff --git a/superset-frontend/src/dashboard/containers/FiltersBadge.tsx b/superset-frontend/src/dashboard/containers/FiltersBadge.tsx
new file mode 100644
index 0000000000..6dccc7d51d
--- /dev/null
+++ b/superset-frontend/src/dashboard/containers/FiltersBadge.tsx
@@ -0,0 +1,70 @@
+/**
+ * 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 { connect, Dispatch } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import { setDirectPathToChild } from 'src/dashboard/actions/dashboardState';
+import {
+ selectIndicatorsForChart,
+ IndicatorStatus,
+} from 'src/dashboard/components/FiltersBadge/selectors';
+import FiltersBadge from 'src/dashboard/components/FiltersBadge';
+
+export interface FiltersBadgeProps {
+ chartId: number;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch) => {
+ return bindActionCreators(
+ {
+ onHighlightFilterSource: setDirectPathToChild,
+ },
+ dispatch,
+ );
+};
+
+const mapStateToProps = (
+ { datasources, dashboardFilters, charts }: any,
+ { chartId }: FiltersBadgeProps,
+) => {
+ const indicators = selectIndicatorsForChart(
+ chartId,
+ dashboardFilters,
+ datasources,
+ charts,
+ );
+
+ const appliedIndicators = indicators.filter(
+ indicator => indicator.status === IndicatorStatus.Applied,
+ );
+ const unsetIndicators = indicators.filter(
+ indicator => indicator.status === IndicatorStatus.Unset,
+ );
+ const incompatibleIndicators = indicators.filter(
+ indicator => indicator.status === IndicatorStatus.Incompatible,
+ );
+
+ return {
+ chartId,
+ appliedIndicators,
+ unsetIndicators,
+ incompatibleIndicators,
+ };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(FiltersBadge);
diff --git a/superset-frontend/src/dashboard/reducers/dashboardFilters.js b/superset-frontend/src/dashboard/reducers/dashboardFilters.js
index 6bb35f889b..f508c1bfe3 100644
--- a/superset-frontend/src/dashboard/reducers/dashboardFilters.js
+++ b/superset-frontend/src/dashboard/reducers/dashboardFilters.js
@@ -28,7 +28,6 @@ import {
import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox';
import { DASHBOARD_ROOT_ID } from '../util/constants';
import getFilterConfigsFromFormdata from '../util/getFilterConfigsFromFormdata';
-import { buildFilterColorMap } from '../util/dashboardFiltersColorMap';
import { buildActiveFilters } from '../util/activeDashboardFilters';
import { getChartIdAndColumnFromFilterKey } from '../util/getDashboardFilterKey';
@@ -159,7 +158,6 @@ export default function dashboardFiltersReducer(dashboardFilters = {}, action) {
const { chartId } = action;
const { [chartId]: deletedFilter, ...updatedFilters } = dashboardFilters;
buildActiveFilters({ dashboardFilters: updatedFilters });
- buildFilterColorMap(updatedFilters);
return updatedFilters;
}
@@ -173,7 +171,6 @@ export default function dashboardFiltersReducer(dashboardFilters = {}, action) {
if (CHANGE_FILTER_VALUE_ACTIONS.includes(action.type)) {
buildActiveFilters({ dashboardFilters: updatedFilters });
- buildFilterColorMap(updatedFilters);
}
return updatedFilters;
diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.js b/superset-frontend/src/dashboard/reducers/dashboardState.js
index b85df0e90a..c7fde4abeb 100644
--- a/superset-frontend/src/dashboard/reducers/dashboardState.js
+++ b/superset-frontend/src/dashboard/reducers/dashboardState.js
@@ -35,6 +35,7 @@ import {
SET_DIRECT_PATH,
SET_MOUNTED_TAB,
SET_FOCUSED_FILTER_FIELD,
+ UNSET_FOCUSED_FILTER_FIELD,
} from '../actions/dashboardState';
export default function dashboardStateReducer(state = {}, action) {
@@ -139,18 +140,28 @@ export default function dashboardStateReducer(state = {}, action) {
};
},
[SET_FOCUSED_FILTER_FIELD]() {
- const { focusedFilterField } = state;
- if (action.chartId && action.column) {
- focusedFilterField.push({
+ return {
+ ...state,
+ focusedFilterField: {
chartId: action.chartId,
column: action.column,
- });
- } else {
- focusedFilterField.shift();
+ },
+ };
+ },
+ [UNSET_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.
+ if (
+ !state.focusedFilterField ||
+ action.chartId !== state.focusedFilterField.chartId ||
+ action.column !== state.focusedFilterField.column
+ ) {
+ return state;
}
return {
...state,
- focusedFilterField,
+ focusedFilterField: null,
};
},
};
diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js
index 1f5122854b..b572c4d363 100644
--- a/superset-frontend/src/dashboard/reducers/getInitialState.js
+++ b/superset-frontend/src/dashboard/reducers/getInitialState.js
@@ -40,7 +40,6 @@ import {
CHART_TYPE,
ROW_TYPE,
} from '../util/componentTypes';
-import { buildFilterColorMap } from '../util/dashboardFiltersColorMap';
import findFirstParentContainerId from '../util/findFirstParentContainer';
import getEmptyLayout from '../util/getEmptyLayout';
import getFilterConfigsFromFormdata from '../util/getFilterConfigsFromFormdata';
@@ -232,7 +231,6 @@ export default function getInitialState(bootstrapData) {
dashboardFilters,
components: layout,
});
- buildFilterColorMap(dashboardFilters, layout);
// store the header as a layout component so we can undo/redo changes
layout[DASHBOARD_HEADER_ID] = {
@@ -283,12 +281,7 @@ export default function getInitialState(bootstrapData) {
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: [],
+ focusedFilterField: null,
expandedSlices: dashboard.metadata.expanded_slices || {},
refreshFrequency: dashboard.metadata.refresh_frequency || 0,
// dashboard viewers can set refresh frequency for the current visit,
diff --git a/superset-frontend/src/dashboard/stylesheets/builder.less b/superset-frontend/src/dashboard/stylesheets/builder.less
index fbf0ab93fc..82b4c59ef0 100644
--- a/superset-frontend/src/dashboard/stylesheets/builder.less
+++ b/superset-frontend/src/dashboard/stylesheets/builder.less
@@ -31,13 +31,6 @@
box-shadow: 0 4px 4px 0 fade(@darkest, @opacity-light); /* @TODO color */
}
-.dashboard-content {
- display: flex;
- flex-direction: row;
- flex-wrap: nowrap;
- height: auto;
-}
-
/* only top-level tabs have popover, give it more padding to match header + tabs */
.dashboard > .with-popover-menu > .popover-menu {
left: 24px;
@@ -49,17 +42,6 @@
padding-left: 8px; /* note this is added to tab-level padding, to match header */
}
-.dashboard-content .grid-container .dashboard-component-tabs {
- box-shadow: none;
- padding-left: 0;
-}
-
-.dashboard-content > div:first-child {
- width: 100%;
- flex-grow: 1;
- position: relative;
-}
-
.dropdown-toggle.btn.btn-primary .caret {
color: @lightest;
}
diff --git a/superset-frontend/src/dashboard/stylesheets/components/chart.less b/superset-frontend/src/dashboard/stylesheets/components/chart.less
index 25fb2066cd..b23ace530f 100644
--- a/superset-frontend/src/dashboard/stylesheets/components/chart.less
+++ b/superset-frontend/src/dashboard/stylesheets/components/chart.less
@@ -24,6 +24,10 @@
position: relative;
padding: 16px;
+ // transitionable traits for when a filter is being actively focused
+ transition: opacity 0.2s, border-color 0.2s, box-shadow 0.2s;
+ border: 2px solid transparent;
+
.missing-chart-container {
display: flex;
flex-direction: column;
@@ -109,10 +113,7 @@
}
.slice-header-controls-trigger {
- padding: 0 16px;
- position: absolute;
- top: 0;
- right: -16px; //increase the click-able area for the button
+ padding: 2px 6px;
&:hover {
cursor: pointer;
diff --git a/superset-frontend/src/dashboard/stylesheets/dashboard.less b/superset-frontend/src/dashboard/stylesheets/dashboard.less
index 97f2f26cf9..c86a7d983a 100644
--- a/superset-frontend/src/dashboard/stylesheets/dashboard.less
+++ b/superset-frontend/src/dashboard/stylesheets/dashboard.less
@@ -57,14 +57,34 @@ body {
}
.dashboard .chart-header {
- position: relative;
font-size: @font-size-l;
font-weight: @font-weight-bold;
margin-bottom: 4px;
+ display: flex;
+ max-width: 100%;
+
+ & > .header-title {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ max-width: 100%;
+ flex-grow: 1;
+ }
+
+ & > .header-controls {
+ display: flex;
+
+ & > * {
+ margin-left: 8px;
+ }
+ }
.dropdown.btn-group {
- position: absolute;
- right: 0;
+ pointer-events: none;
+ vertical-align: top;
+ & > * {
+ pointer-events: auto;
+ }
}
.dropdown-toggle.btn.btn-default {
@@ -110,12 +130,6 @@ body {
height: 100%;
}
}
-
- .button-container {
- display: flex;
- flex-direction: row;
- flex-wrap: nowrap;
- }
}
.dashboard .chart-header,
@@ -198,6 +212,10 @@ body {
flex-direction: row;
align-items: center;
+ .editable-title {
+ margin-right: 8px;
+ }
+
.favstar {
font-size: @font-size-xl;
position: relative;
diff --git a/superset-frontend/src/dashboard/stylesheets/filter-indicator-tooltip.less b/superset-frontend/src/dashboard/stylesheets/filter-indicator-tooltip.less
deleted file mode 100644
index 8af824e496..0000000000
--- a/superset-frontend/src/dashboard/stylesheets/filter-indicator-tooltip.less
+++ /dev/null
@@ -1,80 +0,0 @@
-/**
- * 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 '../../../stylesheets/less/variables.less';
-
-#filter-indicator-tooltip {
- font-size: @font-size-m;
- text-align: left;
-
- > .tooltip-arrow {
- margin-right: 8px;
- border-left-color: @gray-dark;
- }
-
- > .tooltip-inner {
- width: 200px;
- max-width: 200px;
- border-radius: @border-radius-large;
- margin-right: 8px;
- padding: 16px 12px;
- background-color: @gray-dark;
- text-align: left;
- }
-}
-
-.tooltip-item {
- position: relative;
-
- .filter-content {
- margin-right: 22px;
- text-align: left;
- display: flex;
- flex-flow: row wrap;
- align-items: stretch;
-
- label {
- font-weight: @font-weight-bold;
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
- margin-right: 4px;
- }
- }
-
- .filter-edit {
- cursor: pointer;
- position: absolute;
- top: 4px;
- right: 0;
- }
-}
-
-.group-title {
- margin-bottom: 8px;
-}
-
-.tooltip-group {
- margin: 0;
- padding: 0;
- list-style: none;
-
- li {
- margin-bottom: 8px;
- }
-}
diff --git a/superset-frontend/src/dashboard/stylesheets/filter-indicator.less b/superset-frontend/src/dashboard/stylesheets/filter-indicator.less
deleted file mode 100644
index c35a730bb2..0000000000
--- a/superset-frontend/src/dashboard/stylesheets/filter-indicator.less
+++ /dev/null
@@ -1,91 +0,0 @@
-/**
- * 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 '../../../stylesheets/less/variables.less';
-
-.dashboard-filter-indicators-container {
- position: absolute;
- right: -20px;
- top: 40px;
- width: 20px;
- height: 125px;
-
- .indicator-container {
- position: relative;
- margin-bottom: 4px;
- }
-
- .filter-indicator,
- .filter-indicator-group {
- width: 3px;
- height: 20px;
- overflow: hidden;
- display: flex;
- background-color: @gray-light;
- transition: width @timing-normal;
- border-top-right-radius: 2px;
- border-bottom-right-radius: 2px;
-
- .color-bar {
- width: 0px;
- height: 20px;
- position: absolute;
- right: 100%;
- transition: width @timing-normal;
- }
-
- .filter-badge {
- width: 20px;
- height: 20px;
- }
- }
-
- .filter-indicator-group {
- box-shadow: @lightest -2px 0 0 0, @gray-light -4px 0 0 0;
- }
-}
-
-.show-outline .filter-indicator-group {
- box-shadow: @shadow-highlight -2px 0 0 0, @lightest -4px 0 0 0,
- @gray-light -6px 0 0 0;
-}
-
-.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;
- }
-
- .filter-badge {
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
- }
- }
-
- .filter-indicator-group {
- box-shadow: @lightest -4px 0 0 0, @gray-light -6px 0 0 0;
- }
-}
diff --git a/superset-frontend/src/dashboard/stylesheets/index.less b/superset-frontend/src/dashboard/stylesheets/index.less
index 80fbe061a7..2f882ebe71 100644
--- a/superset-frontend/src/dashboard/stylesheets/index.less
+++ b/superset-frontend/src/dashboard/stylesheets/index.less
@@ -24,8 +24,6 @@
@import './dashboard.less';
@import './dnd.less';
@import './filter-scope-selector.less';
-@import './filter-indicator.less';
-@import './filter-indicator-tooltip.less';
@import './grid.less';
@import './hover-menu.less';
@import './popover-menu.less';
diff --git a/superset-frontend/src/dashboard/util/dashboardFiltersColorMap.js b/superset-frontend/src/dashboard/util/dashboardFiltersColorMap.js
deleted file mode 100644
index 129acf5ced..0000000000
--- a/superset-frontend/src/dashboard/util/dashboardFiltersColorMap.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * 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 { getDashboardFilterKey } from './getDashboardFilterKey';
-
-// should be consistent with @badge-colors .less variable
-const FILTER_COLORS_COUNT = 20;
-
-let filterColorMap = {};
-
-export function getFilterColorMap() {
- return filterColorMap;
-}
-
-export function buildFilterColorMap(allDashboardFilters = {}) {
- let filterColorIndex = 1;
- filterColorMap = Object.values(allDashboardFilters).reduce(
- (colorMap, filter) => {
- const { chartId, columns } = filter;
-
- Object.keys(columns)
- .sort()
- .forEach(column => {
- const key = getDashboardFilterKey({ chartId, column });
- const colorCode = `badge-${filterColorIndex % FILTER_COLORS_COUNT}`;
- /* eslint-disable no-param-reassign */
- colorMap[key] = colorCode;
-
- filterColorIndex += 1;
- });
-
- return colorMap;
- },
- {},
- );
-}
diff --git a/superset-frontend/src/dashboard/util/getDashboardFilterKey.ts b/superset-frontend/src/dashboard/util/getDashboardFilterKey.ts
index 26f00b7388..64e9aca1ca 100644
--- a/superset-frontend/src/dashboard/util/getDashboardFilterKey.ts
+++ b/superset-frontend/src/dashboard/util/getDashboardFilterKey.ts
@@ -29,8 +29,10 @@ export function getDashboardFilterKey({
return `${chartId}_${column}`;
}
+const filterKeySplitter = /^([0-9]+)_(.*)$/;
+
export function getChartIdAndColumnFromFilterKey(key: string) {
- const [chartId, ...parts] = key.split('_');
- const column = parts.slice().join('_');
- return { chartId: parseInt(chartId, 10), column };
+ const match = filterKeySplitter.exec(key);
+ if (!match) throw new Error('Cannot parse invalid filter key');
+ return { chartId: parseInt(match[1], 10), column: match[2] };
}
diff --git a/superset-frontend/src/dashboard/util/propShapes.jsx b/superset-frontend/src/dashboard/util/propShapes.jsx
index 3743b94132..b8c2b09547 100644
--- a/superset-frontend/src/dashboard/util/propShapes.jsx
+++ b/superset-frontend/src/dashboard/util/propShapes.jsx
@@ -71,19 +71,6 @@ export const slicePropShape = PropTypes.shape({
owners: PropTypes.arrayOf(PropTypes.string),
});
-export const filterIndicatorPropShape = PropTypes.shape({
- chartId: PropTypes.number.isRequired,
- colorCode: PropTypes.string.isRequired,
- componentId: PropTypes.string.isRequired,
- directPathToFilter: PropTypes.arrayOf(PropTypes.string).isRequired,
- isDateFilter: PropTypes.bool.isRequired,
- isFilterFieldActive: PropTypes.bool.isRequired,
- isInstantFilter: PropTypes.bool.isRequired,
- label: PropTypes.string.isRequired,
- name: PropTypes.string.isRequired,
- values: PropTypes.array.isRequired,
-});
-
export const dashboardFilterPropShape = PropTypes.shape({
chartId: PropTypes.number.isRequired,
componentId: PropTypes.string.isRequired,
diff --git a/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx b/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx
index ad9b290bc9..e835e4aa15 100644
--- a/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx
+++ b/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx
@@ -32,14 +32,11 @@ import Control from 'src/explore/components/Control';
import { controls } from 'src/explore/controls';
import { getExploreUrl } from 'src/explore/exploreUtils';
import OnPasteSelect from 'src/components/Select/OnPasteSelect';
-import { getDashboardFilterKey } from 'src/dashboard/util/getDashboardFilterKey';
-import { getFilterColorMap } from 'src/dashboard/util/dashboardFiltersColorMap';
import {
FILTER_CONFIG_ATTRIBUTES,
FILTER_OPTIONS_LIMIT,
TIME_FILTER_LABELS,
} from 'src/explore/constants';
-import FilterBadgeIcon from 'src/components/FilterBadgeIcon';
import './FilterBox.less';
@@ -125,13 +122,17 @@ class FilterBox extends React.Component {
return this.props.onFilterMenuOpen(this.props.chartId, column);
}
+ onFilterMenuClose(column) {
+ return this.props.onFilterMenuClose(this.props.chartId, column);
+ }
+
onOpenDateFilterControl() {
return this.onFilterMenuOpen(TIME_RANGE);
}
- onFilterMenuClose() {
- return this.props.onFilterMenuClose(this.props.chartId);
- }
+ onCloseDateFilterControl = () => {
+ return this.onFilterMenuClose(TIME_RANGE);
+ };
getControlData(controlName) {
const { selectedValues } = this.state;
@@ -263,7 +264,7 @@ class FilterBox extends React.Component {
}
renderDateFilter() {
- const { showDateFilter, chartId } = this.props;
+ const { showDateFilter } = this.props;
const label = TIME_FILTER_LABELS.time_range;
if (showDateFilter) {
return (
@@ -272,7 +273,6 @@ class FilterBox extends React.Component {
className="col-lg-12 col-xs-12 filter-container"
data-test="date-filter-container"
>
- {this.renderFilterBadge(chartId, TIME_RANGE, label)}
@@ -393,10 +393,11 @@ class FilterBox extends React.Component {
this.changeFilter(key, newValue);
}
}}
- onFocus={() => this.onFilterMenuOpen(key)}
+ // TODO try putting this back once react-select is upgraded
+ // onFocus={() => this.onFilterMenuOpen(key)}
onMenuOpen={() => this.onFilterMenuOpen(key)}
- onBlur={this.onFilterMenuClose}
- onMenuClose={this.onFilterMenuClose}
+ onBlur={() => this.onFilterMenuClose(key)}
+ onMenuClose={() => this.onFilterMenuClose(key)}
selectWrap={
filterConfig[FILTER_CONFIG_ATTRIBUTES.SEARCH_ALL_OPTIONS] &&
data.length >= FILTER_OPTIONS_LIMIT
@@ -409,33 +410,18 @@ class FilterBox extends React.Component {
}
renderFilters() {
- const { filtersFields = [], chartId } = this.props;
+ const { filtersFields = [] } = this.props;
return filtersFields.map(filterConfig => {
const { label, key } = filterConfig;
return (
- {this.renderFilterBadge(chartId, key, label)}
-
- {label}
- {this.renderSelect(filterConfig)}
-
+
{label}
+ {this.renderSelect(filterConfig)}
);
});
}
- renderFilterBadge(chartId, column) {
- const colorKey = getDashboardFilterKey({ chartId, column });
- const filterColorMap = getFilterColorMap();
- const colorCode = filterColorMap[colorKey];
-
- return (
-
-
-
- );
- }
-
render() {
const { instantFiltering } = this.props;
diff --git a/superset-frontend/src/visualizations/FilterBox/FilterBox.less b/superset-frontend/src/visualizations/FilterBox/FilterBox.less
index 7e83175001..f200bc6fc1 100644
--- a/superset-frontend/src/visualizations/FilterBox/FilterBox.less
+++ b/superset-frontend/src/visualizations/FilterBox/FilterBox.less
@@ -55,6 +55,7 @@
.filter-container {
display: flex;
+ flex-direction: column;
label {
display: flex;
diff --git a/superset-frontend/stylesheets/less/variables.less b/superset-frontend/stylesheets/less/variables.less
index e7ef74964f..f47d0f9f14 100644
--- a/superset-frontend/stylesheets/less/variables.less
+++ b/superset-frontend/stylesheets/less/variables.less
@@ -87,30 +87,6 @@
/* general component effects */
@shadow-highlight: @primary-color;
-/*************************** filter indicators **************************/
-/* make sure be consistent with FILTER_COLORS_COUNT in
- dashboardFiltersColorMap.js
-*/
-@badge-colors: #228be6, #40c057, #fab005, #f76707, #e64980, #15aabf, #7950f2,
- #fa5252, #74b816, #12b886, #1864ab, #2b8a3e, #e67700, #d9480f, #a61e4d,
- #0b7285, #5f3dc4, #c92a2a, #5c940d, #087f5b;
-
-@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;
- }
-
- .badge-loop(@i - 1);
-}
-
-.badge-loop(@iterations);
-
/************************************************************************/
/* OPACITIES */
/* Used in LESS filters, e.g. fade(@someColorVar, @someOpacityBelow) */
diff --git a/superset-frontend/stylesheets/superset.less b/superset-frontend/stylesheets/superset.less
index 155380fbed..4cd60805a9 100644
--- a/superset-frontend/stylesheets/superset.less
+++ b/superset-frontend/stylesheets/superset.less
@@ -201,10 +201,6 @@ table.table-no-hover tr:hover {
background-color: initial;
}
-.editable-title {
- margin-right: 8px;
-}
-
.editable-title input {
outline: none;
background: transparent;
diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py
index 51a6dd46ee..60d468ab8f 100644
--- a/superset/charts/schemas.py
+++ b/superset/charts/schemas.py
@@ -1016,6 +1016,12 @@ class ChartDataResponseResult(Schema):
description="Amount of rows in result set", allow_none=False,
)
data = fields.List(fields.Dict(), description="A list with results")
+ applied_filters = fields.List(
+ fields.Dict(), description="A list with applied filters"
+ )
+ rejected_filters = fields.List(
+ fields.Dict(), description="A list with rejected filters"
+ )
class ChartDataResponseSchema(Schema):
diff --git a/superset/common/query_context.py b/superset/common/query_context.py
index c17ab45ed8..61f038f53e 100644
--- a/superset/common/query_context.py
+++ b/superset/common/query_context.py
@@ -18,7 +18,7 @@ import copy
import logging
import math
from datetime import datetime, timedelta
-from typing import Any, ClassVar, Dict, List, Optional, Union
+from typing import Any, cast, ClassVar, Dict, List, Optional, Union
import numpy as np
import pandas as pd
@@ -162,6 +162,22 @@ class QueryContext:
if status != utils.QueryStatus.FAILED:
payload["data"] = self.get_data(df)
del payload["df"]
+
+ filters = query_obj.filter
+ filter_columns = cast(List[str], [flt.get("col") for flt in filters])
+ columns = set(self.datasource.column_names)
+ applied_time_columns, rejected_time_columns = utils.get_time_filter_status(
+ self.datasource, query_obj.applied_time_extras
+ )
+ payload["applied_filters"] = [
+ {"column": col} for col in filter_columns if col in columns
+ ] + applied_time_columns
+ payload["rejected_filters"] = [
+ {"reason": "not_in_datasource", "column": col}
+ for col in filter_columns
+ if col not in columns
+ ] + rejected_time_columns
+
if self.result_type == utils.ChartDataResultType.RESULTS:
return {"data": payload["data"]}
return payload
diff --git a/superset/common/query_object.py b/superset/common/query_object.py
index be138d03a7..aa2d3147dc 100644
--- a/superset/common/query_object.py
+++ b/superset/common/query_object.py
@@ -50,6 +50,7 @@ DEPRECATED_EXTRAS_FIELDS = (
DeprecatedField(old_name="where", new_name="where"),
DeprecatedField(old_name="having", new_name="having"),
DeprecatedField(old_name="having_filters", new_name="having_druid"),
+ DeprecatedField(old_name="druid_time_origin", new_name="druid_time_origin"),
)
@@ -60,6 +61,7 @@ class QueryObject:
"""
annotation_layers: List[Dict[str, Any]]
+ applied_time_extras: Dict[str, str]
granularity: Optional[str]
from_dttm: Optional[datetime]
to_dttm: Optional[datetime]
@@ -81,6 +83,7 @@ class QueryObject:
def __init__(
self,
annotation_layers: Optional[List[Dict[str, Any]]] = None,
+ applied_time_extras: Optional[Dict[str, str]] = None,
granularity: Optional[str] = None,
metrics: Optional[List[Union[Dict[str, Any], str]]] = None,
groupby: Optional[List[str]] = None,
@@ -104,6 +107,7 @@ class QueryObject:
extras = extras or {}
is_sip_38 = is_feature_enabled("SIP_38_VIZ_REARCHITECTURE")
self.annotation_layers = annotation_layers
+ self.applied_time_extras = applied_time_extras or {}
self.granularity = granularity
self.from_dttm, self.to_dttm = utils.get_since_until(
relative_start=extras.get(
diff --git a/superset/utils/core.py b/superset/utils/core.py
index a084b53a81..2429c2c129 100644
--- a/superset/utils/core.py
+++ b/superset/utils/core.py
@@ -910,6 +910,8 @@ def merge_extra_filters( # pylint: disable=too-many-branches
# that are external to the slice definition. We use those for dynamic
# interactive filters like the ones emitted by the "Filter Box" visualization.
# Note extra_filters only support simple filters.
+ applied_time_extras: Dict[str, str] = {}
+ form_data["applied_time_extras"] = applied_time_extras
if "extra_filters" in form_data:
# __form and __to are special extra_filters that target time
# boundaries. The rest of extra_filters are simple
@@ -948,9 +950,13 @@ def merge_extra_filters( # pylint: disable=too-many-branches
]:
filtr["isExtra"] = True
# Pull out time filters/options and merge into form data
- if date_options.get(filtr["col"]):
- if filtr.get("val"):
- form_data[date_options[filtr["col"]]] = filtr["val"]
+ filter_column = filtr["col"]
+ time_extra = date_options.get(filter_column)
+ if time_extra:
+ time_extra_value = filtr.get("val")
+ if time_extra_value:
+ form_data[time_extra] = time_extra_value
+ applied_time_extras[filter_column] = time_extra_value
elif filtr["val"]:
# Merge column filters
filter_key = get_filter_key(filtr)
@@ -1567,5 +1573,80 @@ class RowLevelSecurityFilterType(str, Enum):
BASE = "Base"
+class ExtraFiltersTimeColumnType(str, Enum):
+ GRANULARITY = "__granularity"
+ TIME_COL = "__time_col"
+ TIME_GRAIN = "__time_grain"
+ TIME_ORIGIN = "__time_origin"
+ TIME_RANGE = "__time_range"
+
+
def is_test() -> bool:
return strtobool(os.environ.get("SUPERSET_TESTENV", "false"))
+
+
+def get_time_filter_status( # pylint: disable=too-many-branches
+ datasource: "BaseDatasource", applied_time_extras: Dict[str, str],
+) -> Tuple[List[Dict[str, str]], List[Dict[str, str]]]:
+ temporal_columns = {col.column_name for col in datasource.columns if col.is_dttm}
+ applied: List[Dict[str, str]] = []
+ rejected: List[Dict[str, str]] = []
+ time_column = applied_time_extras.get(ExtraFiltersTimeColumnType.TIME_COL)
+ if time_column:
+ if time_column in temporal_columns:
+ applied.append({"column": ExtraFiltersTimeColumnType.TIME_COL})
+ else:
+ rejected.append(
+ {
+ "reason": "not_in_datasource",
+ "column": ExtraFiltersTimeColumnType.TIME_COL,
+ }
+ )
+
+ if ExtraFiltersTimeColumnType.TIME_GRAIN in applied_time_extras:
+ # are there any temporal columns to assign the time grain to?
+ if temporal_columns:
+ applied.append({"column": ExtraFiltersTimeColumnType.TIME_GRAIN})
+ else:
+ rejected.append(
+ {
+ "reason": "no_temporal_column",
+ "column": ExtraFiltersTimeColumnType.TIME_GRAIN,
+ }
+ )
+
+ if ExtraFiltersTimeColumnType.TIME_RANGE in applied_time_extras:
+ # are there any temporal columns to assign the time grain to?
+ if temporal_columns:
+ applied.append({"column": ExtraFiltersTimeColumnType.TIME_RANGE})
+ else:
+ rejected.append(
+ {
+ "reason": "no_temporal_column",
+ "column": ExtraFiltersTimeColumnType.TIME_RANGE,
+ }
+ )
+
+ if ExtraFiltersTimeColumnType.TIME_ORIGIN in applied_time_extras:
+ if datasource.type == "druid":
+ applied.append({"column": ExtraFiltersTimeColumnType.TIME_ORIGIN})
+ else:
+ rejected.append(
+ {
+ "reason": "not_druid_datasource",
+ "column": ExtraFiltersTimeColumnType.TIME_ORIGIN,
+ }
+ )
+
+ if ExtraFiltersTimeColumnType.GRANULARITY in applied_time_extras:
+ if datasource.type == "druid":
+ applied.append({"column": ExtraFiltersTimeColumnType.GRANULARITY})
+ else:
+ rejected.append(
+ {
+ "reason": "not_druid_datasource",
+ "column": ExtraFiltersTimeColumnType.GRANULARITY,
+ }
+ )
+
+ return applied, rejected
diff --git a/superset/views/core.py b/superset/views/core.py
index 84c9cb16e4..1951d98385 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -505,6 +505,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
payloads based on the request args in the first block
TODO: break into one endpoint for each return shape"""
+
response_type = utils.ChartDataResultFormat.JSON.value
responses: List[
Union[utils.ChartDataResultFormat, utils.ChartDataResultType]
diff --git a/superset/viz.py b/superset/viz.py
index 1e67596bf2..921daf4e04 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -173,6 +173,9 @@ class BaseViz:
self.process_metrics()
+ self.applied_filters: List[Dict[str, str]] = []
+ self.rejected_filters: List[Dict[str, str]] = []
+
def process_metrics(self) -> None:
# metrics in TableViz is order sensitive, so metric_dict should be
# OrderedDict
@@ -468,14 +471,33 @@ class BaseViz:
def get_payload(self, query_obj: Optional[QueryObjectDict] = None) -> VizPayload:
"""Returns a payload of metadata and data"""
+
self.run_extra_queries()
payload = self.get_df_payload(query_obj)
df = payload.get("df")
+
if self.status != utils.QueryStatus.FAILED:
payload["data"] = self.get_data(df)
if "df" in payload:
del payload["df"]
+
+ filters = self.form_data.get("filters", [])
+ filter_columns = [flt.get("col") for flt in filters]
+ columns = set(self.datasource.column_names)
+ applied_time_extras = self.form_data.get("applied_time_extras", {})
+ applied_time_columns, rejected_time_columns = utils.get_time_filter_status(
+ self.datasource, applied_time_extras
+ )
+ payload["applied_filters"] = [
+ {"column": col} for col in filter_columns if col in columns
+ ] + applied_time_columns
+ payload["rejected_filters"] = [
+ {"reason": "not_in_datasource", "column": col}
+ for col in filter_columns
+ if col not in columns
+ ] + rejected_time_columns
+
return payload
def get_df_payload(
diff --git a/tests/charts/api_tests.py b/tests/charts/api_tests.py
index 4d3a67e262..429d26c414 100644
--- a/tests/charts/api_tests.py
+++ b/tests/charts/api_tests.py
@@ -820,6 +820,30 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin):
data = json.loads(rv.data.decode("utf-8"))
self.assertEqual(data["result"][0]["rowcount"], 45)
+ def test_chart_data_applied_time_extras(self):
+ """
+ Chart data API: Test chart data query with applied time extras
+ """
+ self.login(username="admin")
+ table = self.get_table_by_name("birth_names")
+ request_payload = get_query_context(table.name, table.id, table.type)
+ request_payload["queries"][0]["applied_time_extras"] = {
+ "__time_range": "100 years ago : now",
+ "__time_origin": "now",
+ }
+ rv = self.post_assert_metric(CHART_DATA_URI, request_payload, "data")
+ self.assertEqual(rv.status_code, 200)
+ data = json.loads(rv.data.decode("utf-8"))
+ self.assertEqual(
+ data["result"][0]["applied_filters"],
+ [{"column": "gender"}, {"column": "__time_range"},],
+ )
+ self.assertEqual(
+ data["result"][0]["rejected_filters"],
+ [{"column": "__time_origin", "reason": "not_druid_datasource"},],
+ )
+ self.assertEqual(data["result"][0]["rowcount"], 45)
+
def test_chart_data_limit_offset(self):
"""
Chart data API: Test chart data query with limit and offset
diff --git a/tests/utils_tests.py b/tests/utils_tests.py
index 035a7864fe..a6ca62ba84 100644
--- a/tests/utils_tests.py
+++ b/tests/utils_tests.py
@@ -174,12 +174,18 @@ class TestUtils(SupersetTestCase):
def test_merge_extra_filters(self):
# does nothing if no extra filters
form_data = {"A": 1, "B": 2, "c": "test"}
- expected = {"A": 1, "B": 2, "c": "test"}
+ expected = {**form_data, "applied_time_extras": {}}
merge_extra_filters(form_data)
self.assertEqual(form_data, expected)
# empty extra_filters
form_data = {"A": 1, "B": 2, "c": "test", "extra_filters": []}
- expected = {"A": 1, "B": 2, "c": "test", "adhoc_filters": []}
+ expected = {
+ "A": 1,
+ "B": 2,
+ "c": "test",
+ "adhoc_filters": [],
+ "applied_time_extras": {},
+ }
merge_extra_filters(form_data)
self.assertEqual(form_data, expected)
# copy over extra filters into empty filters
@@ -205,7 +211,8 @@ class TestUtils(SupersetTestCase):
"operator": "==",
"subject": "B",
},
- ]
+ ],
+ "applied_time_extras": {},
}
merge_extra_filters(form_data)
self.assertEqual(form_data, expected)
@@ -248,7 +255,8 @@ class TestUtils(SupersetTestCase):
"operator": "==",
"subject": "B",
},
- ]
+ ],
+ "applied_time_extras": {},
}
merge_extra_filters(form_data)
self.assertEqual(form_data, expected)
@@ -278,6 +286,13 @@ class TestUtils(SupersetTestCase):
"time_grain_sqla": "years",
"granularity": "90 seconds",
"druid_time_origin": "now",
+ "applied_time_extras": {
+ "__time_range": "1 year ago :",
+ "__time_col": "birth_year",
+ "__time_grain": "years",
+ "__time_origin": "now",
+ "__granularity": "90 seconds",
+ },
}
merge_extra_filters(form_data)
self.assertEqual(form_data, expected)
@@ -290,7 +305,7 @@ class TestUtils(SupersetTestCase):
{"col": "B", "op": "==", "val": []},
]
}
- expected = {"adhoc_filters": []}
+ expected = {"adhoc_filters": [], "applied_time_extras": {}}
merge_extra_filters(form_data)
self.assertEqual(form_data, expected)
@@ -317,7 +332,8 @@ class TestUtils(SupersetTestCase):
"operator": "in",
"subject": None,
}
- ]
+ ],
+ "applied_time_extras": {},
}
merge_extra_filters(form_data)
self.assertEqual(form_data, expected)
@@ -377,7 +393,8 @@ class TestUtils(SupersetTestCase):
"operator": "in",
"subject": "c",
},
- ]
+ ],
+ "applied_time_extras": {},
}
merge_extra_filters(form_data)
self.assertEqual(form_data, expected)
@@ -429,7 +446,8 @@ class TestUtils(SupersetTestCase):
"operator": "in",
"subject": "a",
},
- ]
+ ],
+ "applied_time_extras": {},
}
merge_extra_filters(form_data)
self.assertEqual(form_data, expected)
@@ -478,7 +496,8 @@ class TestUtils(SupersetTestCase):
"operator": "in",
"subject": "a",
},
- ]
+ ],
+ "applied_time_extras": {},
}
merge_extra_filters(form_data)
self.assertEqual(form_data, expected)
@@ -537,7 +556,8 @@ class TestUtils(SupersetTestCase):
"operator": "==",
"subject": "B",
},
- ]
+ ],
+ "applied_time_extras": {},
}
merge_extra_filters(form_data)
self.assertEqual(form_data, expected)