sidekiq-undertaker 1.1.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby-build.yml +31 -3
  3. data/.rubocop.yml +4 -0
  4. data/.rubocop_codeclimate.yml +9 -0
  5. data/CHANGELOG.md +19 -0
  6. data/README.md +8 -6
  7. data/assets/Undertaker_demo_1_job_1_error.png +0 -0
  8. data/assets/Undertaker_demo_1_job_all_errors.png +0 -0
  9. data/assets/Undertaker_demo_all_errors.png +0 -0
  10. data/assets/Undertaker_demo_all_jobs.png +0 -0
  11. data/assets/Undertaker_demo_all_jobs_1_error.png +0 -0
  12. data/lib/sidekiq/undertaker/dead_job.rb +10 -2
  13. data/lib/sidekiq/undertaker/job_distributor.rb +4 -0
  14. data/lib/sidekiq/undertaker/job_filter.rb +7 -2
  15. data/lib/sidekiq/undertaker/version.rb +1 -1
  16. data/lib/sidekiq/undertaker/web_extension/api_helpers.rb +104 -26
  17. data/lib/sidekiq/undertaker/web_extension.rb +17 -8
  18. data/sidekiq-undertaker.gemspec +6 -5
  19. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_filter/when_filter_page_is_called/behaves_like_a_page/the_displayed_page_is_correct_for_sidekiqv6.approved.txt +32 -18
  20. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_filter/when_job_classbucket_page_is_called/behaves_like_a_page/the_displayed_page_is_correct_for_sidekiqv6.approved.txt +37 -39
  21. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_filter/when_job_classbucket_page_is_polled/behaves_like_a_page/the_displayed_page_is_correct_for_sidekiqv6.approved.txt +37 -39
  22. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_filter/when_job_classerror_classbucket_page_is_called/behaves_like_a_page/the_displayed_page_is_correct_for_sidekiqv6.approved.txt +231 -0
  23. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_filter/when_job_classerror_classbucket_page_is_polled/behaves_like_a_page/the_displayed_page_is_correct_for_sidekiqv6.approved.txt +231 -0
  24. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_morgue/when_job_classerrorbucket_is_called/with_all_failures_and_errors/behaves_like_a_page/the_displayed_page_is_correct_for_sidekiqv6.approved.txt +26 -21
  25. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_morgue/when_job_classerrorbucket_is_called/with_job_class_error_and_specific_error_message/behaves_like_a_page/the_displayed_page_is_correct_for_sidekiqv6.approved.txt +284 -0
  26. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_morgue/when_job_classerrorbucket_is_called/with_job_class_error_and_specific_error_message/with_pagination/behaves_like_a_page/the_displayed_page_is_correct_for_sidekiqv6.approved.txt +1310 -0
  27. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_morgue/when_job_classerrorbucket_is_called/with_specific_job_class_and_a_specific_error/behaves_like_a_page/the_displayed_page_is_correct_for_sidekiqv6.approved.txt +24 -19
  28. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_morgue/when_job_classerrorbucket_is_called/with_specific_job_class_and_a_specific_error/with_pagination/behaves_like_a_page/the_displayed_page_is_correct_for_sidekiqv6.approved.txt +72 -67
  29. data/spec/sidekiq/undertaker/dead_jobs_spec.rb +7 -4
  30. data/spec/sidekiq/undertaker/job_distributor_spec.rb +41 -19
  31. data/spec/sidekiq/undertaker/job_filter_spec.rb +26 -9
  32. data/spec/sidekiq/undertaker/web_extension_spec.rb +172 -14
  33. data/spec/spec_helper.rb +4 -0
  34. data/web/locales/en.yml +2 -0
  35. data/web/views/filter.erb +17 -1
  36. data/web/views/filter_job_class.erb +8 -8
  37. data/web/views/filter_job_class_error_class.erb +36 -0
  38. data/web/views/morgue.erb +9 -2
  39. metadata +38 -30
  40. data/.travis.yml +0 -51
  41. data/Demo_Filter.png +0 -0
  42. data/Demo_Job_Filter.png +0 -0
  43. data/Demo_Morgue_1_Job.png +0 -0
  44. data/Demo_Morgue_all.png +0 -0
@@ -3,40 +3,45 @@
3
3
  require "spec_helper"
4
4
 
5
5
  module Sidekiq
6
+ # rubocop:disable Metrics/ModuleLength
6
7
  module Undertaker
7
8
  describe JobDistributor do
