rocketjob_mission_control 5.0.1 → 6.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/app/assets/config/manifest.js +3 -0
  4. data/app/assets/javascripts/rocket_job_mission_control/application.js +1 -1
  5. data/app/assets/javascripts/rocket_job_mission_control/nested_fields.js +112 -0
  6. data/app/assets/stylesheets/rocket_job_mission_control/{application.scss → application.css} +9 -8
  7. data/app/assets/stylesheets/rocket_job_mission_control/base.css +420 -0
  8. data/app/assets/stylesheets/rocket_job_mission_control/{callout.scss → callout.css} +50 -52
  9. data/app/assets/stylesheets/rocket_job_mission_control/jobs.css +57 -0
  10. data/app/assets/stylesheets/rocket_job_mission_control/worker_processes.css +7 -0
  11. data/app/controllers/rocket_job_mission_control/dirmon_entries_controller.rb +20 -20
  12. data/app/controllers/rocket_job_mission_control/jobs_controller.rb +44 -4
  13. data/app/datatables/rocket_job_mission_control/jobs_datatable.rb +1 -1
  14. data/app/helpers/rocket_job_mission_control/application_helper.rb +43 -26
  15. data/app/helpers/rocket_job_mission_control/dirmon_entries_helper.rb +8 -0
  16. data/app/helpers/rocket_job_mission_control/jobs_helper.rb +76 -8
  17. data/app/models/rocket_job_mission_control/dirmon_sanitizer.rb +68 -0
  18. data/app/models/rocket_job_mission_control/job_sanitizer.rb +35 -0
  19. data/app/views/rocket_job_mission_control/dirmon_entries/_form.html.erb +78 -42
  20. data/app/views/rocket_job_mission_control/dirmon_entries/_input_categories.html.erb +59 -0
  21. data/app/views/rocket_job_mission_control/dirmon_entries/_input_category_fields.html.erb +56 -0
  22. data/app/views/rocket_job_mission_control/dirmon_entries/_output_categories.html.erb +44 -0
  23. data/app/views/rocket_job_mission_control/dirmon_entries/_output_category_fields.html.erb +36 -0
  24. data/app/views/rocket_job_mission_control/dirmon_entries/_status.html.erb +22 -16
  25. data/app/views/rocket_job_mission_control/dirmon_entries/copy.html.erb +1 -1
  26. data/app/views/rocket_job_mission_control/dirmon_entries/edit.html.erb +7 -3
  27. data/app/views/rocket_job_mission_control/dirmon_entries/show.html.erb +2 -3
  28. data/app/views/rocket_job_mission_control/jobs/_attributes.html.erb +89 -0
  29. data/app/views/rocket_job_mission_control/jobs/_dates.html.erb +39 -0
  30. data/app/views/rocket_job_mission_control/jobs/_details.html.erb +86 -0
  31. data/app/views/rocket_job_mission_control/jobs/_exception.html.erb +33 -0
  32. data/app/views/rocket_job_mission_control/jobs/_input_categories.html.erb +126 -0
  33. data/app/views/rocket_job_mission_control/jobs/_input_category_fields.html.erb +56 -0
  34. data/app/views/rocket_job_mission_control/jobs/_output_categories.html.erb +60 -0
  35. data/app/views/rocket_job_mission_control/jobs/_output_category_fields.html.erb +36 -0
  36. data/app/views/rocket_job_mission_control/jobs/_retryable.html.erb +27 -0
  37. data/app/views/rocket_job_mission_control/jobs/_status.html.erb +21 -49
  38. data/app/views/rocket_job_mission_control/jobs/edit.html.erb +28 -0
  39. data/app/views/rocket_job_mission_control/jobs/exception.html.erb +1 -1
  40. data/app/views/rocket_job_mission_control/jobs/index.html.erb +24 -18
  41. data/app/views/rocket_job_mission_control/jobs/show.html.erb +32 -58
  42. data/app/views/rocket_job_mission_control/jobs/view_slice.html.erb +4 -4
  43. data/config/initializers/assets.rb +3 -4
  44. data/lib/rocket_job_mission_control/engine.rb +0 -1
  45. data/lib/rocket_job_mission_control/version.rb +1 -1
  46. data/test/controllers/rocket_job_mission_control/jobs_controller_test.rb +2 -1
  47. data/test/models/rocket_job_mission_control/dirmon_sanitizer_test.rb +146 -0
  48. data/test/test_helper.rb +5 -11
  49. data/vendor/assets/fonts/glyphicons-halflings-regular.eot +0 -0
  50. data/vendor/assets/fonts/glyphicons-halflings-regular.svg +288 -0
  51. data/vendor/assets/fonts/glyphicons-halflings-regular.ttf +0 -0
  52. data/vendor/assets/fonts/glyphicons-halflings-regular.woff +0 -0
  53. data/vendor/assets/fonts/glyphicons-halflings-regular.woff2 +0 -0
  54. data/vendor/assets/stylesheets/bootstrap.min.css.erb +6 -0
  55. metadata +37 -29
  56. data/app/assets/stylesheets/rocket_job_mission_control/base.scss +0 -436
  57. data/app/assets/stylesheets/rocket_job_mission_control/bootstrap_and_overrides.scss +0 -488
  58. data/app/assets/stylesheets/rocket_job_mission_control/jobs.scss +0 -72
  59. data/app/assets/stylesheets/rocket_job_mission_control/worker_processes.scss +0 -9
  60. data/vendor/assets/stylesheets/bootstrap.min.css +0 -6
