solid_stack_web 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +22 -2
  3. data/app/assets/stylesheets/solid_stack_web/_02_layout.css +8 -0
  4. data/app/assets/stylesheets/solid_stack_web/_04_table.css +1 -0
  5. data/app/assets/stylesheets/solid_stack_web/_07_dashboard.css +13 -0
  6. data/app/assets/stylesheets/solid_stack_web/_08_filters.css +59 -0
  7. data/app/assets/stylesheets/solid_stack_web/_09_detail.css +85 -0
  8. data/app/controllers/solid_stack_web/application_controller.rb +5 -1
  9. data/app/controllers/solid_stack_web/failed_jobs/arguments_controller.rb +17 -0
  10. data/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb +28 -0
  11. data/app/controllers/solid_stack_web/failed_jobs_controller.rb +35 -4
  12. data/app/controllers/solid_stack_web/history_controller.rb +42 -0
  13. data/app/controllers/solid_stack_web/jobs/selections_controller.rb +24 -0
  14. data/app/controllers/solid_stack_web/jobs_controller.rb +60 -21
  15. data/app/controllers/solid_stack_web/queues/pauses_controller.rb +13 -0
  16. data/app/controllers/solid_stack_web/queues_controller.rb +9 -8
  17. data/app/controllers/solid_stack_web/recurring_tasks/runs_controller.rb +18 -0
  18. data/app/controllers/solid_stack_web/recurring_tasks_controller.rb +7 -0
  19. data/app/controllers/solid_stack_web/scheduled_jobs_controller.rb +52 -0
  20. data/app/helpers/solid_stack_web/application_helper.rb +8 -0
  21. data/app/javascript/solid_stack_web/application.js +6 -0
  22. data/app/javascript/solid_stack_web/selection_controller.js +42 -0
  23. data/app/models/solid_stack_web/job.rb +21 -0
  24. data/app/views/layouts/solid_stack_web/application.html.erb +5 -0
  25. data/app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb +9 -0
  26. data/app/views/solid_stack_web/failed_jobs/index.html.erb +61 -30
  27. data/app/views/solid_stack_web/failed_jobs/show.html.erb +58 -0
  28. data/app/views/solid_stack_web/history/index.html.erb +73 -0
  29. data/app/views/solid_stack_web/jobs/destroy.turbo_stream.erb +2 -2
  30. data/app/views/solid_stack_web/jobs/index.html.erb +138 -34
  31. data/app/views/solid_stack_web/jobs/show.html.erb +57 -0
  32. data/app/views/solid_stack_web/queues/index.html.erb +4 -4
  33. data/app/views/solid_stack_web/queues/show.html.erb +67 -0
  34. data/app/views/solid_stack_web/recurring_tasks/index.html.erb +67 -0
  35. data/app/views/solid_stack_web/scheduled_jobs/update.turbo_stream.erb +9 -0
  36. data/config/importmap.rb +2 -0
  37. data/config/routes.rb +23 -7
  38. data/lib/solid_stack_web/engine.rb +15 -0
  39. data/lib/solid_stack_web/version.rb +1 -1
  40. metadata +64 -1
