Note
Go to the end to download the full example code.
Transaction Supervisor#
A minimal Dash app showing how to use
TransactionSupervisor
to monitor the progress of a SAF transaction method.
See the Transaction Supervisor page in the User Guide for the full reference documentation.
In a real SAF solution every step exposes one or more transaction methods
(decorated with @transaction). The GLOW REST API exposes their status at:
GET /steps/<step-name>:<method-name>
The TransactionSupervisor
component polls that endpoint at a configurable interval and renders live
status information for the transaction method.
This example embeds a minimal Flask server that mimics the GLOW REST API so
you can explore the component without a running GLOW server. The mock server
listens on http://127.0.0.1:8060.
For a detailed example of how to set up this component with a full SAF solution, see the
showcase example
in the repository.
Note
Run this file directly to launch the app:
pip install ansys-solutions-dash-super-components requests
python example_transaction_supervisor.py
Then open http://localhost:8050. Use the buttons to trigger a
successful or failing transaction and watch the transaction supervisor update in
real time.
Set up imports and the Dash application#
MantineProvider must wrap the entire layout
for Mantine-based components to render correctly. The React version must also
be set before importing the component library — this is a Dash Mantine
Components requirement when using Dash 2.x.
A tiny Flask server is embedded in the same process as the Dash app so the mock GLOW API is ready before the component starts polling. This requires additional imports for the mock server and callbacks.
import threading
import time
from ansys.solutions.dash_super_components import TransactionSupervisor
from dash import _dash_renderer
from dash.exceptions import PreventUpdate
from dash_extensions.enrich import DashProxy, Input, Output, callback, callback_context, dcc, html
import dash_mantine_components as dmc
from flask import Flask, Response, jsonify, request as flask_request
import requests
from werkzeug.serving import make_server
# Required only for Dash 2.x to use Mantine-based components
_dash_renderer._set_react_version("18.2.0")
app = DashProxy(__name__)
Embedded mock GLOW API server#
TransactionSupervisor polls {glow_api_url}/steps/{step-name}:{method-name}
and expects a JSON body that contains a "status" field. Recognized
status values are "run-required", "running", "completed" and
"failed".
The mock server stores the current status for each step:method key in a
plain Python dict that can be updated from Dash callbacks by POSTing to the
mock server’s /steps/<step_method> endpoint.
MOCK_API_PORT = 8060
MOCK_API_URL = f"http://127.0.0.1:{MOCK_API_PORT}"
# Simple dict owned exclusively by the mock server process/thread. Do not access from Dash callbacks
# as this is not thread-safe and is only intended to be updated via the mock server endpoints.
_statuses: dict[str, str] = {}
mock_server = Flask("mock_glow_api")
@mock_server.route("/steps/<path:step_method>", methods=["GET"])
def get_step_status(step_method: str) -> Response:
"""Get the status for *step_method*."""
return jsonify({"status": _statuses.get(step_method, "run-required")})
@mock_server.route("/steps/<path:step_method>", methods=["POST"])
def set_step_status(step_method: str) -> Response:
"""Set the status for *step_method* (used by Dash callbacks to mock transaction status)."""
_statuses[step_method] = flask_request.json["status"]
return jsonify({"ok": True})
def _run_mock_server() -> None:
server = make_server("127.0.0.1", MOCK_API_PORT, mock_server)
server.serve_forever()
threading.Thread(target=_run_mock_server, daemon=True).start()
Define steps and methods to monitor#
Each TransactionSupervisor monitors a single (step, method) pair. Only one
pair is defined here, but a real solution could include multiple supervisors monitoring different
steps and methods.
STEP_NAME = "solver"
METHOD = "run_solution"
Build the layout#
A TransactionSupervisor can’t be added directly to
the layout because in a real SAF-based solution it needs access to the project URL, which is only
available through the GLOW DashClient in a callback.
Therefore, a placeholder html.Div is added to the layout and populated with the
TransactionSupervisor instance later via a callback.
app.layout = dmc.MantineProvider(
html.Div(
[
dcc.Location("url", refresh=False),
dmc.Title("Transaction Supervisor", order=2, mb="md"),
dmc.Text(
"Use the buttons below to simulate different transaction outcomes "
"and watch the status information update in real time.",
c="dimmed",
mb="xl",
),
dmc.Group(
[
dmc.Button("Simulate success", id="btn-success", color="green"),
dmc.Button("Simulate failure", id="btn-failure", color="red"),
],
mb="xl",
),
dmc.Text(f"Method: {METHOD}", fw=500, mb="sm"),
html.Div(id="transaction_supervisor_placeholder"),
dmc.Divider(my="xl"),
],
style={"maxWidth": 960, "margin": "40px auto", "padding": "0 16px"},
)
)
Add the TransactionSupervisor in a callback#
Add the TransactionSupervisor to the layout via a callback. The callback is triggered on page
load and in a real SAF-based solution, the project URL would be injected via the GLOW
DashClient. In this example, the MOCK_API_URL is used directly.
@callback(
Output("transaction_supervisor_placeholder", "children"),
Input("url", "pathname"),
)
def initialize_transaction_supervisor(pathname: str) -> TransactionSupervisor:
"""Add the TransactionSupervisor component to the layout."""
# In a SAF-based solution, retrieve the project.url:
# project = DashClient[SuperComponentsExamplesSolution].get_project(pathname)
# project_url = project.url
# In this example, the MOCK_API_URL is used:
project_url = MOCK_API_URL
return TransactionSupervisor(
url=project_url,
step_name=STEP_NAME,
method_name=METHOD,
aio_id=f"transaction_supervisor_{METHOD}",
)
Add callbacks to simulate transactions#
The buttons update the mock-server status and then activate the supervisor so it starts polling.
In a real SAF app the “activate” call would be triggered after calling a transaction method.
Each TransactionSupervisor already contains its own activate_monitoring
store internally — writing True to it via a callback Output is sufficient to activate it.
@callback(
Output(
TransactionSupervisor.ids.activate_monitoring(f"transaction_supervisor_{METHOD}"), "data"
),
Input("btn-success", "n_clicks"),
Input("btn-failure", "n_clicks"),
prevent_initial_call=True,
)
def mock_run_transaction_and_activate_supervison(
n_success: int, n_failure: int
) -> bool | PreventUpdate:
"""Update mock statuses and activate supervision on all supervisors."""
if not callback_context.triggered_id:
raise PreventUpdate
# Convert step and method names to the format expected by the mock server endpoint (hyphenated)
step_name_hyphenated = STEP_NAME.replace("_", "-")
method_hyphenated = METHOD.replace("_", "-")
requests.post(
f"{MOCK_API_URL}/steps/{step_name_hyphenated}:{method_hyphenated}",
json={"status": "running"},
)
# Simulate async completion after 5s using a background thread
def _complete():
time.sleep(5)
requests.post(
f"{MOCK_API_URL}/steps/{step_name_hyphenated}:{method_hyphenated}",
json={"status": "completed"},
)
# Simulate async failure after 4s using a background thread
def _fail():
time.sleep(4)
requests.post(
f"{MOCK_API_URL}/steps/{step_name_hyphenated}:{method_hyphenated}",
json={"status": "failed"},
)
if callback_context.triggered_id == "btn-success":
threading_target = _complete
elif callback_context.triggered_id == "btn-failure":
threading_target = _fail
else:
raise PreventUpdate
threading.Thread(target=threading_target, daemon=True).start()
# Returning True activates each supervisor's polling loop
return True
Run the app#
if __name__ == "__main__":
app.run()