@@ -3,5 +3,13 @@ module RocketJobMissionControl
3
3
  def dirmon_counts_by_state(state)
4
4
  RocketJob::DirmonEntry.counts_by_state.fetch(state.downcase.to_sym, 0)
5
5
  end
6
+
7
+
8
+ def dirmon_entry_find_category(categories, category_name = :main)
9
+ return unless categories
10
+
11
+ categories.each { |category| return category if category_name == (category["name"] || :main).to_sym }
12
+ nil
13
+ end
6
14
  end
7
15
  end
@@ -1,5 +1,23 @@
1
1
  module RocketJobMissionControl
2
2
  module JobsHelper
3
+ # The fields that RJMC already displays, all other will be rendered under the custom section.
4
+ DISPLAYED_FIELDS = %w[
5
+ _id _type
6
+ completed_at created_at cron_schedule
7
+ description destroy_on_complete download_encryption
8
+ exception expires_at
9
+ failed_at_list failure_count
10
+ input_categories
11
+ log_level
12
+ output_categories output_path
13
+ percent_complete priority
14
+ record_count retry_limit run_at
15
+ started_at state statistics sub_state
16
+ throttle_group throttle_running_workers
17
+ upload_file_name
18
+ worker_name
19
+ ]
20
+
3
21
  def job_icon(job)
4
22
  state = job_state(job)
5
23
  state_icon(state)
@@ -15,6 +33,47 @@ module RocketJobMissionControl
15
33
  end
16
34
  end
17
35
 
36
+ def job_state_name(job)
37
+ job_state(@job).to_s.camelcase
38
+ end
39
+
40
+ def job_time(time)
41
+ return "" unless time
42
+ time.in_time_zone(Time.zone)
43
+ end
44
+
45
+ def job_state_time(job)
46
+ return job_time(job.run_at) if job.scheduled?
47
+
48
+ job_time(job.completed_at || job.started_at || job.created_at )
49
+ # job_time(job.running? ? job.started_at : job.completed_at)
50
+ end
51
+
52
+ def job_estimated_time_left(job)
53
+ if job.record_count && job.running? && job.record_count.positive?
54
+ percent = job.percent_complete
55
+ if percent >= 5
56
+ secs = job.seconds.to_f
57
+ RocketJob.seconds_as_duration((((secs / percent) * 100) - secs))
58
+ end
59
+ end
60
+ end
61
+
62
+ def job_records_per_hour(job)
63
+ return unless job.completed?
64
+
65
+ secs = job.seconds.to_f
66
+ ((job.record_count.to_f / secs) * 60 * 60).round if job.record_count&.positive? && (secs > 0.0)
67
+ end
68
+
69
+ def job_custom_fields(job)
70
+ attrs = job.attributes.dup
71
+ DISPLAYED_FIELDS.each { |key| attrs.delete(key) }
72
+ # Convert time zones for any custom time fields
73
+ attrs.keys { |key| attrs[key] = attrs[key].in_time_zone(Time.zone) if attrs[key].is_a?(Time) }
74
+ attrs
75
+ end
76
+
18
77
  def job_states