@@ -0,0 +1,52 @@
1
+ module SolidStackWeb
2
+ class ScheduledJobsController < ApplicationController
3
+ def create
4
+ @period = params[:period].presence_in(PERIOD_DURATIONS.keys)
5
+ job_ids = scheduled_scope.pluck("solid_queue_jobs.id")
6
+ SolidQueue::ScheduledExecution.where(job_id: job_ids).update_all(scheduled_at: 1.second.ago)
7
+ SolidQueue::Job.where(id: job_ids).update_all(scheduled_at: 1.second.ago)
8
+ redirect_to jobs_path(status: "scheduled", period: @period),
9
+ notice: "#{job_ids.size} #{"job".pluralize(job_ids.size)} scheduled to run immediately."
10
+ rescue => e
11
+ redirect_to jobs_path(status: "scheduled", period: @period),
12
+ alert: "Could not run jobs: #{e.message}"
13
+ end
14
+
15
+ def update
16
+ @execution = SolidQueue::ScheduledExecution.find(params[:id])
17
+ @period = params[:period].presence_in(PERIOD_DURATIONS.keys)
18
+ @run_now = params[:offset] == "now"
19
+ new_time = resolve_new_time(@execution, params[:offset])
20
+
21
+ @execution.update!(scheduled_at: new_time)
22
+ @execution.job.update!(scheduled_at: new_time)
23
+
24
+ respond_to do |format|
25
+ format.turbo_stream
26
+ format.html do
27
+ notice = @run_now ? "Job scheduled to run immediately." : "Job rescheduled by +#{params[:offset]}."
28
+ redirect_to jobs_path(status: "scheduled", period: @period), notice: notice
29
+ end
30
+ end
31
+ rescue ArgumentError => e
32
+ redirect_to jobs_path(status: "scheduled"), alert: e.message
33
+ rescue => e
34
+ redirect_to jobs_path(status: "scheduled"), alert: "Could not reschedule job: #{e.message}"
35
+ end
36
+
37
+ private
38
+
39
+ def scheduled_scope
40
+ scope = SolidQueue::ScheduledExecution.joins(:job)
41
+ scope = scope.where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
42
+ scope
43
+ end
44
+
45
+ def resolve_new_time(execution, offset)
46
+ return 1.second.ago if offset == "now"
47
+ raise ArgumentError, "Invalid offset." unless PERIOD_DURATIONS.key?(offset)
48
+
49
+ execution.scheduled_at + PERIOD_DURATIONS[offset]
50
+ end
51
+ end
52
+ end
@@ -1,5 +1,13 @@
1
1
  module SolidStackWeb
2
2
  module ApplicationHelper
3
+ def format_duration(seconds)
4
+ s = seconds.to_i
5
+ return "#{s}s" if s < 60
6
+ return "#{s / 60}m #{s % 60}s" if s < 3600
7
+
8
+ "#{s / 3600}h #{(s % 3600) / 60}m"
9
+ end
10
+
3
11
  def inline_styles
4
12
  dir = SolidStackWeb::Engine.root.join("app/assets/stylesheets/solid_stack_web")
5
13
  css = dir.glob("_*.css").sort.map(&:read).join("\n")
