-
- {({ width }) => (
- /*
- We use a TabContainer irrespective of whether top-level tabs exist to maintain
- a consistent React component tree. This avoids expensive mounts/unmounts of
- the entire dashboard upon adding/removing top-level tabs, which would otherwise
- happen because of React's diffing algorithm
- */
-
-
- {childIds.map((id, index) => (
- // Matching the key of the first TabPane irrespective of topLevelTabs
- // lets us keep the same React component tree when !!topLevelTabs changes.
- // This avoids expensive mounts/unmounts of the entire dashboard.
-
-
-
- ))}
-
-
- )}
-
-
- {editMode && (
-
- )}
-
-
-
- );
- }
-}
-
-DashboardBuilder.propTypes = propTypes;
-DashboardBuilder.defaultProps = defaultProps;
-DashboardBuilder.childContextTypes = {
- dragDropManager: PropTypes.object.isRequired,
-};
-
-export default DashboardBuilder;
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
new file mode 100644
index 000000000..17a2362d0
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
@@ -0,0 +1,243 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/* eslint-env browser */
+import cx from 'classnames';
+import React, { FC, SyntheticEvent, useEffect, useState } from 'react';
+import { Sticky, StickyContainer } from 'react-sticky';
+import { TabContainer } from 'react-bootstrap';
+import { JsonObject, styled } from '@superset-ui/core';
+
+import ErrorBoundary from 'src/components/ErrorBoundary';
+import BuilderComponentPane from 'src/dashboard/components/BuilderComponentPane';
+import DashboardHeader from 'src/dashboard/containers/DashboardHeader';
+import IconButton from 'src/dashboard/components/IconButton';
+import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
+import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
+import ToastPresenter from 'src/messageToasts/containers/ToastPresenter';
+import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
+
+import getDirectPathToTabIndex from 'src/dashboard/util/getDirectPathToTabIndex';
+import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
+import { URL_PARAMS } from 'src/constants';
+import { useDispatch, useSelector } from 'react-redux';
+import { getUrlParam } from 'src/utils/urlUtils';
+import { DashboardLayout, RootState } from 'src/dashboard/types';
+import { setDirectPathToChild } from 'src/dashboard/actions/dashboardState';
+import {
+ deleteTopLevelTabs,
+ handleComponentDrop,
+} from 'src/dashboard/actions/dashboardLayout';
+import {
+ DASHBOARD_GRID_ID,
+ DASHBOARD_ROOT_ID,
+ DASHBOARD_ROOT_DEPTH,
+ DashboardStandaloneMode,
+} from 'src/dashboard/util/constants';
+import FilterBar from '../nativeFilters/FilterBar/FilterBar';
+import { StickyVerticalBar } from '../StickyVerticalBar';
+import { shouldFocusTabs, getRootLevelTabsComponent } from './utils';
+import { useFilters } from '../nativeFilters/FilterBar/state';
+import { Filter } from '../nativeFilters/types';
+import DashboardContainer from './DashboardContainer';
+
+const TABS_HEIGHT = 47;
+const HEADER_HEIGHT = 67;
+
+type DashboardBuilderProps = {};
+
+const StyledDashboardContent = styled.div<{ dashboardFiltersOpen: boolean }>`
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ height: auto;
+ flex-grow: 1;
+
+ .grid-container .dashboard-component-tabs {
+ box-shadow: none;
+ padding-left: 0;
+ }
+
+ .grid-container {
+ /* without this, the grid will not get smaller upon toggling the builder panel on */
+ min-width: 0;
+ width: 100%;
+ flex-grow: 1;
+ position: relative;
+ margin: ${({ theme }) => theme.gridUnit * 6}px
+ ${({ theme }) => theme.gridUnit * 8}px
+ ${({ theme }) => theme.gridUnit * 6}px
+ ${({ theme, dashboardFiltersOpen }) => {
+ if (dashboardFiltersOpen) return theme.gridUnit * 8;
+ return 0;
+ }}px;
+ }
+
+ .dashboard-component-chart-holder {
+ // transitionable traits to show filter relevance
+ transition: opacity ${({ theme }) => theme.transitionTiming}s,
+ border-color ${({ theme }) => theme.transitionTiming}s,
+ box-shadow ${({ theme }) => theme.transitionTiming}s;
+ border: 0 solid transparent;
+ }
+`;
+
+const DashboardBuilder: FC = () => {
+ const dispatch = useDispatch();
+ const dashboardLayout = useSelector(
+ state => state.dashboardLayout.present,
+ );
+ const editMode = useSelector(
+ state => state.dashboardState.editMode,
+ );
+ const directPathToChild = useSelector(
+ state => state.dashboardState.directPathToChild,
+ );
+
+ const filters = useFilters();
+ const filterValues = Object.values(filters);
+
+ const [dashboardFiltersOpen, setDashboardFiltersOpen] = useState(true);
+
+ const toggleDashboardFiltersOpen = (visible?: boolean) => {
+ setDashboardFiltersOpen(visible ?? !dashboardFiltersOpen);
+ };
+
+ const handleChangeTab = ({
+ pathToTabIndex,
+ }: SyntheticEvent & { pathToTabIndex: string[] }) => {
+ dispatch(setDirectPathToChild(pathToTabIndex));
+ };
+
+ const handleDeleteTopLevelTabs = () => {
+ dispatch(deleteTopLevelTabs());
+
+ const firstTab = getDirectPathToTabIndex(
+ getRootLevelTabsComponent(dashboardLayout),
+ 0,
+ );
+ dispatch(setDirectPathToChild(firstTab));
+ };
+
+ const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
+ const rootChildId = dashboardRoot.children[0];
+ const topLevelTabs =
+ rootChildId !== DASHBOARD_GRID_ID
+ ? dashboardLayout[rootChildId]
+ : undefined;
+
+ const hideDashboardHeader =
+ getUrlParam(URL_PARAMS.standalone, 'number') ===
+ DashboardStandaloneMode.HIDE_NAV_AND_TITLE;
+
+ const barTopOffset =
+ (hideDashboardHeader ? 0 : HEADER_HEIGHT) +
+ (topLevelTabs ? TABS_HEIGHT : 0);
+
+ useEffect(() => {
+ if (filterValues.length === 0 && dashboardFiltersOpen) {
+ toggleDashboardFiltersOpen(false);
+ }
+ }, [filterValues.length]);
+
+ return (
+
+
+ {({ style }) => (
+ // @ts-ignore
+ dispatch(handleComponentDrop)}
+ editMode={editMode}
+ // you cannot drop on/displace tabs if they already exist
+ disableDragdrop={!!topLevelTabs}
+ style={{
+ zIndex: 100,
+ ...style,
+ }}
+ >
+ {({ dropIndicatorProps }: { dropIndicatorProps: JsonObject }) => (
+
+ )}
+
+ )}
+
+
+ {isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) && !editMode && (
+
+
+
+
+
+ )}
+
+ {editMode && }
+
+
+
+ );
+};
+
+export default DashboardBuilder;
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx
new file mode 100644
index 000000000..cd0120937
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx
@@ -0,0 +1,106 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+// ParentSize uses resize observer so the dashboard will update size
+// when its container size changes, due to e.g., builder side panel opening
+import { ParentSize } from '@vx/responsive';
+import { TabContainer, TabContent, TabPane } from 'react-bootstrap';
+import React, { FC, SyntheticEvent, useEffect, useState } from 'react';
+import { 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';
+import {
+ DASHBOARD_GRID_ID,
+ DASHBOARD_ROOT_DEPTH,
+} from 'src/dashboard/util/constants';
+import { getRootLevelTabIndex } from './utils';
+
+type DashboardContainerProps = {
+ topLevelTabs?: LayoutItem;
+ handleChangeTab: (event: SyntheticEvent) => void;
+};
+
+const DashboardContainer: FC = ({
+ topLevelTabs,
+ handleChangeTab,
+}) => {
+ const dashboardLayout = useSelector(
+ state => state.dashboardLayout.present,
+ );
+ const directPathToChild = useSelector(
+ state => state.dashboardState.directPathToChild,
+ );
+ const [tabIndex, setTabIndex] = useState(
+ getRootLevelTabIndex(dashboardLayout, directPathToChild),
+ );
+
+ useEffect(() => {
+ setTabIndex(getRootLevelTabIndex(dashboardLayout, directPathToChild));
+ }, [getLeafComponentIdFromPath(directPathToChild)]);
+
+ const childIds: string[] = topLevelTabs
+ ? topLevelTabs.children
+ : [DASHBOARD_GRID_ID];
+
+ return (
+
+
+ {({ width }) => (
+ /*
+ We use a TabContainer irrespective of whether top-level tabs exist to maintain
+ a consistent React component tree. This avoids expensive mounts/unmounts of
+ the entire dashboard upon adding/removing top-level tabs, which would otherwise
+ happen because of React's diffing algorithm
+ */
+
+
+ {childIds.map((id, index) => (
+ // Matching the key of the first TabPane irrespective of topLevelTabs
+ // lets us keep the same React component tree when !!topLevelTabs changes.
+ // This avoids expensive mounts/unmounts of the entire dashboard.
+
+
+
+ ))}
+
+
+ )}
+
+
+ );
+};
+
+export default DashboardContainer;
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/utils.ts b/superset-frontend/src/dashboard/components/DashboardBuilder/utils.ts
new file mode 100644
index 000000000..999adf62a
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/utils.ts
@@ -0,0 +1,53 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {
+ DASHBOARD_GRID_ID,
+ DASHBOARD_ROOT_ID,
+} from 'src/dashboard/util/constants';
+import { DashboardLayout } from 'src/dashboard/types';
+import findTabIndexByComponentId from 'src/dashboard/util/findTabIndexByComponentId';
+
+export const getRootLevelTabsComponent = (dashboardLayout: DashboardLayout) => {
+ const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
+ const rootChildId = dashboardRoot.children[0];
+ return rootChildId === DASHBOARD_GRID_ID
+ ? dashboardLayout[DASHBOARD_ROOT_ID]
+ : dashboardLayout[rootChildId];
+};
+
+export const shouldFocusTabs = (
+ event: { target: { className: string } },
+ container: { contains: (arg0: any) => any },
+) =>
+ // don't focus the tabs when we click on a tab
+ event.target.className === 'ant-tabs-nav-wrap' ||
+ (/icon-button/.test(event.target.className) &&
+ container.contains(event.target));
+
+export const getRootLevelTabIndex = (
+ dashboardLayout: DashboardLayout,
+ directPathToChild: string[],
+): number =>
+ Math.max(
+ 0,
+ findTabIndexByComponentId({
+ currentComponent: getRootLevelTabsComponent(dashboardLayout),
+ directPathToChild,
+ }),
+ );
diff --git a/superset-frontend/src/dashboard/components/StickyVerticalBar.tsx b/superset-frontend/src/dashboard/components/StickyVerticalBar.tsx
index afb5a71a2..80e76282e 100644
--- a/superset-frontend/src/dashboard/components/StickyVerticalBar.tsx
+++ b/superset-frontend/src/dashboard/components/StickyVerticalBar.tsx
@@ -50,7 +50,7 @@ const Contents = styled.div`
export interface SVBProps {
topOffset: number;
- width: number;
+ width?: number;
filtersOpen: boolean;
}
diff --git a/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx b/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx
index 55991462a..36be1833e 100644
--- a/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx
+++ b/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx
@@ -37,7 +37,7 @@ const propTypes = {
component: componentShape.isRequired,
parentComponent: componentShape,
depth: PropTypes.number.isRequired,
- disableDragDrop: PropTypes.bool,
+ disableDragdrop: PropTypes.bool,
orientation: PropTypes.oneOf(['row', 'column']),
index: PropTypes.number.isRequired,
style: PropTypes.object,
@@ -58,7 +58,7 @@ const defaultProps = {
className: null,
style: null,
parentComponent: null,
- disableDragDrop: false,
+ disableDragdrop: false,
children() {},
onDrop() {},
orientation: 'row',
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilterControl.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadeFilterControl.tsx
similarity index 95%
rename from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilterControl.tsx
rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadeFilterControl.tsx
index a0e6d673a..3b5ce92b6 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilterControl.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadeFilterControl.tsx
@@ -19,8 +19,8 @@
import React from 'react';
import { styled, DataMask } from '@superset-ui/core';
import Icon from 'src/components/Icon';
-import FilterControl from './FilterControl';
-import { Filter } from '../types';
+import FilterControl from '../FilterControls/FilterControl';
+import { Filter } from '../../types';
import { CascadeFilter } from './types';
interface CascadeFilterControlProps {
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadePopover.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadePopover.tsx
similarity index 98%
rename from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadePopover.tsx
rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadePopover.tsx
index 430003bb1..5792ece91 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadePopover.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadePopover.tsx
@@ -24,10 +24,10 @@ import { Pill } from 'src/dashboard/components/FiltersBadge/Styles';
import { useSelector } from 'react-redux';
import { getInitialMask } from 'src/dataMask/reducer';
import { MaskWithId } from 'src/dataMask/types';
-import FilterControl from './FilterControl';
+import FilterControl from '../FilterControls/FilterControl';
import CascadeFilterControl from './CascadeFilterControl';
import { CascadeFilter } from './types';
-import { Filter } from '../types';
+import { Filter } from '../../types';
interface CascadePopoverProps {
filter: CascadeFilter;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/types.ts
new file mode 100644
index 000000000..435404669
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/types.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Filter } from '../../types';
+
+export interface CascadeFilter extends Filter {
+ cascadeChildren: CascadeFilter[];
+}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx
index 2e3814760..ef30ab807 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx
@@ -19,25 +19,29 @@
/* eslint-disable no-param-reassign */
import { HandlerFunction, styled, t } from '@superset-ui/core';
-import React, { useState, useEffect, useMemo } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import React, { useMemo, useState } from 'react';
+import { useDispatch } from 'react-redux';
import cx from 'classnames';
-import Button from 'src/components/Button';
import Icon from 'src/components/Icon';
import { Tabs } from 'src/common/components';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { updateDataMask } from 'src/dataMask/actions';
-import { DataMaskUnit, DataMaskState } from 'src/dataMask/types';
+import { DataMaskState, DataMaskUnit } from 'src/dataMask/types';
import { useImmer } from 'use-immer';
-import { getInitialMask } from 'src/dataMask/reducer';
import { areObjectsEqual } from 'src/reduxUtils';
-import FilterConfigurationLink from './FilterConfigurationLink';
import { Filter } from '../types';
-import { buildCascadeFiltersTree, mapParentFiltersToChildren } from './utils';
-import CascadePopover from './CascadePopover';
+import { mapParentFiltersToChildren, TabIds } from './utils';
import FilterSets from './FilterSets/FilterSets';
-import { useDataMask, useFilters, useFilterSets } from './state';
+import {
+ useDataMask,
+ useFilters,
+ useFilterSets,
+ useFiltersInitialisation,
+ useFilterUpdates,
+} from './state';
import EditSection from './FilterSets/EditSection';
+import Header from './Header';
+import FilterControls from './FilterControls/FilterControls';
const barWidth = `250px`;
@@ -122,18 +126,6 @@ const StyledCollapseIcon = styled(Icon)`
margin-bottom: ${({ theme }) => theme.gridUnit * 3}px;
`;
-const TitleArea = styled.h4`
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- margin: 0;
- padding: ${({ theme }) => theme.gridUnit * 2}px;
-
- & > span {
- flex-grow: 1;
- }
-`;
-
const StyledTabs = styled(Tabs)`
& .ant-tabs-nav-list {
width: 100%;
@@ -146,39 +138,12 @@ const StyledTabs = styled(Tabs)`
}
`;
-const ActionButtons = styled.div`
- display: grid;
- flex-direction: row;
- justify-content: center;
- align-items: center;
- grid-gap: 10px;
- grid-template-columns: 1fr 1fr;
- ${({ theme }) =>
- `padding: 0 ${theme.gridUnit * 2}px ${theme.gridUnit * 2}px`};
-
- .btn {
- flex: 1;
- }
-`;
-
-const FilterControls = styled.div`
- padding: ${({ theme }) => theme.gridUnit * 4}px;
- &:hover {
- cursor: pointer;
- }
-`;
-
interface FiltersBarProps {
filtersOpen: boolean;
toggleFiltersBar: any;
directPathToChild?: string[];
}
-enum TabIds {
- AllFilters = 'allFilters',
- FilterSets = 'filterSets',
-}
-
const FilterBar: React.FC = ({
filtersOpen,
toggleFiltersBar,
@@ -193,90 +158,17 @@ const FilterBar: React.FC = ({
const dispatch = useDispatch();
const filterSets = useFilterSets();
const filterSetFilterValues = Object.values(filterSets);
- const [isFilterSetChanged, setIsFilterSetChanged] = useState(false);
const [tab, setTab] = useState(TabIds.AllFilters);
const filters = useFilters();
const filterValues = Object.values(filters);
const dataMaskApplied = useDataMask();
- const canEdit = useSelector(
- ({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
- );
- const [visiblePopoverId, setVisiblePopoverId] = useState(null);
- const [isInitialized, setIsInitialized] = useState(false);
-
- const handleApply = () => {
- const filterIds = Object.keys(dataMaskSelected);
- filterIds.forEach(filterId => {
- if (dataMaskSelected[filterId]) {
- dispatch(
- updateDataMask(filterId, {
- nativeFilters: dataMaskSelected[filterId],
- }),
- );
- }
- });
- setLastAppliedFilterData(() => dataMaskSelected);
- };
-
- useEffect(() => {
- if (isInitialized) {
- return;
- }
- const areFiltersInitialized = filterValues.every(filterValue =>
- areObjectsEqual(
- filterValue?.defaultValue,
- dataMaskSelected[filterValue?.id]?.currentState?.value,
- ),
- );
- if (areFiltersInitialized) {
- handleApply();
- setIsInitialized(true);
- }
- }, [filterValues, dataMaskSelected, isInitialized]);
-
- useEffect(() => {
- if (filterValues.length === 0 && filtersOpen) {
- toggleFiltersBar(false);
- }
- }, [filterValues.length]);
-
- useEffect(() => {
- // Remove deleted filters from local state
- Object.keys(dataMaskSelected).forEach(selectedId => {
- if (!filters[selectedId]) {
- setDataMaskSelected(draft => {
- delete draft[selectedId];
- });
- }
- });
- Object.keys(dataMaskApplied).forEach(appliedId => {
- if (!filters[appliedId]) {
- setLastAppliedFilterData(draft => {
- delete draft[appliedId];
- });
- }
- });
- }, [
- dataMaskApplied,
- dataMaskSelected,
- filters,
- setDataMaskSelected,
- setLastAppliedFilterData,
- ]);
+ const [isFilterSetChanged, setIsFilterSetChanged] = useState(false);
const cascadeChildren = useMemo(
() => mapParentFiltersToChildren(filterValues),
[filterValues],
);
- const cascadeFilters = useMemo(() => {
- const filtersWithValue = filterValues.map(filter => ({
- ...filter,
- currentValue: dataMaskSelected[filter.id]?.currentState?.value,
- }));
- return buildCascadeFiltersTree(filtersWithValue);
- }, [filterValues, dataMaskSelected]);
-
const handleFilterSelectionChange = (
filter: Pick & Partial,
dataMask: Partial,
@@ -295,36 +187,29 @@ const FilterBar: React.FC = ({
});
};
- const handleClearAll = () => {
- filterValues.forEach(filter => {
- setDataMaskSelected(draft => {
- draft[filter.id] = getInitialMask(filter.id);
- });
+ const handleApply = () => {
+ const filterIds = Object.keys(dataMaskSelected);
+ filterIds.forEach(filterId => {
+ if (dataMaskSelected[filterId]) {
+ dispatch(
+ updateDataMask(filterId, {
+ nativeFilters: dataMaskSelected[filterId],
+ }),
+ );
+ }
});
+ setLastAppliedFilterData(() => dataMaskSelected);
};
- const isClearAllDisabled = Object.values(dataMaskApplied).every(
- filter =>
- dataMaskSelected[filter.id]?.currentState?.value === null ||
- (!dataMaskSelected[filter.id] && filter.currentState?.value === null),
+ const { isInitialized } = useFiltersInitialisation(
+ dataMaskSelected,
+ handleApply,
);
- const getFilterControls = () => (
-
- {cascadeFilters.map(filter => (
-
- setVisiblePopoverId(visible ? filter.id : null)
- }
- filter={filter}
- onFilterSelectionChange={handleFilterSelectionChange}
- directPathToChild={directPathToChild}
- />
- ))}
-
+ useFilterUpdates(
+ dataMaskSelected,
+ setDataMaskSelected,
+ setLastAppliedFilterData,
);
const isApplyDisabled =
@@ -340,38 +225,14 @@ const FilterBar: React.FC = ({
-
- {t('Filters')}
- {canEdit && (
-
-
-
- )}
- toggleFiltersBar(false)} />
-
-
-
-
-
+
{isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) ? (
= ({
filterSetId={editFilterSetId}
/>
)}
- {getFilterControls()}
+ = ({
) : (
- getFilterControls()
+
)}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControl.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx
similarity index 100%
rename from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControl.tsx
rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx
new file mode 100644
index 000000000..424e58721
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React, { FC, useMemo, useState } from 'react';
+import { DataMaskUnit } from 'src/dataMask/types';
+import { DataMask, styled } from '@superset-ui/core';
+import CascadePopover from '../CascadeFilters/CascadePopover';
+import { buildCascadeFiltersTree } from './utils';
+import { useFilters } from '../state';
+import { Filter } from '../../types';
+
+const Wrapper = styled.div`
+ padding: ${({ theme }) => theme.gridUnit * 4}px;
+ &:hover {
+ cursor: pointer;
+ }
+`;
+
+type FilterControlsProps = {
+ directPathToChild?: string[];
+ dataMaskSelected: DataMaskUnit;
+ onFilterSelectionChange: (filter: Filter, dataMask: DataMask) => void;
+};
+
+const FilterControls: FC = ({
+ directPathToChild,
+ dataMaskSelected,
+ onFilterSelectionChange,
+}) => {
+ const [visiblePopoverId, setVisiblePopoverId] = useState(null);
+ const filters = useFilters();
+ const filterValues = Object.values(filters);
+
+ const cascadeFilters = useMemo(() => {
+ const filtersWithValue = filterValues.map(filter => ({
+ ...filter,
+ currentValue: dataMaskSelected[filter.id]?.currentState?.value,
+ }));
+ return buildCascadeFiltersTree(filtersWithValue);
+ }, [filterValues, dataMaskSelected]);
+
+ return (
+
+ {cascadeFilters.map(filter => (
+
+ setVisiblePopoverId(visible ? filter.id : null)
+ }
+ filter={filter}
+ onFilterSelectionChange={onFilterSelectionChange}
+ directPathToChild={directPathToChild}
+ />
+ ))}
+
+ );
+};
+
+export default FilterControls;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
similarity index 98%
rename from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx
rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
index 9b5fb9207..6e7a4eaca 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
@@ -30,7 +30,7 @@ import { getChartDataRequest } from 'src/chart/chartAction';
import Loading from 'src/components/Loading';
import BasicErrorAlert from 'src/components/ErrorMessage/BasicErrorAlert';
import { FilterProps } from './types';
-import { getFormData } from '../utils';
+import { getFormData } from '../../utils';
import { useCascadingFilters } from './state';
const StyledLoadingBox = styled.div`
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/state.ts
new file mode 100644
index 000000000..0a60c2c02
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/state.ts
@@ -0,0 +1,39 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useSelector } from 'react-redux';
+import { NativeFiltersState } from 'src/dashboard/reducers/types';
+import { mergeExtraFormData } from '../../utils';
+import { useDataMask } from '../state';
+
+// eslint-disable-next-line import/prefer-default-export
+export function useCascadingFilters(id: string) {
+ const { filters } = useSelector(
+ state => state.nativeFilters,
+ );
+ const filter = filters[id];
+ const cascadeParentIds: string[] = filter?.cascadeParentIds ?? [];
+ let cascadedFilters = {};
+ const nativeFilters = useDataMask();
+ cascadeParentIds.forEach(parentId => {
+ const parentState = nativeFilters[parentId] || {};
+ const { extraFormData: parentExtra = {} } = parentState;
+ cascadedFilters = mergeExtraFormData(cascadedFilters, parentExtra);
+ });
+ return cascadedFilters;
+}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/types.ts
similarity index 89%
rename from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts
rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/types.ts
index 896aa47a5..67e50d562 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/types.ts
@@ -18,7 +18,7 @@
*/
import React from 'react';
import { DataMask } from '@superset-ui/core';
-import { Filter } from '../types';
+import { Filter } from '../../types';
export interface FilterProps {
filter: Filter;
@@ -26,7 +26,3 @@ export interface FilterProps {
directPathToChild?: string[];
onFilterSelectionChange: (filter: Filter, dataMask: DataMask) => void;
}
-
-export interface CascadeFilter extends Filter {
- cascadeChildren: CascadeFilter[];
-}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/utils.ts
new file mode 100644
index 000000000..19b50ddf6
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/utils.ts
@@ -0,0 +1,38 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Filter } from '../../types';
+import { CascadeFilter } from '../CascadeFilters/types';
+import { mapParentFiltersToChildren } from '../utils';
+
+// eslint-disable-next-line import/prefer-default-export
+export function buildCascadeFiltersTree(filters: Filter[]): CascadeFilter[] {
+ const cascadeChildren = mapParentFiltersToChildren(filters);
+
+ const getCascadeFilter = (filter: Filter): CascadeFilter => {
+ const children = cascadeChildren[filter.id] || [];
+ return {
+ ...filter,
+ cascadeChildren: children.map(getCascadeFilter),
+ };
+ };
+
+ return filters
+ .filter(filter => !filter.cascadeParentIds?.length)
+ .map(getCascadeFilter);
+}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header.tsx
new file mode 100644
index 000000000..0f2282a99
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header.tsx
@@ -0,0 +1,131 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/* eslint-disable no-param-reassign */
+import { styled, t } from '@superset-ui/core';
+import React, { FC } from 'react';
+import Icon from 'src/components/Icon';
+import Button from 'src/components/Button';
+import { useSelector } from 'react-redux';
+import { getInitialMask } from 'src/dataMask/reducer';
+import { DataMaskUnit, DataMaskUnitWithId } from 'src/dataMask/types';
+import FilterConfigurationLink from './FilterConfigurationLink';
+import { useFilters } from './state';
+import { Filter } from '../types';
+
+const TitleArea = styled.h4`
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ margin: 0;
+ padding: ${({ theme }) => theme.gridUnit * 2}px;
+
+ & > span {
+ flex-grow: 1;
+ }
+`;
+
+const ActionButtons = styled.div`
+ display: grid;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ grid-gap: 10px;
+ grid-template-columns: 1fr 1fr;
+ ${({ theme }) =>
+ `padding: 0 ${theme.gridUnit * 2}px ${theme.gridUnit * 2}px`};
+
+ .btn {
+ flex: 1;
+ }
+`;
+
+type HeaderProps = {
+ toggleFiltersBar: (arg0: boolean) => void;
+ onApply: () => void;
+ setDataMaskSelected: (arg0: (draft: DataMaskUnit) => void) => void;
+ dataMaskSelected: DataMaskUnit;
+ dataMaskApplied: DataMaskUnitWithId;
+ isApplyDisabled: boolean;
+};
+
+const Header: FC = ({
+ onApply,
+ isApplyDisabled,
+ dataMaskSelected,
+ dataMaskApplied,
+ setDataMaskSelected,
+ toggleFiltersBar,
+}) => {
+ const filters = useFilters();
+ const filterValues = Object.values(filters);
+ const canEdit = useSelector(
+ ({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
+ );
+
+ const handleClearAll = () => {
+ filterValues.forEach(filter => {
+ setDataMaskSelected(draft => {
+ draft[filter.id] = getInitialMask(filter.id);
+ });
+ });
+ };
+
+ const isClearAllDisabled = Object.values(dataMaskApplied).every(
+ filter =>
+ dataMaskSelected[filter.id]?.currentState?.value === null ||
+ (!dataMaskSelected[filter.id] && filter.currentState?.value === null),
+ );
+
+ return (
+ <>
+
+ {t('Filters')}
+ {canEdit && (
+
+
+
+ )}
+ toggleFiltersBar(false)} />
+
+
+
+
+
+ >
+ );
+};
+
+export default Header;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts
index db51f30c5..6f3d10263 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts
@@ -16,14 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
+/* eslint-disable no-param-reassign */
import { useSelector } from 'react-redux';
import {
Filters,
FilterSets as FilterSetsType,
- NativeFiltersState,
} from 'src/dashboard/reducers/types';
-import { DataMaskUnitWithId } from 'src/dataMask/types';
-import { mergeExtraFormData } from '../utils';
+import { DataMaskUnit, DataMaskUnitWithId } from 'src/dataMask/types';
+import { useEffect, useState } from 'react';
+import { areObjectsEqual } from 'src/reduxUtils';
+import { Filter } from '../types';
export const useFilterSets = () =>
useSelector(
@@ -36,18 +38,63 @@ export const useFilters = () =>
export const useDataMask = () =>
useSelector(state => state.dataMask.nativeFilters);
-export function useCascadingFilters(id: string) {
- const { filters } = useSelector(
- state => state.nativeFilters,
- );
- const filter = filters[id];
- const cascadeParentIds: string[] = filter?.cascadeParentIds ?? [];
- let cascadedFilters = {};
- const nativeFilters = useDataMask();
- cascadeParentIds.forEach(parentId => {
- const parentState = nativeFilters[parentId] || {};
- const { extraFormData: parentExtra = {} } = parentState;
- cascadedFilters = mergeExtraFormData(cascadedFilters, parentExtra);
- });
- return cascadedFilters;
-}
+export const useFiltersInitialisation = (
+ dataMaskSelected: DataMaskUnit,
+ handleApply: () => void,
+) => {
+ const [isInitialized, setIsInitialized] = useState(false);
+ const filters = useFilters();
+ const filterValues = Object.values(filters);
+ useEffect(() => {
+ if (isInitialized) {
+ return;
+ }
+ const areFiltersInitialized = filterValues.every(filterValue =>
+ areObjectsEqual(
+ filterValue?.defaultValue,
+ dataMaskSelected[filterValue?.id]?.currentState?.value,
+ ),
+ );
+ if (areFiltersInitialized) {
+ handleApply();
+ setIsInitialized(true);
+ }
+ }, [filterValues, dataMaskSelected, isInitialized]);
+
+ return {
+ isInitialized,
+ };
+};
+
+export const useFilterUpdates = (
+ dataMaskSelected: DataMaskUnit,
+ setDataMaskSelected: (arg0: (arg0: DataMaskUnit) => void) => void,
+ setLastAppliedFilterData: (arg0: (arg0: DataMaskUnit) => void) => void,
+) => {
+ const filters = useFilters();
+ const dataMaskApplied = useDataMask();
+
+ useEffect(() => {
+ // Remove deleted filters from local state
+ Object.keys(dataMaskSelected).forEach(selectedId => {
+ if (!filters[selectedId]) {
+ setDataMaskSelected(draft => {
+ delete draft[selectedId];
+ });
+ }
+ });
+ Object.keys(dataMaskApplied).forEach(appliedId => {
+ if (!filters[appliedId]) {
+ setLastAppliedFilterData(draft => {
+ delete draft[appliedId];
+ });
+ }
+ });
+ }, [
+ dataMaskApplied,
+ dataMaskSelected,
+ filters,
+ setDataMaskSelected,
+ setLastAppliedFilterData,
+ ]);
+};
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts
index 71681a23b..dc7097a50 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts
@@ -18,7 +18,11 @@
*/
import { Filter } from '../types';
-import { CascadeFilter } from './types';
+
+export enum TabIds {
+ AllFilters = 'allFilters',
+ FilterSets = 'filterSets',
+}
export function mapParentFiltersToChildren(
filters: Filter[],
@@ -35,19 +39,3 @@ export function mapParentFiltersToChildren(
});
return cascadeChildren;
}
-
-export function buildCascadeFiltersTree(filters: Filter[]): CascadeFilter[] {
- const cascadeChildren = mapParentFiltersToChildren(filters);
-
- const getCascadeFilter = (filter: Filter): CascadeFilter => {
- const children = cascadeChildren[filter.id] || [];
- return {
- ...filter,
- cascadeChildren: children.map(getCascadeFilter),
- };
- };
-
- return filters
- .filter(filter => !filter.cascadeParentIds?.length)
- .map(getCascadeFilter);
-}
diff --git a/superset-frontend/src/dashboard/containers/DashboardBuilder.jsx b/superset-frontend/src/dashboard/containers/DashboardBuilder.jsx
deleted file mode 100644
index dba43d189..000000000
--- a/superset-frontend/src/dashboard/containers/DashboardBuilder.jsx
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { bindActionCreators } from 'redux';
-import { connect } from 'react-redux';
-import DashboardBuilder from '../components/DashboardBuilder';
-
-import {
- setColorSchemeAndUnsavedChanges,
- showBuilderPane,
- setDirectPathToChild,
-} from '../actions/dashboardState';
-import {
- deleteTopLevelTabs,
- handleComponentDrop,
-} from '../actions/dashboardLayout';
-
-function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState }) {
- return {
- dashboardLayout: undoableLayout.present,
- editMode: dashboardState.editMode,
- showBuilderPane: dashboardState.showBuilderPane,
- directPathToChild: dashboardState.directPathToChild,
- colorScheme: dashboardState.colorScheme,
- focusedFilterField: dashboardState.focusedFilterField,
- };
-}
-
-function mapDispatchToProps(dispatch) {
- return bindActionCreators(
- {
- deleteTopLevelTabs,
- handleComponentDrop,
- showBuilderPane,
- setColorSchemeAndUnsavedChanges,
- setDirectPathToChild,
- },
- dispatch,
- );
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(DashboardBuilder);
diff --git a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx
index e8331b270..4d5e14bc0 100644
--- a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx
+++ b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx
@@ -38,6 +38,13 @@ import {
import { setDirectPathToChild } from '../actions/dashboardState';
const propTypes = {
+ id: PropTypes.string,
+ parentId: PropTypes.string,
+ depth: PropTypes.number,
+ index: PropTypes.number,
+ renderHoverMenu: PropTypes.bool,
+ renderTabContent: PropTypes.bool,
+ onChangeTab: PropTypes.func,
component: componentShape.isRequired,
parentComponent: componentShape.isRequired,
createComponent: PropTypes.func.isRequired,
diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts
index 696eda40b..e74fb43c2 100644
--- a/superset-frontend/src/dashboard/types.ts
+++ b/superset-frontend/src/dashboard/types.ts
@@ -40,11 +40,16 @@ export type Chart = {
};
};
+export type DashboardLayout = { [key: string]: LayoutItem };
+export type DashboardLayoutState = { present: DashboardLayout };
+export type DashboardState = { editMode: boolean; directPathToChild: string[] };
+
/** Root state of redux */
export type RootState = {
charts: { [key: string]: Chart };
- dashboardLayout: { present: { [key: string]: LayoutItem } };
+ dashboardLayout: DashboardLayoutState;
dashboardFilters: {};
+ dashboardState: DashboardState;
dataMask: DataMaskStateWithId;
};