19
78
  @job_states ||= RocketJob::Job.aasm.states.map { |state| state.name.to_s }
20
79
  end
@@ -25,10 +84,10 @@ module RocketJobMissionControl
25
84
 
26
85
  def job_counts_by_state(state)
27
86
  @job_counts ||= begin
28
- counts = RocketJob::Job.counts_by_state
29
- counts[:queued] = counts[:queued_now] || 0
30
- counts
31
- end
87
+ counts = RocketJob::Job.counts_by_state
88
+ counts[:queued] = counts[:queued_now] || 0
89
+ counts
90
+ end
32
91
  @job_counts.fetch(state.downcase.to_sym, 0)
33
92
  end
34
93
 
@@ -38,8 +97,9 @@ module RocketJobMissionControl
38
97
  path,
39
98
  method: http_method,
40
99
  title: "#{action} job",
41
- class: "btn btn-default",
42
- data: {confirm: t(:confirm, scope: %i[job action], action: action)}
100
+ class: "btn btn-default btn-group",
101
+ role: "group",
102
+ data: { confirm: t(:confirm, scope: %i[job action], action: action) }
43
103
  )
44
104
  end
45
105
 
@@ -49,8 +109,9 @@ module RocketJobMissionControl
49
109
  path,
50
110
  method: http_method,
51
111
  title: "#{action} job",
52
- class: "btn btn-primary",
53
- data: {confirm: t(:confirm, scope: %i[job action], action: action)}
112
+ class: "btn btn-default btn-group",
113
+ role: "group",
114
+ data: { confirm: t(:confirm, scope: %i[job action], action: action) }
54
115
  )
55
116
  end
56
117
 
@@ -61,5 +122,12 @@ module RocketJobMissionControl
61
122
  ""
62
123
  end
63
124
  end
125
+
126
+ def job_find_category(categories, category_name = :main)
127
+ return unless categories
128
+
129
+ categories.each { |category| return category if category_name == (category["name"] || :main).to_sym }
130
+ nil
131
+ end
64
132
  end
65
133
  end
