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 full code at the end of this document or download it here.

Project:

You can download the optiSLang project used in this tutorial 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:

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:

  1. Open the project in optiSLang and save it as Ansys Web App.

  2. Unpack the created .awa file.

  3. Set up the development environment and run the app as described in Test the changes.

When you run this default solution, it looks like this:

Screenshot of the Camera app default solution

Add a custom icon#

Add a custom icon to use in the tree view.

  1. 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:

    Screenshot of the Camera app custom icon
  2. Add the icon to the Icon class.
    1. Open the file src\ansys\solutions\camera_app\ui\utils\icons.py.

    2. Add the following line, making sure that the filename is the same as the name of the file you added:

      12class Icon(Enum):
      13    PLAY = "play-solid.svg"
      14    ROOT_NODE = "account_tree.svg"
      15    RESET = "backward-fast-solid.svg"
      16    STOP = "hand-solid.svg"
      17    ABORT = "stop-solid.svg"
      18    SHUTDOWN = "power-off-solid.svg"
      19    DOWNLOAD = "file-export-solid.svg"
      20    SYSTEM = "workspaces_filled.svg"
      21    NODE = "workspaces_outline.svg"
      22    SETTINGS = "sliders-solid.svg"
      23    CAMERA = "camera_custom.svg"
      

This adds a new icon to the Icon class so it can be used in the 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:

Screenshot of the Camera app tree view

Camera app tree view#

  1. Import the app layout.
    1. Open the file src/ansys/solutions/camera_app/ui/pages/page.py.

    2. Add the following line:

      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
      

      Note

      Note that create ansys.solutions.camera_app.ui.customization.camera_app isn’t yet created. This happens later, when you create the app layout.

  2. 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.

    74def get_treeview_items(project: Camera_AppSolution) -> list[dict]:
    75    """Get the treeview items for the navigation tree."""
    76
    77    problem_setup_step_item = AwcDashTreeViewItem(
    78        id="problem_setup_step",
    79        text="Problem Setup",
    80        icon=Icon.SETTINGS,
    81    )
    82
    83    tree_items = [problem_setup_step_item]
    84
    85    if DataAccess(project).get_osl_instance_started():
    86        camera_app_item = AwcDashTreeViewItem(
    87            id="camera_app",
    88            text="Camera App",
    89            icon=Icon.CAMERA,
    90        )
    91
    92        tree_items.append(camera_app_item)
    93
    94   osl_project_tree = DataAccess(project).get_project_tree()
    95
    96   if not osl_project_tree.is_uninitialized:
    97      tree_items.append(AwcDashTreeViewItem.from_actor_tree_node(osl_project_tree))
    98
    99    return AwcDashTreeView(tree_items=tree_items).create()
    

    This results in the following tree view:

    Screenshot of the Camera app tree view with monitoring pages
  3. Remove the default monitoring pages.

    Remove the following lines to remove the standard monitoring pages from the tree:

    74def get_treeview_items(project: Camera_AppSolution) -> list[dict]:
    75    """Get the treeview items for the navigation tree."""
    76
    77    problem_setup_step_item = AwcDashTreeViewItem(
    78        id="problem_setup_step",
    79        text="Problem Setup",
    80        icon=Icon.SETTINGS,
    81    )
    82
    83    tree_items = [problem_setup_step_item]
    84
    85    if DataAccess(project).get_osl_instance_started():
    86        camera_app_item = AwcDashTreeViewItem(
    87            id="camera_app",
    88            text="Camera App",
    89            icon=Icon.CAMERA,
    90        )
    91
    92        tree_items.append(camera_app_item)
    93
    94-  osl_project_tree = DataAccess(project).get_project_tree()
    95-
    96-  if not osl_project_tree.is_uninitialized:
    97-     tree_items.append(AwcDashTreeViewItem.from_actor_tree_node(osl_project_tree))
    98-
    99    return AwcDashTreeView(tree_items=tree_items).create()
    
  4. 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.

     85 if DataAccess(project).get_osl_instance_started():
     86     camera_app_item = AwcDashTreeViewItem(
     87         id="camera_app",
     88         text="Camera App",
     89         icon=Icon.CAMERA,
     90     )
     91
     92     tree_items.append(camera_app_item)
     93
     94 return AwcDashTreeView(tree_items=tree_items).create()
     95
     96
     97def get_page_layout_by_clicked_tree_item(clicked: str | None, project: Camera_AppSolution) -> html.Div:
     98 """Get the page layout for the clicked tree item."""
     99 if clicked is None:
    100     return html.Div(html.H1("Welcome!"))
    101 elif clicked == "problem_setup_step":
    102     return problem_setup_page.layout(project)
    103 elif clicked == "camera_app":
    104     return camera_app.layout
    105 else:
    106     return monitoring_page.layout(selected_actor=clicked, project=project)
    

    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.

