job-workflow 0.4.0 → 0.6.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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -2
  3. data/CHANGELOG.md +30 -0
  4. data/README.md +1 -1
  5. data/app/controllers/job_workflow/monitoring/application_controller.rb +11 -0
  6. data/app/controllers/job_workflow/monitoring/executions_controller.rb +28 -0
  7. data/app/controllers/job_workflow/monitoring/workflows_controller.rb +11 -0
  8. data/app/views/job_workflow/monitoring/executions/index.html.erb +57 -0
  9. data/app/views/job_workflow/monitoring/executions/show.html.erb +200 -0
  10. data/app/views/job_workflow/monitoring/workflows/index.html.erb +39 -0
  11. data/app/views/layouts/job_workflow/monitoring/application.html.erb +117 -0
  12. data/config/routes.rb +8 -0
  13. data/guides/API_REFERENCE.md +79 -6
  14. data/guides/DEPENDENCY_WAIT.md +9 -5
  15. data/guides/MONITORING_UI.md +74 -0
  16. data/guides/PARALLEL_PROCESSING.md +33 -21
  17. data/guides/PRODUCTION_DEPLOYMENT.md +1 -1
  18. data/guides/README.md +6 -1
  19. data/guides/THROTTLING.md +24 -0
  20. data/guides/WORKFLOW_STATUS_QUERY.md +7 -1
  21. data/lib/job_workflow/context.rb +68 -6
  22. data/lib/job_workflow/dsl.rb +1 -5
  23. data/lib/job_workflow/instrumentation/opentelemetry_subscriber.rb +1 -1
  24. data/lib/job_workflow/instrumentation.rb +14 -14
  25. data/lib/job_workflow/job_status.rb +16 -1
  26. data/lib/job_workflow/monitoring/dag_layout.rb +186 -0
  27. data/lib/job_workflow/monitoring/engine.rb +15 -0
  28. data/lib/job_workflow/monitoring/execution_page.rb +16 -0
  29. data/lib/job_workflow/monitoring/execution_registry.rb +50 -0
  30. data/lib/job_workflow/monitoring/execution_view_model.rb +262 -0
  31. data/lib/job_workflow/monitoring/parameter_filter.rb +37 -0
  32. data/lib/job_workflow/monitoring/workflow_definition.rb +24 -0
  33. data/lib/job_workflow/monitoring/workflow_registry.rb +24 -0
  34. data/lib/job_workflow/monitoring.rb +120 -0
  35. data/lib/job_workflow/queue_adapters/abstract.rb +7 -2
  36. data/lib/job_workflow/queue_adapters/null_adapter.rb +12 -1
  37. data/lib/job_workflow/queue_adapters/solid_queue_adapter.rb +42 -12
  38. data/lib/job_workflow/railtie.rb +12 -0
  39. data/lib/job_workflow/runner.rb +38 -15
  40. data/lib/job_workflow/sub_task_job.rb +93 -0
  41. data/lib/job_workflow/task.rb +7 -0
  42. data/lib/job_workflow/task_enqueue.rb +19 -12
  43. data/lib/job_workflow/version.rb +1 -1
  44. data/lib/job_workflow/workflow_status.rb +20 -1
  45. data/lib/job_workflow.rb +5 -1
  46. data/sig/generated/job_workflow/context.rbs +31 -7
  47. data/sig/generated/job_workflow/instrumentation/opentelemetry_subscriber.rbs +0 -1
  48. data/sig/generated/job_workflow/instrumentation.rbs +28 -28
  49. data/sig/generated/job_workflow/job_status.rbs +5 -2
  50. data/sig/generated/job_workflow/monitoring/dag_layout.rbs +80 -0
  51. data/sig/generated/job_workflow/monitoring/engine.rbs +8 -0
  52. data/sig/generated/job_workflow/monitoring/execution_page.rbs +14 -0
  53. data/sig/generated/job_workflow/monitoring/execution_registry.rbs +21 -0
  54. data/sig/generated/job_workflow/monitoring/execution_view_model.rbs +111 -0
  55. data/sig/generated/job_workflow/monitoring/parameter_filter.rbs +16 -0
  56. data/sig/generated/job_workflow/monitoring/workflow_definition.rbs +18 -0
  57. data/sig/generated/job_workflow/monitoring/workflow_registry.rbs +13 -0
  58. data/sig/generated/job_workflow/monitoring.rbs +38 -0
  59. data/sig/generated/job_workflow/queue_adapters/abstract.rbs +7 -4
  60. data/sig/generated/job_workflow/queue_adapters/null_adapter.rbs +5 -2
  61. data/sig/generated/job_workflow/queue_adapters/solid_queue_adapter.rbs +18 -6
  62. data/sig/generated/job_workflow/railtie.rbs +6 -0
  63. data/sig/generated/job_workflow/runner.rbs +8 -5
  64. data/sig/generated/job_workflow/sub_task_job.rbs +40 -0
  65. data/sig/generated/job_workflow/task.rbs +5 -0
  66. data/sig/generated/job_workflow/task_enqueue.rbs +5 -8
  67. data/sig/generated/job_workflow/workflow_status.rbs +6 -0
  68. data/sig-private/job-workflow.rbs +11 -0
  69. data/sig-private/rails.rbs +5 -0
  70. metadata +34 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 213a0654cb0482f1a2574f4e55d26c72f5274d24b61b933abd04e9e21f4c19d1
4
- data.tar.gz: 0f19fce390ff18dcb13d80c8a4c6841dc2e096d4ea7586f0a2d6e2c6a467a45b
3
+ metadata.gz: fbf6231d334745f4814282d93fa8c7b54461680e6bb57edc6632cd39b86cb71b
4
+ data.tar.gz: c627082defb65e2a4cd79cb2b88d3f54ce8b082a88d9ccaff3d96029ee49ea65
5
5
  SHA512:
6
- metadata.gz: 89e0d194c4f14817bd2866b4b07f2efe3eea0ffdbc74ff4b7c1f5474d59fb0a18749d68fc2c724ea690f370d1b335d1e69bf0f006ab9bb1964f74fcfd689232b
7
- data.tar.gz: 49150a706fb10a4ba79a87397b4cbbfcb4554eba5e974d22d6176799d377f5d9814f20862be996bca253328fc1efa6a10429947cc308f4aa3dd884eaadd378d0
6
+ metadata.gz: f8caf717541c7f549d3957a5939d636881f8ad98389e5e53759dde43e561c8bbdeda0211feab0843f2625fbc974fb217b479bc2421e0bc828b9f6d910b749cd5
7
+ data.tar.gz: 466d87d40597d0d3c68de4cec3f2f6d158faab74100d098420cd376108fb88667f15cf1ecccdf2b810e41df402a9db5f7d84945a71de2dd756e71ae99588fe58
data/.rubocop.yml CHANGED
@@ -41,7 +41,7 @@ Metrics/BlockLength:
41
41
  Metrics/ClassLength:
42
42
  Enabled: true
43
43
  CountComments: false
44
- Max: 100
44
+ Max: 124
45
45
  CountAsOne:
46
46
  - array
47
47
  - hash
@@ -88,4 +88,3 @@ Style/StringLiteralsInInterpolation:
88
88
 
89
89
  ThreadSafety/ClassAndModuleAttributes:
90
90
  Enabled: false
91
-
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2026-05-24
4
+
5
+ ### Added
6
+
7
+ - Add `JobWorkflow::SubTaskJob` for `enqueue: true` task fan-out so each-task sub-jobs run on a dedicated job class instead of reusing the workflow DSL job class directly
8
+ - Add a JobWorkflow monitoring UI engine with an execution DAG overview for browsing workflow definitions, root executions, DAG state, arguments, outputs, fan-out progress, and linked Mission Control Jobs rows
9
+
10
+ ### Changed
11
+
12
+ - **Breaking:** `WorkflowStatus.find` / `find_by` are now root-workflow-only APIs. Sub-task job IDs are excluded and should be tracked via `JobStatus`.
13
+
14
+ ### Fixed
15
+
16
+ - Route enqueued each-task execution through `SubTaskJob`, restoring workflow/task context from the parent workflow job and keeping parent/sub-job identity separate during async execution
17
+ - Extend queue-adapter and workflow-status hydration so monitoring can paginate root workflow executions, hydrate sub-task detail on demand, and keep partially completed fan-out tasks visible as `running` until the workflow actually settles
18
+
19
+ ### Removed
20
+
21
+ - Remove `enqueue: { concurrency: N }`. Async map task fan-out is still controlled by `enqueue`, but execution caps are now configured exclusively with `throttle`.
22
+
23
+ ## [0.5.0] - 2026-05-13
24
+
25
+ ### Added
26
+
27
+ - Add task-scoped cursor helpers on `Context` so tasks can persist progress, restore task-local continuation state, and delegate explicit checkpoints without exposing ActiveJob continuation internals
28
+
29
+ ### Fixed
30
+
31
+ - Initialize the SolidQueue adapter from a Railtie after Rails boot so the `ClaimedExecution` patch is applied reliably, rescheduled `dependency_wait` jobs are not marked finished early, and Rails apps no longer need a manual initializer
32
+
3
33
  ## [0.4.0] - 2026-05-12
4
34
 
5
35
  ### Fixed
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # JobWorkflow
2
2
 
3
- > ⚠️ **Early Stage (v0.4.0):** This library is in active development. APIs and features may change in breaking ways without notice. Use in production at your own risk and expect potential breaking changes in future releases.
3
+ > ⚠️ **Early Stage (v0.6.0):** This library is in active development. APIs and features may change in breaking ways without notice. Use in production at your own risk and expect potential breaking changes in future releases.
4
4
 
5
5
  ## Overview
