foreman_remote_execution 16.0.4 → 16.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +33 -6
  3. data/app/controllers/job_invocations_controller.rb +28 -1
  4. data/app/controllers/template_invocations_controller.rb +1 -1
  5. data/app/helpers/remote_execution_helper.rb +1 -1
  6. data/config/routes.rb +4 -2
  7. data/lib/foreman_remote_execution/engine.rb +9 -256
  8. data/lib/foreman_remote_execution/plugin.rb +240 -0
  9. data/lib/foreman_remote_execution/version.rb +1 -1
  10. data/test/functional/api/v2/job_invocations_controller_test.rb +1 -1
  11. data/test/unit/job_invocation_report_template_test.rb +6 -6
  12. data/webpack/JobInvocationDetail/CheckboxesActions.js +327 -0
  13. data/webpack/JobInvocationDetail/{JobInvocationHostTableToolbar.js → DropdownFilter.js} +3 -6
  14. data/webpack/JobInvocationDetail/JobInvocationConstants.js +8 -9
  15. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +18 -0
  16. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +205 -89
  17. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +30 -3
  18. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +23 -28
  19. data/webpack/JobInvocationDetail/OpenAllInvocationsModal.js +118 -0
  20. data/webpack/JobInvocationDetail/TemplateInvocation.js +54 -24
  21. data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +8 -10
  22. data/webpack/JobInvocationDetail/TemplateInvocationPage.js +1 -1
  23. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +2 -4
  24. data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +204 -0
  25. data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +34 -28
  26. data/webpack/JobInvocationDetail/index.js +71 -41
  27. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +26 -7
  28. data/webpack/Routes/routes.js +1 -1
  29. data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +1 -1
  30. metadata +8 -6
  31. data/webpack/JobInvocationDetail/OpenAlInvocations.js +0 -111
  32. data/webpack/JobInvocationDetail/__tests__/OpenAlInvocations.test.js +0 -110
@@ -52,14 +52,26 @@
52
52
  height: $chart_size;
53
53
  }
54
54
  }
55
+
55
56
  .job-additional-info {
56
57
  padding: 0;
57
58
  margin-bottom: -10px;
58
59
  }
60
+
59
61
  .job-details-table-section {
60
62
  section:nth-child(1) {
61
63
  padding: 0;
62
64
  }
65
+
66
+ .pf-v5-c-toolbar__group.pf-m-button-group {
67
+ display: flex;
68
+ align-items: center;
69
+ }
70
+
71
+ .open-all-button {
72
+ margin-left: 10px;
73
+ margin-right: 15px;
74
+ }
63
75
  }
64
76
 
65
77
  .template-invocation {
@@ -122,3 +134,9 @@
122
134
  margin-bottom: 10px;
123
135
  }
124
136
  }
137
+
138
+ .open-all-modal .pf-v5-c-modal-box__title-text {
139
+ white-space: normal;
140
+ text-overflow: unset;
141
+ word-break: break-word;
142
+ }
@@ -1,48 +1,58 @@
1
1
  /* eslint-disable max-lines */
2
2
  /* eslint-disable camelcase */
3
- import PropTypes from 'prop-types';
4
- import React, { useMemo, useEffect, useState } from 'react';
5
- import { Icon } from 'patternfly-react';
6
- import { translate as __ } from 'foremanReact/common/I18n';
7
- import { FormattedMessage } from 'react-intl';
8
- import { Tr, Td, Tbody, ExpandableRowContent } from '@patternfly/react-table';
9
3
  import {
10
4
  EmptyState,
11
- EmptyStateVariant,
12
5
  EmptyStateBody,
13
6
  EmptyStateHeader,
7
+ EmptyStateVariant,
8
+ ToolbarItem,
14
9
  } from '@patternfly/react-core';
10
+ import { ExpandableRowContent, Tbody, Td, Tr } from '@patternfly/react-table';
11
+ import { translate as __ } from 'foremanReact/common/I18n';
15
12
  import { foremanUrl } from 'foremanReact/common/helpers';
16
13
  import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
14
+ import { RowSelectTd } from 'foremanReact/components/HostsIndex/RowSelectTd';
15
+ import SelectAllCheckbox from 'foremanReact/components/PF4/TableIndexPage/Table/SelectAllCheckbox';
17
16
  import { Table } from 'foremanReact/components/PF4/TableIndexPage/Table/Table';
