rocketjob_mission_control 5.0.0.beta1 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/Rakefile +1 -2
  4. data/app/assets/config/manifest.js +3 -0
  5. data/app/assets/javascripts/rocket_job_mission_control/application.js +3 -5
  6. data/app/assets/javascripts/rocket_job_mission_control/nested_fields.js +112 -0
  7. data/app/assets/stylesheets/rocket_job_mission_control/{application.scss → application.css} +9 -9
  8. data/app/assets/stylesheets/rocket_job_mission_control/base.css +420 -0
  9. data/app/assets/stylesheets/rocket_job_mission_control/{callout.scss → callout.css} +50 -52
  10. data/app/assets/stylesheets/rocket_job_mission_control/jobs.css +56 -0
  11. data/app/assets/stylesheets/rocket_job_mission_control/worker_processes.css +7 -0
  12. data/app/controllers/rocket_job_mission_control/application_controller.rb +3 -1
  13. data/app/controllers/rocket_job_mission_control/dirmon_entries_controller.rb +40 -21
  14. data/app/controllers/rocket_job_mission_control/jobs_controller.rb +50 -7
  15. data/app/controllers/rocket_job_mission_control/servers_controller.rb +7 -7
  16. data/app/datatables/rocket_job_mission_control/dirmon_entries_datatable.rb +1 -1
  17. data/app/datatables/rocket_job_mission_control/jobs_datatable.rb +1 -1
  18. data/app/helpers/rocket_job_mission_control/application_helper.rb +51 -26
  19. data/app/helpers/rocket_job_mission_control/dirmon_entries_helper.rb +12 -0
  20. data/app/helpers/rocket_job_mission_control/jobs_helper.rb +76 -8
  21. data/app/helpers/rocket_job_mission_control/servers_helper.rb +4 -0
  22. data/app/models/rocket_job_mission_control/access_policy.rb +2 -2
  23. data/app/models/rocket_job_mission_control/dirmon_sanitizer.rb +68 -0
  24. data/app/models/rocket_job_mission_control/job_sanitizer.rb +37 -0
  25. data/app/views/layouts/rocket_job_mission_control/partials/_flash.html.erb +4 -4
  26. data/app/views/rocket_job_mission_control/dirmon_entries/_form.html.erb +78 -42
  27. data/app/views/rocket_job_mission_control/dirmon_entries/_input_categories.html.erb +59 -0
  28. data/app/views/rocket_job_mission_control/dirmon_entries/_input_category_fields.html.erb +56 -0
  29. data/app/views/rocket_job_mission_control/dirmon_entries/_output_categories.html.erb +44 -0
  30. data/app/views/rocket_job_mission_control/dirmon_entries/_output_category_fields.html.erb +36 -0
  31. data/app/views/rocket_job_mission_control/dirmon_entries/_status.html.erb +22 -16
  32. data/app/views/rocket_job_mission_control/dirmon_entries/copy.html.erb +4 -0
  33. data/app/views/rocket_job_mission_control/dirmon_entries/edit.html.erb +7 -3
  34. data/app/views/rocket_job_mission_control/dirmon_entries/show.html.erb +8 -5
  35. data/app/views/rocket_job_mission_control/jobs/_attributes.html.erb +28 -0
  36. data/app/views/rocket_job_mission_control/jobs/_dates.html.erb +35 -0
  37. data/app/views/rocket_job_mission_control/jobs/_details.html.erb +125 -0
  38. data/app/views/rocket_job_mission_control/jobs/_exception.html.erb +22 -0
  39. data/app/views/rocket_job_mission_control/jobs/_input_categories.html.erb +76 -0
  40. data/app/views/rocket_job_mission_control/jobs/_input_category_fields.html.erb +56 -0
  41. data/app/views/rocket_job_mission_control/jobs/_output_categories.html.erb +49 -0
  42. data/app/views/rocket_job_mission_control/jobs/_output_category_fields.html.erb +36 -0
  43. data/app/views/rocket_job_mission_control/jobs/_retryable.html.erb +16 -0
  44. data/app/views/rocket_job_mission_control/jobs/_status.html.erb +20 -48
  45. data/app/views/rocket_job_mission_control/jobs/edit.html.erb +28 -0
  46. data/app/views/rocket_job_mission_control/jobs/edit_slice.html.erb +2 -2
  47. data/app/views/rocket_job_mission_control/jobs/exception.html.erb +1 -1
  48. data/app/views/rocket_job_mission_control/jobs/index.html.erb +24 -18
  49. data/app/views/rocket_job_mission_control/jobs/show.html.erb +32 -58
  50. data/app/views/rocket_job_mission_control/jobs/view_slice.html.erb +6 -4
  51. data/config/initializers/assets.rb +3 -4
  52. data/config/routes.rb +4 -0
  53. data/lib/rocket_job_mission_control/engine.rb +0 -3
  54. data/lib/rocket_job_mission_control/version.rb +1 -1
  55. data/test/controllers/rocket_job_mission_control/application_controller_test.rb +4 -12
  56. data/test/controllers/rocket_job_mission_control/dirmon_entries_controller_test.rb +20 -52
  57. data/test/controllers/rocket_job_mission_control/jobs_controller_test.rb +21 -41
  58. data/test/controllers/rocket_job_mission_control/servers_controller_test.rb +3 -3
  59. data/test/models/rocket_job_mission_control/dirmon_sanitizer_test.rb +146 -0
  60. data/test/models/rocket_job_mission_control/job_sanitizer_test.rb +9 -3
  61. data/test/test_helper.rb +6 -12
  62. data/vendor/assets/fonts/glyphicons-halflings-regular.eot +0 -0
  63. data/vendor/assets/fonts/glyphicons-halflings-regular.svg +288 -0
  64. data/vendor/assets/fonts/glyphicons-halflings-regular.ttf +0 -0
  65. data/vendor/assets/fonts/glyphicons-halflings-regular.woff +0 -0
  66. data/vendor/assets/fonts/glyphicons-halflings-regular.woff2 +0 -0
  67. data/vendor/assets/javascripts/bootstrap.min.js +3 -4
  68. data/vendor/assets/javascripts/datatables.min.js +881 -0
  69. data/vendor/assets/javascripts/jquery-3.5.1.min.js +2 -0
  70. data/vendor/assets/stylesheets/bootstrap.min.css.erb +6 -0
  71. data/vendor/assets/stylesheets/bootstrap.min.css.map +1 -1
  72. data/vendor/assets/stylesheets/datatables.min.css +141 -0
  73. metadata +60 -63
  74. data/app/assets/stylesheets/rocket_job_mission_control/base.scss +0 -436
  75. data/app/assets/stylesheets/rocket_job_mission_control/bootstrap_and_overrides.scss +0 -488
  76. data/app/assets/stylesheets/rocket_job_mission_control/jobs.scss +0 -72
  77. data/app/assets/stylesheets/rocket_job_mission_control/worker_processes.scss +0 -9
  78. data/vendor/assets/stylesheets/bootstrap.min.css +0 -6
