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 +4 -4
- data/README.md +5 -14
- data/app/assets/stylesheets/que/view/application.css +120 -60
- data/app/controllers/que/view/jobs_controller.rb +34 -27
- data/app/controllers/que/view/queue_metrics_controller.rb +17 -0
- data/app/controllers/que/view/welcome_controller.rb +2 -1
- data/app/views/layouts/que/view/application.html.erb +15 -4
- data/app/views/que/view/jobs/index.html.erb +70 -69
- data/app/views/que/view/queue_metrics/index.html.erb +29 -0
- data/app/views/que/view/welcome/index.html.erb +81 -33
- data/config/routes.rb +1 -0
- data/lib/que/view/dsl.rb +126 -26
- data/lib/que/view/version.rb +1 -1
- data/lib/que/view.rb +4 -2
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1b7b44890b63b063c130537e5fcc117f40bf1236dd89b156ca48e87a78d71248
|
4
|
+
data.tar.gz: 629fd230eb265fdbe7a41f0aec19e37f4047d797750eb635612687cceb7d5af9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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="
|
12
|
-
<img width="
|
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
|
-
|
29
|
-
|
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
|
-
|
52
|
-
|
32
|
+
h1 {
|
33
|
+
margin-top: 0;
|
53
34
|
}
|
54
35
|
|
55
|
-
|
56
|
-
|
36
|
+
.row {
|
37
|
+
display: flex;
|
57
38
|
}
|
58
39
|
|
59
|
-
|
60
|
-
|
40
|
+
.content {
|
41
|
+
padding: 1rem;
|
61
42
|
}
|
62
43
|
|
63
|
-
|
64
|
-
|
44
|
+
.actions {
|
45
|
+
display: flex;
|
65
46
|
}
|
66
47
|
|
67
|
-
.
|
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:
|
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:
|
110
|
-
line-height:
|
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:
|
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:
|
148
|
-
background: #
|
149
|
-
|
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: #
|
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:
|
179
|
+
padding: 1rem;
|
182
180
|
text-decoration: none;
|
183
|
-
color: #
|
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
|
-
.
|
187
|
-
|
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(
|
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
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
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
|
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
|
-
|
40
|
+
<section class="content">
|
41
|
+
<%= yield %>
|
42
|
+
</section>
|
32
43
|
</body>
|
33
44
|
</html>
|
@@ -1,76 +1,77 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
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">< 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 ></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
|
-
|
23
|
-
|
24
|
-
|
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
|
-
<%
|
69
|
-
|
70
|
-
|
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">< 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 ></p>
|
25
|
+
<p class="pagination-link disabled">Last </p>
|
26
|
+
<% end %>
|
71
27
|
</div>
|
72
28
|
<% end %>
|
73
|
-
|
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="
|
3
|
-
<div class="
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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="
|
15
|
-
|
16
|
-
|
17
|
-
<
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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="
|
27
|
-
|
28
|
-
|
29
|
-
<
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
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
|
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
|
89
|
+
def fetch_dashboard_stats_sql
|
55
90
|
<<-SQL.squish
|
56
|
-
SELECT count(*)
|
57
|
-
count(locks.job_id)
|
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
|
106
|
+
def fetch_que_lockers_sql
|
73
107
|
<<-SQL.squish
|
74
|
-
SELECT
|
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
|
-
|
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,
|
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
|
-
|
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
|
data/lib/que/view/version.rb
CHANGED
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
|
-
:
|
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.
|
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-
|
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
|