@@ -0,0 +1,68 @@
1
+ module RocketJobMissionControl
2
+ module DirmonSanitizer
3
+ DIRMON_FIELDS = %i(archive_directory job_class_name name pattern).freeze
4
+
5
+ def self.sanitize(params, job_class, target)
6
+ permissible_params = {}
7
+
8
+ DIRMON_FIELDS.each do |field_name|
9
+ value = params[field_name]
10
+ next if value.blank?
11
+
12
+ permissible_params[field_name] = value
13
+ end
14
+
15
+ if params.key?(:properties)
16
+ properties = JobSanitizer.sanitize(params[:properties], job_class, target, false)
17
+ permissible_params[:properties] = properties unless properties.blank?
18
+ end
19
+
20
+ permissible_params
21
+ end
22
+
23
+ # Returns [Hash] the difference between the supplied params and those already set in the job itself
24
+ def self.diff_properties(sanitized_properties, dirmon_entry)
25
+ default_job = dirmon_entry.job_class.new
26
+ updated_job = dirmon_entry.job_class.from_properties(sanitized_properties)
27
+ properties = {}
28
+ sanitized_properties&.each_pair do |name, value|
29
+ if name == :input_categories
30
+ categories = []
31
+ value.each do |category_properties|
32
+ category_name = category_properties[:name].to_sym
33
+ props = diff_category(category_properties, updated_job.input_category(category_name), default_job.input_category(category_name))
34
+ categories << props unless props.empty?
35
+ end
36
+ properties[:input_categories] = categories unless categories.empty?
37
+ elsif name == :output_categories
38
+ categories = []
39
+ value.each do |category_properties|
40
+ category_name = category_properties[:name].to_sym
41
+ props = diff_category(category_properties, updated_job.output_category(category_name), default_job.output_category(category_name))
42
+ categories << props unless props.empty?
43
+ end
44
+ properties[:output_categories] = categories unless categories.empty?
45
+ else
46
+ properties[name] = value unless default_job.public_send(name) == updated_job.public_send(name)
47
+ end
48
+ end
49
+ properties
50
+ end
51
+
52
+ def self.diff_category(properties, updated_category, default_category)
53
+ diff = {}
54
+ name = nil
55
+ properties&.each_pair do |key, value|
56
+ if key == :name
57
+ name = value
58
+ next
59
+ end
60
+ next if updated_category.public_send(key) == default_category.public_send(key)
61
+
62
+ diff[key] = value
63
+ end
64
+ diff[:name] = name unless diff.empty?
65
+ diff
66
+ end
67
+ end
68
+ end
@@ -1,5 +1,7 @@
1
1
  module RocketJobMissionControl
2
2
  module JobSanitizer
3
+ CATEGORIES_FIELDS = %i[id name format format_options mode skip_unknown slice_size columns].freeze
4
+
3
5
  # Returns [Hash] the permissible params for the specified job class, after sanitizing.
4
6
  # Parameters
5
7
  # properties [Hash]
@@ -17,6 +19,7 @@ module RocketJobMissionControl
17
19
  # Default: true
18
20
  def self.sanitize(properties, job_class, target, nil_blank = true)
19
21
  permissible_params = {}
22
+
20
23
  job_class.user_editable_fields.each do |field_name|
21
24
  next unless value = properties[field_name]
22
25
 
@@ -38,7 +41,39 @@ module RocketJobMissionControl
38
41
  permissible_params[field_name] = value
39
42
  end
40
43
  end
44
+
45
+ if properties.key?(:input_categories_attributes)
46
+ categories = sanitize_categories(properties[:input_categories_attributes])
47
+ permissible_params[:input_categories] = categories unless categories == [{}]
48
+ end
49
+
50
+ if properties.key?(:output_categories_attributes)
51
+ categories = sanitize_categories(properties[:output_categories_attributes])
52
+ permissible_params[:output_categories] = categories unless categories == [{}]
53
+ end
54
+
41
55
  permissible_params
42
56
  end
57
+
58
+ def self.sanitize_categories(properties)
59
+ categories = []
60
+
61
+ properties.each_pair do |_, category|
62
+ hash = {}
63
+ CATEGORIES_FIELDS.each do |key|
64
+ next unless category.key?(key)
65
+
66
+ value = category[key]
67
+ next if value.blank?
68
+ next if (key == :columns) && value == [""]
69
+
70
+ value = JSON.parse(value) if key == :format_options
71
+ hash[key] = value
72
+ end
73
+ categories << hash
74
+ end
75
+
76
+ categories
77
+ end
43
78
  end
44
79
  end
@@ -1,69 +1,105 @@
1
1
  <% action ||= :create %>
2
2
 
3
3
  <% if @dirmon_entry.errors.present? %>
4
- <div class='alert alert-alert'>Invalid Dirmon entry!</div>
4
+ <div class="alert alert-alert">Invalid Dirmon entry!</div>
5
+
5
6
  <% @dirmon_entry.errors.messages.each_pair do |field, message| %>