6
6
 
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobWorkflow
4
+ module Monitoring
5
+ # Resolve the host app controller at load time so engine controllers inherit
6
+ # the app's existing authentication and access control hooks.
7
+ class ApplicationController < JobWorkflow::Monitoring.resolved_base_controller_class.constantize
8
+ layout "job_workflow/monitoring/application"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobWorkflow
4
+ module Monitoring
5
+ class ExecutionsController < ApplicationController
6
+ def index
7
+ @workflow = WorkflowRegistry.find(params[:workflow_job_class_name])
8
+ return render plain: "Workflow definition not found.", status: :not_found if @workflow.nil?
9
+
10
+ @page = ExecutionRegistry.page_for(
11
+ job_class_name: @workflow.name,
12
+ cursor: params[:cursor]
13
+ )
14
+ @executions = @page.executions
15
+ end
16
+
17
+ def show
18
+ @workflow = WorkflowRegistry.find(params[:workflow_job_class_name])
19
+ return render plain: "Workflow definition not found.", status: :not_found if @workflow.nil?
20
+
21
+ @execution = ExecutionRegistry.find(params[:id])
22
+ return if @execution && @execution.job_class_name == @workflow.name
23
+
24
+ render plain: "Workflow execution is no longer available.", status: :not_found
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobWorkflow
4
+ module Monitoring
5
+ class WorkflowsController < ApplicationController
6
+ def index
7
+ @workflows = Monitoring.workflows
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,57 @@
1
+ <section class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-end gap-3 mb-4">
2
+ <div>
3
+ <div class="text-body-secondary small mb-1">Workflow</div>
4
+ <h1 class="display-6 fw-semibold mb-2"><%= @workflow.name %></h1>
5
+ <p class="text-body-secondary mb-0">Paginated root executions. sub task jobs appear only inside execution detail.</p>
6
+ </div>
7
+ <div>
8
+ <%= link_to "Back to workflows", root_path, class: "btn btn-outline-secondary" %>
9
+ </div>
10
+ </section>
11
+
12
+ <div class="card jw-card">
13
+ <div class="card-body p-0">
14
+ <% if @executions.empty? %>
15
+ <div class="p-4 text-body-secondary">No currently available workflow executions.</div>
16
+ <% else %>
17
+ <div class="table-responsive">
18
+ <table class="table table-hover align-middle mb-0">
19
+ <thead class="table-light">
20
+ <tr>
21
+ <th class="ps-4">Status</th>
22
+ <th>Current task</th>
23
+ <th>Queue</th>
24
+ <th>Job</th>
25
+ <th class="text-end pe-4">Detail</th>
26
+ </tr>
27
+ </thead>
28
+ <tbody>
29
+ <% @executions.each do |execution| %>
30
+ <% status_class = case execution.workflow_status
31
+ when :succeeded then "text-bg-success"
32
+ when :failed then "text-bg-danger"
33
+ when :running then "text-bg-primary"
34
+ else "text-bg-secondary"
35
+ end %>
36
+ <tr>
37
+ <td class="ps-4"><span class="badge <%= status_class %>"><%= execution.workflow_status %></span></td>
38
+ <td><%= execution.current_task_name || "-" %></td>
39
+ <td><span class="jw-mono"><%= execution.queue_name || "-" %></span></td>
40
+ <td class="jw-mono"><%= execution.job_id %></td>
41
+ <td class="text-end pe-4">
42
+ <%= link_to "Open", workflow_execution_path(@workflow.name, execution.job_id), class: "btn btn-sm btn-primary" %>
43
+ </td>
44
+ </tr>
45
+ <% end %>
46
+ </tbody>
47
+ </table>
48
+ </div>
49
+ <% end %>
50
+ </div>
51
+ </div>
52
+
53
+ <% if @page.next_cursor %>
54
+ <div class="d-flex justify-content-end mt-3">
55
+ <%= link_to "Next page", workflow_executions_path(@workflow.name, cursor: @page.next_cursor), class: "btn btn-outline-primary" %>
56
+ </div>
57
+ <% end %>
@@ -0,0 +1,200 @@
1
+ <% status_class = case @execution.workflow_status
2
+ when :succeeded then "text-bg-success"
3
+ when :failed then "text-bg-danger"
4
+ when :running then "text-bg-primary"
5
+ else "text-bg-secondary"
6
+ end %>
7
+ <% dag_layout = @execution.dag_layout %>
8
+
9
+ <section class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-start gap-3 mb-4">
10
+ <div>
11
+ <div class="text-body-secondary small mb-1">Execution</div>
12
+ <h1 class="display-6 fw-semibold mb-3"><%= @execution.job_class_name %></h1>
13
+ <div class="d-flex flex-wrap gap-2">
14
+ <span class="badge <%= status_class %>"><%= @execution.workflow_status %></span>
15
+ <span class="badge text-bg-light border text-dark">task <%= @execution.current_task_name || "-" %></span>
16
+ <span class="badge text-bg-light border text-dark">failed <%= @execution.failed_task_name || "-" %></span>
17
+ </div>
18
+ </div>
19
+ <div class="d-flex flex-wrap gap-2">
20
+ <%= link_to "Back to executions", workflow_executions_path(@workflow.name), class: "btn btn-outline-secondary" %>
21
+ <% if @execution.mission_control_job_path %>
22
+ <%= link_to "Mission Control Jobs", @execution.mission_control_job_path, class: "btn btn-primary" %>
23
+ <% end %>
24
+ </div>
25
+ </section>
26
+
27
+ <div class="row g-4 mb-4">
28
+ <div class="col-12 col-xl-4">
29
+ <div class="card jw-card h-100">
30
+ <div class="card-header bg-white fw-semibold">Execution Summary</div>
31
+ <div class="card-body">
32
+ <dl class="row mb-0">
33
+ <dt class="col-sm-4">Job ID</dt>
34
+ <dd class="col-sm-8 jw-mono"><%= @execution.job_id %></dd>
35
+ <dt class="col-sm-4">Queue</dt>
36
+ <dd class="col-sm-8"><%= @execution.queue_name || "-" %></dd>
37
+ <dt class="col-sm-4">Current</dt>
38
+ <dd class="col-sm-8"><%= @execution.current_task_name || "-" %></dd>
39
+ <dt class="col-sm-4">Failed</dt>
40
+ <dd class="col-sm-8"><%= @execution.failed_task_name || "-" %></dd>
41
+ </dl>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <div class="col-12 col-xl-8">
47
+ <div class="card jw-card h-100">
48
+ <div class="card-header bg-white fw-semibold">Arguments</div>
49
+ <div class="card-body">
50
+ <pre class="jw-pre"><%= JSON.pretty_generate(@execution.filtered_arguments.as_json) %></pre>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+
56
+ <div class="card jw-card">
57
+ <div class="card-header bg-white fw-semibold">DAG</div>
58
+ <% if dag_layout[:nodes].any? %>
59
+ <div class="card-body border-bottom" aria-hidden="true">
60
+ <div class="small text-body-secondary mb-3">graph overview</div>
61
+ <div class="jw-dag-viewport">
62
+ <div
63
+ class="jw-dag-canvas"
64
+ style="width: <%= dag_layout[:width] %>px; height: <%= dag_layout[:height] %>px;"
65
+ >
66
+ <svg
67
+ class="jw-dag-svg"
68
+ viewBox="0 0 <%= dag_layout[:width] %> <%= dag_layout[:height] %>"
69
+ preserveAspectRatio="xMinYMin meet"
70
+ >
71
+ <% dag_layout[:edges].each do |edge| %>
72
+ <path class="jw-dag-edge" d="<%= edge[:path] %>"></path>
73
+ <% end %>
74
+ </svg>
75
+
76
+ <% dag_layout[:nodes].each do |task| %>
77
+ <% graph_status_class = case task[:status]
78
+ when :succeeded then "text-bg-success"
79
+ when :failed then "text-bg-danger"
80
+ when :running then "text-bg-primary"
81
+ else "text-bg-secondary"
82
+ end %>
83
+ <div
84
+ class="jw-dag-node"
85
+ style="left: <%= task[:x] %>px; top: <%= task[:y] %>px; width: <%= task[:width] %>px; height: <%= task[:height] %>px;"
86
+ >
87
+ <div class="d-flex flex-column align-items-start gap-1">
88
+ <div class="fw-semibold jw-dag-node-title" title="<%= task[:label] %>"><%= task[:truncated_label] %></div>
89
+ <span class="badge <%= graph_status_class %>"><%= task[:status] %></span>
90
+ </div>
91
+
92
+ <% if task[:meta_label] %>
93
+ <div class="jw-dag-node-meta"><%= task[:meta_label] %></div>
94
+ <% end %>
95
+ </div>
96
+ <% end %>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ <% end %>
101
+ <div class="list-group list-group-flush">
102
+ <% @execution.tasks.each do |task| %>
103
+ <% task_status_class = case task[:status]
104
+ when :succeeded then "text-bg-success"
105
+ when :failed then "text-bg-danger"
106
+ when :running then "text-bg-primary"
107
+ else "text-bg-secondary"
108
+ end %>
109
+ <div class="list-group-item p-4">
110
+ <div class="d-flex flex-column flex-lg-row justify-content-between gap-3">
111
+ <div>
112
+ <div class="d-flex flex-wrap align-items-center gap-2 mb-2">
113
+ <span class="fw-semibold"><%= task[:name] %></span>
114
+ <span class="badge <%= task_status_class %>"><%= task[:status] %></span>
115
+ <% if task[:each] %>
116
+ <span class="badge text-bg-light border text-dark">
117
+ each <%= task[:each_progress][:succeeded] %>/<%= task[:each_progress][:total] %>
118
+ </span>
119
+ <% end %>
120
+ </div>
121
+ <% if task[:depends_on].any? %>
122
+ <div class="text-body-secondary small">depends on <%= task[:depends_on].join(", ") %></div>
123
+ <% end %>
124
+ </div>
125
+
126
+ <% if task[:each] %>
127
+ <div class="text-lg-end">
128
+ <div class="small text-body-secondary mb-1">fan-out progress</div>
129
+ <div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="<%= task[:each_progress][:total] %>" aria-valuenow="<%= task[:each_progress][:succeeded] %>">
130
+ <div
131
+ class="progress-bar"
132
+ style="width: <%= task[:each_progress][:total].positive? ? (task[:each_progress][:succeeded] * 100 / task[:each_progress][:total]) : 0 %>%"
133
+ ></div>
134
+ </div>
135
+ </div>
136
+ <% end %>
137
+ </div>
138
+
139
+ <div class="row g-3 mt-1">
140
+ <div class="col-12 col-xl-6">
141
+ <div class="small text-body-secondary mb-2">configuration</div>
142
+ <pre class="jw-pre"><%= JSON.pretty_generate(task[:configuration].as_json) %></pre>
143
+ </div>
144
+
145
+ <div class="col-12 col-xl-6">
146
+ <div class="small text-body-secondary mb-2">progress</div>
147
+ <pre class="jw-pre"><%= JSON.pretty_generate(task[:each_progress].as_json) %></pre>
148
+ </div>
149
+ </div>
150
+
151
+ <% if task[:outputs].any? %>
152
+ <div class="mt-3">
153
+ <div class="small text-body-secondary mb-2">outputs</div>
154
+ <pre class="jw-pre"><%= JSON.pretty_generate(task[:outputs].as_json) %></pre>
155
+ </div>
156
+ <% end %>
157
+
158
+ <% if Array(task[:sub_task_jobs]).any? %>
159
+ <div class="mt-3">
160
+ <div class="small text-body-secondary mb-2">sub task jobs</div>
161
+ <div class="table-responsive">
162
+ <table class="table table-sm align-middle mb-0">
163
+ <thead>
164
+ <tr>
165
+ <th>Each</th>
166
+ <th>Status</th>
167
+ <th>Job ID</th>
168
+ <th class="text-end">Mission Control</th>
169
+ </tr>
170
+ </thead>
171
+ <tbody>
172
+ <% Array(task[:sub_task_jobs]).each do |sub_task_job| %>
173
+ <% sub_task_status_class = case sub_task_job[:status]
174
+ when :succeeded then "text-bg-success"
175
+ when :failed then "text-bg-danger"
176
+ when :running then "text-bg-primary"
177
+ else "text-bg-secondary"
178
+ end %>
179
+ <tr>
180
+ <td><%= sub_task_job[:each_index] %></td>
181
+ <td><span class="badge <%= sub_task_status_class %>"><%= sub_task_job[:status] %></span></td>
182
+ <td class="jw-mono"><%= sub_task_job[:job_id] %></td>
183
+ <td class="text-end">
184
+ <% if sub_task_job[:mission_control_job_path] %>
185
+ <%= link_to "Open", sub_task_job[:mission_control_job_path], class: "btn btn-sm btn-outline-primary" %>
186
+ <% else %>
187
+ <span class="text-body-secondary">-</span>
188
+ <% end %>
189
+ </td>
190
+ </tr>
191
+ <% end %>
192
+ </tbody>
193
+ </table>
194
+ </div>
195
+ </div>
196
+ <% end %>
197
+ </div>
198
+ <% end %>
199
+ </div>
200
+ </div>
@@ -0,0 +1,39 @@
1
+ <section class="mb-4">
2
+ <h1 class="display-6 fw-semibold mb-2">Workflow Definitions</h1>
3
+ <p class="text-body-secondary mb-0">Root execution only. Select one workflow, then drill into its executions.</p>
4
+ </section>
5
+
6
+ <div class="card jw-card">
7
+ <div class="card-body p-0">
8
+ <% if @workflows.empty? %>
9
+ <div class="p-4 text-body-secondary">No workflows are registered.</div>
10
+ <% else %>
11
+ <div class="table-responsive">
12
+ <table class="table table-hover align-middle mb-0">
13
+ <thead class="table-light">
14
+ <tr>
15
+ <th class="ps-4">Workflow</th>
16
+ <th>Tasks</th>
17
+ <th class="text-end pe-4">Executions</th>
18
+ </tr>
19
+ </thead>
20
+ <tbody>
21
+ <% @workflows.each do |workflow| %>
22
+ <tr>
23
+ <td class="ps-4">
24
+ <div class="fw-semibold"><%= workflow.job_class_name %></div>
25
+ </td>
26
+ <td>
27
+ <span class="badge text-bg-secondary"><%= workflow.task_count %> tasks</span>
28
+ </td>
29
+ <td class="text-end pe-4">
30
+ <%= link_to "View executions", workflow_executions_path(workflow.job_class_name), class: "btn btn-sm btn-primary" %>
31
+ </td>
32
+ </tr>
33
+ <% end %>
34
+ </tbody>
35
+ </table>
36
+ </div>
37
+ <% end %>
38
+ </div>
39
+ </div>
@@ -0,0 +1,117 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>JobWorkflow Monitoring</title>
7
+ <link
8
+ href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
9
+ rel="stylesheet"
10
+ integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB"
11
+ crossorigin="anonymous"
12
+ >
13
+ <style>
14
+ :root {
15
+ --jw-surface: #f8f9fa;
16
+ --jw-border: #dee2e6;
17
+ }
18
+
19
+ body {
20
+ background:
21
+ radial-gradient(circle at top right, rgba(13, 110, 253, 0.08), transparent 24rem),
22
+ linear-gradient(180deg, #f8fbff 0%, #f8f9fa 100%);
23
+ }
24
+
25
+ .jw-shell {
26
+ max-width: 1200px;
27
+ }
28
+
29
+ .jw-mono {
30
+ font-family: ui-monospace, SFMono-Regular, SFMono-Regular, Menlo, Consolas, monospace;
31
+ word-break: break-all;
32
+ }
33
+
34
+ .jw-pre {
35
+ background: #212529;
36
+ color: #f8f9fa;
37
+ border-radius: 0.75rem;
38
+ padding: 1rem;
39
+ margin: 0;
40
+ white-space: pre-wrap;
41
+ }
42
+
43
+ .jw-card {
44
+ border-color: var(--jw-border);
45
+ box-shadow: 0 0.5rem 1.5rem rgba(33, 37, 41, 0.06);
46
+ }
47
+
48
+ .jw-dag-viewport {
49
+ overflow-x: auto;
50
+ padding-bottom: 0.25rem;
51
+ }
52
+
53
+ .jw-dag-canvas {
54
+ position: relative;
55
+ min-height: 12rem;
56
+ }
57
+
58
+ .jw-dag-svg {
59
+ position: absolute;
60
+ inset: 0;
61
+ overflow: visible;
62
+ }
63
+
64
+ .jw-dag-edge {
65
+ fill: none;
66
+ stroke: #adb5bd;
67
+ stroke-width: 2;
68
+ stroke-linecap: round;
69
+ stroke-linejoin: round;
70
+ }
71
+
72
+ .jw-dag-node {
73
+ position: absolute;
74
+ display: flex;
75
+ flex-direction: column;
76
+ justify-content: flex-start;
77
+ gap: 0.4rem;
78
+ background: #fff;
79
+ border: 1px solid var(--jw-border);
80
+ border-radius: 0.85rem;
81
+ box-shadow: 0 0.4rem 1rem rgba(33, 37, 41, 0.08);
82
+ padding: 0.75rem 0.875rem;
83
+ }
84
+
85
+ .jw-dag-node-title {
86
+ font-size: 0.875rem;
87
+ line-height: 1.15;
88
+ max-width: 10rem;
89
+ overflow: hidden;
90
+ text-overflow: ellipsis;
91
+ white-space: nowrap;
92
+ }
93
+
94
+ .jw-dag-node-meta {
95
+ color: #6c757d;
96
+ font-size: 0.72rem;
97
+ }
98
+ </style>
99
+ </head>
100
+ <body>
101
+ <nav class="navbar navbar-expand-lg bg-white border-bottom sticky-top">
102
+ <div class="container-fluid jw-shell px-3 px-lg-4">
103
+ <a class="navbar-brand fw-semibold" href="<%= root_path %>">JobWorkflow Monitoring</a>
104
+ </div>
105
+ </nav>
106
+
107
+ <main class="container-fluid jw-shell py-4 py-lg-5 px-3 px-lg-4">
108
+ <%= yield %>
109
+ </main>
110
+
111
+ <script
112
+ src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
113
+ integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
114
+ crossorigin="anonymous"
115
+ ></script>
116
+ </body>
117
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ JobWorkflow::Monitoring::Engine.routes.draw do
4
+ resources :workflows, only: [:index], param: :job_class_name do
5
+ resources :executions, only: %i[index show]
6
+ end
7
+ root "workflows#index"
8
+ end