@@ -0,0 +1,6 @@
1
+ import "@hotwired/turbo"
2
+ import { Application } from "@hotwired/stimulus"
3
+ import SelectionController from "solid_stack_web/selection_controller"
4
+
5
+ const application = Application.start()
6
+ application.register("selection", SelectionController)
@@ -0,0 +1,42 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["checkbox", "selectAll", "bar", "count"]
5
+
6
+ toggle() {
7
+ this._update()
8
+ }
9
+
10
+ selectAll({ target }) {
11
+ this.checkboxTargets.forEach(cb => cb.checked = target.checked)
12
+ this._update()
13
+ }
14
+
15
+ submit({ params: { formId } }) {
16
+ const form = document.getElementById(formId)
17
+ if (!form) return
18
+ form.querySelectorAll("[data-injected-id]").forEach(el => el.remove())
19
+ this.checkboxTargets
20
+ .filter(cb => cb.checked)
21
+ .forEach(cb => {
22
+ const input = document.createElement("input")
23
+ input.type = "hidden"
24
+ input.name = "job_ids[]"
25
+ input.value = cb.value
26
+ input.dataset.injectedId = true
27
+ form.appendChild(input)
28
+ })
29
+ form.requestSubmit()
30
+ }
31
+
32
+ _update() {
33
+ const checked = this.checkboxTargets.filter(cb => cb.checked).length
34
+ const total = this.checkboxTargets.length
35
+ if (this.hasBarTarget) this.barTarget.style.display = checked > 0 ? "" : "none"
36
+ if (this.hasCountTarget) this.countTarget.textContent = checked
37
+ if (this.hasSelectAllTarget) {
38
+ this.selectAllTarget.indeterminate = checked > 0 && checked < total
39
+ this.selectAllTarget.checked = total > 0 && checked === total
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,21 @@
1
+ module SolidStackWeb
2
+ class Job
3
+ STATUSES = %w[ready scheduled claimed blocked].freeze
4
+
5
+ EXECUTION_MODELS = {
6
+ "ready" => SolidQueue::ReadyExecution,
7
+ "scheduled" => SolidQueue::ScheduledExecution,
8
+ "claimed" => SolidQueue::ClaimedExecution,
9
+ "blocked" => SolidQueue::BlockedExecution
10
+ }.freeze
11
+
12
+ DISCARDABLE = %w[ready scheduled blocked].freeze
13
+
14
+ TAB_LABELS = {
15
+ "ready" => "Ready",
16
+ "scheduled" => "Scheduled",
17
+ "claimed" => "Running",
18
+ "blocked" => "Blocked"
19
+ }.freeze
20
+ end
21
+ end
@@ -7,6 +7,7 @@
7
7
  <%= csrf_meta_tags %>
8
8
  <%= csp_meta_tag %>
9
9
  <%= inline_styles %>
10
+ <%= javascript_importmap_tags "solid_stack_web" %>
10
11
  </head>
11
12
  <body>
12
13
  <header class="sqw-header">
@@ -32,6 +33,10 @@
32
33
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "failed_jobs"}" %>
33
34
  <%= link_to "Queues", queues_path,
34
35
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "queues"}" %>
36
+ <%= link_to "Recurring", recurring_tasks_path,
37
+ class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "recurring_tasks"}" %>
38
+ <%= link_to "History", history_path,
39
+ class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "history"}" %>
35
40
  <%= link_to "Processes", processes_path,
36
41
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "processes"}" %>
37
42
  </div>
@@ -0,0 +1,9 @@
1
+ <% if @executions_remain %>
2
+ <%= turbo_stream.remove "execution_#{@execution.id}" %>
3
+ <% else %>
4
+ <%= turbo_stream.replace "sqw-jobs-table" do %>
5
+ <div class="sqw-empty">
6
+ <p>No failed jobs.</p>
7
+ </div>
8
+ <% end %>
9
+ <% end %>
@@ -1,41 +1,72 @@
1
- <div class="sqw-page-header">
1
+ <div class="sqw-page-header sqw-page-header--split">
2
2
  <h1 class="sqw-page-title">Failed Jobs</h1>
3
+ <div class="sqw-header-actions">
4
+ <%= link_to "Export CSV", failed_jobs_path(format: :csv),
5
+ class: "sqw-btn sqw-btn--muted sqw-btn--sm", data: { turbo: false } %>
6
+ </div>
3
7
  </div>
4
8
 
5
9
  <div id="sqw-jobs-table">
6
10
  <% if @executions.any? %>
7
- <table class="sqw-table">
8
- <thead>
9
- <tr>
10
- <th>Job Class</th>
11
- <th>Queue</th>
12
- <th>Error</th>
13
- <th>Failed At</th>
14
- <th></th>
15
- </tr>
16
- </thead>
17
- <tbody>
18
- <% @executions.each do |execution| %>
19
- <tr id="execution_<%= execution.id %>">
20
- <td class="sqw-monospace"><%= execution.job.class_name %></td>
21
- <td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
22
- <td class="sqw-muted sqw-truncate"><%= execution.error&.lines&.first&.strip %></td>
23
- <td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
24
- <td class="sqw-actions">
25
- <%= button_to "Retry", retry_failed_job_path(execution),
26
- method: :post, class: "sqw-btn sqw-btn--sm" %>
27
- <%= button_to "Discard", failed_job_path(execution),
28
- method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
29
- data: { turbo_confirm: "Discard this job?" } %>
30
- </td>
11
+ <div data-controller="selection">
12
+ <%= form_with url: failed_job_selection_path, method: :post, id: "retry-selection-form" do |f| %>
13
+ <% end %>
14
+
15
+ <%= form_with url: failed_job_selection_path, method: :delete, id: "discard-selection-form",
16
+ data: { turbo_confirm: "Discard selected failed jobs? This cannot be undone." } do |f| %>
17
+ <% end %>
18
+
19
+ <div class="sqw-selection-bar" data-selection-target="bar" style="display: none;">
20
+ <span class="sqw-muted"><span data-selection-target="count">0</span> selected</span>
21
+ <button type="button" class="sqw-btn sqw-btn--sm"
22
+ data-action="click->selection#submit"
23
+ data-selection-form-id-param="retry-selection-form">Retry Selected</button>
24
+ <button type="button" class="sqw-btn sqw-btn--danger sqw-btn--sm"
25
+ data-action="click->selection#submit"
26
+ data-selection-form-id-param="discard-selection-form">Discard Selected</button>
27
+ </div>
28
+
29
+ <table class="sqw-table">
30
+ <thead>
31
+ <tr>
32
+ <th><input type="checkbox" class="sqw-checkbox" aria-label="Select all"
33
+ data-selection-target="selectAll"
34
+ data-action="change->selection#selectAll"></th>
35
+ <th>Job Class</th>
36
+ <th>Queue</th>
37
+ <th>Error</th>
38
+ <th>Failed At</th>
39
+ <th></th>
31
40
  </tr>
32
- <% end %>
33
- </tbody>
34
- </table>
35
- <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
41
+ </thead>
42
+ <tbody>
43
+ <% @executions.each do |execution| %>
44
+ <tr id="execution_<%= execution.id %>">
45
+ <td><input type="checkbox" value="<%= execution.id %>"
46
+ class="sqw-checkbox"
47
+ aria-label="Select <%= execution.job.class_name %>"
48
+ data-selection-target="checkbox"
49
+ data-action="change->selection#toggle"></td>
50
+ <td class="sqw-monospace"><%= link_to execution.job.class_name, failed_job_path(execution) %></td>
51
+ <td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
52
+ <td class="sqw-muted sqw-truncate" title="<%= execution.exception_class %>: <%= execution.message %>"><%= execution.exception_class %></td>
53
+ <td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
54
+ <td class="sqw-actions">
55
+ <%= button_to "Retry", retry_failed_job_path(execution),
56
+ method: :post, class: "sqw-btn sqw-btn--sm" %>
57
+ <%= button_to "Discard", failed_job_path(execution),
58
+ method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
59
+ data: { turbo_confirm: "Discard this job?" } %>
60
+ </td>
61
+ </tr>
62
+ <% end %>
63
+ </tbody>
64
+ </table>
65
+ <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
66
+ </div>
36
67
  <% else %>
37
68
  <div class="sqw-empty">
38
69
  <p>No failed jobs.</p>
39
70
  </div>
40
71
  <% end %>
