qless 0.9.3 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/Gemfile +9 -3
  2. data/README.md +70 -25
  3. data/Rakefile +125 -9
  4. data/exe/install_phantomjs +21 -0
  5. data/lib/qless.rb +115 -76
  6. data/lib/qless/config.rb +11 -9
  7. data/lib/qless/failure_formatter.rb +43 -0
  8. data/lib/qless/job.rb +201 -102
  9. data/lib/qless/job_reservers/ordered.rb +7 -1
  10. data/lib/qless/job_reservers/round_robin.rb +16 -6
  11. data/lib/qless/job_reservers/shuffled_round_robin.rb +9 -2
  12. data/lib/qless/lua/qless-lib.lua +2463 -0
  13. data/lib/qless/lua/qless.lua +2012 -0
  14. data/lib/qless/lua_script.rb +63 -12
  15. data/lib/qless/middleware/memory_usage_monitor.rb +62 -0
  16. data/lib/qless/middleware/metriks.rb +45 -0
  17. data/lib/qless/middleware/redis_reconnect.rb +6 -3
  18. data/lib/qless/middleware/requeue_exceptions.rb +94 -0
  19. data/lib/qless/middleware/retry_exceptions.rb +38 -9
  20. data/lib/qless/middleware/sentry.rb +3 -7
  21. data/lib/qless/middleware/timeout.rb +64 -0
  22. data/lib/qless/queue.rb +90 -55
  23. data/lib/qless/server.rb +177 -130
  24. data/lib/qless/server/views/_job.erb +33 -15
  25. data/lib/qless/server/views/completed.erb +11 -0
  26. data/lib/qless/server/views/layout.erb +70 -11
  27. data/lib/qless/server/views/overview.erb +93 -53
  28. data/lib/qless/server/views/queue.erb +9 -8
  29. data/lib/qless/server/views/queues.erb +18 -1
  30. data/lib/qless/subscriber.rb +37 -22
  31. data/lib/qless/tasks.rb +5 -10
  32. data/lib/qless/test_helpers/worker_helpers.rb +55 -0
  33. data/lib/qless/version.rb +3 -1
  34. data/lib/qless/worker.rb +4 -413
  35. data/lib/qless/worker/base.rb +247 -0
  36. data/lib/qless/worker/forking.rb +245 -0
  37. data/lib/qless/worker/serial.rb +41 -0
  38. metadata +135 -52
  39. data/lib/qless/qless-core/cancel.lua +0 -101
  40. data/lib/qless/qless-core/complete.lua +0 -233
  41. data/lib/qless/qless-core/config.lua +0 -56
  42. data/lib/qless/qless-core/depends.lua +0 -65
  43. data/lib/qless/qless-core/deregister_workers.lua +0 -12
  44. data/lib/qless/qless-core/fail.lua +0 -117
  45. data/lib/qless/qless-core/failed.lua +0 -83
  46. data/lib/qless/qless-core/get.lua +0 -37
  47. data/lib/qless/qless-core/heartbeat.lua +0 -51
  48. data/lib/qless/qless-core/jobs.lua +0 -41
  49. data/lib/qless/qless-core/pause.lua +0 -18
  50. data/lib/qless/qless-core/peek.lua +0 -165
  51. data/lib/qless/qless-core/pop.lua +0 -314
  52. data/lib/qless/qless-core/priority.lua +0 -32
  53. data/lib/qless/qless-core/put.lua +0 -169
  54. data/lib/qless/qless-core/qless-lib.lua +0 -2354
  55. data/lib/qless/qless-core/qless.lua +0 -1862
  56. data/lib/qless/qless-core/queues.lua +0 -58
  57. data/lib/qless/qless-core/recur.lua +0 -190
  58. data/lib/qless/qless-core/retry.lua +0 -73
  59. data/lib/qless/qless-core/stats.lua +0 -92
  60. data/lib/qless/qless-core/tag.lua +0 -100
  61. data/lib/qless/qless-core/track.lua +0 -79
  62. data/lib/qless/qless-core/unfail.lua +0 -54
  63. data/lib/qless/qless-core/unpause.lua +0 -12
  64. data/lib/qless/qless-core/workers.lua +0 -69
  65. data/lib/qless/wait_until.rb +0 -19
@@ -4,13 +4,13 @@
4
4
  <div class="row">
5
5
  <div class="span6">
6
6
  <h2 style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