8
9
  let(:job1) do
9
10
  instance_double(Sidekiq::JobRecord, item: {
10
- "class" => "A",
11
- "failed_at" => 1,
12
- "error_class" => "E1"
11
+ "class" => "A",
12
+ "failed_at" => 1,
13
+ "error_class" => "E1",
14
+ "error_message" => "M1"
13
15
  })
14
16
  end
15
17
  let(:job2) do
16
18
  instance_double(Sidekiq::JobRecord, item: {
17
- "class" => "A",
18
- "failed_at" => 1,
19
- "error_class" => "E1"
19
+ "class" => "A",
20
+ "failed_at" => 1,
21
+ "error_class" => "E1",
22
+ "error_message" => "M1"
20
23
  })
21
24
  end
22
25
  let(:job3) do
23
26
  instance_double(Sidekiq::JobRecord, item: {
24
- "class" => "B",
25
- "failed_at" => 1,
26
- "error_class" => "E1"
27
+ "class" => "B",
28
+ "failed_at" => 1,
29
+ "error_class" => "E1",
30
+ "error_message" => "M2"
27
31
  })
28
32
  end
29
33
  let(:job4) do
30
34
  instance_double(Sidekiq::JobRecord, item: {
31
- "class" => "B",
32
- "failed_at" => 1,
33
- "error_class" => "E2"
35
+ "class" => "B",
36
+ "failed_at" => 1,
37
+ "error_class" => "E2",
38
+ "error_message" => "M1"
34
39
  })
35
40
  end
36
41
 
37
42
  let(:dead_job1) do
38
43
  DeadJob.new(
39
- job: job1, # 'A', 'E1'
44
+ job: job1, # 'A', 'E1', 'M1'
40
45
  time_elapsed_since_failure: 10,
41
46
  bucket_name: "1_hour"
42
47
  )
@@ -44,22 +49,22 @@ module Sidekiq
44
49
 
45
50
  let(:dead_job2) do
46
51
  DeadJob.new(
47
- job: job2, # 'A', 'E1'
48
- time_elapsed_since_failure: 10 + 60 * 60,
52
+ job: job2, # 'A', 'E1', 'M1'
53
+ time_elapsed_since_failure: 10 + (60 * 60),
49
54
  bucket_name: "3_hours"
50
55
  )
51
56
  end
52
57
  let(:dead_job3) do
53
58
  DeadJob.new(
54
- job: job3, # 'B', 'E1'
55
- time_elapsed_since_failure: 10 + 60 * 60,
59
+ job: job3, # 'B', 'E1', 'M2'
60
+ time_elapsed_since_failure: 10 + (60 * 60),
56
61
  bucket_name: "3_hours"
57
62
  )
58
63
  end
59
64
  let(:dead_job4) do
60
65
  DeadJob.new(
61
- job: job4, # 'B', 'E2'
62
- time_elapsed_since_failure: 10 + 60 * 60 * 24,
66
+ job: job4, # 'B', 'E2', 'M1'
67
+ time_elapsed_since_failure: 10 + (60 * 60 * 24),
63
68
  bucket_name: "1_day"
64
69
  )
65
70
  end
@@ -98,7 +103,24 @@ module Sidekiq
98
103
  expect(distribution).to eq expected_distribution
99
104
  end
100
105
  end
106
+
107
+ describe "#group_by_error_msg" do
108
+ subject(:distribution) { described_class.new(dead_jobs).group_by_error_msg }
109
+
110
+ let(:expected_distribution) do
111
+ [
112
+ ["all", { "1_hour" => 1, "3_hours" => 2, "1_day" => 1, "3_days" => 0, "1_week" => 0, "older" => 0, "total_dead" => 4 }],
113
+ ["M1", { "1_hour" => 1, "3_hours" => 1, "1_day" => 1, "3_days" => 0, "1_week" => 0, "older" => 0, "total_dead" => 3 }],
114
+ ["M2", { "1_hour" => 0, "3_hours" => 1, "1_day" => 0, "3_days" => 0, "1_week" => 0, "older" => 0, "total_dead" => 1 }]
115
+ ]
116
+ end
117
+
118
+ it "distributes the dead jobs into buckets and groups them by error_msg" do
119
+ expect(distribution).to eq expected_distribution
120
+ end
121
+ end
101
122
  # rubocop:enable Layout/LineLength
102
123
  end
103
124
  end
125
+ # rubocop:enable Metrics/ModuleLength
104
126
  end
