.. _camera_example: Full-code customization tutorial: Camera app ============================================ This tutorial shows how to create a full code customization of the web app. It is intended for advanced users for whom the default customization options are not sufficient. .. note:: This tutorial assumes that you have some knowledge of Python and the Python-based UI framework, Dash. For information on Dash, see the `Dash documentation `_. Code You can access the :ref:`full code ` at the end of this document or download it :download:`here `. Project: You can download the optiSLang project used in this tutorial :download:`here `. The simplified tutorial project consists of nodes that run Python code without calling any other Ansys software. The aim is to mimic the behavior of an optiSLang project that runs an optical simulation, where a picture and report file are generated at the end. Customized UI: When you complete this tutorial, the customized UI will look like this: .. image:: /img/camera_full_page.png :width: 100% :align: center :alt: Screenshot of the Camera app In the final customized UI, you can see that: * On the left, the tree has been edited to add a new item called **Camera App**, while the usual monitoring pages have been removed. * On the main page, there are several tables, a button to download result files, and a picture which is expected to be generated by optiSLang on project completion. The following sections describe how to create this custom UI step by step, starting from the default example solution created from the supplied optiSLang project. Create the default solution ----------------------------- The first step is to create a new solution from the supplied optiSLang project ``Camera_App.opf``: #. Open the project in optiSLang and save it as Ansys Web App. #. :ref:`Unpack the created .awa file `. #. Set up the development environment and run the app as described in :ref:`test_the_changes`. When you run this default solution, it looks like this: .. image:: /img/camera_default.png :width: 100% :align: center :alt: Screenshot of the Camera app default solution .. _adding_custom_icon: Add a custom icon -------------------- Add a custom icon to use in the tree view. #. Add the image. Add an icon image to the folder ``src\ansys\solutions\camera_app\ui\assets\images``. In this case, the file is called ``camera_custom.svg``, which is shown here: .. image:: tutorial_files/camera_custom.svg :width: 5% :align: center :alt: Screenshot of the Camera app custom icon #. Add the icon to the ``Icon`` class. a. Open the file ``src\ansys\solutions\camera_app\ui\utils\icons.py``. b. Add the following line, making sure that the filename is the same as the name of the file you added: .. vale off .. code-block:: python :linenos: :lineno-start: 12 :emphasize-lines: 12 class Icon(Enum): PLAY = "play-solid.svg" ROOT_NODE = "account_tree.svg" RESET = "backward-fast-solid.svg" STOP = "hand-solid.svg" ABORT = "stop-solid.svg" SHUTDOWN = "power-off-solid.svg" DOWNLOAD = "file-export-solid.svg" SYSTEM = "workspaces_filled.svg" NODE = "workspaces_outline.svg" SETTINGS = "sliders-solid.svg" CAMERA = "camera_custom.svg" .. vale on This adds a new icon to the ``Icon`` class so it can be used in the tree view. .. _camera_tree_view: Update the tree view ---------------------- Add the Camera app to the tree view and remove the default monitoring pages so that the tree will look like this: .. figure:: /img/camera_tree_view.jpeg :class: with-border :width: 30% :align: center :alt: Screenshot of the Camera app tree view Camera app tree view #. Import the app layout. a. Open the file ``src/ansys/solutions/camera_app/ui/pages/page.py``. b. Add the following line: .. vale off .. code-block:: python :emphasize-lines: 2 from ansys.solutions.camera_app.ui.components import event_listeners from ansys.solutions.camera_app.ui.customization import camera_app from ansys.solutions.camera_app.ui.pages import monitoring_page, problem_setup_page .. vale on .. note:: Note that ``create ansys.solutions.camera_app.ui.customization.camera_app`` isn't yet created. This happens later, when you :ref:`create the app layout `. #. Add the app to the tree view. Add a new item to the ``get_treeview_items`` function so the item is displayed in the navigation tree. In this case, you're adding ``icon=Icon.CAMERA``, which is the icon you added previously. .. vale off .. code-block:: python :lineno-start: 74 :emphasize-lines: 12-19 :linenos: def get_treeview_items(project: Camera_AppSolution) -> list[dict]: """Get the treeview items for the navigation tree.""" problem_setup_step_item = AwcDashTreeViewItem( id="problem_setup_step", text="Problem Setup", icon=Icon.SETTINGS, ) tree_items = [problem_setup_step_item] if DataAccess(project).get_osl_instance_started(): camera_app_item = AwcDashTreeViewItem( id="camera_app", text="Camera App", icon=Icon.CAMERA, ) tree_items.append(camera_app_item) osl_project_tree = DataAccess(project).get_project_tree() if not osl_project_tree.is_uninitialized: tree_items.append(AwcDashTreeViewItem.from_actor_tree_node(osl_project_tree)) return AwcDashTreeView(tree_items=tree_items).create() .. vale on This results in the following tree view: .. image:: /img/camera_tree_view_with_monitoring.jpeg :width: 30% :align: center :alt: Screenshot of the Camera app tree view with monitoring pages #. Remove the default monitoring pages. Remove the following lines to remove the standard monitoring pages from the tree: .. vale off .. code-block:: diff :lineno-start: 74 :linenos: def get_treeview_items(project: Camera_AppSolution) -> list[dict]: """Get the treeview items for the navigation tree.""" problem_setup_step_item = AwcDashTreeViewItem( id="problem_setup_step", text="Problem Setup", icon=Icon.SETTINGS, ) tree_items = [problem_setup_step_item] if DataAccess(project).get_osl_instance_started(): camera_app_item = AwcDashTreeViewItem( id="camera_app", text="Camera App", icon=Icon.CAMERA, ) tree_items.append(camera_app_item) - osl_project_tree = DataAccess(project).get_project_tree() - - if not osl_project_tree.is_uninitialized: - tree_items.append(AwcDashTreeViewItem.from_actor_tree_node(osl_project_tree)) - return AwcDashTreeView(tree_items=tree_items).create() .. vale on #. Display the app when clicked. Update the ``get_page_layout_by_clicked_tree_item`` function to display the Camera app when the item is clicked in the navigation tree. Check if the ``clicked_tree_item_id`` is ``bolt_app``. If so, it should return the app layout. .. vale off .. code-block:: python :lineno-start: 85 :emphasize-lines: 3, 19-20 :linenos: if DataAccess(project).get_osl_instance_started(): camera_app_item = AwcDashTreeViewItem( id="camera_app", text="Camera App", icon=Icon.CAMERA, ) tree_items.append(camera_app_item) return AwcDashTreeView(tree_items=tree_items).create() def get_page_layout_by_clicked_tree_item(clicked: str | None, project: Camera_AppSolution) -> html.Div: """Get the page layout for the clicked tree item.""" if clicked is None: return html.Div(html.H1("Welcome!")) elif clicked == "problem_setup_step": return problem_setup_page.layout(project) elif clicked == "camera_app": return camera_app.layout else: return monitoring_page.layout(selected_actor=clicked, project=project) .. vale on If the project has been initialized, this modified function adds a new item to the tree view with the text "Camera App" and the camera icon you added previously. The ``id`` is used to identify the item in the tree view --- for example, to display the app when the item is clicked. If the project hasn't been initialized yet, the tree view doesn't contain the **Camera App** item, only the standard **Problem Setup** item. .. _creating-app-layout: Create the app layout ---------------------- Create the layout for the Camera app. #. Create the app layout file. a. In the folder ``src/ansys/solutions/camera_app/ui``, create a folder named ``customization``. b. In this folder, create a file named ``camera_app.py``. c. Open the file for editing. #. Add the imports. Add the imports to be used in the app: .. literalinclude:: tutorial_files/camera_app.py :language: python :lines: 3-14 :lineno-start: 3 :linenos: .. note:: If you use the case built from the supplied project, the namespace is ``ansys.solutions.camera_app`` and the solution name is ``Camera_AppSolution``. These names are derived from the name of the solution ``.awa`` file, so you need to adjust them when the solution has been created from a different optiSLang project or when you specified a different filename during the solution creation process. You can use the imports in ``src/ansys/solutions/camera_app/ui/pages/page.py`` as a reference to find the correct names. #. Add the tables. In the customized app, there are to be four tables: **Actor Status**, **Project Status**, **Project Information**, and **Parameters**. Create these tables using the standard ``dash_table.DataTable``: .. vale off .. code-block:: python :linenos: :lineno-start: 19 actor_status_table = dash_table.DataTable(id="actor-status-table",) project_status_table = dash_table.DataTable(id="project-status") project_information_table = dash_table.DataTable(id="project-table") parameters_table = dash_table.DataTable(id="parameter-table") .. vale on .. note:: At the moment, these tables are just placeholders which don't contain any data. The data is to be filled in by callbacks at a later stage, as described in :ref:`camera-add-callbacks`. The styling of the tables is also applied later, as described in :ref:`styling-tables`. #. Add the results button and image. Add a button for downloading results and an image from the project results (once the results become available): .. literalinclude:: tutorial_files/camera_app.py :language: python :lines: 91-97 :lineno-start: 91 :linenos: #. Add the page layout. Create a simple linear page layout using a single ``div`` element that contains the tables and other components: .. literalinclude:: tutorial_files/camera_app.py :language: python :lines: 101-124 :lineno-start: 101 :linenos: #. Add the root node constant. Define the constant for the ``ROOT_NODE_HID``. This is used later to get the root node of the project: .. literalinclude:: tutorial_files/camera_app.py :language: python :lines: 16-17 :lineno-start: 16 :linenos: .. _camera-add-callbacks: Add the callbacks ------------------ The tables are now added to the layout, but they don't yet contain any data. Create the callbacks to fill in and update the data used in the tables. #. Add the project status table callback. In this callback, the data for the **Project Status** table is retrieved from the server. The table should have a single row containing the string "Project Status" in the first column and the current project status in the second. Create the callback using the following code: .. literalinclude:: tutorial_files/camera_app.py :language: python :lines: 165-178 :lineno-start: 165 :linenos: About this code: * The ``@callback`` decorator is used to define a callback function in Dash. * The ``Output`` specifies the component and property to update --- in this case, the ``data`` property of the ``project-status`` table. * The ``Trigger`` specifies the event that triggers the callback --- in this case, a message from the ``WebsocketStreamListeners.PROJECT_STATE`` stream. This stream sends a message whenever the project state changes. * The ``State`` specifies the current value of the ``pathname`` property of the ``url`` component, which is used to access the current project data within the callback. * The ``DashClient`` is used to retrieve the ``project``. With this project, a ``DataAccess`` object can be created. The ``DataAccess`` object provides access to the data in the project. * The ``monitoring_data`` object is used to get information about the project and its actors. * The ``get_osl_project_state`` method is used to retrieve the current project state, which is a string with values such as "RUNNING," "FINISHED," "FAILED," and so on. * Finally, a ``pandas`` data frame is created with the project state and returned as a dictionary. The ``to_dict("records")`` method converts the data frame to a list of dictionaries, which is the format expected by the ``data`` property of the table. #. Add the project information table callback. The second callback is for the **Project Information** table. This table contains information about the project, as one would get from ``get_full_project_tree`` from the optiSLang server. Create the callback using the following code: .. literalinclude:: tutorial_files/camera_app.py :language: python :lines: 181-196 :lineno-start: 181 :linenos: This callback is similar to the previous one, but in this case: * The trigger is the ``PROJECT_INFORMATION`` stream, which updates when the project information changes. * Instead of getting the project state, the project information is returned. The ``get_project_information`` method returns an object with the project information, which is converted to a dict using ``to_dict_by_title``. #. Add actor status table callback. The **Actor Status** table contains information about the actors in the project. This is a bit more complicated, as the status and the actor name for each actor need to be retrieved. Create the callback using the following code: .. literalinclude:: tutorial_files/camera_app.py :language: python :lines: 127-162 :lineno-start: 127 :linenos: About this code: * The stream ``WebsocketStreamListeners.ACTOR_DATA`` is used to trigger this callback when there are updates to the actor data available. * In the callback, the data access module provides the root system of the project. This is the top-level system in the project and contains all the other systems and actors. * If the project is not initialized, a ``PreventUpdate`` exception is raised, which prevents the callback from updating the table. If it is initialized, the children of the root system are iterated over. These are the direct descendants of the root system, which can be systems or actors. * All system children are skipped. This is an defensive measure because for the optiSLang project used, no direct descendants of the root of system type are expected. * For all actor children, the name and unique identifier of the actor are stored and the actor states are retrieved using ``get_actor_states_ids``. This function returns a list of states for the given actor. A state is a unique identifier for a particular state. A state can, for example, be ``0.1``, ``0.2``, and so on. In this case, only a single state is expected for each actor, so the first state is used in the following: * Using the first state, an ``ActorAndHid`` object is created with the unique identifier and state. The actor information is then retrieved using ``get_actor_information_actor``. The processing state is obtained from this object. * Finally, a data frame with the actor name and status is created and is returned as a list of dictionaries using ``to_dict("record")``. #. Add the parameters table callback. For the **Parameters** table callback, you'll use some additional capabilities from the ``DataAccess`` module to get the design points for the project and the parameters for each design point. Create the callback using the following code: .. literalinclude:: tutorial_files/camera_app.py :language: python :lines: 199-233 :lineno-start: 199 :linenos: About this code: * A ``DataAccess`` object is created, and the root system is retrieved. If the project is not initialized, a ``PreventUpdate`` exception is raised, as before. * Using the root system unique identifier and the ``ROOT_NODE_HID``, an ``ActorAndHid`` object is created. This is used to get the design points for the root system of the project. * The design points object can be converted to a dictionary in different ways. In this case, the ``to_dict_by_states`` method is used, which returns a dictionary with the design points states as keys and the parameter names as values. This is useful when the parameters are to be the rows in the table. The "state" column is renamed to **Value** and a list of table columns is created. * Finally, a data frame is created and returned a list of dictionaries, and the columns as a list of dictionaries. .. _styling-tables: Style the tables ------------------ The project as described up to this point works, but is a bit bare: .. image:: /img/camera_not_styled.png :width: 80% :align: center :alt: Screenshot of the customized Camera app solution without styling Make the following modifications to improve the presentation of the tables: #. Align table widths. Use the following setting to align the width of all the tables: .. literalinclude:: tutorial_files/camera_app.py :language: python :lines: 21-24 :lineno-start: 21 :linenos: #. Apply conditional formatting. Use these settings to introduce conditional formatting to change the foreground color for project and actor status tables based on the project state: .. literalinclude:: tutorial_files/camera_app.py :language: python :lines: 26-69 :lineno-start: 26 :linenos: #. Hide table headers. Use this setting to hide the header for the actor and project state tables: .. literalinclude:: tutorial_files/camera_app.py :language: python :lines: 71-73 :lineno-start: 71 :linenos: #. Apply modifications. Apply your customizations to the tables as follows: .. literalinclude:: tutorial_files/camera_app.py :language: python :lines: 76-89 :lineno-start: 76 :linenos: This yields the desired final customized look of the Camera app: .. image:: /img/camera_full_page.png :width: 100% :align: center :alt: Screenshot of the Camera app .. _full_code_camera: Full code --------- Here is the full code for the Camera app after the customizations are complete: .. literalinclude:: tutorial_files/camera_app.py :language: python :linenos: