foreman_remote_execution 16.7.0 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87f61f660cc9bb15e87453747aca6cef5d600d4fd3994d090c5af8a4e48f1b8a
4
- data.tar.gz: 16e86afd877b2c2dec48daba1ff05c98822208a9c777d5351a9bf04edbd21322
3
+ metadata.gz: d0c5f9e975897ee24ccf2c91fafe81e998742ec7453ca0e7cf3aea4846962b23
4
+ data.tar.gz: 20f9ca73d78bd41ce758b3ac39e1e880be6630230eb64d735850a88ae7b9cbec
5
5
  SHA512:
6
- metadata.gz: 16a08a5afdbab46351b5fe9eb4fcf55c82ad0a56160585dbedbec48a32e2d47c9f595a8562505d5c97f69141d5e6174c0fcbb6e3fd0df481dd692b6c2b811b91
7
- data.tar.gz: f1e886b6f85fc08820986b812d2d959b65c42e49ab411a42df47da1ec5ad077842f472f58e78f635fd71712fdf15a7bf2541d6921fc1d3788ebec9be1bcad995
6
+ metadata.gz: 57609a9b217ab4e7e5e60536a6a4de788b26d46d09f68330ca385fbd4df59d65cabc33581026ba15795aacbfc96b1fe6a97b735e006dfcf0a51188032ea525b9
7
+ data.tar.gz: 23d742ef78d0f9dc5cc205d541c74a8c3a3472618c4e67a6cb1c5c94c552a2ea6aa601ce38b498f9fae2403227f0efdf44055fefe8cc90df1de86e3c7b6fc96f
@@ -34,6 +34,7 @@ end
34
34
 
35
35
  child :task do
36
36
  attributes :id, :state, :started_at
37
+ node(:cancellable) { |task| task.try(:cancellable?) }
37
38
  end
38
39
 
39
40
  if @template_invocations
@@ -0,0 +1,4 @@
1
+ <% if @template.locked? -%>
2
+ <%= locked_warning(::Template.find(params[:id])) %>
3
+ <% end -%>
4
+
@@ -1,5 +1,5 @@
1
1
  Foreman::Plugin.register :foreman_remote_execution do
2
- requires_foreman '>= 3.17'
2
+ requires_foreman '>= 5.0'
3
3
  register_global_js_file 'global'
4
4
  register_gettext
5
5
 
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '16.7.0'.freeze
2
+ VERSION = '17.0.0'.freeze
3
3
  end
@@ -1,52 +1,67 @@
1
1
  import { translate as __, sprintf } from 'foremanReact/common/I18n';
2
- import { foremanUrl } from 'foremanReact/common/helpers';
3
2
  import { addToast } from 'foremanReact/components/ToastsList';
4
- import { APIActions, get } from 'foremanReact/redux/API';
5
- import {
6
- stopInterval,
7
- withInterval,
8
- } from 'foremanReact/redux/middlewares/IntervalMiddleware';
3
+ import { APIActions } from 'foremanReact/redux/API';
9
4
  import {
10
5
  CANCEL_JOB,
11
6
  CANCEL_RECURRING_LOGIC,
12
7
  CHANGE_ENABLED_RECURRING_LOGIC,
13
- GET_TASK,
14
8
  JOB_INVOCATION_KEY,
15
- UPDATE_JOB,
9
+ STATUS,
16
10
  } from './JobInvocationConstants';
17
11
 
