diff --git a/joeflow/admin.py b/joeflow/admin.py index 07f430d..7f7dbc5 100644 --- a/joeflow/admin.py +++ b/joeflow/admin.py @@ -147,12 +147,25 @@ def get_inlines(self, *args, **kwargs): def get_readonly_fields(self, *args, **kwargs): return [ - "get_instance_graph_svg", + "display_workflow_diagram", *super().get_readonly_fields(*args, **kwargs), "modified", "created", ] + @admin.display(description="Workflow Diagram") + def display_workflow_diagram(self, obj): + """Display workflow diagram using MermaidJS for client-side rendering.""" + if obj.pk: + # Get Mermaid diagram syntax + mermaid_syntax = obj.get_instance_graph_mermaid() + # Wrap in div with mermaid class for client-side rendering + return format_html( + '
{}
', + mermaid_syntax + ) + return "" + @transaction.atomic() def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) diff --git a/joeflow/models.py b/joeflow/models.py index cfe51e1..f332f19 100644 --- a/joeflow/models.py +++ b/joeflow/models.py @@ -242,6 +242,39 @@ def get_graph_svg(cls): get_graph_svg.short_description = t("graph") + @classmethod + def get_graph_mermaid(cls, color="black"): + """ + Return workflow graph as Mermaid diagram syntax. + + This can be used with MermaidJS for client-side rendering in browsers. + + Returns: + (str): Mermaid diagram syntax. + """ + lines = [f"graph {cls.rankdir}"] + + # Add nodes + for name, node in cls.get_nodes(): + node_id = name.replace(" ", "_") + label = name + + # Determine shape based on node type + if node.type == HUMAN: + # Rounded rectangle for human tasks + lines.append(f" {node_id}({label})") + else: + # Rectangle for machine tasks + lines.append(f" {node_id}[{label}]") + + # Add edges + for start, end in cls.edges: + start_id = start.name.replace(" ", "_") + end_id = end.name.replace(" ", "_") + lines.append(f" {start_id} --> {end_id}") + + return "\n".join(lines) + def get_instance_graph(self): """Return workflow instance graph.""" graph = self.get_graph(color="#888888") @@ -332,6 +365,41 @@ def get_instance_graph_svg(self, output_format="svg"): get_instance_graph_svg.short_description = t("instance graph") + def get_instance_graph_mermaid(self): + """ + Return instance graph as Mermaid diagram syntax. + + This can be used with MermaidJS for client-side rendering in admin. + + Returns: + (str): Mermaid diagram syntax for the instance graph. + """ + lines = [f"graph {self.rankdir}"] + + names = dict(self.get_nodes()).keys() + + # Add all nodes from workflow definition + for name, node in self.get_nodes(): + node_id = name.replace(" ", "_") + label = name + + # Determine shape based on node type + if node.type == HUMAN: + lines.append(f" {node_id}({label})") + else: + lines.append(f" {node_id}[{label}]") + + # Add edges from workflow definition + for start, end in self.edges: + start_id = start.name.replace(" ", "_") + end_id = end.name.replace(" ", "_") + lines.append(f" {start_id} --> {end_id}") + + # TODO: Add styling for completed/active tasks + # This would require additional Mermaid syntax for node styling + + return "\n".join(lines) + def cancel(self, user=None): self.task_set.cancel(user) diff --git a/joeflow/templates/admin/change_form.html b/joeflow/templates/admin/change_form.html new file mode 100644 index 0000000..ba3a4b9 --- /dev/null +++ b/joeflow/templates/admin/change_form.html @@ -0,0 +1,10 @@ +{% extends "admin/change_form.html" %} + +{% block extrahead %} +{{ block.super }} + + +{% endblock %} diff --git a/pyproject.toml b/pyproject.toml index be18ada..a5fbe9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,6 @@ requires-python = ">=3.9" dependencies = [ "django>=2.2", "django-appconf", - "graphviz>=0.18", ] [project.optional-dependencies] @@ -74,6 +73,10 @@ docs = [ "dramatiq", "django_dramatiq", "redis", + "graphviz>=0.18", +] +graphviz = [ + "graphviz>=0.18", ] reversion = [ "django-reversion",