foreman_remote_execution 13.1.1 → 13.2.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 +1 -0
- data/app/models/job_invocation.rb +23 -0
- data/app/models/job_invocation_composer.rb +2 -0
- data/app/views/api/v2/job_invocations/base.json.rabl +3 -2
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +2 -1
- data/webpack/JobInvocationDetail/JobInvocationActions.js +134 -3
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +14 -0
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +5 -2
- data/webpack/JobInvocationDetail/JobInvocationSelectors.js +5 -0
- data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +13 -9
- data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +268 -0
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +259 -0
- data/webpack/JobInvocationDetail/__tests__/fixtures.js +117 -0
- data/webpack/JobInvocationDetail/index.js +58 -38
- data/webpack/JobWizard/JobWizardPageRerun.js +15 -10
- data/webpack/__mocks__/foremanReact/components/BreadcrumbBar/index.js +4 -0
- data/webpack/__mocks__/foremanReact/components/Head/index.js +10 -0
- data/webpack/__mocks__/foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState.js +8 -0
- data/webpack/__mocks__/foremanReact/components/ToastsList/index.js +3 -0
- data/webpack/__mocks__/foremanReact/redux/API/APIActions.js +21 -0
- data/webpack/__mocks__/foremanReact/redux/API/APIConstants.js +7 -0
- data/webpack/__mocks__/foremanReact/redux/API/index.js +14 -0
- data/webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware/index.js +9 -0
- metadata +12 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ab4f560e39c672bfce2319a0487db47a3f772a11142591262727b5a16a7a0573
|
4
|
+
data.tar.gz: 6dab6dc9a69cc43df9a13cc0173651e37639fd29f38b1153e6541567da6ff01c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0226bedd8d571be5c687abf520499cc26b94981980f6924b51e34a8de5ff300106b8d750474961da5f4270eaaa773cde04fcfe1d8b5050772fb6efada15b5f63
|
7
|
+
data.tar.gz: 37147e8f31b7a3050f883d24783e2aa10ed0e782c97badeabb511164990c6fe95b65caaf0d2290216c4d9f5616fcc2bcd2538f90dfcb21bad0526c8c24cd65f2
|
@@ -139,6 +139,7 @@ module Api
|
|
139
139
|
api :POST, '/job_invocations/:id/rerun', N_('Rerun job on failed hosts')
|
140
140
|
param :id, :identifier, :required => true
|
141
141
|
param :failed_only, :bool
|
142
|
+
param :succeeded_only, :bool
|
142
143
|
def rerun
|
143
144
|
composer = JobInvocationComposer.from_job_invocation(@job_invocation, params)
|
144
145
|
if composer.rerun_possible?
|
@@ -194,6 +194,10 @@ class JobInvocation < ApplicationRecord
|
|
194
194
|
failed_hosts.pluck(:id)
|
195
195
|
end
|
196
196
|
|
197
|
+
def succeeded_host_ids
|
198
|
+
succeeded_hosts.pluck(:id)
|
199
|
+
end
|
200
|
+
|
197
201
|
def failed_hosts
|
198
202
|
base = targeting.hosts
|
199
203
|
if finished?
|
@@ -203,6 +207,15 @@ class JobInvocation < ApplicationRecord
|
|
203
207
|
end
|
204
208
|
end
|
205
209
|
|
210
|
+
def succeeded_hosts
|
211
|
+
base = targeting.hosts
|
212
|
+
if finished?
|
213
|
+
base.where.not(:id => not_succeeded_template_invocations.select(:host_id))
|
214
|
+
else
|
215
|
+
base.where(:id => succeeded_template_invocations.select(:host_id))
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
206
219
|
def total_hosts_count
|
207
220
|
count = _('N/A')
|
208
221
|
|
@@ -290,4 +303,14 @@ class JobInvocation < ApplicationRecord
|
|
290
303
|
results = [:cancelled, :failed].map { |state| TemplateInvocation::TaskResultMap.status_to_task_result(state) }.flatten
|
291
304
|
template_invocations.joins(:run_host_job_task).where.not(ForemanTasks::Task.table_name => { :result => results })
|
292
305
|
end
|
306
|
+
|
307
|
+
def succeeded_template_invocations
|
308
|
+
result = TemplateInvocation::TaskResultMap.status_to_task_result(:success)
|
309
|
+
template_invocations.joins(:run_host_job_task).where(ForemanTasks::Task.table_name => { :result => result })
|
310
|
+
end
|
311
|
+
|
312
|
+
def not_succeeded_template_invocations
|
313
|
+
result = TemplateInvocation::TaskResultMap.status_to_task_result(:success)
|
314
|
+
template_invocations.joins(:run_host_job_task).where.not(ForemanTasks::Task.table_name => { :result => result })
|
315
|
+
end
|
293
316
|
end
|
@@ -25,9 +25,10 @@ end
|
|
25
25
|
if params.key?(:include_permissions)
|
26
26
|
node :permissions do |invocation|
|
27
27
|
authorizer = Authorizer.new(User.current)
|
28
|
-
edit_job_templates_permission = Permission.where(name: "edit_job_templates", resource_type: "JobTemplate").first
|
29
28
|
{
|
30
|
-
"edit_job_templates" =>
|
29
|
+
"edit_job_templates" => authorizer.can?("edit_job_templates", invocation, false),
|
30
|
+
"view_foreman_tasks" => authorizer.can?("view_foreman_tasks", invocation.task, false),
|
31
|
+
"edit_recurring_logics" => authorizer.can?("edit_recurring_logics", invocation.recurring_logic, false),
|
31
32
|
}
|
32
33
|
end
|
33
34
|
end
|
data/package.json
CHANGED
@@ -1,9 +1,19 @@
|
|
1
|
-
import {
|
1
|
+
import { translate as __, sprintf } from 'foremanReact/common/I18n';
|
2
|
+
import { foremanUrl } from 'foremanReact/common/helpers';
|
3
|
+
import { addToast } from 'foremanReact/components/ToastsList';
|
4
|
+
import { APIActions, get } from 'foremanReact/redux/API';
|
2
5
|
import {
|
3
|
-
withInterval,
|
4
6
|
stopInterval,
|
7
|
+
withInterval,
|
5
8
|
} from 'foremanReact/redux/middlewares/IntervalMiddleware';
|
6
|
-
import {
|
9
|
+
import {
|
10
|
+
CANCEL_JOB,
|
11
|
+
CANCEL_RECURRING_LOGIC,
|
12
|
+
CHANGE_ENABLED_RECURRING_LOGIC,
|
13
|
+
GET_TASK,
|
14
|
+
JOB_INVOCATION_KEY,
|
15
|
+
UPDATE_JOB,
|
16
|
+
} from './JobInvocationConstants';
|
7
17
|
|
8
18
|
export const getData = url => dispatch => {
|
9
19
|
const fetchData = withInterval(
|
@@ -20,3 +30,124 @@ export const getData = url => dispatch => {
|
|
20
30
|
|
21
31
|
dispatch(fetchData);
|
22
32
|
};
|
33
|
+
|
34
|
+
export const updateJob = jobId => dispatch => {
|
35
|
+
const url = foremanUrl(`/api/job_invocations/${jobId}`);
|
36
|
+
dispatch(
|
37
|
+
APIActions.get({
|
38
|
+
url,
|
39
|
+
key: UPDATE_JOB,
|
40
|
+
})
|
41
|
+
);
|
42
|
+
};
|
43
|
+
|
44
|
+
export const cancelJob = (jobId, force) => dispatch => {
|
45
|
+
const infoToast = () =>
|
46
|
+
force
|
47
|
+
? sprintf(__('Trying to abort the job %s.'), jobId)
|
48
|
+
: sprintf(__('Trying to cancel the job %s.'), jobId);
|
49
|
+
const errorToast = response =>
|
50
|
+
force
|
51
|
+
? sprintf(__(`Could not abort the job %s: ${response}`), jobId)
|
52
|
+
: sprintf(__(`Could not cancel the job %s: ${response}`), jobId);
|
53
|
+
const url = force
|
54
|
+
? `/job_invocations/${jobId}/cancel?force=true`
|
55
|
+
: `/job_invocations/${jobId}/cancel`;
|
56
|
+
|
57
|
+
dispatch(
|
58
|
+
APIActions.post({
|
59
|
+
url,
|
60
|
+
key: CANCEL_JOB,
|
61
|
+
errorToast: ({ response }) =>
|
62
|
+
errorToast(
|
63
|
+
// eslint-disable-next-line camelcase
|
64
|
+
response?.data?.error?.full_messages ||
|
65
|
+
response?.data?.error?.message ||
|
66
|
+
'Unknown error.'
|
67
|
+
),
|
68
|
+
handleSuccess: () => {
|
69
|
+
dispatch(
|
70
|
+
addToast({
|
71
|
+
key: `cancel-job-error`,
|
72
|
+
type: 'info',
|
73
|
+
message: infoToast(),
|
74
|
+
})
|
75
|
+
);
|
76
|
+
dispatch(updateJob(jobId));
|
77
|
+
},
|
78
|
+
})
|
79
|
+
);
|
80
|
+
};
|
81
|
+
|
82
|
+
export const getTask = taskId => dispatch => {
|
83
|
+
dispatch(
|
84
|
+
get({
|
85
|
+
key: GET_TASK,
|
86
|
+
url: `/foreman_tasks/api/tasks/${taskId}`,
|
87
|
+
})
|
88
|
+
);
|
89
|
+
};
|
90
|
+
|
91
|
+
export const enableRecurringLogic = (
|
92
|
+
recurrenceId,
|
93
|
+
enabled,
|
94
|
+
jobId
|
95
|
+
) => dispatch => {
|
96
|
+
const successToast = () =>
|
97
|
+
enabled
|
98
|
+
? sprintf(__('Recurring logic %s disabled successfully.'), recurrenceId)
|
99
|
+
: sprintf(__('Recurring logic %s enabled successfully.'), recurrenceId);
|
100
|
+
const errorToast = response =>
|
101
|
+
enabled
|
102
|
+
? sprintf(
|
103
|
+
__(`Could not disable recurring logic %s: ${response}`),
|
104
|
+
recurrenceId
|
105
|
+
)
|
106
|
+
: sprintf(
|
107
|
+
__(`Could not enable recurring logic %s: ${response}`),
|
108
|
+
recurrenceId
|
109
|
+
);
|
110
|
+
const url = `/foreman_tasks/api/recurring_logics/${recurrenceId}`;
|
111
|
+
dispatch(
|
112
|
+
APIActions.put({
|
113
|
+
url,
|
114
|
+
key: CHANGE_ENABLED_RECURRING_LOGIC,
|
115
|
+
params: { recurring_logic: { enabled: !enabled } },
|
116
|
+
successToast,
|
117
|
+
errorToast: ({ response }) =>
|
118
|
+
errorToast(
|
119
|
+
// eslint-disable-next-line camelcase
|
120
|
+
response?.data?.error?.full_messages ||
|
121
|
+
response?.data?.error?.message ||
|
122
|
+
'Unknown error.'
|
123
|
+
),
|
124
|
+
handleSuccess: () => dispatch(updateJob(jobId)),
|
125
|
+
})
|
126
|
+
);
|
127
|
+
};
|
128
|
+
|
129
|
+
export const cancelRecurringLogic = (recurrenceId, jobId) => dispatch => {
|
130
|
+
const successToast = () =>
|
131
|
+
sprintf(__('Recurring logic %s cancelled successfully.'), recurrenceId);
|
132
|
+
const errorToast = response =>
|
133
|
+
sprintf(
|
134
|
+
__(`Could not cancel recurring logic %s: ${response}`),
|
135
|
+
recurrenceId
|
136
|
+
);
|
137
|
+
const url = `/foreman_tasks/recurring_logics/${recurrenceId}/cancel`;
|
138
|
+
dispatch(
|
139
|
+
APIActions.post({
|
140
|
+
url,
|
141
|
+
key: CANCEL_RECURRING_LOGIC,
|
142
|
+
successToast,
|
143
|
+
errorToast: ({ response }) =>
|
144
|
+
errorToast(
|
145
|
+
// eslint-disable-next-line camelcase
|
146
|
+
response?.data?.error?.full_messages ||
|
147
|
+
response?.data?.error?.message ||
|
148
|
+
'Unknown error.'
|
149
|
+
),
|
150
|
+
handleSuccess: () => dispatch(updateJob(jobId)),
|
151
|
+
})
|
152
|
+
);
|
153
|
+
};
|
@@ -1,9 +1,23 @@
|
|
1
|
+
import { foremanUrl } from 'foremanReact/common/helpers';
|
2
|
+
|
1
3
|
export const JOB_INVOCATION_KEY = 'JOB_INVOCATION_KEY';
|
4
|
+
export const CURRENT_PERMISSIONS = 'CURRENT_PERMISSIONS';
|
5
|
+
export const UPDATE_JOB = 'UPDATE_JOB';
|
6
|
+
export const CANCEL_JOB = 'CANCEL_JOB';
|
7
|
+
export const GET_TASK = 'GET_TASK';
|
8
|
+
export const CHANGE_ENABLED_RECURRING_LOGIC = 'CHANGE_ENABLED_RECURRING_LOGIC';
|
9
|
+
export const CANCEL_RECURRING_LOGIC = 'CANCEL_RECURRING_LOGIC';
|
10
|
+
export const GET_REPORT_TEMPLATES = 'GET_REPORT_TEMPLATES';
|
11
|
+
export const GET_REPORT_TEMPLATE_INPUTS = 'GET_REPORT_TEMPLATE_INPUTS';
|
12
|
+
export const currentPermissionsUrl = foremanUrl(
|
13
|
+
'/api/v2/permissions/current_permissions'
|
14
|
+
);
|
2
15
|
|
3
16
|
export const STATUS = {
|
4
17
|
PENDING: 'pending',
|
5
18
|
SUCCEEDED: 'succeeded',
|
6
19
|
FAILED: 'failed',
|
20
|
+
CANCELLED: 'cancelled',
|
7
21
|
};
|
8
22
|
|
9
23
|
export const DATE_OPTIONS = {
|
@@ -1,6 +1,8 @@
|
|
1
|
-
.job-invocation-detail-
|
1
|
+
.job-invocation-detail-flex {
|
2
2
|
$chart_size: 105px;
|
3
|
-
|
3
|
+
padding-top: 0px;
|
4
|
+
padding-left: 10px;
|
5
|
+
|
4
6
|
.chart-donut {
|
5
7
|
height: $chart_size;
|
6
8
|
width: $chart_size;
|
@@ -36,3 +38,4 @@
|
|
36
38
|
height: $chart_size;
|
37
39
|
}
|
38
40
|
}
|
41
|
+
|
@@ -3,3 +3,8 @@ import { JOB_INVOCATION_KEY } from './JobInvocationConstants';
|
|
3
3
|
|
4
4
|
export const selectItems = state =>
|
5
5
|
selectAPIResponse(state, JOB_INVOCATION_KEY);
|
6
|
+
|
7
|
+
export const selectTask = state => selectAPIResponse(state, 'GET_TASK');
|
8
|
+
|
9
|
+
export const selectTaskCancelable = state =>
|
10
|
+
selectTask(state).available_actions?.cancellable || false;
|
@@ -1,6 +1,7 @@
|
|
1
|
-
import React, { useEffect, useState } from 'react';
|
2
1
|
import PropTypes from 'prop-types';
|
2
|
+
import React, { useEffect, useState } from 'react';
|
3
3
|
import { translate as __, sprintf } from 'foremanReact/common/I18n';
|
4
|
+
import DefaultLoaderEmptyState from 'foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState';
|
4
5
|
import {
|
5
6
|
ChartDonut,
|
6
7
|
ChartLabel,
|
@@ -9,20 +10,19 @@ import {
|
|
9
10
|
} from '@patternfly/react-charts';
|
10
11
|
import {
|
11
12
|
DescriptionList,
|
12
|
-
DescriptionListTerm,
|
13
|
-
DescriptionListGroup,
|
14
13
|
DescriptionListDescription,
|
14
|
+
DescriptionListGroup,
|
15
|
+
DescriptionListTerm,
|
15
16
|
FlexItem,
|
16
17
|
Text,
|
17
18
|
} from '@patternfly/react-core';
|
18
19
|
import {
|
19
|
-
global_palette_green_500 as successedColor,
|
20
|
-
global_palette_red_100 as failedColor,
|
21
|
-
global_palette_blue_300 as inProgressColor,
|
22
20
|
global_palette_black_600 as canceledColor,
|
23
21
|
global_palette_black_500 as emptyChartDonut,
|
22
|
+
global_palette_red_100 as failedColor,
|
23
|
+
global_palette_blue_300 as inProgressColor,
|
24
|
+
global_palette_green_500 as successedColor,
|
24
25
|
} from '@patternfly/react-tokens';
|
25
|
-
import DefaultLoaderEmptyState from 'foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState';
|
26
26
|
import './JobInvocationDetail.scss';
|
27
27
|
|
28
28
|
const JobInvocationSystemStatusChart = ({
|
@@ -35,9 +35,9 @@ const JobInvocationSystemStatusChart = ({
|
|
35
35
|
failed,
|
36
36
|
pending,
|
37
37
|
cancelled,
|
38
|
-
total,
|
39
38
|
total_hosts: totalHosts, // includes scheduled
|
40
39
|
} = data;
|
40
|
+
const total = succeeded + failed + pending + cancelled;
|
41
41
|
const chartData = [
|
42
42
|
{ title: __('Succeeded:'), count: succeeded, color: successedColor.value },
|
43
43
|
{ title: __('Failed:'), count: failed, color: failedColor.value },
|
@@ -82,7 +82,11 @@ const JobInvocationSystemStatusChart = ({
|
|
82
82
|
total > 0 ? chartData.map(d => d.color) : [emptyChartDonut.value]
|
83
83
|
}
|
84
84
|
labelComponent={
|
85
|
-
<ChartTooltip
|
85
|
+
<ChartTooltip
|
86
|
+
pointerLength={0}
|
87
|
+
constrainToVisibleArea
|
88
|
+
renderInPortal={false}
|
89
|
+
/>
|
86
90
|
}
|
87
91
|
title={chartDonutTitle}
|
88
92
|
titleComponent={
|
@@ -0,0 +1,268 @@
|
|
1
|
+
import PropTypes from 'prop-types';
|
2
|
+
import React, { useEffect, useState } from 'react';
|
3
|
+
import { useDispatch, useSelector } from 'react-redux';
|
4
|
+
import {
|
5
|
+
Button,
|
6
|
+
Dropdown,
|
7
|
+
DropdownItem,
|
8
|
+
DropdownPosition,
|
9
|
+
DropdownSeparator,
|
10
|
+
DropdownToggle,
|
11
|
+
Split,
|
12
|
+
SplitItem,
|
13
|
+
} from '@patternfly/react-core';
|
14
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
15
|
+
import { foremanUrl } from 'foremanReact/common/helpers';
|
16
|
+
import { STATUS as APIStatus } from 'foremanReact/constants';
|
17
|
+
import { get } from 'foremanReact/redux/API';
|
18
|
+
import {
|
19
|
+
cancelJob,
|
20
|
+
cancelRecurringLogic,
|
21
|
+
enableRecurringLogic,
|
22
|
+
} from './JobInvocationActions';
|
23
|
+
import {
|
24
|
+
STATUS,
|
25
|
+
GET_REPORT_TEMPLATES,
|
26
|
+
GET_REPORT_TEMPLATE_INPUTS,
|
27
|
+
} from './JobInvocationConstants';
|
28
|
+
import { selectTaskCancelable } from './JobInvocationSelectors';
|
29
|
+
|
30
|
+
const JobInvocationToolbarButtons = ({
|
31
|
+
jobId,
|
32
|
+
data,
|
33
|
+
currentPermissions,
|
34
|
+
permissionsStatus,
|
35
|
+
}) => {
|
36
|
+
const { succeeded, failed, task, recurrence, permissions } = data;
|
37
|
+
const recurringEnabled = recurrence?.state === 'active';
|
38
|
+
const canViewForemanTasks = permissions
|
39
|
+
? permissions.view_foreman_tasks
|
40
|
+
: false;
|
41
|
+
const canEditRecurringLogic = permissions
|
42
|
+
? permissions.edit_recurring_logics
|
43
|
+
: false;
|
44
|
+
const isTaskCancelable = useSelector(selectTaskCancelable);
|
45
|
+
const [isActionOpen, setIsActionOpen] = useState(false);
|
46
|
+
const [reportTemplateJobId, setReportTemplateJobId] = useState(undefined);
|
47
|
+
const [templateInputId, setTemplateInputId] = useState(undefined);
|
48
|
+
const queryParams = new URLSearchParams({
|
49
|
+
[`report_template_report[input_values][${templateInputId}][value]`]: jobId,
|
50
|
+
});
|
51
|
+
const dispatch = useDispatch();
|
52
|
+
|
53
|
+
const onActionFocus = () => {
|
54
|
+
const element = document.getElementById(
|
55
|
+
`toggle-split-button-action-primary-${jobId}`
|
56
|
+
);
|
57
|
+
element.focus();
|
58
|
+
};
|
59
|
+
const onActionSelect = () => {
|
60
|
+
setIsActionOpen(false);
|
61
|
+
onActionFocus();
|
62
|
+
};
|
63
|
+
const hasPermission = permissionRequired =>
|
64
|
+
permissionsStatus === APIStatus.RESOLVED
|
65
|
+
? currentPermissions?.some(
|
66
|
+
permission => permission.name === permissionRequired
|
67
|
+
)
|
68
|
+
: false;
|
69
|
+
|
70
|
+
useEffect(() => {
|
71
|
+
dispatch(
|
72
|
+
get({
|
73
|
+
key: GET_REPORT_TEMPLATES,
|
74
|
+
url: '/api/report_templates',
|
75
|
+
handleSuccess: ({ data: { results } }) => {
|
76
|
+
setReportTemplateJobId(
|
77
|
+
results.find(result => result.name === 'Job - Invocation Report')
|
78
|
+
?.id
|
79
|
+
);
|
80
|
+
},
|
81
|
+
handleError: () => {
|
82
|
+
setReportTemplateJobId(undefined);
|
83
|
+
},
|
84
|
+
})
|
85
|
+
);
|
86
|
+
}, [dispatch]);
|
87
|
+
useEffect(() => {
|
88
|
+
if (reportTemplateJobId !== undefined) {
|
89
|
+
dispatch(
|
90
|
+
get({
|
91
|
+
key: GET_REPORT_TEMPLATE_INPUTS,
|
92
|
+
url: `/api/templates/${reportTemplateJobId}/template_inputs`,
|
93
|
+
handleSuccess: ({ data: { results } }) => {
|
94
|
+
setTemplateInputId(
|
95
|
+
results.find(result => result.name === 'job_id')?.id
|
96
|
+
);
|
97
|
+
},
|
98
|
+
handleError: () => {
|
99
|
+
setTemplateInputId(undefined);
|
100
|
+
},
|
101
|
+
})
|
102
|
+
);
|
103
|
+
}
|
104
|
+
}, [dispatch, reportTemplateJobId]);
|
105
|
+
|
106
|
+
const recurrenceDropdownItems = recurrence
|
107
|
+
? [
|
108
|
+
<DropdownSeparator ouiaId="dropdown-separator-1" key="separator-1" />,
|
109
|
+
<DropdownItem
|
110
|
+
ouiaId="change-enabled-recurring-dropdown-item"
|
111
|
+
onClick={() =>
|
112
|
+
dispatch(
|
113
|
+
enableRecurringLogic(recurrence?.id, recurringEnabled, jobId)
|
114
|
+
)
|
115
|
+
}
|
116
|
+
key="change-enabled-recurring"
|
117
|
+
component="button"
|
118
|
+
isDisabled={
|
119
|
+
recurrence?.id === undefined ||
|
120
|
+
recurrence?.state === 'cancelled' ||
|
121
|
+
!canEditRecurringLogic
|
122
|
+
}
|
123
|
+
>
|
124
|
+
{recurringEnabled ? __('Disable recurring') : __('Enable recurring')}
|
125
|
+
</DropdownItem>,
|
126
|
+
<DropdownItem
|
127
|
+
ouiaId="cancel-recurring-dropdown-item"
|
128
|
+
onClick={() => dispatch(cancelRecurringLogic(recurrence?.id, jobId))}
|
129
|
+
key="cancel-recurring"
|
130
|
+
component="button"
|
131
|
+
isDisabled={
|
132
|
+
recurrence?.id === undefined ||
|
133
|
+
recurrence?.state === 'cancelled' ||
|
134
|
+
!canEditRecurringLogic
|
135
|
+
}
|
136
|
+
>
|
137
|
+
{__('Cancel recurring')}
|
138
|
+
</DropdownItem>,
|
139
|
+
]
|
140
|
+
: [];
|
141
|
+
|
142
|
+
const dropdownItems = [
|
143
|
+
<DropdownItem
|
144
|
+
ouiaId="rerun-succeeded-dropdown-item"
|
145
|
+
href={foremanUrl(`/job_invocations/${jobId}/rerun?succeeded_only=1`)}
|
146
|
+
key="rerun-succeeded"
|
147
|
+
isDisabled={!(succeeded > 0) || !hasPermission('create_job_invocations')}
|
148
|
+
description="Rerun job on successful hosts"
|
149
|
+
>
|
150
|
+
{__('Rerun successful')}
|
151
|
+
</DropdownItem>,
|
152
|
+
<DropdownItem
|
153
|
+
ouiaId="rerun-failed-dropdown-item"
|
154
|
+
href={foremanUrl(`/job_invocations/${jobId}/rerun?failed_only=1`)}
|
155
|
+
key="rerun-failed"
|
156
|
+
isDisabled={!(failed > 0) || !hasPermission('create_job_invocations')}
|
157
|
+
description="Rerun job on failed hosts"
|
158
|
+
>
|
159
|
+
{__('Rerun failed')}
|
160
|
+
</DropdownItem>,
|
161
|
+
<DropdownItem
|
162
|
+
ouiaId="view-task-dropdown-item"
|
163
|
+
href={foremanUrl(`/foreman_tasks/tasks/${task?.id}`)}
|
164
|
+
key="view-task"
|
165
|
+
isDisabled={!canViewForemanTasks || task === undefined}
|
166
|
+
description="See details of latest task"
|
167
|
+
>
|
168
|
+
{__('View task')}
|
169
|
+
</DropdownItem>,
|
170
|
+
<DropdownSeparator ouiaId="dropdown-separator-0" key="separator-0" />,
|
171
|
+
<DropdownItem
|
172
|
+
ouiaId="cancel-dropdown-item"
|
173
|
+
onClick={() => dispatch(cancelJob(jobId, false))}
|
174
|
+
key="cancel"
|
175
|
+
component="button"
|
176
|
+
isDisabled={!isTaskCancelable || !hasPermission('cancel_job_invocations')}
|
177
|
+
description="Cancel job gracefully"
|
178
|
+
>
|
179
|
+
{__('Cancel')}
|
180
|
+
</DropdownItem>,
|
181
|
+
<DropdownItem
|
182
|
+
ouiaId="abort-dropdown-item"
|
183
|
+
onClick={() => dispatch(cancelJob(jobId, true))}
|
184
|
+
key="abort"
|
185
|
+
component="button"
|
186
|
+
isDisabled={!isTaskCancelable || !hasPermission('cancel_job_invocations')}
|
187
|
+
description="Cancel job immediately"
|
188
|
+
>
|
189
|
+
{__('Abort')}
|
190
|
+
</DropdownItem>,
|
191
|
+
...recurrenceDropdownItems,
|
192
|
+
<DropdownSeparator ouiaId="dropdown-separator-2" key="separator-2" />,
|
193
|
+
<DropdownItem
|
194
|
+
ouiaId="legacy-ui-dropdown-item"
|
195
|
+
href={`/job_invocations/${jobId}`}
|
196
|
+
key="legacy-ui"
|
197
|
+
>
|
198
|
+
{__('Legacy UI')}
|
199
|
+
</DropdownItem>,
|
200
|
+
];
|
201
|
+
|
202
|
+
return (
|
203
|
+
<>
|
204
|
+
<Split hasGutter>
|
205
|
+
<SplitItem>
|
206
|
+
<Button
|
207
|
+
component="a"
|
208
|
+
ouiaId="button-create-report"
|
209
|
+
className="button-create-report"
|
210
|
+
href={foremanUrl(
|
211
|
+
`/templates/report_templates/${reportTemplateJobId}/generate?${queryParams.toString()}`
|
212
|
+
)}
|
213
|
+
variant="secondary"
|
214
|
+
isDisabled={
|
215
|
+
task?.state === STATUS.PENDING ||
|
216
|
+
templateInputId === undefined ||
|
217
|
+
!hasPermission('generate_report_templates')
|
218
|
+
}
|
219
|
+
>
|
220
|
+
{__(`Create report`)}
|
221
|
+
</Button>
|
222
|
+
</SplitItem>
|
223
|
+
<SplitItem>
|
224
|
+
<Dropdown
|
225
|
+
ouiaId="job-invocation-global-actions-dropdown"
|
226
|
+
onSelect={onActionSelect}
|
227
|
+
position={DropdownPosition.right}
|
228
|
+
toggle={
|
229
|
+
<DropdownToggle
|
230
|
+
ouiaId="toggle-button-action-primary"
|
231
|
+
id={`toggle-split-button-action-primary-${jobId}`}
|
232
|
+
splitButtonItems={[
|
233
|
+
<Button
|
234
|
+
component="a"
|
235
|
+
ouiaId="button-rerun-all"
|
236
|
+
key="rerun"
|
237
|
+
href={foremanUrl(`/job_invocations/${jobId}/rerun`)}
|
238
|
+
variant="control"
|
239
|
+
isDisabled={!hasPermission('create_job_invocations')}
|
240
|
+
>
|
241
|
+
{__(`Rerun all`)}
|
242
|
+
</Button>,
|
243
|
+
]}
|
244
|
+
splitButtonVariant="action"
|
245
|
+
onToggle={setIsActionOpen}
|
246
|
+
/>
|
247
|
+
}
|
248
|
+
isOpen={isActionOpen}
|
249
|
+
dropdownItems={dropdownItems}
|
250
|
+
/>
|
251
|
+
</SplitItem>
|
252
|
+
</Split>
|
253
|
+
</>
|
254
|
+
);
|
255
|
+
};
|
256
|
+
|
257
|
+
JobInvocationToolbarButtons.propTypes = {
|
258
|
+
jobId: PropTypes.string.isRequired,
|
259
|
+
data: PropTypes.object.isRequired,
|
260
|
+
currentPermissions: PropTypes.array,
|
261
|
+
permissionsStatus: PropTypes.string,
|
262
|
+
};
|
263
|
+
JobInvocationToolbarButtons.defaultProps = {
|
264
|
+
currentPermissions: undefined,
|
265
|
+
permissionsStatus: undefined,
|
266
|
+
};
|
267
|
+
|
268
|
+
export default JobInvocationToolbarButtons;
|
@@ -0,0 +1,259 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import configureMockStore from 'redux-mock-store';
|
3
|
+
import { fireEvent, render, screen, act } from '@testing-library/react';
|
4
|
+
import '@testing-library/jest-dom/extend-expect';
|
5
|
+
import { Provider } from 'react-redux';
|
6
|
+
import thunk from 'redux-thunk';
|
7
|
+
import { foremanUrl } from 'foremanReact/common/helpers';
|
8
|
+
import * as api from 'foremanReact/redux/API';
|
9
|
+
import JobInvocationDetailPage from '../index';
|
10
|
+
import {
|
11
|
+
jobInvocationData,
|
12
|
+
jobInvocationDataScheduled,
|
13
|
+
jobInvocationDataRecurring,
|
14
|
+
mockPermissionsData,
|
15
|
+
mockReportTemplatesResponse,
|
16
|
+
mockReportTemplateInputsResponse,
|
17
|
+
} from './fixtures';
|
18
|
+
import {
|
19
|
+
cancelJob,
|
20
|
+
enableRecurringLogic,
|
21
|
+
cancelRecurringLogic,
|
22
|
+
} from '../JobInvocationActions';
|
23
|
+
import {
|
24
|
+
CANCEL_JOB,
|
25
|
+
CANCEL_RECURRING_LOGIC,
|
26
|
+
CHANGE_ENABLED_RECURRING_LOGIC,
|
27
|
+
GET_REPORT_TEMPLATES,
|
28
|
+
GET_REPORT_TEMPLATE_INPUTS,
|
29
|
+
JOB_INVOCATION_KEY,
|
30
|
+
} from '../JobInvocationConstants';
|
31
|
+
|
32
|
+
jest.spyOn(api, 'get');
|
33
|
+
|
34
|
+
jest.mock('foremanReact/common/hooks/API/APIHooks', () => ({
|
35
|
+
useAPI: jest.fn(() => ({
|
36
|
+
response: mockPermissionsData,
|
37
|
+
})),
|
38
|
+
}));
|
39
|
+
|
40
|
+
jest.mock('foremanReact/routes/common/PageLayout/PageLayout', () =>
|
41
|
+
jest.fn(props => (
|
42
|
+
<div>
|
43
|
+
{props.header && <h1>{props.header}</h1>}
|
44
|
+
{props.toolbarButtons && <div>{props.toolbarButtons}</div>}
|
45
|
+
{props.children}
|
46
|
+
</div>
|
47
|
+
))
|
48
|
+
);
|
49
|
+
|
50
|
+
const initialState = {
|
51
|
+
JOB_INVOCATION_KEY: {
|
52
|
+
response: jobInvocationData,
|
53
|
+
},
|
54
|
+
GET_REPORT_TEMPLATES: mockReportTemplatesResponse,
|
55
|
+
};
|
56
|
+
|
57
|
+
const initialStateScheduled = {
|
58
|
+
JOB_INVOCATION_KEY: {
|
59
|
+
response: jobInvocationDataScheduled,
|
60
|
+
},
|
61
|
+
};
|
62
|
+
|
63
|
+
api.get.mockImplementation(({ handleSuccess, ...action }) => {
|
64
|
+
if (action.key === 'GET_REPORT_TEMPLATES') {
|
65
|
+
handleSuccess &&
|
66
|
+
handleSuccess({
|
67
|
+
data: mockReportTemplatesResponse,
|
68
|
+
});
|
69
|
+
} else if (action.key === 'GET_REPORT_TEMPLATE_INPUTS') {
|
70
|
+
handleSuccess &&
|
71
|
+
handleSuccess({
|
72
|
+
data: mockReportTemplateInputsResponse,
|
73
|
+
});
|
74
|
+
}
|
75
|
+
|
76
|
+
return { type: 'get', ...action };
|
77
|
+
});
|
78
|
+
|
79
|
+
const reportTemplateJobId = mockReportTemplatesResponse.results[0].id;
|
80
|
+
|
81
|
+
const mockStore = configureMockStore([thunk]);
|
82
|
+
|
83
|
+
describe('JobInvocationDetailPage', () => {
|
84
|
+
it('renders main information', async () => {
|
85
|
+
const jobId = jobInvocationData.id;
|
86
|
+
const store = mockStore(initialState);
|
87
|
+
|
88
|
+
const { container } = render(
|
89
|
+
<Provider store={store}>
|
90
|
+
<JobInvocationDetailPage match={{ params: { id: `${jobId}` } }} />
|
91
|
+
</Provider>
|
92
|
+
);
|
93
|
+
|
94
|
+
expect(screen.getByText('Description')).toBeInTheDocument();
|
95
|
+
expect(
|
96
|
+
container.querySelector('.chart-donut .pf-c-chart')
|
97
|
+
).toBeInTheDocument();
|
98
|
+
expect(screen.getByText('2/6')).toBeInTheDocument();
|
99
|
+
expect(screen.getByText('Systems')).toBeInTheDocument();
|
100
|
+
expect(screen.getByText('System status')).toBeInTheDocument();
|
101
|
+
expect(screen.getByText('Succeeded: 2')).toBeInTheDocument();
|
102
|
+
expect(screen.getByText('Failed: 4')).toBeInTheDocument();
|
103
|
+
expect(screen.getByText('In Progress: 0')).toBeInTheDocument();
|
104
|
+
expect(screen.getByText('Canceled: 0')).toBeInTheDocument();
|
105
|
+
|
106
|
+
const informationToCheck = {
|
107
|
+
'Effective user:': jobInvocationData.effective_user,
|
108
|
+
'Started at:': 'Jan 1, 2024, 11:34 UTC',
|
109
|
+
'SSH user:': 'Not available',
|
110
|
+
'Template:': jobInvocationData.template_name,
|
111
|
+
};
|
112
|
+
|
113
|
+
Object.entries(informationToCheck).forEach(([term, expectedText]) => {
|
114
|
+
const termContainers = container.querySelectorAll(
|
115
|
+
'.pf-c-description-list__term .pf-c-description-list__text'
|
116
|
+
);
|
117
|
+
termContainers.forEach(termContainer => {
|
118
|
+
if (termContainer.textContent.includes(term)) {
|
119
|
+
let descriptionContainer;
|
120
|
+
if (term === 'SSH user:') {
|
121
|
+
descriptionContainer = termContainer
|
122
|
+
.closest('.pf-c-description-list__group')
|
123
|
+
.querySelector(
|
124
|
+
'.pf-c-description-list__description .pf-c-description-list__text .disabled-text'
|
125
|
+
);
|
126
|
+
} else {
|
127
|
+
descriptionContainer = termContainer
|
128
|
+
.closest('.pf-c-description-list__group')
|
129
|
+
.querySelector(
|
130
|
+
'.pf-c-description-list__description .pf-c-description-list__text'
|
131
|
+
);
|
132
|
+
}
|
133
|
+
expect(descriptionContainer.textContent).toContain(expectedText);
|
134
|
+
}
|
135
|
+
});
|
136
|
+
});
|
137
|
+
|
138
|
+
// checks the global actions and if they link to the correct url
|
139
|
+
expect(screen.getByText('Create report').getAttribute('href')).toEqual(
|
140
|
+
foremanUrl(
|
141
|
+
`/templates/report_templates/${mockReportTemplatesResponse.results[0].id}/generate?report_template_report%5Binput_values%5D%5B${mockReportTemplateInputsResponse.results[0].id}%5D%5Bvalue%5D=${jobId}`
|
142
|
+
)
|
143
|
+
);
|
144
|
+
expect(screen.getByText('Rerun all').getAttribute('href')).toEqual(
|
145
|
+
foremanUrl(`/job_invocations/${jobId}/rerun`)
|
146
|
+
);
|
147
|
+
act(() => {
|
148
|
+
fireEvent.click(screen.getByRole('button', { name: 'Select' }));
|
149
|
+
});
|
150
|
+
expect(
|
151
|
+
screen
|
152
|
+
.getByText('Rerun successful')
|
153
|
+
.closest('a')
|
154
|
+
.getAttribute('href')
|
155
|
+
).toEqual(foremanUrl(`/job_invocations/${jobId}/rerun?succeeded_only=1`));
|
156
|
+
expect(
|
157
|
+
screen
|
158
|
+
.getByText('Rerun failed')
|
159
|
+
.closest('a')
|
160
|
+
.getAttribute('href')
|
161
|
+
).toEqual(foremanUrl(`/job_invocations/${jobId}/rerun?failed_only=1`));
|
162
|
+
expect(
|
163
|
+
screen
|
164
|
+
.getByText('View task')
|
165
|
+
.closest('a')
|
166
|
+
.getAttribute('href')
|
167
|
+
).toEqual(foremanUrl(`/foreman_tasks/tasks/${jobInvocationData.task.id}`));
|
168
|
+
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
169
|
+
expect(screen.getByText('Abort')).toBeInTheDocument();
|
170
|
+
expect(screen.queryByText('Enable recurring')).not.toBeInTheDocument();
|
171
|
+
expect(screen.queryByText('Cancel recurring')).not.toBeInTheDocument();
|
172
|
+
expect(
|
173
|
+
screen
|
174
|
+
.getByText('Legacy UI')
|
175
|
+
.closest('a')
|
176
|
+
.getAttribute('href')
|
177
|
+
).toEqual(`/job_invocations/${jobId}`);
|
178
|
+
});
|
179
|
+
|
180
|
+
it('shows scheduled date', async () => {
|
181
|
+
const store = mockStore(initialStateScheduled);
|
182
|
+
render(
|
183
|
+
<Provider store={store}>
|
184
|
+
<JobInvocationDetailPage
|
185
|
+
match={{ params: { id: `${jobInvocationDataScheduled.id}` } }}
|
186
|
+
/>
|
187
|
+
</Provider>
|
188
|
+
);
|
189
|
+
|
190
|
+
expect(screen.getByText('Scheduled at:')).toBeInTheDocument();
|
191
|
+
expect(screen.getByText('Jan 1, 3000, 11:34 UTC')).toBeInTheDocument();
|
192
|
+
expect(screen.getByText('Not yet')).toBeInTheDocument();
|
193
|
+
});
|
194
|
+
|
195
|
+
it('should dispatch global actions', async () => {
|
196
|
+
// recurring in the future
|
197
|
+
const jobId = jobInvocationDataRecurring.id;
|
198
|
+
const recurrenceId = jobInvocationDataRecurring.recurrence.id;
|
199
|
+
const store = mockStore(jobInvocationDataRecurring);
|
200
|
+
render(
|
201
|
+
<Provider store={store}>
|
202
|
+
<JobInvocationDetailPage match={{ params: { id: `${jobId}` } }} />
|
203
|
+
</Provider>
|
204
|
+
);
|
205
|
+
|
206
|
+
const expectedActions = [
|
207
|
+
{ key: GET_REPORT_TEMPLATES, url: '/api/report_templates' },
|
208
|
+
{
|
209
|
+
key: JOB_INVOCATION_KEY,
|
210
|
+
url: `/api/job_invocations/${jobId}`,
|
211
|
+
},
|
212
|
+
{
|
213
|
+
key: GET_REPORT_TEMPLATE_INPUTS,
|
214
|
+
url: `/api/templates/${reportTemplateJobId}/template_inputs`,
|
215
|
+
},
|
216
|
+
{
|
217
|
+
key: CANCEL_JOB,
|
218
|
+
url: `/job_invocations/${jobId}/cancel`,
|
219
|
+
},
|
220
|
+
{
|
221
|
+
key: CANCEL_JOB,
|
222
|
+
url: `/job_invocations/${jobId}/cancel?force=true`,
|
223
|
+
},
|
224
|
+
{
|
225
|
+
key: CHANGE_ENABLED_RECURRING_LOGIC,
|
226
|
+
url: `/foreman_tasks/api/recurring_logics/${recurrenceId}`,
|
227
|
+
},
|
228
|
+
{
|
229
|
+
key: CHANGE_ENABLED_RECURRING_LOGIC,
|
230
|
+
url: `/foreman_tasks/api/recurring_logics/${recurrenceId}`,
|
231
|
+
},
|
232
|
+
{
|
233
|
+
key: CANCEL_RECURRING_LOGIC,
|
234
|
+
url: `/foreman_tasks/recurring_logics/${recurrenceId}/cancel`,
|
235
|
+
},
|
236
|
+
];
|
237
|
+
|
238
|
+
store.dispatch(cancelJob(jobId, false));
|
239
|
+
store.dispatch(cancelJob(jobId, true));
|
240
|
+
store.dispatch(enableRecurringLogic(recurrenceId, true, jobId));
|
241
|
+
store.dispatch(enableRecurringLogic(recurrenceId, false, jobId));
|
242
|
+
store.dispatch(cancelRecurringLogic(recurrenceId, jobId));
|
243
|
+
|
244
|
+
const actualActions = store.getActions();
|
245
|
+
expect(actualActions).toHaveLength(expectedActions.length);
|
246
|
+
|
247
|
+
expectedActions.forEach((expectedAction, index) => {
|
248
|
+
if (actualActions[index].type === 'WITH_INTERVAL') {
|
249
|
+
expect(actualActions[index].key.key).toEqual(expectedAction.key);
|
250
|
+
expect(actualActions[index].key.url).toEqual(expectedAction.url);
|
251
|
+
} else {
|
252
|
+
expect(actualActions[index].key).toEqual(expectedAction.key);
|
253
|
+
if (expectedAction.url) {
|
254
|
+
expect(actualActions[index].url).toEqual(expectedAction.url);
|
255
|
+
}
|
256
|
+
}
|
257
|
+
});
|
258
|
+
});
|
259
|
+
});
|
@@ -0,0 +1,117 @@
|
|
1
|
+
export const jobInvocationData = {
|
2
|
+
id: 123,
|
3
|
+
description: 'Description',
|
4
|
+
job_category: 'Commands',
|
5
|
+
targeting_id: 123,
|
6
|
+
status: 1,
|
7
|
+
start_at: '2024-01-01 12:34:56 +0100',
|
8
|
+
status_label: 'failed',
|
9
|
+
ssh_user: null,
|
10
|
+
time_to_pickup: null,
|
11
|
+
template_id: 321,
|
12
|
+
template_name: 'Run Command - Script Default',
|
13
|
+
effective_user: 'root',
|
14
|
+
succeeded: 2,
|
15
|
+
failed: 4,
|
16
|
+
pending: 0,
|
17
|
+
cancelled: 0,
|
18
|
+
total: 6,
|
19
|
+
missing: 5,
|
20
|
+
total_hosts: 6,
|
21
|
+
task: {
|
22
|
+
id: '37ad5ead-51de-4798-bc73-a17687c4d5aa',
|
23
|
+
state: 'stopped',
|
24
|
+
},
|
25
|
+
template_invocations: [
|
26
|
+
{
|
27
|
+
template_id: 321,
|
28
|
+
template_name: 'Run Command - Script Default',
|
29
|
+
host_id: 1,
|
30
|
+
template_invocation_input_values: [
|
31
|
+
{
|
32
|
+
template_input_name: 'command',
|
33
|
+
template_input_id: 59,
|
34
|
+
value:
|
35
|
+
'echo start; for i in $(seq 1 120); do echo $i; sleep 1; done; echo done',
|
36
|
+
},
|
37
|
+
],
|
38
|
+
},
|
39
|
+
],
|
40
|
+
};
|
41
|
+
|
42
|
+
export const jobInvocationDataScheduled = {
|
43
|
+
id: 456,
|
44
|
+
description: 'Description',
|
45
|
+
job_category: 'Commands',
|
46
|
+
targeting_id: 456,
|
47
|
+
status: 1,
|
48
|
+
start_at: '3000-01-01 12:34:56 +0100',
|
49
|
+
status_label: 'failed',
|
50
|
+
ssh_user: null,
|
51
|
+
time_to_pickup: null,
|
52
|
+
template_id: 321,
|
53
|
+
template_name: 'Run Command - Script Default',
|
54
|
+
effective_user: 'root',
|
55
|
+
succeeded: 2,
|
56
|
+
failed: 4,
|
57
|
+
pending: 0,
|
58
|
+
cancelled: 0,
|
59
|
+
total: 6,
|
60
|
+
missing: 5,
|
61
|
+
total_hosts: 6,
|
62
|
+
};
|
63
|
+
|
64
|
+
export const jobInvocationDataRecurring = {
|
65
|
+
id: 789,
|
66
|
+
description: 'Description',
|
67
|
+
job_category: 'Commands',
|
68
|
+
targeting_id: 456,
|
69
|
+
status: 2,
|
70
|
+
start_at: '3000-01-01 12:00:00 +0100',
|
71
|
+
status_label: 'queued',
|
72
|
+
ssh_user: null,
|
73
|
+
time_to_pickup: null,
|
74
|
+
template_id: 321,
|
75
|
+
template_name: 'Run Command - Script Default',
|
76
|
+
effective_user: 'root',
|
77
|
+
succeeded: 0,
|
78
|
+
failed: 0,
|
79
|
+
pending: 0,
|
80
|
+
cancelled: 0,
|
81
|
+
total: 'N/A',
|
82
|
+
missing: 0,
|
83
|
+
total_hosts: 1,
|
84
|
+
task: {
|
85
|
+
id: '37ad5ead-51de-4798-bc73-a17687c4d5aa',
|
86
|
+
state: 'scheduled',
|
87
|
+
},
|
88
|
+
mode: 'recurring',
|
89
|
+
recurrence: {
|
90
|
+
id: 1,
|
91
|
+
cron_line: '00 12 * * *',
|
92
|
+
end_time: null,
|
93
|
+
iteration: 1,
|
94
|
+
task_group_id: 12,
|
95
|
+
state: 'active',
|
96
|
+
max_iteration: null,
|
97
|
+
purpose: null,
|
98
|
+
task_count: 1,
|
99
|
+
action: 'Run hosts job:',
|
100
|
+
last_occurence: null,
|
101
|
+
next_occurence: '3000-01-01 12:00:00 +0100',
|
102
|
+
},
|
103
|
+
};
|
104
|
+
|
105
|
+
export const mockPermissionsData = {
|
106
|
+
edit_job_templates: true,
|
107
|
+
view_foreman_tasks: true,
|
108
|
+
edit_recurring_logics: true,
|
109
|
+
};
|
110
|
+
|
111
|
+
export const mockReportTemplatesResponse = {
|
112
|
+
results: [{ id: '12', name: 'Job - Invocation Report' }],
|
113
|
+
};
|
114
|
+
|
115
|
+
export const mockReportTemplateInputsResponse = {
|
116
|
+
results: [{ id: '34', name: 'job_id' }],
|
117
|
+
};
|
@@ -1,20 +1,24 @@
|
|
1
|
-
import React, { useEffect } from 'react';
|
2
|
-
import { useSelector, useDispatch } from 'react-redux';
|
3
1
|
import PropTypes from 'prop-types';
|
4
|
-
import
|
2
|
+
import React, { useEffect } from 'react';
|
3
|
+
import { useDispatch, useSelector } from 'react-redux';
|
4
|
+
import { Divider, Flex } from '@patternfly/react-core';
|
5
5
|
import { translate as __, documentLocale } from 'foremanReact/common/I18n';
|
6
6
|
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
|
7
|
+
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
|
7
8
|
import { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
|
8
|
-
import { getData } from './JobInvocationActions';
|
9
|
-
import { selectItems } from './JobInvocationSelectors';
|
10
|
-
import JobInvocationOverview from './JobInvocationOverview';
|
11
|
-
import JobInvocationSystemStatusChart from './JobInvocationSystemStatusChart';
|
9
|
+
import { getData, getTask } from './JobInvocationActions';
|
12
10
|
import {
|
11
|
+
CURRENT_PERMISSIONS,
|
12
|
+
DATE_OPTIONS,
|
13
13
|
JOB_INVOCATION_KEY,
|
14
14
|
STATUS,
|
15
|
-
|
15
|
+
currentPermissionsUrl,
|
16
16
|
} from './JobInvocationConstants';
|
17
17
|
import './JobInvocationDetail.scss';
|
18
|
+
import JobInvocationOverview from './JobInvocationOverview';
|
19
|
+
import { selectItems } from './JobInvocationSelectors';
|
20
|
+
import JobInvocationSystemStatusChart from './JobInvocationSystemStatusChart';
|
21
|
+
import JobInvocationToolbarButtons from './JobInvocationToolbarButtons';
|
18
22
|
|
19
23
|
const JobInvocationDetailPage = ({
|
20
24
|
match: {
|
@@ -30,8 +34,15 @@ const JobInvocationDetailPage = ({
|
|
30
34
|
start_at: startAt,
|
31
35
|
} = items;
|
32
36
|
const finished =
|
33
|
-
statusLabel === STATUS.FAILED ||
|
37
|
+
statusLabel === STATUS.FAILED ||
|
38
|
+
statusLabel === STATUS.SUCCEEDED ||
|
39
|
+
statusLabel === STATUS.CANCELLED;
|
34
40
|
const autoRefresh = task?.state === STATUS.PENDING || false;
|
41
|
+
const { response, status } = useAPI(
|
42
|
+
'get',
|
43
|
+
currentPermissionsUrl,
|
44
|
+
CURRENT_PERMISSIONS
|
45
|
+
);
|
35
46
|
|
36
47
|
let isAlreadyStarted = false;
|
37
48
|
let formattedStartDate;
|
@@ -56,6 +67,12 @@ const JobInvocationDetailPage = ({
|
|
56
67
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
57
68
|
}, [dispatch, id, finished, autoRefresh]);
|
58
69
|
|
70
|
+
useEffect(() => {
|
71
|
+
if (task?.id !== undefined) {
|
72
|
+
dispatch(getTask(`${task?.id}`));
|
73
|
+
}
|
74
|
+
}, [dispatch, task]);
|
75
|
+
|
59
76
|
const breadcrumbOptions = {
|
60
77
|
breadcrumbItems: [
|
61
78
|
{ caption: __('Jobs'), url: `/job_invocations` },
|
@@ -68,38 +85,41 @@ const JobInvocationDetailPage = ({
|
|
68
85
|
<PageLayout
|
69
86
|
header={description}
|
70
87
|
breadcrumbOptions={breadcrumbOptions}
|
88
|
+
toolbarButtons={
|
89
|
+
<JobInvocationToolbarButtons
|
90
|
+
jobId={id}
|
91
|
+
data={items}
|
92
|
+
currentPermissions={response.results}
|
93
|
+
permissionsStatus={status}
|
94
|
+
/>
|
95
|
+
}
|
71
96
|
searchable={false}
|
72
97
|
>
|
73
|
-
<
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
98
|
+
<Flex
|
99
|
+
className="job-invocation-detail-flex"
|
100
|
+
alignItems={{ default: 'alignItemsFlexStart' }}
|
101
|
+
>
|
102
|
+
<JobInvocationSystemStatusChart
|
103
|
+
data={items}
|
104
|
+
isAlreadyStarted={isAlreadyStarted}
|
105
|
+
formattedStartDate={formattedStartDate}
|
106
|
+
/>
|
107
|
+
<Divider
|
108
|
+
orientation={{
|
109
|
+
default: 'vertical',
|
110
|
+
}}
|
111
|
+
/>
|
112
|
+
<Flex
|
113
|
+
className="job-overview"
|
114
|
+
alignItems={{ default: 'alignItemsCenter' }}
|
78
115
|
>
|
79
|
-
<
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
orientation={{
|
87
|
-
default: 'vertical',
|
88
|
-
}}
|
89
|
-
/>
|
90
|
-
<Flex
|
91
|
-
className="job-overview"
|
92
|
-
alignItems={{ default: 'alignItemsCenter' }}
|
93
|
-
>
|
94
|
-
<JobInvocationOverview
|
95
|
-
data={items}
|
96
|
-
isAlreadyStarted={isAlreadyStarted}
|
97
|
-
formattedStartDate={formattedStartDate}
|
98
|
-
/>
|
99
|
-
</Flex>
|
100
|
-
</Flex>
|
101
|
-
</PageSection>
|
102
|
-
</React.Fragment>
|
116
|
+
<JobInvocationOverview
|
117
|
+
data={items}
|
118
|
+
isAlreadyStarted={isAlreadyStarted}
|
119
|
+
formattedStartDate={formattedStartDate}
|
120
|
+
/>
|
121
|
+
</Flex>
|
122
|
+
</Flex>
|
103
123
|
</PageLayout>
|
104
124
|
);
|
105
125
|
};
|
@@ -1,15 +1,15 @@
|
|
1
|
-
import React from 'react';
|
2
1
|
import PropTypes from 'prop-types';
|
2
|
+
import React from 'react';
|
3
3
|
import URI from 'urijs';
|
4
|
-
import { Alert, Divider, Skeleton
|
5
|
-
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
6
|
-
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
|
7
|
-
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
|
8
|
-
import { STATUS } from 'foremanReact/constants';
|
4
|
+
import { Alert, Button, Divider, Skeleton } from '@patternfly/react-core';
|
9
5
|
import {
|
10
|
-
useForemanOrganization,
|
11
6
|
useForemanLocation,
|
7
|
+
useForemanOrganization,
|
12
8
|
} from 'foremanReact/Root/Context/ForemanContext';
|
9
|
+
import { translate as __, sprintf } from 'foremanReact/common/I18n';
|
10
|
+
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
|
11
|
+
import { STATUS } from 'foremanReact/constants';
|
12
|
+
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
|
13
13
|
import { JobWizard } from './JobWizard';
|
14
14
|
import { JOB_API_KEY } from './JobWizardConstants';
|
15
15
|
|
@@ -21,11 +21,16 @@ const JobWizardPageRerun = ({
|
|
21
21
|
}) => {
|
22
22
|
const uri = new URI(search);
|
23
23
|
const { failed_only: failedOnly } = uri.search(true);
|
24
|
+
const { succeeded_only: succeededOnly } = uri.search(true);
|
25
|
+
let queryParams = '';
|
26
|
+
if (failedOnly) {
|
27
|
+
queryParams = '&failed_only=1';
|
28
|
+
} else if (succeededOnly) {
|
29
|
+
queryParams = '&succeeded_only=1';
|
30
|
+
}
|
24
31
|
const { response, status } = useAPI(
|
25
32
|
'get',
|
26
|
-
`/ui_job_wizard/job_invocation?id=${id}${
|
27
|
-
failedOnly ? '&failed_only=1' : ''
|
28
|
-
}`,
|
33
|
+
`/ui_job_wizard/job_invocation?id=${id}${queryParams}`,
|
29
34
|
JOB_API_KEY
|
30
35
|
);
|
31
36
|
const title = __('Run job');
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { API_OPERATIONS } from 'foremanReact/redux/API/APIConstants';
|
2
|
+
|
3
|
+
const { GET, POST, PUT, DELETE, PATCH } = API_OPERATIONS;
|
4
|
+
|
5
|
+
const apiAction = (type, payload) => ({ type, payload });
|
6
|
+
|
7
|
+
export const get = payload => apiAction(GET, payload);
|
8
|
+
|
9
|
+
export const post = payload => apiAction(POST, payload);
|
10
|
+
|
11
|
+
export const put = payload => apiAction(PUT, payload);
|
12
|
+
|
13
|
+
export const patch = payload => apiAction(PATCH, payload);
|
14
|
+
|
15
|
+
export const APIActions = {
|
16
|
+
get,
|
17
|
+
post,
|
18
|
+
put,
|
19
|
+
patch,
|
20
|
+
delete: payload => apiAction(DELETE, payload),
|
21
|
+
};
|
@@ -1,5 +1,19 @@
|
|
1
1
|
export const API = {
|
2
2
|
get: jest.fn(),
|
3
|
+
put: jest.fn(),
|
4
|
+
post: jest.fn(),
|
5
|
+
delete: jest.fn(),
|
6
|
+
patch: jest.fn(),
|
3
7
|
};
|
4
8
|
|
5
9
|
export const get = data => ({ type: 'get-some-type', ...data });
|
10
|
+
export const post = data => ({ type: 'post-some-type', ...data });
|
11
|
+
export const put = data => ({ type: 'put-some-type', ...data });
|
12
|
+
export const patch = data => ({ type: 'patch-some-type', ...data });
|
13
|
+
|
14
|
+
export const APIActions = {
|
15
|
+
get,
|
16
|
+
post,
|
17
|
+
put,
|
18
|
+
patch,
|
19
|
+
};
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foreman_remote_execution
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 13.
|
4
|
+
version: 13.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Foreman Remote Execution team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-05-
|
11
|
+
date: 2024-05-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: deface
|
@@ -440,6 +440,9 @@ files:
|
|
440
440
|
- webpack/JobInvocationDetail/JobInvocationOverview.js
|
441
441
|
- webpack/JobInvocationDetail/JobInvocationSelectors.js
|
442
442
|
- webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js
|
443
|
+
- webpack/JobInvocationDetail/JobInvocationToolbarButtons.js
|
444
|
+
- webpack/JobInvocationDetail/__tests__/MainInformation.test.js
|
445
|
+
- webpack/JobInvocationDetail/__tests__/fixtures.js
|
443
446
|
- webpack/JobInvocationDetail/index.js
|
444
447
|
- webpack/JobWizard/Footer.js
|
445
448
|
- webpack/JobWizard/JobWizard.js
|
@@ -512,14 +515,21 @@ files:
|
|
512
515
|
- webpack/__mocks__/foremanReact/common/hooks/API/APIHooks.js
|
513
516
|
- webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js
|
514
517
|
- webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js
|
518
|
+
- webpack/__mocks__/foremanReact/components/BreadcrumbBar/index.js
|
519
|
+
- webpack/__mocks__/foremanReact/components/Head/index.js
|
520
|
+
- webpack/__mocks__/foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState.js
|
515
521
|
- webpack/__mocks__/foremanReact/components/Pagination.js
|
516
522
|
- webpack/__mocks__/foremanReact/components/SearchBar.js
|
523
|
+
- webpack/__mocks__/foremanReact/components/ToastsList/index.js
|
517
524
|
- webpack/__mocks__/foremanReact/components/common/ActionButtons/ActionButtons.js
|
518
525
|
- webpack/__mocks__/foremanReact/constants.js
|
519
526
|
- webpack/__mocks__/foremanReact/history.js
|
527
|
+
- webpack/__mocks__/foremanReact/redux/API/APIActions.js
|
528
|
+
- webpack/__mocks__/foremanReact/redux/API/APIConstants.js
|
520
529
|
- webpack/__mocks__/foremanReact/redux/API/APISelectors.js
|
521
530
|
- webpack/__mocks__/foremanReact/redux/API/index.js
|
522
531
|
- webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware/IntervalSelectors.js
|
532
|
+
- webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware/index.js
|
523
533
|
- webpack/__mocks__/foremanReact/routes/Hosts/constants.js
|
524
534
|
- webpack/__mocks__/foremanReact/routes/RouterSelector.js
|
525
535
|
- webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js
|