@@ -8,25 +8,28 @@ module Sidekiq
8
8
  describe ".filter_dead_jobs" do
9
9
  let(:job1) do
10
10
  instance_double(Sidekiq::JobRecord, item: {
11
- "class" => "HardWorkTask",
12
- "failed_at" => Time.now.to_i - 5 * 60,
13
- "error_class" => "NoMethodError"
11
+ "class" => "HardWorkTask",
12
+ "failed_at" => Time.now.to_i - (5 * 60),
13
+ "error_class" => "NoMethodError",
14
+ "error_message" => "undefined method `pause` for H..."
14
15
  })
15
16
  end
16
17
 
17
18
  let(:job2) do
18
19
  instance_double(Sidekiq::JobRecord, item: {
19
- "class" => "HardWorkTask",
20
- "failed_at" => Time.now.to_i - 2 * 60 * 60,
21
- "error_class" => "RandomError"
20
+ "class" => "HardWorkTask",
21
+ "failed_at" => Time.now.to_i - (2 * 60 * 60),
22
+ "error_class" => "RandomError",
23
+ "error_message" => "random error message"
22
24
  })
23
25
  end
24
26
 
25
27
  let(:job3) do
26
28
  instance_double(Sidekiq::JobRecord, item: {
27
- "class" => "LazyWorkTask",
28
- "failed_at" => Time.now.to_i - 2 * 60 * 60,
29
- "error_class" => "NoMethodError"
29
+ "class" => "LazyWorkTask",
30
+ "failed_at" => Time.now.to_i - (2 * 60 * 60),
31
+ "error_class" => "NoMethodError",
32
+ "error_message" => "undefined method `work_hard` for LazyWork:Class"
30
33
  })
31
34
  end
32
35
 
@@ -85,6 +88,20 @@ module Sidekiq
85
88
  it { expect(dead_jobs.size).to eq 2 }
86
89
  end
87
90
 
91
+ context "when the error_msg filter is given" do
92
+ subject(:dead_jobs) do
93
+ described_class.filter_dead_jobs("error_msg" => "undefined method `pause` for H...")
94
+ end
95
+
96
+ it "filters jobs based on error_message" do
97
+ dead_jobs.each do |dead_job|
98
+ expect(dead_job.error_msg).to eq "undefined method `pause` for H..."
99
+ end
100
+ end
101
+
102
+ it { expect(dead_jobs.size).to eq 1 }
103
+ end
104
+
88
105
  context "when no filters are applied" do
89
106
  subject(:dead_jobs) { described_class.filter_dead_jobs }
90
107
 
@@ -6,6 +6,7 @@ require "sidekiq/api"
6
6
  require "sidekiq/web"
7
7
  require "sinatra"
8
8
  require "rack/test"
9
+ require "stringio"
9
10
 
10
11
  module Sidekiq
11
12
  # rubocop:disable Metrics/ModuleLength
@@ -32,13 +33,15 @@ module Sidekiq
32
33
  "class" => "HardWorker",
33
34
  "args" => ["asdf", 1234],
34
35
  "queue" => "foo",
35
- "error_message" => "Some fake message",
36
+ "error_message" => "Option 'data/file_name' is required! This is an extra long error message.",
36
37
  "error_class" => "RuntimeError",
37
38
  "retry_count" => 0,
38
39
  "failed_at" => Time.now.utc
39
40
  }
40
41
  end
41
42
 
43
+ let(:encoded_error_msg) { "T3B0aW9uICdkYXRhL2ZpbGVfbmFtZScgaXMgcmVxLi4u" }
44
+
42
45
  # rubocop:disable RSpec/AnyInstance
43
46
  before do
44
47
  Timecop.freeze(Time.gm(2018, 12, 16, 20, 57))
@@ -100,6 +103,20 @@ module Sidekiq
100
103
 
101
104
  it_behaves_like "a page"
102
105
  end
106
+
107
+ # /undertaker/filter/:job_class/:error_class/:bucket_name
108
+ context "when job-class/error-class/bucket page is called" do
109
+ subject { get "/undertaker/filter/HardWorker/RuntimeError/1_hour" }
110
+
111
+ it_behaves_like "a page"
112
+ end
113
+
114
+ # /undertaker/filter/:job_class/:error_class/:bucket_name?poll=true
115
+ context "when job-class/error-class/bucket page is polled" do
116
+ subject { get "/undertaker/filter/HardWorker/RuntimeError/1_hour?poll=true" }
117
+
118
+ it_behaves_like "a page"
119
+ end
103
120
  end
