foreman_remote_execution 17.2.0 → 18.0.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 (26) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/foreign_input_sets_controller.rb +4 -0
  3. data/app/controllers/job_invocations_controller.rb +16 -1
  4. data/config/routes.rb +2 -1
  5. data/db/seeds.d/60-ssh_proxy_feature.rb +2 -2
  6. data/db/seeds.d/90-bookmarks.rb +1 -1
  7. data/db/seeds.d/95-mail_notifications.rb +1 -1
  8. data/lib/foreman_remote_execution/plugin.rb +2 -2
  9. data/lib/foreman_remote_execution/version.rb +1 -1
  10. data/test/functional/job_invocations_controller_test.rb +84 -0
  11. data/webpack/JobInvocationDetail/JobInvocationConstants.js +10 -2
  12. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +8 -1
  13. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +6 -78
  14. data/webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js +113 -7
  15. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +4 -39
  16. data/webpack/JobInvocationDetail/__tests__/areAllHostsTerminal.test.js +40 -0
  17. data/webpack/JobInvocationDetail/__tests__/fixtures.js +0 -8
  18. data/webpack/JobWizard/Footer.js +16 -13
  19. data/webpack/JobWizard/__tests__/Footer.test.js +64 -0
  20. data/webpack/JobWizard/__tests__/integration.test.js +98 -53
  21. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +191 -212
  22. data/webpack/react_app/components/TargetingHosts/__tests__/HostItem.test.js +73 -3
  23. data/webpack/react_app/components/jobInvocations/AggregateStatus/index.test.js +69 -36
  24. metadata +5 -5
  25. data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +0 -51
  26. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostItem.test.js.snap +0 -31
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e240991349f9cb6bc60f979202f8de2bde03d3d8100c13e701469aa2f8e29a97
4
- data.tar.gz: f9936d90d62ce33221969367e5625cdf750c36922855142f460f46b18a8bc925
3
+ metadata.gz: 4b301e962ec24666ada0b4a297277232ee94dc74a36b86875f4ce2230659c254
4
+ data.tar.gz: 923c5679450846add2cfba3a6741654e7bfd06f23e9e33b8ad5f19dfd2b6c217
5
5
  SHA512:
6
- metadata.gz: eaf96768e4b112d387e660cca80d6b9dd360dd1145cf40b5540ea428bdf4bf3482e47591a05d2e2f0bcc05e36808e8163346258f323cc7f7f60917db3b930686
7
- data.tar.gz: 56dbf3ac2a8cb8f90c8ae719fc5213c2fbf40588812bf98552fa8bdd967b6d1150632543dfb6992df09050d74813dd3eeb367cc857c3709a921a404b3772c823
6
+ metadata.gz: 561489f3cee2bf28837084baa1fd3fb8b3a68b8acd632c0337f1b3204dc176acafefb85ecc15d058c464038ec38413a56899ce4ebd2d723596571622cb9c130f
7
+ data.tar.gz: '0938ab65b49a36d63fb709d905322559305ebd691f83bcf1aee8d81b9cb6cffd83db2fe50381295b7f589d37beddc457a7ee0aef3b8c02819f12008c9f461461'
@@ -76,6 +76,10 @@ module Api
76
76
  def resource_class
77
77
  ForeignInputSet
78
78
  end
79
+
80
+ def allowed_nested_id
81
+ %w(template_id)
82
+ end
79
83
  end
80
84
  end
81
85
  end
@@ -2,6 +2,7 @@ class JobInvocationsController < ApplicationController
2
2
  include ::Foreman::Controller::AutoCompleteSearch
3
3
  include ::ForemanTasks::Concerns::Parameters::Triggering
4
4
  include ::JobInvocationsChartHelper
5
+ include RemoteExecutionHelper
5
6
 
6
7
  def new
7
8
  return @composer = prepare_composer if params[:feature].present?
