foreman_remote_execution 16.6.5 → 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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +22 -13
  3. data/app/views/api/v2/job_invocations/main.json.rabl +16 -11
  4. data/app/views/job_templates/_alerts.html.erb +4 -0
  5. data/lib/foreman_remote_execution/plugin.rb +1 -1
  6. data/lib/foreman_remote_execution/version.rb +1 -1
  7. data/package.json +3 -3
  8. data/test/functional/api/v2/job_invocations_controller_test.rb +68 -3
  9. data/webpack/JobInvocationDetail/CheckboxesActions.js +4 -10
  10. data/webpack/JobInvocationDetail/JobInvocationActions.js +80 -90
  11. data/webpack/JobInvocationDetail/JobInvocationConstants.js +7 -6
  12. data/webpack/JobInvocationDetail/JobInvocationEmptyState.js +52 -0
  13. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +76 -42
  14. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +1 -19
  15. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +202 -153
  16. data/webpack/JobInvocationDetail/TemplateInvocation.js +31 -25
  17. data/webpack/JobInvocationDetail/__tests__/JobInvocationDetailEmptyState.test.js +53 -0
  18. data/webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js +336 -0
  19. data/webpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.js +223 -0
  20. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +260 -111
  21. data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +3 -1
  22. data/webpack/JobInvocationDetail/__tests__/TemplateInvocationPolling.test.js +254 -0
  23. data/webpack/JobInvocationDetail/__tests__/fixtures.js +2 -0
  24. data/webpack/JobInvocationDetail/__tests__/foremanTestHelpers.js +30 -0
  25. data/webpack/JobInvocationDetail/index.js +50 -30
  26. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +15 -0
  27. data/webpack/__mocks__/foremanReact/common/hooks/Permissions/permissionHooks.js +1 -0
  28. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +5 -0
  29. data/webpack/test_setup.js +1 -13
  30. metadata +11 -3
@@ -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;
@@ -3,22 +3,17 @@ import {
3
3
  selectAPIResponse,
4
4
  selectAPIStatus,
5
5
  } from 'foremanReact/redux/API/APISelectors';
6
- import { STATUS as APIStatus } from 'foremanReact/constants';
7
6
  import {
8
7
  JOB_INVOCATION_KEY,
9
- GET_TASK,
10
8
  GET_TEMPLATE_INVOCATION,
11
9
  LIST_TEMPLATE_INVOCATIONS,
12
- CURRENT_PERMISSIONS,
13
10
  } from './JobInvocationConstants';
14
11
 
15
12
  export const selectItems = state =>
16
13
  selectAPIResponse(state, JOB_INVOCATION_KEY);
17
14
 
18
- export const selectTask = state => selectAPIResponse(state, GET_TASK);
19
-
20
15
  export const selectTaskCancelable = state =>
21
- selectTask(state).available_actions?.cancellable || false;
16
+ selectItems(state).task?.cancellable || false;
22
17
 
23
18
  export const selectTemplateInvocation = hostID => state =>
24
19
  selectAPIResponse(state, `${GET_TEMPLATE_INVOCATION}_${hostID}`);
@@ -29,16 +24,3 @@ export const selectTemplateInvocationStatus = hostID => state =>
29
24
  export const selectTemplateInvocationList = state =>
30
25
  selectAPIResponse(state, LIST_TEMPLATE_INVOCATIONS)
31
26
  ?.template_invocations_task_by_hosts;
32
-
33
- export const selectCurrentPermisions = state =>
34
- selectAPIResponse(state, CURRENT_PERMISSIONS);
35
-
36
- export const selectHasPermission = permissionRequired => state => {
37
- const status = selectAPIStatus(state, CURRENT_PERMISSIONS);
38
- const selectCurrentPermissions = selectCurrentPermisions(state)?.results;
39
- return status === APIStatus.RESOLVED
40
- ? selectCurrentPermissions?.some(
41
- permission => permission.name === permissionRequired
42
- )
43
- : false;
44
- };