41
- </div>
72
+ </div>
@@ -0,0 +1,58 @@
1
+ <div class="sqw-page-header sqw-page-header--split">
2
+ <div>
3
+ <div class="sqw-breadcrumb">
4
+ <%= link_to "Failed Jobs", failed_jobs_path %> &rsaquo; Detail
5
+ </div>
6
+ <h1 class="sqw-page-title sqw-monospace"><%= @execution.job.class_name %></h1>
7
+ </div>
8
+
9
+ <div class="sqw-detail-actions">
10
+ <%= button_to "Retry", retry_failed_job_path(@execution),
11
+ method: :post, class: "sqw-btn sqw-btn--sm" %>
12
+ <%= button_to "Discard", failed_job_path(@execution),
13
+ method: :delete, class: "sqw-btn sqw-btn--danger",
14
+ data: { turbo_confirm: "Discard this job?" } %>
15
+ </div>
16
+ </div>
17
+
18
+ <div class="sqw-detail-grid">
19
+ <div class="sqw-detail-card sqw-detail-section">
20
+ <h2 class="sqw-section-title">Details</h2>
21
+ <dl class="sqw-dl">
22
+ <dt>Queue</dt>
23
+ <dd class="sqw-monospace"><%= @execution.job.queue_name %></dd>
24
+
25
+ <dt>Priority</dt>
26
+ <dd><%= @execution.job.priority %></dd>
27
+
28
+ <dt>Active Job ID</dt>
29
+ <dd class="sqw-monospace sqw-truncate" title="<%= @execution.job.active_job_id %>"><%= @execution.job.active_job_id.presence || "—" %></dd>
30
+
31
+ <dt>Failed At</dt>
32
+ <dd class="sqw-monospace"><%= @execution.created_at.strftime("%Y-%m-%d %H:%M:%S UTC") %></dd>
33
+
34
+ <dt>Error</dt>
35
+ <dd class="sqw-monospace"><%= @execution.exception_class %></dd>
36
+
37
+ <dt>Message</dt>
38
+ <dd class="sqw-muted"><%= @execution.message %></dd>
39
+ </dl>
40
+ </div>
41
+
42
+ <div class="sqw-detail-card sqw-detail-section">
43
+ <h2 class="sqw-section-title">Backtrace</h2>
44
+ <pre class="sqw-code-block"><%= Array(@execution.backtrace).first(10).join("\n").presence || "—" %></pre>
45
+ </div>
46
+ </div>
47
+
48
+ <div class="sqw-detail-card sqw-detail-section" style="margin-top: 1.5rem;">
49
+ <h2 class="sqw-section-title">Arguments</h2>
50
+ <%= form_with url: failed_job_arguments_path(@execution), method: :patch do |f| %>
51
+ <%= f.text_area :arguments, value: @arguments, rows: 12,
52
+ class: "sqw-code-input", aria: { label: "Job arguments JSON" },
53
+ spellcheck: false %>
54
+ <div style="margin-top: 0.75rem;">
55
+ <%= f.submit "Update &amp; Retry".html_safe, class: "sqw-btn sqw-btn--sm" %>
56
+ </div>
57
+ <% end %>
58
+ </div>
@@ -0,0 +1,73 @@
1
+ <div class="sqw-page-header sqw-page-header--split">
2
+ <h1 class="sqw-page-title">Job History</h1>
3
+ <div class="sqw-header-actions">
4
+ <% if @jobs&.any? %>
5
+ <%= link_to "Export CSV", history_path(format: :csv, queue: @queue, q: @search, period: @period),
6
+ class: "sqw-btn sqw-btn--muted sqw-btn--sm", data: { turbo: false } %>
7
+ <% end %>
8
+ </div>
9
+ </div>
10
+
11
+ <form class="sqw-filters" action="<%= history_path %>" method="get">
12
+ <% if @queue.present? %>
13
+ <input type="hidden" name="queue" value="<%= @queue %>">
14
+ <% end %>
15
+ <input type="hidden" name="period" value="<%= @period %>">
16
+ <input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
17
+ placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class">
18
+ <button type="submit" class="sqw-btn sqw-btn--muted sqw-btn--sm">Search</button>
19
+ <% if @search.present? %>
20
+ <%= link_to "Clear", history_path(queue: @queue, period: @period), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
21
+ <% end %>
22
+ <div class="sqw-period-filter" role="group" aria-label="Time period">
23
+ <%= link_to "All", history_path(queue: @queue, q: @search),
24
+ class: "sqw-period-btn #{"sqw-period-btn--active" if @period.nil?}" %>
25
+ <%= link_to "1h", history_path(queue: @queue, q: @search, period: "1h"),
26
+ class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "1h"}" %>
27
+ <%= link_to "24h", history_path(queue: @queue, q: @search, period: "24h"),
28
+ class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "24h"}" %>
29
+ <%= link_to "7d", history_path(queue: @queue, q: @search, period: "7d"),
30
+ class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "7d"}" %>
31
+ </div>
32
+ </form>
33
+
34
+ <% if @queue.present? %>
35
+ <p class="sqw-muted" style="font-size: 13px; margin-bottom: 0.75rem;">
36
+ Filtering by queue: <strong><%= @queue %></strong> &mdash;
37
+ <%= link_to "Clear filter", history_path(q: @search, period: @period) %>
38
+ </p>
39
+ <% end %>
40
+
41
+ <% if @jobs.any? %>
42
+ <div class="sqw-detail-card">
43
+ <table class="sqw-table">
44
+ <thead>
45
+ <tr>
46
+ <th>Job Class</th>
47
+ <th>Queue</th>
48
+ <th>Duration</th>
49
+ <th>Finished At</th>
50
+ </tr>
51
+ </thead>
52
+ <tbody>
53
+ <% @jobs.each do |job| %>
54
+ <tr>
55
+ <td class="sqw-monospace"><%= job.class_name %></td>
56
+ <td>
57
+ <%= link_to job.queue_name,
58
+ history_path(queue: job.queue_name, q: @search, period: @period),
59
+ class: "sqw-badge sqw-badge--queue" %>
60
+ </td>
61
+ <td class="sqw-monospace"><%= format_duration(job.finished_at - job.created_at) %></td>
62
+ <td class="sqw-muted"><%= job.finished_at.strftime("%b %d %H:%M:%S") %></td>
63
+ </tr>
64
+ <% end %>
65
+ </tbody>
66
+ </table>
67
+ <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
68
+ </div>
69
+ <% else %>
70
+ <div class="sqw-empty">
71
+ <p>No finished jobs found.</p>
72
+ </div>
73
+ <% end %>
@@ -1,7 +1,7 @@
1
1
  <% if @executions_remain %>
