foreman_remote_execution 16.7.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 (44) 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/app/views/api/v2/job_invocations/main.json.rabl +1 -0
  6. data/app/views/job_templates/_alerts.html.erb +4 -0
  7. data/config/routes.rb +0 -1
  8. data/lib/foreman_remote_execution/plugin.rb +3 -3
  9. data/lib/foreman_remote_execution/version.rb +1 -1
  10. data/test/functional/api/v2/job_invocations_controller_test.rb +46 -7
  11. data/webpack/JobInvocationDetail/JobInvocationActions.js +86 -91
  12. data/webpack/JobInvocationDetail/JobInvocationConstants.js +6 -4
  13. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +5 -3
  14. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +92 -46
  15. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +1 -9
  16. data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +5 -2
  17. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +10 -12
  18. data/webpack/JobInvocationDetail/TemplateInvocation.js +38 -26
  19. data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +2 -1
  20. data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +17 -6
  21. data/webpack/JobInvocationDetail/TemplateInvocationComponents/index.scss +0 -1
  22. data/webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js +369 -0
  23. data/webpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.js +228 -0
  24. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +205 -154
  25. data/webpack/JobInvocationDetail/__tests__/TemplateInvocationPolling.test.js +254 -0
  26. data/webpack/JobInvocationDetail/__tests__/fixtures.js +2 -0
  27. data/webpack/JobInvocationDetail/index.js +25 -25
  28. data/webpack/JobWizard/JobWizard.js +5 -1
  29. data/webpack/JobWizard/JobWizard.scss +21 -0
  30. data/webpack/JobWizard/JobWizardConstants.js +10 -1
  31. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +4 -2
  32. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +4 -2
  33. data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +2 -1
  34. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +4 -1
  35. data/webpack/JobWizard/steps/form/DateTimePicker.js +12 -6
  36. data/webpack/JobWizard/steps/form/FormHelpers.js +5 -4
  37. data/webpack/react_app/components/FeaturesDropdown/index.scss +2 -0
  38. data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +6 -2
  39. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -1
  40. data/webpack/react_app/components/TargetingHosts/index.js +2 -1
  41. data/webpack/react_app/redux/actions/jobInvocations/index.js +3 -2
  42. data/webpack/test_setup.js +1 -13
  43. metadata +7 -4
  44. data/webpack/__mocks__/foremanReact/constants.js +0 -25
@@ -39,10 +39,9 @@ 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
- ALL_JOB_HOSTS,
45
43
  AWAITING_STATUS_FILTER,
44
+ AUTO_REFRESH_INTERVAL_MS,
46
45
  } from './JobInvocationConstants';
47
46
  import { TemplateInvocation } from './TemplateInvocation';
48
47
  import { RowActions } from './TemplateInvocationComponents/TemplateActionButtons';
