diff --git a/.vscode/settings.json b/.vscode/settings.json index c291b5d577..244ed75060 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,20 +7,24 @@ "editor.wordWrapColumn": 80 }, "[python]": { - "editor.tabSize": 4, - "editor.defaultFormatter": "ms-python.black-formatter" + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.tabSize": 4 }, "black-formatter.args": [ "--line-length", "80" ], "debug.allowBreakpointsEverywhere": true, - "doxdocgen.c.commentPrefix": "/// ", - "doxdocgen.c.firstLine": "///", - "doxdocgen.c.lastLine": "///", - "doxdocgen.c.triggerSequence": "///", - "doxdocgen.generic.boolReturnsTrueFalse": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, "editor.formatOnSave": true, + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], "files.eol": "\n", "files.trimFinalNewlines": true, "files.trimTrailingWhitespace": true, diff --git a/core/src/ten_manager/Cargo.lock b/core/src/ten_manager/Cargo.lock index 32d0d08c9f..b4b56bb412 100644 --- a/core/src/ten_manager/Cargo.lock +++ b/core/src/ten_manager/Cargo.lock @@ -2,6 +2,31 @@ # It is not intended for manual editing. version = 4 +[[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", +] + [[package]] name = "actix-codec" version = "0.5.2" @@ -212,6 +237,24 @@ dependencies = [ "url", ] +[[package]] +name = "actix-web-actors" +version = "4.3.1+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98c5300b38fd004fe7d2a964f9a90813fdbe8a81fed500587e78b1b71c6f980" +dependencies = [ + "actix", + "actix-codec", + "actix-http", + "actix-web", + "bytes", + "bytestring", + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "actix-web-codegen" version = "4.3.0" @@ -224,6 +267,17 @@ 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" @@ -476,9 +530,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" dependencies = [ "memchr", "serde", @@ -703,6 +757,15 @@ 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" @@ -854,6 +917,12 @@ dependencies = [ "syn", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dyn-clone" version = "1.0.17" @@ -914,6 +983,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "filedescriptor" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "flate2" version = "1.0.35" @@ -959,7 +1039,7 @@ dependencies = [ "cfg-if", "cvt", "libc", - "nix", + "nix 0.29.0", "windows-sys 0.52.0", ] @@ -1469,6 +1549,15 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "ioctl-rs" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d" +dependencies = [ + "libc", +] + [[package]] name = "ipnet" version = "2.10.1" @@ -1687,6 +1776,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "mimalloc" version = "0.1.43" @@ -1760,6 +1858,20 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "nix" version = "0.29.0" @@ -2032,6 +2144,27 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" +[[package]] +name = "portable-pty" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806ee80c2a03dbe1a9fb9534f8d19e4c0546b790cde8fd1fea9d6390644cb0be" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "downcast-rs", + "filedescriptor", + "lazy_static", + "libc", + "log", + "nix 0.25.1", + "serial", + "shared_library", + "shell-words", + "winapi", + "winreg", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2159,9 +2292,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags 2.6.0", ] @@ -2360,9 +2493,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.19" +version = "0.23.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" dependencies = [ "once_cell", "ring", @@ -2424,27 +2557,27 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", @@ -2476,6 +2609,48 @@ dependencies = [ "serde", ] +[[package]] +name = "serial" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1237a96570fc377c13baa1b88c7589ab66edced652e43ffb17088f003db3e86" +dependencies = [ + "serial-core", + "serial-unix", + "serial-windows", +] + +[[package]] +name = "serial-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581" +dependencies = [ + "libc", +] + +[[package]] +name = "serial-unix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7" +dependencies = [ + "ioctl-rs", + "libc", + "serial-core", + "termios", +] + +[[package]] +name = "serial-windows" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162" +dependencies = [ + "libc", + "serial-core", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2498,6 +2673,22 @@ dependencies = [ "digest", ] +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" @@ -2637,10 +2828,12 @@ dependencies = [ name = "ten_manager" version = "0.1.0" dependencies = [ + "actix", "actix-cors", "actix-files", "actix-rt", "actix-web", + "actix-web-actors", "anyhow", "clap", "clingo", @@ -2655,6 +2848,7 @@ dependencies = [ "linked-hash-map", "mimalloc", "mime_guess", + "portable-pty", "regex", "remove_dir_all", "reqwest", @@ -2689,6 +2883,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "termios" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a" +dependencies = [ + "libc", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -3310,6 +3513,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/core/src/ten_manager/Cargo.toml b/core/src/ten_manager/Cargo.toml index 9b0892beeb..c949c86ca6 100644 --- a/core/src/ten_manager/Cargo.toml +++ b/core/src/ten_manager/Cargo.toml @@ -8,10 +8,12 @@ version = "0.1.0" edition = "2021" [dependencies] +actix = "0.13.5" actix-cors = "0.7" actix-files = "0.6" actix-rt = "2.10" actix-web = "4.8" +actix-web-actors = "4.3.1" anyhow = "1.0" clap = "4.5" clingo = "0.8" @@ -24,6 +26,7 @@ ignore = "0.4" indicatif = "0.17" inquire = "0.7.5" mime_guess = "2.0.5" +portable-pty = "0.8.1" regex = "1.11" remove_dir_all = "1.0" rust-embed = "8.5.0" diff --git a/core/src/ten_manager/designer_frontend/LICENSE b/core/src/ten_manager/designer_frontend/LICENSE new file mode 100644 index 0000000000..b994306565 --- /dev/null +++ b/core/src/ten_manager/designer_frontend/LICENSE @@ -0,0 +1,44 @@ +Open Source License + +The TEN Framework is licensed pursuant to the Apache License v2.0, with the +following additional conditions. You may reproduce, prepare Derivative Works +of, publicly display, publicly perform, sublicense, distribute, or otherwise +make available (together, "Deploy") the TEN Framework, for commercial or +non-commercial purposes, provided that you agree to abide by the terms below: + + 1. You may not (i) host the TEN Framework or the Derivative Works on any End + User devices, including but not limited to any mobile terminal devices + or (ii) Deploy the TEN Framework in a way that competes with Agora's + offerings and/or that allows others to compete with Agora's offerings, + including without limitation enabling any third party to develop or + deploy Applications. + + 2. You may Deploy the TEN Framework solely to create and enable deployment + of your Application(s) solely for your benefit and the benefit of your + direct End Users. If you prefer, you may include the following notice in + the documentation of your Application(s): "Powered by TEN FRAMEWORK". + + 3. Derivative Works of the TEN Framework remain subject to this Open Source + License. + + 4. "End Users" shall mean the end-users of your Application(s) who access + the TEN Framework solely to the extent necessary to access and use the + Application(s) you create or deploy using TEN Framework. + + 5. "Application(s)" shall mean your software programs designed or developed + by using the TEN Framework or where deployment is enabled by the TEN + Framework. + + Copyright © 2024 Agora + + Licensed 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. diff --git a/core/src/ten_manager/designer_frontend/eslint.config.mjs b/core/src/ten_manager/designer_frontend/eslint.config.mjs new file mode 100644 index 0000000000..77e5dbf053 --- /dev/null +++ b/core/src/ten_manager/designer_frontend/eslint.config.mjs @@ -0,0 +1,58 @@ +// +// 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 eslintPluginReact from "eslint-plugin-react"; +import eslintPluginJsxA11y from "eslint-plugin-jsx-a11y"; +import eslintPluginImport from "eslint-plugin-import"; +import eslintPluginTypeScript from "@typescript-eslint/eslint-plugin"; +import typescriptParser from "@typescript-eslint/parser"; + +export default [ + { + files: ["src/**/*.{js,jsx,ts,tsx}"], + ignores: ["node_modules", "dist"], + languageOptions: { + parser: typescriptParser, + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + project: "./tsconfig.json", + }, + }, + settings: { + react: { + version: "detect", + }, + "import/resolver": { + typescript: {}, + }, + }, + plugins: { + react: eslintPluginReact, + "jsx-a11y": eslintPluginJsxA11y, + import: eslintPluginImport, + "@typescript-eslint": eslintPluginTypeScript, + }, + rules: { + "react/react-in-jsx-scope": "off", + "import/extensions": [ + "error", + "ignorePackages", + { + ts: "never", + tsx: "never", + js: "never", + jsx: "never", + }, + ], + "@typescript-eslint/no-unused-vars": ["error"], + "max-len": ["error", { code: 80, tabWidth: 2, ignoreUrls: true }], + }, + }, +]; diff --git a/core/src/ten_manager/designer_frontend/package.json b/core/src/ten_manager/designer_frontend/package.json new file mode 100644 index 0000000000..a6db32041e --- /dev/null +++ b/core/src/ten_manager/designer_frontend/package.json @@ -0,0 +1,61 @@ +{ + "name": "designer-frontend", + "version": "0.1.0", + "main": "index.js", + "type": "module", + "scripts": { + "build": "webpack --mode production", + "start": "webpack serve --mode development", + "lint": "eslint", + "lint:fix": "eslint --fix" + }, + "dependencies": { + "@monaco-editor/react": "^4.6.0", + "@xterm/addon-attach": "^0.11.0", + "@xterm/addon-fit": "^0.10.0", + "@xterm/addon-unicode11": "^0.8.0", + "@xterm/addon-web-links": "^0.11.0", + "@xterm/xterm": "^5.5.0", + "@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", + "monaco-editor": "^0.52.2", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-i18next": "^15.1.4", + "react-icons": "^5.4.0" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@types/dagre": "^0.7.52", + "@types/react": "^18.0.1", + "@types/react-dom": "^18", + "@types/react-resizable": "^3.0.8", + "@typescript-eslint/eslint-plugin": "^8.18.0", + "@typescript-eslint/parser": "^8.18.0", + "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^12.0.2", + "css-loader": "^7.1.2", + "eslint": "^9.17", + "eslint-import-resolver-typescript": "^3.7.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-webpack-plugin": "^4.2.0", + "globals": "^15.13.0", + "html-webpack-plugin": "^5.6.3", + "monaco-editor-webpack-plugin": "^7.1.0", + "sass": "^1.82.0", + "sass-loader": "^16.0.4", + "style-loader": "^4.0.0", + "ts-loader": "^9.5.1", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0", + "webpack": "^5.97.1", + "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/public/index.html b/core/src/ten_manager/designer_frontend/public/index.html similarity index 100% rename from core/src/ten_manager/frontend/public/index.html rename to core/src/ten_manager/designer_frontend/public/index.html diff --git a/core/src/ten_manager/frontend/public/locales/en/common.json b/core/src/ten_manager/designer_frontend/public/locales/en/common.json similarity index 50% rename from core/src/ten_manager/frontend/public/locales/en/common.json rename to core/src/ten_manager/designer_frontend/public/locales/en/common.json index 3ec9887c6b..298748c06a 100644 --- a/core/src/ten_manager/frontend/public/locales/en/common.json +++ b/core/src/ten_manager/designer_frontend/public/locales/en/common.json @@ -6,6 +6,9 @@ "Dark": "Dark", "Light": "Light", "Language": "Language", - "Edit": "Edit", - "Delete": "Delete" + "Edit manifest.json": "Edit manifest.json", + "Edit property.json": "Edit property.json", + "Delete": "Delete", + "Launch terminal": "Launch terminal", + "Official site": "Official site" } \ No newline at end of file diff --git a/core/src/ten_manager/designer_frontend/public/locales/zh_cn/common.json b/core/src/ten_manager/designer_frontend/public/locales/zh_cn/common.json new file mode 100644 index 0000000000..075bf80ec2 --- /dev/null +++ b/core/src/ten_manager/designer_frontend/public/locales/zh_cn/common.json @@ -0,0 +1,14 @@ +{ + "error_fetching": "无法获取版本.", + "Settings": "设定", + "Theme": "主题", + "Switch to": "切换至", + "Dark": "深色", + "Light": "淡色", + "Language": "语言", + "Edit manifest.json": "编辑 manifest.json", + "Edit property.json": "编辑 property.json", + "Delete": "删除", + "Launch terminal": "开启 terminal", + "Official site": "官网" +} \ No newline at end of file diff --git a/core/src/ten_manager/frontend/src/App.tsx b/core/src/ten_manager/designer_frontend/src/App.tsx similarity index 96% rename from core/src/ten_manager/frontend/src/App.tsx rename to core/src/ten_manager/designer_frontend/src/App.tsx index aa476e7ef8..bd7f48f79e 100644 --- a/core/src/ten_manager/frontend/src/App.tsx +++ b/core/src/ten_manager/designer_frontend/src/App.tsx @@ -5,11 +5,12 @@ // Refer to the "LICENSE" file in the root directory for more information. // 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 { @@ -23,7 +24,6 @@ interface DevServerVersion { } const App: React.FC = () => { - const { t } = useTranslation("common"); const [version, setVersion] = useState(""); const [error, setError] = useState(""); const [showSettings, setShowSettings] = useState(false); diff --git a/core/src/ten_manager/designer_frontend/src/api/api.ts b/core/src/ten_manager/designer_frontend/src/api/api.ts new file mode 100644 index 0000000000..fed27dc6ad --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/api/api.ts @@ -0,0 +1,74 @@ +// +// 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 { + ApiResponse, + BackendConnection, + BackendNode, + SuccessResponse, +} from "./interface"; + +export interface ExtensionAddon { + addon_name: string; + url: string; + api?: any; +} + +export const fetchNodes = async (): Promise => { + const response = await fetch(`/api/dev-server/v1/graphs/default/nodes`); + if (!response.ok) { + throw new Error(`Failed to fetch nodes: ${response.status}`); + } + const data: ApiResponse = await response.json(); + + if (!isSuccessResponse(data)) { + throw new Error(`Error fetching nodes: ${data.message}`); + } + + return data.data; +}; + +export const fetchConnections = async (): Promise => { + const response = await fetch(`/api/dev-server/v1/graphs/default/connections`); + if (!response.ok) { + throw new Error(`Failed to fetch connections: ${response.status}`); + } + const data: ApiResponse = await response.json(); + + if (!isSuccessResponse(data)) { + throw new Error(`Error fetching connections: ${data.message}`); + } + + return data.data; +}; + +/** + * Fetch extension addon information by name. + * @param name - The name of the extension addon. + * @returns The extension addon information. + */ +export const fetchExtensionAddonByName = async ( + name: string +): Promise => { + const response = await fetch(`/api/dev-server/v1/addons/extensions/${name}`); + if (!response.ok) { + throw new Error(`Failed to fetch addon '${name}': ${response.status}`); + } + + const data: ApiResponse = await response.json(); + + if (isSuccessResponse(data)) { + return data.data; + } else { + throw new Error(`Error fetching addon '${name}': ${data.message}`); + } +}; + +export const isSuccessResponse = ( + response: ApiResponse +): response is SuccessResponse => { + return response.status === "ok"; +}; diff --git a/core/src/ten_manager/frontend/src/api/interface.ts b/core/src/ten_manager/designer_frontend/src/api/interface.ts similarity index 63% rename from core/src/ten_manager/frontend/src/api/interface.ts rename to core/src/ten_manager/designer_frontend/src/api/interface.ts index c308aa35e9..d6ed192633 100644 --- a/core/src/ten_manager/frontend/src/api/interface.ts +++ b/core/src/ten_manager/designer_frontend/src/api/interface.ts @@ -4,12 +4,28 @@ // Licensed under the Apache License, Version 2.0, with certain conditions. // Refer to the "LICENSE" file in the root directory for more information. // -export interface ApiResponse { - status: string; +export interface SuccessResponse { + status: "ok"; data: T; meta?: any; } +export interface ErrorResponse { + status: "fail"; + message: string; + error?: InnerError; +} + +export interface InnerError { + // Note: The value of 'type' must adhere to the 'error_type' in rust. + type: string; + + code?: string; + message: string; +} + +export type ApiResponse = SuccessResponse | ErrorResponse; + export interface BackendNode { addon: string; name: string; diff --git a/core/src/ten_manager/frontend/src/components/AboutPopup/AboutPopup.scss b/core/src/ten_manager/designer_frontend/src/components/AboutPopup/AboutPopup.scss similarity index 100% rename from core/src/ten_manager/frontend/src/components/AboutPopup/AboutPopup.scss rename to core/src/ten_manager/designer_frontend/src/components/AboutPopup/AboutPopup.scss diff --git a/core/src/ten_manager/frontend/src/components/AboutPopup/AboutPopup.tsx b/core/src/ten_manager/designer_frontend/src/components/AboutPopup/AboutPopup.tsx similarity index 90% rename from core/src/ten_manager/frontend/src/components/AboutPopup/AboutPopup.tsx rename to core/src/ten_manager/designer_frontend/src/components/AboutPopup/AboutPopup.tsx index bb3f2420a5..9ed94ea6b6 100644 --- a/core/src/ten_manager/frontend/src/components/AboutPopup/AboutPopup.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/AboutPopup/AboutPopup.tsx @@ -5,7 +5,10 @@ // Refer to the "LICENSE" file in the root directory for more information. // import React from "react"; + import Popup from "../Popup/Popup"; +import { useTranslation } from "react-i18next"; + import "./AboutPopup.scss"; interface AboutPopupProps { @@ -13,12 +16,14 @@ interface AboutPopupProps { } const AboutPopup: React.FC = ({ onClose }) => { + const { t } = useTranslation(); + return (

Powered by TEN Framework.

- Official site:{" "} + {t("Official site")}:{" "} void; + separator?: boolean; +} + +interface DropdownMenuProps { + title: string; + isOpen: boolean; + onClick: () => void; + onHover: () => void; + items: DropdownMenuItem[]; +} + +const DropdownMenu: React.FC = ({ + title, + isOpen, + onClick, + onHover, + items, +}) => { + return ( +

+ ); +}; + +export default DropdownMenu; diff --git a/core/src/ten_manager/frontend/src/components/AppBar/EditMenu.tsx b/core/src/ten_manager/designer_frontend/src/components/AppBar/EditMenu.tsx similarity index 58% rename from core/src/ten_manager/frontend/src/components/AppBar/EditMenu.tsx rename to core/src/ten_manager/designer_frontend/src/components/AppBar/EditMenu.tsx index d6fe195dcc..e25d64c055 100644 --- a/core/src/ten_manager/frontend/src/components/AppBar/EditMenu.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/AppBar/EditMenu.tsx @@ -5,7 +5,9 @@ // Refer to the "LICENSE" file in the root directory for more information. // import React from "react"; -import DropdownMenu from "./DropdownMenu"; +import { FaCogs, FaArrowsAlt } from "react-icons/fa"; + +import DropdownMenu, { DropdownMenuItem } from "./DropdownMenu"; interface EditMenuProps { isOpen: boolean; @@ -24,33 +26,36 @@ const EditMenu: React.FC = ({ closeMenu, onAutoLayout, }) => { + const items: DropdownMenuItem[] = [ + { + label: "Auto Layout", + icon: , + onClick: () => { + onAutoLayout(); + closeMenu(); + }, + }, + { + label: "Settings", + icon: , + onClick: () => { + onOpenSettings(); + closeMenu(); + }, + }, + { + separator: true, + }, + ]; + return ( - {" "} -
{ - onAutoLayout(); - closeMenu(); - }} - > - Auto Layout -
-
{ - onOpenSettings(); - closeMenu(); - }} - > - Settings -
-
+ items={items} // 傳遞 items + /> ); }; diff --git a/core/src/ten_manager/frontend/src/components/AppBar/FileMenu.tsx b/core/src/ten_manager/designer_frontend/src/components/AppBar/FileMenu.tsx similarity index 67% rename from core/src/ten_manager/frontend/src/components/AppBar/FileMenu.tsx rename to core/src/ten_manager/designer_frontend/src/components/AppBar/FileMenu.tsx index 30127f660b..a1ace1ab23 100644 --- a/core/src/ten_manager/frontend/src/components/AppBar/FileMenu.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/AppBar/FileMenu.tsx @@ -5,7 +5,9 @@ // Refer to the "LICENSE" file in the root directory for more information. // import React from "react"; -import DropdownMenu from "./DropdownMenu"; +import { FaFolderOpen } from "react-icons/fa"; + +import DropdownMenu, { DropdownMenuItem } from "./DropdownMenu"; interface FileMenuProps { isOpen: boolean; @@ -20,22 +22,24 @@ const FileMenu: React.FC = ({ onHover, closeMenu, }) => { + const items: DropdownMenuItem[] = [ + { + label: "Open TEN app folder", + icon: , + onClick: () => { + closeMenu(); + }, + }, + ]; + return ( -
{ - closeMenu(); - }} - > - Open TEN app folder -
-
+ items={items} + /> ); }; diff --git a/core/src/ten_manager/frontend/src/components/AppBar/HelpMenu.tsx b/core/src/ten_manager/designer_frontend/src/components/AppBar/HelpMenu.tsx similarity index 71% rename from core/src/ten_manager/frontend/src/components/AppBar/HelpMenu.tsx rename to core/src/ten_manager/designer_frontend/src/components/AppBar/HelpMenu.tsx index c127763307..74ec2f38be 100644 --- a/core/src/ten_manager/frontend/src/components/AppBar/HelpMenu.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/AppBar/HelpMenu.tsx @@ -5,7 +5,9 @@ // Refer to the "LICENSE" file in the root directory for more information. // import React, { useState } from "react"; -import DropdownMenu from "./DropdownMenu"; +import { FaInfoCircle } from "react-icons/fa"; + +import DropdownMenu, { DropdownMenuItem } from "./DropdownMenu"; import AboutPopup from "../AboutPopup/AboutPopup"; interface HelpMenuProps { @@ -21,18 +23,8 @@ const HelpMenu: React.FC = ({ 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(); @@ -42,6 +34,14 @@ const HelpMenu: React.FC = ({ setIsAboutOpen(false); }; + const items: DropdownMenuItem[] = [ + { + label: "About", + icon: , + onClick: openAbout, + }, + ]; + return ( <> = ({ isOpen={isOpen} onClick={onClick} onHover={onHover} - > -
- About -
-
+ items={items} + /> {isAboutOpen && } 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 new file mode 100644 index 0000000000..d7a090dd9d --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/components/EditorPopup/EditorPopup.scss @@ -0,0 +1,18 @@ +// +// 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-editor { + .popup-content { + padding: 0; // Remove padding to prevent overflow. + + // Ensure padding and borders are included in the element's total width and + // height. + box-sizing: border-box; + + display: flex; + flex-direction: column; + } +} 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 new file mode 100644 index 0000000000..eb15ed507e --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/components/EditorPopup/EditorPopup.tsx @@ -0,0 +1,80 @@ +// +// 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, { useEffect, useState } from "react"; +import Editor from "@monaco-editor/react"; + +import Popup from "../Popup/Popup"; + +import "./EditorPopup.scss"; + +const DEFAULT_WIDTH = 800; +const DEFAULT_HEIGHT = 400; + +export interface EditorData { + title: string; + + // The url (path) of the editor to display. + url: string; + + // The content of the editor to display. + content: string; +} + +interface EditorPopupProps { + data: EditorData; + onClose: () => void; +} + +const EditorPopup: React.FC = ({ data, onClose }) => { + const [fileContent, setFileContent] = useState(data.content); + + // Fetch the specified file content from the backend. + useEffect(() => { + const fetchFileContent = async () => { + try { + const encodedUrl = encodeURIComponent(data.url); + const response = await fetch( + `/api/dev-server/v1/file-content/${encodedUrl}` + ); + if (!response.ok) throw new Error("Failed to fetch file content"); + const respData = await response.json(); + setFileContent(respData.data.content); + } catch (error) { + console.error("Failed to fetch file content:", error); + } + }; + + fetchFileContent(); + }, [data.url]); + + return ( + + { + editor.focus(); // Set the keyboard focus to the editor. + }} + /> + + ); +}; + +export default EditorPopup; diff --git a/core/src/ten_manager/frontend/src/components/Popup/Popup.scss b/core/src/ten_manager/designer_frontend/src/components/Popup/Popup.scss similarity index 62% rename from core/src/ten_manager/frontend/src/components/Popup/Popup.scss rename to core/src/ten_manager/designer_frontend/src/components/Popup/Popup.scss index 59963b966c..ca5c485d75 100644 --- a/core/src/ten_manager/frontend/src/components/Popup/Popup.scss +++ b/core/src/ten_manager/designer_frontend/src/components/Popup/Popup.scss @@ -6,7 +6,6 @@ // .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); @@ -14,10 +13,12 @@ color: var(--popup-fg); border-radius: 4px; z-index: 1001; // Ensure it's above other elements. + display: flex; + flex-direction: column; // Hide the transient movement when the popup initially appears. opacity: 0; // Default opacity is 0. - transition: opacity 0.3s ease; + // transition: opacity 0.3s ease; &.visible { opacity: 1; @@ -63,5 +64,33 @@ .popup-content { padding: 10px; + overflow: hidden; + display: flex; + visibility: visible; + // transition: visibility 0.3s, height 0.3s ease; + + &.collapsed { + // Keep it in the DOM but hide the content. Ensure that the popup's outer + // frame does not shrink when the content is hidden. + visibility: hidden; + + overflow: hidden; // Prevent visual glitches. + height: 0; // Collapse height to minimize space usage. + padding-top: 0; + padding-bottom: 0; + } + } + + .resize-handle { + width: 5px; + height: 5px; + background: transparent; + position: absolute; + right: 0; + bottom: 0; + cursor: se-resize; + + // Ensure the resize handle does not block bring-to-front behavior. + z-index: 1002; } } diff --git a/core/src/ten_manager/designer_frontend/src/components/Popup/Popup.tsx b/core/src/ten_manager/designer_frontend/src/components/Popup/Popup.tsx new file mode 100644 index 0000000000..a1ea2a367d --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/components/Popup/Popup.tsx @@ -0,0 +1,282 @@ +// +// 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"; + +const DEFAULT_WIDTH = 800; +const DEFAULT_HEIGHT = 400; + +interface PopupProps { + title: string; + children: React.ReactNode; + className?: string; + resizable?: boolean; + + // When a popup appears, it takes keyboard focus, allowing the popup to be + // closed directly by pressing "Esc." However, in some scenarios, the popup + // should not take keyboard focus. For example, in the case of xterm, if the + // popup takes keyboard focus, xterm will continuously fail to regain keyboard + // focus, making it impossible to receive input from the keyboard. + preventFocusSteal?: boolean; + + initialWidth?: number; + initialHeight?: number; + + onClose: () => void; + onCollapseToggle?: (isCollapsed: boolean) => void; +} + +const Popup: React.FC = ({ + title, + children, + className, + resizable = false, // Default to non-resizable. + preventFocusSteal = false, + initialWidth, + initialHeight, + onClose, + onCollapseToggle, +}) => { + const popupRef = useRef(null); + const headerRef = useRef(null); + + const [isCollapsed, setIsCollapsed] = useState(false); + const [isVisible, setIsVisible] = useState(false); + + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [size, setSize] = useState<{ width?: number; height?: number }>({ + width: initialWidth, + height: initialHeight, + }); + const [prevHeight, setPrevHeight] = useState(null); + const [headerHeight, setHeaderHeight] = useState(0); + const [isResizing, setIsResizing] = useState(false); + const [resizeStart, setResizeStart] = useState<{ + x: number; + y: number; + width: number; + height: number; + } | null>(null); + + const [isDragging, setIsDragging] = useState(false); + const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>( + 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, + }); + if (!preventFocusSteal) { + popupRef.current.focus(); + } + } + setIsVisible(true); + }, [preventFocusSteal]); + + // If an initial size is not provided, the actual size will be determined + // after the popup becomes visible. + useEffect(() => { + if ( + isVisible && + popupRef.current && + size.width === undefined && + size.height === undefined + ) { + const rect = popupRef.current.getBoundingClientRect(); + setSize({ width: rect.width, height: rect.height }); + } + }, [isVisible, size.width, size.height]); + + // Handle keydown for ESC. + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }; + + // Only add listener if this popup is focused and preventFocusSteal is + // false. + if (!preventFocusSteal && popupRef.current) { + popupRef.current.addEventListener("keydown", handleKeyDown); + } + + return () => { + if (!preventFocusSteal && popupRef.current) { + popupRef.current.removeEventListener("keydown", handleKeyDown); + } + }; + }, [onClose, preventFocusSteal]); + + const handleMouseDown = (e: React.MouseEvent) => { + // Bring to front immediately on mouse down. + bringToFront(); + + setIsDragging(true); + setDragStart({ x: e.clientX - position.x, y: e.clientY - position.y }); + }; + + const handleMouseMove = (e: MouseEvent) => { + if (isDragging && dragStart) { + setPosition({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y }); + } + + if (isResizing && resizeStart) { + const newWidth = Math.max(e.clientX - resizeStart.x, 300); + const newHeight = Math.max(e.clientY - resizeStart.y, 200); + setSize({ + width: newWidth, + height: newHeight, + }); + } + }; + + const handleMouseUp = () => { + setIsDragging(false); + setDragStart(null); + + setIsResizing(false); + setResizeStart(null); + }; + + useEffect(() => { + if (isDragging || isResizing) { + 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); + }; + }, [isDragging, isResizing, dragStart, resizeStart]); + + const handleResizeMouseDown = (e: React.MouseEvent) => { + e.stopPropagation(); // Prevent from triggering dragging. + setIsResizing(true); + setResizeStart({ + x: position.x, + y: position.y, + width: size.width ?? DEFAULT_WIDTH, + height: size.height ?? DEFAULT_HEIGHT, + }); + }; + + useEffect(() => { + if (headerRef.current) { + const height = headerRef.current.getBoundingClientRect().height; + setHeaderHeight(height); + } + bringToFront(); + }, []); + + const toggleCollapse = () => { + setIsCollapsed((prev) => { + const newState = !prev; + if (newState) { + // Store the current height before collapsing. + setPrevHeight(size.height ?? null); + + // Set the height to the header's height to collapse. + setSize((prevSize) => ({ ...prevSize, height: headerHeight })); + } else if (prevHeight !== null) { + // Restore the previous height when un-collapsing. + setSize((prevSize) => ({ ...prevSize, height: prevHeight })); + + setPrevHeight(null); + } + onCollapseToggle?.(newState); + return newState; + }); + }; + + // Handle focus and bring to front when clicked. + const handleClick = () => { + if (!preventFocusSteal) { + 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 ( +
{ + if (!preventFocusSteal) { + popupRef.current?.focus(); + } + bringToFront(); + }} + > +
+ {title} +
+ + +
+
+ +
+ {children} +
+ {resizable && ( +
+ )} +
+ ); +}; + +export default Popup; diff --git a/core/src/ten_manager/designer_frontend/src/components/SettingsPopup/SettingsPopup.scss b/core/src/ten_manager/designer_frontend/src/components/SettingsPopup/SettingsPopup.scss new file mode 100644 index 0000000000..ae355402da --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/components/SettingsPopup/SettingsPopup.scss @@ -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. +// +.settings-content { + display: flex; + flex-direction: column; + gap: 15px; // Adds space between sections. + + .theme-toggle, + .language-selector { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 10px; // Adds space between label and button/select. + + 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); + } + } + + select { + margin-left: 10px; + padding: 5px; + border: 1px solid var(--button-border); + border-radius: 4px; + background: var(--button-bg); + color: var(--button-fg); + } + } +} diff --git a/core/src/ten_manager/frontend/src/components/SettingsPopup/SettingsPopup.tsx b/core/src/ten_manager/designer_frontend/src/components/SettingsPopup/SettingsPopup.tsx similarity index 61% rename from core/src/ten_manager/frontend/src/components/SettingsPopup/SettingsPopup.tsx rename to core/src/ten_manager/designer_frontend/src/components/SettingsPopup/SettingsPopup.tsx index 12995df75d..1543a2bc66 100644 --- a/core/src/ten_manager/frontend/src/components/SettingsPopup/SettingsPopup.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/SettingsPopup/SettingsPopup.tsx @@ -5,9 +5,11 @@ // Refer to the "LICENSE" file in the root directory for more information. // import React from "react"; +import { useTranslation } from "react-i18next"; + import Popup from "../Popup/Popup"; + import "./SettingsPopup.scss"; -import { useTranslation } from "react-i18next"; interface SettingsPopupProps { theme: string; @@ -35,21 +37,23 @@ const SettingsPopup: React.FC = ({ return ( -
- - {t("Theme")}: {theme.charAt(0).toUpperCase() + theme.slice(1)} - {" "} - -
-
- {t("Language")}: - +
+
+ + {t("Theme")}: {theme.charAt(0).toUpperCase() + theme.slice(1)} + + +
+
+ {t("Language")}: + +
); diff --git a/core/src/ten_manager/designer_frontend/src/components/TerminalPopup/TerminalPopup.scss b/core/src/ten_manager/designer_frontend/src/components/TerminalPopup/TerminalPopup.scss new file mode 100644 index 0000000000..3b5a3c26b8 --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/components/TerminalPopup/TerminalPopup.scss @@ -0,0 +1,17 @@ +.popup-terminal { + .popup-content { + padding: 0px; + display: flex; + width: 100%; + height: 100%; + box-sizing: border-box; + + // The terminal container fills the entire content area. + > div { + flex: 1; + width: 100%; + height: 100%; + background: #000; + } + } +} diff --git a/core/src/ten_manager/designer_frontend/src/components/TerminalPopup/TerminalPopup.tsx b/core/src/ten_manager/designer_frontend/src/components/TerminalPopup/TerminalPopup.tsx new file mode 100644 index 0000000000..6da447fa41 --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/components/TerminalPopup/TerminalPopup.tsx @@ -0,0 +1,267 @@ +// +// 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, { useEffect, useLayoutEffect, useRef, useState } from "react"; +import { Terminal as XTermTerminal } from "@xterm/xterm"; +import { Unicode11Addon } from "@xterm/addon-unicode11"; +import { WebLinksAddon } from "@xterm/addon-web-links"; +import { FitAddon } from "@xterm/addon-fit"; + +import Popup from "../Popup/Popup"; + +import "@xterm/xterm/css/xterm.css"; +import "./TerminalPopup.scss"; + +const DEFAULT_WIDTH = 800; +const DEFAULT_HEIGHT = 400; + +export interface TerminalData { + title: string; + url?: string; +} + +interface TerminalPopupProps { + data: TerminalData; + onClose: () => void; +} + +const TerminalPopup: React.FC = ({ data, onClose }) => { + const terminalRef = useRef(null); + const xtermRef = useRef(null); + const ws = useRef(null); + const resizeObserverRef = useRef(null); + const fitAddonRef = useRef(null); + const [terminalSize, setTerminalSize] = useState<{ + cols: number; + rows: number; + }>({ cols: 80, rows: 24 }); + + useLayoutEffect(() => { + if (!terminalRef.current) { + return; + } + + if (!data.url) { + return; + } + + const xterm = new XTermTerminal({ + cursorBlink: true, + macOptionIsMeta: true, + convertEol: true, + allowProposedApi: true, + }); + + xtermRef.current = xterm; + + const fitAddon = new FitAddon(); + fitAddonRef.current = fitAddon; + xterm.loadAddon(fitAddon); + + xterm.loadAddon(new Unicode11Addon()); + xterm.loadAddon(new WebLinksAddon()); + + // Due to the introduction of StrictMode in React 18, and the fact that we + // enable StrictMode for more comprehensive checks, React components will + // render twice under StrictMode. This means that `useEffect` will be called + // once, followed by a cleanup, and then called again. The `fit` method of + // the `FitAddon` is an asynchronous operation. Therefore, if `fit` is + // performed directly during the first `useEffect` call, it may lead to + // issues when the `fit` action actually occurs, as the HTML DOM element for + // the xterm might have already disappeared. This causes xterm to throw an + // error about missing dimensions (essentially due to the disappearance of + // the xterm HTML DOM element). + // + // To overcome this issue, the direct `fit` action inside `useEffect` is + // placed within a timer to asynchronously check if the xterm HTML DOM + // element still exists. If the element does not exist, the `fit` call is + // skipped to avoid this problem. This workaround logic works properly in + // both StrictMode and non-StrictMode environments. + const timeoutId = setTimeout(() => { + if (terminalRef.current) { + xterm.open(terminalRef.current); + fitAddon.fit(); // Initialize fit size. + + // Set keyboard focus when the Terminal is shown. + xterm.focus(); + + // Change the terminal size. + setTerminalSize({ cols: xterm.cols, rows: xterm.rows }); + } else { + console.warn( + "Terminal container no longer exists during initialization." + ); + } + }, 0); + + // Initialize the websocket connection to the backend. + const wsUrl = `ws://localhost:49483/ws/terminal?path=${encodeURIComponent( + data.url + )}`; + ws.current = new WebSocket(wsUrl); + + ws.current.onopen = () => { + console.log("WebSocket to the backend is connected!"); + sendResize(xterm.cols, xterm.rows); + }; + + ws.current.onmessage = async (event) => { + if (!xtermRef.current) { + return; + } + + // Handle the data from the backend. + if (event.data instanceof Blob) { + try { + const text = await event.data.text(); + xtermRef.current.write(text); + } catch (error) { + console.error("Failed to convert received blob from backend:", error); + } + } else if (typeof event.data === "string") { + // Try to parse the string data as a JSON. + try { + const msg = JSON.parse(event.data); + + // Check if its a `exit` message. + if (msg.type === "exit") { + // Display a exit message in the terminal UI. + xtermRef.current.writeln( + `\r\nProcess exited with code ${msg.code}\r\n` + ); + + // Close the websocket actively. + ws.current?.close(); + + // Close the terminal popup. + onClose(); + return; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + // It's not a JSON message, write it to the terminal UI directly. + } + + // Output to the terminal UI. + xtermRef.current.write(event.data); + } else if (event.data instanceof ArrayBuffer) { + const uint8Array = new Uint8Array(event.data); + xtermRef.current.write(uint8Array); + } else { + console.warn("Unknown received data type:", typeof event.data); + } + }; + + ws.current.onerror = (err) => { + console.error("WebSocket error:", err); + }; + + ws.current.onclose = () => { + console.log("WebSocket closed!"); + if (xtermRef.current) { + xtermRef.current.writeln("\r\nConnection closed."); + } + }; + + xterm.onData(handleInput); + + const handleResize = () => { + if (fitAddonRef.current && xtermRef.current) { + // Fit xterm to its DOM container, and enable xterm to calculate the + // updated cols/rows. + fitAddonRef.current.fit(); + + // Get the updated cols/rows from xterm, and update to the backend. + const cols = xtermRef.current.cols; + const rows = xtermRef.current.rows; + + // Change the terminal size. + setTerminalSize({ cols, rows }); + + // Notify the backend that the size of terminal should be changed. + sendResize(cols, rows); + } + }; + + resizeObserverRef.current = new ResizeObserver(() => { + handleResize(); + }); + + resizeObserverRef.current.observe(terminalRef.current); + + return () => { + // Cleanup. + + // Remove the timer. + clearTimeout(timeoutId); + + // Remove the resize handler. + resizeObserverRef.current?.disconnect(); + + // Close the websocket connection. + ws.current?.close(); + + // Close the xterm. + xterm.dispose(); + }; + }, [data]); + + useEffect(() => { + console.log("Terminal size updated:", terminalSize); + }, [terminalSize]); + + const handleInput = (data: string) => { + if (ws.current && ws.current.readyState === WebSocket.OPEN) { + ws.current.send(data); + } + }; + + const sendResize = (cols: number, rows: number) => { + if (ws.current && ws.current.readyState === WebSocket.OPEN) { + const resizeMessage = JSON.stringify({ + type: "resize", + cols, + rows, + }); + ws.current.send(resizeMessage); + } + }; + + const handleCollapseToggle = (isCollapsed: boolean) => { + if (!isCollapsed && fitAddonRef.current) { + // Wait for DOM updates before fitting the terminal. + setTimeout(() => { + if (terminalRef.current) { + terminalRef.current.style.display = "block"; // Restore display. + } + fitAddonRef.current?.fit(); + }, 0); + } else if (isCollapsed && terminalRef.current) { + // Hide the terminal container on collapse. + terminalRef.current.style.display = "none"; + } + }; + + return ( + +
+
+ ); +}; + +export default TerminalPopup; diff --git a/core/src/ten_manager/designer_frontend/src/flow/ContextMenu/ContextMenu.scss b/core/src/ten_manager/designer_frontend/src/flow/ContextMenu/ContextMenu.scss new file mode 100644 index 0000000000..da7406340d --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/flow/ContextMenu/ContextMenu.scss @@ -0,0 +1,55 @@ +// +// 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. +// +.context-menu { + position: fixed; + background-color: #fff; + border: 1px solid #ccc; + padding: 5px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + z-index: 9999; + width: auto; + + // Ensure the width and border calculation is correct. + box-sizing: border-box; + + .separator { + height: 1px; + background: var(--separator-bg, #ccc); + margin: 5px 0; + } + + .menu-item { + display: flex; + align-items: center; + padding: 5px 10px; + white-space: nowrap; + box-sizing: border-box; + cursor: pointer; + + &:hover { + background-color: #f0f0f0; + } + + .menu-icon { + margin-right: 8px; + display: flex; + align-items: center; + height: 1em; + + // Prevent the icon to be shrink. + flex-shrink: 0; + + width: 20px; // Fixed sized. + justify-content: center; // Horizontally center the icon. + } + + .menu-label { + flex: 1; // Allow the label section to occupy the remaining space. + text-align: left; // Ensure the text is left-aligned. + } + } +} diff --git a/core/src/ten_manager/designer_frontend/src/flow/ContextMenu/ContextMenu.tsx b/core/src/ten_manager/designer_frontend/src/flow/ContextMenu/ContextMenu.tsx new file mode 100644 index 0000000000..b31cb9246a --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/flow/ContextMenu/ContextMenu.tsx @@ -0,0 +1,55 @@ +// +// 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 "./ContextMenu.scss"; + +export interface ContextMenuItem { + label?: string; + icon?: React.ReactNode; + onClick?: () => void; + separator?: boolean; +} + +interface ContextMenuProps { + visible: boolean; + x: number; + y: number; + items: ContextMenuItem[]; +} + +const ContextMenu: React.FC = ({ visible, x, y, items }) => { + if (!visible) return null; + + return ( +
e.stopPropagation()} + > + {items.map((item, index) => ( + + {/* Separator. */} + {item.separator &&
} + {/* Menu item. */} + {!item.separator && ( +
+ {/* Icon. Always render the icon, even if no icon. */} + {item.icon || null}{" "} + {item.label} +
+ )} +
+ ))} +
+ ); +}; + +export default ContextMenu; diff --git a/core/src/ten_manager/designer_frontend/src/flow/ContextMenu/EdgeContextMenu.tsx b/core/src/ten_manager/designer_frontend/src/flow/ContextMenu/EdgeContextMenu.tsx new file mode 100644 index 0000000000..79f90f6391 --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/flow/ContextMenu/EdgeContextMenu.tsx @@ -0,0 +1,50 @@ +// +// 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 { useTranslation } from "react-i18next"; +import { FaEdit, FaTrash } from "react-icons/fa"; + +import ContextMenu, { ContextMenuItem } from "./ContextMenu"; +import { CustomEdgeType } from "../CustomEdge"; + +interface EdgeContextMenuProps { + visible: boolean; + x: number; + y: number; + edge: CustomEdgeType; + onClose: () => void; +} + +const EdgeContextMenu: React.FC = ({ + visible, + x, + y, + onClose, +}) => { + const { t } = useTranslation(); + + const items: ContextMenuItem[] = [ + { + label: t("Edit"), + icon: , + onClick: () => { + onClose(); + }, + }, + { + label: t("Delete"), + icon: , + onClick: () => { + onClose(); + }, + }, + ]; + + return ; +}; + +export default EdgeContextMenu; diff --git a/core/src/ten_manager/designer_frontend/src/flow/ContextMenu/NodeContextMenu.tsx b/core/src/ten_manager/designer_frontend/src/flow/ContextMenu/NodeContextMenu.tsx new file mode 100644 index 0000000000..5af74334e4 --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/flow/ContextMenu/NodeContextMenu.tsx @@ -0,0 +1,90 @@ +// +// 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 { useTranslation } from "react-i18next"; +import { FaEdit, FaTrash, FaTerminal } from "react-icons/fa"; + +import ContextMenu, { ContextMenuItem } from "./ContextMenu"; +import { CustomNodeType } from "../CustomNode"; +import { TerminalData } from "../../components/TerminalPopup/TerminalPopup"; +import { EditorData } from "../../components/EditorPopup/EditorPopup"; + +interface NodeContextMenuProps { + visible: boolean; + x: number; + y: number; + node: CustomNodeType; + onClose: () => void; + onLaunchTerminal: (data: TerminalData) => void; + onLaunchEditor: (data: EditorData) => void; +} + +const NodeContextMenu: React.FC = ({ + visible, + x, + y, + node, + onClose, + onLaunchTerminal, + onLaunchEditor, +}) => { + const { t } = useTranslation(); + + const items: ContextMenuItem[] = [ + { + label: t("Edit manifest.json"), + icon: , + onClick: () => { + onClose(); + if (node?.data.url) + onLaunchEditor({ + title: `${node.data.addon} manifest.json`, + content: "", + url: `${node.data.url}/manifest.json`, + }); + }, + }, + { + label: t("Edit property.json"), + icon: , + onClick: () => { + onClose(); + if (node?.data.url) + onLaunchEditor({ + title: `${node.data.addon} property.json`, + content: "", + url: `${node.data.url}/property.json`, + }); + }, + }, + { + separator: true, + }, + { + label: t("Launch terminal"), + icon: , + onClick: () => { + onClose(); + onLaunchTerminal({ title: node.data.name, url: node.data.url }); + }, + }, + { + separator: true, + }, + { + label: t("Delete"), + icon: , + onClick: () => { + onClose(); + }, + }, + ]; + + return ; +}; + +export default NodeContextMenu; diff --git a/core/src/ten_manager/frontend/src/flow/CustomEdge.tsx b/core/src/ten_manager/designer_frontend/src/flow/CustomEdge.tsx similarity index 100% rename from core/src/ten_manager/frontend/src/flow/CustomEdge.tsx rename to core/src/ten_manager/designer_frontend/src/flow/CustomEdge.tsx diff --git a/core/src/ten_manager/frontend/src/flow/CustomHandle.tsx b/core/src/ten_manager/designer_frontend/src/flow/CustomHandle.tsx similarity index 100% rename from core/src/ten_manager/frontend/src/flow/CustomHandle.tsx rename to core/src/ten_manager/designer_frontend/src/flow/CustomHandle.tsx diff --git a/core/src/ten_manager/frontend/src/flow/CustomNode.tsx b/core/src/ten_manager/designer_frontend/src/flow/CustomNode.tsx similarity index 92% rename from core/src/ten_manager/frontend/src/flow/CustomNode.tsx rename to core/src/ten_manager/designer_frontend/src/flow/CustomNode.tsx index fd4e05a6d0..9fe43c1592 100644 --- a/core/src/ten_manager/frontend/src/flow/CustomNode.tsx +++ b/core/src/ten_manager/designer_frontend/src/flow/CustomNode.tsx @@ -6,20 +6,27 @@ // import { memo } from "react"; import { Position, NodeProps, Connection, Edge, Node } from "@xyflow/react"; + import CustomHandle from "./CustomHandle"; const onConnect = (params: Connection | Edge) => console.log("Handle onConnect", params); export type CustomNodeType = Node< - { label: string; sourceCmds: string[]; targetCmds: string[] }, + { + name: string; + addon: string; + sourceCmds: string[]; + targetCmds: string[]; + url?: string; + }, "customNode" >; export function CustomNode({ data, isConnectable }: NodeProps) { return (
-
{data.label}
+
{data.name}
{/* Render source handles (for outgoing edges) */} {data.sourceCmds.map((cmd, index) => { diff --git a/core/src/ten_manager/designer_frontend/src/flow/FlowCanvas.tsx b/core/src/ten_manager/designer_frontend/src/flow/FlowCanvas.tsx new file mode 100644 index 0000000000..1c146cf93f --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/flow/FlowCanvas.tsx @@ -0,0 +1,286 @@ +// +// 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, + applyNodeChanges, + applyEdgeChanges, + Connection, +} from "@xyflow/react"; + +import CustomNode, { CustomNodeType } from "./CustomNode"; +import CustomEdge, { CustomEdgeType } from "./CustomEdge"; +import NodeContextMenu from "./ContextMenu/NodeContextMenu"; +import EdgeContextMenu from "./ContextMenu/EdgeContextMenu"; +import TerminalPopup, { + TerminalData, +} from "../components/TerminalPopup/TerminalPopup"; +import EditorPopup, { EditorData } from "../components/EditorPopup/EditorPopup"; +import { fetchNodes, fetchConnections } from "../api/api"; +import { + enhanceNodesWithCommands, + fetchAddonInfoForNodes, + getLayoutedElements, + processConnections, + processNodes, +} from "./graph"; + +export interface FlowCanvasRef { + performAutoLayout: () => void; +} + +const FlowCanvas = forwardRef((props, ref) => { + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); + const [error, setError] = useState(""); + + const [terminalPopups, setTerminalPopups] = useState< + { id: string; data: TerminalData }[] + >([]); + const [editorPopups, setEditorPopups] = useState< + { id: string; data: EditorData }[] + >([]); + + const [contextMenu, setContextMenu] = useState<{ + visible: boolean; + x: number; + y: number; + type?: "node" | "edge"; + edge?: CustomEdgeType; + node?: CustomNodeType; + }>({ visible: false, x: 0, y: 0 }); + + // Export `performAutoLayout`. + 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 backendNodes = await fetchNodes(); + const backendConnections = await fetchConnections(); + + let initialNodes: CustomNodeType[] = processNodes(backendNodes); + + const { initialEdges, nodeSourceCmdMap, nodeTargetCmdMap } = + processConnections(backendConnections); + + // Write back the cmd information to nodes, so that CustomNode could + // generate corresponding handles. + initialNodes = enhanceNodesWithCommands( + initialNodes, + nodeSourceCmdMap, + nodeTargetCmdMap + ); + + // Fetch additional addon information for each node. + const nodesWithAddonInfo = await fetchAddonInfoForNodes(initialNodes); + + // Auto-layout the nodes and edges. + const { nodes: layoutedNodes, edges: layoutedEdges } = + getLayoutedElements(nodesWithAddonInfo, initialEdges); + + setNodes(layoutedNodes); + setEdges(layoutedEdges); + } catch (err: any) { + console.error(err); + setError("Failed to fetch workflow data."); + } + }; + fetchData(); + }, []); + + const launchTerminal = (data: TerminalData) => { + const newPopup = { id: `${data.title}-${Date.now()}`, data }; + setTerminalPopups((prev) => [...prev, newPopup]); + }; + + const closeTerminal = (id: string) => { + setTerminalPopups((prev) => prev.filter((popup) => popup.id !== id)); + }; + + const launchEditor = (data: EditorData) => { + setEditorPopups((prev) => { + const existingPopup = prev.find((popup) => popup.data.url === data.url); + if (existingPopup) { + return prev; + } else { + return [ + ...prev, + { + id: `${data.url}-${Date.now()}`, + data: { + title: data.title, + url: data.url, + + // Initializes content to empty, the content will be fetched by + // EditorPopup. + content: "", + }, + }, + ]; + } + }); + }; + + const closeEditor = (id: string) => { + setEditorPopups((prev) => prev.filter((popup) => popup.id !== id)); + }; + + const onConnect = useCallback( + (params: Connection | CustomEdgeType) => { + setEdges((eds) => addEdge(params, eds)); + }, + [setEdges] + ); + + const renderContextMenu = () => { + if (contextMenu.type === "node" && contextMenu.node) { + return ( + + ); + } else if (contextMenu.type === "edge" && contextMenu.edge) { + return ( + + ); + } + return null; + }; + + // Right click nodes. + const clickNodeContextMenu = useCallback( + (event: MouseEvent, node: CustomNodeType) => { + event.preventDefault(); + setContextMenu({ + visible: true, + x: event.clientX, + y: event.clientY, + type: "node", + node: node, + }); + }, + [] + ); + + // Right click Edges. + const clickEdgeContextMenu = useCallback( + (event: MouseEvent, edge: CustomEdgeType) => { + event.preventDefault(); + setContextMenu({ + visible: true, + x: event.clientX, + y: event.clientY, + type: "edge", + edge: edge, + }); + }, + [] + ); + + // Close context menu. + const closeContextMenu = useCallback(() => { + setContextMenu({ visible: false, x: 0, y: 0 }); + }, []); + + // Click empty space to close context menu. + useEffect(() => { + const handleClick = () => { + closeContextMenu(); + }; + window.addEventListener("click", handleClick); + return () => window.removeEventListener("click", handleClick); + }, [closeContextMenu]); + + 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={clickNodeContextMenu} + onEdgeContextMenu={clickEdgeContextMenu} + > + + + + {error &&
{error}
} + + {renderContextMenu()} + + {terminalPopups.map((popup) => ( + closeTerminal(popup.id)} + /> + ))} + + {editorPopups.map((popup) => ( + closeEditor(popup.id)} + /> + ))} +
+ ); +}); + +export default FlowCanvas; diff --git a/core/src/ten_manager/designer_frontend/src/flow/graph.tsx b/core/src/ten_manager/designer_frontend/src/flow/graph.tsx new file mode 100644 index 0000000000..c341209c0e --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/flow/graph.tsx @@ -0,0 +1,169 @@ +// +// 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 { MarkerType } from "@xyflow/react"; +import dagre from "dagre"; + +import { CustomNodeType } from "./CustomNode"; +import { CustomEdgeType } from "./CustomEdge"; +import { BackendNode, BackendConnection } from "../api/interface"; +import { fetchExtensionAddonByName, ExtensionAddon } from "../api/api"; + +const NODE_WIDTH = 172; +const NODE_HEIGHT = 36; + +export const getLayoutedElements = ( + nodes: CustomNodeType[], + edges: CustomEdgeType[], + direction = "TB" +) => { + const dagreGraph = new dagre.graphlib.Graph(); + dagreGraph.setDefaultEdgeLabel(() => ({})); + dagreGraph.setGraph({ rankdir: direction }); + + nodes.forEach((node) => { + dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT }); + }); + + 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 - NODE_WIDTH / 2, + y: nodeWithPosition.y - NODE_HEIGHT / 2, + }, + }; + }); + + return { nodes: layoutedNodes, edges }; +}; + +export const processNodes = (backendNodes: BackendNode[]): CustomNodeType[] => { + return backendNodes.map((n, index) => ({ + id: n.name, + position: { x: index * 200, y: 100 }, + type: "customNode", + data: { + name: `${n.name}`, + addon: n.addon, + sourceCmds: [], + targetCmds: [], + }, + })); +}; + +export const processConnections = ( + backendConnections: BackendConnection[] +): { + initialEdges: CustomEdgeType[]; + nodeSourceCmdMap: Record>; + nodeTargetCmdMap: Record>; +} => { + let initialEdges: CustomEdgeType[] = []; + const nodeSourceCmdMap: Record> = {}; + const nodeTargetCmdMap: Record> = {}; + + backendConnections.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: "customEdge", + label: cmdName, + sourceHandle: `source-${cmdName}`, + targetHandle: `target-${cmdName}`, + markerEnd: { + type: MarkerType.ArrowClosed, + }, + }); + }); + }); + } + }); + + return { initialEdges, nodeSourceCmdMap, nodeTargetCmdMap }; +}; + +export const fetchAddonInfoForNodes = async ( + nodes: CustomNodeType[] +): Promise => { + return await Promise.all( + nodes.map(async (node) => { + try { + const addonInfo: ExtensionAddon = await fetchExtensionAddonByName( + node.data.addon + ); + console.log(`URL for addon '${node.data.addon}': ${addonInfo.url}`); + return { + ...node, + data: { + ...node.data, + url: addonInfo.url, + }, + }; + } catch (addonError: any) { + console.error( + `Failed to fetch addon info for '${node.data.addon}': ` + + `${addonError.message}` + ); + return node; + } + }) + ); +}; + +export const enhanceNodesWithCommands = ( + nodes: CustomNodeType[], + nodeSourceCmdMap: Record>, + nodeTargetCmdMap: Record> +): CustomNodeType[] => { + return nodes.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.name || `${node.id}`, + sourceCmds, + targetCmds, + }, + }; + }); +}; diff --git a/core/src/ten_manager/frontend/src/hooks/useTheme.ts b/core/src/ten_manager/designer_frontend/src/hooks/useTheme.ts similarity index 100% rename from core/src/ten_manager/frontend/src/hooks/useTheme.ts rename to core/src/ten_manager/designer_frontend/src/hooks/useTheme.ts diff --git a/core/src/ten_manager/frontend/src/i18n.ts b/core/src/ten_manager/designer_frontend/src/i18n.ts similarity index 100% rename from core/src/ten_manager/frontend/src/i18n.ts rename to core/src/ten_manager/designer_frontend/src/i18n.ts diff --git a/core/src/ten_manager/frontend/src/index.tsx b/core/src/ten_manager/designer_frontend/src/index.tsx similarity index 99% rename from core/src/ten_manager/frontend/src/index.tsx rename to core/src/ten_manager/designer_frontend/src/index.tsx index 35a57965ef..d3d55a6714 100644 --- a/core/src/ten_manager/frontend/src/index.tsx +++ b/core/src/ten_manager/designer_frontend/src/index.tsx @@ -6,6 +6,7 @@ // import React from "react"; import ReactDOM from "react-dom/client"; + import App from "./App"; // Import and initialize i18n. diff --git a/core/src/ten_manager/frontend/src/theme/_dark.scss b/core/src/ten_manager/designer_frontend/src/theme/_dark.scss similarity index 100% rename from core/src/ten_manager/frontend/src/theme/_dark.scss rename to core/src/ten_manager/designer_frontend/src/theme/_dark.scss diff --git a/core/src/ten_manager/frontend/src/theme/_light.scss b/core/src/ten_manager/designer_frontend/src/theme/_light.scss similarity index 100% rename from core/src/ten_manager/frontend/src/theme/_light.scss rename to core/src/ten_manager/designer_frontend/src/theme/_light.scss diff --git a/core/src/ten_manager/frontend/src/theme/_reactflow.scss b/core/src/ten_manager/designer_frontend/src/theme/_reactflow.scss similarity index 100% rename from core/src/ten_manager/frontend/src/theme/_reactflow.scss rename to core/src/ten_manager/designer_frontend/src/theme/_reactflow.scss diff --git a/core/src/ten_manager/frontend/src/theme/_variables.scss b/core/src/ten_manager/designer_frontend/src/theme/_variables.scss similarity index 96% rename from core/src/ten_manager/frontend/src/theme/_variables.scss rename to core/src/ten_manager/designer_frontend/src/theme/_variables.scss index 03e809b98b..046dd6f174 100644 --- a/core/src/ten_manager/frontend/src/theme/_variables.scss +++ b/core/src/ten_manager/designer_frontend/src/theme/_variables.scss @@ -23,4 +23,5 @@ --button-fg: #000000; --button-border: #cccccc; --button-hover-bg: #d5d5d5; + --separator-bg: #cccccc; } diff --git a/core/src/ten_manager/frontend/src/theme/index.scss b/core/src/ten_manager/designer_frontend/src/theme/index.scss similarity index 100% rename from core/src/ten_manager/frontend/src/theme/index.scss rename to core/src/ten_manager/designer_frontend/src/theme/index.scss diff --git a/core/src/ten_manager/frontend/tsconfig.json b/core/src/ten_manager/designer_frontend/tsconfig.json similarity index 100% rename from core/src/ten_manager/frontend/tsconfig.json rename to core/src/ten_manager/designer_frontend/tsconfig.json diff --git a/core/src/ten_manager/frontend/webpack.config.js b/core/src/ten_manager/designer_frontend/webpack.config.js similarity index 61% rename from core/src/ten_manager/frontend/webpack.config.js rename to core/src/ten_manager/designer_frontend/webpack.config.js index 092d4b1aa1..feb642307f 100644 --- a/core/src/ten_manager/frontend/webpack.config.js +++ b/core/src/ten_manager/designer_frontend/webpack.config.js @@ -4,18 +4,23 @@ // Licensed under the Apache License, Version 2.0, with certain conditions. // Refer to the "LICENSE" file in the root directory for more information. // -const path = require("path"); -const HtmlWebpackPlugin = require("html-webpack-plugin"); -const { CleanWebpackPlugin } = require("clean-webpack-plugin"); -const CopyWebpackPlugin = require("copy-webpack-plugin"); +import { fileURLToPath } from "url"; +import path from "path"; +import HtmlWebpackPlugin from "html-webpack-plugin"; +import { CleanWebpackPlugin } from "clean-webpack-plugin"; +import CopyWebpackPlugin from "copy-webpack-plugin"; +import ESLintPlugin from "eslint-webpack-plugin"; +import MonacoWebpackPlugin from "monaco-editor-webpack-plugin"; -module.exports = { +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export default { entry: "./src/index.tsx", mode: "development", // Or 'production' as needed. output: { path: path.resolve(__dirname, "dist"), filename: "bundle.js", - publicPath: "/", // Ensure the routing is correct. + publicPath: "/", }, resolve: { extensions: [".tsx", ".ts", ".js"], @@ -48,15 +53,26 @@ module.exports = { new CopyWebpackPlugin({ patterns: [ { - from: path.resolve(__dirname, "public", "locales"), // Source folder. - to: path.resolve(__dirname, "dist", "locales"), // Target folder. + from: path.resolve(__dirname, "public", "locales"), + to: path.resolve(__dirname, "dist", "locales"), }, ], }), + new ESLintPlugin({ + extensions: ["js", "jsx", "ts", "tsx"], + emitWarning: true, + failOnError: false, + configType: "flat", + overrideConfigFile: path.resolve(__dirname, "eslint.config.mjs"), + }), + new MonacoWebpackPlugin({ + // Only the resources of those languages needed. + languages: ["python", "go", "cpp", "javascript", "typescript", "json"], + }), ], devServer: { static: path.resolve(__dirname, "dist"), - historyApiFallback: true, // Handle React Router. + historyApiFallback: true, port: 3000, proxy: [ { diff --git a/core/src/ten_manager/frontend/package.json b/core/src/ten_manager/frontend/package.json deleted file mode 100644 index 2e2cb74e3a..0000000000 --- a/core/src/ten_manager/frontend/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "frontend", - "version": "0.1.0", - "main": "index.js", - "scripts": { - "build": "webpack --mode production", - "start": "webpack serve --mode development" - }, - "keywords": [], - "author": "", - "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", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "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", - "webpack": "^5.97.1", - "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.1.0" - } -} diff --git a/core/src/ten_manager/frontend/public/locales/zh_cn/common.json b/core/src/ten_manager/frontend/public/locales/zh_cn/common.json deleted file mode 100644 index 943545a43a..0000000000 --- a/core/src/ten_manager/frontend/public/locales/zh_cn/common.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "error_fetching": "无法获取版本.", - "Settings": "设定", - "Theme": "主题", - "Switch to": "切换至", - "Dark": "暗色", - "Light": "淡色", - "Language": "语言", - "Edit": "编辑", - "Delete": "删除" -} \ No newline at end of file diff --git a/core/src/ten_manager/frontend/src/components/AppBar/DropdownMenu.scss b/core/src/ten_manager/frontend/src/components/AppBar/DropdownMenu.scss deleted file mode 100644 index 6373440949..0000000000 --- a/core/src/ten_manager/frontend/src/components/AppBar/DropdownMenu.scss +++ /dev/null @@ -1,36 +0,0 @@ -// -// 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 deleted file mode 100644 index 7a5d862451..0000000000 --- a/core/src/ten_manager/frontend/src/components/AppBar/DropdownMenu.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// -// 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/Popup/Popup.tsx b/core/src/ten_manager/frontend/src/components/Popup/Popup.tsx deleted file mode 100644 index 0a2b23ccf1..0000000000 --- a/core/src/ten_manager/frontend/src/components/Popup/Popup.tsx +++ /dev/null @@ -1,140 +0,0 @@ -// -// 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 deleted file mode 100644 index 6591b93835..0000000000 --- a/core/src/ten_manager/frontend/src/components/SettingsPopup/SettingsPopup.scss +++ /dev/null @@ -1,42 +0,0 @@ -// -// 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: 10px; - - 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); - } - } -} - -.language-selector { - display: flex; - justify-content: flex-start; - align-items: center; - margin-top: 10px; - - select { - margin-left: 10px; - padding: 5px; - border: 1px solid var(--button-border); - border-radius: 4px; - background: var(--button-bg); - color: var(--button-fg); - } -} diff --git a/core/src/ten_manager/frontend/src/flow/ContextMenu.scss b/core/src/ten_manager/frontend/src/flow/ContextMenu.scss deleted file mode 100644 index 3ca7669ab0..0000000000 --- a/core/src/ten_manager/frontend/src/flow/ContextMenu.scss +++ /dev/null @@ -1,23 +0,0 @@ -// -// 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. -// -.context-menu { - position: fixed; - background-color: #fff; - border: 1px solid #ccc; - padding: 5px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); - z-index: 9999; -} - -.context-menu div { - padding: 5px 10px; - cursor: pointer; -} - -.context-menu div:hover { - background-color: #f0f0f0; -} diff --git a/core/src/ten_manager/frontend/src/flow/ContextMenu.tsx b/core/src/ten_manager/frontend/src/flow/ContextMenu.tsx deleted file mode 100644 index aff4f02f40..0000000000 --- a/core/src/ten_manager/frontend/src/flow/ContextMenu.tsx +++ /dev/null @@ -1,94 +0,0 @@ -// -// 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 "./ContextMenu.scss"; -import { useTranslation } from "react-i18next"; - -interface ContextMenuProps { - visible: boolean; - x: number; - y: number; - type?: "node" | "edge"; - data?: any; - onClose: () => void; -} - -const ContextMenu: React.FC = ({ - visible, - x, - y, - type, - data, - onClose, -}) => { - const { t } = useTranslation(); - - if (!visible) return null; - - const renderMenuItems = () => { - if (type === "node" && data) { - return ( - <> -
{ - onClose(); - }} - > - {t("Edit")} -
-
{ - onClose(); - }} - > - {t("Delete")} -
- - ); - } else if (type === "edge" && data) { - return ( - <> -
{ - onClose(); - }} - > - {t("Edit")} -
-
{ - onClose(); - }} - > - {t("Delete")} -
- - ); - } else { - return ( - <> -
Close Menu
- - ); - } - }; - - return ( -
e.stopPropagation()} - > - {renderMenuItems()} -
- ); -}; - -export default ContextMenu; diff --git a/core/src/ten_manager/frontend/src/flow/FlowCanvas.tsx b/core/src/ten_manager/frontend/src/flow/FlowCanvas.tsx deleted file mode 100644 index 486d4330ba..0000000000 --- a/core/src/ten_manager/frontend/src/flow/FlowCanvas.tsx +++ /dev/null @@ -1,302 +0,0 @@ -// -// 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, -} from "@xyflow/react"; -import dagre from "dagre"; -import CustomNode, { CustomNodeType } from "./CustomNode"; -import CustomEdge, { CustomEdgeType } from "./CustomEdge"; -import { ApiResponse, BackendNode, BackendConnection } from "../api/interface"; -import ContextMenu from "./ContextMenu"; - -const nodeWidth = 172; -const nodeHeight = 36; - -const getLayoutedElements = ( - nodes: CustomNodeType[], - edges: CustomEdgeType[], - 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; -} - -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 context menu. - const closeContextMenu = useCallback(() => { - setContextMenu({ visible: false, x: 0, y: 0 }); - }, []); - - // Click empty space to close context menu. - useEffect(() => { - const handleClick = () => { - closeContextMenu(); - }; - window.addEventListener("click", handleClick); - return () => window.removeEventListener("click", handleClick); - }, [closeContextMenu]); - - // Export `performAutoLayout`. - 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/default/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/default/connections` - ); - if (!connectionsRes.ok) { - throw new Error( - `Failed to fetch connections: ${connectionsRes.status}` - ); - } - const connectionsPayload: ApiResponse = - await connectionsRes.json(); - - // Create initial nodes. - let initialNodes: CustomNodeType[] = nodesPayload.data.map( - (n, index) => ({ - id: n.name, - position: { x: index * 200, y: 100 }, - type: "customNode", - data: { - label: `${n.name}`, - sourceCmds: [], - targetCmds: [], - }, - }) - ); - - // Parse connections to collect cmds for all nodes, so that the correct - // handles could be generated later. - let initialEdges: CustomEdgeType[] = []; - 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: "customEdge", - label: cmdName, - sourceHandle: `source-${cmdName}`, - targetHandle: `target-${cmdName}`, - markerEnd: { - type: MarkerType.ArrowClosed, - }, - }); - }); - }); - } - }); - - // Write back the cmd information to nodes, so that CustomNode could - // generate corresponding 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 | CustomEdgeType) => { - setEdges((eds) => addEdge(params, eds)); - }, - [setEdges] - ); - - // Right click nodes. - const onNodeContextMenu = useCallback((event: MouseEvent, node: Node) => { - event.preventDefault(); - setContextMenu({ - visible: true, - x: event.clientX, - y: event.clientY, - type: "node", - data: node, - }); - }, []); - - // Right click Edges. - 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}
} - - -
- ); -}); - -export default FlowCanvas; diff --git a/core/src/ten_manager/src/cmd/cmd_dev_server.rs b/core/src/ten_manager/src/cmd/cmd_designer.rs similarity index 96% rename from core/src/ten_manager/src/cmd/cmd_dev_server.rs rename to core/src/ten_manager/src/cmd/cmd_designer.rs index 9b43d440a6..5f2a19db6f 100644 --- a/core/src/ten_manager/src/cmd/cmd_dev_server.rs +++ b/core/src/ten_manager/src/cmd/cmd_designer.rs @@ -18,11 +18,11 @@ use console::Emoji; use crate::{ config::TmanConfig, - dev_server::{ + designer::{ configure_routes, // TODO(Wei): Enable this. // frontend::get_frontend_asset, - DevServerState, + DesignerState, }, error::TmanError, log::tman_verbose_println, @@ -89,8 +89,8 @@ pub fn create_sub_cmd(_args_cfg: &crate::cmd_line::ArgsCfg) -> Command { pub fn parse_sub_cmd( sub_cmd_args: &ArgMatches, -) -> crate::cmd::cmd_dev_server::DevServerCommand { - let cmd = crate::cmd::cmd_dev_server::DevServerCommand { +) -> crate::cmd::cmd_designer::DevServerCommand { + let cmd = crate::cmd::cmd_designer::DevServerCommand { ip_address: sub_cmd_args .get_one::("IP_ADDRESS") .unwrap() @@ -118,7 +118,7 @@ pub async fn execute_cmd( .base_dir .unwrap_or_else(|| cwd.to_str().unwrap().to_string()); - let state = Arc::new(RwLock::new(DevServerState { + let state = Arc::new(RwLock::new(DesignerState { base_dir: Some(base_dir.clone()), all_pkgs: None, tman_config: TmanConfig::default(), diff --git a/core/src/ten_manager/src/cmd/mod.rs b/core/src/ten_manager/src/cmd/mod.rs index cfa905b388..1c367b04e7 100644 --- a/core/src/ten_manager/src/cmd/mod.rs +++ b/core/src/ten_manager/src/cmd/mod.rs @@ -6,7 +6,7 @@ // pub mod cmd_check; pub mod cmd_delete; -pub mod cmd_dev_server; +pub mod cmd_designer; pub mod cmd_install; pub mod cmd_package; pub mod cmd_publish; @@ -22,7 +22,7 @@ pub enum CommandData { Package(self::cmd_package::PackageCommand), Publish(self::cmd_publish::PublishCommand), Delete(self::cmd_delete::DeleteCommand), - DevServer(self::cmd_dev_server::DevServerCommand), + DevServer(self::cmd_designer::DevServerCommand), Check(self::cmd_check::CheckCommandData), } @@ -47,7 +47,7 @@ pub async fn execute_cmd( crate::cmd::cmd_delete::execute_cmd(tman_config, cmd).await } CommandData::DevServer(cmd) => { - crate::cmd::cmd_dev_server::execute_cmd(tman_config, cmd).await + crate::cmd::cmd_designer::execute_cmd(tman_config, cmd).await } CommandData::Check(cmd) => { crate::cmd::cmd_check::execute_cmd(tman_config, cmd).await diff --git a/core/src/ten_manager/src/cmd_line.rs b/core/src/ten_manager/src/cmd_line.rs index cbf3966650..dfb152320c 100644 --- a/core/src/ten_manager/src/cmd_line.rs +++ b/core/src/ten_manager/src/cmd_line.rs @@ -100,7 +100,7 @@ fn create_cmd() -> clap::ArgMatches { .subcommand(crate::cmd::cmd_package::create_sub_cmd(&args_cfg)) .subcommand(crate::cmd::cmd_publish::create_sub_cmd(&args_cfg)) .subcommand(crate::cmd::cmd_delete::create_sub_cmd(&args_cfg)) - .subcommand(crate::cmd::cmd_dev_server::create_sub_cmd(&args_cfg)) + .subcommand(crate::cmd::cmd_designer::create_sub_cmd(&args_cfg)) .subcommand(crate::cmd::cmd_check::create_sub_cmd(&args_cfg)) .get_matches() } @@ -137,7 +137,7 @@ pub fn parse_cmd( ), Some(("dev-server", sub_cmd_args)) => { crate::cmd::CommandData::DevServer( - crate::cmd::cmd_dev_server::parse_sub_cmd(sub_cmd_args), + crate::cmd::cmd_designer::parse_sub_cmd(sub_cmd_args), ) } Some(("check", sub_cmd_args)) => crate::cmd::CommandData::Check( diff --git a/core/src/ten_manager/src/dev_server/addons/extensions.rs b/core/src/ten_manager/src/designer/addons/extensions.rs similarity index 52% rename from core/src/ten_manager/src/dev_server/addons/extensions.rs rename to core/src/ten_manager/src/designer/addons/extensions.rs index f6800cf0c7..c1977f74d9 100644 --- a/core/src/ten_manager/src/dev_server/addons/extensions.rs +++ b/core/src/ten_manager/src/designer/addons/extensions.rs @@ -9,18 +9,18 @@ use std::sync::{Arc, RwLock}; use actix_web::{web, HttpResponse, Responder}; use serde::{Deserialize, Serialize}; -use ten_rust::pkg_info::pkg_type::PkgType; +use ten_rust::pkg_info::{pkg_type::PkgType, PkgInfo}; -use crate::dev_server::{ +use crate::designer::{ common::{ - get_dev_server_api_cmd_likes_from_pkg, - get_dev_server_api_data_likes_from_pkg, - get_dev_server_property_hashmap_from_pkg, + get_designer_api_cmd_likes_from_pkg, + get_designer_api_data_likes_from_pkg, + get_designer_property_hashmap_from_pkg, }, get_all_pkgs::get_all_pkgs, graphs::nodes::DevServerApi, response::{ApiResponse, ErrorResponse, Status}, - DevServerState, + DesignerState, }; #[derive(Serialize, Deserialize, Debug, PartialEq)] @@ -28,120 +28,162 @@ struct DevServerExtensionAddon { #[serde(rename = "name")] addon_name: String, + url: String, + #[serde(skip_serializing_if = "Option::is_none")] pub api: Option, } -pub async fn get_extension_addons( - state: web::Data>>, -) -> impl Responder { - let mut state = state.write().unwrap(); - - // Fetch all packages if not already done. - if let Err(err) = get_all_pkgs(&mut state) { - let error_response = - ErrorResponse::from_error(&err, "Error fetching packages:"); - return HttpResponse::NotFound().json(error_response); +fn retrieve_extension_addons( + state: &mut DesignerState, +) -> Result, ErrorResponse> { + if let Err(err) = get_all_pkgs(state) { + return Err(ErrorResponse::from_error( + &err, + "Error fetching packages:", + )); } if let Some(all_pkgs) = &state.all_pkgs { - let mut extensions = Vec::new(); - - // Find all the packages with type "extension". - for pkg_info_with_src in all_pkgs { - if pkg_info_with_src.pkg_identity.pkg_type == PkgType::Extension { - extensions.push(DevServerExtensionAddon { - addon_name: pkg_info_with_src.pkg_identity.name.clone(), - api: pkg_info_with_src.api.as_ref().map(|api| { - DevServerApi { - property: if api.property.is_empty() { - None - } else { - Some(get_dev_server_property_hashmap_from_pkg( - api.property.clone(), - )) - }, + let extensions = all_pkgs + .iter() + .filter(|pkg| pkg.pkg_identity.pkg_type == PkgType::Extension) + .map(|pkg_info_with_src| { + map_pkg_to_extension_addon(pkg_info_with_src) + }) + .collect(); - cmd_in: if api.cmd_in.is_empty() { - None - } else { - Some(get_dev_server_api_cmd_likes_from_pkg( - api.cmd_in.clone(), - )) - }, - cmd_out: if api.cmd_out.is_empty() { - None - } else { - Some(get_dev_server_api_cmd_likes_from_pkg( - api.cmd_out.clone(), - )) - }, + Ok(extensions) + } else { + Err(ErrorResponse { + status: Status::Fail, + message: "Base directory or package information is not set" + .to_string(), + error: None, + }) + } +} - data_in: if api.data_in.is_empty() { - None - } else { - Some(get_dev_server_api_data_likes_from_pkg( - api.data_in.clone(), - )) - }, - data_out: if api.data_out.is_empty() { - None - } else { - Some(get_dev_server_api_data_likes_from_pkg( - api.data_out.clone(), - )) - }, +fn map_pkg_to_extension_addon( + pkg_info_with_src: &PkgInfo, +) -> DevServerExtensionAddon { + DevServerExtensionAddon { + addon_name: pkg_info_with_src.pkg_identity.name.clone(), + url: pkg_info_with_src.url.clone(), + api: pkg_info_with_src.api.as_ref().map(|api| DevServerApi { + property: if api.property.is_empty() { + None + } else { + Some(get_designer_property_hashmap_from_pkg( + api.property.clone(), + )) + }, - audio_frame_in: if api.audio_frame_in.is_empty() { - None - } else { - Some(get_dev_server_api_data_likes_from_pkg( - api.audio_frame_in.clone(), - )) - }, - audio_frame_out: if api.audio_frame_out.is_empty() { - None - } else { - Some(get_dev_server_api_data_likes_from_pkg( - api.audio_frame_out.clone(), - )) - }, + cmd_in: if api.cmd_in.is_empty() { + None + } else { + Some(get_designer_api_cmd_likes_from_pkg(api.cmd_in.clone())) + }, + cmd_out: if api.cmd_out.is_empty() { + None + } else { + Some(get_designer_api_cmd_likes_from_pkg(api.cmd_out.clone())) + }, - video_frame_in: if api.video_frame_in.is_empty() { - None - } else { - Some(get_dev_server_api_data_likes_from_pkg( - api.video_frame_in.clone(), - )) - }, - video_frame_out: if api.video_frame_out.is_empty() { - None - } else { - Some(get_dev_server_api_data_likes_from_pkg( - api.video_frame_out.clone(), - )) - }, - } - }), - }); - } + data_in: if api.data_in.is_empty() { + None + } else { + Some(get_designer_api_data_likes_from_pkg(api.data_in.clone())) + }, + data_out: if api.data_out.is_empty() { + None + } else { + Some(get_designer_api_data_likes_from_pkg(api.data_out.clone())) + }, + + audio_frame_in: if api.audio_frame_in.is_empty() { + None + } else { + Some(get_designer_api_data_likes_from_pkg( + api.audio_frame_in.clone(), + )) + }, + audio_frame_out: if api.audio_frame_out.is_empty() { + None + } else { + Some(get_designer_api_data_likes_from_pkg( + api.audio_frame_out.clone(), + )) + }, + + video_frame_in: if api.video_frame_in.is_empty() { + None + } else { + Some(get_designer_api_data_likes_from_pkg( + api.video_frame_in.clone(), + )) + }, + video_frame_out: if api.video_frame_out.is_empty() { + None + } else { + Some(get_designer_api_data_likes_from_pkg( + api.video_frame_out.clone(), + )) + }, + }), + } +} + +pub async fn get_extension_addons( + state: web::Data>>, +) -> impl Responder { + let mut state = state.write().unwrap(); + + match retrieve_extension_addons(&mut state) { + Ok(extensions) => { + let response = ApiResponse { + status: Status::Ok, + data: extensions, + meta: None, + }; + HttpResponse::Ok().json(response) } + Err(error_response) => HttpResponse::BadRequest().json(error_response), + } +} - let response = ApiResponse { - status: Status::Ok, - data: extensions, - meta: None, - }; +pub async fn get_extension_addon_by_name( + state: web::Data>>, + path: web::Path, // NEW +) -> impl Responder { + let addon_name = path.into_inner(); // NEW + let mut state = state.write().unwrap(); - HttpResponse::Ok().json(response) - } else { - let error_response = ErrorResponse { - status: Status::Fail, - message: "Base directory or package information is not set" - .to_string(), - error: None, - }; - HttpResponse::BadRequest().json(error_response) + match retrieve_extension_addons(&mut state) { + Ok(extensions) => { + if let Some(extension) = extensions + .into_iter() + .find(|ext| ext.addon_name == addon_name) + { + let response = ApiResponse { + status: Status::Ok, + data: extension, + meta: None, + }; + HttpResponse::Ok().json(response) + } else { + let error_response = ErrorResponse { + status: Status::Fail, + message: format!( + "Extension addon with name '{}' not found", + addon_name + ), + error: None, + }; + HttpResponse::NotFound().json(error_response) + } + } + Err(error_response) => HttpResponse::BadRequest().json(error_response), } } @@ -149,7 +191,7 @@ pub async fn get_extension_addons( mod tests { use crate::{ config::TmanConfig, - dev_server::{ + designer::{ graphs::nodes::{ DevServerApiCmdLike, DevServerApiDataLike, DevServerPropertyAttributes, DevServerPropertyItem, @@ -163,7 +205,7 @@ mod tests { #[actix_web::test] async fn test_get_extension_addons() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -192,13 +234,13 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/addons/extensions", web::get().to(get_extension_addons), ), @@ -221,6 +263,7 @@ mod tests { let expected_addons = vec![ DevServerExtensionAddon { addon_name: "extension_addon_1".to_string(), + url: "".to_string(), api: Some(DevServerApi { property: None, cmd_in: None, @@ -269,6 +312,7 @@ mod tests { }, DevServerExtensionAddon { addon_name: "extension_addon_2".to_string(), + url: "".to_string(), api: Some(DevServerApi { property: None, cmd_in: Some(vec![ @@ -337,6 +381,7 @@ mod tests { }, DevServerExtensionAddon { addon_name: "extension_addon_3".to_string(), + url: "".to_string(), api: Some(DevServerApi { property: None, cmd_in: Some(vec![DevServerApiCmdLike { @@ -377,4 +422,133 @@ mod tests { let pretty_json = serde_json::to_string_pretty(&json).unwrap(); println!("Response body: {}", pretty_json); } + + #[actix_web::test] + async fn test_get_extension_addon_by_name() { + let mut designer_state = DesignerState { + base_dir: None, + all_pkgs: None, + tman_config: TmanConfig::default(), + }; + + let all_pkgs_json = vec![ + ( + include_str!("test_data_embed/app_manifest.json").to_string(), + include_str!("test_data_embed/app_property.json").to_string(), + ), + ( + include_str!("test_data_embed/extension_addon_1_manifest.json") + .to_string(), + "{}".to_string(), + ), + ( + include_str!("test_data_embed/extension_addon_2_manifest.json") + .to_string(), + "{}".to_string(), + ), + ( + include_str!("test_data_embed/extension_addon_3_manifest.json") + .to_string(), + "{}".to_string(), + ), + ]; + + let inject_ret = + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); + assert!(inject_ret.is_ok()); + + let designer_state = Arc::new(RwLock::new(designer_state)); + + let app = test::init_service( + App::new() + .app_data(web::Data::new(designer_state.clone())) + .route( + "/api/dev-server/v1/addons/extensions/{name}", + web::get().to(get_extension_addon_by_name), + ), + ) + .await; + + let req = test::TestRequest::get() + .uri("/api/dev-server/v1/addons/extensions/extension_addon_1") + .to_request(); + let resp = test::call_service(&app, req).await; + + assert!(resp.status().is_success()); + + let body = test::read_body(resp).await; + let body_str = std::str::from_utf8(&body).unwrap(); + + let addon: ApiResponse = + serde_json::from_str(body_str).unwrap(); + + let expected_addon = DevServerExtensionAddon { + addon_name: "extension_addon_1".to_string(), + url: "".to_string(), + api: Some(DevServerApi { + property: None, + cmd_in: None, + cmd_out: Some(vec![ + DevServerApiCmdLike { + name: "test_cmd".to_string(), + property: Some(vec![DevServerPropertyItem { + name: "test_property".to_string(), + attributes: DevServerPropertyAttributes { + prop_type: "int8".to_string(), + }, + }]), + required: None, + result: None, + }, + DevServerApiCmdLike { + name: "has_required".to_string(), + property: Some(vec![DevServerPropertyItem { + name: "foo".to_string(), + attributes: DevServerPropertyAttributes { + prop_type: "string".to_string(), + }, + }]), + required: Some(vec!["foo".to_string()]), + result: None, + }, + DevServerApiCmdLike { + name: "has_required_mismatch".to_string(), + property: Some(vec![DevServerPropertyItem { + name: "foo".to_string(), + attributes: DevServerPropertyAttributes { + prop_type: "string".to_string(), + }, + }]), + required: Some(vec!["foo".to_string()]), + result: None, + }, + ]), + data_in: None, + data_out: None, + audio_frame_in: None, + audio_frame_out: None, + video_frame_in: None, + video_frame_out: None, + }), + }; + + assert_eq!(addon.data, expected_addon); + + let req = test::TestRequest::get() + .uri("/api/dev-server/v1/addons/extensions/non_existent_addon") + .to_request(); + let resp = test::call_service(&app, req).await; + + assert_eq!(resp.status(), actix_web::http::StatusCode::NOT_FOUND); + + let body = test::read_body(resp).await; + let body_str = std::str::from_utf8(&body).unwrap(); + + let error_response: ErrorResponse = + serde_json::from_str(body_str).unwrap(); + assert_eq!( + error_response.message, + "Extension addon with name 'non_existent_addon' not found" + ); + } } diff --git a/core/src/ten_manager/src/dev_server/addons/mod.rs b/core/src/ten_manager/src/designer/addons/mod.rs similarity index 100% rename from core/src/ten_manager/src/dev_server/addons/mod.rs rename to core/src/ten_manager/src/designer/addons/mod.rs diff --git a/core/src/ten_manager/src/dev_server/addons/test_data_embed/app_manifest.json b/core/src/ten_manager/src/designer/addons/test_data_embed/app_manifest.json similarity index 100% rename from core/src/ten_manager/src/dev_server/addons/test_data_embed/app_manifest.json rename to core/src/ten_manager/src/designer/addons/test_data_embed/app_manifest.json diff --git a/core/src/ten_manager/src/dev_server/addons/test_data_embed/app_property.json b/core/src/ten_manager/src/designer/addons/test_data_embed/app_property.json similarity index 100% rename from core/src/ten_manager/src/dev_server/addons/test_data_embed/app_property.json rename to core/src/ten_manager/src/designer/addons/test_data_embed/app_property.json diff --git a/core/src/ten_manager/src/dev_server/addons/test_data_embed/extension_addon_1_manifest.json b/core/src/ten_manager/src/designer/addons/test_data_embed/extension_addon_1_manifest.json similarity index 100% rename from core/src/ten_manager/src/dev_server/addons/test_data_embed/extension_addon_1_manifest.json rename to core/src/ten_manager/src/designer/addons/test_data_embed/extension_addon_1_manifest.json diff --git a/core/src/ten_manager/src/dev_server/addons/test_data_embed/extension_addon_2_manifest.json b/core/src/ten_manager/src/designer/addons/test_data_embed/extension_addon_2_manifest.json similarity index 100% rename from core/src/ten_manager/src/dev_server/addons/test_data_embed/extension_addon_2_manifest.json rename to core/src/ten_manager/src/designer/addons/test_data_embed/extension_addon_2_manifest.json diff --git a/core/src/ten_manager/src/dev_server/addons/test_data_embed/extension_addon_3_manifest.json b/core/src/ten_manager/src/designer/addons/test_data_embed/extension_addon_3_manifest.json similarity index 100% rename from core/src/ten_manager/src/dev_server/addons/test_data_embed/extension_addon_3_manifest.json rename to core/src/ten_manager/src/designer/addons/test_data_embed/extension_addon_3_manifest.json diff --git a/core/src/ten_manager/src/dev_server/common.rs b/core/src/ten_manager/src/designer/common.rs similarity index 87% rename from core/src/ten_manager/src/dev_server/common.rs rename to core/src/ten_manager/src/designer/common.rs index 12e8054544..ec2de4d50a 100644 --- a/core/src/ten_manager/src/dev_server/common.rs +++ b/core/src/ten_manager/src/designer/common.rs @@ -16,19 +16,19 @@ use super::graphs::nodes::{ DevServerApiCmdLike, DevServerApiDataLike, DevServerPropertyAttributes, }; -pub fn get_dev_server_property_hashmap_from_pkg( +pub fn get_designer_property_hashmap_from_pkg( items: HashMap, ) -> HashMap { items.into_iter().map(|(k, v)| (k, v.into())).collect() } -pub fn get_dev_server_api_cmd_likes_from_pkg( +pub fn get_designer_api_cmd_likes_from_pkg( items: Vec, ) -> Vec { items.into_iter().map(|v| v.into()).collect() } -pub fn get_dev_server_api_data_likes_from_pkg( +pub fn get_designer_api_data_likes_from_pkg( items: Vec, ) -> Vec { items.into_iter().map(|v| v.into()).collect() diff --git a/core/src/ten_manager/src/designer/file_content/mod.rs b/core/src/ten_manager/src/designer/file_content/mod.rs new file mode 100644 index 0000000000..b6c45e87ab --- /dev/null +++ b/core/src/ten_manager/src/designer/file_content/mod.rs @@ -0,0 +1,48 @@ +// +// 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 std::fs; +use std::sync::{Arc, RwLock}; + +use actix_web::{web, HttpResponse, Responder}; +use serde::{Deserialize, Serialize}; + +use super::{ + response::{ApiResponse, Status}, + DesignerState, +}; + +#[derive(Serialize, Deserialize, Debug)] +struct FileContentResponse { + content: String, +} + +pub async fn get_file_content( + path: web::Path, + _state: web::Data>>, +) -> impl Responder { + let file_path = path.into_inner(); + + match fs::read_to_string(&file_path) { + Ok(content) => { + let response = ApiResponse { + status: Status::Ok, + data: FileContentResponse { content }, + meta: None, + }; + HttpResponse::Ok().json(response) + } + Err(err) => { + eprintln!("Error reading file at path {}: {}", file_path, err); + let response = ApiResponse { + status: Status::Fail, + data: (), + meta: None, + }; + HttpResponse::BadRequest().json(response) + } + } +} diff --git a/core/src/ten_manager/src/dev_server/frontend.rs b/core/src/ten_manager/src/designer/frontend.rs similarity index 100% rename from core/src/ten_manager/src/dev_server/frontend.rs rename to core/src/ten_manager/src/designer/frontend.rs diff --git a/core/src/ten_manager/src/dev_server/get_all_pkgs.rs b/core/src/ten_manager/src/designer/get_all_pkgs.rs similarity index 90% rename from core/src/ten_manager/src/dev_server/get_all_pkgs.rs rename to core/src/ten_manager/src/designer/get_all_pkgs.rs index fb3cc306c0..c8e186585a 100644 --- a/core/src/ten_manager/src/dev_server/get_all_pkgs.rs +++ b/core/src/ten_manager/src/designer/get_all_pkgs.rs @@ -6,10 +6,10 @@ // use anyhow::{anyhow, Result}; -use super::DevServerState; +use super::DesignerState; use crate::package_info::tman_get_all_existed_pkgs_info_of_app; -pub fn get_all_pkgs(state: &mut DevServerState) -> Result<()> { +pub fn get_all_pkgs(state: &mut DesignerState) -> Result<()> { use std::path::PathBuf; if state.all_pkgs.is_none() { diff --git a/core/src/ten_manager/src/dev_server/graphs/connections.rs b/core/src/ten_manager/src/designer/graphs/connections.rs similarity index 90% rename from core/src/ten_manager/src/dev_server/graphs/connections.rs rename to core/src/ten_manager/src/designer/graphs/connections.rs index d619ba50bc..d05e491a0c 100644 --- a/core/src/ten_manager/src/dev_server/graphs/connections.rs +++ b/core/src/ten_manager/src/designer/graphs/connections.rs @@ -16,9 +16,9 @@ use ten_rust::pkg_info::graph::{ use ten_rust::pkg_info::pkg_type::PkgType; use ten_rust::pkg_info::predefined_graphs::pkg_predefined_graphs_find; -use crate::dev_server::get_all_pkgs::get_all_pkgs; -use crate::dev_server::response::{ApiResponse, ErrorResponse, Status}; -use crate::dev_server::DevServerState; +use crate::designer::get_all_pkgs::get_all_pkgs; +use crate::designer::response::{ApiResponse, ErrorResponse, Status}; +use crate::designer::DesignerState; #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct DevServerConnection { @@ -46,17 +46,17 @@ impl From for DevServerConnection { extension_group: conn.extension_group, extension: conn.extension, - cmd: conn.cmd.map(get_dev_server_msg_flow_from_property), + cmd: conn.cmd.map(get_designer_msg_flow_from_property), - data: conn.data.map(get_dev_server_msg_flow_from_property), + data: conn.data.map(get_designer_msg_flow_from_property), audio_frame: conn .audio_frame - .map(get_dev_server_msg_flow_from_property), + .map(get_designer_msg_flow_from_property), video_frame: conn .video_frame - .map(get_dev_server_msg_flow_from_property), + .map(get_designer_msg_flow_from_property), } } } @@ -71,12 +71,12 @@ impl From for DevServerMessageFlow { fn from(msg_flow: GraphMessageFlow) -> Self { DevServerMessageFlow { name: msg_flow.name, - dest: get_dev_server_destination_from_property(msg_flow.dest), + dest: get_designer_destination_from_property(msg_flow.dest), } } } -fn get_dev_server_msg_flow_from_property( +fn get_designer_msg_flow_from_property( msg_flow: Vec, ) -> Vec { if msg_flow.is_empty() { @@ -107,14 +107,14 @@ impl From for DevServerDestination { } } -fn get_dev_server_destination_from_property( +fn get_designer_destination_from_property( destinations: Vec, ) -> Vec { destinations.into_iter().map(|v| v.into()).collect() } pub async fn get_graph_connections( - state: web::Data>>, + state: web::Data>>, path: web::Path, ) -> impl Responder { let graph_name = path.into_inner(); @@ -177,13 +177,13 @@ mod tests { use super::*; use crate::{ - config::TmanConfig, dev_server::mock::tests::inject_all_pkgs_for_mock, + config::TmanConfig, designer::mock::tests::inject_all_pkgs_for_mock, }; use ten_rust::pkg_info::localhost; #[actix_web::test] async fn test_get_connections_success() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -212,13 +212,13 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/graphs/{graph_name}/connections", web::get().to(get_graph_connections), ), @@ -267,7 +267,7 @@ mod tests { #[actix_web::test] async fn test_get_connections_have_all_data_type() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -295,12 +295,12 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/graphs/{graph_name}/connections", web::get().to(get_graph_connections), ), diff --git a/core/src/ten_manager/src/dev_server/graphs/mod.rs b/core/src/ten_manager/src/designer/graphs/mod.rs similarity index 91% rename from core/src/ten_manager/src/dev_server/graphs/mod.rs rename to core/src/ten_manager/src/designer/graphs/mod.rs index 750088e0e7..688bdd4948 100644 --- a/core/src/ten_manager/src/dev_server/graphs/mod.rs +++ b/core/src/ten_manager/src/designer/graphs/mod.rs @@ -18,7 +18,7 @@ use ten_rust::pkg_info::pkg_type::PkgType; use super::{ get_all_pkgs::get_all_pkgs, response::{ApiResponse, ErrorResponse, Status}, - DevServerState, + DesignerState, }; #[derive(Serialize, Deserialize, Debug, PartialEq)] @@ -28,7 +28,7 @@ pub struct RespGraph { } pub async fn get_graphs( - state: web::Data>>, + state: web::Data>>, ) -> Result { let mut state = state.write().unwrap(); @@ -82,7 +82,7 @@ pub async fn get_graphs( #[cfg(test)] mod tests { use crate::{ - config::TmanConfig, dev_server::mock::tests::inject_all_pkgs_for_mock, + config::TmanConfig, designer::mock::tests::inject_all_pkgs_for_mock, }; use super::*; @@ -90,7 +90,7 @@ mod tests { #[actix_web::test] async fn test_get_graphs_success() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -119,14 +119,14 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( App::new() - .app_data(web::Data::new(dev_server_state)) + .app_data(web::Data::new(designer_state)) .route("/api/dev-server/v1/graphs", web::get().to(get_graphs)), ) .await; @@ -165,7 +165,7 @@ mod tests { #[actix_web::test] async fn test_get_graphs_no_app_package() { - let dev_server_state = Arc::new(RwLock::new(DevServerState { + let designer_state = Arc::new(RwLock::new(DesignerState { base_dir: None, all_pkgs: Some(vec![]), tman_config: TmanConfig::default(), @@ -173,7 +173,7 @@ mod tests { let app = test::init_service( App::new() - .app_data(web::Data::new(dev_server_state)) + .app_data(web::Data::new(designer_state)) .route("/api/dev-server/v1/graphs", web::get().to(get_graphs)), ) .await; diff --git a/core/src/ten_manager/src/dev_server/graphs/nodes.rs b/core/src/ten_manager/src/designer/graphs/nodes.rs similarity index 92% rename from core/src/ten_manager/src/dev_server/graphs/nodes.rs rename to core/src/ten_manager/src/designer/graphs/nodes.rs index d3484db2dd..42fa1a39e4 100644 --- a/core/src/ten_manager/src/dev_server/graphs/nodes.rs +++ b/core/src/ten_manager/src/designer/graphs/nodes.rs @@ -20,14 +20,13 @@ use ten_rust::pkg_info::{ api::PkgCmdResult, predefined_graphs::extension::get_pkg_info_for_extension, }; -use crate::dev_server::common::{ - get_dev_server_api_cmd_likes_from_pkg, - get_dev_server_api_data_likes_from_pkg, - get_dev_server_property_hashmap_from_pkg, +use crate::designer::common::{ + get_designer_api_cmd_likes_from_pkg, get_designer_api_data_likes_from_pkg, + get_designer_property_hashmap_from_pkg, }; -use crate::dev_server::get_all_pkgs::get_all_pkgs; -use crate::dev_server::response::{ApiResponse, ErrorResponse, Status}; -use crate::dev_server::DevServerState; +use crate::designer::get_all_pkgs::get_all_pkgs; +use crate::designer::response::{ApiResponse, ErrorResponse, Status}; +use crate::designer::DesignerState; #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct DevServerExtension { @@ -151,7 +150,7 @@ impl From for DevServerApiCmdLike { property: if api_cmd_like.property.is_empty() { None } else { - Some(get_dev_server_property_items_from_pkg( + Some(get_designer_property_items_from_pkg( api_cmd_like.property, )) }, @@ -187,7 +186,7 @@ impl From for DevServerApiDataLike { property: if api_data_like.property.is_empty() { None } else { - Some(get_dev_server_property_items_from_pkg( + Some(get_designer_property_items_from_pkg( api_data_like.property, )) }, @@ -200,14 +199,14 @@ impl From for DevServerApiDataLike { } } -fn get_dev_server_property_items_from_pkg( +fn get_designer_property_items_from_pkg( items: Vec, ) -> Vec { items.into_iter().map(|v| v.into()).collect() } pub async fn get_graph_nodes( - state: web::Data>>, + state: web::Data>>, path: web::Path, ) -> impl Responder { let graph_name = path.into_inner(); @@ -262,7 +261,7 @@ pub async fn get_graph_nodes( property: if api.property.is_empty() { None } else { - Some(get_dev_server_property_hashmap_from_pkg( + Some(get_designer_property_hashmap_from_pkg( api.property.clone(), )) }, @@ -270,14 +269,14 @@ pub async fn get_graph_nodes( cmd_in: if api.cmd_in.is_empty() { None } else { - Some(get_dev_server_api_cmd_likes_from_pkg( + Some(get_designer_api_cmd_likes_from_pkg( api.cmd_in.clone(), )) }, cmd_out: if api.cmd_out.is_empty() { None } else { - Some(get_dev_server_api_cmd_likes_from_pkg( + Some(get_designer_api_cmd_likes_from_pkg( api.cmd_out.clone(), )) }, @@ -285,14 +284,14 @@ pub async fn get_graph_nodes( data_in: if api.data_in.is_empty() { None } else { - Some(get_dev_server_api_data_likes_from_pkg( + Some(get_designer_api_data_likes_from_pkg( api.data_in.clone(), )) }, data_out: if api.data_out.is_empty() { None } else { - Some(get_dev_server_api_data_likes_from_pkg( + Some(get_designer_api_data_likes_from_pkg( api.data_out.clone(), )) }, @@ -300,14 +299,14 @@ pub async fn get_graph_nodes( audio_frame_in: if api.audio_frame_in.is_empty() { None } else { - Some(get_dev_server_api_data_likes_from_pkg( + Some(get_designer_api_data_likes_from_pkg( api.audio_frame_in.clone(), )) }, audio_frame_out: if api.audio_frame_out.is_empty() { None } else { - Some(get_dev_server_api_data_likes_from_pkg( + Some(get_designer_api_data_likes_from_pkg( api.audio_frame_out.clone(), )) }, @@ -315,14 +314,14 @@ pub async fn get_graph_nodes( video_frame_in: if api.video_frame_in.is_empty() { None } else { - Some(get_dev_server_api_data_likes_from_pkg( + Some(get_designer_api_data_likes_from_pkg( api.video_frame_in.clone(), )) }, video_frame_out: if api.video_frame_out.is_empty() { None } else { - Some(get_dev_server_api_data_likes_from_pkg( + Some(get_designer_api_data_likes_from_pkg( api.video_frame_out.clone(), )) }, @@ -355,13 +354,13 @@ mod tests { use super::*; use crate::{ - config::TmanConfig, dev_server::mock::tests::inject_all_pkgs_for_mock, + config::TmanConfig, designer::mock::tests::inject_all_pkgs_for_mock, }; use ten_rust::pkg_info::localhost; #[actix_web::test] async fn test_get_extensions_success() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -390,13 +389,13 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/graphs/{graph_name}/nodes", web::get().to(get_graph_nodes), ), @@ -594,14 +593,14 @@ mod tests { #[actix_web::test] async fn test_get_extensions_no_graph() { - let dev_server_state = Arc::new(RwLock::new(DevServerState { + let designer_state = Arc::new(RwLock::new(DesignerState { base_dir: None, all_pkgs: Some(vec![]), tman_config: TmanConfig::default(), })); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/graphs/{graph_name}/extensions", web::get().to(get_graph_nodes), ), @@ -627,7 +626,7 @@ mod tests { #[actix_web::test] async fn test_get_extensions_has_wrong_addon() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -656,13 +655,13 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/graphs/{graph_name}/extensions", web::get().to(get_graph_nodes), ), diff --git a/core/src/ten_manager/src/dev_server/graphs/test_data_embed/app_manifest.json b/core/src/ten_manager/src/designer/graphs/test_data_embed/app_manifest.json similarity index 100% rename from core/src/ten_manager/src/dev_server/graphs/test_data_embed/app_manifest.json rename to core/src/ten_manager/src/designer/graphs/test_data_embed/app_manifest.json diff --git a/core/src/ten_manager/src/dev_server/graphs/test_data_embed/app_property.json b/core/src/ten_manager/src/designer/graphs/test_data_embed/app_property.json similarity index 100% rename from core/src/ten_manager/src/dev_server/graphs/test_data_embed/app_property.json rename to core/src/ten_manager/src/designer/graphs/test_data_embed/app_property.json diff --git a/core/src/ten_manager/src/dev_server/graphs/test_data_embed/extension_addon_1_manifest.json b/core/src/ten_manager/src/designer/graphs/test_data_embed/extension_addon_1_manifest.json similarity index 100% rename from core/src/ten_manager/src/dev_server/graphs/test_data_embed/extension_addon_1_manifest.json rename to core/src/ten_manager/src/designer/graphs/test_data_embed/extension_addon_1_manifest.json diff --git a/core/src/ten_manager/src/dev_server/graphs/test_data_embed/extension_addon_2_manifest.json b/core/src/ten_manager/src/designer/graphs/test_data_embed/extension_addon_2_manifest.json similarity index 100% rename from core/src/ten_manager/src/dev_server/graphs/test_data_embed/extension_addon_2_manifest.json rename to core/src/ten_manager/src/designer/graphs/test_data_embed/extension_addon_2_manifest.json diff --git a/core/src/ten_manager/src/dev_server/graphs/test_data_embed/extension_addon_3_manifest.json b/core/src/ten_manager/src/designer/graphs/test_data_embed/extension_addon_3_manifest.json similarity index 100% rename from core/src/ten_manager/src/dev_server/graphs/test_data_embed/extension_addon_3_manifest.json rename to core/src/ten_manager/src/designer/graphs/test_data_embed/extension_addon_3_manifest.json diff --git a/core/src/ten_manager/src/dev_server/graphs/test_data_embed/get_connections_have_all_data_type/app_manifest.json b/core/src/ten_manager/src/designer/graphs/test_data_embed/get_connections_have_all_data_type/app_manifest.json similarity index 100% rename from core/src/ten_manager/src/dev_server/graphs/test_data_embed/get_connections_have_all_data_type/app_manifest.json rename to core/src/ten_manager/src/designer/graphs/test_data_embed/get_connections_have_all_data_type/app_manifest.json diff --git a/core/src/ten_manager/src/dev_server/graphs/test_data_embed/get_connections_have_all_data_type/app_property.json b/core/src/ten_manager/src/designer/graphs/test_data_embed/get_connections_have_all_data_type/app_property.json similarity index 100% rename from core/src/ten_manager/src/dev_server/graphs/test_data_embed/get_connections_have_all_data_type/app_property.json rename to core/src/ten_manager/src/designer/graphs/test_data_embed/get_connections_have_all_data_type/app_property.json diff --git a/core/src/ten_manager/src/dev_server/graphs/test_data_embed/get_connections_have_all_data_type/extension_addon_1_manifest.json b/core/src/ten_manager/src/designer/graphs/test_data_embed/get_connections_have_all_data_type/extension_addon_1_manifest.json similarity index 100% rename from core/src/ten_manager/src/dev_server/graphs/test_data_embed/get_connections_have_all_data_type/extension_addon_1_manifest.json rename to core/src/ten_manager/src/designer/graphs/test_data_embed/get_connections_have_all_data_type/extension_addon_1_manifest.json diff --git a/core/src/ten_manager/src/dev_server/graphs/test_data_embed/get_connections_have_all_data_type/extension_addon_2_manifest.json b/core/src/ten_manager/src/designer/graphs/test_data_embed/get_connections_have_all_data_type/extension_addon_2_manifest.json similarity index 100% rename from core/src/ten_manager/src/dev_server/graphs/test_data_embed/get_connections_have_all_data_type/extension_addon_2_manifest.json rename to core/src/ten_manager/src/designer/graphs/test_data_embed/get_connections_have_all_data_type/extension_addon_2_manifest.json diff --git a/core/src/ten_manager/src/dev_server/graphs/test_data_embed/update_graph_success/input_data.json b/core/src/ten_manager/src/designer/graphs/test_data_embed/update_graph_success/input_data.json similarity index 100% rename from core/src/ten_manager/src/dev_server/graphs/test_data_embed/update_graph_success/input_data.json rename to core/src/ten_manager/src/designer/graphs/test_data_embed/update_graph_success/input_data.json diff --git a/core/src/ten_manager/src/dev_server/graphs/update.rs b/core/src/ten_manager/src/designer/graphs/update.rs similarity index 92% rename from core/src/ten_manager/src/dev_server/graphs/update.rs rename to core/src/ten_manager/src/designer/graphs/update.rs index 7440da451a..1f7bf59fd3 100644 --- a/core/src/ten_manager/src/dev_server/graphs/update.rs +++ b/core/src/ten_manager/src/designer/graphs/update.rs @@ -13,8 +13,8 @@ use ten_rust::pkg_info::pkg_type::PkgType; use ten_rust::pkg_info::predefined_graphs::get_pkg_predefined_graph_from_nodes_and_connections; use super::{connections::DevServerConnection, nodes::DevServerExtension}; -use crate::dev_server::response::{ApiResponse, ErrorResponse, Status}; -use crate::dev_server::{get_all_pkgs::get_all_pkgs, DevServerState}; +use crate::designer::response::{ApiResponse, ErrorResponse, Status}; +use crate::designer::{get_all_pkgs::get_all_pkgs, DesignerState}; #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub struct GraphUpdateRequest { @@ -31,7 +31,7 @@ pub struct GraphUpdateResponse { pub async fn update_graph( req: web::Path, body: web::Json, - state: web::Data>>, + state: web::Data>>, ) -> impl Responder { let graph_name = req.into_inner(); let mut state = state.write().unwrap(); @@ -102,7 +102,7 @@ pub async fn update_graph( mod tests { use super::*; use crate::{ - config::TmanConfig, dev_server::mock::tests::inject_all_pkgs_for_mock, + config::TmanConfig, designer::mock::tests::inject_all_pkgs_for_mock, }; use actix_web::{test, App}; use std::{env, fs}; @@ -123,7 +123,7 @@ mod tests { fs::create_dir_all(&test_data_dir) .expect("Failed to create test_data directory"); - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: Some(test_data_dir.to_string_lossy().to_string()), all_pkgs: None, tman_config: TmanConfig::default(), @@ -152,14 +152,14 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( App::new() - .app_data(web::Data::new(dev_server_state.clone())) + .app_data(web::Data::new(designer_state.clone())) .route( "/api/dev-server/v1/graphs/{graph_name}", web::put().to(update_graph), @@ -181,7 +181,7 @@ mod tests { assert!(resp.status().is_success()); - match &dev_server_state.clone().read().unwrap().all_pkgs { + match &designer_state.clone().read().unwrap().all_pkgs { Some(pkgs) => { let app_pkg = pkgs .iter() diff --git a/core/src/ten_manager/src/dev_server/manifest/check.rs b/core/src/ten_manager/src/designer/manifest/check.rs similarity index 96% rename from core/src/ten_manager/src/dev_server/manifest/check.rs rename to core/src/ten_manager/src/designer/manifest/check.rs index 78c07aff12..cbbb8b2fc4 100644 --- a/core/src/ten_manager/src/dev_server/manifest/check.rs +++ b/core/src/ten_manager/src/designer/manifest/check.rs @@ -11,11 +11,11 @@ use serde::Serialize; use ten_rust::pkg_info::pkg_type::PkgType; -use crate::dev_server::{ +use crate::designer::{ common::CheckTypeQuery, get_all_pkgs::get_all_pkgs, response::{ApiResponse, ErrorResponse, Status}, - DevServerState, + DesignerState, }; #[derive(Serialize)] @@ -24,7 +24,7 @@ struct CheckResponse { } pub async fn check_manifest( - state: web::Data>>, + state: web::Data>>, query: web::Query, ) -> impl Responder { let mut state = state.write().unwrap(); diff --git a/core/src/ten_manager/src/dev_server/manifest/dump.rs b/core/src/ten_manager/src/designer/manifest/dump.rs similarity index 96% rename from core/src/ten_manager/src/dev_server/manifest/dump.rs rename to core/src/ten_manager/src/designer/manifest/dump.rs index 1132795d8d..e3a81c11a8 100644 --- a/core/src/ten_manager/src/dev_server/manifest/dump.rs +++ b/core/src/ten_manager/src/designer/manifest/dump.rs @@ -17,10 +17,10 @@ use ten_rust::pkg_info::pkg_type::PkgType; use crate::{ constants::MANIFEST_JSON_FILENAME, - dev_server::{ + designer::{ get_all_pkgs::get_all_pkgs, response::{ApiResponse, ErrorResponse, Status}, - DevServerState, + DesignerState, }, }; @@ -30,7 +30,7 @@ struct DumpResponse { } pub async fn dump_manifest( - state: web::Data>>, + state: web::Data>>, ) -> impl Responder { let mut state = state.write().unwrap(); diff --git a/core/src/ten_manager/src/dev_server/manifest/mod.rs b/core/src/ten_manager/src/designer/manifest/mod.rs similarity index 100% rename from core/src/ten_manager/src/dev_server/manifest/mod.rs rename to core/src/ten_manager/src/designer/manifest/mod.rs diff --git a/core/src/ten_manager/src/dev_server/messages/compatible.rs b/core/src/ten_manager/src/designer/messages/compatible.rs similarity index 92% rename from core/src/ten_manager/src/dev_server/messages/compatible.rs rename to core/src/ten_manager/src/designer/messages/compatible.rs index dd130a4434..f5b9dd2ba4 100644 --- a/core/src/ten_manager/src/dev_server/messages/compatible.rs +++ b/core/src/ten_manager/src/designer/messages/compatible.rs @@ -21,10 +21,10 @@ use ten_rust::pkg_info::{ }, }; -use crate::dev_server::{ +use crate::designer::{ get_all_pkgs::get_all_pkgs, response::{ApiResponse, ErrorResponse, Status}, - DevServerState, + DesignerState, }; #[derive(Debug, Deserialize, Serialize)] @@ -68,7 +68,7 @@ impl From> for DevServerCompatibleMsg { pub async fn get_compatible_messages( req: HttpRequest, - state: web::Data>>, + state: web::Data>>, input: Result, actix_web::Error>, ) -> impl Responder { if req.content_type() != "application/json" { @@ -309,12 +309,12 @@ mod tests { use super::*; use crate::{ - config::TmanConfig, dev_server::mock::tests::inject_all_pkgs_for_mock, + config::TmanConfig, designer::mock::tests::inject_all_pkgs_for_mock, }; #[actix_web::test] async fn test_get_compatible_messages_success() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -338,13 +338,13 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/messages/compatible", web::post().to(get_compatible_messages), ), @@ -394,7 +394,7 @@ mod tests { #[actix_web::test] async fn test_get_compatible_messages_fail() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -418,13 +418,13 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/messages/compatible", web::post().to(get_compatible_messages), ), @@ -456,7 +456,7 @@ mod tests { #[actix_web::test] async fn test_get_compatible_messages_cmd_has_required_success() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -480,13 +480,13 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/messages/compatible", web::post().to(get_compatible_messages), ), @@ -536,7 +536,7 @@ mod tests { #[actix_web::test] async fn test_get_compatible_messages_has_required_subset() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -560,13 +560,13 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/messages/compatible", web::post().to(get_compatible_messages), ), @@ -615,7 +615,7 @@ mod tests { #[actix_web::test] async fn test_get_compatible_messages_cmd_no_property() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -641,13 +641,13 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/messages/compatible", web::post().to(get_compatible_messages), ), @@ -707,7 +707,7 @@ mod tests { #[actix_web::test] async fn test_get_compatible_messages_cmd_property_overlap() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -733,13 +733,13 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/messages/compatible", web::post().to(get_compatible_messages), ), @@ -789,7 +789,7 @@ mod tests { #[actix_web::test] async fn test_get_compatible_messages_cmd_property_required_missing() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -815,13 +815,13 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/messages/compatible", web::post().to(get_compatible_messages), ), @@ -860,7 +860,7 @@ mod tests { #[actix_web::test] async fn test_get_compatible_messages_cmd_result() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -886,13 +886,13 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/messages/compatible", web::post().to(get_compatible_messages), ), @@ -941,7 +941,7 @@ mod tests { #[actix_web::test] async fn test_get_compatible_messages_data_no_property() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -967,13 +967,13 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/messages/compatible", web::post().to(get_compatible_messages), ), @@ -1024,7 +1024,7 @@ mod tests { // properties/items. // #[actix_web::test] async fn test_get_compatible_messages_data_required_superset() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -1050,13 +1050,13 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/messages/compatible", web::post().to(get_compatible_messages), ), @@ -1095,7 +1095,7 @@ mod tests { #[actix_web::test] async fn test_get_compatible_messages_video_target_has_property() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -1121,13 +1121,13 @@ mod tests { ]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/messages/compatible", web::post().to(get_compatible_messages), ), diff --git a/core/src/ten_manager/src/dev_server/messages/mod.rs b/core/src/ten_manager/src/designer/messages/mod.rs similarity index 100% rename from core/src/ten_manager/src/dev_server/messages/mod.rs rename to core/src/ten_manager/src/designer/messages/mod.rs diff --git a/core/src/ten_manager/src/dev_server/messages/test_data_embed/app_manifest.json b/core/src/ten_manager/src/designer/messages/test_data_embed/app_manifest.json similarity index 100% rename from core/src/ten_manager/src/dev_server/messages/test_data_embed/app_manifest.json rename to core/src/ten_manager/src/designer/messages/test_data_embed/app_manifest.json diff --git a/core/src/ten_manager/src/dev_server/messages/test_data_embed/app_property.json b/core/src/ten_manager/src/designer/messages/test_data_embed/app_property.json similarity index 100% rename from core/src/ten_manager/src/dev_server/messages/test_data_embed/app_property.json rename to core/src/ten_manager/src/designer/messages/test_data_embed/app_property.json diff --git a/core/src/ten_manager/src/dev_server/messages/test_data_embed/extension_addon_1_manifest.json b/core/src/ten_manager/src/designer/messages/test_data_embed/extension_addon_1_manifest.json similarity index 100% rename from core/src/ten_manager/src/dev_server/messages/test_data_embed/extension_addon_1_manifest.json rename to core/src/ten_manager/src/designer/messages/test_data_embed/extension_addon_1_manifest.json diff --git a/core/src/ten_manager/src/dev_server/messages/test_data_embed/extension_addon_2_manifest.json b/core/src/ten_manager/src/designer/messages/test_data_embed/extension_addon_2_manifest.json similarity index 100% rename from core/src/ten_manager/src/dev_server/messages/test_data_embed/extension_addon_2_manifest.json rename to core/src/ten_manager/src/designer/messages/test_data_embed/extension_addon_2_manifest.json diff --git a/core/src/ten_manager/src/dev_server/mock.rs b/core/src/ten_manager/src/designer/mock.rs similarity index 93% rename from core/src/ten_manager/src/dev_server/mock.rs rename to core/src/ten_manager/src/designer/mock.rs index 49c71879a5..b0fa427f63 100644 --- a/core/src/ten_manager/src/dev_server/mock.rs +++ b/core/src/ten_manager/src/designer/mock.rs @@ -13,10 +13,10 @@ pub mod tests { use ten_rust::pkg_info::PkgInfo; use ten_rust::pkg_info::{manifest::Manifest, property::Property}; - use crate::dev_server::DevServerState; + use crate::designer::DesignerState; pub fn inject_all_pkgs_for_mock( - state: &mut DevServerState, + state: &mut DesignerState, all_pkgs_manifest_json: Vec<(String, String)>, ) -> Result<()> { if state.all_pkgs.is_some() { diff --git a/core/src/ten_manager/src/dev_server/mod.rs b/core/src/ten_manager/src/designer/mod.rs similarity index 82% rename from core/src/ten_manager/src/dev_server/mod.rs rename to core/src/ten_manager/src/designer/mod.rs index 8a86246733..01e8be03d6 100644 --- a/core/src/ten_manager/src/dev_server/mod.rs +++ b/core/src/ten_manager/src/designer/mod.rs @@ -6,7 +6,7 @@ // mod addons; mod common; -// TODO(Wei): Enable this. +mod file_content; // pub mod frontend; mod get_all_pkgs; pub mod graphs; @@ -16,6 +16,7 @@ mod mock; mod packages; mod property; pub mod response; +mod terminal; mod version; use std::sync::{Arc, RwLock}; @@ -23,11 +24,12 @@ use std::sync::{Arc, RwLock}; use actix_web::web; use ten_rust::pkg_info::PkgInfo; +use terminal::ws_terminal; use super::config::TmanConfig; use version::get_version; -pub struct DevServerState { +pub struct DesignerState { pub base_dir: Option, pub all_pkgs: Option>, pub tman_config: TmanConfig, @@ -35,7 +37,7 @@ pub struct DevServerState { pub fn configure_routes( cfg: &mut web::ServiceConfig, - state: web::Data>>, + state: web::Data>>, ) { cfg.app_data(state.clone()) .route("/api/dev-server/v1/version", web::get().to(get_version)) @@ -43,6 +45,10 @@ pub fn configure_routes( "/api/dev-server/v1/addons/extensions", web::get().to(addons::extensions::get_extension_addons), ) + .route( + "/api/dev-server/v1/addons/extensions/{name}", + web::get().to(addons::extensions::get_extension_addon_by_name), + ) .route( "/api/dev-server/v1/packages/reload", web::post().to(packages::reload::clear_and_reload_pkgs), @@ -82,5 +88,10 @@ pub fn configure_routes( .route( "/api/dev-server/v1/messages/compatible", web::post().to(messages::compatible::get_compatible_messages), - ); + ) + .route( + "/api/dev-server/v1/file-content/{path}", + web::get().to(file_content::get_file_content), + ) + .route("/ws/terminal", web::get().to(ws_terminal)); } diff --git a/core/src/ten_manager/src/dev_server/packages/mod.rs b/core/src/ten_manager/src/designer/packages/mod.rs similarity index 100% rename from core/src/ten_manager/src/dev_server/packages/mod.rs rename to core/src/ten_manager/src/designer/packages/mod.rs diff --git a/core/src/ten_manager/src/dev_server/packages/reload.rs b/core/src/ten_manager/src/designer/packages/reload.rs similarity index 91% rename from core/src/ten_manager/src/dev_server/packages/reload.rs rename to core/src/ten_manager/src/designer/packages/reload.rs index 58294998fe..d6bbf6de31 100644 --- a/core/src/ten_manager/src/dev_server/packages/reload.rs +++ b/core/src/ten_manager/src/designer/packages/reload.rs @@ -8,14 +8,14 @@ use std::sync::{Arc, RwLock}; use actix_web::{web, HttpResponse, Responder}; -use crate::dev_server::{ +use crate::designer::{ get_all_pkgs::get_all_pkgs, response::{ApiResponse, ErrorResponse, Status}, - DevServerState, + DesignerState, }; pub async fn clear_and_reload_pkgs( - state: web::Data>>, + state: web::Data>>, ) -> impl Responder { let mut state = state.write().unwrap(); diff --git a/core/src/ten_manager/src/dev_server/property/check.rs b/core/src/ten_manager/src/designer/property/check.rs similarity index 89% rename from core/src/ten_manager/src/dev_server/property/check.rs rename to core/src/ten_manager/src/designer/property/check.rs index 68ff667351..3bc1234fcb 100644 --- a/core/src/ten_manager/src/dev_server/property/check.rs +++ b/core/src/ten_manager/src/designer/property/check.rs @@ -11,11 +11,11 @@ use serde::{Deserialize, Serialize}; use ten_rust::pkg_info::pkg_type::PkgType; -use crate::dev_server::{ +use crate::designer::{ common::CheckTypeQuery, get_all_pkgs::get_all_pkgs, response::{ApiResponse, ErrorResponse, Status}, - DevServerState, + DesignerState, }; #[derive(Serialize, Deserialize)] @@ -24,7 +24,7 @@ struct CheckResponse { } pub async fn check_property( - state: web::Data>>, + state: web::Data>>, query: web::Query, ) -> impl Responder { let mut state = state.write().unwrap(); @@ -84,14 +84,14 @@ pub async fn check_property( mod tests { use super::*; use crate::{ - config::TmanConfig, dev_server::mock::tests::inject_all_pkgs_for_mock, + config::TmanConfig, designer::mock::tests::inject_all_pkgs_for_mock, }; use actix_web::{test, App}; use std::vec; #[actix_web::test] async fn test_check_property_is_not_dirty() { - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), @@ -105,12 +105,12 @@ mod tests { )]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/property/check", web::get().to(check_property), ), diff --git a/core/src/ten_manager/src/dev_server/property/dump.rs b/core/src/ten_manager/src/designer/property/dump.rs similarity index 94% rename from core/src/ten_manager/src/dev_server/property/dump.rs rename to core/src/ten_manager/src/designer/property/dump.rs index cdaef6d0a4..53dfe0f90b 100644 --- a/core/src/ten_manager/src/dev_server/property/dump.rs +++ b/core/src/ten_manager/src/designer/property/dump.rs @@ -16,10 +16,10 @@ use ten_rust::pkg_info::pkg_type::PkgType; use crate::{ constants::PROPERTY_JSON_FILENAME, - dev_server::{ + designer::{ get_all_pkgs::get_all_pkgs, response::{ApiResponse, ErrorResponse, Status}, - DevServerState, + DesignerState, }, }; @@ -29,7 +29,7 @@ struct DumpResponse { } pub async fn dump_property( - state: web::Data>>, + state: web::Data>>, ) -> impl Responder { let mut state = state.write().unwrap(); @@ -101,7 +101,7 @@ mod tests { use super::*; use crate::{ config::TmanConfig, - dev_server::{ + designer::{ graphs::update::{update_graph, GraphUpdateRequest}, mock::tests::inject_all_pkgs_for_mock, }, @@ -124,7 +124,7 @@ mod tests { fs::create_dir_all(&test_data_dir) .expect("Failed to create test_data directory"); - let mut dev_server_state = DevServerState { + let mut designer_state = DesignerState { base_dir: Some(test_data_dir.to_string_lossy().to_string()), all_pkgs: None, tman_config: TmanConfig::default(), @@ -136,14 +136,14 @@ mod tests { )]; let inject_ret = - inject_all_pkgs_for_mock(&mut dev_server_state, all_pkgs_json); + inject_all_pkgs_for_mock(&mut designer_state, all_pkgs_json); assert!(inject_ret.is_ok()); - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( App::new() - .app_data(web::Data::new(dev_server_state.clone())) + .app_data(web::Data::new(designer_state.clone())) .route( "/api/dev-server/v1/graphs/{graph_name}", web::put().to(update_graph), diff --git a/core/src/ten_manager/src/dev_server/property/mod.rs b/core/src/ten_manager/src/designer/property/mod.rs similarity index 100% rename from core/src/ten_manager/src/dev_server/property/mod.rs rename to core/src/ten_manager/src/designer/property/mod.rs diff --git a/core/src/ten_manager/src/dev_server/property/test_data_embed/app_manifest.json b/core/src/ten_manager/src/designer/property/test_data_embed/app_manifest.json similarity index 100% rename from core/src/ten_manager/src/dev_server/property/test_data_embed/app_manifest.json rename to core/src/ten_manager/src/designer/property/test_data_embed/app_manifest.json diff --git a/core/src/ten_manager/src/dev_server/property/test_data_embed/app_property.json b/core/src/ten_manager/src/designer/property/test_data_embed/app_property.json similarity index 100% rename from core/src/ten_manager/src/dev_server/property/test_data_embed/app_property.json rename to core/src/ten_manager/src/designer/property/test_data_embed/app_property.json diff --git a/core/src/ten_manager/src/dev_server/property/test_data_embed/dump_property_success_input_data.json b/core/src/ten_manager/src/designer/property/test_data_embed/dump_property_success_input_data.json similarity index 100% rename from core/src/ten_manager/src/dev_server/property/test_data_embed/dump_property_success_input_data.json rename to core/src/ten_manager/src/designer/property/test_data_embed/dump_property_success_input_data.json diff --git a/core/src/ten_manager/src/dev_server/response.rs b/core/src/ten_manager/src/designer/response.rs similarity index 100% rename from core/src/ten_manager/src/dev_server/response.rs rename to core/src/ten_manager/src/designer/response.rs diff --git a/core/src/ten_manager/src/designer/terminal/mod.rs b/core/src/ten_manager/src/designer/terminal/mod.rs new file mode 100644 index 0000000000..c04cfacceb --- /dev/null +++ b/core/src/ten_manager/src/designer/terminal/mod.rs @@ -0,0 +1,214 @@ +// +// 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. +// +mod pty_manager; + +use std::collections::HashMap; +use std::fmt::Display; +use std::sync::{Arc, Mutex}; + +use actix::{Actor, AsyncContext, Message, StreamHandler}; +use actix_web::{web, Error, HttpRequest, HttpResponse}; +use actix_web_actors::ws; +use serde::__private::from_utf8_lossy; + +use pty_manager::PtyManager; +use serde_json::Value; + +// The message to/from the pty. +#[derive(Message, Debug, Eq, PartialEq)] +#[rtype(result = "()")] +pub enum PtyMessage { + Buffer(Vec), + + Exit(i32), +} + +impl Display for PtyMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PtyMessage::Buffer(data) => { + write!(f, "Buffer({:?})", from_utf8_lossy(data)) + } + PtyMessage::Exit(code) => { + write!(f, "Exit({})", code) + } + } + } +} + +struct WsProcessor { + pty: Arc>, +} + +impl WsProcessor { + fn new(path: String) -> Self { + Self { + pty: Arc::new(Mutex::new(PtyManager::new(path))), + } + } +} + +impl actix::Handler for WsProcessor { + type Result = (); + + fn handle( + &mut self, + msg: PtyMessage, + ctx: &mut Self::Context, + ) -> Self::Result { + // Receive a message from actor (pty), and to interact with the + // websocket client. + + match msg { + PtyMessage::Buffer(data) => { + ctx.binary(data); + } + PtyMessage::Exit(code) => { + // Notify the WebSocket client that the process is exited. + let exit_msg = serde_json::json!({ + "type": "exit", + "code": code + }); + ctx.text(exit_msg.to_string()); + + // Close the current actor to close the websocket channel. + ctx.close(None) + } + } + } +} + +impl Actor for WsProcessor { + type Context = ws::WebsocketContext; + + fn started(&mut self, ctx: &mut Self::Context) { + ctx.text(" _______ ______ _ _ \r\n"); + ctx.text(" |__ __| | ____| | \\ | |\r\n"); + ctx.text(" | | | |__ | \\| |\r\n"); + ctx.text(" | | | __| | . ` |\r\n"); + ctx.text(" | | | |____ | |\\ |\r\n"); + ctx.text(" |_| |______| |_| \\_|\r\n"); + ctx.text("\r\n"); + ctx.text("Website: https://github.com/TEN-framework/\r\n"); + ctx.text("Documentation: https://doc.theten.ai/\r\n"); + ctx.text("Enjoy your journey!\r\n"); + + // `rx` is a receiver which get data from pty. + let rx = self.pty.lock().unwrap().start(); + + let addr = ctx.address(); + + std::thread::spawn(move || loop { + // Continue to get data from pty. + match rx.recv() { + Ok(msg) => { + let should_exit = matches!(msg, PtyMessage::Exit(_)); + + // Re-send the data to the actor, the actual logic is done + // in `handle` function. + addr.do_send(msg); + + // If its the Exit message, exit the loop. + if should_exit { + break; + } + } + Err(e) => { + println!("recv error: {}", e); + break; + } + } + }); + } +} + +// Handle the data received from the websocket. +impl StreamHandler> for WsProcessor { + fn handle( + &mut self, + msg: Result, + ctx: &mut Self::Context, + ) { + match msg { + Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), + Ok(ws::Message::Text(text)) => { + // Try to parse the received message as a json. + if let Ok(json) = serde_json::from_str::(&text) { + if let Some(message_type) = + json.get("type").and_then(|v| v.as_str()) + { + match message_type { + "resize" => { + if let (Some(cols), Some(rows)) = ( + json.get("cols").and_then(|v| v.as_u64()), + json.get("rows").and_then(|v| v.as_u64()), + ) { + let cols = cols as u16; + let rows = rows as u16; + self.pty + .lock() + .unwrap() + .resize_pty(cols, rows) + .unwrap_or_else(|_| { + println!("Failed to resize PTY to cols: {}, rows: {}", cols, rows) + }); + } + } + _ => { + panic!("Should not happen."); + } + } + } else { + // If it's not a control message, treat it as user + // input. + self.pty + .lock() + .unwrap() + .write_to_pty(&text) + .unwrap_or_else(|_| { + println!( + "Failed to write to PTY with data: {}", + text + ) + }); + } + } else { + // If it's not JSON, treat it as user input. + self.pty + .lock() + .unwrap() + .write_to_pty(&text) + .unwrap_or_else(|_| { + println!( + "Failed to write to PTY with data: {}", + text + ) + }); + } + } + Ok(ws::Message::Binary(bin)) => ctx.binary(bin), + _ => (), + } + } +} + +pub async fn ws_terminal( + req: HttpRequest, + stream: web::Payload, +) -> Result { + // Extract 'path' from query parameters. + let query = req.query_string(); + let params: HashMap<_, _> = url::form_urlencoded::parse(query.as_bytes()) + .into_owned() + .collect(); + let path = params + .get("path") + .cloned() + .unwrap_or_else(|| "".to_string()); + + ws::start(WsProcessor::new(path), &req, stream) +} diff --git a/core/src/ten_manager/src/designer/terminal/pty_manager.rs b/core/src/ten_manager/src/designer/terminal/pty_manager.rs new file mode 100644 index 0000000000..27da6177c4 --- /dev/null +++ b/core/src/ten_manager/src/designer/terminal/pty_manager.rs @@ -0,0 +1,134 @@ +// +// 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 std::{ + io::{BufRead, BufReader, Write}, + path::{Path, PathBuf}, + thread::{self, sleep}, + time::Duration, +}; + +use portable_pty::{native_pty_system, CommandBuilder, PtyPair, PtySize}; + +use super::PtyMessage; + +pub struct PtyManager { + pty_pair: PtyPair, + writer: Box, + child: Option>, +} + +impl PtyManager { + pub fn new(path: String) -> Self { + // Validate the path. + let sanitized_path = if path.is_empty() { + std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")) + } else { + let path = Path::new(&path); + if path.is_absolute() { + path.to_path_buf() + } else { + std::env::current_dir() + .unwrap_or_else(|_| PathBuf::from("/")) + .join(path) + } + }; + + let pty_system = native_pty_system(); + + let pty_pair = pty_system + .openpty(PtySize { + rows: 24, + cols: 80, + pixel_width: 0, + pixel_height: 0, + }) + .unwrap(); + + #[cfg(target_os = "windows")] + let mut cmd_builder = CommandBuilder::new("powershell.exe"); + #[cfg(not(target_os = "windows"))] + let mut cmd_builder = CommandBuilder::new("bash"); + + // Add the $TERM env variable so we can use clear and other commands. + #[cfg(target_os = "windows")] + cmd_builder.env("TERM", "cygwin"); + #[cfg(not(target_os = "windows"))] + cmd_builder.env("TERM", "xterm-256color"); + + cmd_builder.cwd(sanitized_path.clone()); + + // Create the shell process. + let child = pty_pair.slave.spawn_command(cmd_builder).unwrap(); + + let writer = pty_pair.master.take_writer().unwrap(); + + Self { + pty_pair, + writer, + child: Some(child), + } + } + + pub fn start(&mut self) -> std::sync::mpsc::Receiver { + let (tx, rx) = std::sync::mpsc::channel::(); + + // Create a thread to wait for the process to finish. + let reader = self.pty_pair.master.try_clone_reader().unwrap(); + if let Some(mut child) = self.child.take() { + let tx_clone = tx.clone(); + std::thread::spawn(move || { + let status = child.wait().unwrap(); + println!( + "The terminal process exited: {}", + status.exit_code() as i32 + ); + + // Send Exit message after the child process exited. + let _ = + tx_clone.send(PtyMessage::Exit(status.exit_code() as i32)); + }); + } + + // Create a thread to interact with the terminal process. + thread::spawn(move || { + let mut buf = BufReader::new(reader); + + loop { + sleep(Duration::from_millis(10)); + + // Read all available text. + let data = buf.fill_buf().unwrap().to_vec(); + buf.consume(data.len()); + + // Send te data to the websocket if necessary. + if !data.is_empty() { + if let Err(e) = tx.send(PtyMessage::Buffer(data)) { + println!("Failed to send pty message: {}", e); + break; + } + } + } + }); + + rx + } + + pub fn write_to_pty(&mut self, data: &str) -> Result<(), ()> { + write!(self.writer, "{}", data).map_err(|_| ()) + } + + pub fn resize_pty(&self, cols: u16, rows: u16) -> Result<(), ()> { + self.pty_pair + .master + .resize(PtySize { + rows, + cols, + ..Default::default() + }) + .map_err(|_| ()) + } +} diff --git a/core/src/ten_manager/src/dev_server/version/mod.rs b/core/src/ten_manager/src/designer/version/mod.rs similarity index 93% rename from core/src/ten_manager/src/dev_server/version/mod.rs rename to core/src/ten_manager/src/designer/version/mod.rs index 0e0705ac18..ab26c19402 100644 --- a/core/src/ten_manager/src/dev_server/version/mod.rs +++ b/core/src/ten_manager/src/designer/version/mod.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use super::{ response::{ApiResponse, Status}, - DevServerState, + DesignerState, }; use crate::version::VERSION; @@ -21,7 +21,7 @@ struct DevServerVersion { } pub async fn get_version( - _state: web::Data>>, + _state: web::Data>>, ) -> impl Responder { let version_info = DevServerVersion { version: VERSION.to_string(), @@ -45,8 +45,8 @@ mod tests { #[actix_web::test] async fn test_get_version() { - // Initialize the DevServerState. - let state = web::Data::new(Arc::new(RwLock::new(DevServerState { + // Initialize the DesignerState. + let state = web::Data::new(Arc::new(RwLock::new(DesignerState { base_dir: None, all_pkgs: None, tman_config: TmanConfig::default(), diff --git a/core/src/ten_manager/src/lib.rs b/core/src/ten_manager/src/lib.rs index f7583eaab5..0a77869d88 100644 --- a/core/src/ten_manager/src/lib.rs +++ b/core/src/ten_manager/src/lib.rs @@ -26,7 +26,7 @@ pub mod cmd_line; pub mod config; pub mod constants; mod dep_and_candidate; -pub mod dev_server; +pub mod designer; mod error; mod fs; mod install; diff --git a/core/src/ten_manager/src/package_info/predefined_graphs/connection.rs b/core/src/ten_manager/src/package_info/predefined_graphs/connection.rs index fb3ceafe9a..e5763c5d02 100644 --- a/core/src/ten_manager/src/package_info/predefined_graphs/connection.rs +++ b/core/src/ten_manager/src/package_info/predefined_graphs/connection.rs @@ -8,44 +8,44 @@ use ten_rust::pkg_info::graph::{ GraphConnection, GraphDestination, GraphMessageFlow, }; -use crate::dev_server::graphs::connections::{ +use crate::designer::graphs::connections::{ DevServerConnection, DevServerDestination, DevServerMessageFlow, }; impl From for GraphConnection { - fn from(dev_server_connection: DevServerConnection) -> Self { + fn from(designer_connection: DevServerConnection) -> Self { GraphConnection { - app: Some(dev_server_connection.app), - extension_group: dev_server_connection.extension_group, - extension: dev_server_connection.extension, + app: Some(designer_connection.app), + extension_group: designer_connection.extension_group, + extension: designer_connection.extension, - cmd: dev_server_connection + cmd: designer_connection .cmd - .map(get_property_msg_flow_from_dev_server), - data: dev_server_connection + .map(get_property_msg_flow_from_designer), + data: designer_connection .data - .map(get_property_msg_flow_from_dev_server), - audio_frame: dev_server_connection + .map(get_property_msg_flow_from_designer), + audio_frame: designer_connection .audio_frame - .map(get_property_msg_flow_from_dev_server), - video_frame: dev_server_connection + .map(get_property_msg_flow_from_designer), + video_frame: designer_connection .video_frame - .map(get_property_msg_flow_from_dev_server), + .map(get_property_msg_flow_from_designer), } } } -fn get_property_msg_flow_from_dev_server( +fn get_property_msg_flow_from_designer( msg_flow: Vec, ) -> Vec { msg_flow.into_iter().map(|v| v.into()).collect() } impl From for GraphMessageFlow { - fn from(dev_server_msg_flow: DevServerMessageFlow) -> Self { + fn from(designer_msg_flow: DevServerMessageFlow) -> Self { GraphMessageFlow { - name: dev_server_msg_flow.name, - dest: dev_server_msg_flow + name: designer_msg_flow.name, + dest: designer_msg_flow .dest .into_iter() .map(|d| d.into()) @@ -55,12 +55,12 @@ impl From for GraphMessageFlow { } impl From for GraphDestination { - fn from(dev_server_destination: DevServerDestination) -> Self { + fn from(designer_destination: DevServerDestination) -> Self { GraphDestination { - app: Some(dev_server_destination.app), - extension_group: dev_server_destination.extension_group, - extension: dev_server_destination.extension, - msg_conversion: dev_server_destination.msg_conversion, + app: Some(designer_destination.app), + extension_group: designer_destination.extension_group, + extension: designer_destination.extension, + msg_conversion: designer_destination.msg_conversion, } } } diff --git a/core/src/ten_manager/src/package_info/predefined_graphs/node.rs b/core/src/ten_manager/src/package_info/predefined_graphs/node.rs index 4d40736b97..f0a45255df 100644 --- a/core/src/ten_manager/src/package_info/predefined_graphs/node.rs +++ b/core/src/ten_manager/src/package_info/predefined_graphs/node.rs @@ -6,17 +6,17 @@ // use ten_rust::pkg_info::{graph::GraphNode, pkg_type::PkgType}; -use crate::dev_server::graphs::nodes::DevServerExtension; +use crate::designer::graphs::nodes::DevServerExtension; impl From for GraphNode { - fn from(dev_server_extension: DevServerExtension) -> Self { + fn from(designer_extension: DevServerExtension) -> Self { GraphNode { node_type: PkgType::Extension, - name: dev_server_extension.name, - addon: dev_server_extension.addon, - extension_group: Some(dev_server_extension.extension_group.clone()), - app: Some(dev_server_extension.app), - property: dev_server_extension.property, + name: designer_extension.name, + addon: designer_extension.addon, + extension_group: Some(designer_extension.extension_group.clone()), + app: Some(designer_extension.app), + property: designer_extension.property, } } } diff --git a/core/src/ten_manager/tests/cmd_dev_server.rs b/core/src/ten_manager/tests/cmd_designer.rs similarity index 78% rename from core/src/ten_manager/tests/cmd_dev_server.rs rename to core/src/ten_manager/tests/cmd_designer.rs index 3bbe1fe399..b28501d090 100644 --- a/core/src/ten_manager/tests/cmd_dev_server.rs +++ b/core/src/ten_manager/tests/cmd_designer.rs @@ -10,31 +10,31 @@ use actix_web::{http::StatusCode, test, web, App}; use ten_manager::{ config::TmanConfig, - dev_server::{ + designer::{ graphs::{ connections::{get_graph_connections, DevServerConnection}, get_graphs, RespGraph, }, response::{ApiResponse, ErrorResponse}, - DevServerState, + DesignerState, }, }; #[actix_rt::test] -async fn test_cmd_dev_server_graphs_some_property_invalid() { - let dev_server_state = DevServerState { +async fn test_cmd_designer_graphs_some_property_invalid() { + let designer_state = DesignerState { base_dir: Some( - "tests/test_data/cmd_dev_server_graphs_some_property_invalid" + "tests/test_data/cmd_designer_graphs_some_property_invalid" .to_string(), ), all_pkgs: None, tman_config: TmanConfig::default(), }; - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( App::new() - .app_data(web::Data::new(dev_server_state)) + .app_data(web::Data::new(designer_state)) .route("/api/dev-server/v1/graphs", web::get().to(get_graphs)), ) .await; @@ -58,20 +58,20 @@ async fn test_cmd_dev_server_graphs_some_property_invalid() { } #[actix_rt::test] -async fn test_cmd_dev_server_graphs_app_property_not_exist() { - let dev_server_state = DevServerState { +async fn test_cmd_designer_graphs_app_property_not_exist() { + let designer_state = DesignerState { base_dir: Some( - "tests/test_data/cmd_dev_server_graphs_app_property_not_exist" + "tests/test_data/cmd_designer_graphs_app_property_not_exist" .to_string(), ), all_pkgs: None, tman_config: TmanConfig::default(), }; - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( App::new() - .app_data(web::Data::new(dev_server_state)) + .app_data(web::Data::new(designer_state)) .route("/api/dev-server/v1/graphs", web::get().to(get_graphs)), ) .await; @@ -94,19 +94,19 @@ async fn test_cmd_dev_server_graphs_app_property_not_exist() { } #[actix_rt::test] -async fn test_cmd_dev_server_connections_has_msg_conversion() { - let dev_server_state = DevServerState { +async fn test_cmd_designer_connections_has_msg_conversion() { + let designer_state = DesignerState { base_dir: Some( - "tests/test_data/cmd_dev_server_connections_has_msg_conversion" + "tests/test_data/cmd_designer_connections_has_msg_conversion" .to_string(), ), all_pkgs: None, tman_config: TmanConfig::default(), }; - let dev_server_state = Arc::new(RwLock::new(dev_server_state)); + let designer_state = Arc::new(RwLock::new(designer_state)); let app = test::init_service( - App::new().app_data(web::Data::new(dev_server_state)).route( + App::new().app_data(web::Data::new(designer_state)).route( "/api/dev-server/v1/graphs/{graph_name}/connections", web::get().to(get_graph_connections), ), diff --git a/core/src/ten_manager/tests/integration_test.rs b/core/src/ten_manager/tests/integration_test.rs index 4768d0d2a2..e89eae0a16 100644 --- a/core/src/ten_manager/tests/integration_test.rs +++ b/core/src/ten_manager/tests/integration_test.rs @@ -16,4 +16,4 @@ fn main() { // Those following mods will be compiled in one executable. mod cmd_check_graph; -mod cmd_dev_server; +mod cmd_designer; diff --git a/core/src/ten_manager/tests/test_data/cmd_dev_server_connections_has_msg_conversion/manifest.json b/core/src/ten_manager/tests/test_data/cmd_designer_connections_has_msg_conversion/manifest.json similarity index 100% rename from core/src/ten_manager/tests/test_data/cmd_dev_server_connections_has_msg_conversion/manifest.json rename to core/src/ten_manager/tests/test_data/cmd_designer_connections_has_msg_conversion/manifest.json diff --git a/core/src/ten_manager/tests/test_data/cmd_dev_server_connections_has_msg_conversion/property.json b/core/src/ten_manager/tests/test_data/cmd_designer_connections_has_msg_conversion/property.json similarity index 100% rename from core/src/ten_manager/tests/test_data/cmd_dev_server_connections_has_msg_conversion/property.json rename to core/src/ten_manager/tests/test_data/cmd_designer_connections_has_msg_conversion/property.json diff --git a/core/src/ten_manager/tests/test_data/cmd_dev_server_connections_has_msg_conversion/ten_packages/extension/addon_a/manifest.json b/core/src/ten_manager/tests/test_data/cmd_designer_connections_has_msg_conversion/ten_packages/extension/addon_a/manifest.json similarity index 100% rename from core/src/ten_manager/tests/test_data/cmd_dev_server_connections_has_msg_conversion/ten_packages/extension/addon_a/manifest.json rename to core/src/ten_manager/tests/test_data/cmd_designer_connections_has_msg_conversion/ten_packages/extension/addon_a/manifest.json diff --git a/core/src/ten_manager/tests/test_data/cmd_dev_server_connections_has_msg_conversion/ten_packages/extension/addon_b/manifest.json b/core/src/ten_manager/tests/test_data/cmd_designer_connections_has_msg_conversion/ten_packages/extension/addon_b/manifest.json similarity index 100% rename from core/src/ten_manager/tests/test_data/cmd_dev_server_connections_has_msg_conversion/ten_packages/extension/addon_b/manifest.json rename to core/src/ten_manager/tests/test_data/cmd_designer_connections_has_msg_conversion/ten_packages/extension/addon_b/manifest.json diff --git a/core/src/ten_manager/tests/test_data/cmd_dev_server_graphs_app_property_not_exist/manifest.json b/core/src/ten_manager/tests/test_data/cmd_designer_graphs_app_property_not_exist/manifest.json similarity index 100% rename from core/src/ten_manager/tests/test_data/cmd_dev_server_graphs_app_property_not_exist/manifest.json rename to core/src/ten_manager/tests/test_data/cmd_designer_graphs_app_property_not_exist/manifest.json diff --git a/core/src/ten_manager/tests/test_data/cmd_dev_server_graphs_app_property_not_exist/ten_packages/extension/addon_a/manifest.json b/core/src/ten_manager/tests/test_data/cmd_designer_graphs_app_property_not_exist/ten_packages/extension/addon_a/manifest.json similarity index 100% rename from core/src/ten_manager/tests/test_data/cmd_dev_server_graphs_app_property_not_exist/ten_packages/extension/addon_a/manifest.json rename to core/src/ten_manager/tests/test_data/cmd_designer_graphs_app_property_not_exist/ten_packages/extension/addon_a/manifest.json diff --git a/core/src/ten_manager/tests/test_data/cmd_dev_server_graphs_app_property_not_exist/ten_packages/extension/addon_b/manifest.json b/core/src/ten_manager/tests/test_data/cmd_designer_graphs_app_property_not_exist/ten_packages/extension/addon_b/manifest.json similarity index 100% rename from core/src/ten_manager/tests/test_data/cmd_dev_server_graphs_app_property_not_exist/ten_packages/extension/addon_b/manifest.json rename to core/src/ten_manager/tests/test_data/cmd_designer_graphs_app_property_not_exist/ten_packages/extension/addon_b/manifest.json diff --git a/core/src/ten_manager/tests/test_data/cmd_dev_server_graphs_some_property_invalid/manifest.json b/core/src/ten_manager/tests/test_data/cmd_designer_graphs_some_property_invalid/manifest.json similarity index 100% rename from core/src/ten_manager/tests/test_data/cmd_dev_server_graphs_some_property_invalid/manifest.json rename to core/src/ten_manager/tests/test_data/cmd_designer_graphs_some_property_invalid/manifest.json diff --git a/core/src/ten_manager/tests/test_data/cmd_dev_server_graphs_some_property_invalid/property.json b/core/src/ten_manager/tests/test_data/cmd_designer_graphs_some_property_invalid/property.json similarity index 100% rename from core/src/ten_manager/tests/test_data/cmd_dev_server_graphs_some_property_invalid/property.json rename to core/src/ten_manager/tests/test_data/cmd_designer_graphs_some_property_invalid/property.json diff --git a/core/src/ten_manager/tests/test_data/cmd_dev_server_graphs_some_property_invalid/ten_packages/extension/addon_a/manifest.json b/core/src/ten_manager/tests/test_data/cmd_designer_graphs_some_property_invalid/ten_packages/extension/addon_a/manifest.json similarity index 100% rename from core/src/ten_manager/tests/test_data/cmd_dev_server_graphs_some_property_invalid/ten_packages/extension/addon_a/manifest.json rename to core/src/ten_manager/tests/test_data/cmd_designer_graphs_some_property_invalid/ten_packages/extension/addon_a/manifest.json diff --git a/core/src/ten_manager/tests/test_data/cmd_dev_server_graphs_some_property_invalid/ten_packages/extension/addon_b/manifest.json b/core/src/ten_manager/tests/test_data/cmd_designer_graphs_some_property_invalid/ten_packages/extension/addon_b/manifest.json similarity index 100% rename from core/src/ten_manager/tests/test_data/cmd_dev_server_graphs_some_property_invalid/ten_packages/extension/addon_b/manifest.json rename to core/src/ten_manager/tests/test_data/cmd_designer_graphs_some_property_invalid/ten_packages/extension/addon_b/manifest.json diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 1dd567557a..ae40a5aa05 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -61,7 +61,7 @@ ### TEN Manager * [Overview](ten_framework/ten_manager/overview.md) -* [Dev-Server](ten_framework/ten_manager/dev_server_cn.md) +* [Designer](ten_framework/ten_manager/designer_cn.md) * [Check-Graph](ten_framework/ten_manager/check_graph.md) ## Tutorials diff --git a/docs/ten_framework/ten_manager/dev_server_cn.md b/docs/ten_framework/ten_manager/designer_cn.md similarity index 99% rename from docs/ten_framework/ten_manager/dev_server_cn.md rename to docs/ten_framework/ten_manager/designer_cn.md index 24d4375f06..c16e07bd4a 100644 --- a/docs/ten_framework/ten_manager/dev_server_cn.md +++ b/docs/ten_framework/ten_manager/designer_cn.md @@ -1,11 +1,11 @@ -# TEN Manager - Dev Server +# TEN Manager - Designer To start the `tman` development server, use the following command: {% code title=">_ Terminal" %} ```shell -tman dev-server +tman designer ``` {% endcode %}