104
121
 
105
122
  describe "show morgue" do
@@ -107,10 +124,28 @@ module Sidekiq
107
124
  allow_any_instance_of(Sidekiq::Web::CsrfProtection).to receive(:mask_token).and_return("stubbed-csrf-token")
108
125
  end
109
126
 
110
- # /undertaker/morgue/:job_class/:error_class/:bucket_name
127
+ # /undertaker/morgue/:job_class/:error_class/:error_msg/:bucket_name
111
128
  context "when job-class/error/bucket is called" do
112
129
  context "with specific job-class and a specific error" do
113
- subject { get "/undertaker/morgue/HardWorker/RuntimeError/1_hour" }
130
+ subject { get "/undertaker/morgue/HardWorker/RuntimeError/all/1_hour" }
131
+
132
+ it_behaves_like "a page"
133
+
134
+ context "with pagination" do
135
+ before do
136
+ 50.times do |i|
137
+ job_refs.push add_dead("jid" => i.to_s)
138
+ end
139
+ end
140
+
141
+ it_behaves_like "a page"
142
+ end
143
+ end
144
+
145
+ context "with job-class, error and specific error message" do
146
+ subject do
147
+ get "/undertaker/morgue/HardWorker/RuntimeError/#{encoded_error_msg}/1_hour"
148
+ end
114
149
 
115
150
  it_behaves_like "a page"
116
151
 
@@ -126,7 +161,7 @@ module Sidekiq
126
161
  end
127
162
 
128
163
  context "with all failures and errors" do
129
- subject { get "/undertaker/morgue/all/all/total_dead" }
164
+ subject { get "/undertaker/morgue/all/all/all/total_dead" }
130
165
 
131
166
  it_behaves_like "a page"
132
167
  end
@@ -135,12 +170,17 @@ module Sidekiq
135
170
  # rubocop:enable RSpec/AnyInstance
136
171
 
137
172
  describe "delete" do
138
- context "when job-class, error and bucket are given" do
139
- subject { post "/undertaker/morgue/HardWorker/RuntimeError/1_hour/delete" }
173
+ context "when job-class, error, error message and bucket are given" do
174
+ subject do
175
+ post "/undertaker/morgue/HardWorker/RuntimeError/#{encoded_error_msg}/1_hour/delete"
176
+ end
140
177
 
141
- let(:expected_redirect_url) { "http://example.org/undertaker/morgue/HardWorker/RuntimeError/1_hour" }
178
+ let(:expected_redirect_url) { "http://example.org/undertaker/morgue/HardWorker/RuntimeError/#{encoded_error_msg}/1_hour" }
142
179
 
143
- let(:params) { { "job_class" => "HardWorker", "error_class" => "RuntimeError", "bucket_name" => "1_hour" } }
180
+ let(:params) do
181
+ { "job_class" => "HardWorker", "error_class" => "RuntimeError", "bucket_name" => "1_hour",
182
+ "error_msg" => "Option 'data/file_name' is req..." }
183
+ end
144
184
  let(:dead_jobs_set) { [dead_job1, dead_job2] }
145
185
  let(:dead_job1) { Sidekiq::Undertaker::DeadJob.to_dead_job(Sidekiq::DeadSet.new.find_job(jid1)) }
146
186
  let(:dead_job2) { Sidekiq::Undertaker::DeadJob.to_dead_job(Sidekiq::DeadSet.new.find_job(jid2)) }
@@ -160,7 +200,7 @@ module Sidekiq
160
200
  expect { subject }.to change { Sidekiq::DeadSet.new.size }.from(4).to(2)
161
201
  end
162
202
 
163
- it "redirects to /undertaker/morgue/HardWorker/RuntimeError/1_hour after the delete" do
203
+ it "redirects to morgue view after the delete" do
164
204
  subject
165
205
  expect(last_response.status).to eq 302
166
206
 
@@ -205,13 +245,77 @@ module Sidekiq
205
245
  end
206
246
  end
207
247
 
