diff --git a/.vscode/launch.json b/.vscode/launch.json index 33b79f75d..4e01fea3b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -207,6 +207,9 @@ "LD_LIBRARY_PATH": "${workspaceFolder}/out/linux/x64/gen/cmake/clingo/install/lib/", "RUST_BACKTRACE": "1", }, + "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\"}\"')" + ], }, { "name": "tman test (rust)", @@ -237,6 +240,9 @@ "LD_LIBRARY_PATH": "${workspaceFolder}/out/linux/x64/ten_manager/lib/", "RUST_BACKTRACE": "1", }, + "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\"}\"')" + ], }, { "name": "tman install (lldb)", @@ -248,6 +254,9 @@ "install" ], "env": {}, + "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\"}\"')" + ], }, { "name": "tman delete (lldb)", @@ -262,6 +271,9 @@ "0.1.1", ], "env": {}, + "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\"}\"')" + ], }, { "name": "tman_test (lldb)", @@ -270,6 +282,9 @@ "program": "${workspaceFolder}/out/linux/x64/tman_test", "cwd": "${workspaceFolder}/out/linux/x64/", "args": [], + "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\"}\"')" + ], }, { "name": "tman check (lldb)", @@ -280,6 +295,9 @@ "args": [ "check" ], + "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\"}\"')" + ], }, { "name": "tman install_cpp_app_mock (lldb)", @@ -293,6 +311,9 @@ "ddd", "--mock=${workspaceFolder}/out/linux/x64/tests/local_registry/" ], + "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\"}\"')" + ], }, { "name": "tman install_cpp_app_oss (lldb)", @@ -309,7 +330,10 @@ "env": { "aliyun_oss_access_key_id": "", "aliyun_oss_access_key_secret": "" - } + }, + "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\"}\"')" + ], }, { "name": "tman install_nodejs_app_mock (lldb)", @@ -323,6 +347,9 @@ "app", "smart_meeting_minutes", ], + "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\"}\"')" + ], }, { "name": "tman install_extension_mock (lldb)", @@ -335,6 +362,9 @@ "extension", "default_extension_cpp" ], + "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\"}\"')" + ], }, { "name": "tman install_protocol_mock (lldb)", @@ -349,6 +379,9 @@ "--build=debug", "--mock=${workspaceFolder}/out/linux/x64/tests/local_registry/" ], + "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\"}\"')" + ], }, { "name": "tman install_app_mock (lldb)", @@ -360,6 +393,9 @@ "--config-file=${workspaceFolder}/out/linux/x64/tests/local_registry/config.json", "install", ], + "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\"}\"')" + ], }, { "name": "tman publish_mock (lldb)", @@ -371,18 +407,24 @@ "--config-file=${workspaceFolder}/out/linux/x64/tests/local_registry/config.json", "install", ], + "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\"}\"')" + ], }, { "name": "tman dev-server (lldb)", "type": "lldb", "request": "launch", - "program": "${workspaceFolder}/out/linux/x64/ten_manager/bin/tman", - "cwd": "${workspaceFolder}/out/linux/x64/", + "program": "${workspaceFolder}/core/src/ten_manager/target/x86_64-unknown-linux-gnu/debug/tman", + "cwd": "${workspaceFolder}/core/src/ten_manager/target/x86_64-unknown-linux-gnu/debug/", "args": [ "--verbose", "dev-server", - "--base-dir=/home/sunxilin/ten_framework_internal_base/ten_framework/out/linux/x64/tests/ten_runtime/integration/python/two_async_extensions_in_different_groups_python/two_async_extensions_in_different_groups_python_app", - "--port=49483" + "--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/", + ], + "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\"}\"')" ], }, { @@ -397,6 +439,9 @@ "../../../tests/ten_runtime/integration/ten_manager/res/dest_manifest.json", "hello_world", ], + "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\"}\"')" + ], }, { "name": "tman check graph (lldb)", @@ -408,7 +453,10 @@ "check", "graph", "/home/workspace/pcm-pusher" - ] + ], + "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\"}\"')" + ], }, { "name": "ten_rust (rust)", @@ -434,7 +482,10 @@ }, "args": [ "test_create_schema_invalid_json" - ] + ], + "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\"}\"')" + ], }, { "name": "app (C/C++) (lldb, launch)", diff --git a/core/src/ten_manager/Cargo.lock b/core/src/ten_manager/Cargo.lock index 69c22e471..32d0d08c9 100644 --- a/core/src/ten_manager/Cargo.lock +++ b/core/src/ten_manager/Cargo.lock @@ -1,31 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 - -[[package]] -name = "actix" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" -dependencies = [ - "actix-macros", - "actix-rt", - "actix_derive", - "bitflags 2.6.0", - "bytes", - "crossbeam-channel", - "futures-core", - "futures-sink", - "futures-task", - "futures-util", - "log", - "once_cell", - "parking_lot", - "pin-project-lite", - "smallvec", - "tokio", - "tokio-util", -] +version = 4 [[package]] name = "actix-codec" @@ -249,17 +224,6 @@ dependencies = [ "syn", ] -[[package]] -name = "actix_derive" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "addr2line" version = "0.24.2" @@ -739,15 +703,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -2682,7 +2637,6 @@ dependencies = [ name = "ten_manager" version = "0.1.0" dependencies = [ - "actix", "actix-cors", "actix-files", "actix-rt", diff --git a/core/src/ten_manager/Cargo.toml b/core/src/ten_manager/Cargo.toml index 85fc819bb..9b0892bee 100644 --- a/core/src/ten_manager/Cargo.toml +++ b/core/src/ten_manager/Cargo.toml @@ -8,7 +8,6 @@ version = "0.1.0" edition = "2021" [dependencies] -actix = "0.13" actix-cors = "0.7" actix-files = "0.6" actix-rt = "2.10" diff --git a/core/src/ten_manager/frontend/package.json b/core/src/ten_manager/frontend/package.json index 481ac71ba..2e2cb74e3 100644 --- a/core/src/ten_manager/frontend/package.json +++ b/core/src/ten_manager/frontend/package.json @@ -11,6 +11,8 @@ "license": "ISC", "description": "", "dependencies": { + "@xyflow/react": "^12.3.6", + "dagre": "^0.8.5", "i18next": "^24.0.5", "i18next-browser-languagedetector": "^8.0.2", "i18next-http-backend": "^3.0.1", @@ -19,12 +21,15 @@ "react-i18next": "^15.1.4" }, "devDependencies": { + "@types/dagre": "^0.7.52", "@types/react": "^19.0.1", "@types/react-dom": "^19.0.2", "clean-webpack-plugin": "^4.0.0", "copy-webpack-plugin": "^12.0.2", "css-loader": "^7.1.2", "html-webpack-plugin": "^5.6.3", + "sass": "^1.82.0", + "sass-loader": "^16.0.4", "style-loader": "^4.0.0", "ts-loader": "^9.5.1", "typescript": "^5.7.2", @@ -32,4 +37,4 @@ "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.1.0" } -} \ No newline at end of file +} diff --git a/core/src/ten_manager/frontend/src/App.tsx b/core/src/ten_manager/frontend/src/App.tsx index 173e8f839..aa476e7ef 100644 --- a/core/src/ten_manager/frontend/src/App.tsx +++ b/core/src/ten_manager/frontend/src/App.tsx @@ -4,8 +4,13 @@ // Licensed under the Apache License, Version 2.0, with certain conditions. // Refer to the "LICENSE" file in the root directory for more information. // -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useRef } from "react"; import { useTranslation } from "react-i18next"; +import AppBar from "./components/AppBar/AppBar"; +import FlowCanvas, { FlowCanvasRef } from "./flow/FlowCanvas"; +import SettingsPopup from "./components/SettingsPopup/SettingsPopup"; +import { useTheme } from "./hooks/useTheme"; +import "./theme/index.scss"; interface ApiResponse { status: string; @@ -18,10 +23,15 @@ interface DevServerVersion { } const App: React.FC = () => { - const { t, i18n } = useTranslation("common"); + const { t } = useTranslation("common"); const [version, setVersion] = useState(""); const [error, setError] = useState(""); + const [showSettings, setShowSettings] = useState(false); + const { theme, setTheme } = useTheme(); + const flowCanvasRef = useRef(null); + + // Get the version of tman. useEffect(() => { fetch("/api/dev-server/v1/version") .then((response) => { @@ -43,21 +53,25 @@ const App: React.FC = () => { }); }, []); - const changeLanguage = (lng: string) => { - i18n.changeLanguage(lng); + const handleAutoLayout = () => { + flowCanvasRef.current?.performAutoLayout(); }; return ( -
-
- - -
- - {error ? ( -

{t("error_fetching")}

- ) : ( -

{t("current_version", { version })}

+
+ setShowSettings(true)} + onAutoLayout={handleAutoLayout} + /> + + {showSettings && ( + setShowSettings(false)} + /> )}
); diff --git a/core/src/ten_manager/frontend/src/components/AboutPopup/AboutPopup.scss b/core/src/ten_manager/frontend/src/components/AboutPopup/AboutPopup.scss new file mode 100644 index 000000000..18111710d --- /dev/null +++ b/core/src/ten_manager/frontend/src/components/AboutPopup/AboutPopup.scss @@ -0,0 +1,24 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +.about-content { + text-align: center; + .powered-by { + font-style: italic; + font-size: 16px; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + margin-bottom: 20px; + } + + p { + margin: 5px 0; + } + + .about-link { + color: blue; + text-decoration: underline; + } +} diff --git a/core/src/ten_manager/frontend/src/components/AboutPopup/AboutPopup.tsx b/core/src/ten_manager/frontend/src/components/AboutPopup/AboutPopup.tsx new file mode 100644 index 000000000..bb3f2420a --- /dev/null +++ b/core/src/ten_manager/frontend/src/components/AboutPopup/AboutPopup.tsx @@ -0,0 +1,47 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +import React from "react"; +import Popup from "../Popup/Popup"; +import "./AboutPopup.scss"; + +interface AboutPopupProps { + onClose: () => void; +} + +const AboutPopup: React.FC = ({ onClose }) => { + return ( + +
+

Powered by TEN Framework.

+

+ Official site:{" "} + + https://www.theten.ai/ + +

+

+ Github:{" "} + + https://github.com/TEN-framework/ + +

+
+
+ ); +}; + +export default AboutPopup; diff --git a/core/src/ten_manager/frontend/src/components/AppBar/AppBar.scss b/core/src/ten_manager/frontend/src/components/AppBar/AppBar.scss new file mode 100644 index 000000000..ba222f8c8 --- /dev/null +++ b/core/src/ten_manager/frontend/src/components/AppBar/AppBar.scss @@ -0,0 +1,26 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +.app-bar { + display: flex; + justify-content: space-between; + align-items: center; + background: var(--app-bar-bg); + color: var(--app-bar-fg); + height: 40px; + padding: 0 20px; + font-size: 14px; + user-select: none; + + .app-bar-left { + display: flex; + align-items: center; + } + + .app-bar-right { + margin-left: auto; + } +} diff --git a/core/src/ten_manager/frontend/src/components/AppBar/AppBar.tsx b/core/src/ten_manager/frontend/src/components/AppBar/AppBar.tsx new file mode 100644 index 000000000..48f0497a7 --- /dev/null +++ b/core/src/ten_manager/frontend/src/components/AppBar/AppBar.tsx @@ -0,0 +1,125 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +import React, { useState, useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import FileMenu from "./FileMenu"; +import EditMenu from "./EditMenu"; +import HelpMenu from "./HelpMenu"; +import "./AppBar.scss"; + +interface AppBarProps { + // The current version of tman. + version: string; + + // An error message to be displayed if any issue occurs. + error: string; + + onOpenSettings: () => void; + onAutoLayout: () => void; +} + +type MenuType = "file" | "edit" | "help" | null; + +const AppBar: React.FC = ({ + version, + error, + onOpenSettings, + onAutoLayout, +}) => { + const { t } = useTranslation("common"); + const [openMenu, setOpenMenu] = useState(null); + const appBarRef = useRef(null); + + useEffect(() => { + const handleMouseDown = (event: MouseEvent) => { + const targetElement = event.target as HTMLElement; + + // Check if the click is inside the AppBar. + if (appBarRef.current && appBarRef.current.contains(targetElement)) { + // Check if the click is inside any menu-container. + const menuContainer = targetElement.closest(".menu-container"); + if (!menuContainer) { + // Clicked inside AppBar but not on any menu-container, close any open + // dropdown. + setOpenMenu(null); + } + // Else, let the menu component handle its own logic. + } else { + // Clicked outside AppBar, close any open dropdown. + setOpenMenu(null); + } + }; + + // Add the mousedown listener in the capturing phase. + // + // Note: It is necessary to intercept the `mousedown` event during the + // **capturing phase**, as third-party components like ReactFlow may + // intercept the event and prevent it from propagating further. Therefore, + // intercepting the event in the **bubbling phase** might not work as + // expected. + document.addEventListener("mousedown", handleMouseDown, true); + return () => { + document.removeEventListener("mousedown", handleMouseDown, true); + }; + }, []); + + // Function to open a specific menu or toggle it if it's already open. + const handleOpenMenu = (menu: MenuType) => { + setOpenMenu((prevMenu) => (prevMenu === menu ? null : menu)); + }; + + // Function to switch menus on hover only if a menu is already open. + const handleSwitchMenu = (menu: MenuType) => { + if (openMenu !== null && menu !== openMenu) { + setOpenMenu(menu); + } + }; + + // Function to close any open menu. + const closeMenu = () => { + setOpenMenu(null); + }; + + return ( +
+ {/* Left part is the menu itself. */} +
+ handleOpenMenu("file")} + onHover={() => handleSwitchMenu("file")} + closeMenu={closeMenu} + /> + handleOpenMenu("edit")} + onHover={() => handleSwitchMenu("edit")} + onOpenSettings={onOpenSettings} + closeMenu={closeMenu} + onAutoLayout={onAutoLayout} + /> + handleOpenMenu("help")} + onHover={() => handleSwitchMenu("help")} + closeMenu={closeMenu} + /> +
+ + {/* Right part is the logo. */} +
+ {error ? ( + {t("error_fetching")} + ) : ( + Powered by TEN Framework {version} + )} +
+
+ ); +}; + +export default AppBar; diff --git a/core/src/ten_manager/frontend/src/components/AppBar/DropdownMenu.scss b/core/src/ten_manager/frontend/src/components/AppBar/DropdownMenu.scss new file mode 100644 index 000000000..637344094 --- /dev/null +++ b/core/src/ten_manager/frontend/src/components/AppBar/DropdownMenu.scss @@ -0,0 +1,36 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +.menu-container { + position: relative; + margin-right: 20px; + cursor: pointer; + + .menu-title { + padding: 0 5px; + &:hover { + background: var(--menu-hover-bg); + } + } + + .dropdown-menu { + position: absolute; + top: 100%; + left: 0; + background: var(--dropdown-bg); + border: 1px solid var(--dropdown-border); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + min-width: 150px; + z-index: 100; + + .menu-item { + padding: 5px 10px; + &:hover { + background: var(--menu-hover-bg); + } + } + } +} diff --git a/core/src/ten_manager/frontend/src/components/AppBar/DropdownMenu.tsx b/core/src/ten_manager/frontend/src/components/AppBar/DropdownMenu.tsx new file mode 100644 index 000000000..7a5d86245 --- /dev/null +++ b/core/src/ten_manager/frontend/src/components/AppBar/DropdownMenu.tsx @@ -0,0 +1,35 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +import React from "react"; +import "./DropdownMenu.scss"; + +interface DropdownMenuProps { + title: string; + isOpen: boolean; + onClick: () => void; + onHover: () => void; + children: React.ReactNode; +} + +const DropdownMenu: React.FC = ({ + title, + isOpen, + onClick, + onHover, + children, +}) => { + return ( +
+
+ {title} +
+ {isOpen &&
{children}
} +
+ ); +}; + +export default DropdownMenu; diff --git a/core/src/ten_manager/frontend/src/components/AppBar/EditMenu.tsx b/core/src/ten_manager/frontend/src/components/AppBar/EditMenu.tsx new file mode 100644 index 000000000..d6fe195dc --- /dev/null +++ b/core/src/ten_manager/frontend/src/components/AppBar/EditMenu.tsx @@ -0,0 +1,57 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +import React from "react"; +import DropdownMenu from "./DropdownMenu"; + +interface EditMenuProps { + isOpen: boolean; + onClick: () => void; + onHover: () => void; + onOpenSettings: () => void; + closeMenu: () => void; + onAutoLayout: () => void; +} + +const EditMenu: React.FC = ({ + isOpen, + onClick, + onHover, + onOpenSettings, + closeMenu, + onAutoLayout, +}) => { + return ( + + {" "} +
{ + onAutoLayout(); + closeMenu(); + }} + > + Auto Layout +
+
{ + onOpenSettings(); + closeMenu(); + }} + > + Settings +
+
+ ); +}; + +export default EditMenu; diff --git a/core/src/ten_manager/frontend/src/components/AppBar/FileMenu.tsx b/core/src/ten_manager/frontend/src/components/AppBar/FileMenu.tsx new file mode 100644 index 000000000..30127f660 --- /dev/null +++ b/core/src/ten_manager/frontend/src/components/AppBar/FileMenu.tsx @@ -0,0 +1,42 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +import React from "react"; +import DropdownMenu from "./DropdownMenu"; + +interface FileMenuProps { + isOpen: boolean; + onClick: () => void; + onHover: () => void; + closeMenu: () => void; +} + +const FileMenu: React.FC = ({ + isOpen, + onClick, + onHover, + closeMenu, +}) => { + return ( + +
{ + closeMenu(); + }} + > + Open TEN app folder +
+
+ ); +}; + +export default FileMenu; diff --git a/core/src/ten_manager/frontend/src/components/AppBar/HelpMenu.tsx b/core/src/ten_manager/frontend/src/components/AppBar/HelpMenu.tsx new file mode 100644 index 000000000..c12776330 --- /dev/null +++ b/core/src/ten_manager/frontend/src/components/AppBar/HelpMenu.tsx @@ -0,0 +1,63 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +import React, { useState } from "react"; +import DropdownMenu from "./DropdownMenu"; +import AboutPopup from "../AboutPopup/AboutPopup"; + +interface HelpMenuProps { + isOpen: boolean; + onClick: () => void; + onHover: () => void; + closeMenu: () => void; +} + +const HelpMenu: React.FC = ({ + isOpen, + onClick, + onHover, + closeMenu, +}) => { + const [isDocumentationOpen, setIsDocumentationOpen] = useState(false); + const [isAboutOpen, setIsAboutOpen] = useState(false); + + const openDocumentation = () => { + setIsDocumentationOpen(true); + closeMenu(); + }; + + const closeDocumentation = () => { + setIsDocumentationOpen(false); + }; + + const openAbout = () => { + setIsAboutOpen(true); + closeMenu(); + }; + + const closeAbout = () => { + setIsAboutOpen(false); + }; + + return ( + <> + +
+ About +
+
+ + {isAboutOpen && } + + ); +}; + +export default HelpMenu; diff --git a/core/src/ten_manager/frontend/src/components/Popup/Popup.scss b/core/src/ten_manager/frontend/src/components/Popup/Popup.scss new file mode 100644 index 000000000..59963b966 --- /dev/null +++ b/core/src/ten_manager/frontend/src/components/Popup/Popup.scss @@ -0,0 +1,67 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +.popup { + position: fixed; + width: 400px; + background: var(--popup-bg); + border: 1px solid var(--popup-border); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); + font-size: 14px; + color: var(--popup-fg); + border-radius: 4px; + z-index: 1001; // Ensure it's above other elements. + + // Hide the transient movement when the popup initially appears. + opacity: 0; // Default opacity is 0. + transition: opacity 0.3s ease; + + &.visible { + opacity: 1; + } + + &.hidden { + opacity: 0; + } + + &:focus { + outline: none; + } + + .popup-header { + background: var(--popup-header-bg); + padding: 10px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: move; + user-select: none; + border-bottom: 1px solid var(--dropdown-border); + + .popup-title { + font-weight: bold; + } + + .popup-buttons { + display: flex; + align-items: center; + + button { + background: transparent; + border: none; + cursor: pointer; + color: var(--popup-fg); + font-size: 16px; + line-height: 1; + margin-left: 5px; + } + } + } + + .popup-content { + padding: 10px; + } +} diff --git a/core/src/ten_manager/frontend/src/components/Popup/Popup.tsx b/core/src/ten_manager/frontend/src/components/Popup/Popup.tsx new file mode 100644 index 000000000..0a2b23ccf --- /dev/null +++ b/core/src/ten_manager/frontend/src/components/Popup/Popup.tsx @@ -0,0 +1,140 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +import React, { useState, useEffect, useRef } from "react"; +import "./Popup.scss"; + +interface PopupProps { + title: string; + onClose: () => void; + children: React.ReactNode; +} + +const Popup: React.FC = ({ title, onClose, children }) => { + const [isCollapsed, setIsCollapsed] = useState(false); + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [dragging, setDragging] = useState(false); + const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>( + null + ); + const [isVisible, setIsVisible] = useState(false); + const popupRef = useRef(null); + + // Center the popup on mount. + useEffect(() => { + if (popupRef.current) { + const { innerWidth, innerHeight } = window; + const rect = popupRef.current.getBoundingClientRect(); + setPosition({ + x: (innerWidth - rect.width) / 2, + y: (innerHeight - rect.height) / 2, + }); + popupRef.current.focus(); + } + setIsVisible(true); + }, []); + + // Handle keydown for ESC. + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }; + + // Only add listener if this popup is focused. + const currentPopup = popupRef.current; + if (currentPopup) { + currentPopup.addEventListener("keydown", handleKeyDown); + } + + return () => { + if (currentPopup) { + currentPopup.removeEventListener("keydown", handleKeyDown); + } + }; + }, [onClose]); + + const handleMouseDown = (e: React.MouseEvent) => { + setDragging(true); + setDragStart({ x: e.clientX - position.x, y: e.clientY - position.y }); + }; + + const handleMouseMove = (e: MouseEvent) => { + if (dragging && dragStart) { + setPosition({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y }); + } + }; + + const handleMouseUp = () => { + setDragging(false); + setDragStart(null); + }; + + useEffect(() => { + if (dragging) { + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); + } else { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + } + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + }; + }, [dragging, dragStart]); + + const toggleCollapse = () => { + setIsCollapsed((prev) => !prev); + }; + + // Handle focus when clicked. + const handleClick = () => { + popupRef.current?.focus(); + bringToFront(); + }; + + const bringToFront = () => { + const highestZIndex = Math.max( + ...Array.from(document.querySelectorAll(".popup")).map( + (el) => parseInt(window.getComputedStyle(el).zIndex) || 0 + ) + ); + if (popupRef.current) { + popupRef.current.style.zIndex = (highestZIndex + 1).toString(); + } + }; + + return ( +
+
+ {title} +
+ + +
+
+ {!isCollapsed &&
{children}
} +
+ ); +}; + +export default Popup; diff --git a/core/src/ten_manager/frontend/src/components/SettingsPopup/SettingsPopup.scss b/core/src/ten_manager/frontend/src/components/SettingsPopup/SettingsPopup.scss new file mode 100644 index 000000000..24f4f15ea --- /dev/null +++ b/core/src/ten_manager/frontend/src/components/SettingsPopup/SettingsPopup.scss @@ -0,0 +1,26 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +.theme-toggle { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0px; + + button { + background: var(--button-bg); + color: var(--button-fg); + border: 1px solid var(--button-border); + padding: 5px 10px; + cursor: pointer; + border-radius: 4px; + transition: background 0.3s; + + &:hover { + background: var(--button-hover-bg); + } + } +} diff --git a/core/src/ten_manager/frontend/src/components/SettingsPopup/SettingsPopup.tsx b/core/src/ten_manager/frontend/src/components/SettingsPopup/SettingsPopup.tsx new file mode 100644 index 000000000..c7a48c575 --- /dev/null +++ b/core/src/ten_manager/frontend/src/components/SettingsPopup/SettingsPopup.tsx @@ -0,0 +1,38 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +import React from "react"; +import Popup from "../Popup/Popup"; +import "./SettingsPopup.scss"; + +interface SettingsPopupProps { + theme: string; + onChangeTheme: (theme: string) => void; + onClose: () => void; +} + +const SettingsPopup: React.FC = ({ + theme, + onChangeTheme, + onClose, +}) => { + const toggleTheme = () => { + onChangeTheme(theme === "light" ? "dark" : "light"); + }; + + return ( + +
+ Theme: {theme.charAt(0).toUpperCase() + theme.slice(1)} + +
+
+ ); +}; + +export default SettingsPopup; diff --git a/core/src/ten_manager/frontend/src/flow/CustomNode.tsx b/core/src/ten_manager/frontend/src/flow/CustomNode.tsx new file mode 100644 index 000000000..2ecf7ea9a --- /dev/null +++ b/core/src/ten_manager/frontend/src/flow/CustomNode.tsx @@ -0,0 +1,59 @@ +import { memo, CSSProperties } from "react"; +import { + Handle, + Position, + NodeProps, + Connection, + Edge, + Node, +} from "@xyflow/react"; + +const targetHandleStyle: CSSProperties = { background: "#555" }; +const sourceHandleStyle: CSSProperties = { ...targetHandleStyle }; + +const onConnect = (params: Connection | Edge) => + console.log("Handle onConnect", params); + +export type CustomNodeType = Node< + { label: string; sourceCmds: string[]; targetCmds: string[] }, + "customNode" +>; + +export function CustomNode({ data, isConnectable }: NodeProps) { + return ( +
+ {/* Render target handles (for incoming edges) */} + {data.targetCmds.map((cmd, index) => { + return ( + + ); + })} + +
{data.label}
+ + {/* Render source handles (for outgoing edges) */} + {data.sourceCmds.map((cmd, index) => { + return ( + + ); + })} +
+ ); +} + +export default memo(CustomNode); diff --git a/core/src/ten_manager/frontend/src/flow/FlowCanvas.tsx b/core/src/ten_manager/frontend/src/flow/FlowCanvas.tsx new file mode 100644 index 000000000..2a773a90f --- /dev/null +++ b/core/src/ten_manager/frontend/src/flow/FlowCanvas.tsx @@ -0,0 +1,336 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +import { + useEffect, + useState, + useCallback, + forwardRef, + useImperativeHandle, + MouseEvent, +} from "react"; +import { + ReactFlow, + addEdge, + MiniMap, + Controls, + Edge, + Node, + applyNodeChanges, + applyEdgeChanges, + Connection, + MarkerType, + BuiltInNode, +} from "@xyflow/react"; +import dagre from "dagre"; +import CustomNode, { CustomNodeType } from "./CustomNode"; + +interface ApiResponse { + status: string; + data: T; + meta?: any; +} + +interface BackendNode { + addon: string; + name: string; + extension_group: string; + app: string; + property: any; + api?: any; +} + +interface BackendConnection { + app: string; + extension_group: string; + extension: string; + cmd?: { + name: string; + dest: { + app: string; + extension_group: string; + extension: string; + }[]; + }[]; +} + +const nodeWidth = 172; +const nodeHeight = 36; + +const getLayoutedElements = ( + nodes: MyNodeType[], + edges: Edge[], + direction = "TB" +) => { + const dagreGraph = new dagre.graphlib.Graph(); + dagreGraph.setDefaultEdgeLabel(() => ({})); + dagreGraph.setGraph({ rankdir: direction }); + + nodes.forEach((node) => { + dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); + }); + + edges.forEach((edge) => { + dagreGraph.setEdge(edge.source, edge.target); + }); + + dagre.layout(dagreGraph); + + const layoutedNodes = nodes.map((node) => { + const nodeWithPosition = dagreGraph.node(node.id); + return { + ...node, + position: { + x: nodeWithPosition.x - nodeWidth / 2, + y: nodeWithPosition.y - nodeHeight / 2, + }, + }; + }); + + return { nodes: layoutedNodes, edges }; +}; + +export interface FlowCanvasRef { + performAutoLayout: () => void; +} + +export type MyNodeType = BuiltInNode | CustomNodeType; + +const FlowCanvas = forwardRef((props, ref) => { + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); + const [error, setError] = useState(""); + + const [contextMenu, setContextMenu] = useState<{ + visible: boolean; + x: number; + y: number; + type?: "node" | "edge"; + data?: any; + }>({ visible: false, x: 0, y: 0 }); + + // Close the context menu. + const closeContextMenu = useCallback(() => { + setContextMenu({ visible: false, x: 0, y: 0 }); + }, []); + + // Click on the blank space to close the context menu. + useEffect(() => { + const handleClick = () => { + closeContextMenu(); + }; + window.addEventListener("click", handleClick); + return () => window.removeEventListener("click", handleClick); + }, [closeContextMenu]); + + // Export performAutoLayout function. + const performAutoLayout = useCallback(() => { + const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements( + nodes, + edges + ); + setNodes(layoutedNodes); + setEdges(layoutedEdges); + }, [nodes, edges]); + + useImperativeHandle(ref, () => ({ + performAutoLayout, + })); + + useEffect(() => { + const fetchData = async () => { + try { + const nodesRes = await fetch( + `/api/dev-server/v1/graphs/http_service/nodes` + ); + if (!nodesRes.ok) { + throw new Error(`Failed to fetch nodes: ${nodesRes.status}`); + } + const nodesPayload: ApiResponse = await nodesRes.json(); + + const connectionsRes = await fetch( + `/api/dev-server/v1/graphs/http_service/connections` + ); + if (!connectionsRes.ok) { + throw new Error( + `Failed to fetch connections: ${connectionsRes.status}` + ); + } + const connectionsPayload: ApiResponse = + await connectionsRes.json(); + + // Create initial node. + let initialNodes: MyNodeType[] = nodesPayload.data.map((n, index) => ({ + id: n.name, + position: { x: index * 200, y: 100 }, + type: "customNode", + data: { + label: `${n.name}`, + sourceCmds: [], + targetCmds: [], + }, + })); + + // Analyze edges, collect cmds for all nodes, so that we can generate + // corresponding handles later. + let initialEdges: Edge[] = []; + const nodeSourceCmdMap: Record> = {}; + const nodeTargetCmdMap: Record> = {}; + + connectionsPayload.data.forEach((c) => { + const sourceNodeId = c.extension; + if (c.cmd) { + c.cmd.forEach((cmdItem) => { + cmdItem.dest.forEach((d) => { + const targetNodeId = d.extension; + const edgeId = `edge-${sourceNodeId}-${cmdItem.name}-${targetNodeId}`; + const cmdName = cmdItem.name; + + // Record the cmd name of the source node. + if (!nodeSourceCmdMap[sourceNodeId]) { + nodeSourceCmdMap[sourceNodeId] = new Set(); + } + nodeSourceCmdMap[sourceNodeId].add(cmdName); + + // Record the cmd name of the target node. + if (!nodeTargetCmdMap[targetNodeId]) { + nodeTargetCmdMap[targetNodeId] = new Set(); + } + nodeTargetCmdMap[targetNodeId].add(cmdName); + + initialEdges.push({ + id: edgeId, + source: sourceNodeId, + target: targetNodeId, + type: "default", + label: cmdName, + sourceHandle: `source-${cmdName}`, + targetHandle: `target-${cmdName}`, + markerEnd: { + type: MarkerType.ArrowClosed, + }, + }); + }); + }); + } + }); + + // Write cmds back to Nodes's data to let CustomNode dynamically + // generate handles. + initialNodes = initialNodes.map((node) => { + const sourceCmds = nodeSourceCmdMap[node.id] + ? Array.from(nodeSourceCmdMap[node.id]) + : []; + const targetCmds = nodeTargetCmdMap[node.id] + ? Array.from(nodeTargetCmdMap[node.id]) + : []; + return { + ...node, + type: "customNode", + data: { + ...node.data, + label: node.data.label || `${node.id}`, + sourceCmds, + targetCmds, + }, + }; + }); + + const { nodes: layoutedNodes, edges: layoutedEdges } = + getLayoutedElements(initialNodes, initialEdges); + + setNodes(layoutedNodes); + setEdges(layoutedEdges); + } catch (err: any) { + console.error(err); + setError("Failed to fetch workflow data."); + } + }; + fetchData(); + }, []); + + const onConnect = useCallback( + (params: Connection | Edge) => { + setEdges((eds) => addEdge(params, eds)); + }, + [setEdges] + ); + + // Right-click Node. + const onNodeContextMenu = useCallback((event: MouseEvent, node: Node) => { + event.preventDefault(); + setContextMenu({ + visible: true, + x: event.clientX, + y: event.clientY, + type: "node", + data: node, + }); + }, []); + + // Right-click Edge. + const onEdgeContextMenu = useCallback((event: MouseEvent, edge: Edge) => { + event.preventDefault(); + setContextMenu({ + visible: true, + x: event.clientX, + y: event.clientY, + type: "edge", + data: edge, + }); + }, []); + + return ( +
+ + setNodes((nds) => applyNodeChanges(changes, nds)) + } + onEdgesChange={(changes) => + setEdges((eds) => applyEdgeChanges(changes, eds)) + } + onConnect={(p) => onConnect(p)} + fitView + nodesDraggable={true} + edgesFocusable={true} + style={{ width: "100%", height: "100%" }} + onNodeContextMenu={onNodeContextMenu} + onEdgeContextMenu={onEdgeContextMenu} + > + + + + {error &&
{error}
} + + {contextMenu.visible && ( +
+
Fake Menu Item
+
+ )} +
+ ); +}); + +export default FlowCanvas; diff --git a/core/src/ten_manager/frontend/src/hooks/useTheme.ts b/core/src/ten_manager/frontend/src/hooks/useTheme.ts new file mode 100644 index 000000000..85ebbfab2 --- /dev/null +++ b/core/src/ten_manager/frontend/src/hooks/useTheme.ts @@ -0,0 +1,17 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +import { useState, useEffect } from "react"; + +export const useTheme = () => { + const [theme, setTheme] = useState("light"); + + useEffect(() => { + document.documentElement.setAttribute("data-theme", theme); + }, [theme]); + + return { theme, setTheme }; +}; diff --git a/core/src/ten_manager/frontend/src/index.tsx b/core/src/ten_manager/frontend/src/index.tsx index 2b9e417ca..35a57965e 100644 --- a/core/src/ten_manager/frontend/src/index.tsx +++ b/core/src/ten_manager/frontend/src/index.tsx @@ -11,6 +11,9 @@ import App from "./App"; // Import and initialize i18n. import "./i18n"; +// Import react-flow style. +import "@xyflow/react/dist/style.css"; + const rootElement = document.getElementById("root"); if (rootElement) { diff --git a/core/src/ten_manager/frontend/src/theme/_dark.scss b/core/src/ten_manager/frontend/src/theme/_dark.scss new file mode 100644 index 000000000..aa1cfce3a --- /dev/null +++ b/core/src/ten_manager/frontend/src/theme/_dark.scss @@ -0,0 +1,21 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +[data-theme="dark"] { + --app-bar-bg: #333333; + --app-bar-fg: #ffffff; + --menu-hover-bg: #333333; + --dropdown-bg: #555555; + --dropdown-border: #666666; + --popup-bg: #444444; + --popup-fg: #ffffff; + --popup-border: #666666; + --popup-header-bg: #555555; + --button-bg: #666666; + --button-fg: #ffffff; + --button-border: #777777; + --button-hover-bg: #777777; +} diff --git a/core/src/ten_manager/frontend/src/theme/_light.scss b/core/src/ten_manager/frontend/src/theme/_light.scss new file mode 100644 index 000000000..2c7e8fcec --- /dev/null +++ b/core/src/ten_manager/frontend/src/theme/_light.scss @@ -0,0 +1,21 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +[data-theme="light"] { + --app-bar-bg: #f0f0f0; + --app-bar-fg: #000000; + --menu-hover-bg: #f0f0f0; + --dropdown-bg: #ffffff; + --dropdown-border: #cccccc; + --popup-bg: #ffffff; + --popup-fg: #000000; + --popup-border: #cccccc; + --popup-header-bg: #f5f5f5; + --button-bg: #e0e0e0; + --button-fg: #000000; + --button-border: #cccccc; + --button-hover-bg: #d5d5d5; +} diff --git a/core/src/ten_manager/frontend/src/theme/_reactflow.scss b/core/src/ten_manager/frontend/src/theme/_reactflow.scss new file mode 100644 index 000000000..fef7a22ee --- /dev/null +++ b/core/src/ten_manager/frontend/src/theme/_reactflow.scss @@ -0,0 +1,107 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +.react-flow { + // Custom Variables + --xy-theme-selected: #f57dbd; + --xy-theme-hover: #c5c5c5; + --xy-theme-edge-hover: black; + --xy-theme-color-focus: #e8e8e8; + + // Built-in Variables see https://reactflow.dev/learn/customization/theming + --xy-node-border-default: 1px solid #ededed; + + --xy-node-boxshadow-default: 0px 3.54px 4.55px 0px #00000005, + 0px 3.54px 4.55px 0px #0000000d, 0px 0.51px 1.01px 0px #0000001a; + + --xy-node-border-radius-default: 8px; + + --xy-handle-background-color-default: #ffffff; + --xy-handle-border-color-default: #aaaaaa; + + --xy-edge-label-color-default: #505050; +} + +.react-flow.dark { + --xy-node-boxshadow-default: 0px 3.54px 4.55px 0px rgba(255, 255, 255, 0.05), + 0px 3.54px 4.55px 0px rgba(255, 255, 255, 0.13), + 0px 0.51px 1.01px 0px rgba(255, 255, 255, 0.2); +} + +// Customizing Default Theming + +.react-flow__node { + box-shadow: var(--xy-node-boxshadow-default); + border-radius: var(--xy-node-border-radius-default); + background-color: var(--xy-node-background-color-default); + display: flex; + justify-content: center; + align-items: center; + text-align: center; + padding: 10px; + font-size: 12px; + flex-direction: column; + border: var(--xy-node-border-default); + color: var(--xy-node-color, var(--xy-node-color-default)); +} + +.react-flow__node.selectable:focus { + box-shadow: 0px 0px 0px 4px var(--xy-theme-color-focus); + border-color: #d9d9d9; +} + +.react-flow__node.selectable:focus:active { + box-shadow: var(--xy-node-boxshadow-default); +} + +.react-flow__node.selectable:hover, +.react-flow__node.draggable:hover { + border-color: var(--xy-theme-hover); +} + +.react-flow__node.selectable.selected { + border-color: var(--xy-theme-selected); + box-shadow: var(--xy-node-boxshadow-default); +} + +.react-flow__node-group { + background-color: rgba(207, 182, 255, 0.4); + border-color: #9e86ed; +} + +.react-flow__edge.selectable:hover .react-flow__edge-path, +.react-flow__edge.selectable.selected .react-flow__edge-path { + stroke: var(--xy-theme-edge-hover); +} + +.react-flow__handle { + background-color: var(--xy-handle-background-color-default); +} + +.react-flow__handle.connectionindicator:hover { + pointer-events: all; + border-color: var(--xy-theme-edge-hover); + background-color: white; +} + +.react-flow__handle.connectionindicator:focus, +.react-flow__handle.connectingfrom, +.react-flow__handle.connectingto { + border-color: var(--xy-theme-edge-hover); +} + +.react-flow__node-resizer { + border-radius: 0; + border: none; +} + +.react-flow__resize-control.handle { + background-color: #ffffff; + border-color: #9e86ed; + border-radius: 0; + width: 5px; + height: 5px; +} diff --git a/core/src/ten_manager/frontend/src/theme/_variables.scss b/core/src/ten_manager/frontend/src/theme/_variables.scss new file mode 100644 index 000000000..03e809b98 --- /dev/null +++ b/core/src/ten_manager/frontend/src/theme/_variables.scss @@ -0,0 +1,26 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +:root { + --app-bar-height: 40px; + --app-bar-padding: 0 20px; + --app-bar-font-size: 14px; + + // Default theme. + --app-bar-bg: #ffffff; + --app-bar-fg: #000000; + --menu-hover-bg: #f0f0f0; + --dropdown-bg: #ffffff; + --dropdown-border: #cccccc; + --popup-bg: #ffffff; + --popup-fg: #000000; + --popup-border: #cccccc; + --popup-header-bg: #f5f5f5; + --button-bg: #e0e0e0; + --button-fg: #000000; + --button-border: #cccccc; + --button-hover-bg: #d5d5d5; +} diff --git a/core/src/ten_manager/frontend/src/theme/index.scss b/core/src/ten_manager/frontend/src/theme/index.scss new file mode 100644 index 000000000..23c1eb3e2 --- /dev/null +++ b/core/src/ten_manager/frontend/src/theme/index.scss @@ -0,0 +1,22 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +@use "variables"; +@use "light"; +@use "dark"; +@use "reactflow"; + +// Global style. +body, +html, +#root { + margin: 0; + padding: 0; + height: 100%; + background-color: var(--popup-bg); + color: var(--app-bar-fg); + font-family: Arial, sans-serif; +} diff --git a/core/src/ten_manager/frontend/webpack.config.js b/core/src/ten_manager/frontend/webpack.config.js index d583993a4..092d4b1aa 100644 --- a/core/src/ten_manager/frontend/webpack.config.js +++ b/core/src/ten_manager/frontend/webpack.config.js @@ -33,6 +33,10 @@ module.exports = { test: /\.css$/i, use: ["style-loader", "css-loader"], }, + { + test: /\.scss$/, + use: ["style-loader", "css-loader", "sass-loader"], + }, // Other loaders (such as images, fonts) can be added here. ], }, diff --git a/core/src/ten_manager/src/cmd/cmd_dev_server.rs b/core/src/ten_manager/src/cmd/cmd_dev_server.rs index b7fb94b97..9b43d440a 100644 --- a/core/src/ten_manager/src/cmd/cmd_dev_server.rs +++ b/core/src/ten_manager/src/cmd/cmd_dev_server.rs @@ -10,6 +10,7 @@ use std::{ }; use actix_cors::Cors; +use actix_files::Files; use actix_web::{http::header, web, App, HttpServer}; use anyhow::{Ok, Result}; use clap::{value_parser, Arg, ArgMatches, Command}; @@ -17,7 +18,12 @@ use console::Emoji; use crate::{ config::TmanConfig, - dev_server::{configure_routes, DevServerState}, + dev_server::{ + configure_routes, + // TODO(Wei): Enable this. + // frontend::get_frontend_asset, + DevServerState, + }, error::TmanError, log::tman_verbose_println, utils::{check_is_app_folder, get_cwd}, @@ -28,6 +34,7 @@ pub struct DevServerCommand { pub ip_address: String, pub port: u16, pub base_dir: Option, + pub external_frontend_asset_path: Option, } pub fn create_sub_cmd(_args_cfg: &crate::cmd_line::ArgsCfg) -> Command { @@ -67,6 +74,17 @@ pub fn create_sub_cmd(_args_cfg: &crate::cmd_line::ArgsCfg) -> Command { .help("The base directory") .required(false), ) + // This is a hidden feature that allows the use of frontend asset + // resources from the file system instead of the frontend resources + // originally bundled into the tman executable, providing a bit more + // flexibility. + .arg( + Arg::new("EXTERNAL_FRONTEND_ASSET_PATH") + .long("external-frontend-asset-path") + .help("Sets the external frontend asset path") + .required(false) + .hide(true), + ) } pub fn parse_sub_cmd( @@ -79,6 +97,9 @@ pub fn parse_sub_cmd( .to_string(), port: *sub_cmd_args.get_one::("PORT").unwrap(), base_dir: sub_cmd_args.get_one::("BASE_DIR").cloned(), + external_frontend_asset_path: sub_cmd_args + .get_one::("EXTERNAL_FRONTEND_ASSET_PATH") + .cloned(), }; cmd @@ -121,10 +142,26 @@ pub async fn execute_cmd( .allowed_header(header::CONTENT_TYPE) .max_age(3600); - App::new() + let mut app = App::new() .app_data(state.clone()) .wrap(cors) - .configure(|cfg| configure_routes(cfg, state.clone())) + .configure(|cfg| configure_routes(cfg, state.clone())); + + if let Some(external_frontend_asset_path) = + &command_data.external_frontend_asset_path + { + let static_files = Files::new("/", external_frontend_asset_path) + .index_file("index.html") + .use_last_modified(true) + .use_etag(true); + + app = app.service(static_files); + } else { + // TODO(Wei): Enable this. + // app = app.default_service(web::route().to(get_frontend_asset)); + } + + app }); let bind_address = diff --git a/core/src/ten_manager/src/cmd_line.rs b/core/src/ten_manager/src/cmd_line.rs index e38d3e38a..cbf396665 100644 --- a/core/src/ten_manager/src/cmd_line.rs +++ b/core/src/ten_manager/src/cmd_line.rs @@ -75,13 +75,6 @@ fn create_cmd() -> clap::ArgMatches { .help("The user token") .default_value(None), ) - .arg( - Arg::new("MI") - .long("mi") - .help("Machine interface") - .action(clap::ArgAction::SetTrue) - .hide(true), - ) .arg( Arg::new("VERBOSE") .long("verbose") @@ -95,6 +88,13 @@ fn create_cmd() -> clap::ArgMatches { .help("Automatically answer 'yes' to all prompts") .action(clap::ArgAction::SetTrue), ) + .arg( + Arg::new("MI") + .long("mi") + .help("Machine interface mode") + .action(clap::ArgAction::SetTrue) + .hide(true), + ) .subcommand(crate::cmd::cmd_install::create_sub_cmd(&args_cfg)) .subcommand(crate::cmd::cmd_uninstall::create_sub_cmd(&args_cfg)) .subcommand(crate::cmd::cmd_package::create_sub_cmd(&args_cfg)) @@ -113,9 +113,9 @@ pub fn parse_cmd( tman_config.config_file = matches.get_one::("CONFIG_FILE").cloned(); tman_config.admin_token = matches.get_one::("ADMIN_TOKEN").cloned(); tman_config.user_token = matches.get_one::("USER_TOKEN").cloned(); - tman_config.mi_mode = matches.get_flag("MI"); tman_config.verbose = matches.get_flag("VERBOSE"); tman_config.assume_yes = matches.get_flag("ASSUME_YES"); + tman_config.mi_mode = matches.get_flag("MI"); match matches.subcommand() { Some(("install", sub_cmd_args)) => crate::cmd::CommandData::Install( diff --git a/core/src/ten_manager/src/config.rs b/core/src/ten_manager/src/config.rs index 34b09db16..a128dfbdf 100644 --- a/core/src/ten_manager/src/config.rs +++ b/core/src/ten_manager/src/config.rs @@ -33,9 +33,10 @@ pub struct TmanConfig { pub admin_token: Option, pub user_token: Option, - pub mi_mode: bool, pub verbose: bool, pub assume_yes: bool, + + pub mi_mode: bool, } // Determine the config file path based on the platform. @@ -93,8 +94,8 @@ pub fn read_config(config_file_path: &Option) -> TmanConfig { config_file: Some(config_path.to_string_lossy().to_string()), admin_token: config_file_content.admin_token, user_token: config_file_content.user_token, - mi_mode: false, verbose: false, assume_yes: false, + mi_mode: false, } } diff --git a/core/src/ten_manager/src/dev_server/frontend.rs b/core/src/ten_manager/src/dev_server/frontend.rs index be199c4a1..fe86c6da7 100644 --- a/core/src/ten_manager/src/dev_server/frontend.rs +++ b/core/src/ten_manager/src/dev_server/frontend.rs @@ -4,8 +4,41 @@ // Licensed under the Apache License, Version 2.0, with certain conditions. // Refer to the "LICENSE" file in the root directory for more information. // +use actix_web::{HttpRequest, HttpResponse, Responder}; +use mime_guess::from_path; use rust_embed::RustEmbed; #[derive(RustEmbed)] #[folder = "frontend/dist/"] // Points to the frontend build output directory. -pub struct Asset; +struct Asset; + +pub async fn get_frontend_asset(req: HttpRequest) -> impl Responder { + let path = req.path().trim_start_matches('/').to_owned(); + + if path.is_empty() { + // If the root path is requested, return `index.html`. + match Asset::get("index.html") { + Some(content) => HttpResponse::Ok() + .content_type("text/html") + .body(content.data.into_owned()), + None => HttpResponse::NotFound().body("404 Not Found"), + } + } else { + match Asset::get(&path) { + Some(content) => { + let mime = from_path(&path).first_or_octet_stream(); + HttpResponse::Ok() + .content_type(mime.as_ref()) + .body(content.data.into_owned()) + } + // If the file is not found, return `index.html` to support React + // Router. + None => match Asset::get("index.html") { + Some(content) => HttpResponse::Ok() + .content_type("text/html") + .body(content.data.into_owned()), + None => HttpResponse::NotFound().body("404 Not Found"), + }, + } + } +} diff --git a/core/src/ten_manager/src/dev_server/mod.rs b/core/src/ten_manager/src/dev_server/mod.rs index 783f1c54f..8a8624673 100644 --- a/core/src/ten_manager/src/dev_server/mod.rs +++ b/core/src/ten_manager/src/dev_server/mod.rs @@ -6,7 +6,8 @@ // mod addons; mod common; -mod frontend; +// TODO(Wei): Enable this. +// pub mod frontend; mod get_all_pkgs; pub mod graphs; mod manifest; @@ -19,10 +20,8 @@ mod version; use std::sync::{Arc, RwLock}; -use actix_web::{web, HttpRequest, HttpResponse, Responder}; -use mime_guess::from_path; +use actix_web::web; -use frontend::Asset; use ten_rust::pkg_info::PkgInfo; use super::config::TmanConfig; @@ -34,37 +33,6 @@ pub struct DevServerState { pub tman_config: TmanConfig, } -async fn get_frontend_asset(req: HttpRequest) -> impl Responder { - let path = req.path().trim_start_matches('/').to_owned(); - - if path.is_empty() { - // If the root path is requested, return `index.html`. - match Asset::get("index.html") { - Some(content) => HttpResponse::Ok() - .content_type("text/html") - .body(content.data.into_owned()), - None => HttpResponse::NotFound().body("404 Not Found"), - } - } else { - match Asset::get(&path) { - Some(content) => { - let mime = from_path(&path).first_or_octet_stream(); - HttpResponse::Ok() - .content_type(mime.as_ref()) - .body(content.data.into_owned()) - } - // If the file is not found, return `index.html` to support React - // Router. - None => match Asset::get("index.html") { - Some(content) => HttpResponse::Ok() - .content_type("text/html") - .body(content.data.into_owned()), - None => HttpResponse::NotFound().body("404 Not Found"), - }, - } - } -} - pub fn configure_routes( cfg: &mut web::ServiceConfig, state: web::Data>>, @@ -114,40 +82,5 @@ pub fn configure_routes( .route( "/api/dev-server/v1/messages/compatible", web::post().to(messages::compatible::get_compatible_messages), - ) - .default_service(web::route().to(get_frontend_asset)); -} - -#[cfg(test)] -mod tests { - use super::*; - use actix_web::{http::StatusCode, test, App}; - - #[actix_web::test] - async fn test_undefined_endpoint() { - // Initialize the DevServerState. - let state = web::Data::new(Arc::new(RwLock::new(DevServerState { - base_dir: None, - all_pkgs: None, - tman_config: TmanConfig::default(), - }))); - - // Create the App with the routes configured. - let app = test::init_service( - App::new().configure(|cfg| configure_routes(cfg, state.clone())), - ) - .await; - - // Send a request to an undefined endpoint. - let req = test::TestRequest::get().uri("/undefined/path").to_request(); - let resp = test::call_service(&app, req).await; - - // Check that the response status is 404 Not Found. - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - - // Check the response body. - let body = test::read_body(resp).await; - let expected_body = "Endpoint '/undefined/path' not found"; - assert_eq!(body, expected_body); - } + ); } diff --git a/core/src/ten_manager/src/error.rs b/core/src/ten_manager/src/error.rs index 108c6ae51..a90db1ac7 100644 --- a/core/src/ten_manager/src/error.rs +++ b/core/src/ten_manager/src/error.rs @@ -6,10 +6,8 @@ // #[derive(Debug)] pub enum TmanError { - IsNotAppDirectory, FileNotFound(String), ReadFileContentError(String), - IncorrectAppContent(String), InvalidPath(String, String), Custom(String), } @@ -19,22 +17,12 @@ pub enum TmanError { impl std::fmt::Display for TmanError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - TmanError::IsNotAppDirectory => { - write!(f, "The current working directory does not belong to the `app`.") - } TmanError::FileNotFound(file_path) => { write!(f, "'{}' is not found.", file_path) } TmanError::ReadFileContentError(file_path) => { write!(f, "Failed to read '{}'.", file_path) } - TmanError::IncorrectAppContent(root_cause) => { - write!( - f, - "The content of the TEN app is incorrect: {}.", - root_cause - ) - } TmanError::InvalidPath(path, root_cause) => { write!(f, "The path '{}' is not valid: {}.", path, root_cause) } diff --git a/core/src/ten_manager/src/main.rs b/core/src/ten_manager/src/main.rs index 7e79525b5..3d9ad2ff9 100644 --- a/core/src/ten_manager/src/main.rs +++ b/core/src/ten_manager/src/main.rs @@ -23,9 +23,9 @@ fn merge(cmd_line: TmanConfig, config_file: TmanConfig) -> TmanConfig { config_file: cmd_line.config_file, admin_token: cmd_line.admin_token.or(config_file.admin_token), user_token: cmd_line.user_token.or(config_file.user_token), - mi_mode: cmd_line.mi_mode, verbose: cmd_line.verbose, assume_yes: cmd_line.assume_yes, + mi_mode: cmd_line.mi_mode, } } diff --git a/core/src/ten_manager/src/solver/solve.rs b/core/src/ten_manager/src/solver/solve.rs index 9858c66f2..89d897036 100644 --- a/core/src/ten_manager/src/solver/solve.rs +++ b/core/src/ten_manager/src/solver/solve.rs @@ -51,6 +51,7 @@ fn get_model( Some(result) } +#[allow(dead_code)] fn print_prefix(tman_config: &TmanConfig, depth: u8) { tman_verbose_println!(tman_config, ""); for _ in 0..depth { @@ -59,6 +60,7 @@ fn print_prefix(tman_config: &TmanConfig, depth: u8) { } // Recursively print the configuration object. +#[allow(dead_code)] fn print_configuration( tman_config: &TmanConfig, conf: &Configuration, @@ -102,6 +104,7 @@ fn print_configuration( } // recursively print the statistics object +#[allow(dead_code)] fn print_statistics( tman_config: &TmanConfig, stats: &Statistics, @@ -152,6 +155,7 @@ fn print_statistics( } } +#[allow(unused_assignments)] fn solve( tman_config: &TmanConfig, input: &str, @@ -278,8 +282,8 @@ fn solve( // Get the statistics object, get the root key, then print the statistics // recursively. - let stats = ctl.statistics().unwrap(); - let stats_key = stats.root().unwrap(); + // let stats = ctl.statistics().unwrap(); + // let stats_key = stats.root().unwrap(); // print_statistics(tman_config, stats, stats_key, 0); Ok((usable_model, non_usable_models)) diff --git a/core/src/ten_manager/tools/build_all.sh b/core/src/ten_manager/tools/build_all.sh index 1c78feef3..dd022ee3d 100755 --- a/core/src/ten_manager/tools/build_all.sh +++ b/core/src/ten_manager/tools/build_all.sh @@ -1,24 +1,45 @@ #!/bin/bash +# The folder of this script. SCRIPT_DIR=$( cd "$(dirname "$0")" || exit pwd ) +# The folder of the project. PROJECT_ROOT_DIR=$( cd "$SCRIPT_DIR/.." || exit pwd ) +# The folder of the `frontend`. +FRONTEND_DIR="$PROJECT_ROOT_DIR/frontend" + # Change to `frontend` folder and build the frontend. echo "Building frontend..." -cd "$PROJECT_ROOT_DIR/frontend" || exit 1 -npm install || exit 1 -npm run build || exit 1 +cd "$FRONTEND_DIR" || { + echo "Error: Cannot change to frontend directory." + exit 1 +} +npm install || { + echo "Error: npm install failed." + exit 1 +} +npm run build || { + echo "Error: npm run build failed." + exit 1 +} +echo "Frontend built successfully." # Change back to the project root folder. -cd "$PROJECT_ROOT_DIR" || exit 1 +cd "$PROJECT_ROOT_DIR" || { + echo "Error: Cannot change to project root directory." + exit 1 +} # Build tman rust project. echo "Building ..." -cargo build --release || exit 1 +cargo build --release || { + echo "Error: tman failed to build." + exit 1 +} diff --git a/core/src/ten_manager/tools/build_all_and_run.sh b/core/src/ten_manager/tools/build_all_and_run.sh index c602a98f9..7dafdd6b4 100755 --- a/core/src/ten_manager/tools/build_all_and_run.sh +++ b/core/src/ten_manager/tools/build_all_and_run.sh @@ -1,33 +1,59 @@ #!/bin/bash +# The folder of this script. SCRIPT_DIR=$( cd "$(dirname "$0")" || exit pwd ) +# The folder of the project. PROJECT_ROOT_DIR=$( cd "$SCRIPT_DIR/.." || exit pwd ) -# 檢查是否提供了參數 -if [ -z "$1" ]; then - echo "Error: Missing base directory argument." +# The folder of the `frontend`. +FRONTEND_DIR="$PROJECT_ROOT_DIR/frontend" + +usage() { echo "Usage: $0 " + echo " Base directory argument for Rust CLI" exit 1 +} + +# Check if 'base-dir' option has provided. +if [[ $# -ne 1 ]]; then + echo "Error: Missing base directory argument." + usage fi -BASE_DIR=$1 +BASE_DIR="$1" # Change to `frontend` folder and build the frontend. echo "Building frontend..." -cd "$PROJECT_ROOT_DIR/frontend" || exit 1 -npm install || exit 1 -npm run build || exit 1 +cd "$FRONTEND_DIR" || { + echo "Error: Cannot change to frontend directory." + exit 1 +} +npm install || { + echo "Error: npm install failed." + exit 1 +} +npm run build || { + echo "Error: npm run build failed." + exit 1 +} +echo "Frontend built successfully." # Change back to the project root folder. -cd "$PROJECT_ROOT_DIR" || exit 1 +cd "$PROJECT_ROOT_DIR" || { + echo "Error: Cannot change to project root directory." + exit 1 +} # Build tman rust project. echo "Building and running tman dev-server ..." -cargo run dev-server --base-dir="$BASE_DIR" +cargo run dev-server --base-dir="$BASE_DIR" || { + echo "Error: tman failed to run." + exit 1 +} diff --git a/core/src/ten_manager/tools/cargo_sort.py b/core/src/ten_manager/tools/cargo_sort.py index 93480c1be..d2c88caca 100644 --- a/core/src/ten_manager/tools/cargo_sort.py +++ b/core/src/ten_manager/tools/cargo_sort.py @@ -1,3 +1,9 @@ +# +# Copyright © 2024 Agora +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0, with certain conditions. +# Refer to the "LICENSE" file in the root directory for more information. +# import toml diff --git a/core/src/ten_manager/tools/lldb_rust_pre_run_commands.py b/core/src/ten_manager/tools/lldb_rust_pre_run_commands.py new file mode 100644 index 000000000..ff3a0a14b --- /dev/null +++ b/core/src/ten_manager/tools/lldb_rust_pre_run_commands.py @@ -0,0 +1,23 @@ +# +# Copyright © 2024 Agora +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0, with certain conditions. +# Refer to the "LICENSE" file in the root directory for more information. +# + +# These commands need to be loaded into LLDB before starting Rust debugging for +# LLDB to perform Rust debugging more effectively. + +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"}"' +)