2
- <%= turbo_stream.remove "execution_#{params[:id]}" %>
2
+ <%= turbo_stream.remove "execution_#{@execution.id}" %>
3
3
  <% else %>
4
4
  <%= turbo_stream.replace "sqw-jobs-table" do %>
5
5
  <%= render "empty" %>
6
6
  <% end %>
7
- <% end %>
7
+ <% end %>
@@ -1,50 +1,154 @@
1
- <div class="sqw-page-header">
1
+ <div class="sqw-page-header sqw-page-header--split">
2
2
  <h1 class="sqw-page-title">Jobs</h1>
3
+ <div class="sqw-header-actions">
4
+ <%= link_to "Export CSV", jobs_path(format: :csv, status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
5
+ class: "sqw-btn sqw-btn--muted sqw-btn--sm", data: { turbo: false } %>
6
+ <% if @status == "scheduled" && @executions&.any? %>
7
+ <%= button_to "Run All Now (#{@pagy.count})",
8
+ run_all_now_scheduled_jobs_path(period: @period),
9
+ method: :post,
10
+ class: "sqw-btn sqw-btn--sm",
11
+ data: { turbo_confirm: "Run all #{@pagy.count} scheduled jobs immediately?",
12
+ turbo_frame: "_top" } %>
13
+ <% end %>
14
+ <% if SolidStackWeb::Job::DISCARDABLE.include?(@status) && @executions&.any? %>
15
+ <%= button_to "Discard All (#{@pagy.count})",
16
+ discard_all_jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
17
+ method: :post,
18
+ class: "sqw-btn sqw-btn--danger sqw-btn--sm",
19
+ data: { turbo_confirm: "Discard all #{@pagy.count} jobs? This cannot be undone.",
20
+ turbo_frame: "_top" } %>
21
+ <% end %>
22
+ </div>
3
23
  </div>
4
24
 
5
25
  <div class="sqw-tabs">
6
- <% [["ready", "Ready"], ["scheduled", "Scheduled"], ["claimed", "Running"], ["blocked", "Blocked"]].each do |status, label| %>
7
- <%= link_to label, jobs_path(status: status),
26
+ <% SolidStackWeb::Job::TAB_LABELS.each do |status, label| %>
27
+ <%= link_to label, jobs_path(status: status, q: @search, queue: @queue, period: @period, priority: @priority),
8
28
  class: "sqw-tab #{"sqw-tab--active" if @status == status}" %>
9
29
  <% end %>
10
30
  </div>
11
31
 
32
+ <turbo-frame id="sqw-jobs-filter" data-turbo-action="advance">
33
+ <form class="sqw-filters" action="<%= jobs_path %>" method="get">
34
+ <%= hidden_field_tag :status, @status %>
35
+ <%= hidden_field_tag :period, @period %>
36
+ <input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
37
+ placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class">
38
+ <% if @queue_options.size > 1 %>
39
+ <select name="queue" class="sqw-select" aria-label="Filter by queue" onchange="this.form.requestSubmit()">
40
+ <option value="">All queues</option>
41
+ <% @queue_options.each do |q| %>
42
+ <option value="<%= q %>" <%= "selected" if @queue == q %>><%= q %></option>
43
+ <% end %>
44
+ </select>
45
+ <% end %>
46
+ <% if @priority_options.size > 1 %>
47
+ <select name="priority" class="sqw-select" aria-label="Filter by priority" onchange="this.form.requestSubmit()">
48
+ <option value="">All priorities</option>
49
+ <% @priority_options.each do |p| %>
50
+ <option value="<%= p %>" <%= "selected" if @priority.to_s == p.to_s %>>Priority <%= p %></option>
51
+ <% end %>
52
+ </select>
53
+ <% end %>
54
+ <button type="submit" class="sqw-btn sqw-btn--muted sqw-btn--sm">Search</button>
55
+ <% if @search.present? || @queue.present? || @priority.present? %>
56
+ <%= link_to "Clear", jobs_path(status: @status, period: @period), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
57
+ <% end %>
58
+ <div class="sqw-period-filter" role="group" aria-label="Time period">
59
+ <%= link_to "All", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority),
60
+ class: "sqw-period-btn #{"sqw-period-btn--active" if @period.nil?}" %>
61
+ <%= link_to "1h", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "1h"),
62
+ class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "1h"}" %>
63
+ <%= link_to "24h", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "24h"),
64
+ class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "24h"}" %>
65
+ <%= link_to "7d", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "7d"),
66
+ class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "7d"}" %>
67
+ </div>
68
+ </form>
69
+
12
70
  <div id="sqw-jobs-table">