Create the app layout#

Create the layout for the Camera app.

  1. Create the app layout file.
    1. In the folder src/ansys/solutions/camera_app/ui, create a folder named customization.

    2. In this folder, create a file named camera_app.py.

    3. Open the file for editing.

  2. Add the imports.

    Add the imports to be used in the app:

     3import base64
     4
     5from dash.exceptions import PreventUpdate
     6import dash_bootstrap_components as dbc
     7from dash_extensions.enrich import Input, Output, State, Trigger, dash_table, dcc, html
     8import pandas as pd
     9
    10from ansys.saf.glow.client import DashClient, callback
    11from ansys.solutions.camera_app.datamodel.shared.models import ActorAndHid, DesignFields
    12from ansys.solutions.camera_app.datamodel.shared.websocket_streams import WebsocketStreamListeners
    13from ansys.solutions.camera_app.solution.data_access import DataAccess
    14from ansys.solutions.camera_app.solution.definition import Camera_AppSolution
    

    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.

  3. 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:

    19actor_status_table = dash_table.DataTable(id="actor-status-table",)
    20project_status_table = dash_table.DataTable(id="project-status")
    21project_information_table = dash_table.DataTable(id="project-table")
    22parameters_table = dash_table.DataTable(id="parameter-table")
    

    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 Add the callbacks. The styling of the tables is also applied later, as described in Style the tables.

  4. Add the results button and image.

    Add a button for downloading results and an image from the project results (once the results become available):

    91results_download_button = dbc.Button(
    92    id="results-button", disabled=True, children="Download Result files", color="dark", outline=True
    93)
    94result_download = dcc.Download(id="result-download")
    95
    96result_image = html.Img(id="result-image", style={"width": "200px"})
    97result_image_loading = dcc.Loading(id="image-loading", type="circle", display="show", children=result_image)
    
  5. Add the page layout.

    Create a simple linear page layout using a single div element that contains the tables and other components:

    101layout = html.Div(
    102    [
    103        html.H4("Analysis Status"),
    104        actor_status_table,
    105        html.Br(),
    106        html.H4("Status"),
    107        project_status_table,
    108        html.Br(),
    109        html.H4("Project"),
    110        project_information_table,
    111        html.Br(),
    112        html.H4("Parameters"),
    113        parameters_table,
    114        html.Br(),
    115        html.H4("Results"),
    116        results_download_button,
    117        html.Br(),
    118        result_download,
    119        html.Br(),
    120        html.Br(),
    121        html.Br(),
    122        result_image_loading,
    123    ],
    124)
    
  6. 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:

    16ROOT_NODE_HID = "0"  
    17"""For this project we expect the root node states to always be a single state with hid "0"."""
    

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.

  1. 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:

    165@callback(
    166    Output("project-status", "data"),
    167    Trigger(WebsocketStreamListeners.PROJECT_STATE.value, "message"),
    168    State("url", "pathname"),
    169)
    170def update_project_status(pathname):
    171    project = DashClient[Camera_AppSolution].get_project(pathname)
    172
    173    monitoring_data = DataAccess(project).monitoring_data
    174
    175    project_status = monitoring_data.get_osl_project_state()
    176
    177    df = pd.DataFrame([["Project status", project_status]], columns=["Name", "Value"])
    178    return df.to_dict("records")
    

    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.

  2. 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:

    181@callback(
    182    Output("project-table", "data"),
    183    Trigger(WebsocketStreamListeners.PROJECT_INFORMATION.value, "message"),
    184    State("url", "pathname"),
    185)
    186def update_project_information(pathname):
    187    project = DashClient[Camera_AppSolution].get_project(pathname)
    188
    189    monitoring_data = DataAccess(project).monitoring_data
    190
    191    project_information = monitoring_data.get_project_information()
    192    project_information_as_dict = project_information.to_dict_by_title()
    193
    194    df = pd.DataFrame(list(project_information_as_dict.items()), columns=["Name", "Value"])
    195
    196    return df.to_dict("records")
    

    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.

  3. 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:

    127@callback(
    128    Output("actor-status-table", "data"),
    129    Trigger(WebsocketStreamListeners.ACTOR_DATA.value, "message"),
    130    State("url", "pathname"),
    131)
    132def update_actor_status(pathname):
    133    project = DashClient[Camera_AppSolution].get_project(pathname)
    134
    135    data_access = DataAccess(project)
    136    monitoring_data = data_access.monitoring_data
    137
    138    root_system = data_access.get_project_tree()
    139
    140    if root_system.is_uninitialized:
    141        # The project is not initialized yet
    142        raise PreventUpdate
    143
    144    status_by_name = {}
    145
    146    for node in root_system.children:
    147        if node.is_system:
    148            continue
    149
    150        name = node.name
    151        uid = node.uid
    152        actor_states = monitoring_data.get_actor_states_ids(uid)
    153
    154        state_id = actor_states.states_ids[0]
    155        actor_and_hid = ActorAndHid(actor_uid=uid, hid=state_id)
    156        actor_info = monitoring_data.get_actor_information_actor(actor_and_hid)
    157        status = actor_info.processing_state
    158
    159        status_by_name[name] = status
    160
    161    df = pd.DataFrame(status_by_name.items(), columns=["Name", "Value"])
    162    return df.to_dict("records")
    

    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").

  4. 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:

    199@callback(
    200    Output("parameter-table", "data"),
    201    Output("parameter-table", "columns"),
    202    Trigger(WebsocketStreamListeners.ACTOR_DATA.value, "message"),
    203    State("url", "pathname"),
    204)
    205def update_parameter_status(pathname):
    206    project = DashClient[Camera_AppSolution].get_project(pathname)
    207    data_access = DataAccess(project)
    208    root_system = data_access.get_project_tree()
    209
    210    if root_system.is_uninitialized:
    211        # The project is not initialized yet
    212        raise PreventUpdate
    213    actor_and_hid = ActorAndHid(actor_uid=root_system.uid, hid=ROOT_NODE_HID)
    214    design_points = data_access.monitoring_data.get_design_points(actor_and_hid)
    215    parameters_dict = design_points.to_dict_by_states(DesignFields.PARAMETERS)
    216    if not parameters_dict:
    217        raise PreventUpdate
    218    
    219    states = design_points.get_states()
    220
    221    if not len(states) == 1:
    222        # For this case, we assume only one state for the root node
    223        raise ValueError(f"Expected only one state, got {len(states)}: {states}")
    224    
    225    state = states[0]
    226
    227    df = pd.DataFrame(parameters_dict)
    228
    229    # Rename the column "state" to "Value"
    230    df = df.rename(columns={state: "Value"})
    231    columns = [{"name": i, "id": i} for i in df.columns]
    232
    233    return df.to_dict("records"), columns
    

    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.

