mirror of
https://github.com/apache/superset.git
synced 2026-05-12 19:35:17 +00:00
fix(export): replace iframe with fetch to avoid CSP frame-src violations (#35584)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
e437ae1f2f
commit
3dcf85caef
@@ -25,8 +25,19 @@ import {
|
||||
import { VizType } from '@superset-ui/core';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import handleResourceExport from 'src/utils/export';
|
||||
import ChartTable from './ChartTable';
|
||||
|
||||
// Mock the export module
|
||||
jest.mock('src/utils/export', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockExport = handleResourceExport as jest.MockedFunction<
|
||||
typeof handleResourceExport
|
||||
>;
|
||||
|
||||
const chartsEndpoint = 'glob:*/api/v1/chart/?*';
|
||||
const chartsInfoEndpoint = 'glob:*/api/v1/chart/_info*';
|
||||
const chartFavoriteStatusEndpoint = 'glob:*/api/v1/chart/favorite_status*';
|
||||
@@ -47,7 +58,7 @@ fetchMock.get(chartsEndpoint, {
|
||||
});
|
||||
|
||||
fetchMock.get(chartsInfoEndpoint, {
|
||||
permissions: ['can_add', 'can_edit', 'can_delete'],
|
||||
permissions: ['can_add', 'can_edit', 'can_delete', 'can_export'],
|
||||
});
|
||||
|
||||
fetchMock.get(chartFavoriteStatusEndpoint, {
|
||||
@@ -115,3 +126,53 @@ test('renders mine tab on click', async () => {
|
||||
expect(screen.getAllByText(/cool chart/i)).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
test('handles chart export with correct ID and shows spinner', async () => {
|
||||
// Mock export to take some time before calling the done callback
|
||||
mockExport.mockImplementation(
|
||||
(resource: string, ids: number[], done: () => void) =>
|
||||
new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
done();
|
||||
resolve();
|
||||
}, 100);
|
||||
}),
|
||||
);
|
||||
|
||||
await renderChartTable(mineTabProps);
|
||||
|
||||
// Click Mine tab to see charts
|
||||
userEvent.click(screen.getByText(/mine/i));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText(/cool chart/i)).toHaveLength(3);
|
||||
});
|
||||
|
||||
// Find and click the more options button for the first chart
|
||||
const moreButtons = screen.getAllByRole('img', { name: /more/i });
|
||||
await userEvent.click(moreButtons[0]);
|
||||
|
||||
// Wait for dropdown menu
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Export')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const exportOption = screen.getByText('Export');
|
||||
await userEvent.click(exportOption);
|
||||
|
||||
// Verify spinner appears during export
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify the export was called with correct parameters
|
||||
expect(mockExport).toHaveBeenCalledWith('chart', [0], expect.any(Function));
|
||||
|
||||
// Wait for export to complete and spinner to disappear
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -132,12 +132,17 @@ function ChartTable({
|
||||
setLoaded(true);
|
||||
}, [activeTab]);
|
||||
|
||||
const handleBulkChartExport = (chartsToExport: Chart[]) => {
|
||||
const handleBulkChartExport = async (chartsToExport: Chart[]) => {
|
||||
const ids = chartsToExport.map(({ id }) => id);
|
||||
handleResourceExport('chart', ids, () => {
|
||||
setPreparingExport(false);
|
||||
});
|
||||
setPreparingExport(true);
|
||||
try {
|
||||
await handleResourceExport('chart', ids, () => {
|
||||
setPreparingExport(false);
|
||||
});
|
||||
} catch (error) {
|
||||
setPreparingExport(false);
|
||||
addDangerToast(t('There was an issue exporting the selected charts'));
|
||||
}
|
||||
};
|
||||
|
||||
const menuTabs = [
|
||||
|
||||
@@ -28,8 +28,19 @@ import { Router } from 'react-router-dom';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import * as hooks from 'src/views/CRUD/hooks';
|
||||
import handleResourceExport from 'src/utils/export';
|
||||
import DashboardTable from './DashboardTable';
|
||||
|
||||
// Mock the export module
|
||||
jest.mock('src/utils/export', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockExport = handleResourceExport as jest.MockedFunction<
|
||||
typeof handleResourceExport
|
||||
>;
|
||||
|
||||
jest.mock('src/views/CRUD/utils', () => ({
|
||||
...jest.requireActual('src/views/CRUD/utils'),
|
||||
handleDashboardDelete: jest
|
||||
@@ -254,12 +265,38 @@ describe('DashboardTable', () => {
|
||||
expect(otherTab).toHaveClass('active');
|
||||
});
|
||||
|
||||
test('handles bulk dashboard export', async () => {
|
||||
test('handles bulk dashboard export with correct ID and shows spinner', async () => {
|
||||
// Mock export to take some time before calling the done callback
|
||||
mockExport.mockImplementation(
|
||||
(resource: string, ids: number[], done: () => void) =>
|
||||
new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
done();
|
||||
resolve();
|
||||
}, 100);
|
||||
}),
|
||||
);
|
||||
|
||||
const props = {
|
||||
...defaultProps,
|
||||
mine: mockDashboards,
|
||||
};
|
||||
|
||||
jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({
|
||||
state: {
|
||||
loading: false,
|
||||
resourceCollection: mockDashboards,
|
||||
resourceCount: mockDashboards.length,
|
||||
bulkSelectEnabled: false,
|
||||
lastFetched: new Date().toISOString(),
|
||||
},
|
||||
setResourceCollection: jest.fn(),
|
||||
hasPerm: jest.fn().mockReturnValue(true),
|
||||
refreshData: jest.fn(),
|
||||
fetchData: jest.fn(),
|
||||
toggleBulkSelect: jest.fn(),
|
||||
}));
|
||||
|
||||
render(
|
||||
<Router history={history}>
|
||||
<DashboardTable {...props} />
|
||||
@@ -280,7 +317,25 @@ describe('DashboardTable', () => {
|
||||
const exportOption = screen.getByText('Export');
|
||||
await userEvent.click(exportOption);
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
// Verify spinner shows up during export
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify the export was called with correct parameters
|
||||
expect(mockExport).toHaveBeenCalledWith(
|
||||
'dashboard',
|
||||
[1],
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
// Wait for export to complete and spinner to disappear
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
});
|
||||
|
||||
test('handles dashboard deletion confirmation', async () => {
|
||||
|
||||
@@ -118,12 +118,17 @@ function DashboardTable({
|
||||
setLoaded(true);
|
||||
}, [activeTab]);
|
||||
|
||||
const handleBulkDashboardExport = (dashboardsToExport: Dashboard[]) => {
|
||||
const handleBulkDashboardExport = async (dashboardsToExport: Dashboard[]) => {
|
||||
const ids = dashboardsToExport.map(({ id }) => id);
|
||||
handleResourceExport('dashboard', ids, () => {
|
||||
setPreparingExport(false);
|
||||
});
|
||||
setPreparingExport(true);
|
||||
try {
|
||||
await handleResourceExport('dashboard', ids, () => {
|
||||
setPreparingExport(false);
|
||||
});
|
||||
} catch (error) {
|
||||
setPreparingExport(false);
|
||||
addDangerToast(t('There was an issue exporting the selected dashboards'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDashboardEdit = (edits: Dashboard) =>
|
||||
|
||||
Reference in New Issue
Block a user