6
- <div class='message'><%= field %>: <%= message %></div>
7
+ <div class="message"><%= field %>: <%= message %></div>
7
8
  <% end %>
8
9
  <% end %>
9
10
 
10
- <%= form_for @dirmon_entry, url: {action: action} do |f| %>
11
- <div class='row'>
12
- <div class='col-md-6'>
13
- <div class='name form-group'>
14
- <%= f.label :name %>
15
- <%= f.text_field :name, class: 'form-control' %>
16
- </div>
17
- </div>
11
+ <%= form_for @dirmon_entry, url: { action: action } do |f| %>
12
+ <div class="row">
13
+ <div class="col-md-12">
14
+ <div class="arguments">
15
+ <div class="panel panel-default">
16
+ <div class="panel-body">
17
+ <div class="job_arguments form-group"></div>
18
18
 
19
- <div class='col-md-6'>
20
- <div class='job form-group'>
21
- <%= f.label :job_class_name %>
22
- <%= f.text_field :job_class_name, class: 'form-control', disabled: action != :create %>
23
- </div>
24
- </div>
25
- </div>
19
+ <div class="form-group">
20
+ <%= f.label :name %>
21
+ <%= f.text_field :name, class: "form-control" %>
22
+ </div>
26
23
 
27
- <div class='path form-group'>
28
- <%= f.label :pattern %>
29
- <%= f.text_field :pattern, class: 'form-control' %>
30
- </div>
24
+ <div class="form-group">
25
+ <%= f.label "Job Class" %>
26
+ <%= f.text_field :job_class_name, class: "form-control", disabled: action != :create %>
27
+ </div>
31
28
 
32
- <div class='path form-group'>
33
- <%= f.label :archive_directory %>
34
- <%= f.text_field :archive_directory, class: 'form-control' %>
35
- </div>
29
+ <div class="form-group">
30
+ <%= f.label :pattern %>
31
+ <%= f.text_field :pattern, class: "form-control" %>
32
+ </div>
33
+
34
+ <div class="form-group">
35
+ <%= f.label :archive_directory %>
36
+ <%= f.text_field :archive_directory, class: "form-control" %>
37
+ </div>
38
+ </div>
39
+ </div>
40
+
41
+ <% if @dirmon_entry.job_class %>
42
+ <% @job = @dirmon_entry.job_class.from_properties(@dirmon_entry.properties) %>
43
+ <%= f.fields_for :properties do |p| %>
44
+ <div class="panel panel-default">
45
+ <div class="panel-heading">
46
+ <strong>Properties</strong>
47
+ </div>
48
+
49
+ <div class="panel-body">
50
+ <% @dirmon_entry.job_class.user_editable_fields.sort.each do |property_name| %>
51
+ <% next if property_name == :run_at %>
52
+
53
+ <div class="form-group">
54
+ <%= p.label property_name.to_s %>
55
+ <%= editable_field_html(@dirmon_entry.job_class, property_name, @job.public_send(property_name), p) %>
56
+ </div>
57
+ <% end %>
58
+ </div>
59
+ </div>
60
+
61
+ <% if @dirmon_entry.job_class.respond_to?(:defined_input_categories) && @dirmon_entry.job_class.respond_to?(:defined_output_categories) %>
62
+ <div class="row">
63
+ <div class="col-sm-6">
64
+ <div class="panel panel-primary">
65
+ <div class="panel-body">
66
+ <div class='lead'>Input Categories</div>
36
67
 
37
- <div class='row'>
38
- <div class='col-md-12'>
39
- <% if @dirmon_entry.job_class %>
40
- <div class='lead'>Job Properties</div>
68
+ <% @job.input_categories.each do |input_categories| %>
69
+ <%= p.fields_for "input_categories_attributes[]", input_categories do |i| %>
70
+ <%= render "input_category_fields.html", f: i %>
71
+ <% end %>
72
+ <% end %>
73
+ </div>
74
+ </div>
75
+ </div>
41
76
 
