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:
Kamil Gabryjelski 2021-06-11 13:41:08 +02:00 committed by GitHub
parent 5e825cf063
commit 51f0d4fd98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 111 additions and 60 deletions

View File

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

View File

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

View File

@ -377,7 +377,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => (
hasUnsavedChanges: false,
maxUndoHistoryExceeded: false,
lastModifiedTime: dashboardData.changed_on,
lastFocusedTabId: null,
activeTabs: [],
},
dashboardLayout,
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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