@@ -62,9 +62,9 @@ module RocketJobMissionControl
62
62
  RocketJob::Server.destroy_zombies
63
63
  elsif VALID_ACTIONS.include?(server_action)
64
64
  RocketJob::Subscribers::Server.publish(server_action)
65
- flash[:notice] = t(:success, scope: %i[server update_all], action: server_action.to_s)
65
+ flash[:success] = t(:success, scope: %i[server update_all], action: server_action.to_s)
66
66
  else
67
- flash[:alert] = t(:invalid, scope: %i[server update_all])
67
+ flash[:danger] = t(:invalid, scope: %i[server update_all])
68
68
  end
69
69
 
70
70
  # TODO: Refresh the same page it was on
@@ -76,7 +76,7 @@ module RocketJobMissionControl
76
76
  def stop
77
77
  authorize! :stop, @server
78
78
  RocketJob::Subscribers::Server.publish(:stop, server_id: @server.id)
79
- flash[:notice] = t(:success, scope: %i[server update_one], action: "stop", name: @server.name)
79
+ flash[:success] = t(:success, scope: %i[server update_one], action: "stop", name: @server.name)
80
80
 
81
81
  respond_to do |format|
82
82
  format.html { redirect_to servers_path }
@@ -86,7 +86,7 @@ module RocketJobMissionControl
86
86
  def destroy
