Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FSM state diagram #306

Merged
merged 11 commits into from
Jan 23, 2025
1 change: 1 addition & 0 deletions controller/templates/controller/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
href="https://cdn.jsdelivr.net/npm/@shoelace-style/[email protected]/cdn/themes/light.css" />
<script type="module"
src="https://cdn.jsdelivr.net/npm/@shoelace-style/[email protected]/cdn/shoelace-autoloader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/mermaid.min.js"></script>
{% endblock extra_js %}
{% block content %}
<div class="container-fluid no-padding no-margin">
Expand Down
34 changes: 30 additions & 4 deletions controller/templates/controller/partials/state_machine.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
{% load render_table from django_tables2 %}
<div _="on load if #argsDialog is not null remove #argsDialog"></div>
<form>
{% csrf_token %}
{% render_table table %}
</form>
<div class="row">
<pre class="mermaid">
{{ flowchart }}
</pre>
<form>
{% csrf_token %}
{% render_table table %}
</form>
</div>
<script>
// Initialize mermaid diagrams.
mermaid.initialize({
theme: "base",
// Make flowchart text uppercase.
themeCSS: ".label { text-transform: uppercase; }",
themeVariables: {
primaryColor: "#b5b3ae",
primaryTextColor: "white",
// HACK: for setting custom edge label backgrounds.
edgeLabelBackground: "transparent",
},
});
// Refresh mermaid diagrams after htmx swap.
document.body.addEventListener("htmx:afterSwap", (event) => {
const target = event.detail.target;
if (target && target.querySelector(".mermaid")) {
mermaid.run({nodes: target.querySelectorAll(".mermaid")});
}
});
</script>
32 changes: 30 additions & 2 deletions controller/views/partials.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,31 @@
from .. import forms, fsm, tables


def make_fsm_flowchart(states: dict[str, dict[str, str]], current_state: str) -> str:
"""Create Mermaid syntax for a flowchart of FSM states and transitions.

Args:
states (dict[str, dict[str, str]]): The FSM states and events.
current_state (str): The current state of the FSM.

Returns:
str: Mermaid syntax for the flowchart.
"""
link = 0
chart = "flowchart LR\n"
chart += "classDef default stroke:black,stroke-width:2px\n"
chart += "linkStyle default background-color:#b5b3ae,stroke-width:2px\n"
for state, events in states.items():
for event, target in events.items():
chart += f"{state}({state}) -->|{event}| {target}({target})\n"
if state == current_state:
chart += f"style {state} fill:#93c54b,color:#325d88\n"
chart += f"linkStyle {link} background-color:#93c54b,color:#325d88\n"
link += 1

return chart


@login_required
def state_machine(request: HttpRequest) -> HttpResponse:
"""Triggers a chan."""
Expand All @@ -27,11 +52,14 @@ def state_machine(request: HttpRequest) -> HttpResponse:
else:
raise ValueError(f"Invalid form: {form.errors}")

table = tables.FSMTable.from_dict(fsm.get_fsm_architecture(), ci.get_fsm_state())
states = fsm.get_fsm_architecture()
current_state = ci.get_fsm_state()
table = tables.FSMTable.from_dict(states, current_state)
flowchart = make_fsm_flowchart(states, current_state)

return render(
request=request,
context=dict(table=table),
context=dict(table=table, flowchart=flowchart),
template_name="controller/partials/state_machine.html",
)

Expand Down
29 changes: 29 additions & 0 deletions tests/controller/views/test_partial_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,35 @@
from ...utils import LoginRequiredTest


def test_make_fsm_flowchart():
"""Test the make_fsm_flowchart function."""
from controller.views.partials import make_fsm_flowchart

states = {
"state1": {
"event1": "state2",
"event2": "state3",
},
"state2": {
"event3": "state1",
},
}
current_state = "state2"
result = make_fsm_flowchart(states, current_state)

assert "flowchart LR\n" in result
assert "classDef default " in result
assert "linkStyle default " in result

assert "state1(state1) -->|event1| state2(state2)\n" in result
assert "state1(state1) -->|event2| state3(state3)\n" in result
assert "state2(state2) -->|event3| state1(state1)\n" in result

# Check current state is highlighted.
assert "style state2 fill:#93c54b,color:#325d88\n" in result
assert "linkStyle 2 background-color:#93c54b,color:#325d88\n" in result


class TestFSMView(LoginRequiredTest):
"""Test the controller.views.state_machine view function."""

Expand Down
Loading