@@ -110,6 +111,20 @@ class JobInvocationsController < ApplicationController
110
111
  render :partial => 'job_invocations/preview_hosts_list'
111
112
  end
112
113
 
114
+ def report
115
+ @job_invocation = resource_base.find(params[:id])
116
+ template = job_report_template
117
+ unless template
118
+ return not_found(_('Report template not found or not configured properly'))
119
+ end
120
+
121
+ unless ReportTemplate.authorized(:generate_report_templates).find_by(:id => template.id)
122
+ return render_403(_('Missing permissions to generate report templates'))
123
+ end
124
+
125
+ redirect_to generate_report_template_path(template, job_report_template_parameters(@job_invocation, template))
126
+ end
127
+
113
128
  def cancel
114
129
  @job_invocation = resource_base.find(params[:id])
115
130
  result = @job_invocation.cancel(params[:force])
@@ -159,7 +174,7 @@ class JobInvocationsController < ApplicationController
159
174
  'create'
160
175
  when 'cancel'
161
176
  'cancel'
162
- when 'chart', 'preview_job_invocations_per_host'
177
+ when 'chart', 'preview_job_invocations_per_host', 'report'
163
178
  'view'
164
179
  else
165
180
  super
data/config/routes.rb CHANGED
@@ -36,6 +36,7 @@ Rails.application.routes.draw do
36
36
  end
37
37
  member do
38
38
  post 'cancel'
39
+ get 'report'
39
40
  end
40
41
  end
41
42
 
@@ -70,7 +71,7 @@ Rails.application.routes.draw do
70
71
  get 'hosts'
71
72
  post 'cancel'
72
73
  post 'rerun'
73
- get 'template_invocations', :to => 'template_invocations#template_invocations'
74
+ get 'template_invocations', :to => 'template_invocations#template_invocations'
74
75
  get 'outputs'
75
76
  post 'outputs'
76
77
  end
@@ -1,5 +1,5 @@
1
1
  f = Feature.where(:name => 'SSH').first_or_create
2
- raise "Unable to create proxy feature: #{format_errors f}" if f.nil? || f.errors.any?
2
+ raise "Unable to create proxy feature: #{SeedHelper.format_errors(f)}" if f.nil? || f.errors.any?
3
3
 
4
4
  f = Feature.where(:name => 'Script').first_or_create
5
- raise "Unable to create proxy feature: #{format_errors f}" if f.nil? || f.errors.any?
5
+ raise "Unable to create proxy feature: #{SeedHelper.format_errors(f)}" if f.nil? || f.errors.any?
@@ -15,6 +15,6 @@ Bookmark.without_auditing do
15
15
  b = Bookmark.where(:name => input[:name], :controller => input[:controller]).first || Bookmark.new
16
16
  b.attributes = attributes
17
17
  b.save
18
- raise "Unable to create bookmark: #{format_errors b}" if b.errors.any?
18
+ raise "Unable to create bookmark: #{SeedHelper.format_errors(b)}" if b.errors.any?
19
19
  end
20
20
  end
@@ -18,7 +18,7 @@ notifications.each do |notification|
18
18
  created_notification = RexMailNotification.create(notification)
19
19
  if created_notification.nil? || created_notification.errors.any?
20
20
  raise ::Foreman::Exception.new(N_("Unable to create mail notification: %s"),
21
- format_errors(created_notification))
21
+ SeedHelper.format_errors(created_notification))
22
22
  end
23
23
  end
24
24
  end
@@ -1,5 +1,5 @@
1
1
  Foreman::Plugin.register :foreman_remote_execution do
2
- requires_foreman '>= 5.0'
2
+ requires_foreman '>= 5.1'
3
3
  register_global_js_file 'global'
4
4
  register_gettext
5
5
 
@@ -143,7 +143,7 @@ Foreman::Plugin.register :foreman_remote_execution do
143
143
  permission :lock_job_templates, { :job_templates => [:lock, :unlock] }, :resource_type => 'JobTemplate'
