Skip to content

Commit 146d843

Browse files
authored
Fixes #39246 - Add sorting and searching to the new Leapp preupgrade report (#171)
* Fixes #39246 - Add sorting and searching to the new Leapp preupgrade report * fix bulk_remediate and remove div * address code review * fix "Fix Selected" with no fixable entries + replace redux-mock-store * add custom layout for preupgrade entries index because of fixable_count * replace NoReports snapshot test with RTL * Revert "add custom layout for preupgrade entries index because of fixable_count" This reverts commit 71e7469. * fetch fixable count on demand for Select All * fix PreupgradeReportsTable.test.js * fix SelectAllCheckbox count glitch and invalid prop * fix PreupgradeReportsTable.test.js * review fix - SelectAllCheckbox in model_of_controller * fix subtotal fallback in pagination count
1 parent 79e73de commit 146d843

15 files changed

Lines changed: 1311 additions & 332 deletions

File tree

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# frozen_string_literal: true
2+
3+
module Api
4+
module V2
5+
class PreupgradeReportEntriesController < ::Api::V2::BaseController
6+
include ApiAuthorizer
7+
include Foreman::Controller::AutoCompleteSearch
8+
9+
skip_before_action :store_redirect_to_url, :reset_redirect_to_url, raise: false
10+
before_action :find_preupgrade_report, if: -> { params[:preupgrade_report_id].present? }
11+
12+
api :GET, '/preupgrade_reports/:preupgrade_report_id/preupgrade_report_entries',
13+
N_('List entries for a specific preupgrade report')
14+
api :GET, '/preupgrade_report_entries', N_('List all preupgrade report entries')
15+
param :preupgrade_report_id, :identifier, required: false,
16+
desc: N_('ID of the preupgrade report')
17+
param_group :search_and_pagination, ::Api::V2::BaseController
18+
def index
19+
@total = resource_scope.count
20+
@preupgrade_report_entries = resource_scope_for_index
21+
@subtotal = @preupgrade_report_entries.total_entries
22+
end
23+
24+
api :GET, '/preupgrade_report_entries/auto_complete_search',
25+
N_('Search autocomplete for preupgrade report entries')
26+
param :search, String, required: false, desc: N_('Search string')
27+
param :preupgrade_report_id, :identifier, required: false, desc: N_('ID of the preupgrade report')
28+
29+
api :POST, '/preupgrade_reports/:preupgrade_report_id/preupgrade_report_entries/bulk_remediate',
30+
N_('Trigger a remediation job for selected preupgrade report entries')
31+
param :search, String, required: false, desc: N_('Search string')
32+
param :excluded_ids, Array, required: false, desc: N_('Array of excluded entry IDs')
33+
def bulk_remediate
34+
entries = filtered_remediation_entries
35+
remediation_ids = entries.pluck(:id)
36+
37+
if remediation_ids.empty?
38+
return render json: { error: _('No fixable entries found matching the selection.') },
39+
status: :unprocessable_entity
40+
end
41+
42+
composer = JobInvocationComposer.for_feature(
43+
'leapp_remediation_plan',
44+
target_host_ids(entries),
45+
{ 'remediation_ids' => remediation_ids.join(',') }
46+
)
47+
composer.trigger!
48+
job_invocation = composer.job_invocation
49+
50+
render json: { id: job_invocation.id }
51+
rescue StandardError => e
52+
Foreman::Logging.exception('Failed to trigger bulk remediation job', e)
53+
render json: { error: _('An unexpected error occurred while creating the remediation job.') },
54+
status: :internal_server_error
55+
end
56+
57+
protected
58+
59+
def model_of_controller
60+
PreupgradeReportEntry
61+
end
62+
63+
def resource_scope(_options = {})
64+
scope = if @preupgrade_report
65+
@preupgrade_report.preupgrade_report_entries
66+
else
67+
PreupgradeReportEntry
68+
end
69+
scope.joins(:host).merge(Host.authorized(:view_hosts, Host))
70+
end
71+
72+
def resource_scope_for_index(options = {})
73+
order_str = params[:order].to_s
74+
return super unless order_str.match?(/^(severity|risk_factor)\b/i)
75+
direction = order_str.match?(/desc$/i) ? 'DESC' : 'ASC'
76+
77+
resource_scope(options)
78+
.search_for(params[:search].to_s)
79+
.order_by_severity(direction)
80+
.paginate(paginate_options)
81+
end
82+
83+
private
84+
85+
def filtered_remediation_entries
86+
combined_search = [params[:search].presence, 'fix_type = command'].compact.join(' AND ')
87+
entries = resource_scope.search_for(combined_search)
88+
entries = entries.where.not(id: params[:excluded_ids]) if params[:excluded_ids].present?
89+
entries
90+
end
91+
92+
def target_host_ids(entries)
93+
host_ids = entries.pluck(:host_id).uniq.compact
94+
host_ids = [@preupgrade_report.host_id].compact if host_ids.empty? && @preupgrade_report
95+
host_ids
96+
end
97+
98+
def find_preupgrade_report
99+
@preupgrade_report = PreupgradeReport
100+
.joins(:host)
101+
.merge(Host.authorized(:view_hosts, Host))
102+
.find(params[:preupgrade_report_id])
103+
end
104+
105+
def path_to_authenticate
106+
auth_action = case action_name
107+
when 'auto_complete_search'
108+
'index'
109+
when 'bulk_remediate'
110+
'create'
111+
else
112+
action_name
113+
end
114+
path_params = params.slice(:id, :user_id)
115+
.merge(action: auth_action, controller: 'api/v2/job_invocations')
116+
Foreman::AccessControl.normalize_path_hash(path_params)
117+
end
118+
end
119+
end
120+
end