7
- <a href="<%= u "/jobs/#{job.jid}" %>"><%= job.jid[0..8] %>...</a> | <%= job.klass_name %>
7
+ <a href="<%= u "/jobs/#{job.jid}" %>" title="<%= job.jid %>"><%= job.jid[0..8] %>...</a> | <span title="<%= job.klass_name %>"><%= job.klass_name %></span>
8
8
  </h2>
9
9
  </div>
10
10
  <div class="span3">
11
11
  <h2 style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
12
12
  <strong>
13
- | <%= job.state %> / <a href="<%= u "/queues/#{job.queue_name}" %>"><%= job.queue_name %></a><%= job.worker_name.nil? ? "/ #{job.worker_name}" : "" %>
13
+ | <%= job.state %> / <a href="<%= u "/queues/#{CGI::escape(job.queue_name)}" %>" title="<%= job.queue_name %><%= job.worker_name.nil? ? "/ #{job.worker_name}" : "" %>"><%= job.queue_name %></a><%= job.worker_name.nil? ? "/ #{job.worker_name}" : "" %>
14
14
  </strong>
15
15
  </h2>
16
16
  </div>
@@ -20,9 +20,12 @@
20
20
  <% if (job.state != "complete") %>
21
21
  <button title="delete" class="btn btn-danger" onclick="confirmation(this, 'Delete?', function() { cancel('<%= job.jid %>', fade) })"><i class="icon-remove"></i></button>
22
22
  <% end %>
23
+ <% if (job.state == "running") %>
24
+ <button title="Time out job" class="btn btn-danger" onclick="confirmation(this, 'Time out job?', function() { timeout('<%= job.jid %>') })"><i class="icon-time"></i></button>
25
+ <% end %>
23
26
  <button title="track" class="btn<%= job.tracked ? " active" : "" %>" data-toggle="button" onclick="$(this).hasClass('active') ? untrack('<%= job.jid %>', fade) : track('<%= job.jid %>', [], fade)"><i class="icon-flag"></i></button>
24
27
  <% if (job.state == 'failed') %>
25
- <button title="retry" class="btn btn-success" onclick="retry('<%= job.jid %>', fade)"><i class="icon-repeat"></i></button>
28
+ <button title="requeue" class="btn btn-success" onclick="retry('<%= job.jid %>', fade)"><i class="icon-repeat"></i></button>
26
29
  <% end %>
27
30
  <button title="move" class="btn dropdown-toggle btn-success" data-toggle="dropdown">
28
31
  <i class="caret"></i>
@@ -56,7 +59,7 @@
56
59
  <div style="float:left; margin-right: 10px"><h3>Dependencies:</h3></div>
57
60
  <% job.dependencies.each do |jid| %>
58
61
  <div class="btn-group" style="float:left; margin-right: 10px" id="<%= sanitize_attr("#{job.jid}-dependson-#{jid}") %>">
59
- <button class="btn" onclick="window.open('<%= u "/jobs/#{jid}" %>', '_blank')"><%= jid[0...8] %>...</button>
62
+ <button class="btn" onclick="window.open('<%= u "/jobs/#{jid}" %>', '_blank')" title="<%= jid %>"><%= jid[0...8] %>...</button>
60
63
  <button class="btn dropdown-toggle" onclick="confirmation(this, 'Undepend?', function() { undepend('<%= job.jid %>', '<%= jid %>', function() { $('#<%= sanitize_attr("#{job.jid}-dependson-#{jid}") %>').remove()} ); })">
61
64
  <i class="icon-remove"></i>
62
65
  </button>
@@ -72,7 +75,7 @@
72
75
  <div style="float:left; margin-right: 10px"><h3>Dependents:</h3></div>
73
76
  <% job.dependents.each do |jid| %>
74
77
  <div class="btn-group" style="float:left; margin-right: 10px" id="<%= sanitize_attr("#{job.jid}-dependents-#{jid}") %>">
75
- <button class="btn" onclick="window.open('<%= u "/jobs/#{jid}" %>', '_blank')"><%= jid[0...8] %>...</button>
78
+ <button class="btn" onclick="window.open('<%= u "/jobs/#{jid}" %>', '_blank')" title="<%= jid %>"><%= jid[0...8] %>...</button>
76
79
  <button class="btn dropdown-toggle" onclick="confirmation(this, 'Undepend?', function() { undepend('<%= jid %>', '<%= job.jid %>', function() { $('#<%= sanitize_attr("#{job.jid}-dependents-#{jid}") %>').remove()} ); })">
