job_harbor 0.1.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 +7 -0
- data/README.md +98 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/solidqueue_dashboard/application.css +1 -0
- data/app/components/job_harbor/application_component.rb +13 -0
- data/app/components/job_harbor/badge_component.rb +26 -0
- data/app/components/job_harbor/chart_component.rb +82 -0
- data/app/components/job_harbor/empty_state_component.rb +41 -0
- data/app/components/job_harbor/failure_rates_component.rb +84 -0
- data/app/components/job_harbor/job_filters_component.rb +92 -0
- data/app/components/job_harbor/job_row_component.rb +106 -0
- data/app/components/job_harbor/nav_link_component.rb +50 -0
- data/app/components/job_harbor/pagination_component.rb +72 -0
- data/app/components/job_harbor/per_page_selector_component.rb +40 -0
- data/app/components/job_harbor/queue_card_component.rb +59 -0
- data/app/components/job_harbor/refresh_selector_component.rb +57 -0
- data/app/components/job_harbor/stat_card_component.rb +77 -0
- data/app/components/job_harbor/theme_toggle_component.rb +48 -0
- data/app/components/job_harbor/worker_card_component.rb +86 -0
- data/app/controllers/job_harbor/application_controller.rb +44 -0
- data/app/controllers/job_harbor/dashboard_controller.rb +17 -0
- data/app/controllers/job_harbor/jobs_controller.rb +151 -0
- data/app/controllers/job_harbor/queues_controller.rb +40 -0
- data/app/controllers/job_harbor/recurring_tasks_controller.rb +35 -0
- data/app/controllers/job_harbor/workers_controller.rb +12 -0
- data/app/helpers/job_harbor/application_helper.rb +4 -0
- data/app/models/job_harbor/chart_data.rb +104 -0
- data/app/models/job_harbor/dashboard_stats.rb +90 -0
- data/app/models/job_harbor/failure_stats.rb +63 -0
- data/app/models/job_harbor/job_presenter.rb +246 -0
- data/app/models/job_harbor/queue_stats.rb +77 -0
- data/app/views/job_harbor/dashboard/index.html.erb +112 -0
- data/app/views/job_harbor/jobs/index.html.erb +100 -0
- data/app/views/job_harbor/jobs/search.html.erb +43 -0
- data/app/views/job_harbor/jobs/show.html.erb +133 -0
- data/app/views/job_harbor/queues/index.html.erb +13 -0
- data/app/views/job_harbor/queues/show.html.erb +88 -0
- data/app/views/job_harbor/recurring_tasks/index.html.erb +36 -0
- data/app/views/job_harbor/recurring_tasks/show.html.erb +97 -0
- data/app/views/job_harbor/workers/index.html.erb +33 -0
- data/app/views/layouts/job_harbor/application.html.erb +1434 -0
- data/config/routes.rb +39 -0
- data/lib/job_harbor/configuration.rb +31 -0
- data/lib/job_harbor/engine.rb +28 -0
- data/lib/job_harbor/version.rb +3 -0
- data/lib/job_harbor.rb +19 -0
- data/lib/tasks/solidqueue_dashboard_tasks.rake +4 -0
- metadata +134 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<% content_for :header_actions do %>
|
|
2
|
+
<%= form_with url: search_path, method: :get, class: "sqd-search-form" do |f| %>
|
|
3
|
+
<%= f.text_field :q, placeholder: "Search jobs...", class: "sqd-input", value: params[:q] %>
|
|
4
|
+
<%= f.submit "Search", class: "sqd-btn sqd-btn-secondary" %>
|
|
5
|
+
<% end %>
|
|
6
|
+
<% end %>
|
|
7
|
+
|
|
8
|
+
<nav class="sqd-tabs">
|
|
9
|
+
<%= link_to jobs_path, class: "sqd-tab #{'active' unless @status}" do %>
|
|
10
|
+
All
|
|
11
|
+
<span class="sqd-tab-count"><%= @counts[:all] %></span>
|
|
12
|
+
<% end %>
|
|
13
|
+
|
|
14
|
+
<%= link_to pending_jobs_path, class: "sqd-tab #{'active' if @status == 'pending'}" do %>
|
|
15
|
+
Pending
|
|
16
|
+
<span class="sqd-tab-count"><%= @counts[:pending] %></span>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<%= link_to scheduled_jobs_path, class: "sqd-tab #{'active' if @status == 'scheduled'}" do %>
|
|
20
|
+
Scheduled
|
|
21
|
+
<span class="sqd-tab-count"><%= @counts[:scheduled] %></span>
|
|
22
|
+
<% end %>
|
|
23
|
+
|
|
24
|
+
<%= link_to in_progress_jobs_path, class: "sqd-tab #{'active' if @status == 'in_progress'}" do %>
|
|
25
|
+
In Progress
|
|
26
|
+
<span class="sqd-tab-count"><%= @counts[:in_progress] %></span>
|
|
27
|
+
<% end %>
|
|
28
|
+
|
|
29
|
+
<%= link_to failed_jobs_path, class: "sqd-tab #{'active' if @status == 'failed'}" do %>
|
|
30
|
+
Failed
|
|
31
|
+
<span class="sqd-tab-count"><%= @counts[:failed] %></span>
|
|
32
|
+
<% end %>
|
|
33
|
+
|
|
34
|
+
<%= link_to blocked_jobs_path, class: "sqd-tab #{'active' if @status == 'blocked'}" do %>
|
|
35
|
+
Blocked
|
|
36
|
+
<span class="sqd-tab-count"><%= @counts[:blocked] %></span>
|
|
37
|
+
<% end %>
|
|
38
|
+
|
|
39
|
+
<%= link_to finished_jobs_path, class: "sqd-tab #{'active' if @status == 'finished'}" do %>
|
|
40
|
+
Finished
|
|
41
|
+
<span class="sqd-tab-count"><%= @counts[:finished] %></span>
|
|
42
|
+
<% end %>
|
|
43
|
+
</nav>
|
|
44
|
+
|
|
45
|
+
<%= render JobHarbor::JobFiltersComponent.new(
|
|
46
|
+
class_names: @filter_data[:class_names],
|
|
47
|
+
queue_names: @filter_data[:queue_names],
|
|
48
|
+
current_class: @class_name,
|
|
49
|
+
current_queue: @queue_name,
|
|
50
|
+
current_path: request.path,
|
|
51
|
+
params: request.query_parameters
|
|
52
|
+
) %>
|
|
53
|
+
|
|
54
|
+
<% if @status == "failed" && @counts[:failed] > 0 %>
|
|
55
|
+
<div class="sqd-actions" style="margin-bottom: 1rem;">
|
|
56
|
+
<%= button_to "Retry All Failed", retry_all_jobs_path(status: "failed"), method: :post, class: "sqd-btn sqd-btn-secondary",
|
|
57
|
+
data: { confirm: "Are you sure you want to retry all failed jobs?" } %>
|
|
58
|
+
<%= button_to "Discard All Failed", discard_all_jobs_path(status: "failed"), method: :delete, class: "sqd-btn sqd-btn-danger",
|
|
59
|
+
data: { confirm: "Are you sure you want to discard all failed jobs? This cannot be undone." } %>
|
|
60
|
+
</div>
|
|
61
|
+
<% end %>
|
|
62
|
+
|
|
63
|
+
<% if @jobs.empty? %>
|
|
64
|
+
<%= render JobHarbor::EmptyStateComponent.new(
|
|
65
|
+
title: "No jobs found",
|
|
66
|
+
description: @status ? "There are no #{@status.humanize.downcase} jobs." : "There are no jobs in the queue.",
|
|
67
|
+
icon: :jobs
|
|
68
|
+
) %>
|
|
69
|
+
<% else %>
|
|
70
|
+
<div class="sqd-card">
|
|
71
|
+
<div class="sqd-table-container">
|
|
72
|
+
<table class="sqd-table">
|
|
73
|
+
<thead>
|
|
74
|
+
<tr>
|
|
75
|
+
<th>ID</th>
|
|
76
|
+
<th>Job Class</th>
|
|
77
|
+
<th>Queue</th>
|
|
78
|
+
<th>Status</th>
|
|
79
|
+
<th>Scheduled At</th>
|
|
80
|
+
<th>Actions</th>
|
|
81
|
+
</tr>
|
|
82
|
+
</thead>
|
|
83
|
+
<tbody>
|
|
84
|
+
<% @jobs.each do |job| %>
|
|
85
|
+
<%= render JobHarbor::JobRowComponent.new(job: job) %>
|
|
86
|
+
<% end %>
|
|
87
|
+
</tbody>
|
|
88
|
+
</table>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="sqd-pagination-row">
|
|
93
|
+
<%= render JobHarbor::PaginationComponent.new(pagy: @pagy) %>
|
|
94
|
+
<%= render JobHarbor::PerPageSelectorComponent.new(
|
|
95
|
+
current_per_page: @per_page,
|
|
96
|
+
current_path: request.path,
|
|
97
|
+
params: request.query_parameters
|
|
98
|
+
) %>
|
|
99
|
+
</div>
|
|
100
|
+
<% end %>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<% content_for :header_actions do %>
|
|
2
|
+
<%= form_with url: search_path, method: :get, class: "sqd-search-form" do |f| %>
|
|
3
|
+
<%= f.text_field :q, placeholder: "Search jobs...", class: "sqd-input", value: params[:q] %>
|
|
4
|
+
<%= f.submit "Search", class: "sqd-btn sqd-btn-secondary" %>
|
|
5
|
+
<% end %>
|
|
6
|
+
<% end %>
|
|
7
|
+
|
|
8
|
+
<p style="margin-bottom: 1rem; color: var(--sqd-text-secondary);">
|
|
9
|
+
Found <%= @pagy.count %> results for "<%= params[:q] %>"
|
|
10
|
+
<%= link_to "Clear search", jobs_path, class: "sqd-table-link", style: "margin-left: 0.5rem;" %>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<% if @jobs.empty? %>
|
|
14
|
+
<%= render JobHarbor::EmptyStateComponent.new(
|
|
15
|
+
title: "No results found",
|
|
16
|
+
description: "Try a different search term or check your spelling.",
|
|
17
|
+
icon: :search
|
|
18
|
+
) %>
|
|
19
|
+
<% else %>
|
|
20
|
+
<div class="sqd-card">
|
|
21
|
+
<div class="sqd-table-container">
|
|
22
|
+
<table class="sqd-table">
|
|
23
|
+
<thead>
|
|
24
|
+
<tr>
|
|
25
|
+
<th>ID</th>
|
|
26
|
+
<th>Job Class</th>
|
|
27
|
+
<th>Queue</th>
|
|
28
|
+
<th>Status</th>
|
|
29
|
+
<th>Scheduled At</th>
|
|
30
|
+
<th>Actions</th>
|
|
31
|
+
</tr>
|
|
32
|
+
</thead>
|
|
33
|
+
<tbody>
|
|
34
|
+
<% @jobs.each do |job| %>
|
|
35
|
+
<%= render JobHarbor::JobRowComponent.new(job: job) %>
|
|
36
|
+
<% end %>
|
|
37
|
+
</tbody>
|
|
38
|
+
</table>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<%= render JobHarbor::PaginationComponent.new(pagy: @pagy) %>
|
|
43
|
+
<% end %>
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<% content_for :header_actions do %>
|
|
2
|
+
<div class="sqd-actions">
|
|
3
|
+
<% if @job.can_retry? %>
|
|
4
|
+
<%= button_to "Retry Job", retry_job_path(@job), method: :post, class: "sqd-btn sqd-btn-primary" %>
|
|
5
|
+
<% end %>
|
|
6
|
+
<% if @job.can_discard? %>
|
|
7
|
+
<%= button_to "Discard Job", discard_job_path(@job), method: :delete, class: "sqd-btn sqd-btn-danger",
|
|
8
|
+
data: { confirm: "Are you sure you want to discard this job?" } %>
|
|
9
|
+
<% end %>
|
|
10
|
+
<%= link_to "← Back to Jobs", jobs_path, class: "sqd-btn sqd-btn-secondary" %>
|
|
11
|
+
</div>
|
|
12
|
+
<% end %>
|
|
13
|
+
|
|
14
|
+
<div class="sqd-detail-grid">
|
|
15
|
+
<div class="sqd-card">
|
|
16
|
+
<div class="sqd-card-header">
|
|
17
|
+
<h2 class="sqd-card-title">Job Details</h2>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="sqd-card-body">
|
|
20
|
+
<div class="sqd-detail-item">
|
|
21
|
+
<div class="sqd-detail-label">Status</div>
|
|
22
|
+
<div class="sqd-detail-value">
|
|
23
|
+
<%= render JobHarbor::BadgeComponent.new(status: @job.status) %>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="sqd-detail-item">
|
|
28
|
+
<div class="sqd-detail-label">Job Class</div>
|
|
29
|
+
<div class="sqd-detail-value">
|
|
30
|
+
<code class="sqd-code"><%= @job.class_name %></code>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="sqd-detail-item">
|
|
35
|
+
<div class="sqd-detail-label">Queue</div>
|
|
36
|
+
<div class="sqd-detail-value">
|
|
37
|
+
<%= link_to @job.queue_name, queue_path(@job.queue_name), class: "sqd-table-link" %>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="sqd-detail-item">
|
|
42
|
+
<div class="sqd-detail-label">Priority</div>
|
|
43
|
+
<div class="sqd-detail-value"><%= @job.priority || "Default" %></div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="sqd-detail-item">
|
|
47
|
+
<div class="sqd-detail-label">Active Job ID</div>
|
|
48
|
+
<div class="sqd-detail-value">
|
|
49
|
+
<code class="sqd-code"><%= @job.active_job_id %></code>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<% if @job.worker_name %>
|
|
54
|
+
<div class="sqd-detail-item">
|
|
55
|
+
<div class="sqd-detail-label">Worker</div>
|
|
56
|
+
<div class="sqd-detail-value"><%= @job.worker_name %></div>
|
|
57
|
+
</div>
|
|
58
|
+
<% end %>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="sqd-card">
|
|
63
|
+
<div class="sqd-card-header">
|
|
64
|
+
<h2 class="sqd-card-title">Timing</h2>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="sqd-card-body">
|
|
67
|
+
<div class="sqd-detail-item">
|
|
68
|
+
<div class="sqd-detail-label">Created At</div>
|
|
69
|
+
<div class="sqd-detail-value"><%= @job.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<% if @job.scheduled_at %>
|
|
73
|
+
<div class="sqd-detail-item">
|
|
74
|
+
<div class="sqd-detail-label">Scheduled At</div>
|
|
75
|
+
<div class="sqd-detail-value"><%= @job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></div>
|
|
76
|
+
</div>
|
|
77
|
+
<% end %>
|
|
78
|
+
|
|
79
|
+
<% if @job.finished_at %>
|
|
80
|
+
<div class="sqd-detail-item">
|
|
81
|
+
<div class="sqd-detail-label">Finished At</div>
|
|
82
|
+
<div class="sqd-detail-value"><%= @job.finished_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></div>
|
|
83
|
+
</div>
|
|
84
|
+
<% end %>
|
|
85
|
+
|
|
86
|
+
<% if @job.failed_at %>
|
|
87
|
+
<div class="sqd-detail-item">
|
|
88
|
+
<div class="sqd-detail-label">Failed At</div>
|
|
89
|
+
<div class="sqd-detail-value"><%= @job.failed_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></div>
|
|
90
|
+
</div>
|
|
91
|
+
<% end %>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="sqd-card" style="margin-top: 1.5rem;">
|
|
97
|
+
<div class="sqd-card-header">
|
|
98
|
+
<h2 class="sqd-card-title">Arguments</h2>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="sqd-card-body">
|
|
101
|
+
<pre class="sqd-code"><%= JSON.pretty_generate(@job.parsed_arguments) rescue @job.arguments %></pre>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<% if @job.status == "failed" %>
|
|
106
|
+
<div class="sqd-card" style="margin-top: 1.5rem; border-color: var(--sqd-danger);">
|
|
107
|
+
<div class="sqd-card-header" style="background-color: rgba(239, 68, 68, 0.1);">
|
|
108
|
+
<h2 class="sqd-card-title" style="color: var(--sqd-danger);">Error Details</h2>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="sqd-card-body">
|
|
111
|
+
<div class="sqd-detail-item">
|
|
112
|
+
<div class="sqd-detail-label">Exception Class</div>
|
|
113
|
+
<div class="sqd-detail-value">
|
|
114
|
+
<code class="sqd-code"><%= @job.error_class || "Unknown" %></code>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="sqd-detail-item">
|
|
119
|
+
<div class="sqd-detail-label">Error Message</div>
|
|
120
|
+
<div class="sqd-detail-value"><%= @job.error_message || "No error message available" %></div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<% if @job.error_backtrace.present? %>
|
|
124
|
+
<div class="sqd-detail-item">
|
|
125
|
+
<div class="sqd-detail-label">Backtrace</div>
|
|
126
|
+
<div class="sqd-detail-value">
|
|
127
|
+
<pre class="sqd-code" style="max-height: 400px; overflow-y: auto;"><%= @job.error_backtrace %></pre>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
<% end %>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
<% end %>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<% if @queues.empty? %>
|
|
2
|
+
<%= render JobHarbor::EmptyStateComponent.new(
|
|
3
|
+
title: "No queues found",
|
|
4
|
+
description: "Queues will appear here once jobs are enqueued.",
|
|
5
|
+
icon: :queues
|
|
6
|
+
) %>
|
|
7
|
+
<% else %>
|
|
8
|
+
<div class="sqd-card-grid">
|
|
9
|
+
<% @queues.each do |queue| %>
|
|
10
|
+
<%= render JobHarbor::QueueCardComponent.new(queue: queue) %>
|
|
11
|
+
<% end %>
|
|
12
|
+
</div>
|
|
13
|
+
<% end %>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<% content_for :header_actions do %>
|
|
2
|
+
<div class="sqd-actions">
|
|
3
|
+
<% if @queue.paused? %>
|
|
4
|
+
<%= button_to "Resume Queue", resume_queue_path(@queue.name), method: :delete, class: "sqd-btn sqd-btn-primary" %>
|
|
5
|
+
<% else %>
|
|
6
|
+
<%= button_to "Pause Queue", pause_queue_path(@queue.name), method: :post, class: "sqd-btn sqd-btn-secondary" %>
|
|
7
|
+
<% end %>
|
|
8
|
+
<%= link_to "← Back to Queues", queues_path, class: "sqd-btn sqd-btn-secondary" %>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<div class="sqd-stats-grid" style="margin-bottom: 1.5rem;">
|
|
13
|
+
<%= render JobHarbor::StatCardComponent.new(
|
|
14
|
+
label: "Pending",
|
|
15
|
+
value: @queue.pending_count,
|
|
16
|
+
type: :pending
|
|
17
|
+
) %>
|
|
18
|
+
|
|
19
|
+
<%= render JobHarbor::StatCardComponent.new(
|
|
20
|
+
label: "Scheduled",
|
|
21
|
+
value: @queue.scheduled_count,
|
|
22
|
+
type: :scheduled
|
|
23
|
+
) %>
|
|
24
|
+
|
|
25
|
+
<%= render JobHarbor::StatCardComponent.new(
|
|
26
|
+
label: "In Progress",
|
|
27
|
+
value: @queue.in_progress_count,
|
|
28
|
+
type: :in_progress
|
|
29
|
+
) %>
|
|
30
|
+
|
|
31
|
+
<%= render JobHarbor::StatCardComponent.new(
|
|
32
|
+
label: "Failed",
|
|
33
|
+
value: @queue.failed_count,
|
|
34
|
+
type: :failed
|
|
35
|
+
) %>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<% if @queue.paused? %>
|
|
39
|
+
<div class="sqd-flash">
|
|
40
|
+
<div class="sqd-flash-alert">
|
|
41
|
+
This queue is currently paused. Jobs will not be processed until it is resumed.
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|
|
45
|
+
|
|
46
|
+
<% if @jobs.empty? %>
|
|
47
|
+
<%= render JobHarbor::EmptyStateComponent.new(
|
|
48
|
+
title: "No jobs in this queue",
|
|
49
|
+
description: "Jobs will appear here when they are enqueued to '#{@queue.name}'.",
|
|
50
|
+
icon: :jobs
|
|
51
|
+
) %>
|
|
52
|
+
<% else %>
|
|
53
|
+
<div class="sqd-card">
|
|
54
|
+
<div class="sqd-card-header">
|
|
55
|
+
<h2 class="sqd-card-title">Jobs in Queue</h2>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="sqd-table-container">
|
|
58
|
+
<table class="sqd-table">
|
|
59
|
+
<thead>
|
|
60
|
+
<tr>
|
|
61
|
+
<th>ID</th>
|
|
62
|
+
<th>Job Class</th>
|
|
63
|
+
<th>Status</th>
|
|
64
|
+
<th>Scheduled At</th>
|
|
65
|
+
<th>Actions</th>
|
|
66
|
+
</tr>
|
|
67
|
+
</thead>
|
|
68
|
+
<tbody>
|
|
69
|
+
<% @jobs.each do |job| %>
|
|
70
|
+
<tr>
|
|
71
|
+
<td><%= link_to job.id, job_path(job), class: "sqd-table-link" %></td>
|
|
72
|
+
<td><code class="sqd-code"><%= job.class_name %></code></td>
|
|
73
|
+
<td><%= render JobHarbor::BadgeComponent.new(status: job.status) %></td>
|
|
74
|
+
<td><%= job.scheduled_at&.strftime("%b %d, %H:%M:%S") || "—" %></td>
|
|
75
|
+
<td class="sqd-actions">
|
|
76
|
+
<% if job.can_retry? %>
|
|
77
|
+
<%= button_to "Retry", retry_job_path(job), method: :post, class: "sqd-btn sqd-btn-sm sqd-btn-secondary" %>
|
|
78
|
+
<% end %>
|
|
79
|
+
</td>
|
|
80
|
+
</tr>
|
|
81
|
+
<% end %>
|
|
82
|
+
</tbody>
|
|
83
|
+
</table>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<%= render JobHarbor::PaginationComponent.new(pagy: @pagy) %>
|
|
88
|
+
<% end %>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<% if @tasks.empty? %>
|
|
2
|
+
<%= render JobHarbor::EmptyStateComponent.new(
|
|
3
|
+
title: "No recurring tasks found",
|
|
4
|
+
description: "Configure recurring tasks in config/recurring.yml to see them here.",
|
|
5
|
+
icon: :recurring
|
|
6
|
+
) %>
|
|
7
|
+
<% else %>
|
|
8
|
+
<div class="sqd-card">
|
|
9
|
+
<div class="sqd-table-container">
|
|
10
|
+
<table class="sqd-table">
|
|
11
|
+
<thead>
|
|
12
|
+
<tr>
|
|
13
|
+
<th>Key</th>
|
|
14
|
+
<th>Job Class</th>
|
|
15
|
+
<th>Schedule</th>
|
|
16
|
+
<th>Queue</th>
|
|
17
|
+
<th>Actions</th>
|
|
18
|
+
</tr>
|
|
19
|
+
</thead>
|
|
20
|
+
<tbody>
|
|
21
|
+
<% @tasks.each do |task| %>
|
|
22
|
+
<tr>
|
|
23
|
+
<td><%= link_to task.key, recurring_task_path(task), class: "sqd-table-link" %></td>
|
|
24
|
+
<td><code class="sqd-code"><%= task.class_name %></code></td>
|
|
25
|
+
<td><%= task.schedule %></td>
|
|
26
|
+
<td><%= task.queue_name || "default" %></td>
|
|
27
|
+
<td class="sqd-actions">
|
|
28
|
+
<%= button_to "Run Now", enqueue_now_recurring_task_path(task), method: :post, class: "sqd-btn sqd-btn-sm sqd-btn-secondary" %>
|
|
29
|
+
</td>
|
|
30
|
+
</tr>
|
|
31
|
+
<% end %>
|
|
32
|
+
</tbody>
|
|
33
|
+
</table>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
<% end %>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<% content_for :header_actions do %>
|
|
2
|
+
<div class="sqd-actions">
|
|
3
|
+
<%= button_to "Run Now", enqueue_now_recurring_task_path(@task), method: :post, class: "sqd-btn sqd-btn-primary" %>
|
|
4
|
+
<%= link_to "← Back to Tasks", recurring_tasks_path, class: "sqd-btn sqd-btn-secondary" %>
|
|
5
|
+
</div>
|
|
6
|
+
<% end %>
|
|
7
|
+
|
|
8
|
+
<div class="sqd-detail-grid">
|
|
9
|
+
<div class="sqd-card">
|
|
10
|
+
<div class="sqd-card-header">
|
|
11
|
+
<h2 class="sqd-card-title">Task Details</h2>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="sqd-card-body">
|
|
14
|
+
<div class="sqd-detail-item">
|
|
15
|
+
<div class="sqd-detail-label">Key</div>
|
|
16
|
+
<div class="sqd-detail-value"><%= @task.key %></div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="sqd-detail-item">
|
|
20
|
+
<div class="sqd-detail-label">Job Class</div>
|
|
21
|
+
<div class="sqd-detail-value">
|
|
22
|
+
<code class="sqd-code"><%= @task.class_name %></code>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="sqd-detail-item">
|
|
27
|
+
<div class="sqd-detail-label">Schedule</div>
|
|
28
|
+
<div class="sqd-detail-value"><%= @task.schedule %></div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="sqd-detail-item">
|
|
32
|
+
<div class="sqd-detail-label">Queue</div>
|
|
33
|
+
<div class="sqd-detail-value"><%= @task.queue_name || "default" %></div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<% if @task.respond_to?(:arguments) && @task.arguments.present? %>
|
|
37
|
+
<div class="sqd-detail-item">
|
|
38
|
+
<div class="sqd-detail-label">Arguments</div>
|
|
39
|
+
<div class="sqd-detail-value">
|
|
40
|
+
<pre class="sqd-code"><%= JSON.pretty_generate(@task.arguments) rescue @task.arguments %></pre>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="sqd-card">
|
|
48
|
+
<div class="sqd-card-header">
|
|
49
|
+
<h2 class="sqd-card-title">Statistics</h2>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="sqd-card-body">
|
|
52
|
+
<div class="sqd-detail-item">
|
|
53
|
+
<div class="sqd-detail-label">Total Executions</div>
|
|
54
|
+
<div class="sqd-detail-value"><%= @recent_executions.count %></div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<% if @recent_executions.any? %>
|
|
58
|
+
<div class="sqd-detail-item">
|
|
59
|
+
<div class="sqd-detail-label">Last Run</div>
|
|
60
|
+
<div class="sqd-detail-value"><%= @recent_executions.first.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></div>
|
|
61
|
+
</div>
|
|
62
|
+
<% end %>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<% if @recent_executions.any? %>
|
|
68
|
+
<div class="sqd-card" style="margin-top: 1.5rem;">
|
|
69
|
+
<div class="sqd-card-header">
|
|
70
|
+
<h2 class="sqd-card-title">Recent Executions</h2>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="sqd-table-container">
|
|
73
|
+
<table class="sqd-table">
|
|
74
|
+
<thead>
|
|
75
|
+
<tr>
|
|
76
|
+
<th>Job ID</th>
|
|
77
|
+
<th>Run At</th>
|
|
78
|
+
</tr>
|
|
79
|
+
</thead>
|
|
80
|
+
<tbody>
|
|
81
|
+
<% @recent_executions.each do |execution| %>
|
|
82
|
+
<tr>
|
|
83
|
+
<td>
|
|
84
|
+
<% if execution.job_id %>
|
|
85
|
+
<%= link_to execution.job_id, job_path(execution.job_id), class: "sqd-table-link" %>
|
|
86
|
+
<% else %>
|
|
87
|
+
—
|
|
88
|
+
<% end %>
|
|
89
|
+
</td>
|
|
90
|
+
<td><%= execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
91
|
+
</tr>
|
|
92
|
+
<% end %>
|
|
93
|
+
</tbody>
|
|
94
|
+
</table>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<% end %>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<div class="sqd-stats-grid" style="margin-bottom: 1.5rem;">
|
|
2
|
+
<%= render JobHarbor::StatCardComponent.new(
|
|
3
|
+
label: "Total Workers",
|
|
4
|
+
value: @workers.count,
|
|
5
|
+
type: :workers
|
|
6
|
+
) %>
|
|
7
|
+
|
|
8
|
+
<%= render JobHarbor::StatCardComponent.new(
|
|
9
|
+
label: "Active",
|
|
10
|
+
value: @active_count,
|
|
11
|
+
type: :finished
|
|
12
|
+
) %>
|
|
13
|
+
|
|
14
|
+
<%= render JobHarbor::StatCardComponent.new(
|
|
15
|
+
label: "Stale",
|
|
16
|
+
value: @stale_count,
|
|
17
|
+
type: :blocked
|
|
18
|
+
) %>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<% if @workers.empty? %>
|
|
22
|
+
<%= render JobHarbor::EmptyStateComponent.new(
|
|
23
|
+
title: "No workers found",
|
|
24
|
+
description: "Start a Solid Queue worker process to see it here.",
|
|
25
|
+
icon: :workers
|
|
26
|
+
) %>
|
|
27
|
+
<% else %>
|
|
28
|
+
<div class="sqd-card-grid">
|
|
29
|
+
<% @workers.each do |worker| %>
|
|
30
|
+
<%= render JobHarbor::WorkerCardComponent.new(worker: worker) %>
|
|
31
|
+
<% end %>
|
|
32
|
+
</div>
|
|
33
|
+
<% end %>
|