foreman_remote_execution 16.0.5 → 16.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 +33 -6
- data/app/helpers/remote_execution_helper.rb +1 -1
- data/config/routes.rb +3 -2
- data/lib/foreman_remote_execution/plugin.rb +0 -6
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/webpack/JobInvocationDetail/CheckboxesActions.js +176 -45
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +2 -3
- data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +3 -1
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +2 -4
- data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +28 -26
- data/webpack/JobInvocationDetail/index.js +11 -12
- data/webpack/Routes/routes.js +1 -1
- 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: 8150a0940b1cf9352e24f99f5083c36fb7bf42aefdfd99406eaeb191c109ec55
|
4
|
+
data.tar.gz: 673400df79bdee44cb8eccbcb72ce654963dca2f1e94f015d8ffcbdcb625a710
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f812d26367d5f24535531150d58aa224967f47db0651b6de2bb267ba5d037608665ec380e5b3d96003796a81f3b9f4c376d2e7de4365bdca4719ea012828a98b
|
7
|
+
data.tar.gz: 102ef1de3d1f874fb70ac04cde6fb1f91ecb4980198c25d29afb23a9e3f3e28a2b98ec5e1bbb69a7271e3639a31277e4dd1828848c322fc0989e69c4e263c17a
|
@@ -134,16 +134,43 @@ module Api
|
|
134
134
|
render :json => host_output(@nested_obj, @host, :raw => true)
|
135
135
|
end
|
136
136
|
|
137
|
-
api :POST, '/job_invocations/:id/cancel', N_('Cancel job invocation')
|
137
|
+
api :POST, '/job_invocations/:id/cancel', N_('Cancel job invocation or matching tasks only')
|
138
138
|
param :id, :identifier, :required => true
|
139
139
|
param :force, :bool
|
140
|
+
param :search, String, :desc => N_('Search query to cancel tasks only on matching hosts. If not provided, the whole job invocation will be cancelled.')
|
140
141
|
def cancel
|
141
|
-
|
142
|
-
|
143
|
-
|
142
|
+
force = params.fetch('force', false)
|
143
|
+
search = params[:search]
|
144
|
+
|
145
|
+
if search.present?
|
146
|
+
begin
|
147
|
+
hosts_scope = @job_invocation.targeting.hosts.authorized(:view_hosts, Host)
|
148
|
+
matching_hosts = hosts_scope.search_for(search)
|
149
|
+
tasks_to_process = @job_invocation.template_invocations
|
150
|
+
.where(host_id: matching_hosts.select(:id))
|
151
|
+
.includes(:run_host_job_task)
|
152
|
+
|
153
|
+
cancelled, skipped = tasks_to_process.partition { |ti| ti.run_host_job_task&.cancellable? }
|
154
|
+
cancelled.each do |ti|
|
155
|
+
if force
|
156
|
+
ti.run_host_job_task.abort
|
157
|
+
else
|
158
|
+
ti.run_host_job_task.cancel
|
159
|
+
end
|
160
|
+
end
|
161
|
+
render json: {
|
162
|
+
total: tasks_to_process.size,
|
163
|
+
cancelled: cancelled.map(&:run_host_job_task_id),
|
164
|
+
skipped: skipped.map(&:run_host_job_task_id),
|
165
|
+
}
|
166
|
+
rescue StandardError => e
|
167
|
+
render json: { error: { message: "Failed to cancel tasks on hosts: #{e.message}" } }, status: :unprocessable_entity
|
168
|
+
end
|
169
|
+
elsif @job_invocation.task.cancellable?
|
170
|
+
result = @job_invocation.cancel(force)
|
171
|
+
render json: { cancelled: result, id: @job_invocation.id }
|
144
172
|
else
|
145
|
-
render :
|
146
|
-
:status => :unprocessable_entity
|
173
|
+
render json: { message: _('The job could not be cancelled.') }, status: :unprocessable_entity
|
147
174
|
end
|
148
175
|
end
|
149
176
|
|
@@ -99,7 +99,7 @@ module RemoteExecutionHelper
|
|
99
99
|
:disabled => !task.cancellable?,
|
100
100
|
:method => :post)
|
101
101
|
end
|
102
|
-
buttons << link_to(_('New UI'),
|
102
|
+
buttons << link_to(_('New UI'), job_invocation_path(:id => job_invocation.id),
|
103
103
|
class: 'btn btn-default',
|
104
104
|
title: _('Switch to the new job invocation detail UI'))
|
105
105
|
|
data/config/routes.rb
CHANGED
@@ -22,11 +22,12 @@ Rails.application.routes.draw do
|
|
22
22
|
match 'job_invocations/:id/rerun', to: 'react#index', :via => [:get], as: 'rerun_job_invocation'
|
23
23
|
match 'old/job_invocations/new', to: 'job_invocations#new', via: [:get], as: 'form_new_job_invocation'
|
24
24
|
match 'old/job_invocations/:id/rerun', to: 'job_invocations#rerun', via: [:get, :post], as: 'form_rerun_job_invocation'
|
25
|
-
|
25
|
+
get 'job_invocations/:id', to: 'react#index', as: 'job_invocation'
|
26
|
+
get 'legacy/job_invocations/:id', to: 'job_invocations#show', as: 'legacy_job_invocation'
|
26
27
|
match 'job_invocations_detail/:id/host_invocation/:host_id', to: 'react#index', :via => [:get], as: 'new_job_invocation_detail_by_host'
|
27
28
|
get 'show_template_invocation_by_host/:host_id/job_invocation/:id', to: 'template_invocations#show_template_invocation_by_host'
|
28
29
|
|
29
|
-
resources :job_invocations, :only => [:create, :
|
30
|
+
resources :job_invocations, :only => [:create, :index] do
|
30
31
|
collection do
|
31
32
|
get 'preview_job_invocations_per_host'
|
32
33
|
post 'refresh'
|
@@ -195,12 +195,6 @@ Foreman::Plugin.register :foreman_remote_execution do
|
|
195
195
|
parent: :monitor_menu,
|
196
196
|
after: :audits
|
197
197
|
|
198
|
-
menu :labs_menu, :job_invocations_detail,
|
199
|
-
url_hash: { controller: :job_invocations, action: :show },
|
200
|
-
caption: N_('Job invocations detail'),
|
201
|
-
parent: :lab_features_menu,
|
202
|
-
url: '/experimental/job_invocations_detail/1'
|
203
|
-
|
204
198
|
register_custom_status HostStatus::ExecutionStatus
|
205
199
|
# add dashboard widget
|
206
200
|
# widget 'foreman_remote_execution_widget', name: N_('Foreman plugin template widget'), sizex: 4, sizey: 1
|
@@ -1,6 +1,7 @@
|
|
1
|
-
/* eslint-disable
|
1
|
+
/* eslint-disable max-lines */
|
2
2
|
import {
|
3
3
|
Button,
|
4
|
+
Divider,
|
4
5
|
Dropdown,
|
5
6
|
DropdownItem,
|
6
7
|
DropdownList,
|
@@ -11,20 +12,97 @@ import {
|
|
11
12
|
EllipsisVIcon,
|
12
13
|
OutlinedWindowRestoreIcon,
|
13
14
|
} from '@patternfly/react-icons';
|
14
|
-
import
|
15
|
+
import axios from 'axios';
|
15
16
|
import { foremanUrl } from 'foremanReact/common/helpers';
|
16
17
|
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
|
18
|
+
import { translate as __, sprintf } from 'foremanReact/common/I18n';
|
19
|
+
import { addToast } from 'foremanReact/components/ToastsList';
|
17
20
|
import PropTypes from 'prop-types';
|
18
21
|
import React, { useState } from 'react';
|
19
|
-
import { useSelector } from 'react-redux';
|
22
|
+
import { useDispatch, useSelector } from 'react-redux';
|
23
|
+
|
20
24
|
import {
|
21
|
-
templateInvocationPageUrl,
|
22
|
-
MAX_HOSTS_API_SIZE,
|
23
25
|
DIRECT_OPEN_HOST_LIMIT,
|
26
|
+
MAX_HOSTS_API_SIZE,
|
27
|
+
templateInvocationPageUrl,
|
24
28
|
} from './JobInvocationConstants';
|
25
|
-
import {
|
29
|
+
import {
|
30
|
+
selectHasPermission,
|
31
|
+
selectItems,
|
32
|
+
selectTaskCancelable,
|
33
|
+
} from './JobInvocationSelectors';
|
26
34
|
import OpenAllInvocationsModal, { PopupAlert } from './OpenAllInvocationsModal';
|
27
35
|
|
36
|
+
/* eslint-disable camelcase */
|
37
|
+
const ActionsKebab = ({
|
38
|
+
selectedIds,
|
39
|
+
failedCount,
|
40
|
+
isTaskCancelable,
|
41
|
+
hasCancelPermission,
|
42
|
+
handleTaskAction,
|
43
|
+
handleOpenHosts,
|
44
|
+
isDropdownOpen,
|
45
|
+
setIsDropdownOpen,
|
46
|
+
}) => {
|
47
|
+
const dropdownItems = [
|
48
|
+
<DropdownItem
|
49
|
+
ouiaId="cancel-host-dropdown-item"
|
50
|
+
onClick={() => handleTaskAction('cancel')}
|
51
|
+
key="cancel"
|
52
|
+
component="button"
|
53
|
+
isDisabled={
|
54
|
+
selectedIds.length === 0 || !isTaskCancelable || !hasCancelPermission
|
55
|
+
}
|
56
|
+
>
|
57
|
+
{__('Cancel selected')}
|
58
|
+
</DropdownItem>,
|
59
|
+
<DropdownItem
|
60
|
+
ouiaId="abort-host-dropdown-item"
|
61
|
+
onClick={() => handleTaskAction('abort')}
|
62
|
+
key="abort"
|
63
|
+
component="button"
|
64
|
+
isDisabled={
|
65
|
+
selectedIds.length === 0 || !isTaskCancelable || !hasCancelPermission
|
66
|
+
}
|
67
|
+
>
|
68
|
+
{__('Abort selected')}
|
69
|
+
</DropdownItem>,
|
70
|
+
<Divider component="li" key="separator" />,
|
71
|
+
<DropdownItem
|
72
|
+
ouiaId="open-failed-dropdown-item"
|
73
|
+
key="open-failed"
|
74
|
+
onClick={() => handleOpenHosts('failed')}
|
75
|
+
isDisabled={failedCount === 0}
|
76
|
+
>
|
77
|
+
{sprintf(__('Open all failed runs (%s)'), failedCount)}
|
78
|
+
</DropdownItem>,
|
79
|
+
];
|
80
|
+
|
81
|
+
return (
|
82
|
+
<Dropdown
|
83
|
+
isOpen={isDropdownOpen}
|
84
|
+
onOpenChange={setIsDropdownOpen}
|
85
|
+
onSelect={() => setIsDropdownOpen(false)}
|
86
|
+
ouiaId="actions-kebab"
|
87
|
+
shouldFocusToggleOnSelect
|
88
|
+
toggle={toggleRef => (
|
89
|
+
<MenuToggle
|
90
|
+
aria-label="actions dropdown toggle"
|
91
|
+
id="toggle-kebab"
|
92
|
+
isExpanded={isDropdownOpen}
|
93
|
+
onClick={() => setIsDropdownOpen(prev => !prev)}
|
94
|
+
ref={toggleRef}
|
95
|
+
variant="plain"
|
96
|
+
>
|
97
|
+
<EllipsisVIcon />
|
98
|
+
</MenuToggle>
|
99
|
+
)}
|
100
|
+
>
|
101
|
+
<DropdownList>{dropdownItems}</DropdownList>
|
102
|
+
</Dropdown>
|
103
|
+
);
|
104
|
+
};
|
105
|
+
|
28
106
|
export const CheckboxesActions = ({
|
29
107
|
selectedIds,
|
30
108
|
failedCount,
|
@@ -35,9 +113,16 @@ export const CheckboxesActions = ({
|
|
35
113
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
36
114
|
const [showAlert, setShowAlert] = useState(false);
|
37
115
|
const [isOpenFailed, setIsOpenFailed] = useState(false);
|
38
|
-
const
|
116
|
+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
117
|
+
const isTaskCancelable = useSelector(selectTaskCancelable);
|
118
|
+
const dispatch = useDispatch();
|
119
|
+
|
120
|
+
const hasCreatePermission = useSelector(
|
39
121
|
selectHasPermission('create_job_invocations')
|
40
122
|
);
|
123
|
+
const hasCancelPermission = useSelector(
|
124
|
+
selectHasPermission('cancel_job_invocations')
|
125
|
+
);
|
41
126
|
const jobSearchQuery = useSelector(selectItems)?.targeting?.search_query;
|
42
127
|
const filterQuery =
|
43
128
|
filter && filter !== 'all_statuses'
|
@@ -89,43 +174,64 @@ export const CheckboxesActions = ({
|
|
89
174
|
setIsModalOpen(true);
|
90
175
|
};
|
91
176
|
|
92
|
-
const
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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>
|
177
|
+
const cancelJobTasks = (search, action) => async () => {
|
178
|
+
dispatch(
|
179
|
+
addToast({
|
180
|
+
key: `cancel-job-info`,
|
181
|
+
type: 'info',
|
182
|
+
message: sprintf(__('Trying to %s the task'), action),
|
183
|
+
})
|
128
184
|
);
|
185
|
+
|
186
|
+
try {
|
187
|
+
const response = await axios.post(
|
188
|
+
`/api/v2/job_invocations/${jobID}/cancel`,
|
189
|
+
{
|
190
|
+
search,
|
191
|
+
force: action !== 'cancel',
|
192
|
+
}
|
193
|
+
);
|
194
|
+
|
195
|
+
const cancelledTasks = response.data?.cancelled;
|
196
|
+
const pastTenseAction =
|
197
|
+
action === 'cancel' ? __('cancelled') : __('aborted');
|
198
|
+
|
199
|
+
if (cancelledTasks && cancelledTasks.length > 0) {
|
200
|
+
const idList = cancelledTasks.join(', ');
|
201
|
+
dispatch(
|
202
|
+
addToast({
|
203
|
+
key: `success-tasks-cancelled`,
|
204
|
+
type: 'success',
|
205
|
+
message: sprintf(
|
206
|
+
__('%s task(s) successfully %s: %s'),
|
207
|
+
cancelledTasks.length,
|
208
|
+
pastTenseAction,
|
209
|
+
idList
|
210
|
+
),
|
211
|
+
})
|
212
|
+
);
|
213
|
+
} else {
|
214
|
+
dispatch(
|
215
|
+
addToast({
|
216
|
+
key: `warn-no-tasks-cancelled-${Date.now()}`,
|
217
|
+
type: 'warning',
|
218
|
+
message: sprintf(__('Task(s) were not %s'), pastTenseAction),
|
219
|
+
})
|
220
|
+
);
|
221
|
+
}
|
222
|
+
} catch (error) {
|
223
|
+
dispatch(
|
224
|
+
addToast({
|
225
|
+
key: `error-cancelling-tasks`,
|
226
|
+
type: 'danger',
|
227
|
+
message: error.response?.data?.error || __('An error occurred.'),
|
228
|
+
})
|
229
|
+
);
|
230
|
+
}
|
231
|
+
};
|
232
|
+
|
233
|
+
const handleTaskAction = action => {
|
234
|
+
dispatch(cancelJobTasks(combinedQuery, action));
|
129
235
|
};
|
130
236
|
|
131
237
|
const OpenAllButton = () => (
|
@@ -153,7 +259,7 @@ export const CheckboxesActions = ({
|
|
153
259
|
`/job_invocations/${jobID}/rerun?search=(${jobSearchQuery}) AND (${combinedQuery})`
|
154
260
|
)}
|
155
261
|
// eslint-disable-next-line camelcase
|
156
|
-
isDisabled={selectedIds.length === 0 || !
|
262
|
+
isDisabled={selectedIds.length === 0 || !hasCreatePermission}
|
157
263
|
isInline
|
158
264
|
ouiaId="template-invocation-rerun-selected-button"
|
159
265
|
variant="secondary"
|
@@ -166,7 +272,16 @@ export const CheckboxesActions = ({
|
|
166
272
|
<>
|
167
273
|
<OpenAllButton />
|
168
274
|
<RerunSelectedButton />
|
169
|
-
<ActionsKebab
|
275
|
+
<ActionsKebab
|
276
|
+
selectedIds={selectedIds}
|
277
|
+
failedCount={failedCount}
|
278
|
+
isTaskCancelable={isTaskCancelable}
|
279
|
+
hasCancelPermission={hasCancelPermission}
|
280
|
+
handleTaskAction={handleTaskAction}
|
281
|
+
handleOpenHosts={handleOpenHosts}
|
282
|
+
isDropdownOpen={isDropdownOpen}
|
283
|
+
setIsDropdownOpen={setIsDropdownOpen}
|
284
|
+
/>
|
170
285
|
{showAlert && <PopupAlert setShowAlert={setShowAlert} />}
|
171
286
|
<OpenAllInvocationsModal
|
172
287
|
isOpen={isModalOpen}
|
@@ -182,6 +297,22 @@ export const CheckboxesActions = ({
|
|
182
297
|
);
|
183
298
|
};
|
184
299
|
|
300
|
+
ActionsKebab.propTypes = {
|
301
|
+
selectedIds: PropTypes.array.isRequired,
|
302
|
+
failedCount: PropTypes.number.isRequired,
|
303
|
+
isTaskCancelable: PropTypes.bool,
|
304
|
+
hasCancelPermission: PropTypes.bool,
|
305
|
+
handleTaskAction: PropTypes.func.isRequired,
|
306
|
+
handleOpenHosts: PropTypes.func.isRequired,
|
307
|
+
isDropdownOpen: PropTypes.bool.isRequired,
|
308
|
+
setIsDropdownOpen: PropTypes.func.isRequired,
|
309
|
+
};
|
310
|
+
|
311
|
+
ActionsKebab.defaultProps = {
|
312
|
+
isTaskCancelable: false,
|
313
|
+
hasCancelPermission: false,
|
314
|
+
};
|
315
|
+
|
185
316
|
CheckboxesActions.propTypes = {
|
186
317
|
selectedIds: PropTypes.array.isRequired,
|
187
318
|
failedCount: PropTypes.number.isRequired,
|
@@ -1,7 +1,7 @@
|
|
1
1
|
/* eslint-disable camelcase */
|
2
2
|
import React from 'react';
|
3
|
-
import { foremanUrl } from 'foremanReact/common/helpers';
|
4
3
|
import { translate as __ } from 'foremanReact/common/I18n';
|
4
|
+
import { foremanUrl } from 'foremanReact/common/helpers';
|
5
5
|
import { useForemanHostDetailsPageUrl } from 'foremanReact/Root/Context/ForemanContext';
|
6
6
|
import JobStatusIcon from '../react_app/components/RecentJobsCard/JobStatusIcon';
|
7
7
|
|
@@ -31,8 +31,7 @@ export const LIST_TEMPLATE_INVOCATIONS = 'LIST_TEMPLATE_INVOCATIONS';
|
|
31
31
|
export const templateInvocationPageUrl = (hostID, jobID) =>
|
32
32
|
`/job_invocations_detail/${jobID}/host_invocation/${hostID}`;
|
33
33
|
|
34
|
-
export const jobInvocationDetailsUrl = id =>
|
35
|
-
`/experimental/job_invocations_detail/${id}`;
|
34
|
+
export const jobInvocationDetailsUrl = id => `/job_invocations/${id}`;
|
36
35
|
|
37
36
|
export const STATUS = {
|
38
37
|
PENDING: 'pending',
|
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|
2
2
|
import React, { useEffect, useState } from 'react';
|
3
3
|
import { useDispatch, useSelector } from 'react-redux';
|
4
4
|
import { Button, Split, SplitItem } from '@patternfly/react-core';
|
5
|
+
import { UndoIcon } from '@patternfly/react-icons';
|
5
6
|
import {
|
6
7
|
Dropdown,
|
7
8
|
DropdownItem,
|
@@ -189,7 +190,8 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
|
|
189
190
|
<DropdownSeparator ouiaId="dropdown-separator-2" key="separator-2" />,
|
190
191
|
<DropdownItem
|
191
192
|
ouiaId="legacy-ui-dropdown-item"
|
192
|
-
|
193
|
+
icon={<UndoIcon />}
|
194
|
+
href={`/legacy/job_invocations/${jobId}`}
|
193
195
|
key="legacy-ui"
|
194
196
|
>
|
195
197
|
{__('Legacy UI')}
|
@@ -96,9 +96,7 @@ describe('JobInvocationDetailPage', () => {
|
|
96
96
|
);
|
97
97
|
|
98
98
|
expect(screen.getByText('Description')).toBeInTheDocument();
|
99
|
-
expect(
|
100
|
-
container.querySelector('.chart-donut .pf-v5-c-chart')
|
101
|
-
).toBeInTheDocument();
|
99
|
+
expect(container.querySelector('.chart-donut')).toBeInTheDocument();
|
102
100
|
expect(screen.getByText('2/6')).toBeInTheDocument();
|
103
101
|
expect(screen.getByText('Systems')).toBeInTheDocument();
|
104
102
|
expect(screen.getByText('System status')).toBeInTheDocument();
|
@@ -178,7 +176,7 @@ describe('JobInvocationDetailPage', () => {
|
|
178
176
|
.getByText('Legacy UI')
|
179
177
|
.closest('a')
|
180
178
|
.getAttribute('href')
|
181
|
-
).toEqual(`/job_invocations/${jobId}`);
|
179
|
+
).toEqual(`/legacy/job_invocations/${jobId}`);
|
182
180
|
});
|
183
181
|
|
184
182
|
it('shows scheduled date', async () => {
|
@@ -1,16 +1,20 @@
|
|
1
|
+
import '@testing-library/jest-dom/extend-expect';
|
2
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
3
|
+
import axios from 'axios';
|
4
|
+
import { foremanUrl } from 'foremanReact/common/helpers';
|
5
|
+
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
|
1
6
|
import React from 'react';
|
2
7
|
import { Provider } from 'react-redux';
|
3
8
|
import configureStore from 'redux-mock-store';
|
4
|
-
|
5
|
-
import '@testing-library/jest-dom/extend-expect';
|
6
|
-
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
|
9
|
+
|
7
10
|
import { CheckboxesActions } from '../CheckboxesActions';
|
8
11
|
import * as selectors from '../JobInvocationSelectors';
|
9
12
|
import { PopupAlert } from '../OpenAllInvocationsModal';
|
10
13
|
|
14
|
+
jest.mock('axios');
|
11
15
|
jest.mock('foremanReact/common/hooks/API/APIHooks');
|
12
|
-
|
13
16
|
jest.mock('../JobInvocationSelectors');
|
17
|
+
|
14
18
|
jest.mock('../JobInvocationConstants', () => ({
|
15
19
|
...jest.requireActual('../JobInvocationConstants'),
|
16
20
|
templateInvocationPageUrl: jest.fn(
|
@@ -18,18 +22,15 @@ jest.mock('../JobInvocationConstants', () => ({
|
|
18
22
|
),
|
19
23
|
DIRECT_OPEN_HOST_LIMIT: 3,
|
20
24
|
}));
|
25
|
+
|
21
26
|
selectors.selectItems.mockImplementation(() => ({
|
22
27
|
targeting: { search_query: 'name~*' },
|
23
28
|
}));
|
24
|
-
selectors.selectHasPermission.mockImplementation(() => () =>
|
29
|
+
selectors.selectHasPermission.mockImplementation(() => () => true);
|
30
|
+
selectors.selectTaskCancelable.mockImplementation(() => true);
|
31
|
+
|
25
32
|
const mockStore = configureStore([]);
|
26
|
-
const store = mockStore({
|
27
|
-
templateInvocation: {
|
28
|
-
permissions: {
|
29
|
-
execute_jobs: true,
|
30
|
-
},
|
31
|
-
},
|
32
|
-
});
|
33
|
+
const store = mockStore({});
|
33
34
|
|
34
35
|
describe('TableToolbarActions', () => {
|
35
36
|
const jobID = '42';
|
@@ -38,6 +39,7 @@ describe('TableToolbarActions', () => {
|
|
38
39
|
beforeEach(() => {
|
39
40
|
openSpy = jest.spyOn(window, 'open').mockImplementation(jest.fn());
|
40
41
|
useAPI.mockClear();
|
42
|
+
axios.post.mockResolvedValue({ data: {} });
|
41
43
|
useAPI.mockReturnValue({
|
42
44
|
response: null,
|
43
45
|
status: 'initial',
|
@@ -68,7 +70,7 @@ describe('TableToolbarActions', () => {
|
|
68
70
|
expect(openSpy).toHaveBeenCalledTimes(selectedIds.length);
|
69
71
|
});
|
70
72
|
|
71
|
-
test('shows modal when results length is greater than 3', () => {
|
73
|
+
test('shows modal when results length is greater than 3', async () => {
|
72
74
|
const selectedIds = [1, 2, 3, 4];
|
73
75
|
render(
|
74
76
|
<Provider store={store}>
|
@@ -83,13 +85,13 @@ describe('TableToolbarActions', () => {
|
|
83
85
|
screen.getByLabelText(/open all template invocations in new tab/i)
|
84
86
|
);
|
85
87
|
expect(
|
86
|
-
screen.
|
87
|
-
name: /open all
|
88
|
+
await screen.findByRole('heading', {
|
89
|
+
name: /open all.*invocations in new tabs \+ selected/i,
|
88
90
|
})
|
89
91
|
).toBeInTheDocument();
|
90
92
|
});
|
91
93
|
|
92
|
-
test('shows alert when popups are blocked', () => {
|
94
|
+
test('shows alert when popups are blocked', async () => {
|
93
95
|
openSpy.mockReturnValue(null);
|
94
96
|
const selectedIds = [1, 2];
|
95
97
|
render(
|
@@ -105,7 +107,7 @@ describe('TableToolbarActions', () => {
|
|
105
107
|
screen.getByLabelText(/open all template invocations in new tab/i)
|
106
108
|
);
|
107
109
|
expect(
|
108
|
-
screen.
|
110
|
+
await screen.findByText(/Popups are blocked by your browser/)
|
109
111
|
).toBeInTheDocument();
|
110
112
|
});
|
111
113
|
});
|
@@ -123,23 +125,23 @@ describe('TableToolbarActions', () => {
|
|
123
125
|
</Provider>
|
124
126
|
);
|
125
127
|
fireEvent.click(screen.getByLabelText(/actions dropdown toggle/i));
|
126
|
-
fireEvent.click(screen.
|
128
|
+
fireEvent.click(await screen.findByText(/open all failed runs/i));
|
127
129
|
await waitFor(() => {
|
128
130
|
expect(openSpy).toHaveBeenCalledTimes(failedHosts.length);
|
129
131
|
});
|
130
132
|
});
|
131
133
|
|
132
|
-
test('shows modal when results length is greater than 3', () => {
|
134
|
+
test('shows modal when results length is greater than 3', async () => {
|
133
135
|
render(
|
134
136
|
<Provider store={store}>
|
135
137
|
<CheckboxesActions selectedIds={[]} failedCount={4} jobID={jobID} />
|
136
138
|
</Provider>
|
137
139
|
);
|
138
140
|
fireEvent.click(screen.getByLabelText(/actions dropdown toggle/i));
|
139
|
-
fireEvent.click(screen.
|
141
|
+
fireEvent.click(await screen.findByText(/open all failed runs/i));
|
140
142
|
expect(
|
141
|
-
screen.
|
142
|
-
name: /open all
|
143
|
+
await screen.findByRole('heading', {
|
144
|
+
name: /open all.*invocations in new tabs \+ failed/i,
|
143
145
|
})
|
144
146
|
).toBeInTheDocument();
|
145
147
|
});
|
@@ -152,7 +154,7 @@ describe('TableToolbarActions', () => {
|
|
152
154
|
);
|
153
155
|
expect(useAPI).toHaveBeenCalledWith(
|
154
156
|
'get',
|
155
|
-
|
157
|
+
foremanUrl(`/api/job_invocations/${jobID}/hosts`),
|
156
158
|
expect.objectContaining({
|
157
159
|
skip: true,
|
158
160
|
})
|
@@ -193,9 +195,9 @@ describe('TableToolbarActions', () => {
|
|
193
195
|
);
|
194
196
|
const rerunLink = screen.getByRole('link', { name: /rerun/i });
|
195
197
|
expect(rerunLink).toBeEnabled();
|
196
|
-
const
|
197
|
-
|
198
|
-
|
198
|
+
const expectedHref = foremanUrl(
|
199
|
+
`/job_invocations/42/rerun?search=(name~*) AND ((id ^ (101, 102, 103)))`
|
200
|
+
);
|
199
201
|
expect(rerunLink).toHaveAttribute('href', expectedHref);
|
200
202
|
});
|
201
203
|
});
|
@@ -5,32 +5,31 @@ import {
|
|
5
5
|
PageSectionVariants,
|
6
6
|
Skeleton,
|
7
7
|
} from '@patternfly/react-core';
|
8
|
-
import {
|
8
|
+
import React, { useEffect, useState } from 'react';
|
9
9
|
import { translate as __, documentLocale } from 'foremanReact/common/I18n';
|
10
|
-
import {
|
10
|
+
import { useDispatch, useSelector } from 'react-redux';
|
11
11
|
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
|
12
|
-
import SkeletonLoader from 'foremanReact/components/common/SkeletonLoader';
|
13
12
|
import PropTypes from 'prop-types';
|
14
|
-
import
|
15
|
-
import {
|
13
|
+
import SkeletonLoader from 'foremanReact/components/common/SkeletonLoader';
|
14
|
+
import { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
|
15
|
+
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
|
16
16
|
|
17
17
|
import { JobAdditionInfo } from './JobAdditionInfo';
|
18
|
+
import JobInvocationHostTable from './JobInvocationHostTable';
|
19
|
+
import JobInvocationOverview from './JobInvocationOverview';
|
20
|
+
import JobInvocationSystemStatusChart from './JobInvocationSystemStatusChart';
|
21
|
+
import JobInvocationToolbarButtons from './JobInvocationToolbarButtons';
|
18
22
|
import { getJobInvocation, getTask } from './JobInvocationActions';
|
23
|
+
import './JobInvocationDetail.scss';
|
19
24
|
import {
|
20
25
|
CURRENT_PERMISSIONS,
|
21
|
-
currentPermissionsUrl,
|
22
26
|
DATE_OPTIONS,
|
23
27
|
JOB_INVOCATION_KEY,
|
24
28
|
STATUS,
|
25
29
|
STATUS_UPPERCASE,
|
30
|
+
currentPermissionsUrl,
|
26
31
|
} from './JobInvocationConstants';
|
27
|
-
import JobInvocationHostTable from './JobInvocationHostTable';
|
28
|
-
import JobInvocationOverview from './JobInvocationOverview';
|
29
32
|
import { selectItems } from './JobInvocationSelectors';
|
30
|
-
import JobInvocationSystemStatusChart from './JobInvocationSystemStatusChart';
|
31
|
-
import JobInvocationToolbarButtons from './JobInvocationToolbarButtons';
|
32
|
-
|
33
|
-
import './JobInvocationDetail.scss';
|
34
33
|
|
35
34
|
const JobInvocationDetailPage = ({
|
36
35
|
match: {
|
data/webpack/Routes/routes.js
CHANGED
@@ -16,7 +16,7 @@ const ForemanREXRoutes = [
|
|
16
16
|
render: props => <JobWizardPageRerun {...props} />,
|
17
17
|
},
|
18
18
|
{
|
19
|
-
path: '/
|
19
|
+
path: '/job_invocations/:id',
|
20
20
|
exact: true,
|
21
21
|
render: props => <JobInvocationDetailPage {...props} />,
|
22
22
|
},
|
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.0
|
4
|
+
version: 16.1.0
|
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-08-05 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: deface
|