248
+ describe "import" do
249
+ subject { post "/undertaker/import_jobs", "upload_file" => file }
250
+
251
+ let(:file) do
252
+ Rack::Test::UploadedFile.new(StringIO.new(file_content), file_content_type, original_filename: file_name)
253
+ end
254
+ let(:job) do
255
+ opts = default_job_opts.merge({ "class" => "SuperHardWorking" })
256
+
257
+ build_job(opts)
258
+ end
259
+
260
+ context "when the file is valid" do
261
+ let(:file_content) { [job.item].to_json }
262
+ let(:file_name) { "jobs.json" }
263
+ let(:file_content_type) { "application/json" }
264
+
265
+ it "redirects the response" do
266
+ subject
267
+ expect(last_response.status).to eq 302
268
+ end
269
+
270
+ it "adds the jobs to the deadset" do
271
+ expect { subject }.to change { Sidekiq::DeadSet.new.size }.from(4).to(5)
272
+ end
273
+ end
274
+
275
+ context "when the file type is not valid" do
276
+ let(:file_content) { "" }
277
+ let(:file_name) { "jobs.zip" }
278
+ let(:file_content_type) { "application/zip" }
279
+
280
+ it "returns status 400" do
281
+ subject
282
+ expect(last_response.status).to eq 400
283
+ end
284
+ end
285
+
286
+ context "when the file type is a json but not a Sidekiq Job" do
287
+ let(:file_content) { "{am_i_a_job: \"no\"}" }
288
+ let(:file_name) { "jobs.json" }
289
+ let(:file_content_type) { "application/json" }
290
+
291
+ it "returns status 400" do
292
+ subject
293
+ expect(last_response.status).to eq 400
294
+ end
295
+ end
296
+
297
+ context "when the content of the file is not a json" do
298
+ let(:file_content) { "DEFINETLY NOT A JSON" }
299
+ let(:file_name) { "jobs.json" }
300
+ let(:file_content_type) { "application/json" }
301
+
302
+ it "returns status 400" do
303
+ subject
304
+ expect(last_response.status).to eq 400
305
+ end
306
+ end
307
+ end
308
+
208
309
  describe "retry" do
209
310
  context "when job class, error and bucket are given" do
210
- subject { post "/undertaker/morgue/HardWorker/RuntimeError/1_hour/retry" }
311
+ subject { post "/undertaker/morgue/HardWorker/RuntimeError/all/1_hour/retry" }
211
312
 
212
- let(:expected_redirect_url) { "http://example.org/undertaker/morgue/HardWorker/RuntimeError/1_hour" }
313
+ let(:expected_redirect_url) { "http://example.org/undertaker/morgue/HardWorker/RuntimeError/all/1_hour" }
213
314
 
214
- let(:params) { { "job_class" => "HardWorker", "error_class" => "RuntimeError", "bucket_name" => "1_hour" } }
315
+ let(:params) do
316
+ { "job_class" => "HardWorker", "error_class" => "RuntimeError", "bucket_name" => "1_hour",
317
+ "error_msg" => "all" }
318
+ end
215
319
  let(:dead_jobs_set) { [dead_job1, dead_job2] }
216
320
  let(:dead_job1) { Sidekiq::Undertaker::DeadJob.to_dead_job(Sidekiq::DeadSet.new.find_job(jid1)) }
217
321
  let(:dead_job2) { Sidekiq::Undertaker::DeadJob.to_dead_job(Sidekiq::DeadSet.new.find_job(jid2)) }
@@ -262,6 +366,51 @@ module Sidekiq
262
366
  end
263
367
  end
264
368
 
