mirror of
https://github.com/we-promise/sure.git
synced 2026-05-25 13:34:58 +00:00
* Add blocked count to rule run summary * test(rules): cover rule run blocked counts * fix(rules): derive blocked count from modified rows Blocked rule transactions are the processed rows that were not modified. This keeps the displayed queued / processed / modified / blocked summary aligned when a run has already processed all matching rows but some were skipped by enrichment locks. * fix(rules): count processed rows for rule jobs Synchronous rule actions return the number of rows they modified, but rule-run processed counts should represent the number of matched transactions the job attempted to process. Using queued matches for processed preserves the distinction between processed and modified rows, which lets locked manual edits appear as blocked instead of making processed collapse to modified. This changes RuleJob counter semantics, so it was committed separately from the derived blocked-count display change.
188 lines
8.6 KiB
Plaintext
188 lines
8.6 KiB
Plaintext
<%= content_for :page_title, "Rules" %>
|
|
<%= content_for :page_actions do %>
|
|
<% if @rules.any? %>
|
|
<%= render DS::Menu.new do |menu| %>
|
|
<% menu.with_item(
|
|
variant: "button",
|
|
text: t("rules.clear_ai_cache.button"),
|
|
href: clear_ai_cache_rules_path,
|
|
icon: "refresh-cw",
|
|
method: :post,
|
|
confirm: CustomConfirm.new(
|
|
title: t("rules.clear_ai_cache.confirm_title"),
|
|
body: t("rules.clear_ai_cache.confirm_body"),
|
|
btn_text: t("rules.clear_ai_cache.confirm_button")
|
|
)) %>
|
|
<% menu.with_item(
|
|
variant: "button",
|
|
text: "Delete all rules",
|
|
href: destroy_all_rules_path,
|
|
icon: "trash-2",
|
|
method: :delete,
|
|
confirm: CustomConfirm.for_resource_deletion("all rules", high_severity: true)) %>
|
|
<% end %>
|
|
<%= render DS::Link.new(
|
|
text: t("rules.apply_all.button"),
|
|
variant: "secondary",
|
|
href: confirm_all_rules_path,
|
|
icon: "play",
|
|
frame: :modal
|
|
) %>
|
|
<% end %>
|
|
<%= render DS::Link.new(
|
|
text: "New rule",
|
|
variant: "primary",
|
|
href: new_rule_path(resource_type: "transaction"),
|
|
icon: "plus",
|
|
frame: :modal
|
|
) %>
|
|
<% end %>
|
|
|
|
<% if self_hosted? %>
|
|
<div class="flex items-center gap-2 mb-2 py-4">
|
|
<%= icon("circle-alert", size: "sm") %>
|
|
<p class="text-sm text-secondary">
|
|
AI-enabled rule actions will cost money. Be sure to filter as narrowly as possible to avoid unnecessary costs.
|
|
</p>
|
|
</div>
|
|
<% end %>
|
|
<div class="bg-container rounded-xl shadow-border-xs p-4">
|
|
<% if @rules.any? %>
|
|
<div class="bg-container-inset rounded-xl">
|
|
<div class="flex justify-between px-4 py-2 text-xs uppercase">
|
|
<div class="flex items-center gap-1.5 font-medium text-secondary">
|
|
<p>Rules</p>
|
|
<span class="text-subdued">·</span>
|
|
<p><%= @rules.count %></p>
|
|
</div>
|
|
<div class="flex items-center gap-1">
|
|
<span class="text-secondary">Sort by:</span>
|
|
<%= form_with url: rules_path, method: :get, local: true, class: "flex items-center", data: { controller: "auto-submit-form" } do |form| %>
|
|
<%= form.select :sort_by,
|
|
options_for_select([["Name", "name"], ["Updated At", "updated_at"]], @sort_by),
|
|
{},
|
|
class: "min-w-[120px] bg-transparent rounded border-none cursor-pointer text-primary uppercase text-xs w-auto",
|
|
data: { auto_submit_form_target: "auto", autosubmit_trigger_event: "change" } %>
|
|
<%= form.hidden_field :direction, value: @direction %>
|
|
<% end %>
|
|
<%= render DS::Link.new(
|
|
href: rules_path(direction: @direction == "asc" ? "desc" : "asc", sort_by: @sort_by),
|
|
variant: "icon",
|
|
icon: "arrow-up-down",
|
|
size: :sm,
|
|
title: "Toggle sort direction"
|
|
) %>
|
|
</div>
|
|
</div>
|
|
<div class="p-1">
|
|
<div class="flex flex-col bg-container rounded-lg shadow-border-xs">
|
|
<%= render partial: "rule", collection: @rules, spacer_template: "shared/ruler" %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<div class="flex justify-center items-center py-20">
|
|
<div class="text-center flex flex-col items-center max-w-[500px]">
|
|
<p class="text-sm text-primary font-medium mb-1">No rules yet</p>
|
|
<p class="text-sm text-secondary mb-4">Set up rules to perform actions to your transactions and other data on every account sync.</p>
|
|
<div class="flex items-center gap-2">
|
|
<%= render DS::Link.new(
|
|
text: "New rule",
|
|
variant: "primary",
|
|
href: new_rule_path(resource_type: "transaction"),
|
|
icon: "plus",
|
|
frame: :modal
|
|
) %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
|
|
<!-- Recent Runs Section -->
|
|
<% if @recent_runs.any? %>
|
|
<div class="mt-6 bg-container rounded-xl shadow-border-xs p-4">
|
|
<div class="mb-4">
|
|
<h2 class="text-primary text-lg font-medium mb-1"><%= t("rules.recent_runs.title") %></h2>
|
|
<p class="text-sm text-secondary"><%= t("rules.recent_runs.description") %></p>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full">
|
|
<thead class="bg-surface-default border-b border-primary">
|
|
<tr>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-secondary uppercase tracking-wider">
|
|
<%= t("rules.recent_runs.columns.date_time") %>
|
|
</th>
|
|
<th class="px-4 py-3 text-center text-xs font-medium text-secondary uppercase tracking-wider">
|
|
<%= t("rules.recent_runs.columns.execution_type") %>
|
|
</th>
|
|
<th class="px-4 py-3 text-center text-xs font-medium text-secondary uppercase tracking-wider">
|
|
<%= t("rules.recent_runs.columns.status") %>
|
|
</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-secondary uppercase tracking-wider">
|
|
<%= t("rules.recent_runs.columns.rule_name") %>
|
|
</th>
|
|
<th class="px-4 py-3 text-center text-xs font-medium text-secondary uppercase tracking-wider">
|
|
<div class="flex flex-col leading-tight">
|
|
<div><%= t("rules.recent_runs.columns.transactions_counts.queued") %></div>
|
|
<div><%= t("rules.recent_runs.columns.transactions_counts.processed") %></div>
|
|
<div><%= t("rules.recent_runs.columns.transactions_counts.modified") %></div>
|
|
<div><%= t("rules.recent_runs.columns.transactions_counts.blocked", default: "Blocked") %></div>
|
|
</div>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-100">
|
|
<% @recent_runs.each do |run| %>
|
|
<tr class="<%= "bg-red-50 theme-dark:bg-red-950/30" if run.failed? %>">
|
|
<td class="px-4 py-3 text-sm text-primary whitespace-nowrap">
|
|
<%= run.executed_at.strftime("%b %d, %Y %I:%M %p") %>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-primary text-center">
|
|
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium <%= run.execution_type == "manual" ? "bg-blue-50 text-blue-700 theme-dark:bg-blue-950/30 theme-dark:text-blue-400" : "bg-purple-50 text-purple-700 theme-dark:bg-purple-950/30 theme-dark:text-purple-400" %>">
|
|
<%= t("rules.recent_runs.execution_types.#{run.execution_type}") %>
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-center">
|
|
<div class="flex items-center justify-center gap-2">
|
|
<% if run.pending? %>
|
|
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-yellow-50 text-yellow-700 theme-dark:bg-yellow-950/30 theme-dark:text-yellow-400">
|
|
<%= t("rules.recent_runs.statuses.#{run.status}") %>
|
|
</span>
|
|
<% elsif run.success? %>
|
|
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-green-50 text-green-700 theme-dark:bg-green-950/30 theme-dark:text-green-400">
|
|
<%= t("rules.recent_runs.statuses.#{run.status}") %>
|
|
</span>
|
|
<% else %>
|
|
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-red-50 text-red-700 theme-dark:bg-red-950/30 theme-dark:text-red-400">
|
|
<%= t("rules.recent_runs.statuses.#{run.status}") %>
|
|
</span>
|
|
<% end %>
|
|
<% if run.failed? && run.error_message.present? %>
|
|
<div data-controller="tooltip" data-tooltip-content-value="<%= run.error_message %>">
|
|
<%= icon("info", size: "sm", class: "text-red-500") %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-primary">
|
|
<%= run.rule_name.presence || run.rule&.name.presence || t("rules.recent_runs.unnamed_rule") %>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-primary text-center tabular-nums">
|
|
<%= "#{number_with_delimiter(run.transactions_queued)} / #{number_with_delimiter(run.transactions_processed)} / #{number_with_delimiter(run.transactions_modified)} / #{number_with_delimiter(run.transactions_blocked)}" %>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<% if @pagy.pages > 1 %>
|
|
<div class="mt-4">
|
|
<%= render "shared/pagination", pagy: @pagy %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|