42
- <div class='arguments'>
43
- <div class='job_arguments form-group'></div>
77
+ <div class="col-sm-6">
78
+ <div class="panel panel-success">
79
+ <div class="panel-body">
80
+ <div class='lead'>Output Categories</div>
44
81
 
45
- <%= f.fields_for :properties do |properties| %>
46
- <% @dirmon_entry.job_class.user_editable_fields.each do |property_name| %>
47
- <div class='form-group'>
48
- <%= properties.label property_name.to_s %>
49
- <%= editable_field_html(@dirmon_entry.job_class, property_name, @dirmon_entry.properties[property_name], properties, true) %>
82
+ <% @job.output_categories.each do |output_categories| %>
83
+ <%= p.fields_for "output_categories_attributes[]", output_categories do |o| %>
84
+ <%= render "output_category_fields.html", f: o %>
85
+ <% end %>
86
+ <% end %>
87
+ </div>
88
+ </div>
89
+ </div>
50
90
  </div>
51
91
  <% end %>
52
92
  <% end %>
53
93
  </div>
54
94
  <% end %>
55
95
 
56
- <div class='buttons pull-right'>
96
+ <div class='buttons'>
57
97
  <%= f.submit action, class: 'btn btn-primary' %>
58
98
  <%= link_to 'cancel', :back, class: 'btn btn-default' %>
59
99
  </div>
60
100
 
61
101
  <% if action == :create %>
62
- <%= button_tag 'properties',
63
- type: 'button',
64
- class: 'btn btn-default',
65
- id: 'properties',
66
- data: { url: rocket_job_mission_control.new_dirmon_entry_path } %>
102
+ <%= button_tag 'properties', type: 'button', class: 'btn btn-default', id: 'properties', data: {url: rocket_job_mission_control.new_dirmon_entry_path} %>
67
103
  <% end %>
68
104
  </div>
69
105
  </div>
@@ -0,0 +1,59 @@
1
+ <div class='lead'>Input Categories:</div>
2
+ <% @job.input_categories.each do |category| %>
3
+ <div class='row'>
4
+ <div class='col-md-12'>
5
+ <div class='status-message'><label><%= category.name.to_s.camelcase %>:</label></div>
6
+ <div class='row'>
7
+ <div class='col-md-12'>
8
+ <table>
9
+ <% if category.name == :main %>
10
+ <tr>
11
+ <td><label>Slice Size:</label></td>
12
+ <td><%= category.slice_size %></td>
13
+ </tr>
14
+ <% end %>
15
+ </tr>
16
+ <tr>
17
+ <td><label>Format:</label></td>
18
+ <td><%= category.format %></td>
19
+ </tr>
20
+ <tr>
21
+ <td><label>Skip Unknown Columns:</label></td>
22
+ <td><%= category.skip_unknown %></td>
23
+ </tr>
24
+ <tr>
25
+ <td><label>Mode:</label></td>
26
+ <td><%= category.mode %></td>
27
+ </tr>
28
+ <% if category.format_options %>
29
+ <tr>
30
+ <td><label>Format Options:</label></td>
31
+ <td>
32
+ <pre><code><%= category.format_options.ai(plain: true, ruby19_syntax: true, sort_keys: true) %></code></pre>
33
+ </td>
34
+ </tr>
35
+ <% end %>
36
+ <% if category.columns %>
37
+ <% first = true %>
38
+ <% category.columns.each do |item| %>
39
+ <% if first %>
40
+ <tr>
41
+ <td><label>Columns:</label></td>
42
+ <td><%= item %></td>
43
+ </tr>
44
+ <% first = false %>
45
+ <% else %>
46
+ <tr>
47
+ <td></td>
48
+ <td><%= item %></td>
49
+ </tr>
50
+ <% end %>
51
+ <% end %>
52
+ <% end %>
53
+ </table>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ <br/>
59
+ <% end %>