87
87
  authorize! :destroy, @server
88
88
  @server.destroy
89
- flash[:notice] = t(:success, scope: %i[server destroy])
89
+ flash[:success] = t(:success, scope: %i[server destroy])
90
90
 
91
91
  respond_to do |format|
92
92
  format.html { redirect_to servers_path }
@@ -96,7 +96,7 @@ module RocketJobMissionControl
96
96
  def pause
97
97
  authorize! :pause, @server
98
98
  RocketJob::Subscribers::Server.publish(:pause, server_id: @server.id)
99
- flash[:notice] = t(:success, scope: %i[server update_one], action: "pause", name: @server.name)
99
+ flash[:success] = t(:success, scope: %i[server update_one], action: "pause", name: @server.name)
100
100
 
101
101
  respond_to do |format|
102
102
  format.html { redirect_to servers_path }
@@ -106,7 +106,7 @@ module RocketJobMissionControl
106
106
  def resume
107
107
  authorize! :resume, @server
108
108
  RocketJob::Subscribers::Server.publish(:resume, server_id: @server.id)
109
- flash[:notice] = t(:success, scope: %i[server update_one], action: "resume", name: @server.name)
109
+ flash[:success] = t(:success, scope: %i[server update_one], action: "resume", name: @server.name)
110
110
 
111
111
  respond_to do |format|
112
112
  format.html { redirect_to servers_path }
@@ -140,7 +140,7 @@ module RocketJobMissionControl
140
140
 
141
141
  def find_server_or_redirect
142
142
  unless @server = RocketJob::Server.where(id: params[:id]).first
143
- flash[:alert] = t(:failure, scope: %i[server find], id: params[:id])
143
+ flash[:danger] = t(:failure, scope: %i[server find], id: params[:id])
144
144
 
145
145
  redirect_to(servers_path)
146
146
  end
@@ -17,7 +17,7 @@ module RocketJobMissionControl
17
17
  <<-EOS
18
18
  <a href="#{dirmon_entry_path(dirmon.id)}">
19
19
  <i class="#{state_icon(dirmon.state)}" style="font-size: 75%" title="#{dirmon.state}"></i>
20
- #{dirmon.name}
20
+ #{h(dirmon.name)}
21
21
  </a>
22
22
  EOS
23
23
  end
@@ -47,7 +47,7 @@ module RocketJobMissionControl
47
47
  {display: "Started", value: :started, field: "started_at"},
48
48
  {display: "Actions", value: :action_buttons, orderable: false}
49
49
  ].freeze
50
- RUNNING_FIELDS = (COMMON_FIELDS + %i[record_count collect_output input_categories output_categories encrypt compress slice_size priority sub_state percent_complete]).freeze
50
+ RUNNING_FIELDS = (COMMON_FIELDS + %i[record_count input_categories output_categories priority sub_state percent_complete]).freeze
51
51
 