13
71
  <% if @executions.any? %>
14
- <table class="sqw-table">
15
- <thead>
16
- <tr>
17
- <th>Job Class</th>
18
- <th>Queue</th>
19
- <th>Priority</th>
20
- <th>Enqueued At</th>
21
- <% if @status == "scheduled" %><th>Scheduled At</th><% end %>
22
- <th></th>
23
- </tr>
24
- </thead>
25
- <tbody>
26
- <% @executions.each do |execution| %>
27
- <tr id="execution_<%= execution.id %>">
28
- <td class="sqw-monospace"><%= execution.job.class_name %></td>
29
- <td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
30
- <td><%= execution.job.priority %></td>
31
- <td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
32
- <% if @status == "scheduled" %>
33
- <td class="sqw-muted"><%= execution.scheduled_at&.strftime("%b %d %H:%M") %></td>
72
+ <div data-controller="<%= "selection" if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>">
73
+ <% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>
74
+ <%= form_with url: job_selection_path, method: :delete, id: "job-selection-form",
75
+ data: { turbo_confirm: "Discard selected jobs? This cannot be undone." } do |f| %>
76
+ <%= f.hidden_field :status, value: @status %>
77
+ <%= f.hidden_field :q, value: @search %>
78
+ <%= f.hidden_field :queue, value: @queue %>
79
+ <%= f.hidden_field :period, value: @period %>
80
+ <%= f.hidden_field :priority, value: @priority %>
81
+ <% end %>
82
+
83
+ <div class="sqw-selection-bar" data-selection-target="bar" style="display: none;">
84
+ <span class="sqw-muted"><span data-selection-target="count">0</span> selected</span>
85
+ <button type="button" class="sqw-btn sqw-btn--danger sqw-btn--sm"
86
+ data-action="click->selection#submit"
87
+ data-selection-form-id-param="job-selection-form">Discard Selected</button>
88
+ </div>
89
+ <% end %>
90
+
91
+ <table class="sqw-table">
92
+ <thead>
93
+ <tr>
94
+ <% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>
95
+ <th><input type="checkbox" class="sqw-checkbox" aria-label="Select all"
96
+ data-selection-target="selectAll"
97
+ data-action="change->selection#selectAll"></th>
34
98
  <% end %>
