You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2024/06/20 15:26:57 UTC
(superset) branch master updated: fix(ui): Disable ability to export data when user does not have the correct permission (#28429)
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 70f6f5f3ef fix(ui): Disable ability to export data when user does not have the correct permission (#28429)
70f6f5f3ef is described below
commit 70f6f5f3efc20d3ac7f573dad69eeacc2a125317
Author: Ed Jannoo <16...@users.noreply.github.com>
AuthorDate: Thu Jun 20 16:26:51 2024 +0100
fix(ui): Disable ability to export data when user does not have the correct permission (#28429)
---
.../SqlLab/components/ResultSet/ResultSet.test.tsx | 78 ++++++++++++++++++++++
.../src/SqlLab/components/ResultSet/index.tsx | 41 ++++++++----
.../Chart/ChartContextMenu/ChartContextMenu.tsx | 4 ++
.../Chart/DrillBy/DrillByMenuItems.test.tsx | 1 +
.../components/Chart/DrillBy/DrillByMenuItems.tsx | 3 +
.../components/Chart/DrillBy/DrillByModal.test.tsx | 1 +
.../src/components/Chart/DrillBy/DrillByModal.tsx | 3 +
.../Chart/DrillBy/useResultsTableView.test.ts | 4 +-
.../Chart/DrillBy/useResultsTableView.tsx | 3 +
.../components/SliceHeaderControls/index.tsx | 1 +
.../components/DataTablesPane/DataTablesPane.tsx | 3 +
.../components/DataTableControls.tsx | 5 +-
.../components/ResultsPaneOnDashboard.tsx | 2 +
.../DataTablesPane/components/SamplesPane.tsx | 3 +
.../components/SingleQueryResultPane.tsx | 2 +
.../DataTablesPane/components/useResultsPane.tsx | 3 +
.../DataTablesPane/test/DataTablesPane.test.tsx | 28 ++++++++
.../components/DataTablesPane/test/fixture.tsx | 3 +
.../src/explore/components/DataTablesPane/types.ts | 5 ++
.../ExploreChartHeader/ExploreChartHeader.test.tsx | 58 +++++++++++++++-
.../explore/components/ExploreChartPanel/index.jsx | 2 +
.../useExploreAdditionalActionsMenu/index.jsx | 30 +++++----
22 files changed, 253 insertions(+), 30 deletions(-)
diff --git a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
index d5f30a575a..9c04fee8e7 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
@@ -446,4 +446,82 @@ describe('ResultSet', () => {
}),
).toBe(null);
});
+
+ test('should allow download as CSV when user has permission to export data', async () => {
+ const { queryByTestId } = setup(
+ mockedProps,
+ mockStore({
+ ...initialState,
+ user: {
+ ...user,
+ roles: {
+ sql_lab: [['can_export_csv', 'SQLLab']],
+ },
+ },
+ sqlLab: {
+ ...initialState.sqlLab,
+ queries: {
+ [queries[0].id]: queries[0],
+ },
+ },
+ }),
+ );
+ expect(queryByTestId('export-csv-button')).toBeInTheDocument();
+ });
+
+ test('should not allow download as CSV when user does not have permission to export data', async () => {
+ const { queryByTestId } = setup(
+ mockedProps,
+ mockStore({
+ ...initialState,
+ user,
+ sqlLab: {
+ ...initialState.sqlLab,
+ queries: {
+ [queries[0].id]: queries[0],
+ },
+ },
+ }),
+ );
+ expect(queryByTestId('export-csv-button')).not.toBeInTheDocument();
+ });
+
+ test('should allow copy to clipboard when user has permission to export data', async () => {
+ const { queryByTestId } = setup(
+ mockedProps,
+ mockStore({
+ ...initialState,
+ user: {
+ ...user,
+ roles: {
+ sql_lab: [['can_export_csv', 'SQLLab']],
+ },
+ },
+ sqlLab: {
+ ...initialState.sqlLab,
+ queries: {
+ [queries[0].id]: queries[0],
+ },
+ },
+ }),
+ );
+ expect(queryByTestId('copy-to-clipboard-button')).toBeInTheDocument();
+ });
+
+ test('should not allow copy to clipboard when user does not have permission to export data', async () => {
+ const { queryByTestId } = setup(
+ mockedProps,
+ mockStore({
+ ...initialState,
+ user,
+ sqlLab: {
+ ...initialState.sqlLab,
+ queries: {
+ [queries[0].id]: queries[0],
+ },
+ },
+ }),
+ );
+ expect(queryByTestId('copy-to-clipboard-button')).not.toBeInTheDocument();
+ });
});
diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
index ac41d19b82..6e62c17da5 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
@@ -78,6 +78,7 @@ import {
LOG_ACTIONS_SQLLAB_DOWNLOAD_CSV,
} from 'src/logger/LogUtils';
import Icons from 'src/components/Icons';
+import { findPermission } from 'src/utils/findPermission';
import ExploreCtasResultsButton from '../ExploreCtasResultsButton';
import ExploreResultsButton from '../ExploreResultsButton';
import HighlightedSql from '../HighlightedSql';
@@ -309,6 +310,12 @@ const ResultSet = ({
schema: query?.schema,
};
+ const canExportData = findPermission(
+ 'can_export_csv',
+ 'SQLLab',
+ user?.roles,
+ );
+
return (
<ResultSetControls>
<SaveDatasetModal
@@ -328,29 +335,35 @@ const ResultSet = ({
onClick={createExploreResultsOnClick}
/>
)}
- {csv && (
+ {csv && canExportData && (
<Button
buttonSize="small"
href={getExportCsvUrl(query.id)}
+ data-test="export-csv-button"
onClick={() => logAction(LOG_ACTIONS_SQLLAB_DOWNLOAD_CSV, {})}
>
<i className="fa fa-file-text-o" /> {t('Download to CSV')}
</Button>
)}
- <CopyToClipboard
- text={prepareCopyToClipboardTabularData(data, columns)}
- wrapped={false}
- copyNode={
- <Button buttonSize="small">
- <i className="fa fa-clipboard" /> {t('Copy to Clipboard')}
- </Button>
- }
- hideTooltip
- onCopyEnd={() =>
- logAction(LOG_ACTIONS_SQLLAB_COPY_RESULT_TO_CLIPBOARD, {})
- }
- />
+ {canExportData && (
+ <CopyToClipboard
+ text={prepareCopyToClipboardTabularData(data, columns)}
+ wrapped={false}
+ copyNode={
+ <Button
+ buttonSize="small"
+ data-test="copy-to-clipboard-button"
+ >
+ <i className="fa fa-clipboard" /> {t('Copy to Clipboard')}
+ </Button>
+ }
+ hideTooltip
+ onCopyEnd={() =>
+ logAction(LOG_ACTIONS_SQLLAB_COPY_RESULT_TO_CLIPBOARD, {})
+ }
+ />
+ )}
</ResultSetButtons>
{search && (
<input
diff --git a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx
index bf1ea84ff1..9df0e73bc9 100644
--- a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx
+++ b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx
@@ -97,6 +97,9 @@ const ChartContextMenu = (
const canDatasourceSamples = useSelector((state: RootState) =>
findPermission('can_samples', 'Datasource', state.user?.roles),
);
+ const canDownload = useSelector((state: RootState) =>
+ findPermission('can_csv', 'Superset', state.user?.roles),
+ );
const canDrill = useSelector((state: RootState) =>
findPermission('can_drill', 'Dashboard', state.user?.roles),
);
@@ -256,6 +259,7 @@ const ChartContextMenu = (
formData={formData}
contextMenuY={clientY}
submenuIndex={submenuIndex}
+ canDownload={canDownload}
open={openKeys.includes('drill-by-submenu')}
key="drill-by-submenu"
{...(additionalConfig?.drillBy || {})}
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx
index 8b65d21409..2e60c08586 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx
@@ -74,6 +74,7 @@ const renderMenu = ({
<DrillByMenuItems
formData={formData ?? defaultFormData}
drillByConfig={drillByConfig}
+ canDownload
open
{...rest}
/>
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
index 6df180cb78..f694db3d27 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
@@ -74,6 +74,7 @@ export interface DrillByMenuItemsProps {
onClick?: (event: MouseEvent) => void;
openNewModal?: boolean;
excludedColumns?: Column[];
+ canDownload: boolean;
open: boolean;
}
@@ -105,6 +106,7 @@ export const DrillByMenuItems = ({
onClick = () => {},
excludedColumns,
openNewModal = true,
+ canDownload,
open,
...rest
}: DrillByMenuItemsProps) => {
@@ -344,6 +346,7 @@ export const DrillByMenuItems = ({
formData={formData}
onHideModal={closeModal}
dataset={{ ...dataset!, verbose_map: verboseMap }}
+ canDownload={canDownload}
/>
)}
</>
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx
index d546c071cb..2299f091b6 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx
@@ -86,6 +86,7 @@ const renderModal = async (
onHideModal={() => setShowModal(false)}
dataset={dataset}
drillByConfig={{ groupbyFieldName: 'groupby', filters: [] }}
+ canDownload
{...modalProps}
/>
)}
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
index 0cf6b1f016..2a9262e19e 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
@@ -151,6 +151,7 @@ export interface DrillByModalProps {
drillByConfig: Required<ContextMenuFilters>['drillBy'];
formData: BaseFormData & { [key: string]: any };
onHideModal: () => void;
+ canDownload: boolean;
}
type DrillByConfigs = (ContextMenuFilters['drillBy'] & { column?: Column })[];
@@ -161,6 +162,7 @@ export default function DrillByModal({
drillByConfig,
formData,
onHideModal,
+ canDownload,
}: DrillByModalProps) {
const dispatch = useDispatch();
const theme = useTheme();
@@ -200,6 +202,7 @@ export default function DrillByModal({
const resultsTable = useResultsTableView(
chartDataResult,
formData.datasource,
+ canDownload,
);
const [currentFormData, setCurrentFormData] = useState(formData);
diff --git a/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.test.ts b/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.test.ts
index 36cc51a684..7e64d3a42f 100644
--- a/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.test.ts
+++ b/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.test.ts
@@ -65,7 +65,7 @@ const MOCK_CHART_DATA_RESULT = [
test('Displays results table for 1 query', () => {
const { result } = renderHook(() =>
- useResultsTableView(MOCK_CHART_DATA_RESULT.slice(0, 1), '1__table'),
+ useResultsTableView(MOCK_CHART_DATA_RESULT.slice(0, 1), '1__table', true),
);
render(result.current, { useRedux: true });
expect(screen.queryByRole('tablist')).not.toBeInTheDocument();
@@ -76,7 +76,7 @@ test('Displays results table for 1 query', () => {
test('Displays results for 2 queries', async () => {
const { result } = renderHook(() =>
- useResultsTableView(MOCK_CHART_DATA_RESULT, '1__table'),
+ useResultsTableView(MOCK_CHART_DATA_RESULT, '1__table', true),
);
render(result.current, { useRedux: true });
const getActiveTabElement = () =>
diff --git a/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx b/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx
index 8b7fd684b0..4d85f949b2 100644
--- a/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx
@@ -33,6 +33,7 @@ const PaginationContainer = styled.div`
export const useResultsTableView = (
chartDataResult: QueryData[] | undefined,
datasourceId: string,
+ canDownload: boolean,
) => {
if (!isDefined(chartDataResult)) {
return <div />;
@@ -48,6 +49,7 @@ export const useResultsTableView = (
dataSize={DATA_SIZE}
datasourceId={datasourceId}
isVisible
+ canDownload={canDownload}
/>
</PaginationContainer>
);
@@ -65,6 +67,7 @@ export const useResultsTableView = (
dataSize={DATA_SIZE}
datasourceId={datasourceId}
isVisible
+ canDownload={canDownload}
/>
</PaginationContainer>
</Tabs.TabPane>
diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
index 756f2ed934..632428f32f 100644
--- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
@@ -809,6 +809,7 @@ const SliceHeaderControls = (props: SliceHeaderControlsPropsWithRouter) => {
dataSize={20}
isRequest
isVisible
+ canDownload={!!props.supersetCanCSV}
/>
}
/>
diff --git a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx
index 8601c41a66..640834ba94 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx
@@ -88,6 +88,7 @@ export const DataTablesPane = ({
ownState,
errorMessage,
actions,
+ canDownload,
}: DataTablesPaneProps) => {
const theme = useTheme();
const [activeTabKey, setActiveTabKey] = useState<string>(ResultTypes.Results);
@@ -198,6 +199,7 @@ export const DataTablesPane = ({
isRequest: isRequest.results,
actions,
isVisible: ResultTypes.Results === activeTabKey,
+ canDownload,
}).map((pane, idx) => {
if (idx === 0) {
return (
@@ -235,6 +237,7 @@ export const DataTablesPane = ({
isRequest={isRequest.samples}
actions={actions}
isVisible={ResultTypes.Samples === activeTabKey}
+ canDownload={canDownload}
/>
</Tabs.TabPane>
</Tabs>
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx
index ff134da1f7..30dfb87c74 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx
@@ -49,6 +49,7 @@ export const TableControls = ({
columnTypes,
rowcount,
isLoading,
+ canDownload,
}: TableControlsProps) => {
const originalTimeColumns = getTimeColumns(datasourceId);
const formattedTimeColumns = zip<string, GenericDataType>(
@@ -76,7 +77,9 @@ export const TableControls = ({
`}
>
<RowCountLabel rowcount={rowcount} loading={isLoading} />
- <CopyToClipboardButton data={formattedData} columns={columnNames} />
+ {canDownload && (
+ <CopyToClipboardButton data={formattedData} columns={columnNames} />
+ )}
</div>
</TableControlsWrapper>
);
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx
index 410d55cbfd..b6691f87af 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx
@@ -53,6 +53,7 @@ export const ResultsPaneOnDashboard = ({
actions,
isVisible,
dataSize = 50,
+ canDownload,
}: ResultsPaneProps) => {
const resultsPanes = useResultsPane({
errorMessage,
@@ -63,6 +64,7 @@ export const ResultsPaneOnDashboard = ({
actions,
dataSize,
isVisible,
+ canDownload,
});
if (resultsPanes.length === 1) {
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx
index dd9f430a33..ca0a1d6af4 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx
@@ -42,6 +42,7 @@ export const SamplesPane = ({
actions,
dataSize = 50,
isVisible,
+ canDownload,
}: SamplesPaneProps) => {
const [filterText, setFilterText] = useState('');
const [data, setData] = useState<Record<string, any>[][]>([]);
@@ -114,6 +115,7 @@ export const SamplesPane = ({
datasourceId={datasourceId}
onInputChange={input => setFilterText(input)}
isLoading={isLoading}
+ canDownload={canDownload}
/>
<Error>{responseError}</Error>
</>
@@ -135,6 +137,7 @@ export const SamplesPane = ({
datasourceId={datasourceId}
onInputChange={input => setFilterText(input)}
isLoading={isLoading}
+ canDownload={canDownload}
/>
<TableView
columns={columns}
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx
index a990703586..9a3dd4eded 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx
@@ -34,6 +34,7 @@ export const SingleQueryResultPane = ({
datasourceId,
dataSize = 50,
isVisible,
+ canDownload,
}: SingleQueryResultPaneProp) => {
const [filterText, setFilterText] = useState('');
@@ -60,6 +61,7 @@ export const SingleQueryResultPane = ({
datasourceId={datasourceId}
onInputChange={input => setFilterText(input)}
isLoading={false}
+ canDownload={canDownload}
/>
<TableView
columns={columns}
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx
index 4b39062208..fe262fe720 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx
@@ -47,6 +47,7 @@ export const useResultsPane = ({
actions,
isVisible,
dataSize = 50,
+ canDownload,
}: ResultsPaneProps): ReactElement[] => {
const metadata = getChartMetadataRegistry().get(
queryFormData?.viz_type || queryFormData?.vizType,
@@ -124,6 +125,7 @@ export const useResultsPane = ({
datasourceId={queryFormData.datasource}
onInputChange={() => {}}
isLoading={false}
+ canDownload={canDownload}
/>
<Error>{responseError}</Error>
</>
@@ -149,6 +151,7 @@ export const useResultsPane = ({
datasourceId={queryFormData.datasource}
key={idx}
isVisible={isVisible}
+ canDownload={canDownload}
/>
));
};
diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx
index fb5a20a0d6..4dcb5c7000 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx
@@ -114,6 +114,34 @@ describe('DataTablesPane', () => {
fetchMock.restore();
});
+ test('Should not allow copy data table content when canDownload=false', async () => {
+ fetchMock.post(
+ 'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A456%7D',
+ {
+ result: [
+ {
+ data: [{ __timestamp: 1230768000000, genre: 'Action' }],
+ colnames: ['__timestamp', 'genre'],
+ coltypes: [2, 1],
+ rowcount: 1,
+ sql_rowcount: 1,
+ },
+ ],
+ },
+ );
+ const props = {
+ ...createDataTablesPaneProps(456),
+ canDownload: false,
+ };
+ render(<DataTablesPane {...props} />, {
+ useRedux: true,
+ });
+ userEvent.click(screen.getByText('Results'));
+ expect(await screen.findByText('1 row')).toBeVisible();
+ expect(screen.queryByLabelText('Copy')).not.toBeInTheDocument();
+ fetchMock.restore();
+ });
+
test('Search table', async () => {
fetchMock.post(
'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A789%7D',
diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx
index ee766acd6a..c09419e7d0 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx
@@ -73,6 +73,7 @@ export const createDataTablesPaneProps = (sliceId: number) =>
chartStatus: 'rendered' as ChartStatus,
onCollapseChange: jest.fn(),
actions: exploreActions,
+ canDownload: true,
}) as DataTablesPaneProps;
export const createSamplesPaneProps = ({
@@ -90,6 +91,7 @@ export const createSamplesPaneProps = ({
queryForce,
isVisible: true,
actions: exploreActions,
+ canDownload: true,
}) as SamplesPaneProps;
export const createResultsPaneOnDashboardProps = ({
@@ -116,4 +118,5 @@ export const createResultsPaneOnDashboardProps = ({
isVisible: true,
actions: exploreActions,
errorMessage,
+ canDownload: true,
}) as ResultsPaneProps;
diff --git a/superset-frontend/src/explore/components/DataTablesPane/types.ts b/superset-frontend/src/explore/components/DataTablesPane/types.ts
index 080388e256..5e62e21fa3 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/types.ts
+++ b/superset-frontend/src/explore/components/DataTablesPane/types.ts
@@ -40,6 +40,7 @@ export interface DataTablesPaneProps {
onCollapseChange: (isOpen: boolean) => void;
errorMessage?: JSX.Element;
actions: ExploreActions;
+ canDownload: boolean;
}
export interface ResultsPaneProps {
@@ -52,6 +53,7 @@ export interface ResultsPaneProps {
dataSize?: number;
// reload OriginalFormattedTimeColumns from localStorage when isVisible is true
isVisible: boolean;
+ canDownload: boolean;
}
export interface SamplesPaneProps {
@@ -62,6 +64,7 @@ export interface SamplesPaneProps {
dataSize?: number;
// reload OriginalFormattedTimeColumns from localStorage when isVisible is true
isVisible: boolean;
+ canDownload: boolean;
}
export interface TableControlsProps {
@@ -73,6 +76,7 @@ export interface TableControlsProps {
columnTypes: GenericDataType[];
isLoading: boolean;
rowcount: number;
+ canDownload: boolean;
}
export interface QueryResultInterface {
@@ -88,4 +92,5 @@ export interface SingleQueryResultPaneProp extends QueryResultInterface {
dataSize?: number;
// reload OriginalFormattedTimeColumns from localStorage when isVisible is true
isVisible: boolean;
+ canDownload: boolean;
}
diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx b/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx
index fc28b4e8f6..f064643a22 100644
--- a/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx
+++ b/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx
@@ -394,11 +394,25 @@ describe('Additional actions tests', () => {
spyExportChart.restore();
});
- test('Should export to JSON', async () => {
+ test('Should not export to JSON if canDownload=false', async () => {
const props = createProps();
render(<ExploreHeader {...props} />, {
useRedux: true,
});
+ userEvent.click(screen.getByLabelText('Menu actions trigger'));
+ userEvent.hover(screen.getByText('Download'));
+ const exportJsonElement = await screen.findByText('Export to .JSON');
+ userEvent.click(exportJsonElement);
+ expect(spyExportChart.callCount).toBe(0);
+ spyExportChart.restore();
+ });
+
+ test('Should export to JSON if canDownload=true', async () => {
+ const props = createProps();
+ props.canDownload = true;
+ render(<ExploreHeader {...props} />, {
+ useRedux: true,
+ });
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Download'));
@@ -407,6 +421,22 @@ describe('Additional actions tests', () => {
expect(spyExportChart.callCount).toBe(1);
});
+ test('Should not export to pivoted CSV if canDownloadCSV=false and viz_type=pivot_table_v2', async () => {
+ const props = createProps();
+ props.chart.latestQueryFormData.viz_type = 'pivot_table_v2';
+ render(<ExploreHeader {...props} />, {
+ useRedux: true,
+ });
+
+ userEvent.click(screen.getByLabelText('Menu actions trigger'));
+ userEvent.hover(screen.getByText('Download'));
+ const exportCSVElement = await screen.findByText(
+ 'Export to pivoted .CSV',
+ );
+ userEvent.click(exportCSVElement);
+ expect(spyExportChart.callCount).toBe(0);
+ });
+
test('Should export to pivoted CSV if canDownloadCSV=true and viz_type=pivot_table_v2', async () => {
const props = createProps();
props.canDownload = true;
@@ -423,5 +453,31 @@ describe('Additional actions tests', () => {
userEvent.click(exportCSVElement);
expect(spyExportChart.callCount).toBe(1);
});
+
+ test('Should not export to Excel if canDownload=false', async () => {
+ const props = createProps();
+ render(<ExploreHeader {...props} />, {
+ useRedux: true,
+ });
+ userEvent.click(screen.getByLabelText('Menu actions trigger'));
+ userEvent.hover(screen.getByText('Download'));
+ const exportExcelElement = await screen.findByText('Export to Excel');
+ userEvent.click(exportExcelElement);
+ expect(spyExportChart.callCount).toBe(0);
+ spyExportChart.restore();
+ });
+
+ test('Should export to Excel if canDownload=true', async () => {
+ const props = createProps();
+ props.canDownload = true;
+ render(<ExploreHeader {...props} />, {
+ useRedux: true,
+ });
+ userEvent.click(screen.getByLabelText('Menu actions trigger'));
+ userEvent.hover(screen.getByText('Download'));
+ const exportExcelElement = await screen.findByText('Export to Excel');
+ userEvent.click(exportExcelElement);
+ expect(spyExportChart.callCount).toBe(1);
+ });
});
});
diff --git a/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx b/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx
index bf95495c61..189177019c 100644
--- a/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx
@@ -137,6 +137,7 @@ const ExploreChartPanel = ({
standalone,
chartIsStale,
chartAlert,
+ can_download: canDownload,
}) => {
const theme = useTheme();
const gutterMargin = theme.gridUnit * GUTTER_SIZE_FACTOR;
@@ -449,6 +450,7 @@ const ExploreChartPanel = ({
chartStatus={chart.chartStatus}
errorMessage={errorMessage}
actions={actions}
+ canDownload={canDownload}
/>
</Split>
{showDatasetModal && (
diff --git a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
index a40d0f5e30..448836254c 100644
--- a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
+++ b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
@@ -173,22 +173,26 @@ export const useExploreAdditionalActionsMenu = (
const exportJson = useCallback(
() =>
- exportChart({
- formData: latestQueryFormData,
- resultType: 'results',
- resultFormat: 'json',
- }),
- [latestQueryFormData],
+ canDownloadCSV
+ ? exportChart({
+ formData: latestQueryFormData,
+ resultType: 'results',
+ resultFormat: 'json',
+ })
+ : null,
+ [canDownloadCSV, latestQueryFormData],
);
const exportExcel = useCallback(
() =>
- exportChart({
- formData: latestQueryFormData,
- resultType: 'results',
- resultFormat: 'xlsx',
- }),
- [latestQueryFormData],
+ canDownloadCSV
+ ? exportChart({
+ formData: latestQueryFormData,
+ resultType: 'results',
+ resultFormat: 'xlsx',
+ })
+ : null,
+ [canDownloadCSV, latestQueryFormData],
);
const copyLink = useCallback(async () => {
@@ -350,6 +354,7 @@ export const useExploreAdditionalActionsMenu = (
<Menu.Item
key={MENU_KEYS.EXPORT_TO_JSON}
icon={<Icons.FileOutlined css={iconReset} />}
+ disabled={!canDownloadCSV}
>
{t('Export to .JSON')}
</Menu.Item>
@@ -362,6 +367,7 @@ export const useExploreAdditionalActionsMenu = (
<Menu.Item
key={MENU_KEYS.EXPORT_TO_XLSX}
icon={<Icons.FileOutlined css={iconReset} />}
+ disabled={!canDownloadCSV}
>
{t('Export to Excel')}
</Menu.Item>