foreman_remote_execution 17.0.0 → 17.1.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/job_invocations_controller.rb +24 -2
- data/app/controllers/job_invocations_controller.rb +1 -28
- data/app/views/api/v2/job_invocations/hosts.json.rabl +8 -0
- data/config/routes.rb +0 -1
- data/lib/foreman_remote_execution/plugin.rb +2 -2
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/functional/api/v2/job_invocations_controller_test.rb +46 -7
- data/webpack/JobInvocationDetail/JobInvocationActions.js +10 -4
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +3 -2
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +5 -3
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +24 -12
- data/webpack/JobInvocationDetail/JobInvocationSelectors.js +0 -5
- data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +5 -2
- data/webpack/JobInvocationDetail/TemplateInvocation.js +7 -1
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +2 -1
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +17 -6
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/index.scss +0 -1
- data/webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js +33 -0
- data/webpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.js +17 -12
- data/webpack/JobInvocationDetail/index.js +14 -1
- data/webpack/JobWizard/JobWizard.js +5 -1
- data/webpack/JobWizard/JobWizard.scss +21 -0
- data/webpack/JobWizard/JobWizardConstants.js +10 -1
- data/webpack/JobWizard/steps/Schedule/RepeatHour.js +4 -2
- data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +4 -2
- data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +2 -1
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +4 -1
- data/webpack/JobWizard/steps/form/DateTimePicker.js +12 -6
- data/webpack/JobWizard/steps/form/FormHelpers.js +5 -4
- data/webpack/react_app/components/FeaturesDropdown/index.scss +2 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +6 -2
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -1
- data/webpack/react_app/components/TargetingHosts/index.js +2 -1
- data/webpack/react_app/redux/actions/jobInvocations/index.js +3 -2
- metadata +2 -3
- data/webpack/__mocks__/foremanReact/constants.js +0 -25
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 248ba991802a19c3ad70d4c93a1a80931065c77f5e075520c01d35d07d3ef4e3
|
|
4
|
+
data.tar.gz: 83cad360b0893506f1ace4b8c38e60bdab716a74aaa2ff0a741908059f92e56f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d4221e0916237573f00934dac48f0e169cb78ac278ec127b6f57e72cb8982ba12e79624a082986efe878ccbb325dd426ee41dcf0f2a155e08830791e389e3124
|
|
7
|
+
data.tar.gz: 927fa72b970f31090f729ade6acdbf7b73f33c4ca337af7e3ae4f0dfc998d0f77bc69a6bc32ede1ba3a052a46e98acf38d016277ab0085c75b0bd1edeec93068
|
|
@@ -117,12 +117,14 @@ module Api
|
|
|
117
117
|
param_group :search_and_pagination, ::Api::V2::BaseController
|
|
118
118
|
add_scoped_search_description_for(JobInvocation)
|
|
119
119
|
param :id, :identifier, :required => true
|
|
120
|
+
param :include_permissions, :bool, :required => false, :desc => N_('Include per-host task and permission data in the response')
|
|
120
121
|
def hosts
|
|
121
122
|
set_hosts_and_template_invocations
|
|
122
123
|
@total = @hosts.size
|
|
123
124
|
@hosts = @hosts.search_for(params[:search], :order => params[:order]).paginate(:page => params[:page], :per_page => params[:per_page])
|
|
124
125
|
@subtotal = @hosts.total_entries
|
|
125
126
|
set_statuses_and_smart_proxies
|
|
127
|
+
set_tasks_and_permissions if Foreman::Cast.to_bool(params[:include_permissions])
|
|
126
128
|
if params[:awaiting]
|
|
127
129
|
@hosts = @hosts.select { |host| @host_statuses[host.id] == 'N/A' }
|
|
128
130
|
end
|
|
@@ -321,15 +323,35 @@ module Api
|
|
|
321
323
|
template_invocations = @template_invocations.where(host_id: @hosts.select(:id))
|
|
322
324
|
.includes(:run_host_job_task).to_a
|
|
323
325
|
hosts = @hosts.to_a
|
|
324
|
-
template_invocations_by_host_id = template_invocations.index_by(&:host_id)
|
|
326
|
+
@template_invocations_by_host_id = template_invocations.index_by(&:host_id)
|
|
325
327
|
@host_statuses = hosts.to_h do |host|
|
|
326
|
-
template_invocation = template_invocations_by_host_id[host.id]
|
|
328
|
+
template_invocation = @template_invocations_by_host_id[host.id]
|
|
327
329
|
task = template_invocation.try(:run_host_job_task)
|
|
328
330
|
[host.id, template_invocation_status(task, @job_invocation.task)]
|
|
329
331
|
end
|
|
330
332
|
@smart_proxy_id = template_invocations.to_h { |ti| [ti.host_id, ti.smart_proxy_id] }
|
|
331
333
|
@smart_proxy_name = template_invocations.to_h { |ti| [ti.host_id, ti.smart_proxy_name] }
|
|
332
334
|
end
|
|
335
|
+
|
|
336
|
+
def set_tasks_and_permissions
|
|
337
|
+
can_cancel = authorized_for(:permission => :cancel_job_invocations, :auth_object => @job_invocation)
|
|
338
|
+
can_create = authorized_for(controller: :job_invocations, action: :create)
|
|
339
|
+
can_execute_on_infra = User.current.can?(:execute_jobs_on_infrastructure_hosts)
|
|
340
|
+
|
|
341
|
+
hosts = @hosts.to_a
|
|
342
|
+
@task_by_host = hosts.to_h do |host|
|
|
343
|
+
task = @template_invocations_by_host_id[host.id].try(:run_host_job_task)
|
|
344
|
+
[host.id, task ? { :id => task.id, :cancellable => task.try(:cancellable?) || false } : nil]
|
|
345
|
+
end
|
|
346
|
+
@permissions_by_host = hosts.to_h do |host|
|
|
347
|
+
task = @template_invocations_by_host_id[host.id].try(:run_host_job_task)
|
|
348
|
+
[host.id, {
|
|
349
|
+
:view_foreman_tasks => authorized_for(:permission => :view_foreman_tasks, :auth_object => task),
|
|
350
|
+
:cancel_job_invocations => can_cancel,
|
|
351
|
+
:execute_jobs => can_create && (!host.infrastructure_host? || can_execute_on_infra),
|
|
352
|
+
}]
|
|
353
|
+
end
|
|
354
|
+
end
|
|
333
355
|
end
|
|
334
356
|
end
|
|
335
357
|
end
|
|
@@ -149,33 +149,6 @@ class JobInvocationsController < ApplicationController
|
|
|
149
149
|
render :json => {:job_invocations => job_invocations}
|
|
150
150
|
end
|
|
151
151
|
|
|
152
|
-
def list_jobs_hosts
|
|
153
|
-
@job_invocation = resource_base.find(params[:id])
|
|
154
|
-
hosts = @job_invocation.targeting.hosts.authorized(:view_hosts, Host)
|
|
155
|
-
hosts = hosts.search_for(params[:search])
|
|
156
|
-
template_invocations_task_by_hosts = {}
|
|
157
|
-
hosts.each do |host|
|
|
158
|
-
template_invocation = @job_invocation.template_invocations.find { |template_inv| template_inv.host_id == host.id }
|
|
159
|
-
next unless template_invocation
|
|
160
|
-
template_invocation_task = template_invocation.run_host_job_task
|
|
161
|
-
template_invocations_task_by_hosts[host.id] =
|
|
162
|
-
{
|
|
163
|
-
:host_name => host.name,
|
|
164
|
-
:id => host.id,
|
|
165
|
-
:task => template_invocation_task.attributes.merge({cancellable: template_invocation_task.cancellable? }),
|
|
166
|
-
:permissions => {
|
|
167
|
-
:view_foreman_tasks => authorized_for(:permission => :view_foreman_tasks, :auth_object => template_invocation_task),
|
|
168
|
-
:cancel_job_invocations => authorized_for(:permission => :cancel_job_invocations, :auth_object => @job_invocation),
|
|
169
|
-
:execute_jobs => authorized_for(controller: :job_invocations, action: :create) && (!host.infrastructure_host? || User.current.can?(:execute_jobs_on_infrastructure_hosts)),
|
|
170
|
-
},
|
|
171
|
-
}
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
render json: {
|
|
175
|
-
:template_invocations_task_by_hosts => template_invocations_task_by_hosts,
|
|
176
|
-
}
|
|
177
|
-
end
|
|
178
|
-
|
|
179
152
|
private
|
|
180
153
|
|
|
181
154
|
def action_permission
|
|
@@ -186,7 +159,7 @@ class JobInvocationsController < ApplicationController
|
|
|
186
159
|
'create'
|
|
187
160
|
when 'cancel'
|
|
188
161
|
'cancel'
|
|
189
|
-
when 'chart', 'preview_job_invocations_per_host'
|
|
162
|
+
when 'chart', 'preview_job_invocations_per_host'
|
|
190
163
|
'view'
|
|
191
164
|
else
|
|
192
165
|
super
|
|
@@ -13,3 +13,11 @@ end
|
|
|
13
13
|
node :smart_proxy_name do |host|
|
|
14
14
|
@smart_proxy_name[host.id]
|
|
15
15
|
end
|
|
16
|
+
|
|
17
|
+
node(:task, :if => ->(_host) { @task_by_host }) do |host|
|
|
18
|
+
@task_by_host[host.id]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
node(:permissions, :if => ->(_host) { @permissions_by_host }) do |host|
|
|
22
|
+
@permissions_by_host[host.id]
|
|
23
|
+
end
|
data/config/routes.rb
CHANGED
|
@@ -134,9 +134,9 @@ Foreman::Plugin.register :foreman_remote_execution do
|
|
|
134
134
|
permission :lock_job_templates, { :job_templates => [:lock, :unlock] }, :resource_type => 'JobTemplate'
|
|
135
135
|
permission :create_job_invocations, { :job_invocations => [:new, :create, :legacy_create, :refresh, :rerun, :preview_hosts],
|
|
136
136
|
'api/v2/job_invocations' => [:create, :rerun] }, :resource_type => 'JobInvocation'
|
|
137
|
-
permission :view_job_invocations, { :job_invocations => [:index, :chart, :show, :auto_complete_search, :preview_job_invocations_per_host
|
|
137
|
+
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],
|
|
138
138
|
'api/v2/job_invocations' => [:index, :show, :output, :raw_output, :outputs, :hosts] }, :resource_type => 'JobInvocation'
|
|
139
|
-
permission :view_template_invocations, { :template_invocations => [:show, :template_invocation_preview, :show_template_invocation_by_host],
|
|
139
|
+
permission :view_template_invocations, { :template_invocations => [:show, :template_invocation_preview, :show_template_invocation_by_host],
|
|
140
140
|
'api/v2/template_invocations' => [:template_invocations], :ui_job_wizard => [:job_invocation] }, :resource_type => 'TemplateInvocation'
|
|
141
141
|
permission :create_template_invocations, {}, :resource_type => 'TemplateInvocation'
|
|
142
142
|
permission :execute_jobs_on_infrastructure_hosts, {}, :resource_type => 'JobInvocation'
|
|
@@ -284,20 +284,59 @@ module Api
|
|
|
284
284
|
end
|
|
285
285
|
|
|
286
286
|
describe '#hosts' do
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
2.times {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
287
|
+
setup do
|
|
288
|
+
@hosts_invocation = FactoryBot.create(:job_invocation, :with_template, :with_task)
|
|
289
|
+
2.times { @hosts_invocation.template_invocations << FactoryBot.create(:template_invocation, :with_task, :with_host, :job_invocation => @hosts_invocation) }
|
|
290
|
+
@hosts_invocation.job_category = @hosts_invocation.pattern_template_invocations.first.template.job_category
|
|
291
|
+
@hosts_invocation.targeting.hosts = @hosts_invocation.template_invocations.map(&:host)
|
|
292
|
+
@hosts_invocation.save!
|
|
293
|
+
end
|
|
293
294
|
|
|
294
|
-
|
|
295
|
+
test 'should compute job_status for the paginated subset' do
|
|
296
|
+
get :hosts, params: { :id => @hosts_invocation.id, :page => 2, :per_page => 1 }
|
|
295
297
|
assert_response :success
|
|
296
298
|
result = ActiveSupport::JSON.decode(@response.body)
|
|
297
299
|
assert_equal 3, result['total']
|
|
298
300
|
assert_equal 1, result['results'].size
|
|
299
301
|
assert_equal(['success'], result['results'].map { |r| r['job_status'] })
|
|
300
302
|
end
|
|
303
|
+
|
|
304
|
+
test 'should not include task and permissions without include_permissions' do
|
|
305
|
+
get :hosts, params: { :id => @hosts_invocation.id }
|
|
306
|
+
assert_response :success
|
|
307
|
+
result = ActiveSupport::JSON.decode(@response.body)
|
|
308
|
+
result['results'].each do |host|
|
|
309
|
+
assert_not host.key?('task'), 'Expected task to be absent'
|
|
310
|
+
assert_not host.key?('permissions'), 'Expected permissions to be absent'
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
test 'should include task and permissions with include_permissions=true' do
|
|
315
|
+
get :hosts, params: { :id => @hosts_invocation.id, :include_permissions => true }
|
|
316
|
+
assert_response :success
|
|
317
|
+
result = ActiveSupport::JSON.decode(@response.body)
|
|
318
|
+
result['results'].each do |host|
|
|
319
|
+
assert host.key?('task'), 'Expected task to be present'
|
|
320
|
+
assert host.key?('permissions'), 'Expected permissions to be present'
|
|
321
|
+
permissions = host['permissions']
|
|
322
|
+
assert permissions.key?('view_foreman_tasks')
|
|
323
|
+
assert permissions.key?('cancel_job_invocations')
|
|
324
|
+
assert permissions.key?('execute_jobs')
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
test 'should handle hosts with nil task gracefully when include_permissions=true' do
|
|
329
|
+
invocation = FactoryBot.create(:job_invocation, :with_template, :with_task, :with_unplanned_host)
|
|
330
|
+
invocation.job_category = invocation.pattern_template_invocations.first.template.job_category
|
|
331
|
+
invocation.save!
|
|
332
|
+
|
|
333
|
+
get :hosts, params: { :id => invocation.id, :include_permissions => true }
|
|
334
|
+
assert_response :success
|
|
335
|
+
result = ActiveSupport::JSON.decode(@response.body)
|
|
336
|
+
result['results'].each do |host|
|
|
337
|
+
assert host.key?('permissions'), 'Expected permissions to be present'
|
|
338
|
+
end
|
|
339
|
+
end
|
|
301
340
|
end
|
|
302
341
|
|
|
303
342
|
describe 'raw output' do
|
|
@@ -7,10 +7,9 @@ import {
|
|
|
7
7
|
CHANGE_ENABLED_RECURRING_LOGIC,
|
|
8
8
|
JOB_INVOCATION_KEY,
|
|
9
9
|
STATUS,
|
|
10
|
+
AUTO_REFRESH_INTERVAL_MS,
|
|
10
11
|
} from './JobInvocationConstants';
|
|
11
12
|
|
|
12
|
-
const POLL_INTERVAL = 5000;
|
|
13
|
-
|
|
14
13
|
export const isJobFinished = statusLabel =>
|
|
15
14
|
statusLabel === STATUS.FAILED ||
|
|
16
15
|
statusLabel === STATUS.SUCCEEDED ||
|
|
@@ -26,20 +25,27 @@ const createPollState = cancel => ({ timeoutId: null, cancel });
|
|
|
26
25
|
|
|
27
26
|
export const getJobInvocation = (url, pollTimeoutRef) => dispatch => {
|
|
28
27
|
let cancelled = false;
|
|
28
|
+
let isFirstCall = true;
|
|
29
29
|
|
|
30
30
|
const poll = () => {
|
|
31
31
|
if (cancelled) return;
|
|
32
|
+
const params = { include_hosts: false };
|
|
33
|
+
if (isFirstCall) {
|
|
34
|
+
params.include_permissions = true;
|
|
35
|
+
isFirstCall = false;
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
dispatch(
|
|
33
39
|
APIActions.get({
|
|
34
40
|
key: JOB_INVOCATION_KEY,
|
|
35
|
-
params
|
|
41
|
+
params,
|
|
36
42
|
url,
|
|
37
43
|
handleSuccess: ({ data }) => {
|
|
38
44
|
if (cancelled) return;
|
|
39
45
|
// eslint-disable-next-line camelcase
|
|
40
46
|
pollTimeoutRef.current.timeoutId = isJobFinished(data?.status_label)
|
|
41
47
|
? null
|
|
42
|
-
: setTimeout(poll,
|
|
48
|
+
: setTimeout(poll, AUTO_REFRESH_INTERVAL_MS);
|
|
43
49
|
},
|
|
44
50
|
handleError: () => {
|
|
45
51
|
if (cancelled) return;
|
|
@@ -15,14 +15,15 @@ export const GET_REPORT_TEMPLATE_INPUTS = 'GET_REPORT_TEMPLATE_INPUTS';
|
|
|
15
15
|
export const JOB_INVOCATION_HOSTS = 'JOB_INVOCATION_HOSTS';
|
|
16
16
|
export const GET_TEMPLATE_INVOCATION = 'GET_TEMPLATE_INVOCATION';
|
|
17
17
|
export const DIRECT_OPEN_HOST_LIMIT = 3;
|
|
18
|
-
export const ALL_JOB_HOSTS = 'ALL_JOB_HOSTS';
|
|
19
18
|
export const AWAITING_STATUS_FILTER = '(job_invocation.result = N/A)';
|
|
20
19
|
|
|
21
20
|
export const AUTO_REFRESH_INTERVAL_MS = 5000;
|
|
21
|
+
export const DEFAULT_CHART_LEGEND_WIDTH = 270;
|
|
22
|
+
export const CLIPBOARD_COPIED_EXIT_DELAY_MS = 1500;
|
|
23
|
+
export const CLIPBOARD_DEFAULT_EXIT_DELAY_MS = 600;
|
|
22
24
|
|
|
23
25
|
export const showTemplateInvocationUrl = (hostID, jobID) =>
|
|
24
26
|
`/show_template_invocation_by_host/${hostID}/job_invocation/${jobID}`;
|
|
25
|
-
export const LIST_TEMPLATE_INVOCATIONS = 'LIST_TEMPLATE_INVOCATIONS';
|
|
26
27
|
|
|
27
28
|
export const templateInvocationPageUrl = (hostID, jobID) =>
|
|
28
29
|
`/job_invocations_detail/${jobID}/host_invocation/${hostID}`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
.job-invocation-detail-flex {
|
|
2
2
|
$chart_size: 105px;
|
|
3
|
+
|
|
3
4
|
padding-top: 0px;
|
|
4
5
|
padding-left: 10px;
|
|
5
6
|
display: flex;
|
|
@@ -92,16 +93,17 @@ section.job-additional-info {
|
|
|
92
93
|
max-height: 25em;
|
|
93
94
|
}
|
|
94
95
|
}
|
|
96
|
+
|
|
95
97
|
div.invocation-output {
|
|
96
98
|
display: block;
|
|
97
99
|
padding: 9.5px;
|
|
98
100
|
margin: 0 0 10px;
|
|
99
101
|
font-size: 12px;
|
|
100
102
|
word-break: break-all;
|
|
101
|
-
|
|
103
|
+
overflow-wrap: break-word;
|
|
102
104
|
color: rgba(255, 255, 255, 1);
|
|
103
105
|
background-color: rgba(47, 47, 47, 1);
|
|
104
|
-
border: 1px solid #
|
|
106
|
+
border: 1px solid #000;
|
|
105
107
|
border-radius: 0px;
|
|
106
108
|
font-family: Menlo, Monaco, Consolas, monospace;
|
|
107
109
|
|
|
@@ -127,7 +129,7 @@ section.job-additional-info {
|
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
a {
|
|
130
|
-
color: #
|
|
132
|
+
color: #fff;
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
a.scroll-link{
|
|
@@ -39,7 +39,6 @@ import { CheckboxesActions } from './CheckboxesActions';
|
|
|
39
39
|
import DropdownFilter from './DropdownFilter';
|
|
40
40
|
import Columns, {
|
|
41
41
|
JOB_INVOCATION_HOSTS,
|
|
42
|
-
LIST_TEMPLATE_INVOCATIONS,
|
|
43
42
|
STATUS_UPPERCASE,
|
|
44
43
|
AWAITING_STATUS_FILTER,
|
|
45
44
|
AUTO_REFRESH_INTERVAL_MS,
|
|
@@ -163,11 +162,24 @@ const JobInvocationHostTable = ({
|
|
|
163
162
|
[initialFilter, urlSearchQuery]
|
|
164
163
|
);
|
|
165
164
|
|
|
165
|
+
const [hostPermissions, setHostPermissions] = useState({});
|
|
166
|
+
|
|
166
167
|
const updateHostsState = useCallback(data => {
|
|
167
168
|
const ids = data.data.results.map(i => i.id);
|
|
168
169
|
setApiResponse(data.data);
|
|
169
170
|
setAllHostsIds(ids);
|
|
170
171
|
setStatus(STATUS_UPPERCASE.RESOLVED);
|
|
172
|
+
|
|
173
|
+
const resultsWithPermissions = data.data.results.filter(r => r.permissions);
|
|
174
|
+
if (resultsWithPermissions.length > 0) {
|
|
175
|
+
setHostPermissions(prev => {
|
|
176
|
+
const updated = { ...prev };
|
|
177
|
+
resultsWithPermissions.forEach(r => {
|
|
178
|
+
updated[r.id] = { task: r.task, permissions: r.permissions };
|
|
179
|
+
});
|
|
180
|
+
return updated;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
171
183
|
}, []);
|
|
172
184
|
|
|
173
185
|
// Call hosts data with params
|
|
@@ -229,7 +241,10 @@ const JobInvocationHostTable = ({
|
|
|
229
241
|
finalParams.search = filterSearch;
|
|
230
242
|
}
|
|
231
243
|
|
|
232
|
-
|
|
244
|
+
finalParams.include_permissions = true;
|
|
245
|
+
|
|
246
|
+
currentPollParams.current = { ...finalParams };
|
|
247
|
+
delete currentPollParams.current.include_permissions;
|
|
233
248
|
clearTimeout(pollTimeoutId.current);
|
|
234
249
|
pollTimeoutId.current = null;
|
|
235
250
|
|
|
@@ -258,20 +273,12 @@ const JobInvocationHostTable = ({
|
|
|
258
273
|
const initializedRef = useRef(false);
|
|
259
274
|
useEffect(() => {
|
|
260
275
|
if (!initializedRef.current) {
|
|
261
|
-
dispatch(
|
|
262
|
-
APIActions.get({
|
|
263
|
-
key: LIST_TEMPLATE_INVOCATIONS,
|
|
264
|
-
url: `/job_invocations/${id}/hosts`,
|
|
265
|
-
params: {},
|
|
266
|
-
})
|
|
267
|
-
);
|
|
268
|
-
|
|
269
276
|
if (initialFilter === '') {
|
|
270
277
|
onFilterUpdate('all_statuses');
|
|
271
278
|
}
|
|
272
279
|
initializedRef.current = true;
|
|
273
280
|
}
|
|
274
|
-
}, [
|
|
281
|
+
}, [initialFilter, onFilterUpdate]);
|
|
275
282
|
|
|
276
283
|
useEffect(() => {
|
|
277
284
|
const filterChanged = initialFilter !== prevFilter.current;
|
|
@@ -506,7 +513,12 @@ const JobInvocationHostTable = ({
|
|
|
506
513
|
<Td key={k}>{columns[k].wrapper(result)}</Td>
|
|
507
514
|
))}
|
|
508
515
|
<Td isActionCell>
|
|
509
|
-
<RowActions
|
|
516
|
+
<RowActions
|
|
517
|
+
hostID={result.id}
|
|
518
|
+
jobID={id}
|
|
519
|
+
task={hostPermissions[result.id]?.task}
|
|
520
|
+
permissions={hostPermissions[result.id]?.permissions}
|
|
521
|
+
/>
|
|
510
522
|
</Td>
|
|
511
523
|
</Tr>
|
|
512
524
|
<Tr
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
JOB_INVOCATION_KEY,
|
|
8
8
|
GET_TEMPLATE_INVOCATION,
|
|
9
|
-
LIST_TEMPLATE_INVOCATIONS,
|
|
10
9
|
} from './JobInvocationConstants';
|
|
11
10
|
|
|
12
11
|
export const selectItems = state =>
|
|
@@ -20,7 +19,3 @@ export const selectTemplateInvocation = hostID => state =>
|
|
|
20
19
|
|
|
21
20
|
export const selectTemplateInvocationStatus = hostID => state =>
|
|
22
21
|
selectAPIStatus(state, `${GET_TEMPLATE_INVOCATION}_${hostID}`);
|
|
23
|
-
|
|
24
|
-
export const selectTemplateInvocationList = state =>
|
|
25
|
-
selectAPIResponse(state, LIST_TEMPLATE_INVOCATIONS)
|
|
26
|
-
?.template_invocations_task_by_hosts;
|
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
global_palette_blue_300 as inProgressColor,
|
|
24
24
|
global_palette_green_500 as successedColor,
|
|
25
25
|
} from '@patternfly/react-tokens';
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
STATUS_TITLES,
|
|
28
|
+
DEFAULT_CHART_LEGEND_WIDTH,
|
|
29
|
+
} from './JobInvocationConstants';
|
|
27
30
|
import './JobInvocationDetail.scss';
|
|
28
31
|
|
|
29
32
|
const JobInvocationSystemStatusChart = ({
|
|
@@ -52,7 +55,7 @@ const JobInvocationSystemStatusChart = ({
|
|
|
52
55
|
return '0';
|
|
53
56
|
};
|
|
54
57
|
const chartSize = 105;
|
|
55
|
-
const [legendWidth, setLegendWidth] = useState(
|
|
58
|
+
const [legendWidth, setLegendWidth] = useState(DEFAULT_CHART_LEGEND_WIDTH);
|
|
56
59
|
|
|
57
60
|
// Calculates chart legend width based on its content
|
|
58
61
|
useEffect(() => {
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
templateInvocationPageUrl,
|
|
13
13
|
GET_TEMPLATE_INVOCATION,
|
|
14
14
|
AUTO_REFRESH_INTERVAL_MS,
|
|
15
|
+
CLIPBOARD_COPIED_EXIT_DELAY_MS,
|
|
16
|
+
CLIPBOARD_DEFAULT_EXIT_DELAY_MS,
|
|
15
17
|
} from './JobInvocationConstants';
|
|
16
18
|
import {
|
|
17
19
|
selectTemplateInvocationStatus,
|
|
@@ -42,7 +44,11 @@ const CopyToClipboard = ({ fullOutput }) => {
|
|
|
42
44
|
textId="code-content"
|
|
43
45
|
aria-label="Copy to clipboard"
|
|
44
46
|
onClick={e => onClick(e, fullOutput)}
|
|
45
|
-
exitDelay={
|
|
47
|
+
exitDelay={
|
|
48
|
+
copied
|
|
49
|
+
? CLIPBOARD_COPIED_EXIT_DELAY_MS
|
|
50
|
+
: CLIPBOARD_DEFAULT_EXIT_DELAY_MS
|
|
51
|
+
}
|
|
46
52
|
maxWidth="110px"
|
|
47
53
|
variant="plain"
|
|
48
54
|
onTooltipHidden={() => setCopied(false)}
|
|
@@ -7,6 +7,7 @@ import React, {
|
|
|
7
7
|
import PropTypes from 'prop-types';
|
|
8
8
|
import { Button } from '@patternfly/react-core';
|
|
9
9
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
10
|
+
import { MS_PER_SECOND } from 'foremanReact/constants';
|
|
10
11
|
|
|
11
12
|
export const OutputCodeBlock = ({ code, showOutputType, scrollElement }) => {
|
|
12
13
|
let lineCounter = 0;
|
|
@@ -122,7 +123,7 @@ export const OutputCodeBlock = ({ code, showOutputType, scrollElement }) => {
|
|
|
122
123
|
<div key={index} className={`line ${line.output_type}`}>
|
|
123
124
|
<span
|
|
124
125
|
className="counter"
|
|
125
|
-
title={new Date(line.timestamp *
|
|
126
|
+
title={new Date(line.timestamp * MS_PER_SECOND).toISOString()}
|
|
126
127
|
>
|
|
127
128
|
{lineCounter.toString().padStart(4, '\u00A0')}:{' '}
|
|
128
129
|
</span>
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
-
import { useDispatch
|
|
3
|
+
import { useDispatch } from 'react-redux';
|
|
4
4
|
import { Flex, FlexItem, Button } from '@patternfly/react-core';
|
|
5
5
|
import { ActionsColumn } from '@patternfly/react-table';
|
|
6
6
|
import { APIActions } from 'foremanReact/redux/API';
|
|
7
7
|
import { addToast } from 'foremanReact/components/ToastsList';
|
|
8
8
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
9
|
-
import { selectTemplateInvocationList } from '../JobInvocationSelectors';
|
|
10
9
|
import './index.scss';
|
|
11
10
|
|
|
12
11
|
const actions = ({
|
|
@@ -81,11 +80,9 @@ const actions = ({
|
|
|
81
80
|
},
|
|
82
81
|
});
|
|
83
82
|
|
|
84
|
-
export const RowActions = ({ hostID, jobID }) => {
|
|
83
|
+
export const RowActions = ({ hostID, jobID, task, permissions }) => {
|
|
85
84
|
const dispatch = useDispatch();
|
|
86
|
-
|
|
87
|
-
if (!response?.permissions) return null;
|
|
88
|
-
const { task, permissions } = response;
|
|
85
|
+
if (!permissions) return null;
|
|
89
86
|
const { id: taskID, cancellable: taskCancellable } = task || {};
|
|
90
87
|
const getActions = actions({
|
|
91
88
|
taskID,
|
|
@@ -216,4 +213,18 @@ TemplateActionButtons.defaultProps = {
|
|
|
216
213
|
RowActions.propTypes = {
|
|
217
214
|
hostID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
218
215
|
jobID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
216
|
+
task: PropTypes.shape({
|
|
217
|
+
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
218
|
+
cancellable: PropTypes.bool,
|
|
219
|
+
}),
|
|
220
|
+
permissions: PropTypes.shape({
|
|
221
|
+
view_foreman_tasks: PropTypes.bool,
|
|
222
|
+
cancel_job_invocations: PropTypes.bool,
|
|
223
|
+
execute_jobs: PropTypes.bool,
|
|
224
|
+
}),
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
RowActions.defaultProps = {
|
|
228
|
+
task: null,
|
|
229
|
+
permissions: null,
|
|
219
230
|
};
|
|
@@ -311,6 +311,39 @@ describe('JobInvocationHostTable polling', () => {
|
|
|
311
311
|
expect(hostsCalls.length).toBeGreaterThan(callsAfterFilterChange);
|
|
312
312
|
});
|
|
313
313
|
|
|
314
|
+
it('sends include_permissions only on the first request', () => {
|
|
315
|
+
renderTable();
|
|
316
|
+
|
|
317
|
+
expect(hostsCalls).toHaveLength(1);
|
|
318
|
+
expect(hostsCalls[0].params.include_permissions).toBe(true);
|
|
319
|
+
|
|
320
|
+
act(() => {
|
|
321
|
+
flushPendingCallbacks();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
act(() => {
|
|
325
|
+
jest.advanceTimersByTime(5000);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
act(() => {
|
|
329
|
+
flushPendingCallbacks();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
expect(hostsCalls).toHaveLength(2);
|
|
333
|
+
expect(hostsCalls[1].params.include_permissions).toBeUndefined();
|
|
334
|
+
|
|
335
|
+
act(() => {
|
|
336
|
+
jest.advanceTimersByTime(5000);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
act(() => {
|
|
340
|
+
flushPendingCallbacks();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
expect(hostsCalls).toHaveLength(3);
|
|
344
|
+
expect(hostsCalls[2].params.include_permissions).toBeUndefined();
|
|
345
|
+
});
|
|
346
|
+
|
|
314
347
|
it('stops polling on API error', () => {
|
|
315
348
|
apiGetSpy.mockRestore();
|
|
316
349
|
setupErrorMock();
|
|
@@ -5,7 +5,10 @@ import {
|
|
|
5
5
|
getJobInvocation,
|
|
6
6
|
stopJobInvocationPolling,
|
|
7
7
|
} from '../JobInvocationActions';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
JOB_INVOCATION_KEY,
|
|
10
|
+
AUTO_REFRESH_INTERVAL_MS,
|
|
11
|
+
} from '../JobInvocationConstants';
|
|
9
12
|
|
|
10
13
|
jest.useFakeTimers();
|
|
11
14
|
|
|
@@ -66,22 +69,24 @@ describe('job invocation polling', () => {
|
|
|
66
69
|
store.dispatch(getJobInvocation(url, ref));
|
|
67
70
|
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
68
71
|
|
|
69
|
-
jest.advanceTimersByTime(
|
|
72
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS);
|
|
70
73
|
expect(apiGetSpy).toHaveBeenCalledTimes(2);
|
|
71
74
|
});
|
|
72
75
|
|
|
73
|
-
it('
|
|
76
|
+
it('does not include include_permissions on subsequent poll calls', () => {
|
|
74
77
|
setupGetMock(runningData);
|
|
75
78
|
const store = makeStore();
|
|
76
79
|
|
|
77
80
|
store.dispatch(getJobInvocation(url, ref));
|
|
78
|
-
jest.advanceTimersByTime(
|
|
81
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS);
|
|
79
82
|
|
|
80
83
|
expect(apiGetSpy).toHaveBeenCalledTimes(2);
|
|
81
84
|
expect(apiGetSpy.mock.calls[1][0].params).toMatchObject({
|
|
82
85
|
include_hosts: false,
|
|
83
|
-
include_permissions: true,
|
|
84
86
|
});
|
|
87
|
+
expect(apiGetSpy.mock.calls[1][0].params).not.toHaveProperty(
|
|
88
|
+
'include_permissions'
|
|
89
|
+
);
|
|
85
90
|
});
|
|
86
91
|
|
|
87
92
|
it('stops polling when job succeeds', () => {
|
|
@@ -91,7 +96,7 @@ describe('job invocation polling', () => {
|
|
|
91
96
|
store.dispatch(getJobInvocation(url, ref));
|
|
92
97
|
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
93
98
|
|
|
94
|
-
jest.advanceTimersByTime(
|
|
99
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS);
|
|
95
100
|
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
96
101
|
});
|
|
97
102
|
|
|
@@ -100,7 +105,7 @@ describe('job invocation polling', () => {
|
|
|
100
105
|
const store = makeStore();
|
|
101
106
|
|
|
102
107
|
store.dispatch(getJobInvocation(url, ref));
|
|
103
|
-
jest.advanceTimersByTime(
|
|
108
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS);
|
|
104
109
|
|
|
105
110
|
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
106
111
|
});
|
|
@@ -110,7 +115,7 @@ describe('job invocation polling', () => {
|
|
|
110
115
|
const store = makeStore();
|
|
111
116
|
|
|
112
117
|
store.dispatch(getJobInvocation(url, ref));
|
|
113
|
-
jest.advanceTimersByTime(
|
|
118
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS);
|
|
114
119
|
|
|
115
120
|
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
116
121
|
});
|
|
@@ -125,7 +130,7 @@ describe('job invocation polling', () => {
|
|
|
125
130
|
const store = makeStore();
|
|
126
131
|
|
|
127
132
|
store.dispatch(getJobInvocation(url, ref));
|
|
128
|
-
jest.advanceTimersByTime(
|
|
133
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS);
|
|
129
134
|
|
|
130
135
|
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
131
136
|
});
|
|
@@ -138,7 +143,7 @@ describe('job invocation polling', () => {
|
|
|
138
143
|
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
139
144
|
|
|
140
145
|
stopJobInvocationPolling(ref);
|
|
141
|
-
jest.advanceTimersByTime(
|
|
146
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS);
|
|
142
147
|
|
|
143
148
|
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
144
149
|
});
|
|
@@ -154,7 +159,7 @@ describe('job invocation polling', () => {
|
|
|
154
159
|
store.dispatch(getJobInvocation(url, ref));
|
|
155
160
|
expect(apiGetSpy).toHaveBeenCalledTimes(2);
|
|
156
161
|
|
|
157
|
-
jest.advanceTimersByTime(
|
|
162
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS);
|
|
158
163
|
|
|
159
164
|
// Only the second poll chain fires (first was cancelled)
|
|
160
165
|
expect(apiGetSpy).toHaveBeenCalledTimes(3);
|
|
@@ -165,7 +170,7 @@ describe('job invocation polling', () => {
|
|
|
165
170
|
const store = makeStore();
|
|
166
171
|
|
|
167
172
|
store.dispatch(getJobInvocation(url, ref));
|
|
168
|
-
jest.advanceTimersByTime(
|
|
173
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS * 3);
|
|
169
174
|
|
|
170
175
|
expect(apiGetSpy).toHaveBeenCalledTimes(4);
|
|
171
176
|
});
|
|
@@ -54,6 +54,11 @@ const JobInvocationDetailPage = ({
|
|
|
54
54
|
} = items;
|
|
55
55
|
const finished = isJobFinished(statusLabel);
|
|
56
56
|
const pollTimeoutRef = useRef({ timeoutId: null, cancel: () => {} });
|
|
57
|
+
const permissionsRef = useRef(null);
|
|
58
|
+
if (items.permissions && !permissionsRef.current) {
|
|
59
|
+
permissionsRef.current = items.permissions;
|
|
60
|
+
}
|
|
61
|
+
const permissions = items.permissions || permissionsRef.current;
|
|
57
62
|
const jobInvocationApiStatus = useSelector(state =>
|
|
58
63
|
selectAPIStatus(state, JOB_INVOCATION_KEY)
|
|
59
64
|
);
|
|
@@ -87,6 +92,11 @@ const JobInvocationDetailPage = ({
|
|
|
87
92
|
};
|
|
88
93
|
}, [dispatch, id]);
|
|
89
94
|
|
|
95
|
+
const dataWithPermissions = useMemo(() => ({ ...items, permissions }), [
|
|
96
|
+
items,
|
|
97
|
+
permissions,
|
|
98
|
+
]);
|
|
99
|
+
|
|
90
100
|
const apiFailed = jobInvocationApiStatus === API_STATUS.ERROR;
|
|
91
101
|
|
|
92
102
|
const backendErrorMessage = useMemo(() => {
|
|
@@ -143,7 +153,10 @@ const JobInvocationDetailPage = ({
|
|
|
143
153
|
breadcrumbOptions={breadcrumbOptions}
|
|
144
154
|
toolbarButtons={
|
|
145
155
|
items.id !== undefined && (
|
|
146
|
-
<JobInvocationToolbarButtons
|
|
156
|
+
<JobInvocationToolbarButtons
|
|
157
|
+
jobId={id}
|
|
158
|
+
data={dataWithPermissions}
|
|
159
|
+
/>
|
|
147
160
|
)
|
|
148
161
|
}
|
|
149
162
|
searchable={false}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
WIZARD_TITLES,
|
|
19
19
|
SCHEDULE_TYPES,
|
|
20
20
|
initialScheduleState,
|
|
21
|
+
STARTS_ERROR_CHECK_INTERVAL_MS,
|
|
21
22
|
} from './JobWizardConstants';
|
|
22
23
|
import {
|
|
23
24
|
selectTemplateError,
|
|
@@ -237,7 +238,10 @@ export const JobWizard = ({ rerunData }) => {
|
|
|
237
238
|
}
|
|
238
239
|
};
|
|
239
240
|
updateStartsError();
|
|
240
|
-
const interval = setInterval(
|
|
241
|
+
const interval = setInterval(
|
|
242
|
+
updateStartsError,
|
|
243
|
+
STARTS_ERROR_CHECK_INTERVAL_MS
|
|
244
|
+
);
|
|
241
245
|
|
|
242
246
|
return () => {
|
|
243
247
|
interval && clearInterval(interval);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
.job-wizard {
|
|
2
2
|
font-size: var(--pf-v5-global--FontSize--md);
|
|
3
|
+
|
|
3
4
|
.wizard-title {
|
|
4
5
|
margin-bottom: 25px;
|
|
5
6
|
}
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
var(--pf-v5-cwizard__toggle--ZIndex) + 1
|
|
16
17
|
); // So the select box can be shown above the wizard footer and navigation toggle
|
|
17
18
|
}
|
|
19
|
+
|
|
18
20
|
.pf-v5-c-wizard__main-body {
|
|
19
21
|
@media (max-width: 600px) {
|
|
20
22
|
max-width: 100%;
|
|
@@ -41,6 +43,7 @@
|
|
|
41
43
|
.pf-v5-c-chip-group.pf-m-category {
|
|
42
44
|
margin-bottom: 10px;
|
|
43
45
|
}
|
|
46
|
+
|
|
44
47
|
.pf-v5-c-select__toggle-typeahead {
|
|
45
48
|
border: 0px;
|
|
46
49
|
}
|
|
@@ -50,62 +53,76 @@
|
|
|
50
53
|
flex-wrap: nowrap;
|
|
51
54
|
}
|
|
52
55
|
}
|
|
56
|
+
|
|
53
57
|
.foreman-search-field {
|
|
54
58
|
width: 100%;
|
|
55
59
|
}
|
|
56
60
|
}
|
|
61
|
+
|
|
57
62
|
input[type='radio'],
|
|
58
63
|
input[type='checkbox'] {
|
|
59
64
|
margin: 0;
|
|
60
65
|
}
|
|
66
|
+
|
|
61
67
|
.schedule-tab {
|
|
62
68
|
#repeat-on-weekly {
|
|
63
69
|
display: grid;
|
|
64
70
|
grid-template-columns: repeat(7, 1fr);
|
|
65
71
|
}
|
|
72
|
+
|
|
66
73
|
.pf-v5-l-grid {
|
|
67
74
|
gap: var(--pf-v5-cform--GridGap);
|
|
68
75
|
}
|
|
76
|
+
|
|
69
77
|
#repeat-on-hourly {
|
|
70
78
|
max-height: 300px;
|
|
71
79
|
overflow: scroll;
|
|
72
80
|
}
|
|
81
|
+
|
|
73
82
|
.schedule-radio label {
|
|
74
83
|
width: 100%;
|
|
75
84
|
}
|
|
85
|
+
|
|
76
86
|
.schedule-radio input {
|
|
77
87
|
align-self: center;
|
|
78
88
|
}
|
|
89
|
+
|
|
79
90
|
.schedule-radio-repeat-text {
|
|
80
91
|
width: 100px;
|
|
81
92
|
display: inline-block;
|
|
82
93
|
margin-right: 5px;
|
|
83
94
|
align-self: center;
|
|
84
95
|
}
|
|
96
|
+
|
|
85
97
|
.schedule-radio-title {
|
|
86
98
|
width: 80px;
|
|
87
99
|
display: inline-block;
|
|
88
100
|
align-self: center;
|
|
89
101
|
}
|
|
102
|
+
|
|
90
103
|
.schedule-radio-occurences {
|
|
91
104
|
display: inline-block;
|
|
92
105
|
align-self: center;
|
|
93
106
|
}
|
|
107
|
+
|
|
94
108
|
.schedule-radio-wrapper {
|
|
95
109
|
display: flex;
|
|
96
110
|
}
|
|
97
111
|
}
|
|
112
|
+
|
|
98
113
|
.future-schedule-tab {
|
|
99
114
|
.clear-datetime-button {
|
|
100
115
|
margin-left: 10px;
|
|
101
116
|
align-self: center;
|
|
102
117
|
font-size: var(--pf-v5-global--FontSize--md);
|
|
103
118
|
}
|
|
119
|
+
|
|
104
120
|
.pf-v5-c-form__group-control {
|
|
105
121
|
display: flex;
|
|
106
122
|
flex-wrap: wrap;
|
|
107
123
|
}
|
|
108
124
|
}
|
|
125
|
+
|
|
109
126
|
.pf-v5-c-date-picker {
|
|
110
127
|
vertical-align: top;
|
|
111
128
|
}
|
|
@@ -119,10 +136,12 @@
|
|
|
119
136
|
// overwriting bootstrap/_forms.scss margin: 4px 0 0;
|
|
120
137
|
margin: 0;
|
|
121
138
|
}
|
|
139
|
+
|
|
122
140
|
textarea {
|
|
123
141
|
min-height: 40px;
|
|
124
142
|
min-width: 100px;
|
|
125
143
|
}
|
|
144
|
+
|
|
126
145
|
.pf-v5-c-modal-box {
|
|
127
146
|
width: auto;
|
|
128
147
|
}
|
|
@@ -132,9 +151,11 @@
|
|
|
132
151
|
margin-left: 10px;
|
|
133
152
|
}
|
|
134
153
|
}
|
|
154
|
+
|
|
135
155
|
.pf-v5-c-radio__body {
|
|
136
156
|
font-size: var(--pf-v5-cradio__label--FontSize);
|
|
137
157
|
}
|
|
158
|
+
|
|
138
159
|
.reset-default {
|
|
139
160
|
padding-bottom: 0;
|
|
140
161
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
2
2
|
import { foremanUrl } from 'foremanReact/common/helpers';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getControllerSearchProps,
|
|
5
|
+
MS_PER_SECOND,
|
|
6
|
+
} from 'foremanReact/constants';
|
|
4
7
|
|
|
5
8
|
export const JOB_TEMPLATES = 'JOB_TEMPLATES';
|
|
6
9
|
export const JOB_CATEGORIES = 'JOB_CATEGORIES';
|
|
@@ -78,3 +81,9 @@ export const HOST_IDS = 'HOST_IDS';
|
|
|
78
81
|
export const REX_FEATURE = 'REX_FEATURE';
|
|
79
82
|
|
|
80
83
|
export const JOB_API_KEY = 'JOB_API_KEY';
|
|
84
|
+
|
|
85
|
+
export const SUNDAY_BASE_YEAR = 2017;
|
|
86
|
+
export const DEFAULT_MINUTE_OPTIONS = [0, 15, 30, 45];
|
|
87
|
+
export const STARTS_ERROR_CHECK_INTERVAL_MS = 5000;
|
|
88
|
+
export const DATE_PADDING_SLICE = -2;
|
|
89
|
+
export const DEBOUNCE_INPUT_MS = MS_PER_SECOND;
|
|
@@ -12,13 +12,15 @@ import {
|
|
|
12
12
|
SelectVariant,
|
|
13
13
|
} from '@patternfly/react-core/deprecated';
|
|
14
14
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
15
|
+
import { MINUTES_PER_HOUR } from 'foremanReact/constants';
|
|
15
16
|
import { helpLabel } from '../form/FormHelpers';
|
|
17
|
+
import { DEFAULT_MINUTE_OPTIONS } from '../../JobWizardConstants';
|
|
16
18
|
|
|
17
19
|
export const RepeatHour = ({ repeatData, setRepeatData }) => {
|
|
18
20
|
const isValidMinute = newMinute =>
|
|
19
21
|
Number.isInteger(parseInt(newMinute, 10)) &&
|
|
20
22
|
newMinute >= 0 &&
|
|
21
|
-
newMinute <
|
|
23
|
+
newMinute < MINUTES_PER_HOUR;
|
|
22
24
|
|
|
23
25
|
const { minute } = repeatData;
|
|
24
26
|
useEffect(() => {
|
|
@@ -27,7 +29,7 @@ export const RepeatHour = ({ repeatData, setRepeatData }) => {
|
|
|
27
29
|
}
|
|
28
30
|
}, [minute, setRepeatData]);
|
|
29
31
|
const [minuteOpen, setMinuteOpen] = useState(false);
|
|
30
|
-
const [options, setOptions] = useState(
|
|
32
|
+
const [options, setOptions] = useState(DEFAULT_MINUTE_OPTIONS);
|
|
31
33
|
const [isAlertOpen, setIsAlertOpen] = useState(false);
|
|
32
34
|
return (
|
|
33
35
|
<FormGroup
|
|
@@ -2,15 +2,17 @@ import React, { useEffect } from 'react';
|
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { FormGroup, Checkbox } from '@patternfly/react-core';
|
|
4
4
|
import { translate as __, documentLocale } from 'foremanReact/common/I18n';
|
|
5
|
+
import { DAYS_PER_WEEK } from 'foremanReact/constants';
|
|
5
6
|
import { RepeatDaily } from './RepeatDaily';
|
|
6
7
|
import { noop } from '../../../helpers';
|
|
8
|
+
import { SUNDAY_BASE_YEAR } from '../../JobWizardConstants';
|
|
7
9
|
|
|
8
10
|
export const getWeekDays = () => {
|
|
9
11
|
const locale = documentLocale().replace(/-/g, '_');
|
|
10
|
-
const baseDate = new Date(Date.UTC(
|
|
12
|
+
const baseDate = new Date(Date.UTC(SUNDAY_BASE_YEAR, 0, 1)); // just a Sunday
|
|
11
13
|
const weekDays = [];
|
|
12
14
|
const formatOptions = { weekday: 'short', timeZone: 'UTC' };
|
|
13
|
-
for (let i = 0; i <
|
|
15
|
+
for (let i = 0; i < DAYS_PER_WEEK; i++) {
|
|
14
16
|
try {
|
|
15
17
|
weekDays.push(baseDate.toLocaleDateString(locale, formatOptions));
|
|
16
18
|
} catch {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '@patternfly/react-core';
|
|
13
13
|
import { ExclamationCircleIcon } from '@patternfly/react-icons';
|
|
14
14
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
15
|
+
import { MS_PER_MINUTE } from 'foremanReact/constants';
|
|
15
16
|
import { RepeatOn } from './RepeatOn';
|
|
16
17
|
import { SCHEDULE_TYPES } from '../../JobWizardConstants';
|
|
17
18
|
import { PurposeField } from './PurposeField';
|
|
@@ -114,7 +115,7 @@ export const ScheduleRecurring = ({
|
|
|
114
115
|
setScheduleValue(current => ({
|
|
115
116
|
...current,
|
|
116
117
|
startsAt: new Date(
|
|
117
|
-
new Date().getTime() +
|
|
118
|
+
new Date().getTime() + MS_PER_MINUTE
|
|
118
119
|
).toISOString(), // 1 minute in the future
|
|
119
120
|
isFuture: true,
|
|
120
121
|
}))
|
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { Form, FormGroup, Radio, Divider } from '@patternfly/react-core';
|
|
4
4
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
|
+
import { MS_PER_MINUTE } from 'foremanReact/constants';
|
|
5
6
|
import {
|
|
6
7
|
WIZARD_TITLES,
|
|
7
8
|
SCHEDULE_TYPES,
|
|
@@ -49,7 +50,9 @@ export const ScheduleType = ({
|
|
|
49
50
|
onChange={() => {
|
|
50
51
|
setScheduleValue(current => ({
|
|
51
52
|
...current,
|
|
52
|
-
startsAt: new Date(
|
|
53
|
+
startsAt: new Date(
|
|
54
|
+
new Date().getTime() + MS_PER_MINUTE
|
|
55
|
+
).toISOString(), // 1 minute in the future
|
|
53
56
|
scheduleType: SCHEDULE_TYPES.FUTURE,
|
|
54
57
|
repeatType: repeatTypes.noRepeat,
|
|
55
58
|
}));
|
|
@@ -7,13 +7,19 @@ import {
|
|
|
7
7
|
} from '@patternfly/react-core';
|
|
8
8
|
import { debounce } from 'lodash';
|
|
9
9
|
import { translate as __, documentLocale } from 'foremanReact/common/I18n';
|
|
10
|
+
import {
|
|
11
|
+
DATE_PADDING_SLICE,
|
|
12
|
+
DEBOUNCE_INPUT_MS,
|
|
13
|
+
} from '../../JobWizardConstants';
|
|
10
14
|
|
|
11
15
|
const formatDateTime = d =>
|
|
12
16
|
`${d.getFullYear()}-${`0${d.getMonth() + 1}`.slice(
|
|
13
|
-
|
|
14
|
-
)}-${`0${d.getDate()}`.slice(
|
|
15
|
-
|
|
16
|
-
)}:${`0${d.getMinutes()}`.slice(
|
|
17
|
+
DATE_PADDING_SLICE
|
|
18
|
+
)}-${`0${d.getDate()}`.slice(DATE_PADDING_SLICE)} ${`0${d.getHours()}`.slice(
|
|
19
|
+
DATE_PADDING_SLICE
|
|
20
|
+
)}:${`0${d.getMinutes()}`.slice(
|
|
21
|
+
DATE_PADDING_SLICE
|
|
22
|
+
)}:${`0${d.getSeconds()}`.slice(DATE_PADDING_SLICE)}`;
|
|
17
23
|
|
|
18
24
|
export const DateTimePicker = ({
|
|
19
25
|
dateTime,
|
|
@@ -103,7 +109,7 @@ export const DateTimePicker = ({
|
|
|
103
109
|
aria-label={`${ariaLabel} datepicker`}
|
|
104
110
|
value={formattedDate}
|
|
105
111
|
placeholder="yyyy/mm/dd"
|
|
106
|
-
onChange={debounce(onDateChange,
|
|
112
|
+
onChange={debounce(onDateChange, DEBOUNCE_INPUT_MS, {
|
|
107
113
|
leading: false,
|
|
108
114
|
trailing: true,
|
|
109
115
|
})}
|
|
@@ -123,7 +129,7 @@ export const DateTimePicker = ({
|
|
|
123
129
|
time={dateTime ? dateObject.toString() : ''}
|
|
124
130
|
inputProps={dateTime ? {} : { value: '' }}
|
|
125
131
|
placeholder={includeSeconds ? 'hh:mm:ss' : 'hh:mm'}
|
|
126
|
-
onChange={debounce(onTimeChange,
|
|
132
|
+
onChange={debounce(onTimeChange, DEBOUNCE_INPUT_MS, {
|
|
127
133
|
leading: false,
|
|
128
134
|
trailing: true,
|
|
129
135
|
})}
|
|
@@ -8,16 +8,17 @@ export const helpLabel = (text, id) => {
|
|
|
8
8
|
if (!text) return null;
|
|
9
9
|
return (
|
|
10
10
|
<Popover id={`${id}-help`} bodyContent={text} aria-label="help-text">
|
|
11
|
-
<
|
|
12
|
-
|
|
11
|
+
<Button
|
|
12
|
+
ouiaId={`${id}-help-button`}
|
|
13
|
+
variant="plain"
|
|
13
14
|
aria-label={__('open-help-tooltip-button')}
|
|
14
15
|
onClick={e => e.preventDefault()}
|
|
15
|
-
|
|
16
|
+
isInline
|
|
16
17
|
>
|
|
17
18
|
<Icon isInline>
|
|
18
19
|
<HelpIcon />
|
|
19
20
|
</Icon>
|
|
20
|
-
</
|
|
21
|
+
</Button>
|
|
21
22
|
</Popover>
|
|
22
23
|
);
|
|
23
24
|
};
|
|
@@ -3,24 +3,28 @@
|
|
|
3
3
|
.tasks-labels-row {
|
|
4
4
|
margin: 0;
|
|
5
5
|
padding: 10px;
|
|
6
|
+
|
|
6
7
|
.title {
|
|
7
8
|
font-weight: 600;
|
|
8
9
|
font-size: 13px;
|
|
9
10
|
}
|
|
11
|
+
|
|
10
12
|
.label {
|
|
11
13
|
font-size: 100%;
|
|
12
|
-
|
|
13
14
|
margin-left: 5px;
|
|
14
15
|
margin-right: 5px;
|
|
16
|
+
|
|
15
17
|
a {
|
|
16
18
|
padding-left: 10px;
|
|
17
19
|
}
|
|
18
20
|
}
|
|
21
|
+
|
|
19
22
|
.pficon-close {
|
|
20
23
|
color: $color-pf-white;
|
|
21
24
|
}
|
|
25
|
+
|
|
22
26
|
.compound-label-pf {
|
|
23
|
-
margin-left: 0;
|
|
24
27
|
margin: 10px;
|
|
28
|
+
margin-left: 0;
|
|
25
29
|
}
|
|
26
30
|
}
|
|
@@ -25,7 +25,7 @@ exports[`TargetingHostsPage renders 1`] = `
|
|
|
25
25
|
},
|
|
26
26
|
"bookmarks": Object {
|
|
27
27
|
"canCreate": true,
|
|
28
|
-
"documentationUrl": "4.1.5Searching",
|
|
28
|
+
"documentationUrl": "/links/manual/4.1.5Searching",
|
|
29
29
|
"id": "targeting_hosts_search",
|
|
30
30
|
"url": "/api/bookmarks",
|
|
31
31
|
},
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
withInterval,
|
|
8
8
|
stopInterval,
|
|
9
9
|
} from 'foremanReact/redux/middlewares/IntervalMiddleware';
|
|
10
|
+
import { MS_PER_SECOND } from 'foremanReact/constants';
|
|
10
11
|
|
|
11
12
|
import {
|
|
12
13
|
selectItems,
|
|
@@ -100,7 +101,7 @@ const WrappedTargetingHosts = () => {
|
|
|
100
101
|
dispatch(stopInterval(TARGETING_HOSTS));
|
|
101
102
|
},
|
|
102
103
|
}),
|
|
103
|
-
|
|
104
|
+
MS_PER_SECOND
|
|
104
105
|
),
|
|
105
106
|
[dispatch]
|
|
106
107
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
2
2
|
import API from 'foremanReact/API';
|
|
3
|
+
import { HTTP_STATUS_CODES, MS_PER_SECOND } from 'foremanReact/constants';
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
JOB_INVOCATIONS_GET_JOB_INVOCATIONS,
|
|
@@ -7,7 +8,7 @@ import {
|
|
|
7
8
|
JOB_INVOCATIONS_JOB_FINISHED,
|
|
8
9
|
} from '../../consts';
|
|
9
10
|
|
|
10
|
-
const defaultJobInvocationsPollingInterval =
|
|
11
|
+
const defaultJobInvocationsPollingInterval = MS_PER_SECOND;
|
|
11
12
|
const jobInvocationsInterval =
|
|
12
13
|
process.env.JOB_INVOCATIONS_POLLING || defaultJobInvocationsPollingInterval;
|
|
13
14
|
|
|
@@ -32,7 +33,7 @@ const getJobInvocations = url => async (dispatch, getState) => {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function onGetJobInvocationsFailed(error) {
|
|
35
|
-
if (error.response.status ===
|
|
36
|
+
if (error.response.status === HTTP_STATUS_CODES.UNAUTHORIZED) {
|
|
36
37
|
window.location.replace('/users/login');
|
|
37
38
|
}
|
|
38
39
|
}
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foreman_remote_execution
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 17.
|
|
4
|
+
version: 17.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Foreman Remote Execution team
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-08-
|
|
10
|
+
date: 2026-08-17 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: deface
|
|
@@ -485,7 +485,6 @@ files:
|
|
|
485
485
|
- webpack/__mocks__/foremanReact/components/Pagination.js
|
|
486
486
|
- webpack/__mocks__/foremanReact/components/ToastsList/index.js
|
|
487
487
|
- webpack/__mocks__/foremanReact/components/common/ActionButtons/ActionButtons.js
|
|
488
|
-
- webpack/__mocks__/foremanReact/constants.js
|
|
489
488
|
- webpack/__mocks__/foremanReact/history.js
|
|
490
489
|
- webpack/__mocks__/foremanReact/redux/API/APIActions.js
|
|
491
490
|
- webpack/__mocks__/foremanReact/redux/API/APIConstants.js
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export const STATUS = {
|
|
2
|
-
PENDING: 'PENDING',
|
|
3
|
-
RESOLVED: 'RESOLVED',
|
|
4
|
-
ERROR: 'ERROR',
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export const getControllerSearchProps = (
|
|
8
|
-
controller,
|
|
9
|
-
id = 'searchBar',
|
|
10
|
-
canCreate = true
|
|
11
|
-
) => ({
|
|
12
|
-
controller,
|
|
13
|
-
autocomplete: {
|
|
14
|
-
id,
|
|
15
|
-
searchQuery: '',
|
|
16
|
-
url: `${controller}/auto_complete_search`,
|
|
17
|
-
useKeyShortcuts: true,
|
|
18
|
-
},
|
|
19
|
-
bookmarks: {
|
|
20
|
-
id,
|
|
21
|
-
url: '/api/bookmarks',
|
|
22
|
-
canCreate,
|
|
23
|
-
documentationUrl: `4.1.5Searching`,
|
|
24
|
-
},
|
|
25
|
-
});
|