mirror of https://github.com/apache/superset.git
feat(native-filters): Hide filters which don't affect any visible charts (#15063)
* feat(native-filters): Hide filters which don't affect any visible charts * Fix lint errors * Add comments * Code review fix * Fix tests * Refactor logic in FilterControls
This commit is contained in:
parent
5e825cf063
commit
51f0d4fd98
|
@ -50,21 +50,21 @@ jest.mock('src/dashboard/actions/dashboardState');
|
|||
|
||||
describe('DashboardBuilder', () => {
|
||||
let favStarStub;
|
||||
let focusedTabStub;
|
||||
let activeTabsStub;
|
||||
|
||||
beforeAll(() => {
|
||||
// this is invoked on mount, so we stub it instead of making a request
|
||||
favStarStub = sinon
|
||||
.stub(dashboardStateActions, 'fetchFaveStar')
|
||||
.returns({ type: 'mock-action' });
|
||||
focusedTabStub = sinon
|
||||
.stub(dashboardStateActions, 'setLastFocusedTab')
|
||||
activeTabsStub = sinon
|
||||
.stub(dashboardStateActions, 'setActiveTabs')
|
||||
.returns({ type: 'mock-action' });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
favStarStub.restore();
|
||||
focusedTabStub.restore();
|
||||
activeTabsStub.restore();
|
||||
});
|
||||
|
||||
function setup(overrideState = {}, overrideStore) {
|
||||
|
|
|
@ -344,9 +344,9 @@ export function setDirectPathToChild(path) {
|
|||
return { type: SET_DIRECT_PATH, path };
|
||||
}
|
||||
|
||||
export const SET_LAST_FOCUSED_TAB = 'SET_LAST_FOCUSED_TAB';
|
||||
export function setLastFocusedTab(tabId) {
|
||||
return { type: SET_LAST_FOCUSED_TAB, tabId };
|
||||
export const SET_ACTIVE_TABS = 'SET_ACTIVE_TABS';
|
||||
export function setActiveTabs(tabIds) {
|
||||
return { type: SET_ACTIVE_TABS, tabIds };
|
||||
}
|
||||
|
||||
export const SET_FOCUSED_FILTER_FIELD = 'SET_FOCUSED_FILTER_FIELD';
|
||||
|
|
|
@ -377,7 +377,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => (
|
|||
hasUnsavedChanges: false,
|
||||
maxUndoHistoryExceeded: false,
|
||||
lastModifiedTime: dashboardData.changed_on,
|
||||
lastFocusedTabId: null,
|
||||
activeTabs: [],
|
||||
},
|
||||
dashboardLayout,
|
||||
},
|
||||
|
|
|
@ -31,11 +31,7 @@ import findTabIndexByComponentId from '../../util/findTabIndexByComponentId';
|
|||
import getDirectPathToTabIndex from '../../util/getDirectPathToTabIndex';
|
||||
import getLeafComponentIdFromPath from '../../util/getLeafComponentIdFromPath';
|
||||
import { componentShape } from '../../util/propShapes';
|
||||
import {
|
||||
NEW_TAB_ID,
|
||||
DASHBOARD_ROOT_ID,
|
||||
DASHBOARD_GRID_ID,
|
||||
} from '../../util/constants';
|
||||
import { NEW_TAB_ID, DASHBOARD_ROOT_ID } from '../../util/constants';
|
||||
import { RENDER_TAB, RENDER_TAB_CONTENT } from './Tab';
|
||||
import { TAB_TYPE } from '../../util/componentTypes';
|
||||
|
||||
|
@ -50,9 +46,11 @@ const propTypes = {
|
|||
editMode: PropTypes.bool.isRequired,
|
||||
renderHoverMenu: PropTypes.bool,
|
||||
directPathToChild: PropTypes.arrayOf(PropTypes.string),
|
||||
activeTabs: PropTypes.arrayOf(PropTypes.string),
|
||||
|
||||
// actions (from DashboardComponent.jsx)
|
||||
logEvent: PropTypes.func.isRequired,
|
||||
setActiveTabs: PropTypes.func,
|
||||
|
||||
// grid related
|
||||
availableColumnCount: PropTypes.number,
|
||||
|
@ -75,6 +73,8 @@ const defaultProps = {
|
|||
availableColumnCount: 0,
|
||||
columnWidth: 0,
|
||||
directPathToChild: [],
|
||||
activeTabs: [],
|
||||
setActiveTabs() {},
|
||||
onResizeStart() {},
|
||||
onResize() {},
|
||||
onResizeStop() {},
|
||||
|
@ -130,6 +130,19 @@ class Tabs extends React.PureComponent {
|
|||
this.handleDropOnTab = this.handleDropOnTab.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.setActiveTabs([...this.props.activeTabs, this.state.activeKey]);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.activeKey !== this.state.activeKey) {
|
||||
this.props.setActiveTabs([
|
||||
...this.props.activeTabs.filter(tabId => tabId !== prevState.activeKey),
|
||||
this.state.activeKey,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
const maxIndex = Math.max(0, nextProps.component.children.length - 1);
|
||||
const currTabsIds = this.props.component.children;
|
||||
|
@ -277,22 +290,11 @@ class Tabs extends React.PureComponent {
|
|||
isComponentVisible: isCurrentTabVisible,
|
||||
editMode,
|
||||
nativeFilters,
|
||||
dashboardLayout,
|
||||
lastFocusedTabId,
|
||||
setLastFocusedTab,
|
||||
} = this.props;
|
||||
|
||||
const { children: tabIds } = tabsComponent;
|
||||
const { tabIndex: selectedTabIndex, activeKey } = this.state;
|
||||
|
||||
// On dashboards with top level tabs, set initial focus to the active top level tab
|
||||
const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
|
||||
const rootChildId = dashboardRoot.children[0];
|
||||
const isTopLevelTabs = rootChildId !== DASHBOARD_GRID_ID;
|
||||
if (isTopLevelTabs && !lastFocusedTabId) {
|
||||
setLastFocusedTab(activeKey);
|
||||
}
|
||||
|
||||
let tabsToHighlight;
|
||||
if (nativeFilters.focusedFilterId) {
|
||||
tabsToHighlight =
|
||||
|
@ -332,7 +334,6 @@ class Tabs extends React.PureComponent {
|
|||
onEdit={this.handleEdit}
|
||||
data-test="nav-list"
|
||||
type={editMode ? 'editable-card' : 'card'}
|
||||
onTabClick={setLastFocusedTab}
|
||||
>
|
||||
{tabIds.map((tabId, tabIndex) => (
|
||||
<LineEditableTabs.TabPane
|
||||
|
|
|
@ -19,18 +19,14 @@
|
|||
import React, { FC, useMemo, useState } from 'react';
|
||||
import { DataMask, styled, t } from '@superset-ui/core';
|
||||
import { css } from '@emotion/react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import * as portals from 'react-reverse-portal';
|
||||
import { DataMaskStateWithId } from 'src/dataMask/types';
|
||||
import { Collapse } from 'src/common/components';
|
||||
import { TAB_TYPE } from 'src/dashboard/util/componentTypes';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
import CascadePopover from '../CascadeFilters/CascadePopover';
|
||||
import { buildCascadeFiltersTree } from './utils';
|
||||
import { useFilters } from '../state';
|
||||
import { Filter } from '../../types';
|
||||
import { CascadeFilter } from '../CascadeFilters/types';
|
||||
import { useDashboardLayout } from '../../state';
|
||||
import { useDashboardHasTabs, useSelectFiltersInScope } from '../../state';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
padding: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
|
@ -52,10 +48,6 @@ const FilterControls: FC<FilterControlsProps> = ({
|
|||
}) => {
|
||||
const [visiblePopoverId, setVisiblePopoverId] = useState<string | null>(null);
|
||||
const filters = useFilters();
|
||||
const dashboardLayout = useDashboardLayout();
|
||||
const lastFocusedTabId = useSelector<RootState, string | null>(
|
||||
state => state.dashboardState?.lastFocusedTabId,
|
||||
);
|
||||
const filterValues = Object.values<Filter>(filters);
|
||||
const portalNodes = React.useMemo(() => {
|
||||
const nodes = new Array(filterValues.length);
|
||||
|
@ -74,23 +66,11 @@ const FilterControls: FC<FilterControlsProps> = ({
|
|||
}, [filterValues, dataMaskSelected]);
|
||||
const cascadeFilterIds = new Set(cascadeFilters.map(item => item.id));
|
||||
|
||||
let filtersInScope: CascadeFilter[] = [];
|
||||
const filtersOutOfScope: CascadeFilter[] = [];
|
||||
const dashboardHasTabs = Object.values(dashboardLayout).some(
|
||||
element => element.type === TAB_TYPE,
|
||||
const [filtersInScope, filtersOutOfScope] = useSelectFiltersInScope(
|
||||
cascadeFilters,
|
||||
);
|
||||
const dashboardHasTabs = useDashboardHasTabs();
|
||||
const showCollapsePanel = dashboardHasTabs && cascadeFilters.length > 0;
|
||||
if (!lastFocusedTabId || !dashboardHasTabs) {
|
||||
filtersInScope = cascadeFilters;
|
||||
} else {
|
||||
cascadeFilters.forEach((filter, index) => {
|
||||
if (cascadeFilters[index].tabsInScope?.includes(lastFocusedTabId)) {
|
||||
filtersInScope.push(filter);
|
||||
} else {
|
||||
filtersOutOfScope.push(filter);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
|
|
|
@ -19,7 +19,9 @@
|
|||
import { useSelector } from 'react-redux';
|
||||
import { useMemo } from 'react';
|
||||
import { Filter, FilterConfiguration } from './types';
|
||||
import { DashboardLayout } from '../../types';
|
||||
import { ActiveTabs, DashboardLayout, RootState } from '../../types';
|
||||
import { TAB_TYPE } from '../../util/componentTypes';
|
||||
import { CascadeFilter } from './FilterBar/CascadeFilters/types';
|
||||
|
||||
const defaultFilterConfiguration: Filter[] = [];
|
||||
|
||||
|
@ -52,3 +54,73 @@ export function useDashboardLayout() {
|
|||
state => state.dashboardLayout?.present,
|
||||
);
|
||||
}
|
||||
|
||||
export function useDashboardHasTabs() {
|
||||
const dashboardLayout = useDashboardLayout();
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.values(dashboardLayout).some(element => element.type === TAB_TYPE),
|
||||
[dashboardLayout],
|
||||
);
|
||||
}
|
||||
|
||||
function useActiveDashboardTabs() {
|
||||
return useSelector<RootState, ActiveTabs>(
|
||||
state => state.dashboardState?.activeTabs,
|
||||
);
|
||||
}
|
||||
|
||||
function useSelectChartTabParents() {
|
||||
const dashboardLayout = useDashboardLayout();
|
||||
return (chartId: number) => {
|
||||
const chartLayoutItem = Object.values(dashboardLayout).find(
|
||||
layoutItem => layoutItem.meta?.chartId === chartId,
|
||||
);
|
||||
return chartLayoutItem?.parents.filter(
|
||||
(parent: string) => dashboardLayout[parent].type === TAB_TYPE,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function useIsFilterInScope() {
|
||||
const activeTabs = useActiveDashboardTabs();
|
||||
const selectChartTabParents = useSelectChartTabParents();
|
||||
|
||||
// Filter is in scope if any of it's charts is visible.
|
||||
// Chart is visible if it's placed in an active tab tree or if it's not attached to any tab.
|
||||
// Chart is in an active tab tree if all of it's ancestors of type TAB are active
|
||||
return (filter: CascadeFilter) =>
|
||||
filter.chartsInScope?.some((chartId: number) => {
|
||||
const tabParents = selectChartTabParents(chartId);
|
||||
return (
|
||||
tabParents?.length === 0 ||
|
||||
tabParents?.every(tab => activeTabs.includes(tab))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useSelectFiltersInScope(cascadeFilters: CascadeFilter[]) {
|
||||
const dashboardHasTabs = useDashboardHasTabs();
|
||||
const isFilterInScope = useIsFilterInScope();
|
||||
|
||||
return useMemo(() => {
|
||||
let filtersInScope: CascadeFilter[] = [];
|
||||
const filtersOutOfScope: CascadeFilter[] = [];
|
||||
|
||||
// we check native filters scopes only on dashboards with tabs
|
||||
if (!dashboardHasTabs) {
|
||||
filtersInScope = cascadeFilters;
|
||||
} else {
|
||||
cascadeFilters.forEach((filter: CascadeFilter) => {
|
||||
const filterInScope = isFilterInScope(filter);
|
||||
|
||||
if (filterInScope) {
|
||||
filtersInScope.push(filter);
|
||||
} else {
|
||||
filtersOutOfScope.push(filter);
|
||||
}
|
||||
});
|
||||
}
|
||||
return [filtersInScope, filtersOutOfScope];
|
||||
}, [cascadeFilters, dashboardHasTabs, isFilterInScope]);
|
||||
}
|
||||
|
|
|
@ -35,10 +35,7 @@ import {
|
|||
updateComponents,
|
||||
handleComponentDrop,
|
||||
} from '../actions/dashboardLayout';
|
||||
import {
|
||||
setDirectPathToChild,
|
||||
setLastFocusedTab,
|
||||
} from '../actions/dashboardState';
|
||||
import { setDirectPathToChild, setActiveTabs } from '../actions/dashboardState';
|
||||
|
||||
const propTypes = {
|
||||
id: PropTypes.string,
|
||||
|
@ -104,7 +101,7 @@ function mapStateToProps(
|
|||
redoLength: undoableLayout.future.length,
|
||||
filters: getActiveFilters(),
|
||||
directPathToChild: dashboardState.directPathToChild,
|
||||
lastFocusedTabId: dashboardState.lastFocusedTabId,
|
||||
activeTabs: dashboardState.activeTabs,
|
||||
directPathLastUpdated: dashboardState.directPathLastUpdated,
|
||||
dashboardId: dashboardInfo.id,
|
||||
nativeFilters,
|
||||
|
@ -141,7 +138,7 @@ function mapDispatchToProps(dispatch) {
|
|||
updateComponents,
|
||||
handleComponentDrop,
|
||||
setDirectPathToChild,
|
||||
setLastFocusedTab,
|
||||
setActiveTabs,
|
||||
logEvent,
|
||||
},
|
||||
dispatch,
|
||||
|
|
|
@ -35,7 +35,7 @@ import {
|
|||
SET_DIRECT_PATH,
|
||||
SET_FOCUSED_FILTER_FIELD,
|
||||
UNSET_FOCUSED_FILTER_FIELD,
|
||||
SET_LAST_FOCUSED_TAB,
|
||||
SET_ACTIVE_TABS,
|
||||
} from '../actions/dashboardState';
|
||||
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
|
||||
|
||||
|
@ -134,10 +134,10 @@ export default function dashboardStateReducer(state = {}, action) {
|
|||
directPathLastUpdated: Date.now(),
|
||||
};
|
||||
},
|
||||
[SET_LAST_FOCUSED_TAB]() {
|
||||
[SET_ACTIVE_TABS]() {
|
||||
return {
|
||||
...state,
|
||||
lastFocusedTabId: action.tabId,
|
||||
activeTabs: action.tabIds,
|
||||
};
|
||||
},
|
||||
[SET_FOCUSED_FILTER_FIELD]() {
|
||||
|
|
|
@ -41,12 +41,13 @@ export type Chart = ChartState & {
|
|||
};
|
||||
};
|
||||
|
||||
export type ActiveTabs = string[];
|
||||
export type DashboardLayout = { [key: string]: LayoutItem };
|
||||
export type DashboardLayoutState = { present: DashboardLayout };
|
||||
export type DashboardState = {
|
||||
editMode: boolean;
|
||||
directPathToChild: string[];
|
||||
lastFocusedTabId: string | null;
|
||||
activeTabs: ActiveTabs;
|
||||
};
|
||||
export type DashboardInfo = {
|
||||
common: {
|
||||
|
|
Loading…
Reference in New Issue