mirror of
https://github.com/apache/superset.git
synced 2024-09-12 08:39:45 -04:00
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', () => {
|
describe('DashboardBuilder', () => {
|
||||||
let favStarStub;
|
let favStarStub;
|
||||||
let focusedTabStub;
|
let activeTabsStub;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
// this is invoked on mount, so we stub it instead of making a request
|
// this is invoked on mount, so we stub it instead of making a request
|
||||||
favStarStub = sinon
|
favStarStub = sinon
|
||||||
.stub(dashboardStateActions, 'fetchFaveStar')
|
.stub(dashboardStateActions, 'fetchFaveStar')
|
||||||
.returns({ type: 'mock-action' });
|
.returns({ type: 'mock-action' });
|
||||||
focusedTabStub = sinon
|
activeTabsStub = sinon
|
||||||
.stub(dashboardStateActions, 'setLastFocusedTab')
|
.stub(dashboardStateActions, 'setActiveTabs')
|
||||||
.returns({ type: 'mock-action' });
|
.returns({ type: 'mock-action' });
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
favStarStub.restore();
|
favStarStub.restore();
|
||||||
focusedTabStub.restore();
|
activeTabsStub.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
function setup(overrideState = {}, overrideStore) {
|
function setup(overrideState = {}, overrideStore) {
|
||||||
|
@ -344,9 +344,9 @@ export function setDirectPathToChild(path) {
|
|||||||
return { type: SET_DIRECT_PATH, path };
|
return { type: SET_DIRECT_PATH, path };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SET_LAST_FOCUSED_TAB = 'SET_LAST_FOCUSED_TAB';
|
export const SET_ACTIVE_TABS = 'SET_ACTIVE_TABS';
|
||||||
export function setLastFocusedTab(tabId) {
|
export function setActiveTabs(tabIds) {
|
||||||
return { type: SET_LAST_FOCUSED_TAB, tabId };
|
return { type: SET_ACTIVE_TABS, tabIds };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SET_FOCUSED_FILTER_FIELD = 'SET_FOCUSED_FILTER_FIELD';
|
export const SET_FOCUSED_FILTER_FIELD = 'SET_FOCUSED_FILTER_FIELD';
|
||||||
|
@ -377,7 +377,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => (
|
|||||||
hasUnsavedChanges: false,
|
hasUnsavedChanges: false,
|
||||||
maxUndoHistoryExceeded: false,
|
maxUndoHistoryExceeded: false,
|
||||||
lastModifiedTime: dashboardData.changed_on,
|
lastModifiedTime: dashboardData.changed_on,
|
||||||
lastFocusedTabId: null,
|
activeTabs: [],
|
||||||
},
|
},
|
||||||
dashboardLayout,
|
dashboardLayout,
|
||||||
},
|
},
|
||||||
|
@ -31,11 +31,7 @@ import findTabIndexByComponentId from '../../util/findTabIndexByComponentId';
|
|||||||
import getDirectPathToTabIndex from '../../util/getDirectPathToTabIndex';
|
import getDirectPathToTabIndex from '../../util/getDirectPathToTabIndex';
|
||||||
import getLeafComponentIdFromPath from '../../util/getLeafComponentIdFromPath';
|
import getLeafComponentIdFromPath from '../../util/getLeafComponentIdFromPath';
|
||||||
import { componentShape } from '../../util/propShapes';
|
import { componentShape } from '../../util/propShapes';
|
||||||
import {
|
import { NEW_TAB_ID, DASHBOARD_ROOT_ID } from '../../util/constants';
|
||||||
NEW_TAB_ID,
|
|
||||||
DASHBOARD_ROOT_ID,
|
|
||||||
DASHBOARD_GRID_ID,
|
|
||||||
} from '../../util/constants';
|
|
||||||
import { RENDER_TAB, RENDER_TAB_CONTENT } from './Tab';
|
import { RENDER_TAB, RENDER_TAB_CONTENT } from './Tab';
|
||||||
import { TAB_TYPE } from '../../util/componentTypes';
|
import { TAB_TYPE } from '../../util/componentTypes';
|
||||||
|
|
||||||
@ -50,9 +46,11 @@ const propTypes = {
|
|||||||
editMode: PropTypes.bool.isRequired,
|
editMode: PropTypes.bool.isRequired,
|
||||||
renderHoverMenu: PropTypes.bool,
|
renderHoverMenu: PropTypes.bool,
|
||||||
directPathToChild: PropTypes.arrayOf(PropTypes.string),
|
directPathToChild: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
activeTabs: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
|
||||||
// actions (from DashboardComponent.jsx)
|
// actions (from DashboardComponent.jsx)
|
||||||
logEvent: PropTypes.func.isRequired,
|
logEvent: PropTypes.func.isRequired,
|
||||||
|
setActiveTabs: PropTypes.func,
|
||||||
|
|
||||||
// grid related
|
// grid related
|
||||||
availableColumnCount: PropTypes.number,
|
availableColumnCount: PropTypes.number,
|
||||||
@ -75,6 +73,8 @@ const defaultProps = {
|
|||||||
availableColumnCount: 0,
|
availableColumnCount: 0,
|
||||||
columnWidth: 0,
|
columnWidth: 0,
|
||||||
directPathToChild: [],
|
directPathToChild: [],
|
||||||
|
activeTabs: [],
|
||||||
|
setActiveTabs() {},
|
||||||
onResizeStart() {},
|
onResizeStart() {},
|
||||||
onResize() {},
|
onResize() {},
|
||||||
onResizeStop() {},
|
onResizeStop() {},
|
||||||
@ -130,6 +130,19 @@ class Tabs extends React.PureComponent {
|
|||||||
this.handleDropOnTab = this.handleDropOnTab.bind(this);
|
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) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
const maxIndex = Math.max(0, nextProps.component.children.length - 1);
|
const maxIndex = Math.max(0, nextProps.component.children.length - 1);
|
||||||
const currTabsIds = this.props.component.children;
|
const currTabsIds = this.props.component.children;
|
||||||
@ -277,22 +290,11 @@ class Tabs extends React.PureComponent {
|
|||||||
isComponentVisible: isCurrentTabVisible,
|
isComponentVisible: isCurrentTabVisible,
|
||||||
editMode,
|
editMode,
|
||||||
nativeFilters,
|
nativeFilters,
|
||||||
dashboardLayout,
|
|
||||||
lastFocusedTabId,
|
|
||||||
setLastFocusedTab,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { children: tabIds } = tabsComponent;
|
const { children: tabIds } = tabsComponent;
|
||||||
const { tabIndex: selectedTabIndex, activeKey } = this.state;
|
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;
|
let tabsToHighlight;
|
||||||
if (nativeFilters.focusedFilterId) {
|
if (nativeFilters.focusedFilterId) {
|
||||||
tabsToHighlight =
|
tabsToHighlight =
|
||||||
@ -332,7 +334,6 @@ class Tabs extends React.PureComponent {
|
|||||||
onEdit={this.handleEdit}
|
onEdit={this.handleEdit}
|
||||||
data-test="nav-list"
|
data-test="nav-list"
|
||||||
type={editMode ? 'editable-card' : 'card'}
|
type={editMode ? 'editable-card' : 'card'}
|
||||||
onTabClick={setLastFocusedTab}
|
|
||||||
>
|
>
|
||||||
{tabIds.map((tabId, tabIndex) => (
|
{tabIds.map((tabId, tabIndex) => (
|
||||||
<LineEditableTabs.TabPane
|
<LineEditableTabs.TabPane
|
||||||
|
@ -19,18 +19,14 @@
|
|||||||
import React, { FC, useMemo, useState } from 'react';
|
import React, { FC, useMemo, useState } from 'react';
|
||||||
import { DataMask, styled, t } from '@superset-ui/core';
|
import { DataMask, styled, t } from '@superset-ui/core';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import * as portals from 'react-reverse-portal';
|
import * as portals from 'react-reverse-portal';
|
||||||
import { DataMaskStateWithId } from 'src/dataMask/types';
|
import { DataMaskStateWithId } from 'src/dataMask/types';
|
||||||
import { Collapse } from 'src/common/components';
|
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 CascadePopover from '../CascadeFilters/CascadePopover';
|
||||||
import { buildCascadeFiltersTree } from './utils';
|
import { buildCascadeFiltersTree } from './utils';
|
||||||
import { useFilters } from '../state';
|
import { useFilters } from '../state';
|
||||||
import { Filter } from '../../types';
|
import { Filter } from '../../types';
|
||||||
import { CascadeFilter } from '../CascadeFilters/types';
|
import { useDashboardHasTabs, useSelectFiltersInScope } from '../../state';
|
||||||
import { useDashboardLayout } from '../../state';
|
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
padding: ${({ theme }) => theme.gridUnit * 4}px;
|
padding: ${({ theme }) => theme.gridUnit * 4}px;
|
||||||
@ -52,10 +48,6 @@ const FilterControls: FC<FilterControlsProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [visiblePopoverId, setVisiblePopoverId] = useState<string | null>(null);
|
const [visiblePopoverId, setVisiblePopoverId] = useState<string | null>(null);
|
||||||
const filters = useFilters();
|
const filters = useFilters();
|
||||||
const dashboardLayout = useDashboardLayout();
|
|
||||||
const lastFocusedTabId = useSelector<RootState, string | null>(
|
|
||||||
state => state.dashboardState?.lastFocusedTabId,
|
|
||||||
);
|
|
||||||
const filterValues = Object.values<Filter>(filters);
|
const filterValues = Object.values<Filter>(filters);
|
||||||
const portalNodes = React.useMemo(() => {
|
const portalNodes = React.useMemo(() => {
|
||||||
const nodes = new Array(filterValues.length);
|
const nodes = new Array(filterValues.length);
|
||||||
@ -74,23 +66,11 @@ const FilterControls: FC<FilterControlsProps> = ({
|
|||||||
}, [filterValues, dataMaskSelected]);
|
}, [filterValues, dataMaskSelected]);
|
||||||
const cascadeFilterIds = new Set(cascadeFilters.map(item => item.id));
|
const cascadeFilterIds = new Set(cascadeFilters.map(item => item.id));
|
||||||
|
|
||||||
let filtersInScope: CascadeFilter[] = [];
|
const [filtersInScope, filtersOutOfScope] = useSelectFiltersInScope(
|
||||||
const filtersOutOfScope: CascadeFilter[] = [];
|
cascadeFilters,
|
||||||
const dashboardHasTabs = Object.values(dashboardLayout).some(
|
|
||||||
element => element.type === TAB_TYPE,
|
|
||||||
);
|
);
|
||||||
|
const dashboardHasTabs = useDashboardHasTabs();
|
||||||
const showCollapsePanel = dashboardHasTabs && cascadeFilters.length > 0;
|
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 (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
|
@ -19,7 +19,9 @@
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Filter, FilterConfiguration } from './types';
|
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[] = [];
|
const defaultFilterConfiguration: Filter[] = [];
|
||||||
|
|
||||||
@ -52,3 +54,73 @@ export function useDashboardLayout() {
|
|||||||
state => state.dashboardLayout?.present,
|
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,
|
updateComponents,
|
||||||
handleComponentDrop,
|
handleComponentDrop,
|
||||||
} from '../actions/dashboardLayout';
|
} from '../actions/dashboardLayout';
|
||||||
import {
|
import { setDirectPathToChild, setActiveTabs } from '../actions/dashboardState';
|
||||||
setDirectPathToChild,
|
|
||||||
setLastFocusedTab,
|
|
||||||
} from '../actions/dashboardState';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
@ -104,7 +101,7 @@ function mapStateToProps(
|
|||||||
redoLength: undoableLayout.future.length,
|
redoLength: undoableLayout.future.length,
|
||||||
filters: getActiveFilters(),
|
filters: getActiveFilters(),
|
||||||
directPathToChild: dashboardState.directPathToChild,
|
directPathToChild: dashboardState.directPathToChild,
|
||||||
lastFocusedTabId: dashboardState.lastFocusedTabId,
|
activeTabs: dashboardState.activeTabs,
|
||||||
directPathLastUpdated: dashboardState.directPathLastUpdated,
|
directPathLastUpdated: dashboardState.directPathLastUpdated,
|
||||||
dashboardId: dashboardInfo.id,
|
dashboardId: dashboardInfo.id,
|
||||||
nativeFilters,
|
nativeFilters,
|
||||||
@ -141,7 +138,7 @@ function mapDispatchToProps(dispatch) {
|
|||||||
updateComponents,
|
updateComponents,
|
||||||
handleComponentDrop,
|
handleComponentDrop,
|
||||||
setDirectPathToChild,
|
setDirectPathToChild,
|
||||||
setLastFocusedTab,
|
setActiveTabs,
|
||||||
logEvent,
|
logEvent,
|
||||||
},
|
},
|
||||||
dispatch,
|
dispatch,
|
||||||
|
@ -35,7 +35,7 @@ import {
|
|||||||
SET_DIRECT_PATH,
|
SET_DIRECT_PATH,
|
||||||
SET_FOCUSED_FILTER_FIELD,
|
SET_FOCUSED_FILTER_FIELD,
|
||||||
UNSET_FOCUSED_FILTER_FIELD,
|
UNSET_FOCUSED_FILTER_FIELD,
|
||||||
SET_LAST_FOCUSED_TAB,
|
SET_ACTIVE_TABS,
|
||||||
} from '../actions/dashboardState';
|
} from '../actions/dashboardState';
|
||||||
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
|
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
|
||||||
|
|
||||||
@ -134,10 +134,10 @@ export default function dashboardStateReducer(state = {}, action) {
|
|||||||
directPathLastUpdated: Date.now(),
|
directPathLastUpdated: Date.now(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[SET_LAST_FOCUSED_TAB]() {
|
[SET_ACTIVE_TABS]() {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
lastFocusedTabId: action.tabId,
|
activeTabs: action.tabIds,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[SET_FOCUSED_FILTER_FIELD]() {
|
[SET_FOCUSED_FILTER_FIELD]() {
|
||||||
|
@ -41,12 +41,13 @@ export type Chart = ChartState & {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ActiveTabs = string[];
|
||||||
export type DashboardLayout = { [key: string]: LayoutItem };
|
export type DashboardLayout = { [key: string]: LayoutItem };
|
||||||
export type DashboardLayoutState = { present: DashboardLayout };
|
export type DashboardLayoutState = { present: DashboardLayout };
|
||||||
export type DashboardState = {
|
export type DashboardState = {
|
||||||
editMode: boolean;
|
editMode: boolean;
|
||||||
directPathToChild: string[];
|
directPathToChild: string[];
|
||||||
lastFocusedTabId: string | null;
|
activeTabs: ActiveTabs;
|
||||||
};
|
};
|
||||||
export type DashboardInfo = {
|
export type DashboardInfo = {
|
||||||
common: {
|
common: {
|
||||||
|
Loading…
Reference in New Issue
Block a user