35
- <td class="sqw-actions">
36
- <% if %w[ready scheduled blocked].include?(@status) %>
37
- <%= button_to "Discard", job_path(execution, status: @status),
38
- method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
39
- data: { turbo_confirm: "Discard this job?" } %>
40
- <% end %>
41
- </td>
99
+ <th>Job Class</th>
100
+ <th>Queue</th>
101
+ <th>Priority</th>
102
+ <th>Enqueued At</th>
103
+ <% if @status == "scheduled" %><th>Scheduled At</th><% end %>
104
+ <th></th>
42
105
  </tr>
43
- <% end %>
44
- </tbody>
45
- </table>
46
- <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
106
+ </thead>
107
+ <tbody>
108
+ <% @executions.each do |execution| %>
109
+ <tr id="execution_<%= execution.id %>">
110
+ <% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>
111
+ <td><input type="checkbox" value="<%= execution.id %>"
112
+ class="sqw-checkbox"
113
+ aria-label="Select <%= execution.job.class_name %>"
114
+ data-selection-target="checkbox"
115
+ data-action="change->selection#toggle"></td>
116
+ <% end %>
117
+ <td class="sqw-monospace"><%= link_to execution.job.class_name, job_path(execution.id, status: @status), data: { turbo_frame: "_top" } %></td>
118
+ <td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
119
+ <td><%= execution.job.priority %></td>
120
+ <td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
121
+ <% if @status == "scheduled" %>
122
+ <td id="scheduled_at_<%= execution.id %>" class="sqw-muted"><%= execution.scheduled_at&.strftime("%b %d %H:%M") %></td>
123
+ <% end %>
124
+ <td class="sqw-actions">
125
+ <% if @status == "scheduled" %>
126
+ <%= button_to "Run Now", scheduled_job_path(execution),
127
+ method: :patch,
128
+ params: { offset: "now", period: @period },
129
+ class: "sqw-btn sqw-btn--sm",
130
+ data: { turbo_confirm: "Run this job immediately?" } %>
131
+ <% %w[1h 24h 7d].each do |offset| %>
132
+ <%= button_to "+#{offset}", scheduled_job_path(execution),
133
+ method: :patch,
134
+ params: { offset: offset, period: @period },
135
+ class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
136
+ <% end %>
137
+ <% end %>
138
+ <% if %w[ready scheduled blocked].include?(@status) %>
139
+ <%= button_to "Discard", job_path(execution, status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
140
+ method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
141
+ data: { turbo_confirm: "Discard this job?" } %>
142
+ <% end %>
143
+ </td>
144
+ </tr>
145
+ <% end %>
146
+ </tbody>
147
+ </table>
148
+ <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
149
+ </div>
47
150
  <% else %>
48
151
  <%= render "empty" %>
49
152
  <% end %>
50
153
  </div>
154
+ </turbo-frame>