rocketjob_mission_control 5.0.1 → 6.0.0.beta

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 (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 %>