77
80
  <i class="icon-remove"></i>
78
81
  </button>
@@ -92,7 +95,7 @@
92
95
  </button>
93
96
  </div>
94
97
  <% end %>
95
-
98
+
96
99
  <!-- One for adding new tags -->
97
100
  <div class="btn-group" style="float:left">
98
101
  <input class="span1 add-tag" type="text" placeholder="Add Tag" onchange="tag('<%= job.jid %>', $(this).val())"></input>
@@ -113,11 +116,26 @@
113
116
  <h3><small>History</small></h3>
114
117
  <div style="overflow-y:scroll; height: 200px">
115
118
  <% job.queue_history.reverse.each do |h| %>
116
- <pre><strong><%= h['q'] %></strong>
117
- Put: <%= strftime(h['put']) %><% if not h['popped'].nil? %>
118
- Pop: <%= strftime(h['popped']) %> by <%= h['worker'] %><% end %><% if not h['completed'].nil? %>
119
- Completed: <%= strftime(h['completed']) %><% end %><% if not h['failed'].nil? %>
120
- Failed: <%= strftime(h['failed']) %><% end %></pre>
119
+ <% if h['what'] == 'put' %>
120
+ <pre><strong><%= h['what'] %></strong> at <%= strftime(h['when']) %>
121
+ in queue <strong><%= h['q'] %></strong></pre>
122
+ <% elsif h['what'] == 'popped' %>
123
+ <pre><strong><%= h['what'] %></strong> at <%= strftime(h['when']) %>
124
+ by <strong><%= h['worker'] %></strong></pre>
125
+ <% elsif h['what'] == 'done' %>
126
+ <pre><strong>completed</strong> at <%= strftime(h['when']) %></pre>
127
+ <% elsif h['what'] == 'failed' %>
128
+ <% if h['worker'] %>
129
+ <pre><strong><%= h['what'] %></strong> at <%= strftime(h['when']) %>
130
+ by <strong><%= h['worker'] %></strong>
131
+ in group <strong><%= h['group'] %></strong></pre>
132
+ <% else %>
133
+ <pre><strong><%= h['what'] %></strong> at <%= strftime(h['when']) %>
134
+ in group <strong><%= h['group'] %></strong></pre>
135
+ <% end %>
136
+ <% else %>
137
+ <pre><strong><%= h['what'] %></strong> at <%= strftime(h['when']) %></pre>
138
+ <% end %>
121
139
  <% end %>
122
140
  </div>
123
141
  </div>
@@ -144,13 +162,13 @@
144
162
  <div class="row">
145
163
  <div class="span6">
146
164
  <h2 style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
147
- <a href="<%= u "/jobs/#{job.jid}" %>"><%= job.jid[0..8] %>...</a> | <%= job.klass_name %>
165
+ <a href="<%= u "/jobs/#{job.jid}" %>" title="<%= job.jid %>"><%= job.jid[0..8] %>...</a> | <span title="<%= job.klass_name %>"><%= job.klass_name %></span>
148
166
  </h2>
149
167
  </div>
150
168
  <div class="span3">
151
169
  <h2 style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
152
170
  <strong>
153
- | recurring / <a href="<%= u "/queues/#{job.queue_name}" %>"><%= job.queue_name %></a>
171
+ | recurring / <a href="<%= u "/queues/#{CGI::escape(job.queue_name)}" %>" title="<%= job.queue_name %>"><%= job.queue_name %></a>
154
172
  </strong>
155
173
  </h2>
156
174
  </div>
@@ -183,7 +201,7 @@
183
201
  </div>
184
202
  </div>
185
203
  </div>
186
-
204
+
187
205
  <div class="row">
188
206
  <div class="span12 tags" style="margin-bottom: 3px;">
189
207
  <% job.tags.each do |tag| %>
@@ -194,7 +212,7 @@
194
212
  </button>
195
213
  </div>
196
214
  <% end %>
197
-
215
+
198
216
  <!-- One for adding new tags -->
199
217
  <div class="btn-group" style="float:left">
200
218
  <input class="span1 add-tag" type="text" placeholder="Add Tag" onchange="tag('<%= job.jid %>', $(this).val())"></input>
