rigid_workflow 1.0.0
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +648 -0
- data/README.md +427 -0
- data/app/assets/stylesheets/rigid_workflow/application.css +68 -0
- data/app/controllers/rigid_workflow/application_controller.rb +90 -0
- data/app/controllers/rigid_workflow/runs_controller.rb +133 -0
- data/app/controllers/rigid_workflow/workflows_controller.rb +11 -0
- data/app/helpers/rigid_workflow/runs_helper.rb +63 -0
- data/app/helpers/rigid_workflow/workflows_helper.rb +110 -0
- data/app/javascript/rigid_workflow/application.js +9 -0
- data/app/javascript/rigid_workflow/controllers/application.js +7 -0
- data/app/javascript/rigid_workflow/controllers/index.js +4 -0
- data/app/javascript/rigid_workflow/controllers/selection_controller.js +91 -0
- data/app/javascript/rigid_workflow/controllers/workflow_viz_controller.js +160 -0
- data/app/jobs/rigid_workflow/activity_job.rb +14 -0
- data/app/jobs/rigid_workflow/timer_job.rb +13 -0
- data/app/jobs/rigid_workflow/workflow_job.rb +17 -0
- data/app/models/rigid_workflow/run.rb +209 -0
- data/app/models/rigid_workflow/signal.rb +77 -0
- data/app/models/rigid_workflow/step.rb +182 -0
- data/app/models/rigid_workflow/step_attempt.rb +48 -0
- data/app/views/rigid_workflow/layouts/application.html.erb +77 -0
- data/app/views/rigid_workflow/runs/index.html.erb +113 -0
- data/app/views/rigid_workflow/runs/show.html.erb +130 -0
- data/app/views/rigid_workflow/workflows/index.html.erb +82 -0
- data/config/importmap.rb +11 -0
- data/config/routes.rb +17 -0
- data/lib/generators/rigid_workflow/activity_generator.rb +15 -0
- data/lib/generators/rigid_workflow/install_generator.rb +58 -0
- data/lib/generators/rigid_workflow/templates/activity.rb.erb +6 -0
- data/lib/generators/rigid_workflow/templates/initializer.rb.erb +24 -0
- data/lib/generators/rigid_workflow/templates/migration.rb.erb +54 -0
- data/lib/generators/rigid_workflow/templates/workflow.rb.erb +6 -0
- data/lib/generators/rigid_workflow/workflow_generator.rb +15 -0
- data/lib/rigid_workflow/activity.rb +54 -0
- data/lib/rigid_workflow/engine.rb +35 -0
- data/lib/rigid_workflow/id_generator.rb +21 -0
- data/lib/rigid_workflow/orchestrator.rb +83 -0
- data/lib/rigid_workflow/step_result.rb +50 -0
- data/lib/rigid_workflow/version.rb +5 -0
- data/lib/rigid_workflow/workflow.rb +59 -0
- data/lib/rigid_workflow/workflow_runner.rb +334 -0
- data/lib/rigid_workflow.rb +65 -0
- metadata +204 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<div class="p-6 space-y-6">
|
|
2
|
+
<div class="flex items-center justify-between">
|
|
3
|
+
<div>
|
|
4
|
+
<%= link_to overview_path, class: "text-sm text-gray-500 hover:text-gray-700 mb-2 block" do %>
|
|
5
|
+
Rigid Workflow »
|
|
6
|
+
<% end %>
|
|
7
|
+
<h1 class="text-3xl font-bold text-gray-900"><%= action_name.humanize %></h1>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div class="flex items-center justify-between my-4">
|
|
12
|
+
<div class="text-sm text-gray-500"><%= page_entries_info @runs %></div>
|
|
13
|
+
|
|
14
|
+
<% if @runs.total_count > @runs.size %>
|
|
15
|
+
<div class="text-sm">
|
|
16
|
+
<%= paginate @runs %>
|
|
17
|
+
</div>
|
|
18
|
+
<% end %>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden relative" data-controller="selection">
|
|
22
|
+
<%= form_with url: runs_bulk_action_path, method: :post, data: { selection_target: "actionForm" } do |f| %>
|
|
23
|
+
<!-- Floating Action Bar -->
|
|
24
|
+
<div data-selection-target="actionBar" class="hidden absolute mt-1 ml-10 flex items-center shadow-lg border border-gray-200 rounded-lg overflow-hidden bg-white z-10">
|
|
25
|
+
<%= hidden_field_tag :bulk_action, "", data: { selection_target: "action" } %>
|
|
26
|
+
<div class="flex items-center divide-x divide-gray-200">
|
|
27
|
+
<button type="button" data-action="click->selection#submitBulk" data-type="retry" data-selection-target="actionButton" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 flex items-center gap-2">
|
|
28
|
+
Retry <span data-selection-target="count" data-type="retry" class="ml-1 px-1.5 py-0.5 text-xs bg-gray-200 rounded-full">0</span>
|
|
29
|
+
</button>
|
|
30
|
+
<button type="button" data-action="click->selection#submitBulk" data-type="cancel" data-selection-target="actionButton" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 flex items-center gap-2">
|
|
31
|
+
Cancel <span data-selection-target="count" data-type="cancel" class="ml-1 px-1.5 py-0.5 text-xs bg-gray-200 rounded-full">0</span>
|
|
32
|
+
</button>
|
|
33
|
+
<button type="button" disabled class="hidden px-4 py-2 text-sm font-medium text-gray-400 bg-gray-100 flex items-center gap-2" data-selection-target="noActions">
|
|
34
|
+
No actions
|
|
35
|
+
</button>
|
|
36
|
+
<button type="button" data-action="click->selection#deselectAll" class="px-2 py-2 text-gray-400 hover:text-gray-600">
|
|
37
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="overflow-x-auto">
|
|
43
|
+
<table class="w-full">
|
|
44
|
+
<thead class="bg-gray-50 border-b border-gray-200 ">
|
|
45
|
+
<tr>
|
|
46
|
+
<th class="px-4 py-3 text-left"><input type="checkbox" data-selection-target="selectAll" data-action="change->selection#toggleAll" class="cursor-default rounded border-gray-300 text-blue-600 focus:ring-blue-500" <%= 'disabled' if @runs.empty? %>></th>
|
|
47
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Run ID</th>
|
|
48
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Workflow</th>
|
|
49
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
50
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Iters</th>
|
|
51
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Started</th>
|
|
52
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Duration</th>
|
|
53
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
|
|
54
|
+
</tr>
|
|
55
|
+
</thead>
|
|
56
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
|
57
|
+
<% if @runs.any? %>
|
|
58
|
+
<% @runs.each do |run| %>
|
|
59
|
+
<tr class="cursor-pointer hover:bg-gray-50" data-actions="<%= run_actions_for(run.status) %>" data-id="<%= run.id %>" data-url="<%= runs_show_path(run) %>">
|
|
60
|
+
<td class="px-4 py-4 text-left"><input type="checkbox" name="ids[]" data-selection-target="row" data-action="change->selection#toggleRow" class="cursor-default row-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" value="<%= run.id %>"></td>
|
|
61
|
+
<td class="px-4 py-4 whitespace-nowrap text-xs font-mono text-gray-500 select-all"><%= run.id.to_s[0..12] %></td>
|
|
62
|
+
<td class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-900 select-all"><%= run.workflow_class %></td>
|
|
63
|
+
<td class="px-4 py-4 whitespace-nowrap">
|
|
64
|
+
<span class="<%= {
|
|
65
|
+
'pending' => 'bg-gray-100 text-gray-700',
|
|
66
|
+
'running' => 'bg-blue-100 text-blue-700',
|
|
67
|
+
'completed' => 'bg-green-100 text-green-700',
|
|
68
|
+
'failed' => 'bg-red-100 text-red-700',
|
|
69
|
+
'compensating' => 'bg-orange-100 text-orange-700'
|
|
70
|
+
}[run.status.to_s] || 'bg-gray-100 text-gray-700' %> px-2 py-1 rounded-full text-xs font-medium capitalize"><%= run.status %></span>
|
|
71
|
+
</td>
|
|
72
|
+
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500"><%= run.iterations %></td>
|
|
73
|
+
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500"><%= format_datetime(run.started_at) %></td>
|
|
74
|
+
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500"><%= format_duration(run.duration) %></td>
|
|
75
|
+
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500"><%= relative_time_in_words(run.created_at) %></td>
|
|
76
|
+
</tr>
|
|
77
|
+
<% end %>
|
|
78
|
+
<% else %>
|
|
79
|
+
<tr>
|
|
80
|
+
<td colspan="9" class="px-4 py-4 text-sm text-center text-gray-500">Nothing to see</td>
|
|
81
|
+
</tr>
|
|
82
|
+
<% end %>
|
|
83
|
+
</tbody>
|
|
84
|
+
</table>
|
|
85
|
+
</div>
|
|
86
|
+
<% end %>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<% if @runs.total_count >= 20 %>
|
|
90
|
+
<div class="flex items-center justify-between my-4">
|
|
91
|
+
<div class="text-sm text-gray-500"><%= page_entries_info @runs %></div>
|
|
92
|
+
|
|
93
|
+
<% if @runs.total_count > @runs.size %>
|
|
94
|
+
<div class="text-sm">
|
|
95
|
+
<%= paginate @runs %>
|
|
96
|
+
</div>
|
|
97
|
+
<% end %>
|
|
98
|
+
</div>
|
|
99
|
+
<% end %>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<script>
|
|
103
|
+
document.addEventListener("turbo:load", () => {
|
|
104
|
+
const rows = document.querySelectorAll("tr[data-url]");
|
|
105
|
+
rows.forEach(row => {
|
|
106
|
+
row.addEventListener("click", (e) => {
|
|
107
|
+
if (e.target.type !== 'checkbox') {
|
|
108
|
+
window.location.href = row.dataset.url;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
</script>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<div class="p-6 space-y-6">
|
|
2
|
+
<div class="flex items-center justify-between">
|
|
3
|
+
<div>
|
|
4
|
+
<%= link_to overview_path, class: "text-sm text-gray-500 hover:text-gray-700 mb-2 block" do %>
|
|
5
|
+
Rigid Workflow »
|
|
6
|
+
<% end %>
|
|
7
|
+
<h1 class="text-3xl font-bold text-gray-900"><%= @run.workflow_class %> <%= content_tag(:code, @run.id.to_s[0..12], class:"font-mono select-all") %></h1>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="flex items-center" data-controller="selection">
|
|
10
|
+
<%= form_with url: runs_bulk_action_path, method: :post, data: { selection_target: "actionForm" }, class: "inline" do |f| %>
|
|
11
|
+
<% available_actions = run_actions_for(@run.status).split(",").map(&:strip) %>
|
|
12
|
+
<%= hidden_field_tag :bulk_action, "", data: { selection_target: "action" } %>
|
|
13
|
+
<%= hidden_field_tag "ids[]", @run.id, data: { selection_target: "actionIds" } %>
|
|
14
|
+
<div class="flex items-center shadow-lg border border-gray-200 rounded-lg overflow-hidden">
|
|
15
|
+
<button type="button" data-action="click->selection#submitBulk" data-type="retry" data-selection-target="actionButton" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 border-r border-gray-200 <%= 'opacity-50 cursor-not-allowed' unless available_actions.include?("retry") %>" <%= "disabled" unless available_actions.include?("retry") %>>
|
|
16
|
+
Retry
|
|
17
|
+
</button>
|
|
18
|
+
<button type="button" data-action="click->selection#submitBulk" data-type="cancel" data-selection-target="actionButton" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 <%= 'opacity-50 cursor-not-allowed' unless available_actions.include?("cancel") %>" <%= "disabled" unless available_actions.include?("cancel") %>>
|
|
19
|
+
Cancel
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="bg-white p-4 border border-gray-300 rounded-lg">
|
|
27
|
+
<h2 class="text-lg font-semibold text-gray-900 mb-4">Run Details</h2>
|
|
28
|
+
<div class="grid grid-flow-col grid-rows-4 gap-4">
|
|
29
|
+
<% %w[created_at started_at finished_at updated_at duration id workflow_class version params status iterations].each do |key| %>
|
|
30
|
+
<%
|
|
31
|
+
value = if @run.respond_to?(key)
|
|
32
|
+
@run.send(key)
|
|
33
|
+
elsif @run.attributes.key?(key)
|
|
34
|
+
@run.attributes[key]
|
|
35
|
+
else
|
|
36
|
+
@run.try(key.to_sym)
|
|
37
|
+
end
|
|
38
|
+
%>
|
|
39
|
+
<% next if value.nil? %>
|
|
40
|
+
<div class="space-y-1">
|
|
41
|
+
<p class="text-xs font-medium text-gray-400 uppercase"><%= key.humanize %></p>
|
|
42
|
+
<p class="text-sm text-gray-800">
|
|
43
|
+
<%= format_attribute(key, value) %>
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
<% end %>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="bg-white p-4 border border-gray-300 rounded-lg">
|
|
51
|
+
<h2 class="text-lg font-semibold text-gray-900 mb-4">Steps History</h2>
|
|
52
|
+
<% if @run.steps.any? %>
|
|
53
|
+
<% @run.steps.order(:id).each do |step| %>
|
|
54
|
+
<div class="mb-2 pb-2 border-b border-gray-100 last:border-0">
|
|
55
|
+
<div class="flex items-center justify-between">
|
|
56
|
+
<h3 class="font-medium text-gray-900">
|
|
57
|
+
<%= step.step_name %>
|
|
58
|
+
<span class="text-sm font-normal text-gray-500 ml-2"><%= step.activity_class %></span>
|
|
59
|
+
</h3>
|
|
60
|
+
<span class="<%= step.completed? ? 'bg-green-100 text-green-700' : step.failed? ? 'bg-red-100 text-red-700' : 'bg-blue-100 text-blue-700' %> px-2 py-1 rounded-full text-xs font-medium capitalize">
|
|
61
|
+
<%= step.status %>
|
|
62
|
+
</span>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<% if step.attempts.any? %>
|
|
66
|
+
<div class="mt-2 space-y-1">
|
|
67
|
+
<% step.attempts.order(:attempt_number).each do |attempt| %>
|
|
68
|
+
<div class="ml-4 px-3 py-1.5 bg-gray-50 rounded text-xs leading-tight">
|
|
69
|
+
<div>
|
|
70
|
+
<span class="font-medium">Attempt #<%= attempt.attempt_number %></span>
|
|
71
|
+
<span class="<%= attempt.completed? ? 'text-green-600' : attempt.failed? ? 'text-red-600' : 'text-blue-600' %> ml-2"><%= attempt.status %></span>
|
|
72
|
+
<% if attempt.started_at %>
|
|
73
|
+
<span class="text-gray-400 ml-2">Started: <%= format_datetime(attempt.started_at) %></span>
|
|
74
|
+
<% end %>
|
|
75
|
+
<% if attempt.finished_at %>
|
|
76
|
+
<span class="text-gray-400 ml-2">Finished: <%= format_datetime(attempt.finished_at) %></span>
|
|
77
|
+
<% end %>
|
|
78
|
+
</div>
|
|
79
|
+
<% if attempt.error_details.present? %>
|
|
80
|
+
<div class="text-red-500">
|
|
81
|
+
<strong>Error:</strong> <%= attempt.error_details["message"] %>
|
|
82
|
+
</div>
|
|
83
|
+
<% end %>
|
|
84
|
+
</div>
|
|
85
|
+
<% end %>
|
|
86
|
+
</div>
|
|
87
|
+
<% end %>
|
|
88
|
+
</div>
|
|
89
|
+
<% end %>
|
|
90
|
+
<% else %>
|
|
91
|
+
<p class="text-gray-500 text-sm">No steps recorded for this run.</p>
|
|
92
|
+
<% end %>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div data-controller="workflow-viz"
|
|
96
|
+
data-workflow-viz-run-id-value="<%= @run.id %>"
|
|
97
|
+
class="w-full border border-gray-300 rounded-lg bg-white p-4">
|
|
98
|
+
<div data-workflow-viz-target="chart" class="w-full"></div>
|
|
99
|
+
|
|
100
|
+
<!-- Templates -->
|
|
101
|
+
<template data-workflow-viz-target="stepDetailTemplate">
|
|
102
|
+
<h3 class="text-md font-semibold text-gray-900 mb-2">Step: <span data-field="name"></span></h3>
|
|
103
|
+
<div class="grid grid-cols-2 gap-4">
|
|
104
|
+
<p class="text-sm"><span class="font-medium text-gray-500">Started:</span> <span data-field="started"></span></p>
|
|
105
|
+
<p class="text-sm"><span class="font-medium text-gray-500">Finished:</span> <span data-field="finished"></span></p>
|
|
106
|
+
<p class="text-sm"><span class="font-medium text-gray-500">Status:</span> <span data-field="status"></span></p>
|
|
107
|
+
<p class="text-sm text-red-500 col-span-2 hidden" data-field="error-container"><span class="font-medium">Error:</span> <span data-field="error"></span></p>
|
|
108
|
+
</div>
|
|
109
|
+
</template>
|
|
110
|
+
|
|
111
|
+
<template data-workflow-viz-target="signalDetailTemplate">
|
|
112
|
+
<h3 class="text-md font-semibold text-gray-900 mb-2"><span data-field="type"></span>: <span data-field="name"></span></h3>
|
|
113
|
+
<div class="grid grid-cols-2 gap-4">
|
|
114
|
+
<p class="text-sm"><span class="font-medium text-gray-500">Started:</span> <span data-field="started"></span></p>
|
|
115
|
+
<p class="text-sm"><span class="font-medium text-gray-500">Finished:</span> <span data-field="finished"></span></p>
|
|
116
|
+
<p class="text-sm text-red-500 col-span-2 hidden" data-field="error-container"><span class="font-medium">Error:</span> <span data-field="error"></span></p>
|
|
117
|
+
</div>
|
|
118
|
+
</template>
|
|
119
|
+
|
|
120
|
+
<template data-workflow-viz-target="listItemTemplate">
|
|
121
|
+
<div class="border-b border-gray-100 py-2 flex items-center justify-between">
|
|
122
|
+
<div>
|
|
123
|
+
<span class="text-sm font-medium text-gray-900" data-field="name"></span>
|
|
124
|
+
<span class="text-xs text-gray-500 ml-2" data-field="type"></span>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="text-sm text-gray-600" data-field="status"></div>
|
|
127
|
+
</div>
|
|
128
|
+
</template>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
|
|
2
|
+
<div class="p-6 space-y-6">
|
|
3
|
+
<div class="flex items-center justify-between">
|
|
4
|
+
<div>
|
|
5
|
+
<%= link_to overview_path, class: "text-sm text-gray-500 hover:text-gray-700 mb-2 block" do %>
|
|
6
|
+
Rigid Workflow »
|
|
7
|
+
<% end %>
|
|
8
|
+
<h1 class="text-3xl font-bold text-gray-900">Overview</h1>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden relative" data-controller="selection">
|
|
12
|
+
<!-- Floating Action Bar -->
|
|
13
|
+
<div data-selection-target="actionBar" class="hidden absolute mt-1 ml-10 flex items-center shadow-lg border border-gray-200 rounded-lg overflow-hidden bg-white z-10">
|
|
14
|
+
<%= form_with url: "#", method: :post, data: { selection_target: "actionForm" } do |f| %>
|
|
15
|
+
<div class="flex items-center divide-x divide-gray-200">
|
|
16
|
+
<button type="button" data-action="click->selection#deselectAll" class="px-2 py-2 text-gray-400 hover:text-gray-600">
|
|
17
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="overflow-x-auto">
|
|
24
|
+
<table class="w-full">
|
|
25
|
+
<thead class="bg-gray-50 border-b border-gray-200">
|
|
26
|
+
<tr>
|
|
27
|
+
<th class="px-4 py-3 text-left"><input type="checkbox" data-selection-target="selectAll" data-action="change->selection#toggleAll" class="cursor-default rounded border-gray-300 text-blue-600 focus:ring-blue-500"></th>
|
|
28
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Workflow</th>
|
|
29
|
+
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Completed</th>
|
|
30
|
+
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Active</th>
|
|
31
|
+
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Pending</th>
|
|
32
|
+
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Failed</th>
|
|
33
|
+
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Success %</th>
|
|
34
|
+
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Duration P50</th>
|
|
35
|
+
</tr>
|
|
36
|
+
</thead>
|
|
37
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
|
38
|
+
<% total_completed = 0; total_active = 0; total_pending = 0; total_failed = 0 %>
|
|
39
|
+
<% @stats.each do |stat| %>
|
|
40
|
+
<% stat = stat.with_indifferent_access %>
|
|
41
|
+
<% total_completed += stat[:completed_runs].to_i %>
|
|
42
|
+
<% total_active += stat[:active_runs].to_i %>
|
|
43
|
+
<% total_pending += stat[:pending_runs].to_i %>
|
|
44
|
+
<% total_failed += stat[:failed_runs].to_i %>
|
|
45
|
+
<tr>
|
|
46
|
+
<td class="px-4 py-4 whitespace-nowrap"><input type="checkbox" data-selection-target="row" data-action="change->selection#toggleRow" class="cursor-default rounded border-gray-300 text-gray-600 focus:ring-gray-500" value="<%= stat[:workflow_class] %>"></td>
|
|
47
|
+
<td class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-900"><%= stat[:workflow_class] %></td>
|
|
48
|
+
<td class="px-4 py-4 text-center whitespace-nowrap text-sm">
|
|
49
|
+
<%= link_to number_with_delimiter(stat[:completed_runs]), completed_runs_path(workflow: stat[:workflow_class]), class: "text-gray-600 hover:text-gray-800" %>
|
|
50
|
+
</td>
|
|
51
|
+
<td class="px-4 py-4 text-center whitespace-nowrap text-sm">
|
|
52
|
+
<%= link_to number_with_delimiter(stat[:active_runs]), active_runs_path(workflow: stat[:workflow_class]), class: "text-gray-600 hover:text-gray-800" %>
|
|
53
|
+
</td>
|
|
54
|
+
<td class="px-4 py-4 text-center whitespace-nowrap text-sm">
|
|
55
|
+
<%= link_to number_with_delimiter(stat[:pending_runs]), pending_runs_path(workflow: stat[:workflow_class]), class: "text-gray-600 hover:text-gray-800" %>
|
|
56
|
+
</td>
|
|
57
|
+
<td class="px-4 py-4 text-center whitespace-nowrap text-sm">
|
|
58
|
+
<%= link_to number_with_delimiter(stat[:failed_runs]), failed_runs_path(workflow: stat[:workflow_class]), class: "text-gray-600 hover:text-gray-800" %>
|
|
59
|
+
</td>
|
|
60
|
+
<td class="px-4 py-4 text-center whitespace-nowrap text-sm"><%= stat[:success_rate].to_f.round(1) %>%</td>
|
|
61
|
+
<td class="px-4 py-4 text-center whitespace-nowrap text-sm"><%= stat[:p50_duration] ? format_duration(ActiveSupport::Duration.parse(stat[:p50_duration])) : "-" %></td>
|
|
62
|
+
</tr>
|
|
63
|
+
<% end %>
|
|
64
|
+
</tbody>
|
|
65
|
+
<tfoot class="bg-gray-50 border-t border-gray-200">
|
|
66
|
+
<tr>
|
|
67
|
+
<td class="px-4 py-4"></td>
|
|
68
|
+
<td class="px-4 py-4"></td>
|
|
69
|
+
<td class="px-4 py-4 text-center text-sm font-bold text-gray-900"><%= number_with_delimiter(total_completed) %></td>
|
|
70
|
+
<td class="px-4 py-4 text-center text-sm font-bold text-gray-900"><%= number_with_delimiter(total_active) %></td>
|
|
71
|
+
<td class="px-4 py-4 text-center text-sm font-bold text-gray-900"><%= number_with_delimiter(total_pending) %></td>
|
|
72
|
+
<td class="px-4 py-4 text-center text-sm font-bold text-gray-900"><%= number_with_delimiter(total_failed) %></td>
|
|
73
|
+
<td class="px-4 py-4 text-center text-sm font-bold text-gray-900">
|
|
74
|
+
<% total_runs = total_completed + total_failed %>
|
|
75
|
+
<%= total_runs > 0 ? number_to_percentage(100.0 * total_completed / total_runs, precision: 1) : 0 %>
|
|
76
|
+
</td>
|
|
77
|
+
<td class="px-4 py-4"></td>
|
|
78
|
+
</tr>
|
|
79
|
+
</tfoot>
|
|
80
|
+
</table>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
pin "@hotwired/turbo-rails", to: "turbo.min.js"
|
|
3
|
+
pin "@hotwired/stimulus", to: "stimulus.min.js"
|
|
4
|
+
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
|
5
|
+
pin "local-time", to: "local-time.es2017-esm.js"
|
|
6
|
+
|
|
7
|
+
pin "rigid_workflow/application", to: "rigid_workflow/application.js"
|
|
8
|
+
pin_all_from RigidWorkflow::Engine.root.join(
|
|
9
|
+
"app/javascript/rigid_workflow/controllers"
|
|
10
|
+
),
|
|
11
|
+
under: "rigid_workflow/controllers"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
return if RigidWorkflow::Engine.routes.named_routes.names.include?(:overview)
|
|
4
|
+
|
|
5
|
+
RigidWorkflow::Engine.routes.draw do
|
|
6
|
+
get "/" => "workflows#index", :as => :overview
|
|
7
|
+
|
|
8
|
+
scope :runs do
|
|
9
|
+
get "/" => "runs#index", :as => :runs
|
|
10
|
+
get "/pending" => "runs#pending_runs", :as => :pending_runs
|
|
11
|
+
get "/active" => "runs#active_runs", :as => :active_runs
|
|
12
|
+
get "/completed" => "runs#completed_runs", :as => :completed_runs
|
|
13
|
+
get "/failed" => "runs#failed_runs", :as => :failed_runs
|
|
14
|
+
get "/:id" => "runs#show", :as => :runs_show
|
|
15
|
+
post "/bulk_action" => "runs#bulk_action", :as => :runs_bulk_action
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RigidWorkflow
|
|
4
|
+
module Generators
|
|
5
|
+
# Generates a new activity class file.
|
|
6
|
+
class ActivityGenerator < ::Rails::Generators::NamedBase
|
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
|
8
|
+
|
|
9
|
+
def create_activity_file
|
|
10
|
+
template "activity.rb.erb",
|
|
11
|
+
File.join("app/workflows/activities", "#{file_name}.rb")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/migration"
|
|
5
|
+
|
|
6
|
+
module RigidWorkflow
|
|
7
|
+
module Generators
|
|
8
|
+
# Installs RigidWorkflow into a Rails application.
|
|
9
|
+
# Generates initializer, routes, and optional migrations.
|
|
10
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
11
|
+
include Rails::Generators::Migration
|
|
12
|
+
|
|
13
|
+
source_root File.expand_path("templates", __dir__)
|
|
14
|
+
|
|
15
|
+
class_option :with_migrations,
|
|
16
|
+
type: :boolean,
|
|
17
|
+
default: true,
|
|
18
|
+
desc: "Generate migrations"
|
|
19
|
+
|
|
20
|
+
# Standard Rails migration timestamp format
|
|
21
|
+
def self.next_migration_number(dirname)
|
|
22
|
+
next_num = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
|
|
23
|
+
current_num = current_migration_number(dirname)
|
|
24
|
+
next_num <= current_num ? (current_num + 1).to_s : next_num.to_s
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def install
|
|
28
|
+
template "initializer.rb.erb", "config/initializers/rigid_workflow.rb"
|
|
29
|
+
route "mount RigidWorkflow::Engine => '/admin/rigid_workflow'"
|
|
30
|
+
|
|
31
|
+
if options[:with_migrations]
|
|
32
|
+
migration_template(
|
|
33
|
+
"migration.rb.erb",
|
|
34
|
+
"db/migrate/create_rigid_workflow_tables.rb"
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
display_success_message
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# Accessible inside your migration.rb.erb as <%= migration_version %>
|
|
44
|
+
def migration_version
|
|
45
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def display_success_message
|
|
49
|
+
say "\nRigidWorkflow installed successfully!", :green
|
|
50
|
+
say "\nNext steps:"
|
|
51
|
+
say " 1. Run: rails db:migrate"
|
|
52
|
+
say " 2. Define your workflow in app/workflows/"
|
|
53
|
+
say " 3. Start workflow: MyWorkflow(param: 1)"
|
|
54
|
+
say ""
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# RigidWorkflow Configuration
|
|
4
|
+
#
|
|
5
|
+
# This initializer configures RigidWorkflow's behavior. All options have
|
|
6
|
+
# sensible defaults - only configure what you need to override.
|
|
7
|
+
#
|
|
8
|
+
# SECURITY: The admin_controller is REQUIRED for authentication.
|
|
9
|
+
# The engine's admin UI has NO built-in authentication.
|
|
10
|
+
# Set this to a controller that already enforces authentication
|
|
11
|
+
# (e.g., Admin::BaseController with Devise, or your custom admin controller).
|
|
12
|
+
# The engine's ApplicationController will inherit from this class.
|
|
13
|
+
# Without this, all workflow data is publicly accessible when mounted.
|
|
14
|
+
RigidWorkflow.configure do |config|
|
|
15
|
+
# The controller to use for the admin UI. Inherit from your admin base controller
|
|
16
|
+
# to automatically apply authentication/authorization.
|
|
17
|
+
config.admin_controller = "MyAdminController"
|
|
18
|
+
|
|
19
|
+
# Maximum retry attempts for failed activities.
|
|
20
|
+
config.max_attempts = 3
|
|
21
|
+
|
|
22
|
+
# Base delay for exponential backoff retries (in seconds).
|
|
23
|
+
config.retry_delay = 15.seconds
|
|
24
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateRigidWorkflowTables < ActiveRecord::Migration[<%= migration_version.delete("[]") %>]
|
|
4
|
+
def change
|
|
5
|
+
create_table :rigid_workflow_runs, id: :string, if_not_exists: true do |t|
|
|
6
|
+
t.string :workflow_class, null: false
|
|
7
|
+
t.integer :version, default: 1, null: false
|
|
8
|
+
t.json :params, default: {}
|
|
9
|
+
t.json :memory, default: {}
|
|
10
|
+
t.string :status, default: "pending", null: false
|
|
11
|
+
t.integer :iterations, default: 0
|
|
12
|
+
t.datetime :started_at
|
|
13
|
+
t.datetime :finished_at
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :rigid_workflow_runs, :workflow_class, if_not_exists: true
|
|
18
|
+
add_index :rigid_workflow_runs, :status, if_not_exists: true
|
|
19
|
+
|
|
20
|
+
create_table :rigid_workflow_steps, id: :string, if_not_exists: true do |t|
|
|
21
|
+
t.string :rigid_workflow_run_id, null: false
|
|
22
|
+
t.string :step_name, null: false
|
|
23
|
+
t.string :activity_class, null: false
|
|
24
|
+
t.json :input, default: {}
|
|
25
|
+
t.json :output, default: {}
|
|
26
|
+
t.json :error_details
|
|
27
|
+
t.string :status, default: "pending", null: false
|
|
28
|
+
t.integer :attempt, default: 1
|
|
29
|
+
t.integer :max_attempts, default: 3
|
|
30
|
+
t.datetime :started_at
|
|
31
|
+
t.datetime :finished_at
|
|
32
|
+
t.datetime :failed_at
|
|
33
|
+
t.datetime :scheduled_at
|
|
34
|
+
t.timestamps
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
add_index :rigid_workflow_steps, :rigid_workflow_run_id, if_not_exists: true
|
|
38
|
+
add_index :rigid_workflow_steps, :step_name, if_not_exists: true
|
|
39
|
+
add_index :rigid_workflow_steps, [:rigid_workflow_run_id, :step_name], unique: true, if_not_exists: true
|
|
40
|
+
|
|
41
|
+
create_table :rigid_workflow_signals, id: :string, if_not_exists: true do |t|
|
|
42
|
+
t.string :rigid_workflow_run_id, null: false
|
|
43
|
+
t.string :name, null: false
|
|
44
|
+
t.json :payload, default: {}
|
|
45
|
+
t.datetime :received_at
|
|
46
|
+
t.datetime :canceled_at
|
|
47
|
+
t.datetime :expires_at
|
|
48
|
+
t.timestamps
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
add_index :rigid_workflow_signals, :rigid_workflow_run_id, if_not_exists: true
|
|
52
|
+
add_index :rigid_workflow_signals, [:rigid_workflow_run_id, :name], unique: true, if_not_exists: true
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RigidWorkflow
|
|
4
|
+
module Generators
|
|
5
|
+
# Generates a new workflow class file.
|
|
6
|
+
class WorkflowGenerator < ::Rails::Generators::NamedBase
|
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
|
8
|
+
|
|
9
|
+
def create_workflow_file
|
|
10
|
+
template "workflow.rb.erb",
|
|
11
|
+
File.join("app/workflows", "#{file_name}.rb")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RigidWorkflow
|
|
4
|
+
# Base class for all activities.
|
|
5
|
+
# Activities represent a single, idempotent unit of work within a workflow.
|
|
6
|
+
class Activity
|
|
7
|
+
class_attribute :configs, default: { force_async: false }
|
|
8
|
+
|
|
9
|
+
# Forces all instances of this activity to run asynchronously.
|
|
10
|
+
# @param value [Boolean]
|
|
11
|
+
def self.force_async(value)
|
|
12
|
+
self.configs = self.configs.merge(force_async: value)
|
|
13
|
+
self
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Boolean] Whether this activity is forced to run asynchronously
|
|
17
|
+
def self.force_async?
|
|
18
|
+
self.configs[:force_async] == true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @api private
|
|
22
|
+
def initialize(step)
|
|
23
|
+
@workflow_run_id = step.rigid_workflow_run_id
|
|
24
|
+
@step_name = step.step_name
|
|
25
|
+
@step = step
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The main activity logic. Must be implemented by subclasses.
|
|
29
|
+
# @param args [Hash] Keyword arguments passed from the workflow step input
|
|
30
|
+
# @return [Object] The result of the activity, which will be persisted
|
|
31
|
+
def perform(**args)
|
|
32
|
+
return if Rails.env.test? && !RigidWorkflow.config.logging?
|
|
33
|
+
raise NotImplementedError, "Subclass must implement #perform"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Logs a message associated with this activity execution.
|
|
37
|
+
# @param message [String]
|
|
38
|
+
def log(message, severity: Logger::INFO)
|
|
39
|
+
return if !RigidWorkflow.config.logging? && Rails.env.test?
|
|
40
|
+
|
|
41
|
+
puts message unless Rails.env.test?
|
|
42
|
+
Rails.logger.log(
|
|
43
|
+
severity,
|
|
44
|
+
"[#{@workflow_run_id.to_s[0..12]}][#{@step_name}] #{message}"
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Called during compensation to undo this activity's side effects.
|
|
49
|
+
# @step is available with full access to output/input via current_attempt.
|
|
50
|
+
# Override in subclasses. Default does nothing.
|
|
51
|
+
def compensate
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RigidWorkflow
|
|
4
|
+
# Rails engine for RigidWorkflow.
|
|
5
|
+
# Provides the plugin infrastructure for integrating with Rails applications.
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
isolate_namespace RigidWorkflow
|
|
8
|
+
|
|
9
|
+
# Zeitwerk handles autoloading and reloading of the app/ but not the lib/.
|
|
10
|
+
initializer "rigid_workflow.zeitwerk" do
|
|
11
|
+
Rails.autoloaders.main.push_dir(Engine.root.join("lib/rigid_workflow"), namespace: RigidWorkflow)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
initializer "rigid_workflow.assets" do |app|
|
|
15
|
+
app.config.assets.paths << root.join("app/assets/stylesheets")
|
|
16
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
initializer "rigid_workflow.importmap", before: "importmap" do |app|
|
|
20
|
+
if app.config.respond_to?(:importmap)
|
|
21
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
22
|
+
|
|
23
|
+
if Rails.env.development?
|
|
24
|
+
app.config.importmap.cache_sweepers << Engine.root.join(
|
|
25
|
+
"app/javascript"
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
initializer "rigid_workflow.assets.precompile" do |app|
|
|
32
|
+
app.config.assets.precompile += %w[rigid_workflow/application.css]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|