Style the tables#

The project as described up to this point works, but is a bit bare:

Screenshot of the customized Camera app solution without styling

Make the following modifications to improve the presentation of the tables:

  1. Align table widths.

    Use the following setting to align the width of all the tables:

    21style_cell_conditional = [
    22    {"if": {"column_id": "Name"}, "width": "30%", "textAlign": "left"},
    23    {"if": {"column_id": "Value"}, "width": "70%", "textAlign": "left"},
    24]
    
  2. 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:

    26style_actor_status_data_conditional = [
    27    {
    28        "if": {"column_id": "Value", "filter_query": '{Value} eq "Succeeded"'},
    29        "color": "green",
    30    },
    31    {
    32        "if": {"column_id": "Value", "filter_query": '{Value} eq "Failed"'},
    33        "color": "red",
    34    },
    35    {
    36        "if": {"column_id": "Value", "filter_query": '{Value} eq "Predecessor failed"'},
    37        "color": "grey",
    38    },
    39]
    40
    41style_project_status_data_conditional = [
    42    {
    43        "if": {"column_id": "Value", "filter_query": '{Value} eq "FINISHED"'},
    44        "color": "green",
    45    },
    46    {
    47        "if": {"column_id": "Value", "filter_query": '{Value} eq "RUNNING"'},
    48        "color": "black",
    49    },
    50    {
    51        "if": {"column_id": "Value", "filter_query": '{Value} eq "FAILED"'},
    52        "color": "red",
    53    },
    54    {
    55        "if": {"column_id": "Value", "filter_query": '{Value} eq "NOT STARTED"'},
    56        "color": "grey",
    57    },
    58]
    59
    60common_data_table_kwargs = {
    61    "style_cell_conditional": style_cell_conditional,
    62    "style_as_list_view": True,
    63    "cell_selectable": False,
    64    "data": [{"Name": "", "Value": "NOT STARTED"}],
    65    "columns": [{"name": "Name", "id": "Name"}, {"name": "Value", "id": "Value"}],
    66}
    67
    68# Needed to get the conditional coloring of text to work correctly
    69COMMON_CSS = [{"selector": "div", "rule": "color: inherit"}]
    
  3. Hide table headers.

    Use this setting to hide the header for the actor and project state tables:

    71# For hiding the header of the first row
    72HIDE_HEADER_CSS = COMMON_CSS + [{"selector": "tr:first-child", "rule": "display: none"}]
    73
    
  4. Apply modifications.

    Apply your customizations to the tables as follows:

    76actor_status_table = dash_table.DataTable(
    77    id="actor-status-table",
    78    css=HIDE_HEADER_CSS,
    79    style_data_conditional=style_actor_status_data_conditional,
    80    **common_data_table_kwargs,
    81)
    82project_status_table = dash_table.DataTable(
    83    id="project-status",
    84    css=HIDE_HEADER_CSS,
    85    style_data_conditional=style_project_status_data_conditional,
    86    **common_data_table_kwargs,
    87)
    88project_information_table = dash_table.DataTable(id="project-table", css=COMMON_CSS, **common_data_table_kwargs)
    89parameters_table = dash_table.DataTable(id="parameter-table", css=COMMON_CSS, **common_data_table_kwargs)
    