369
+ describe "export" do
370
+ context "when job class, error and bucket are given" do
371
+ subject { post "/undertaker/morgue/HardWorker/RuntimeError/all/1_hour/export" }
372
+
373
+ let(:expected_redirect_url) { "http://example.org/undertaker/morgue/HardWorker/RuntimeError/all/1_hour" }
374
+ let(:expected_content_disposition_header) { "attachment; filename=\"2018-12-16_20-57.json\"" }
375
+
376
+ let(:params) do
377
+ { "job_class" => "HardWorker", "error_class" => "RuntimeError", "bucket_name" => "1_hour",
378
+ "error_msg" => "all" }
379
+ end
380
+ let(:dead_jobs_set) { [dead_job1, dead_job2] }
381
+ let(:dead_job1) { Sidekiq::Undertaker::DeadJob.to_dead_job(Sidekiq::DeadSet.new.find_job(jid1)) }
382
+ let(:dead_job2) { Sidekiq::Undertaker::DeadJob.to_dead_job(Sidekiq::DeadSet.new.find_job(jid2)) }
383
+
384
+ before do
385
+ allow(Sidekiq::Undertaker::JobFilter).to receive(:filter_dead_jobs).with(params)
386
+ .and_return(dead_jobs_set)
387
+ end
388
+
389
+ it "exports the dead jobs" do
390
+ subject
391
+ expect(last_response.status).to eq 200
392
+ expect(last_response.content_type).to eq "application/json"
393
+ expect(last_response.headers["Content-Disposition"]).to eq expected_content_disposition_header
394
+ expect(last_response.body).to eq dead_jobs_set.map { |t| t.job.item }.to_json
395
+ end
396
+
397
+ context "when there are more jobs than the current CHUNK_SIZE" do
398
+ before do
399
+ stub_const("Sidekiq::Undertaker::WebExtension::APIHelpers::EXPORT_CHUNK_SIZE", 1)
400
+ end
401
+
402
+ let(:expected_content_disposition_header) { "attachment; filename=\"HardWorker_2018-12-16_20-57.zip\"" }
403
+
404
+ it "exports the dead jobs" do
405
+ subject
406
+ expect(last_response.status).to eq 200
407
+ expect(last_response.content_type).to eq "application/zip"
408
+ expect(last_response.headers["Content-Disposition"]).to eq expected_content_disposition_header
409
+ end
410
+ end
411
+ end
412
+ end
413
+
265
414
  describe "specific jobs" do
266
415
  let(:dead_job) { Sidekiq::DeadSet.new.find_job(jid1) }
267
416
 
@@ -282,12 +431,21 @@ module Sidekiq
282
431
  post "/undertaker/morgue", "key[]=#{job_refs[0]}&retry=Retry+Now"
283
432
  end
284
433
 
434
+ it "exports specific dead job now" do
435
+ post "/undertaker/morgue", "key[]=#{job_refs[0]}&export=now"
436
+
437
+ expect(last_response.status).to eq 200
438
+ expect(last_response.content_type).to eq "application/json"
439
+ expect(last_response.headers["Content-Disposition"]).to eq "attachment; filename=\"2018-12-16_20-57.json\""
440
+ expect(last_response.body).to eq [dead_job.item].to_json
441
+ end
442
+
285
443
  it "redirects on specific retry post" do
286
444
  post("/undertaker/morgue",
287
445
  "key[]=#{job_refs[0]}&retry=Retry+Now",
288
- "HTTP_REFERER" => "/undertaker/morgue/all/all/total_dead")
446
+ "HTTP_REFERER" => "/undertaker/morgue/all/all/all/total_dead")
289
447
  expect(last_response.status).to eq 302
290
- expect(last_response.header["Location"]).to include("/undertaker/morgue/all/all/total_dead")
448
+ expect(last_response.header["Location"]).to include("/undertaker/morgue/all/all/all/total_dead")
291
449
  end
292
450
  end
293
451
  end
data/spec/spec_helper.rb CHANGED
@@ -19,6 +19,10 @@ require "timecop"
19
19
 
20
20
  if ENV["COVERAGE"]
21
21
  require "simplecov"
22
+ require "simplecov_json_formatter"
23
+
24
+ SimpleCov.formatter = SimpleCov::Formatter::JSONFormatter
25
+
22
26
  SimpleCov.start do
23
27
  add_group "lib", "lib"
24
28
  add_group "spec", "spec"
data/web/locales/en.yml CHANGED
@@ -3,3 +3,5 @@ en:
3
3
  UndertakerBuryAll: Bury All
4
4
  UndertakerRevive: Revive
5
5
  UndertakerReviveAll: Revive All
6
+ UndertakerExport: Export
7
+ UndertakerExportAll: Export All
data/web/views/filter.erb CHANGED
@@ -21,7 +21,7 @@
21
21
  </thead>
22
22
  <% @distribution.each do |group, bucket_counts| %>
23
23
  <tr>
24
- <td><a href='<%= "#{root_path}undertaker/morgue/#{group}/all/total_dead" %>'> <%= group %></a></td>
24
+ <td><a href='<%= "#{root_path}undertaker/morgue/#{group}/all/all/total_dead" %>'> <%= group %></a></td>
25
25
  <td><a href='<%= "#{root_path}undertaker/filter/#{group}/total_dead" %>'><%= bucket_counts['total_dead']%></a></td>
26
26
  <td><a href='<%= "#{root_path}undertaker/filter/#{group}/1_hour" %>'><%= bucket_counts['1_hour']%></a></td>
27
27
  <td><a href='<%= "#{root_path}undertaker/filter/#{group}/3_hours" %>'><%= bucket_counts['3_hours']%></a></td>