@@ -0,0 +1,11 @@
1
+ <% if jobs.empty? %>
2
+ <div class="page-header">
3
+ <h1>No Completed Jobs<small>(yet)</small></h1>
4
+ </div>
5
+ <% else %>
6
+ <div class="page-header">
7
+ <h1>Completed Jobs <small>You must be doing something right!</small></h1>
8
+ </div>
9
+ <% end %>
10
+
11
+ <%= erb :_job_list, :locals => { :jobs => jobs, :queues => queues } %>
@@ -12,13 +12,13 @@
12
12
  <link href="<%= u '/css/docs.css' %>" rel="stylesheet">
13
13
  <link href="<%= u '/css/jquery.noty.css' %>" rel="stylesheet">
14
14
  <link href="<%= u '/css/noty_theme_twitter.css' %>" rel="stylesheet">
15
- <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
15
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
16
16
 
17
17
  <style type="text/css">
18
18
  body {
19
19
  padding-top: 60px;
20
20
  }
21
-
21
+
22
22
  .btn-group span {
23
23
  /* This is ugly. Anyone want to change it? */
24
24
  border-color: #e6e6e6 #e6e6e6 #bfbfbf;
@@ -30,7 +30,7 @@
30
30
  line-height: 18px;
31
31
  padding: 4px 10px 4px;
32
32
  }
33
-
33
+
34
34
  .btn-group input, .btn-group span {
35
35
  position: relative;
36
36
  float: left;
@@ -40,7 +40,7 @@
40
40
  border-radius: 0;
41
41
  margin-bottom: 0px;
42
42
  }
43
-
43
+
44
44
  .btn-group input:first-child, .btn-group span:first-child {
45
45
  margin-left: 0;
46
46
  -webkit-border-top-left-radius: 4px;
@@ -50,6 +50,15 @@
50
50
  -moz-border-radius-bottomleft: 4px;
51
51
  border-bottom-left-radius: 4px;
52
52
  }
53
+
54
+ .large-text {
55
+ font-size:18px;
56
+ }
57
+
58
+ .queue-column {
59
+ min-width:300px;
60
+ }
61
+
53
62
  </style>
54
63
 
55
64
  <script type="text/javascript">
@@ -84,7 +93,7 @@
84
93
  url: '<%= u "/move" %>',
85
94
  data: {id:jid, queue:queue},
86
95
  success: function() { flash('Moved ' + jid + ' to ' + queue, 'success', 1500); cb(jid, queue); },
87
- erorr: function() { flash('Failed to move ' + jid + ' to ' + queue); }
96
+ error: function() { flash('Failed to move ' + jid + ' to ' + queue); }
88
97
  });
89
98
  }
90
99
 
@@ -94,7 +103,7 @@
94
103
  url: '<%= u "/retry" %>',
95
104
  data: {id:jid},
96
105
  success: function() { flash('Retrying ' + jid, 'success', 1500); if (cb) { cb(jid, 'retry'); } },
97
- erorr: function() { flash('Failed to retry ' + jid); }
106
+ error: function() { flash('Failed to retry ' + jid); }
98
107
  });
99
108
  }
100
109
 
@@ -198,13 +207,13 @@
198
207
  action();
199
208
  });
200
209
  }
201
-
210
+
202
211
  /* Helper function for adding a tag to a job */
203
212
  var tag = function(jid, tag) {
204
213
  var data = {};
205
214
  data[jid] = [tag];
206
215
  // The button group of the 'add tag' bit
207
- var group =
216
+ var group =
208
217
  _ajax({
209
218
  url: '<%= u "/tag" %>',
210
219
  data: data,
@@ -222,13 +231,13 @@
222
231
  }
223
232
  });
224
233
  }
225
-
234
+
226
235
  /* Helper function for untagging a job */
227
236
  var untag = function(jid, tag) {
228
237
  var data = {};
229
238
  data[jid] = [tag];
230
239
  // The button group of the 'add tag' bit
231
- var group =
240
+ var group =
232
241
  _ajax({
233
242
  url: '<%= u "/untag" %>',
234
243
  data: data,
@@ -241,7 +250,7 @@
241
250
  }
242
251
  });
243
252
  }
244
-
253
+
245
254
  /* Helper function for changing a job's priority */
246
255
  var priority = function(jid, priority) {
247
256
  var p = parseInt(priority);
@@ -272,6 +281,56 @@
272
281
  }
