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 442288e1494d72c3e061029fea4644bddb57a34d Author: Beto Dealmeida <[email protected]> AuthorDate: Wed Nov 19 14:33:12 2025 -0500 feat: file handler for CSV/XSL --- superset-frontend/src/pages/FileHandler/index.tsx | 128 ++++++++++++++++++++++ superset-frontend/src/pwa-manifest.json | 30 +++++ superset-frontend/src/service-worker.ts | 30 +++++ superset-frontend/src/views/routes.tsx | 8 ++ superset-frontend/webpack.config.js | 7 +- superset/templates/superset/spa.html | 14 ++- superset/views/core.py | 14 +++ 7 files changed, 229 insertions(+), 2 deletions(-) diff --git a/superset-frontend/src/pages/FileHandler/index.tsx b/superset-frontend/src/pages/FileHandler/index.tsx new file mode 100644 index 0000000000..960fcf0add --- /dev/null +++ b/superset-frontend/src/pages/FileHandler/index.tsx @@ -0,0 +1,128 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { useEffect, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { t } from '@superset-ui/core'; +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 FileHandlerProps { + addDangerToast: (msg: string) => void; + addSuccessToast: (msg: string) => void; +} + +const FileHandler = ({ addDangerToast, addSuccessToast }: FileHandlerProps) => { + const history = useHistory(); + const [uploadFile, setUploadFile] = useState<File | null>(null); + const [uploadType, setUploadType] = useState< + 'csv' | 'excel' | 'columnar' | null + >(null); + const [showModal, setShowModal] = useState(false); + const [allowedExtensions, setAllowedExtensions] = useState<string[]>([]); + + useEffect(() => { + const handleFileLaunch = async () => { + if (!('launchQueue' in window)) { + addDangerToast( + t( + 'File handling is not supported in this browser. Please use a modern browser like Chrome or Edge.', + ), + ); + history.push('/superset/welcome/'); + return; + } + + const launchQueue = (window as any).launchQueue; + launchQueue.setConsumer(async (launchParams: LaunchParams) => { + if (!launchParams.files || launchParams.files.length === 0) { + history.push('/superset/welcome/'); + return; + } + + try { + const fileHandle = launchParams.files[0]; + const file = await fileHandle.getFile(); + const fileName = file.name.toLowerCase(); + + let type: 'csv' | 'excel' | 'columnar' | null = null; + let extensions: string[] = []; + + if (fileName.endsWith('.csv')) { + type = 'csv'; + extensions = ['.csv']; + } else if (fileName.endsWith('.xls') || fileName.endsWith('.xlsx')) { + type = 'excel'; + extensions = ['.xls', '.xlsx']; + } else if (fileName.endsWith('.parquet')) { + type = 'columnar'; + extensions = ['.parquet']; + } else { + addDangerToast( + t( + 'Unsupported file type. Please use CSV, Excel, or Columnar files.', + ), + ); + history.push('/superset/welcome/'); + return; + } + + setUploadFile(file); + setUploadType(type); + setAllowedExtensions(extensions); + setShowModal(true); + } catch (error) { + console.error('Error handling file launch:', error); + addDangerToast(t('Failed to open file. Please try again.')); + history.push('/superset/welcome/'); + } + }); + }; + + handleFileLaunch(); + }, [history, addDangerToast]); + + const handleModalClose = () => { + setShowModal(false); + setUploadFile(null); + setUploadType(null); + history.push('/superset/welcome/'); + }; + + if (!uploadFile || !uploadType) { + return <Loading />; + } + + return ( + <UploadDataModal + show={showModal} + onHide={handleModalClose} + allowedExtensions={allowedExtensions} + type={uploadType} + addDangerToast={addDangerToast} + addSuccessToast={addSuccessToast} + /> + ); +}; + +export default withToasts(FileHandler); diff --git a/superset-frontend/src/pwa-manifest.json b/superset-frontend/src/pwa-manifest.json new file mode 100644 index 0000000000..07805a88ad --- /dev/null +++ b/superset-frontend/src/pwa-manifest.json @@ -0,0 +1,30 @@ +{ + "name": "Apache Superset", + "short_name": "Superset", + "description": "Modern data exploration and visualization platform", + "start_url": "/superset/welcome/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#20a7c9", + "icons": [ + { + "src": "/static/assets/images/superset-logo-horiz.png", + "sizes": "any", + "type": "image/png", + "purpose": "any" + } + ], + "file_handlers": [ + { + "action": "/file-handler", + "accept": { + "text/csv": [".csv"], + "application/vnd.ms-excel": [".xls"], + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [ + ".xlsx" + ], + "application/vnd.apache.parquet": [".parquet"] + } + } + ] +} diff --git a/superset-frontend/src/service-worker.ts b/superset-frontend/src/service-worker.ts new file mode 100644 index 0000000000..0e0bbb5763 --- /dev/null +++ b/superset-frontend/src/service-worker.ts @@ -0,0 +1,30 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +declare const self: ServiceWorkerGlobalScope; + +self.addEventListener('install', (event: ExtendableEvent) => { + self.skipWaiting(); +}); + +self.addEventListener('activate', (event: ExtendableEvent) => { + event.waitUntil(self.clients.claim()); +}); + +export {}; diff --git a/superset-frontend/src/views/routes.tsx b/superset-frontend/src/views/routes.tsx index 0cb0ba9473..eb6dbd05dd 100644 --- a/superset-frontend/src/views/routes.tsx +++ b/superset-frontend/src/views/routes.tsx @@ -178,6 +178,10 @@ const UserRegistrations = lazy( ), ); +const FileHandler = lazy( + () => import(/* webpackChunkName: "FileHandler" */ 'src/pages/FileHandler'), +); + type Routes = { path: string; Component: ComponentType; @@ -206,6 +210,10 @@ export const routes: Routes = [ path: '/superset/welcome/', Component: Home, }, + { + path: '/file-handler', + Component: FileHandler, + }, { path: '/dashboard/list/', Component: DashboardList, diff --git a/superset-frontend/webpack.config.js b/superset-frontend/webpack.config.js index 03e48cc3c5..5be2395759 100644 --- a/superset-frontend/webpack.config.js +++ b/superset-frontend/webpack.config.js @@ -139,7 +139,11 @@ const plugins = [ }), new CopyPlugin({ - patterns: ['package.json', { from: 'src/assets/images', to: 'images' }], + patterns: [ + 'package.json', + { from: 'src/assets/images', to: 'images' }, + { from: 'src/pwa-manifest.json', to: 'pwa-manifest.json' }, + ], }), // static pages @@ -300,6 +304,7 @@ const config = { menu: addPreamble('src/views/menu.tsx'), spa: addPreamble('/src/views/index.tsx'), embedded: addPreamble('/src/embedded/index.tsx'), + 'service-worker': path.join(APP_DIR, '/src/service-worker.ts'), }, cache: { type: 'filesystem', // Enable filesystem caching diff --git a/superset/templates/superset/spa.html b/superset/templates/superset/spa.html index 8ce70725c2..e8e71015b6 100644 --- a/superset/templates/superset/spa.html +++ b/superset/templates/superset/spa.html @@ -28,7 +28,9 @@ {% endblock %} </title> - {% block head_meta %}{% endblock %} + {% block head_meta %} + <link rel="manifest" href="{{ assets_prefix }}/static/assets/pwa-manifest.json"> + {% endblock %} <style> body { @@ -73,6 +75,16 @@ /> {% block head_js %} {% include "head_custom_extra.html" %} + <script nonce="{{ macros.get_nonce() }}"> + if ('serviceWorker' in navigator) { + window.addEventListener('load', function() { + navigator.serviceWorker.register('{{ assets_prefix }}/static/assets/service-worker.js') + .catch(function(err) { + console.error('Service Worker registration failed:', err); + }); + }); + } + </script> {% endblock %} </head> diff --git a/superset/views/core.py b/superset/views/core.py index ced2078b14..af1bfef6dc 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -908,6 +908,20 @@ class Superset(BaseSupersetView): return self.render_app_template(extra_bootstrap_data=payload) + @event_logger.log_this + @expose("/file-handler") + def file_handler(self) -> FlaskResponse: + """File handler page for PWA file handling""" + if not g.user or not get_user_id(): + return redirect_to_login() + + payload = { + "user": bootstrap_user_data(g.user, include_perms=True), + "common": common_bootstrap_payload(), + } + + return self.render_app_template(extra_bootstrap_data=payload) + @has_access @event_logger.log_this @expose("/sqllab/history/", methods=("GET",))