144
144
  permission :create_job_invocations, { :job_invocations => [:new, :create, :legacy_create, :refresh, :rerun, :preview_hosts],
145
145
  'api/v2/job_invocations' => [:create, :rerun] }, :resource_type => 'JobInvocation'
146
- permission :view_job_invocations, { :job_invocations => [:index, :chart, :show, :auto_complete_search, :preview_job_invocations_per_host], :template_invocations => [:show, :show_template_invocation_by_host],
146
+ permission :view_job_invocations, { :job_invocations => [:index, :chart, :show, :auto_complete_search, :preview_job_invocations_per_host, :report], :template_invocations => [:show, :show_template_invocation_by_host],
147
147
  'api/v2/job_invocations' => [:index, :show, :output, :raw_output, :outputs, :hosts] }, :resource_type => 'JobInvocation'
148
148
  permission :view_template_invocations, { :template_invocations => [:show, :template_invocation_preview, :show_template_invocation_by_host],
149
149
  'api/v2/template_invocations' => [:template_invocations], :ui_job_wizard => [:job_invocation] }, :resource_type => 'TemplateInvocation'
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '17.2.0'.freeze
2
+ VERSION = '18.0.0'.freeze
3
3
  end
@@ -60,6 +60,90 @@ class JobInvocationsControllerTest < ActionController::TestCase
60
60
  assert_response :success
61
61
  end
62
62
 
63
+ context '#report' do
64
+ setup do
65
+ @invocation = FactoryBot.create(:job_invocation, :with_template, :with_task)
66
+ @report_template = FactoryBot.create(:report_template, :name => 'Job - Invocation Report', :template => '<%= "report output" %>')
67
+ @report_template.template_inputs.create!(:name => 'job_id', :input_type => 'user')
68
+ Setting['remote_execution_job_invocation_report_template'] = @report_template.name
69
+ end
70
+
71
+ test 'should redirect to report generation page' do
72
+ get :report, params: { :id => @invocation.id }, session: set_session_user
73
+ template_input = @report_template.template_inputs.where(name: 'job_id').first
74
+ expected_params = {
75
+ report_template_report: {
76
+ input_values: {
77
+ "#{template_input.id}": {
78
+ value: @invocation.id,
79
+ },
80
+ },
81
+ },
82
+ }
83
+ assert_redirected_to generate_report_template_path(@report_template, expected_params)
84
+ end
85
+
86
+ test 'should redirect to report generation page with custom template name' do
87
+ custom_template = FactoryBot.create(:report_template, :name => 'My Custom Job Report', :template => '<%= "custom report" %>')
88
+ custom_template.template_inputs.create!(:name => 'job_id', :input_type => 'user')
89
+ Setting['remote_execution_job_invocation_report_template'] = custom_template.name
90
+
91
+ get :report, params: { :id => @invocation.id }, session: set_session_user
92
+ template_input = custom_template.template_inputs.where(name: 'job_id').first
93
+ expected_params = {
94
+ report_template_report: {
95
+ input_values: {
96
+ "#{template_input.id}": {
97
+ value: @invocation.id,
98
+ },
99
+ },
100
+ },
101
+ }
102
+ assert_redirected_to generate_report_template_path(custom_template, expected_params)
103
+ end
104
+
105
+ test 'should return 404 when report template is not configured' do
106
+ Setting['remote_execution_job_invocation_report_template'] = 'Nonexistent Template'
107
+ get :report, params: { :id => @invocation.id }, session: set_session_user
108
+ assert_response :not_found
109
+ end
110
+
111
+ test 'should deny access when user lacks generate_report_templates permission' do
112
+ user = FactoryBot.create(:user, :admin => false)
113
+ @report_template.organizations = user.organizations
114
+ @report_template.locations = user.locations
115
+ setup_user('view', 'job_invocations', nil, user)
116
+ setup_user('view', 'hosts', nil, user)
117
+ setup_user('view', 'report_templates', nil, user)
118
+
119
+ get :report, params: { :id => @invocation.id }, session: set_session_user(user)
120
+ assert_response :forbidden
121
+ end
122
+
123
+ test 'should redirect when user has generate_report_templates permission' do
124
+ user = FactoryBot.create(:user, :admin => false)
125
+ @report_template.organizations = user.organizations
126
+ @report_template.locations = user.locations
127
+ setup_user('view', 'job_invocations', nil, user)
128
+ setup_user('view', 'hosts', nil, user)
129
+ setup_user('view', 'report_templates', nil, user)
130
+ setup_user('generate', 'report_templates', nil, user)
131
+
132
+ get :report, params: { :id => @invocation.id }, session: set_session_user(user)
133
+ template_input = @report_template.template_inputs.where(name: 'job_id').first
134
+ expected_params = {
135
+ report_template_report: {
136
+ input_values: {
137
+ "#{template_input.id}": {
138
+ value: @invocation.id,
139
+ },
140
+ },
141
+ },
142
+ }
143
+ assert_redirected_to generate_report_template_path(@report_template, expected_params)
144
+ end
145
+ end
146
+
63
147
  context 'restricted access' do
64
148
  setup do
65
149
  @admin = users(:admin)
@@ -10,8 +10,6 @@ export const CANCEL_JOB = 'CANCEL_JOB';
10
10
  export const GET_TEMPLATE_INVOCATIONS = 'GET_TEMPLATE_INVOCATIONS';
11
11
  export const CHANGE_ENABLED_RECURRING_LOGIC = 'CHANGE_ENABLED_RECURRING_LOGIC';
12
12
  export const CANCEL_RECURRING_LOGIC = 'CANCEL_RECURRING_LOGIC';
13
- export const GET_REPORT_TEMPLATES = 'GET_REPORT_TEMPLATES';
14
- export const GET_REPORT_TEMPLATE_INPUTS = 'GET_REPORT_TEMPLATE_INPUTS';
15
13
  export const JOB_INVOCATION_HOSTS = 'JOB_INVOCATION_HOSTS';
16
14
  export const GET_TEMPLATE_INVOCATION = 'GET_TEMPLATE_INVOCATION';
17
15
  export const DIRECT_OPEN_HOST_LIMIT = 3;
@@ -57,6 +55,16 @@ export const STATUS_TITLES = {
57
55
  NOT_STARTED: { id: 'N/A', title: __('Scheduled') },
58
56
  };
59
57
 
58
+ export const TERMINAL_HOST_STATUSES = new Set([
59
+ 'success',
60
+ 'error',
61
+ 'cancelled',
62
+ ]);
63
+
64
+ export const areAllHostsTerminal = results =>
65
+ results?.length > 0 &&
66
+ results.every(host => TERMINAL_HOST_STATUSES.has(host.job_status));
67
+
60
68
  export const DATE_OPTIONS = {
61
69
  day: 'numeric',
62
70
  month: 'short',
@@ -42,6 +42,7 @@ import Columns, {
42
42
  STATUS_UPPERCASE,
43
43
  AWAITING_STATUS_FILTER,
44
44
  AUTO_REFRESH_INTERVAL_MS,
45
+ areAllHostsTerminal,
45
46
  } from './JobInvocationConstants';
46
47
  import { TemplateInvocation } from './TemplateInvocation';
47
48
  import { RowActions } from './TemplateInvocationComponents/TemplateActionButtons';
@@ -196,7 +197,13 @@ const JobInvocationHostTable = ({
196
197
  if (thisRequest !== requestIdRef.current) return;
197
198
  if (!mountedRef.current) return;
198
199
  updateHostsState(data);
199
- if (!jobFinishedRef.current) {
200
+ const hasActiveFilter = !!(
201
+ currentPollParams.current.search ||
202
+ currentPollParams.current.awaiting
203
+ );
204
+ const pageAllTerminal =
205
+ !hasActiveFilter && areAllHostsTerminal(data.data?.results);
206
+ if (!jobFinishedRef.current && !pageAllTerminal) {
200
207
  pollTimeoutId.current = setTimeout(
201
208
  () => makeApiCall(currentPollParams.current),
202
209
  AUTO_REFRESH_INTERVAL_MS
@@ -1,6 +1,5 @@
1
- /* eslint-disable max-lines */
2
1
  import PropTypes from 'prop-types';
3
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import React, { useCallback, useMemo, useState } from 'react';
4
3
  import { useDispatch, useSelector } from 'react-redux';
5
4
  import { Button, Split, SplitItem } from '@patternfly/react-core';
6
5
  import { UndoIcon } from '@patternfly/react-icons';
@@ -14,17 +13,12 @@ import {
14
13
  import { translate as __ } from 'foremanReact/common/I18n';
15
14
  import { foremanUrl } from 'foremanReact/common/helpers';
16
15
  import { usePermissions } from 'foremanReact/common/hooks/Permissions/permissionHooks';
17
- import { get } from 'foremanReact/redux/API';
18
16
  import {
19
17
  cancelJob,
20
18
  cancelRecurringLogic,
21
19
  enableRecurringLogic,
22
20
  } from './JobInvocationActions';
23
- import {
24
- STATUS,
25
- GET_REPORT_TEMPLATES,
26
- GET_REPORT_TEMPLATE_INPUTS,
27
- } from './JobInvocationConstants';
21
+ import { STATUS } from './JobInvocationConstants';
28
22
  import { selectTaskCancelable } from './JobInvocationSelectors';
29
23
 
30
24
  const JobInvocationToolbarButtons = ({ jobId, data }) => {
@@ -43,26 +37,13 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
43
37
  'generate_report_templates',
44
38
  ]);
45
39
  const [isActionOpen, setIsActionOpen] = useState(false);
46
- const [reportTemplateJobId, setReportTemplateJobId] = useState(undefined);
47
- const [templateInputId, setTemplateInputId] = useState(undefined);
48
40
  const dispatch = useDispatch();
49
- const reportHref = useMemo(() => {
50
- if (reportTemplateJobId === undefined || templateInputId === undefined) {
51
- return undefined;
52
- }
53
- const queryParams = new URLSearchParams({
54
- [`report_template_report[input_values][${templateInputId}][value]`]: jobId,
55
- });
56
- return foremanUrl(
57
- `/templates/report_templates/${reportTemplateJobId}/generate?${queryParams.toString()}`
58
- );
59
- }, [jobId, reportTemplateJobId, templateInputId]);
41
+ const reportHref = foremanUrl(`/job_invocations/${jobId}/report`);
60
42
 
61
43
  const isCreateReportDisabled =
62
44
  !canGenerateReportTemplates ||
63
45
  task?.state === STATUS.RUNNING ||
64
- task?.state === STATUS.PENDING ||
65
- reportHref === undefined;
46
+ task?.state === STATUS.PENDING;
66
47
 
67
48
  const onActionFocus = useCallback(() => {
68
49
  const element = document.getElementById(
@@ -78,58 +59,6 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
78
59
  }, [onActionFocus]);
79
60
  const onActionToggle = useCallback((_event, val) => setIsActionOpen(val), []);
80
61
 
81
- useEffect(() => {
82
- let isMounted = true;
83
- dispatch(
84
- get({
85
- key: GET_REPORT_TEMPLATES,
86
- url: '/api/report_templates',
87
- handleSuccess: ({ data: { results } }) => {
88
- if (isMounted) {
89
- setReportTemplateJobId(
90
- results.find(result => result.name === 'Job - Invocation Report')
91
- ?.id
92
- );
93
- }
94
- },
95
- handleError: () => {
96
- if (isMounted) {
97
- setReportTemplateJobId(undefined);
98
- }
99
- },
100
- })
101
- );
102
- return () => {
103
- isMounted = false;
104
- };
105
- }, [dispatch]);
106
- useEffect(() => {
107
- let isMounted = true;
108
- if (reportTemplateJobId !== undefined) {
109
- dispatch(
110
- get({
111
- key: GET_REPORT_TEMPLATE_INPUTS,
112
- url: `/api/templates/${reportTemplateJobId}/template_inputs`,
113
- handleSuccess: ({ data: { results } }) => {
114
- if (isMounted) {
115
- setTemplateInputId(
116
- results.find(result => result.name === 'job_id')?.id
117
- );
118
- }
119
- },
120
- handleError: () => {
121
- if (isMounted) {
122
- setTemplateInputId(undefined);
123
- }
124
- },
125
- })
126
- );
127
- }
128
- return () => {
129
- isMounted = false;
130
- };
131
- }, [dispatch, reportTemplateJobId]);
132
-
133
62
  const recurrenceDropdownItems = useMemo(
134
63
  () =>
135
64
  recurrence
@@ -262,7 +191,7 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
262
191
  variant="control"
263
192
  isDisabled={!canCreateJobInvocations}
264
193
  >
265
- {__(`Rerun all`)}
194
+ {__('Rerun all')}
266
195
  </Button>,
267
196
  ]}
268
197
  splitButtonVariant="action"
@@ -279,12 +208,11 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
279
208
  <Button
280
209
  component="a"
281
210
  ouiaId="button-create-report"
282
- className="button-create-report"
283
211
  href={reportHref}
284
212
  variant="secondary"
285
213
  isDisabled={isCreateReportDisabled}
286
214
  >
287
- {__(`Create report`)}
215
+ {__('Create report')}
288
216
  </Button>
289
217
  </SplitItem>
290
218
  <SplitItem>
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  import React from 'react';
2
3
  import { render, act } from '@testing-library/react';
3
4
  import '@testing-library/jest-dom';
@@ -44,13 +45,28 @@ const hostsResponse = {
44
45
  operatingsystem_name: 'RHEL 9',
45
46
  hostgroup_id: 1,
46
47
  hostgroup_name: 'default',
47
- job_status: 'success',
48
+ job_status: 'running',
48
49
  smart_proxy_id: 1,
49
50
  smart_proxy_name: 'proxy1',
50
51
  },
51
52
  ],
52
53
  };
53
54
 
55
+ const terminalHostsResponse = {
56
+ ...hostsResponse,
57
+ results: [{ ...hostsResponse.results[0], job_status: 'success' }],
58
+ };
59
+
60
+ const mixedHostsResponse = {
61
+ ...hostsResponse,
62
+ total: 2,
63
+ subtotal: 2,
64
+ results: [
65
+ { ...hostsResponse.results[0], id: 1, job_status: 'success' },
66
+ { ...hostsResponse.results[0], id: 2, job_status: 'running' },
67
+ ],
68
+ };
69
+
54
70
  let apiGetSpy;
55
71
  let hostsCalls;
56
72
  let pendingCallbacks;
@@ -66,9 +82,9 @@ const flushPendingCallbacks = () => {
66
82
  batch.forEach(cb => cb());
67
83
  };
68
84
 
69
- const renderTable = (props = {}) => {
85
+ const renderTable = (props = {}, historyEntries = ['/']) => {
70
86
  const store = createStore();
71
- const history = createMemoryHistory();
87
+ const history = createMemoryHistory({ initialEntries: historyEntries });
72
88
  const Wrapper = createForemanContextWrapper();
73
89
 
74
90
  const defaultProps = {
@@ -93,16 +109,14 @@ const renderTable = (props = {}) => {
93
109
  return { ...result, store, history };
94
110
  };
95
111
 
96
- const setupSuccessMock = () => {
112
+ const setupSuccessMock = (response = hostsResponse) => {
97
113
  apiGetSpy = jest
98
114
  .spyOn(APIActions, 'get')
99
115
  .mockImplementation(opts => dispatch => {
100
116
  if (opts.key === JOB_INVOCATION_HOSTS) {
101
117
  hostsCalls.push(opts);
102
118
  if (opts.handleSuccess) {
103
- pendingCallbacks.push(() =>
104
- opts.handleSuccess({ data: hostsResponse })
105
- );
119
+ pendingCallbacks.push(() => opts.handleSuccess({ data: response }));
106
120
  }
107
121
  }
108
122
  });
@@ -311,6 +325,98 @@ describe('JobInvocationHostTable polling', () => {
311
325
  expect(hostsCalls.length).toBeGreaterThan(callsAfterFilterChange);
312
326
  });
313
327
 
328
+ it('stops polling when all hosts on the page are terminal', () => {
329
+ apiGetSpy.mockRestore();
330
+ setupSuccessMock(terminalHostsResponse);
331
+
332
+ renderTable();
333
+
334
+ expect(hostsCalls).toHaveLength(1);
335
+
336
+ act(() => {
337
+ flushPendingCallbacks();
338
+ });
339
+
340
+ act(() => {
341
+ jest.advanceTimersByTime(10000);
342
+ });
343
+
344
+ act(() => {
345
+ flushPendingCallbacks();
346
+ });
347
+
348
+ expect(hostsCalls).toHaveLength(1);
349
+ });
350
+
351
+ it('continues polling when at least one host on the page is non-terminal', () => {
352
+ apiGetSpy.mockRestore();
353
+ setupSuccessMock(mixedHostsResponse);
354
+
355
+ renderTable();
356
+
357
+ expect(hostsCalls).toHaveLength(1);
358
+
359
+ act(() => {
360
+ flushPendingCallbacks();
361
+ });
362
+
363
+ act(() => {
364
+ jest.advanceTimersByTime(5000);
365
+ });
366
+
367
+ act(() => {
368
+ flushPendingCallbacks();
369
+ });
370
+
371
+ expect(hostsCalls).toHaveLength(2);
372
+ });
373
+
374
+ it('continues polling when all hosts on the page are terminal but a status filter is active', () => {
375
+ apiGetSpy.mockRestore();
376
+ setupSuccessMock(terminalHostsResponse);
377
+
378
+ renderTable({ initialFilter: 'success' });
379
+
380
+ expect(hostsCalls).toHaveLength(1);
381
+
382
+ act(() => {
383
+ flushPendingCallbacks();
384
+ });
385
+
386
+ act(() => {
387
+ jest.advanceTimersByTime(5000);
388
+ });
389
+
390
+ act(() => {
391
+ flushPendingCallbacks();
392
+ });
393
+
394
+ expect(hostsCalls).toHaveLength(2);
395
+ });
396
+
397
+ it('continues polling when all hosts on the page are terminal but a search query is active', () => {
398
+ apiGetSpy.mockRestore();
399
+ setupSuccessMock(terminalHostsResponse);
400
+
401
+ renderTable({}, ['/?search=host1']);
402
+
403
+ expect(hostsCalls).toHaveLength(1);
404
+
405
+ act(() => {
406
+ flushPendingCallbacks();
407
+ });
408
+
409
+ act(() => {
410
+ jest.advanceTimersByTime(5000);
411
+ });
412
+
413
+ act(() => {
414
+ flushPendingCallbacks();
415
+ });
416
+
417
+ expect(hostsCalls).toHaveLength(2);
418
+ });
419
+
314
420
  it('sends include_permissions only on the first request', () => {
315
421
  renderTable();
316
422
 
@@ -15,8 +15,6 @@ import {
15
15
  jobInvocationData,
16
16
  jobInvocationDataScheduled,
17
17
  jobInvocationDataRecurring,
18
- mockReportTemplatesResponse,
19
- mockReportTemplateInputsResponse,
20
18
  } from './fixtures';
21
19
  import {
22
20
  cancelJob,
@@ -28,8 +26,6 @@ import {
28
26
  CANCEL_JOB,
29
27
  CANCEL_RECURRING_LOGIC,
30
28
  CHANGE_ENABLED_RECURRING_LOGIC,
31
- GET_REPORT_TEMPLATES,
32
- GET_REPORT_TEMPLATE_INPUTS,
33
29
  JOB_INVOCATION_KEY,
34
30
  } from '../JobInvocationConstants';
35
31
 
@@ -83,23 +79,9 @@ jest.mock('foremanReact/routes/common/PageLayout/PageLayout', () =>
83
79
  );
84
80
 
85
81
  const setupApiMocks = () => {
86
- api.get.mockImplementation(({ handleSuccess, key, ...action }) => {
87
- if (key === GET_REPORT_TEMPLATES) {
88
- if (handleSuccess) {
89
- handleSuccess({
90
- data: mockReportTemplatesResponse,
91
- });
92
- }
93
- } else if (key === GET_REPORT_TEMPLATE_INPUTS) {
94
- if (handleSuccess) {
95
- handleSuccess({
96
- data: mockReportTemplateInputsResponse,
97
- });
98
- }
99
- }
100
-
101
- return { type: 'get', key, ...action };
102
- });
82
+ api.get.mockImplementation(({ handleSuccess, key, ...action }) =>
83
+ ({ type: 'get', key, ...action })
84
+ );
103
85
  APIActions.post.mockImplementation(payload => ({ type: 'post', ...payload }));
104
86
  APIActions.put.mockImplementation(payload => ({ type: 'put', ...payload }));
105
87
  };
@@ -131,8 +113,6 @@ jest.mock('../JobInvocationHostTable.js', () => () => (
131
113
  <div data-testid="mock-table">Mock Table</div>
132
114
  ));
133
115
 
134
- const reportTemplateJobId = mockReportTemplatesResponse.results[0].id;
135
-
136
116
  const defaultHistory = { push: jest.fn() };
137
117
 
138
118
  const renderJobInvocationDetailPage = (
@@ -235,9 +215,7 @@ describe('JobInvocationDetailPage', () => {
235
215
 
236
216
  // checks the global actions and if they link to the correct url
237
217
  expect(screen.getByText('Create report').getAttribute('href')).toEqual(
238
- foremanUrl(
239
- `/templates/report_templates/${mockReportTemplatesResponse.results[0].id}/generate?report_template_report%5Binput_values%5D%5B${mockReportTemplateInputsResponse.results[0].id}%5D%5Bvalue%5D=${jobId}`
240
- )
218
+ foremanUrl(`/job_invocations/${jobId}/report`)
241
219
  );
242
220
  expect(screen.getByText('Rerun all').getAttribute('href')).toEqual(
243
221
  foremanUrl(`/job_invocations/${jobId}/rerun`)
@@ -319,19 +297,6 @@ describe('JobInvocationDetailPage', () => {
319
297
  { jobId }
320
298
  );
321
299
 
322
- expect(api.get).toHaveBeenCalledWith(
323
- expect.objectContaining({
324
- key: GET_REPORT_TEMPLATES,
325
- url: '/api/report_templates',
326
- })
327
- );
328
- expect(api.get).toHaveBeenCalledWith(
329
- expect.objectContaining({
330
- key: GET_REPORT_TEMPLATE_INPUTS,
331
- url: `/api/templates/${reportTemplateJobId}/template_inputs`,
332
- })
333
- );
334
-
335
300
  api.get.mockClear();
336
301
  APIActions.post.mockClear();
337
302
  APIActions.put.mockClear();