18
- import TableIndexPage from 'foremanReact/components/PF4/TableIndexPage/TableIndexPage';
19
- import { useSetParamsAndApiAndSearch } from 'foremanReact/components/PF4/TableIndexPage/Table/TableIndexHooks';
20
17
  import {
21
18
  useBulkSelect,
22
19
  useUrlParams,
23
20
  } from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks';
21
+ import { getPageStats } from 'foremanReact/components/PF4/TableIndexPage/Table/helpers';
22
+ import TableIndexPage from 'foremanReact/components/PF4/TableIndexPage/TableIndexPage';
24
23
  import { getControllerSearchProps } from 'foremanReact/constants';
24
+ import { Icon } from 'patternfly-react';
25
+ import PropTypes from 'prop-types';
26
+ import React, { useEffect, useMemo, useState } from 'react';
27
+ import { FormattedMessage } from 'react-intl';
28
+ import { useHistory } from 'react-router-dom';
29
+ import URI from 'urijs';
30
+ import { CheckboxesActions } from './CheckboxesActions';
31
+ import DropdownFilter from './DropdownFilter';
25
32
  import Columns, {
26
33
  JOB_INVOCATION_HOSTS,
34
+ MAX_HOSTS_API_SIZE,
27
35
  STATUS_UPPERCASE,
36
+ LIST_TEMPLATE_INVOCATIONS,
37
+ ALL_JOB_HOSTS,
28
38
  } from './JobInvocationConstants';
39
+ import { PopupAlert } from './OpenAllInvocationsModal';
29
40
  import { TemplateInvocation } from './TemplateInvocation';
30
- import { OpenAlInvocations, PopupAlert } from './OpenAlInvocations';
31
41
  import { RowActions } from './TemplateInvocationComponents/TemplateActionButtons';
32
- import JobInvocationHostTableToolbar from './JobInvocationHostTableToolbar';
33
42
 
