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:
Kamil Gabryjelski
2021-06-02 20:38:18 +02:00
committed by GitHub
parent 723a67156c
commit 405f95b6af
20 changed files with 329 additions and 105 deletions

View File

@@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)
}
/>
}

View File

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

View File

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

View File

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

View File

@@ -56,6 +56,8 @@ export interface Filter {
sortMetric?: string | null;
adhoc_filters?: AdhocFilter[];
time_range?: string;
tabsInScope?: string[];
chartsInScope?: number[];
}
export type FilterConfiguration = Filter[];

View File

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

View File

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

View File

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

View File

@@ -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[];