que-view 0.3.0 → 0.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a54cde016dccd6207b38924f717a5aeb3ea9ccae0eef215f848c2621c8af4945
4
- data.tar.gz: 0d44f6c8e723e8533c8f640b58996a2b25b7d444115c761e95879c6a91ca3331
3
+ metadata.gz: 1b7b44890b63b063c130537e5fcc117f40bf1236dd89b156ca48e87a78d71248
4
+ data.tar.gz: 629fd230eb265fdbe7a41f0aec19e37f4047d797750eb635612687cceb7d5af9
5
5
  SHA512:
6
- metadata.gz: 72bde7eb6e7d6a2ae64e380eedb7f8ce45eb609116a017e71dc1af0ea7acdb5df7293cfb5015474b6d0793570b9062ca735e95878ea1e65e570dd95f0ba4f3d3
7
- data.tar.gz: 659f6cfb80c3bb746f2f0088638e3e6f8d7acd63cc47ab0584df44762a9b18266736fd80bb4820c0a91d0e1b12b3262f7209c7321c7cf867b3ca9f2ee7c7eae9
6
+ metadata.gz: 1412a8a378aaa977fd92b40c79dad153267cbf50d1e7ac7409380469106269e5b96484d12193ac57ee78187ad172e79a61783e54e0948aff9242e4947bf6d381
7
+ data.tar.gz: ac32754839e4105584d330cc34a19cb5fccb48f8ce60afd58c1c3f5bb9b933a7ab4bd8616e109d7a2a5b91b384dff878b4a5699ee576f5d5fa1822c60d4392b0
data/README.md CHANGED
@@ -4,12 +4,13 @@ SQL queries came from Que::Web, some styling from there too.
4
4
 
5
5
  Benefits for using this one:
6
6
  - no Sinatra (que-web based on Sinatra),
7
- - no Foundation for styles,
8
- - no jQuery,
7
+ - no Foundation for styles (and no external styles at all),
8
+ - no jQuery (and no java scripts at all),
9
9
  - Que::Web was last updated 2 years ago.
10
10
 
11
- <img width="1735" alt="Снимок экрана 2024-02-21 в 15 10 35" src="https://github.com/kortirso/que-view/assets/6195394/cd2812c7-abb0-48d9-92d5-4dbef93bcd9e">
12
- <img width="1735" alt="Снимок экрана 2024-02-21 в 15 11 12" src="https://github.com/kortirso/que-view/assets/6195394/8af01e7f-a002-4ef1-aeff-f96fd27c639f">
11
+ <img width="1731" alt="Снимок экрана 2024-03-03 в 08 28 09" src="https://github.com/kortirso/que-view/assets/6195394/fdaf315d-6c6e-40ee-a60f-cd740cc7ec93">
12
+ <img width="1734" alt="Снимок экрана 2024-03-03 в 08 27 47" src="https://github.com/kortirso/que-view/assets/6195394/e45d334e-7637-41f8-af16-d6a9ac35f263">
13
+ <img width="1730" alt="Снимок экрана 2024-03-03 в 23 09 57" src="https://github.com/kortirso/que-view/assets/6195394/d790066f-3e96-4775-afd6-018750d0afd3">
13
14
 
14
15
 
15
16
  ## Installation
@@ -53,15 +54,5 @@ Add this line to assets/config/manifest.js
53
54
  //= link que/view/application.css
