From 59ca2ae33ea762e1bc482ec697262036f9e44996 Mon Sep 17 00:00:00 2001 From: Hu Yueh-Wei Date: Tue, 17 Dec 2024 23:42:21 +0800 Subject: [PATCH] feat: add open app base dir functionality (#420) --- .../ten_manager/designer_frontend/src/App.tsx | 40 +++++ .../designer_frontend/src/api/api.ts | 24 +++ .../designer_frontend/src/api/interface.ts | 8 + .../src/components/AppBar/AppBar.tsx | 3 + .../src/components/AppBar/FileMenu.tsx | 137 +++++++++++++++++- 5 files changed, 204 insertions(+), 8 deletions(-) diff --git a/core/src/ten_manager/designer_frontend/src/App.tsx b/core/src/ten_manager/designer_frontend/src/App.tsx index de465c7b9..9055865f2 100644 --- a/core/src/ten_manager/designer_frontend/src/App.tsx +++ b/core/src/ten_manager/designer_frontend/src/App.tsx @@ -16,6 +16,7 @@ import { fetchDesignerVersion, fetchGraphs, fetchNodes, + setBaseDir, } from "./api/api"; import { Graph } from "./api/interface"; import { CustomNodeType } from "./flow/CustomNode"; @@ -38,6 +39,8 @@ const App: React.FC = () => { const [showGraphSelection, setShowGraphSelection] = useState(false); const [nodes, setNodes] = useState([]); const [edges, setEdges] = useState([]); + const [successMessage, setSuccessMessage] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); const { theme, setTheme } = useTheme(); @@ -113,6 +116,18 @@ const App: React.FC = () => { setEdges((eds) => applyEdgeChanges(changes, eds)); }, []); + const handleSetBaseDir = useCallback(async (folderPath: string) => { + try { + await setBaseDir(folderPath); + setSuccessMessage("Successfully opened a new app folder."); + setNodes([]); // Clear the contents of the FlowCanvas. + setEdges([]); + } catch (error) { + setErrorMessage("Failed to open a new app folder."); + console.error(error); + } + }, []); + return (
{ onOpenSettings={() => setShowSettings(true)} onAutoLayout={performAutoLayout} onOpenExistingGraph={handleOpenExistingGraph} + onSetBaseDir={handleSetBaseDir} /> { )} + {successMessage && ( + setSuccessMessage(null)} + resizable={false} + initialWidth={300} + initialHeight={150} + onCollapseToggle={() => {}} + > +

{successMessage}

+
+ )} + {errorMessage && ( + setErrorMessage(null)} + resizable={false} + initialWidth={300} + initialHeight={150} + onCollapseToggle={() => {}} + > +

{errorMessage}

+
+ )}
); }; diff --git a/core/src/ten_manager/designer_frontend/src/api/api.ts b/core/src/ten_manager/designer_frontend/src/api/api.ts index 1e3e39be7..08824fee8 100644 --- a/core/src/ten_manager/designer_frontend/src/api/api.ts +++ b/core/src/ten_manager/designer_frontend/src/api/api.ts @@ -12,6 +12,8 @@ import { FileContentResponse, SaveFileRequest, SuccessResponse, + SetBaseDirResponse, + SetBaseDirRequest, } from "./interface"; export interface ExtensionAddon { @@ -165,3 +167,25 @@ export const saveFileContent = async ( throw new Error(`Failed to save file content: ${data.status}`); } }; + +export const setBaseDir = async ( + baseDir: string +): Promise> => { + const requestBody: SetBaseDirRequest = { base_dir: baseDir }; + + const response = await fetch("/api/designer/v1/base-dir", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + throw new Error(`Failed to set base directory: ${response.status}`); + } + + const data: ApiResponse = await response.json(); + + return data; +}; diff --git a/core/src/ten_manager/designer_frontend/src/api/interface.ts b/core/src/ten_manager/designer_frontend/src/api/interface.ts index c15522de7..c382b17c6 100644 --- a/core/src/ten_manager/designer_frontend/src/api/interface.ts +++ b/core/src/ten_manager/designer_frontend/src/api/interface.ts @@ -61,3 +61,11 @@ export interface Graph { name: string; auto_start: boolean; } + +export interface SetBaseDirRequest { + base_dir: string; +} + +export interface SetBaseDirResponse { + success: boolean; +} diff --git a/core/src/ten_manager/designer_frontend/src/components/AppBar/AppBar.tsx b/core/src/ten_manager/designer_frontend/src/components/AppBar/AppBar.tsx index ffba4e02a..095a206a8 100644 --- a/core/src/ten_manager/designer_frontend/src/components/AppBar/AppBar.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/AppBar/AppBar.tsx @@ -19,6 +19,7 @@ interface AppBarProps { onOpenExistingGraph: () => void; onAutoLayout: () => void; onOpenSettings: () => void; + onSetBaseDir: (folderPath: string) => void; } type MenuType = "file" | "edit" | "help" | null; @@ -28,6 +29,7 @@ const AppBar: React.FC = ({ onOpenExistingGraph, onAutoLayout, onOpenSettings, + onSetBaseDir, }) => { const [openMenu, setOpenMenu] = useState(null); const appBarRef = useRef(null); @@ -91,6 +93,7 @@ const AppBar: React.FC = ({ onClick={() => handleOpenMenu("file")} onHover={() => handleSwitchMenu("file")} closeMenu={closeMenu} + onSetBaseDir={onSetBaseDir} /> void; onHover: () => void; closeMenu: () => void; + onSetBaseDir: (folderPath: string) => void; } const FileMenu: React.FC = ({ @@ -21,25 +24,143 @@ const FileMenu: React.FC = ({ onClick, onHover, closeMenu, + onSetBaseDir, }) => { + const fileInputRef = useRef(null); + const [isManualInput, setIsManualInput] = useState(false); + const [manualPath, setManualPath] = useState(""); + const [showPopup, setShowPopup] = useState(false); + const [popupMessage, setPopupMessage] = useState(""); + + // Try to use `showDirectoryPicker` in the File System Access API. + const handleOpenFolder = async () => { + if ("showDirectoryPicker" in window) { + try { + const dirHandle = await (window as any).showDirectoryPicker(); + const folderPath = dirHandle.name; + onSetBaseDir(folderPath); + } catch (error) { + console.error("Directory selection canceled or failed:", error); + } + } else { + // Fallback to input[type="file"]. + if (fileInputRef.current) { + fileInputRef.current.click(); + } + } + }; + + const handleFolderSelected = (event: React.ChangeEvent) => { + const files = event.target.files; + if (files && files.length > 0) { + const firstFile = files[0]; + const relativePath = firstFile.webkitRelativePath; + const folderPath = relativePath.split("/")[0]; + onSetBaseDir(folderPath); + } + }; + + const handleManualSubmit = async () => { + if (!manualPath.trim()) { + setPopupMessage("The folder path cannot be empty."); + setShowPopup(true); + return; + } + + try { + await setBaseDir(manualPath.trim()); + setPopupMessage("Successfully opened a new app folder."); + onSetBaseDir(manualPath.trim()); + setManualPath(""); + } catch (error) { + setPopupMessage("Failed to open a new app folder."); + console.error(error); + } finally { + setShowPopup(true); + } + }; + const items: DropdownMenuItem[] = [ { label: "Open TEN app folder", icon: , onClick: () => { + handleOpenFolder(); + closeMenu(); + }, + }, + { + label: "Set App Folder Manually", + icon: , + onClick: () => { + setIsManualInput(true); closeMenu(); }, }, ]; return ( - + <> + + {/* Hidden file input used for selecting folders. */} + + + {/* Popup for manually entering the folder path. */} + {isManualInput && ( + setIsManualInput(false)} + resizable={false} + initialWidth={400} + initialHeight={200} + onCollapseToggle={() => {}} + > +
+ + setManualPath(e.target.value)} + style={{ width: "100%", padding: "8px", marginTop: "5px" }} + placeholder="Enter folder path" + /> + +
+
+ )} + + {/* Popup to display success or error messages. */} + {showPopup && ( + setShowPopup(false)} + resizable={false} + initialWidth={300} + initialHeight={150} + onCollapseToggle={() => {}} + > +

{popupMessage}

+
+ )} + {/* >>>> END NEW */} + ); };