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.
- checksums.yaml +4 -4
- data/app/controllers/api/v2/job_invocations_controller.rb +24 -2
- data/app/controllers/job_invocations_controller.rb +1 -28
- data/app/views/api/v2/job_invocations/hosts.json.rabl +8 -0
- data/app/views/api/v2/job_invocations/main.json.rabl +1 -0
- data/app/views/job_templates/_alerts.html.erb +4 -0
- data/config/routes.rb +0 -1
- data/lib/foreman_remote_execution/plugin.rb +3 -3
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/functional/api/v2/job_invocations_controller_test.rb +46 -7
- data/webpack/JobInvocationDetail/JobInvocationActions.js +86 -91
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +6 -4
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +5 -3
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +92 -46
- data/webpack/JobInvocationDetail/JobInvocationSelectors.js +1 -9
- data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +5 -2
- data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +10 -12
- data/webpack/JobInvocationDetail/TemplateInvocation.js +38 -26
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +2 -1
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +17 -6
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/index.scss +0 -1
- data/webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js +369 -0
- data/webpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.js +228 -0
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +205 -154
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocationPolling.test.js +254 -0
- data/webpack/JobInvocationDetail/__tests__/fixtures.js +2 -0
- data/webpack/JobInvocationDetail/index.js +25 -25
- data/webpack/JobWizard/JobWizard.js +5 -1
- data/webpack/JobWizard/JobWizard.scss +21 -0
- data/webpack/JobWizard/JobWizardConstants.js +10 -1
- data/webpack/JobWizard/steps/Schedule/RepeatHour.js +4 -2
- data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +4 -2
- data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +2 -1
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +4 -1
- data/webpack/JobWizard/steps/form/DateTimePicker.js +12 -6
- data/webpack/JobWizard/steps/form/FormHelpers.js +5 -4
- data/webpack/react_app/components/FeaturesDropdown/index.scss +2 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +6 -2
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -1
- data/webpack/react_app/components/TargetingHosts/index.js +2 -1
- data/webpack/react_app/redux/actions/jobInvocations/index.js +3 -2
- data/webpack/test_setup.js +1 -13
- metadata +7 -4
- 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
|
|
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
|
|
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
|
-
|
|
187
|
+
requestParams => {
|
|
188
|
+
requestIdRef.current += 1;
|
|
189
|
+
const thisRequest = requestIdRef.current;
|
|
170
190
|
dispatch(
|
|
171
191
|
APIActions.get({
|
|
172
|
-
key:
|
|
173
|
-
url:
|
|
192
|
+
key: JOB_INVOCATION_HOSTS,
|
|
193
|
+
url: `/api/job_invocations/${id}/hosts`,
|
|
174
194
|
params: requestParams,
|
|
175
|
-
handleSuccess: data =>
|
|
176
|
-
|
|
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] ||
|
|
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,
|
|
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
|
-
|
|
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
|
-
}, [
|
|
281
|
+
}, [initialFilter, onFilterUpdate]);
|
|
253
282
|
|
|
254
283
|
useEffect(() => {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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,
|
|
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={
|
|
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
|
|
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
|
-
|
|
590
|
+
jobFinished: PropTypes.bool,
|
|
545
591
|
onFilterUpdate: PropTypes.func,
|
|
546
592
|
};
|
|
547
593
|
|
|
548
594
|
JobInvocationHostTable.defaultProps = {
|
|
549
595
|
onFilterUpdate: () => {},
|
|
550
|
-
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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
|
|
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={
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
intervalRef.current = null;
|
|
96
|
-
}
|
|
121
|
+
clearTimeout(timeoutRef.current);
|
|
122
|
+
timeoutRef.current = null;
|
|
97
123
|
|
|
98
124
|
if (isExpanded) {
|
|
99
|
-
if (
|
|
100
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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 *
|
|
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
|
|
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
|
-
|
|
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
|
};
|