foreman_remote_execution 13.1.1 → 13.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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