foreman_remote_execution 13.1.1 → 13.2.1

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/api/v2/job_invocations_controller.rb +1 -0
  3. data/app/models/job_invocation.rb +23 -0
  4. data/app/models/job_invocation_composer.rb +2 -0
  5. data/app/views/api/v2/job_invocations/base.json.rabl +3 -2
  6. data/app/views/templates/script/package_action.erb +18 -10
  7. data/lib/foreman_remote_execution/version.rb +1 -1
  8. data/package.json +2 -1
  9. data/webpack/JobInvocationDetail/JobInvocationActions.js +134 -3
  10. data/webpack/JobInvocationDetail/JobInvocationConstants.js +14 -0
  11. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +5 -2
  12. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +5 -0
  13. data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +13 -9
  14. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +268 -0
  15. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +259 -0
  16. data/webpack/JobInvocationDetail/__tests__/fixtures.js +117 -0
  17. data/webpack/JobInvocationDetail/index.js +58 -38
  18. data/webpack/JobWizard/JobWizard.scss +14 -5
  19. data/webpack/JobWizard/JobWizardPageRerun.js +15 -10
  20. data/webpack/JobWizard/steps/HostsAndInputs/index.js +4 -1
  21. data/webpack/__mocks__/foremanReact/components/BreadcrumbBar/index.js +4 -0
  22. data/webpack/__mocks__/foremanReact/components/Head/index.js +10 -0
  23. data/webpack/__mocks__/foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState.js +8 -0
  24. data/webpack/__mocks__/foremanReact/components/ToastsList/index.js +3 -0
  25. data/webpack/__mocks__/foremanReact/redux/API/APIActions.js +21 -0
  26. data/webpack/__mocks__/foremanReact/redux/API/APIConstants.js +7 -0
  27. data/webpack/__mocks__/foremanReact/redux/API/index.js +14 -0
  28. data/webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware/index.js +9 -0
  29. metadata +12 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9e4c8b6eee94e6c8e6d4e851c81182e7ab4e36de9afdfb1fed8b07250ef7ca6
4
- data.tar.gz: 2f9b08ec83484d0133d8f53544d5048a6e5c39675cb84cbf2786427c426d0631
3
+ metadata.gz: 473900c99e054678a879651060b7596221f32c7f2713d28efb472957b7ac73ed
4
+ data.tar.gz: 14e55ba559f96def212e56aad5087b3e171fceb0cae2209b2ba1ef0566bda620
5
5
  SHA512:
6
- metadata.gz: 2b2ad3235424ec6c3b0e10ca8483d9fab1f2249d6abfc8f52b641e60ea2187687ac4f670d3b3666318c0de9688a782c5ebc851352269d0f8ba36040272943b91
7
- data.tar.gz: 616cc9e8a67ed624cfa4bcd03be82454c56d770a23a621daf14964be27a4ae3d58bd2916f272d3192aafcb02978e232d7a24709ec7ad55568c4e980051eb8c78
6
+ metadata.gz: 715de4ec99b4f17450ec02cca4db12e7778001098f49c1a733fae18c84d72855dd59e8dc86c1a6edddd72856a26c345c1397b2e2c1a66f14b0e605497f59e747
7
+ data.tar.gz: 7dd731683f6f68d5c9f32c14b9411cbde8a65e0de38de249f691cd70447b2ff936bac8f7413b4edad8bb776ae0612ccbcb487650a20a16c35e37fc2ded3b4af2
@@ -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
@@ -232,6 +232,8 @@ class JobInvocationComposer
232
232
  @host_ids = params[:host_ids]
233
233
  elsif params[:failed_only]
234
234
  @host_ids = job_invocation.failed_host_ids
235
+ elsif params[:succeeded_only]
236
+ @host_ids = job_invocation.succeeded_host_ids
235
237
  end
236
238
  end
237
239
 
@@ -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" => (edit_job_templates_permission && authorizer.can?("edit_job_templates", invocation, false)),
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
@@ -118,19 +118,27 @@ handle_zypp_res_codes () {
118
118
  end
119
119
  end
120
120
  -%>
121
- OUTFILE=$(mktemp)
122
- trap "rm -f $OUTFILE" EXIT
123
- set -o pipefail
121
+ run_cmd()
122
+ {
123
+ LANG=C apt-get -o APT::Get::Upgrade-Allow-New="true" -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y <%= input("options") %> <%= action %> <%= input("package") %> 2>&1
124
+ }
124
125
  export DEBIAN_FRONTEND=noninteractive
125
126
  apt-get -y update
126
- LANG=C apt-get -o APT::Get::Upgrade-Allow-New="true" -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y <%= input("options") %> <%= action %> <%= input("package") %> 2>&1 | tee $OUTFILE
127
- RETVAL=$?
128
- if grep -q "Unable to locate" $OUTFILE && [ "$action" = "remove" ]; then
129
- true
127
+ if [ "<%= action %>" = "remove" ]; then
128
+ OUTFILE=$(mktemp)
129
+ trap "rm -f $OUTFILE" EXIT
130
+ run_cmd > $OUTFILE
131
+ RETVAL=$?
132
+ cat $OUTFILE
133
+ if grep -q "Unable to locate" $OUTFILE; then
134
+ RETVAL=0
135
+ fi
130
136
  else
131
- setReturnValue() { RETVAL=$1; return $RETVAL; }
132
- setReturnValue $RETVAL
137
+ run_cmd
138
+ RETVAL=$?
133
139
  fi
140
+ setReturnValue() { RETVAL=$1; return $RETVAL; }
141
+ setReturnValue $RETVAL
134
142
  <% elsif package_manager == 'zypper' -%>
135
143
  <%-
136
144
  if action == "group install"
@@ -141,7 +149,7 @@ handle_zypp_res_codes () {
141
149
  -%>
142
150
  zypper refresh
143
151
  zypper -n <%= action %> <%= input("options") %> <%= input("package") %>
144
- handle_zypp_res_codes $? $action
152
+ handle_zypp_res_codes $? "<%= action %>"
145
153
  <% end -%>
146
154
  RETVAL=$?
147
155
  [ $RETVAL -eq 0 ] || exit_with_message "Package action failed" $RETVAL
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '13.1.1'.freeze
2
+ VERSION = '13.2.1'.freeze
3
3
  end
data/package.json CHANGED
@@ -29,7 +29,8 @@
29
29
  "prettier": "^1.19.1",
30
30
  "redux-mock-store": "^1.2.2",
31
31
  "graphql-tag": "^2.11.0",
32
- "graphql": "^15.5.0"
32
+ "graphql": "^15.5.0",
33
+ "victory-core": "~36.8.6"
33
34
  },
34
35
  "peerDependencies": {
35
36
  "@theforeman/vendor": ">= 12.0.1"
@@ -1,9 +1,19 @@
1
- import { get } from 'foremanReact/redux/API';
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 { JOB_INVOCATION_KEY } from './JobInvocationConstants';
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-page-section {
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 pointerLength={0} constrainToVisibleArea />
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;