foreman_remote_execution 13.1.1 → 13.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) 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/lib/foreman_remote_execution/version.rb +1 -1
  7. data/package.json +2 -1
  8. data/webpack/JobInvocationDetail/JobInvocationActions.js +134 -3
  9. data/webpack/JobInvocationDetail/JobInvocationConstants.js +14 -0
  10. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +5 -2
  11. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +5 -0
  12. data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +13 -9
  13. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +268 -0
  14. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +259 -0
  15. data/webpack/JobInvocationDetail/__tests__/fixtures.js +117 -0
  16. data/webpack/JobInvocationDetail/index.js +58 -38
  17. data/webpack/JobWizard/JobWizardPageRerun.js +15 -10
  18. data/webpack/__mocks__/foremanReact/components/BreadcrumbBar/index.js +4 -0
  19. data/webpack/__mocks__/foremanReact/components/Head/index.js +10 -0
  20. data/webpack/__mocks__/foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState.js +8 -0
  21. data/webpack/__mocks__/foremanReact/components/ToastsList/index.js +3 -0
  22. data/webpack/__mocks__/foremanReact/redux/API/APIActions.js +21 -0
  23. data/webpack/__mocks__/foremanReact/redux/API/APIConstants.js +7 -0
  24. data/webpack/__mocks__/foremanReact/redux/API/index.js +14 -0
  25. data/webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware/index.js +9 -0
  26. 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: ab4f560e39c672bfce2319a0487db47a3f772a11142591262727b5a16a7a0573
4
+ data.tar.gz: 6dab6dc9a69cc43df9a13cc0173651e37639fd29f38b1153e6541567da6ff01c
5
5
  SHA512:
6
- metadata.gz: 2b2ad3235424ec6c3b0e10ca8483d9fab1f2249d6abfc8f52b641e60ea2187687ac4f670d3b3666318c0de9688a782c5ebc851352269d0f8ba36040272943b91
7
- data.tar.gz: 616cc9e8a67ed624cfa4bcd03be82454c56d770a23a621daf14964be27a4ae3d58bd2916f272d3192aafcb02978e232d7a24709ec7ad55568c4e980051eb8c78
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '13.1.1'.freeze
2
+ VERSION = '13.2.0'.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;
@@ -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 { Divider, PageSection, Flex } from '@patternfly/react-core';
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
- DATE_OPTIONS,
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 || statusLabel === STATUS.SUCCEEDED;
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
- <React.Fragment>
74
- <PageSection
75
- className="job-invocation-detail-page-section"
76
- isFilled
77
- variant="light"
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
- <Flex alignItems={{ default: 'alignItemsFlexStart' }}>
80
- <JobInvocationSystemStatusChart
81
- data={items}
82
- isAlreadyStarted={isAlreadyStarted}
83
- formattedStartDate={formattedStartDate}
84
- />
85
- <Divider
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, Button } from '@patternfly/react-core';
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,4 @@
1
+ import React from 'react';
2
+
3
+ const BreadcrumbBar = () => <div />;
4
+ export default BreadcrumbBar;
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ const Head = ({ children }) => <div>{children}</div>;
5
+
6
+ Head.propTypes = {
7
+ children: PropTypes.node.isRequired,
8
+ };
9
+
10
+ export default Head;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { translate as __ } from '../../../common/I18n';
3
+
4
+ const DefaultLoaderEmptyState = () => (
5
+ <span className="disabled-text">{__('Not available')}</span>
6
+ );
7
+
8
+ export default DefaultLoaderEmptyState;
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+
3
+ export const ToastsList = () => <div />;
@@ -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
+ };
@@ -0,0 +1,7 @@
1
+ export const API_OPERATIONS = {
2
+ GET: 'API_GET',
3
+ POST: 'API_POST',
4
+ PUT: 'API_PUT',
5
+ DELETE: 'API_DELETE',
6
+ PATCH: 'API_PATCH',
7
+ };
@@ -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
+ };
@@ -0,0 +1,9 @@
1
+ export const stopInterval = key => ({
2
+ type: 'STOP_INTERVAL',
3
+ key,
4
+ });
5
+
6
+ export const withInterval = key => ({
7
+ type: 'WITH_INTERVAL',
8
+ key,
9
+ });
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.1.1
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-23 00:00:00.000000000 Z
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