@@ -32,3 +32,19 @@
32
32
  </tr>
33
33
  <% end %>
34
34
  </table>
35
+
36
+ <header class="row header">
37
+ <div class="col-sm-12">
38
+ <h3>
39
+ Import Jobs
40
+ </h3>
41
+ </div>
42
+ </header>
43
+
44
+ <form enctype="multipart/form-data" method="post" action='<%="#{root_path}undertaker/import_jobs"%>'>
45
+ <%= respond_to?(:csrf_tag) && csrf_tag %>
46
+ <input type="file" id=upload_file" name="upload_file" >
47
+ <button class="btn btn-danger" style="margin-top: 10px" type="submit">
48
+ Import
49
+ </button>
50
+ </form>
@@ -22,14 +22,14 @@
22
22
  </thead>
23
23
  <% @distribution.each do |group, bucket_counts| %>
24
24
  <tr>
25
- <td><a href='<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{group}/total_dead" %>'><%= group %></a></td>
26
- <td><a href='<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{group}/total_dead" %>'><%= bucket_counts['total_dead'] %></a></td>
27
- <td><a href='<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{group}/1_hour" %>'><%= bucket_counts['1_hour']%></a></td>
28
- <td><a href='<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{group}/3_hours" %>'><%= bucket_counts['3_hours']%></a></td>
29
- <td><a href='<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{group}/1_day" %>'><%= bucket_counts['1_day']%></a></td>
30
- <td><a href='<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{group}/3_days" %>'><%= bucket_counts['3_days']%></a></td>
31
- <td><a href='<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{group}/1_week" %>'><%= bucket_counts['1_week']%></a></td>
32
- <td><a href='<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{group}/older" %>'><%= bucket_counts['older']%></a></td>
25
+ <td><a href='<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{group}/all/total_dead" %>'><%= group %></a></td>
26
+ <td><a href='<%= "#{root_path}undertaker/filter/#{@req_job_class}/#{group}/total_dead" %>'><%= bucket_counts['total_dead'] %></a></td>
27
+ <td><a href='<%= "#{root_path}undertaker/filter/#{@req_job_class}/#{group}/1_hour" %>'><%= bucket_counts['1_hour']%></a></td>
28
+ <td><a href='<%= "#{root_path}undertaker/filter/#{@req_job_class}/#{group}/3_hours" %>'><%= bucket_counts['3_hours']%></a></td>
29
+ <td><a href='<%= "#{root_path}undertaker/filter/#{@req_job_class}/#{group}/1_day" %>'><%= bucket_counts['1_day']%></a></td>
30
+ <td><a href='<%= "#{root_path}undertaker/filter/#{@req_job_class}/#{group}/3_days" %>'><%= bucket_counts['3_days']%></a></td>
31
+ <td><a href='<%= "#{root_path}undertaker/filter/#{@req_job_class}/#{group}/1_week" %>'><%= bucket_counts['1_week']%></a></td>
32
+ <td><a href='<%= "#{root_path}undertaker/filter/#{@req_job_class}/#{group}/older" %>'><%= bucket_counts['older']%></a></td>
33
33
  </tr>
34
34
  <% end %>
35
35
  </table>