273
282
  }
274
283
 
284
+ var pause = function(queue) {
285
+ _ajax({
286
+ url: '<%= u "/pause" %>',
287
+ data: {
288
+ 'queue': queue
289
+ }, success: function(data) {
290
+ var button = $('#' + queue + '-pause');
291
+ button.attr('title', 'Unpause').attr(
292
+ 'data-original-title', 'Unpause');
293
+ button.addClass('btn-success').removeClass('btn-warning');
294
+ button.children().addClass('icon-play').removeClass('icon-pause');
295
+ button.attr('onclick', 'unpause("' + queue + '")');
296
+ }, error: function() {
297
+ flash('Couldn\'t pause queue ' + queue);
298
+ }
299
+ })
300
+ }
301
+
302
+ var unpause = function(queue) {
303
+ _ajax({
304
+ url: '<%= u "/unpause" %>',
305
+ data: {
306
+ 'queue': queue
307
+ }, success: function(data) {
308
+ var button = $('#' + queue + '-pause');
309
+ button.attr('title', 'Pause').attr(
310
+ 'data-original-title', 'Pause');
311
+ button.addClass('btn-warning').removeClass('btn-success');
312
+ button.children().addClass('icon-pause').removeClass('icon-play');
313
+ button.attr('onclick', 'pause("' + queue + '")');
314
+ }, error: function() {
315
+ flash('Couldn\'t unpause queue ' + queue);
316
+ }
317
+ })
318
+ }
319
+
320
+ var timeout = function(jid) {
321
+ _ajax({
322
+ url: '<%= u "/timeout" %>',
323
+ data: {
324
+ 'jid': jid
325
+ }, success: function(data) {
326
+ flash('Job timed out', 'success');
327
+ }, error: function(data) {
328
+ flash('Failed to time out job: ' + data);
329
+ console.log(data);
330
+ }
331
+ })
332
+ }
333
+
275
334
  $(document).ready(function() {
276
335
  $('button').tooltip({delay:200});
277
336
  });
@@ -7,23 +7,49 @@
7
7
  <h1>Queues <small>And their job counts</small></h1>
8
8
  </div>
9
9
 
10
- <% queues.each do |queue| %>
11
- <div class="row">
12
- <div class="span4">
13
- <h3><a href="<%= u "/queues/#{queue['name']}" %>"><%= queue['name'] %></a></h3>
14
- </div>
15
- <div class="span8">
16
- <h3> |
17
- <%= queue['running'] %> /
18
- <%= queue['waiting'] %> /
19
- <%= queue['scheduled'] %> /
20
- <%= queue['stalled'] %> /
21
- <%= queue['depends'] %> /
22
- <%= queue['recurring'] %> <small>(running / waiting / scheduled / stalled / depends / recurring)</small>
23
- </h3>
24
- </div>
25
- </div>
10
+ <table class="table">
11
+ <thead>
12
+ <tr>
13
+ <th></th>
14
+ <th>running</th>
15
+ <th>waiting</th>
16
+ <th>scheduled</th>
17
+ <th>stalled</th>
18
+ <th>depends</th>
19
+ <th>recurring</th>
20
+ </tr>
21
+ </thead>
22
+ <tbody>
23
+ <% queues.each do |queue| %>
24
+ <tr class="queue-row">
25
+ <td class="queue-column large-text">
26
+ <% if queue['paused'] %>
27
+ <button
28
+ id="<%= queue['name'] %>-pause"
29
+ title="Unpause"
30
+ class="btn btn-success"
31
+ onclick="unpause('<%= queue['name'] %>')"><i class="icon-play"></i>
32
+ </button>
33
+ <% else %>
34
+ <button
35
+ id="<%= queue['name'] %>-pause"
36
+ title="Pause"
37
+ class="btn btn-warning"
38
+ onclick="pause('<%= queue['name'] %>')"><i class="icon-pause"></i>
39
+ </button>
40
+ <% end %>
41
+ <a href="<%= u "/queues/#{CGI::escape(queue['name'])}" %>"><%= queue['name'] %></a>
42
+ </td>
43
+ <td><%= queue['running'] %></td>
44
+ <td><%= queue['waiting'] %></td>
45
+ <td><%= queue['scheduled'] %></td>
46
+ <td><%= queue['stalled'] %></td>
47
+ <td><%= queue['depends'] %></td>
48
+ <td><%= queue['recurring'] %></td>
49
+ </tr>
26
50
  <% end %>
