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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +24 -2
  3. data/app/controllers/job_invocations_controller.rb +1 -28
  4. data/app/views/api/v2/job_invocations/hosts.json.rabl +8 -0
  5. data/config/routes.rb +0 -1
  6. data/lib/foreman_remote_execution/plugin.rb +2 -2
  7. data/lib/foreman_remote_execution/version.rb +1 -1
  8. data/test/functional/api/v2/job_invocations_controller_test.rb +46 -7
  9. data/webpack/JobInvocationDetail/JobInvocationActions.js +10 -4
  10. data/webpack/JobInvocationDetail/JobInvocationConstants.js +3 -2
  11. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +5 -3
  12. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +24 -12
  13. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +0 -5
  14. data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +5 -2
  15. data/webpack/JobInvocationDetail/TemplateInvocation.js +7 -1
  16. data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +2 -1
  17. data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +17 -6
  18. data/webpack/JobInvocationDetail/TemplateInvocationComponents/index.scss +0 -1
  19. data/webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js +33 -0
  20. data/webpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.js +17 -12
  21. data/webpack/JobInvocationDetail/index.js +14 -1
  22. data/webpack/JobWizard/JobWizard.js +5 -1
  23. data/webpack/JobWizard/JobWizard.scss +21 -0
  24. data/webpack/JobWizard/JobWizardConstants.js +10 -1
  25. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +4 -2
  26. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +4 -2
  27. data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +2 -1
  28. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +4 -1
  29. data/webpack/JobWizard/steps/form/DateTimePicker.js +12 -6
  30. data/webpack/JobWizard/steps/form/FormHelpers.js +5 -4
  31. data/webpack/react_app/components/FeaturesDropdown/index.scss +2 -0
  32. data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +6 -2
  33. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -1
  34. data/webpack/react_app/components/TargetingHosts/index.js +2 -1
  35. data/webpack/react_app/redux/actions/jobInvocations/index.js +3 -2
  36. metadata +2 -3
  37. data/webpack/__mocks__/foremanReact/constants.js +0 -25
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d0c5f9e975897ee24ccf2c91fafe81e998742ec7453ca0e7cf3aea4846962b23
4
- data.tar.gz: 20f9ca73d78bd41ce758b3ac39e1e880be6630230eb64d735850a88ae7b9cbec
3
+ metadata.gz: 248ba991802a19c3ad70d4c93a1a80931065c77f5e075520c01d35d07d3ef4e3
4
+ data.tar.gz: 83cad360b0893506f1ace4b8c38e60bdab716a74aaa2ff0a741908059f92e56f
5
5
  SHA512:
6
- metadata.gz: 57609a9b217ab4e7e5e60536a6a4de788b26d46d09f68330ca385fbd4df59d65cabc33581026ba15795aacbfc96b1fe6a97b735e006dfcf0a51188032ea525b9
7
- data.tar.gz: 23d742ef78d0f9dc5cc205d541c74a8c3a3472618c4e67a6cb1c5c94c552a2ea6aa601ce38b498f9fae2403227f0efdf44055fefe8cc90df1de86e3c7b6fc96f
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', 'list_jobs_hosts'
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
@@ -35,7 +35,6 @@ Rails.application.routes.draw do
35
35
  get 'auto_complete_search'
36
36
  end
37
37
  member do
38
- get 'hosts', to: 'job_invocations#list_jobs_hosts'
39
38
  post 'cancel'
40
39
  end
41
40
  end
@@ -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, :list_jobs_hosts], :template_invocations => [:show, :show_template_invocation_by_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], :job_invocations => [:list_jobs_hosts],
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'
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '17.0.0'.freeze
2
+ VERSION = '17.1.0'.freeze
3
3
  end
@@ -284,20 +284,59 @@ module Api
284
284
  end
285
285
 
286
286
  describe '#hosts' do
287
- test 'should compute job_status for the paginated subset' do
288
- invocation = FactoryBot.create(:job_invocation, :with_template, :with_task)
289
- 2.times { invocation.template_invocations << FactoryBot.create(:template_invocation, :with_task, :with_host, :job_invocation => invocation) }
290
- invocation.job_category = invocation.pattern_template_invocations.first.template.job_category
291
- invocation.targeting.hosts = invocation.template_invocations.map(&:host)
292
- invocation.save!
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
- get :hosts, params: { :id => invocation.id, :page => 2, :per_page => 1 }
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: { include_permissions: true, include_hosts: false },
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, POLL_INTERVAL);
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
- word-wrap: break-word;
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 #000000;
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: #ffffff;
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
- currentPollParams.current = finalParams;
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
- }, [dispatch, id, initialFilter, onFilterUpdate]);
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 hostID={result.id} jobID={id} />
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 { STATUS_TITLES } from './JobInvocationConstants';
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(270);
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={copied ? 1500 : 600}
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 * 1000).toISOString()}
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, useSelector } from 'react-redux';
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
- const response = useSelector(selectTemplateInvocationList)?.[hostID];
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
  };
@@ -1,5 +1,4 @@
1
1
  .jobs-table-action-item a {
2
-
3
2
  color: inherit;
4
3
  text-decoration: none;
5
4
 
@@ -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 { JOB_INVOCATION_KEY } from '../JobInvocationConstants';
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(5000);
72
+ jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS);
70
73
  expect(apiGetSpy).toHaveBeenCalledTimes(2);
71
74
  });
72
75
 
73
- it('includes include_permissions=true in every poll call', () => {
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(5000);
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(5000);
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(5000);
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(5000);
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(5000);
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(5000);
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(5000);
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(15000);
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 jobId={id} data={items} />
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(updateStartsError, 5000);
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 { getControllerSearchProps } from 'foremanReact/constants';
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 < 60;
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([0, 15, 30, 45]);
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(2017, 0, 1)); // just a Sunday
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 < 7; 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() + 60000
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(new Date().getTime() + 60000).toISOString(), // 1 minute in the future
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
- -2
14
- )}-${`0${d.getDate()}`.slice(-2)} ${`0${d.getHours()}`.slice(
15
- -2
16
- )}:${`0${d.getMinutes()}`.slice(-2)}:${`0${d.getSeconds()}`.slice(-2)}`;
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, 1000, {
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, 1000, {
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
- <button
12
- type="button"
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
- className="pf-v5-c-form__group-label-help"
16
+ isInline
16
17
  >
17
18
  <Icon isInline>
18
19
  <HelpIcon />
19
20
  </Icon>
20
- </button>
21
+ </Button>
21
22
  </Popover>
22
23
  );
23
24
  };
@@ -1,7 +1,9 @@
1
1
  #schedule-a-job-dropdown ul.pf-v5-c-dropdown__menu {
2
2
  padding-left: 0;
3
+
3
4
  li {
4
5
  display: unset;
6
+
5
7
  a {
6
8
  font-size: 16px;
7
9
  color: var(--pf-v5-cdropdown__menu-item--Color);
@@ -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
- 1000
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 = 1000;
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 === 401) {
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.0.0
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-03 00:00:00.000000000 Z
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
- });