foreman_remote_execution 16.0.4 → 16.0.5
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/job_invocations_controller.rb +28 -1
- data/app/controllers/template_invocations_controller.rb +1 -1
- data/config/routes.rb +1 -0
- data/lib/foreman_remote_execution/engine.rb +9 -256
- data/lib/foreman_remote_execution/plugin.rb +246 -0
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/functional/api/v2/job_invocations_controller_test.rb +1 -1
- data/test/unit/job_invocation_report_template_test.rb +6 -6
- data/webpack/JobInvocationDetail/CheckboxesActions.js +196 -0
- data/webpack/JobInvocationDetail/{JobInvocationHostTableToolbar.js → DropdownFilter.js} +3 -6
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +6 -6
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +18 -0
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +205 -89
- data/webpack/JobInvocationDetail/JobInvocationSelectors.js +30 -3
- data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +20 -27
- data/webpack/JobInvocationDetail/OpenAllInvocationsModal.js +118 -0
- data/webpack/JobInvocationDetail/TemplateInvocation.js +54 -24
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +8 -10
- data/webpack/JobInvocationDetail/TemplateInvocationPage.js +1 -1
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +1 -1
- data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +202 -0
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +34 -28
- data/webpack/JobInvocationDetail/index.js +61 -30
- data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +26 -7
- data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +1 -1
- metadata +8 -6
- data/webpack/JobInvocationDetail/OpenAlInvocations.js +0 -111
- data/webpack/JobInvocationDetail/__tests__/OpenAlInvocations.test.js +0 -110
@@ -28,9 +28,9 @@ class JobReportTemplateTest < ActiveSupport::TestCase
|
|
28
28
|
describe 'task reporting' do
|
29
29
|
let(:fake_outputs) do
|
30
30
|
[
|
31
|
-
{ 'output_type' => 'stderr', 'output' => "error" },
|
32
|
-
{ 'output_type' => 'stdout', 'output' => "output" },
|
33
|
-
{ 'output_type' => 'debug', 'output' => "debug" },
|
31
|
+
{ 'output_type' => 'stderr', 'output' => "error\n" },
|
32
|
+
{ 'output_type' => 'stdout', 'output' => "output\n" },
|
33
|
+
{ 'output_type' => 'debug', 'output' => "debug\n" },
|
34
34
|
]
|
35
35
|
end
|
36
36
|
let(:fake_task) { FakeTask.new(result: 'success', action_continuous_output: fake_outputs, :ended_at => Time.new(2020, 12, 1, 0, 0, 0).utc) }
|
@@ -50,9 +50,9 @@ class JobReportTemplateTest < ActiveSupport::TestCase
|
|
50
50
|
row = rows.first
|
51
51
|
assert_equal host.name, row['Host']
|
52
52
|
assert_equal 'success', row['Result']
|
53
|
-
assert_equal
|
54
|
-
assert_equal
|
55
|
-
assert_equal
|
53
|
+
assert_equal "error\n", row['stderr']
|
54
|
+
assert_equal "output\n", row['stdout']
|
55
|
+
assert_equal "debug\n", row['debug']
|
56
56
|
assert_kind_of Time, Time.zone.parse(row['Finished']), 'Parsing of time column failed'
|
57
57
|
end
|
58
58
|
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
/* eslint-disable camelcase */
|
2
|
+
import {
|
3
|
+
Button,
|
4
|
+
Dropdown,
|
5
|
+
DropdownItem,
|
6
|
+
DropdownList,
|
7
|
+
MenuToggle,
|
8
|
+
Tooltip,
|
9
|
+
} from '@patternfly/react-core';
|
10
|
+
import {
|
11
|
+
EllipsisVIcon,
|
12
|
+
OutlinedWindowRestoreIcon,
|
13
|
+
} from '@patternfly/react-icons';
|
14
|
+
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
15
|
+
import { foremanUrl } from 'foremanReact/common/helpers';
|
16
|
+
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
|
17
|
+
import PropTypes from 'prop-types';
|
18
|
+
import React, { useState } from 'react';
|
19
|
+
import { useSelector } from 'react-redux';
|
20
|
+
import {
|
21
|
+
templateInvocationPageUrl,
|
22
|
+
MAX_HOSTS_API_SIZE,
|
23
|
+
DIRECT_OPEN_HOST_LIMIT,
|
24
|
+
} from './JobInvocationConstants';
|
25
|
+
import { selectHasPermission, selectItems } from './JobInvocationSelectors';
|
26
|
+
import OpenAllInvocationsModal, { PopupAlert } from './OpenAllInvocationsModal';
|
27
|
+
|
28
|
+
export const CheckboxesActions = ({
|
29
|
+
selectedIds,
|
30
|
+
failedCount,
|
31
|
+
jobID,
|
32
|
+
filter,
|
33
|
+
bulkParams,
|
34
|
+
}) => {
|
35
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
36
|
+
const [showAlert, setShowAlert] = useState(false);
|
37
|
+
const [isOpenFailed, setIsOpenFailed] = useState(false);
|
38
|
+
const hasPermission = useSelector(
|
39
|
+
selectHasPermission('create_job_invocations')
|
40
|
+
);
|
41
|
+
const jobSearchQuery = useSelector(selectItems)?.targeting?.search_query;
|
42
|
+
const filterQuery =
|
43
|
+
filter && filter !== 'all_statuses'
|
44
|
+
? ` and job_invocation.result = ${filter}`
|
45
|
+
: '';
|
46
|
+
const combinedQuery = `${bulkParams}${filterQuery}`;
|
47
|
+
|
48
|
+
const { response: failedHostsData } = useAPI(
|
49
|
+
'get',
|
50
|
+
foremanUrl(`/api/job_invocations/${jobID}/hosts`),
|
51
|
+
{
|
52
|
+
params: {
|
53
|
+
per_page: MAX_HOSTS_API_SIZE,
|
54
|
+
search: `job_invocation.result = failed`,
|
55
|
+
},
|
56
|
+
skip: failedCount === 0,
|
57
|
+
}
|
58
|
+
);
|
59
|
+
|
60
|
+
const failedHosts = failedHostsData?.results || [];
|
61
|
+
|
62
|
+
const openLink = url => {
|
63
|
+
const newWin = window.open(url);
|
64
|
+
|
65
|
+
if (!newWin || newWin.closed || typeof newWin.closed === 'undefined') {
|
66
|
+
setShowAlert(true);
|
67
|
+
}
|
68
|
+
};
|
69
|
+
|
70
|
+
const handleOpenHosts = async (type = 'all') => {
|
71
|
+
if (type === 'failed') {
|
72
|
+
if (failedCount <= DIRECT_OPEN_HOST_LIMIT) {
|
73
|
+
failedHosts.forEach(host =>
|
74
|
+
openLink(templateInvocationPageUrl(host.id, jobID))
|
75
|
+
);
|
76
|
+
return;
|
77
|
+
}
|
78
|
+
setIsOpenFailed(true);
|
79
|
+
setIsModalOpen(true);
|
80
|
+
return;
|
81
|
+
}
|
82
|
+
|
83
|
+
if (selectedIds.length <= DIRECT_OPEN_HOST_LIMIT) {
|
84
|
+
selectedIds.forEach(id => openLink(templateInvocationPageUrl(id, jobID)));
|
85
|
+
return;
|
86
|
+
}
|
87
|
+
|
88
|
+
setIsOpenFailed(false);
|
89
|
+
setIsModalOpen(true);
|
90
|
+
};
|
91
|
+
|
92
|
+
const ActionsKebab = () => {
|
93
|
+
const [isDropdownOpen, setIsDropdownOpen] = React.useState(false);
|
94
|
+
|
95
|
+
const dropdownItems = [
|
96
|
+
<DropdownItem
|
97
|
+
ouiaId="open-failed-dropdown-item"
|
98
|
+
key="open-failed"
|
99
|
+
onClick={() => handleOpenHosts('failed')}
|
100
|
+
isDisabled={failedCount === 0}
|
101
|
+
>
|
102
|
+
{sprintf(__('Open all failed runs (%s)'), failedCount)}
|
103
|
+
</DropdownItem>,
|
104
|
+
];
|
105
|
+
|
106
|
+
return (
|
107
|
+
<Dropdown
|
108
|
+
isOpen={isDropdownOpen}
|
109
|
+
onOpenChange={setIsDropdownOpen}
|
110
|
+
onSelect={() => setIsDropdownOpen(false)}
|
111
|
+
ouiaId="actions-kebab"
|
112
|
+
shouldFocusToggleOnSelect
|
113
|
+
toggle={toggleRef => (
|
114
|
+
<MenuToggle
|
115
|
+
aria-label="actions dropdown toggle"
|
116
|
+
id="toggle-kebab"
|
117
|
+
isExpanded={isDropdownOpen}
|
118
|
+
onClick={() => setIsDropdownOpen(prev => !prev)}
|
119
|
+
ref={toggleRef}
|
120
|
+
variant="plain"
|
121
|
+
>
|
122
|
+
<EllipsisVIcon />
|
123
|
+
</MenuToggle>
|
124
|
+
)}
|
125
|
+
>
|
126
|
+
<DropdownList>{dropdownItems}</DropdownList>
|
127
|
+
</Dropdown>
|
128
|
+
);
|
129
|
+
};
|
130
|
+
|
131
|
+
const OpenAllButton = () => (
|
132
|
+
<Button
|
133
|
+
aria-label="open all template invocations in new tab"
|
134
|
+
className="open-all-button"
|
135
|
+
isDisabled={selectedIds.length === 0}
|
136
|
+
isInline
|
137
|
+
onClick={() => handleOpenHosts('all')}
|
138
|
+
ouiaId="template-invocation-new-tab-button"
|
139
|
+
variant="link"
|
140
|
+
>
|
141
|
+
<Tooltip content={__('Open selected in new tab')}>
|
142
|
+
<OutlinedWindowRestoreIcon />
|
143
|
+
</Tooltip>
|
144
|
+
</Button>
|
145
|
+
);
|
146
|
+
|
147
|
+
const RerunSelectedButton = () => (
|
148
|
+
<Button
|
149
|
+
aria-label="rerun selected template invocations"
|
150
|
+
className="rerun-selected-button"
|
151
|
+
component="a"
|
152
|
+
href={foremanUrl(
|
153
|
+
`/job_invocations/${jobID}/rerun?search=(${jobSearchQuery}) AND (${combinedQuery})`
|
154
|
+
)}
|
155
|
+
// eslint-disable-next-line camelcase
|
156
|
+
isDisabled={selectedIds.length === 0 || !hasPermission}
|
157
|
+
isInline
|
158
|
+
ouiaId="template-invocation-rerun-selected-button"
|
159
|
+
variant="secondary"
|
160
|
+
>
|
161
|
+
{__('Rerun')}
|
162
|
+
</Button>
|
163
|
+
);
|
164
|
+
|
165
|
+
return (
|
166
|
+
<>
|
167
|
+
<OpenAllButton />
|
168
|
+
<RerunSelectedButton />
|
169
|
+
<ActionsKebab />
|
170
|
+
{showAlert && <PopupAlert setShowAlert={setShowAlert} />}
|
171
|
+
<OpenAllInvocationsModal
|
172
|
+
isOpen={isModalOpen}
|
173
|
+
onClose={() => setIsModalOpen(false)}
|
174
|
+
failedCount={failedCount}
|
175
|
+
failedHosts={failedHosts}
|
176
|
+
jobID={jobID}
|
177
|
+
isOpenFailed={isOpenFailed}
|
178
|
+
setShowAlert={setShowAlert}
|
179
|
+
selectedIds={selectedIds}
|
180
|
+
/>
|
181
|
+
</>
|
182
|
+
);
|
183
|
+
};
|
184
|
+
|
185
|
+
CheckboxesActions.propTypes = {
|
186
|
+
selectedIds: PropTypes.array.isRequired,
|
187
|
+
failedCount: PropTypes.number.isRequired,
|
188
|
+
jobID: PropTypes.string.isRequired,
|
189
|
+
bulkParams: PropTypes.string,
|
190
|
+
filter: PropTypes.string,
|
191
|
+
};
|
192
|
+
|
193
|
+
CheckboxesActions.defaultProps = {
|
194
|
+
bulkParams: '',
|
195
|
+
filter: '',
|
196
|
+
};
|
@@ -10,10 +10,7 @@ import {
|
|
10
10
|
} from '@patternfly/react-core';
|
11
11
|
import { STATUS_TITLES } from './JobInvocationConstants';
|
12
12
|
|
13
|
-
const
|
14
|
-
dropdownFilter,
|
15
|
-
setDropdownFilter,
|
16
|
-
}) => {
|
13
|
+
const DropdownFilter = ({ dropdownFilter, setDropdownFilter }) => {
|
17
14
|
const [isOpen, setIsOpen] = React.useState(false);
|
18
15
|
const onSelect = (_event, itemId) => {
|
19
16
|
setDropdownFilter(itemId);
|
@@ -60,9 +57,9 @@ const JobInvocationHostTableToolbar = ({
|
|
60
57
|
);
|
61
58
|
};
|
62
59
|
|
63
|
-
|
60
|
+
DropdownFilter.propTypes = {
|
64
61
|
dropdownFilter: PropTypes.string.isRequired,
|
65
62
|
setDropdownFilter: PropTypes.func.isRequired,
|
66
63
|
};
|
67
64
|
|
68
|
-
export default
|
65
|
+
export default DropdownFilter;
|
@@ -16,12 +16,17 @@ export const CANCEL_RECURRING_LOGIC = 'CANCEL_RECURRING_LOGIC';
|
|
16
16
|
export const GET_REPORT_TEMPLATES = 'GET_REPORT_TEMPLATES';
|
17
17
|
export const GET_REPORT_TEMPLATE_INPUTS = 'GET_REPORT_TEMPLATE_INPUTS';
|
18
18
|
export const JOB_INVOCATION_HOSTS = 'JOB_INVOCATION_HOSTS';
|
19
|
+
export const GET_TEMPLATE_INVOCATION = 'GET_TEMPLATE_INVOCATION';
|
20
|
+
export const MAX_HOSTS_API_SIZE = 100;
|
21
|
+
export const DIRECT_OPEN_HOST_LIMIT = 3;
|
22
|
+
export const ALL_JOB_HOSTS = 'ALL_JOB_HOSTS';
|
19
23
|
export const currentPermissionsUrl = foremanUrl(
|
20
24
|
'/api/v2/permissions/current_permissions'
|
21
25
|
);
|
22
|
-
|
26
|
+
|
23
27
|
export const showTemplateInvocationUrl = (hostID, jobID) =>
|
24
28
|
`/show_template_invocation_by_host/${hostID}/job_invocation/${jobID}`;
|
29
|
+
export const LIST_TEMPLATE_INVOCATIONS = 'LIST_TEMPLATE_INVOCATIONS';
|
25
30
|
|
26
31
|
export const templateInvocationPageUrl = (hostID, jobID) =>
|
27
32
|
`/job_invocations_detail/${jobID}/host_invocation/${hostID}`;
|
@@ -135,11 +140,6 @@ const Columns = () => {
|
|
135
140
|
},
|
136
141
|
weight: 5,
|
137
142
|
},
|
138
|
-
actions: {
|
139
|
-
title: ' ',
|
140
|
-
weight: 6,
|
141
|
-
wrapper: () => null,
|
142
|
-
},
|
143
143
|
};
|
144
144
|
};
|
145
145
|
|
@@ -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
|
+
}
|