This is an automated email from the ASF dual-hosted git repository. beto pushed a commit to branch file-handler2 in repository https://gitbox.apache.org/repos/asf/superset.git
commit 3c06a56f836703e1229e96466e7fe55f33559e85 Author: Beto Dealmeida <[email protected]> AuthorDate: Thu Nov 20 10:03:36 2025 -0500 Improve code --- .../features/databases/UploadDataModel/index.tsx | 20 ++++- .../src/pages/FileHandler/index.test.tsx | 96 ++++++++++++++-------- superset-frontend/src/pages/FileHandler/index.tsx | 26 ++++-- superset-frontend/src/pwa-manifest.json | 1 + superset-frontend/webpack.config.js | 30 ++++--- superset/templates/superset/spa.html | 5 +- superset/views/core.py | 1 + 7 files changed, 124 insertions(+), 55 deletions(-) diff --git a/superset-frontend/src/features/databases/UploadDataModel/index.tsx b/superset-frontend/src/features/databases/UploadDataModel/index.tsx index 67122178dc..4c416293f4 100644 --- a/superset-frontend/src/features/databases/UploadDataModel/index.tsx +++ b/superset-frontend/src/features/databases/UploadDataModel/index.tsx @@ -67,6 +67,7 @@ interface UploadDataModalProps { show: boolean; allowedExtensions: string[]; type: UploadType; + fileListOverride?: File[]; } const CSVSpecificFields = [ @@ -215,6 +216,7 @@ const UploadDataModal: FunctionComponent<UploadDataModalProps> = ({ show, allowedExtensions, type = 'csv', + fileListOverride, }) => { const [form] = Form.useForm(); const [currentDatabaseId, setCurrentDatabaseId] = useState<number>(0); @@ -524,10 +526,26 @@ const UploadDataModal: FunctionComponent<UploadDataModalProps> = ({ await loadFileMetadata(info.file.originFileObj); }; + useEffect(() => { + if (fileListOverride?.length) { + setFileList( + fileListOverride.map(file => ({ + uid: file.name, + name: file.name, + originFileObj: file, + status: 'done', + })), + ); + if (previewUploadedFile) { + loadFileMetadata(fileListOverride[0]).then(r => r); + } + } + }, [fileListOverride, previewUploadedFile]); + useEffect(() => { if ( columns.length > 0 && - fileList[0].originFileObj && + fileList.length > 0 && fileList[0].originFileObj instanceof File ) { if (!previewUploadedFile) { diff --git a/superset-frontend/src/pages/FileHandler/index.test.tsx b/superset-frontend/src/pages/FileHandler/index.test.tsx index 2e9cf6b473..4d99471a69 100644 --- a/superset-frontend/src/pages/FileHandler/index.test.tsx +++ b/superset-frontend/src/pages/FileHandler/index.test.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { ComponentType } from 'react'; import { render, screen, waitFor } from 'spec/helpers/testing-library'; import { MemoryRouter, Route } from 'react-router-dom'; import FileHandler from './index'; @@ -24,26 +25,48 @@ const mockAddDangerToast = jest.fn(); const mockAddSuccessToast = jest.fn(); const mockHistoryPush = jest.fn(); +type ToastInjectedProps = { + addDangerToast: (msg: string) => void; + addSuccessToast: (msg: string) => void; +}; + // Mock the withToasts HOC jest.mock('src/components/MessageToasts/withToasts', () => ({ __esModule: true, - default: (Component: any) => (props: any) => ( - <Component - {...props} - addDangerToast={mockAddDangerToast} - addSuccessToast={mockAddSuccessToast} - /> - ), + default: + <P extends object>(Component: ComponentType<P & ToastInjectedProps>) => + (props: P) => ( + <Component + {...props} + addDangerToast={mockAddDangerToast} + addSuccessToast={mockAddSuccessToast} + /> + ), })); +interface UploadDataModalProps { + show: boolean; + onHide: () => void; + type: string; + allowedExtensions: string[]; + fileListOverride?: File[]; +} + // Mock the UploadDataModal jest.mock('src/features/databases/UploadDataModel', () => ({ __esModule: true, - default: ({ show, onHide, type, allowedExtensions }: any) => ( + default: ({ + show, + onHide, + type, + allowedExtensions, + fileListOverride, + }: UploadDataModalProps) => ( <div data-test="upload-modal"> <div data-test="modal-show">{show.toString()}</div> <div data-test="modal-type">{type}</div> <div data-test="modal-extensions">{allowedExtensions.join(',')}</div> + <div data-test="modal-file">{fileListOverride?.[0]?.name ?? ''}</div> <button onClick={onHide}>Close</button> </div> ), @@ -58,28 +81,36 @@ jest.mock('react-router-dom', () => ({ })); // Mock the File API -class MockFile { +type MockFileHandle = { + kind: 'file'; name: string; - - constructor(name: string) { - this.name = name; - } -} - -interface MockFileHandle { - getFile: () => Promise<MockFile>; -} + getFile: () => Promise<File>; + isSameEntry: () => Promise<boolean>; + queryPermission: () => Promise<PermissionState>; + requestPermission: () => Promise<PermissionState>; +}; const createMockFileHandle = (fileName: string): MockFileHandle => ({ - getFile: async () => new MockFile(fileName), + kind: 'file', + name: fileName, + getFile: async () => new File(['test'], fileName), + isSameEntry: async () => false, + queryPermission: async () => 'granted', + requestPermission: async () => 'granted', }); +type LaunchQueue = { + setConsumer: ( + consumer: (params: { files?: MockFileHandle[] }) => void, + ) => void; +}; + const setupLaunchQueue = (fileHandle: MockFileHandle | null = null) => { - let savedConsumer: ((params: any) => void) | null = null; - (window as any).launchQueue = { - setConsumer: (consumer: (params: any) => void) => { + let savedConsumer: ((params: { files?: MockFileHandle[] }) => void) | null = + null; + (window as Window & { launchQueue: LaunchQueue }).launchQueue = { + setConsumer: (consumer: (params: { files?: MockFileHandle[] }) => void) => { savedConsumer = consumer; - // Automatically trigger the consumer if a fileHandle is provided if (fileHandle) { setTimeout(() => { consumer({ @@ -90,10 +121,8 @@ const setupLaunchQueue = (fileHandle: MockFileHandle | null = null) => { }, }; return { - triggerConsumer: (params: any) => { - if (savedConsumer) { - savedConsumer(params); - } + triggerConsumer: (params: { files?: MockFileHandle[] }) => { + savedConsumer?.(params); }, }; }; @@ -158,7 +187,8 @@ test('handles CSV file correctly', async () => { expect(modal).toBeInTheDocument(); expect(screen.getByTestId('modal-show')).toHaveTextContent('true'); expect(screen.getByTestId('modal-type')).toHaveTextContent('csv'); - expect(screen.getByTestId('modal-extensions')).toHaveTextContent('.csv'); + expect(screen.getByTestId('modal-extensions')).toHaveTextContent('csv'); + expect(screen.getByTestId('modal-file')).toHaveTextContent('test.csv'); }); test('handles Excel (.xls) file correctly', async () => { @@ -177,9 +207,7 @@ test('handles Excel (.xls) file correctly', async () => { const modal = await screen.findByTestId('upload-modal'); expect(modal).toBeInTheDocument(); expect(screen.getByTestId('modal-type')).toHaveTextContent('excel'); - expect(screen.getByTestId('modal-extensions')).toHaveTextContent( - '.xls,.xlsx', - ); + expect(screen.getByTestId('modal-extensions')).toHaveTextContent('xls,xlsx'); }); test('handles Excel (.xlsx) file correctly', async () => { @@ -198,9 +226,7 @@ test('handles Excel (.xlsx) file correctly', async () => { const modal = await screen.findByTestId('upload-modal'); expect(modal).toBeInTheDocument(); expect(screen.getByTestId('modal-type')).toHaveTextContent('excel'); - expect(screen.getByTestId('modal-extensions')).toHaveTextContent( - '.xls,.xlsx', - ); + expect(screen.getByTestId('modal-extensions')).toHaveTextContent('xls,xlsx'); }); test('handles Parquet file correctly', async () => { @@ -219,7 +245,7 @@ test('handles Parquet file correctly', async () => { const modal = await screen.findByTestId('upload-modal'); expect(modal).toBeInTheDocument(); expect(screen.getByTestId('modal-type')).toHaveTextContent('columnar'); - expect(screen.getByTestId('modal-extensions')).toHaveTextContent('.parquet'); + expect(screen.getByTestId('modal-extensions')).toHaveTextContent('parquet'); }); test('shows error for unsupported file type', async () => { diff --git a/superset-frontend/src/pages/FileHandler/index.tsx b/superset-frontend/src/pages/FileHandler/index.tsx index 960fcf0add..68ed035769 100644 --- a/superset-frontend/src/pages/FileHandler/index.tsx +++ b/superset-frontend/src/pages/FileHandler/index.tsx @@ -23,8 +23,16 @@ import { Loading } from '@superset-ui/core/components'; import UploadDataModal from 'src/features/databases/UploadDataModel'; import withToasts from 'src/components/MessageToasts/withToasts'; -interface LaunchParams { - readonly files: readonly FileSystemFileHandle[]; +interface FileLaunchParams { + readonly files?: readonly FileSystemFileHandle[]; +} + +interface LaunchQueue { + setConsumer: (consumer: (params: FileLaunchParams) => void) => void; +} + +interface WindowWithLaunchQueue extends Window { + launchQueue?: LaunchQueue; } interface FileHandlerProps { @@ -43,7 +51,9 @@ const FileHandler = ({ addDangerToast, addSuccessToast }: FileHandlerProps) => { useEffect(() => { const handleFileLaunch = async () => { - if (!('launchQueue' in window)) { + const { launchQueue } = window as WindowWithLaunchQueue; + + if (!launchQueue) { addDangerToast( t( 'File handling is not supported in this browser. Please use a modern browser like Chrome or Edge.', @@ -53,8 +63,7 @@ const FileHandler = ({ addDangerToast, addSuccessToast }: FileHandlerProps) => { return; } - const launchQueue = (window as any).launchQueue; - launchQueue.setConsumer(async (launchParams: LaunchParams) => { + launchQueue.setConsumer(async (launchParams: FileLaunchParams) => { if (!launchParams.files || launchParams.files.length === 0) { history.push('/superset/welcome/'); return; @@ -70,13 +79,13 @@ const FileHandler = ({ addDangerToast, addSuccessToast }: FileHandlerProps) => { if (fileName.endsWith('.csv')) { type = 'csv'; - extensions = ['.csv']; + extensions = ['csv']; } else if (fileName.endsWith('.xls') || fileName.endsWith('.xlsx')) { type = 'excel'; - extensions = ['.xls', '.xlsx']; + extensions = ['xls', 'xlsx']; } else if (fileName.endsWith('.parquet')) { type = 'columnar'; - extensions = ['.parquet']; + extensions = ['parquet']; } else { addDangerToast( t( @@ -117,6 +126,7 @@ const FileHandler = ({ addDangerToast, addSuccessToast }: FileHandlerProps) => { <UploadDataModal show={showModal} onHide={handleModalClose} + fileListOverride={[uploadFile]} allowedExtensions={allowedExtensions} type={uploadType} addDangerToast={addDangerToast} diff --git a/superset-frontend/src/pwa-manifest.json b/superset-frontend/src/pwa-manifest.json index 07805a88ad..8b8755e189 100644 --- a/superset-frontend/src/pwa-manifest.json +++ b/superset-frontend/src/pwa-manifest.json @@ -3,6 +3,7 @@ "short_name": "Superset", "description": "Modern data exploration and visualization platform", "start_url": "/superset/welcome/", + "scope": "/", "display": "standalone", "background_color": "#ffffff", "theme_color": "#20a7c9", diff --git a/superset-frontend/webpack.config.js b/superset-frontend/webpack.config.js index 5be2395759..3bed029086 100644 --- a/superset-frontend/webpack.config.js +++ b/superset-frontend/webpack.config.js @@ -71,20 +71,30 @@ const isDevServer = process.argv[1]?.includes('webpack-dev-server') ?? false; // TypeScript checker memory limit (in MB) const TYPESCRIPT_MEMORY_LIMIT = 4096; +const defaultEntryFilename = isDevMode + ? '[name].[contenthash:8].entry.js' + : nameChunks + ? '[name].[chunkhash].entry.js' + : '[name].[chunkhash].entry.js'; + +const defaultChunkFilename = isDevMode + ? '[name].[contenthash:8].chunk.js' + : nameChunks + ? '[name].[chunkhash].chunk.js' + : '[chunkhash].chunk.js'; + const output = { path: BUILD_DIR, publicPath: '/static/assets/', + filename: pathData => + pathData.chunk?.name === 'service-worker' + ? '../service-worker.js' + : defaultEntryFilename, + chunkFilename: pathData => + pathData.chunk?.name === 'service-worker' + ? '../service-worker.js' + : defaultChunkFilename, }; -if (isDevMode) { - output.filename = '[name].[contenthash:8].entry.js'; - output.chunkFilename = '[name].[contenthash:8].chunk.js'; -} else if (nameChunks) { - output.filename = '[name].[chunkhash].entry.js'; - output.chunkFilename = '[name].[chunkhash].chunk.js'; -} else { - output.filename = '[name].[chunkhash].entry.js'; - output.chunkFilename = '[chunkhash].chunk.js'; -} if (!isDevMode) { output.clean = true; diff --git a/superset/templates/superset/spa.html b/superset/templates/superset/spa.html index e8e71015b6..a50fe95bf0 100644 --- a/superset/templates/superset/spa.html +++ b/superset/templates/superset/spa.html @@ -78,7 +78,10 @@ <script nonce="{{ macros.get_nonce() }}"> if ('serviceWorker' in navigator) { window.addEventListener('load', function() { - navigator.serviceWorker.register('{{ assets_prefix }}/static/assets/service-worker.js') + navigator.serviceWorker + .register('{{ assets_prefix }}/static/service-worker.js', { + scope: '/', + }) .catch(function(err) { console.error('Service Worker registration failed:', err); }); diff --git a/superset/views/core.py b/superset/views/core.py index af1bfef6dc..845ba163bc 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -908,6 +908,7 @@ class Superset(BaseSupersetView): return self.render_app_template(extra_bootstrap_data=payload) + @has_access @event_logger.log_this @expose("/file-handler") def file_handler(self) -> FlaskResponse:
