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.
- checksums.yaml +4 -4
- data/app/controllers/api/v2/foreign_input_sets_controller.rb +4 -0
- data/app/controllers/job_invocations_controller.rb +16 -1
- data/config/routes.rb +2 -1
- data/db/seeds.d/60-ssh_proxy_feature.rb +2 -2
- data/db/seeds.d/90-bookmarks.rb +1 -1
- data/db/seeds.d/95-mail_notifications.rb +1 -1
- data/lib/foreman_remote_execution/plugin.rb +2 -2
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/functional/job_invocations_controller_test.rb +84 -0
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +10 -2
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +8 -1
- data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +6 -78
- data/webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js +113 -7
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +4 -39
- data/webpack/JobInvocationDetail/__tests__/areAllHostsTerminal.test.js +40 -0
- data/webpack/JobInvocationDetail/__tests__/fixtures.js +0 -8
- data/webpack/JobWizard/Footer.js +16 -13
- data/webpack/JobWizard/__tests__/Footer.test.js +64 -0
- data/webpack/JobWizard/__tests__/integration.test.js +98 -53
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +191 -212
- data/webpack/react_app/components/TargetingHosts/__tests__/HostItem.test.js +73 -3
- data/webpack/react_app/components/jobInvocations/AggregateStatus/index.test.js +69 -36
- metadata +5 -5
- data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +0 -51
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b301e962ec24666ada0b4a297277232ee94dc74a36b86875f4ce2230659c254
|
|
4
|
+
data.tar.gz: 923c5679450846add2cfba3a6741654e7bfd06f23e9e33b8ad5f19dfd2b6c217
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 561489f3cee2bf28837084baa1fd3fb8b3a68b8acd632c0337f1b3204dc176acafefb85ecc15d058c464038ec38413a56899ce4ebd2d723596571622cb9c130f
|
|
7
|
+
data.tar.gz: '0938ab65b49a36d63fb709d905322559305ebd691f83bcf1aee8d81b9cb6cffd83db2fe50381295b7f589d37beddc457a7ee0aef3b8c02819f12008c9f461461'
|
|
@@ -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
|
|
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
|
|
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
|
|
5
|
+
raise "Unable to create proxy feature: #{SeedHelper.format_errors(f)}" if f.nil? || f.errors.any?
|
data/db/seeds.d/90-bookmarks.rb
CHANGED
|
@@ -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
|
|
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.
|
|
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'
|
|
@@ -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
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
{__(
|
|
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
|
-
{__(
|
|
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: '
|
|
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
|
-
|
|
88
|
-
|
|
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();
|