From a00ec1d06238f0f1f0bba68f091cbe4796d2b750 Mon Sep 17 00:00:00 2001 From: kavinvenkatachalam Date: Tue, 3 Mar 2026 14:43:01 +0530 Subject: [PATCH 01/19] feat: add JS Libraries management feature with dynamic loading and setup script execution --- .../GlobalSettings/JSLibraries.jsx | 209 ++++++++++++++++++ .../LeftSidebar/GlobalSettings/index.jsx | 6 + .../LeftSidebar/GlobalSettings/styles.scss | 186 ++++++++++++++++ .../src/AppBuilder/_helpers/libraryLoader.js | 106 +++++++++ frontend/src/AppBuilder/_hooks/useAppData.js | 31 ++- .../AppBuilder/_stores/slices/librarySlice.js | 13 ++ .../_stores/slices/queryPanelSlice.js | 9 +- frontend/src/AppBuilder/_stores/store.js | 2 + 8 files changed, 559 insertions(+), 3 deletions(-) create mode 100644 frontend/src/AppBuilder/LeftSidebar/GlobalSettings/JSLibraries.jsx create mode 100644 frontend/src/AppBuilder/_helpers/libraryLoader.js create mode 100644 frontend/src/AppBuilder/_stores/slices/librarySlice.js diff --git a/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/JSLibraries.jsx b/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/JSLibraries.jsx new file mode 100644 index 0000000000..dc07a98f21 --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/JSLibraries.jsx @@ -0,0 +1,209 @@ +import React, { useState } from 'react'; +import useStore from '@/AppBuilder/_stores/store'; +import { shallow } from 'zustand/shallow'; +import toast from 'react-hot-toast'; +import { loadLibraryFromURL, initializeLibraries, executeSetupScript } from '@/AppBuilder/_helpers/libraryLoader'; + +const JSLibraries = ({ darkMode }) => { + const { globalSettings, globalSettingsChanged, jsLibraryRegistry, setJsLibraryRegistry } = useStore( + (state) => ({ + globalSettings: state.globalSettings, + globalSettingsChanged: state.globalSettingsChanged, + jsLibraryRegistry: state.jsLibraryRegistry, + setJsLibraryRegistry: state.setJsLibraryRegistry, + }), + shallow + ); + + const jsLibraries = globalSettings?.jsLibraries || []; + const setupScript = globalSettings?.setupScript || ''; + + const [newUrl, setNewUrl] = useState(''); + const [newName, setNewName] = useState(''); + const [adding, setAdding] = useState(false); + const [showSetupScript, setShowSetupScript] = useState(false); + + const handleAddLibrary = async () => { + if (!newUrl.trim() || !newName.trim()) { + toast.error('Both name and URL are required'); + return; + } + + if (jsLibraries.some((lib) => lib.name === newName.trim())) { + toast.error(`A library with name "${newName.trim()}" already exists`); + return; + } + + setAdding(true); + try { + // Test loading the library before saving + const module = await loadLibraryFromURL(newUrl.trim()); + if (module == null) { + toast.error('Library loaded but exported nothing. Ensure it is a UMD/IIFE build.'); + setAdding(false); + return; + } + + const updatedLibraries = [...jsLibraries, { name: newName.trim(), url: newUrl.trim(), enabled: true }]; + await globalSettingsChanged({ jsLibraries: updatedLibraries }); + + // Update the runtime registry + const newRegistry = { ...jsLibraryRegistry, [newName.trim()]: module }; + setJsLibraryRegistry(newRegistry); + + setNewUrl(''); + setNewName(''); + toast.success(`Library "${newName.trim()}" added successfully`); + } catch (error) { + console.error('Failed to add library:', error); + toast.error(`Failed to load library: ${error.message}`); + } finally { + setAdding(false); + } + }; + + const handleRemoveLibrary = async (index) => { + const lib = jsLibraries[index]; + const updatedLibraries = jsLibraries.filter((_, i) => i !== index); + await globalSettingsChanged({ jsLibraries: updatedLibraries }); + + // Remove from runtime registry + const newRegistry = { ...jsLibraryRegistry }; + delete newRegistry[lib.name]; + setJsLibraryRegistry(newRegistry); + }; + + const handleSetupScriptChange = async (value) => { + await globalSettingsChanged({ setupScript: value }); + }; + + const handleRunSetupScript = async () => { + try { + await executeSetupScript(setupScript, jsLibraryRegistry); + toast.success('Setup script executed successfully'); + } catch (error) { + toast.error('Setup script failed: ' + error.message); + } + }; + + const handleReloadLibraries = async () => { + try { + const registry = await initializeLibraries(jsLibraries); + setJsLibraryRegistry(registry); + if (setupScript) { + await executeSetupScript(setupScript, registry); + } + toast.success('Libraries reloaded successfully'); + } catch (error) { + toast.error('Failed to reload libraries: ' + error.message); + } + }; + + return ( +
+ {/* Library list */} + {jsLibraries.length > 0 && ( +
+ {jsLibraries.map((lib, index) => ( +
+
+ {lib.name} + + {lib.url.length > 40 ? lib.url.substring(0, 40) + '...' : lib.url} + +
+ +
+ ))} +
+ )} + + {/* Add library form */} +
+ setNewName(e.target.value)} + data-cy="js-library-name-input" + /> + setNewUrl(e.target.value)} + data-cy="js-library-url-input" + /> + +
+ + {/* Setup script section */} +
+ + {showSetupScript && ( +
+