You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ju...@apache.org on 2024/03/19 21:32:52 UTC
(superset) branch justin--4.0 updated: DO NOT MERGE: test show allowlist only (#27578)
This is an automated email from the ASF dual-hosted git repository.
justinpark pushed a commit to branch justin--4.0
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/justin--4.0 by this push:
new 8536cf0c60 DO NOT MERGE: test show allowlist only (#27578)
8536cf0c60 is described below
commit 8536cf0c60cf2dd1af2dd216402c24a5f4a4b7c6
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Tue Mar 19 14:32:46 2024 -0700
DO NOT MERGE: test show allowlist only (#27578)
---
.../explore/components/DatasourcePanel/index.tsx | 217 ++++++++++++---------
.../ExploreContainer/ExploreContainer.test.tsx | 85 ++++++++
.../explore/components/ExploreContainer/index.tsx | 88 +++++++++
.../components/ExploreViewContainer/index.jsx | 8 +-
.../DndColumnSelectControl/DndSelectLabel.tsx | 15 +-
.../components/controls/OptionControls/index.tsx | 40 +++-
superset-frontend/src/pages/Chart/Chart.test.tsx | 3 +
7 files changed, 352 insertions(+), 104 deletions(-)
diff --git a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
index 99f6b48b89..b084801181 100644
--- a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
+++ b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
@@ -16,19 +16,20 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { useEffect, useMemo, useRef, useState } from 'react';
+import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import {
css,
DatasourceType,
Metric,
QueryFormData,
+ SLOW_DEBOUNCE,
styled,
t,
} from '@superset-ui/core';
import { ControlConfig } from '@superset-ui/chart-controls';
-
-import { debounce, isArray } from 'lodash';
+import { isArray } from 'lodash';
+import { Checkbox } from 'antd';
import { matchSorter, rankings } from 'match-sorter';
import Collapse from 'src/components/Collapse';
import Alert from 'src/components/Alert';
@@ -38,9 +39,11 @@ import { Input } from 'src/components/Input';
import { FAST_DEBOUNCE } from 'src/constants';
import { ExploreActions } from 'src/explore/actions/exploreActions';
import Control from 'src/explore/components/Control';
+import { useDebounceValue } from 'src/hooks/useDebounceValue';
import DatasourcePanelDragOption from './DatasourcePanelDragOption';
import { DndItemType } from '../DndItemType';
import { DndItemValue } from './types';
+import { DropzoneContext } from '../ExploreContainer';
interface DatasourceControl extends ControlConfig {
datasource?: IDatasource;
@@ -202,6 +205,9 @@ const LabelContainer = (props: {
);
};
+const sortCertifiedFirst = (slice: DataSourcePanelColumn[]) =>
+ slice.sort((a, b) => (b?.is_certified ?? 0) - (a?.is_certified ?? 0));
+
export default function DataSourcePanel({
datasource,
formData,
@@ -209,11 +215,30 @@ export default function DataSourcePanel({
actions,
shouldForceUpdate,
}: Props) {
+ const [dropzones] = useContext(DropzoneContext);
+ const [allowlistOnly, setAllowlistOnly] = useState(false);
+
const { columns: _columns, metrics } = datasource;
+
+ const allowedColumns = useMemo(() => {
+ const validators = Object.values(dropzones);
+ if (!isArray(_columns)) return [];
+ return allowlistOnly
+ ? _columns.filter(column =>
+ validators.some(validator =>
+ validator({
+ value: column as DndItemValue,
+ type: DndItemType.Column,
+ }),
+ ),
+ )
+ : _columns;
+ }, [dropzones, _columns, allowlistOnly]);
+
// display temporal column first
const columns = useMemo(
() =>
- [...(isArray(_columns) ? _columns : [])].sort((col1, col2) => {
+ [...allowedColumns].sort((col1, col2) => {
if (col1?.is_dttm && !col2?.is_dttm) {
return -1;
}
@@ -222,107 +247,105 @@ export default function DataSourcePanel({
}
return 0;
}),
- [_columns],
+ [allowedColumns],
);
+ const allowedMetrics = useMemo(() => {
+ const validators = Object.values(dropzones);
+ return allowlistOnly
+ ? metrics.filter(metric =>
+ validators.some(validator =>
+ validator({ value: metric, type: DndItemType.Metric }),
+ ),
+ )
+ : metrics;
+ }, [dropzones, metrics, allowlistOnly]);
+
+ console.log('allowedMetrics', allowedMetrics);
+
const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false);
const [inputValue, setInputValue] = useState('');
- const [lists, setList] = useState({
- columns,
- metrics,
- });
const [showAllMetrics, setShowAllMetrics] = useState(false);
const [showAllColumns, setShowAllColumns] = useState(false);
+ const searchKeyword = useDebounceValue(inputValue, FAST_DEBOUNCE);
const DEFAULT_MAX_COLUMNS_LENGTH = 50;
const DEFAULT_MAX_METRICS_LENGTH = 50;
- const search = useMemo(
- () =>
- debounce((value: string) => {
- if (value === '') {
- setList({ columns, metrics });
- return;
- }
- setList({
- columns: matchSorter(columns, value, {
- keys: [
- {
- key: 'verbose_name',
- threshold: rankings.CONTAINS,
- },
- {
- key: 'column_name',
- threshold: rankings.CONTAINS,
- },
- {
- key: item =>
- [item?.description ?? '', item?.expression ?? ''].map(
- x => x?.replace(/[_\n\s]+/g, ' ') || '',
- ),
- threshold: rankings.CONTAINS,
- maxRanking: rankings.CONTAINS,
- },
- ],
- keepDiacritics: true,
- }),
- metrics: matchSorter(metrics, value, {
- keys: [
- {
- key: 'verbose_name',
- threshold: rankings.CONTAINS,
- },
- {
- key: 'metric_name',
- threshold: rankings.CONTAINS,
- },
- {
- key: item =>
- [item?.description ?? '', item?.expression ?? ''].map(
- x => x?.replace(/[_\n\s]+/g, ' ') || '',
- ),
- threshold: rankings.CONTAINS,
- maxRanking: rankings.CONTAINS,
- },
- ],
- keepDiacritics: true,
- baseSort: (a, b) =>
- Number(b?.item?.is_certified ?? 0) -
- Number(a?.item?.is_certified ?? 0) ||
- String(a?.rankedValue ?? '').localeCompare(b?.rankedValue ?? ''),
- }),
- });
- }, FAST_DEBOUNCE),
- [columns, metrics],
- );
-
- useEffect(() => {
- setList({
- columns,
- metrics,
+ const filteredColumns = useMemo(() => {
+ if (!searchKeyword) {
+ return columns ?? [];
+ }
+ return matchSorter(columns, searchKeyword, {
+ keys: [
+ {
+ key: 'verbose_name',
+ threshold: rankings.CONTAINS,
+ },
+ {
+ key: 'column_name',
+ threshold: rankings.CONTAINS,
+ },
+ {
+ key: item =>
+ [item?.description ?? '', item?.expression ?? ''].map(
+ x => x?.replace(/[_\n\s]+/g, ' ') || '',
+ ),
+ threshold: rankings.CONTAINS,
+ maxRanking: rankings.CONTAINS,
+ },
+ ],
+ keepDiacritics: true,
});
- setInputValue('');
- }, [columns, datasource, metrics]);
+ }, [columns, searchKeyword]);
- const sortCertifiedFirst = (slice: DataSourcePanelColumn[]) =>
- slice.sort((a, b) => (b?.is_certified ?? 0) - (a?.is_certified ?? 0));
+ const filteredMetrics = useMemo(() => {
+ if (!searchKeyword) {
+ return allowedMetrics ?? [];
+ }
+ return matchSorter(allowedMetrics, searchKeyword, {
+ keys: [
+ {
+ key: 'verbose_name',
+ threshold: rankings.CONTAINS,
+ },
+ {
+ key: 'metric_name',
+ threshold: rankings.CONTAINS,
+ },
+ {
+ key: item =>
+ [item?.description ?? '', item?.expression ?? ''].map(
+ x => x?.replace(/[_\n\s]+/g, ' ') || '',
+ ),
+ threshold: rankings.CONTAINS,
+ maxRanking: rankings.CONTAINS,
+ },
+ ],
+ keepDiacritics: true,
+ baseSort: (a, b) =>
+ Number(b?.item?.is_certified ?? 0) -
+ Number(a?.item?.is_certified ?? 0) ||
+ String(a?.rankedValue ?? '').localeCompare(b?.rankedValue ?? ''),
+ });
+ }, [allowedMetrics, searchKeyword]);
const metricSlice = useMemo(
() =>
showAllMetrics
- ? lists?.metrics
- : lists?.metrics?.slice?.(0, DEFAULT_MAX_METRICS_LENGTH),
- [lists?.metrics, showAllMetrics],
+ ? filteredMetrics
+ : filteredMetrics?.slice?.(0, DEFAULT_MAX_METRICS_LENGTH),
+ [filteredMetrics, showAllMetrics],
);
const columnSlice = useMemo(
() =>
showAllColumns
- ? sortCertifiedFirst(lists?.columns)
+ ? sortCertifiedFirst(filteredColumns)
: sortCertifiedFirst(
- lists?.columns?.slice?.(0, DEFAULT_MAX_COLUMNS_LENGTH),
+ filteredColumns?.slice?.(0, DEFAULT_MAX_COLUMNS_LENGTH),
),
- [lists.columns, showAllColumns],
+ [filteredColumns, showAllColumns],
);
const showInfoboxCheck = () => {
@@ -349,12 +372,26 @@ export default function DataSourcePanel({
allowClear
onChange={evt => {
setInputValue(evt.target.value);
- search(evt.target.value);
}}
value={inputValue}
className="form-control input-md"
placeholder={t('Search Metrics & Columns')}
/>
+ <div
+ css={css`
+ display: flex;
+ margin: 0 16px;
+ column-gap: 4px;
+ `}
+ >
+ <Checkbox
+ id="allowlistOnly"
+ onChange={({ target }) => setAllowlistOnly(Boolean(target.checked))}
+ checked={allowlistOnly}
+ >
+ {t('Show only allowlist')}
+ </Checkbox>
+ </div>
<div className="field-selections">
{datasourceIsSaveable && showInfoboxCheck() && (
<StyledInfoboxWrapper>
@@ -399,7 +436,7 @@ export default function DataSourcePanel({
{t(
`Showing %s of %s`,
metricSlice?.length,
- lists?.metrics.length,
+ filteredMetrics.length,
)}
</div>
{metricSlice?.map?.((m: Metric) => (
@@ -413,7 +450,7 @@ export default function DataSourcePanel({
/>
</LabelContainer>
))}
- {lists?.metrics?.length > DEFAULT_MAX_METRICS_LENGTH ? (
+ {filteredMetrics.length > DEFAULT_MAX_METRICS_LENGTH ? (
<ButtonContainer>
<Button onClick={() => setShowAllMetrics(!showAllMetrics)}>
{showAllMetrics ? t('Show less...') : t('Show all...')}
@@ -432,7 +469,7 @@ export default function DataSourcePanel({
{t(
`Showing %s of %s`,
columnSlice.length,
- lists.columns.length,
+ filteredColumns.length,
)}
</div>
{columnSlice.map(col => (
@@ -446,7 +483,7 @@ export default function DataSourcePanel({
/>
</LabelContainer>
))}
- {lists.columns.length > DEFAULT_MAX_COLUMNS_LENGTH ? (
+ {filteredColumns.length > DEFAULT_MAX_COLUMNS_LENGTH ? (
<ButtonContainer>
<Button onClick={() => setShowAllColumns(!showAllColumns)}>
{showAllColumns ? t('Show Less...') : t('Show all...')}
@@ -464,14 +501,14 @@ export default function DataSourcePanel({
[
columnSlice,
inputValue,
- lists.columns.length,
- lists?.metrics?.length,
+ filteredColumns.length,
+ filteredMetrics.length,
metricSlice,
- search,
showAllColumns,
showAllMetrics,
datasourceIsSaveable,
shouldForceUpdate,
+ allowlistOnly,
],
);
diff --git a/superset-frontend/src/explore/components/ExploreContainer/ExploreContainer.test.tsx b/superset-frontend/src/explore/components/ExploreContainer/ExploreContainer.test.tsx
new file mode 100644
index 0000000000..50922256ea
--- /dev/null
+++ b/superset-frontend/src/explore/components/ExploreContainer/ExploreContainer.test.tsx
@@ -0,0 +1,85 @@
+/**
+ * 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 from 'react';
+import { fireEvent, render } from 'spec/helpers/testing-library';
+import { OptionControlLabel } from 'src/explore/components/controls/OptionControls';
+
+import ExploreContainer, { DraggingContext } from '.';
+import OptionWrapper from '../controls/DndColumnSelectControl/OptionWrapper';
+
+const MockChildren = () => {
+ const dragging = React.useContext(DraggingContext);
+ return (
+ <div data-test="mock-children" className={dragging ? 'dragging' : ''}>
+ {dragging ? 'dragging' : 'not dragging'}
+ </div>
+ );
+};
+
+test('should render children', () => {
+ const { getByTestId, getByText } = render(
+ <ExploreContainer>
+ <MockChildren />
+ </ExploreContainer>,
+ { useRedux: true, useDnd: true },
+ );
+ expect(getByTestId('mock-children')).toBeInTheDocument();
+ expect(getByText('not dragging')).toBeInTheDocument();
+});
+
+test('should update the style on dragging state', () => {
+ const defaultProps = {
+ label: <span>Test label</span>,
+ tooltipTitle: 'This is a tooltip title',
+ onRemove: jest.fn(),
+ onMoveLabel: jest.fn(),
+ onDropLabel: jest.fn(),
+ type: 'test',
+ index: 0,
+ };
+ const { container, getByText } = render(
+ <ExploreContainer>
+ <OptionControlLabel
+ {...defaultProps}
+ index={1}
+ label={<span>Label 1</span>}
+ />
+ <OptionWrapper
+ {...defaultProps}
+ index={2}
+ label="Label 2"
+ clickClose={() => {}}
+ onShiftOptions={() => {}}
+ />
+ <MockChildren />
+ </ExploreContainer>,
+ {
+ useRedux: true,
+ useDnd: true,
+ },
+ );
+ expect(container.getElementsByClassName('dragging')).toHaveLength(0);
+ fireEvent.dragStart(getByText('Label 1'));
+ expect(container.getElementsByClassName('dragging')).toHaveLength(1);
+ fireEvent.dragEnd(getByText('Label 1'));
+ expect(container.getElementsByClassName('dragging')).toHaveLength(0);
+ // don't show dragging state for the sorting item
+ fireEvent.dragStart(getByText('Label 2'));
+ expect(container.getElementsByClassName('dragging')).toHaveLength(0);
+});
diff --git a/superset-frontend/src/explore/components/ExploreContainer/index.tsx b/superset-frontend/src/explore/components/ExploreContainer/index.tsx
new file mode 100644
index 0000000000..6f4bb7a370
--- /dev/null
+++ b/superset-frontend/src/explore/components/ExploreContainer/index.tsx
@@ -0,0 +1,88 @@
+/**
+ * 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, { useEffect, Dispatch, useReducer } from 'react';
+import { styled } from '@superset-ui/core';
+import { useDragDropManager } from 'react-dnd';
+import { DatasourcePanelDndItem } from '../DatasourcePanel/types';
+
+type CanDropValidator = (item: DatasourcePanelDndItem) => boolean;
+type DropzoneSet = Record<string, CanDropValidator>;
+type Action = { key: string; canDrop?: CanDropValidator };
+
+export const DraggingContext = React.createContext(false);
+export const DropzoneContext = React.createContext<
+ [DropzoneSet, Dispatch<Action>]
+>([{}, () => {}]);
+const StyledDiv = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ min-height: 0;
+`;
+
+const reducer = (state: DropzoneSet = {}, action: Action) => {
+ if (action.canDrop) {
+ return {
+ ...state,
+ [action.key]: action.canDrop,
+ };
+ }
+ if (action.key) {
+ const newState = { ...state };
+ delete newState[action.key];
+ return newState;
+ }
+ return state;
+};
+
+const ExploreContainer: React.FC<{}> = ({ children }) => {
+ const dragDropManager = useDragDropManager();
+ const [dragging, setDragging] = React.useState(
+ dragDropManager.getMonitor().isDragging(),
+ );
+
+ useEffect(() => {
+ const monitor = dragDropManager.getMonitor();
+ const unsub = monitor.subscribeToStateChange(() => {
+ const item = monitor.getItem() || {};
+ // don't show dragging state for the sorting item
+ if ('dragIndex' in item) {
+ return;
+ }
+ const isDragging = monitor.isDragging();
+ setDragging(isDragging);
+ });
+
+ return () => {
+ unsub();
+ };
+ }, [dragDropManager]);
+
+ const dropzoneValue = useReducer(reducer, {});
+
+ return (
+ <DropzoneContext.Provider value={dropzoneValue}>
+ <DraggingContext.Provider value={dragging}>
+ <StyledDiv>{children}</StyledDiv>
+ </DraggingContext.Provider>
+ </DropzoneContext.Provider>
+ );
+};
+
+export default ExploreContainer;
diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
index d3a32a2bd6..0da43ebdc0 100644
--- a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
@@ -68,6 +68,7 @@ import ConnectedControlPanelsContainer from '../ControlPanelsContainer';
import SaveModal from '../SaveModal';
import DataSourcePanel from '../DatasourcePanel';
import ConnectedExploreChartHeader from '../ExploreChartHeader';
+import ExploreContainer from '../ExploreContainer';
const propTypes = {
...ExploreChartPanel.propTypes,
@@ -90,13 +91,6 @@ const propTypes = {
isSaveModalVisible: PropTypes.bool,
};
-const ExploreContainer = styled.div`
- display: flex;
- flex-direction: column;
- height: 100%;
- min-height: 0;
-`;
-
const ExplorePanelContainer = styled.div`
${({ theme }) => css`
background: ${theme.colors.grayscale.light5};
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx
index 397d6f54e0..3eb6daa69b 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { ReactNode, useMemo } from 'react';
+import React, { ReactNode, useContext, useEffect, useMemo } from 'react';
import { useDrop } from 'react-dnd';
import { t, useTheme } from '@superset-ui/core';
import ControlHeader from 'src/explore/components/ControlHeader';
@@ -31,6 +31,7 @@ import {
} from 'src/explore/components/DatasourcePanel/types';
import Icons from 'src/components/Icons';
import { DndItemType } from '../../DndItemType';
+import { DraggingContext, DropzoneContext } from '../../ExploreContainer';
export type DndSelectLabelProps = {
name: string;
@@ -71,6 +72,17 @@ export default function DndSelectLabel({
}),
});
+ const dispatch = useContext(DropzoneContext)[1];
+ console.log('name', props.name);
+ useEffect(() => {
+ dispatch({ key: props.name, canDrop: props.canDrop });
+ return () => {
+ dispatch({ key: props.name });
+ };
+ }, [dispatch, props.name, props.canDrop]);
+
+ const isDragging = useContext(DraggingContext);
+
const values = useMemo(() => valuesRenderer(), [valuesRenderer]);
function renderGhostButton() {
@@ -94,6 +106,7 @@ export default function DndSelectLabel({
data-test="dnd-labels-container"
canDrop={canDrop}
isOver={isOver}
+ isDragging={isDragging}
>
{values}
{displayGhostButton && renderGhostButton()}
diff --git a/superset-frontend/src/explore/components/controls/OptionControls/index.tsx b/superset-frontend/src/explore/components/controls/OptionControls/index.tsx
index 90ae73c637..dd484f3296 100644
--- a/superset-frontend/src/explore/components/controls/OptionControls/index.tsx
+++ b/superset-frontend/src/explore/components/controls/OptionControls/index.tsx
@@ -106,18 +106,46 @@ export const LabelsContainer = styled.div`
export const DndLabelsContainer = styled.div<{
canDrop?: boolean;
isOver?: boolean;
+ isDragging?: boolean;
}>`
+ position: relative;
padding: ${({ theme }) => theme.gridUnit}px;
- border: ${({ canDrop, isOver, theme }) => {
- if (canDrop) {
- return `dashed 1px ${theme.colors.info.dark1}`;
- }
- if (isOver && !canDrop) {
- return `dashed 1px ${theme.colors.error.dark1}`;
+ border: ${({ canDrop, isDragging, theme }) => {
+ if (isDragging) {
+ return `dashed 1px ${
+ canDrop ? theme.colors.info.dark1 : theme.colors.error.dark1
+ }`;
}
return `solid 1px ${theme.colors.grayscale.light2}`;
}};
border-radius: ${({ theme }) => theme.gridUnit}px;
+ &:before,
+ &:after {
+ content: ' ';
+ position: absolute;
+ border-radius: ${({ theme }) => theme.gridUnit}px;
+ }
+ &:before {
+ display: ${({ isDragging }) => (isDragging ? 'block' : 'none')};
+ background-color: ${({ theme, canDrop }) =>
+ canDrop ? theme.colors.primary.base : theme.colors.error.light1};
+ z-index: ${({ theme }) => theme.zIndex.aboveDashboardCharts};
+ opacity: ${({ theme }) => theme.opacity.light};
+ top: 1px;
+ right: 1px;
+ bottom: 1px;
+ left: 1px;
+ }
+ &:after {
+ display: ${({ isOver, canDrop }) => (canDrop && isOver ? 'block' : 'none')};
+ background-color: ${({ theme }) => theme.colors.primary.base};
+ z-index: ${({ theme }) => theme.zIndex.dropdown};
+ opacity: ${({ theme }) => theme.opacity.mediumLight};
+ top: ${({ theme }) => -theme.gridUnit}px;
+ right: ${({ theme }) => -theme.gridUnit}px;
+ bottom: ${({ theme }) => -theme.gridUnit}px;
+ left: ${({ theme }) => -theme.gridUnit}px;
+ }
`;
export const AddControlLabel = styled.div<{
diff --git a/superset-frontend/src/pages/Chart/Chart.test.tsx b/superset-frontend/src/pages/Chart/Chart.test.tsx
index 674a33d44d..f8fb9fdcbb 100644
--- a/superset-frontend/src/pages/Chart/Chart.test.tsx
+++ b/superset-frontend/src/pages/Chart/Chart.test.tsx
@@ -65,6 +65,7 @@ describe('ChartPage', () => {
const { getByTestId } = render(<ChartPage />, {
useRouter: true,
useRedux: true,
+ useDnd: true,
});
await waitFor(() =>
expect(fetchMock.calls(exploreApiRoute).length).toBe(1),
@@ -110,6 +111,7 @@ describe('ChartPage', () => {
const { getByTestId } = render(<ChartPage />, {
useRouter: true,
useRedux: true,
+ useDnd: true,
});
await waitFor(() =>
expect(fetchMock.calls(exploreApiRoute).length).toBe(1),
@@ -156,6 +158,7 @@ describe('ChartPage', () => {
{
useRouter: true,
useRedux: true,
+ useDnd: true,
},
);
await waitFor(() =>