foreman_remote_execution 16.3.1 → 16.3.2
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 +3 -5
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +4 -0
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +123 -62
- data/webpack/JobInvocationDetail/TemplateInvocation.js +19 -15
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputToggleGroup.js +43 -25
- data/webpack/JobInvocationDetail/TemplateInvocationPage.js +16 -1
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +114 -72
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7254f1da767b1e48ba82c93e57cd093611b42cb302e9eca41f0bb844a0d1883
|
|
4
|
+
data.tar.gz: c19c6c596d7e531794003be27795b665ad4e2ef9055e94a51c4d6c50c361a75f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 792c2e9e61c5e42e471832f47503189eab7edb69a2f4c857a1ab9b659eef114da16fc89b08d78f891b600997f92221cbe0651249fbe953f8ad0f9bd5d9de4c37
|
|
7
|
+
data.tar.gz: 0e3797e25d92fb9e39606913937c0cb9a266f48c1780141969927362e095bd253a75426cf7e6309c14f4d981d29b77ff5aea018ad3a8f0b14fe49818d2c46050
|
|
@@ -116,13 +116,11 @@ module Api
|
|
|
116
116
|
def hosts
|
|
117
117
|
set_hosts_and_template_invocations
|
|
118
118
|
set_statuses_and_smart_proxies
|
|
119
|
-
@total = @
|
|
119
|
+
@total = @hosts.size
|
|
120
120
|
@hosts = @hosts.search_for(params[:search], :order => params[:order]).paginate(:page => params[:page], :per_page => params[:per_page])
|
|
121
|
+
@subtotal = @hosts.total_entries
|
|
121
122
|
if params[:awaiting]
|
|
122
123
|
@hosts = @hosts.select { |host| @host_statuses[host.id] == 'N/A' }
|
|
123
|
-
@subtotal = @hosts.size
|
|
124
|
-
else
|
|
125
|
-
@subtotal = @hosts.respond_to?(:total_entries) ? @hosts.total_entries : @hosts.sizes
|
|
126
124
|
end
|
|
127
125
|
render :hosts, :layout => 'api/v2/layouts/index_layout'
|
|
128
126
|
end
|
|
@@ -303,7 +301,7 @@ module Api
|
|
|
303
301
|
@pattern_template_invocations = @job_invocation.pattern_template_invocations.includes(:input_values)
|
|
304
302
|
@hosts = @job_invocation.targeting.hosts.authorized(:view_hosts, Host)
|
|
305
303
|
|
|
306
|
-
|
|
304
|
+
if params[:search].present?
|
|
307
305
|
@hosts = @hosts.joins(:template_invocations)
|
|
308
306
|
.where(:template_invocations => { :job_invocation_id => @job_invocation.id})
|
|
309
307
|
end
|
|
@@ -61,9 +61,47 @@ const JobInvocationHostTable = ({
|
|
|
61
61
|
const [allHostsIds, setAllHostsIds] = useState([]);
|
|
62
62
|
|
|
63
63
|
// Expansive items
|
|
64
|
-
const [expandedHost, setExpandedHost] = useState(
|
|
64
|
+
const [expandedHost, setExpandedHost] = useState(new Set());
|
|
65
65
|
const prevStatusLabel = useRef(statusLabel);
|
|
66
66
|
|
|
67
|
+
const [hostInvocationStates, setHostInvocationStates] = useState({});
|
|
68
|
+
|
|
69
|
+
const getInvocationState = hostId =>
|
|
70
|
+
hostInvocationStates[hostId] || {
|
|
71
|
+
showOutputType: { stderr: true, stdout: true, debug: true },
|
|
72
|
+
showTemplatePreview: false,
|
|
73
|
+
showCommand: false,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const updateInvocationState = (hostId, stateKey, value) => {
|
|
77
|
+
setHostInvocationStates(prevStates => {
|
|
78
|
+
const currentHostState = getInvocationState(hostId);
|
|
79
|
+
|
|
80
|
+
const newValue =
|
|
81
|
+
typeof value === 'function' ? value(currentHostState[stateKey]) : value;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
...prevStates,
|
|
85
|
+
[hostId]: {
|
|
86
|
+
...currentHostState,
|
|
87
|
+
[stateKey]: newValue,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const isHostExpanded = hostId => expandedHost.has(hostId);
|
|
94
|
+
const setHostExpanded = (hostId, isExpanding = true) =>
|
|
95
|
+
setExpandedHost(prevExpandedSet => {
|
|
96
|
+
const newSet = new Set(prevExpandedSet);
|
|
97
|
+
if (isExpanding) {
|
|
98
|
+
newSet.add(hostId);
|
|
99
|
+
} else {
|
|
100
|
+
newSet.delete(hostId);
|
|
101
|
+
}
|
|
102
|
+
return newSet;
|
|
103
|
+
});
|
|
104
|
+
|
|
67
105
|
// Page table params
|
|
68
106
|
// Parse URL
|
|
69
107
|
const {
|
|
@@ -307,29 +345,18 @@ const JobInvocationHostTable = ({
|
|
|
307
345
|
</Tr>
|
|
308
346
|
);
|
|
309
347
|
|
|
310
|
-
const isHostExpanded = host => expandedHost.includes(host.id);
|
|
311
|
-
|
|
312
|
-
const setHostExpanded = (host, isExpanding = true) =>
|
|
313
|
-
setExpandedHost(prevExpanded => {
|
|
314
|
-
const otherExpandedHosts = prevExpanded.filter(h => h !== host.id);
|
|
315
|
-
return isExpanding
|
|
316
|
-
? [...otherExpandedHosts, host.id]
|
|
317
|
-
: otherExpandedHosts;
|
|
318
|
-
});
|
|
319
|
-
|
|
320
348
|
const pageHostIds = results.map(h => h.id);
|
|
321
349
|
|
|
322
350
|
const areAllPageRowsExpanded =
|
|
323
351
|
pageHostIds.length > 0 &&
|
|
324
|
-
pageHostIds.every(hostId => expandedHost.
|
|
352
|
+
pageHostIds.every(hostId => expandedHost.has(hostId));
|
|
325
353
|
|
|
326
354
|
const onExpandAll = () => {
|
|
327
355
|
setExpandedHost(() => {
|
|
328
356
|
if (areAllPageRowsExpanded) {
|
|
329
|
-
return
|
|
357
|
+
return new Set();
|
|
330
358
|
}
|
|
331
|
-
|
|
332
|
-
return pageHostIds;
|
|
359
|
+
return new Set(pageHostIds);
|
|
333
360
|
});
|
|
334
361
|
};
|
|
335
362
|
|
|
@@ -393,54 +420,88 @@ const JobInvocationHostTable = ({
|
|
|
393
420
|
isDeleteable={false}
|
|
394
421
|
childrenOutsideTbody
|
|
395
422
|
>
|
|
396
|
-
{results.map((result, rowIndex) =>
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
<
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
+
{results.map((result, rowIndex) => {
|
|
424
|
+
const currentInvocationState = getInvocationState(result.id);
|
|
425
|
+
return (
|
|
426
|
+
<Tbody key={result.id}>
|
|
427
|
+
<Tr ouiaId={`table-row-${result.id}`}>
|
|
428
|
+
<Td
|
|
429
|
+
expand={{
|
|
430
|
+
rowIndex,
|
|
431
|
+
isExpanded: isHostExpanded(result.id),
|
|
432
|
+
onToggle: () =>
|
|
433
|
+
setHostExpanded(result.id, !isHostExpanded(result.id)),
|
|
434
|
+
expandId: 'host-expandable',
|
|
435
|
+
}}
|
|
436
|
+
/>
|
|
437
|
+
<RowSelectTd
|
|
438
|
+
rowData={result}
|
|
439
|
+
selectOne={selectOne}
|
|
440
|
+
isSelected={isSelected}
|
|
441
|
+
/>
|
|
442
|
+
{columnNamesKeys.map(k => (
|
|
443
|
+
<Td key={k}>{columns[k].wrapper(result)}</Td>
|
|
444
|
+
))}
|
|
445
|
+
<Td isActionCell>
|
|
446
|
+
<RowActions hostID={result.id} jobID={id} />
|
|
447
|
+
</Td>
|
|
448
|
+
</Tr>
|
|
449
|
+
<Tr
|
|
450
|
+
isExpanded={isHostExpanded(result.id)}
|
|
451
|
+
ouiaId="table-row-expanded-sections"
|
|
452
|
+
className={!isHostExpanded(result.id) ? 'row-hidden' : ''}
|
|
423
453
|
>
|
|
424
|
-
<
|
|
425
|
-
{result.
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
454
|
+
<Td
|
|
455
|
+
dataLabel={`${result.id}-expandable-content`}
|
|
456
|
+
colSpan={columnNamesKeys.length + 3}
|
|
457
|
+
>
|
|
458
|
+
<ExpandableRowContent>
|
|
459
|
+
{result.job_status === 'cancelled' ||
|
|
460
|
+
result.job_status === 'N/A' ? (
|
|
461
|
+
<div>
|
|
462
|
+
{__('A task for this host has not been started')}
|
|
463
|
+
</div>
|
|
464
|
+
) : (
|
|
465
|
+
<TemplateInvocation
|
|
466
|
+
key={result.id}
|
|
467
|
+
hostID={result.id}
|
|
468
|
+
jobID={id}
|
|
469
|
+
isInTableView
|
|
470
|
+
isExpanded={isHostExpanded(result.id)}
|
|
471
|
+
showOutputType={currentInvocationState.showOutputType}
|
|
472
|
+
showTemplatePreview={
|
|
473
|
+
currentInvocationState.showTemplatePreview
|
|
474
|
+
}
|
|
475
|
+
showCommand={currentInvocationState.showCommand}
|
|
476
|
+
setShowOutputType={value =>
|
|
477
|
+
updateInvocationState(
|
|
478
|
+
result.id,
|
|
479
|
+
'showOutputType',
|
|
480
|
+
value
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
setShowTemplatePreview={value =>
|
|
484
|
+
updateInvocationState(
|
|
485
|
+
result.id,
|
|
486
|
+
'showTemplatePreview',
|
|
487
|
+
value
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
setShowCommand={value =>
|
|
491
|
+
updateInvocationState(
|
|
492
|
+
result.id,
|
|
493
|
+
'showCommand',
|
|
494
|
+
value
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
/>
|
|
498
|
+
)}
|
|
499
|
+
</ExpandableRowContent>
|
|
500
|
+
</Td>
|
|
501
|
+
</Tr>
|
|
502
|
+
</Tbody>
|
|
503
|
+
);
|
|
504
|
+
})}
|
|
444
505
|
</Table>
|
|
445
506
|
</TableIndexPage>
|
|
446
507
|
</>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
2
|
import { isEmpty } from 'lodash';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
4
|
import { ClipboardCopyButton, Alert, Skeleton } from '@patternfly/react-core';
|
|
@@ -60,6 +60,12 @@ export const TemplateInvocation = ({
|
|
|
60
60
|
isExpanded,
|
|
61
61
|
hostName,
|
|
62
62
|
hostProxy,
|
|
63
|
+
showOutputType,
|
|
64
|
+
setShowOutputType,
|
|
65
|
+
showTemplatePreview,
|
|
66
|
+
setShowTemplatePreview,
|
|
67
|
+
showCommand,
|
|
68
|
+
setShowCommand,
|
|
63
69
|
}) => {
|
|
64
70
|
const intervalRef = useRef(null);
|
|
65
71
|
const templateURL = showTemplateInvocationUrl(hostID, jobID);
|
|
@@ -74,14 +80,6 @@ export const TemplateInvocation = ({
|
|
|
74
80
|
responseRef.current = response;
|
|
75
81
|
}, [response]);
|
|
76
82
|
|
|
77
|
-
const [showOutputType, setShowOutputType] = useState({
|
|
78
|
-
stderr: true,
|
|
79
|
-
stdout: true,
|
|
80
|
-
debug: true,
|
|
81
|
-
});
|
|
82
|
-
const [showTemplatePreview, setShowTemplatePreview] = useState(false);
|
|
83
|
-
const [showCommand, setShowCommand] = useState(false);
|
|
84
|
-
|
|
85
83
|
useEffect(() => {
|
|
86
84
|
const dispatchFetch = () => {
|
|
87
85
|
dispatch(
|
|
@@ -123,12 +121,8 @@ export const TemplateInvocation = ({
|
|
|
123
121
|
};
|
|
124
122
|
}, [isExpanded, dispatch, templateURL, hostID]);
|
|
125
123
|
|
|
126
|
-
if (!
|
|
127
|
-
return
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if ((status === STATUS.PENDING && isEmpty(response)) || !response) {
|
|
131
|
-
return <Skeleton />;
|
|
124
|
+
if (!response || (status === STATUS.PENDING && isEmpty(response))) {
|
|
125
|
+
return <Skeleton data-testid="template-invocation-skeleton" />;
|
|
132
126
|
}
|
|
133
127
|
|
|
134
128
|
const errorMessage =
|
|
@@ -239,6 +233,16 @@ TemplateInvocation.propTypes = {
|
|
|
239
233
|
jobID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
240
234
|
isInTableView: PropTypes.bool,
|
|
241
235
|
isExpanded: PropTypes.bool,
|
|
236
|
+
showOutputType: PropTypes.shape({
|
|
237
|
+
stderr: PropTypes.bool,
|
|
238
|
+
stdout: PropTypes.bool,
|
|
239
|
+
debug: PropTypes.bool,
|
|
240
|
+
}).isRequired,
|
|
241
|
+
setShowOutputType: PropTypes.func.isRequired,
|
|
242
|
+
showTemplatePreview: PropTypes.bool.isRequired,
|
|
243
|
+
setShowTemplatePreview: PropTypes.func.isRequired,
|
|
244
|
+
showCommand: PropTypes.bool.isRequired,
|
|
245
|
+
setShowCommand: PropTypes.func.isRequired,
|
|
242
246
|
};
|
|
243
247
|
|
|
244
248
|
TemplateInvocation.defaultProps = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import {
|
|
4
4
|
ToggleGroup,
|
|
@@ -27,31 +27,49 @@ export const OutputToggleGroup = ({
|
|
|
27
27
|
taskCancellable,
|
|
28
28
|
permissions,
|
|
29
29
|
}) => {
|
|
30
|
-
const handleSTDERRClick =
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
const handleSTDERRClick = useCallback(
|
|
31
|
+
_isSelected => {
|
|
32
|
+
setShowOutputType(prevShowOutputType => ({
|
|
33
|
+
...prevShowOutputType,
|
|
34
|
+
stderr: _isSelected,
|
|
35
|
+
}));
|
|
36
|
+
},
|
|
37
|
+
[setShowOutputType]
|
|
38
|
+
);
|
|
36
39
|
|
|
37
|
-
const handleSTDOUTClick =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
setShowOutputType
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
40
|
+
const handleSTDOUTClick = useCallback(
|
|
41
|
+
_isSelected => {
|
|
42
|
+
setShowOutputType(prevShowOutputType => ({
|
|
43
|
+
...prevShowOutputType,
|
|
44
|
+
stdout: _isSelected,
|
|
45
|
+
}));
|
|
46
|
+
},
|
|
47
|
+
[setShowOutputType]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const handleDEBUGClick = useCallback(
|
|
51
|
+
_isSelected => {
|
|
52
|
+
setShowOutputType(prevShowOutputType => ({
|
|
53
|
+
...prevShowOutputType,
|
|
54
|
+
debug: _isSelected,
|
|
55
|
+
}));
|
|
56
|
+
},
|
|
57
|
+
[setShowOutputType]
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const handlePreviewTemplateClick = useCallback(
|
|
61
|
+
_isSelected => {
|
|
62
|
+
setShowTemplatePreview(_isSelected);
|
|
63
|
+
},
|
|
64
|
+
[setShowTemplatePreview]
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const handleCommandClick = useCallback(
|
|
68
|
+
_isSelected => {
|
|
69
|
+
setShowCommand(_isSelected);
|
|
70
|
+
},
|
|
71
|
+
[setShowCommand]
|
|
72
|
+
);
|
|
55
73
|
|
|
56
74
|
const toggleGroupItems = {
|
|
57
75
|
stderr: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { useSelector } from 'react-redux';
|
|
4
4
|
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
|
|
@@ -26,6 +26,15 @@ const TemplateInvocationPage = ({
|
|
|
26
26
|
],
|
|
27
27
|
isPf4: true,
|
|
28
28
|
};
|
|
29
|
+
|
|
30
|
+
const [showOutputType, setShowOutputType] = useState({
|
|
31
|
+
stderr: true,
|
|
32
|
+
stdout: true,
|
|
33
|
+
debug: true,
|
|
34
|
+
});
|
|
35
|
+
const [showTemplatePreview, setShowTemplatePreview] = useState(false);
|
|
36
|
+
const [showCommand, setShowCommand] = useState(false);
|
|
37
|
+
|
|
29
38
|
return (
|
|
30
39
|
<PageLayout
|
|
31
40
|
header={description}
|
|
@@ -39,6 +48,12 @@ const TemplateInvocationPage = ({
|
|
|
39
48
|
isExpanded
|
|
40
49
|
hostName={hostName}
|
|
41
50
|
hostProxy={hostProxy}
|
|
51
|
+
showOutputType={showOutputType}
|
|
52
|
+
setShowOutputType={setShowOutputType}
|
|
53
|
+
showTemplatePreview={showTemplatePreview}
|
|
54
|
+
setShowTemplatePreview={setShowTemplatePreview}
|
|
55
|
+
showCommand={showCommand}
|
|
56
|
+
setShowCommand={setShowCommand}
|
|
42
57
|
/>
|
|
43
58
|
</PageLayout>
|
|
44
59
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import configureMockStore from 'redux-mock-store';
|
|
3
3
|
import { Provider } from 'react-redux';
|
|
4
|
-
import { render, screen,
|
|
4
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
5
5
|
import '@testing-library/jest-dom/extend-expect';
|
|
6
6
|
import * as api from 'foremanReact/redux/API';
|
|
7
7
|
import * as selectors from '../JobInvocationSelectors';
|
|
@@ -10,12 +10,7 @@ import { mockTemplateInvocationResponse } from './fixtures';
|
|
|
10
10
|
|
|
11
11
|
jest.spyOn(api, 'get');
|
|
12
12
|
jest.mock('../JobInvocationSelectors');
|
|
13
|
-
|
|
14
|
-
'RESOLVED'
|
|
15
|
-
);
|
|
16
|
-
selectors.selectTemplateInvocation.mockImplementation(() => () =>
|
|
17
|
-
mockTemplateInvocationResponse
|
|
18
|
-
);
|
|
13
|
+
|
|
19
14
|
const mockStore = configureMockStore([]);
|
|
20
15
|
const store = mockStore({
|
|
21
16
|
HOSTS_API: {
|
|
@@ -24,79 +19,122 @@ const store = mockStore({
|
|
|
24
19
|
},
|
|
25
20
|
},
|
|
26
21
|
});
|
|
22
|
+
|
|
23
|
+
Object.assign(navigator, {
|
|
24
|
+
clipboard: {
|
|
25
|
+
writeText: jest.fn().mockResolvedValue(undefined),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const mockProps = {
|
|
30
|
+
hostID: '1',
|
|
31
|
+
jobID: '1',
|
|
32
|
+
isInTableView: false,
|
|
33
|
+
isExpanded: true,
|
|
34
|
+
hostName: 'example-host',
|
|
35
|
+
hostProxy: { name: 'example-proxy', href: '#' },
|
|
36
|
+
showOutputType: { stderr: true, stdout: true, debug: true },
|
|
37
|
+
setShowOutputType: jest.fn(),
|
|
38
|
+
showTemplatePreview: false,
|
|
39
|
+
setShowTemplatePreview: jest.fn(),
|
|
40
|
+
showCommand: false,
|
|
41
|
+
setShowCommand: jest.fn(),
|
|
42
|
+
};
|
|
43
|
+
|
|
27
44
|
describe('TemplateInvocation', () => {
|
|
28
|
-
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
|
|
47
|
+
'RESOLVED'
|
|
48
|
+
);
|
|
49
|
+
selectors.selectTemplateInvocation.mockImplementation(() => () =>
|
|
50
|
+
mockTemplateInvocationResponse
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('render', () => {
|
|
29
55
|
render(
|
|
30
56
|
<Provider store={store}>
|
|
31
|
-
<TemplateInvocation
|
|
32
|
-
hostID="1"
|
|
33
|
-
jobID="1"
|
|
34
|
-
isInTableView={false}
|
|
35
|
-
isExpanded
|
|
36
|
-
hostName="example-host"
|
|
37
|
-
hostProxy={{ name: 'example-proxy', href: '#' }}
|
|
38
|
-
/>
|
|
57
|
+
<TemplateInvocation {...mockProps} />
|
|
39
58
|
</Provider>
|
|
40
59
|
);
|
|
41
60
|
|
|
42
61
|
expect(screen.getByText('example-host')).toBeInTheDocument();
|
|
43
62
|
expect(screen.getByText('example-proxy')).toBeInTheDocument();
|
|
44
|
-
|
|
45
63
|
expect(screen.getByText(/using Smart Proxy/)).toBeInTheDocument();
|
|
46
64
|
expect(screen.getByText(/Target:/)).toBeInTheDocument();
|
|
47
|
-
|
|
48
65
|
expect(screen.getByText('This is red text')).toBeInTheDocument();
|
|
49
66
|
expect(screen.getByText('This is default text')).toBeInTheDocument();
|
|
67
|
+
expect(screen.getByLabelText('Copy to clipboard')).toBeInTheDocument();
|
|
50
68
|
});
|
|
51
|
-
|
|
52
|
-
|
|
69
|
+
|
|
70
|
+
test('shows "No output" message when all toggles are off', () => {
|
|
71
|
+
const { rerender } = render(
|
|
53
72
|
<Provider store={store}>
|
|
54
|
-
<TemplateInvocation
|
|
55
|
-
hostID="1"
|
|
56
|
-
jobID="1"
|
|
57
|
-
isInTableView={false}
|
|
58
|
-
isExpanded
|
|
59
|
-
hostName="example-host"
|
|
60
|
-
hostProxy={{ name: 'example-proxy', href: '#' }}
|
|
61
|
-
/>
|
|
73
|
+
<TemplateInvocation {...mockProps} />
|
|
62
74
|
</Provider>
|
|
63
75
|
);
|
|
64
76
|
|
|
65
|
-
act(() => {
|
|
66
|
-
fireEvent.click(screen.getByText('STDOUT'));
|
|
67
|
-
fireEvent.click(screen.getByText('DEBUG'));
|
|
68
|
-
fireEvent.click(screen.getByText('STDERR'));
|
|
69
|
-
});
|
|
70
77
|
expect(
|
|
71
|
-
screen.
|
|
72
|
-
).
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
screen.queryByText('No output for the selected filters')
|
|
79
|
+
).not.toBeInTheDocument();
|
|
80
|
+
|
|
81
|
+
const newProps = {
|
|
82
|
+
...mockProps,
|
|
83
|
+
showOutputType: { stderr: false, stdout: false, debug: false },
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
rerender(
|
|
87
|
+
<Provider store={store}>
|
|
88
|
+
<TemplateInvocation {...newProps} />
|
|
89
|
+
</Provider>
|
|
90
|
+
);
|
|
77
91
|
|
|
78
|
-
act(() => {
|
|
79
|
-
fireEvent.click(screen.getByText('STDOUT'));
|
|
80
|
-
});
|
|
81
92
|
expect(
|
|
82
|
-
screen.
|
|
83
|
-
).
|
|
84
|
-
|
|
93
|
+
screen.getByText('No output for the selected filters')
|
|
94
|
+
).toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('correctly filters specific output types', () => {
|
|
98
|
+
const { rerender } = render(
|
|
99
|
+
<Provider store={store}>
|
|
100
|
+
<TemplateInvocation {...mockProps} />
|
|
101
|
+
</Provider>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(screen.getByText('Exit status: 1')).toBeInTheDocument(); // stdout
|
|
85
105
|
expect(
|
|
86
|
-
screen.
|
|
87
|
-
).
|
|
106
|
+
screen.getByText('StandardError: Job execution failed')
|
|
107
|
+
).toBeInTheDocument(); // debug
|
|
88
108
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
// Turn off stdout
|
|
110
|
+
rerender(
|
|
111
|
+
<Provider store={store}>
|
|
112
|
+
<TemplateInvocation
|
|
113
|
+
{...mockProps}
|
|
114
|
+
showOutputType={{ stderr: true, stdout: false, debug: true }}
|
|
115
|
+
/>
|
|
116
|
+
</Provider>
|
|
117
|
+
);
|
|
118
|
+
expect(screen.queryByText('Exit status: 1')).not.toBeInTheDocument();
|
|
92
119
|
expect(
|
|
93
|
-
screen.
|
|
94
|
-
).
|
|
95
|
-
|
|
120
|
+
screen.getByText('StandardError: Job execution failed')
|
|
121
|
+
).toBeInTheDocument();
|
|
122
|
+
|
|
123
|
+
// Turn off debug
|
|
124
|
+
rerender(
|
|
125
|
+
<Provider store={store}>
|
|
126
|
+
<TemplateInvocation
|
|
127
|
+
{...mockProps}
|
|
128
|
+
showOutputType={{ stderr: true, stdout: false, debug: false }}
|
|
129
|
+
/>
|
|
130
|
+
</Provider>
|
|
131
|
+
);
|
|
132
|
+
expect(screen.queryByText('Exit status: 1')).not.toBeInTheDocument();
|
|
96
133
|
expect(
|
|
97
|
-
screen.
|
|
98
|
-
).
|
|
134
|
+
screen.queryByText('StandardError: Job execution failed')
|
|
135
|
+
).not.toBeInTheDocument();
|
|
99
136
|
});
|
|
137
|
+
|
|
100
138
|
test('displays an error alert when there is an error', async () => {
|
|
101
139
|
selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
|
|
102
140
|
'ERROR'
|
|
@@ -106,14 +144,7 @@ describe('TemplateInvocation', () => {
|
|
|
106
144
|
}));
|
|
107
145
|
render(
|
|
108
146
|
<Provider store={store}>
|
|
109
|
-
<TemplateInvocation
|
|
110
|
-
hostID="1"
|
|
111
|
-
jobID="1"
|
|
112
|
-
isInTableView={false}
|
|
113
|
-
isExpanded
|
|
114
|
-
hostName="example-host"
|
|
115
|
-
hostProxy={{ name: 'example-proxy', href: '#' }}
|
|
116
|
-
/>
|
|
147
|
+
<TemplateInvocation {...mockProps} />
|
|
117
148
|
</Provider>
|
|
118
149
|
);
|
|
119
150
|
|
|
@@ -129,19 +160,30 @@ describe('TemplateInvocation', () => {
|
|
|
129
160
|
selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
|
|
130
161
|
'PENDING'
|
|
131
162
|
);
|
|
132
|
-
selectors.selectTemplateInvocation.mockImplementation(() => () =>
|
|
163
|
+
selectors.selectTemplateInvocation.mockImplementation(() => () => null);
|
|
133
164
|
render(
|
|
134
165
|
<Provider store={store}>
|
|
135
|
-
<TemplateInvocation
|
|
136
|
-
hostID="1"
|
|
137
|
-
jobID="1"
|
|
138
|
-
isInTableView={false}
|
|
139
|
-
isExpanded
|
|
140
|
-
hostName="example-host"
|
|
141
|
-
/>
|
|
166
|
+
<TemplateInvocation {...mockProps} />
|
|
142
167
|
</Provider>
|
|
143
168
|
);
|
|
144
169
|
|
|
145
|
-
expect(
|
|
170
|
+
expect(
|
|
171
|
+
screen.getByTestId('template-invocation-skeleton')
|
|
172
|
+
).toBeInTheDocument();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('copies text to clipboard when clicked', async () => {
|
|
176
|
+
render(
|
|
177
|
+
<Provider store={store}>
|
|
178
|
+
<TemplateInvocation {...mockProps} />
|
|
179
|
+
</Provider>
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const copyButton = screen.getByLabelText('Copy to clipboard');
|
|
183
|
+
fireEvent.click(copyButton);
|
|
184
|
+
expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1);
|
|
185
|
+
expect(
|
|
186
|
+
await screen.findByText('Successfully copied to clipboard!')
|
|
187
|
+
).toBeInTheDocument();
|
|
146
188
|
});
|
|
147
189
|
});
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foreman_remote_execution
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 16.3.
|
|
4
|
+
version: 16.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Foreman Remote Execution team
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2025-
|
|
10
|
+
date: 2025-12-08 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: deface
|