mirror of
https://github.com/apache/superset.git
synced 2026-04-07 10:31:50 +00:00
feat(dashboard/native-filters): Hide filters out of scope of current tab (#14933)
* Optimize finding charts and tabs in scope * Put filters out of scope in Collapse * Use lastFocusedTabId instead of directPathToChild * Fix tests * Fix cypress test * Uncomment e2e test
This commit is contained in:
committed by
GitHub
parent
723a67156c
commit
405f95b6af
@@ -61,7 +61,6 @@ describe('dashboard filters card view', () => {
|
||||
cy.get('.Select__menu').contains('Published').click({ timeout: 5000 });
|
||||
cy.get('[data-test="styled-card"]').should('have.length', 2);
|
||||
cy.get('[data-test="styled-card"]')
|
||||
.first()
|
||||
.contains('USA Births Names')
|
||||
.should('be.visible');
|
||||
cy.get('.Select__control').eq(1).click();
|
||||
@@ -107,13 +106,12 @@ describe('dashboard filters list view', () => {
|
||||
cy.get('[data-test="table-row"]').should('not.exist');
|
||||
});
|
||||
|
||||
xit('should filter by published correctly', () => {
|
||||
it('should filter by published correctly', () => {
|
||||
// filter by published
|
||||
cy.get('.Select__control').eq(2).click();
|
||||
cy.get('.Select__menu').contains('Published').click();
|
||||
cy.get('[data-test="table-row"]').should('have.length', 2);
|
||||
cy.get('[data-test="table-row"]')
|
||||
.first()
|
||||
.contains('USA Births Names')
|
||||
.should('be.visible');
|
||||
cy.get('.Select__control').eq(2).click();
|
||||
|
||||
16
superset-frontend/package-lock.json
generated
16
superset-frontend/package-lock.json
generated
@@ -102,6 +102,7 @@
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-resize-detector": "^6.0.1-rc.1",
|
||||
"react-reverse-portal": "^2.0.1",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-search-input": "^0.11.3",
|
||||
"react-select": "^3.1.0",
|
||||
@@ -44737,6 +44738,15 @@
|
||||
"resize-observer-polyfill": "^1.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-reverse-portal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-reverse-portal/-/react-reverse-portal-2.0.1.tgz",
|
||||
"integrity": "sha512-sj/D9nSHspqV8i8hWkTSZ5Ohnrqk2A5fkDKw4Xe/zV4OfF1UYwmbzrxLdmNRdKkWgQwnXIxaa2E3FC7QYdZAeA==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
|
||||
@@ -92328,6 +92338,12 @@
|
||||
"resize-observer-polyfill": "^1.5.1"
|
||||
}
|
||||
},
|
||||
"react-reverse-portal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-reverse-portal/-/react-reverse-portal-2.0.1.tgz",
|
||||
"integrity": "sha512-sj/D9nSHspqV8i8hWkTSZ5Ohnrqk2A5fkDKw4Xe/zV4OfF1UYwmbzrxLdmNRdKkWgQwnXIxaa2E3FC7QYdZAeA==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-router": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-resize-detector": "^6.0.1-rc.1",
|
||||
"react-reverse-portal": "^2.0.1",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-search-input": "^0.11.3",
|
||||
"react-select": "^3.1.0",
|
||||
|
||||
@@ -50,16 +50,21 @@ jest.mock('src/dashboard/actions/dashboardState');
|
||||
|
||||
describe('DashboardBuilder', () => {
|
||||
let favStarStub;
|
||||
let focusedTabStub;
|
||||
|
||||
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')
|
||||
.returns({ type: 'mock-action' });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
favStarStub.restore();
|
||||
focusedTabStub.restore();
|
||||
});
|
||||
|
||||
function setup(overrideState = {}, overrideStore) {
|
||||
|
||||
@@ -36,6 +36,7 @@ import { sliceId } from 'spec/fixtures/mockChartQueries';
|
||||
import dashboardInfo from 'spec/fixtures/mockDashboardInfo';
|
||||
import { dashboardLayout as mockLayout } from 'spec/fixtures/mockDashboardLayout';
|
||||
import { sliceEntitiesForChart } from 'spec/fixtures/mockSliceEntities';
|
||||
import { nativeFiltersInfo } from '../../fixtures/mockNativeFilters';
|
||||
|
||||
describe('ChartHolder', () => {
|
||||
const props = {
|
||||
@@ -55,6 +56,7 @@ describe('ChartHolder', () => {
|
||||
handleComponentDrop() {},
|
||||
updateComponents() {},
|
||||
deleteComponent() {},
|
||||
nativeFilters: nativeFiltersInfo.filters,
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
|
||||
@@ -33,8 +33,10 @@ import HoverMenu from 'src/dashboard/components/menu/HoverMenu';
|
||||
import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
|
||||
import Tabs from 'src/dashboard/components/gridComponents/Tabs';
|
||||
import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
|
||||
import emptyDashboardLayout from 'src/dashboard/fixtures/emptyDashboardLayout';
|
||||
import { dashboardLayoutWithTabs } from 'spec/fixtures/mockDashboardLayout';
|
||||
import { mockStoreWithTabs } from 'spec/fixtures/mockStore';
|
||||
import { nativeFilters } from 'spec/fixtures/mockNativeFilters';
|
||||
|
||||
describe('Tabs', () => {
|
||||
fetchMock.post('glob:*/r/shortner/', {});
|
||||
@@ -59,6 +61,8 @@ describe('Tabs', () => {
|
||||
deleteComponent() {},
|
||||
updateComponents() {},
|
||||
logEvent() {},
|
||||
dashboardLayout: emptyDashboardLayout,
|
||||
nativeFilters: nativeFilters.filters,
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
|
||||
@@ -344,6 +344,11 @@ 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_FOCUSED_FILTER_FIELD = 'SET_FOCUSED_FILTER_FIELD';
|
||||
export function setFocusedFilterField(chartId, column) {
|
||||
return { type: SET_FOCUSED_FILTER_FIELD, chartId, column };
|
||||
|
||||
@@ -377,6 +377,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => (
|
||||
hasUnsavedChanges: false,
|
||||
maxUndoHistoryExceeded: false,
|
||||
lastModifiedTime: dashboardData.changed_on,
|
||||
lastFocusedTabId: null,
|
||||
},
|
||||
dashboardLayout,
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
import { ParentSize } from '@vx/responsive';
|
||||
import Tabs from 'src/components/Tabs';
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import DashboardGrid from 'src/dashboard/containers/DashboardGrid';
|
||||
import getLeafComponentIdFromPath from 'src/dashboard/util/getLeafComponentIdFromPath';
|
||||
import { DashboardLayout, LayoutItem, RootState } from 'src/dashboard/types';
|
||||
@@ -30,6 +30,10 @@ import {
|
||||
DASHBOARD_ROOT_DEPTH,
|
||||
} from 'src/dashboard/util/constants';
|
||||
import { getRootLevelTabIndex } from './utils';
|
||||
import { Filters } from '../../reducers/types';
|
||||
import { getChartIdsInFilterScope } from '../../util/activeDashboardFilters';
|
||||
import { findTabsWithChartsInScope } from '../nativeFilters/utils';
|
||||
import { setFilterConfiguration } from '../../actions/nativeFilters';
|
||||
|
||||
type DashboardContainerProps = {
|
||||
topLevelTabs?: LayoutItem;
|
||||
@@ -39,6 +43,9 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
|
||||
const dashboardLayout = useSelector<RootState, DashboardLayout>(
|
||||
state => state.dashboardLayout.present,
|
||||
);
|
||||
const nativeFilters = useSelector<RootState, Filters>(
|
||||
state => state.nativeFilters.filters,
|
||||
);
|
||||
const directPathToChild = useSelector<RootState, string[]>(
|
||||
state => state.dashboardState.directPathToChild,
|
||||
);
|
||||
@@ -46,10 +53,37 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
|
||||
getRootLevelTabIndex(dashboardLayout, directPathToChild),
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
setTabIndex(getRootLevelTabIndex(dashboardLayout, directPathToChild));
|
||||
}, [getLeafComponentIdFromPath(directPathToChild)]);
|
||||
|
||||
// recalculate charts and tabs in scopes of native filters only when a scope or dashboard layout changes
|
||||
const nativeFiltersValues = Object.values(nativeFilters);
|
||||
const scopes = nativeFiltersValues.map(filter => filter.scope);
|
||||
useEffect(() => {
|
||||
nativeFiltersValues.forEach(filter => {
|
||||
const filterScope = filter.scope;
|
||||
const chartsInScope = getChartIdsInFilterScope({
|
||||
filterScope: {
|
||||
scope: filterScope.rootPath,
|
||||
// @ts-ignore
|
||||
immune: filterScope.excluded,
|
||||
},
|
||||
});
|
||||
const tabsInScope = findTabsWithChartsInScope(
|
||||
dashboardLayout,
|
||||
chartsInScope,
|
||||
);
|
||||
Object.assign(filter, {
|
||||
chartsInScope,
|
||||
tabsInScope: Array.from(tabsInScope),
|
||||
});
|
||||
});
|
||||
dispatch(setFilterConfiguration(nativeFiltersValues));
|
||||
}, [JSON.stringify(scopes), JSON.stringify(dashboardLayout)]);
|
||||
|
||||
const childIds: string[] = topLevelTabs
|
||||
? topLevelTabs.children
|
||||
: [DASHBOARD_GRID_ID];
|
||||
|
||||
@@ -85,37 +85,46 @@ const defaultProps = {
|
||||
* If ChartHolder were a function component, this could be implemented as a hook instead.
|
||||
*/
|
||||
const FilterFocusHighlight = React.forwardRef(
|
||||
({ chartId, focusedFilterScope, ...otherProps }, ref) => {
|
||||
({ chartId, focusedFilterScope, nativeFilters, ...otherProps }, ref) => {
|
||||
const theme = useTheme();
|
||||
if (!focusedFilterScope) return <div ref={ref} {...otherProps} />;
|
||||
const focusedNativeFilterId = nativeFilters.focusedFilterId;
|
||||
if (!(focusedFilterScope || focusedNativeFilterId))
|
||||
return <div ref={ref} {...otherProps} />;
|
||||
|
||||
// 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' };
|
||||
const unfocusedChartStyles = { opacity: 0.3, pointerEvents: 'none' };
|
||||
const focusedChartStyles = {
|
||||
borderColor: theme.colors.primary.light2,
|
||||
opacity: 1,
|
||||
boxShadow: `0px 0px ${theme.gridUnit * 2}px ${
|
||||
theme.colors.primary.light2
|
||||
}`,
|
||||
pointerEvents: 'auto',
|
||||
};
|
||||
|
||||
if (
|
||||
if (focusedNativeFilterId) {
|
||||
if (
|
||||
nativeFilters.filters[focusedNativeFilterId].chartsInScope.includes(
|
||||
chartId,
|
||||
)
|
||||
) {
|
||||
return <div ref={ref} style={focusedChartStyles} {...otherProps} />;
|
||||
}
|
||||
} else 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.gridUnit * 2}px ${
|
||||
theme.colors.primary.light2
|
||||
}`,
|
||||
pointerEvents: 'auto',
|
||||
};
|
||||
return <div ref={ref} style={focusedChartStyles} {...otherProps} />;
|
||||
}
|
||||
|
||||
// inline styles are used here due to a performance issue when adding/changing a class, which causes a reflow
|
||||
return <div ref={ref} style={styles} {...otherProps} />;
|
||||
return <div ref={ref} style={unfocusedChartStyles} {...otherProps} />;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -233,6 +242,7 @@ class ChartHolder extends React.Component {
|
||||
isComponentVisible,
|
||||
dashboardId,
|
||||
focusedFilterScope,
|
||||
nativeFilters,
|
||||
} = this.props;
|
||||
|
||||
// inherit the size of parent columns
|
||||
@@ -291,6 +301,7 @@ class ChartHolder extends React.Component {
|
||||
<FilterFocusHighlight
|
||||
chartId={chartId}
|
||||
focusedFilterScope={focusedFilterScope}
|
||||
nativeFilters={nativeFilters}
|
||||
ref={dragSourceRef}
|
||||
data-test="dashboard-component-chart-holder"
|
||||
className={cx(
|
||||
|
||||
@@ -18,12 +18,13 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import mockState from 'spec/fixtures/mockState';
|
||||
import { sliceId as chartId } from 'spec/fixtures/mockChartQueries';
|
||||
import { nativeFiltersInfo } from 'spec/javascripts/dashboard/fixtures/mockNativeFilters';
|
||||
import newComponentFactory from 'src/dashboard/util/newComponentFactory';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { ChartHolder } from './index';
|
||||
import { CHART_TYPE, ROW_TYPE } from '../../util/componentTypes';
|
||||
|
||||
@@ -60,6 +61,7 @@ describe('ChartHolder', () => {
|
||||
editMode: false,
|
||||
isComponentVisible: true,
|
||||
dashboardId: 123,
|
||||
nativeFilters: nativeFiltersInfo.filters,
|
||||
};
|
||||
|
||||
const renderWrapper = (props = defaultProps, state = mockState) =>
|
||||
|
||||
@@ -31,40 +31,13 @@ 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 } from '../../util/constants';
|
||||
import {
|
||||
NEW_TAB_ID,
|
||||
DASHBOARD_ROOT_ID,
|
||||
DASHBOARD_GRID_ID,
|
||||
} from '../../util/constants';
|
||||
import { RENDER_TAB, RENDER_TAB_CONTENT } from './Tab';
|
||||
import { CHART_TYPE, TAB_TYPE } from '../../util/componentTypes';
|
||||
import { getChartIdsInFilterScope } from '../../util/activeDashboardFilters';
|
||||
|
||||
const findTabsWithChartsInScope = (
|
||||
dashboardLayout,
|
||||
chartsInScope,
|
||||
childId,
|
||||
tabId,
|
||||
tabsToHighlight,
|
||||
) => {
|
||||
if (
|
||||
dashboardLayout[childId].type === CHART_TYPE &&
|
||||
chartsInScope.includes(dashboardLayout[childId].meta.chartId)
|
||||
) {
|
||||
tabsToHighlight.add(tabId);
|
||||
}
|
||||
if (
|
||||
dashboardLayout[childId].children.length === 0 ||
|
||||
(dashboardLayout[childId].type === TAB_TYPE && tabsToHighlight.has(childId))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
dashboardLayout[childId].children.forEach(subChildId =>
|
||||
findTabsWithChartsInScope(
|
||||
dashboardLayout,
|
||||
chartsInScope,
|
||||
subChildId,
|
||||
tabId,
|
||||
tabsToHighlight,
|
||||
),
|
||||
);
|
||||
};
|
||||
import { TAB_TYPE } from '../../util/componentTypes';
|
||||
|
||||
const propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
@@ -299,29 +272,27 @@ class Tabs extends React.PureComponent {
|
||||
renderHoverMenu,
|
||||
isComponentVisible: isCurrentTabVisible,
|
||||
editMode,
|
||||
focusedFilterScope,
|
||||
nativeFilters,
|
||||
dashboardLayout,
|
||||
lastFocusedTabId,
|
||||
setLastFocusedTab,
|
||||
} = this.props;
|
||||
|
||||
const { children: tabIds } = tabsComponent;
|
||||
const { tabIndex: selectedTabIndex, activeKey } = this.state;
|
||||
|
||||
const tabsToHighlight = new Set();
|
||||
if (focusedFilterScope) {
|
||||
const chartsInScope = getChartIdsInFilterScope({
|
||||
filterScope: focusedFilterScope.scope,
|
||||
});
|
||||
tabIds.forEach(tabId => {
|
||||
if (!tabsToHighlight.has(tabId)) {
|
||||
findTabsWithChartsInScope(
|
||||
dashboardLayout,
|
||||
chartsInScope,
|
||||
tabId,
|
||||
tabId,
|
||||
tabsToHighlight,
|
||||
);
|
||||
}
|
||||
});
|
||||
// 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 =
|
||||
nativeFilters.filters[nativeFilters.focusedFilterId].tabsInScope;
|
||||
}
|
||||
return (
|
||||
<DragDroppable
|
||||
@@ -357,6 +328,7 @@ 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
|
||||
@@ -373,7 +345,7 @@ class Tabs extends React.PureComponent {
|
||||
onDropOnTab={this.handleDropOnTab}
|
||||
isFocused={activeKey === tabId}
|
||||
isHighlighted={
|
||||
activeKey !== tabId && tabsToHighlight.has(tabId)
|
||||
activeKey !== tabId && tabsToHighlight?.includes(tabId)
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -20,11 +20,12 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import { nativeFiltersInfo } from 'spec/javascripts/dashboard/fixtures/mockNativeFilters';
|
||||
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
|
||||
import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
|
||||
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
|
||||
import getLeafComponentIdFromPath from 'src/dashboard/util/getLeafComponentIdFromPath';
|
||||
|
||||
import emptyDashboardLayout from 'src/dashboard/fixtures/emptyDashboardLayout';
|
||||
import Tabs from './Tabs';
|
||||
|
||||
jest.mock('src/dashboard/containers/DashboardComponent', () =>
|
||||
@@ -110,6 +111,8 @@ const createProps = () => ({
|
||||
onChangeTab: jest.fn(),
|
||||
deleteComponent: jest.fn(),
|
||||
updateComponents: jest.fn(),
|
||||
dashboardLayout: emptyDashboardLayout,
|
||||
nativeFilters: nativeFiltersInfo.filters,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -17,12 +17,20 @@
|
||||
* under the License.
|
||||
*/
|
||||
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 { DataMaskState } from 'src/dataMask/types';
|
||||
import { DataMask, styled } from '@superset-ui/core';
|
||||
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';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
padding: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
@@ -44,7 +52,18 @@ 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);
|
||||
for (let i = 0; i < filterValues.length; i += 1) {
|
||||
nodes[i] = portals.createHtmlPortalNode();
|
||||
}
|
||||
return nodes;
|
||||
}, [filterValues.length]);
|
||||
|
||||
const cascadeFilters = useMemo(() => {
|
||||
const filtersWithValue = filterValues.map(filter => ({
|
||||
@@ -54,21 +73,86 @@ const FilterControls: FC<FilterControlsProps> = ({
|
||||
return buildCascadeFiltersTree(filtersWithValue);
|
||||
}, [filterValues, dataMaskSelected]);
|
||||
|
||||
let filtersInScope: CascadeFilter[] = [];
|
||||
const filtersOutOfScope: CascadeFilter[] = [];
|
||||
const dashboardHasTabs = Object.values(dashboardLayout).some(
|
||||
element => element.type === TAB_TYPE,
|
||||
);
|
||||
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>
|
||||
{cascadeFilters.map(filter => (
|
||||
<CascadePopover
|
||||
data-test="cascade-filters-control"
|
||||
key={filter.id}
|
||||
visible={visiblePopoverId === filter.id}
|
||||
onVisibleChange={visible =>
|
||||
setVisiblePopoverId(visible ? filter.id : null)
|
||||
}
|
||||
filter={filter}
|
||||
onFilterSelectionChange={onFilterSelectionChange}
|
||||
directPathToChild={directPathToChild}
|
||||
/>
|
||||
{portalNodes.map((node, index) => (
|
||||
<portals.InPortal node={node}>
|
||||
<CascadePopover
|
||||
data-test="cascade-filters-control"
|
||||
key={cascadeFilters[index].id}
|
||||
visible={visiblePopoverId === cascadeFilters[index].id}
|
||||
onVisibleChange={visible =>
|
||||
setVisiblePopoverId(visible ? cascadeFilters[index].id : null)
|
||||
}
|
||||
filter={cascadeFilters[index]}
|
||||
onFilterSelectionChange={onFilterSelectionChange}
|
||||
directPathToChild={directPathToChild}
|
||||
/>
|
||||
</portals.InPortal>
|
||||
))}
|
||||
{filtersInScope.map(filter => {
|
||||
const index = cascadeFilters.findIndex(f => f.id === filter.id);
|
||||
return <portals.OutPortal node={portalNodes[index]} />;
|
||||
})}
|
||||
{showCollapsePanel && (
|
||||
<Collapse
|
||||
ghost
|
||||
bordered
|
||||
expandIconPosition="right"
|
||||
collapsible={filtersOutOfScope.length === 0 ? 'disabled' : undefined}
|
||||
css={theme => css`
|
||||
&.ant-collapse {
|
||||
margin-top: ${filtersInScope.length > 0
|
||||
? theme.gridUnit * 6
|
||||
: 0}px;
|
||||
& > .ant-collapse-item {
|
||||
& > .ant-collapse-header {
|
||||
padding-left: 0;
|
||||
padding-bottom: ${theme.gridUnit * 2}px;
|
||||
|
||||
& > .ant-collapse-arrow {
|
||||
right: ${theme.gridUnit}px;
|
||||
}
|
||||
}
|
||||
|
||||
& .ant-collapse-content-box {
|
||||
padding: ${theme.gridUnit * 4}px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Collapse.Panel
|
||||
header={`${t('Filters out of scope')} (${
|
||||
filtersOutOfScope.length
|
||||
})`}
|
||||
key="1"
|
||||
>
|
||||
{filtersOutOfScope.map(filter => {
|
||||
const index = cascadeFilters.findIndex(f => f.id === filter.id);
|
||||
return <portals.OutPortal node={portalNodes[index]} />;
|
||||
})}
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useMemo } from 'react';
|
||||
import { Filter, FilterConfiguration } from './types';
|
||||
import { DashboardLayout } from '../../types';
|
||||
|
||||
const defaultFilterConfiguration: Filter[] = [];
|
||||
|
||||
@@ -45,3 +46,9 @@ export function useFilterConfigMap() {
|
||||
[filterConfig],
|
||||
);
|
||||
}
|
||||
|
||||
export function useDashboardLayout() {
|
||||
return useSelector<any, DashboardLayout>(
|
||||
state => state.dashboardLayout?.present,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ export interface Filter {
|
||||
sortMetric?: string | null;
|
||||
adhoc_filters?: AdhocFilter[];
|
||||
time_range?: string;
|
||||
tabsInScope?: string[];
|
||||
chartsInScope?: number[];
|
||||
}
|
||||
|
||||
export type FilterConfiguration = Filter[];
|
||||
|
||||
@@ -26,12 +26,14 @@ import {
|
||||
AdhocFilter,
|
||||
FeatureFlag,
|
||||
} from '@superset-ui/core';
|
||||
import { Charts } from 'src/dashboard/types';
|
||||
import { Charts, DashboardLayout } from 'src/dashboard/types';
|
||||
import { RefObject } from 'react';
|
||||
import { DataMaskStateWithId } from 'src/dataMask/types';
|
||||
import extractUrlParams from 'src/dashboard/util/extractUrlParams';
|
||||
import { isFeatureEnabled } from 'src/featureFlags';
|
||||
import { Filter } from './types';
|
||||
import { CHART_TYPE, TAB_TYPE } from '../../util/componentTypes';
|
||||
import { DASHBOARD_GRID_ID, DASHBOARD_ROOT_ID } from '../../util/constants';
|
||||
|
||||
export const getFormData = ({
|
||||
datasetId,
|
||||
@@ -142,3 +144,73 @@ export function nativeFilterGate(behaviors: Behavior[]): boolean {
|
||||
behaviors.includes(Behavior.INTERACTIVE_CHART))
|
||||
);
|
||||
}
|
||||
|
||||
const isComponentATab = (
|
||||
dashboardLayout: DashboardLayout,
|
||||
componentId: string,
|
||||
) => dashboardLayout[componentId].type === TAB_TYPE;
|
||||
|
||||
const findTabsWithChartsInScopeHelper = (
|
||||
dashboardLayout: DashboardLayout,
|
||||
chartsInScope: number[],
|
||||
componentId: string,
|
||||
tabIds: string[],
|
||||
tabsToHighlight: Set<string>,
|
||||
) => {
|
||||
if (
|
||||
dashboardLayout[componentId].type === CHART_TYPE &&
|
||||
chartsInScope.includes(dashboardLayout[componentId].meta.chartId)
|
||||
) {
|
||||
tabIds.forEach(tabsToHighlight.add, tabsToHighlight);
|
||||
}
|
||||
if (
|
||||
dashboardLayout[componentId].children.length === 0 ||
|
||||
(isComponentATab(dashboardLayout, componentId) &&
|
||||
tabsToHighlight.has(componentId))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
dashboardLayout[componentId].children.forEach(childId =>
|
||||
findTabsWithChartsInScopeHelper(
|
||||
dashboardLayout,
|
||||
chartsInScope,
|
||||
childId,
|
||||
isComponentATab(dashboardLayout, childId) ? [...tabIds, childId] : tabIds,
|
||||
tabsToHighlight,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export const findTabsWithChartsInScope = (
|
||||
dashboardLayout: DashboardLayout,
|
||||
chartsInScope: number[],
|
||||
) => {
|
||||
const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
|
||||
const rootChildId = dashboardRoot.children[0];
|
||||
const hasTopLevelTabs = rootChildId !== DASHBOARD_GRID_ID;
|
||||
const tabsInScope = new Set<string>();
|
||||
if (hasTopLevelTabs) {
|
||||
dashboardLayout[rootChildId].children?.forEach(tabId =>
|
||||
findTabsWithChartsInScopeHelper(
|
||||
dashboardLayout,
|
||||
chartsInScope,
|
||||
tabId,
|
||||
[tabId],
|
||||
tabsInScope,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Object.values(dashboardLayout)
|
||||
.filter(element => element.type === TAB_TYPE)
|
||||
.forEach(element =>
|
||||
findTabsWithChartsInScopeHelper(
|
||||
dashboardLayout,
|
||||
chartsInScope,
|
||||
element.id,
|
||||
[element.id],
|
||||
tabsInScope,
|
||||
),
|
||||
);
|
||||
}
|
||||
return tabsInScope;
|
||||
};
|
||||
|
||||
@@ -35,7 +35,10 @@ import {
|
||||
updateComponents,
|
||||
handleComponentDrop,
|
||||
} from '../actions/dashboardLayout';
|
||||
import { setDirectPathToChild } from '../actions/dashboardState';
|
||||
import {
|
||||
setDirectPathToChild,
|
||||
setLastFocusedTab,
|
||||
} from '../actions/dashboardState';
|
||||
|
||||
const propTypes = {
|
||||
id: PropTypes.string,
|
||||
@@ -79,19 +82,6 @@ function selectFocusedFilterScope(dashboardState, dashboardFilters) {
|
||||
};
|
||||
}
|
||||
|
||||
function selectFocusedNativeFilterScope(nativeFilters) {
|
||||
if (!nativeFilters.focusedFilterId) return null;
|
||||
const id = nativeFilters.focusedFilterId;
|
||||
const focusedFilterScope = nativeFilters.filters[id].scope;
|
||||
return {
|
||||
chartId: id,
|
||||
scope: {
|
||||
scope: focusedFilterScope.rootPath,
|
||||
immune: focusedFilterScope.excluded,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function mapStateToProps(
|
||||
{
|
||||
dashboardLayout: undoableLayout,
|
||||
@@ -114,11 +104,14 @@ function mapStateToProps(
|
||||
redoLength: undoableLayout.future.length,
|
||||
filters: getActiveFilters(),
|
||||
directPathToChild: dashboardState.directPathToChild,
|
||||
lastFocusedTabId: dashboardState.lastFocusedTabId,
|
||||
directPathLastUpdated: dashboardState.directPathLastUpdated,
|
||||
dashboardId: dashboardInfo.id,
|
||||
focusedFilterScope:
|
||||
selectFocusedFilterScope(dashboardState, dashboardFilters) ||
|
||||
selectFocusedNativeFilterScope(nativeFilters),
|
||||
nativeFilters,
|
||||
focusedFilterScope: selectFocusedFilterScope(
|
||||
dashboardState,
|
||||
dashboardFilters,
|
||||
),
|
||||
};
|
||||
|
||||
// rows and columns need more data about their child dimensions
|
||||
@@ -148,6 +141,7 @@ function mapDispatchToProps(dispatch) {
|
||||
updateComponents,
|
||||
handleComponentDrop,
|
||||
setDirectPathToChild,
|
||||
setLastFocusedTab,
|
||||
logEvent,
|
||||
},
|
||||
dispatch,
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
SET_DIRECT_PATH,
|
||||
SET_FOCUSED_FILTER_FIELD,
|
||||
UNSET_FOCUSED_FILTER_FIELD,
|
||||
SET_LAST_FOCUSED_TAB,
|
||||
} from '../actions/dashboardState';
|
||||
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
|
||||
|
||||
@@ -133,6 +134,12 @@ export default function dashboardStateReducer(state = {}, action) {
|
||||
directPathLastUpdated: Date.now(),
|
||||
};
|
||||
},
|
||||
[SET_LAST_FOCUSED_TAB]() {
|
||||
return {
|
||||
...state,
|
||||
lastFocusedTabId: action.tabId,
|
||||
};
|
||||
},
|
||||
[SET_FOCUSED_FILTER_FIELD]() {
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -43,7 +43,11 @@ export type Chart = ChartState & {
|
||||
|
||||
export type DashboardLayout = { [key: string]: LayoutItem };
|
||||
export type DashboardLayoutState = { present: DashboardLayout };
|
||||
export type DashboardState = { editMode: boolean; directPathToChild: string[] };
|
||||
export type DashboardState = {
|
||||
editMode: boolean;
|
||||
directPathToChild: string[];
|
||||
lastFocusedTabId: string | null;
|
||||
};
|
||||
export type DashboardInfo = {
|
||||
common: {
|
||||
flash_messages: string[];
|
||||
|
||||
Reference in New Issue
Block a user