52
52
  SCHEDULED_COLUMNS = [
53
53
  {display: "Class", value: :class_with_link, field: "_type"},
@@ -56,8 +56,16 @@ module RocketJobMissionControl
56
56
  values
57
57
  end
58
58
 
59
+ def escape(s)
60
+ s.dump[1..-2]
61
+ end
62
+
63
+ def unescape(s)
64
+ "\"#{s}\"".undump
65
+ end
66
+
59
67
  # Returns the editable field as html for use in editing dynamic fields from a Job class.
60
- def editable_field_html(klass, field_name, value, f, include_nil_selectors = false)
68
+ def editable_field_html(klass, field_name, value, f)
61
69
  # When editing a job the values are of the correct type.
62
70
  # When editing a dirmon entry values are strings.
63
71
  field = klass.fields[field_name.to_s]
@@ -67,44 +75,61 @@ module RocketJobMissionControl
67
75
  placeholder = nil if placeholder.is_a?(Proc)
68
76
 
69
77
  case field.type.name
70
- when "Symbol", "String", "Integer"
78
+ when "Integer"
79
+ options = extract_inclusion_values(klass, field_name)
80
+ f.number_field(field_name, in: options, include_blank: false, value: value, class: "form-control", placeholder: placeholder)
81
+ when "String", "Symbol", "Mongoid::StringifiedSymbol"
71
82
  options = extract_inclusion_values(klass, field_name)
72
- str = "[#{field.type.name}]\n".html_safe
73
83
  if options
74
- str + f.select(field_name, options, {include_blank: options.include?(nil) || include_nil_selectors, selected: value}, {class: "selectize form-control"})
84
+ f.select(field_name, options, {include_blank: options.include?(nil), selected: value}, {class: "selectize form-control"})
75
85
  else
76
- if field.type.name == "Integer"
77
- str + f.number_field(field_name, value: value, class: "form-control", placeholder: placeholder)
78
- else
79
- str + f.text_field(field_name, value: value, class: "form-control", placeholder: placeholder)
80
- end
86
+ f.text_area(field_name, value: value ? value : "", class: "form-control", placeholder: placeholder)
81
87
  end
88
+ when "Boolean", "Mongoid::Boolean"
89
+ options = extract_inclusion_values(klass, field_name) || [nil, "true", "false"]
90
+ f.select(field_name, options, {include_blank: options.include?(nil), selected: value}, {class: "selectize form-control"})
82
91
  when "Hash"
83
92
  "[JSON Hash]\n".html_safe +
84
93
  f.text_field(field_name, value: value ? value.to_json : "", class: "form-control", placeholder: '{"key1":"value1", "key2":"value2", "key3":"value3"}')
85
94
  when "Array"
86
95
  options = Array(value)
87
- "[Array]\n".html_safe +
88
- f.select(field_name, options_for_select(options, options), {include_hidden: false}, {class: "selectize form-control", multiple: true})
89
- when "Mongoid::Boolean"
90
- name = "#{field_name}_true".to_sym
91
- value = value.to_s
92
- str = '<div class="radio-buttons">'.html_safe
93
- str << f.radio_button(field_name, "true", checked: value == "true")
94
- str << " ".html_safe + f.label(name, "true")
95
- str << " ".html_safe + f.radio_button(field_name, "false", checked: value == "false")
96
- str << " ".html_safe + f.label(name, "false")
97
- # Allow this field to be unset (nil).
98
- if include_nil_selectors
99
- str << " ".html_safe + f.radio_button(field_name, "", checked: value == "")
100
- str << " ".html_safe + f.label(name, "nil")
101
- end
102
-
103
- str << "</div>".html_safe
96
+ f.select(field_name, options_for_select(options, options), {include_hidden: false}, {class: "selectize form-control", multiple: true})
104
97
  else
105
98
  "[#{field.type.name}]".html_safe +
106
99
  f.text_field(field_name, value: value, class: "form-control", placeholder: placeholder)
107
100
  end
108
101
  end
102
+
103
+ # This method creates a link with `data-id` `data-fields` attributes. These attributes are used to create new instances of the nested fields through Javascript.
104
+ def link_to_add_fields(name, f, association, option)
105
+ # Takes an object (@job) and creates a new instance of its associated model (:properties)
106
+ new_object = f.object.send(association).klass.new
107
+
108
+ # Saves the unique ID of the object into a variable.
109
+ # This is needed to ensure the key of the associated array is unique. This is makes parsing the content in the `data-fields` attribute easier through Javascript.
110
+ # We could use another method to achive this.
111
+ id = new_object.object_id
112
+
113
+ # https://api.rubyonrails.org/ fields_for(record_name, record_object = nil, fields_options = {}, &block)
114
+ # record_name = :addresses
115
+ # record_object = new_object
116
+ # fields_options = { child_index: id }
117
+ # child_index` is used to ensure the key of the associated array is unique, and that it matched the value in the `data-id` attribute.
118
+ # `person[addresses_attributes][child_index_value][_destroy]`
119
+ fields = f.fields_for(association, new_object, child_index: id) do |builder|
120
+ # `association.to_s.singularize + "_fields"` ends up evaluating to `address_fields`
121
+ # The render function will then look for `views/people/_address_fields.html.erb`
122
+ # The render function also needs to be passed the value of 'builder', because `views/dirmon_entries/_input_categories.html.erb` needs this to render the form tags.
123
+ render(association.to_s.singularize + "_fields", f: builder)
124
+ end
125
+
126
+ # This renders a simple link, but passes information into `data` attributes.
127
+ # This info can be named anything we want, but in this case we chose `data-id:` and `data-fields:`.
128
+ # The `id:` is from `new_object.object_id`.
129
+ # The `fields:` are rendered from the `fields` blocks.
130
+ # We use `gsub("\n", "")` to remove anywhite space from the rendered partial.
131
+ # The `id:` value needs to match the value used in `child_index: id`.
132
+ link_to(name, '#', class: "add_fields btn btn-#{option}", data: { id: id, fields: fields.gsub("\n", "") })
133
+ end
109
134
  end
110
135
  end
@@ -3,5 +3,17 @@ 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
14
+
15
+ def rocket_job_mission_control
16
+ @@rocket_job_mission_control_engine_url_helpers ||= RocketJobMissionControl::Engine.routes.url_helpers
17
+ end
6
18
  end
7
19
  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
@@ -26,5 +26,9 @@ module RocketJobMissionControl
26
26
  map[server.state] || "callout-info"
27
27
  end
28
28
  end
29
+
30
+ def rocket_job_mission_control
31
+ @@rocket_job_mission_control_engine_url_helpers ||= RocketJobMissionControl::Engine.routes.url_helpers
32
+ end
29
33
  end
30
34
  end
@@ -25,9 +25,9 @@ module RocketJobMissionControl
25
25
  can %i[edit pause resume retry abort fail update run_now], RocketJob::Job
26
26
  end
27
27
 
28
- # Create, Destroy, Enable, Disable, Edit Dirmon Entries
28
+ # Create, Destroy, Enable, Disable, Edit, Copy, Replicate Dirmon Entries
29
29
  role :dirmon, {dirmon: true} do
30
- can %i[create enable disable update edit], RocketJob::DirmonEntry
30
+ can %i[create enable disable update edit copy replicate], RocketJob::DirmonEntry
31
31
  end
32
32
 
33
33
  # A User can only edit their own jobs
@@ -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
+ elsif default_job.public_send(name) != updated_job.public_send(name)
46
+ properties[name] = value.is_a?(String) ? value.gsub(/\r\n/, "\n") : value
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
 
@@ -24,6 +27,8 @@ module RocketJobMissionControl
24
27
  next unless field&.type
25
28
 
26
29
  case field.type.name
30
+ when "String"
31
+ value.gsub(/\r\n/, "\n")
27
32
  when "Hash"
28
33
  begin
29
34
  value = value.blank? ? nil : JSON.parse(value)
@@ -38,7 +43,39 @@ module RocketJobMissionControl
38
43
  permissible_params[field_name] = value
39
44
  end
40
45
  end
46
+
47
+ if properties.key?(:input_categories_attributes)
48
+ categories = sanitize_categories(properties[:input_categories_attributes])
49
+ permissible_params[:input_categories] = categories unless categories == [{}]
50
+ end
51
+
52
+ if properties.key?(:output_categories_attributes)
53
+ categories = sanitize_categories(properties[:output_categories_attributes])
54
+ permissible_params[:output_categories] = categories unless categories == [{}]
55
+ end
56
+
41
57
  permissible_params
42
58
  end
59
+
60
+ def self.sanitize_categories(properties)
61
+ categories = []
62
+
63
+ properties.each_pair do |_, category|
64
+ hash = {}
65
+ CATEGORIES_FIELDS.each do |key|
66
+ next unless category.key?(key)
67
+
68
+ value = category[key]
69
+ next if value.blank?
70
+ next if (key == :columns) && value == [""]
71
+
72
+ value = JSON.parse(value) if key == :format_options
73
+ hash[key] = value
74
+ end
75
+ categories << hash
76
+ end
77
+
78
+ categories
79
+ end
43
80
  end
44
81
  end