51
+ </tbody>
52
+ </table>
27
53
  <% end %>
28
54
 
29
55
  <% if failed.empty? %>
@@ -34,22 +60,23 @@
34
60
  <div class="page-header">
35
61
  <h1>Failed Jobs <small>D'oh!</small></h1>
36
62
  </div>
37
- <div class="l-sidebyside failed-job">
38
- <div class="min-col-size">
39
- <% failed.sort_by { |t, count| -count }.each do |t, count| %>
40
- <div class="row">
41
- <h3><a href="<%= u "/failed/#{t}" %>"><%= t %></a></h3>
42
- </div>
43
- <% end %>
44
- </div>
45
- <div>
46
- <% failed.sort_by { |t, count| -count }.each do |t, count| %>
47
- <div class="row">
48
- <h3>| <%= count %> <small>Jobs</small></h3>
49
- </div>
50
- <% end %>
51
- </div>
52
- </div>
63
+
64
+ <table class="table">
65
+ <thead>
66
+ <tr>
67
+ <th>failure</th>
68
+ <th>count</th>
69
+ </tr>
70
+ </thead>
71
+ <tbody>
72
+ <% failed.sort_by { |t, count| -count }.each do |t, count| %>
73
+ <tr class="failed-row">
74
+ <td class="large-text"><a href="<%= u "/failed/#{t}" %>"><%= t %></a></td>
75
+ <td><%= count %></td>
76
+ </tr>
77
+ <% end %>
78
+ </tbody>
79
+ </table>
53
80
  <% end %>
54
81
 
55
82
  <% if tracked['jobs'].empty? %>
@@ -61,16 +88,22 @@
61
88
  <h1>Tracked Jobs <small>These <i>are</i> the droids you're looking for</small></h1>
62
89
  </div>
63
90
  <% counts = Hash.new; tracked['jobs'].each { |job| counts[job.state] ||= 0; counts[job.state] += 1 } %>
64
- <% counts.sort_by { |state, count| - count }.each do |state, count| %>
65
- <div class="row">
66
- <div class="span4">
67
- <h3><a href="<%= u "/track##{state}" %>"><%= state %></a></h3>
68
- </div>
69
- <div class="span8">
70
- <h3>| <%= count %> <small>Jobs</small></h3>
71
- </div>
72
- </div>
73
- <% end %>
91
+ <table class="table">
92
+ <thead>
93
+ <tr>
94
+ <th>state</th>
95
+ <th>count</th>
96
+ </tr>
97
+ </thead>
98
+ <tbody>
99
+ <% counts.sort_by { |state, count| - count }.each do |state, count| %>
100
+ <tr class="tracked-row">
101
+ <td class="large-text"><a href="<%= u "/track##{state}" %>"><%= state %></a></td>
102
+ <td><%= count %></td>
103
+ </tr>
104
+ <% end %>
105
+ </tbody>
106
+ </table>
74
107
  <% end %>
75
108
 
76
109
  <% if workers.empty? %>
@@ -81,15 +114,22 @@
81
114
  <div class="page-header">
82
115
  <h1>Current Workers <small>And their job counts</small></h1>
83
116
  </div>
84
-
85
- <% workers.each do |worker| %>
86
- <div class="row">
87
- <div class="span4">
88
- <h3><a href="<%= u "/workers/#{worker['name']}" %>"><%= worker['name'] %></a></h3>
89
- </div>
90
- <div class="span8">
91
- <h3>| <%= worker['jobs'] %> / <%= worker['stalled'] %> <small>Running / Stalled</small></h3>
92
- </div>
93
- </div>
94
- <% end %>
117
+ <table class="table">
118
+ <thead>
119
+ <tr>
120
+ <th></th>
121
+ <th>running</th>
122
+ <th>stalled</th>
123
+ </tr>
124
+ </thead>
125
+ <tbody>
126
+ <% workers.each do |worker| %>
127
+ <tr class='worker-row'>
128
+ <td class="large-text"><a href="<%= u "/workers/#{worker['name']}" %>"><%= worker['name'] %></a></td>
129
+ <td><%= worker['jobs'] %></td>
130
+ <td><%= worker['stalled'] %></td>
131
+ </tr>
132
+ <% end %>
133
+ </tbody>
134
+ </table>
95
135
  <% end %>