@@ -51,8 +50,8 @@ import { PopupAlert } from './OpenAllInvocationsModal';
51
50
  const JobInvocationHostTable = ({
52
51
  id,
53
52
  initialFilter,
53
+ jobFinished,
54
54
  onFilterUpdate,
55
- statusLabel,
56
55
  targeting,
57
56
  }) => {
58
57
  const columns = Columns();
@@ -69,7 +68,17 @@ const JobInvocationHostTable = ({
69
68
 
70
69
  // Expansive items
71
70
  const [expandedHost, setExpandedHost] = useState(new Set());
72
- const prevStatusLabel = useRef(statusLabel);
71
+ const prevJobFinished = useRef(jobFinished);
72
+ const prevFilter = useRef('');
73
+ const prevId = useRef(id);
74
+ const pollTimeoutId = useRef(null);
75
+ const currentPollParams = useRef({});
76
+ const mountedRef = useRef(true);
77
+ const requestIdRef = useRef(0);
78
+ const jobFinishedRef = useRef(jobFinished);
79
+ useEffect(() => {
80
+ jobFinishedRef.current = jobFinished;
81
+ }, [jobFinished]);
73
82
 
74
83
  const [hostInvocationStates, setHostInvocationStates] = useState({});
75
84
 
@@ -153,33 +162,63 @@ const JobInvocationHostTable = ({
153
162
  [initialFilter, urlSearchQuery]
154
163
  );
155
164
 
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
- }
165
+ const [hostPermissions, setHostPermissions] = useState({});
163
166
 
167
+ const updateHostsState = useCallback(data => {
168
+ const ids = data.data.results.map(i => i.id);
169
+ setApiResponse(data.data);
170
+ setAllHostsIds(ids);
164
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
+ }
165
183
  }, []);
166
184
 
167
185
  // Call hosts data with params
168
186
  const makeApiCall = useCallback(
169
- (requestParams, callParams = {}) => {
187
+ requestParams => {
188
+ requestIdRef.current += 1;
189
+ const thisRequest = requestIdRef.current;
170
190
  dispatch(
171
191
  APIActions.get({
172
- key: callParams.key ?? ALL_JOB_HOSTS,
173
- url: callParams.url ?? `/api/job_invocations/${id}/hosts`,
192
+ key: JOB_INVOCATION_HOSTS,
193
+ url: `/api/job_invocations/${id}/hosts`,
174
194
  params: requestParams,
175
- handleSuccess: data => handleResponse(data, callParams.key),
176
- handleError: () => setStatus(STATUS_UPPERCASE.ERROR),
195
+ handleSuccess: data => {
196
+ if (thisRequest !== requestIdRef.current) return;
197
+ if (!mountedRef.current) return;
198
+ updateHostsState(data);
199
+ if (!jobFinishedRef.current) {
200
+ pollTimeoutId.current = setTimeout(
201
+ () => makeApiCall(currentPollParams.current),
202
+ AUTO_REFRESH_INTERVAL_MS
203
+ );
204
+ } else {
205
+ pollTimeoutId.current = null;
206
+ }
207
+ },
208
+ handleError: () => {
209
+ if (thisRequest !== requestIdRef.current) return;
210
+ if (!mountedRef.current) return;
211
+ pollTimeoutId.current = null;
212
+ setStatus(STATUS_UPPERCASE.ERROR);
213
+ },
177
214
  errorToast: ({ response }) =>
178
- response?.data?.error?.full_messages?.[0] || response,
215
+ response?.data?.error?.full_messages?.[0] ||
216
+ response?.data?.error?.message ||
217
+ __('Failed to load host invocation data'),
179
218
  })
180
219
  );
181
220
  },
182
- [dispatch, id, handleResponse]
221
+ [dispatch, id, updateHostsState]
183
222
  );
184
223
 
185
224
  const filterApiCall = useCallback(
@@ -202,7 +241,14 @@ const JobInvocationHostTable = ({
202
241
  finalParams.search = filterSearch;
203
242
  }
204
243
 
205
- makeApiCall(finalParams, { key: JOB_INVOCATION_HOSTS });
244
+ finalParams.include_permissions = true;
245
+
246
+ currentPollParams.current = { ...finalParams };
247
+ delete currentPollParams.current.include_permissions;
248
+ clearTimeout(pollTimeoutId.current);
249
+ pollTimeoutId.current = null;
250
+
251
+ makeApiCall(finalParams);
206
252
 
207
253
  const urlSearchParams = new URLSearchParams(window.location.search);
208
254
 
@@ -222,43 +268,38 @@ const JobInvocationHostTable = ({
222
268
  ]
223
269
  );
224
270
 
225
- // Filter change
226
- const handleFilterChange = useCallback(
227
- newFilter => {
228
- onFilterUpdate(newFilter);
229
- },
230
- [onFilterUpdate]
231
- );
232
-
233
271
  // Effects
234
272
  // run after mount
235
273
  const initializedRef = useRef(false);
236
274
  useEffect(() => {
237
275
  if (!initializedRef.current) {
238
- // Job Invo template load
239
- makeApiCall(
240
- {},
241
- {
242
- url: `/job_invocations/${id}/hosts`,
243
- key: LIST_TEMPLATE_INVOCATIONS,
244
- }
245
- );
246
-
247
276
  if (initialFilter === '') {
248
277
  onFilterUpdate('all_statuses');
249
278
  }
250
279
  initializedRef.current = true;
251
280
  }
252
- }, [makeApiCall, id, initialFilter, onFilterUpdate]);
281
+ }, [initialFilter, onFilterUpdate]);
253
282
 
254
283
  useEffect(() => {
255
- if (initialFilter !== '') filterApiCall();
256
-
257
- if (statusLabel !== prevStatusLabel.current) {
258
- prevStatusLabel.current = statusLabel;
284
+ const filterChanged = initialFilter !== prevFilter.current;
285
+ const statusChanged = jobFinished !== prevJobFinished.current;
286
+ const idChanged = id !== prevId.current;
287
+
288
+ if ((filterChanged || statusChanged || idChanged) && initialFilter !== '') {
289
+ prevFilter.current = initialFilter;
290
+ prevJobFinished.current = jobFinished;
291
+ prevId.current = id;
259
292
  filterApiCall();
260
293
  }
261
- }, [initialFilter, statusLabel, id, filterApiCall]);
294
+ }, [initialFilter, jobFinished, id, filterApiCall]);
295
+
296
+ useEffect(
297
+ () => () => {
298
+ mountedRef.current = false;
299
+ clearTimeout(pollTimeoutId.current);
300
+ },
301
+ []
302
+ );
262
303
 
263
304
  const {
264
305
  updateSearchQuery: updateSearchQueryBulk,
@@ -403,7 +444,7 @@ const JobInvocationHostTable = ({
403
444
  <DropdownFilter
404
445
  key="dropdown-filter"
405
446
  dropdownFilter={initialFilter}
406
- setDropdownFilter={handleFilterChange}
447
+ setDropdownFilter={onFilterUpdate}
407
448
  />,
408
449
  <CheckboxesActions
409
450
  bulkParams={selectedCount > 0 ? fetchBulkParams() : null}
@@ -472,7 +513,12 @@ const JobInvocationHostTable = ({
472
513
  <Td key={k}>{columns[k].wrapper(result)}</Td>
473
514
  ))}
474
515
  <Td isActionCell>
475
- <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
+ />
476
522
  </Td>
477
523
  </Tr>
478
524
  <Tr
@@ -541,13 +587,13 @@ JobInvocationHostTable.propTypes = {
541
587
  id: PropTypes.string.isRequired,
542
588
  targeting: PropTypes.object.isRequired,
543
589
  initialFilter: PropTypes.string.isRequired,
544
- statusLabel: PropTypes.string,
590
+ jobFinished: PropTypes.bool,
545
591
  onFilterUpdate: PropTypes.func,
546
592
  };
547
593
 
548
594
  JobInvocationHostTable.defaultProps = {
549
595
  onFilterUpdate: () => {},
550
- statusLabel: undefined,
596
+ jobFinished: false,
551
597
  };
552
598
 
553
599
  export default JobInvocationHostTable;
@@ -5,25 +5,17 @@ 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
- LIST_TEMPLATE_INVOCATIONS,
11
9
  } from './JobInvocationConstants';
12
10
 
13
11
  export const selectItems = state =>
14
12
  selectAPIResponse(state, JOB_INVOCATION_KEY);
15
13
 
16
- export const selectTask = state => selectAPIResponse(state, GET_TASK);
17
-
18
14
  export const selectTaskCancelable = state =>
19
- selectTask(state).available_actions?.cancellable || false;
15
+ selectItems(state).task?.cancellable || false;
20
16
 
21
17
  export const selectTemplateInvocation = hostID => state =>
22
18
  selectAPIResponse(state, `${GET_TEMPLATE_INVOCATION}_${hostID}`);
23
19
 
24
20
  export const selectTemplateInvocationStatus = hostID => state =>
25
21
  selectAPIStatus(state, `${GET_TEMPLATE_INVOCATION}_${hostID}`);
26
-
27
- export const selectTemplateInvocationList = state =>
28
- selectAPIResponse(state, LIST_TEMPLATE_INVOCATIONS)
29
- ?.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(() => {
@@ -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,9 @@ import {
11
11
  showTemplateInvocationUrl,
12
12
  templateInvocationPageUrl,
13
13
  GET_TEMPLATE_INVOCATION,
14
+ AUTO_REFRESH_INTERVAL_MS,
15
+ CLIPBOARD_COPIED_EXIT_DELAY_MS,
16
+ CLIPBOARD_DEFAULT_EXIT_DELAY_MS,
14
17
  } from './JobInvocationConstants';
15
18
  import {
16
19
  selectTemplateInvocationStatus,
@@ -41,7 +44,11 @@ const CopyToClipboard = ({ fullOutput }) => {
41
44
  textId="code-content"
42
45
  aria-label="Copy to clipboard"
43
46
  onClick={e => onClick(e, fullOutput)}
44
- exitDelay={copied ? 1500 : 600}
47
+ exitDelay={
48
+ copied
49
+ ? CLIPBOARD_COPIED_EXIT_DELAY_MS
50
+ : CLIPBOARD_DEFAULT_EXIT_DELAY_MS
51
+ }
45
52
  maxWidth="110px"
46
53
  variant="plain"
47
54
  onTooltipHidden={() => setCopied(false)}
@@ -67,7 +74,7 @@ export const TemplateInvocation = ({
67
74
  showCommand,
68
75
  setShowCommand,
69
76
  }) => {
70
- const intervalRef = useRef(null);
77
+ const timeoutRef = useRef(null);
71
78
  const templateURL = showTemplateInvocationUrl(hostID, jobID);
72
79
  const hostDetailsPageUrl = useForemanHostDetailsPageUrl();
73
80
 
@@ -81,43 +88,48 @@ export const TemplateInvocation = ({
81
88
  }, [response]);
82
89
 
83
90
  useEffect(() => {
84
- const dispatchFetch = () => {
91
+ let cancelled = false;
92
+
93
+ const schedulePoll = () => {
94
+ if (cancelled) return;
85
95
  dispatch(
86
96
  APIActions.get({
87
97
  url: templateURL,
88
98
  key: `${GET_TEMPLATE_INVOCATION}_${hostID}`,
99
+ handleSuccess: ({ data }) => {
100
+ if (cancelled) return;
101
+ const isFinished = data?.finished ?? true;
102
+ // eslint-disable-next-line camelcase
103
+ const autoRefresh = data?.auto_refresh || false;
104
+ if (!isFinished && autoRefresh) {
105
+ timeoutRef.current = setTimeout(
106
+ schedulePoll,
107
+ AUTO_REFRESH_INTERVAL_MS
108
+ );
109
+ } else {
110
+ timeoutRef.current = null;
111
+ }
112
+ },
113
+ handleError: () => {
114
+ if (cancelled) return;
115
+ timeoutRef.current = null;
116
+ },
89
117
  })
90
118
  );
91
119
  };
92
120
 
93
- if (intervalRef.current) {
94
- clearInterval(intervalRef.current);
95
- intervalRef.current = null;
96
- }
121
+ clearTimeout(timeoutRef.current);
122
+ timeoutRef.current = null;
97
123
 
98
124
  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);
125
+ if (responseRef.current?.finished) return undefined;
126
+ schedulePoll();
115
127
  }
116
128
 
117
129
  return () => {
118
- if (intervalRef.current) {
119
- clearInterval(intervalRef.current);
120
- }
130
+ cancelled = true;
131
+ clearTimeout(timeoutRef.current);
132
+ timeoutRef.current = null;
121
133
  };
122
134
  }, [isExpanded, dispatch, templateURL, hostID]);
123
135
 
@@ -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