foreman-tasks 12.2.3 → 13.0.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/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/TaskActions/TaskAction.test.js +212 -42
- data/webpack/ForemanTasks/Components/TaskDetails/Components/Errors.js +224 -55
- data/webpack/ForemanTasks/Components/TaskDetails/Components/Errors.scss +41 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +6 -51
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskSkeleton.js +1 -6
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Errors.test.js +159 -11
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +0 -2
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +0 -2
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +29 -15
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +1 -5
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsConstants.js +2 -1
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +20 -5
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +1 -1
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +97 -10
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetailsSelectors.test.js +81 -0
- data/webpack/ForemanTasks/Components/TaskDetails/index.js +6 -4
- data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/TasksDashboard.test.js +94 -7
- data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/TasksDashboardActions.test.js +97 -16
- data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/TasksDashboardReducer.test.js +112 -46
- data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/TasksDashboardSelectors.test.js +103 -15
- data/webpack/ForemanTasks/Components/TasksTable/TasksTableConstants.js +1 -1
- data/webpack/ForemanTasks/Components/common/taskResultIcon.js +53 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsPage.js +74 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +265 -0
- data/webpack/Routes/routes.js +6 -0
- data/webpack/Routes/routes.test.js +16 -5
- metadata +10 -7
- data/app/views/foreman_tasks/tasks/show.html.erb +0 -18
- data/webpack/ForemanTasks/Components/TaskActions/__snapshots__/TaskAction.test.js.snap +0 -233
- data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/__snapshots__/TasksDashboard.test.js.snap +0 -51
- data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/__snapshots__/TasksDashboardActions.test.js.snap +0 -151
- data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/__snapshots__/TasksDashboardReducer.test.js.snap +0 -275
- data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/__snapshots__/TasksDashboardSelectors.test.js.snap +0 -119
|
@@ -1,69 +1,238 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
-
import
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import {
|
|
5
|
+
Alert,
|
|
6
|
+
AlertVariant,
|
|
7
|
+
CodeBlock,
|
|
8
|
+
CodeBlockCode,
|
|
9
|
+
EmptyState,
|
|
10
|
+
EmptyStateBody,
|
|
11
|
+
EmptyStateHeader,
|
|
12
|
+
Icon,
|
|
13
|
+
EmptyStateVariant,
|
|
14
|
+
Flex,
|
|
15
|
+
FlexItem,
|
|
16
|
+
Grid,
|
|
17
|
+
GridItem,
|
|
18
|
+
Split,
|
|
19
|
+
SplitItem,
|
|
20
|
+
Stack,
|
|
21
|
+
StackItem,
|
|
22
|
+
Tab,
|
|
23
|
+
Tabs,
|
|
24
|
+
TabTitleIcon,
|
|
25
|
+
TabTitleText,
|
|
26
|
+
} from '@patternfly/react-core';
|
|
27
|
+
import {
|
|
28
|
+
CheckCircleIcon,
|
|
29
|
+
ExclamationCircleIcon,
|
|
30
|
+
} from '@patternfly/react-icons';
|
|
4
31
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
32
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
33
|
+
import './Errors.scss';
|
|
34
|
+
|
|
35
|
+
const isStoppedStep = step =>
|
|
36
|
+
['skipped', 'skipping'].includes(String(step.state ?? ''));
|
|
37
|
+
|
|
38
|
+
const STEP_SUMMARY_MAX_LENGTH = 120;
|
|
39
|
+
|
|
40
|
+
const getStepSummary = step =>
|
|
41
|
+
step.error?.message || step.action_class || __('Unknown error');
|
|
42
|
+
|
|
43
|
+
const truncateStepSummary = summary => {
|
|
44
|
+
if (summary.length <= STEP_SUMMARY_MAX_LENGTH) {
|
|
45
|
+
return summary;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return `${summary.slice(0, STEP_SUMMARY_MAX_LENGTH - 3)}...`;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getStepStatus = step => (isStoppedStep(step) ? 'warning' : 'danger');
|
|
52
|
+
|
|
53
|
+
const ErrorTabTitle = ({ step }) => {
|
|
54
|
+
const status = getStepStatus(step);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
<TabTitleIcon>
|
|
59
|
+
<Icon status={status}>
|
|
60
|
+
<ExclamationCircleIcon />
|
|
61
|
+
</Icon>
|
|
62
|
+
</TabTitleIcon>
|
|
63
|
+
<TabTitleText
|
|
64
|
+
className={classNames(
|
|
65
|
+
'task-errors-tab-title',
|
|
66
|
+
`task-errors-tab-title--${status}`
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
{truncateStepSummary(getStepSummary(step))}
|
|
70
|
+
</TabTitleText>
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
ErrorTabTitle.propTypes = {
|
|
76
|
+
step: PropTypes.shape({
|
|
77
|
+
action_class: PropTypes.string,
|
|
78
|
+
state: PropTypes.string,
|
|
79
|
+
error: PropTypes.shape({
|
|
80
|
+
message: PropTypes.string,
|
|
81
|
+
}),
|
|
82
|
+
}).isRequired,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const ErrorDetailSection = ({ label, children }) => (
|
|
86
|
+
<StackItem className="task-errors-detail-section">
|
|
87
|
+
<Flex
|
|
88
|
+
direction={{ default: 'column' }}
|
|
89
|
+
spaceItems={{ default: 'spaceItemsXs' }}
|
|
90
|
+
>
|
|
91
|
+
<FlexItem className="task-errors-detail-label">
|
|
92
|
+
<strong>{label}</strong>
|
|
93
|
+
</FlexItem>
|
|
94
|
+
<FlexItem>
|
|
95
|
+
<CodeBlock className="task-errors-codeblock">
|
|
96
|
+
<CodeBlockCode className="task-errors-codeblock-code">
|
|
97
|
+
{children}
|
|
98
|
+
</CodeBlockCode>
|
|
99
|
+
</CodeBlock>
|
|
100
|
+
</FlexItem>
|
|
101
|
+
</Flex>
|
|
102
|
+
</StackItem>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
ErrorDetailSection.propTypes = {
|
|
106
|
+
label: PropTypes.node.isRequired,
|
|
107
|
+
children: PropTypes.node.isRequired,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const ErrorDetailsPane = ({ step }) => {
|
|
111
|
+
if (!step) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Stack>
|
|
117
|
+
{step.error && (
|
|
118
|
+
<>
|
|
119
|
+
<ErrorDetailSection label={`${__('Exception')}:`}>
|
|
120
|
+
{step.error.exception_class}: {step.error.message}
|
|
121
|
+
</ErrorDetailSection>
|
|
122
|
+
<ErrorDetailSection label={__('Backtrace')}>
|
|
123
|
+
{(step.error.backtrace || []).join('\n')}
|
|
124
|
+
</ErrorDetailSection>
|
|
125
|
+
</>
|
|
126
|
+
)}
|
|
127
|
+
<ErrorDetailSection label={__('Input')}>{step.input}</ErrorDetailSection>
|
|
128
|
+
<ErrorDetailSection label={__('Output')}>
|
|
129
|
+
{step.output}
|
|
130
|
+
</ErrorDetailSection>
|
|
131
|
+
</Stack>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
ErrorDetailsPane.propTypes = {
|
|
136
|
+
step: PropTypes.shape({
|
|
137
|
+
input: PropTypes.node,
|
|
138
|
+
output: PropTypes.node,
|
|
139
|
+
error: PropTypes.shape({
|
|
140
|
+
exception_class: PropTypes.string,
|
|
141
|
+
message: PropTypes.string,
|
|
142
|
+
backtrace: PropTypes.array,
|
|
143
|
+
}),
|
|
144
|
+
}),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
ErrorDetailsPane.defaultProps = {
|
|
148
|
+
step: null,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const Errors = ({ executionPlan, failedSteps }) => {
|
|
152
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
setSelectedIndex(idx => {
|
|
156
|
+
if (idx >= failedSteps.length) {
|
|
157
|
+
return Math.max(0, failedSteps.length - 1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return idx;
|
|
161
|
+
});
|
|
162
|
+
}, [failedSteps.length]);
|
|
163
|
+
|
|
164
|
+
if (!executionPlan) {
|
|
9
165
|
return (
|
|
10
166
|
<Alert
|
|
11
|
-
|
|
12
|
-
|
|
167
|
+
title={__('Execution plan data not available ')}
|
|
168
|
+
variant={AlertVariant.danger}
|
|
13
169
|
ouiaId="task-errors-plan-missing"
|
|
14
|
-
|
|
15
|
-
>
|
|
16
|
-
{__('Execution plan data not available ')}
|
|
17
|
-
</Alert>
|
|
170
|
+
/>
|
|
18
171
|
);
|
|
19
|
-
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!failedSteps.length) {
|
|
20
175
|
return (
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
176
|
+
<Grid>
|
|
177
|
+
<GridItem span={12}>
|
|
178
|
+
<Flex
|
|
179
|
+
direction={{ default: 'column' }}
|
|
180
|
+
alignItems={{ default: 'alignItemsCenter' }}
|
|
181
|
+
justifyContent={{ default: 'justifyContentCenter' }}
|
|
182
|
+
fullWidth={{ default: 'fullWidth' }}
|
|
183
|
+
>
|
|
184
|
+
<FlexItem>
|
|
185
|
+
<EmptyState variant={EmptyStateVariant.full}>
|
|
186
|
+
<EmptyStateHeader
|
|
187
|
+
titleText={__('No errors found')}
|
|
188
|
+
headingLevel="h2"
|
|
189
|
+
icon={
|
|
190
|
+
<Icon size="xl" status="success">
|
|
191
|
+
<CheckCircleIcon />
|
|
192
|
+
</Icon>
|
|
193
|
+
}
|
|
194
|
+
/>
|
|
195
|
+
<EmptyStateBody>
|
|
196
|
+
{__('The task finished with no errors or warnings.')}
|
|
197
|
+
</EmptyStateBody>
|
|
198
|
+
</EmptyState>
|
|
199
|
+
</FlexItem>
|
|
200
|
+
</Flex>
|
|
201
|
+
</GridItem>
|
|
202
|
+
</Grid>
|
|
27
203
|
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const selectedStep = failedSteps[selectedIndex];
|
|
207
|
+
|
|
28
208
|
return (
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
209
|
+
<Split hasGutter>
|
|
210
|
+
<SplitItem className="task-errors-tabs-split">
|
|
211
|
+
<Tabs
|
|
212
|
+
id="task-errors-tabs"
|
|
213
|
+
ouiaId="task-errors-tabs"
|
|
214
|
+
activeKey={selectedIndex}
|
|
215
|
+
className="task-errors-tabs"
|
|
216
|
+
onSelect={(_event, tabIndex) => setSelectedIndex(Number(tabIndex))}
|
|
217
|
+
isVertical
|
|
218
|
+
isBox
|
|
219
|
+
aria-label={__('Failed task errors')}
|
|
37
220
|
>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<span>
|
|
54
|
-
<pre>
|
|
55
|
-
{step.error.exception_class}: {step.error.message}
|
|
56
|
-
</pre>
|
|
57
|
-
</span>
|
|
58
|
-
<span>{__('Backtrace')}:</span>
|
|
59
|
-
<span>
|
|
60
|
-
<pre>{(step.error.backtrace || []).join('\n')}</pre>
|
|
61
|
-
</span>
|
|
62
|
-
</React.Fragment>
|
|
63
|
-
)}
|
|
64
|
-
</Alert>
|
|
65
|
-
))}
|
|
66
|
-
</div>
|
|
221
|
+
{failedSteps.map((step, i) => (
|
|
222
|
+
<Tab
|
|
223
|
+
key={`${step.action_class}-${i}`}
|
|
224
|
+
eventKey={i}
|
|
225
|
+
ouiaId={`task-error-${i}`}
|
|
226
|
+
title={<ErrorTabTitle step={step} />}
|
|
227
|
+
aria-label={getStepSummary(step)}
|
|
228
|
+
/>
|
|
229
|
+
))}
|
|
230
|
+
</Tabs>
|
|
231
|
+
</SplitItem>
|
|
232
|
+
<SplitItem isFilled>
|
|
233
|
+
<ErrorDetailsPane step={selectedStep} />
|
|
234
|
+
</SplitItem>
|
|
235
|
+
</Split>
|
|
67
236
|
);
|
|
68
237
|
};
|
|
69
238
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
.task-details-react {
|
|
2
|
+
.task-errors-detail-section {
|
|
3
|
+
margin-top: 1rem;
|
|
4
|
+
margin-left: 1.5rem;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.task-errors-detail-label {
|
|
8
|
+
padding-bottom: 0.25rem;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.task-errors-tabs-split {
|
|
12
|
+
flex: 0 0 min(33%, 20rem);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
.task-errors-codeblock {
|
|
17
|
+
background-color: transparent;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.task-errors-codeblock-code {
|
|
21
|
+
margin: -1rem;
|
|
22
|
+
background-color: transparent;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.task-errors-tab-title {
|
|
26
|
+
word-break: break-all;
|
|
27
|
+
font-weight: var(--pf-v5-global--FontWeight--bold);
|
|
28
|
+
|
|
29
|
+
&--danger {
|
|
30
|
+
color: var(--pf-v5-global--danger-color--200);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
&--warning {
|
|
34
|
+
color: var(--pf-v5-global--warning-color--200);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.task-errors-tabs {
|
|
39
|
+
--pf-v5-c-tabs--m-vertical--MaxWidth: 100%;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -5,17 +5,12 @@ import {
|
|
|
5
5
|
GridItem,
|
|
6
6
|
Progress,
|
|
7
7
|
ProgressVariant,
|
|
8
|
-
Icon,
|
|
9
8
|
} from '@patternfly/react-core';
|
|
10
|
-
import {
|
|
11
|
-
CheckCircleIcon,
|
|
12
|
-
ExclamationCircleIcon,
|
|
13
|
-
ExclamationTriangleIcon,
|
|
14
|
-
QuestionCircleIcon,
|
|
15
|
-
} from '@patternfly/react-icons';
|
|
16
9
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
17
10
|
import RelativeDateTime from 'foremanReact/components/common/dates/RelativeDateTime';
|
|
18
11
|
|
|
12
|
+
import { taskResultIconEl } from '../../common/taskResultIcon';
|
|
13
|
+
|
|
19
14
|
const isDelayed = ({ startAt, startedAt }) => {
|
|
20
15
|
if (
|
|
21
16
|
startAt == null ||
|
|
@@ -35,41 +30,6 @@ const isDelayed = ({ startAt, startedAt }) => {
|
|
|
35
30
|
return a.getTime() !== b.getTime();
|
|
36
31
|
};
|
|
37
32
|
|
|
38
|
-
const resultIconEl = (state, result) => {
|
|
39
|
-
if (state !== 'stopped')
|
|
40
|
-
return (
|
|
41
|
-
<Icon>
|
|
42
|
-
<QuestionCircleIcon />
|
|
43
|
-
</Icon>
|
|
44
|
-
);
|
|
45
|
-
switch (result) {
|
|
46
|
-
case 'success':
|
|
47
|
-
return (
|
|
48
|
-
<Icon status="success">
|
|
49
|
-
<CheckCircleIcon />
|
|
50
|
-
</Icon>
|
|
51
|
-
);
|
|
52
|
-
case 'error':
|
|
53
|
-
return (
|
|
54
|
-
<Icon status="danger">
|
|
55
|
-
<ExclamationCircleIcon />
|
|
56
|
-
</Icon>
|
|
57
|
-
);
|
|
58
|
-
case 'warning':
|
|
59
|
-
return (
|
|
60
|
-
<Icon status="warning">
|
|
61
|
-
<ExclamationTriangleIcon />
|
|
62
|
-
</Icon>
|
|
63
|
-
);
|
|
64
|
-
default:
|
|
65
|
-
return (
|
|
66
|
-
<Icon>
|
|
67
|
-
<QuestionCircleIcon />
|
|
68
|
-
</Icon>
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
33
|
const progressVariantForResult = result => {
|
|
74
34
|
switch (result) {
|
|
75
35
|
case 'error':
|
|
@@ -117,7 +77,7 @@ const TaskInfo = props => {
|
|
|
117
77
|
title: 'Result',
|
|
118
78
|
value: (
|
|
119
79
|
<span>
|
|
120
|
-
{
|
|
80
|
+
{taskResultIconEl(state, result)} {result}
|
|
121
81
|
</span>
|
|
122
82
|
),
|
|
123
83
|
},
|
|
@@ -182,14 +142,9 @@ const TaskInfo = props => {
|
|
|
182
142
|
</React.Fragment>
|
|
183
143
|
))}
|
|
184
144
|
<GridItem span={12} className="pf-v5-u-pb-lg" />
|
|
185
|
-
<GridItem span={
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
{state}
|
|
189
|
-
</div>
|
|
190
|
-
</GridItem>
|
|
191
|
-
<GridItem span={6} className="progress-label-top-right">
|
|
192
|
-
<span>{`${progress}% ${__('Complete')}`}</span>
|
|
145
|
+
<GridItem span={12}>
|
|
146
|
+
<span className="list-group-item-heading">{__('State')}: </span>
|
|
147
|
+
{state}
|
|
193
148
|
</GridItem>
|
|
194
149
|
<GridItem span={12}>
|
|
195
150
|
<Progress
|
|
@@ -27,12 +27,7 @@ export const TaskSkeleton = () => {
|
|
|
27
27
|
</React.Fragment>
|
|
28
28
|
))}
|
|
29
29
|
<GridItem span={12} className="pf-v5-u-pb-lg" />
|
|
30
|
-
<GridItem span={
|
|
31
|
-
<div className="progress-description">
|
|
32
|
-
<Skeleton />
|
|
33
|
-
</div>
|
|
34
|
-
</GridItem>
|
|
35
|
-
<GridItem span={6} className="progress-label-top-right">
|
|
30
|
+
<GridItem span={12}>
|
|
36
31
|
<Skeleton />
|
|
37
32
|
</GridItem>
|
|
38
33
|
<GridItem span={12}>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
3
|
import '@testing-library/jest-dom';
|
|
4
4
|
|
|
5
5
|
import Errors from '../Errors';
|
|
@@ -20,6 +20,8 @@ const failedStepFixture = {
|
|
|
20
20
|
output: '{}\n',
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
const executionPlan = { state: 'paused', cancellable: false };
|
|
24
|
+
|
|
23
25
|
describe('Errors', () => {
|
|
24
26
|
it('renders warning when execution plan is missing', () => {
|
|
25
27
|
render(<Errors failedSteps={[]} executionPlan={null} />);
|
|
@@ -30,30 +32,176 @@ describe('Errors', () => {
|
|
|
30
32
|
|
|
31
33
|
it('renders success state when there are no failed steps', () => {
|
|
32
34
|
render(<Errors failedSteps={[]} executionPlan={{ state: 'paused' }} />);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
expect(
|
|
36
|
+
screen.getByRole('heading', { level: 2, name: /no errors found/i })
|
|
37
|
+
).toBeInTheDocument();
|
|
38
|
+
expect(
|
|
39
|
+
screen.getByText(/the task finished with no errors or warnings/i)
|
|
40
|
+
).toBeInTheDocument();
|
|
35
41
|
});
|
|
36
42
|
|
|
37
43
|
it('renders failed step details when failedSteps is non-empty', () => {
|
|
38
44
|
const { container } = render(
|
|
39
45
|
<Errors
|
|
40
|
-
executionPlan={
|
|
46
|
+
executionPlan={executionPlan}
|
|
41
47
|
failedSteps={[failedStepFixture]}
|
|
42
48
|
/>
|
|
43
49
|
);
|
|
44
|
-
const
|
|
50
|
+
const errorTab = container.querySelector(
|
|
45
51
|
'[data-ouia-component-id="task-error-0"]'
|
|
46
52
|
);
|
|
47
|
-
expect(
|
|
48
|
-
expect(
|
|
53
|
+
expect(errorTab).toBeInTheDocument();
|
|
54
|
+
expect(
|
|
55
|
+
screen.getByRole('tab', {
|
|
56
|
+
name: /action actions::katello::eventqueue::monitor is already active/i,
|
|
57
|
+
})
|
|
58
|
+
).toBeInTheDocument();
|
|
49
59
|
expect(
|
|
50
|
-
screen.
|
|
60
|
+
screen.getByLabelText(/failed task errors/i)
|
|
51
61
|
).toBeInTheDocument();
|
|
62
|
+
expect(screen.getByText('Input')).toBeInTheDocument();
|
|
63
|
+
expect(screen.getByText('Output')).toBeInTheDocument();
|
|
64
|
+
expect(screen.getByText('Exception:')).toBeInTheDocument();
|
|
65
|
+
expect(screen.getByText('Backtrace')).toBeInTheDocument();
|
|
52
66
|
expect(screen.getByText(/runtimeerror/i)).toBeInTheDocument();
|
|
67
|
+
expect(screen.getByText(/singleton_lock/i)).toBeInTheDocument();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('switches detail pane when a different error tab is clicked', () => {
|
|
71
|
+
const firstStep = {
|
|
72
|
+
...failedStepFixture,
|
|
73
|
+
action_class: 'Action::First',
|
|
74
|
+
input: 'INPUT_FROM_FIRST_STEP',
|
|
75
|
+
output: '{}',
|
|
76
|
+
};
|
|
77
|
+
const secondStep = {
|
|
78
|
+
...failedStepFixture,
|
|
79
|
+
action_class: 'Action::Second',
|
|
80
|
+
error: {
|
|
81
|
+
exception_class: 'StandardError',
|
|
82
|
+
message: 'second step failure',
|
|
83
|
+
backtrace: [],
|
|
84
|
+
},
|
|
85
|
+
input: 'INPUT_FROM_SECOND_STEP',
|
|
86
|
+
output: 'OUTPUT_SECOND',
|
|
87
|
+
state: 'error',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
render(
|
|
91
|
+
<Errors
|
|
92
|
+
executionPlan={executionPlan}
|
|
93
|
+
failedSteps={[firstStep, secondStep]}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(screen.getByText('INPUT_FROM_FIRST_STEP')).toBeInTheDocument();
|
|
98
|
+
expect(screen.queryByText('INPUT_FROM_SECOND_STEP')).not.toBeInTheDocument();
|
|
99
|
+
|
|
100
|
+
const tabs = screen.getAllByRole('tab');
|
|
101
|
+
expect(tabs[0]).toHaveAttribute('aria-selected', 'true');
|
|
102
|
+
expect(tabs[1]).toHaveAttribute('aria-selected', 'false');
|
|
103
|
+
|
|
104
|
+
fireEvent.click(tabs[1]);
|
|
105
|
+
|
|
106
|
+
expect(screen.getByText('INPUT_FROM_SECOND_STEP')).toBeInTheDocument();
|
|
107
|
+
expect(screen.queryByText('INPUT_FROM_FIRST_STEP')).not.toBeInTheDocument();
|
|
108
|
+
expect(tabs[0]).toHaveAttribute('aria-selected', 'false');
|
|
109
|
+
expect(tabs[1]).toHaveAttribute('aria-selected', 'true');
|
|
110
|
+
expect(screen.getByText('OUTPUT_SECOND')).toBeInTheDocument();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('clamps selection when failedSteps shrinks', () => {
|
|
114
|
+
const firstStep = {
|
|
115
|
+
...failedStepFixture,
|
|
116
|
+
action_class: 'Action::First',
|
|
117
|
+
input: 'AFTER_CLAMP',
|
|
118
|
+
output: '{}',
|
|
119
|
+
};
|
|
120
|
+
const secondStep = {
|
|
121
|
+
...failedStepFixture,
|
|
122
|
+
action_class: 'Action::Second',
|
|
123
|
+
input: 'REMOVED',
|
|
124
|
+
output: '{}',
|
|
125
|
+
state: 'error',
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const { rerender } = render(
|
|
129
|
+
<Errors
|
|
130
|
+
executionPlan={executionPlan}
|
|
131
|
+
failedSteps={[firstStep, secondStep]}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
fireEvent.click(screen.getAllByRole('tab')[1]);
|
|
136
|
+
expect(screen.getByText('REMOVED')).toBeInTheDocument();
|
|
137
|
+
|
|
138
|
+
rerender(
|
|
139
|
+
<Errors executionPlan={executionPlan} failedSteps={[firstStep]} />
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
expect(screen.getAllByRole('tab')).toHaveLength(1);
|
|
143
|
+
expect(screen.getByText('AFTER_CLAMP')).toBeInTheDocument();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('truncates long tab titles to 120 characters with ellipsis', () => {
|
|
147
|
+
const longMessage = 'x'.repeat(150);
|
|
148
|
+
const longStep = {
|
|
149
|
+
...failedStepFixture,
|
|
150
|
+
error: {
|
|
151
|
+
...failedStepFixture.error,
|
|
152
|
+
message: longMessage,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const { container } = render(
|
|
157
|
+
<Errors executionPlan={executionPlan} failedSteps={[longStep]} />
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const tabTitle = container.querySelector('.task-errors-tab-title');
|
|
161
|
+
expect(tabTitle.textContent).toHaveLength(120);
|
|
162
|
+
expect(tabTitle.textContent.endsWith('...')).toBe(true);
|
|
163
|
+
expect(
|
|
164
|
+
screen.getByRole('tab', { name: longMessage })
|
|
165
|
+
).toBeInTheDocument();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('uses warning styling for skipped steps', () => {
|
|
169
|
+
const skippedStep = {
|
|
170
|
+
action_class: 'Actions::Example',
|
|
171
|
+
state: 'skipped',
|
|
172
|
+
input: '{}',
|
|
173
|
+
output: '{}',
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const { container } = render(
|
|
177
|
+
<Errors executionPlan={executionPlan} failedSteps={[skippedStep]} />
|
|
178
|
+
);
|
|
179
|
+
|
|
53
180
|
expect(
|
|
54
|
-
screen.
|
|
55
|
-
/action actions::katello::eventqueue::monitor is already active/i
|
|
56
|
-
)
|
|
181
|
+
screen.getByRole('tab', { name: /actions::example/i })
|
|
57
182
|
).toBeInTheDocument();
|
|
183
|
+
|
|
184
|
+
const errorTab = container.querySelector(
|
|
185
|
+
'[data-ouia-component-id="task-error-0"]'
|
|
186
|
+
);
|
|
187
|
+
expect(errorTab.querySelector('.pf-m-warning')).toBeInTheDocument();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('omits exception and backtrace when step has no error object', () => {
|
|
191
|
+
const stepWithoutError = {
|
|
192
|
+
action_class: 'Actions::NoError',
|
|
193
|
+
state: 'error',
|
|
194
|
+
input: 'plain-input',
|
|
195
|
+
output: 'plain-output',
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
render(
|
|
199
|
+
<Errors executionPlan={executionPlan} failedSteps={[stepWithoutError]} />
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
expect(screen.getByText('plain-input')).toBeInTheDocument();
|
|
203
|
+
expect(screen.getByText('plain-output')).toBeInTheDocument();
|
|
204
|
+
expect(screen.queryByText('Exception:')).not.toBeInTheDocument();
|
|
205
|
+
expect(screen.queryByText('Backtrace')).not.toBeInTheDocument();
|
|
58
206
|
});
|
|
59
207
|
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, screen } from '@testing-library/react';
|
|
3
3
|
import '@testing-library/jest-dom';
|
|
4
|
-
import { STATUS } from 'foremanReact/constants';
|
|
5
4
|
|
|
6
5
|
import Task from '../Task';
|
|
7
6
|
|
|
@@ -29,7 +28,6 @@ describe('Task', () => {
|
|
|
29
28
|
parentTask="parent-id"
|
|
30
29
|
taskReload
|
|
31
30
|
canEdit
|
|
32
|
-
status={STATUS.RESOLVED}
|
|
33
31
|
taskProgressToggle={jest.fn()}
|
|
34
32
|
taskReloadStart={jest.fn()}
|
|
35
33
|
/>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
3
|
import '@testing-library/jest-dom';
|
|
4
|
-
import { STATUS } from 'foremanReact/constants';
|
|
5
4
|
import { TaskButtons } from '../TaskButtons';
|
|
6
5
|
|
|
7
6
|
const setUnlockModalOpen = jest.fn();
|
|
@@ -169,7 +168,6 @@ describe('TaskButtons', () => {
|
|
|
169
168
|
resumeTaskRequest,
|
|
170
169
|
taskProgressToggle,
|
|
171
170
|
taskReloadStart,
|
|
172
|
-
status: STATUS.RESOLVED,
|
|
173
171
|
canEdit: true,
|
|
174
172
|
resumable: true,
|
|
175
173
|
cancellable: true,
|