34
43
  const JobInvocationHostTable = ({
44
+ failedCount,
35
45
  id,
36
- targeting,
37
- finished,
38
- autoRefresh,
39
46
  initialFilter,
40
47
  onFilterUpdate,
48
+ targeting,
41
49
  }) => {
42
50
  const columns = Columns();
43
51
  const columnNamesKeys = Object.keys(columns);
44
52
  const apiOptions = { key: JOB_INVOCATION_HOSTS };
53
+ const history = useHistory();
45
54
  const [selectedFilter, setSelectedFilter] = useState(initialFilter);
55
+ const [expandedHost, setExpandedHost] = useState([]);
46
56
 
47
57
  useEffect(() => {
48
58
  if (initialFilter !== selectedFilter) {
@@ -55,7 +65,9 @@ const JobInvocationHostTable = ({
55
65
  searchParam: urlSearchQuery = '',
56
66
  page: urlPage,
57
67
  per_page: urlPerPage,
68
+ order: urlOrder,
58
69
  } = useUrlParams();
70
+
59
71
  const constructFilter = (
60
72
  filter = selectedFilter,
61
73
  search = urlSearchQuery
@@ -71,11 +83,21 @@ const JobInvocationHostTable = ({
71
83
  .join(' AND ');
72
84
  };
73
85
 
74
- const search = constructFilter();
75
- const defaultParams = search !== '' ? { search } : {};
76
- if (urlPage) defaultParams.page = Number(urlPage);
77
- if (urlPerPage) defaultParams.per_page = Number(urlPerPage);
78
- const [expandedHost, setExpandedHost] = useState([]);
86
+ const defaultParams = useMemo(
87
+ () => ({
88
+ ...(urlPage ? { page: Number(urlPage) } : {}),
89
+ ...(urlPerPage ? { per_page: Number(urlPerPage) } : {}),
90
+ ...(urlOrder ? { order: urlOrder } : {}),
91
+ }),
92
+ [urlPage, urlPerPage, urlOrder]
93
+ );
94
+
95
+ useAPI('get', `/job_invocations/${id}/hosts`, {
96
+ params: {
97
+ search: defaultParams.search,
98
+ },
99
+ key: LIST_TEMPLATE_INVOCATIONS,
100
+ });
79
101
  const { response, status, setAPIOptions } = useAPI(
80
102
  'get',
81
103
  `/api/job_invocations/${id}/hosts`,
@@ -84,18 +106,93 @@ const JobInvocationHostTable = ({
84
106
  }
85
107
  );
86
108
 
87
- const { params } = useSetParamsAndApiAndSearch({
88
- defaultParams,
89
- apiOptions,
90
- setAPIOptions,
91
- });
109
+ const [allPagesResponse, setAllPagesResponse] = useState([]);
110
+ const apiAllParams = {
111
+ page: 1,
112
+ per_page: Math.min(response?.subtotal || 1, MAX_HOSTS_API_SIZE),
113
+ search: constructFilter(selectedFilter, urlSearchQuery),
114
+ };
92
115
 
93
- const { updateSearchQuery: updateSearchQueryBulk } = useBulkSelect({
116
+ const { response: allResponse, setAPIOptions: setAllAPIOptions } = useAPI(
117
+ 'get',
118
+ `/api/job_invocations/${id}/hosts`,
119
+ {
120
+ params: apiAllParams,
121
+ key: ALL_JOB_HOSTS,
122
+ }
123
+ );
124
+
125
+ useEffect(() => {
126
+ if (response?.subtotal) {
127
+ setAllAPIOptions(prevOptions => ({
128
+ ...prevOptions,
129
+ params: apiAllParams,
130
+ }));
131
+ }
132
+ // eslint-disable-next-line react-hooks/exhaustive-deps
133
+ }, [response?.subtotal, selectedFilter, urlSearchQuery]);
134
+
135
+ useEffect(() => {
136
+ if (allResponse?.results) {
137
+ setAllPagesResponse(allResponse.results);
138
+ }
139
+ }, [allResponse]);
140
+
141
+ const {
142
+ updateSearchQuery: updateSearchQueryBulk,
143
+ fetchBulkParams,
144
+ inclusionSet,
145
+ exclusionSet,
146
+ ...selectAllOptions
147
+ } = useBulkSelect({
148
+ results: response?.results,
149
+ metadata: {
150
+ total: response?.total,
151
+ page: response?.page,
152
+ selectable: response?.subtotal,
153
+ },
94
154
  initialSearchQuery: urlSearchQuery,
95
155
  });
96
- const updateSearchQuery = searchQuery => {
97
- updateSearchQueryBulk(searchQuery);
98
- };
156
+
157
+ const {
158
+ selectAll,
159
+ selectPage,
160
+ selectNone,
161
+ selectedCount,
162
+ selectOne,
163
+ areAllRowsOnPageSelected,
164
+ areAllRowsSelected,
165
+ isSelected,
166
+ } = selectAllOptions;
167
+
168
+ const allHostIds = allPagesResponse?.map(item => item.id) || [];
169
+ const selectedIds =
170
+ areAllRowsSelected() || exclusionSet.size > 0
171
+ ? allHostIds.filter(hostId => !exclusionSet.has(hostId))
172
+ : Array.from(inclusionSet);
173
+
174
+ const { pageRowCount } = getPageStats({
175
+ total: response?.total || 0,
176
+ page: response?.page || urlPage || 1,
177
+ perPage: response?.per_page || urlPerPage || 0,
178
+ });
179
+
180
+ const selectionToolbar = (
181
+ <ToolbarItem key="selectAll">
182
+ <SelectAllCheckbox
183
+ {...{
184
+ selectAll,
185
+ selectPage,
186
+ selectNone,
187
+ selectedCount,
188
+ pageRowCount,
189
+ }}
190
+ totalCount={response?.total}
191
+ areAllRowsOnPageSelected={areAllRowsOnPageSelected()}
192
+ areAllRowsSelected={areAllRowsSelected()}
193
+ />
194
+ </ToolbarItem>
195
+ );
99
196
 
100
197
  const controller = 'hosts';
101
198
  const memoDefaultSearchProps = useMemo(
@@ -106,50 +203,53 @@ const JobInvocationHostTable = ({
106
203
  `/${controller}/auto_complete_search`
107
204
  );
108
205
 
109
- const wrapSetSelectedFilter = filter => {
110
- const filterSearch = constructFilter(filter);
111
- setAPIOptions(prevOptions => {
112
- if (prevOptions.params.search !== filterSearch) {
113
- return {
114
- ...prevOptions,
115
- params: {
116
- ...prevOptions.params,
117
- search: filterSearch,
118
- },
119
- };
120
- }
121
- return prevOptions;
122
- });
123
- setSelectedFilter(filter);
124
- onFilterUpdate(filter);
125
- };
206
+ const wrapSetSelectedFilter = newFilter => {
207
+ setSelectedFilter(newFilter);
208
+ onFilterUpdate(newFilter);
126
209
 
127
- useEffect(() => {
128
- const intervalId = setInterval(() => {
129
- if (!finished || autoRefresh) {
130
- setAPIOptions(prevOptions => ({
131
- ...prevOptions,
132
- params: {
133
- ...prevOptions.params,
134
- },
135
- }));
136
- }
137
- }, 5000);
138
-
139
- return () => {
140
- clearInterval(intervalId);
210
+ const filterSearch = constructFilter(newFilter, urlSearchQuery);
211
+
212
+ const newParams = {
213
+ ...defaultParams,
214
+ page: 1,
141
215
  };
142
- }, [finished, autoRefresh, setAPIOptions]);
216
+
217
+ if (filterSearch !== '') {
218
+ newParams.search = filterSearch;
219
+ }
220
+
221
+ setAPIOptions(prev => ({ ...prev, params: newParams }));
222
+
223
+ const urlSearchParams = new URLSearchParams(window.location.search);
224
+ urlSearchParams.set('page', '1');
225
+ history.push({ search: urlSearchParams.toString() });
226
+ };
143
227
 
144
228
  const wrapSetAPIOptions = newAPIOptions => {
145
- setAPIOptions(prevOptions => ({
146
- ...prevOptions,
147
- params: {
148
- ...prevOptions.params,
149
- ...newAPIOptions.params,
150
- search: constructFilter(undefined, newAPIOptions?.params?.search),
151
- },
152
- }));
229
+ const newParams = newAPIOptions?.params ?? newAPIOptions ?? {};
230
+
231
+ const filterSearch = constructFilter(
232
+ selectedFilter,
233
+ newParams.search ?? urlSearchQuery
234
+ );
235
+
236
+ const mergedParams = {
237
+ ...defaultParams,
238
+ ...newParams,
239
+ };
240
+
241
+ if (filterSearch !== '') {
242
+ mergedParams.search = filterSearch;
243
+ } else if ('search' in mergedParams) {
244
+ delete mergedParams.search;
245
+ }
246
+
247
+ setAPIOptions(prev => ({ ...prev, params: mergedParams }));
248
+
249
+ const { search: _search, ...paramsForUrl } = mergedParams;
250
+ const uri = new URI();
251
+ uri.setSearch(paramsForUrl);
252
+ history.push({ search: uri.search() });
153
253
  };
154
254
 
155
255
  const combinedResponse = {
@@ -158,8 +258,8 @@ const JobInvocationHostTable = ({
158
258
  can_create: false,
159
259
  results: response?.results || [],
160
260
  total: response?.total || 0,
161
- per_page: response?.perPage,
162
- page: response?.page,
261
+ per_page: defaultParams?.perPage,
262
+ page: defaultParams?.page,
163
263
  subtotal: response?.subtotal || 0,
164
264
  message: response?.message || 'error',
165
265
  },
@@ -213,6 +313,7 @@ const JobInvocationHostTable = ({
213
313
  return isExpanding ? [...otherExpandedHosts, host] : otherExpandedHosts;
214
314
  });
215
315
  const [showAlert, setShowAlert] = useState(false);
316
+
216
317
  return (
217
318
  <>
218
319
  {showAlert && <PopupAlert setShowAlert={setShowAlert} />}
@@ -223,34 +324,44 @@ const JobInvocationHostTable = ({
223
324
  controller="hosts"
224
325
  creatable={false}
225
326
  replacementResponse={combinedResponse}
226
- updateSearchQuery={updateSearchQuery}
327
+ updateSearchQuery={updateSearchQueryBulk}
227
328
  customToolbarItems={[
228
- <OpenAlInvocations
229
- setShowAlert={setShowAlert}
230
- results={results}
231
- id={id}
232
- key="OpenAlInvocations"
233
- />,
234
- <JobInvocationHostTableToolbar
329
+ <DropdownFilter
330
+ key="dropdown-filter"
235
331
  dropdownFilter={selectedFilter}
236
332
  setDropdownFilter={wrapSetSelectedFilter}
237
- key="JobInvocationHostTableToolbar"
333
+ />,
334
+ <CheckboxesActions
335
+ bulkParams={selectedCount > 0 ? fetchBulkParams() : null}
336
+ selectedIds={selectedIds}
337
+ failedCount={failedCount}
338
+ jobID={id}
339
+ key="checkboxes-actions"
340
+ filter={selectedFilter}
238
341
  />,
239
342
  ]}
343
+ selectionToolbar={selectionToolbar}
240
344
  >
241
345
  <Table
242
346
  ouiaId="job-invocation-hosts-table"
243
347
  columns={columns}
244
348
  customEmptyState={
245
- status === STATUS_UPPERCASE.RESOLVED && !response?.results?.length
349
+ status === STATUS_UPPERCASE.RESOLVED && !results.length
246
350
  ? customEmptyState
247
351
  : null
248
352
  }
249
- params={params}
353
+ params={{
354
+ page: response?.page || Number(urlPage),
355
+ per_page: response?.per_page || Number(urlPerPage),
356
+ order: urlOrder,
357
+ }}
358
+ page={response?.page || Number(urlPage)}
359
+ perPage={response?.per_page || Number(urlPerPage)}
250
360
  setParams={wrapSetAPIOptions}
251
361
  itemCount={response?.subtotal}
252
- results={response?.results}
362
+ results={results}
253
363
  url=""
364
+ showCheckboxes
254
365
  refreshData={() => {}}
255
366
  errorMessage={
256
367
  status === STATUS_UPPERCASE.ERROR && response?.message
@@ -261,9 +372,9 @@ const JobInvocationHostTable = ({
261
372
  isDeleteable={false}
262
373
  childrenOutsideTbody
263
374
  >
264
- {results?.map((result, rowIndex) => (
265
- <Tbody key={rowIndex}>
266
- <Tr ouiaId={`table-row-${rowIndex}`}>
375
+ {results.map((result, rowIndex) => (
376
+ <Tbody key={result.id}>
377
+ <Tr ouiaId={`table-row-${result.id}`}>
267
378
  <Td
268
379
  expand={{
269
380
  rowIndex,
@@ -273,6 +384,7 @@ const JobInvocationHostTable = ({
273
384
  expandId: 'host-expandable',
274
385
  }}
275
386
  />
387
+ <RowSelectTd rowData={result} {...{ selectOne, isSelected }} />
276
388
  {columnNamesKeys.map(k => (
277
389
  <Td key={k}>{columns[k].wrapper(result)}</Td>
278
390
  ))}
@@ -286,7 +398,7 @@ const JobInvocationHostTable = ({
286
398
  >
287
399
  <Td
288
400
  dataLabel={`${result.id}-expandable-content`}
289
- colSpan={columnNamesKeys.length}
401
+ colSpan={columnNamesKeys.length + 3}
290
402
  >
291
403
  <ExpandableRowContent>
292
404
  {result.job_status === 'cancelled' ||
@@ -295,7 +407,12 @@ const JobInvocationHostTable = ({
295
407
  {__('A task for this host has not been started')}
296
408
  </div>
297
409
  ) : (
298
- <TemplateInvocation hostID={result.id} jobID={id} />
410
+ <TemplateInvocation
411
+ hostID={result.id}
412
+ jobID={id}
413
+ isInTableView
414
+ isExpanded={isHostExpanded(result.id)}
415
+ />
299
416
  )}
300
417
  </ExpandableRowContent>
301
418
  </Td>
@@ -311,8 +428,7 @@ const JobInvocationHostTable = ({
311
428
  JobInvocationHostTable.propTypes = {
312
429
  id: PropTypes.string.isRequired,
313
430
  targeting: PropTypes.object.isRequired,
314
- finished: PropTypes.bool.isRequired,
315
- autoRefresh: PropTypes.bool.isRequired,
431
+ failedCount: PropTypes.number.isRequired,
316
432
  initialFilter: PropTypes.string.isRequired,
317
433
  onFilterUpdate: PropTypes.func,
318
434
  };
@@ -1,8 +1,15 @@
1
- import { selectAPIResponse } from 'foremanReact/redux/API/APISelectors';
1
+ /* eslint-disable camelcase */
2
+ import {
3
+ selectAPIResponse,
4
+ selectAPIStatus,
5
+ } from 'foremanReact/redux/API/APISelectors';
6
+ import { STATUS as APIStatus } from 'foremanReact/constants';
2
7
  import {
3
8
  JOB_INVOCATION_KEY,
4
9
  GET_TASK,
5
10
  GET_TEMPLATE_INVOCATION,
11
+ LIST_TEMPLATE_INVOCATIONS,
12
+ CURRENT_PERMISSIONS,
6
13
  } from './JobInvocationConstants';
7
14
 
8
15
  export const selectItems = state =>
@@ -13,5 +20,25 @@ export const selectTask = state => selectAPIResponse(state, GET_TASK);
13
20
  export const selectTaskCancelable = state =>
14
21
  selectTask(state).available_actions?.cancellable || false;
15
22
 
16
- export const selectTemplateInvocation = state =>
17
- selectAPIResponse(state, GET_TEMPLATE_INVOCATION);
23
+ export const selectTemplateInvocation = hostID => state =>
24
+ selectAPIResponse(state, `${GET_TEMPLATE_INVOCATION}_${hostID}`);
25
+
26
+ export const selectTemplateInvocationStatus = hostID => state =>
27
+ selectAPIStatus(state, `${GET_TEMPLATE_INVOCATION}_${hostID}`);
28
+
29
+ export const selectTemplateInvocationList = state =>
30
+ selectAPIResponse(state, LIST_TEMPLATE_INVOCATIONS)
31
+ ?.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
+ };
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
2
2
  import React, { useEffect, useState } from 'react';
3
3
  import { useDispatch, useSelector } from 'react-redux';
4
4
  import { Button, Split, SplitItem } from '@patternfly/react-core';
5
+ import { UndoIcon } from '@patternfly/react-icons';
5
6
  import {
6
7
  Dropdown,
7
8
  DropdownItem,
@@ -11,7 +12,6 @@ import {
11
12
  } from '@patternfly/react-core/deprecated';
12
13
  import { translate as __ } from 'foremanReact/common/I18n';
13
14
  import { foremanUrl } from 'foremanReact/common/helpers';
14
- import { STATUS as APIStatus } from 'foremanReact/constants';
15
15
  import { get } from 'foremanReact/redux/API';
16
16
  import {
17
17
  cancelJob,
@@ -23,14 +23,12 @@ import {
23
23
  GET_REPORT_TEMPLATES,
24
24
  GET_REPORT_TEMPLATE_INPUTS,
25
25
  } from './JobInvocationConstants';
26
- import { selectTaskCancelable } from './JobInvocationSelectors';
26
+ import {
27
+ selectTaskCancelable,
28
+ selectHasPermission,
29
+ } from './JobInvocationSelectors';
27
30
 
28
- const JobInvocationToolbarButtons = ({
29
- jobId,
30
- data,
31
- currentPermissions,
32
- permissionsStatus,
33
- }) => {
31
+ const JobInvocationToolbarButtons = ({ jobId, data }) => {
34
32
  const { succeeded, failed, task, recurrence, permissions } = data;
35
33
  const recurringEnabled = recurrence?.state === 'active';
36
34
  const canViewForemanTasks = permissions
@@ -40,6 +38,8 @@ const JobInvocationToolbarButtons = ({
40
38
  ? permissions.edit_recurring_logics
41
39
  : false;
42
40
  const isTaskCancelable = useSelector(selectTaskCancelable);
41
+ const useHasPermission = permissionRequired =>
42
+ useSelector(selectHasPermission(permissionRequired));
43
43
  const [isActionOpen, setIsActionOpen] = useState(false);
44
44
  const [reportTemplateJobId, setReportTemplateJobId] = useState(undefined);
45
45
  const [templateInputId, setTemplateInputId] = useState(undefined);
@@ -58,12 +58,6 @@ const JobInvocationToolbarButtons = ({
58
58
  setIsActionOpen(false);
59
59
  onActionFocus();
60
60
  };
61
- const hasPermission = permissionRequired =>
62
- permissionsStatus === APIStatus.RESOLVED
63
- ? currentPermissions?.some(
64
- permission => permission.name === permissionRequired
65
- )
66
- : false;
67
61
 
68
62
  useEffect(() => {
69
63
  dispatch(
@@ -142,7 +136,9 @@ const JobInvocationToolbarButtons = ({
142
136
  ouiaId="rerun-succeeded-dropdown-item"
143
137
  href={foremanUrl(`/job_invocations/${jobId}/rerun?succeeded_only=1`)}
144
138
  key="rerun-succeeded"
145
- isDisabled={!(succeeded > 0) || !hasPermission('create_job_invocations')}
139
+ isDisabled={
140
+ !useHasPermission('create_job_invocations') || !(succeeded > 0)
141
+ }
146
142
  description="Rerun job on successful hosts"
147
143
  >
148
144
  {__('Rerun successful')}
@@ -151,7 +147,7 @@ const JobInvocationToolbarButtons = ({
151
147
  ouiaId="rerun-failed-dropdown-item"
152
148
  href={foremanUrl(`/job_invocations/${jobId}/rerun?failed_only=1`)}
153
149
  key="rerun-failed"
154
- isDisabled={!(failed > 0) || !hasPermission('create_job_invocations')}
150
+ isDisabled={!useHasPermission('create_job_invocations') || !(failed > 0)}
155
151
  description="Rerun job on failed hosts"
156
152
  >
157
153
  {__('Rerun failed')}
@@ -171,7 +167,9 @@ const JobInvocationToolbarButtons = ({
171
167
  onClick={() => dispatch(cancelJob(jobId, false))}
172
168
  key="cancel"
173
169
  component="button"
174
- isDisabled={!isTaskCancelable || !hasPermission('cancel_job_invocations')}
170
+ isDisabled={
171
+ !useHasPermission('cancel_job_invocations') || !isTaskCancelable
172
+ }
175
173
  description="Cancel job gracefully"
176
174
  >
177
175
  {__('Cancel')}
@@ -181,7 +179,9 @@ const JobInvocationToolbarButtons = ({
181
179
  onClick={() => dispatch(cancelJob(jobId, true))}
182
180
  key="abort"
183
181
  component="button"
184
- isDisabled={!isTaskCancelable || !hasPermission('cancel_job_invocations')}
182
+ isDisabled={
183
+ !useHasPermission('cancel_job_invocations') || !isTaskCancelable
184
+ }
185
185
  description="Cancel job immediately"
186
186
  >
187
187
  {__('Abort')}
@@ -190,7 +190,8 @@ const JobInvocationToolbarButtons = ({
190
190
  <DropdownSeparator ouiaId="dropdown-separator-2" key="separator-2" />,
191
191
  <DropdownItem
192
192
  ouiaId="legacy-ui-dropdown-item"
193
- href={`/job_invocations/${jobId}`}
193
+ icon={<UndoIcon />}
194
+ href={`/legacy/job_invocations/${jobId}`}
194
195
  key="legacy-ui"
195
196
  >
196
197
  {__('Legacy UI')}
@@ -210,9 +211,9 @@ const JobInvocationToolbarButtons = ({
210
211
  )}
211
212
  variant="secondary"
212
213
  isDisabled={
214
+ !useHasPermission('generate_report_templates') ||
213
215
  task?.state === STATUS.PENDING ||
214
- templateInputId === undefined ||
215
- !hasPermission('generate_report_templates')
216
+ templateInputId === undefined
216
217
  }
217
218
  >
218
219
  {__(`Create report`)}
@@ -234,7 +235,7 @@ const JobInvocationToolbarButtons = ({
234
235
  key="rerun"
235
236
  href={foremanUrl(`/job_invocations/${jobId}/rerun`)}
236
237
  variant="control"
237
- isDisabled={!hasPermission('create_job_invocations')}
238
+ isDisabled={!useHasPermission('create_job_invocations')}
238
239
  >
239
240
  {__(`Rerun all`)}
240
241
  </Button>,
@@ -255,12 +256,6 @@ const JobInvocationToolbarButtons = ({
255
256
  JobInvocationToolbarButtons.propTypes = {
256
257
  jobId: PropTypes.string.isRequired,
257
258
  data: PropTypes.object.isRequired,
258
- currentPermissions: PropTypes.array,
259
- permissionsStatus: PropTypes.string,
260
- };
261
- JobInvocationToolbarButtons.defaultProps = {
262
- currentPermissions: undefined,
263
- permissionsStatus: undefined,
264
259
  };
265
260
 
266
261
  export default JobInvocationToolbarButtons;