54
55
  ```
55
56
 
56
- ## TODO
57
-
58
- - [X] rescheduling jobs
59
- - [X] deleting jobs
60
- - [X] better styles for UI
61
- - [X] rendering running jobs
62
- - [ ] searching/filtering through jobs by name, queue
63
- - [X] pagination for jobs list page
64
- - [ ] tests
65
-
66
57
  ## License
67
58
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -21,71 +21,67 @@
21
21
  }
22
22
 
23
23
  html, body {
24
- padding: 0;
25
24
  margin: 0;
25
+ padding: 0;
26
26
  }
27
27
 
28
- .row {
29
- display: flex;
30
- flex-direction: row;
31
- margin: 0 auto;
32
- max-width: 75rem;
33
- width: 100%;
34
- }
35
-
36
- .row .column {
37
- flex: 1;
38
- padding: 1rem;
39
- }
40
-
41
- .row table {
42
- flex: 1;
43
- margin-bottom: 1rem;
44
- }
45
-
46
- table th, table td {
47
- padding: .25rem .75rem;
48
- border-bottom: 1px solid #BBB;
28
+ body {
29
+ background: #fafafa;
49
30
  }
50
31
 
51
- table th {
52
- text-align: left;
32
+ h1 {
33
+ margin-top: 0;
53
34
  }
54
35
 
55
- table tbody tr:nth-of-type(2n), table tbody tr:hover {
56
- background: #EEE;
36
+ .row {
37
+ display: flex;
57
38
  }
58
39
 
59
- table tbody tr:nth-of-type(2n):hover {
60
- background: #DDD;
40
+ .content {
41
+ padding: 1rem;
61
42
  }
62
43
 
63
- table p {
64
- margin: 0;
44
+ .actions {
45
+ display: flex;
65
46
  }
66
47
 
67
- .actions {
48
+ .dashboard-row {
68
49
  display: flex;
50
+ margin: 0 auto;
51
+ width: 100%;
52
+ border: 1px solid #a8a29e;
53
+ border-radius: .25rem;
54
+ overflow: hidden;
69
55
  }
70
56
 
71
- .dashboard-stat {
57
+ .dashboard-row .dashboard-stat {
72
58
  text-align: center;
73
59
  display: table;
74
60
  width: 100%;
61
+ background: #fff;
62
+ border-right: 1px solid #a8a29e;
63
+ }
64
+
65
+ .dashboard-row .dashboard-stat:hover {
66
+ background: #f5f5f4;
67
+ }
68
+
69
+ .dashboard-row .dashboard-stat:nth-last-of-type(1) {
70
+ border: none;
75
71
  }
76
72
 
77
- .dashboard-stat a {
73
+ .dashboard-row .dashboard-stat a {
78
74
  text-decoration: none;
79
75
  }
80
76
 
81
- .dashboard-stat .cell {
77
+ .dashboard-row .dashboard-stat .cell {
82
78
  display: flex;
83
79
  flex-direction: column;
84
80
  justify-content: center;
85
- height: 200px;
81
+ height: 150px;
86
82
  }
87
83
 
88
- .dashboard-stat h2 {
84
+ .dashboard-row .dashboard-stat h2 {
89
85
  color: #222;
90
86
  font-size: 1rem;
91
87
  font-weight: normal;
@@ -93,24 +89,23 @@ table p {
93
89
  margin: 0;
94
90
  }
95
91
 
96
- .dashboard-stat.running {
97
- background: #CFD0C1;
98
- }
99
-
100
- .dashboard-stat.scheduled {
101
- background: #828E8C;
102
- }
103
-
104
- .dashboard-stat.failing {
105
- background: #E8866C;
106
- }
107
-
108
92
  .dashboard-value {
109
- font-size: 5rem;
110
- line-height: 5rem;
93
+ font-size: 2rem;
94
+ line-height: 2rem;
111
95
  color: black;
112
96
  }
113
97
 
98
+ .btn-primary {
99
+ cursor: pointer;
100
+ border: none;
101
+ border-radius: .25rem;
102
+ background: #fde68a;
103
+ border: 1px solid #fcd34d;
104
+ border-radius: .25rem;
105
+ padding: .25rem .5rem;
106
+ margin-right: .5rem;
107
+ }
108
+
114
109
  .btn-danger {
115
110
  cursor: pointer;
116
111
  border: none;
@@ -121,7 +116,7 @@ table p {
121
116
  }
122
117
 
123
118
  .pagination {
124
- margin: 1rem auto;
119
+ margin: 0 auto 1rem;
125
120
  }
126
121
 
127
122
  .pagination .pagination-link {
@@ -144,18 +139,21 @@ table p {
144
139
 
145
140
  .navigation {
146
141
  display: flex;
147
- padding: .5rem 1rem;
148
- background: #52525b;
149
- color: #fff;
142
+ padding: 0 1rem;
143
+ background: #fff;
144
+ border-bottom: 1px solid #e7e5e4;
145
+ color: #000;
150
146
  }
151
147
 
152
148
  .navigation h1 {
153
149
  margin: 0 2rem 0 0;
150
+ display: flex;
151
+ align-items: center;
154
152
  }
155
153
 
156
154
  .navigation h1 a {
157
155
  text-decoration: none;
158
- color: #fff;
156
+ color: #000;
159
157
  }
160
158
 
161
159
  .navigation .navigation-section {
@@ -178,17 +176,79 @@ table p {
178
176
 
179
177
  .navigation .navigation-section ul li a {
180
178
  display: inline-block;
181
- padding: .5rem;
179
+ padding: 1rem;
182
180
  text-decoration: none;
183
- color: #fff;
181
+ color: #000;
182
+ }
183
+
184
+ .navigation .navigation-section ul li.active {
185
+ background: #e7e5e4;
186
+ }
187
+
188
+ .search-form {
189
+ margin-bottom: 1rem;
184
190
  }
185
191
 
186
- .navigation .navigation-section ul li.active a {
187
- text-decoration: underline;
192
+ .search-form .form-select {
193
+ margin-right: 1rem;
194
+ padding: .5rem 1rem;
195
+ border: 1px solid #e7e5e4;
196
+ border-radius: .25rem;
197
+ background: #fff;
198
+ }
199
+
200
+ table {
201
+ width: 100%;
202
+ margin-bottom: 1rem;
203
+ background: #fff;
204
+ border: 1px solid #e7e5e4;
205
+ border-bottom: none;
206
+ border-radius: .25rem;
207
+ }
208
+
209
+ table th, table td {
210
+ padding: .5rem .75rem;
211
+ border-bottom: 1px solid #e7e5e4;
212
+ }
213
+
214
+ table th {
215
+ text-align: left;
216
+ font-weight: normal;
217
+ text-transform: uppercase;
218
+ }
219
+
220
+ table tbody tr:nth-of-type(2n), table tbody tr:hover {
221
+ background: #EEE;
222
+ }
223
+
224
+ table tbody tr:nth-of-type(2n):hover {
225
+ background: #DDD;
226
+ }
227
+
228
+ table p {
229
+ margin: 0;
230
+ }
231
+
232
+ .lockers {
233
+ margin-top: 2rem;
234
+ }
235
+
236
+ .lockers .locker-process {
237
+ display: flex;
238
+ flex-direction: column;
239
+ }
240
+
241
+ .metrics a {
242
+ color: #000;
188
243
  }
189
244
 
190
245
  @media screen and (max-width: 768px) {
191
- .row {
246
+ .dashboard-row {
192
247
  flex-direction: column;
193
248
  }
249
+
250
+ .dashboard-row .dashboard-stat {
251
+ border-right: none;
252
+ border-bottom: 1px solid #a8a29e;
253
+ }
194
254
  }
@@ -5,10 +5,12 @@ module Que
5
5
  class JobsController < Que::View::ApplicationController
6
6
  PER_PAGE = 20
7
7
 
8
+ before_action :find_queue_names, only: %i[index]
9
+ before_action :find_job_names, only: %i[index]
8
10
  before_action :find_job, only: %i[show]
9
11
 
10
12
  def index
11
- @jobs = find_jobs(params[:status])
13
+ @jobs = find_jobs(index_params)
12
14
  paginate
13
15
  end
14
16
 
@@ -48,6 +50,25 @@ module Que
48
50
 
49
51
  private
50
52
 
53
+ def find_queue_names
54
+ @queue_names = [
55
+ ['All queues', nil]
56
+ ] + ::Que::View.fetch_queue_names
57
+ end
58
+
59
+ def find_job_names
60
+ @job_names = [
61
+ ['All jobs', nil]
62
+ ] + ::Que::View.fetch_job_names(params[:queue_name])
63
+ end
64
+
65
+ def find_job
66
+ @job = ::Que::View.fetch_job(params[:id])[0]
67
+ return if @job
68
+
69
+ redirect_to root_path, notice: 'Job is not found'
70
+ end
71
+
51
72
  def paginate
52
73
  return if %w[failing scheduled].exclude?(params[:status])
53
74
  return unless @jobs.any?
@@ -61,24 +82,19 @@ module Que
61
82
  )
62
83
  end
63
84
 
64
- def find_job
65
- @job = ::Que::View.fetch_job(params[:id])[0]
66
- return if @job
67
-
68
- redirect_to root_path, notice: 'Job is not found'
69
- end
70
-
71
- def find_jobs(status)
72
- case status&.to_sym
73
- when :running then ::Que::View.fetch_running_jobs(search)
74
- when :failing then ::Que::View.fetch_failing_jobs(PER_PAGE, offset, search)
75
- when :scheduled then ::Que::View.fetch_scheduled_jobs(PER_PAGE, offset, search)
85
+ def find_jobs(params)
86
+ case params[:status]&.to_sym
87
+ when :running then ::Que::View.fetch_running_jobs
88
+ when :failing then ::Que::View.fetch_failing_jobs(PER_PAGE, offset, params)
89
+ when :scheduled then ::Que::View.fetch_scheduled_jobs(PER_PAGE, offset, params)
90
+ when :finished then ::Que::View.fetch_finished_jobs(PER_PAGE, offset, params)
91
+ when :expired then ::Que::View.fetch_expired_jobs(PER_PAGE, offset, params)
76
92
  else []
77
93
  end
78
94
  end
79
95
 
80
96
  def find_jobs_total_amount(status)
81
- ::Que::View.fetch_dashboard_stats(search)[0][status&.to_sym]
97
+ ::Que::View.fetch_dashboard_stats[0][status&.to_sym]
82
98
  end
83
99
 
84
100
  def reschedule_all_jobs(status, time)
@@ -97,19 +113,6 @@ module Que
97
113
  end
98
114
  end
99
115
 
100
- def search
101
- return '%' unless search_param
102
-
103
- "%#{search_param}%"
104
- end
105
-
106
- def search_param
107
- sanitised = (params[:search] || '').gsub(/[^0-9a-z:]/i, '')
108
- return if sanitised.empty?
109
-
110
- sanitised
111
- end
112
-
113
116
  def offset
114
117
  (page - 1) * PER_PAGE
115
118
  end
@@ -117,6 +120,10 @@ module Que
117
120
  def page
118
121
  (params[:page] || 1).to_i
119
122
  end
123
+
124
+ def index_params
125
+ params.permit(:status, :queue_name, :job_name).to_h.symbolize_keys
126
+ end
120
127
  end
121
128
  end
122
129
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Que
4
+ module View
5
+ class QueueMetricsController < Que::View::ApplicationController
6
+ before_action :find_queue_metrics, only: %i[index]
7
+
8
+ def index; end
9
+
10
+ private
11
+
12
+ def find_queue_metrics
13
+ @queue_metrics = ::Que::View.fetch_queue_metrics
14
+ end
15
+ end
16
+ end
17
+ end
@@ -4,7 +4,8 @@ module Que
4
4
  module View
5
5
  class WelcomeController < Que::View::ApplicationController
6
6
  def index
7
- @dashboard_stats = ::Que::View.fetch_dashboard_stats('%')[0]
7
+ @dashboard_stats = ::Que::View.fetch_dashboard_stats[0]
8
+ @lockers = ::Que::View.fetch_que_lockers
8
9
  end
9
10
  end
10
11
  end
@@ -11,15 +11,24 @@
11
11
  <h1><%= link_to 'Que View', root_path %></h1>
12
12
  <section class="navigation-section">
13
13
  <ul>
14
- <li class="<%= 'active' if params[:status] == 'running' %>">
15
- <%= link_to 'Running', jobs_path(status: 'running') %>
16
- </li>
17
14
  <li class="<%= 'active' if params[:status] == 'scheduled' %>">
18
15
  <%= link_to 'Scheduled', jobs_path(status: 'scheduled') %>
19
16
  </li>
20
17
  <li class="<%= 'active' if params[:status] == 'failing' %>">
21
18
  <%= link_to 'Failing', jobs_path(status: 'failing') %>
22
19
  </li>
20
+ <li class="<%= 'active' if params[:status] == 'running' %>">
21
+ <%= link_to 'Running', jobs_path(status: 'running') %>
22
+ </li>
23
+ <li class="<%= 'active' if params[:status] == 'finished' %>">
24
+ <%= link_to 'Finished', jobs_path(status: 'finished') %>
25
+ </li>
26
+ <li class="<%= 'active' if params[:status] == 'expired' %>">
27
+ <%= link_to 'Expired', jobs_path(status: 'expired') %>
28
+ </li>
29
+ <li class="<%= 'active' if current_page?(queue_metrics_path) %>">
30
+ <%= link_to 'Queue metrics', queue_metrics_path %>
31
+ </li>
23
32
  </ul>
24
33
  <ul class="version">
25
34
  <li>Que <%= Que::VERSION %></li>
@@ -28,6 +37,8 @@
28
37
  </ul>
29
38
  </section>
30
39
  </nav>
31
- <%= yield %>
40
+ <section class="content">
41
+ <%= yield %>
42
+ </section>
32
43
  </body>
33
44
  </html>
@@ -1,76 +1,77 @@
1
- <% if @jobs.any? %>
2
- <% if @pagination %>
3
- <div class="row pagination">
4
- <% if @pagination.previous_page? %>
5
- <%= link_to 'First', jobs_path(status: params[:status], page: 1), class: 'pagination-link' %>
6
- <%= link_to '< Previous', jobs_path(status: params[:status], page: @pagination.previous_page), class: 'pagination-link' %>
7
- <% else %>
8
- <p class="pagination-link disabled">First</p>
9
- <p class="pagination-link disabled">&#60; Previous</p>
10
- <% end %>
11
- <p class="total-pages"><%= "Page #{@pagination.page} of #{@pagination.total_pages}" %></p>
12
- <% if @pagination.next_page? %>
13
- <%= link_to 'Next >', jobs_path(status: params[:status], page: @pagination.next_page), class: 'pagination-link' %>
14
- <%= link_to 'Last', jobs_path(status: params[:status], page: @pagination.total_pages), class: 'pagination-link' %>
15
- <% else %>
16
- <p class="pagination-link disabled">Next &#62;</p>
17
- <p class="pagination-link disabled">Last </p>
18
- <% end %>
19
- </div>
20
- <% end %>
1
+ <h1><%= params[:status].capitalize %> jobs</h1>
2
+ <%= form_with url: jobs_path, method: :get, class: 'search-form' do |form| %>
3
+ <%= form.hidden_field :status, value: params[:status] %>
21
4
  <div class="row">
22
- <table cellspacing="0">
23
- <thead>
24
- <tr>
25
- <th>ID</th>
26
- <th>Run at</th>
27
- <th>Job</th>
28
- <th>Queue</th>
29
- <th>Arguments</th>
30
- <% if params[:status] == 'failing' %>
31
- <th>Failures</th>
32
- <th>Error</th>
33
- <% end %>
34
- <% if %w[failing scheduled].include?(params[:status]) %>
35
- <th></th>
36
- <% end %>
37
- </tr>
38
- </thead>
39
- <tbody>
40
- <% @jobs.each do |job| %>
41
- <tr>
42
- <td><%= link_to job[:id], job_path(job[:id]) %></td>
43
- <td><%= job[:run_at].strftime("%Y-%m-%d %H:%M:%S") %></td>
44
- <td><%= humanized_job_class(job) %></td>
45
- <td><%= job[:queue] %></td>
46
- <td>
47
- <% job.dig(:args, 0, :arguments).each do |argument| %>
48
- <p><%= argument %></p>
49
- <% end %>
50
- </td>
51
- <% if params[:status] == 'failing' %>
52
- <td><%= job[:error_count] %></td>
53
- <td><%= format_error(job) %></td>
54
- <% end %>
55
- <% if %w[failing scheduled].include?(params[:status]) %>
56
- <td>
57
- <div class="actions">
58
- <%= button_to 'Run', job_path(job[:id]), class: 'btn-danger', method: :patch, onclick: "return confirm('Are you sure you wish to reschedule job?')" %>
59
- <%= button_to 'Delete', job_path(job[:id]), class: 'btn-danger', method: :delete, onclick: "return confirm('Are you sure you wish to delete job?')" %>
60
- </div>
61
- </td>
62
- <% end %>
63
- </tr>
64
- <% end %>
65
- </tbody>
66
- </table>
5
+ <%= form.select :queue_name, options_for_select(@queue_names, params[:queue_name]), {}, class: 'form-select' %>
6
+ <%= form.select :job_name, options_for_select(@job_names, params[:job_name]), {}, class: 'form-select' %>
7
+ <%= form.submit 'Search', class: 'btn-primary' %>
67
8
  </div>
68
- <% else %>
69
- <div class="row">
70
- <p>No jobs found</p>
9
+ <% end %>
10
+ <% if @pagination %>
11
+ <div class="row pagination">
12
+ <% if @pagination.previous_page? %>
13
+ <%= link_to 'First', jobs_path(status: params[:status], page: 1), class: 'pagination-link' %>
14
+ <%= link_to '< Previous', jobs_path(status: params[:status], page: @pagination.previous_page), class: 'pagination-link' %>
15
+ <% else %>
16
+ <p class="pagination-link disabled">First</p>
17
+ <p class="pagination-link disabled">&#60; Previous</p>
18
+ <% end %>
19
+ <p class="total-pages"><%= "Page #{@pagination.page} of #{@pagination.total_pages}" %></p>
20
+ <% if @pagination.next_page? %>
21
+ <%= link_to 'Next >', jobs_path(status: params[:status], page: @pagination.next_page), class: 'pagination-link' %>
22
+ <%= link_to 'Last', jobs_path(status: params[:status], page: @pagination.total_pages), class: 'pagination-link' %>
23
+ <% else %>
24
+ <p class="pagination-link disabled">Next &#62;</p>
25
+ <p class="pagination-link disabled">Last </p>
26
+ <% end %>
71
27
  </div>
72
28
  <% end %>
73
- <% if @pagination&.count&.positive? %>
29
+ <table cellspacing="0" class="jobs-list">
30
+ <thead>
31
+ <tr>
32
+ <th>ID</th>
33
+ <th>Job</th>
34
+ <th>Queue</th>
35
+ <th>Run at</th>
36
+ <th>Arguments</th>
37
+ <% if params[:status] == 'failing' %>
38
+ <th>Failures</th>
39
+ <th>Error</th>
40
+ <% end %>
41
+ <% if %w[failing scheduled].include?(params[:status]) %>
42
+ <th>Actions</th>
43
+ <% end %>
44
+ </tr>
45
+ </thead>
46
+ <tbody>
47
+ <% @jobs.each do |job| %>
48
+ <tr>
49
+ <td><%= link_to job[:id], job_path(job[:id]) %></td>
50
+ <td><%= humanized_job_class(job) %></td>
51
+ <td><%= job[:queue] %></td>
52
+ <td><%= job[:run_at].strftime("%Y-%m-%d %H:%M:%S") %></td>
53
+ <td>
54
+ <% job.dig(:args, 0, :arguments).each do |argument| %>
55
+ <p><%= argument %></p>
56
+ <% end %>
57
+ </td>
58
+ <% if params[:status] == 'failing' %>
59
+ <td><%= job[:error_count] %></td>
60
+ <td><%= format_error(job) %></td>
61
+ <% end %>
62
+ <% if %w[failing scheduled].include?(params[:status]) %>
63
+ <td>
64
+ <div class="actions">
65
+ <%= button_to 'Run', job_path(job[:id]), class: 'btn-danger', method: :patch, onclick: "return confirm('Are you sure you wish to reschedule job?')" %>
66
+ <%= button_to 'Delete', job_path(job[:id]), class: 'btn-danger', method: :delete, onclick: "return confirm('Are you sure you wish to delete job?')" %>
67
+ </div>
68
+ </td>
69
+ <% end %>
70
+ </tr>
71
+ <% end %>
72
+ </tbody>
73
+ </table>
74
+ <% if @pagination && %w[failing scheduled].include?(params[:status]) %>
74
75
  <div class="row">
75
76
  <%= button_to 'Run All', reschedule_all_jobs_path(status: params[:status]), class: 'btn-danger', method: :post, onclick: "return confirm('Are you sure you wish to reschedule all jobs?')" %>
76
77
  <%= button_to 'Delete All', destroy_all_jobs_path(status: params[:status]), class: 'btn-danger', method: :delete, onclick: "return confirm('Are you sure you wish to delete all jobs?')" %>
@@ -0,0 +1,29 @@
1
+ <div class="metrics">
2
+ <h1>Queue Metrics</h1>
3
+ <table cellspacing="0">
4
+ <thead>
5
+ <tr>
6
+ <th>Queue</th>
7
+ <th>Scheduled</th>
8
+ <th>Failing</th>
9
+ <th>Running</th>
10
+ <th>Finished</th>
11
+ <th>Expired</th>
12
+ <th>Latency</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody>
16
+ <% @queue_metrics.each do |queue, values| %>
17
+ <tr>
18
+ <td><%= queue %></td>
19
+ <td><%= link_to values[:scheduled], jobs_path(status: 'scheduled', queue_name: queue) %></td>
20
+ <td><%= link_to values[:failing], jobs_path(status: 'failing', queue_name: queue) %></td>
21
+ <td><%= link_to values[:running], jobs_path(status: 'running', queue_name: queue) %></td>
22
+ <td><%= link_to values[:finished], jobs_path(status: 'finished', queue_name: queue) %></td>
23
+ <td><%= link_to values[:expired], jobs_path(status: 'expired', queue_name: queue) %></td>
24
+ <td></td>
25
+ </tr>
26
+ <% end %>
27
+ </tbody>
28
+ </table>
29
+ </div>
@@ -1,38 +1,86 @@
1
- <div class="row">
2
- <div class="column">
3
- <div class="dashboard-stat running">
4
- <%= link_to jobs_path(status: 'running') do %>
5
- <div class="cell">
6
- <h2>Running</h2>
7
- <span class="dashboard-value">
8
- <%= @dashboard_stats[:running] %>
9
- </span>
10
- </div>
11
- <% end %>
1
+ <div class="dashboard-row">
2
+ <div class="dashboard-stat">
3
+ <div class="cell">
4
+ <h2>Total</h2>
5
+ <span class="dashboard-value">
6
+ <%= @dashboard_stats[:total] %>
7
+ </span>
12
8
  </div>
13
9
  </div>
14
- <div class="column">
15
- <div class="dashboard-stat scheduled">
16
- <%= link_to jobs_path(status: 'scheduled') do %>
17
- <div class="cell">
18
- <h2>Scheduled</h2>
19
- <span class="dashboard-value">
20
- <%= @dashboard_stats[:scheduled] %>
21
- </span>
22
- </div>
23
- <% end %>
24
- </div>
10
+ <div class="dashboard-stat">
11
+ <%= link_to jobs_path(status: 'scheduled') do %>
12
+ <div class="cell">
13
+ <h2>Scheduled</h2>
14
+ <span class="dashboard-value">
15
+ <%= @dashboard_stats[:scheduled] %>
16
+ </span>
17
+ </div>
18
+ <% end %>
25
19
  </div>
26
- <div class="column">
27
- <div class="dashboard-stat failing">
28
- <%= link_to jobs_path(status: 'failing') do %>
29
- <div class="cell">
30
- <h2>Failing</h2>
31
- <span class="dashboard-value">
32
- <%= @dashboard_stats[:failing] %>
33
- </span>
34
- </div>
35
- <% end %>
36
- </div>
20
+ <div class="dashboard-stat">
21
+ <%= link_to jobs_path(status: 'failing') do %>
22
+ <div class="cell">
23
+ <h2>Failing</h2>
24
+ <span class="dashboard-value">
25
+ <%= @dashboard_stats[:failing] %>
26
+ </span>
27
+ </div>
28
+ <% end %>
37
29
  </div>
30
+ <div class="dashboard-stat">
31
+ <%= link_to jobs_path(status: 'running') do %>
32
+ <div class="cell">
33
+ <h2>Running</h2>
34
+ <span class="dashboard-value">
35
+ <%= @dashboard_stats[:running] %>
36
+ </span>
37
+ </div>
38
+ <% end %>
39
+ </div>
40
+ <div class="dashboard-stat">
41
+ <%= link_to jobs_path(status: 'finished') do %>
42
+ <div class="cell">
43
+ <h2>Finished</h2>
44
+ <span class="dashboard-value">
45
+ <%= @dashboard_stats[:finished] %>
46
+ </span>
47
+ </div>
48
+ <% end %>
49
+ </div>
50
+ <div class="dashboard-stat">
51
+ <%= link_to jobs_path(status: 'expired') do %>
52
+ <div class="cell">
53
+ <h2>Expired</h2>
54
+ <span class="dashboard-value">
55
+ <%= @dashboard_stats[:expired] %>
56
+ </span>
57
+ </div>
58
+ <% end %>
59
+ </div>
60
+ </div>
61
+ <div class="lockers">
62
+ <h1>Processes</h1>
63
+ <table cellspacing="0">
64
+ <thead>
65
+ <tr>
66
+ <th>ID</th>
67
+ <th>Worker count</th>
68
+ <th>Worker priorities</th>
69
+ <th>Listening</th>
70
+ </tr>
71
+ </thead>
72
+ <tbody>
73
+ <% @lockers.each.with_index do |locker, index| %>
74
+ <tr>
75
+ <td class="locker-process">
76
+ <span>Process <%= index + 1 %></span>
77
+ <span><strong>Queues:</strong> <%= locker[:queues][1..-2].split(',').join(', ') %></span>
78
+ </td>
79
+ <td><%= locker[:worker_count] %></td>
80
+ <td><%= locker[:worker_priorities] %></td>
81
+ <td><%= locker[:listening] %></td>
82
+ </tr>
83
+ <% end %>
84
+ </tbody>
85
+ </table>
38
86
  </div>
data/config/routes.rb CHANGED
@@ -5,6 +5,7 @@ Que::View::Engine.routes.draw do
5
5
  post :reschedule_all, on: :collection
6
6
  delete :destroy_all, on: :collection
7
7
  end
8
+ resources :queue_metrics, only: %i[index]
8
9
 
9
10
  root 'welcome#index'
10
11
  end
data/lib/que/view/dsl.rb CHANGED
@@ -4,11 +4,34 @@ module Que
4
4
  module View
5
5
  # rubocop: disable Metrics/ClassLength
6
6
  class DSL
7
- def fetch_dashboard_stats(...)
8
- execute(fetch_dashboard_stats_sql(...))
7
+ def fetch_dashboard_stats
8
+ execute(fetch_dashboard_stats_sql)
9
9
  end
10
10
 
11
- def fetch_running_jobs(...)
11
+ def fetch_que_lockers
12
+ execute(fetch_que_lockers_sql)
13
+ end
14
+
15
+ def fetch_queue_metrics
16
+ execute(fetch_queue_metrics_sql).each_with_object({}) { |element, acc|
17
+ acc[element[:queue_name].to_sym] ||= basis_queue_stats
18
+ acc[element[:queue_name].to_sym][element[:status].to_sym] = element[:count_all]
19
+ }
20
+ end
21
+
22
+ def fetch_queue_names
23
+ execute(fetch_queue_names_sql).map { |queues_data|
24
+ ["#{queues_data[:queue_name]} (#{queues_data[:count_all]})", queues_data[:queue_name]]
25
+ }
26
+ end
27
+
28
+ def fetch_job_names(...)
29
+ execute(fetch_job_names_sql(...)).map { |jobs_data|
30
+ ["#{jobs_data[:job_name]} (#{jobs_data[:count_all]})", jobs_data[:job_name]]
31
+ }
32
+ end
33
+
34
+ def fetch_running_jobs
12
35
  Que.job_states
13
36
  end
14
37
 
@@ -20,6 +43,14 @@ module Que
20
43
  execute(fetch_scheduled_jobs_sql(...))
21
44
  end
22
45
 
46
+ def fetch_finished_jobs(...)
47
+ execute(fetch_finished_jobs_sql(...))
48
+ end
49
+
50
+ def fetch_expired_jobs(...)
51
+ execute(fetch_expired_jobs_sql(...))
52
+ end
53
+
23
54
  def fetch_job(...)
24
55
  execute(fetch_job_sql(...))
25
56
  end
@@ -50,47 +81,121 @@ module Que
50
81
 
51
82
  private
52
83
 
84
+ def basis_queue_stats
85
+ { scheduled: 0, failing: 0, running: 0, finished: 0, expired: 0 }
86
+ end
87
+
53
88
  # rubocop: disable Metrics/MethodLength
54
- def fetch_dashboard_stats_sql(search)
89
+ def fetch_dashboard_stats_sql
55
90
  <<-SQL.squish
56
- SELECT count(*) AS total,
57
- count(locks.job_id) AS running,
91
+ SELECT count(*) AS total,
92
+ count(locks.job_id) AS running,
58
93
  coalesce(sum((error_count > 0 AND locks.job_id IS NULL)::int), 0) AS failing,
59
- coalesce(sum((error_count = 0 AND locks.job_id IS NULL)::int), 0) AS scheduled
94
+ coalesce(sum((error_count = 0 AND locks.job_id IS NULL)::int), 0) AS scheduled,
95
+ coalesce(sum((finished_at IS NOT NULL)::int), 0) AS finished,
96
+ coalesce(sum((expired_at IS NOT NULL)::int), 0) AS expired
60
97
  FROM que_jobs
61
98
  LEFT JOIN (
62
99
  SELECT (classid::bigint << 32) + objid::bigint AS job_id
63
100
  FROM pg_locks
64
101
  WHERE locktype = 'advisory'
65
102
  ) locks ON (que_jobs.id=locks.job_id)
66
- WHERE
67
- job_class ILIKE ('#{search}')
68
- OR que_jobs.args #>> '{0, job_class}' ILIKE ('#{search}')
69
103
  SQL
70
104
  end
71
105
 
72
- def fetch_failing_jobs_sql(per_page, offset, search)
106
+ def fetch_que_lockers_sql
73
107
  <<-SQL.squish
74
- SELECT que_jobs.*
108
+ SELECT *
109
+ FROM que_lockers
110
+ SQL
111
+ end
112
+
113
+ def fetch_queue_metrics_sql
114
+ <<-SQL.squish
115
+ SELECT COUNT(*) AS count_all, queue AS queue_name,
116
+ CASE
117
+ WHEN expired_at IS NOT NULL THEN 'expired'
118
+ WHEN finished_at IS NOT NULL THEN 'finished'
119
+ WHEN locks.job_id IS NULL AND error_count > 0 THEN 'failing'
120
+ WHEN locks.job_id IS NULL AND error_count = 0 THEN 'scheduled'
121
+ ELSE 'running'
122
+ END status
75
123
  FROM que_jobs
76
124
  LEFT JOIN (
77
125
  SELECT (classid::bigint << 32) + objid::bigint AS job_id
78
126
  FROM pg_locks
79
127
  WHERE locktype = 'advisory'
80
128
  ) locks ON (que_jobs.id=locks.job_id)
129
+ GROUP BY queue,
130
+ CASE
131
+ WHEN expired_at IS NOT NULL THEN 'expired'
132
+ WHEN finished_at IS NOT NULL THEN 'finished'
133
+ WHEN locks.job_id IS NULL AND error_count > 0 THEN 'failing'
134
+ WHEN locks.job_id IS NULL AND error_count = 0 THEN 'scheduled'
135
+ ELSE 'running'
136
+ END
137
+ SQL
138
+ end
139
+
140
+ def fetch_queue_names_sql
141
+ <<-SQL.squish
142
+ SELECT COUNT(*) AS count_all, queue AS queue_name
143
+ FROM que_jobs
144
+ GROUP BY queue
145
+ SQL
146
+ end
147
+
148
+ def fetch_job_names_sql(queue_name)
149
+ <<-SQL.squish
150
+ SELECT COUNT(*) AS count_all, args #>> '{0, job_class}' AS job_name
151
+ FROM que_jobs
152
+ #{queue_name.present? ? "WHERE queue = '#{queue_name}'" : ""}
153
+ GROUP BY args #>> '{0, job_class}'
154
+ SQL
155
+ end
156
+
157
+ def fetch_failing_jobs_sql(per_page, offset, params)
158
+ where_condition = <<-SQL.squish
81
159
  WHERE locks.job_id IS NULL
82
160
  AND error_count > 0
83
- AND (
84
- job_class ILIKE ('#{search}')
85
- OR que_jobs.args #>> '{0, job_class}' ILIKE ('#{search}')
86
- )
87
- ORDER BY run_at, id
88
- LIMIT #{per_page}::int
89
- OFFSET #{offset}::int
161
+ #{search_condition(params)}
90
162
  SQL
163
+ fetch_jobs_sql(per_page, offset, where_condition)
91
164
  end
92
165
 
93
- def fetch_scheduled_jobs_sql(per_page, offset, search)
166
+ def fetch_scheduled_jobs_sql(per_page, offset, params)
167
+ where_condition = <<-SQL.squish
168
+ WHERE locks.job_id IS NULL
169
+ AND error_count = 0
170
+ #{search_condition(params)}
171
+ SQL
172
+ fetch_jobs_sql(per_page, offset, where_condition)
173
+ end
174
+
175
+ def fetch_finished_jobs_sql(per_page, offset, params)
176
+ where_condition = <<-SQL.squish
177
+ WHERE finished_at IS NOT NULL
178
+ #{search_condition(params)}
179
+ SQL
180
+ fetch_jobs_sql(per_page, offset, where_condition)
181
+ end
182
+
183
+ def fetch_expired_jobs_sql(per_page, offset, params)
184
+ where_condition = <<-SQL.squish
185
+ WHERE expired_at IS NOT NULL
186
+ #{search_condition(params)}
187
+ SQL
188
+ fetch_jobs_sql(per_page, offset, where_condition)
189
+ end
190
+
191
+ def search_condition(params)
192
+ result = ''
193
+ result += "AND queue = '#{params[:queue_name]}'" if params[:queue_name].present?
194
+ result += "AND args #>> '{0, job_class}' = ('#{params[:job_name]}')" if params[:job_name].present?
195
+ result
196
+ end
197
+
198
+ def fetch_jobs_sql(per_page, offset, where_condition)
94
199
  <<-SQL.squish
95
200
  SELECT que_jobs.*
96
201
  FROM que_jobs
@@ -99,12 +204,7 @@ module Que
99
204
  FROM pg_locks
100
205
  WHERE locktype = 'advisory'
101
206
  ) locks ON (que_jobs.id=locks.job_id)
102
- WHERE locks.job_id IS NULL
103
- AND error_count = 0
104
- AND (
105
- job_class ILIKE ('#{search}')
106
- OR que_jobs.args #>> '{0, job_class}' ILIKE ('#{search}')
107
- )
207
+ #{where_condition}
108
208
  ORDER BY run_at, id
109
209
  LIMIT #{per_page}::int
110
210
  OFFSET #{offset}::int
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Que
4
4
  module View
5
- VERSION = '0.3.0'
5
+ VERSION = '0.3.2'
6
6
  end
7
7
  end
data/lib/que/view.rb CHANGED
@@ -34,8 +34,10 @@ module Que
34
34
  end
35
35
 
36
36
  # Public: All the methods delegated to instance. These should match the interface of Que::View::DSL.
37
- def_delegators :instance, :fetch_dashboard_stats,
38
- :fetch_running_jobs, :fetch_failing_jobs, :fetch_scheduled_jobs, :fetch_job,
37
+ def_delegators :instance, :fetch_dashboard_stats, :fetch_que_lockers, :fetch_queue_metrics,
38
+ :fetch_queue_names, :fetch_job_names,
39
+ :fetch_running_jobs, :fetch_failing_jobs, :fetch_scheduled_jobs, :fetch_finished_jobs,
40
+ :fetch_expired_jobs, :fetch_job,
39
41
  :delete_failing_jobs, :delete_scheduled_jobs, :delete_job,
40
42
  :reschedule_scheduled_jobs, :reschedule_failing_jobs, :reschedule_job
41
43
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: que-view
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bogdanov Anton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-21 00:00:00.000000000 Z
11
+ date: 2024-03-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: que
@@ -164,11 +164,13 @@ files:
164
164
  - app/assets/stylesheets/que/view/application.css
165
165
  - app/controllers/que/view/application_controller.rb
166
166
  - app/controllers/que/view/jobs_controller.rb
167
+ - app/controllers/que/view/queue_metrics_controller.rb
167
168
  - app/controllers/que/view/welcome_controller.rb
168
169
  - app/helpers/que/view/application_helper.rb
169
170
  - app/views/layouts/que/view/application.html.erb
170
171
  - app/views/que/view/jobs/index.html.erb
171
172
  - app/views/que/view/jobs/show.html.erb
173
+ - app/views/que/view/queue_metrics/index.html.erb
172
174
  - app/views/que/view/welcome/index.html.erb
173
175
  - config/routes.rb
174
176
  - lib/que/view.rb