This yields the desired final customized look of the Camera app:

Screenshot of the Camera app

Full code#

Here is the full code for the Camera app after the customizations are complete:

  1# ©2025, ANSYS Inc. Unauthorized use, distribution or duplication is prohibited.
  2
  3import base64
  4
  5from dash.exceptions import PreventUpdate
  6import dash_bootstrap_components as dbc
  7from dash_extensions.enrich import Input, Output, State, Trigger, dash_table, dcc, html
  8import pandas as pd
  9
 10from ansys.saf.glow.client import DashClient, callback
 11from ansys.solutions.camera_app.datamodel.shared.models import ActorAndHid, DesignFields
 12from ansys.solutions.camera_app.datamodel.shared.websocket_streams import WebsocketStreamListeners
 13from ansys.solutions.camera_app.solution.data_access import DataAccess
 14from ansys.solutions.camera_app.solution.definition import Camera_AppSolution
 15
 16ROOT_NODE_HID = "0"  
 17"""For this project we expect the root node states to always be a single state with hid "0"."""
 18
 19# Styles
 20
 21style_cell_conditional = [
 22    {"if": {"column_id": "Name"}, "width": "30%", "textAlign": "left"},
 23    {"if": {"column_id": "Value"}, "width": "70%", "textAlign": "left"},
 24]
 25
 26style_actor_status_data_conditional = [
 27    {
 28        "if": {"column_id": "Value", "filter_query": '{Value} eq "Succeeded"'},
 29        "color": "green",
 30    },
 31    {
 32        "if": {"column_id": "Value", "filter_query": '{Value} eq "Failed"'},
 33        "color": "red",
 34    },
 35    {
 36        "if": {"column_id": "Value", "filter_query": '{Value} eq "Predecessor failed"'},
 37        "color": "grey",
 38    },
 39]
 40
 41style_project_status_data_conditional = [
 42    {
 43        "if": {"column_id": "Value", "filter_query": '{Value} eq "FINISHED"'},
 44        "color": "green",
 45    },
 46    {
 47        "if": {"column_id": "Value", "filter_query": '{Value} eq "RUNNING"'},
 48        "color": "black",
 49    },
 50    {
 51        "if": {"column_id": "Value", "filter_query": '{Value} eq "FAILED"'},
 52        "color": "red",
 53    },
 54    {
 55        "if": {"column_id": "Value", "filter_query": '{Value} eq "NOT STARTED"'},
 56        "color": "grey",
 57    },
 58]
 59
 60common_data_table_kwargs = {
 61    "style_cell_conditional": style_cell_conditional,
 62    "style_as_list_view": True,
 63    "cell_selectable": False,
 64    "data": [{"Name": "", "Value": "NOT STARTED"}],
 65    "columns": [{"name": "Name", "id": "Name"}, {"name": "Value", "id": "Value"}],
 66}
 67
 68# Needed to get the conditional coloring of text to work correctly
 69COMMON_CSS = [{"selector": "div", "rule": "color: inherit"}]
 70
 71# For hiding the header of the first row
 72HIDE_HEADER_CSS = COMMON_CSS + [{"selector": "tr:first-child", "rule": "display: none"}]
 73
 74# Elements with data which will be automatically updated by callbacks
 75
 76actor_status_table = dash_table.DataTable(
 77    id="actor-status-table",
 78    css=HIDE_HEADER_CSS,
 79    style_data_conditional=style_actor_status_data_conditional,
 80    **common_data_table_kwargs,
 81)
 82project_status_table = dash_table.DataTable(
 83    id="project-status",
 84    css=HIDE_HEADER_CSS,
 85    style_data_conditional=style_project_status_data_conditional,
 86    **common_data_table_kwargs,
 87)
 88project_information_table = dash_table.DataTable(id="project-table", css=COMMON_CSS, **common_data_table_kwargs)
 89parameters_table = dash_table.DataTable(id="parameter-table", css=COMMON_CSS, **common_data_table_kwargs)
 90
 91results_download_button = dbc.Button(
 92    id="results-button", disabled=True, children="Download Result files", color="dark", outline=True
 93)
 94result_download = dcc.Download(id="result-download")
 95
 96result_image = html.Img(id="result-image", style={"width": "200px"})
 97result_image_loading = dcc.Loading(id="image-loading", type="circle", display="show", children=result_image)
 98
 99# Forming the layout