app/controllers/api/v2/preupgrade_reports_controller.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ def job_invocation
3030
# By overriding path_to_authenticate we can require REX's permission view_job_invocations
3131
def path_to_authenticate
3232
params['action'] = 'show' if params['action'] == 'job_invocation'
33-
Foreman::AccessControl.normalize_path_hash params.slice(:action, :id, :user_id)
34-
.merge({ controller: 'api/v2/job_invocations' })
33+
path_params = params.slice(:action, :id, :user_id).merge(controller: 'api/v2/job_invocations')
34+
Foreman::AccessControl.normalize_path_hash(path_params)
3535
end
3636
end
3737
end

app/models/preupgrade_report_entry.rb

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,81 @@ class PreupgradeReportEntry < ApplicationRecord
88
serialize :flags, Array
99
serialize :detail, JSON
1010

11-
validates :preupgrade_report, :host, :hostname, :title, :actor, :audience, :severity, :leapp_run_id, presence: true
11+
validates :preupgrade_report, :host, :hostname, :title, :actor,
12+
:audience, :severity, :leapp_run_id, presence: true
13+
14+
scoped_search on: :title, complete_value: true
15+
scoped_search on: :hostname, complete_value: true
16+
scoped_search on: :summary
17+
18+
scoped_search on: :severity,
19+
rename: :risk_factor,
20+
complete_value: { high: 'high', medium: 'medium', low: 'low', info: 'info' }
21+
22+
scoped_search on: :flags,
23+
rename: :inhibitor,
24+
only_explicit: true,
25+
operators: ['=', '!='],
26+
complete_value: { yes: 'yes', no: 'no' },
27+
ext_method: :search_yes_no_fields
28+
29+
scoped_search on: :detail,
30+
rename: :has_remediation,
31+
only_explicit: true,
32+
operators: ['=', '!='],
33+
complete_value: { yes: 'yes', no: 'no' },
34+
ext_method: :search_yes_no_fields
35+
36+
scoped_search on: :detail,
37+
rename: :fix_type,
38+
only_explicit: true,
39+
operators: ['=', '!='],
40+
complete_value: { hint: 'hint', command: 'command' },
41+
ext_method: :search_fix_type
42+
43+
# info(1) < low(2) < medium(3) < high(4)
44+
scope :order_by_severity, lambda { |direction = 'ASC'|
45+
dir = direction.to_s.casecmp('DESC').zero? ? 'DESC' : 'ASC'
46+
order(Arel.sql(
47+
"CASE preupgrade_report_entries.severity " \
48+
"WHEN 'high' THEN 4 WHEN 'medium' THEN 3 " \
49+
"WHEN 'low' THEN 2 WHEN 'info' THEN 1 ELSE 0 END #{dir}"
50+
))
51+
}
1252

