mirror of https://github.com/apache/superset.git
fix(ui): Disable ability to export data when user does not have the correct permission (#28429)
This commit is contained in:
parent
313ee596f5
commit
70f6f5f3ef
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 || {})}
|
||||
|
|
|
@ -74,6 +74,7 @@ const renderMenu = ({
|
|||
<DrillByMenuItems
|
||||
formData={formData ?? defaultFormData}
|
||||
drillByConfig={drillByConfig}
|
||||
canDownload
|
||||
open
|
||||
{...rest}
|
||||
/>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -86,6 +86,7 @@ const renderModal = async (
|
|||
onHideModal={() => setShowModal(false)}
|
||||
dataset={dataset}
|
||||
drillByConfig={{ groupbyFieldName: 'groupby', filters: [] }}
|
||||
canDownload
|
||||
{...modalProps}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 = () =>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -809,6 +809,7 @@ const SliceHeaderControls = (props: SliceHeaderControlsPropsWithRouter) => {
|
|||
dataSize={20}
|
||||
isRequest
|
||||
isVisible
|
||||
canDownload={!!props.supersetCanCSV}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue