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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/job_invocations_controller.rb +28 -1
  3. data/app/controllers/template_invocations_controller.rb +1 -1
  4. data/config/routes.rb +1 -0
  5. data/lib/foreman_remote_execution/engine.rb +9 -256
  6. data/lib/foreman_remote_execution/plugin.rb +246 -0
  7. data/lib/foreman_remote_execution/version.rb +1 -1
  8. data/test/functional/api/v2/job_invocations_controller_test.rb +1 -1
  9. data/test/unit/job_invocation_report_template_test.rb +6 -6
  10. data/webpack/JobInvocationDetail/CheckboxesActions.js +196 -0
  11. data/webpack/JobInvocationDetail/{JobInvocationHostTableToolbar.js → DropdownFilter.js} +3 -6
  12. data/webpack/JobInvocationDetail/JobInvocationConstants.js +6 -6
  13. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +18 -0
  14. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +205 -89
  15. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +30 -3
  16. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +20 -27
  17. data/webpack/JobInvocationDetail/OpenAllInvocationsModal.js +118 -0
  18. data/webpack/JobInvocationDetail/TemplateInvocation.js +54 -24
  19. data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +8 -10
  20. data/webpack/JobInvocationDetail/TemplateInvocationPage.js +1 -1
  21. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +1 -1
  22. data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +202 -0
  23. data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +34 -28
  24. data/webpack/JobInvocationDetail/index.js +61 -30
  25. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +26 -7
  26. data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +1 -1
  27. metadata +8 -6
  28. data/webpack/JobInvocationDetail/OpenAlInvocations.js +0 -111
  29. 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 'error', row['stderr']
54
- assert_equal 'output', row['stdout']
55
- assert_equal 'debug', row['debug']
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 JobInvocationHostTableToolbar = ({
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
- JobInvocationHostTableToolbar.propTypes = {
60
+ DropdownFilter.propTypes = {
64
61
  dropdownFilter: PropTypes.string.isRequired,
65
62
  setDropdownFilter: PropTypes.func.isRequired,
66
63
  };
67
64
 
68
- export default JobInvocationHostTableToolbar;
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
- export const GET_TEMPLATE_INVOCATION = 'GET_TEMPLATE_INVOCATION';
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
+ }