diff --git a/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx index 20f63a6fca..4533fc5610 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx +++ b/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx @@ -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) { diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js b/superset-frontend/src/dashboard/actions/dashboardState.js index d0b39e2ff1..400e22b424 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.js @@ -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'; diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index 62751033ad..0cba0cddf9 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -377,7 +377,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( hasUnsavedChanges: false, maxUndoHistoryExceeded: false, lastModifiedTime: dashboardData.changed_on, - lastFocusedTabId: null, + activeTabs: [], }, dashboardLayout, }, diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx index f103653492..aaa7ba406d 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx @@ -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) => ( theme.gridUnit * 4}px; @@ -52,10 +48,6 @@ const FilterControls: FC = ({ }) => { const [visiblePopoverId, setVisiblePopoverId] = useState(null); const filters = useFilters(); - const dashboardLayout = useDashboardLayout(); - const lastFocusedTabId = useSelector( - state => state.dashboardState?.lastFocusedTabId, - ); const filterValues = Object.values(filters); const portalNodes = React.useMemo(() => { const nodes = new Array(filterValues.length); @@ -74,23 +66,11 @@ const FilterControls: FC = ({ }, [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 ( diff --git a/superset-frontend/src/dashboard/components/nativeFilters/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/state.ts index 027bf2ac96..280a203da7 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/state.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/state.ts @@ -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( + 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]); +} diff --git a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx index 387e67c926..296b3bd141 100644 --- a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx +++ b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx @@ -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, diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.js b/superset-frontend/src/dashboard/reducers/dashboardState.js index 842916e1cc..28405ebc91 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardState.js +++ b/superset-frontend/src/dashboard/reducers/dashboardState.js @@ -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]() { diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts index bd6eeedd27..8659ba3541 100644 --- a/superset-frontend/src/dashboard/types.ts +++ b/superset-frontend/src/dashboard/types.ts @@ -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: {