1353
def self.remediation_details(remediation_ids, host)
1454
where(id: remediation_ids, host: host).where.not(detail: nil).pluck(:detail)
1555
end
56+
57+
def self.search_yes_no_fields(key, operator, value)
58+
column, term = case key.to_sym
59+
when :inhibitor then %w[flags inhibitor]
60+
when :has_remediation then %w[detail remediations]
61+
else raise Foreman::Exception,
62+
N_("Unknown search key '%s' for search_yes_no_fields") % key
63+
end
64+
65+
if (operator == '=' && value == 'yes') || (operator == '!=' && value == 'no')
66+
{ conditions: "preupgrade_report_entries.#{column} LIKE '%%#{term}%%'" }
67+
else
68+
{ conditions: "(preupgrade_report_entries.#{column} NOT LIKE '%%#{term}%%' OR " \
69+
"preupgrade_report_entries.#{column} IS NULL)" }
70+
end
71+
end
72+
73+
def self.search_fix_type(_key, operator, value)
74+
unless %w[hint command].include?(value.to_s.downcase)
75+
raise Foreman::Exception, N_("Unknown fix_type value '%s' for search_fix_type") % value
76+
end
77+
78+
safe_value = sanitize_sql_like(value.to_s.downcase)
79+
like_op = (operator == '=') ? 'LIKE' : 'NOT LIKE'
80+
81+
conditions = "(preupgrade_report_entries.detail #{like_op} " \
82+
"'%%\"type\":\"#{safe_value}\"%%' OR " \
83+
"preupgrade_report_entries.detail #{like_op} " \
84+
"'%%\"type\": \"#{safe_value}\"%%')"
85+
conditions = "(#{conditions} OR preupgrade_report_entries.detail IS NULL)" if operator == '!='
86+
{ conditions: conditions }
87+
end
1688
end
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# frozen_string_literal: true
2+
3+
collection @preupgrade_report_entries
4+
extends 'api/v2/preupgrade_report_entries/base'
Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
object @preupgrade_report
1+
# frozen_string_literal: true
22

3+
object @preupgrade_report
34
extends 'api/v2/preupgrade_reports/base'
4-
5-
child :preupgrade_report_entries do
6-
extends 'api/v2/preupgrade_report_entries/base'
7-
end

config/routes.rb

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
11
# frozen_string_literal: true
22

33
Rails.application.routes.draw do
4-
resources :preupgrade_reports, :only => %i[index]
4+
resources :preupgrade_reports, only: %i[index]
55

66
namespace :api, defaults: { format: 'json' } do
77
scope '(:apiv)', module: :v2, defaults: { apiv: 'v2' }, apiv: /v2/,
8-
constraints: ApiConstraints.new( version: 2, default: true) do
9-
resources :preupgrade_reports, only: %i[index show]
8+
constraints: ApiConstraints.new(version: 2, default: true) do
9+
resources :preupgrade_report_entries, only: %i[index] do
10+
collection do
11+
get :auto_complete_search
12+
end
13+
end
14+
15+
resources :preupgrade_reports, only: %i[index show] do
16+
resources :preupgrade_report_entries, only: %i[index] do
17+
collection do
18+
get :auto_complete_search
19+
post :bulk_remediate
20+
end
21+
end
22+
end
23+
1024
get 'job_invocations/:id/preupgrade_reports', to: 'preupgrade_reports#job_invocation'
1125
end
1226
end

0 commit comments

Comments
 (0)