pgbus 0.19.4 → 0.20.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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +96 -7
- data/Rakefile +6 -1
- data/app/controllers/pgbus/dashboard_controller.rb +2 -0
- data/app/controllers/pgbus/workflow_definitions_controller.rb +28 -0
- data/app/controllers/pgbus/workflows_controller.rb +197 -0
- data/app/frontend/pgbus/modules/charts.js +130 -4
- data/app/frontend/pgbus/modules/charts.min.js +1 -1
- data/app/frontend/pgbus/modules/charts.min.js.map +1 -1
- data/app/frontend/pgbus/style.css +1 -1
- data/app/helpers/pgbus/button_helper.rb +4 -2
- data/app/helpers/pgbus/workflow_insights_helper.rb +129 -0
- data/app/helpers/pgbus/workflows_helper.rb +165 -0
- data/app/models/pgbus/workflow_entry.rb +172 -0
- data/app/models/pgbus/workflow_event.rb +43 -0
- data/app/models/pgbus/workflow_signal.rb +77 -0
- data/app/models/pgbus/workflow_step.rb +142 -0
- data/app/views/layouts/pgbus/application.html.erb +2 -0
- data/app/views/pgbus/dashboard/_stats_cards.html.erb +17 -0
- data/app/views/pgbus/workflows/_actions.html.erb +59 -0
- data/app/views/pgbus/workflows/_events_table.html.erb +24 -0
- data/app/views/pgbus/workflows/_filters.html.erb +29 -0
- data/app/views/pgbus/workflows/_signal_form.html.erb +31 -0
- data/app/views/pgbus/workflows/_signals_table.html.erb +40 -0
- data/app/views/pgbus/workflows/_status_filter.html.erb +12 -0
- data/app/views/pgbus/workflows/_steps_table.html.erb +79 -0
- data/app/views/pgbus/workflows/_timeline.html.erb +39 -0
- data/app/views/pgbus/workflows/_workflows_table.html.erb +59 -0
- data/app/views/pgbus/workflows/attention.html.erb +88 -0
- data/app/views/pgbus/workflows/definitions/_by_revision.html.erb +52 -0
- data/app/views/pgbus/workflows/definitions/_duration_table.html.erb +18 -0
- data/app/views/pgbus/workflows/definitions/_funnel.html.erb +49 -0
- data/app/views/pgbus/workflows/definitions/_pipeline.html.erb +16 -0
- data/app/views/pgbus/workflows/definitions/_step_card.html.erb +31 -0
- data/app/views/pgbus/workflows/definitions/_throughput_table.html.erb +32 -0
- data/app/views/pgbus/workflows/definitions/_window.html.erb +17 -0
- data/app/views/pgbus/workflows/definitions/index.html.erb +71 -0
- data/app/views/pgbus/workflows/definitions/not_found.html.erb +5 -0
- data/app/views/pgbus/workflows/definitions/show.html.erb +80 -0
- data/app/views/pgbus/workflows/index.html.erb +15 -0
- data/app/views/pgbus/workflows/not_found.html.erb +5 -0
- data/app/views/pgbus/workflows/show.html.erb +93 -0
- data/config/i18n-tasks.yml +6 -0
- data/config/locales/da.yml +442 -0
- data/config/locales/de.yml +442 -0
- data/config/locales/en.yml +442 -0
- data/config/locales/es.yml +442 -0
- data/config/locales/fi.yml +442 -0
- data/config/locales/fr.yml +442 -0
- data/config/locales/it.yml +442 -0
- data/config/locales/ja.yml +442 -0
- data/config/locales/nb.yml +442 -0
- data/config/locales/nl.yml +442 -0
- data/config/locales/pt.yml +442 -0
- data/config/locales/sv.yml +442 -0
- data/config/routes.rb +26 -0
- data/lib/generators/pgbus/add_workflow_signals_generator.rb +56 -0
- data/lib/generators/pgbus/add_workflows_generator.rb +54 -0
- data/lib/generators/pgbus/templates/add_workflow_signals.rb.erb +51 -0
- data/lib/generators/pgbus/templates/add_workflows.rb.erb +150 -0
- data/lib/generators/pgbus/templates/migration.rb.erb +127 -0
- data/lib/pgbus/active_job/delivery.rb +49 -0
- data/lib/pgbus/active_job/executor.rb +3 -1
- data/lib/pgbus/configuration/workflow_settings.rb +99 -0
- data/lib/pgbus/configuration.rb +21 -3
- data/lib/pgbus/generators/migration_detector.rb +40 -0
- data/lib/pgbus/instrumentation.rb +32 -0
- data/lib/pgbus/integrations/appsignal/dashboard.json +128 -1
- data/lib/pgbus/integrations/appsignal/subscriber.rb +16 -1
- data/lib/pgbus/mcp/rack_app.rb +7 -2
- data/lib/pgbus/mcp/runner.rb +2 -1
- data/lib/pgbus/mcp/serialization.rb +25 -0
- data/lib/pgbus/mcp/server.rb +53 -10
- data/lib/pgbus/mcp/tools/deploy_tasks_tool.rb +1 -10
- data/lib/pgbus/mcp/tools/workflow_cancel_tool.rb +23 -0
- data/lib/pgbus/mcp/tools/workflow_definitions_tool.rb +36 -0
- data/lib/pgbus/mcp/tools/workflow_detail_tool.rb +38 -0
- data/lib/pgbus/mcp/tools/workflow_resume_at_tool.rb +32 -0
- data/lib/pgbus/mcp/tools/workflow_resume_tool.rb +24 -0
- data/lib/pgbus/mcp/tools/workflow_run_now_tool.rb +23 -0
- data/lib/pgbus/mcp/tools/workflow_signal_tool.rb +77 -0
- data/lib/pgbus/mcp/tools/workflow_skip_step_tool.rb +23 -0
- data/lib/pgbus/mcp/tools/workflow_stats_tool.rb +42 -0
- data/lib/pgbus/mcp/tools/workflows_attention_tool.rb +42 -0
- data/lib/pgbus/mcp/tools/workflows_tool.rb +60 -0
- data/lib/pgbus/mcp/workflow_insights_payload.rb +15 -0
- data/lib/pgbus/mcp/workflow_payloads.rb +53 -0
- data/lib/pgbus/mcp/write_tool.rb +134 -0
- data/lib/pgbus/mcp.rb +15 -0
- data/lib/pgbus/metrics/backends/statsd.rb +6 -0
- data/lib/pgbus/metrics/subscriber.rb +14 -1
- data/lib/pgbus/metrics/workflow_metrics.rb +65 -0
- data/lib/pgbus/process/dispatcher.rb +26 -0
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source/workflow_actions.rb +109 -0
- data/lib/pgbus/web/data_source/workflow_insights.rb +99 -0
- data/lib/pgbus/web/data_source/workflow_list.rb +248 -0
- data/lib/pgbus/web/data_source.rb +3 -0
- data/lib/pgbus/web/workflow_action_plan.rb +62 -0
- data/lib/pgbus/web/workflow_insights_queries.rb +250 -0
- data/lib/pgbus/web/workflow_insights_report.rb +171 -0
- data/lib/pgbus/web/workflow_payload.rb +36 -0
- data/lib/pgbus/web/workflow_query.rb +82 -0
- data/lib/pgbus/web/workflow_state.rb +76 -0
- data/lib/pgbus/web/workflow_timeline.rb +113 -0
- data/lib/pgbus/web/workflow_window.rb +51 -0
- data/lib/pgbus/workflow/batch_finished_job.rb +21 -0
- data/lib/pgbus/workflow/dsl.rb +105 -0
- data/lib/pgbus/workflow/error_capture.rb +63 -0
- data/lib/pgbus/workflow/exception_policy.rb +170 -0
- data/lib/pgbus/workflow/fan_out.rb +157 -0
- data/lib/pgbus/workflow/fan_out_definition.rb +41 -0
- data/lib/pgbus/workflow/flow_control.rb +94 -0
- data/lib/pgbus/workflow/instance.rb +183 -0
- data/lib/pgbus/workflow/outside_verbs.rb +138 -0
- data/lib/pgbus/workflow/payload.rb +53 -0
- data/lib/pgbus/workflow/registry.rb +53 -0
- data/lib/pgbus/workflow/removed_step.rb +14 -0
- data/lib/pgbus/workflow/runner.rb +218 -0
- data/lib/pgbus/workflow/signal_matcher.rb +53 -0
- data/lib/pgbus/workflow/signals.rb +132 -0
- data/lib/pgbus/workflow/states.rb +119 -0
- data/lib/pgbus/workflow/step_context.rb +58 -0
- data/lib/pgbus/workflow/step_definition.rb +131 -0
- data/lib/pgbus/workflow/step_execution.rb +242 -0
- data/lib/pgbus/workflow/step_job.rb +28 -0
- data/lib/pgbus/workflow/step_outcome.rb +151 -0
- data/lib/pgbus/workflow/steps.rb +103 -0
- data/lib/pgbus/workflow/sweep.rb +156 -0
- data/lib/pgbus/workflow/test_helpers.rb +152 -0
- data/lib/pgbus/workflow/wait_timeout.rb +73 -0
- data/lib/pgbus/workflow/waiting.rb +82 -0
- data/lib/pgbus/workflow.rb +183 -0
- data/sig/pgbus/configuration.rbs +22 -0
- metadata +92 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<%# The pipeline (issue #549): the class's steps in order, one card each,
|
|
2
|
+
side by side and scrolled sideways when they do not fit. Locals:
|
|
3
|
+
definition. %>
|
|
4
|
+
<section class="mb-8" aria-labelledby="workflow-pipeline-title">
|
|
5
|
+
<h2 id="workflow-pipeline-title" class="text-lg font-medium text-gray-900 dark:text-white"><%= t("pgbus.workflow_insights.show.pipeline.title") %></h2>
|
|
6
|
+
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.pipeline.caption") %></p>
|
|
7
|
+
<% if definition[:steps].empty? %>
|
|
8
|
+
<p class="mt-3 rounded-lg bg-white dark:bg-gray-800 p-6 text-center text-sm text-gray-500 dark:text-gray-400 shadow ring-1 ring-gray-200 dark:ring-gray-700"><%= t("pgbus.workflow_insights.show.pipeline.empty") %></p>
|
|
9
|
+
<% else %>
|
|
10
|
+
<ol class="mt-3 flex gap-4 overflow-x-auto pb-2" data-testid="workflow-pipeline" tabindex="0" aria-labelledby="workflow-pipeline-title">
|
|
11
|
+
<% definition[:steps].each_with_index do |step, index| %>
|
|
12
|
+
<%= render "pgbus/workflows/definitions/step_card", step: step, position: index + 1, name: definition[:workflow_class] %>
|
|
13
|
+
<% end %>
|
|
14
|
+
</ol>
|
|
15
|
+
<% end %>
|
|
16
|
+
</section>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<%# One step of the pipeline (issue #549): the runs sitting at it now, each
|
|
2
|
+
count linked to those runs, then how its attempts that finished in the
|
|
3
|
+
window did. Locals: step, position, name (the workflow class). %>
|
|
4
|
+
<li data-step="<%= step[:name] %>" class="w-72 shrink-0 rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
5
|
+
<div class="flex items-start justify-between gap-2">
|
|
6
|
+
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
|
|
7
|
+
<span class="text-gray-600 dark:text-gray-300"><%= position %>.</span>
|
|
8
|
+
<%= pgbus_link_to step[:name], pgbus_insight_runs_path(workflow_class: name, step: step[:name]), class: "font-mono",
|
|
9
|
+
aria: { label: t("pgbus.workflow_insights.show.pipeline.step_link", step: step[:name]) } %>
|
|
10
|
+
</h3>
|
|
11
|
+
<%= pgbus_insight_kind(step) %>
|
|
12
|
+
</div>
|
|
13
|
+
<ul class="mt-3 grid grid-cols-2 gap-x-3 gap-y-1 text-sm" aria-label="<%= t("pgbus.workflow_insights.show.pipeline.now_label") %>">
|
|
14
|
+
<% Pgbus::WorkflowInsightsHelper::LIVE_STATUSES.each do |status| %>
|
|
15
|
+
<% count = step[:counts][status.to_sym] %>
|
|
16
|
+
<li class="flex items-center justify-between gap-2">
|
|
17
|
+
<span class="text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.pipeline.live.#{status}") %></span>
|
|
18
|
+
<span class="font-mono text-gray-900 dark:text-white"><%= pgbus_insight_count(count, pgbus_insight_runs_path(status: status, workflow_class: name, step: step[:name]),
|
|
19
|
+
t("pgbus.workflow_insights.links.at_step", status: t("pgbus.workflow_insights.show.pipeline.live.#{status}").downcase, step: step[:name])) %></span>
|
|
20
|
+
</li>
|
|
21
|
+
<% end %>
|
|
22
|
+
</ul>
|
|
23
|
+
<ul class="mt-3 space-y-1 border-t border-gray-200 pt-3 text-sm dark:border-gray-700" aria-label="<%= t("pgbus.workflow_insights.show.pipeline.window_label") %>">
|
|
24
|
+
<li><span class="text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.pipeline.run_time") %></span> <span class="font-mono text-gray-900 dark:text-white"><%= pgbus_insight_percentiles(step[:run_ms]) %></span></li>
|
|
25
|
+
<li><span class="text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.pipeline.pickup") %></span> <span class="font-mono text-gray-900 dark:text-white"><%= pgbus_insight_percentiles(step[:pickup_ms], %i[p50 p95]) %></span></li>
|
|
26
|
+
<% %i[failed reattempted skipped].each do |rate| %>
|
|
27
|
+
<li><span class="text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.pipeline.#{rate}") %></span> <span class="font-mono text-gray-900 dark:text-white"><%= pgbus_insight_rate(step[:rates][rate]) %></span></li>
|
|
28
|
+
<% end %>
|
|
29
|
+
<li class="text-xs text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.pipeline.attempts", count: step[:sample_size]) %></li>
|
|
30
|
+
</ul>
|
|
31
|
+
</li>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<%# The throughput chart's data table (issue #549): one row per bucket, each
|
|
2
|
+
count linked to those runs (from the bucket's first moment to just before
|
|
3
|
+
the next one's, so a 23- or 25-hour DST day links exactly). Locals: definition. %>
|
|
4
|
+
<% name = definition[:workflow_class] %>
|
|
5
|
+
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
6
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
7
|
+
<tr>
|
|
8
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.charts.throughput.headers.from") %></th>
|
|
9
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.charts.throughput.headers.started") %></th>
|
|
10
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.charts.throughput.headers.finished") %></th>
|
|
11
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.charts.throughput.headers.cancelled") %></th>
|
|
12
|
+
</tr>
|
|
13
|
+
</thead>
|
|
14
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
15
|
+
<% definition[:throughput].each do |bucket| %>
|
|
16
|
+
<% from = bucket[:from] %>
|
|
17
|
+
<% to = bucket[:to] - Rational(1, 1_000_000) %>
|
|
18
|
+
<tr>
|
|
19
|
+
<td data-label="<%= t("pgbus.workflow_insights.show.charts.throughput.headers.from") %>" class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300"><%= pgbus_absolute_time(from) %></td>
|
|
20
|
+
<td data-label="<%= t("pgbus.workflow_insights.show.charts.throughput.headers.started") %>" class="px-4 py-2 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
|
|
21
|
+
<%= pgbus_insight_count(bucket[:started], pgbus_insight_runs_path(status: "all", workflow_class: name, created_from: from, created_to: to), t("pgbus.workflow_insights.links.started_bucket")) %>
|
|
22
|
+
</td>
|
|
23
|
+
<td data-label="<%= t("pgbus.workflow_insights.show.charts.throughput.headers.finished") %>" class="px-4 py-2 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
|
|
24
|
+
<%= pgbus_insight_count(bucket[:finished], pgbus_insight_runs_path(status: "finished", workflow_class: name, ended_from: from, ended_to: to), t("pgbus.workflow_insights.links.finished_bucket")) %>
|
|
25
|
+
</td>
|
|
26
|
+
<td data-label="<%= t("pgbus.workflow_insights.show.charts.throughput.headers.cancelled") %>" class="px-4 py-2 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
|
|
27
|
+
<%= pgbus_insight_count(bucket[:cancelled], pgbus_insight_runs_path(status: "cancelled", workflow_class: name, ended_from: from, ended_to: to), t("pgbus.workflow_insights.links.cancelled_bucket")) %>
|
|
28
|
+
</td>
|
|
29
|
+
</tr>
|
|
30
|
+
<% end %>
|
|
31
|
+
</tbody>
|
|
32
|
+
</table>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<%# The window selector (issue #549): 1h, 24h, 7d, 30d. Locals: path, a
|
|
2
|
+
lambda from a window name to the same page over that window. A window
|
|
3
|
+
longer than workflow_retention is capped, and the page says so. %>
|
|
4
|
+
<div>
|
|
5
|
+
<nav class="flex flex-wrap gap-2" aria-label="<%= t("pgbus.workflow_insights.window_label") %>" data-testid="workflow-insights-window">
|
|
6
|
+
<% Pgbus::Web::WorkflowWindow::CHOICES.each_key do |name| %>
|
|
7
|
+
<% active = name == @window.name %>
|
|
8
|
+
<a href="<%= path.call(name) %>" aria-current="<%= active ? "page" : "false" %>"
|
|
9
|
+
class="inline-flex items-center rounded-md px-3 py-2 text-sm font-medium <%= active ? 'bg-gray-800 text-white dark:bg-gray-700' : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white' %>">
|
|
10
|
+
<%= name %>
|
|
11
|
+
</a>
|
|
12
|
+
<% end %>
|
|
13
|
+
</nav>
|
|
14
|
+
<% if @window.capped? %>
|
|
15
|
+
<p class="mt-2 text-xs text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.capped", retention: pgbus_duration(@window.seconds)) %></p>
|
|
16
|
+
<% end %>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<%# The workflow insights' class list (issue #549): every registered
|
|
2
|
+
workflow class and every class still in the rows, with its runs going on
|
|
3
|
+
now, the runs that ended in the window and how long a run takes end to
|
|
4
|
+
end. Each count links to the Workflows list filtered to those runs. %>
|
|
5
|
+
<div class="flex flex-wrap items-start justify-between gap-4 mb-6">
|
|
6
|
+
<div>
|
|
7
|
+
<div class="mb-2"><%= pgbus_link_to t("pgbus.workflow_insights.back_to_runs"), pgbus.workflows_path %></div>
|
|
8
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.workflow_insights.index.title") %></h1>
|
|
9
|
+
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.index.description", window: pgbus_insight_window_name(@window)) %></p>
|
|
10
|
+
</div>
|
|
11
|
+
<%= render "pgbus/workflows/definitions/window", path: ->(name) { pgbus.workflow_definitions_path(window: name) } %>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<%# The window's own lower bound: the one the counts were read with. %>
|
|
15
|
+
<% since = @window.since %>
|
|
16
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
17
|
+
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
18
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
19
|
+
<tr>
|
|
20
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.index.headers.workflow_class") %></th>
|
|
21
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.index.headers.ongoing") %></th>
|
|
22
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.index.headers.paused") %></th>
|
|
23
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.index.headers.attention") %></th>
|
|
24
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.index.headers.finished") %></th>
|
|
25
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.index.headers.cancelled") %></th>
|
|
26
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.index.headers.duration_p50") %></th>
|
|
27
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.index.headers.duration_p95") %></th>
|
|
28
|
+
</tr>
|
|
29
|
+
</thead>
|
|
30
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
31
|
+
<% @definitions.each do |row| %>
|
|
32
|
+
<% name = row[:workflow_class] %>
|
|
33
|
+
<tr data-workflow-class="<%= name %>" class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
34
|
+
<td data-label="<%= t("pgbus.workflow_insights.index.headers.workflow_class") %>" class="px-4 py-3 text-sm">
|
|
35
|
+
<div><%= pgbus_link_to name, pgbus_insight_definition_path(name, @window), class: "font-mono" %></div>
|
|
36
|
+
<div class="mt-1 flex flex-wrap gap-1">
|
|
37
|
+
<%= pgbus_insight_not_loaded_badge unless row[:loaded] %>
|
|
38
|
+
<%= pgbus_insight_removed_links(name, row[:removed_in_flight]) %>
|
|
39
|
+
</div>
|
|
40
|
+
</td>
|
|
41
|
+
<td data-label="<%= t("pgbus.workflow_insights.index.headers.ongoing") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
|
|
42
|
+
<%= pgbus_insight_count(row[:ongoing], pgbus_insight_runs_path(workflow_class: name), t("pgbus.workflow_insights.links.ongoing", workflow: name)) %>
|
|
43
|
+
</td>
|
|
44
|
+
<td data-label="<%= t("pgbus.workflow_insights.index.headers.paused") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
|
|
45
|
+
<%= pgbus_insight_count(row[:paused], pgbus_insight_runs_path(status: "paused", workflow_class: name), t("pgbus.workflow_insights.links.paused", workflow: name)) %>
|
|
46
|
+
</td>
|
|
47
|
+
<td data-label="<%= t("pgbus.workflow_insights.index.headers.attention") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
|
|
48
|
+
<%= pgbus_insight_count(row[:attention], pgbus.attention_workflows_path(workflow_class: name), t("pgbus.workflow_insights.links.attention", workflow: name)) %>
|
|
49
|
+
</td>
|
|
50
|
+
<td data-label="<%= t("pgbus.workflow_insights.index.headers.finished") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
|
|
51
|
+
<%= pgbus_insight_count(row[:finished], pgbus_insight_runs_path(status: "finished", workflow_class: name, ended_from: since, ended_to: @window.now), t("pgbus.workflow_insights.links.finished", workflow: name)) %>
|
|
52
|
+
</td>
|
|
53
|
+
<td data-label="<%= t("pgbus.workflow_insights.index.headers.cancelled") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
|
|
54
|
+
<%= pgbus_insight_count(row[:cancelled], pgbus_insight_runs_path(status: "cancelled", workflow_class: name, ended_from: since, ended_to: @window.now), t("pgbus.workflow_insights.links.cancelled", workflow: name)) %>
|
|
55
|
+
</td>
|
|
56
|
+
<td data-label="<%= t("pgbus.workflow_insights.index.headers.duration_p50") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
|
|
57
|
+
<%= pgbus_insight_ms(row[:e2e_ms][:p50]) %>
|
|
58
|
+
</td>
|
|
59
|
+
<td data-label="<%= t("pgbus.workflow_insights.index.headers.duration_p95") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300">
|
|
60
|
+
<%= pgbus_insight_ms(row[:e2e_ms][:p95]) %>
|
|
61
|
+
</td>
|
|
62
|
+
</tr>
|
|
63
|
+
<% end %>
|
|
64
|
+
<% if @definitions.empty? %>
|
|
65
|
+
<%= render "pgbus/shared/empty_row", colspan: 8,
|
|
66
|
+
message: safe_join([content_tag(:p, t("pgbus.workflow_insights.index.empty_title"), class: "text-lg font-medium"),
|
|
67
|
+
content_tag(:p, t("pgbus.workflow_insights.index.empty_hint"), class: "mt-1")]) %>
|
|
68
|
+
<% end %>
|
|
69
|
+
</tbody>
|
|
70
|
+
</table>
|
|
71
|
+
</div>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<div class="mb-2"><%= pgbus_link_to t("pgbus.workflow_insights.back_to_classes"), pgbus.workflow_definitions_path(window: @window.name) %></div>
|
|
3
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.workflow_insights.not_found.title") %></h1>
|
|
4
|
+
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.not_found.hint", workflow: params[:workflow_class]) %></p>
|
|
5
|
+
</div>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<%# One workflow class's insights (issue #549) over a window: the runs it
|
|
2
|
+
started and ended, its pipeline (each step with the runs at it now and
|
|
3
|
+
how its attempts did), the throughput and duration charts, the funnel of
|
|
4
|
+
the runs started in the window and the same numbers per app revision. %>
|
|
5
|
+
<% definition = @definition %>
|
|
6
|
+
<% name = definition[:workflow_class] %>
|
|
7
|
+
<% since = definition[:window][:since] %>
|
|
8
|
+
<div class="flex flex-wrap items-start justify-between gap-4 mb-6">
|
|
9
|
+
<div>
|
|
10
|
+
<div class="mb-2"><%= pgbus_link_to t("pgbus.workflow_insights.back_to_classes"), pgbus.workflow_definitions_path(window: @window.name) %></div>
|
|
11
|
+
<h1 class="text-2xl font-bold font-mono text-gray-900 dark:text-white"><%= name %></h1>
|
|
12
|
+
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.description", window: pgbus_insight_window_name(@window)) %></p>
|
|
13
|
+
<div class="mt-2 flex flex-wrap gap-1">
|
|
14
|
+
<%= pgbus_insight_not_loaded_badge unless definition[:loaded] %>
|
|
15
|
+
<%= pgbus_insight_removed_links(name, definition[:removed_in_flight]) %>
|
|
16
|
+
</div>
|
|
17
|
+
<% unless definition[:loaded] %>
|
|
18
|
+
<p class="mt-2 max-w-2xl text-sm text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.not_loaded_hint") %></p>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
<%= render "pgbus/workflows/definitions/window", path: ->(window) { pgbus.workflow_definition_path(name, window: window) } %>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6 mb-8" data-testid="workflow-insights-summary">
|
|
25
|
+
<% summary = {
|
|
26
|
+
started: pgbus_insight_count(definition[:started], pgbus_insight_runs_path(status: "all", workflow_class: name, created_from: since, created_to: @window.now), t("pgbus.workflow_insights.links.started", workflow: name), size: :lg),
|
|
27
|
+
finished: pgbus_insight_count(definition[:finished], pgbus_insight_runs_path(status: "finished", workflow_class: name, ended_from: since, ended_to: @window.now), t("pgbus.workflow_insights.links.finished", workflow: name), size: :lg),
|
|
28
|
+
cancelled: pgbus_insight_count(definition[:cancelled], pgbus_insight_runs_path(status: "cancelled", workflow_class: name, ended_from: since, ended_to: @window.now), t("pgbus.workflow_insights.links.cancelled", workflow: name), size: :lg),
|
|
29
|
+
ongoing: pgbus_insight_count(definition[:ongoing], pgbus_insight_runs_path(workflow_class: name), t("pgbus.workflow_insights.links.ongoing", workflow: name), size: :lg),
|
|
30
|
+
attention: pgbus_insight_count(definition[:attention], pgbus.attention_workflows_path(workflow_class: name), t("pgbus.workflow_insights.links.attention", workflow: name), size: :lg)
|
|
31
|
+
} %>
|
|
32
|
+
<% summary.each do |key, value| %>
|
|
33
|
+
<dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
34
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.summary.#{key}") %></dt>
|
|
35
|
+
<dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= value %></dd>
|
|
36
|
+
</dl>
|
|
37
|
+
<% end %>
|
|
38
|
+
<dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
39
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflow_insights.show.summary.duration") %></dt>
|
|
40
|
+
<dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_insight_ms(definition[:e2e_ms][:p50]) %></dd>
|
|
41
|
+
<dd class="mt-1 text-xs text-gray-600 dark:text-gray-300"><%= pgbus_insight_percentiles(definition[:e2e_ms], %i[p95 p99]) %></dd>
|
|
42
|
+
</dl>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<%= render "pgbus/workflows/definitions/pipeline", definition: definition %>
|
|
46
|
+
|
|
47
|
+
<% payload = pgbus_insight_chart_payload(definition) %>
|
|
48
|
+
<% totals = definition[:throughput].each_with_object(Hash.new(0)) { |bucket, sums| %i[started finished cancelled].each { |key| sums[key] += bucket[key] } } %>
|
|
49
|
+
<% bucket = pgbus_insight_span(definition[:bucket_seconds]) %>
|
|
50
|
+
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2 mb-8">
|
|
51
|
+
<%= render layout: "pgbus/insights/chart_card",
|
|
52
|
+
locals: { id: "workflow-throughput-chart", title: t("pgbus.workflow_insights.show.charts.throughput.title", bucket: bucket),
|
|
53
|
+
caption: t("pgbus.workflow_insights.show.charts.throughput.caption", bucket: bucket, window: pgbus_insight_window_name(@window),
|
|
54
|
+
started: totals[:started], finished: totals[:finished], cancelled: totals[:cancelled]),
|
|
55
|
+
empty: totals.values.sum.zero? } do %>
|
|
56
|
+
<%= render "pgbus/workflows/definitions/throughput_table", definition: definition %>
|
|
57
|
+
<% end %>
|
|
58
|
+
<% finished = definition[:duration_histogram].sum { |bin| bin[:count] } %>
|
|
59
|
+
<%= render layout: "pgbus/insights/chart_card",
|
|
60
|
+
locals: { id: "workflow-duration-chart", title: t("pgbus.workflow_insights.show.charts.durations.title"),
|
|
61
|
+
caption: t("pgbus.workflow_insights.show.charts.durations.caption", count: finished),
|
|
62
|
+
empty: finished.zero? } do %>
|
|
63
|
+
<%= render "pgbus/workflows/definitions/duration_table", definition: definition %>
|
|
64
|
+
<% end %>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<%= render "pgbus/workflows/definitions/funnel", definition: definition %>
|
|
68
|
+
<%= render "pgbus/workflows/definitions/by_revision", definition: definition %>
|
|
69
|
+
|
|
70
|
+
<%# The chart series, embedded once; json_escape keeps a value from closing the tag. %>
|
|
71
|
+
<%= tag.script(json_escape(payload.to_json).html_safe, type: "application/json", id: "pgbus-workflow-insights-data") %> <%# herb:disable erb-no-unsafe-raw %>
|
|
72
|
+
<script type="module" nonce="<%= content_security_policy_nonce %>">
|
|
73
|
+
import { renderWorkflowInsights, observeThemeChanges } from "charts";
|
|
74
|
+
|
|
75
|
+
const i18n = <%= json_escape(pgbus_insight_chart_i18n.to_json).html_safe %>;
|
|
76
|
+
const data = JSON.parse(document.getElementById("pgbus-workflow-insights-data").textContent);
|
|
77
|
+
|
|
78
|
+
renderWorkflowInsights(data, i18n);
|
|
79
|
+
observeThemeChanges(() => data, i18n, renderWorkflowInsights);
|
|
80
|
+
</script>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<div class="flex flex-wrap items-start justify-between gap-4 mb-6">
|
|
2
|
+
<div>
|
|
3
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.workflows.index.title") %></h1>
|
|
4
|
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.index.description") %></p>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
7
|
+
<%= pgbus_link_to t("pgbus.workflows.index.insights_link"), pgbus.workflow_definitions_path, variant: :secondary %>
|
|
8
|
+
<%= pgbus_link_to t("pgbus.workflows.index.attention_link", count: @attention_count), pgbus.attention_workflows_path,
|
|
9
|
+
variant: @attention_count.positive? ? :warning : :secondary %>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<%= render "pgbus/workflows/status_filter" %>
|
|
14
|
+
<%= render "pgbus/workflows/filters" %>
|
|
15
|
+
<%= render "pgbus/workflows/workflows_table" %>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<div class="mb-2"><%= pgbus_link_to t("pgbus.workflows.show.back"), pgbus.workflows_path %></div>
|
|
3
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.workflows.not_found.title") %></h1>
|
|
4
|
+
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300"><%= t("pgbus.workflows.not_found.hint") %></p>
|
|
5
|
+
</div>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<% result = pgbus_workflow_state(@workflow) %>
|
|
2
|
+
<% card = "rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" %>
|
|
3
|
+
<div class="mb-6">
|
|
4
|
+
<div class="mb-2"><%= pgbus_link_to t("pgbus.workflows.show.back"), pgbus.workflows_path %></div>
|
|
5
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
6
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= @workflow[:workflow_class] %></h1>
|
|
7
|
+
<%= pgbus_workflow_badge(result) %>
|
|
8
|
+
</div>
|
|
9
|
+
<p class="mt-1 font-mono text-xs text-gray-500 dark:text-gray-400"><%= @workflow[:workflow_id] %></p>
|
|
10
|
+
<p class="mt-2 text-sm text-gray-700 dark:text-gray-300" data-testid="workflow-next"><%= pgbus_workflow_next(@workflow, result) %></p>
|
|
11
|
+
<% if @workflow[:definition_outdated] %>
|
|
12
|
+
<p class="mt-3 rounded-md bg-yellow-50 px-3 py-2 text-sm text-yellow-800 ring-1 ring-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-200 dark:ring-yellow-800">
|
|
13
|
+
<%= t("pgbus.workflows.show.definition_outdated", workflow_class: @workflow[:workflow_class]) %>
|
|
14
|
+
</p>
|
|
15
|
+
<% end %>
|
|
16
|
+
<% unless @workflow[:class_loaded] %>
|
|
17
|
+
<p class="mt-3 rounded-md bg-gray-50 px-3 py-2 text-sm text-gray-700 ring-1 ring-gray-200 dark:bg-gray-900/40 dark:text-gray-200 dark:ring-gray-700">
|
|
18
|
+
<%= t("pgbus.workflows.show.class_missing", workflow_class: @workflow[:workflow_class]) %>
|
|
19
|
+
</p>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3 mb-8">
|
|
24
|
+
<section class="<%= card %> lg:col-span-2" aria-labelledby="workflow-details-title">
|
|
25
|
+
<h2 id="workflow-details-title" class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 text-lg font-medium text-gray-900 dark:text-white"><%= t("pgbus.workflows.show.details") %></h2>
|
|
26
|
+
<dl class="grid grid-cols-1 gap-4 p-4 sm:grid-cols-2">
|
|
27
|
+
<div>
|
|
28
|
+
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.headers.subject") %></dt>
|
|
29
|
+
<dd class="mt-1 text-sm text-gray-900 dark:text-white"><%= pgbus_workflow_subject(@workflow) %></dd>
|
|
30
|
+
</div>
|
|
31
|
+
<div>
|
|
32
|
+
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.headers.current_step") %></dt>
|
|
33
|
+
<dd class="mt-1 text-sm text-gray-900 dark:text-white"><code class="font-mono"><%= @workflow[:current_step] || Pgbus::Web::TimeFormat::NONE %></code></dd>
|
|
34
|
+
</div>
|
|
35
|
+
<div>
|
|
36
|
+
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.headers.started") %></dt>
|
|
37
|
+
<dd class="mt-1 text-sm text-gray-900 dark:text-white"><%= pgbus_timestamp(@workflow[:created_at]) %></dd>
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.headers.last_activity") %></dt>
|
|
41
|
+
<dd class="mt-1 text-sm text-gray-900 dark:text-white"><%= pgbus_timestamp(@workflow[:updated_at]) %></dd>
|
|
42
|
+
</div>
|
|
43
|
+
<% { paused_at: :paused_at, finished_at: :finished_at, cancelled_at: :cancelled_at }.each do |field, label| %>
|
|
44
|
+
<% next unless @workflow[field] %>
|
|
45
|
+
<div>
|
|
46
|
+
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.show.#{label}") %></dt>
|
|
47
|
+
<dd class="mt-1 text-sm text-gray-900 dark:text-white"><%= pgbus_timestamp(@workflow[field]) %></dd>
|
|
48
|
+
</div>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% if @workflow[:rerun_of_workflow_id] %>
|
|
51
|
+
<div>
|
|
52
|
+
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.show.rerun_of") %></dt>
|
|
53
|
+
<dd class="mt-1 text-sm font-mono"><%= pgbus_link_to @workflow[:rerun_of_workflow_id][0, 8], pgbus.workflow_path(@workflow[:rerun_of_workflow_id]) %></dd>
|
|
54
|
+
</div>
|
|
55
|
+
<% end %>
|
|
56
|
+
<% if @workflow[:reruns].any? %>
|
|
57
|
+
<div>
|
|
58
|
+
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.show.reruns") %></dt>
|
|
59
|
+
<dd class="mt-1 space-y-1 text-sm text-gray-900 dark:text-white">
|
|
60
|
+
<% @workflow[:reruns].each do |rerun| %>
|
|
61
|
+
<span class="block"><%= pgbus_link_to rerun[:workflow_id][0, 8], pgbus.workflow_path(rerun[:workflow_id]), class: "font-mono" %>
|
|
62
|
+
· <%= t("pgbus.workflows.statuses.#{rerun[:status]}") %> · <%= pgbus_time(rerun[:created_at]) %></span>
|
|
63
|
+
<% end %>
|
|
64
|
+
</dd>
|
|
65
|
+
</div>
|
|
66
|
+
<% end %>
|
|
67
|
+
</dl>
|
|
68
|
+
</section>
|
|
69
|
+
|
|
70
|
+
<section class="<%= card %>" aria-labelledby="workflow-arguments-title" data-testid="workflow-arguments">
|
|
71
|
+
<h2 id="workflow-arguments-title" class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 text-lg font-medium text-gray-900 dark:text-white"><%= t("pgbus.workflows.show.arguments") %></h2>
|
|
72
|
+
<% arguments = Pgbus::Web::PayloadFilter.filter(@workflow[:arguments] || {}) %>
|
|
73
|
+
<% if arguments.empty? %>
|
|
74
|
+
<p class="p-4 text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.show.no_arguments") %></p>
|
|
75
|
+
<% else %>
|
|
76
|
+
<dl class="space-y-3 p-4">
|
|
77
|
+
<% arguments.each do |key, value| %>
|
|
78
|
+
<div>
|
|
79
|
+
<dt class="font-mono text-xs font-medium text-gray-500 dark:text-gray-400"><%= key %></dt>
|
|
80
|
+
<dd class="mt-0.5 break-all font-mono text-sm text-gray-900 dark:text-white"><%= pgbus_workflow_value(value) %></dd>
|
|
81
|
+
</div>
|
|
82
|
+
<% end %>
|
|
83
|
+
</dl>
|
|
84
|
+
<% end %>
|
|
85
|
+
</section>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<%= render "pgbus/workflows/actions" %>
|
|
89
|
+
<%= render "pgbus/workflows/signal_form" if @workflow[:available_actions].include?(:signal) %>
|
|
90
|
+
<%= render "pgbus/workflows/timeline" %>
|
|
91
|
+
<%= render "pgbus/workflows/steps_table" %>
|
|
92
|
+
<%= render "pgbus/workflows/signals_table" %>
|
|
93
|
+
<%= render "pgbus/workflows/events_table" %>
|
data/config/i18n-tasks.yml
CHANGED
|
@@ -45,6 +45,12 @@ ignore_unused:
|
|
|
45
45
|
- "pgbus.processes.state.activity.slots.*"
|
|
46
46
|
- "pgbus.processes.state.activity.threads.*"
|
|
47
47
|
- "pgbus.processes.state.kinds.*"
|
|
48
|
+
# Workflows (issue #542): plural forms reached through a dynamic key.
|
|
49
|
+
- "pgbus.workflows.attention.confirm.*"
|
|
50
|
+
- "pgbus.workflows.flash.bulk_resume.*"
|
|
51
|
+
- "pgbus.workflows.flash.bulk_cancel.*"
|
|
52
|
+
# Workflow insights (issue #549): plural forms reached through a dynamic key.
|
|
53
|
+
- "pgbus.workflow_insights.show.funnel.here.*"
|
|
48
54
|
# Plural forms reached through the dynamic summary.<key> lookup (issue #491).
|
|
49
55
|
- "pgbus.queues.show.summary.drained_by_html.*"
|
|
50
56
|
- "pgbus.queues.show.summary.drained_wildcard_html.*"
|