diff --git a/.vscode/launch.json b/.vscode/launch.json index 60246c5573..cf70093ba4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -420,8 +420,7 @@ "args": [ "--verbose", "designer", - "--frontend-dev", - "--base-dir=/home/wei/MyData/MyProject/ten_framework_internal_base/ten_framework/out/linux/x64/tests/ten_runtime/integration/cpp/http_basic/restful_http_app/", + "--base-dir=/home/wei/MyData/MyProject/ten_framework_internal_base/ten_framework/out/linux/x64/tests/ten_runtime/integration/python/aio_http_server_python/aio_http_server_python_app/", ], "preRunCommands": [ "script import pathlib;import subprocess;import lldb;rustc_sysroot = subprocess.getoutput(\"rustc --print sysroot\");rustlib_etc = pathlib.Path(rustc_sysroot) / \"lib\" / \"rustlib\" / \"etc\";lldb.debugger.HandleCommand(f'command script import \"{rustlib_etc / \"lldb_lookup.py\"}\"');lldb.debugger.HandleCommand(f'command source -s 0 \"{rustlib_etc / \"lldb_commands\"}\"')" 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 afb9c51bda..8cf7f67c18 100644 --- a/core/src/ten_manager/designer_frontend/src/api/api.ts +++ b/core/src/ten_manager/designer_frontend/src/api/api.ts @@ -8,6 +8,8 @@ import { ApiResponse, BackendConnection, BackendNode, + FileContentResponse, + SaveFileRequest, SuccessResponse, } from "./interface"; @@ -17,6 +19,12 @@ export interface ExtensionAddon { api?: any; } +export const isSuccessResponse = ( + response: ApiResponse +): response is SuccessResponse => { + return response.status === "ok"; +}; + export const fetchNodes = async (): Promise => { const response = await fetch(`/api/designer/v1/graphs/default/nodes`); if (!response.ok) { @@ -25,7 +33,7 @@ export const fetchNodes = async (): Promise => { const data: ApiResponse = await response.json(); if (!isSuccessResponse(data)) { - throw new Error(`Error fetching nodes: ${data.message}`); + throw new Error(`Failed to fetch nodes: ${data.message}`); } return data.data; @@ -39,7 +47,7 @@ export const fetchConnections = async (): Promise => { const data: ApiResponse = await response.json(); if (!isSuccessResponse(data)) { - throw new Error(`Error fetching connections: ${data.message}`); + throw new Error(`Failed to fetch connection: ${data.message}`); } return data.data; @@ -63,12 +71,58 @@ export const fetchExtensionAddonByName = async ( if (isSuccessResponse(data)) { return data.data; } else { - throw new Error(`Error fetching addon '${name}': ${data.message}`); + throw new Error(`Failed to fetch addon '${name}': ${data.message}`); } }; -export const isSuccessResponse = ( - response: ApiResponse -): response is SuccessResponse => { - return response.status === "ok"; +// Get the contents of the file at the specified path. +export const getFileContent = async ( + path: string +): Promise => { + const encodedPath = encodeURIComponent(path); + const response = await fetch(`/api/designer/v1/file-content/${encodedPath}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch file content: ${response.status}`); + } + + const data: ApiResponse = await response.json(); + + if (!isSuccessResponse(data)) { + throw new Error(`Failed to fetch file content: ${data.status}`); + } + + return data.data; +}; + +// Save the contents of the file at the specified path. +export const saveFileContent = async ( + path: string, + content: string +): Promise => { + const encodedPath = encodeURIComponent(path); + const body: SaveFileRequest = { content }; + + const response = await fetch(`/api/designer/v1/file-content/${encodedPath}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + throw new Error(`Failed to save file content: ${response.status}`); + } + + const data: ApiResponse = await response.json(); + + if (data.status !== "ok") { + throw new Error(`Failed to save file content: ${data.status}`); + } }; 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 d6ed192633..23b3a459a2 100644 --- a/core/src/ten_manager/designer_frontend/src/api/interface.ts +++ b/core/src/ten_manager/designer_frontend/src/api/interface.ts @@ -48,3 +48,11 @@ export interface BackendConnection { }[]; }[]; } + +export interface FileContentResponse { + content: string; +} + +export interface SaveFileRequest { + content: string; +} diff --git a/core/src/ten_manager/designer_frontend/src/components/EditorPopup/EditorPopup.scss b/core/src/ten_manager/designer_frontend/src/components/EditorPopup/EditorPopup.scss index d7a090dd9d..9d5c2e8640 100644 --- a/core/src/ten_manager/designer_frontend/src/components/EditorPopup/EditorPopup.scss +++ b/core/src/ten_manager/designer_frontend/src/components/EditorPopup/EditorPopup.scss @@ -16,3 +16,16 @@ flex-direction: column; } } + +.popup-confirm { + .confirm-dialog-content { + padding: 20px; + text-align: center; + } + + .confirm-dialog-actions { + margin-top: 20px; + display: flex; + justify-content: space-around; + } +} diff --git a/core/src/ten_manager/designer_frontend/src/components/EditorPopup/EditorPopup.tsx b/core/src/ten_manager/designer_frontend/src/components/EditorPopup/EditorPopup.tsx index 78eb581eff..2e9ab1512a 100644 --- a/core/src/ten_manager/designer_frontend/src/components/EditorPopup/EditorPopup.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/EditorPopup/EditorPopup.tsx @@ -10,6 +10,7 @@ import Editor from "@monaco-editor/react"; import Popup from "../Popup/Popup"; import "./EditorPopup.scss"; +import { getFileContent, saveFileContent } from "../../api/api"; const DEFAULT_WIDTH = 800; const DEFAULT_HEIGHT = 400; @@ -29,20 +30,50 @@ interface EditorPopupProps { onClose: () => void; } +interface ConfirmDialogProps { + message: string; + onConfirm: () => void; + onCancel: () => void; +} + +const ConfirmDialog: React.FC = ({ + message, + onConfirm, + onCancel, +}) => { + return ( + +
+

{message}

+
+ + +
+
+
+ ); +}; + const EditorPopup: React.FC = ({ data, onClose }) => { const [fileContent, setFileContent] = useState(data.content); + const [showConfirmDialog, setShowConfirmDialog] = useState(false); + const [pendingAction, setPendingAction] = useState void)>(null); + // Fetch the specified file content from the backend. useEffect(() => { const fetchFileContent = async () => { try { - const encodedUrl = encodeURIComponent(data.url); - const response = await fetch( - `/api/designer/v1/file-content/${encodedUrl}` - ); - if (!response.ok) throw new Error("Failed to fetch file content"); - const respData = await response.json(); - setFileContent(respData.data.content); + const respData = await getFileContent(data.url); + setFileContent(respData.content); } catch (error) { console.error("Failed to fetch file content:", error); } @@ -51,29 +82,91 @@ const EditorPopup: React.FC = ({ data, onClose }) => { fetchFileContent(); }, [data.url]); + const saveFile = async (content: string) => { + try { + await saveFileContent(data.url, content); + console.log("File saved successfully"); + // We can add UI prompts, such as displaying a success notification. + } catch (error) { + console.error("Failed to save file content:", error); + // We can add UI prompts, such as popping up an error notification. + } + }; + + const handleActionWithOptionalConfirm = ( + action: () => void, + needsConfirm: boolean + ) => { + if (needsConfirm) { + setPendingAction(() => action); + setShowConfirmDialog(true); + } else { + action(); + } + }; + return ( - - { - editor.focus(); // Set the keyboard focus to the editor. - }} - /> - + <> + + setFileContent(value || "")} + onMount={(editor) => { + editor.focus(); // Set the keyboard focus to the editor. + + editor.addAction({ + id: "save-file", + label: "Save", + contextMenuGroupId: "navigation", + contextMenuOrder: 1.5, + run: async (ed) => { + // When the user clicks this menu item, first display a + // confirmation popup. + handleActionWithOptionalConfirm( + () => { + // Confirm before saving. + const currentContent = ed.getValue(); + setFileContent(currentContent); + saveFile(currentContent); + }, + true // Display a confirmation popup. + ); + }, + }); + }} + /> + + + {/* Conditional Rendering Confirmation Popup. */} + {showConfirmDialog && pendingAction && ( + { + setShowConfirmDialog(false); + pendingAction(); + setPendingAction(null); + }} + onCancel={() => { + setShowConfirmDialog(false); + setPendingAction(null); + }} + /> + )} + ); }; diff --git a/core/src/ten_manager/src/designer/file_content/mod.rs b/core/src/ten_manager/src/designer/file_content/mod.rs index b6c45e87ab..56caaf985e 100644 --- a/core/src/ten_manager/src/designer/file_content/mod.rs +++ b/core/src/ten_manager/src/designer/file_content/mod.rs @@ -5,6 +5,7 @@ // Refer to the "LICENSE" file in the root directory for more information. // use std::fs; +use std::path::Path; use std::sync::{Arc, RwLock}; use actix_web::{web, HttpResponse, Responder}; @@ -46,3 +47,60 @@ pub async fn get_file_content( } } } + +#[derive(Serialize, Deserialize, Debug)] +pub struct SaveFileRequest { + content: String, +} + +pub async fn save_file_content( + path: web::Path, + req: web::Json, + _state: web::Data>>, +) -> HttpResponse { + let file_path_str = path.into_inner(); + let content = &req.content; // Access the content field. + + let file_path = Path::new(&file_path_str); + + // Attempt to create parent directories if they don't exist. + if let Some(parent) = file_path.parent() { + if let Err(e) = fs::create_dir_all(parent) { + eprintln!( + "Error creating directories for {}: {}", + parent.display(), + e + ); + let response = ApiResponse { + status: Status::Fail, + data: (), + meta: None, + }; + return HttpResponse::BadRequest().json(response); + } + } + + match fs::write(file_path, content) { + Ok(_) => { + let response = ApiResponse { + status: Status::Ok, + data: (), + meta: None, + }; + HttpResponse::Ok().json(response) + } + Err(err) => { + eprintln!( + "Error writing file at path {}: {}", + file_path.display(), + err + ); + let response = ApiResponse { + status: Status::Fail, + data: (), + meta: None, + }; + HttpResponse::BadRequest().json(response) + } + } +} diff --git a/core/src/ten_manager/src/designer/mod.rs b/core/src/ten_manager/src/designer/mod.rs index 32b6cfebff..d54ea3eacf 100644 --- a/core/src/ten_manager/src/designer/mod.rs +++ b/core/src/ten_manager/src/designer/mod.rs @@ -90,5 +90,9 @@ pub fn configure_routes( "/api/designer/v1/file-content/{path}", web::get().to(file_content::get_file_content), ) + .route( + "/api/designer/v1/file-content/{path}", + web::put().to(file_content::save_file_content), + ) .route("/ws/terminal", web::get().to(ws_terminal)); }