From 83a39b5bc9931785e1cb676a19e3e7662fb131a4 Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Tue, 7 Jan 2025 11:45:27 +0100 Subject: [PATCH 01/31] Testing --- backend/tree_api/json_translator.py | 38 ++++ backend/tree_api/tree_generator.py | 89 ++++++++ backend/tree_api/views.py | 212 ++++++++++++------ frontend/src/api_helper/TreeWrapper.ts | 18 +- .../src/components/header_menu/HeaderMenu.tsx | 29 ++- 5 files changed, 297 insertions(+), 89 deletions(-) diff --git a/backend/tree_api/json_translator.py b/backend/tree_api/json_translator.py index fa4b7db2c..bfd90a0e8 100644 --- a/backend/tree_api/json_translator.py +++ b/backend/tree_api/json_translator.py @@ -157,6 +157,44 @@ def get_start_node_id(node_models, link_models): return start_node_id +def translate_raw(content, raw_order): + + # Parse the JSON data + try: + parsed_json = json.loads(content) + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON content: {e}") + + try: + # Extract nodes and links information + node_models = parsed_json["layers"][1]["models"] + link_models = parsed_json["layers"][0]["models"] + + # Get the tree structure + tree_structure = get_tree_structure(link_models, node_models) + + # Get the order of bt: True = Ascendent; False = Descendent + order = raw_order == "bottom-to-top" + + # Generate XML + root = Element("Root", name="Tree Root") + behavior_tree = SubElement(root, "BehaviorTree") + start_node_id = get_start_node_id(node_models, link_models) + build_xml( + node_models, + link_models, + tree_structure, + start_node_id, + behavior_tree, + order, + ) + except Exception as e: + raise RuntimeError(f"Failed to translate tree: {e}") + + # Save the xml in the specified route + xml_string = prettify_xml(root) + return xml_string + def translate(content, tree_path, raw_order): # Parse the JSON data diff --git a/backend/tree_api/tree_generator.py b/backend/tree_api/tree_generator.py index 6b1120abb..f45e7bd77 100644 --- a/backend/tree_api/tree_generator.py +++ b/backend/tree_api/tree_generator.py @@ -99,6 +99,26 @@ def add_actions_code(tree, actions, action_path): action_section = ET.SubElement(code_section, action_name) action_section.text = "\n" + action_code + "\n" +# Add the code of the different actions +def add_actions_code_raw(tree, actions, all_actions): + + code_section = ET.SubElement(tree, "Code") + + # Add each action code to the tree + for action_name in actions: + action_code = None + for action in all_actions: + if action_name == action["name"]: + action_code = action["content"] + + # Verify if action code exists + if action_code == None: + continue + + # Add a new subelement to the code_section + action_section = ET.SubElement(code_section, action_name) + action_section.text = "\n" + action_code + "\n" + # Replaces all the subtrees in a given tree depth def replace_subtrees_in_tree(tree, subtrees, tree_path): @@ -130,6 +150,35 @@ def replace_subtrees_in_tree(tree, subtrees, tree_path): # Remove the original subtree tag parent.remove(subtree_tag) +# Replaces all the subtrees in a given tree depth +def replace_subtrees_in_tree_raw(tree, subtrees): + + for subtree in subtrees: + subtree_name = subtree["name"] + subtree_xml = subtree["content"] + subtree_tree = ET.fromstring(subtree_xml) + + # Find the content inside the tag in the subtree + subtree_behavior_tree = subtree_tree.find(".//BehaviorTree") + if subtree_behavior_tree is not None: + # Locate tags in the main tree that refer to this subtree + subtree_parents = tree.findall(".//" + subtree_name + "/..") + for parent in subtree_parents: + subtree_tags = parent.findall(subtree_name) + for subtree_tag in subtree_tags: + # Get the index of the original subtree tag + index = list(parent).index(subtree_tag) + + # Insert new elements from the subtree at the correct position + for i, subtree_elem in enumerate(subtree_behavior_tree): + try: + parent.insert(index + i, subtree_elem) + except Exception as e: + print(str(e)) + # Remove the original subtree tag + parent.remove(subtree_tag) + + # Recursively replace all subtrees in a given tree def replace_all_subtrees(tree, tree_path, depth=0, max_depth=15): @@ -152,6 +201,27 @@ def replace_all_subtrees(tree, tree_path, depth=0, max_depth=15): # Recursively call the function to replace subtrees in the newly added subtrees replace_all_subtrees(tree, tree_path, depth + 1, max_depth) +# Recursively replace all subtrees in a given tree +def replace_all_subtrees_raw(tree, all_subtrees, depth=0, max_depth=15): + + # Avoid infinite recursion + if depth > max_depth: + return + + # Get the subtrees that are present in the tree + possible_trees = [x["name"] for x in all_subtrees] + subtrees = get_subtree_set(tree, possible_trees) + + # If no subtrees are found, stop the recursion + if not subtrees: + return + + # Replace subtrees in the main tree + replace_subtrees_in_tree_raw(tree, all_subtrees) + + # Recursively call the function to replace subtrees in the newly added subtrees + replace_all_subtrees_raw(tree, all_subtrees, depth + 1, max_depth) + # Read the tree and the actions and generate a formatted tree string def parse_tree(tree_path, action_path): @@ -180,6 +250,25 @@ def parse_tree(tree_path, action_path): return formatted_tree +def parse_tree_raw(tree_xml, subtrees, all_actions): + # Parse the tree file + tree = get_bt_structure(tree_xml) + + # Obtain the defined subtrees recursively + replace_all_subtrees_raw(tree, subtrees) + + # Obtain the defined actions + possible_actions = [x["name"] for x in all_actions] + actions = get_action_set(tree, possible_actions) + + # Add subsections for the action code + add_actions_code_raw(tree, actions, all_actions) + + # Serialize the modified XML to a properly formatted string + formatted_tree = prettify_xml(tree) + formatted_tree = fix_indentation(formatted_tree, actions) + + return formatted_tree ############################################################################## # Main section diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index 1c4afcd32..7cbba856b 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -995,53 +995,146 @@ def generate_app(request): ) -@api_view(["POST"]) -def generate_dockerized_app(request): +# @api_view(["POST"]) +# def generate_dockerized_app(request): + +# if ( +# "app_name" not in request.data +# or "tree_graph" not in request.data +# or "bt_order" not in request.data +# ): +# return Response( +# {"error": "Incorrect request parameters"}, +# status=status.HTTP_400_BAD_REQUEST, +# ) + +# # Get the request parameters +# app_name = request.data.get("app_name") +# main_tree_graph = request.data.get("tree_graph") +# bt_order = request.data.get("bt_order") +# print("Dockerized bt order: ", bt_order) + +# # Make folder path relative to Django app +# base_path = os.path.join(settings.BASE_DIR, "filesystem") +# project_path = os.path.join(base_path, app_name) +# action_path = os.path.join(project_path, "code/actions") + +# working_folder = "/tmp/wf" +# subtree_path = os.path.join(project_path, "code/trees/subtrees/json") +# result_trees_tmp_path = os.path.join("/tmp/trees/") +# self_contained_tree_path = os.path.join(working_folder, "self_contained_tree.xml") +# tree_gardener_src = os.path.join(settings.BASE_DIR, "tree_gardener") +# template_path = os.path.join(settings.BASE_DIR, "ros_template") + +# try: +# # Init the trees temp folder +# if os.path.exists(result_trees_tmp_path): +# shutil.rmtree(result_trees_tmp_path) +# os.makedirs(result_trees_tmp_path) + +# # 1. Create the working folder +# if os.path.exists(working_folder): +# shutil.rmtree(working_folder) +# os.mkdir(working_folder) + +# # 2. Generate a basic tree from the JSON definition +# main_tree_tmp_path = os.path.join(result_trees_tmp_path, "main.xml") +# json_translator.translate(main_tree_graph, main_tree_tmp_path, bt_order) + +# # 3. Copy all the subtrees to the temp folder +# try: +# for subtree_file in os.listdir(subtree_path): +# if subtree_file.endswith(".json"): +# subtree_name = base = os.path.splitext( +# os.path.basename(subtree_file) +# )[0] +# print(os.path.join(subtree_path, subtree_file)) + +# xml_path = os.path.join( +# project_path, "code", "trees", "subtrees", f"{subtree_name}.xml" +# ) + +# with open(os.path.join(subtree_path, subtree_file), "r+") as f: +# # Reading from a file +# subtree_json = f.read() + +# json_translator.translate(subtree_json, xml_path, bt_order) + +# shutil.copy(xml_path, result_trees_tmp_path) +# except: +# print("No subtrees") + +# # 4. Generate a self-contained tree +# tree_generator.generate( +# result_trees_tmp_path, action_path, self_contained_tree_path +# ) + +# # 5. Copy necessary files to execute the app in the RB +# factory_location = tree_gardener_src + "/tree_gardener/tree_factory.py" +# tools_location = tree_gardener_src + "/tree_gardener/tree_tools.py" +# entrypoint_location = template_path + "/ros_template/execute_docker.py" +# shutil.copy(factory_location, working_folder) +# shutil.copy(tools_location, working_folder) +# shutil.copy(entrypoint_location, working_folder) + +# # 6. Generate the zip +# zip_path = working_folder + ".zip" +# with zipfile.ZipFile(zip_path, "w") as zipf: +# for root, dirs, files in os.walk(working_folder): +# for file in files: +# zipf.write( +# os.path.join(root, file), +# os.path.relpath(os.path.join(root, file), working_folder), +# ) + +# # 6. Return the zip file as a response +# with open(zip_path, "rb") as zip_file: +# response = HttpResponse(zip_file, content_type="application/zip") +# response["Content-Disposition"] = ( +# f"attachment; filename={os.path.basename(zip_path)}" +# ) +# return response + +# except Exception as e: +# print(e) +# import traceback + +# traceback.print_exc() +# return Response( +# {"success": False, "message": str(e)}, +# status=status.HTTP_500_INTERNAL_SERVER_ERROR, +# ) - if ( - "app_name" not in request.data - or "tree_graph" not in request.data - or "bt_order" not in request.data - ): - return Response( - {"error": "Incorrect request parameters"}, - status=status.HTTP_400_BAD_REQUEST, - ) + +@api_view(["GET"]) +def generate_dockerized_app(request): # Get the request parameters - app_name = request.data.get("app_name") - main_tree_graph = request.data.get("tree_graph") - bt_order = request.data.get("bt_order") - print("Dockerized bt order: ", bt_order) + app_name = request.GET.get("app_name", None) + main_tree_graph = request.GET.get("tree_graph", None) + bt_order = request.GET.get("bt_order", None) # Make folder path relative to Django app base_path = os.path.join(settings.BASE_DIR, "filesystem") project_path = os.path.join(base_path, app_name) action_path = os.path.join(project_path, "code/actions") - working_folder = "/tmp/wf" subtree_path = os.path.join(project_path, "code/trees/subtrees/json") - result_trees_tmp_path = os.path.join("/tmp/trees/") - self_contained_tree_path = os.path.join(working_folder, "self_contained_tree.xml") tree_gardener_src = os.path.join(settings.BASE_DIR, "tree_gardener") template_path = os.path.join(settings.BASE_DIR, "ros_template") - try: - # Init the trees temp folder - if os.path.exists(result_trees_tmp_path): - shutil.rmtree(result_trees_tmp_path) - os.makedirs(result_trees_tmp_path) + factory_location = tree_gardener_src + "/tree_gardener/tree_factory.py" + tools_location = tree_gardener_src + "/tree_gardener/tree_tools.py" + entrypoint_location = template_path + "/ros_template/execute_docker.py" - # 1. Create the working folder - if os.path.exists(working_folder): - shutil.rmtree(working_folder) - os.mkdir(working_folder) + subtrees = [] + actions = [] - # 2. Generate a basic tree from the JSON definition - main_tree_tmp_path = os.path.join(result_trees_tmp_path, "main.xml") - json_translator.translate(main_tree_graph, main_tree_tmp_path, bt_order) + try: + # 1. Generate a basic tree from the JSON definition + main_tree = json_translator.translate_raw(main_tree_graph, bt_order) - # 3. Copy all the subtrees to the temp folder + # 2. Get all possible subtrees name and content try: for subtree_file in os.listdir(subtree_path): if subtree_file.endswith(".json"): @@ -1050,50 +1143,39 @@ def generate_dockerized_app(request): )[0] print(os.path.join(subtree_path, subtree_file)) - xml_path = os.path.join( - project_path, "code", "trees", "subtrees", f"{subtree_name}.xml" - ) - with open(os.path.join(subtree_path, subtree_file), "r+") as f: # Reading from a file subtree_json = f.read() - json_translator.translate(subtree_json, xml_path, bt_order) - - shutil.copy(xml_path, result_trees_tmp_path) + subtree = json_translator.translate_raw(subtree_json, bt_order) + subtrees.append({"name": subtree_name, "content": subtree}) except: print("No subtrees") - # 4. Generate a self-contained tree - tree_generator.generate( - result_trees_tmp_path, action_path, self_contained_tree_path - ) + # 3. Get all possible actions name and content + for action_file in os.listdir(action_path): + if action_file.endswith(".py"): + action_name = base = os.path.splitext(os.path.basename(action_file))[0] + print(os.path.join(action_path, action_file)) - # 5. Copy necessary files to execute the app in the RB - factory_location = tree_gardener_src + "/tree_gardener/tree_factory.py" - tools_location = tree_gardener_src + "/tree_gardener/tree_tools.py" - entrypoint_location = template_path + "/ros_template/execute_docker.py" - shutil.copy(factory_location, working_folder) - shutil.copy(tools_location, working_folder) - shutil.copy(entrypoint_location, working_folder) + with open(os.path.join(action_path, action_file), "r+") as f: + action_content = f.read() - # 6. Generate the zip - zip_path = working_folder + ".zip" - with zipfile.ZipFile(zip_path, "w") as zipf: - for root, dirs, files in os.walk(working_folder): - for file in files: - zipf.write( - os.path.join(root, file), - os.path.relpath(os.path.join(root, file), working_folder), - ) + actions.append({"name": action_name, "content": action_content}) - # 6. Return the zip file as a response - with open(zip_path, "rb") as zip_file: - response = HttpResponse(zip_file, content_type="application/zip") - response["Content-Disposition"] = ( - f"attachment; filename={os.path.basename(zip_path)}" - ) - return response + # 4. Generate a self-contained tree + final_tree = tree_generator.parse_tree_raw(main_tree, subtrees, actions) + + # 5. Get necessary files to execute the app in the RB + with open(factory_location, "r+") as f: + factory_content = f.read() + with open(tools_location, "r+") as f: + tools_content = f.read() + with open(entrypoint_location, "r+") as f: + entrypoint_content = f.read() + + # 6. Return the files as a response + return JsonResponse({"success": True, "tree": final_tree, "factory":factory_content, "tools": tools_content, "entrypoint":entrypoint_content}) except Exception as e: print(e) diff --git a/frontend/src/api_helper/TreeWrapper.ts b/frontend/src/api_helper/TreeWrapper.ts index d33501fac..e33837c4c 100644 --- a/frontend/src/api_helper/TreeWrapper.ts +++ b/frontend/src/api_helper/TreeWrapper.ts @@ -325,23 +325,9 @@ const generateDockerizedApp = async ( if (!currentProjectname) throw new Error("Current Project name is not set"); if (!btOrder) throw new Error("Behavior Tree order is not set"); - const apiUrl = "/bt_studio/generate_dockerized_app/"; + const apiUrl = `/bt_studio/generate_dockerized_app?app_name=${currentProjectname}&tree_graph=${JSON.stringify(modelJson)}&bt_order=${btOrder}`; try { - const response = await axios.post( - apiUrl, - { - app_name: currentProjectname, - tree_graph: JSON.stringify(modelJson), - bt_order: btOrder, - }, - { - responseType: "blob", // Ensure the response is treated as a Blob - headers: { - //@ts-ignore Needed for compatibility with Unibotics - "X-CSRFToken": context.csrf, - }, - } - ); + const response = await axios.get(apiUrl); // Handle unsuccessful response status (e.g., non-2xx status) if (!isSuccessful(response)) { diff --git a/frontend/src/components/header_menu/HeaderMenu.tsx b/frontend/src/components/header_menu/HeaderMenu.tsx index a596752eb..1bc21f36e 100644 --- a/frontend/src/components/header_menu/HeaderMenu.tsx +++ b/frontend/src/components/header_menu/HeaderMenu.tsx @@ -1,4 +1,5 @@ import { MouseEventHandler, useContext, useEffect, useState } from "react"; +import JSZip from "jszip"; import AppBar from "@mui/material/AppBar"; import Toolbar from "@mui/material/Toolbar"; import { @@ -231,26 +232,38 @@ const HeaderMenu = ({ if (!appRunning) { try { // Get the blob from the API wrapper - const appBlob = await generateDockerizedApp( + const appFiles = await generateDockerizedApp( modelJson, currentProjectname, settings.btOrder.value, ); - // Convert the blob to base64 using FileReader - const reader = new FileReader(); - reader.onloadend = async () => { - const base64data = reader.result; // Get the zip in base64 + // Create the zip with the files + const zip = new JSZip(); + + zip.file("self_contained_tree_path.xml", appFiles.tree); + zip.file("tree_factory.py", appFiles.factory); + zip.file("tree_tools.py", appFiles.tools); + zip.file("execute_docker.py", appFiles.entrypoint); + + zip.generateAsync({type:"base64"}).then(async function(content) { // Send the base64 encoded blob await manager.run({ type: "bt-studio", - code: base64data, + code: content, }); console.log("Dockerized app started successfully"); - }; - reader.readAsDataURL(appBlob); + }); + + // // Convert the blob to base64 using FileReader + // const reader = new FileReader(); + // reader.onloadend = async () => { + // const base64data = reader.result; // Get the zip in base64 + + // }; + // reader.readAsDataURL(appBlob); setAppRunning(true); console.log("App started successfully"); From a62714bca178907579db98512a9c1f2cc2cafb10 Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Tue, 7 Jan 2025 11:52:21 +0100 Subject: [PATCH 02/31] Working docker zip --- .../src/components/header_menu/HeaderMenu.tsx | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/header_menu/HeaderMenu.tsx b/frontend/src/components/header_menu/HeaderMenu.tsx index 1bc21f36e..47baef498 100644 --- a/frontend/src/components/header_menu/HeaderMenu.tsx +++ b/frontend/src/components/header_menu/HeaderMenu.tsx @@ -242,28 +242,27 @@ const HeaderMenu = ({ const zip = new JSZip(); - zip.file("self_contained_tree_path.xml", appFiles.tree); + zip.file("self_contained_tree.xml", appFiles.tree); zip.file("tree_factory.py", appFiles.factory); zip.file("tree_tools.py", appFiles.tools); zip.file("execute_docker.py", appFiles.entrypoint); - zip.generateAsync({type:"base64"}).then(async function(content) { + // Convert the blob to base64 using FileReader + const reader = new FileReader(); + reader.onloadend = async () => { + const base64data = reader.result; // Get the zip in base64 // Send the base64 encoded blob await manager.run({ type: "bt-studio", - code: content, + code: base64data, }); - console.log("Dockerized app started successfully"); - }); + }; - // // Convert the blob to base64 using FileReader - // const reader = new FileReader(); - // reader.onloadend = async () => { - // const base64data = reader.result; // Get the zip in base64 + zip.generateAsync({type:"blob"}).then(function(content) { + reader.readAsDataURL(content); + }); - // }; - // reader.readAsDataURL(appBlob); setAppRunning(true); console.log("App started successfully"); From ab0348262d5779bcac4cf22fcc3c6d2156ef05aa Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Tue, 7 Jan 2025 12:02:01 +0100 Subject: [PATCH 03/31] Cleanup --- backend/tree_api/views.py | 118 +----------------- .../src/components/header_menu/HeaderMenu.tsx | 3 +- 2 files changed, 3 insertions(+), 118 deletions(-) diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index 7cbba856b..35485cbd1 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -994,118 +994,6 @@ def generate_app(request): status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) - -# @api_view(["POST"]) -# def generate_dockerized_app(request): - -# if ( -# "app_name" not in request.data -# or "tree_graph" not in request.data -# or "bt_order" not in request.data -# ): -# return Response( -# {"error": "Incorrect request parameters"}, -# status=status.HTTP_400_BAD_REQUEST, -# ) - -# # Get the request parameters -# app_name = request.data.get("app_name") -# main_tree_graph = request.data.get("tree_graph") -# bt_order = request.data.get("bt_order") -# print("Dockerized bt order: ", bt_order) - -# # Make folder path relative to Django app -# base_path = os.path.join(settings.BASE_DIR, "filesystem") -# project_path = os.path.join(base_path, app_name) -# action_path = os.path.join(project_path, "code/actions") - -# working_folder = "/tmp/wf" -# subtree_path = os.path.join(project_path, "code/trees/subtrees/json") -# result_trees_tmp_path = os.path.join("/tmp/trees/") -# self_contained_tree_path = os.path.join(working_folder, "self_contained_tree.xml") -# tree_gardener_src = os.path.join(settings.BASE_DIR, "tree_gardener") -# template_path = os.path.join(settings.BASE_DIR, "ros_template") - -# try: -# # Init the trees temp folder -# if os.path.exists(result_trees_tmp_path): -# shutil.rmtree(result_trees_tmp_path) -# os.makedirs(result_trees_tmp_path) - -# # 1. Create the working folder -# if os.path.exists(working_folder): -# shutil.rmtree(working_folder) -# os.mkdir(working_folder) - -# # 2. Generate a basic tree from the JSON definition -# main_tree_tmp_path = os.path.join(result_trees_tmp_path, "main.xml") -# json_translator.translate(main_tree_graph, main_tree_tmp_path, bt_order) - -# # 3. Copy all the subtrees to the temp folder -# try: -# for subtree_file in os.listdir(subtree_path): -# if subtree_file.endswith(".json"): -# subtree_name = base = os.path.splitext( -# os.path.basename(subtree_file) -# )[0] -# print(os.path.join(subtree_path, subtree_file)) - -# xml_path = os.path.join( -# project_path, "code", "trees", "subtrees", f"{subtree_name}.xml" -# ) - -# with open(os.path.join(subtree_path, subtree_file), "r+") as f: -# # Reading from a file -# subtree_json = f.read() - -# json_translator.translate(subtree_json, xml_path, bt_order) - -# shutil.copy(xml_path, result_trees_tmp_path) -# except: -# print("No subtrees") - -# # 4. Generate a self-contained tree -# tree_generator.generate( -# result_trees_tmp_path, action_path, self_contained_tree_path -# ) - -# # 5. Copy necessary files to execute the app in the RB -# factory_location = tree_gardener_src + "/tree_gardener/tree_factory.py" -# tools_location = tree_gardener_src + "/tree_gardener/tree_tools.py" -# entrypoint_location = template_path + "/ros_template/execute_docker.py" -# shutil.copy(factory_location, working_folder) -# shutil.copy(tools_location, working_folder) -# shutil.copy(entrypoint_location, working_folder) - -# # 6. Generate the zip -# zip_path = working_folder + ".zip" -# with zipfile.ZipFile(zip_path, "w") as zipf: -# for root, dirs, files in os.walk(working_folder): -# for file in files: -# zipf.write( -# os.path.join(root, file), -# os.path.relpath(os.path.join(root, file), working_folder), -# ) - -# # 6. Return the zip file as a response -# with open(zip_path, "rb") as zip_file: -# response = HttpResponse(zip_file, content_type="application/zip") -# response["Content-Disposition"] = ( -# f"attachment; filename={os.path.basename(zip_path)}" -# ) -# return response - -# except Exception as e: -# print(e) -# import traceback - -# traceback.print_exc() -# return Response( -# {"success": False, "message": str(e)}, -# status=status.HTTP_500_INTERNAL_SERVER_ERROR, -# ) - - @api_view(["GET"]) def generate_dockerized_app(request): @@ -1156,7 +1044,6 @@ def generate_dockerized_app(request): for action_file in os.listdir(action_path): if action_file.endswith(".py"): action_name = base = os.path.splitext(os.path.basename(action_file))[0] - print(os.path.join(action_path, action_file)) with open(os.path.join(action_path, action_file), "r+") as f: action_content = f.read() @@ -1324,7 +1211,7 @@ def upload_universe(request): @api_view(["POST"]) def add_docker_universe(request): - # Check if 'name' and 'zipfile' are in the request data + # Check if 'universe_name', 'app_name' and 'id' are in the request data if ( "universe_name" not in request.data or "app_name" not in request.data @@ -1335,7 +1222,7 @@ def add_docker_universe(request): status=status.HTTP_400_BAD_REQUEST, ) - # Get the name and the zip file from the request + # Get the name and the id file from the request universe_name = request.data["universe_name"] app_name = request.data["app_name"] id = request.data["id"] @@ -1351,7 +1238,6 @@ def add_docker_universe(request): os.makedirs(universe_path) # Fill the config dictionary of the universe - ram_launch_path = "/workspace/worlds/" + universe_name + "/universe.launch.py" universe_config = {"name": universe_name, "id": id, "type": "robotics_backend"} # Generate the json config diff --git a/frontend/src/components/header_menu/HeaderMenu.tsx b/frontend/src/components/header_menu/HeaderMenu.tsx index 47baef498..7bd592ba6 100644 --- a/frontend/src/components/header_menu/HeaderMenu.tsx +++ b/frontend/src/components/header_menu/HeaderMenu.tsx @@ -198,7 +198,7 @@ const HeaderMenu = ({ const appBlob = await generateApp( modelJson, currentProjectname, - "top-to-bottom", + settings.btOrder.value, ); // Create a download link and trigger download @@ -263,7 +263,6 @@ const HeaderMenu = ({ reader.readAsDataURL(content); }); - setAppRunning(true); console.log("App started successfully"); } catch (error: unknown) { From 55deae95f7a77ee0437f45db93d26634c517174a Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Tue, 7 Jan 2025 12:19:26 +0100 Subject: [PATCH 04/31] Remove hardcoded files --- backend/tree_api/app_generator.py | 7 +- backend/tree_api/tree_generator.py | 132 +++---------------------- backend/tree_api/urls.py | 4 +- backend/tree_api/views.py | 64 ++++++------ frontend/src/api_helper/TreeWrapper.ts | 2 +- 5 files changed, 49 insertions(+), 160 deletions(-) diff --git a/backend/tree_api/app_generator.py b/backend/tree_api/app_generator.py index 28dd1c916..a84c264e6 100644 --- a/backend/tree_api/app_generator.py +++ b/backend/tree_api/app_generator.py @@ -170,10 +170,6 @@ def generate(app_tree, app_name, template_path, action_path, tree_gardener_src): executor_path = app_path + "/" + app_name tree_gardener_dst = app_path + "/tree_gardener" - # Ensure the files exist - if not os.path.exists(app_tree): - raise FileNotFoundError(f"Tree path '{app_tree}' does not exist!") - # 1. Copy the template to a temporary directory if os.path.exists(executor_path): shutil.rmtree(executor_path) # Delete if it already exists @@ -182,7 +178,8 @@ def generate(app_tree, app_name, template_path, action_path, tree_gardener_src): # 2. Copy the tree to the template directory tree_location = executor_path + "/resource/app_tree.xml" - shutil.copy(app_tree, tree_location) + with open(tree_location, "w") as file: + file.write(app_tree) # 3. Edit some files in the template user_data = {"app_name": app_name} diff --git a/backend/tree_api/tree_generator.py b/backend/tree_api/tree_generator.py index f45e7bd77..88e1e940c 100644 --- a/backend/tree_api/tree_generator.py +++ b/backend/tree_api/tree_generator.py @@ -81,26 +81,8 @@ def get_subtree_set(tree, possible_subtrees) -> set: return subtrees - -# Add the code of the different actions -def add_actions_code(tree, actions, action_path): - - code_section = ET.SubElement(tree, "Code") - - # Add each actiion code to the tree - for action_name in actions: - - # Get the action code - action_route = action_path + "/" + action_name + ".py" - action_file = open(action_route, "r") - action_code = action_file.read() - - # Add a new subelement to the code_section - action_section = ET.SubElement(code_section, action_name) - action_section.text = "\n" + action_code + "\n" - # Add the code of the different actions -def add_actions_code_raw(tree, actions, all_actions): +def add_actions_code_(tree, actions, all_actions): code_section = ET.SubElement(tree, "Code") @@ -119,39 +101,8 @@ def add_actions_code_raw(tree, actions, all_actions): action_section = ET.SubElement(code_section, action_name) action_section.text = "\n" + action_code + "\n" - -# Replaces all the subtrees in a given tree depth -def replace_subtrees_in_tree(tree, subtrees, tree_path): - - for subtree_name in subtrees: - subtree_path = os.path.join(tree_path, f"{subtree_name}.xml") - if os.path.exists(subtree_path): - with open(subtree_path, "r") as sf: - subtree_xml = sf.read() - subtree_tree = ET.fromstring(subtree_xml) - - # Find the content inside the tag in the subtree - subtree_behavior_tree = subtree_tree.find(".//BehaviorTree") - if subtree_behavior_tree is not None: - # Locate tags in the main tree that refer to this subtree - subtree_parents = tree.findall(".//" + subtree_name + "/..") - for parent in subtree_parents: - subtree_tags = parent.findall(subtree_name) - for subtree_tag in subtree_tags: - # Get the index of the original subtree tag - index = list(parent).index(subtree_tag) - - # Insert new elements from the subtree at the correct position - for i, subtree_elem in enumerate(subtree_behavior_tree): - try: - parent.insert(index + i, subtree_elem) - except Exception as e: - print(str(e)) - # Remove the original subtree tag - parent.remove(subtree_tag) - # Replaces all the subtrees in a given tree depth -def replace_subtrees_in_tree_raw(tree, subtrees): +def replace_subtrees_in_tree_(tree, subtrees): for subtree in subtrees: subtree_name = subtree["name"] @@ -178,31 +129,8 @@ def replace_subtrees_in_tree_raw(tree, subtrees): # Remove the original subtree tag parent.remove(subtree_tag) - - # Recursively replace all subtrees in a given tree -def replace_all_subtrees(tree, tree_path, depth=0, max_depth=15): - - # Avoid infinite recursion - if depth > max_depth: - return - - # Get the subtrees that are present in the tree - possible_trees = [file.split(".")[0] for file in os.listdir(tree_path)] - subtrees = get_subtree_set(tree, possible_trees) - - # If no subtrees are found, stop the recursion - if not subtrees: - return - - # Replace subtrees in the main tree - replace_subtrees_in_tree(tree, subtrees, tree_path) - - # Recursively call the function to replace subtrees in the newly added subtrees - replace_all_subtrees(tree, tree_path, depth + 1, max_depth) - -# Recursively replace all subtrees in a given tree -def replace_all_subtrees_raw(tree, all_subtrees, depth=0, max_depth=15): +def replace_all_subtrees_(tree, all_subtrees, depth=0, max_depth=15): # Avoid infinite recursion if depth > max_depth: @@ -217,52 +145,24 @@ def replace_all_subtrees_raw(tree, all_subtrees, depth=0, max_depth=15): return # Replace subtrees in the main tree - replace_subtrees_in_tree_raw(tree, all_subtrees) + replace_subtrees_in_tree_(tree, all_subtrees) # Recursively call the function to replace subtrees in the newly added subtrees - replace_all_subtrees_raw(tree, all_subtrees, depth + 1, max_depth) - - -# Read the tree and the actions and generate a formatted tree string -def parse_tree(tree_path, action_path): - - # Get the tree main XML file and read its content - main_tree_path = os.path.join(tree_path, "main.xml") - f = open(main_tree_path, "r") - tree_xml = f.read() + replace_all_subtrees_(tree, all_subtrees, depth + 1, max_depth) +def parse_tree_(tree_xml, subtrees, all_actions): # Parse the tree file tree = get_bt_structure(tree_xml) # Obtain the defined subtrees recursively - replace_all_subtrees(tree, tree_path) - - # Obtain the defined actions - possible_actions = [file.split(".")[0] for file in os.listdir(action_path)] - actions = get_action_set(tree, possible_actions) - - # Add subsections for the action code - add_actions_code(tree, actions, action_path) - - # Serialize the modified XML to a properly formatted string - formatted_tree = prettify_xml(tree) - formatted_tree = fix_indentation(formatted_tree, actions) - - return formatted_tree - -def parse_tree_raw(tree_xml, subtrees, all_actions): - # Parse the tree file - tree = get_bt_structure(tree_xml) - - # Obtain the defined subtrees recursively - replace_all_subtrees_raw(tree, subtrees) + replace_all_subtrees_(tree, subtrees) # Obtain the defined actions possible_actions = [x["name"] for x in all_actions] actions = get_action_set(tree, possible_actions) # Add subsections for the action code - add_actions_code_raw(tree, actions, all_actions) + add_actions_code_(tree, actions, all_actions) # Serialize the modified XML to a properly formatted string formatted_tree = prettify_xml(tree) @@ -275,20 +175,10 @@ def parse_tree_raw(tree_xml, subtrees, all_actions): ############################################################################## -def generate(tree_path, action_path, result_path): - - # Ensure the provided tree and action paths exist - if not os.path.exists(tree_path): - raise FileNotFoundError(f"Tree path '{tree_path}' does not exist!") - if not os.path.exists(action_path): - raise FileNotFoundError(f"Action path '{action_path}' does not exist!") +def generate(tree_xml, subtrees, actions): # Get a formatted self-contained tree string - formatted_xml = parse_tree(tree_path, action_path) - - # Store the string in a temp xml file - with open(result_path, "w") as result_file: - result_file.write(formatted_xml) + formatted_xml = parse_tree_(tree_xml, subtrees, actions) - # Return the xml string for debugging purposes + # Return the xml string return formatted_xml diff --git a/backend/tree_api/urls.py b/backend/tree_api/urls.py index 38e2f9b92..bc959f77f 100644 --- a/backend/tree_api/urls.py +++ b/backend/tree_api/urls.py @@ -11,7 +11,7 @@ path("delete_universe/", views.delete_universe, name="delete_universe"), path("add_docker_universe/", views.add_docker_universe, name="add_docker_universe"), path("get_universes_list/", views.get_universes_list, name="get_universes_list"), - path("get_universe_zip/", views.get_universe_zip, name="generate_app"), + path("get_universe_zip/", views.get_universe_zip, name="get_universe_zip"), path( "get_universe_configuration/", views.get_universe_configuration, @@ -59,7 +59,7 @@ path("create_action/", views.create_action, name="create_action"), # Other path("translate_json/", views.translate_json, name="translate_json"), - path("generate_app/", views.generate_app, name="generate_app"), + path("generate_local_app/", views.generate_local_app, name="generate_local_app"), path( "generate_dockerized_app/", views.generate_dockerized_app, diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index 35485cbd1..a1fc596cf 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -890,9 +890,8 @@ def download_data(request): else: return Response({"error": "app_name parameter is missing"}, status=500) - @api_view(["POST"]) -def generate_app(request): +def generate_local_app(request): if ( "app_name" not in request.data @@ -908,29 +907,24 @@ def generate_app(request): app_name = request.data.get("app_name") main_tree_graph = request.data.get("tree_graph") bt_order = request.data.get("bt_order") - print("bt order: ", bt_order) - # Make folder path relative to Django app + # Make folder path relative to Django app base_path = os.path.join(settings.BASE_DIR, "filesystem") project_path = os.path.join(base_path, app_name) action_path = os.path.join(project_path, "code/actions") - subtree_path = os.path.join(project_path, "code/trees/subtrees") - result_trees_tmp_path = os.path.join("/tmp/trees/") - self_contained_tree_path = os.path.join("/tmp/self_contained_tree.xml") - template_path = os.path.join(settings.BASE_DIR, "ros_template") + + subtree_path = os.path.join(project_path, "code/trees/subtrees/json") tree_gardener_src = os.path.join(settings.BASE_DIR, "tree_gardener") + template_path = os.path.join(settings.BASE_DIR, "ros_template") - try: - # Init the trees temp folder - if os.path.exists(result_trees_tmp_path): - shutil.rmtree(result_trees_tmp_path) - os.makedirs(result_trees_tmp_path) + subtrees = [] + actions = [] - # Translate the received JSON - main_tree_tmp_path = os.path.join(result_trees_tmp_path, "main.xml") - json_translator.translate(main_tree_graph, main_tree_tmp_path, bt_order) + try: + # 1. Generate a basic tree from the JSON definition + main_tree = json_translator.translate_raw(main_tree_graph, bt_order) - # Copy all the subtrees to the temp folder + # 2. Get all possible subtrees name and content try: for subtree_file in os.listdir(subtree_path): if subtree_file.endswith(".json"): @@ -939,28 +933,31 @@ def generate_app(request): )[0] print(os.path.join(subtree_path, subtree_file)) - xml_path = os.path.join( - project_path, "code", "trees", "subtrees", f"{subtree_name}.xml" - ) - with open(os.path.join(subtree_path, subtree_file), "r+") as f: # Reading from a file subtree_json = f.read() - json_translator.translate(subtree_json, xml_path, bt_order) - - shutil.copy(xml_path, result_trees_tmp_path) + subtree = json_translator.translate_raw(subtree_json, bt_order) + subtrees.append({"name": subtree_name, "content": subtree}) except: print("No subtrees") - # Generate a self-contained tree - tree_generator.generate( - result_trees_tmp_path, action_path, self_contained_tree_path - ) + # 3. Get all possible actions name and content + for action_file in os.listdir(action_path): + if action_file.endswith(".py"): + action_name = base = os.path.splitext(os.path.basename(action_file))[0] + + with open(os.path.join(action_path, action_file), "r+") as f: + action_content = f.read() + + actions.append({"name": action_name, "content": action_content}) + + # 4. Generate a self-contained tree + final_tree = tree_generator.generate(main_tree, subtrees, actions) # Using the self-contained tree, package the ROS 2 app zip_file_path = app_generator.generate( - self_contained_tree_path, + final_tree, app_name, template_path, action_path, @@ -983,9 +980,14 @@ def generate_app(request): return response + + # TODO: files needed + # Tree + # ros execute + # Change template in ros templates + except Exception as e: print(e) - # Also print the traceback import traceback traceback.print_exc() @@ -1051,7 +1053,7 @@ def generate_dockerized_app(request): actions.append({"name": action_name, "content": action_content}) # 4. Generate a self-contained tree - final_tree = tree_generator.parse_tree_raw(main_tree, subtrees, actions) + final_tree = tree_generator.generate(main_tree, subtrees, actions) # 5. Get necessary files to execute the app in the RB with open(factory_location, "r+") as f: diff --git a/frontend/src/api_helper/TreeWrapper.ts b/frontend/src/api_helper/TreeWrapper.ts index e33837c4c..38a2f4ada 100644 --- a/frontend/src/api_helper/TreeWrapper.ts +++ b/frontend/src/api_helper/TreeWrapper.ts @@ -287,7 +287,7 @@ const generateApp = async ( console.log("The modelJson is: ", modelJson); - const apiUrl = "/bt_studio/generate_app/"; + const apiUrl = "/bt_studio/generate_local_app/"; try { const response = await axios.post( apiUrl, From 83d32b818165f85cb2ecd121e6eb84f670bce7c8 Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Tue, 7 Jan 2025 12:38:22 +0100 Subject: [PATCH 05/31] Fix bug and remove hardcoded templates --- backend/tree_api/templates.py | 34 ++++--------------- backend/tree_api/views.py | 9 +++-- .../file_browser/modals/NewFileModal.jsx | 10 +++--- 3 files changed, 19 insertions(+), 34 deletions(-) diff --git a/backend/tree_api/templates.py b/backend/tree_api/templates.py index 270004542..4efdc1bf7 100644 --- a/backend/tree_api/templates.py +++ b/backend/tree_api/templates.py @@ -2,32 +2,10 @@ from django.conf import settings -def create_action_from_template(file_path, filename, template): +def get_action_template(filename, template, template_path): - templates_folder_path = os.path.join(settings.BASE_DIR, "templates") - template_path = os.path.join(templates_folder_path, template) - - replacements = {"ACTION": filename[:-3]} - - if template == "empty": - with open(file_path, "w") as f: - f.write("") # Empty content - return True - elif template == "action": - with open(file_path, "w") as f: - with open(template_path, "r") as temp: - for line in temp: - for src, target in replacements.items(): - line = line.replace(src, target) - f.write(line) - return True - elif template == "io": - with open(file_path, "w") as f: - with open(template_path, "r") as temp: - for line in temp: - for src, target in replacements.items(): - line = line.replace(src, target) - f.write(line) - return True - else: - return False + with open(template_path, "r") as temp: + file_data = temp.read() + new_data = file_data.replace("ACTION", filename[:-3]) + return new_data + return "" diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index a1fc596cf..679d1fbdb 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -654,10 +654,15 @@ def create_action(request): action_path = os.path.join(project_path, "code/actions") file_path = os.path.join(action_path, filename) + templates_folder_path = os.path.join(settings.BASE_DIR, "templates") + template_path = os.path.join(templates_folder_path, template) + if not os.path.exists(file_path): - if templates.create_action_from_template(file_path, filename, template): + try: + with open(file_path, "w") as f: + f.write(templates.get_action_template(filename, template, template_path)) return Response({"success": True}) - else: + except: return Response( {"success": False, "message": "Template does not exist"}, status=400 ) diff --git a/frontend/src/components/file_browser/modals/NewFileModal.jsx b/frontend/src/components/file_browser/modals/NewFileModal.jsx index b99dba2e3..1d0734cb3 100644 --- a/frontend/src/components/file_browser/modals/NewFileModal.jsx +++ b/frontend/src/components/file_browser/modals/NewFileModal.jsx @@ -79,9 +79,11 @@ const NewFileModal = ({ onSubmit, isOpen, onClose, fileList, location }) => { setCreationType("plain"); setTemplate("empty"); - if (isOpen && location) { + if (isOpen) { //NOTE: One for actions and one for location - createValidNamesList(location, setSearchPlainList); + if (location) { + createValidNamesList(location, setSearchPlainList); + } createValidNamesList("actions", setSearchActionsList); } }, [isOpen]); @@ -135,7 +137,7 @@ const NewFileModal = ({ onSubmit, isOpen, onClose, fileList, location }) => { checkList = searchPlainList; } - if (preCheck) { + if (preCheck && checkList) { checkList.some((element) => { var name = element.name; @@ -152,7 +154,7 @@ const NewFileModal = ({ onSubmit, isOpen, onClose, fileList, location }) => { } else { isValidName = false; } - console.log(creationType); + console.log(creationType, checkList); allowCreation(isValidName); }; From 87fc3bc37c30bda4040db150710e0aa980cb5e27 Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Tue, 7 Jan 2025 12:41:36 +0100 Subject: [PATCH 06/31] Removing hardcoded json translator --- backend/tree_api/json_translator.py | 44 ----------------------------- backend/tree_api/views.py | 6 ++-- 2 files changed, 2 insertions(+), 48 deletions(-) diff --git a/backend/tree_api/json_translator.py b/backend/tree_api/json_translator.py index bfd90a0e8..6c5b4810f 100644 --- a/backend/tree_api/json_translator.py +++ b/backend/tree_api/json_translator.py @@ -195,50 +195,6 @@ def translate_raw(content, raw_order): xml_string = prettify_xml(root) return xml_string -def translate(content, tree_path, raw_order): - - # Parse the JSON data - try: - parsed_json = json.loads(content) - except json.JSONDecodeError as e: - raise ValueError(f"Invalid JSON content: {e}") - - try: - # Extract nodes and links information - node_models = parsed_json["layers"][1]["models"] - link_models = parsed_json["layers"][0]["models"] - - # Get the tree structure - tree_structure = get_tree_structure(link_models, node_models) - - # Get the order of bt: True = Ascendent; False = Descendent - order = raw_order == "bottom-to-top" - - # Generate XML - root = Element("Root", name="Tree Root") - behavior_tree = SubElement(root, "BehaviorTree") - start_node_id = get_start_node_id(node_models, link_models) - build_xml( - node_models, - link_models, - tree_structure, - start_node_id, - behavior_tree, - order, - ) - except Exception as e: - tree_name = os.path.splitext(os.path.basename(tree_path))[0] - raise RuntimeError(f"Failed to translate tree '{tree_name}': {e}") - - # Save the xml in the specified route - xml_string = prettify_xml(root) - print(xml_string) - print(tree_path) - f = open(tree_path, "w") - f.write(xml_string) - f.close() - - def translate_tree_structure(content, raw_order): # Parse the JSON data parsed_json = content diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index 679d1fbdb..24c5e789d 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -390,9 +390,6 @@ def save_subtree(request): with open(json_path, "w") as f: f.write(subtree_json) - # TOFIX: order hardcoded - # json_translator.translate(subtree_json, xml_path, "top-to-bottom") - return JsonResponse({"success": True}, status=status.HTTP_200_OK) except Exception as e: @@ -829,7 +826,8 @@ def translate_json(request): ) # Pass the JSON content to the translate function - json_translator.translate(content, folder_path + "/tree.xml", bt_order) + with open(folder_path + "/tree.xml", "w") as f: + f.write(json_translator.translate_raw(content, bt_order)) return Response({"success": True}) except Exception as e: From bfc5eb7b86078358b631c926e222904afd31164a Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Tue, 7 Jan 2025 16:13:29 +0100 Subject: [PATCH 07/31] Moving app creation to the frontend --- backend/tree_api/app_generator.py | 180 +---- backend/tree_api/views.py | 69 +- frontend/src/api_helper/TreeWrapper.ts | 24 +- .../src/components/header_menu/HeaderMenu.tsx | 42 +- frontend/src/templates/RosTemplates.ts | 245 +++++++ frontend/src/templates/TreeGardener.ts | 676 ++++++++++++++++++ 6 files changed, 963 insertions(+), 273 deletions(-) create mode 100644 frontend/src/templates/RosTemplates.ts create mode 100644 frontend/src/templates/TreeGardener.ts diff --git a/backend/tree_api/app_generator.py b/backend/tree_api/app_generator.py index a84c264e6..7a7d9e843 100644 --- a/backend/tree_api/app_generator.py +++ b/backend/tree_api/app_generator.py @@ -9,79 +9,13 @@ # Helper functions ############################################################################## - -# Zip a directory -def zipdir(path, ziph): - - for root, dirs, files in os.walk(path): - for file in files: - ziph.write( - os.path.join(root, file), - os.path.relpath(os.path.join(root, file), path), - ) - - -# Rename all the necessary files in the template -def rename_template_files(root_path, original_str, replacement_str): - - for dirpath, dirnames, filenames in os.walk(root_path, topdown=False): - - # Rename directories - for dirname in dirnames: - if original_str in dirname: - src_dir = os.path.join(dirpath, dirname) - dst_dir = os.path.join( - dirpath, dirname.replace(original_str, replacement_str) - ) - os.rename(src_dir, dst_dir) - - # Rename files - for filename in filenames: - if original_str in filename: - src_file = os.path.join(dirpath, filename) - dst_file = os.path.join( - dirpath, filename.replace(original_str, replacement_str) - ) - os.rename(src_file, dst_file) - - -# Replace a str in a file for another -def replace_contents_in_file(file_path, original_str, replacement_str): - - with open(file_path, "r") as file: - file_data = file.read() - - new_data = file_data.replace(original_str, replacement_str) - - with open(file_path, "w") as file: - file.write(new_data) - - -def get_actions_paths(dir_to_scan): - - files_to_scan = [] - for root, _, filenames in os.walk(dir_to_scan): - for filename in filenames: - if filename.endswith(".py"): - files_to_scan.append(os.path.join(root, filename)) - - return files_to_scan - - # Collect unique imports from files -def get_unique_imports(file_paths): +def get_unique_imports(actions): unique_imports = set() - for file_path in file_paths: - if not os.path.exists(file_path): - print( - f"Warning: File {file_path} does not exist. Skipping import collection for this file." - ) - continue - - with open(file_path, "r") as file: - lines = file.readlines() + for action in actions: + lines = action["content"].splitlines() for line in lines: # Using regex to find lines that don't start with '#' and have 'import ...' or 'from ... import ...' @@ -89,112 +23,4 @@ def get_unique_imports(file_paths): if match: unique_imports.add(match.group(1)) - f = open("/tmp/imports.txt", "w") - f.write(f"Action paths: {unique_imports}.") - f.close() - return list(unique_imports) - - -def update_package_xml(package_xml_path, unique_imports): - # Mapping from Python import names to ROS package names - special_imports = { - "cv2": "python3-opencv", - # Add more mappings here as needed - } - - with open(package_xml_path, "r") as file: - content = file.read() - - # Finding the position of the last tag - last_exec_depend_index = content.rfind("") + len("") - - # Replacing special import names and generating new entries - new_exec_depends = "\n".join( - [ - f" {special_imports.get(imp, imp)}" - for imp in unique_imports - ] - ) - - # Inserting the new dependencies after the last - updated_content = ( - content[:last_exec_depend_index] - + "\n" - + new_exec_depends - + content[last_exec_depend_index:] - ) - - # Writing the updated content back to package.xml - with open(package_xml_path, "w") as file: - file.write(updated_content) - - -# Setup the package with the user data -def setup_package(temp_path, action_path, user_data): - - app_name = user_data["app_name"] - template_str = "ros_template" - - # 1. Rename directories and files recursively - rename_template_files(temp_path, template_str, app_name) - - # 2. Replace the original_str with app_name in the content of relevant files - files_to_edit = ["package.xml", "setup.py", "setup.cfg", app_name + "/execute.py"] - for file_name in files_to_edit: - file_path = os.path.join(temp_path, file_name) - if os.path.exists(file_path): - replace_contents_in_file(file_path, template_str, app_name) - else: - print( - f"Warning: {file_name} not found in {temp_path}. Skipping content replacement for this file." - ) - - # 3. Get a list of unique imports from the user-defined actions - action_paths = get_actions_paths(action_path) - imports = get_unique_imports(action_paths) - - # 4. Update the template package xml so the dependencies can be installed with rosdep - package_xml_path = os.path.join(temp_path, "package.xml") - update_package_xml(package_xml_path, imports) - - -############################################################################## -# Main section -############################################################################## - - -def generate(app_tree, app_name, template_path, action_path, tree_gardener_src): - - app_path = "/tmp/" + app_name - executor_path = app_path + "/" + app_name - tree_gardener_dst = app_path + "/tree_gardener" - - # 1. Copy the template to a temporary directory - if os.path.exists(executor_path): - shutil.rmtree(executor_path) # Delete if it already exists - shutil.copytree(template_path, executor_path) - print(f"Template copied to {executor_path}") - - # 2. Copy the tree to the template directory - tree_location = executor_path + "/resource/app_tree.xml" - with open(tree_location, "w") as file: - file.write(app_tree) - - # 3. Edit some files in the template - user_data = {"app_name": app_name} - setup_package(executor_path, action_path, user_data) - - # 4. Copy the tree_gardener package to the app - if os.path.exists(tree_gardener_dst): - shutil.rmtree(tree_gardener_dst) # Delete if it already exists - shutil.copytree(tree_gardener_src, tree_gardener_dst) - print(f"Tree gardener copied to {tree_gardener_dst}") - - # 3. Generate a zip file in the destination folder with a name specified by the user - dest_path = app_path + ".zip" - with zipfile.ZipFile(dest_path, "w") as zipf: - zipdir(app_path, zipf) - print(f"Directory compressed to {dest_path}") - - return dest_path diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index 24c5e789d..ab4d4d720 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -893,23 +893,13 @@ def download_data(request): else: return Response({"error": "app_name parameter is missing"}, status=500) -@api_view(["POST"]) +@api_view(["GET"]) def generate_local_app(request): - if ( - "app_name" not in request.data - or "tree_graph" not in request.data - or "bt_order" not in request.data - ): - return Response( - {"error": "Incorrect request parameters"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Get the parameters - app_name = request.data.get("app_name") - main_tree_graph = request.data.get("tree_graph") - bt_order = request.data.get("bt_order") + # Get the request parameters + app_name = request.GET.get("app_name", None) + main_tree_graph = request.GET.get("tree_graph", None) + bt_order = request.GET.get("bt_order", None) # Make folder path relative to Django app base_path = os.path.join(settings.BASE_DIR, "filesystem") @@ -917,8 +907,6 @@ def generate_local_app(request): action_path = os.path.join(project_path, "code/actions") subtree_path = os.path.join(project_path, "code/trees/subtrees/json") - tree_gardener_src = os.path.join(settings.BASE_DIR, "tree_gardener") - template_path = os.path.join(settings.BASE_DIR, "ros_template") subtrees = [] actions = [] @@ -958,36 +946,9 @@ def generate_local_app(request): # 4. Generate a self-contained tree final_tree = tree_generator.generate(main_tree, subtrees, actions) - # Using the self-contained tree, package the ROS 2 app - zip_file_path = app_generator.generate( - final_tree, - app_name, - template_path, - action_path, - tree_gardener_src, - ) - - # Confirm ZIP file exists - if not os.path.exists(zip_file_path): - return Response( - {"success": False, "message": "ZIP file not found"}, status=400 - ) - - # Return the zip file as a response - with open(zip_file_path, "rb") as zip_file: - response = HttpResponse(zip_file, content_type="application/zip") - response["Content-Disposition"] = ( - f"attachment; filename={os.path.basename(zip_file_path)}" - ) - return response - - return response + unique_imports = app_generator.get_unique_imports(actions) - - # TODO: files needed - # Tree - # ros execute - # Change template in ros templates + return JsonResponse({"success": True, "tree": final_tree, "dependencies": unique_imports}) except Exception as e: print(e) @@ -1013,12 +974,6 @@ def generate_dockerized_app(request): action_path = os.path.join(project_path, "code/actions") subtree_path = os.path.join(project_path, "code/trees/subtrees/json") - tree_gardener_src = os.path.join(settings.BASE_DIR, "tree_gardener") - template_path = os.path.join(settings.BASE_DIR, "ros_template") - - factory_location = tree_gardener_src + "/tree_gardener/tree_factory.py" - tools_location = tree_gardener_src + "/tree_gardener/tree_tools.py" - entrypoint_location = template_path + "/ros_template/execute_docker.py" subtrees = [] actions = [] @@ -1058,16 +1013,8 @@ def generate_dockerized_app(request): # 4. Generate a self-contained tree final_tree = tree_generator.generate(main_tree, subtrees, actions) - # 5. Get necessary files to execute the app in the RB - with open(factory_location, "r+") as f: - factory_content = f.read() - with open(tools_location, "r+") as f: - tools_content = f.read() - with open(entrypoint_location, "r+") as f: - entrypoint_content = f.read() - # 6. Return the files as a response - return JsonResponse({"success": True, "tree": final_tree, "factory":factory_content, "tools": tools_content, "entrypoint":entrypoint_content}) + return JsonResponse({"success": True, "tree": final_tree}) except Exception as e: print(e) diff --git a/frontend/src/api_helper/TreeWrapper.ts b/frontend/src/api_helper/TreeWrapper.ts index 38a2f4ada..f944c40e4 100644 --- a/frontend/src/api_helper/TreeWrapper.ts +++ b/frontend/src/api_helper/TreeWrapper.ts @@ -276,7 +276,7 @@ const getCustomUniverseZip = async ( // App management -const generateApp = async ( +const generateLocalApp = async ( modelJson: Object, currentProjectname: string, btOrder: string @@ -285,25 +285,9 @@ const generateApp = async ( if (!currentProjectname) throw new Error("Current Project name is not set"); if (!btOrder) throw new Error("Behavior Tree order is not set"); - console.log("The modelJson is: ", modelJson); - - const apiUrl = "/bt_studio/generate_local_app/"; + const apiUrl = `/bt_studio/generate_local_app?app_name=${currentProjectname}&tree_graph=${JSON.stringify(modelJson)}&bt_order=${btOrder}`; try { - const response = await axios.post( - apiUrl, - { - app_name: currentProjectname, - tree_graph: JSON.stringify(modelJson), - bt_order: btOrder, - }, - { - responseType: "blob", // Ensure the response is treated as a Blob - headers: { - //@ts-ignore Needed for compatibility with Unibotics - "X-CSRFToken": context.csrf, - }, - } - ); + const response = await axios.get(apiUrl); // Handle unsuccessful response status (e.g., non-2xx status) if (!isSuccessful(response)) { @@ -460,7 +444,7 @@ export { saveBaseTree, loadProjectConfig, getProjectGraph, - generateApp, + generateLocalApp, generateDockerizedApp, getUniverseConfig, getRoboticsBackendUniversePath, diff --git a/frontend/src/components/header_menu/HeaderMenu.tsx b/frontend/src/components/header_menu/HeaderMenu.tsx index 7bd592ba6..b33101cdc 100644 --- a/frontend/src/components/header_menu/HeaderMenu.tsx +++ b/frontend/src/components/header_menu/HeaderMenu.tsx @@ -5,7 +5,7 @@ import Toolbar from "@mui/material/Toolbar"; import { createProject, saveBaseTree, - generateApp, + generateLocalApp, generateDockerizedApp, getUniverseConfig, getCustomUniverseZip, @@ -30,6 +30,9 @@ import UniversesModal from "./modals/UniverseModal"; import SettingsModal from "../settings_popup/SettingsModal"; import { OptionsContext } from "../options/Options"; +import RosTemplates from './../../templates/RosTemplates'; +import TreeGardener from './../../templates/TreeGardener'; + const HeaderMenu = ({ currentProjectname, setCurrentProjectname, @@ -195,21 +198,32 @@ const HeaderMenu = ({ const onDownloadApp = async () => { try { // Get the blob from the API wrapper - const appBlob = await generateApp( + const appFiles = await generateLocalApp( modelJson, currentProjectname, settings.btOrder.value, ); - // Create a download link and trigger download - const url = window.URL.createObjectURL(appBlob); - const a = document.createElement("a"); - a.style.display = "none"; - a.href = url; - a.download = `${currentProjectname}.zip`; // Set the downloaded file's name - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); // Clean up after the download + // Create the zip with the files + const zip = new JSZip(); + + console.log(appFiles.dependencies) + + TreeGardener.addLocalFiles(zip) + RosTemplates.addLocalFiles(zip, currentProjectname, appFiles.tree, appFiles.dependencies) + + zip.generateAsync({type:"blob"}).then(function(content) { + // Create a download link and trigger download + const url = window.URL.createObjectURL(content); + const a = document.createElement("a"); + a.style.display = "none"; + a.href = url; + a.download = `${currentProjectname}.zip`; // Set the downloaded file's name + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); // Clean up after the download + }); + console.log("App downloaded successfully"); } catch (error) { if (error instanceof Error) { @@ -239,13 +253,11 @@ const HeaderMenu = ({ ); // Create the zip with the files - const zip = new JSZip(); zip.file("self_contained_tree.xml", appFiles.tree); - zip.file("tree_factory.py", appFiles.factory); - zip.file("tree_tools.py", appFiles.tools); - zip.file("execute_docker.py", appFiles.entrypoint); + TreeGardener.addDockerFiles(zip) + RosTemplates.addDockerFiles(zip) // Convert the blob to base64 using FileReader const reader = new FileReader(); diff --git a/frontend/src/templates/RosTemplates.ts b/frontend/src/templates/RosTemplates.ts new file mode 100644 index 000000000..e9063ed83 --- /dev/null +++ b/frontend/src/templates/RosTemplates.ts @@ -0,0 +1,245 @@ +import JSZip from "jszip" + +const executeDocker = +`import functools +import sys +import rclpy +import py_trees +from rclpy.node import Node +import tree_factory +import os +from tree_tools import ascii_bt_to_json + + +def start_console(): + # Get all the file descriptors and choose the latest one + fds = os.listdir("/dev/pts/") + console_fd = str(max(map(int, fds[:-1]))) + + sys.stderr = open("/dev/pts/" + console_fd, "w") + sys.stdout = open("/dev/pts/" + console_fd, "w") + sys.stdin = open("/dev/pts/" + console_fd, "r") + + +def close_console(): + sys.stderr.close() + sys.stdout.close() + sys.stdin.close() + + +class TreeExecutor(Node): + def __init__(self): + super().__init__("tree_executor_node") + # Get the path to the root of the package + ws_path = "/workspace/code" + tree_path = os.path.join(ws_path, "self_contained_tree.xml") + + factory = tree_factory.TreeFactory() + self.tree = factory.create_tree_from_file(tree_path) + snapshot_visitor = py_trees.visitors.SnapshotVisitor() + self.tree.add_post_tick_handler( + functools.partial(self.post_tick_handler, snapshot_visitor) + ) + self.tree.visitors.append(snapshot_visitor) + self.tree.tick_tock(period_ms=50) + + def post_tick_handler(self, snapshot_visitor, behaviour_tree): + with open("/tmp/tree_state", "w") as f: + ascii_bt_to_json( + py_trees.display.ascii_tree( + behaviour_tree.root, + visited=snapshot_visitor.visited, + previously_visited=snapshot_visitor.visited, + ), + py_trees.display.ascii_blackboard(), + f, + ) + + def spin_tree(self): + + try: + rclpy.spin(self.tree.node) + except (KeyboardInterrupt, rclpy.executors.ExternalShutdownException): + self.tree.shutdown() + finally: + print("Shutdown completed") + + +def main(): + # Start the console + start_console() + # Init the components + rclpy.init() + executor = TreeExecutor() + # Spin the tree + executor.spin_tree() + # Close the console + close_console() + + +main() +` + +const thirdparty = `repositories: + ThirdParty/tb4_sim: + type: git + url: https://github.com/OscarMrZ/tb4_sim.git + version: main + ThirdParty/py_trees_ros_viewer: + type: git + url: https://github.com/splintered-reality/py_trees_ros_viewer.git + version: devel +` + +const setupConfig = (name:string) => { + return `[develop] +script_dir=$base/lib/ros_template +[install] +install_scripts=$base/lib/`+name+` +` +} + +const setupPython = (name:string) => { + return `from setuptools import setup +from setuptools import find_packages + +package_name = "`+name+`" + +setup( + name=package_name, + version="0.1.0", + packages=find_packages(exclude=["test"]), + data_files=[ + ("share/ament_index/resource_index/packages", ["resource/" + package_name]), + ("share/" + package_name, ["package.xml"]), + ( + "share/" + package_name + "/resource", + [ + "resource/app_tree.xml", + ], + ), + ], + install_requires=["setuptools"], + zip_safe=True, + author="", + author_email="", + maintainer="", + maintainer_email="", + keywords=["ROS2", "py_trees"], + classifiers=[ + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Topic :: Software Development", + ], + description="A ROS2 app generated with tree_translator", + license="", + tests_require=["pytest"], + entry_points={ + "console_scripts": [ + "executor = ros_template.execute:main", + ], + }, +) + ` +} + +const executeLocal = (name:string) => { + return `import rclpy +from rclpy.node import Node +from tree_gardener import tree_factory +from ament_index_python.packages import get_package_share_directory +import os + + +class TreeExecutor(Node): + def __init__(self): + + super().__init__("tree_executor_node") + + # Get the path to the root of the package + pkg_share_dir = get_package_share_directory("`+name+`") + tree_path = os.path.join(pkg_share_dir, "resource", "app_tree.xml") + + factory = tree_factory.TreeFactory() + self.tree = factory.create_tree_from_file(tree_path) + self.tree.tick_tock(period_ms=50) + + def spin_tree(self): + + try: + rclpy.spin(self.tree.node) + except (KeyboardInterrupt, rclpy.executors.ExternalShutdownException): + self.tree.shutdown() + finally: + print("Shutdown completed") + + +def main(): + + # Init the components + rclpy.init() + executor = TreeExecutor() + + # Spin the tree + executor.spin_tree() + ` +} + +const packageInfo = (name:string, extraDependencies: string[]) => { + //TODO: add also more user info + var dependStr = `rclpy` + + extraDependencies.forEach(depend => { + dependStr += ` + `+ depend +`` + }); + + return ` + + `+name+` + 0.1.0 + A ROS2 app generated BT Studio + Mantainer + GPLv3 + + ` + + dependStr + + ` + + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + + ` +} + +namespace RosTemplates { + export function addDockerFiles(zip:JSZip ) { + zip.file("execute_docker.py", executeDocker); + } + + export function addLocalFiles(zip:JSZip, project_name: string, tree_xml:string, depend: string[]) { + const loc = zip.folder(project_name); + + if (loc == null) { + return + } + + loc.file("resource/"+project_name, ""); + loc.file("resource/app_tree.xml", tree_xml); + + loc.file(project_name + "/execute.py", executeLocal(project_name)); + loc.file(project_name + "/__init__.py", ""); + + loc.file("package.xml", packageInfo(project_name, depend)); + loc.file("setup.cfg", setupConfig(project_name)); + loc.file("setup.py", setupPython(project_name)); + } +} + +export default RosTemplates; \ No newline at end of file diff --git a/frontend/src/templates/TreeGardener.ts b/frontend/src/templates/TreeGardener.ts new file mode 100644 index 000000000..e763cb40a --- /dev/null +++ b/frontend/src/templates/TreeGardener.ts @@ -0,0 +1,676 @@ +import JSZip from "jszip"; + +const treeTools = +`import re +import py_trees + + +class GlobalBlackboard: + + _instance = None + + @staticmethod + def get_instance(): + + if GlobalBlackboard._instance is None: + GlobalBlackboard._instance = py_trees.blackboard.Client(name="Global") + + return GlobalBlackboard._instance + + +def get_port_content(port_value): + + if port_value.startswith("{") and port_value.endswith("}"): + bb_key = port_value.strip("{}") + blackboard = GlobalBlackboard.get_instance() + + # Return the value of the blackboard entry if it exists, otherwise None + return getattr(blackboard, bb_key, "") + else: + return port_value + + +def set_port_content(port_value, value): + + if not (port_value.startswith("{") and port_value.endswith("}")): + raise ValueError( + f"'{port_value}' is a read-only port. Only ports connected to the blackboard are writable" + ) + + # Extract the key from the port_value + key = port_value.strip("{}") + + blackboard = GlobalBlackboard.get_instance() + + # Lazy creation: if key doesn't exist, register it + if not hasattr(blackboard, key): + blackboard.register_key( + key=key, access=py_trees.common.Access.WRITE, required=True + ) + + # Set the value for the key in the blackboard + setattr(blackboard, key, value) + + +########### ASCII BT STATUS TO JSON ############################################ +def ascii_state_to_state(state_raw): + letter = [x for x in state_raw] + state = letter[1] + + match state: + case "*": + return "RUNNING" + case "o": + return "SUCCESS" + case "x": + return "FAILURE" + case "-": + return "INVALID" + case _: + return "INVALID" + + +def ascii_tree_to_json(tree): + indent_levels = 4 # 4 spaces = 1 level deep + do_append_coma = False + last_indent_level = -1 + json_str = '"tree":{' + + # Remove escape chars + ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") + tree = ansi_escape.sub("", tree) + + for line in iter(tree.splitlines()): + entry = line.strip().split(" ") + name = entry[1] + if len(entry) == 2: + state = "NONE" + else: + state = ascii_state_to_state(entry[2]) + + indent = int((len(line) - len(line.lstrip())) / indent_levels) + if not (indent > last_indent_level): + json_str += "}" * (last_indent_level - indent + 1) + + last_indent_level = indent + + if do_append_coma: + json_str += "," + else: + do_append_coma = True + json_str += '"' + name + '":{' + json_str += f'"state":"{state}"' + + json_str += "}" * (last_indent_level + 1) + "}" + return json_str + + +def ascii_blackboard_to_json(blackboard): + json_str = '"blackboard":{' + do_append_coma = False + + ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") + blackboard = ansi_escape.sub("", blackboard) + + for line in iter(blackboard.splitlines()): + if "Blackboard Data" in line: + continue + if len(line.strip()) == 0: + continue + if do_append_coma: + json_str += "," + # Remove whitespaces with strip and remove / from entry + try: + [entry, value] = line.strip()[1:].split(":") + json_str += f'"{entry.strip()}":"{value.strip()}"' + do_append_coma = True + except: + pass + json_str += "}" + return json_str + + +def ascii_bt_to_json(tree, blackboard, file): + file.write("{") + file.write(f"{ascii_tree_to_json(tree)},{ascii_blackboard_to_json(blackboard)}") + # file.write(f"{ascii_tree_to_json(tree)}") + file.write("}") + file.close() +` + +const treeFactory = +`############################################################################## +# Imports +############################################################################## + +import py_trees +import py_trees_ros.trees +import py_trees.console as console +import xml.etree.ElementTree as ET +import typing +import autopep8 +import textwrap +import itertools +import rclpy.node +import time + +############################################################################## +# Tree classes +############################################################################## + + +class ReactiveSequence(py_trees.composites.Composite): + def __init__( + self, + name: str, + memory: bool, + children: typing.Optional[typing.List[py_trees.behaviour.Behaviour]] = None, + ): + super(ReactiveSequence, self).__init__(name, children) + self.memory = memory + + def tick(self) -> typing.Iterator[py_trees.behaviour.Behaviour]: + """ + Tick over the children as a reactive sequence. + """ + + self.logger.debug("%s.tick()" % self.__class__.__name__) + + # Initialize always from the start + self.current_child = self.children[0] if self.children else None + for child in self.children: + if child.status != py_trees.common.Status.INVALID: + child.stop(py_trees.common.Status.INVALID) + + # Nothing to do + if not self.children: + self.current_child = None + self.stop(py_trees.common.Status.SUCCESS) + yield self + return + + # Ticking the children + for child in self.children: + for node in child.tick(): + yield node + if node is child and node.status != py_trees.common.Status.SUCCESS: + self.status = node.status + yield self + return + + self.stop(py_trees.common.Status.SUCCESS) + yield self + + def stop( + self, new_status: py_trees.common.Status = py_trees.common.Status.INVALID + ) -> None: + """ + Ensure that children are appropriately stopped and update status. + + Args: + new_status : the composite is transitioning to this new status + """ + self.logger.debug( + f"{self.__class__.__name__}.stop()[{self.status}->{new_status}]" + ) + py_trees.composites.Composite.stop(self, new_status) + + +class SequenceWithMemory(py_trees.composites.Composite): + def __init__( + self, + name: str, + memory: bool, + children: typing.Optional[typing.List[py_trees.behaviour.Behaviour]] = None, + ): + super(SequenceWithMemory, self).__init__(name, children) + self.memory = memory + + def tick(self) -> typing.Iterator[py_trees.behaviour.Behaviour]: + """ + Tick over the children as a memory-enabled sequence. + """ + + self.logger.debug("%s.tick()" % self.__class__.__name__) + + # Get the index of the current child + index = ( + self.children.index(self.current_child) if self.current_child != None else 0 + ) + + # Nothing to do + if not self.children: + self.current_child = None + self.stop(py_trees.common.Status.SUCCESS) + yield self + return + + # Ticking the children + for child in itertools.islice(self.children, index, None): + for node in child.tick(): + yield node + if node is child: + if node.status in [ + py_trees.common.Status.RUNNING, + py_trees.common.Status.FAILURE, + ]: + self.status = node.status + self.current_child = child + yield self + return + else: + # child has returned SUCCESS, move to next child on the next tick + index += 1 + self.current_child = ( + self.children[index] if index < len(self.children) else None + ) + + # All children have returned SUCCESS + if self.current_child is None: + self.stop(py_trees.common.Status.SUCCESS) + + yield self + + def stop( + self, new_status: py_trees.common.Status = py_trees.common.Status.INVALID + ) -> None: + """ + Ensure that children are appropriately stopped and update status. + + Args: + new_status : the composite is transitioning to this new status + """ + self.logger.debug( + f"{self.__class__.__name__}.stop()[{self.status}->{new_status}]" + ) + py_trees.composites.Composite.stop(self, new_status) + + +class ReactiveFallback(py_trees.composites.Composite): + def __init__( + self, + name: str, + memory: bool, + children: typing.Optional[typing.List[py_trees.behaviour.Behaviour]] = None, + ): + super(ReactiveFallback, self).__init__(name, children) + self.memory = memory + + def tick(self) -> typing.Iterator[py_trees.behaviour.Behaviour]: + + self.logger.debug("%s.tick()" % self.__class__.__name__) + + # Initialize + if self.status != py_trees.common.Status.RUNNING: + self.logger.debug( + "%s.tick() [!RUNNING->reset current_child]" % self.__class__.__name__ + ) + self.current_child = self.children[0] if self.children else None + + # nothing to do + if not self.children: + self.current_child = None + self.stop(py_trees.common.Status.FAILURE) + yield self + return + + # always start from the first child + index = 0 + + # actual work + previous = self.current_child + for child in itertools.islice(self.children, index, None): + for node in child.tick(): + yield node + if node is child: + if ( + node.status == py_trees.common.Status.RUNNING + or node.status == py_trees.common.Status.SUCCESS + ): + self.current_child = child + self.status = node.status + if previous is None or previous != self.current_child: + # we interrupted, invalidate everything at a lower priority + passed = False + for child in self.children: + if passed: + if child.status != py_trees.common.Status.INVALID: + child.stop(py_trees.common.Status.INVALID) + passed = True if child == self.current_child else passed + yield self + return + + # all children failed, set failure ourselves and current child to the last bugger who failed us + self.status = py_trees.common.Status.FAILURE + try: + self.current_child = self.children[-1] + except IndexError: + self.current_child = None + yield self + + def stop( + self, new_status: py_trees.common.Status = py_trees.common.Status.INVALID + ) -> None: + """ + Ensure that children are appropriately stopped and update status. + + Args: + new_status : the composite is transitioning to this new status + """ + self.logger.debug( + f"{self.__class__.__name__}.stop()[{self.status}->{new_status}]" + ) + py_trees.composites.Composite.stop(self, new_status) + + +class Delay(py_trees.decorators.Decorator): + def __init__( + self, name: str, child: py_trees.behaviour.Behaviour, delay_ms: int = 0 + ): + + super(Delay, self).__init__(name=name, child=child) + self.secs = float(delay_ms / 1000) # Convert to seconds + self.start_time = None + + def initialise(self) -> None: + + self.start_time = time.monotonic() + self.secs + + def tick(self) -> typing.Iterator[py_trees.behaviour.Behaviour]: + + # Check if init is needed + if self.status != py_trees.common.Status.RUNNING: + self.initialise() + + current_time = time.monotonic() + if current_time < self.start_time: + # Return the decorator itself + for node in py_trees.behaviour.Behaviour.tick(self): + yield node + else: + # Tick the child + for node in py_trees.decorators.Decorator.tick(self): + yield node + + def update(self) -> py_trees.common.Status: + + current_time = time.monotonic() + if current_time > self.start_time: + return self.decorated.status + else: + return py_trees.common.Status.RUNNING + + def terminate(self, new_status: py_trees.common.Status) -> None: + self.decorated.stop(new_status) + + +############################################################################## +# Auxiliary variables +############################################################################## + +factory = { + "Sequence": py_trees.composites.Sequence, + "ReactiveSequence": ReactiveSequence, + "SequenceWithMemory": SequenceWithMemory, + "Fallback": py_trees.composites.Selector, + "ReactiveFallback": ReactiveFallback, + "Inverter": py_trees.decorators.Inverter, + "ForceSuccess": py_trees.decorators.FailureIsSuccess, + "ForceFailure": py_trees.decorators.SuccessIsFailure, + "Repeat": py_trees.decorators.Repeat, + "RetryUntilSuccessful": py_trees.decorators.Retry, + "KeepRunningUntilFailure": py_trees.decorators.SuccessIsRunning, + "RunOnce": py_trees.decorators.OneShot, + "Delay": Delay, +} + +############################################################################## +# Auxiliary functions +############################################################################## + + +def get_branches(element): + + class_name = element.tag + name_arg = element.get("name") + + Class = factory.get(class_name) + + if Class is None: + print(f"Class {class_name} not found") + return None + + if "Sequence" in class_name or "Fallback" in class_name: + instance = Class(name_arg, memory=True) + for child_element in element: + child_instance = get_branches(child_element) + if child_instance is not None: + instance.add_child(child_instance) + elif "RetryUntilSuccessful" in class_name: + nfailures = element.get("num_attempts") + for child_element in element: + child_instance = get_branches(child_element) + if child_instance is not None: + child = child_instance + retry_name = "Retry_" + str(nfailures) + instance = Class(name=class_name, num_failures=int(nfailures), child=child) + elif "Repeat" in class_name: + num_cycles = element.get("num_cycles") + for child_element in element: + child_instance = get_branches(child_element) + if child_instance is not None: + child = child_instance + repeat_name = "Repeat_" + str(num_cycles) + instance = Class(name=class_name, child=child, num_success=int(num_cycles)) + elif ( + "Inverter" in class_name + or "Force" in class_name + or "KeepRunningUntilFailure" in class_name + ): + for child_element in element: + child_instance = get_branches(child_element) + if child_instance is not None: + child = child_instance + instance = Class(name=class_name, child=child) + elif "RunOnce" in class_name: + for child_element in element: + child_instance = get_branches(child_element) + if child_instance is not None: + child = child_instance + instance = Class( + name=class_name, + child=child, + policy=py_trees.common.OneShotPolicy.ON_COMPLETION, + ) + elif "Delay" in class_name: + delay_ms = element.get("delay_ms") + for child_element in element: + child_instance = get_branches(child_element) + if child_instance is not None: + child = child_instance + instance = Class(name=class_name, child=child, delay_ms=int(delay_ms)) + else: + # Check if there is a port argument + ports = {} + for arg in element.attrib: + if arg != "name": + port_name = arg + port_content = element.get(arg) + + ports[port_name] = port_content + + instance = Class(name_arg, ports) + + return instance + + +def construct_behaviour_tree_from_xml(doc): + + behavior_tree_element = doc.find(".//BehaviorTree") + + if behavior_tree_element is None: + print("No BehaviorTree found in the XML") + return None + + root_behaviour = None + for child in behavior_tree_element: + root_behaviour = get_branches(child) + + return root_behaviour + + +def add_actions_to_factory(doc): + + code_element = doc.find(".//Code") + + for element in code_element: + + # Extract formatted code + class_name = element.tag + code_text = textwrap.dedent(element.text) + formatted_code = autopep8.fix_code(code_text) + + # Copy current global namespace for safety + namespace = globals().copy() + + # Execute in the copied namespace (because of closure, classes may access their imports) + exec(formatted_code, namespace) + + # Access the class from the namespace and create an instance + class_ref = namespace[class_name] + factory[class_name] = class_ref + + +############################################################################## +# Tree factory +############################################################################## + + +class TreeFactory(rclpy.node.Node): + def __init__(self): + + super().__init__("Factory") + + def create_tree_from_file(self, tree_path, timeout=1000): + + # Open the self contained xml file + self_file = open(tree_path, "r") + self_contained_tree = self_file.read() + xml_doc = ET.fromstring(self_contained_tree) + + # Add actions to factory + add_actions_to_factory(xml_doc) + + # Init tree + root = construct_behaviour_tree_from_xml(xml_doc) + self.tree = py_trees_ros.trees.BehaviourTree( + root=root, unicode_tree_debug=False + ) + + # Setup tree + try: + self.tree.setup(timeout=timeout) + except py_trees_ros.exceptions.TimedOutError as e: + console.logerror(console.red + "Failed to setup tree" + console.reset) + self.tree.shutdown() + return None + except KeyboardInterrupt: + # not a warning, nor error, usually a user-initiated shutdown + console.logerror("Tree setup interrupted") + self.tree.shutdown() + return None + + return self.tree +` + +const packageInfo = +` + + tree_gardener + 0.2.0 + BT execution engine for BT Studio + Óscar Martínez + GPLv3 + + rclpy + py_trees_ros + python3-pep8 + + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + +` + +const setupConfig = +`[develop] +script_dir=$base/lib/tree_gardener +[install] +install_scripts=$base/lib/tree_gardener +` + +const setupPython = +`from setuptools import setup +from setuptools import find_packages + +package_name = "tree_gardener" + +setup( + name=package_name, + version="0.2.0", + packages=find_packages(exclude=["test"]), + data_files=[ + ("share/ament_index/resource_index/packages", ["resource/" + package_name]), + ("share/" + package_name, ["package.xml"]), + ], + install_requires=["setuptools"], + zip_safe=True, + author="Óscar Martínez", + author_email="oscar.robotics@tutanota.com", + maintainer="Óscar Martínez", + maintainer_email="oscar.robotics@tutanota.com", + keywords=["ROS2", "py_trees"], + classifiers=[ + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Topic :: Software Development", + ], + description="BT execution engine for BT Studio", + license="GPLv3", + tests_require=["pytest"], +) + +` + + +namespace TreeGardener { + export function addDockerFiles(zip:JSZip ) { + zip.file("tree_factory.py", treeFactory); + zip.file("tree_tools.py", treeTools); + } + + export function addLocalFiles(zip:JSZip ) { + const loc = zip.folder("tree_gardener"); + + if (loc == null) { + return + } + + loc.file("resource/tree_gardener", ""); + + loc.file("tree_gardener/tree_factory.py", treeFactory); + loc.file("tree_gardener/tree_tools.py", treeTools); + loc.file("tree_gardener/__init__.py", ""); + + loc.file("package.xml", packageInfo); + loc.file("setup.cfg", setupConfig); + loc.file("setup.py", setupPython); + } +} + +export default TreeGardener; \ No newline at end of file From a38fb9926c0ec10e816d505b4b788922fe0dffb2 Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Tue, 7 Jan 2025 16:17:06 +0100 Subject: [PATCH 08/31] Linter --- backend/tree_api/app_generator.py | 1 + backend/tree_api/json_translator.py | 1 + backend/tree_api/tree_generator.py | 9 ++++++-- backend/tree_api/views.py | 10 ++++++-- .../src/components/header_menu/HeaderMenu.tsx | 23 +++++++++++-------- 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/backend/tree_api/app_generator.py b/backend/tree_api/app_generator.py index 7a7d9e843..2e4898884 100644 --- a/backend/tree_api/app_generator.py +++ b/backend/tree_api/app_generator.py @@ -9,6 +9,7 @@ # Helper functions ############################################################################## + # Collect unique imports from files def get_unique_imports(actions): diff --git a/backend/tree_api/json_translator.py b/backend/tree_api/json_translator.py index 6c5b4810f..96ab09d55 100644 --- a/backend/tree_api/json_translator.py +++ b/backend/tree_api/json_translator.py @@ -195,6 +195,7 @@ def translate_raw(content, raw_order): xml_string = prettify_xml(root) return xml_string + def translate_tree_structure(content, raw_order): # Parse the JSON data parsed_json = content diff --git a/backend/tree_api/tree_generator.py b/backend/tree_api/tree_generator.py index 88e1e940c..1c43abaa9 100644 --- a/backend/tree_api/tree_generator.py +++ b/backend/tree_api/tree_generator.py @@ -81,6 +81,7 @@ def get_subtree_set(tree, possible_subtrees) -> set: return subtrees + # Add the code of the different actions def add_actions_code_(tree, actions, all_actions): @@ -101,6 +102,7 @@ def add_actions_code_(tree, actions, all_actions): action_section = ET.SubElement(code_section, action_name) action_section.text = "\n" + action_code + "\n" + # Replaces all the subtrees in a given tree depth def replace_subtrees_in_tree_(tree, subtrees): @@ -129,6 +131,7 @@ def replace_subtrees_in_tree_(tree, subtrees): # Remove the original subtree tag parent.remove(subtree_tag) + # Recursively replace all subtrees in a given tree def replace_all_subtrees_(tree, all_subtrees, depth=0, max_depth=15): @@ -137,7 +140,7 @@ def replace_all_subtrees_(tree, all_subtrees, depth=0, max_depth=15): return # Get the subtrees that are present in the tree - possible_trees = [x["name"] for x in all_subtrees] + possible_trees = [x["name"] for x in all_subtrees] subtrees = get_subtree_set(tree, possible_trees) # If no subtrees are found, stop the recursion @@ -150,6 +153,7 @@ def replace_all_subtrees_(tree, all_subtrees, depth=0, max_depth=15): # Recursively call the function to replace subtrees in the newly added subtrees replace_all_subtrees_(tree, all_subtrees, depth + 1, max_depth) + def parse_tree_(tree_xml, subtrees, all_actions): # Parse the tree file tree = get_bt_structure(tree_xml) @@ -158,7 +162,7 @@ def parse_tree_(tree_xml, subtrees, all_actions): replace_all_subtrees_(tree, subtrees) # Obtain the defined actions - possible_actions = [x["name"] for x in all_actions] + possible_actions = [x["name"] for x in all_actions] actions = get_action_set(tree, possible_actions) # Add subsections for the action code @@ -170,6 +174,7 @@ def parse_tree_(tree_xml, subtrees, all_actions): return formatted_tree + ############################################################################## # Main section ############################################################################## diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index ab4d4d720..fc9a2867d 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -657,7 +657,9 @@ def create_action(request): if not os.path.exists(file_path): try: with open(file_path, "w") as f: - f.write(templates.get_action_template(filename, template, template_path)) + f.write( + templates.get_action_template(filename, template, template_path) + ) return Response({"success": True}) except: return Response( @@ -893,6 +895,7 @@ def download_data(request): else: return Response({"error": "app_name parameter is missing"}, status=500) + @api_view(["GET"]) def generate_local_app(request): @@ -948,7 +951,9 @@ def generate_local_app(request): unique_imports = app_generator.get_unique_imports(actions) - return JsonResponse({"success": True, "tree": final_tree, "dependencies": unique_imports}) + return JsonResponse( + {"success": True, "tree": final_tree, "dependencies": unique_imports} + ) except Exception as e: print(e) @@ -960,6 +965,7 @@ def generate_local_app(request): status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) + @api_view(["GET"]) def generate_dockerized_app(request): diff --git a/frontend/src/components/header_menu/HeaderMenu.tsx b/frontend/src/components/header_menu/HeaderMenu.tsx index b33101cdc..e910d7aef 100644 --- a/frontend/src/components/header_menu/HeaderMenu.tsx +++ b/frontend/src/components/header_menu/HeaderMenu.tsx @@ -30,8 +30,8 @@ import UniversesModal from "./modals/UniverseModal"; import SettingsModal from "../settings_popup/SettingsModal"; import { OptionsContext } from "../options/Options"; -import RosTemplates from './../../templates/RosTemplates'; -import TreeGardener from './../../templates/TreeGardener'; +import RosTemplates from "./../../templates/RosTemplates"; +import TreeGardener from "./../../templates/TreeGardener"; const HeaderMenu = ({ currentProjectname, @@ -207,12 +207,17 @@ const HeaderMenu = ({ // Create the zip with the files const zip = new JSZip(); - console.log(appFiles.dependencies) + console.log(appFiles.dependencies); - TreeGardener.addLocalFiles(zip) - RosTemplates.addLocalFiles(zip, currentProjectname, appFiles.tree, appFiles.dependencies) + TreeGardener.addLocalFiles(zip); + RosTemplates.addLocalFiles( + zip, + currentProjectname, + appFiles.tree, + appFiles.dependencies, + ); - zip.generateAsync({type:"blob"}).then(function(content) { + zip.generateAsync({ type: "blob" }).then(function (content) { // Create a download link and trigger download const url = window.URL.createObjectURL(content); const a = document.createElement("a"); @@ -256,8 +261,8 @@ const HeaderMenu = ({ const zip = new JSZip(); zip.file("self_contained_tree.xml", appFiles.tree); - TreeGardener.addDockerFiles(zip) - RosTemplates.addDockerFiles(zip) + TreeGardener.addDockerFiles(zip); + RosTemplates.addDockerFiles(zip); // Convert the blob to base64 using FileReader const reader = new FileReader(); @@ -271,7 +276,7 @@ const HeaderMenu = ({ console.log("Dockerized app started successfully"); }; - zip.generateAsync({type:"blob"}).then(function(content) { + zip.generateAsync({ type: "blob" }).then(function (content) { reader.readAsDataURL(content); }); From 0b7acf389ccf39a56542d4666bb397ca0fcb3c37 Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Tue, 7 Jan 2025 16:35:33 +0100 Subject: [PATCH 09/31] Remove useless function --- backend/tree_api/urls.py | 1 - backend/tree_api/views.py | 22 ---------------------- 2 files changed, 23 deletions(-) diff --git a/backend/tree_api/urls.py b/backend/tree_api/urls.py index bc959f77f..8f2678d8c 100644 --- a/backend/tree_api/urls.py +++ b/backend/tree_api/urls.py @@ -58,7 +58,6 @@ path("get_actions_list/", views.get_actions_list, name="get_actions_list"), path("create_action/", views.create_action, name="create_action"), # Other - path("translate_json/", views.translate_json, name="translate_json"), path("generate_local_app/", views.generate_local_app, name="generate_local_app"), path( "generate_dockerized_app/", diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index fc9a2867d..013b03303 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -814,28 +814,6 @@ def save_file(request): return Response({"success": False, "message": str(e)}, status=400) -@api_view(["POST"]) -def translate_json(request): - - folder_path = os.path.join(settings.BASE_DIR, "filesystem") - bt_order = request.data.get("bt_order") - - try: - content = request.data.get("content") - if content is None: - return Response( - {"success": False, "message": "Content is missing"}, status=400 - ) - - # Pass the JSON content to the translate function - with open(folder_path + "/tree.xml", "w") as f: - f.write(json_translator.translate_raw(content, bt_order)) - - return Response({"success": True}) - except Exception as e: - return Response({"success": False, "message": str(e)}, status=400) - - @api_view(["POST"]) def download_data(request): From 068edc659166c5b2dbb875bd59797a2cc4f96637 Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Tue, 7 Jan 2025 18:31:05 +0100 Subject: [PATCH 10/31] Remove template from file --- backend/tree_api/views.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index 013b03303..9315ed804 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -687,12 +687,9 @@ def create_file(request): file_path = os.path.join(create_path, filename) if not os.path.exists(file_path): - if templates.create_action_from_template(file_path, filename, "empty"): - return Response({"success": True}) - else: - return Response( - {"success": False, "message": "Template does not exist"}, status=400 - ) + with open(file_path, "w") as f: + f.write("") + return Response({"success": True}) else: return Response( {"success": False, "message": "File already exists"}, status=400 From 7a4564fa498a8e38a796fdd37a8ed421a0db9de8 Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Tue, 7 Jan 2025 18:56:32 +0100 Subject: [PATCH 11/31] Fix file creation bug --- .../file_browser/modals/NewFileModal.jsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/file_browser/modals/NewFileModal.jsx b/frontend/src/components/file_browser/modals/NewFileModal.jsx index 1d0734cb3..661d12c95 100644 --- a/frontend/src/components/file_browser/modals/NewFileModal.jsx +++ b/frontend/src/components/file_browser/modals/NewFileModal.jsx @@ -81,9 +81,7 @@ const NewFileModal = ({ onSubmit, isOpen, onClose, fileList, location }) => { if (isOpen) { //NOTE: One for actions and one for location - if (location) { - createValidNamesList(location, setSearchPlainList); - } + createValidNamesList(location, setSearchPlainList); createValidNamesList("actions", setSearchActionsList); } }, [isOpen]); @@ -93,13 +91,16 @@ const NewFileModal = ({ onSubmit, isOpen, onClose, fileList, location }) => { }, [creationType]); const createValidNamesList = (orig_path, callback) => { - var path = orig_path.split("/"); let search_list = fileList; - for (let index = 0; index < path.length; index++) { - search_list = search_list.find( - (entry) => entry.name === path[index] && entry.is_dir, - ).files; + if (orig_path) { + var path = orig_path.split("/"); + + for (let index = 0; index < path.length; index++) { + search_list = search_list.find( + (entry) => entry.name === path[index] && entry.is_dir, + ).files; + } } console.log(search_list); From 595de190dec79f696bda771e390cde850e044048 Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Tue, 7 Jan 2025 19:10:36 +0100 Subject: [PATCH 12/31] Fix bug when creating folder --- .../file_browser/modals/NewFolderModal.jsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/file_browser/modals/NewFolderModal.jsx b/frontend/src/components/file_browser/modals/NewFolderModal.jsx index e91caaea9..452864f94 100644 --- a/frontend/src/components/file_browser/modals/NewFolderModal.jsx +++ b/frontend/src/components/file_browser/modals/NewFolderModal.jsx @@ -20,17 +20,19 @@ const NewFolderModal = ({ onSubmit, isOpen, onClose, fileList, location }) => { }, 0); } - if (isOpen && location) { - var path = location.split("/"); - + if (isOpen) { let search_list = fileList; - for (let index = 0; index < path.length; index++) { - search_list = search_list.find( - (entry) => entry.name === path[index] && entry.is_dir, - ).files; - } + if (location) { + var path = location.split("/"); + for (let index = 0; index < path.length; index++) { + search_list = search_list.find( + (entry) => entry.name === path[index] && entry.is_dir, + ).files; + } + } + if (search_list) { setSearchList(search_list); } else { From f5e0d94cc37dba59456d050293d2559190082567 Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Wed, 8 Jan 2025 10:37:22 +0100 Subject: [PATCH 13/31] Separate delete into 2 --- backend/tree_api/urls.py | 1 + backend/tree_api/views.py | 34 ++++++++++++++++--- .../components/file_browser/FileBrowser.js | 20 ++++++++--- .../file_explorer/MoreActionsMenu.jsx | 2 +- .../file_browser/modals/NewFolderModal.jsx | 2 +- 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/backend/tree_api/urls.py b/backend/tree_api/urls.py index 8f2678d8c..6a814eff3 100644 --- a/backend/tree_api/urls.py +++ b/backend/tree_api/urls.py @@ -52,6 +52,7 @@ path("create_folder/", views.create_folder, name="create_folder"), path("rename_file/", views.rename_file, name="rename_file"), path("delete_file/", views.delete_file, name="delete_file"), + path("delete_folder/", views.delete_folder, name="delete_folder"), path("save_file/", views.save_file, name="save_file"), path("upload_code/", views.upload_code, name="upload_code"), # Actions Management diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index 9315ed804..0a517dccf 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -767,12 +767,36 @@ def delete_file(request): action_path = os.path.join(project_path, "code") file_path = os.path.join(action_path, path) - if os.path.exists(file_path): + if os.path.exists(file_path) and not os.path.isdir(file_path): try: - if os.path.isdir(file_path): - shutil.rmtree(file_path) - else: - os.remove(file_path) + os.remove(file_path) + return JsonResponse({"success": True}) + except Exception as e: + return JsonResponse( + {"success": False, "message": f"Error deleting file: {str(e)}"}, + status=500, + ) + else: + return JsonResponse( + {"success": False, "message": "File does not exist"}, status=404 + ) + +@api_view(["GET"]) +def delete_folder(request): + + # Get the folder info + project_name = request.GET.get("project_name", None) + path = request.GET.get("path", None) + + # Make folder path relative to Django app + folder_path = os.path.join(settings.BASE_DIR, "filesystem") + project_path = os.path.join(folder_path, project_name) + action_path = os.path.join(project_path, "code") + file_path = os.path.join(action_path, path) + + if os.path.exists(file_path) and os.path.isdir(file_path): + try: + shutil.rmtree(file_path) return JsonResponse({"success": True}) except Exception as e: return JsonResponse( diff --git a/frontend/src/components/file_browser/FileBrowser.js b/frontend/src/components/file_browser/FileBrowser.js index 9691333ed..765b4c6f2 100644 --- a/frontend/src/components/file_browser/FileBrowser.js +++ b/frontend/src/components/file_browser/FileBrowser.js @@ -41,6 +41,7 @@ const FileBrowser = ({ const [isUploadModalOpen, setUploadModalOpen] = useState(false); const [selectedEntry, setSelectedEntry] = useState(null); const [deleteEntry, setDeleteEntry] = useState(null); + const [deleteType, setDeleteType] = useState(false); const [renameEntry, setRenameEntry] = useState(null); const [selectedLocation, setSelectedLocation] = useState(""); @@ -123,9 +124,10 @@ const FileBrowser = ({ ///////////////// DELETE FILES AND FOLDERS /////////////////////////////////// - const handleDeleteModal = (file_path) => { + const handleDeleteModal = (file_path, is_dir) => { if (file_path) { setDeleteEntry(file_path); + setDeleteType(is_dir); setDeleteModalOpen(true); } else { alert("No file is currently selected."); @@ -135,15 +137,23 @@ const FileBrowser = ({ const handleCloseDeleteModal = () => { setDeleteModalOpen(false); setDeleteEntry(""); + setDeleteType(false); }; const handleSubmitDeleteModal = async () => { //currentFilename === Absolute File path if (deleteEntry) { try { - const response = await axios.get( - `/bt_studio/delete_file?project_name=${currentProjectname}&path=${deleteEntry}`, - ); + var response; + if (deleteType) { + response = await axios.get( + `/bt_studio/delete_folder?project_name=${currentProjectname}&path=${deleteEntry}`, + ); + } else { + response = await axios.get( + `/bt_studio/delete_file?project_name=${currentProjectname}&path=${deleteEntry}`, + ); + } if (response.data.success) { setProjectChanges(true); fetchFileList(); // Update the file list @@ -168,7 +178,7 @@ const FileBrowser = ({ const handleDeleteCurrentFile = () => { //currentFilename === Absolute File path if (currentFilename) { - handleDeleteModal(currentFilename); + handleDeleteModal(currentFilename, false); } else { alert("No file is currently selected."); } diff --git a/frontend/src/components/file_browser/file_explorer/MoreActionsMenu.jsx b/frontend/src/components/file_browser/file_explorer/MoreActionsMenu.jsx index f5ae1d4bd..3f756d6d4 100644 --- a/frontend/src/components/file_browser/file_explorer/MoreActionsMenu.jsx +++ b/frontend/src/components/file_browser/file_explorer/MoreActionsMenu.jsx @@ -65,7 +65,7 @@ function MoreActionsMenu({
{ - onDelete(menuProps.file.path); + onDelete(menuProps.file.path, menuProps.file.is_dir); closeMenu(); }} > diff --git a/frontend/src/components/file_browser/modals/NewFolderModal.jsx b/frontend/src/components/file_browser/modals/NewFolderModal.jsx index 452864f94..29fc71118 100644 --- a/frontend/src/components/file_browser/modals/NewFolderModal.jsx +++ b/frontend/src/components/file_browser/modals/NewFolderModal.jsx @@ -32,7 +32,7 @@ const NewFolderModal = ({ onSubmit, isOpen, onClose, fileList, location }) => { ).files; } } - + if (search_list) { setSearchList(search_list); } else { From 6d27fecde5ab5f11a666823a4f9fa170ed62ce94 Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Wed, 8 Jan 2025 12:50:40 +0100 Subject: [PATCH 14/31] Fix and separate rename file and folder --- backend/tree_api/urls.py | 1 + backend/tree_api/views.py | 33 ++++++++++++++++++- .../components/file_browser/FileBrowser.js | 24 +++++++++----- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/backend/tree_api/urls.py b/backend/tree_api/urls.py index 6a814eff3..c63fe1b06 100644 --- a/backend/tree_api/urls.py +++ b/backend/tree_api/urls.py @@ -51,6 +51,7 @@ path("create_file/", views.create_file, name="create_file"), path("create_folder/", views.create_folder, name="create_folder"), path("rename_file/", views.rename_file, name="rename_file"), + path("rename_folder/", views.rename_folder, name="rename_folder"), path("delete_file/", views.delete_file, name="delete_file"), path("delete_folder/", views.delete_folder, name="delete_folder"), path("save_file/", views.save_file, name="save_file"), diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index 0a517dccf..d694ed1cf 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -754,6 +754,36 @@ def rename_file(request): ) +@api_view(["GET"]) +def rename_folder(request): + + # Get the folder info + project_name = request.GET.get("project_name", None) + path = request.GET.get("path", None) + rename_path = request.GET.get("rename_to", None) + + # Make folder path relative to Django app + folder_path = os.path.join(settings.BASE_DIR, "filesystem") + project_path = os.path.join(folder_path, project_name) + action_path = os.path.join(project_path, "code") + file_path = os.path.join(action_path, path) + new_path = os.path.join(action_path, rename_path) + + if os.path.exists(file_path): + try: + os.rename(file_path, new_path) + return JsonResponse({"success": True}) + except Exception as e: + return JsonResponse( + {"success": False, "message": f"Error deleting file: {str(e)}"}, + status=500, + ) + else: + return JsonResponse( + {"success": False, "message": "File does not exist"}, status=404 + ) + + @api_view(["GET"]) def delete_file(request): @@ -780,7 +810,8 @@ def delete_file(request): return JsonResponse( {"success": False, "message": "File does not exist"}, status=404 ) - + + @api_view(["GET"]) def delete_folder(request): diff --git a/frontend/src/components/file_browser/FileBrowser.js b/frontend/src/components/file_browser/FileBrowser.js index 765b4c6f2..273613751 100644 --- a/frontend/src/components/file_browser/FileBrowser.js +++ b/frontend/src/components/file_browser/FileBrowser.js @@ -20,7 +20,7 @@ function getParentDir(file) { return file.path; } - var split_path = file.path.split("/"); // TODO: add for windows + var split_path = file.path.split("/"); return split_path.slice(0, split_path.length - 1).join("/"); } @@ -232,14 +232,21 @@ const FileBrowser = ({ const handleSubmitRenameModal = async (new_path) => { if (renameEntry) { try { - const response = await axios.get( - `/bt_studio/rename_file?project_name=${currentProjectname}&path=${renameEntry.path}&rename_to=${new_path}`, - ); + var response; + console.log(renameEntry) + if (renameEntry.is_dir) { + response = await axios.get( + `/bt_studio/rename_folder?project_name=${currentProjectname}&path=${renameEntry.path}&rename_to=${new_path}`, + ); + } else { + response = await axios.get( + `/bt_studio/rename_file?project_name=${currentProjectname}&path=${renameEntry.path}&rename_to=${new_path}`, + ); + } if (response.data.success) { setProjectChanges(true); fetchFileList(); // Update the file list - //TODO: if is a file what was renamed may need to change the path - if (currentFilename === renameEntry.name) { + if (currentFilename === renameEntry.path) { setCurrentFilename(new_path); // Unset the current file } } else { @@ -255,10 +262,9 @@ const FileBrowser = ({ }; const handleRenameCurrentFile = () => { - //TODO: need to obtain all file data to do this - return; if (currentFilename) { - handleRename(currentFilename); + const name = currentFilename.substring(currentFilename.lastIndexOf('/') + 1) + handleRename({is_dir: false, name:name , path: currentFilename, files: []}); } else { alert("No file is currently selected."); } From 5ed660da50d9a90890673a35db01858cc2ab199f Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Wed, 8 Jan 2025 13:53:57 +0100 Subject: [PATCH 15/31] Fix autosave and save on rename --- frontend/src/App.tsx | 8 ++ frontend/src/api_helper/TreeWrapper.ts | 33 +++++++ .../components/file_browser/FileBrowser.js | 28 +++++- .../src/components/file_editor/FileEditor.tsx | 92 +++++++++---------- 4 files changed, 106 insertions(+), 55 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1e0fe6bce..9035a3ed7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21,6 +21,8 @@ const App = ({ isUnibotics }: { isUnibotics: boolean }) => { const [fileBrowserWidth, setFileBrowserWidth] = useState(300); const [editorWidth, setEditorWidth] = useState(800); const [currentFilename, setCurrentFilename] = useState(""); + const [autosaveEnabled, setAutosave] = useState(true); + const [forceSaveCurrent, setForcedSaveCurrent] = useState(false); const [currentProjectname, setCurrentProjectname] = useState(""); const [currentUniverseName, setCurrentUniverseName] = useState(""); const [actionNodesData, setActionNodesData] = useState>( @@ -191,6 +193,9 @@ const App = ({ isUnibotics }: { isUnibotics: boolean }) => { actionNodesData={actionNodesData} showAccentColor={"editorShowAccentColors"} diagramEditorReady={diagramEditorReady} + setAutosave={setAutosave} + forceSaveCurrent={forceSaveCurrent} + setForcedSaveCurrent={setForcedSaveCurrent} />
@@ -217,6 +222,9 @@ const App = ({ isUnibotics }: { isUnibotics: boolean }) => { currentProjectname={currentProjectname} setProjectChanges={setProjectChanges} isUnibotics={isUnibotics} + autosaveEnabled={autosaveEnabled} + setAutosave={setAutosave} + forceSaveCurrent={forceSaveCurrent} /> {showTerminal && } diff --git a/frontend/src/api_helper/TreeWrapper.ts b/frontend/src/api_helper/TreeWrapper.ts index f944c40e4..5b3df6a21 100644 --- a/frontend/src/api_helper/TreeWrapper.ts +++ b/frontend/src/api_helper/TreeWrapper.ts @@ -47,6 +47,38 @@ const getActionsList = async (projectName: string) => { } }; +const saveFile = async (projectName: string, fileName: string, content: string) => { + if (!projectName) throw new Error("Current Project name is not set"); + if (!fileName) throw new Error("Current File name is not set"); + if (!content) throw new Error("Content does not exist"); + + const apiUrl = "/bt_studio/save_file/"; + + try { + const response = await axios.post( + apiUrl, + { + project_name: projectName, + filename: fileName, + content: content, + }, + { + headers: { + //@ts-ignore Needed for compatibility with Unibotics + "X-CSRFToken": context.csrf, + }, + }, + ); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to create project."); // Response error + } + } catch (error) { + throw error; // Rethrow + } +}; + // Project management const createProject = async (projectName: string) => { @@ -442,6 +474,7 @@ const saveSubtree = async ( export { createProject, saveBaseTree, + saveFile, loadProjectConfig, getProjectGraph, generateLocalApp, diff --git a/frontend/src/components/file_browser/FileBrowser.js b/frontend/src/components/file_browser/FileBrowser.js index 273613751..9dec779aa 100644 --- a/frontend/src/components/file_browser/FileBrowser.js +++ b/frontend/src/components/file_browser/FileBrowser.js @@ -32,6 +32,9 @@ const FileBrowser = ({ actionNodesData, showAccentColor, diagramEditorReady, + setAutosave, + forceSaveCurrent, + setForcedSaveCurrent, }) => { const [fileList, setFileList] = useState(null); const [isNewFileModalOpen, setNewFileModalOpen] = useState(false); @@ -179,6 +182,7 @@ const FileBrowser = ({ //currentFilename === Absolute File path if (currentFilename) { handleDeleteModal(currentFilename, false); + setAutosave(false); } else { alert("No file is currently selected."); } @@ -223,6 +227,10 @@ const FileBrowser = ({ } else { alert("No file is currently selected."); } + + if (currentFilename === file.path) { + setForcedSaveCurrent(!forceSaveCurrent); + } }; const handleCloseRenameModal = () => { @@ -233,11 +241,11 @@ const FileBrowser = ({ if (renameEntry) { try { var response; - console.log(renameEntry) + console.log(renameEntry); if (renameEntry.is_dir) { response = await axios.get( `/bt_studio/rename_folder?project_name=${currentProjectname}&path=${renameEntry.path}&rename_to=${new_path}`, - ); + ); } else { response = await axios.get( `/bt_studio/rename_file?project_name=${currentProjectname}&path=${renameEntry.path}&rename_to=${new_path}`, @@ -247,6 +255,7 @@ const FileBrowser = ({ setProjectChanges(true); fetchFileList(); // Update the file list if (currentFilename === renameEntry.path) { + setAutosave(false); setCurrentFilename(new_path); // Unset the current file } } else { @@ -261,10 +270,19 @@ const FileBrowser = ({ handleCloseRenameModal(); }; - const handleRenameCurrentFile = () => { + const handleRenameCurrentFile = async () => { if (currentFilename) { - const name = currentFilename.substring(currentFilename.lastIndexOf('/') + 1) - handleRename({is_dir: false, name:name , path: currentFilename, files: []}); + setForcedSaveCurrent(!forceSaveCurrent); + const name = currentFilename.substring( + currentFilename.lastIndexOf("/") + 1, + ); + handleRename({ + is_dir: false, + name: name, + path: currentFilename, + files: [], + }); + setAutosave(false); } else { alert("No file is currently selected."); } diff --git a/frontend/src/components/file_editor/FileEditor.tsx b/frontend/src/components/file_editor/FileEditor.tsx index 880af95aa..359c37707 100644 --- a/frontend/src/components/file_editor/FileEditor.tsx +++ b/frontend/src/components/file_editor/FileEditor.tsx @@ -8,17 +8,24 @@ import "./FileEditor.css"; import { ReactComponent as SaveIcon } from "./img/save.svg"; import { ReactComponent as SplashIcon } from "./img/logo_jderobot_monocolor.svg"; import { ReactComponent as SplashIconUnibotics } from "./img/logo_unibotics_monocolor.svg"; +import { saveFile } from "../../api_helper/TreeWrapper"; const FileEditor = ({ currentFilename, currentProjectname, setProjectChanges, isUnibotics, + autosaveEnabled, + setAutosave, + forceSaveCurrent, }: { - currentFilename: any; - currentProjectname: any; - setProjectChanges: any; + currentFilename: string; + currentProjectname: string; + setProjectChanges: Function; isUnibotics: boolean; + autosaveEnabled: boolean; + setAutosave: Function; + forceSaveCurrent: boolean; }) => { const [fileContent, setFileContent] = useState(null); const [fontSize, setFontSize] = useState(14); @@ -41,27 +48,17 @@ const FileEditor = ({ const autoSave = async () => { console.log("Auto saving file..."); + + if (fileContent == null) { + console.log("No content to save"); + return; + } + try { - const response = await axios.post( - "/bt_studio/save_file/", - { - project_name: currentProjectname, - filename: filenameToSave, - content: fileContent, - }, - { - headers: { - //@ts-ignore Needed for compatibility with Unibotics - "X-CSRFToken": context.csrf, - }, - }, - ); - if (response.data.success) { - setHasUnsavedChanges(false); // Reset the unsaved changes flag - setProjectChanges(false); - } else { - alert(`Failed to save file: ${response.data.message}`); - } + await saveFile(currentProjectname, filenameToSave, fileContent); + setHasUnsavedChanges(false); // Reset the unsaved changes flag + setProjectChanges(false); + console.log("Auto save completed"); } catch (error) { console.error("Error saving file:", error); } @@ -70,9 +67,10 @@ const FileEditor = ({ useEffect(() => { if (currentFilename != "") { initFile(); - if (filenameToSave) { + if (filenameToSave && autosaveEnabled) { autoSave(); } + setAutosave(true); setFilenameToSave(currentFilename); } else { setFileContent(null); @@ -89,34 +87,28 @@ const FileEditor = ({ setFileContent(null); }, [currentProjectname]); + useEffect(() => { + handleSaveFile(); + }, [forceSaveCurrent]); + const handleSaveFile = async () => { - if (currentFilename !== "") { - try { - const response = await axios.post( - "/bt_studio/save_file/", - { - project_name: projectToSave, - filename: currentFilename, - content: fileContent, - }, - { - headers: { - //@ts-ignore Needed for compatibility with Unibotics - "X-CSRFToken": context.csrf, - }, - }, - ); - if (response.data.success) { - setHasUnsavedChanges(false); // Reset the unsaved changes flag - setProjectChanges(false); - } else { - alert(`Failed to save file: ${response.data.message}`); - } - } catch (error) { - console.error("Error saving file:", error); - } - } else { + if (fileContent === null) { + console.log("No content to save"); + return; + } + + if (currentFilename === "") { + console.log("No file is currently selected"); alert("No file is currently selected."); + return; + } + + try { + await saveFile(projectToSave, currentFilename, fileContent); + setHasUnsavedChanges(false); // Reset the unsaved changes flag + setProjectChanges(false); + } catch (error) { + console.error("Error saving file:", error); } }; From fce79a43e499256a63ec3b4804916de5e02de9ae Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Wed, 8 Jan 2025 16:32:41 +0100 Subject: [PATCH 16/31] Rename save project config --- backend/tree_api/urls.py | 6 +++--- backend/tree_api/views.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/tree_api/urls.py b/backend/tree_api/urls.py index c63fe1b06..71dce4e82 100644 --- a/backend/tree_api/urls.py +++ b/backend/tree_api/urls.py @@ -37,9 +37,9 @@ name="get_subtree_structure", ), path( - "save_base_tree_configuration/", - views.save_base_tree_configuration, - name="save_base_tree_configuration", + "save_project_configuration/", + views.save_project_configuration, + name="save_project_configuration", ), path("get_subtree_list/", views.get_subtree_list, name="get_subtree_list"), path("create_subtree/", views.create_subtree, name="create_subtree"), diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index d694ed1cf..79637f229 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -263,16 +263,16 @@ def get_subtree_structure(request): @api_view(["POST"]) -def save_base_tree_configuration(request): +def save_project_configuration(request): project_name = request.data.get("project_name") + content = request.data.get("settings") folder_path = os.path.join(settings.BASE_DIR, "filesystem") project_path = os.path.join(folder_path, project_name) config_path = os.path.join(project_path, "config.json") try: - content = request.data.get("settings") if content is None: return Response( {"success": False, "message": "Settings are missing"}, status=400 From 6605b1fc90546080ce918b800cf38da5368e8d53 Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Wed, 8 Jan 2025 16:33:43 +0100 Subject: [PATCH 17/31] Fix saving proj config --- frontend/src/api_helper/TreeWrapper.ts | 30 +++++++++++++++++++ .../settings_popup/SettingsModal.jsx | 23 ++++---------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/frontend/src/api_helper/TreeWrapper.ts b/frontend/src/api_helper/TreeWrapper.ts index 5b3df6a21..26d4c872a 100644 --- a/frontend/src/api_helper/TreeWrapper.ts +++ b/frontend/src/api_helper/TreeWrapper.ts @@ -166,6 +166,35 @@ const loadProjectConfig = async ( } }; +const saveProjectConfig = async (currentProjectname: string, settings:string) => { + if (!currentProjectname) throw new Error("Current Project name is not set"); + if (!settings) throw new Error("Settings content is null"); + + const apiUrl = "/bt_studio/save_project_configuration/"; + try { + const response = await axios.post( + apiUrl, + { + project_name: currentProjectname, + settings: settings, + }, + { + headers: { + //@ts-ignore Needed for compatibility with Unibotics + "X-CSRFToken": context.csrf, + }, + } + ); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to create project."); // Response error + } + } catch (error: unknown) { + throw error; // Rethrow + } +}; + const getProjectGraph = async (currentProjectname: string) => { if (!currentProjectname) throw new Error("Current Project name is not set"); @@ -489,4 +518,5 @@ export { getActionsList, saveSubtree, createRoboticsBackendUniverse, + saveProjectConfig, }; diff --git a/frontend/src/components/settings_popup/SettingsModal.jsx b/frontend/src/components/settings_popup/SettingsModal.jsx index 80f41074a..e6407ca45 100644 --- a/frontend/src/components/settings_popup/SettingsModal.jsx +++ b/frontend/src/components/settings_popup/SettingsModal.jsx @@ -14,6 +14,8 @@ import Checkbox from "./options/Checkbox"; import { OptionsContext } from "../options/Options"; +import {saveProjectConfig} from "./../../api_helper/TreeWrapper"; + const SettingsModal = ({ onSubmit, isOpen, onClose, currentProjectname }) => { const [color, setColor] = useColor("rgb(128 0 128)"); const [open, setOpen] = useState(false); @@ -29,7 +31,7 @@ const SettingsModal = ({ onSubmit, isOpen, onClose, currentProjectname }) => { // document.documentElement.style.setProperty("--header", "rgb("+Math.round(color.rgb.r)+","+Math.round(color.rgb.g)+","+Math.round(color.rgb.b)+")"); // }, [color]); - const handleCancel = async () => { + const handleCancel = async (settings) => { // Save settings let json_settings = { name: currentProjectname, config: {} }; @@ -37,21 +39,8 @@ const SettingsModal = ({ onSubmit, isOpen, onClose, currentProjectname }) => { json_settings.config[key] = setting.value; }); - const str = JSON.stringify(json_settings); - - console.log(str); - try { - const response = await fetch("/bt_studio/save_base_tree_configuration/", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - project_name: currentProjectname, - settings: str, - }), - }); + await saveProjectConfig(currentProjectname, JSON.stringify(json_settings)); } catch (error) { console.error("Error saving config:", error); } @@ -68,7 +57,7 @@ const SettingsModal = ({ onSubmit, isOpen, onClose, currentProjectname }) => { >
{handleCancel(settings)}} style={{ display: "flex", flexDirection: "column", flexGrow: "1" }} >
@@ -82,7 +71,7 @@ const SettingsModal = ({ onSubmit, isOpen, onClose, currentProjectname }) => { { - handleCancel(); + handleCancel(settings); }} fill={"var(--icon)"} /> From 9488df6ee50c8d78744b09c0f595edb09a67190e Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Wed, 8 Jan 2025 18:47:39 +0100 Subject: [PATCH 18/31] Save bt before executing --- frontend/src/components/header_menu/HeaderMenu.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/components/header_menu/HeaderMenu.tsx b/frontend/src/components/header_menu/HeaderMenu.tsx index e910d7aef..f9cde5c1f 100644 --- a/frontend/src/components/header_menu/HeaderMenu.tsx +++ b/frontend/src/components/header_menu/HeaderMenu.tsx @@ -248,6 +248,8 @@ const HeaderMenu = ({ return; } + await onSaveProject(); + if (!appRunning) { try { // Get the blob from the API wrapper From 956b49475e6046ee40acc59dbe519711f446b880 Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Wed, 8 Jan 2025 18:52:28 +0100 Subject: [PATCH 19/31] Linter --- .../src/components/settings_popup/SettingsModal.jsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/settings_popup/SettingsModal.jsx b/frontend/src/components/settings_popup/SettingsModal.jsx index e6407ca45..8d92ebd77 100644 --- a/frontend/src/components/settings_popup/SettingsModal.jsx +++ b/frontend/src/components/settings_popup/SettingsModal.jsx @@ -14,7 +14,7 @@ import Checkbox from "./options/Checkbox"; import { OptionsContext } from "../options/Options"; -import {saveProjectConfig} from "./../../api_helper/TreeWrapper"; +import { saveProjectConfig } from "./../../api_helper/TreeWrapper"; const SettingsModal = ({ onSubmit, isOpen, onClose, currentProjectname }) => { const [color, setColor] = useColor("rgb(128 0 128)"); @@ -40,7 +40,10 @@ const SettingsModal = ({ onSubmit, isOpen, onClose, currentProjectname }) => { }); try { - await saveProjectConfig(currentProjectname, JSON.stringify(json_settings)); + await saveProjectConfig( + currentProjectname, + JSON.stringify(json_settings), + ); } catch (error) { console.error("Error saving config:", error); } @@ -57,7 +60,9 @@ const SettingsModal = ({ onSubmit, isOpen, onClose, currentProjectname }) => { > {handleCancel(settings)}} + onReset={() => { + handleCancel(settings); + }} style={{ display: "flex", flexDirection: "column", flexGrow: "1" }} >
From cc290aca063a55d342b54f6d6ab5d3871bc6719d Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Thu, 9 Jan 2025 10:33:53 +0100 Subject: [PATCH 20/31] Remove zips from upload code --- backend/tree_api/views.py | 37 ++++------ frontend/src/api_helper/TreeWrapper.ts | 53 +++++++++++++- .../file_browser/modals/UploadModal.tsx | 71 ++++++++----------- 3 files changed, 92 insertions(+), 69 deletions(-) diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index 79637f229..4191ad209 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -1245,8 +1245,9 @@ def upload_code(request): # Check if 'name' and 'zipfile' are in the request data if ( "project_name" not in request.data + or "file_name" not in request.data or "location" not in request.data - or "zip_file" not in request.data + or "content" not in request.data ): return Response( {"error": "Name and zip file are required."}, @@ -1255,39 +1256,29 @@ def upload_code(request): # Get the name and the zip file from the request project_name = request.data["project_name"] + file_name = request.data["file_name"] location = request.data["location"] - zip_file = request.data["zip_file"] + content = request.data["content"] # Make folder path relative to Django app base_path = os.path.join(settings.BASE_DIR, "filesystem") project_path = os.path.join(base_path, project_name) code_path = os.path.join(project_path, "code") create_path = os.path.join(code_path, location) + file_path = os.path.join(create_path, file_name) - try: - zip_file_data = base64.b64decode(zip_file) - except (TypeError, ValueError): - return Response( - {"error": "Invalid zip file data."}, status=status.HTTP_400_BAD_REQUEST + # If file exist simply return + if os.path.exists(file_path): + return JsonResponse( + {"success": False, "message": "File already exists"}, status=404 ) - # Save the zip file temporarily - temp_zip_path = os.path.join(create_path, "temp.zip") - with open(temp_zip_path, "wb") as temp_zip_file: - temp_zip_file.write(zip_file_data) - - # Unzip the file try: - with zipfile.ZipFile(temp_zip_path, "r") as zip_ref: - zip_ref.extractall(create_path) - except zipfile.BadZipFile: - return Response( - {"error": "Invalid zip file."}, status=status.HTTP_400_BAD_REQUEST - ) - finally: - # Clean up the temporary zip file - if os.path.exists(temp_zip_path): - os.remove(temp_zip_path) + with open(file_path, "wb") as f: + f.write(base64.b64decode(content)) + return Response({"success": True}) + except Exception as e: + return Response({"success": False, "message": str(e)}, status=400) @api_view(["GET"]) diff --git a/frontend/src/api_helper/TreeWrapper.ts b/frontend/src/api_helper/TreeWrapper.ts index 26d4c872a..a6afcc593 100644 --- a/frontend/src/api_helper/TreeWrapper.ts +++ b/frontend/src/api_helper/TreeWrapper.ts @@ -47,7 +47,11 @@ const getActionsList = async (projectName: string) => { } }; -const saveFile = async (projectName: string, fileName: string, content: string) => { +const saveFile = async ( + projectName: string, + fileName: string, + content: string +) => { if (!projectName) throw new Error("Current Project name is not set"); if (!fileName) throw new Error("Current File name is not set"); if (!content) throw new Error("Content does not exist"); @@ -67,7 +71,7 @@ const saveFile = async (projectName: string, fileName: string, content: string) //@ts-ignore Needed for compatibility with Unibotics "X-CSRFToken": context.csrf, }, - }, + } ); // Handle unsuccessful response status (e.g., non-2xx status) @@ -166,7 +170,10 @@ const loadProjectConfig = async ( } }; -const saveProjectConfig = async (currentProjectname: string, settings:string) => { +const saveProjectConfig = async ( + currentProjectname: string, + settings: string +) => { if (!currentProjectname) throw new Error("Current Project name is not set"); if (!settings) throw new Error("Settings content is null"); @@ -499,6 +506,45 @@ const saveSubtree = async ( } }; +const uploadFile = async ( + projectName: string, + fileName: string, + location: string, + content: string +) => { + if (!projectName) throw new Error("Current Project name is not set"); + if (!fileName) throw new Error("File name is not set"); + if (!location) throw new Error("Location is not set"); + if (!content) throw new Error("Content is not defined"); + + const apiUrl = "/bt_studio/upload_code/"; + + try { + const response = await axios.post( + apiUrl, + { + project_name: projectName, + file_name: fileName, + location: location, + content: content, + }, + { + headers: { + //@ts-ignore Needed for compatibility with Unibotics + "X-CSRFToken": context.csrf, + }, + } + ); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to upload file."); // Response error + } + } catch (error: unknown) { + throw error; // Rethrow + } +}; + // Named export export { createProject, @@ -519,4 +565,5 @@ export { saveSubtree, createRoboticsBackendUniverse, saveProjectConfig, + uploadFile, }; diff --git a/frontend/src/components/file_browser/modals/UploadModal.tsx b/frontend/src/components/file_browser/modals/UploadModal.tsx index df86f55f9..6853087a4 100644 --- a/frontend/src/components/file_browser/modals/UploadModal.tsx +++ b/frontend/src/components/file_browser/modals/UploadModal.tsx @@ -7,6 +7,7 @@ import Modal from "../../Modal/Modal"; import ProgressBar from "../../progress_bar/ProgressBar"; import { ReactComponent as CloseIcon } from "../../Modal/img/close.svg"; +import { uploadFile } from "../../../api_helper/TreeWrapper"; const UploadModal = ({ onSubmit, @@ -43,10 +44,6 @@ const UploadModal = ({ } }; - const onZipUpdate = (metadata: any) => { - setUploadPercentage(metadata.percent); - }; - const handleAcceptedFiles = async (files: any) => { // TODO: Redo for directory handleZipFiles(Array.from(files)); @@ -54,49 +51,37 @@ const UploadModal = ({ const handleZipFiles = async (file_array: any) => { // TODO: check if files are valid - const zip = new JSZip(); + const n_files = file_array.lenght; + var n_files_uploaded = 0; file_array.forEach((file: any, index: any) => { - zip.file(file.name, file); + var reader = new FileReader(); + + reader.onprogress = (data) => { + if (data.lengthComputable) { + const progress = Math.round((data.loaded / data.total) * 100); + setUploadPercentage(progress * (n_files_uploaded / n_files)); + } + }; + + reader.onload = (e: any) => { + const base64String = e.target.result.split(",")[1]; // Remove the data URL prefix + try { + uploadFile(currentProject, file.name, location, base64String); + console.log("Uploading file Completed"); + } catch (error) { + console.log(error); + console.log("Error uploading file"); + } + + setUploadStatus("Uploaded"); + setUploadPercentage(100 * (n_files_uploaded / n_files)); + }; + + reader.readAsDataURL(file); + n_files_uploaded++; }); - const zipContent = await zip.generateAsync({ type: "base64" }, onZipUpdate); - - try { - uploadFileToBackend(zipContent); - console.log("Uploading file Completed"); - } catch (error) { - console.log(error); - console.log("Error uploading file"); - } - }; - - const uploadFileToBackend = async (uploadedData: any) => { - console.log("Calling the saving API"); - console.log(currentProject); - - try { - const response = await axios.post( - "/bt_studio/upload_code/", - { - project_name: currentProject, - zip_file: uploadedData, - location: location, - }, - { - headers: { - //@ts-ignore Needed for compatibility with Unibotics - "X-CSRFToken": context.csrf, - }, - }, - ); - if (response.data.success) { - console.log("Universe saved successfully."); - } - } catch (error) { - console.error("Axios Error:", error); - } - onClose(); }; From 178b18909c22a38f5dc340c58d74e27752df193b Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Thu, 9 Jan 2025 11:44:12 +0100 Subject: [PATCH 21/31] Remove download_data --- backend/tree_api/views.py | 60 --------------------------------------- 1 file changed, 60 deletions(-) diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index 4191ad209..ddada0090 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -866,66 +866,6 @@ def save_file(request): return Response({"success": False, "message": str(e)}, status=400) -@api_view(["POST"]) -def download_data(request): - - # Check if 'name' and 'zipfile' are in the request data - if "app_name" not in request.data or "path" not in request.data: - return Response( - {"error": "Incorrect request parameters"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Get the request parameters - app_name = request.data.get("app_name") - path = request.data.get("path") - - # Make folder path relative to Django app - folder_path = os.path.join(settings.BASE_DIR, "filesystem") - project_path = os.path.join(folder_path, app_name) - action_path = os.path.join(project_path, "code") - file_path = os.path.join(action_path, path) - - working_folder = "/tmp/wf" - - if app_name and path: - try: - # 1. Create the working folder - if os.path.exists(working_folder): - shutil.rmtree(working_folder) - os.mkdir(working_folder) - - # 2. Copy files to temp folder - if os.path.isdir(file_path): - copy_tree(file_path, working_folder) - else: - shutil.copy(file_path, working_folder) - - # 5. Generate the zip - zip_path = working_folder + ".zip" - with zipfile.ZipFile(zip_path, "w") as zipf: - for root, dirs, files in os.walk(working_folder): - for file in files: - zipf.write( - os.path.join(root, file), - os.path.relpath(os.path.join(root, file), working_folder), - ) - - # 6. Return the zip - zip_file = open(zip_path, "rb") - mime_type, _ = mimetypes.guess_type(zip_path) - response = HttpResponse(zip_file, content_type=mime_type) - response["Content-Disposition"] = ( - f"attachment; filename={os.path.basename(zip_path)}" - ) - - return response - except Exception as e: - return Response({"success": False, "message": str(e)}, status=400) - else: - return Response({"error": "app_name parameter is missing"}, status=500) - - @api_view(["GET"]) def generate_local_app(request): From 9626c09ade515223234b33134c2c1d3c9078172b Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Thu, 9 Jan 2025 11:44:56 +0100 Subject: [PATCH 22/31] Download zip in frontend --- frontend/src/api_helper/TreeWrapper.ts | 59 ++++++++++++++ .../components/file_browser/FileBrowser.js | 79 ++++++++++--------- .../src/components/file_editor/FileEditor.tsx | 7 +- 3 files changed, 103 insertions(+), 42 deletions(-) diff --git a/frontend/src/api_helper/TreeWrapper.ts b/frontend/src/api_helper/TreeWrapper.ts index a6afcc593..10094fe6b 100644 --- a/frontend/src/api_helper/TreeWrapper.ts +++ b/frontend/src/api_helper/TreeWrapper.ts @@ -28,6 +28,26 @@ const getFileList = async (projectName: string) => { } }; +const getFile = async (projectName: string, fileName:string) => { + if (!projectName) throw new Error("Project name is not set"); + if (!fileName) throw new Error("File name is not set"); + + const apiUrl = `/bt_studio/get_file?project_name=${encodeURIComponent(projectName)}&filename=${encodeURIComponent(fileName)}`; + + try { + const response = await axios.get(apiUrl); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to get file list."); // Response error + } + + return response.data.content; + } catch (error: unknown) { + throw error; // Rethrow + } +}; + const getActionsList = async (projectName: string) => { if (!projectName) throw new Error("Project name is not set"); @@ -545,11 +565,49 @@ const uploadFile = async ( } }; +const downloadData = async (projectName: string, path: string) => { + // const api_response = await fetch("/bt_studio/download_data/", { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // }, + // body: JSON.stringify({ + // app_name: currentProjectname, + // path: file_path, + // }), + // }); + + // if (!api_response.ok) { + // var json_response = await api_response.json(); + // throw new Error(json_response.message || "An error occurred."); + // } + + // return api_response.blob(); + if (!projectName) throw new Error("Project name is not set"); + if (!path) throw new Error("Path is not set"); + + const apiUrl = `/bt_studio/download_data?app_name=${encodeURIComponent(projectName)}&path=${encodeURIComponent(path)}`; + + try { + const response = await axios.get(apiUrl); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to get subtree."); // Response error + } + + return response.data; + } catch (error: unknown) { + throw error; // Rethrow + } +}; + // Named export export { createProject, saveBaseTree, saveFile, + getFile, loadProjectConfig, getProjectGraph, generateLocalApp, @@ -566,4 +624,5 @@ export { createRoboticsBackendUniverse, saveProjectConfig, uploadFile, + downloadData }; diff --git a/frontend/src/components/file_browser/FileBrowser.js b/frontend/src/components/file_browser/FileBrowser.js index 9dec779aa..8267a4d6d 100644 --- a/frontend/src/components/file_browser/FileBrowser.js +++ b/frontend/src/components/file_browser/FileBrowser.js @@ -1,4 +1,5 @@ import React, { useEffect, useState } from "react"; +import JSZip from "jszip"; import axios from "axios"; import "./FileBrowser.css"; import NewFileModal from "./modals/NewFileModal.jsx"; @@ -8,6 +9,8 @@ import UploadModal from "./modals/UploadModal.tsx"; import DeleteModal from "./modals/DeleteModal.jsx"; import FileExplorer from "./file_explorer/FileExplorer.jsx"; +import { getFile, getFileList } from "./../../api_helper/TreeWrapper"; + import { ReactComponent as AddIcon } from "./img/add.svg"; import { ReactComponent as AddFolderIcon } from "./img/add_folder.svg"; import { ReactComponent as DeleteIcon } from "./img/delete.svg"; @@ -68,15 +71,9 @@ const FileBrowser = ({ console.log("Fecthing file list, the project name is:", currentProjectname); if (currentProjectname !== "") { try { - const response = await axios.get( - `/bt_studio/get_file_list?project_name=${currentProjectname}`, - ); - const files = JSON.parse(response.data.file_list); + const file_list = await getFileList(currentProjectname); + const files = JSON.parse(file_list); setFileList(files); - // if (Array.isArray(files)) { - // } else { - // console.error("API response is not an array:", files); - // } } catch (error) { console.error("Error fetching files:", error); } @@ -301,42 +298,50 @@ const FileBrowser = ({ }; ///////////////// DOWNLOAD /////////////////////////////////////////////////// - const fetchDownloadData = async (file_path) => { - const api_response = await fetch("/bt_studio/download_data/", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - app_name: currentProjectname, - path: file_path, - }), - }); - - if (!api_response.ok) { - var json_response = await api_response.json(); - throw new Error(json_response.message || "An error occurred."); - } + const zipFile = async (zip, file_path, file_name) => { + var content = await getFile(currentProjectname, file_path); + zip.file(file_name, content); + }; + + const zipFolder = async (zip, file) => { + const folder = zip.folder(file.name); - return api_response.blob(); + for (let index = 0; index < file.files.length; index++) { + const element = file.files[index]; + console.log(element); + if (element.is_dir) { + await zipFolder(folder, element); + } else { + await zipFile(folder, element.path, element.name); + } + } }; const handleDownload = async (file) => { if (file) { - // Get the data as a base64 blob object - const app_blob = await fetchDownloadData(file.path); - try { - const url = window.URL.createObjectURL(app_blob); - const a = document.createElement("a"); - a.style.display = "none"; - a.href = url; - a.download = `${file.name}.zip`; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); + // Create the zip with the files + const zip = new JSZip(); + + if (file.is_dir) { + await zipFolder(zip, file); + } else { + await zipFile(zip, file.path, file.name); + } + + zip.generateAsync({ type: "blob" }).then(function (content) { + // Create a download link and trigger download + const url = window.URL.createObjectURL(content); + const a = document.createElement("a"); + a.style.display = "none"; + a.href = url; + a.download = `${file.name.split(".")[0]}.zip`; // Set the downloaded file's name + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); // Clean up after the download + }); } catch (error) { - console.error("Error:", error); + console.error("Error downloading file: " + error); } } }; diff --git a/frontend/src/components/file_editor/FileEditor.tsx b/frontend/src/components/file_editor/FileEditor.tsx index 359c37707..6d3acd142 100644 --- a/frontend/src/components/file_editor/FileEditor.tsx +++ b/frontend/src/components/file_editor/FileEditor.tsx @@ -8,7 +8,7 @@ import "./FileEditor.css"; import { ReactComponent as SaveIcon } from "./img/save.svg"; import { ReactComponent as SplashIcon } from "./img/logo_jderobot_monocolor.svg"; import { ReactComponent as SplashIconUnibotics } from "./img/logo_unibotics_monocolor.svg"; -import { saveFile } from "../../api_helper/TreeWrapper"; +import { getFile, saveFile } from "../../api_helper/TreeWrapper"; const FileEditor = ({ currentFilename, @@ -35,10 +35,7 @@ const FileEditor = ({ const initFile = async () => { try { - const response = await axios.get( - `/bt_studio/get_file?project_name=${currentProjectname}&filename=${currentFilename}`, - ); - const content = response.data.content; + const content = await getFile(currentProjectname, currentFilename); setFileContent(content); setHasUnsavedChanges(false); // Reset the unsaved changes flag when a new file is loaded } catch (error) { From 67f67921001172577b81d2d014b58f6d76e544ed Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Thu, 9 Jan 2025 13:57:03 +0100 Subject: [PATCH 23/31] Change get methods to get and move all API calls to TreeWrapper.ts --- backend/tree_api/views.py | 233 ++++++--- frontend/src/api_helper/TreeWrapper.ts | 495 +++++++++++++++--- .../components/file_browser/FileBrowser.js | 97 ++-- .../file_explorer/FileExplorer.jsx | 1 - .../file_browser/modals/UploadModal.tsx | 2 - .../src/components/file_editor/FileEditor.tsx | 1 - .../header_menu/modals/ProjectModal.tsx | 19 +- .../header_menu/modals/UniverseModal.tsx | 20 +- .../modals/UniverseUploadModal.tsx | 23 +- .../modals/universe/CreatePage.tsx | 11 +- .../tree_editor/MainTreeEditorContainer.tsx | 63 +-- 11 files changed, 682 insertions(+), 283 deletions(-) diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index ddada0090..0b02008c9 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -25,10 +25,15 @@ # PROJECT MANAGEMENT -@api_view(["GET"]) +@api_view(["POST"]) def create_project(request): + if "project_name" not in request.data: + return Response( + {"error": "Name and zip file are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) - project_name = request.GET.get("project_name") + project_name = request.data.get("project_name") folder_path = os.path.join(settings.BASE_DIR, "filesystem") project_path = os.path.join(folder_path, project_name) action_path = os.path.join(project_path, "code/actions") @@ -77,9 +82,14 @@ def create_project(request): ) -@api_view(["GET"]) +@api_view(["POST"]) def delete_project(request): - project_name = request.GET.get("project_name") + if "project_name" not in request.data: + return Response( + {"error": "Name and zip file are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + project_name = request.data.get("project_name") folder_path = os.path.join(settings.BASE_DIR, "filesystem") project_path = os.path.join(folder_path, project_name) @@ -114,7 +124,11 @@ def get_project_list(request): @api_view(["POST"]) def save_base_tree(request): - + if "project_name" not in request.data or "graph_json" not in request.data: + return Response( + {"error": "Name and zip file are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) # Get the app name and the graph project_name = request.data.get("project_name") graph_json = request.data.get("graph_json") @@ -264,6 +278,11 @@ def get_subtree_structure(request): @api_view(["POST"]) def save_project_configuration(request): + if "project_name" not in request.data or "settings" not in request.data: + return Response( + {"error": "Name and zip file are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) project_name = request.data.get("project_name") content = request.data.get("settings") @@ -293,6 +312,11 @@ def save_project_configuration(request): @api_view(["POST"]) def create_subtree(request): + if "project_name" not in request.data or "subtree_name" not in request.data: + return Response( + {"error": "Name and zip file are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) project_name = request.data.get("project_name") subtree_name = request.data.get("subtree_name") @@ -472,10 +496,15 @@ def get_subtree_list(request): # UNIVERSE MANAGEMENT -@api_view(["GET"]) +@api_view(["POST"]) def delete_universe(request): - project_name = request.GET.get("project_name") - universe_name = request.GET.get("universe_name") + if "project_name" not in request.data or "universe_name" not in request.data: + return Response( + {"error": "Name and zip file are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + project_name = request.data.get("project_name") + universe_name = request.data.get("universe_name") folder_path = os.path.join(settings.BASE_DIR, "filesystem") project_path = os.path.join(folder_path, project_name) @@ -552,17 +581,6 @@ def get_universe_configuration(request): return Response({"success": False, "message": "File not found"}, status=404) -@api_view(["GET"]) -def import_universe_from_zip(request): - - project_name = request.GET.get("project_name") - zip_file = request.GET.get("zip_file") - - folder_path = os.path.join(settings.BASE_DIR, "filesystem") - project_path = os.path.join(folder_path, project_name) - universes_path = os.path.join(project_path, "universes/") - - # FILE MANAGEMENT @@ -637,13 +655,21 @@ def get_file(request): return Response({"error": "Filename parameter is missing"}, status=400) -@api_view(["GET"]) +@api_view(["POST"]) def create_action(request): - + if ( + "project_name" not in request.data + or "template" not in request.data + or "filename" not in request.data + ): + return Response( + {"error": "Name and zip file are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) # Get the file info - project_name = request.GET.get("project_name", None) - filename = request.GET.get("filename", None) - template = request.GET.get("template", None) + project_name = request.data.get("project_name") + filename = request.data.get("filename") + template = request.data.get("template") # Make folder path relative to Django app folder_path = os.path.join(settings.BASE_DIR, "filesystem") @@ -671,13 +697,21 @@ def create_action(request): ) -@api_view(["GET"]) +@api_view(["POST"]) def create_file(request): - + if ( + "project_name" not in request.data + or "location" not in request.data + or "file_name" not in request.data + ): + return Response( + {"error": "Name and zip file are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) # Get the file info - project_name = request.GET.get("project_name", None) - location = request.GET.get("location", None) - filename = request.GET.get("file_name", None) + project_name = request.data.get("project_name") + location = request.data.get("location") + filename = request.data.get("file_name") # Make folder path relative to Django app folder_path = os.path.join(settings.BASE_DIR, "filesystem") @@ -696,13 +730,21 @@ def create_file(request): ) -@api_view(["GET"]) +@api_view(["POST"]) def create_folder(request): - + if ( + "project_name" not in request.data + or "location" not in request.data + or "folder_name" not in request.data + ): + return Response( + {"error": "Name and zip file are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) # Get the file info - project_name = request.GET.get("project_name", None) - location = request.GET.get("location", None) - folder_name = request.GET.get("folder_name", None) + project_name = request.data.get("project_name") + location = request.data.get("location") + folder_name = request.data.get("folder_name") # Make folder path relative to Django app folder_path = os.path.join(settings.BASE_DIR, "filesystem") @@ -724,13 +766,21 @@ def create_folder(request): ) -@api_view(["GET"]) +@api_view(["POST"]) def rename_file(request): - + if ( + "project_name" not in request.data + or "path" not in request.data + or "rename_to" not in request.data + ): + return Response( + {"error": "Name and zip file are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) # Get the file info - project_name = request.GET.get("project_name", None) - path = request.GET.get("path", None) - rename_path = request.GET.get("rename_to", None) + project_name = request.data.get("project_name") + path = request.data.get("path") + rename_path = request.data.get("rename_to") # Make folder path relative to Django app folder_path = os.path.join(settings.BASE_DIR, "filesystem") @@ -754,13 +804,21 @@ def rename_file(request): ) -@api_view(["GET"]) +@api_view(["POST"]) def rename_folder(request): - + if ( + "project_name" not in request.data + or "path" not in request.data + or "rename_to" not in request.data + ): + return Response( + {"error": "Name and zip file are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) # Get the folder info - project_name = request.GET.get("project_name", None) - path = request.GET.get("path", None) - rename_path = request.GET.get("rename_to", None) + project_name = request.data.get("project_name") + path = request.data.get("path") + rename_path = request.data.get("rename_to") # Make folder path relative to Django app folder_path = os.path.join(settings.BASE_DIR, "filesystem") @@ -784,12 +842,17 @@ def rename_folder(request): ) -@api_view(["GET"]) +@api_view(["POST"]) def delete_file(request): + if "project_name" not in request.data or "path" not in request.data: + return Response( + {"error": "Name and zip file are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) # Get the file info - project_name = request.GET.get("project_name", None) - path = request.GET.get("path", None) + project_name = request.data.get("project_name") + path = request.data.get("path") # Make folder path relative to Django app folder_path = os.path.join(settings.BASE_DIR, "filesystem") @@ -812,12 +875,17 @@ def delete_file(request): ) -@api_view(["GET"]) +@api_view(["POST"]) def delete_folder(request): + if "project_name" not in request.data or "path" not in request.data: + return Response( + {"error": "Name and zip file are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) # Get the folder info - project_name = request.GET.get("project_name", None) - path = request.GET.get("path", None) + project_name = request.data.get("project_name") + path = request.data.get("path") # Make folder path relative to Django app folder_path = os.path.join(settings.BASE_DIR, "filesystem") @@ -842,6 +910,15 @@ def delete_folder(request): @api_view(["POST"]) def save_file(request): + if ( + "project_name" not in request.data + or "filename" not in request.data + or "content" not in request.data + ): + return Response( + {"error": "Name and zip file are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) project_name = request.data.get("project_name") filename = request.data.get("filename") @@ -866,13 +943,23 @@ def save_file(request): return Response({"success": False, "message": str(e)}, status=400) -@api_view(["GET"]) +@api_view(["POST"]) def generate_local_app(request): + # Check if 'app_name', 'main_tree_graph', and 'bt_order' are in the request data + if ( + "app_name" not in request.data + or "tree_graph" not in request.data + or "bt_order" not in request.data + ): + return Response( + {"success": False, "message": "Missing required parameters"}, + status=status.HTTP_400_BAD_REQUEST, + ) # Get the request parameters - app_name = request.GET.get("app_name", None) - main_tree_graph = request.GET.get("tree_graph", None) - bt_order = request.GET.get("bt_order", None) + app_name = request.data.get("app_name") + main_tree_graph = request.data.get("tree_graph") + bt_order = request.data.get("bt_order") # Make folder path relative to Django app base_path = os.path.join(settings.BASE_DIR, "filesystem") @@ -936,13 +1023,23 @@ def generate_local_app(request): ) -@api_view(["GET"]) +@api_view(["POST"]) def generate_dockerized_app(request): + # Check if 'app_name', 'tree_graph', and 'bt_order' are in the request data + if ( + "app_name" not in request.data + or "tree_graph" not in request.data + or "bt_order" not in request.data + ): + return Response( + {"success": False, "message": "Missing required parameters"}, + status=status.HTTP_400_BAD_REQUEST, + ) # Get the request parameters - app_name = request.GET.get("app_name", None) - main_tree_graph = request.GET.get("tree_graph", None) - bt_order = request.GET.get("bt_order", None) + app_name = request.data.get("app_name") + main_tree_graph = request.data.get("tree_graph") + bt_order = request.data.get("bt_order") # Make folder path relative to Django app base_path = os.path.join(settings.BASE_DIR, "filesystem") @@ -1074,9 +1171,9 @@ def upload_universe(request): ) # Get the name and the zip file from the request - universe_name = request.data["universe_name"] - app_name = request.data["app_name"] - zip_file = request.data["zip_file"] + universe_name = request.data.get("universe_name") + app_name = request.data.get("app_name") + zip_file = request.data.get("zip_file") # Make folder path relative to Django app base_path = os.path.join(settings.BASE_DIR, "filesystem") @@ -1151,9 +1248,9 @@ def add_docker_universe(request): ) # Get the name and the id file from the request - universe_name = request.data["universe_name"] - app_name = request.data["app_name"] - id = request.data["id"] + universe_name = request.data.get("universe_name") + app_name = request.data.get("app_name") + id = request.data.get("id") # Make folder path relative to Django app base_path = os.path.join(settings.BASE_DIR, "filesystem") @@ -1195,10 +1292,10 @@ def upload_code(request): ) # Get the name and the zip file from the request - project_name = request.data["project_name"] - file_name = request.data["file_name"] - location = request.data["location"] - content = request.data["content"] + project_name = request.data.get("project_name") + file_name = request.data.get("file_name") + location = request.data.get("location") + content = request.data.get("content") # Make folder path relative to Django app base_path = os.path.join(settings.BASE_DIR, "filesystem") diff --git a/frontend/src/api_helper/TreeWrapper.ts b/frontend/src/api_helper/TreeWrapper.ts index 10094fe6b..a49c990b6 100644 --- a/frontend/src/api_helper/TreeWrapper.ts +++ b/frontend/src/api_helper/TreeWrapper.ts @@ -7,6 +7,13 @@ const isSuccessful = (response: AxiosResponse) => { return response.status >= 200 && response.status < 300; }; +const axiosExtra = { + headers: { + //@ts-ignore Needed for compatibility with Unibotics + "X-CSRFToken": context.csrf, + }, +}; + // File management const getFileList = async (projectName: string) => { @@ -28,7 +35,7 @@ const getFileList = async (projectName: string) => { } }; -const getFile = async (projectName: string, fileName:string) => { +const getFile = async (projectName: string, fileName: string) => { if (!projectName) throw new Error("Project name is not set"); if (!fileName) throw new Error("File name is not set"); @@ -86,12 +93,7 @@ const saveFile = async ( filename: fileName, content: content, }, - { - headers: { - //@ts-ignore Needed for compatibility with Unibotics - "X-CSRFToken": context.csrf, - }, - } + axiosExtra ); // Handle unsuccessful response status (e.g., non-2xx status) @@ -110,10 +112,16 @@ const createProject = async (projectName: string) => { throw new Error("Project name cannot be empty."); } - const apiUrl = `/bt_studio/create_project?project_name=${encodeURIComponent(projectName)}`; + const apiUrl = `/bt_studio/create_project/`; try { - const response = await axios.get(apiUrl); + const response = await axios.post( + apiUrl, + { + project_name: projectName, + }, + axiosExtra + ); // Handle unsuccessful response status (e.g., non-2xx status) if (!isSuccessful(response)) { @@ -124,6 +132,31 @@ const createProject = async (projectName: string) => { } }; +const deleteProject = async (projectName: string) => { + if (!projectName.trim()) { + throw new Error("Project name cannot be empty."); + } + + const apiUrl = `/bt_studio/delete_project/`; + + try { + const response = await axios.post( + apiUrl, + { + project_name: projectName, + }, + axiosExtra + ); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to delete project."); // Response error + } + } catch (error: unknown) { + throw error; // Rethrow + } +}; + const saveBaseTree = async (modelJson: string, currentProjectname: string) => { if (!modelJson) throw new Error("Tree JSON is empty!"); if (!currentProjectname) throw new Error("Current Project name is not set"); @@ -136,12 +169,7 @@ const saveBaseTree = async (modelJson: string, currentProjectname: string) => { project_name: currentProjectname, graph_json: JSON.stringify(modelJson), }, - { - headers: { - //@ts-ignore Needed for compatibility with Unibotics - "X-CSRFToken": context.csrf, - }, - } + axiosExtra ); // Handle unsuccessful response status (e.g., non-2xx status) @@ -205,12 +233,7 @@ const saveProjectConfig = async ( project_name: currentProjectname, settings: settings, }, - { - headers: { - //@ts-ignore Needed for compatibility with Unibotics - "X-CSRFToken": context.csrf, - }, - } + axiosExtra ); // Handle unsuccessful response status (e.g., non-2xx status) @@ -308,12 +331,35 @@ const createRoboticsBackendUniverse = async ( universe_name: universeName, id: universeId, }, + axiosExtra + ); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to save subtree."); // Response error + } + } catch (error: unknown) { + throw error; // Rethrow + } +}; + +const deleteUniverse = async ( + projectName: string, + universeName: string, +) => { + if (!projectName) throw new Error("The project name is not set"); + if (!universeName) throw new Error("The universe name is not set"); + + const apiUrl = "/bt_studio/delete_universe/"; + + try { + const response = await axios.post( + apiUrl, { - headers: { - //@ts-ignore Needed for compatibility with Unibotics - "X-CSRFToken": context.csrf, - }, - } + project_name: projectName, + universe_name: universeName, + }, + axiosExtra ); // Handle unsuccessful response status (e.g., non-2xx status) @@ -373,9 +419,17 @@ const generateLocalApp = async ( if (!currentProjectname) throw new Error("Current Project name is not set"); if (!btOrder) throw new Error("Behavior Tree order is not set"); - const apiUrl = `/bt_studio/generate_local_app?app_name=${currentProjectname}&tree_graph=${JSON.stringify(modelJson)}&bt_order=${btOrder}`; + const apiUrl = `/bt_studio/generate_local_app/`; try { - const response = await axios.get(apiUrl); + const response = await axios.post( + apiUrl, + { + app_name: currentProjectname, + tree_graph: JSON.stringify(modelJson), + bt_order: btOrder, + }, + axiosExtra + ); // Handle unsuccessful response status (e.g., non-2xx status) if (!isSuccessful(response)) { @@ -397,9 +451,17 @@ const generateDockerizedApp = async ( if (!currentProjectname) throw new Error("Current Project name is not set"); if (!btOrder) throw new Error("Behavior Tree order is not set"); - const apiUrl = `/bt_studio/generate_dockerized_app?app_name=${currentProjectname}&tree_graph=${JSON.stringify(modelJson)}&bt_order=${btOrder}`; + const apiUrl = `/bt_studio/generate_dockerized_app/`; try { - const response = await axios.get(apiUrl); + const response = await axios.post( + apiUrl, + { + app_name: currentProjectname, + tree_graph: JSON.stringify(modelJson), + bt_order: btOrder, + }, + axiosExtra + ); // Handle unsuccessful response status (e.g., non-2xx status) if (!isSuccessful(response)) { @@ -434,12 +496,7 @@ const createSubtree = async ( project_name: currentProjectname, subtree_name: subtreeName, }, - { - headers: { - //@ts-ignore Needed for compatibility with Unibotics - "X-CSRFToken": context.csrf, - }, - } + axiosExtra ); // Handle unsuccessful response status (e.g., non-2xx status) @@ -509,12 +566,7 @@ const saveSubtree = async ( subtree_name: subtreeName, subtree_json: JSON.stringify(modelJson), }, - { - headers: { - //@ts-ignore Needed for compatibility with Unibotics - "X-CSRFToken": context.csrf, - }, - } + axiosExtra ); // Handle unsuccessful response status (e.g., non-2xx status) @@ -548,12 +600,38 @@ const uploadFile = async ( location: location, content: content, }, + axiosExtra + ); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to upload file."); // Response error + } + } catch (error: unknown) { + throw error; // Rethrow + } +}; + +const createAction = async ( + projectName: string, + fileName: string, + template: string, +) => { + if (!projectName) throw new Error("Current Project name is not set"); + if (!fileName) throw new Error("File name is not set"); + if (!template) throw new Error("Template is not set"); + + const apiUrl = "/bt_studio/create_action/"; + + try { + const response = await axios.post( + apiUrl, { - headers: { - //@ts-ignore Needed for compatibility with Unibotics - "X-CSRFToken": context.csrf, - }, - } + project_name: projectName, + filename: fileName, + template: template, + }, + axiosExtra ); // Handle unsuccessful response status (e.g., non-2xx status) @@ -565,28 +643,219 @@ const uploadFile = async ( } }; -const downloadData = async (projectName: string, path: string) => { - // const api_response = await fetch("/bt_studio/download_data/", { - // method: "POST", - // headers: { - // "Content-Type": "application/json", - // }, - // body: JSON.stringify({ - // app_name: currentProjectname, - // path: file_path, - // }), - // }); +const createFile = async ( + projectName: string, + fileName: string, + location: string, +) => { + if (!projectName) throw new Error("Current Project name is not set"); + if (!fileName) throw new Error("File name is not set"); + if (!location) throw new Error("Location is not set"); - // if (!api_response.ok) { - // var json_response = await api_response.json(); - // throw new Error(json_response.message || "An error occurred."); - // } + const apiUrl = "/bt_studio/create_file/"; - // return api_response.blob(); - if (!projectName) throw new Error("Project name is not set"); + try { + const response = await axios.post( + apiUrl, + { + project_name: projectName, + location: location, + filename: fileName, + }, + axiosExtra + ); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to upload file."); // Response error + } + } catch (error: unknown) { + throw error; // Rethrow + } +}; + +const createFolder = async ( + projectName: string, + folderName: string, + location: string, +) => { + if (!projectName) throw new Error("Current Project name is not set"); + if (!folderName) throw new Error("Folder name is not set"); + if (!location) throw new Error("Location is not set"); + + const apiUrl = "/bt_studio/create_folder/"; + + try { + const response = await axios.post( + apiUrl, + { + project_name: projectName, + location: location, + folder_name: folderName, + }, + axiosExtra + ); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to upload file."); // Response error + } + } catch (error: unknown) { + throw error; // Rethrow + } +}; + +const renameFile = async ( + projectName: string, + path: string, + new_path: string, +) => { + if (!projectName) throw new Error("Current Project name is not set"); + if (!path) throw new Error("Path is not set"); + if (!new_path) throw new Error("New path is not set"); + + const apiUrl = "/bt_studio/rename_file/"; + + try { + const response = await axios.post( + apiUrl, + { + project_name: projectName, + path: path, + rename_to: new_path, + }, + axiosExtra + ); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to upload file."); // Response error + } + } catch (error: unknown) { + throw error; // Rethrow + } +}; + +const renameFolder = async ( + projectName: string, + path: string, + new_path: string, +) => { + if (!projectName) throw new Error("Current Project name is not set"); if (!path) throw new Error("Path is not set"); + if (!new_path) throw new Error("New path is not set"); + + const apiUrl = "/bt_studio/rename_folder/"; + + try { + const response = await axios.post( + apiUrl, + { + project_name: projectName, + path: path, + rename_to: new_path, + }, + axiosExtra + ); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to upload file."); // Response error + } + } catch (error: unknown) { + throw error; // Rethrow + } +}; + +const deleteFile = async ( + projectName: string, + path: string, +) => { + if (!projectName) throw new Error("Current Project name is not set"); + if (!path) throw new Error("Path is not set"); + + const apiUrl = "/bt_studio/delete_file/"; + + try { + const response = await axios.post( + apiUrl, + { + project_name: projectName, + path: path, + }, + axiosExtra + ); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to upload file."); // Response error + } + } catch (error: unknown) { + throw error; // Rethrow + } +}; + +const deleteFolder = async ( + projectName: string, + path: string, +) => { + if (!projectName) throw new Error("Current Project name is not set"); + if (!path) throw new Error("Path is not set"); + + const apiUrl = "/bt_studio/delete_folder/"; + + try { + const response = await axios.post( + apiUrl, + { + project_name: projectName, + path: path, + }, + axiosExtra + ); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to upload file."); // Response error + } + } catch (error: unknown) { + throw error; // Rethrow + } +}; - const apiUrl = `/bt_studio/download_data?app_name=${encodeURIComponent(projectName)}&path=${encodeURIComponent(path)}`; +const uploadUniverse = async ( + projectName: string, + universeName: string, + uploadedUniverse: any, +) => { + if (!projectName) throw new Error("Current Project name is not set"); + if (!universeName) throw new Error("Universe name is not set"); + if (!uploadedUniverse) throw new Error("Content is not set"); + + const apiUrl = "/bt_studio/upload_universe/"; + + try { + const response = await axios.post( + apiUrl, + { + universe_name: universeName, + zip_file: uploadedUniverse, + app_name: projectName, + }, + axiosExtra + ); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to upload file."); // Response error + } + } catch (error: unknown) { + throw error; // Rethrow + } +}; + +const listDockerUniverses = async () => { + const apiUrl = `/bt_studio/list_docker_universes`; try { const response = await axios.get(apiUrl); @@ -596,15 +865,94 @@ const downloadData = async (projectName: string, path: string) => { throw new Error(response.data.message || "Failed to get subtree."); // Response error } - return response.data; + return response.data.universes; } catch (error: unknown) { throw error; // Rethrow } }; +const listProjects = async () => { + const apiUrl = `/bt_studio/get_project_list`; + + try { + const response = await axios.get(apiUrl); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to get subtree."); // Response error + } + + return response.data.project_list; + } catch (error: unknown) { + throw error; // Rethrow + } +}; + +const listUniverses = async (projectName:string ) => { + if (!projectName) throw new Error("Current Project name is not set"); + + const apiUrl = `/bt_studio/get_universes_list?project_name=${encodeURIComponent(projectName)}`; + + try { + const response = await axios.get(apiUrl); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to get subtree."); // Response error + } + + return response.data.universes_list; + } catch (error: unknown) { + throw error; // Rethrow + } +}; + +const getTreeStructure = async (projectName:string, btOrder: string) => { + if (!projectName) throw new Error("Current Project name is not set"); + if (!btOrder) throw new Error("Behavior Tree order is not set"); + + const apiUrl = `/bt_studio/get_tree_structure?project_name=${encodeURIComponent(projectName)}&bt_order=${encodeURIComponent(btOrder)}`; + + try { + const response = await axios.get(apiUrl); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to get subtree."); // Response error + } + + return response.data.tree_structure; + } catch (error: unknown) { + throw error; // Rethrow + } +}; + +const getSubtreeStructure = async (projectName:string, subtreeName:string, btOrder: string) => { + if (!projectName) throw new Error("Current Project name is not set"); + if (!subtreeName) throw new Error("Subtree name is not set"); + if (!btOrder) throw new Error("Behavior Tree order is not set"); + + const apiUrl = `/bt_studio/get_subtree_structure?project_name=${encodeURIComponent(projectName)}&subtree_name=${encodeURIComponent(subtreeName)}&bt_order=${encodeURIComponent(btOrder)}`; + + try { + const response = await axios.get(apiUrl); + + // Handle unsuccessful response status (e.g., non-2xx status) + if (!isSuccessful(response)) { + throw new Error(response.data.message || "Failed to get subtree."); // Response error + } + + return response.data.tree_structure; + } catch (error: unknown) { + throw error; // Rethrow + } +}; + + // Named export export { createProject, + deleteProject, saveBaseTree, saveFile, getFile, @@ -622,7 +970,20 @@ export { getActionsList, saveSubtree, createRoboticsBackendUniverse, + deleteUniverse, saveProjectConfig, uploadFile, - downloadData + createAction, + createFile, + createFolder, + renameFile, + renameFolder, + deleteFile, + deleteFolder, + uploadUniverse, + listDockerUniverses, + listProjects, + listUniverses, + getSubtreeStructure, + getTreeStructure }; diff --git a/frontend/src/components/file_browser/FileBrowser.js b/frontend/src/components/file_browser/FileBrowser.js index 8267a4d6d..fa309e3dd 100644 --- a/frontend/src/components/file_browser/FileBrowser.js +++ b/frontend/src/components/file_browser/FileBrowser.js @@ -1,6 +1,5 @@ import React, { useEffect, useState } from "react"; import JSZip from "jszip"; -import axios from "axios"; import "./FileBrowser.css"; import NewFileModal from "./modals/NewFileModal.jsx"; import RenameModal from "./modals/RenameModal.jsx"; @@ -9,7 +8,17 @@ import UploadModal from "./modals/UploadModal.tsx"; import DeleteModal from "./modals/DeleteModal.jsx"; import FileExplorer from "./file_explorer/FileExplorer.jsx"; -import { getFile, getFileList } from "./../../api_helper/TreeWrapper"; +import { + getFile, + getFileList, + createAction, + createFile, + createFolder, + renameFile, + renameFolder, + deleteFile, + deleteFolder, +} from "./../../api_helper/TreeWrapper"; import { ReactComponent as AddIcon } from "./img/add.svg"; import { ReactComponent as AddFolderIcon } from "./img/add_folder.svg"; @@ -100,22 +109,18 @@ const FileBrowser = ({ let response; switch (data.fileType) { case "actions": - response = await axios.get( - `/bt_studio/create_action?project_name=${currentProjectname}&filename=${data.fileName}.py&template=${data.templateType}`, + await createAction( + currentProjectname, + data.fileName, + data.templateType, ); break; default: - response = await axios.get( - `/bt_studio/create_file?project_name=${currentProjectname}&location=${location}&file_name=${data.fileName}`, - ); + await createFile(currentProjectname, data.fileName, location); break; } - if (response.data.success) { - setProjectChanges(true); - fetchFileList(); // Update the file list - } else { - alert(response.data.message); - } + setProjectChanges(true); + fetchFileList(); // Update the file list } catch (error) { console.error("Error creating file:", error); } @@ -144,27 +149,20 @@ const FileBrowser = ({ //currentFilename === Absolute File path if (deleteEntry) { try { - var response; if (deleteType) { - response = await axios.get( - `/bt_studio/delete_folder?project_name=${currentProjectname}&path=${deleteEntry}`, - ); + await deleteFolder(currentProjectname, deleteEntry); } else { - response = await axios.get( - `/bt_studio/delete_file?project_name=${currentProjectname}&path=${deleteEntry}`, - ); + await deleteFile(currentProjectname, deleteEntry); } - if (response.data.success) { - setProjectChanges(true); - fetchFileList(); // Update the file list - if (currentFilename === deleteEntry) { - setCurrentFilename(""); // Unset the current file - } - if (selectedEntry.path === deleteEntry) { - setSelectedEntry(null); - } - } else { - alert(response.data.message); + + setProjectChanges(true); + fetchFileList(); // Update the file list + + if (currentFilename === deleteEntry) { + setCurrentFilename(""); // Unset the current file + } + if (selectedEntry.path === deleteEntry) { + setSelectedEntry(null); } } catch (error) { console.error("Error deleting file:", error); @@ -200,15 +198,9 @@ const FileBrowser = ({ const handleCreateFolderSubmit = async (location, folder_name) => { if (folder_name !== "") { try { - const response = await axios.get( - `/bt_studio/create_folder?project_name=${currentProjectname}&location=${location}&folder_name=${folder_name}`, - ); - if (response.data.success) { - setProjectChanges(true); - fetchFileList(); // Update the file list - } else { - alert(response.data.message); - } + await createFolder(currentProjectname, folder_name, location); + setProjectChanges(true); + fetchFileList(); // Update the file list } catch (error) { console.error("Error creating folder:", error); } @@ -237,26 +229,19 @@ const FileBrowser = ({ const handleSubmitRenameModal = async (new_path) => { if (renameEntry) { try { - var response; console.log(renameEntry); if (renameEntry.is_dir) { - response = await axios.get( - `/bt_studio/rename_folder?project_name=${currentProjectname}&path=${renameEntry.path}&rename_to=${new_path}`, - ); + await renameFolder(currentProjectname, renameEntry.path, new_path); } else { - response = await axios.get( - `/bt_studio/rename_file?project_name=${currentProjectname}&path=${renameEntry.path}&rename_to=${new_path}`, - ); + await renameFile(currentProjectname, renameEntry.path, new_path); } - if (response.data.success) { - setProjectChanges(true); - fetchFileList(); // Update the file list - if (currentFilename === renameEntry.path) { - setAutosave(false); - setCurrentFilename(new_path); // Unset the current file - } - } else { - alert(response.data.message); + + setProjectChanges(true); + fetchFileList(); // Update the file list + + if (currentFilename === renameEntry.path) { + setAutosave(false); + setCurrentFilename(new_path); // Unset the current file } } catch (error) { console.error("Error deleting file:", error); diff --git a/frontend/src/components/file_browser/file_explorer/FileExplorer.jsx b/frontend/src/components/file_browser/file_explorer/FileExplorer.jsx index 8459087dd..51d7cdc8b 100644 --- a/frontend/src/components/file_browser/file_explorer/FileExplorer.jsx +++ b/frontend/src/components/file_browser/file_explorer/FileExplorer.jsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from "react"; -import axios from "axios"; import "./FileExplorer.css"; import TreeNode from "./TreeNode.jsx"; diff --git a/frontend/src/components/file_browser/modals/UploadModal.tsx b/frontend/src/components/file_browser/modals/UploadModal.tsx index 6853087a4..20e3810e6 100644 --- a/frontend/src/components/file_browser/modals/UploadModal.tsx +++ b/frontend/src/components/file_browser/modals/UploadModal.tsx @@ -1,6 +1,4 @@ import React, { useState, useEffect, useRef } from "react"; -import axios from "axios"; -import JSZip from "jszip"; import "./UploadModal.css"; import Modal from "../../Modal/Modal"; diff --git a/frontend/src/components/file_editor/FileEditor.tsx b/frontend/src/components/file_editor/FileEditor.tsx index 6d3acd142..75fc3131c 100644 --- a/frontend/src/components/file_editor/FileEditor.tsx +++ b/frontend/src/components/file_editor/FileEditor.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from "react"; import AceEditor from "react-ace"; -import axios from "axios"; import "ace-builds/src-noconflict/mode-python"; import "ace-builds/src-noconflict/theme-monokai"; import "./FileEditor.css"; diff --git a/frontend/src/components/header_menu/modals/ProjectModal.tsx b/frontend/src/components/header_menu/modals/ProjectModal.tsx index 042a58ce7..a2cac9b02 100644 --- a/frontend/src/components/header_menu/modals/ProjectModal.tsx +++ b/frontend/src/components/header_menu/modals/ProjectModal.tsx @@ -4,7 +4,7 @@ import Modal from "../../Modal/Modal"; import { ReactComponent as BackIcon } from "../../Modal/img/back.svg"; import { ReactComponent as CloseIcon } from "../../Modal/img/close.svg"; import { ReactComponent as DeleteIcon } from "../../tree_editor/img/delete.svg"; -import axios from "axios"; +import { deleteProject, listProjects } from "../../../api_helper/TreeWrapper"; const initialProjectData = { projectName: "", @@ -34,10 +34,9 @@ const ProjectModal = ({ const [formState, setFormState] = useState(initialProjectData); const getProjects = async () => { - const listApiUrl = `/bt_studio/get_project_list`; try { - const response = await axios.get(listApiUrl); - setExistingProjects(response.data.project_list); + const response = await listProjects(); + setExistingProjects(response); setFormState(initialProjectData); } catch (error) { console.error("Error while fetching project list:", error); @@ -79,20 +78,16 @@ const ProjectModal = ({ onClose(); }; - const deleteProject = async (project: string) => { + const deleteProjectFunc = async (project: string) => { if (currentProject === project) { //TODO: change this to change project before deleting return; } - const apiUrl = `/bt_studio/delete_project?project_name=${encodeURIComponent(project)}`; - const listApiUrl = `/bt_studio/get_project_list`; // Delete and update - const response = await axios.get(apiUrl); try { - if (response.data.success) { - await getProjects(); - } + await deleteProject(project); + await getProjects(); console.log("Project deleted successfully"); } catch (error) { console.error("Error while fetching project list:", error); @@ -141,7 +136,7 @@ const ProjectModal = ({ className="bt-project-entry-delete bt-icon" title="Delete" onClick={(e) => { - deleteProject(project[1]); + deleteProjectFunc(project[1]); e.stopPropagation(); }} fill={"var(--icon)"} diff --git a/frontend/src/components/header_menu/modals/UniverseModal.tsx b/frontend/src/components/header_menu/modals/UniverseModal.tsx index 203510d38..53307ca69 100644 --- a/frontend/src/components/header_menu/modals/UniverseModal.tsx +++ b/frontend/src/components/header_menu/modals/UniverseModal.tsx @@ -4,8 +4,8 @@ import Modal from "../../Modal/Modal"; import { ReactComponent as CloseIcon } from "../../Modal/img/close.svg"; import { ReactComponent as DeleteIcon } from "../../tree_editor/img/delete.svg"; import CreatePage from "./universe/CreatePage"; -import axios from "axios"; import UniverseUploadModal from "./UniverseUploadModal"; +import { deleteUniverse, listUniverses } from "../../../api_helper/TreeWrapper"; const UniverseModal = ({ onSubmit, @@ -28,9 +28,8 @@ const UniverseModal = ({ const loadUniverseList = async () => { try { - const listApiUrl = `/bt_studio/get_universes_list?project_name=${currentProject}`; - const response = await axios.get(listApiUrl); - setUniversesProjects(response.data.universes_list); + const response = await listUniverses(currentProject); + setUniversesProjects(response); setUniverseAdded(false); } catch (error) { console.error("Error while fetching universes list:", error); @@ -59,14 +58,11 @@ const UniverseModal = ({ } }; - const deleteUniverse = async (universe_name: string) => { + const deleteUniverseFunc = async (universe_name: string) => { try { - const apiUrl = `/bt_studio/delete_universe?project_name=${currentProject}&universe_name=${universe_name}`; - const response = await axios.get(apiUrl); - if (response.data.success) { - loadUniverseList(); - console.log("Universe deleted successfully"); - } + await deleteUniverse(currentProject, universe_name); + loadUniverseList(); + console.log("Universe deleted successfully"); } catch (error: any) { if (error.response) { // The request was made and the server responded with a status code @@ -148,7 +144,7 @@ const UniverseModal = ({ className="bt-project-entry-delete bt-icon" title="Delete" onClick={(e) => { - deleteUniverse(project[1]); + deleteUniverseFunc(project[1]); e.stopPropagation(); }} fill={"var(--icon)"} diff --git a/frontend/src/components/header_menu/modals/UniverseUploadModal.tsx b/frontend/src/components/header_menu/modals/UniverseUploadModal.tsx index abafeeec1..ec056ef01 100644 --- a/frontend/src/components/header_menu/modals/UniverseUploadModal.tsx +++ b/frontend/src/components/header_menu/modals/UniverseUploadModal.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react"; import "./UniverseUploadModal.css"; import Modal from "../../Modal/Modal"; import { ReactComponent as CloseIcon } from "../../Modal/img/close.svg"; -import axios from "axios"; +import { uploadUniverse } from "../../../api_helper/TreeWrapper"; const initialProjectData = { projectName: "", @@ -79,24 +79,9 @@ const UniverseUploadModal = ({ } try { - const response = await axios.post( - "/bt_studio/upload_universe/", - { - universe_name: universeName, - zip_file: uploadedUniverse, - app_name: currentProject, - }, - { - headers: { - //@ts-ignore Needed for compatibility with Unibotics - "X-CSRFToken": context.csrf, - }, - }, - ); - if (response.data.success) { - console.log("Universe saved successfully."); - setUniverseAdded(true); - } + await uploadUniverse(currentProject, universeName, uploadedUniverse); + console.log("Universe saved successfully."); + setUniverseAdded(true); } catch (error) { console.error("Axios Error:", error); } diff --git a/frontend/src/components/header_menu/modals/universe/CreatePage.tsx b/frontend/src/components/header_menu/modals/universe/CreatePage.tsx index 69c25194c..535d03f75 100644 --- a/frontend/src/components/header_menu/modals/universe/CreatePage.tsx +++ b/frontend/src/components/header_menu/modals/universe/CreatePage.tsx @@ -2,8 +2,10 @@ import React, { useState, useEffect, useRef, FormEventHandler } from "react"; import "./CreatePage.css"; import { ReactComponent as BackIcon } from "../../../Modal/img/back.svg"; import { ReactComponent as CloseIcon } from "../../../Modal/img/close.svg"; -import axios from "axios"; -import { createRoboticsBackendUniverse } from "../../../../api_helper/TreeWrapper"; +import { + createRoboticsBackendUniverse, + listDockerUniverses, +} from "../../../../api_helper/TreeWrapper"; const initialUniverseData = { universeName: "", @@ -31,9 +33,8 @@ const CreatePage = ({ const loadUniverseList = async () => { try { - const listApiUrl = `/bt_studio/list_docker_universes`; - const response = await axios.get(listApiUrl); - setUniversesDocker(response.data.universes); + const response = await listDockerUniverses(); + setUniversesDocker(response); } catch (error) { console.error("Error while fetching universes list:", error); openError(`An error occurred while fetching the universes list`); diff --git a/frontend/src/components/tree_editor/MainTreeEditorContainer.tsx b/frontend/src/components/tree_editor/MainTreeEditorContainer.tsx index 34aa9d3bc..23ad27ce7 100644 --- a/frontend/src/components/tree_editor/MainTreeEditorContainer.tsx +++ b/frontend/src/components/tree_editor/MainTreeEditorContainer.tsx @@ -1,5 +1,4 @@ import React, { useRef } from "react"; -import axios from "axios"; import { useState, useEffect } from "react"; import TreeEditor from "./TreeEditor"; import { @@ -7,6 +6,8 @@ import { getSubtree, saveSubtree, saveBaseTree, + getSubtreeStructure, + getTreeStructure, } from "../../api_helper/TreeWrapper"; import { TreeViewType, findSubtree } from "../helper/TreeEditorHelper"; import { OptionsContext } from "../options/Options"; @@ -80,50 +81,32 @@ const MainTreeEditorContainer = ({ // HELPERS - const getSubtreeStructure = async (name: string) => { - try { - const response = await axios.get("/bt_studio/get_subtree_structure/", { - params: { - project_name: projectName, - subtree_name: name, - bt_order: settings.btOrder.value, - }, - }); - if (response.data.success) { - return response.data.tree_structure; - } - } catch (error) { - console.error("Error fetching graph:", error); - } - }; - const getBTTree = async () => { try { - const response = await axios.get("/bt_studio/get_tree_structure/", { - params: { - project_name: projectName, - bt_order: settings.btOrder.value, - }, - }); - if (response.data.success) { - // Navigate until root using baseTree - var path: number[] = []; - var tree_structure = response.data.tree_structure; - for (let index = 0; index < treeHierarchy.length; index++) { - var nextSubtree = treeHierarchy[index]; - if (nextSubtree) { - var new_path = findSubtree(tree_structure, nextSubtree); - if (new_path) { - path = path.concat(new_path); //TODO: check if its not new_path.concat(path) - } - tree_structure = await getSubtreeStructure(nextSubtree); + var tree_structure = await getTreeStructure( + projectName, + settings.btOrder.value, + ); + // Navigate until root using baseTree + var path: number[] = []; + for (let index = 0; index < treeHierarchy.length; index++) { + var nextSubtree = treeHierarchy[index]; + if (nextSubtree) { + var new_path = findSubtree(tree_structure, nextSubtree); + if (new_path) { + path = path.concat(new_path); //TODO: check if its not new_path.concat(path) } - console.log("TreePath", path); + tree_structure = await getSubtreeStructure( + projectName, + nextSubtree, + settings.btOrder.value, + ); } - - setTreeStructure(tree_structure); - setSubTreeStructure(path); + console.log("TreePath", path); } + + setTreeStructure(tree_structure); + setSubTreeStructure(path); } catch (error) { console.error("Error fetching graph:", error); } From ed75ef3ce44e1590b71ff43b50bd1f32461f1bf4 Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Thu, 9 Jan 2025 14:07:45 +0100 Subject: [PATCH 24/31] Remove duplicate header --- frontend/src/api_helper/TreeWrapper.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/frontend/src/api_helper/TreeWrapper.ts b/frontend/src/api_helper/TreeWrapper.ts index a49c990b6..8619bea46 100644 --- a/frontend/src/api_helper/TreeWrapper.ts +++ b/frontend/src/api_helper/TreeWrapper.ts @@ -387,13 +387,7 @@ const getCustomUniverseZip = async ( app_name: currentProjectname, universe_name: universeName, }, - { - responseType: "blob", // Ensure the response is treated as a Blob - headers: { - //@ts-ignore Needed for compatibility with Unibotics - "X-CSRFToken": context.csrf, - }, - } + axiosExtra ); // Handle unsuccessful response status (e.g., non-2xx status) From 064433549d2f706efaece27b869b09151a283c84 Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Thu, 9 Jan 2025 15:10:30 +0100 Subject: [PATCH 25/31] Escape \ correctly --- frontend/src/templates/TreeGardener.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/templates/TreeGardener.ts b/frontend/src/templates/TreeGardener.ts index e763cb40a..077d7e0ed 100644 --- a/frontend/src/templates/TreeGardener.ts +++ b/frontend/src/templates/TreeGardener.ts @@ -1,5 +1,7 @@ import JSZip from "jszip"; +// NOTE: Make sure to escape \ character + const treeTools = `import re import py_trees @@ -77,7 +79,7 @@ def ascii_tree_to_json(tree): json_str = '"tree":{' # Remove escape chars - ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") + ansi_escape = re.compile(r"\\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])") tree = ansi_escape.sub("", tree) for line in iter(tree.splitlines()): @@ -109,7 +111,7 @@ def ascii_blackboard_to_json(blackboard): json_str = '"blackboard":{' do_append_coma = False - ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") + ansi_escape = re.compile(r"\\\\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])") blackboard = ansi_escape.sub("", blackboard) for line in iter(blackboard.splitlines()): From 20f11eae77e629f377358fbfbd1277f027b3e9e9 Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Thu, 9 Jan 2025 15:33:07 +0100 Subject: [PATCH 26/31] Fix state check in reset App --- frontend/src/components/header_menu/HeaderMenu.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/header_menu/HeaderMenu.tsx b/frontend/src/components/header_menu/HeaderMenu.tsx index f9cde5c1f..ac69afca8 100644 --- a/frontend/src/components/header_menu/HeaderMenu.tsx +++ b/frontend/src/components/header_menu/HeaderMenu.tsx @@ -310,6 +310,7 @@ const HeaderMenu = ({ if (!gazeboEnabled) { console.error("Simulation is not ready!"); + return; } await manager.terminateApplication(); From 22469980b3c4dfd3b4075ed58c5d07058885376b Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Thu, 9 Jan 2025 16:25:50 +0100 Subject: [PATCH 27/31] Change save diagram method --- backend/tree_api/views.py | 37 ++++++++---- frontend/src/App.tsx | 9 ++- frontend/src/api_helper/TreeWrapper.ts | 58 +++++++++---------- .../src/components/header_menu/HeaderMenu.tsx | 12 ++-- .../tree_editor/MainTreeEditorContainer.tsx | 22 ++++--- 5 files changed, 74 insertions(+), 64 deletions(-) diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index 0b02008c9..db15c5cf5 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -946,11 +946,7 @@ def save_file(request): @api_view(["POST"]) def generate_local_app(request): # Check if 'app_name', 'main_tree_graph', and 'bt_order' are in the request data - if ( - "app_name" not in request.data - or "tree_graph" not in request.data - or "bt_order" not in request.data - ): + if "app_name" not in request.data or "bt_order" not in request.data: return Response( {"success": False, "message": "Missing required parameters"}, status=status.HTTP_400_BAD_REQUEST, @@ -958,22 +954,31 @@ def generate_local_app(request): # Get the request parameters app_name = request.data.get("app_name") - main_tree_graph = request.data.get("tree_graph") bt_order = request.data.get("bt_order") # Make folder path relative to Django app base_path = os.path.join(settings.BASE_DIR, "filesystem") project_path = os.path.join(base_path, app_name) action_path = os.path.join(project_path, "code/actions") - + tree_path = os.path.join(project_path, "code/trees/main.json") subtree_path = os.path.join(project_path, "code/trees/subtrees/json") subtrees = [] actions = [] try: + + # Check if the project exists + if os.path.exists(tree_path): + with open(tree_path, "r") as f: + graph_data = f.read() + else: + return Response( + {"success": False, "message": "Main tree not found"}, + status=status.HTTP_400_BAD_REQUEST, + ) # 1. Generate a basic tree from the JSON definition - main_tree = json_translator.translate_raw(main_tree_graph, bt_order) + main_tree = json_translator.translate_raw(graph_data, bt_order) # 2. Get all possible subtrees name and content try: @@ -1028,7 +1033,6 @@ def generate_dockerized_app(request): # Check if 'app_name', 'tree_graph', and 'bt_order' are in the request data if ( "app_name" not in request.data - or "tree_graph" not in request.data or "bt_order" not in request.data ): return Response( @@ -1038,22 +1042,31 @@ def generate_dockerized_app(request): # Get the request parameters app_name = request.data.get("app_name") - main_tree_graph = request.data.get("tree_graph") bt_order = request.data.get("bt_order") # Make folder path relative to Django app base_path = os.path.join(settings.BASE_DIR, "filesystem") project_path = os.path.join(base_path, app_name) action_path = os.path.join(project_path, "code/actions") - + tree_path = os.path.join(project_path, "code/trees/main.json") subtree_path = os.path.join(project_path, "code/trees/subtrees/json") subtrees = [] actions = [] try: + + # Check if the project exists + if os.path.exists(tree_path): + with open(tree_path, "r") as f: + graph_data = f.read() + else: + return Response( + {"success": False, "message": "Main tree not found"}, + status=status.HTTP_400_BAD_REQUEST, + ) # 1. Generate a basic tree from the JSON definition - main_tree = json_translator.translate_raw(main_tree_graph, bt_order) + main_tree = json_translator.translate_raw(graph_data, bt_order) # 2. Get all possible subtrees name and content try: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9035a3ed7..55aceed2e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -28,7 +28,7 @@ const App = ({ isUnibotics }: { isUnibotics: boolean }) => { const [actionNodesData, setActionNodesData] = useState>( {}, ); - const [modelJson, setModelJson] = useState(""); + const [saveCurrentDiagram, setSaveCurrentDiagram] = useState(false); const [isErrorModalOpen, setErrorModalOpen] = useState(false); const [projectChanges, setProjectChanges] = useState(false); const [gazeboEnabled, setGazeboEnabled] = useState(false); @@ -158,12 +158,11 @@ const App = ({ isUnibotics }: { isUnibotics: boolean }) => { setCurrentProjectname={setCurrentProjectname} currentUniverseName={currentUniverseName} setCurrentUniverseName={setCurrentUniverseName} - modelJson={modelJson} + setSaveCurrentDiagram={setSaveCurrentDiagram} projectChanges={projectChanges} setProjectChanges={setProjectChanges} gazeboEnabled={gazeboEnabled} setGazeboEnabled={setGazeboEnabled} - // onSetShowExecStatus={onSetShowExecStatus} manager={manager} showVNCViewer={showVNCViewer} isUnibotics={isUnibotics} @@ -244,8 +243,8 @@ const App = ({ isUnibotics }: { isUnibotics: boolean }) => { ) : (

Loading...

diff --git a/frontend/src/api_helper/TreeWrapper.ts b/frontend/src/api_helper/TreeWrapper.ts index 8619bea46..206f6c505 100644 --- a/frontend/src/api_helper/TreeWrapper.ts +++ b/frontend/src/api_helper/TreeWrapper.ts @@ -405,11 +405,9 @@ const getCustomUniverseZip = async ( // App management const generateLocalApp = async ( - modelJson: Object, currentProjectname: string, btOrder: string ) => { - if (!modelJson) throw new Error("Tree JSON is empty!"); if (!currentProjectname) throw new Error("Current Project name is not set"); if (!btOrder) throw new Error("Behavior Tree order is not set"); @@ -419,7 +417,6 @@ const generateLocalApp = async ( apiUrl, { app_name: currentProjectname, - tree_graph: JSON.stringify(modelJson), bt_order: btOrder, }, axiosExtra @@ -437,11 +434,9 @@ const generateLocalApp = async ( }; const generateDockerizedApp = async ( - modelJson: Object, currentProjectname: string, btOrder: string ) => { - if (!modelJson) throw new Error("Tree JSON is empty!"); if (!currentProjectname) throw new Error("Current Project name is not set"); if (!btOrder) throw new Error("Behavior Tree order is not set"); @@ -451,7 +446,6 @@ const generateDockerizedApp = async ( apiUrl, { app_name: currentProjectname, - tree_graph: JSON.stringify(modelJson), bt_order: btOrder, }, axiosExtra @@ -945,39 +939,39 @@ const getSubtreeStructure = async (projectName:string, subtreeName:string, btOr // Named export export { + createAction, + createFile, + createFolder, createProject, + createRoboticsBackendUniverse, + createSubtree, + deleteFile, + deleteFolder, deleteProject, - saveBaseTree, - saveFile, + deleteUniverse, + generateDockerizedApp, + generateLocalApp, + getActionsList, + getCustomUniverseZip, getFile, - loadProjectConfig, + getFileList, getProjectGraph, - generateLocalApp, - generateDockerizedApp, - getUniverseConfig, getRoboticsBackendUniversePath, - getCustomUniverseZip, - createSubtree, - getSubtreeList, getSubtree, - getFileList, - getActionsList, - saveSubtree, - createRoboticsBackendUniverse, - deleteUniverse, - saveProjectConfig, - uploadFile, - createAction, - createFile, - createFolder, - renameFile, - renameFolder, - deleteFile, - deleteFolder, - uploadUniverse, + getSubtreeList, + getSubtreeStructure, + getTreeStructure, + getUniverseConfig, listDockerUniverses, listProjects, listUniverses, - getSubtreeStructure, - getTreeStructure + loadProjectConfig, + renameFile, + renameFolder, + saveBaseTree, + saveFile, + saveProjectConfig, + saveSubtree, + uploadFile, + uploadUniverse, }; diff --git a/frontend/src/components/header_menu/HeaderMenu.tsx b/frontend/src/components/header_menu/HeaderMenu.tsx index ac69afca8..14720535e 100644 --- a/frontend/src/components/header_menu/HeaderMenu.tsx +++ b/frontend/src/components/header_menu/HeaderMenu.tsx @@ -38,12 +38,11 @@ const HeaderMenu = ({ setCurrentProjectname, currentUniverseName, setCurrentUniverseName, - modelJson, + setSaveCurrentDiagram, projectChanges, setProjectChanges, gazeboEnabled, setGazeboEnabled, - // onSetShowExecStatus, manager, showVNCViewer, isUnibotics, @@ -52,7 +51,7 @@ const HeaderMenu = ({ setCurrentProjectname: Function; currentUniverseName: string; setCurrentUniverseName: Function; - modelJson: string; + setSaveCurrentDiagram: Function; projectChanges: boolean; setProjectChanges: Function; gazeboEnabled: boolean; @@ -183,7 +182,8 @@ const HeaderMenu = ({ return; } try { - await saveBaseTree(modelJson, currentProjectname); + //TODO: check if possible consurrency problems + setSaveCurrentDiagram(true); setProjectChanges(false); console.log("Project saved"); } catch (error) { @@ -197,9 +197,10 @@ const HeaderMenu = ({ const onDownloadApp = async () => { try { + await onSaveProject(); + // Get the blob from the API wrapper const appFiles = await generateLocalApp( - modelJson, currentProjectname, settings.btOrder.value, ); @@ -254,7 +255,6 @@ const HeaderMenu = ({ try { // Get the blob from the API wrapper const appFiles = await generateDockerizedApp( - modelJson, currentProjectname, settings.btOrder.value, ); diff --git a/frontend/src/components/tree_editor/MainTreeEditorContainer.tsx b/frontend/src/components/tree_editor/MainTreeEditorContainer.tsx index 23ad27ce7..408b10b63 100644 --- a/frontend/src/components/tree_editor/MainTreeEditorContainer.tsx +++ b/frontend/src/components/tree_editor/MainTreeEditorContainer.tsx @@ -15,13 +15,13 @@ import { OptionsContext } from "../options/Options"; const MainTreeEditorContainer = ({ projectName, setProjectEdited, - setGlobalJson, - modelJson, + saveCurrentDiagram, + setSaveCurrentDiagram, }: { projectName: string; setProjectEdited: React.Dispatch>; - setGlobalJson: Function; - modelJson: any; + saveCurrentDiagram: boolean; + setSaveCurrentDiagram: Function; }) => { const settings = React.useContext(OptionsContext); @@ -55,6 +55,7 @@ const MainTreeEditorContainer = ({ } setProjectEdited(false); }; + // Load const load = async () => { try { @@ -122,15 +123,18 @@ const MainTreeEditorContainer = ({ // EFFECTS useEffect(() => { - // When no subtree is selected, set the global json - if (!subTreeName) { - setGlobalJson(resultJson); + if (saveCurrentDiagram) { + save(resultJson, subTreeName); + setSaveCurrentDiagram(false); } - }, [resultJson]); + }, [saveCurrentDiagram]); useEffect(() => { - // We can go back again now + // Reset everything setWentBack(false); + setGoBack(false); + setTreeHierarchy([]); + setSubTreeName(""); // Fetch the new subtree or project graph load(); From 8432155610db7750fe0c887c9b3a55ce53c1b267 Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Thu, 9 Jan 2025 17:05:18 +0100 Subject: [PATCH 28/31] New add subtree modal --- backend/tree_api/views.py | 5 +- .../components/file_browser/FileBrowser.js | 2 +- ...{NewFolderModal.jsx => NewFolderModal.tsx} | 35 ++- .../src/components/tree_editor/NodeMenu.tsx | 255 +++++++++++------- .../tree_editor/modals/AddSubtreeModal.tsx | 141 ++++++++++ 5 files changed, 321 insertions(+), 117 deletions(-) rename frontend/src/components/file_browser/modals/{NewFolderModal.jsx => NewFolderModal.tsx} (83%) create mode 100644 frontend/src/components/tree_editor/modals/AddSubtreeModal.tsx diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index db15c5cf5..1aaa449ee 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -1031,10 +1031,7 @@ def generate_local_app(request): @api_view(["POST"]) def generate_dockerized_app(request): # Check if 'app_name', 'tree_graph', and 'bt_order' are in the request data - if ( - "app_name" not in request.data - or "bt_order" not in request.data - ): + if "app_name" not in request.data or "bt_order" not in request.data: return Response( {"success": False, "message": "Missing required parameters"}, status=status.HTTP_400_BAD_REQUEST, diff --git a/frontend/src/components/file_browser/FileBrowser.js b/frontend/src/components/file_browser/FileBrowser.js index fa309e3dd..aad741f93 100644 --- a/frontend/src/components/file_browser/FileBrowser.js +++ b/frontend/src/components/file_browser/FileBrowser.js @@ -3,7 +3,7 @@ import JSZip from "jszip"; import "./FileBrowser.css"; import NewFileModal from "./modals/NewFileModal.jsx"; import RenameModal from "./modals/RenameModal.jsx"; -import NewFolderModal from "./modals/NewFolderModal.jsx"; +import NewFolderModal from "./modals/NewFolderModal"; import UploadModal from "./modals/UploadModal.tsx"; import DeleteModal from "./modals/DeleteModal.jsx"; import FileExplorer from "./file_explorer/FileExplorer.jsx"; diff --git a/frontend/src/components/file_browser/modals/NewFolderModal.jsx b/frontend/src/components/file_browser/modals/NewFolderModal.tsx similarity index 83% rename from frontend/src/components/file_browser/modals/NewFolderModal.jsx rename to frontend/src/components/file_browser/modals/NewFolderModal.tsx index 29fc71118..8827d3705 100644 --- a/frontend/src/components/file_browser/modals/NewFolderModal.jsx +++ b/frontend/src/components/file_browser/modals/NewFolderModal.tsx @@ -7,11 +7,30 @@ const initialNewFolderModalData = { folderName: "", }; -const NewFolderModal = ({ onSubmit, isOpen, onClose, fileList, location }) => { - const focusInputRef = useRef(null); +interface Entry { + name: string; + is_dir: boolean; + path: string; + files: Entry[]; +} + +const NewFolderModal = ({ + onSubmit, + isOpen, + onClose, + fileList, + location, +}: { + onSubmit: Function; + isOpen: boolean; + onClose: Function; + fileList: any; + location: string; +}) => { + const focusInputRef = useRef(null); const [formState, setFormState] = useState(initialNewFolderModalData); const [isCreationAllowed, allowCreation] = useState(false); - const [searchList, setSearchList] = useState(null); + const [searchList, setSearchList] = useState([]); useEffect(() => { if (isOpen && focusInputRef.current) { @@ -28,7 +47,7 @@ const NewFolderModal = ({ onSubmit, isOpen, onClose, fileList, location }) => { for (let index = 0; index < path.length; index++) { search_list = search_list.find( - (entry) => entry.name === path[index] && entry.is_dir, + (entry: Entry) => entry.name === path[index] && entry.is_dir, ).files; } } @@ -41,7 +60,7 @@ const NewFolderModal = ({ onSubmit, isOpen, onClose, fileList, location }) => { } }, [isOpen]); - const handleInputChange = (event) => { + const handleInputChange = (event: React.ChangeEvent) => { const { name, value } = event.target; var isValidName = true; @@ -67,7 +86,7 @@ const NewFolderModal = ({ onSubmit, isOpen, onClose, fileList, location }) => { } }; - const handleSubmit = (event) => { + const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); onSubmit(location, formState.folderName); setFormState(initialNewFolderModalData); @@ -75,7 +94,7 @@ const NewFolderModal = ({ onSubmit, isOpen, onClose, fileList, location }) => { onClose(); }; - const handleCancel = (event) => { + const handleCancel = (event: React.FormEvent | null) => { if (event) { event.preventDefault(); } @@ -103,7 +122,7 @@ const NewFolderModal = ({ onSubmit, isOpen, onClose, fileList, location }) => { { - handleCancel(); + handleCancel(null); }} fill={"var(--icon)"} /> diff --git a/frontend/src/components/tree_editor/NodeMenu.tsx b/frontend/src/components/tree_editor/NodeMenu.tsx index ce899d062..3ce193fe4 100644 --- a/frontend/src/components/tree_editor/NodeMenu.tsx +++ b/frontend/src/components/tree_editor/NodeMenu.tsx @@ -17,6 +17,7 @@ import { getActionsList, } from "../../api_helper/TreeWrapper"; import { TreeViewType } from "../helper/TreeEditorHelper"; +import AddSubtreeModal from "./modals/AddSubtreeModal"; var NODE_MENU_ITEMS: Record = { Sequences: ["Sequence", "ReactiveSequence", "SequenceWithMemory"], @@ -92,6 +93,8 @@ const NodeMenu = ({ }) => { const [anchorEl, setAnchorEl] = useState(null); const [menuLabel, setMenuLabel] = useState(""); + const [isNewSubtreeModalOpen, setNewSubtreeModalOpen] = + useState(false); useEffect(() => { const fetchData = async () => { @@ -148,122 +151,166 @@ const NodeMenu = ({ } }; + const handleCreateSubtree = () => { + setNewSubtreeModalOpen(true); + }; + + const handleCloseCreateFolder = () => { + setNewSubtreeModalOpen(false); + var subtree_input = document.getElementById( + "subTreeName", + ) as HTMLInputElement; + if (subtree_input) { + subtree_input.value = ""; + } + }; + + const handleCreateFolderSubmit = async (subtreeName: string) => { + if (subtreeName !== "") { + try { + const subtreeId = await createSubtree(subtreeName, projectName); + console.log("Created subtree:", subtreeId); + fetchSubtreeList(projectName); + } catch (error) { + console.error("Failed to create subtree:", error); + } + } + }; + return ( -
-
- {Object.keys(NODE_MENU_ITEMS).map((label) => { - if (label === "Subtrees" && !hasSubtrees) { - return null; - } - return ( + <> +
+
+ {Object.keys(NODE_MENU_ITEMS).map((label) => { + if (label === "Subtrees" && !hasSubtrees) { + return null; + } + return ( + + ); + })} +
+ + + {NODE_MENU_ITEMS[menuLabel]?.map((item) => ( + handleSelect(item)}> + {item} + + ))} + + +
+ {hasSubtrees && ( - ); - })} -
- - - {NODE_MENU_ITEMS[menuLabel]?.map((item) => ( - handleSelect(item)}> - {item} - - ))} - - -
- {hasSubtrees && ( + )} - )} - - - - - - + + + + + +
+

{subTreeName}

-

{subTreeName}

-
+ + ); }; diff --git a/frontend/src/components/tree_editor/modals/AddSubtreeModal.tsx b/frontend/src/components/tree_editor/modals/AddSubtreeModal.tsx new file mode 100644 index 000000000..c5565ad80 --- /dev/null +++ b/frontend/src/components/tree_editor/modals/AddSubtreeModal.tsx @@ -0,0 +1,141 @@ +import React, { useState, useEffect, useRef } from "react"; +import Modal from "../../Modal/Modal"; + +import { ReactComponent as CloseIcon } from "../../Modal/img/close.svg"; + +const initialAddSubtreeModalData = { + subTreeName: "", +}; + +const AddSubtreeModal = ({ + onSubmit, + isOpen, + onClose, + subTreeList, +}: { + onSubmit: Function; + isOpen: boolean; + onClose: Function; + subTreeList: string[]; +}) => { + const focusInputRef = useRef(null); + const [formState, setFormState] = useState(initialAddSubtreeModalData); + const [isCreationAllowed, allowCreation] = useState(false); + + useEffect(() => { + if (isOpen && focusInputRef.current) { + setTimeout(() => { + focusInputRef.current.focus(); + }, 0); + } + }, [isOpen]); + + const handleInputChange = (event: React.ChangeEvent) => { + const { name, value } = event.target; + var isValidName = true; + + setFormState((prevFormData) => ({ + ...prevFormData, + [name]: value, + })); + + if (name === "subTreeName") { + if (value !== "" && !value.includes(".")) { + subTreeList.some((element: string) => { + if (element === value) { + isValidName = false; + return true; + } + return false; + }); + } else { + isValidName = false; + } + + allowCreation(isValidName); + } + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + onSubmit(formState.subTreeName); + setFormState(initialAddSubtreeModalData); + allowCreation(false); + onClose(); + }; + + const handleCancel = (event: React.FormEvent | null) => { + if (event) { + event.preventDefault(); + } + onClose(); + setFormState(initialAddSubtreeModalData); + allowCreation(false); + }; + + return ( + + +
+ + { + handleCancel(null); + }} + fill={"var(--icon)"} + /> +
+
+
+ + +
+
+
+
+ +
+
+ +
+ ); +}; + +export default AddSubtreeModal; From f8eb564fb190030c290e2205c2c0142a987da04f Mon Sep 17 00:00:00 2001 From: Javier Izquierdo Hernandez Date: Thu, 9 Jan 2025 18:15:14 +0100 Subject: [PATCH 29/31] Fix update problems --- backend/tree_api/templates.py | 2 +- backend/tree_api/views.py | 2 +- frontend/src/App.tsx | 9 +- .../components/file_browser/FileBrowser.js | 14 +++ .../src/components/header_menu/HeaderMenu.tsx | 1 - .../src/components/helper/TreeEditorHelper.ts | 4 +- .../tree_editor/DiagramVisualizer.tsx | 8 +- .../tree_editor/MainTreeEditorContainer.tsx | 8 ++ .../src/components/tree_editor/NodeMenu.tsx | 33 +++---- .../tree_editor/NodeMenuMinimal.tsx | 99 +++++++++++++++++++ .../src/components/tree_editor/TreeEditor.tsx | 6 ++ 11 files changed, 156 insertions(+), 30 deletions(-) create mode 100644 frontend/src/components/tree_editor/NodeMenuMinimal.tsx diff --git a/backend/tree_api/templates.py b/backend/tree_api/templates.py index 4efdc1bf7..3b82ffc01 100644 --- a/backend/tree_api/templates.py +++ b/backend/tree_api/templates.py @@ -6,6 +6,6 @@ def get_action_template(filename, template, template_path): with open(template_path, "r") as temp: file_data = temp.read() - new_data = file_data.replace("ACTION", filename[:-3]) + new_data = file_data.replace("ACTION", filename) return new_data return "" diff --git a/backend/tree_api/views.py b/backend/tree_api/views.py index 1aaa449ee..14004145b 100644 --- a/backend/tree_api/views.py +++ b/backend/tree_api/views.py @@ -675,7 +675,7 @@ def create_action(request): folder_path = os.path.join(settings.BASE_DIR, "filesystem") project_path = os.path.join(folder_path, project_name) action_path = os.path.join(project_path, "code/actions") - file_path = os.path.join(action_path, filename) + file_path = os.path.join(action_path, filename + ".py") templates_folder_path = os.path.join(settings.BASE_DIR, "templates") template_path = os.path.join(templates_folder_path, template) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 55aceed2e..6952aa47c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -26,9 +26,10 @@ const App = ({ isUnibotics }: { isUnibotics: boolean }) => { const [currentProjectname, setCurrentProjectname] = useState(""); const [currentUniverseName, setCurrentUniverseName] = useState(""); const [actionNodesData, setActionNodesData] = useState>( - {}, + {} ); const [saveCurrentDiagram, setSaveCurrentDiagram] = useState(false); + const [updateFileExplorer, setUpdateFileExplorer] = useState(false); const [isErrorModalOpen, setErrorModalOpen] = useState(false); const [projectChanges, setProjectChanges] = useState(false); const [gazeboEnabled, setGazeboEnabled] = useState(false); @@ -195,6 +196,11 @@ const App = ({ isUnibotics }: { isUnibotics: boolean }) => { setAutosave={setAutosave} forceSaveCurrent={forceSaveCurrent} setForcedSaveCurrent={setForcedSaveCurrent} + forceUpdate={{ + value: updateFileExplorer, + callback: setUpdateFileExplorer, + }} + setSaveCurrentDiagram={setSaveCurrentDiagram} />
@@ -245,6 +251,7 @@ const App = ({ isUnibotics }: { isUnibotics: boolean }) => { setProjectEdited={setProjectChanges} saveCurrentDiagram={saveCurrentDiagram} setSaveCurrentDiagram={setSaveCurrentDiagram} + updateFileExplorer={setUpdateFileExplorer} /> ) : (

Loading...

diff --git a/frontend/src/components/file_browser/FileBrowser.js b/frontend/src/components/file_browser/FileBrowser.js index aad741f93..94c4bd8b9 100644 --- a/frontend/src/components/file_browser/FileBrowser.js +++ b/frontend/src/components/file_browser/FileBrowser.js @@ -47,6 +47,8 @@ const FileBrowser = ({ setAutosave, forceSaveCurrent, setForcedSaveCurrent, + forceUpdate, + setSaveCurrentDiagram }) => { const [fileList, setFileList] = useState(null); const [isNewFileModalOpen, setNewFileModalOpen] = useState(false); @@ -64,6 +66,13 @@ const FileBrowser = ({ updateSelectedLocation(null); }, [selectedEntry]); + useEffect(() => { + if (forceUpdate.value) { + forceUpdate.callback(false); + fetchFileList() + } + }, [forceUpdate.value]); + const updateSelectedLocation = (file) => { if (file) { setSelectedLocation(getParentDir(file)); @@ -94,6 +103,7 @@ const FileBrowser = ({ const handleCreateFile = (file) => { updateSelectedLocation(file); setNewFileModalOpen(true); + setSaveCurrentDiagram(true); }; const handleCloseNewFileModal = () => { @@ -134,6 +144,7 @@ const FileBrowser = ({ setDeleteEntry(file_path); setDeleteType(is_dir); setDeleteModalOpen(true); + setSaveCurrentDiagram(true); } else { alert("No file is currently selected."); } @@ -188,6 +199,7 @@ const FileBrowser = ({ const handleCreateFolder = (file) => { updateSelectedLocation(file); setNewFolderModalOpen(true); + setSaveCurrentDiagram(true); }; const handleCloseCreateFolder = () => { @@ -213,6 +225,7 @@ const FileBrowser = ({ if (file) { setRenameEntry(file); setRenameModalOpen(true); + setSaveCurrentDiagram(true); } else { alert("No file is currently selected."); } @@ -275,6 +288,7 @@ const FileBrowser = ({ const handleUpload = (file) => { updateSelectedLocation(file); setUploadModalOpen(true); + setSaveCurrentDiagram(true); }; const handleCloseUploadModal = () => { diff --git a/frontend/src/components/header_menu/HeaderMenu.tsx b/frontend/src/components/header_menu/HeaderMenu.tsx index 14720535e..563d8a075 100644 --- a/frontend/src/components/header_menu/HeaderMenu.tsx +++ b/frontend/src/components/header_menu/HeaderMenu.tsx @@ -4,7 +4,6 @@ import AppBar from "@mui/material/AppBar"; import Toolbar from "@mui/material/Toolbar"; import { createProject, - saveBaseTree, generateLocalApp, generateDockerizedApp, getUniverseConfig, diff --git a/frontend/src/components/helper/TreeEditorHelper.ts b/frontend/src/components/helper/TreeEditorHelper.ts index 67701433e..6d52fc631 100644 --- a/frontend/src/components/helper/TreeEditorHelper.ts +++ b/frontend/src/components/helper/TreeEditorHelper.ts @@ -255,7 +255,9 @@ export const configureEngine = ( // Disable loose links const state: any = engine.current.getStateMachine().getCurrentState(); - state.dragNewLink.config.allowLooseLinks = false; + if (state) { + state.dragNewLink.config.allowLooseLinks = false; + } engine.current .getActionEventBus() diff --git a/frontend/src/components/tree_editor/DiagramVisualizer.tsx b/frontend/src/components/tree_editor/DiagramVisualizer.tsx index 72dc4594b..534863a2a 100644 --- a/frontend/src/components/tree_editor/DiagramVisualizer.tsx +++ b/frontend/src/components/tree_editor/DiagramVisualizer.tsx @@ -12,6 +12,7 @@ import { BasicNodeModel } from "./nodes/basic_node/BasicNodeModel"; import { TagNodeModel } from "./nodes/tag_node/TagNodeModel"; import "./TreeEditor.css"; +import NodeMenuMinimal from "./NodeMenuMinimal"; const setTreeStatus = ( model: DiagramModel, @@ -234,13 +235,8 @@ const DiagramVisualizer = memo( return ( <> - {}} - onDeleteNode={() => {}} + {}} - hasSubtrees={false} view={view} changeView={changeView} setGoBack={setGoBack} diff --git a/frontend/src/components/tree_editor/MainTreeEditorContainer.tsx b/frontend/src/components/tree_editor/MainTreeEditorContainer.tsx index 408b10b63..a100f08d5 100644 --- a/frontend/src/components/tree_editor/MainTreeEditorContainer.tsx +++ b/frontend/src/components/tree_editor/MainTreeEditorContainer.tsx @@ -17,11 +17,13 @@ const MainTreeEditorContainer = ({ setProjectEdited, saveCurrentDiagram, setSaveCurrentDiagram, + updateFileExplorer, }: { projectName: string; setProjectEdited: React.Dispatch>; saveCurrentDiagram: boolean; setSaveCurrentDiagram: Function; + updateFileExplorer: Function; }) => { const settings = React.useContext(OptionsContext); @@ -46,6 +48,11 @@ const MainTreeEditorContainer = ({ if (currentView.current !== TreeViewType.Editor) { return; } + + if (!json) { + return; + } + // If in subtree save subtree, else save base tree if (subtree) { await saveSubtree(json, projectName, subtree); @@ -229,6 +236,7 @@ const MainTreeEditorContainer = ({ subTreeName={subTreeName} setGoBack={setGoBack} subTreeStructure={subTreeStructure} + updateFileExplorer={updateFileExplorer} /> ) : (

Loading...

// Display a loading message until the graph is fetched diff --git a/frontend/src/components/tree_editor/NodeMenu.tsx b/frontend/src/components/tree_editor/NodeMenu.tsx index 3ce193fe4..29a9839f7 100644 --- a/frontend/src/components/tree_editor/NodeMenu.tsx +++ b/frontend/src/components/tree_editor/NodeMenu.tsx @@ -58,6 +58,7 @@ const fetchSubtreeList = async (project_name: string) => { console.log("Subtree list:", subtreeList); if (Array.isArray(subtreeList)) { NODE_MENU_ITEMS["Subtrees"] = subtreeList; + return; } else { console.error("API response is not an array:", subtreeList); } @@ -66,6 +67,7 @@ const fetchSubtreeList = async (project_name: string) => { console.error("Error fetching subtrees:", error.message); } } + NODE_MENU_ITEMS["Subtrees"] = []; }; const NodeMenu = ({ @@ -79,6 +81,7 @@ const NodeMenu = ({ changeView, setGoBack, subTreeName, + updateFileExplorer }: { projectName: string; onAddNode: Function; @@ -90,6 +93,7 @@ const NodeMenu = ({ changeView: Function; setGoBack: Function; subTreeName: string; + updateFileExplorer: Function; }) => { const [anchorEl, setAnchorEl] = useState(null); const [menuLabel, setMenuLabel] = useState(""); @@ -107,10 +111,13 @@ const NodeMenu = ({ const handleClick = ( event: React.MouseEvent, - label: string, + label: string ) => { setAnchorEl(event.currentTarget); setMenuLabel(label); + if (label === "Actions") { + fetchActionList(projectName); + } }; const handleClose = () => setAnchorEl(null); @@ -118,7 +125,7 @@ const NodeMenu = ({ const handleSelect = (nodeName: string) => { console.log("Selected: " + nodeName); const nodeType = Object.keys(NODE_MENU_ITEMS).find((key) => - NODE_MENU_ITEMS[key].includes(nodeName), + NODE_MENU_ITEMS[key].includes(nodeName) ); if (nodeType) { console.log("Node Type: " + nodeType); @@ -138,19 +145,6 @@ const NodeMenu = ({ } }; - const onCreateSubtree = async () => { - try { - const subtreeName = prompt("Enter subtree name:"); - if (subtreeName) { - const subtreeId = await createSubtree(subtreeName, projectName); - console.log("Created subtree:", subtreeId); - fetchSubtreeList(projectName); - } - } catch (error) { - console.error("Failed to create subtree:", error); - } - }; - const handleCreateSubtree = () => { setNewSubtreeModalOpen(true); }; @@ -158,7 +152,7 @@ const NodeMenu = ({ const handleCloseCreateFolder = () => { setNewSubtreeModalOpen(false); var subtree_input = document.getElementById( - "subTreeName", + "subTreeName" ) as HTMLInputElement; if (subtree_input) { subtree_input.value = ""; @@ -171,6 +165,7 @@ const NodeMenu = ({ const subtreeId = await createSubtree(subtreeName, projectName); console.log("Created subtree:", subtreeId); fetchSubtreeList(projectName); + updateFileExplorer(true); } catch (error) { console.error("Failed to create subtree:", error); } @@ -264,8 +259,8 @@ const NodeMenu = ({ onClick={() => { openInNewTab( new URL( - "https://github.com/JdeRobot/bt-studio/tree/unibotics-devel/documentation", - ), + "https://github.com/JdeRobot/bt-studio/tree/unibotics-devel/documentation" + ) ); }} title="Help" @@ -279,7 +274,7 @@ const NodeMenu = ({ changeView( view === TreeViewType.Editor ? TreeViewType.Visualizer - : TreeViewType.Editor, + : TreeViewType.Editor ) } title="Change view" diff --git a/frontend/src/components/tree_editor/NodeMenuMinimal.tsx b/frontend/src/components/tree_editor/NodeMenuMinimal.tsx new file mode 100644 index 000000000..16e2c7eb0 --- /dev/null +++ b/frontend/src/components/tree_editor/NodeMenuMinimal.tsx @@ -0,0 +1,99 @@ +import React, { MouseEventHandler} from "react"; +import "./NodeMenu.css"; + +import { ReactComponent as HelpIcon } from "./img/help.svg"; +import { ReactComponent as ZoomToFitIcon } from "./img/zoom_to_fit.svg"; +import { ReactComponent as EyeOpenIcon } from "./img/eye_open.svg"; +import { ReactComponent as EyeClosedIcon } from "./img/eye_closed.svg"; +import { ReactComponent as ReturnIcon } from "./img/return.svg"; + +import { TreeViewType } from "../helper/TreeEditorHelper"; + +const NodeMenuMinimal = ({ + onZoomToFit, + view, + changeView, + setGoBack, + subTreeName, +}: { + onZoomToFit: MouseEventHandler; + view: TreeViewType; + changeView: Function; + setGoBack: Function; + subTreeName: string; +}) => { + const openInNewTab = (url: URL) => { + const newWindow = window.open(url, "_blank"); + if (newWindow) { + newWindow.focus(); + } else { + console.error("Failed to open new tab/window."); + } + }; + + return ( + <> +
+
+ + + + +
+

{subTreeName}

+
+ + ); +}; + +export default NodeMenuMinimal; diff --git a/frontend/src/components/tree_editor/TreeEditor.tsx b/frontend/src/components/tree_editor/TreeEditor.tsx index 3549715d3..866a34629 100644 --- a/frontend/src/components/tree_editor/TreeEditor.tsx +++ b/frontend/src/components/tree_editor/TreeEditor.tsx @@ -50,6 +50,7 @@ const TreeEditor = memo( subTreeName, setGoBack, subTreeStructure, + updateFileExplorer }: { modelJson: any; setResultJson: Function; @@ -63,6 +64,7 @@ const TreeEditor = memo( subTreeName: string; setGoBack: Function; subTreeStructure: number[]; + updateFileExplorer: Function; }) => { const settings = React.useContext(OptionsContext); @@ -159,6 +161,7 @@ const TreeEditor = memo( setCurrentNode={setCurrentNode} setGoBack={setGoBack} subTreeName={subTreeName} + updateFileExplorer={updateFileExplorer} /> )}