pgbus 0.19.3 → 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 +8 -0
- data/README.md +104 -9
- 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/adapter.rb +67 -26
- data/lib/pgbus/active_job/batch_id.rb +23 -1
- data/lib/pgbus/active_job/delivery.rb +49 -0
- data/lib/pgbus/active_job/executor.rb +3 -1
- data/lib/pgbus/batch/builder.rb +1 -0
- data/lib/pgbus/batch.rb +35 -3
- 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/event_span.rb +43 -0
- data/lib/pgbus/integrations/appsignal/job_span.rb +47 -0
- data/lib/pgbus/integrations/appsignal/span_subscriber.rb +128 -0
- data/lib/pgbus/integrations/appsignal/subscriber.rb +50 -100
- 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 +95 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<%# The operator actions (issue #542). Which buttons show is
|
|
2
|
+
available_actions (Workflow::States), never view logic; every button
|
|
3
|
+
submits the one form, to its own formaction, so the reason typed once
|
|
4
|
+
goes with whichever is pressed. %>
|
|
5
|
+
<% actions = pgbus_workflow_dashboard_actions(@workflow) %>
|
|
6
|
+
<% if actions.any? %>
|
|
7
|
+
<% id = @workflow[:workflow_id] %>
|
|
8
|
+
<%# The form's own action is the first button's, so Enter in the reason box
|
|
9
|
+
does what the first button does (and asks to confirm it). %>
|
|
10
|
+
<% paths = { resume: pgbus.resume_workflow_path(id), run_now: pgbus.run_now_workflow_path(id),
|
|
11
|
+
skip: pgbus.skip_step_workflow_path(id), rerun: pgbus.rerun_workflow_path(id),
|
|
12
|
+
cancel: pgbus.cancel_workflow_path(id), resume_at: pgbus.resume_at_workflow_path(id) } %>
|
|
13
|
+
<% default_path = paths.fetch(paths.keys.find { |action| actions.include?(action) }) %>
|
|
14
|
+
<section class="mb-8 rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700" aria-labelledby="workflow-actions-title" data-testid="workflow-actions">
|
|
15
|
+
<h2 id="workflow-actions-title" class="text-lg font-medium text-gray-900 dark:text-white"><%= t("pgbus.workflows.show.actions") %></h2>
|
|
16
|
+
<form action="<%= default_path %>" method="post" data-turbo-frame="_top" class="mt-3">
|
|
17
|
+
<%= hidden_field_tag :authenticity_token, form_authenticity_token, id: nil %>
|
|
18
|
+
<div class="max-w-xl">
|
|
19
|
+
<label for="workflow-action-reason" class="block text-sm font-medium text-gray-700 dark:text-gray-300"><%= t("pgbus.workflows.show.reason_label") %></label>
|
|
20
|
+
<input type="text" autocomplete="off" id="workflow-action-reason" name="reason" maxlength="1000" aria-describedby="workflow-action-reason-hint" class="<%= Pgbus::WorkflowsHelper::INPUT_CSS %>">
|
|
21
|
+
<p id="workflow-action-reason-hint" class="mt-1 text-xs text-gray-600 dark:text-gray-300"><%= t("pgbus.workflows.show.reason_hint") %></p>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="mt-4 flex flex-wrap items-end gap-2">
|
|
24
|
+
<% if actions.include?(:resume) %>
|
|
25
|
+
<%= pgbus_button_tag pgbus_workflow_resume_label(@workflow), variant: :success, formaction: pgbus.resume_workflow_path(id),
|
|
26
|
+
confirm: t("pgbus.workflows.confirm.resume") %>
|
|
27
|
+
<% end %>
|
|
28
|
+
<% if actions.include?(:run_now) %>
|
|
29
|
+
<%= pgbus_button_tag t("pgbus.workflows.actions.run_now"), variant: :primary, formaction: pgbus.run_now_workflow_path(id),
|
|
30
|
+
confirm: t("pgbus.workflows.confirm.run_now") %>
|
|
31
|
+
<% end %>
|
|
32
|
+
<% if actions.include?(:skip) %>
|
|
33
|
+
<%= pgbus_button_tag t("pgbus.workflows.actions.skip_step"), variant: :warning, formaction: pgbus.skip_step_workflow_path(id),
|
|
34
|
+
confirm: t("pgbus.workflows.confirm.skip_step") %>
|
|
35
|
+
<% end %>
|
|
36
|
+
<% if actions.include?(:rerun) %>
|
|
37
|
+
<%= pgbus_button_tag t("pgbus.workflows.actions.rerun"), variant: :primary, formaction: pgbus.rerun_workflow_path(id),
|
|
38
|
+
confirm: t("pgbus.workflows.confirm.rerun") %>
|
|
39
|
+
<% end %>
|
|
40
|
+
<% if actions.include?(:cancel) %>
|
|
41
|
+
<%= pgbus_button_tag t("pgbus.workflows.actions.cancel"), variant: :danger, formaction: pgbus.cancel_workflow_path(id),
|
|
42
|
+
confirm: t("pgbus.workflows.confirm.cancel") %>
|
|
43
|
+
<% end %>
|
|
44
|
+
</div>
|
|
45
|
+
<% if actions.include?(:resume_at) && @workflow[:step_names].any? %>
|
|
46
|
+
<div class="mt-4 flex flex-wrap items-end gap-2">
|
|
47
|
+
<div>
|
|
48
|
+
<label for="workflow-resume-step" class="block text-sm font-medium text-gray-700 dark:text-gray-300"><%= t("pgbus.workflows.show.resume_at_label") %></label>
|
|
49
|
+
<select id="workflow-resume-step" name="step" class="<%= Pgbus::WorkflowsHelper::INPUT_CSS %>">
|
|
50
|
+
<%= options_for_select(@workflow[:step_names], @workflow[:current_step]) %>
|
|
51
|
+
</select>
|
|
52
|
+
</div>
|
|
53
|
+
<%= pgbus_button_tag t("pgbus.workflows.actions.resume_at"), variant: :secondary, formaction: pgbus.resume_at_workflow_path(id),
|
|
54
|
+
confirm: t("pgbus.workflows.confirm.resume_at") %>
|
|
55
|
+
</div>
|
|
56
|
+
<% end %>
|
|
57
|
+
</form>
|
|
58
|
+
</section>
|
|
59
|
+
<% end %>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<%# The audit trail (issue #542): pgbus_workflow_events, newest first, each
|
|
2
|
+
as "<what> by <actor>: <reason>". %>
|
|
3
|
+
<section class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" aria-labelledby="workflow-events-title" data-testid="workflow-events">
|
|
4
|
+
<h2 id="workflow-events-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.audit.title") %></h2>
|
|
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.workflows.audit.headers.when") %></th>
|
|
9
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.audit.headers.what") %></th>
|
|
10
|
+
</tr>
|
|
11
|
+
</thead>
|
|
12
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
13
|
+
<% @workflow[:events].each do |event| %>
|
|
14
|
+
<tr data-event="<%= event[:event] %>" class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
15
|
+
<td data-label="<%= t("pgbus.workflows.audit.headers.when") %>" class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300 whitespace-nowrap"><%= pgbus_time(event[:created_at]) %></td>
|
|
16
|
+
<td data-label="<%= t("pgbus.workflows.audit.headers.what") %>" class="px-4 py-3 text-sm text-gray-900 dark:text-white"><%= pgbus_workflow_event_sentence(event) %></td>
|
|
17
|
+
</tr>
|
|
18
|
+
<% end %>
|
|
19
|
+
<% if @workflow[:events].empty? %>
|
|
20
|
+
<%= render "pgbus/shared/empty_row", colspan: 2, message: t("pgbus.workflows.audit.empty") %>
|
|
21
|
+
<% end %>
|
|
22
|
+
</tbody>
|
|
23
|
+
</table>
|
|
24
|
+
</section>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<%# The filters beyond status (issue #542). A GET form: the filters combine,
|
|
2
|
+
the status tab is kept, and the pager carries them on. %>
|
|
3
|
+
<% input_css = Pgbus::WorkflowsHelper::INPUT_CSS %>
|
|
4
|
+
<form action="<%= pgbus.workflows_path %>" method="get" data-turbo-frame="_top" data-testid="workflow-filters"
|
|
5
|
+
class="mb-4 rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
6
|
+
<input type="hidden" name="status" value="<%= @query.status %>">
|
|
7
|
+
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
8
|
+
<% Pgbus::Web::WorkflowQuery::TEXT_FILTERS.each do |filter| %>
|
|
9
|
+
<div>
|
|
10
|
+
<label for="workflow-filter-<%= filter %>" class="block text-xs font-medium text-gray-700 dark:text-gray-300"><%= t("pgbus.workflows.filters.#{filter}") %></label>
|
|
11
|
+
<input type="text" autocomplete="off" id="workflow-filter-<%= filter %>" name="<%= filter %>" value="<%= @query.raw[filter] %>" class="<%= input_css %>">
|
|
12
|
+
</div>
|
|
13
|
+
<% end %>
|
|
14
|
+
<% Pgbus::Web::WorkflowQuery::DATE_FILTERS.each do |filter| %>
|
|
15
|
+
<div>
|
|
16
|
+
<label for="workflow-filter-<%= filter %>" class="block text-xs font-medium text-gray-700 dark:text-gray-300"><%= t("pgbus.workflows.filters.#{filter}") %></label>
|
|
17
|
+
<% value = @query.raw[filter] %>
|
|
18
|
+
<%# A date-time from a shared URL stays a text field, so it survives the next Apply. %>
|
|
19
|
+
<input type="<%= value.nil? || value.match?(Pgbus::Web::WorkflowQuery::DATE_ONLY) ? "date" : "text" %>" autocomplete="off" id="workflow-filter-<%= filter %>" name="<%= filter %>" value="<%= @query.raw[filter] %>" class="<%= input_css %>">
|
|
20
|
+
</div>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="mt-3 flex items-center gap-2">
|
|
24
|
+
<%= pgbus_button_tag t("pgbus.workflows.filters.apply"), variant: :primary, size: :sm %>
|
|
25
|
+
<% if @query.filtered? %>
|
|
26
|
+
<%= pgbus_link_to t("pgbus.workflows.filters.clear"), pgbus.workflows_path(status: @query.status), variant: :secondary, size: :sm %>
|
|
27
|
+
<% end %>
|
|
28
|
+
</div>
|
|
29
|
+
</form>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<%# Send a signal (issue #542). The payload must be a JSON object; the
|
|
2
|
+
idempotency key is generated per render, so a double submit sends one
|
|
3
|
+
signal. The name is a select of what the waiting step takes, or free text. %>
|
|
4
|
+
<% id = @workflow[:workflow_id] %>
|
|
5
|
+
<section class="mb-8 rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700" aria-labelledby="workflow-signal-title" data-testid="workflow-signal">
|
|
6
|
+
<h2 id="workflow-signal-title" class="text-lg font-medium text-gray-900 dark:text-white"><%= t("pgbus.workflows.signal.title") %></h2>
|
|
7
|
+
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300"><%= t("pgbus.workflows.signal.hint") %></p>
|
|
8
|
+
<form action="<%= pgbus.signal_workflow_path(id) %>" method="post" data-turbo-frame="_top" class="mt-3 grid max-w-xl grid-cols-1 gap-3">
|
|
9
|
+
<%= hidden_field_tag :authenticity_token, form_authenticity_token, id: nil %>
|
|
10
|
+
<input type="hidden" name="idempotency_key" value="<%= SecureRandom.uuid %>">
|
|
11
|
+
<div>
|
|
12
|
+
<label for="workflow-signal-name" class="block text-sm font-medium text-gray-700 dark:text-gray-300"><%= t("pgbus.workflows.signal.name") %></label>
|
|
13
|
+
<% if @workflow[:signal_names].any? %>
|
|
14
|
+
<select id="workflow-signal-name" name="name" class="<%= Pgbus::WorkflowsHelper::INPUT_CSS %>">
|
|
15
|
+
<% @workflow[:signal_names].each do |name| %>
|
|
16
|
+
<option value="<%= name %>"><%= name %></option>
|
|
17
|
+
<% end %>
|
|
18
|
+
</select>
|
|
19
|
+
<% else %>
|
|
20
|
+
<input type="text" autocomplete="off" id="workflow-signal-name" name="name" required class="<%= Pgbus::WorkflowsHelper::INPUT_CSS %>">
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
23
|
+
<div>
|
|
24
|
+
<label for="workflow-signal-payload" class="block text-sm font-medium text-gray-700 dark:text-gray-300"><%= t("pgbus.workflows.signal.payload") %></label>
|
|
25
|
+
<textarea id="workflow-signal-payload" name="payload" rows="4" class="<%= Pgbus::WorkflowsHelper::INPUT_CSS %> font-mono">{}</textarea>
|
|
26
|
+
</div>
|
|
27
|
+
<div>
|
|
28
|
+
<%= pgbus_button_tag t("pgbus.workflows.signal.send"), variant: :primary, confirm: t("pgbus.workflows.confirm.signal") %>
|
|
29
|
+
</div>
|
|
30
|
+
</form>
|
|
31
|
+
</section>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<%# The signals delivered to this run (issue #542), newest first. %>
|
|
2
|
+
<section class="mb-8 rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" aria-labelledby="workflow-signals-title" data-testid="workflow-signals">
|
|
3
|
+
<h2 id="workflow-signals-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.signals.title") %></h2>
|
|
4
|
+
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
5
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
6
|
+
<tr>
|
|
7
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.signals.headers.name") %></th>
|
|
8
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.signals.headers.status") %></th>
|
|
9
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.signals.headers.received") %></th>
|
|
10
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.signals.headers.claimed") %></th>
|
|
11
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.signals.headers.consumed") %></th>
|
|
12
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.signals.headers.idempotency_key") %></th>
|
|
13
|
+
</tr>
|
|
14
|
+
</thead>
|
|
15
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
16
|
+
<% @workflow[:signals].each do |signal| %>
|
|
17
|
+
<tr class="align-top hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
18
|
+
<td data-label="<%= t("pgbus.workflows.signals.headers.name") %>" class="px-4 py-3 text-sm text-gray-900 dark:text-white">
|
|
19
|
+
<code class="font-mono"><%= signal[:name] %></code>
|
|
20
|
+
<% payload = Pgbus::Web::PayloadFilter.filter(signal[:payload] || {}) %>
|
|
21
|
+
<% if payload.any? %>
|
|
22
|
+
<details class="mt-1">
|
|
23
|
+
<summary class="inline-flex min-h-6 cursor-pointer items-center text-xs font-medium text-indigo-700 hover:text-indigo-900 dark:text-indigo-300 dark:hover:text-indigo-200"><%= t("pgbus.workflows.signals.payload") %></summary>
|
|
24
|
+
<pre tabindex="0" class="mt-1 overflow-auto rounded bg-gray-50 p-2 text-xs text-gray-800 dark:bg-gray-900 dark:text-gray-200"><%= JSON.pretty_generate(payload) %></pre>
|
|
25
|
+
</details>
|
|
26
|
+
<% end %>
|
|
27
|
+
</td>
|
|
28
|
+
<td data-label="<%= t("pgbus.workflows.signals.headers.status") %>" class="px-4 py-3 text-sm"><%= pgbus_workflow_signal_badge(signal[:status]) %></td>
|
|
29
|
+
<td data-label="<%= t("pgbus.workflows.signals.headers.received") %>" class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300"><%= pgbus_time(signal[:created_at]) %></td>
|
|
30
|
+
<td data-label="<%= t("pgbus.workflows.signals.headers.claimed") %>" class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300"><%= pgbus_time(signal[:claimed_at]) %></td>
|
|
31
|
+
<td data-label="<%= t("pgbus.workflows.signals.headers.consumed") %>" class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300"><%= pgbus_time(signal[:consumed_at]) %></td>
|
|
32
|
+
<td data-label="<%= t("pgbus.workflows.signals.headers.idempotency_key") %>" class="px-4 py-3 font-mono text-xs text-gray-600 dark:text-gray-300"><%= signal[:idempotency_key] || Pgbus::Web::TimeFormat::NONE %></td>
|
|
33
|
+
</tr>
|
|
34
|
+
<% end %>
|
|
35
|
+
<% if @workflow[:signals].empty? %>
|
|
36
|
+
<%= render "pgbus/shared/empty_row", colspan: 6, message: t("pgbus.workflows.signals.empty") %>
|
|
37
|
+
<% end %>
|
|
38
|
+
</tbody>
|
|
39
|
+
</table>
|
|
40
|
+
</section>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<%# The status tabs (issue #542): ongoing by default, each state, all. A tab
|
|
2
|
+
keeps the filters and goes back to page 1. %>
|
|
3
|
+
<nav class="mb-4 flex flex-wrap gap-2" aria-label="<%= t("pgbus.workflows.index.status_label") %>" data-testid="workflow-status-tabs">
|
|
4
|
+
<% Pgbus::Web::DataSource::WorkflowList::STATUSES.each do |status| %>
|
|
5
|
+
<% active = status == @query.status %>
|
|
6
|
+
<a href="<%= pgbus_workflows_path_with(@query, status: status) %>"
|
|
7
|
+
data-status="<%= status %>" aria-current="<%= active ? "page" : "false" %>"
|
|
8
|
+
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' %>">
|
|
9
|
+
<%= t("pgbus.workflows.tabs.#{status}") %>
|
|
10
|
+
</a>
|
|
11
|
+
<% end %>
|
|
12
|
+
</nav>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<%# Every attempt, newest first (issue #542): the text equivalent of the
|
|
2
|
+
timeline — the three durations, the worker and app revision, and the
|
|
3
|
+
error with its backtrace and cause chain under a <details>. %>
|
|
4
|
+
<section class="mb-8 rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" aria-labelledby="workflow-steps-title" data-testid="workflow-steps">
|
|
5
|
+
<h2 id="workflow-steps-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.steps.title") %></h2>
|
|
6
|
+
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
7
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
8
|
+
<tr>
|
|
9
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.steps.headers.step") %></th>
|
|
10
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.steps.headers.status") %></th>
|
|
11
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.steps.headers.scheduled") %></th>
|
|
12
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.steps.headers.started") %></th>
|
|
13
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.steps.headers.finished") %></th>
|
|
14
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.steps.headers.waited") %></th>
|
|
15
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.steps.headers.queued") %></th>
|
|
16
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.steps.headers.ran") %></th>
|
|
17
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.steps.headers.worker") %></th>
|
|
18
|
+
</tr>
|
|
19
|
+
</thead>
|
|
20
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
21
|
+
<% @workflow[:steps].each do |step| %>
|
|
22
|
+
<% durations = step[:durations] || {} %>
|
|
23
|
+
<tr data-step-attempt="<%= step[:step_name] %>-<%= step[:attempt] %>" class="align-top hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
24
|
+
<td data-label="<%= t("pgbus.workflows.steps.headers.step") %>" class="px-4 py-3 text-sm text-gray-900 dark:text-white">
|
|
25
|
+
<code class="font-mono"><%= step[:step_name] %></code>
|
|
26
|
+
<span class="text-gray-500 dark:text-gray-400">#<%= step[:attempt] %></span>
|
|
27
|
+
<% if step[:batch_id] %>
|
|
28
|
+
<span class="block text-xs"><%= pgbus_link_to t("pgbus.workflows.steps.batch", batch: step[:batch_id][0, 8]), pgbus.batch_path(step[:batch_id]), size: :sm %></span>
|
|
29
|
+
<% end %>
|
|
30
|
+
<% if step[:wait_for] %>
|
|
31
|
+
<span class="block text-xs text-gray-600 dark:text-gray-300"><%= t("pgbus.workflows.steps.waits_for_html", signal: tag.code(step[:wait_for], class: "font-mono")) %></span>
|
|
32
|
+
<% end %>
|
|
33
|
+
</td>
|
|
34
|
+
<td data-label="<%= t("pgbus.workflows.steps.headers.status") %>" class="px-4 py-3 text-sm max-w-md">
|
|
35
|
+
<%= pgbus_workflow_step_badge(step[:status]) %>
|
|
36
|
+
<% if step[:outcome] && step[:outcome] != step[:status] %>
|
|
37
|
+
<span class="ml-1 text-xs text-gray-600 dark:text-gray-300"><%= t("pgbus.workflows.outcomes.#{step[:outcome]}", default: step[:outcome].to_s.humanize) %></span>
|
|
38
|
+
<% end %>
|
|
39
|
+
<% if step[:error_class] %>
|
|
40
|
+
<p class="mt-1 font-mono text-xs text-red-700 dark:text-red-300"><%= step[:error_class] %></p>
|
|
41
|
+
<p class="text-xs text-gray-700 dark:text-gray-300 break-words"><%= step[:error_message] %></p>
|
|
42
|
+
<% if step[:error_backtrace].present? || step[:error_causes].any? %>
|
|
43
|
+
<details class="mt-1">
|
|
44
|
+
<summary class="inline-flex min-h-6 cursor-pointer items-center text-xs font-medium text-indigo-700 hover:text-indigo-900 dark:text-indigo-300 dark:hover:text-indigo-200"><%= t("pgbus.workflows.steps.show_backtrace") %></summary>
|
|
45
|
+
<% if step[:error_backtrace].present? %>
|
|
46
|
+
<pre tabindex="0" class="mt-1 max-h-64 overflow-auto rounded bg-gray-50 p-2 text-xs text-gray-800 dark:bg-gray-900 dark:text-gray-200"><%= step[:error_backtrace] %></pre>
|
|
47
|
+
<% end %>
|
|
48
|
+
<% step[:error_causes].each do |cause| %>
|
|
49
|
+
<p class="mt-1 text-xs text-gray-700 dark:text-gray-300"><%= t("pgbus.workflows.steps.caused_by", error: "#{cause["class"]}: #{cause["message"]}") %></p>
|
|
50
|
+
<% end %>
|
|
51
|
+
</details>
|
|
52
|
+
<% end %>
|
|
53
|
+
<% end %>
|
|
54
|
+
</td>
|
|
55
|
+
<td data-label="<%= t("pgbus.workflows.steps.headers.scheduled") %>" class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300"><%= pgbus_time(step[:scheduled_at]) %></td>
|
|
56
|
+
<td data-label="<%= t("pgbus.workflows.steps.headers.started") %>" class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300"><%= pgbus_time(step[:started_at]) %></td>
|
|
57
|
+
<td data-label="<%= t("pgbus.workflows.steps.headers.finished") %>" class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300"><%= pgbus_time(step[:finished_at]) %></td>
|
|
58
|
+
<td data-label="<%= t("pgbus.workflows.steps.headers.waited") %>" class="px-4 py-3 text-right font-mono text-sm text-gray-700 dark:text-gray-300">
|
|
59
|
+
<%= pgbus_duration(durations[:waited]) %>
|
|
60
|
+
<% if durations[:waiting] %>
|
|
61
|
+
<span class="block text-xs text-gray-600 dark:text-gray-300"><%= t("pgbus.workflows.steps.waiting_for", duration: pgbus_duration(durations[:waiting])) %></span>
|
|
62
|
+
<% end %>
|
|
63
|
+
</td>
|
|
64
|
+
<td data-label="<%= t("pgbus.workflows.steps.headers.queued") %>" class="px-4 py-3 text-right font-mono text-sm text-gray-700 dark:text-gray-300"><%= pgbus_duration(durations[:queued]) %></td>
|
|
65
|
+
<td data-label="<%= t("pgbus.workflows.steps.headers.ran") %>" class="px-4 py-3 text-right font-mono text-sm text-gray-700 dark:text-gray-300"><%= pgbus_duration(durations[:ran]) %></td>
|
|
66
|
+
<td data-label="<%= t("pgbus.workflows.steps.headers.worker") %>" class="px-4 py-3 text-xs text-gray-600 dark:text-gray-300">
|
|
67
|
+
<span class="block font-mono"><%= step[:worker] || Pgbus::Web::TimeFormat::NONE %></span>
|
|
68
|
+
<% if step[:app_revision] %>
|
|
69
|
+
<span class="block"><%= t("pgbus.workflows.steps.revision", revision: step[:app_revision]) %></span>
|
|
70
|
+
<% end %>
|
|
71
|
+
</td>
|
|
72
|
+
</tr>
|
|
73
|
+
<% end %>
|
|
74
|
+
<% if @workflow[:steps].empty? %>
|
|
75
|
+
<%= render "pgbus/shared/empty_row", colspan: 9, message: t("pgbus.workflows.steps.empty") %>
|
|
76
|
+
<% end %>
|
|
77
|
+
</tbody>
|
|
78
|
+
</table>
|
|
79
|
+
</section>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<%# The timeline (issue #542): one range-bar row per attempt, split into
|
|
2
|
+
waited, queued and ran (ran coloured by how the attempt ended), with the
|
|
3
|
+
waits and pauses as their own segments. charts.js renderTimeline draws
|
|
4
|
+
it from the payload embedded once below; the attempts table under it is
|
|
5
|
+
the full text alternative. %>
|
|
6
|
+
<% timeline = @workflow[:timeline] %>
|
|
7
|
+
<section class="mb-8 rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700" aria-labelledby="workflow-timeline-title">
|
|
8
|
+
<h2 id="workflow-timeline-title" class="text-lg font-medium text-gray-900 dark:text-white"><%= t("pgbus.workflows.timeline.title") %></h2>
|
|
9
|
+
<% if timeline[:rows].empty? %>
|
|
10
|
+
<p class="py-8 text-center text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.timeline.empty") %></p>
|
|
11
|
+
<% else %>
|
|
12
|
+
<% rows = timeline[:rows].size + (timeline[:paused].any? ? 1 : 0) %>
|
|
13
|
+
<figure class="mt-3">
|
|
14
|
+
<div id="workflow-timeline-chart" role="img" aria-label="<%= t("pgbus.workflows.timeline.caption") %>"
|
|
15
|
+
style="min-height: <%= [160, (rows * 36) + 90].max %>px;"></div>
|
|
16
|
+
<figcaption class="mt-3 text-sm text-gray-700 dark:text-gray-300"><%= t("pgbus.workflows.timeline.caption") %></figcaption>
|
|
17
|
+
</figure>
|
|
18
|
+
<%# The pauses as text: the chart's paused row, for those who cannot see it. %>
|
|
19
|
+
<% if timeline[:paused].any? %>
|
|
20
|
+
<ul class="mt-2 space-y-1 text-sm text-gray-700 dark:text-gray-300" data-testid="workflow-pauses">
|
|
21
|
+
<% timeline[:paused].each do |period| %>
|
|
22
|
+
<li><%= t("pgbus.workflows.timeline.paused_period_html", from: pgbus_absolute_time(Time.at(period[:from] / 1000.0)),
|
|
23
|
+
duration: period[:duration]) %></li>
|
|
24
|
+
<% end %>
|
|
25
|
+
</ul>
|
|
26
|
+
<% end %>
|
|
27
|
+
<%# json_escape keeps a value from closing the tag; a data block needs no nonce. %>
|
|
28
|
+
<%= tag.script(json_escape(timeline.to_json).html_safe, type: "application/json", id: "pgbus-workflow-timeline-data") %> <%# herb:disable erb-no-unsafe-raw %>
|
|
29
|
+
<script type="module" nonce="<%= content_security_policy_nonce %>">
|
|
30
|
+
import { renderTimeline, observeThemeChanges } from "charts";
|
|
31
|
+
|
|
32
|
+
const i18n = <%= json_escape(pgbus_workflow_chart_i18n.to_json).html_safe %>;
|
|
33
|
+
const data = JSON.parse(document.getElementById("pgbus-workflow-timeline-data").textContent);
|
|
34
|
+
|
|
35
|
+
renderTimeline(data, i18n);
|
|
36
|
+
observeThemeChanges(() => data, i18n, renderTimeline);
|
|
37
|
+
</script>
|
|
38
|
+
<% end %>
|
|
39
|
+
</section>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<%# The workflow list (issue #542). Each row says, in words, what the run is
|
|
2
|
+
doing and what happens next (Web::WorkflowState). Auto-refreshes; the
|
|
3
|
+
pager keeps the status and the filters. %>
|
|
4
|
+
<turbo-frame id="workflows-list" data-auto-refresh data-turbo-action="advance"
|
|
5
|
+
data-src="<%= pgbus_workflows_path_with(@query, page: (@page if @page > 1), frame: "list") %>">
|
|
6
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
7
|
+
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
8
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
9
|
+
<tr>
|
|
10
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.headers.workflow") %></th>
|
|
11
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.headers.subject") %></th>
|
|
12
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.headers.status") %></th>
|
|
13
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.headers.current_step") %></th>
|
|
14
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.headers.started") %></th>
|
|
15
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.headers.last_activity") %></th>
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
19
|
+
<% @workflows.each do |row| %>
|
|
20
|
+
<% result = pgbus_workflow_state(row) %>
|
|
21
|
+
<tr data-workflow-id="<%= row[:workflow_id] %>" data-status="<%= row[:status] %>" class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
22
|
+
<td data-label="<%= t("pgbus.workflows.headers.workflow") %>" class="px-4 py-3 text-sm">
|
|
23
|
+
<div><%= pgbus_link_to row[:workflow_class], pgbus.workflow_path(row[:workflow_id]), data: { turbo_frame: "_top" } %></div>
|
|
24
|
+
<div class="font-mono text-xs text-gray-500 dark:text-gray-400"><%= row[:workflow_id][0, 8] %></div>
|
|
25
|
+
</td>
|
|
26
|
+
<td data-label="<%= t("pgbus.workflows.headers.subject") %>" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">
|
|
27
|
+
<%= pgbus_workflow_subject(row) %>
|
|
28
|
+
</td>
|
|
29
|
+
<td data-label="<%= t("pgbus.workflows.headers.status") %>" class="px-4 py-3 max-w-md">
|
|
30
|
+
<%= pgbus_workflow_badge(result) %>
|
|
31
|
+
<p class="mt-1 text-xs text-gray-600 dark:text-gray-300"><%= pgbus_workflow_next(row, result) %></p>
|
|
32
|
+
</td>
|
|
33
|
+
<td data-label="<%= t("pgbus.workflows.headers.current_step") %>" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">
|
|
34
|
+
<code class="font-mono"><%= row[:current_step] || Pgbus::Web::TimeFormat::NONE %></code>
|
|
35
|
+
<% if row[:current_attempt].to_i > 1 %>
|
|
36
|
+
<span class="block text-xs text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.attempt", attempt: row[:current_attempt]) %></span>
|
|
37
|
+
<% end %>
|
|
38
|
+
</td>
|
|
39
|
+
<td data-label="<%= t("pgbus.workflows.headers.started") %>" class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300">
|
|
40
|
+
<%= pgbus_time(row[:created_at]) %>
|
|
41
|
+
</td>
|
|
42
|
+
<td data-label="<%= t("pgbus.workflows.headers.last_activity") %>" class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300">
|
|
43
|
+
<%= pgbus_time(row[:updated_at]) %>
|
|
44
|
+
</td>
|
|
45
|
+
</tr>
|
|
46
|
+
<% end %>
|
|
47
|
+
<% if @workflows.empty? %>
|
|
48
|
+
<%= render "pgbus/shared/empty_row", colspan: 6,
|
|
49
|
+
message: safe_join([content_tag(:p, t("pgbus.workflows.index.empty_title"), class: "text-lg font-medium"),
|
|
50
|
+
content_tag(:p, t("pgbus.workflows.index.empty_hint"), class: "mt-1")]) %>
|
|
51
|
+
<% end %>
|
|
52
|
+
</tbody>
|
|
53
|
+
</table>
|
|
54
|
+
<%= render "pgbus/shared/pager", page: @page, per_page: @per_page, shown: @workflows.size,
|
|
55
|
+
total_count: @workflows_count.total,
|
|
56
|
+
has_more: @workflows_count.capped? && @workflows.size == @per_page,
|
|
57
|
+
page_path: ->(n) { pgbus_workflows_path_with(@query, page: n) } %>
|
|
58
|
+
</div>
|
|
59
|
+
</turbo-frame>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<%# The needs-attention view (issue #542): every ongoing run with a stuck
|
|
2
|
+
reason, grouped by (class, step, reason, error class). Each group has bulk
|
|
3
|
+
Resume and Cancel (as its runs' status allows) with an optional reason,
|
|
4
|
+
and a link to the filtered list. Not auto-refreshed: a typed reason stays. %>
|
|
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.workflows.attention.back"), pgbus.workflows_path %></div>
|
|
8
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.workflows.attention.title") %></h1>
|
|
9
|
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.attention.description") %></p>
|
|
10
|
+
<% if @workflow_class %>
|
|
11
|
+
<p class="mt-2 flex flex-wrap items-center gap-2 text-sm text-gray-700 dark:text-gray-300" data-testid="attention-class-filter">
|
|
12
|
+
<span><%= t("pgbus.workflows.attention.only_class_html", workflow: tag.code(@workflow_class, class: "font-mono")) %></span>
|
|
13
|
+
<%= pgbus_link_to t("pgbus.workflows.attention.show_all"), pgbus.attention_workflows_path %>
|
|
14
|
+
</p>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
20
|
+
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
21
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
22
|
+
<tr>
|
|
23
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.attention.headers.workflow") %></th>
|
|
24
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.attention.headers.reason") %></th>
|
|
25
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.attention.headers.runs") %></th>
|
|
26
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.attention.headers.seen") %></th>
|
|
27
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.workflows.attention.headers.actions") %></th>
|
|
28
|
+
</tr>
|
|
29
|
+
</thead>
|
|
30
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
31
|
+
<% @groups.each_with_index do |group, index| %>
|
|
32
|
+
<% actions = group[:available_actions] & %i[resume cancel] %>
|
|
33
|
+
<% list_query = { status: group[:status], workflow_class: group[:workflow_class], step: group[:step_name], error_class: group[:error_class] }.compact %>
|
|
34
|
+
<tr data-reason="<%= group[:reason] %>" class="align-top hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
35
|
+
<td data-label="<%= t("pgbus.workflows.attention.headers.workflow") %>" class="px-4 py-3 text-sm text-gray-900 dark:text-white">
|
|
36
|
+
<span class="block font-medium"><%= group[:workflow_class] %></span>
|
|
37
|
+
<span class="block text-xs text-gray-600 dark:text-gray-300"><%= t("pgbus.workflows.attention.at_step_html", step: tag.code(group[:step_name], class: "font-mono")) %></span>
|
|
38
|
+
</td>
|
|
39
|
+
<td data-label="<%= t("pgbus.workflows.attention.headers.reason") %>" class="px-4 py-3 text-sm max-w-md">
|
|
40
|
+
<span class="block text-gray-900 dark:text-white"><%= t("pgbus.workflows.attention.reasons.#{group[:reason]}") %></span>
|
|
41
|
+
<% if group[:error_class] %>
|
|
42
|
+
<span class="mt-1 block font-mono text-xs text-red-700 dark:text-red-300"><%= group[:error_class] %></span>
|
|
43
|
+
<% end %>
|
|
44
|
+
<% if group[:error_message].present? %>
|
|
45
|
+
<span class="block text-xs text-gray-700 dark:text-gray-300 break-words"><%= group[:error_message] %></span>
|
|
46
|
+
<% end %>
|
|
47
|
+
</td>
|
|
48
|
+
<td data-label="<%= t("pgbus.workflows.attention.headers.runs") %>" class="px-4 py-3 text-right font-mono text-sm text-gray-900 dark:text-white"><%= number_with_delimiter(group[:count]) %></td>
|
|
49
|
+
<td data-label="<%= t("pgbus.workflows.attention.headers.seen") %>" class="px-4 py-3 text-xs text-gray-600 dark:text-gray-300">
|
|
50
|
+
<span class="block"><%= t("pgbus.workflows.attention.first_seen_html", time: pgbus_time(group[:first_seen_at])) %></span>
|
|
51
|
+
<span class="block"><%= t("pgbus.workflows.attention.last_seen_html", time: pgbus_time(group[:last_seen_at])) %></span>
|
|
52
|
+
</td>
|
|
53
|
+
<td data-label="<%= t("pgbus.workflows.attention.headers.actions") %>" class="px-4 py-3 text-sm">
|
|
54
|
+
<% if actions.any? %>
|
|
55
|
+
<form action="<%= pgbus.bulk_cancel_workflows_path %>" method="post" data-turbo-frame="_top" class="space-y-2">
|
|
56
|
+
<%= hidden_field_tag :authenticity_token, form_authenticity_token, id: nil %>
|
|
57
|
+
<% if @workflow_class %>
|
|
58
|
+
<input type="hidden" name="workflow_class" value="<%= @workflow_class %>">
|
|
59
|
+
<% end %>
|
|
60
|
+
<% Pgbus::WorkflowsController::GROUP_FIELDS.each do |field| %>
|
|
61
|
+
<input type="hidden" name="group[<%= field %>]" value="<%= group[field] %>">
|
|
62
|
+
<% end %>
|
|
63
|
+
<label for="attention-reason-<%= index %>" class="block text-xs font-medium text-gray-700 dark:text-gray-300"><%= t("pgbus.workflows.show.reason_label") %></label>
|
|
64
|
+
<input type="text" autocomplete="off" id="attention-reason-<%= index %>" name="reason" maxlength="1000" class="<%= Pgbus::WorkflowsHelper::INPUT_CSS %>">
|
|
65
|
+
<div class="flex flex-wrap gap-2">
|
|
66
|
+
<% if actions.include?(:resume) %>
|
|
67
|
+
<%= pgbus_button_tag t("pgbus.workflows.attention.resume_all", count: group[:count]), variant: :success, size: :sm,
|
|
68
|
+
formaction: pgbus.bulk_resume_workflows_path, confirm: pgbus_workflow_bulk_confirm(:resume, group) %>
|
|
69
|
+
<% end %>
|
|
70
|
+
<% if actions.include?(:cancel) %>
|
|
71
|
+
<%= pgbus_button_tag t("pgbus.workflows.attention.cancel_all", count: group[:count]), variant: :danger, size: :sm,
|
|
72
|
+
formaction: pgbus.bulk_cancel_workflows_path, confirm: pgbus_workflow_bulk_confirm(:cancel, group) %>
|
|
73
|
+
<% end %>
|
|
74
|
+
</div>
|
|
75
|
+
</form>
|
|
76
|
+
<% end %>
|
|
77
|
+
<div class="mt-2"><%= pgbus_link_to t("pgbus.workflows.attention.show_runs"), pgbus.workflows_path(list_query), size: :sm %></div>
|
|
78
|
+
</td>
|
|
79
|
+
</tr>
|
|
80
|
+
<% end %>
|
|
81
|
+
<% if @groups.empty? %>
|
|
82
|
+
<%= render "pgbus/shared/empty_row", colspan: 5,
|
|
83
|
+
message: safe_join([content_tag(:p, t("pgbus.workflows.attention.empty_title"), class: "text-lg font-medium"),
|
|
84
|
+
content_tag(:p, t("pgbus.workflows.attention.empty_hint"), class: "mt-1")]) %>
|
|
85
|
+
<% end %>
|
|
86
|
+
</tbody>
|
|
87
|
+
</table>
|
|
88
|
+
</div>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<%# The numbers per app revision (issue #549): the last five revisions that
|
|
2
|
+
finished an attempt in the window, newest first, each with its totals and
|
|
3
|
+
its steps. A failure rate more than five points above the same row on the
|
|
4
|
+
revision before says so. Locals: definition. %>
|
|
5
|
+
<% revisions = definition[:by_revision] %>
|
|
6
|
+
<section class="mb-8" aria-labelledby="workflow-by-revision-title">
|
|
7
|
+
<h2 id="workflow-by-revision-title" class="text-lg font-medium text-gray-900 dark:text-white"><%= t("pgbus.workflow_insights.show.by_revision.title") %></h2>
|
|
8
|
+
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.by_revision.caption") %></p>
|
|
9
|
+
<div class="mt-3 rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" data-testid="workflow-by-revision">
|
|
10
|
+
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
11
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
12
|
+
<tr>
|
|
13
|
+
<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.by_revision.headers.revision") %></th>
|
|
14
|
+
<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.by_revision.headers.step") %></th>
|
|
15
|
+
<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.by_revision.headers.attempts") %></th>
|
|
16
|
+
<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.by_revision.headers.failed") %></th>
|
|
17
|
+
<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.by_revision.headers.reattempted") %></th>
|
|
18
|
+
<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.by_revision.headers.skipped") %></th>
|
|
19
|
+
<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.by_revision.headers.run_time") %></th>
|
|
20
|
+
</tr>
|
|
21
|
+
</thead>
|
|
22
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
23
|
+
<% revisions.each_with_index do |revision, index| %>
|
|
24
|
+
<% [nil, *revision[:steps]].each do |step| %>
|
|
25
|
+
<% row = step || revision %>
|
|
26
|
+
<% worse = pgbus_insight_worse(revisions, index, step) %>
|
|
27
|
+
<tr data-revision="<%= revision[:revision] %>" data-step="<%= step ? step[:name] : "" %>" class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
28
|
+
<td data-label="<%= t("pgbus.workflow_insights.show.by_revision.headers.revision") %>" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">
|
|
29
|
+
<code class="font-mono text-gray-900 dark:text-white"><%= revision[:revision] %></code>
|
|
30
|
+
<% unless step %>
|
|
31
|
+
<span class="block text-xs text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.by_revision.seen_html", from: pgbus_time(revision[:first_seen_at]), to: pgbus_time(revision[:last_seen_at])) %></span>
|
|
32
|
+
<% end %>
|
|
33
|
+
</td>
|
|
34
|
+
<td data-label="<%= t("pgbus.workflow_insights.show.by_revision.headers.step") %>" class="px-4 py-3 text-sm <%= step ? 'font-mono text-gray-700 dark:text-gray-300' : 'font-medium text-gray-900 dark:text-white' %>"><%= step ? step[:name] : t("pgbus.workflow_insights.show.by_revision.all_steps") %></td>
|
|
35
|
+
<td data-label="<%= t("pgbus.workflow_insights.show.by_revision.headers.attempts") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(row[:attempts]) %></td>
|
|
36
|
+
<td data-label="<%= t("pgbus.workflow_insights.show.by_revision.headers.failed") %>" class="px-4 py-3 text-sm text-right font-mono <%= worse ? 'text-red-700 dark:text-red-300' : 'text-gray-700 dark:text-gray-300' %>">
|
|
37
|
+
<%= pgbus_insight_rate(row[:rates][:failed]) %>
|
|
38
|
+
<% if worse %><span class="block text-xs"><%= worse %></span><% end %>
|
|
39
|
+
</td>
|
|
40
|
+
<td data-label="<%= t("pgbus.workflow_insights.show.by_revision.headers.reattempted") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_insight_rate(row[:rates][:reattempted]) %></td>
|
|
41
|
+
<td data-label="<%= t("pgbus.workflow_insights.show.by_revision.headers.skipped") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_insight_rate(row[:rates][:skipped]) %></td>
|
|
42
|
+
<td data-label="<%= t("pgbus.workflow_insights.show.by_revision.headers.run_time") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_insight_percentiles(row[:run_ms], %i[p50 p95]) %></td>
|
|
43
|
+
</tr>
|
|
44
|
+
<% end %>
|
|
45
|
+
<% end %>
|
|
46
|
+
<% if revisions.empty? %>
|
|
47
|
+
<%= render "pgbus/shared/empty_row", colspan: 7, message: t("pgbus.workflow_insights.show.by_revision.empty") %>
|
|
48
|
+
<% end %>
|
|
49
|
+
</tbody>
|
|
50
|
+
</table>
|
|
51
|
+
</div>
|
|
52
|
+
</section>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<%# The duration chart's data table (issue #549): the finished runs per
|
|
2
|
+
end-to-end duration range. Locals: definition. %>
|
|
3
|
+
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
4
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
5
|
+
<tr>
|
|
6
|
+
<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.durations.headers.range") %></th>
|
|
7
|
+
<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.durations.headers.runs") %></th>
|
|
8
|
+
</tr>
|
|
9
|
+
</thead>
|
|
10
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
11
|
+
<% definition[:duration_histogram].each do |bin| %>
|
|
12
|
+
<tr>
|
|
13
|
+
<td data-label="<%= t("pgbus.workflow_insights.show.charts.durations.headers.range") %>" class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300"><%= pgbus_insight_bin_label(bin) %></td>
|
|
14
|
+
<td data-label="<%= t("pgbus.workflow_insights.show.charts.durations.headers.runs") %>" class="px-4 py-2 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(bin[:count]) %></td>
|
|
15
|
+
</tr>
|
|
16
|
+
<% end %>
|
|
17
|
+
</tbody>
|
|
18
|
+
</table>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<%# The funnel (issue #549): of the runs started in the window, how many
|
|
2
|
+
reached each step (have a row for it), and where each run is now, by
|
|
3
|
+
status, each count linked to those runs. Locals: definition. %>
|
|
4
|
+
<% name = definition[:workflow_class] %>
|
|
5
|
+
<% since = definition[:window][:since] %>
|
|
6
|
+
<% started = definition[:started] %>
|
|
7
|
+
<section class="mb-8" aria-labelledby="workflow-funnel-title">
|
|
8
|
+
<h2 id="workflow-funnel-title" class="text-lg font-medium text-gray-900 dark:text-white"><%= t("pgbus.workflow_insights.show.funnel.title") %></h2>
|
|
9
|
+
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300"><%= t("pgbus.workflow_insights.show.funnel.caption", count: started) %></p>
|
|
10
|
+
<div class="mt-3 rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" data-testid="workflow-funnel">
|
|
11
|
+
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
12
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
13
|
+
<tr>
|
|
14
|
+
<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.funnel.headers.step") %></th>
|
|
15
|
+
<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.funnel.headers.reached") %></th>
|
|
16
|
+
<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.funnel.headers.share") %></th>
|
|
17
|
+
<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.funnel.headers.now_here") %></th>
|
|
18
|
+
</tr>
|
|
19
|
+
</thead>
|
|
20
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
21
|
+
<% if started.positive? %>
|
|
22
|
+
<% definition[:funnel].each do |step| %>
|
|
23
|
+
<% here = Pgbus::WorkflowInsightsHelper::STOPPED_STATUSES.select { |status| step[:stopped][status.to_sym].positive? } %>
|
|
24
|
+
<tr data-step="<%= step[:name] %>" class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
25
|
+
<td data-label="<%= t("pgbus.workflow_insights.show.funnel.headers.step") %>" class="px-4 py-3 text-sm font-mono text-gray-900 dark:text-white"><%= step[:name] %></td>
|
|
26
|
+
<td data-label="<%= t("pgbus.workflow_insights.show.funnel.headers.reached") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(step[:reached]) %></td>
|
|
27
|
+
<td data-label="<%= t("pgbus.workflow_insights.show.funnel.headers.share") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_percent(step[:reached].to_f / started) %></td>
|
|
28
|
+
<td data-label="<%= t("pgbus.workflow_insights.show.funnel.headers.now_here") %>" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">
|
|
29
|
+
<% if here.empty? %>
|
|
30
|
+
<%= Pgbus::Web::TimeFormat::NONE %>
|
|
31
|
+
<% else %>
|
|
32
|
+
<ul class="flex flex-wrap gap-x-3">
|
|
33
|
+
<% here.each do |status| %>
|
|
34
|
+
<li><%= pgbus_link_to t("pgbus.workflow_insights.show.funnel.here.#{status}", count: step[:stopped][status.to_sym]),
|
|
35
|
+
pgbus_insight_runs_path(status: status, workflow_class: name, step: step[:name], created_from: since, created_to: @window.now),
|
|
36
|
+
data: { turbo_frame: "_top" } %></li>
|
|
37
|
+
<% end %>
|
|
38
|
+
</ul>
|
|
39
|
+
<% end %>
|
|
40
|
+
</td>
|
|
41
|
+
</tr>
|
|
42
|
+
<% end %>
|
|
43
|
+
<% else %>
|
|
44
|
+
<%= render "pgbus/shared/empty_row", colspan: 4, message: t("pgbus.workflow_insights.show.funnel.empty") %>
|
|
45
|
+
<% end %>
|
|
46
|
+
</tbody>
|
|
47
|
+
</table>
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|