100
101layout = html.Div(
102    [
103        html.H4("Analysis Status"),
104        actor_status_table,
105        html.Br(),
106        html.H4("Status"),
107        project_status_table,
108        html.Br(),
109        html.H4("Project"),
110        project_information_table,
111        html.Br(),
112        html.H4("Parameters"),
113        parameters_table,
114        html.Br(),
115        html.H4("Results"),
116        results_download_button,
117        html.Br(),
118        result_download,
119        html.Br(),
120        html.Br(),
121        html.Br(),
122        result_image_loading,
123    ],
124)
125
126
127@callback(
128    Output("actor-status-table", "data"),
129    Trigger(WebsocketStreamListeners.ACTOR_DATA.value, "message"),
130    State("url", "pathname"),
131)
132def update_actor_status(pathname):
133    project = DashClient[Camera_AppSolution].get_project(pathname)
134
135    data_access = DataAccess(project)
136    monitoring_data = data_access.monitoring_data
137
138    root_system = data_access.get_project_tree()
139
140    if root_system.is_uninitialized:
141        # The project is not initialized yet
142        raise PreventUpdate
143
144    status_by_name = {}
145
146    for node in root_system.children:
147        if node.is_system:
148            continue
149
150        name = node.name
151        uid = node.uid
152        actor_states = monitoring_data.get_actor_states_ids(uid)
153
154        state_id = actor_states.states_ids[0]
155        actor_and_hid = ActorAndHid(actor_uid=uid, hid=state_id)
156        actor_info = monitoring_data.get_actor_information_actor(actor_and_hid)
157        status = actor_info.processing_state
158
159        status_by_name[name] = status
160
161    df = pd.DataFrame(status_by_name.items(), columns=["Name", "Value"])
162    return df.to_dict("records")
163
164
165@callback(
166    Output("project-status", "data"),
167    Trigger(WebsocketStreamListeners.PROJECT_STATE.value, "message"),
168    State("url", "pathname"),
169)
170def update_project_status(pathname):
171    project = DashClient[Camera_AppSolution].get_project(pathname)
172
173    monitoring_data = DataAccess(project).monitoring_data
174
175    project_status = monitoring_data.get_osl_project_state()
176
177    df = pd.DataFrame([["Project status", project_status]], columns=["Name", "Value"])
178    return df.to_dict("records")
179
180
181@callback(
182    Output("project-table", "data"),
183    Trigger(WebsocketStreamListeners.PROJECT_INFORMATION.value, "message"),
184    State("url", "pathname"),
185)
186def update_project_information(pathname):
187    project = DashClient[Camera_AppSolution].get_project(pathname)
188
189    monitoring_data = DataAccess(project).monitoring_data
190
191    project_information = monitoring_data.get_project_information()
192    project_information_as_dict = project_information.to_dict_by_title()
193
194    df = pd.DataFrame(list(project_information_as_dict.items()), columns=["Name", "Value"])
195
196    return df.to_dict("records")
197
198
199@callback(
200    Output("parameter-table", "data"),
201    Output("parameter-table", "columns"),
202    Trigger(WebsocketStreamListeners.ACTOR_DATA.value, "message"),
203    State("url", "pathname"),
204)
205def update_parameter_status(pathname):
206    project = DashClient[Camera_AppSolution].get_project(pathname)
207    data_access = DataAccess(project)
208    root_system = data_access.get_project_tree()
209
210    if root_system.is_uninitialized:
211        # The project is not initialized yet
212        raise PreventUpdate
213    actor_and_hid = ActorAndHid(actor_uid=root_system.uid, hid=ROOT_NODE_HID)
214    design_points = data_access.monitoring_data.get_design_points(actor_and_hid)
215    parameters_dict = design_points.to_dict_by_states(DesignFields.PARAMETERS)
216    if not parameters_dict:
217        raise PreventUpdate
218    
219    states = design_points.get_states()
220
221    if not len(states) == 1:
222        # For this case, we assume only one state for the root node
223        raise ValueError(f"Expected only one state, got {len(states)}: {states}")
224    
225    state = states[0]
226
227    df = pd.DataFrame(parameters_dict)
228
229    # Rename the column "state" to "Value"
230    df = df.rename(columns={state: "Value"})
231    columns = [{"name": i, "id": i} for i in df.columns]
232
233    return df.to_dict("records"), columns
234
235
236@callback(
237    Output("results-button", "disabled"),
238    Trigger(WebsocketStreamListeners.PROJECT_STATE.value, "message"),
239    State("url", "pathname"),
240)
241def update_results_button_disabled(pathname):
242    project = DashClient[Camera_AppSolution].get_project(pathname)
243
244    root_uid = DataAccess(project).get_project_tree().uid
245    actor_and_hid = ActorAndHid(actor_uid=root_uid, hid=ROOT_NODE_HID)
246
247    results = DataAccess(project).results
248    file_reference = results.get_file_reference_by_name("Result_files.zip", actor_and_hid)
249    return file_reference is None
250
251
252@callback(
253    Output("result-download", "data"),
254    Input("results-button", "n_clicks"),
255    State("url", "pathname"),
256)
257def download_file(n_clicks, pathname):
258    if n_clicks is None:
259        raise PreventUpdate
260
261    project = DashClient[Camera_AppSolution].get_project(pathname)
262
263    root_uid = DataAccess(project).get_project_tree().uid
264    actor_and_hid = ActorAndHid(actor_uid=root_uid, hid=ROOT_NODE_HID)
265
266    results = DataAccess(project).results
267    content = results.get_file_content_by_name("Result_files.zip", actor_and_hid)
268
269    if content is None:
270        return dcc.send_string("No result files available", "Error.txt")
271
272    return dcc.send_bytes(content, "Result_files.zip")
273
274
275@callback(
276    Output("image-loading", "display"),
277    Output("result-image", "src"),
278    Trigger(WebsocketStreamListeners.PROJECT_STATE.value, "message"),
279    Trigger(WebsocketStreamListeners.ACTOR_DATA.value, "message"),
280    State("url", "pathname"),
281)
282def update_images(pathname):
283    project = DashClient[Camera_AppSolution].get_project(pathname)
284
285    root_uid = DataAccess(project).get_project_tree().uid
286    actor_and_hid = ActorAndHid(actor_uid=root_uid, hid=ROOT_NODE_HID)
287
288    results = DataAccess(project).results
289    file_content = results.get_file_content_by_name("Camera.svg", actor_and_hid)
290
291    if file_content is None:
292        return "show", None
293
294    b64_encoded_image = base64.b64encode(file_content)
295    decoded = b64_encoded_image.decode("ascii")
296    img_src = f"data:image/svg+xml;base64,{decoded}"
297
298    return "hide", img_src