que-view 0.3.0 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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