edda-framework 0.6.0__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- edda/storage/protocol.py +18 -4
- edda/storage/sqlalchemy_storage.py +105 -5
- edda/viewer_ui/app.py +552 -126
- edda/viewer_ui/components.py +81 -68
- edda/viewer_ui/data_service.py +42 -3
- edda/viewer_ui/theme.py +200 -0
- {edda_framework-0.6.0.dist-info → edda_framework-0.7.0.dist-info}/METADATA +3 -1
- {edda_framework-0.6.0.dist-info → edda_framework-0.7.0.dist-info}/RECORD +11 -10
- {edda_framework-0.6.0.dist-info → edda_framework-0.7.0.dist-info}/WHEEL +1 -1
- {edda_framework-0.6.0.dist-info → edda_framework-0.7.0.dist-info}/entry_points.txt +0 -0
- {edda_framework-0.6.0.dist-info → edda_framework-0.7.0.dist-info}/licenses/LICENSE +0 -0
edda/viewer_ui/components.py
CHANGED
|
@@ -5,11 +5,14 @@ UI components for generating interactive Mermaid diagrams.
|
|
|
5
5
|
import json
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
+
from edda.viewer_ui.theme import get_edge_color, get_mermaid_node_style, get_mermaid_style
|
|
8
9
|
from edda.visualizer.ast_analyzer import WorkflowAnalyzer
|
|
9
10
|
from edda.visualizer.mermaid_generator import MermaidGenerator
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
def generate_interactive_mermaid(
|
|
13
|
+
def generate_interactive_mermaid(
|
|
14
|
+
instance_id: str, history: list[dict[str, Any]], is_dark: bool = False
|
|
15
|
+
) -> str:
|
|
13
16
|
"""
|
|
14
17
|
Generate interactive Mermaid diagram from execution history.
|
|
15
18
|
|
|
@@ -22,6 +25,7 @@ def generate_interactive_mermaid(instance_id: str, history: list[dict[str, Any]]
|
|
|
22
25
|
Args:
|
|
23
26
|
instance_id: Workflow instance ID
|
|
24
27
|
history: List of execution activity dictionaries
|
|
28
|
+
is_dark: Whether dark mode is enabled
|
|
25
29
|
|
|
26
30
|
Returns:
|
|
27
31
|
Mermaid flowchart diagram code with embedded click events
|
|
@@ -41,6 +45,20 @@ def generate_interactive_mermaid(instance_id: str, history: list[dict[str, Any]]
|
|
|
41
45
|
activity_occurrences: dict[str, list[str]] = {} # Track activity name -> list of activity_ids
|
|
42
46
|
activity_index = 0
|
|
43
47
|
|
|
48
|
+
# Status icon mapping
|
|
49
|
+
status_icons = {
|
|
50
|
+
"completed": "✅",
|
|
51
|
+
"running": "⏳",
|
|
52
|
+
"failed": "❌",
|
|
53
|
+
"waiting_for_event": "⏸️",
|
|
54
|
+
"waiting_for_timer": "⏱️",
|
|
55
|
+
"cancelled": "🚫",
|
|
56
|
+
"compensated": "🔄",
|
|
57
|
+
"compensating": "🔄",
|
|
58
|
+
"compensation_failed": "⚠️",
|
|
59
|
+
"event_received": "📨",
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
for activity_data in history:
|
|
45
63
|
activity_id = activity_data.get("activity_id")
|
|
46
64
|
if not activity_id:
|
|
@@ -64,33 +82,13 @@ def generate_interactive_mermaid(instance_id: str, history: list[dict[str, Any]]
|
|
|
64
82
|
label_suffix = f" ({occurrence_count}x)" if occurrence_count >= 2 else ""
|
|
65
83
|
|
|
66
84
|
# Node label with status icon
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
label = f"❌ {activity}{label_suffix}"
|
|
75
|
-
style_color = "fill:#f8d7da,stroke:#dc3545,stroke-width:2px"
|
|
76
|
-
elif status == "waiting_for_event":
|
|
77
|
-
label = f"⏸️ {activity}{label_suffix}"
|
|
78
|
-
style_color = "fill:#e7f3ff,stroke:#0066cc,stroke-width:2px"
|
|
79
|
-
elif status == "cancelled":
|
|
80
|
-
label = f"🚫 {activity}{label_suffix}"
|
|
81
|
-
style_color = "fill:#fff4e6,stroke:#ff9800,stroke-width:2px"
|
|
82
|
-
elif status == "compensated":
|
|
83
|
-
label = f"🔄 {activity}{label_suffix}"
|
|
84
|
-
style_color = "fill:#ffe6f0,stroke:#e91e63,stroke-width:2px,stroke-dasharray:5"
|
|
85
|
-
elif status == "compensation_failed":
|
|
86
|
-
label = f"⚠️ {activity}{label_suffix}"
|
|
87
|
-
style_color = "fill:#ffcccc,stroke:#cc0000,stroke-width:2px"
|
|
88
|
-
elif status == "event_received":
|
|
89
|
-
label = f"📨 {activity}{label_suffix}"
|
|
90
|
-
style_color = "fill:#e6f7ff,stroke:#1890ff,stroke-width:2px"
|
|
91
|
-
else:
|
|
92
|
-
label = f"{activity}{label_suffix}"
|
|
93
|
-
style_color = "fill:#f0f0f0,stroke:#666,stroke-width:2px"
|
|
85
|
+
icon = status_icons.get(status, "")
|
|
86
|
+
label = f"{icon} {activity}{label_suffix}" if icon else f"{activity}{label_suffix}"
|
|
87
|
+
|
|
88
|
+
# Get themed style colors
|
|
89
|
+
style_color = get_mermaid_style(status, is_dark)
|
|
90
|
+
if status == "compensated":
|
|
91
|
+
style_color += ",stroke-dasharray:5"
|
|
94
92
|
|
|
95
93
|
# Node definition
|
|
96
94
|
lines.append(f' {node_id}["{label}"]')
|
|
@@ -133,6 +131,7 @@ class HybridMermaidGenerator(MermaidGenerator):
|
|
|
133
131
|
compensations: dict[str, dict[str, Any]] | None = None,
|
|
134
132
|
workflow_status: str = "running",
|
|
135
133
|
activity_status_map: dict[str, str] | None = None,
|
|
134
|
+
is_dark: bool = False,
|
|
136
135
|
):
|
|
137
136
|
"""
|
|
138
137
|
Initialize hybrid Mermaid generator.
|
|
@@ -143,6 +142,7 @@ class HybridMermaidGenerator(MermaidGenerator):
|
|
|
143
142
|
compensations: Optional mapping of activity_id -> compensation info
|
|
144
143
|
workflow_status: Status of the workflow instance (running, completed, failed, etc.)
|
|
145
144
|
activity_status_map: Optional mapping of activity name to status (completed, failed, etc.)
|
|
145
|
+
is_dark: Whether dark mode is enabled
|
|
146
146
|
"""
|
|
147
147
|
super().__init__()
|
|
148
148
|
self.instance_id = instance_id
|
|
@@ -150,6 +150,7 @@ class HybridMermaidGenerator(MermaidGenerator):
|
|
|
150
150
|
self.compensations = compensations or {}
|
|
151
151
|
self.workflow_status = workflow_status
|
|
152
152
|
self.activity_status_map = activity_status_map or {}
|
|
153
|
+
self.is_dark = is_dark
|
|
153
154
|
self.activity_id_map: dict[str, str] = {} # Map activity name to activity_id for clicks
|
|
154
155
|
self.activity_execution_counts: dict[str, int] = {} # Map activity name to execution count
|
|
155
156
|
self.edge_counter = 0 # Track edge indices for linkStyle
|
|
@@ -200,9 +201,8 @@ class HybridMermaidGenerator(MermaidGenerator):
|
|
|
200
201
|
# Add a visual separator (compensation execution section)
|
|
201
202
|
comp_start_id = self._next_node_id()
|
|
202
203
|
self.lines.append(f" {comp_start_id}[Compensation Execution]")
|
|
203
|
-
self.
|
|
204
|
-
|
|
205
|
-
)
|
|
204
|
+
comp_header_style = get_mermaid_style("running", self.is_dark)
|
|
205
|
+
self.lines.append(f" style {comp_start_id} {comp_header_style}")
|
|
206
206
|
self.lines.append(f" {end_id} -.->|rollback| {comp_start_id}")
|
|
207
207
|
self.edge_counter += 1
|
|
208
208
|
|
|
@@ -217,9 +217,8 @@ class HybridMermaidGenerator(MermaidGenerator):
|
|
|
217
217
|
label = f"🔄 {comp_name}"
|
|
218
218
|
|
|
219
219
|
self.lines.append(f' {comp_node_id}["{label}"]')
|
|
220
|
-
self.
|
|
221
|
-
|
|
222
|
-
)
|
|
220
|
+
comp_node_style = get_mermaid_style("compensating", self.is_dark)
|
|
221
|
+
self.lines.append(f" style {comp_node_id} {comp_node_style},stroke-width:3px")
|
|
223
222
|
|
|
224
223
|
# Add click event for compensation activity
|
|
225
224
|
self.lines.append(
|
|
@@ -233,10 +232,11 @@ class HybridMermaidGenerator(MermaidGenerator):
|
|
|
233
232
|
|
|
234
233
|
prev_comp_id = comp_node_id
|
|
235
234
|
|
|
236
|
-
# Add linkStyle for executed edges
|
|
235
|
+
# Add linkStyle for executed edges
|
|
237
236
|
if self.executed_edges:
|
|
238
237
|
edge_indices = ",".join(str(i) for i in self.executed_edges)
|
|
239
|
-
|
|
238
|
+
edge_color = get_edge_color("executed", self.is_dark)
|
|
239
|
+
self.lines.append(f" linkStyle {edge_indices} stroke:{edge_color},stroke-width:3px")
|
|
240
240
|
|
|
241
241
|
return "\n".join(self.lines)
|
|
242
242
|
|
|
@@ -296,21 +296,22 @@ class HybridMermaidGenerator(MermaidGenerator):
|
|
|
296
296
|
if has_compensation:
|
|
297
297
|
label += " ⚠"
|
|
298
298
|
|
|
299
|
-
# Style based on status
|
|
299
|
+
# Style based on status using theme colors
|
|
300
300
|
if status == "completed":
|
|
301
|
-
style_color = "
|
|
301
|
+
style_color = get_mermaid_style("completed", self.is_dark) + ",stroke-width:3px"
|
|
302
302
|
elif status == "failed":
|
|
303
|
-
style_color = "
|
|
303
|
+
style_color = get_mermaid_style("failed", self.is_dark) + ",stroke-width:3px"
|
|
304
304
|
elif status == "compensated":
|
|
305
305
|
style_color = (
|
|
306
|
-
"
|
|
306
|
+
get_mermaid_style("compensating", self.is_dark)
|
|
307
|
+
+ ",stroke-width:3px,stroke-dasharray:5"
|
|
307
308
|
)
|
|
308
309
|
elif status is not None:
|
|
309
|
-
# Other executed statuses
|
|
310
|
-
style_color = "
|
|
310
|
+
# Other executed statuses (running, waiting)
|
|
311
|
+
style_color = get_mermaid_style("running", self.is_dark) + ",stroke-width:3px"
|
|
311
312
|
else:
|
|
312
313
|
# Not executed
|
|
313
|
-
style_color = "
|
|
314
|
+
style_color = get_mermaid_style("not_executed", self.is_dark)
|
|
314
315
|
|
|
315
316
|
self.lines.append(f' {node_id}["{label}"]')
|
|
316
317
|
self.lines.append(f" style {node_id} {style_color}")
|
|
@@ -338,7 +339,8 @@ class HybridMermaidGenerator(MermaidGenerator):
|
|
|
338
339
|
func_name = step.get("activity_name", step.get("function", "unknown"))
|
|
339
340
|
self.lines.append(f" {node_id}[register_compensation:<br/>{func_name}]")
|
|
340
341
|
self.lines.append(f" {current_id} --> {node_id}")
|
|
341
|
-
|
|
342
|
+
comp_reg_style = get_mermaid_style("compensating", self.is_dark)
|
|
343
|
+
self.lines.append(f" style {node_id} {comp_reg_style}")
|
|
342
344
|
|
|
343
345
|
# Track compensation for reverse path
|
|
344
346
|
self.compensation_nodes.append((current_id, node_id))
|
|
@@ -357,7 +359,8 @@ class HybridMermaidGenerator(MermaidGenerator):
|
|
|
357
359
|
|
|
358
360
|
self.lines.append(f" {node_id}{{{{{label}}}}}")
|
|
359
361
|
self.lines.append(f" {current_id} --> {node_id}")
|
|
360
|
-
|
|
362
|
+
wait_event_style = get_mermaid_style("waiting_event", self.is_dark)
|
|
363
|
+
self.lines.append(f" style {node_id} {wait_event_style}")
|
|
361
364
|
current_id = node_id
|
|
362
365
|
|
|
363
366
|
elif step_type == "condition":
|
|
@@ -446,13 +449,15 @@ class HybridMermaidGenerator(MermaidGenerator):
|
|
|
446
449
|
self.executed_edges.append(self.edge_counter)
|
|
447
450
|
self.edge_counter += 1
|
|
448
451
|
|
|
449
|
-
|
|
452
|
+
condition_style = get_mermaid_node_style("condition", self.is_dark)
|
|
453
|
+
self.lines.append(f" style {cond_id} {condition_style}")
|
|
450
454
|
|
|
451
455
|
# Create merge node with invisible/minimal label
|
|
452
456
|
merge_id = self._next_node_id()
|
|
453
457
|
# Use a minimal circle node for merging (instead of showing "N4")
|
|
454
458
|
self.lines.append(f" {merge_id}(( ))") # Small empty circle
|
|
455
|
-
|
|
459
|
+
merge_style = get_mermaid_node_style("merge", self.is_dark)
|
|
460
|
+
self.lines.append(f" style {merge_id} {merge_style}")
|
|
456
461
|
|
|
457
462
|
# If branch
|
|
458
463
|
if if_branch:
|
|
@@ -555,21 +560,22 @@ class HybridMermaidGenerator(MermaidGenerator):
|
|
|
555
560
|
else:
|
|
556
561
|
label = f"{label_prefix}{func_name}"
|
|
557
562
|
|
|
558
|
-
# Style based on status
|
|
563
|
+
# Style based on status using theme colors
|
|
559
564
|
if status == "completed":
|
|
560
|
-
style_color = "
|
|
565
|
+
style_color = get_mermaid_style("completed", self.is_dark) + ",stroke-width:3px"
|
|
561
566
|
elif status == "failed":
|
|
562
|
-
style_color = "
|
|
567
|
+
style_color = get_mermaid_style("failed", self.is_dark) + ",stroke-width:3px"
|
|
563
568
|
elif status == "compensated":
|
|
564
569
|
style_color = (
|
|
565
|
-
"
|
|
570
|
+
get_mermaid_style("compensating", self.is_dark)
|
|
571
|
+
+ ",stroke-width:3px,stroke-dasharray:5"
|
|
566
572
|
)
|
|
567
573
|
elif status is not None:
|
|
568
|
-
# Other executed statuses
|
|
569
|
-
style_color = "
|
|
574
|
+
# Other executed statuses (running, waiting)
|
|
575
|
+
style_color = get_mermaid_style("running", self.is_dark) + ",stroke-width:3px"
|
|
570
576
|
else:
|
|
571
577
|
# Not executed
|
|
572
|
-
style_color = "
|
|
578
|
+
style_color = get_mermaid_style("not_executed", self.is_dark)
|
|
573
579
|
|
|
574
580
|
self.lines.append(f' {node_id}["{label}"]')
|
|
575
581
|
self.lines.append(f" style {node_id} {style_color}")
|
|
@@ -732,7 +738,8 @@ class HybridMermaidGenerator(MermaidGenerator):
|
|
|
732
738
|
self.lines.append(f' {loop_id}["{label}"]')
|
|
733
739
|
self.lines.append(f" {prev_id} --> {loop_id}")
|
|
734
740
|
self.edge_counter += 1 # Edge from prev to loop
|
|
735
|
-
|
|
741
|
+
loop_style = get_mermaid_node_style("loop", self.is_dark)
|
|
742
|
+
self.lines.append(f" style {loop_id} {loop_style}")
|
|
736
743
|
|
|
737
744
|
# Check if loop body contains executed activities
|
|
738
745
|
body = loop.get("body", [])
|
|
@@ -750,7 +757,8 @@ class HybridMermaidGenerator(MermaidGenerator):
|
|
|
750
757
|
# Create exit node as small empty circle (instead of labeled node)
|
|
751
758
|
exit_id = self._next_node_id()
|
|
752
759
|
self.lines.append(f" {exit_id}(( ))") # Small empty circle
|
|
753
|
-
|
|
760
|
+
merge_style = get_mermaid_node_style("merge", self.is_dark)
|
|
761
|
+
self.lines.append(f" style {exit_id} {merge_style}")
|
|
754
762
|
self.lines.append(f" {loop_id} -->|exit| {exit_id}")
|
|
755
763
|
self.edge_counter += 1
|
|
756
764
|
|
|
@@ -786,12 +794,14 @@ class HybridMermaidGenerator(MermaidGenerator):
|
|
|
786
794
|
self.lines.append(f" {match_id}{{{{match {subject}}}}}")
|
|
787
795
|
self.lines.append(f" {prev_id} --> {match_id}")
|
|
788
796
|
self.edge_counter += 1
|
|
789
|
-
|
|
797
|
+
match_style = get_mermaid_node_style("match", self.is_dark)
|
|
798
|
+
self.lines.append(f" style {match_id} {match_style}")
|
|
790
799
|
|
|
791
800
|
# Create merge node
|
|
792
801
|
merge_id = self._next_node_id()
|
|
793
802
|
self.lines.append(f" {merge_id}(( ))") # Small empty circle for merge
|
|
794
|
-
|
|
803
|
+
merge_style = get_mermaid_node_style("merge", self.is_dark)
|
|
804
|
+
self.lines.append(f" style {merge_id} {merge_style}")
|
|
795
805
|
|
|
796
806
|
# Process each case
|
|
797
807
|
cases = match.get("cases", [])
|
|
@@ -833,9 +843,8 @@ class HybridMermaidGenerator(MermaidGenerator):
|
|
|
833
843
|
self.executed_edges.append(self.edge_counter)
|
|
834
844
|
self.edge_counter += 1
|
|
835
845
|
|
|
836
|
-
self.
|
|
837
|
-
|
|
838
|
-
)
|
|
846
|
+
case_start_style = get_mermaid_node_style("merge", self.is_dark)
|
|
847
|
+
self.lines.append(f" style {case_start_id} {case_start_style}")
|
|
839
848
|
|
|
840
849
|
# Generate body steps starting from case_start_id
|
|
841
850
|
case_end = self._generate_steps(body, case_start_id)
|
|
@@ -875,12 +884,14 @@ class HybridMermaidGenerator(MermaidGenerator):
|
|
|
875
884
|
self.lines.append(f" {decision_id}{{{{if-elif-else}}}}")
|
|
876
885
|
self.lines.append(f" {prev_id} --> {decision_id}")
|
|
877
886
|
self.edge_counter += 1
|
|
878
|
-
|
|
887
|
+
decision_style = get_mermaid_node_style("condition", self.is_dark)
|
|
888
|
+
self.lines.append(f" style {decision_id} {decision_style}")
|
|
879
889
|
|
|
880
890
|
# Create merge node
|
|
881
891
|
merge_id = self._next_node_id()
|
|
882
892
|
self.lines.append(f" {merge_id}(( ))") # Small empty circle for merge
|
|
883
|
-
|
|
893
|
+
merge_style = get_mermaid_node_style("merge", self.is_dark)
|
|
894
|
+
self.lines.append(f" style {merge_id} {merge_style}")
|
|
884
895
|
|
|
885
896
|
# Process each branch
|
|
886
897
|
branches = multi_cond.get("branches", [])
|
|
@@ -950,9 +961,8 @@ class HybridMermaidGenerator(MermaidGenerator):
|
|
|
950
961
|
self.executed_edges.append(self.edge_counter)
|
|
951
962
|
self.edge_counter += 1
|
|
952
963
|
|
|
953
|
-
self.
|
|
954
|
-
|
|
955
|
-
)
|
|
964
|
+
branch_start_style = get_mermaid_node_style("merge", self.is_dark)
|
|
965
|
+
self.lines.append(f" style {branch_start_id} {branch_start_style}")
|
|
956
966
|
|
|
957
967
|
# Generate body steps starting from branch_start_id
|
|
958
968
|
branch_end = self._generate_steps(body, branch_start_id)
|
|
@@ -1006,6 +1016,7 @@ def generate_hybrid_mermaid(
|
|
|
1006
1016
|
source_code: str,
|
|
1007
1017
|
compensations: dict[str, dict[str, Any]] | None = None,
|
|
1008
1018
|
workflow_status: str = "running",
|
|
1019
|
+
is_dark: bool = False,
|
|
1009
1020
|
) -> str:
|
|
1010
1021
|
"""
|
|
1011
1022
|
Generate hybrid Mermaid diagram combining static analysis and execution history.
|
|
@@ -1017,6 +1028,7 @@ def generate_hybrid_mermaid(
|
|
|
1017
1028
|
source_code: Source code of the workflow function
|
|
1018
1029
|
compensations: Optional mapping of activity_id -> compensation info
|
|
1019
1030
|
workflow_status: Status of the workflow instance (running, completed, failed, etc.)
|
|
1031
|
+
is_dark: Whether dark mode is enabled
|
|
1020
1032
|
|
|
1021
1033
|
Returns:
|
|
1022
1034
|
Mermaid flowchart diagram code with execution highlighting
|
|
@@ -1030,7 +1042,7 @@ def generate_hybrid_mermaid(
|
|
|
1030
1042
|
|
|
1031
1043
|
if not workflows:
|
|
1032
1044
|
# Fallback to history-only diagram
|
|
1033
|
-
return generate_interactive_mermaid(instance_id, history)
|
|
1045
|
+
return generate_interactive_mermaid(instance_id, history, is_dark)
|
|
1034
1046
|
|
|
1035
1047
|
workflow_structure = workflows[0]
|
|
1036
1048
|
|
|
@@ -1079,6 +1091,7 @@ def generate_hybrid_mermaid(
|
|
|
1079
1091
|
compensations,
|
|
1080
1092
|
workflow_status,
|
|
1081
1093
|
activity_status_map,
|
|
1094
|
+
is_dark,
|
|
1082
1095
|
)
|
|
1083
1096
|
generator.activity_id_map = activity_id_map
|
|
1084
1097
|
generator.activity_execution_counts = activity_execution_counts
|
|
@@ -1089,7 +1102,7 @@ def generate_hybrid_mermaid(
|
|
|
1089
1102
|
except Exception as e:
|
|
1090
1103
|
# Fallback to history-only diagram on any error
|
|
1091
1104
|
print(f"Warning: Hybrid diagram generation failed, falling back to history-only: {e}")
|
|
1092
|
-
return generate_interactive_mermaid(instance_id, history)
|
|
1105
|
+
return generate_interactive_mermaid(instance_id, history, is_dark)
|
|
1093
1106
|
|
|
1094
1107
|
|
|
1095
1108
|
def format_json_for_display(data: Any) -> str:
|
edda/viewer_ui/data_service.py
CHANGED
|
@@ -5,7 +5,8 @@ Data service for retrieving workflow instance data from storage.
|
|
|
5
5
|
import inspect
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
|
-
from
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any, cast
|
|
9
10
|
|
|
10
11
|
from edda.pydantic_utils import is_pydantic_model
|
|
11
12
|
from edda.storage.protocol import StorageProtocol
|
|
@@ -36,8 +37,46 @@ class WorkflowDataService:
|
|
|
36
37
|
Returns:
|
|
37
38
|
List of workflow instance dictionaries
|
|
38
39
|
"""
|
|
39
|
-
|
|
40
|
-
return instances
|
|
40
|
+
result = await self.storage.list_instances(limit=limit)
|
|
41
|
+
return cast(list[dict[str, Any]], result["instances"])
|
|
42
|
+
|
|
43
|
+
async def get_instances_paginated(
|
|
44
|
+
self,
|
|
45
|
+
page_size: int = 20,
|
|
46
|
+
page_token: str | None = None,
|
|
47
|
+
status_filter: str | None = None,
|
|
48
|
+
search_query: str | None = None,
|
|
49
|
+
started_after: datetime | None = None,
|
|
50
|
+
started_before: datetime | None = None,
|
|
51
|
+
) -> dict[str, Any]:
|
|
52
|
+
"""
|
|
53
|
+
Get workflow instances with cursor-based pagination and filtering.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
page_size: Number of instances per page (10, 20, or 50)
|
|
57
|
+
page_token: Cursor for pagination (from previous response)
|
|
58
|
+
status_filter: Filter by status (e.g., "running", "completed", "failed")
|
|
59
|
+
search_query: Search by workflow name or instance ID (partial match)
|
|
60
|
+
started_after: Filter instances started after this datetime
|
|
61
|
+
started_before: Filter instances started before this datetime
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Dictionary containing:
|
|
65
|
+
- instances: List of workflow instances
|
|
66
|
+
- next_page_token: Cursor for next page, or None
|
|
67
|
+
- has_more: Boolean indicating if more pages exist
|
|
68
|
+
"""
|
|
69
|
+
# Use search_query for both workflow_name_filter and instance_id_filter
|
|
70
|
+
result = await self.storage.list_instances(
|
|
71
|
+
limit=page_size,
|
|
72
|
+
page_token=page_token,
|
|
73
|
+
status_filter=status_filter,
|
|
74
|
+
workflow_name_filter=search_query,
|
|
75
|
+
instance_id_filter=search_query,
|
|
76
|
+
started_after=started_after,
|
|
77
|
+
started_before=started_before,
|
|
78
|
+
)
|
|
79
|
+
return result
|
|
41
80
|
|
|
42
81
|
async def get_workflow_compensations(self, instance_id: str) -> dict[str, dict[str, Any]]:
|
|
43
82
|
"""
|
edda/viewer_ui/theme.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Theme configuration for Edda Workflow Viewer.
|
|
3
|
+
|
|
4
|
+
Provides centralized color palette definitions and helper functions
|
|
5
|
+
for light and dark mode support.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
# Tailwind CSS compatible color palette
|
|
11
|
+
COLORS: dict[str, dict[str, Any]] = {
|
|
12
|
+
"light": {
|
|
13
|
+
# Background colors
|
|
14
|
+
"bg_page": "#FFFFFF",
|
|
15
|
+
"bg_surface": "#F8FAFC", # Slate 50
|
|
16
|
+
"bg_elevated": "#FFFFFF",
|
|
17
|
+
"border": "#E2E8F0", # Slate 200
|
|
18
|
+
# Text colors
|
|
19
|
+
"text_primary": "#0F172A", # Slate 900
|
|
20
|
+
"text_secondary": "#64748B", # Slate 500
|
|
21
|
+
"text_muted": "#94A3B8", # Slate 400
|
|
22
|
+
# Status colors (bg, stroke, text)
|
|
23
|
+
"completed": {"bg": "#ECFDF5", "stroke": "#10B981", "text": "#065F46"},
|
|
24
|
+
"running": {"bg": "#FEF3C7", "stroke": "#F59E0B", "text": "#92400E"},
|
|
25
|
+
"failed": {"bg": "#FEE2E2", "stroke": "#EF4444", "text": "#991B1B"},
|
|
26
|
+
"waiting_event": {"bg": "#DBEAFE", "stroke": "#3B82F6", "text": "#1E40AF"},
|
|
27
|
+
"waiting_timer": {"bg": "#E0F2FE", "stroke": "#0EA5E9", "text": "#075985"},
|
|
28
|
+
"cancelled": {"bg": "#FFEDD5", "stroke": "#F97316", "text": "#9A3412"},
|
|
29
|
+
"compensating": {"bg": "#F3E8FF", "stroke": "#A855F7", "text": "#6B21A8"},
|
|
30
|
+
"not_executed": {"bg": "#F1F5F9", "stroke": "#CBD5E1", "text": "#64748B"},
|
|
31
|
+
"event_received": {"bg": "#CFFAFE", "stroke": "#06B6D4", "text": "#0E7490"},
|
|
32
|
+
"compensation_failed": {"bg": "#FEE2E2", "stroke": "#B91C1C", "text": "#7F1D1D"},
|
|
33
|
+
# Mermaid diagram specific
|
|
34
|
+
"condition": {"bg": "#FEF3C7", "stroke": "#F59E0B"},
|
|
35
|
+
"loop": {"bg": "#FDF4FF", "stroke": "#D946EF"},
|
|
36
|
+
"match": {"bg": "#ECFDF5", "stroke": "#10B981"},
|
|
37
|
+
"merge": {"bg": "#FFFFFF", "stroke": "#E2E8F0"},
|
|
38
|
+
"edge_executed": "#10B981",
|
|
39
|
+
"edge_not_executed": "#CBD5E1",
|
|
40
|
+
"edge_compensation": "#A855F7",
|
|
41
|
+
},
|
|
42
|
+
"dark": {
|
|
43
|
+
# Background colors
|
|
44
|
+
"bg_page": "#0F172A", # Slate 900
|
|
45
|
+
"bg_surface": "#1E293B", # Slate 800
|
|
46
|
+
"bg_elevated": "#334155", # Slate 700
|
|
47
|
+
"border": "#475569", # Slate 600
|
|
48
|
+
# Text colors
|
|
49
|
+
"text_primary": "#F1F5F9", # Slate 100
|
|
50
|
+
"text_secondary": "#94A3B8", # Slate 400
|
|
51
|
+
"text_muted": "#64748B", # Slate 500
|
|
52
|
+
# Status colors (bg, stroke, text)
|
|
53
|
+
"completed": {"bg": "#064E3B", "stroke": "#34D399", "text": "#A7F3D0"},
|
|
54
|
+
"running": {"bg": "#78350F", "stroke": "#FBBF24", "text": "#FDE68A"},
|
|
55
|
+
"failed": {"bg": "#7F1D1D", "stroke": "#F87171", "text": "#FECACA"},
|
|
56
|
+
"waiting_event": {"bg": "#1E3A8A", "stroke": "#60A5FA", "text": "#BFDBFE"},
|
|
57
|
+
"waiting_timer": {"bg": "#0C4A6E", "stroke": "#38BDF8", "text": "#BAE6FD"},
|
|
58
|
+
"cancelled": {"bg": "#7C2D12", "stroke": "#FB923C", "text": "#FED7AA"},
|
|
59
|
+
"compensating": {"bg": "#581C87", "stroke": "#C084FC", "text": "#E9D5FF"},
|
|
60
|
+
"not_executed": {"bg": "#334155", "stroke": "#64748B", "text": "#94A3B8"},
|
|
61
|
+
"event_received": {"bg": "#164E63", "stroke": "#22D3EE", "text": "#A5F3FC"},
|
|
62
|
+
"compensation_failed": {"bg": "#7F1D1D", "stroke": "#FCA5A5", "text": "#FECACA"},
|
|
63
|
+
# Mermaid diagram specific
|
|
64
|
+
"condition": {"bg": "#78350F", "stroke": "#FBBF24"},
|
|
65
|
+
"loop": {"bg": "#4C1D95", "stroke": "#E879F9"},
|
|
66
|
+
"match": {"bg": "#064E3B", "stroke": "#34D399"},
|
|
67
|
+
"merge": {"bg": "#1E293B", "stroke": "#475569"},
|
|
68
|
+
"edge_executed": "#34D399",
|
|
69
|
+
"edge_not_executed": "#64748B",
|
|
70
|
+
"edge_compensation": "#C084FC",
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_status_color(status: str, is_dark: bool) -> dict[str, str]:
|
|
76
|
+
"""
|
|
77
|
+
Get color configuration for a workflow status.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
status: Status name (e.g., "completed", "running", "failed")
|
|
81
|
+
is_dark: Whether dark mode is enabled
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Dictionary with 'bg', 'stroke', and 'text' colors
|
|
85
|
+
"""
|
|
86
|
+
theme = "dark" if is_dark else "light"
|
|
87
|
+
status_key = status.lower().replace(" ", "_").replace("-", "_")
|
|
88
|
+
|
|
89
|
+
# Handle special status mappings
|
|
90
|
+
status_mapping = {
|
|
91
|
+
"waiting": "waiting_event",
|
|
92
|
+
"waiting_for_event": "waiting_event",
|
|
93
|
+
"waiting_for_timer": "waiting_timer",
|
|
94
|
+
"compensated": "compensating",
|
|
95
|
+
}
|
|
96
|
+
status_key = status_mapping.get(status_key, status_key)
|
|
97
|
+
|
|
98
|
+
return cast(dict[str, str], COLORS[theme].get(status_key, COLORS[theme]["not_executed"]))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_mermaid_style(status: str, is_dark: bool) -> str:
|
|
102
|
+
"""
|
|
103
|
+
Get Mermaid style string for a status.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
status: Status name
|
|
107
|
+
is_dark: Whether dark mode is enabled
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Mermaid style string (e.g., "fill:#ECFDF5,stroke:#10B981,stroke-width:2px")
|
|
111
|
+
"""
|
|
112
|
+
colors = get_status_color(status, is_dark)
|
|
113
|
+
return f"fill:{colors['bg']},stroke:{colors['stroke']},stroke-width:2px"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_mermaid_node_style(node_type: str, is_dark: bool) -> str:
|
|
117
|
+
"""
|
|
118
|
+
Get Mermaid style for structural nodes (condition, loop, merge).
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
node_type: Node type (e.g., "condition", "loop", "merge")
|
|
122
|
+
is_dark: Whether dark mode is enabled
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Mermaid style string
|
|
126
|
+
"""
|
|
127
|
+
theme = "dark" if is_dark else "light"
|
|
128
|
+
colors = COLORS[theme].get(node_type, COLORS[theme]["merge"])
|
|
129
|
+
stroke_width = "1px" if node_type == "merge" else "2px"
|
|
130
|
+
return f"fill:{colors['bg']},stroke:{colors['stroke']},stroke-width:{stroke_width}"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_edge_color(edge_type: str, is_dark: bool) -> str:
|
|
134
|
+
"""
|
|
135
|
+
Get edge (arrow) color for Mermaid diagrams.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
edge_type: Edge type ("executed", "not_executed", "compensation")
|
|
139
|
+
is_dark: Whether dark mode is enabled
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Color hex code
|
|
143
|
+
"""
|
|
144
|
+
theme = "dark" if is_dark else "light"
|
|
145
|
+
key = f"edge_{edge_type}"
|
|
146
|
+
return cast(str, COLORS[theme].get(key, COLORS[theme]["edge_not_executed"]))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Tailwind CSS class mappings for UI components
|
|
150
|
+
# Note: Background colors are controlled via CSS in app.py for proper dark mode support
|
|
151
|
+
# NiceGUI adds 'dark' class to body, but Tailwind's dark: prefix expects it on html
|
|
152
|
+
TAILWIND_CLASSES = {
|
|
153
|
+
"card": "border", # Background handled by CSS
|
|
154
|
+
"card_hover": "", # Hover handled by CSS
|
|
155
|
+
"surface": "", # Background handled by CSS
|
|
156
|
+
"text_primary": "text-slate-900 dark:text-slate-100",
|
|
157
|
+
"text_secondary": "text-slate-500 dark:text-slate-400",
|
|
158
|
+
"text_muted": "text-slate-400 dark:text-slate-500",
|
|
159
|
+
"border": "border-slate-200 dark:border-slate-700",
|
|
160
|
+
"input": "border-slate-300 dark:border-slate-600", # Background handled by CSS
|
|
161
|
+
"code_block": "border", # Background handled by CSS
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Status badge Tailwind classes
|
|
165
|
+
STATUS_BADGE_CLASSES = {
|
|
166
|
+
"completed": ("bg-emerald-50 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300"),
|
|
167
|
+
"running": "bg-amber-50 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300",
|
|
168
|
+
"failed": "bg-red-50 text-red-700 dark:bg-red-900/50 dark:text-red-300",
|
|
169
|
+
"waiting_event": "bg-blue-50 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300",
|
|
170
|
+
"waiting_timer": "bg-sky-50 text-sky-700 dark:bg-sky-900/50 dark:text-sky-300",
|
|
171
|
+
"cancelled": ("bg-orange-50 text-orange-700 dark:bg-orange-900/50 dark:text-orange-300"),
|
|
172
|
+
"compensating": ("bg-purple-50 text-purple-700 dark:bg-purple-900/50 dark:text-purple-300"),
|
|
173
|
+
"not_executed": ("bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-400"),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def get_status_badge_classes(status: str) -> str:
|
|
178
|
+
"""
|
|
179
|
+
Get Tailwind CSS classes for a status badge.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
status: Status name
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Tailwind CSS class string
|
|
186
|
+
"""
|
|
187
|
+
status_key = status.lower().replace(" ", "_").replace("-", "_")
|
|
188
|
+
|
|
189
|
+
# Handle special status mappings
|
|
190
|
+
status_mapping = {
|
|
191
|
+
"waiting": "waiting_event",
|
|
192
|
+
"waiting_for_event": "waiting_event",
|
|
193
|
+
"waiting_for_timer": "waiting_timer",
|
|
194
|
+
"compensated": "compensating",
|
|
195
|
+
}
|
|
196
|
+
status_key = status_mapping.get(status_key, status_key)
|
|
197
|
+
|
|
198
|
+
base_classes = "px-3 py-1 rounded-full text-sm font-medium"
|
|
199
|
+
status_classes = STATUS_BADGE_CLASSES.get(status_key, STATUS_BADGE_CLASSES["not_executed"])
|
|
200
|
+
return f"{base_classes} {status_classes}"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: edda-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Lightweight Durable Execution Framework
|
|
5
5
|
Project-URL: Homepage, https://github.com/i2y/edda
|
|
6
6
|
Project-URL: Documentation, https://github.com/i2y/edda#readme
|
|
@@ -30,11 +30,13 @@ Requires-Dist: sqlalchemy[asyncio]>=2.0.0
|
|
|
30
30
|
Requires-Dist: uvloop>=0.22.1
|
|
31
31
|
Provides-Extra: dev
|
|
32
32
|
Requires-Dist: black>=25.9.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: mcp>=1.22.0; extra == 'dev'
|
|
33
34
|
Requires-Dist: mypy>=1.18.2; extra == 'dev'
|
|
34
35
|
Requires-Dist: pytest-asyncio>=1.2.0; extra == 'dev'
|
|
35
36
|
Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
|
|
36
37
|
Requires-Dist: pytest>=8.4.2; extra == 'dev'
|
|
37
38
|
Requires-Dist: ruff>=0.14.2; extra == 'dev'
|
|
39
|
+
Requires-Dist: starlette>=0.40.0; extra == 'dev'
|
|
38
40
|
Requires-Dist: testcontainers[mysql]>=4.0.0; extra == 'dev'
|
|
39
41
|
Requires-Dist: testcontainers[postgres]>=4.0.0; extra == 'dev'
|
|
40
42
|
Requires-Dist: tsuno>=0.1.3; extra == 'dev'
|