foreman_remote_execution 13.1.1 → 13.2.1

Sign up to get free protection for your applications and to get access to all the features.
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;