@@ -0,0 +1,36 @@
1
+ <header class="row header">
2
+ <div class="col-sm-12">
3
+ <h3>
4
+ <%= "<b>#{@total_dead}</b> dead #{@total_dead == 1 ? 'job' : 'jobs'}" %>
5
+ <%= " of <b>#{@req_job_class}</b> class" unless @req_job_class == "all" %>
6
+ <%= " with <b>#{@req_error_class}</b> exception" unless @req_error_class == "all" %>
7
+ </h3>
8
+ </div>
9
+ </header>
10
+
11
+ <table class="table table-striped table-bordered table-white">
12
+ <thead>
13
+ <tr>
14
+ <th style="width: 20%"><%= t('Error Message') %></th>
15
+ <th style="width: 10%"><%= t('All') %></th>
16
+ <th style="width: 10%"><%= t('1 hour') %></th>
17
+ <th style="width: 10%"><%= t('3 hours') %></th>
18
+ <th style="width: 10%"><%= t('1 day') %></th>
19
+ <th style="width: 10%"><%= t('3 days') %></th>
20
+ <th style="width: 10%"><%= t('1 week') %></th>
21
+ <th style="width: 10%"><%= t('Older') %></th>
22
+ </tr>
23
+ </thead>
24
+ <% @distribution.each do |group, bucket_counts| %>
25
+ <tr>
26
+ <td><a href='<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{@req_error_class}/#{Base64.urlsafe_encode64(group)}/total_dead" %>'><%= group %></a></td>
27
+ <td><a href='<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{@req_error_class}/#{Base64.urlsafe_encode64(group)}/total_dead" %>'><%= bucket_counts['total_dead'] %></a></td>
28
+ <td><a href='<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{@req_error_class}/#{Base64.urlsafe_encode64(group)}/1_hour" %>'><%= bucket_counts['1_hour']%></a></td>
29
+ <td><a href='<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{@req_error_class}/#{Base64.urlsafe_encode64(group)}/3_hours" %>'><%= bucket_counts['3_hours']%></a></td>
30
+ <td><a href='<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{@req_error_class}/#{Base64.urlsafe_encode64(group)}/1_day" %>'><%= bucket_counts['1_day']%></a></td>
31
+ <td><a href='<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{@req_error_class}/#{Base64.urlsafe_encode64(group)}/3_days" %>'><%= bucket_counts['3_days']%></a></td>
32
+ <td><a href='<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{@req_error_class}/#{Base64.urlsafe_encode64(group)}/1_week" %>'><%= bucket_counts['1_week']%></a></td>
33
+ <td><a href='<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{@req_error_class}/#{Base64.urlsafe_encode64(group)}/older" %>'><%= bucket_counts['older']%></a></td>
34
+ </tr>
35
+ <% end %>
36
+ </table>
data/web/views/morgue.erb CHANGED
@@ -4,6 +4,7 @@
4
4
  <%= "<b>#{@total_dead}</b> dead #{@total_dead == 1 ? 'job' : 'jobs'}" %>
5
5
  <%= " of <b>#{@req_job_class}</b> class" unless @req_job_class == "all" %>
6
6
  <%= " with <b>#{@req_error_class}</b> exception" unless @req_error_class == "all" %>
7
+ <%= " with <b>#{@req_error_msg}</b> message" unless @req_error_msg == "all" %>
7
8
  <%= " in <b>#{@req_bucket_name}</b> bucket" %>
8
9
  </h3>
9
10
  </div>
@@ -56,15 +57,21 @@
56
57
  <% end %>
57
58
  </table>
58
59
  <input class="btn btn-primary btn-xs pull-left" type="submit" name="retry" value="<%= t('UndertakerRevive') %>" />
60
+ <input class="btn btn-secondary btn-xs pull-left" type="submit" name="export" value="<%= t('UndertakerExport') %>" />
59
61
  <input class="btn btn-danger btn-xs pull-left" type="submit" name="delete" value="<%= t('UndertakerBury') %>" />
60
62
  </form>
61
63
 
62
- <form action="<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{@req_error_class}/#{@req_bucket_name}/delete" %>" method="post">
64
+ <form action="<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{@req_error_class}/#{Base64.urlsafe_encode64(@req_error_msg)}/#{@req_bucket_name}/delete" %>" method="post">
63
65
  <%= respond_to?(:csrf_tag) && csrf_tag %>
64
66
  <input class="btn btn-danger btn-xs pull-right" type="submit" name="delete" value="<%= t('UndertakerBuryAll') %>" data-confirm="<%= t('AreYouSure') %>" />
65
67
  </form>
66
68
 
67
- <form action="<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{@req_error_class}/#{@req_bucket_name}/retry" %>" method="post">
69
+ <form action="<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{@req_error_class}/#{Base64.urlsafe_encode64(@req_error_msg)}/#{@req_bucket_name}/export" %>" method="post">
70
+ <%= respond_to?(:csrf_tag) && csrf_tag %>
71
+ <input class="btn btn-secondary btn-xs pull-right" type="submit" name="export" value="<%= t('UndertakerExportAll') %>" />
72
+ </form>
73
+
74
+ <form action="<%= "#{root_path}undertaker/morgue/#{@req_job_class}/#{@req_error_class}/#{Base64.urlsafe_encode64(@req_error_msg)}/#{@req_bucket_name}/retry" %>" method="post">
68
75
  <%= respond_to?(:csrf_tag) && csrf_tag %>
69
76
  <input class="btn btn-danger btn-xs pull-right" type="submit" name="retry" value="<%= t('UndertakerReviveAll') %>" data-confirm="<%= t('AreYouSure') %>" />
70
77
  </form>