18
- export const getJobInvocation = url => dispatch => {
19
- const fetchData = withInterval(
20
- get({
21
- key: JOB_INVOCATION_KEY,
22
- params: { include_permissions: true, include_hosts: false },
23
- url,
24
- handleError: () => {
25
- dispatch(stopInterval(JOB_INVOCATION_KEY));
26
- },
27
- errorToast: ({ response }) =>
28
- // eslint-disable-next-line camelcase
29
- response?.data?.error?.full_messages?.[0] ||
30
- // eslint-disable-next-line camelcase
31
- response?.data?.error?.full_messages ||
32
- response?.data?.error?.message ||
33
- 'Error',
34
- }),
35
- 1000
36
- );
12
+ const POLL_INTERVAL = 5000;
13
+
14
+ export const isJobFinished = statusLabel =>
15
+ statusLabel === STATUS.FAILED ||
16
+ statusLabel === STATUS.SUCCEEDED ||
17
+ statusLabel === STATUS.CANCELLED;
18
+
19
+ const extractErrorMessage = response =>
20
+ // eslint-disable-next-line camelcase
21
+ response?.data?.error?.full_messages?.[0] ||
22
+ response?.data?.error?.message ||
23
+ __('Unknown error.');
24
+
25
+ const createPollState = cancel => ({ timeoutId: null, cancel });
37
26
 
38
- dispatch(fetchData);
27
+ export const getJobInvocation = (url, pollTimeoutRef) => dispatch => {
28
+ let cancelled = false;
29
+
30
+ const poll = () => {
31
+ if (cancelled) return;
32
+ dispatch(
33
+ APIActions.get({
34
+ key: JOB_INVOCATION_KEY,
35
+ params: { include_permissions: true, include_hosts: false },
36
+ url,
37
+ handleSuccess: ({ data }) => {
38
+ if (cancelled) return;
39
+ // eslint-disable-next-line camelcase
40
+ pollTimeoutRef.current.timeoutId = isJobFinished(data?.status_label)
41
+ ? null
42
+ : setTimeout(poll, POLL_INTERVAL);
43
+ },
44
+ handleError: () => {
45
+ if (cancelled) return;
46
+ pollTimeoutRef.current.timeoutId = null;
47
+ },
48
+ errorToast: ({ response }) => extractErrorMessage(response),
49
+ })
50
+ );
51
+ };
52
+
53
+ stopJobInvocationPolling(pollTimeoutRef);
54
+ pollTimeoutRef.current = createPollState(() => {
55
+ cancelled = true;
56
+ });
57
+
58
+ poll();
39
59
  };
40
60
 
41
- export const updateJob = jobId => dispatch => {
42
- const url = foremanUrl(`/api/job_invocations/${jobId}`);
43
- dispatch(
44
- APIActions.get({
45
- url,
46
- key: UPDATE_JOB,
47
- params: { include_hosts: false },
48
- })
49
- );
61
+ export const stopJobInvocationPolling = pollTimeoutRef => {
62
+ clearTimeout(pollTimeoutRef.current.timeoutId);
63
+ pollTimeoutRef.current.cancel();
64
+ pollTimeoutRef.current = createPollState(() => {});
50
65
  };
51
66
 
52
67
  export const cancelJob = (jobId, force) => dispatch => {
@@ -54,10 +69,6 @@ export const cancelJob = (jobId, force) => dispatch => {
54
69
  force
55
70
  ? sprintf(__('Trying to abort the job %s.'), jobId)
56
71
  : sprintf(__('Trying to cancel the job %s.'), jobId);
57
- const errorToast = response =>
58
- force
59
- ? sprintf(__(`Could not abort the job %s: ${response}`), jobId)
60
- : sprintf(__(`Could not cancel the job %s: ${response}`), jobId);
61
72
  const url = force
62
73
  ? `/job_invocations/${jobId}/cancel?force=true`
63
74
  : `/job_invocations/${jobId}/cancel`;
@@ -67,12 +78,17 @@ export const cancelJob = (jobId, force) => dispatch => {
67
78
  url,
68
79
  key: CANCEL_JOB,
69
80
  errorToast: ({ response }) =>
70
- errorToast(
71
- // eslint-disable-next-line camelcase
72
- response?.data?.error?.full_messages ||
73
- response?.data?.error?.message ||
74
- 'Unknown error.'
75
- ),
81
+ force
82
+ ? sprintf(
83
+ __('Could not abort the job %s: %s'),
84
+ jobId,
85
+ extractErrorMessage(response)
86
+ )
87
+ : sprintf(
88
+ __('Could not cancel the job %s: %s'),
89
+ jobId,
90
+ extractErrorMessage(response)
91
+ ),
76
92
  handleSuccess: () => {
77
93
  dispatch(
78
94
  addToast({
@@ -81,40 +97,16 @@ export const cancelJob = (jobId, force) => dispatch => {
81
97
  message: infoToast(),
82
98
  })
83
99
  );
84
- dispatch(updateJob(jobId));
85
100
  },
86
101
  })
87
102
  );
88
103
  };
89
104
 
90
- export const getTask = taskId => dispatch => {
91
- dispatch(
92
- get({
93
- key: GET_TASK,
94
- url: `/foreman_tasks/api/tasks/${taskId}`,
95
- })
96
- );
97
- };
98
-
99
- export const enableRecurringLogic = (
100
- recurrenceId,
101
- enabled,
102
- jobId
103
- ) => dispatch => {
105
+ export const enableRecurringLogic = (recurrenceId, enabled) => dispatch => {
104
106
  const successToast = () =>
105
107
  enabled
106
108
  ? sprintf(__('Recurring logic %s disabled successfully.'), recurrenceId)
107
109
  : sprintf(__('Recurring logic %s enabled successfully.'), recurrenceId);
108
- const errorToast = response =>
109
- enabled
110
- ? sprintf(
111
- __(`Could not disable recurring logic %s: ${response}`),
112
- recurrenceId
113
- )
114
- : sprintf(
115
- __(`Could not enable recurring logic %s: ${response}`),
116
- recurrenceId
117
- );
118
110
  const url = `/foreman_tasks/api/recurring_logics/${recurrenceId}`;
119
111
  dispatch(
120
112
  APIActions.put({
@@ -123,25 +115,24 @@ export const enableRecurringLogic = (
123
115
  params: { recurring_logic: { enabled: !enabled } },
124
116
  successToast,
125
117
  errorToast: ({ response }) =>
126
- errorToast(
127
- // eslint-disable-next-line camelcase
128
- response?.data?.error?.full_messages ||
129
- response?.data?.error?.message ||
130
- 'Unknown error.'
131
- ),
132
- handleSuccess: () => dispatch(updateJob(jobId)),
118
+ enabled
119
+ ? sprintf(
120
+ __('Could not disable recurring logic %s: %s'),
121
+ recurrenceId,
122
+ extractErrorMessage(response)
123
+ )
124
+ : sprintf(
125
+ __('Could not enable recurring logic %s: %s'),
126
+ recurrenceId,
127
+ extractErrorMessage(response)
128
+ ),
133
129
  })
134
130
  );
135
131
  };
136
132
 
137
- export const cancelRecurringLogic = (recurrenceId, jobId) => dispatch => {
133
+ export const cancelRecurringLogic = recurrenceId => dispatch => {
138
134
  const successToast = () =>
139
135
  sprintf(__('Recurring logic %s cancelled successfully.'), recurrenceId);
140
- const errorToast = response =>
141
- sprintf(
142
- __(`Could not cancel recurring logic %s: ${response}`),
143
- recurrenceId
144
- );
145
136
  const url = `/foreman_tasks/recurring_logics/${recurrenceId}/cancel`;
146
137
  dispatch(
147
138
  APIActions.post({
@@ -149,13 +140,11 @@ export const cancelRecurringLogic = (recurrenceId, jobId) => dispatch => {
149
140
  key: CANCEL_RECURRING_LOGIC,
150
141
  successToast,
151
142
  errorToast: ({ response }) =>
152
- errorToast(
153
- // eslint-disable-next-line camelcase
154
- response?.data?.error?.full_messages ||
155
- response?.data?.error?.message ||
156
- 'Unknown error.'
143
+ sprintf(
144
+ __('Could not cancel recurring logic %s: %s'),
145
+ recurrenceId,
146
+ extractErrorMessage(response)
157
147
  ),
158
- handleSuccess: () => dispatch(updateJob(jobId)),
159
148
  })
160
149
  );
161
150
  };
@@ -6,9 +6,7 @@ import { useForemanHostDetailsPageUrl } from 'foremanReact/Root/Context/ForemanC
6
6
  import JobStatusIcon from '../react_app/components/RecentJobsCard/JobStatusIcon';
7
7
 
8
8
  export const JOB_INVOCATION_KEY = 'JOB_INVOCATION_KEY';
9
- export const UPDATE_JOB = 'UPDATE_JOB';
10
9
  export const CANCEL_JOB = 'CANCEL_JOB';
11
- export const GET_TASK = 'GET_TASK';
12
10
  export const GET_TEMPLATE_INVOCATIONS = 'GET_TEMPLATE_INVOCATIONS';
13
11
  export const CHANGE_ENABLED_RECURRING_LOGIC = 'CHANGE_ENABLED_RECURRING_LOGIC';
14
12
  export const CANCEL_RECURRING_LOGIC = 'CANCEL_RECURRING_LOGIC';
@@ -20,6 +18,8 @@ export const DIRECT_OPEN_HOST_LIMIT = 3;
20
18
  export const ALL_JOB_HOSTS = 'ALL_JOB_HOSTS';
21
19
  export const AWAITING_STATUS_FILTER = '(job_invocation.result = N/A)';
22
20
 
21
+ export const AUTO_REFRESH_INTERVAL_MS = 5000;
22
+
23
23
  export const showTemplateInvocationUrl = (hostID, jobID) =>
24
24
  `/show_template_invocation_by_host/${hostID}/job_invocation/${jobID}`;
25
25
  export const LIST_TEMPLATE_INVOCATIONS = 'LIST_TEMPLATE_INVOCATIONS';
@@ -38,6 +38,7 @@ export const STATUS = {
38
38
  SUCCEEDED: 'succeeded',
39
39
  FAILED: 'failed',
40
40
  CANCELLED: 'cancelled',
41
+ RUNNING: 'running',
41
42
  };
42
43
 
43
44
  export const STATUS_UPPERCASE = {
@@ -41,8 +41,8 @@ import Columns, {
41
41
  JOB_INVOCATION_HOSTS,
42
42
  LIST_TEMPLATE_INVOCATIONS,
43
43
  STATUS_UPPERCASE,
44
- ALL_JOB_HOSTS,
45
44
  AWAITING_STATUS_FILTER,
45
+ AUTO_REFRESH_INTERVAL_MS,
46
46
  } from './JobInvocationConstants';
47
47
  import { TemplateInvocation } from './TemplateInvocation';
48
48
  import { RowActions } from './TemplateInvocationComponents/TemplateActionButtons';
@@ -51,8 +51,8 @@ import { PopupAlert } from './OpenAllInvocationsModal';
51
51
  const JobInvocationHostTable = ({
52
52
  id,
53
53
  initialFilter,
54
+ jobFinished,
54
55
  onFilterUpdate,
55
- statusLabel,
56
56
  targeting,
57
57
  }) => {
58
58
  const columns = Columns();
@@ -69,7 +69,17 @@ const JobInvocationHostTable = ({
69
69
 
70
70
  // Expansive items
71
71
  const [expandedHost, setExpandedHost] = useState(new Set());
72
- const prevStatusLabel = useRef(statusLabel);
72
+ const prevJobFinished = useRef(jobFinished);
73
+ const prevFilter = useRef('');
74
+ const prevId = useRef(id);
75
+ const pollTimeoutId = useRef(null);
76
+ const currentPollParams = useRef({});
77
+ const mountedRef = useRef(true);
78
+ const requestIdRef = useRef(0);
79
+ const jobFinishedRef = useRef(jobFinished);
80
+ useEffect(() => {
81
+ jobFinishedRef.current = jobFinished;
82
+ }, [jobFinished]);
73
83
 
74
84
  const [hostInvocationStates, setHostInvocationStates] = useState({});
75
85
 
@@ -153,33 +163,50 @@ const JobInvocationHostTable = ({
153
163
  [initialFilter, urlSearchQuery]
154
164
  );
155
165
 
156
- const handleResponse = useCallback((data, key) => {
157
- if (key === JOB_INVOCATION_HOSTS) {
158
- const ids = data.data.results.map(i => i.id);
159
-
160
- setApiResponse(data.data);
161
- setAllHostsIds(ids);
162
- }
163
-
166
+ const updateHostsState = useCallback(data => {
167
+ const ids = data.data.results.map(i => i.id);
168
+ setApiResponse(data.data);
169
+ setAllHostsIds(ids);
164
170
  setStatus(STATUS_UPPERCASE.RESOLVED);
165
171
  }, []);
166
172
 
167
173
  // Call hosts data with params
168
174
  const makeApiCall = useCallback(
169
- (requestParams, callParams = {}) => {
175
+ requestParams => {
176
+ requestIdRef.current += 1;
177
+ const thisRequest = requestIdRef.current;
170
178
  dispatch(
171
179
  APIActions.get({
172
- key: callParams.key ?? ALL_JOB_HOSTS,
173
- url: callParams.url ?? `/api/job_invocations/${id}/hosts`,
180
+ key: JOB_INVOCATION_HOSTS,
181
+ url: `/api/job_invocations/${id}/hosts`,
174
182
  params: requestParams,
175
- handleSuccess: data => handleResponse(data, callParams.key),
176
- handleError: () => setStatus(STATUS_UPPERCASE.ERROR),
183
+ handleSuccess: data => {
184
+ if (thisRequest !== requestIdRef.current) return;
185
+ if (!mountedRef.current) return;
186
+ updateHostsState(data);
187
+ if (!jobFinishedRef.current) {
188
+ pollTimeoutId.current = setTimeout(
189
+ () => makeApiCall(currentPollParams.current),
190
+ AUTO_REFRESH_INTERVAL_MS
191
+ );
192
+ } else {
193
+ pollTimeoutId.current = null;
194
+ }
195
+ },
196
+ handleError: () => {
197
+ if (thisRequest !== requestIdRef.current) return;
198
+ if (!mountedRef.current) return;
199
+ pollTimeoutId.current = null;
200
+ setStatus(STATUS_UPPERCASE.ERROR);
201
+ },
177
202
  errorToast: ({ response }) =>
178
- response?.data?.error?.full_messages?.[0] || response,
203
+ response?.data?.error?.full_messages?.[0] ||
204
+ response?.data?.error?.message ||
205
+ __('Failed to load host invocation data'),
179
206
  })
180
207
  );
181
208
  },
182
- [dispatch, id, handleResponse]
209
+ [dispatch, id, updateHostsState]
183
210
  );
184
211
 
185
212
  const filterApiCall = useCallback(
@@ -202,7 +229,11 @@ const JobInvocationHostTable = ({
202
229
  finalParams.search = filterSearch;
203
230
  }
204
231
 
205
- makeApiCall(finalParams, { key: JOB_INVOCATION_HOSTS });
232
+ currentPollParams.current = finalParams;
233
+ clearTimeout(pollTimeoutId.current);
234
+ pollTimeoutId.current = null;
235
+
236
+ makeApiCall(finalParams);
206
237
 
207
238
  const urlSearchParams = new URLSearchParams(window.location.search);
208
239
 
@@ -222,26 +253,17 @@ const JobInvocationHostTable = ({
222
253
  ]
223
254
  );
224
255
 
225
- // Filter change
226
- const handleFilterChange = useCallback(
227
- newFilter => {
228
- onFilterUpdate(newFilter);
229
- },
230
- [onFilterUpdate]
231
- );
232
-
233
256
  // Effects
234
257
  // run after mount
235
258
  const initializedRef = useRef(false);
236
259
  useEffect(() => {
237
260
  if (!initializedRef.current) {
238
- // Job Invo template load
239
- makeApiCall(
240
- {},
241
- {
242
- url: `/job_invocations/${id}/hosts`,
261
+ dispatch(
262
+ APIActions.get({
243
263
  key: LIST_TEMPLATE_INVOCATIONS,
244
- }
264
+ url: `/job_invocations/${id}/hosts`,
265
+ params: {},
266
+ })
245
267
  );
246
268
 
247
269
  if (initialFilter === '') {
@@ -249,16 +271,28 @@ const JobInvocationHostTable = ({
249
271
  }
250
272
  initializedRef.current = true;
251
273
  }
252
- }, [makeApiCall, id, initialFilter, onFilterUpdate]);
274
+ }, [dispatch, id, initialFilter, onFilterUpdate]);
253
275
 
254
276
  useEffect(() => {
255
- if (initialFilter !== '') filterApiCall();
256
-
257
- if (statusLabel !== prevStatusLabel.current) {
258
- prevStatusLabel.current = statusLabel;
277
+ const filterChanged = initialFilter !== prevFilter.current;
278
+ const statusChanged = jobFinished !== prevJobFinished.current;
279
+ const idChanged = id !== prevId.current;
280
+
281
+ if ((filterChanged || statusChanged || idChanged) && initialFilter !== '') {
282
+ prevFilter.current = initialFilter;
283
+ prevJobFinished.current = jobFinished;
284
+ prevId.current = id;
259
285
  filterApiCall();
260
286
  }
261
- }, [initialFilter, statusLabel, id, filterApiCall]);
287
+ }, [initialFilter, jobFinished, id, filterApiCall]);
288
+
289
+ useEffect(
290
+ () => () => {
291
+ mountedRef.current = false;
292
+ clearTimeout(pollTimeoutId.current);
293
+ },
294
+ []
295
+ );
262
296
 
263
297
  const {
264
298
  updateSearchQuery: updateSearchQueryBulk,
@@ -403,7 +437,7 @@ const JobInvocationHostTable = ({
403
437
  <DropdownFilter
404
438
  key="dropdown-filter"
405
439
  dropdownFilter={initialFilter}
406
- setDropdownFilter={handleFilterChange}
440
+ setDropdownFilter={onFilterUpdate}
407
441
  />,
408
442
  <CheckboxesActions
409
443
  bulkParams={selectedCount > 0 ? fetchBulkParams() : null}
@@ -541,13 +575,13 @@ JobInvocationHostTable.propTypes = {
541
575
  id: PropTypes.string.isRequired,
542
576
  targeting: PropTypes.object.isRequired,
543
577
  initialFilter: PropTypes.string.isRequired,
544
- statusLabel: PropTypes.string,
578
+ jobFinished: PropTypes.bool,
545
579
  onFilterUpdate: PropTypes.func,
546
580
  };
547
581
 
548
582
  JobInvocationHostTable.defaultProps = {
549
583
  onFilterUpdate: () => {},
550
- statusLabel: undefined,
584
+ jobFinished: false,
551
585
  };
552
586
 
553
587
  export default JobInvocationHostTable;
@@ -5,7 +5,6 @@ import {
5
5
  } from 'foremanReact/redux/API/APISelectors';
6
6
  import {
7
7
  JOB_INVOCATION_KEY,
8
- GET_TASK,
9
8
  GET_TEMPLATE_INVOCATION,
10
9
  LIST_TEMPLATE_INVOCATIONS,
11
10
  } from './JobInvocationConstants';
@@ -13,10 +12,8 @@ import {
13
12
  export const selectItems = state =>
14
13
  selectAPIResponse(state, JOB_INVOCATION_KEY);
15
14
 
16
- export const selectTask = state => selectAPIResponse(state, GET_TASK);
17
-
18
15
  export const selectTaskCancelable = state =>
19
- selectTask(state).available_actions?.cancellable || false;
16
+ selectItems(state).task?.cancellable || false;
20
17
 
21
18
  export const selectTemplateInvocation = hostID => state =>
22
19
  selectAPIResponse(state, `${GET_TEMPLATE_INVOCATION}_${hostID}`);
@@ -58,6 +58,12 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
58
58
  );
59
59
  }, [jobId, reportTemplateJobId, templateInputId]);
60
60
 
61
+ const isCreateReportDisabled =
62
+ !canGenerateReportTemplates ||
63
+ task?.state === STATUS.RUNNING ||
64
+ task?.state === STATUS.PENDING ||
65
+ reportHref === undefined;
66
+
61
67
  const onActionFocus = useCallback(() => {
62
68
  const element = document.getElementById(
63
69
  `toggle-split-button-action-primary-${jobId}`
@@ -135,9 +141,7 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
135
141
  <DropdownItem
136
142
  ouiaId="change-enabled-recurring-dropdown-item"
137
143
  onClick={() =>
138
- dispatch(
139
- enableRecurringLogic(recurrence?.id, recurringEnabled, jobId)
140
- )
144
+ dispatch(enableRecurringLogic(recurrence?.id, recurringEnabled))
141
145
  }
142
146
  key="change-enabled-recurring"
143
147
  component="button"
@@ -153,9 +157,7 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
153
157
  </DropdownItem>,
154
158
  <DropdownItem
155
159
  ouiaId="cancel-recurring-dropdown-item"
156
- onClick={() =>
157
- dispatch(cancelRecurringLogic(recurrence?.id, jobId))
158
- }
160
+ onClick={() => dispatch(cancelRecurringLogic(recurrence?.id))}
159
161
  key="cancel-recurring"
160
162
  component="button"
161
163
  isDisabled={
@@ -168,7 +170,7 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
168
170
  </DropdownItem>,
169
171
  ]
170
172
  : [],
171
- [recurrence, recurringEnabled, canEditRecurringLogic, dispatch, jobId]
173
+ [recurrence, recurringEnabled, canEditRecurringLogic, dispatch]
172
174
  );
173
175
 
174
176
  const dropdownItems = useMemo(
@@ -280,11 +282,7 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
280
282
  className="button-create-report"
281
283
  href={reportHref}
282
284
  variant="secondary"
283
- isDisabled={
284
- !canGenerateReportTemplates ||
285
- task?.state === STATUS.PENDING ||
286
- reportHref === undefined
287
- }
285
+ isDisabled={isCreateReportDisabled}
288
286
  >
289
287
  {__(`Create report`)}
290
288
  </Button>
@@ -11,6 +11,7 @@ import {
11
11
  showTemplateInvocationUrl,
12
12
  templateInvocationPageUrl,
13
13
  GET_TEMPLATE_INVOCATION,
14
+ AUTO_REFRESH_INTERVAL_MS,
14
15
  } from './JobInvocationConstants';
15
16
  import {
16
17
  selectTemplateInvocationStatus,
@@ -67,7 +68,7 @@ export const TemplateInvocation = ({
67
68
  showCommand,
68
69
  setShowCommand,
69
70
  }) => {
70
- const intervalRef = useRef(null);
71
+ const timeoutRef = useRef(null);
71
72
  const templateURL = showTemplateInvocationUrl(hostID, jobID);
72
73
  const hostDetailsPageUrl = useForemanHostDetailsPageUrl();
73
74
 
@@ -81,43 +82,48 @@ export const TemplateInvocation = ({
81
82
  }, [response]);
82
83
 
83
84
  useEffect(() => {
84
- const dispatchFetch = () => {
85
+ let cancelled = false;
86
+
87
+ const schedulePoll = () => {
88
+ if (cancelled) return;
85
89
  dispatch(
86
90
  APIActions.get({
87
91
  url: templateURL,
88
92
  key: `${GET_TEMPLATE_INVOCATION}_${hostID}`,
93
+ handleSuccess: ({ data }) => {
94
+ if (cancelled) return;
95
+ const isFinished = data?.finished ?? true;
96
+ // eslint-disable-next-line camelcase
97
+ const autoRefresh = data?.auto_refresh || false;
98
+ if (!isFinished && autoRefresh) {
99
+ timeoutRef.current = setTimeout(
100
+ schedulePoll,
101
+ AUTO_REFRESH_INTERVAL_MS
102
+ );
103
+ } else {
104
+ timeoutRef.current = null;
105
+ }
106
+ },
107
+ handleError: () => {
108
+ if (cancelled) return;
109
+ timeoutRef.current = null;
110
+ },
89
111
  })
90
112
  );
91
113
  };
92
114
 
93
- if (intervalRef.current) {
94
- clearInterval(intervalRef.current);
95
- intervalRef.current = null;
96
- }
115
+ clearTimeout(timeoutRef.current);
116
+ timeoutRef.current = null;
97
117
 
98
118
  if (isExpanded) {
99
- if (isEmpty(responseRef.current)) {
100
- dispatchFetch();
101
- }
102
-
103
- intervalRef.current = setInterval(() => {
104
- const latestResponse = responseRef.current;
105
- const finished = latestResponse?.finished ?? true;
106
- // eslint-disable-next-line camelcase
107
- const autoRefresh = latestResponse?.auto_refresh || false;
108
-
109
- if (!finished && autoRefresh) {
110
- dispatchFetch();
111
- } else if (intervalRef.current) {
112
- clearInterval(intervalRef.current);
113
- }
114
- }, 5000);
119
+ if (responseRef.current?.finished) return undefined;
120
+ schedulePoll();
115
121
  }
116
122
 
117
123
  return () => {
118
- if (intervalRef.current) {
119
- clearInterval(intervalRef.current);
120
- }
124
+ cancelled = true;
125
+ clearTimeout(timeoutRef.current);
126
+ timeoutRef.current = null;
121
127
  };
122
128
  }, [isExpanded, dispatch, templateURL, hostID]);
123
129