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:

Reply via email to