foreman-tasks 12.2.4 → 13.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/foreman_tasks/tasks_controller.rb +0 -5
- data/app/services/foreman_tasks/troubleshooting_help_generator.rb +1 -1
- data/config/routes.rb +3 -2
- data/lib/foreman_tasks/engine.rb +2 -2
- data/lib/foreman_tasks/version.rb +1 -1
- data/test/controllers/tasks_controller_test.rb +0 -9
- data/test/foreman_tasks_test_helper.rb +2 -2
- data/test/integration/tasks_test.rb +17 -0
- data/test/test_plugin_helper.rb +8 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +8 -70
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.js +115 -104
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.scss +45 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskHelper.js +54 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +237 -177
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskSkeleton.js +1 -6
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +27 -35
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +83 -69
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskHelper.test.js +118 -1
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskInfo.test.js +190 -20
- data/webpack/ForemanTasks/Components/TaskDetails/ExecutionDetails.js +65 -0
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +76 -65
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +1 -44
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsConstants.js +2 -1
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +20 -10
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/ExecutionDetails.test.js +194 -0
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +100 -1
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +226 -18
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetailsSelectors.test.js +81 -0
- data/webpack/ForemanTasks/Components/common/taskResultIcon.js +53 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.js +174 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.scss +5 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsPage.js +51 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsHeader.test.js +142 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +277 -0
- data/webpack/ForemanTasks/{Components/TaskDetails → Routes/ShowTaskDetails}/index.js +13 -12
- data/webpack/Routes/routes.js +6 -0
- data/webpack/Routes/routes.test.js +16 -5
- data/webpack/index.js +0 -5
- metadata +16 -3
- data/app/views/foreman_tasks/tasks/show.html.erb +0 -18
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import RunningSteps from './Components/RunningSteps';
|
|
4
|
+
import Errors from './Components/Errors';
|
|
5
|
+
|
|
6
|
+
const ExecutionDetails = ({
|
|
7
|
+
state,
|
|
8
|
+
runningSteps,
|
|
9
|
+
cancelStep,
|
|
10
|
+
id,
|
|
11
|
+
taskReload,
|
|
12
|
+
taskReloadStart,
|
|
13
|
+
executionPlan,
|
|
14
|
+
failedSteps,
|
|
15
|
+
result,
|
|
16
|
+
}) => {
|
|
17
|
+
const showingRunningSteps =
|
|
18
|
+
state === 'running' ||
|
|
19
|
+
state === 'pending' ||
|
|
20
|
+
state === 'paused' ||
|
|
21
|
+
runningSteps.length > 0;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
id="execution-details-panel"
|
|
26
|
+
data-ouia-component-id="execution-details-panel"
|
|
27
|
+
>
|
|
28
|
+
{showingRunningSteps ? (
|
|
29
|
+
<RunningSteps
|
|
30
|
+
executionPlan={executionPlan}
|
|
31
|
+
result={result}
|
|
32
|
+
runningSteps={runningSteps}
|
|
33
|
+
id={id}
|
|
34
|
+
cancelStep={cancelStep}
|
|
35
|
+
taskReload={taskReload}
|
|
36
|
+
taskReloadStart={taskReloadStart}
|
|
37
|
+
/>
|
|
38
|
+
) : (
|
|
39
|
+
<Errors executionPlan={executionPlan} failedSteps={failedSteps} />
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
ExecutionDetails.propTypes = {
|
|
46
|
+
state: PropTypes.string,
|
|
47
|
+
result: PropTypes.string,
|
|
48
|
+
runningSteps: PropTypes.array,
|
|
49
|
+
cancelStep: PropTypes.func.isRequired,
|
|
50
|
+
id: PropTypes.string.isRequired,
|
|
51
|
+
taskReload: PropTypes.bool.isRequired,
|
|
52
|
+
taskReloadStart: PropTypes.func.isRequired,
|
|
53
|
+
executionPlan: PropTypes.shape({}),
|
|
54
|
+
failedSteps: PropTypes.array,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
ExecutionDetails.defaultProps = {
|
|
58
|
+
state: '',
|
|
59
|
+
result: undefined,
|
|
60
|
+
runningSteps: [],
|
|
61
|
+
executionPlan: {},
|
|
62
|
+
failedSteps: [],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default ExecutionDetails;
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { Tabs, Tab, TabTitleText } from '@patternfly/react-core';
|
|
4
|
-
import { translate as __
|
|
4
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
5
|
import { STATUS } from 'foremanReact/constants';
|
|
6
|
-
import
|
|
6
|
+
import { usePermissions } from 'foremanReact/common/hooks/Permissions/permissionHooks';
|
|
7
|
+
import { ResourceLoadFailedEmptyState } from 'foremanReact/components/common/EmptyState';
|
|
7
8
|
import Task from './Components/Task';
|
|
8
|
-
import RunningSteps from './Components/RunningSteps';
|
|
9
|
-
import Errors from './Components/Errors';
|
|
10
9
|
import Locks from './Components/Locks';
|
|
11
10
|
import Raw from './Components/Raw';
|
|
11
|
+
import ExecutionDetails from './ExecutionDetails';
|
|
12
12
|
import Dependencies from './Components/Dependencies';
|
|
13
|
-
import {
|
|
13
|
+
import { TASKS_PATH, VIEW_FOREMAN_TASKS } from './TaskDetailsConstants';
|
|
14
14
|
import { TaskSkeleton } from './Components/TaskSkeleton';
|
|
15
15
|
|
|
16
16
|
import './TaskDetails.scss';
|
|
17
17
|
|
|
18
|
+
export const TASK_DETAILS_TAB_KEYS = Object.freeze({
|
|
19
|
+
EXECUTION: 'execution',
|
|
20
|
+
DEPENDENCIES: 'dependencies',
|
|
21
|
+
LOCKS: 'locks',
|
|
22
|
+
RAW: 'raw',
|
|
23
|
+
});
|
|
24
|
+
|
|
18
25
|
const TaskDetails = ({
|
|
19
26
|
executionPlan,
|
|
20
27
|
failedSteps,
|
|
@@ -26,12 +33,15 @@ const TaskDetails = ({
|
|
|
26
33
|
cancelStep,
|
|
27
34
|
taskReloadStart,
|
|
28
35
|
taskReloadStop,
|
|
29
|
-
|
|
36
|
+
apiStatus,
|
|
37
|
+
apiErrorMessage,
|
|
38
|
+
apiErrorCode,
|
|
39
|
+
id,
|
|
30
40
|
...props
|
|
31
41
|
}) => {
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
42
|
+
const { taskReload, isLoading } = props;
|
|
43
|
+
const [activeTab, setActiveTab] = useState(TASK_DETAILS_TAB_KEYS.EXECUTION);
|
|
44
|
+
const hasViewPermission = usePermissions([VIEW_FOREMAN_TASKS]);
|
|
35
45
|
|
|
36
46
|
useEffect(() => {
|
|
37
47
|
taskReloadStart(id);
|
|
@@ -48,74 +58,83 @@ const TaskDetails = ({
|
|
|
48
58
|
}
|
|
49
59
|
};
|
|
50
60
|
|
|
51
|
-
if (
|
|
61
|
+
if (apiStatus === STATUS.ERROR || !hasViewPermission) {
|
|
52
62
|
return (
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
<ResourceLoadFailedEmptyState
|
|
64
|
+
resourceLabel={__('task')}
|
|
65
|
+
resourceId={id}
|
|
66
|
+
httpStatus={apiErrorCode}
|
|
67
|
+
errorMessage={apiErrorMessage}
|
|
68
|
+
viewPermissions={['view_foreman_tasks']}
|
|
69
|
+
requiredPermissions={['view_foreman_tasks']}
|
|
70
|
+
ouiaIdPrefix="task-details-empty-state"
|
|
71
|
+
primaryAction={{
|
|
72
|
+
label: __('Back to tasks'),
|
|
73
|
+
url: TASKS_PATH,
|
|
74
|
+
ouiaId: 'task-details-empty-state-tasks-list',
|
|
75
|
+
}}
|
|
57
76
|
/>
|
|
58
77
|
);
|
|
59
78
|
}
|
|
79
|
+
|
|
60
80
|
const resumable = executionPlan ? executionPlan.state === 'paused' : false;
|
|
61
81
|
const cancellable = executionPlan ? executionPlan.cancellable : false;
|
|
62
82
|
const lockRecords = locks.concat(links);
|
|
63
83
|
|
|
64
|
-
const
|
|
84
|
+
const taskProps = {
|
|
65
85
|
...props,
|
|
66
86
|
cancellable,
|
|
67
87
|
resumable,
|
|
68
88
|
id,
|
|
69
|
-
status,
|
|
70
89
|
taskProgressToggle,
|
|
71
90
|
taskReloadStart,
|
|
72
91
|
};
|
|
73
92
|
|
|
74
93
|
return (
|
|
75
|
-
<div className="task-details-react
|
|
94
|
+
<div className="task-details-react">
|
|
95
|
+
<section className="task-details-overview-section">
|
|
96
|
+
{isLoading ? <TaskSkeleton /> : <Task {...taskProps} />}
|
|
97
|
+
</section>
|
|
76
98
|
<Tabs
|
|
99
|
+
aria-label={__('Task details')}
|
|
77
100
|
id="task-details-tabs"
|
|
78
101
|
ouiaId="task-details-tabs"
|
|
79
|
-
activeKey={
|
|
80
|
-
onSelect={(_event, tabKey) => setActiveTabKey(tabKey)}
|
|
102
|
+
activeKey={activeTab}
|
|
81
103
|
mountOnEnter
|
|
104
|
+
onSelect={(_e, tabKey) => setActiveTab(tabKey)}
|
|
82
105
|
>
|
|
83
106
|
<Tab
|
|
84
|
-
eventKey={1}
|
|
85
|
-
title={<TabTitleText>{__('Task')}</TabTitleText>}
|
|
86
|
-
aria-label={__('Task')}
|
|
87
|
-
ouiaId="task-details-tab-task"
|
|
88
|
-
>
|
|
89
|
-
{isLoading ? <TaskSkeleton /> : <Task {...taskComponentProps} />}
|
|
90
|
-
</Tab>
|
|
91
|
-
<Tab
|
|
92
|
-
eventKey={2}
|
|
93
|
-
title={<TabTitleText>{__('Running Steps')}</TabTitleText>}
|
|
94
107
|
isDisabled={isLoading}
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
eventKey={TASK_DETAILS_TAB_KEYS.EXECUTION}
|
|
109
|
+
title={<TabTitleText>{__('Execution details')}</TabTitleText>}
|
|
110
|
+
aria-label={__('Execution details')}
|
|
111
|
+
ouiaId="task-details-tab-execution-details"
|
|
97
112
|
>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
{!isLoading && (
|
|
114
|
+
<ExecutionDetails
|
|
115
|
+
state={props.state}
|
|
116
|
+
result={props.result}
|
|
117
|
+
runningSteps={runningSteps}
|
|
118
|
+
cancelStep={cancelStep}
|
|
119
|
+
id={id}
|
|
120
|
+
taskReload={taskReload}
|
|
121
|
+
taskReloadStart={taskReloadStart}
|
|
122
|
+
executionPlan={executionPlan}
|
|
123
|
+
failedSteps={failedSteps}
|
|
124
|
+
/>
|
|
125
|
+
)}
|
|
107
126
|
</Tab>
|
|
108
127
|
<Tab
|
|
109
|
-
eventKey={
|
|
110
|
-
title={<TabTitleText>{__('Errors')}</TabTitleText>}
|
|
128
|
+
eventKey={TASK_DETAILS_TAB_KEYS.DEPENDENCIES}
|
|
111
129
|
isDisabled={isLoading}
|
|
112
|
-
|
|
113
|
-
|
|
130
|
+
title={<TabTitleText>{__('Dependencies')}</TabTitleText>}
|
|
131
|
+
aria-label={__('Dependencies')}
|
|
132
|
+
ouiaId="task-details-tab-dependencies"
|
|
114
133
|
>
|
|
115
|
-
<
|
|
134
|
+
<Dependencies dependsOn={dependsOn} blocks={blocks} />
|
|
116
135
|
</Tab>
|
|
117
136
|
<Tab
|
|
118
|
-
eventKey={
|
|
137
|
+
eventKey={TASK_DETAILS_TAB_KEYS.LOCKS}
|
|
119
138
|
title={<TabTitleText>{__('Locks')}</TabTitleText>}
|
|
120
139
|
isDisabled={isLoading}
|
|
121
140
|
aria-label={__('Locks')}
|
|
@@ -124,16 +143,7 @@ const TaskDetails = ({
|
|
|
124
143
|
<Locks locks={lockRecords} />
|
|
125
144
|
</Tab>
|
|
126
145
|
<Tab
|
|
127
|
-
eventKey={
|
|
128
|
-
title={<TabTitleText>{__('Dependencies')}</TabTitleText>}
|
|
129
|
-
isDisabled={isLoading}
|
|
130
|
-
aria-label={__('Dependencies')}
|
|
131
|
-
ouiaId="task-details-tab-dependencies"
|
|
132
|
-
>
|
|
133
|
-
<Dependencies dependsOn={dependsOn} blocks={blocks} />
|
|
134
|
-
</Tab>
|
|
135
|
-
<Tab
|
|
136
|
-
eventKey={6}
|
|
146
|
+
eventKey={TASK_DETAILS_TAB_KEYS.RAW}
|
|
137
147
|
title={<TabTitleText>{__('Raw')}</TabTitleText>}
|
|
138
148
|
isDisabled={isLoading}
|
|
139
149
|
aria-label={__('Raw')}
|
|
@@ -155,36 +165,37 @@ const TaskDetails = ({
|
|
|
155
165
|
};
|
|
156
166
|
|
|
157
167
|
TaskDetails.propTypes = {
|
|
168
|
+
id: PropTypes.string.isRequired,
|
|
158
169
|
label: PropTypes.string,
|
|
170
|
+
result: PropTypes.string,
|
|
159
171
|
runningSteps: PropTypes.array,
|
|
160
172
|
cancelStep: PropTypes.func.isRequired,
|
|
161
173
|
taskReload: PropTypes.bool.isRequired,
|
|
162
|
-
|
|
163
|
-
|
|
174
|
+
apiStatus: PropTypes.oneOf(Object.keys(STATUS)),
|
|
175
|
+
apiErrorMessage: PropTypes.string,
|
|
176
|
+
apiErrorCode: PropTypes.number,
|
|
164
177
|
taskReloadStop: PropTypes.func.isRequired,
|
|
165
178
|
taskReloadStart: PropTypes.func.isRequired,
|
|
166
179
|
links: PropTypes.array,
|
|
167
180
|
dependsOn: PropTypes.array,
|
|
168
181
|
blocks: PropTypes.array,
|
|
182
|
+
executionPlan: PropTypes.shape({}),
|
|
183
|
+
failedSteps: PropTypes.array,
|
|
169
184
|
...Task.propTypes,
|
|
170
|
-
...Errors.propTypes,
|
|
171
185
|
...Locks.propTypes,
|
|
172
|
-
...Dependencies.propTypes,
|
|
173
186
|
...Raw.propTypes,
|
|
174
187
|
};
|
|
175
188
|
TaskDetails.defaultProps = {
|
|
176
189
|
label: '',
|
|
177
190
|
runningSteps: [],
|
|
178
|
-
|
|
179
|
-
status: STATUS.PENDING,
|
|
191
|
+
apiErrorMessage: '',
|
|
180
192
|
links: [],
|
|
181
193
|
dependsOn: [],
|
|
182
194
|
blocks: [],
|
|
195
|
+
failedSteps: [],
|
|
196
|
+
executionPlan: {},
|
|
183
197
|
...Task.defaultProps,
|
|
184
|
-
...RunningSteps.defaultProps,
|
|
185
|
-
...Errors.defaultProps,
|
|
186
198
|
...Locks.defaultProps,
|
|
187
|
-
...Dependencies.defaultProps,
|
|
188
199
|
...Raw.defaultProps,
|
|
189
200
|
};
|
|
190
201
|
|
|
@@ -1,54 +1,11 @@
|
|
|
1
1
|
.task-details-react {
|
|
2
|
-
.progress-label-top-right {
|
|
3
|
-
font-size: 11px;
|
|
4
|
-
text-align: right;
|
|
5
|
-
}
|
|
6
2
|
a,
|
|
7
|
-
button {
|
|
3
|
+
#pf-tab-section-1-task-details-tabs > button {
|
|
8
4
|
margin-right: 3px;
|
|
9
5
|
}
|
|
10
6
|
.container {
|
|
11
7
|
margin: 0;
|
|
12
8
|
}
|
|
13
|
-
.spin {
|
|
14
|
-
-webkit-animation: spin 1s infinite linear;
|
|
15
|
-
-moz-animation: spin 1s infinite linear;
|
|
16
|
-
-o-animation: spin 1s infinite linear;
|
|
17
|
-
animation: spin 1s infinite linear;
|
|
18
|
-
-webkit-transform-origin: 50% 50%;
|
|
19
|
-
transform-origin: 50% 50%;
|
|
20
|
-
-ms-transform-origin: 50% 50%; /* IE 9 */
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
@-moz-keyframes spin {
|
|
24
|
-
from {
|
|
25
|
-
-moz-transform: rotate(0deg);
|
|
26
|
-
}
|
|
27
|
-
to {
|
|
28
|
-
-moz-transform: rotate(360deg);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
@-webkit-keyframes spin {
|
|
33
|
-
from {
|
|
34
|
-
-webkit-transform: rotate(0deg);
|
|
35
|
-
}
|
|
36
|
-
to {
|
|
37
|
-
-webkit-transform: rotate(360deg);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
@keyframes spin {
|
|
42
|
-
from {
|
|
43
|
-
transform: rotate(0deg);
|
|
44
|
-
}
|
|
45
|
-
to {
|
|
46
|
-
transform: rotate(360deg);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
.dynflow-button > span {
|
|
50
|
-
pointer-events: auto;
|
|
51
|
-
}
|
|
52
9
|
|
|
53
10
|
pre {
|
|
54
11
|
white-space: pre-wrap;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const FOREMAN_TASK_DETAILS = 'FOREMAN_TASK_DETAILS';
|
|
2
2
|
export const FOREMAN_TASK_DETAILS_SUCCESS = 'FOREMAN_TASK_DETAILS_SUCCESS';
|
|
3
|
-
|
|
3
|
+
export const TASKS_PATH = '/foreman_tasks/tasks';
|
|
4
4
|
export const TASK_STEP_CANCEL = 'TASK_STEP_CANCEL';
|
|
5
|
+
export const VIEW_FOREMAN_TASKS = 'view_foreman_tasks';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* eslint-disable camelcase */
|
|
2
2
|
import {
|
|
3
3
|
selectAPIResponse,
|
|
4
|
-
|
|
4
|
+
selectAPIStatus as selectAPIStatusByKey,
|
|
5
|
+
selectAPIError as selectAPIErrorByKey,
|
|
5
6
|
} from 'foremanReact/redux/API/APISelectors';
|
|
6
7
|
import { selectDoesIntervalExist } from 'foremanReact/redux/middlewares/IntervalMiddleware/IntervalSelectors';
|
|
7
8
|
import { STATUS } from 'foremanReact/constants';
|
|
@@ -38,11 +39,6 @@ export const selectResumable = state =>
|
|
|
38
39
|
export const selectCancellable = state =>
|
|
39
40
|
selectTaskDetailsResponse(state).cancellable || false;
|
|
40
41
|
|
|
41
|
-
export const selectErrors = state => {
|
|
42
|
-
const { humanized } = selectTaskDetailsResponse(state);
|
|
43
|
-
return humanized ? humanized.errors : [];
|
|
44
|
-
};
|
|
45
|
-
|
|
46
42
|
export const selectProgress = state =>
|
|
47
43
|
selectTaskDetailsResponse(state).progress
|
|
48
44
|
? Math.trunc(selectTaskDetailsResponse(state).progress * 100)
|
|
@@ -101,14 +97,28 @@ export const selectDynflowEnableConsole = state =>
|
|
|
101
97
|
export const selectCanEdit = state =>
|
|
102
98
|
selectTaskDetailsResponse(state).can_edit || false;
|
|
103
99
|
|
|
104
|
-
export const
|
|
100
|
+
export const selectAPIStatus = state =>
|
|
101
|
+
selectAPIStatusByKey(state, FOREMAN_TASK_DETAILS);
|
|
105
102
|
|
|
106
103
|
export const selectAPIError = state =>
|
|
107
|
-
|
|
104
|
+
selectAPIErrorByKey(state, FOREMAN_TASK_DETAILS);
|
|
105
|
+
|
|
106
|
+
export const selectAPIErrorMessage = state => {
|
|
107
|
+
const apiError = selectAPIError(state);
|
|
108
|
+
|
|
109
|
+
if (apiError?.response?.data?.error) {
|
|
110
|
+
return apiError.response.data.error.message;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return apiError?.message;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const selectAPIErrorCode = state =>
|
|
117
|
+
selectAPIError(state)?.response?.status;
|
|
108
118
|
|
|
109
119
|
export const selectIsLoading = state =>
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
!Object.keys(selectTaskDetailsResponse(state) ?? {}).length &&
|
|
121
|
+
selectAPIStatus(state) === STATUS.PENDING;
|
|
112
122
|
|
|
113
123
|
export const selectDependsOn = state =>
|
|
114
124
|
selectTaskDetailsResponse(state).depends_on || [];
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
|
|
5
|
+
import ExecutionDetails from '../ExecutionDetails';
|
|
6
|
+
import {
|
|
7
|
+
fixtureFailedExecutionDetail,
|
|
8
|
+
fixtureRunningExecutionDetail,
|
|
9
|
+
} from './TaskDetails.fixtures';
|
|
10
|
+
|
|
11
|
+
const rtlBaseProps = {
|
|
12
|
+
cancelStep: jest.fn(),
|
|
13
|
+
taskReloadStart: jest.fn(),
|
|
14
|
+
id: 'a15dd820-32f1-4ced-9ab7-c0fab8234c47',
|
|
15
|
+
taskReload: false,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('ExecutionDetails', () => {
|
|
19
|
+
it('renders execution details panel with OUIA id', () => {
|
|
20
|
+
render(
|
|
21
|
+
<ExecutionDetails
|
|
22
|
+
{...rtlBaseProps}
|
|
23
|
+
state="stopped"
|
|
24
|
+
runningSteps={[]}
|
|
25
|
+
executionPlan={{ state: 'stopped', cancellable: false }}
|
|
26
|
+
failedSteps={[]}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(
|
|
31
|
+
document.querySelector('[data-ouia-component-id="execution-details-panel"]')
|
|
32
|
+
).toBeInTheDocument();
|
|
33
|
+
expect(document.getElementById('execution-details-panel')).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('shows Errors pane when stopped with no running steps', () => {
|
|
37
|
+
render(
|
|
38
|
+
<ExecutionDetails
|
|
39
|
+
{...rtlBaseProps}
|
|
40
|
+
state="stopped"
|
|
41
|
+
runningSteps={[]}
|
|
42
|
+
executionPlan={{ state: 'stopped', cancellable: false }}
|
|
43
|
+
failedSteps={[]}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(screen.getByRole('heading', { name: /^no errors found$/i })).toBeInTheDocument();
|
|
48
|
+
expect(
|
|
49
|
+
screen.getByText(/the task finished with no errors or warnings/i)
|
|
50
|
+
).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('shows RunningSteps when stopped but runningSteps list is non-empty', () => {
|
|
54
|
+
render(
|
|
55
|
+
<ExecutionDetails
|
|
56
|
+
{...rtlBaseProps}
|
|
57
|
+
state="stopped"
|
|
58
|
+
runningSteps={[
|
|
59
|
+
{
|
|
60
|
+
cancellable: false,
|
|
61
|
+
id: 1,
|
|
62
|
+
action_class: 'Actions::Stale',
|
|
63
|
+
state: 'paused',
|
|
64
|
+
input: '{}',
|
|
65
|
+
output: '{}',
|
|
66
|
+
},
|
|
67
|
+
]}
|
|
68
|
+
executionPlan={{ state: 'stopped', cancellable: false }}
|
|
69
|
+
failedSteps={[]}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(
|
|
74
|
+
screen.getByRole('heading', { name: 'Warning alert: Running step 1' })
|
|
75
|
+
).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('forwards executionPlan and result to RunningSteps when state is pending without steps', () => {
|
|
79
|
+
render(
|
|
80
|
+
<ExecutionDetails
|
|
81
|
+
{...rtlBaseProps}
|
|
82
|
+
state="pending"
|
|
83
|
+
runningSteps={[]}
|
|
84
|
+
executionPlan={{ state: 'planned', cancellable: false }}
|
|
85
|
+
result="pending"
|
|
86
|
+
failedSteps={[]}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(
|
|
91
|
+
screen.getByRole('heading', { level: 2, name: /planned task/i })
|
|
92
|
+
).toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('shows temporarily suspended messaging when pending, plan running, result pending', () => {
|
|
96
|
+
render(
|
|
97
|
+
<ExecutionDetails
|
|
98
|
+
{...rtlBaseProps}
|
|
99
|
+
state="pending"
|
|
100
|
+
runningSteps={[]}
|
|
101
|
+
executionPlan={{ state: 'running', cancellable: false }}
|
|
102
|
+
result="pending"
|
|
103
|
+
failedSteps={[]}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(
|
|
108
|
+
screen.getByRole('heading', {
|
|
109
|
+
level: 4,
|
|
110
|
+
name: /temporarily suspended step/i,
|
|
111
|
+
})
|
|
112
|
+
).toBeInTheDocument();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('routes failed steps through Errors pane when stopped', () => {
|
|
116
|
+
render(
|
|
117
|
+
<ExecutionDetails
|
|
118
|
+
{...rtlBaseProps}
|
|
119
|
+
{...fixtureFailedExecutionDetail}
|
|
120
|
+
executionPlan={fixtureFailedExecutionDetail.executionPlan}
|
|
121
|
+
failedSteps={fixtureFailedExecutionDetail.failedSteps}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
expect(
|
|
126
|
+
screen.getByRole('tab', {
|
|
127
|
+
name: /action actions::katello::eventqueue::monitor is already active/i,
|
|
128
|
+
})
|
|
129
|
+
).toBeInTheDocument();
|
|
130
|
+
expect(
|
|
131
|
+
screen.getByLabelText(/failed task errors/i)
|
|
132
|
+
).toBeInTheDocument();
|
|
133
|
+
expect(screen.getByText('Exception:')).toBeInTheDocument();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('renders RunningSteps with cancel when task is running', () => {
|
|
137
|
+
const cancelStep = jest.fn();
|
|
138
|
+
|
|
139
|
+
render(
|
|
140
|
+
<ExecutionDetails
|
|
141
|
+
{...rtlBaseProps}
|
|
142
|
+
cancelStep={cancelStep}
|
|
143
|
+
{...fixtureRunningExecutionDetail}
|
|
144
|
+
runningSteps={[
|
|
145
|
+
{
|
|
146
|
+
...fixtureRunningExecutionDetail.runningSteps[0],
|
|
147
|
+
cancellable: true,
|
|
148
|
+
},
|
|
149
|
+
]}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('shows RunningSteps when state is paused without running steps', () => {
|
|
157
|
+
render(
|
|
158
|
+
<ExecutionDetails
|
|
159
|
+
{...rtlBaseProps}
|
|
160
|
+
state="paused"
|
|
161
|
+
runningSteps={[]}
|
|
162
|
+
executionPlan={{ state: 'paused', cancellable: false }}
|
|
163
|
+
failedSteps={[]}
|
|
164
|
+
/>
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
expect(screen.getByText(/no running steps/i)).toBeInTheDocument();
|
|
168
|
+
expect(
|
|
169
|
+
screen.queryByRole('heading', { name: /^no errors found$/i })
|
|
170
|
+
).not.toBeInTheDocument();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('shows RunningSteps when state is paused with running steps', () => {
|
|
174
|
+
render(
|
|
175
|
+
<ExecutionDetails
|
|
176
|
+
{...rtlBaseProps}
|
|
177
|
+
state="paused"
|
|
178
|
+
runningSteps={[
|
|
179
|
+
{
|
|
180
|
+
...fixtureRunningExecutionDetail.runningSteps[0],
|
|
181
|
+
state: 'paused',
|
|
182
|
+
},
|
|
183
|
+
]}
|
|
184
|
+
executionPlan={{ state: 'paused', cancellable: false }}
|
|
185
|
+
failedSteps={[]}
|
|
186
|
+
/>
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(
|
|
190
|
+
screen.getByRole('heading', { name: 'Warning alert: Running step 1' })
|
|
191
|
+
).toBeInTheDocument();
|
|
192
|
+
expect(screen.getByText('paused')).toBeInTheDocument();
|
|
193
|
+
});
|
|
194
|
+
});
|