foreman-tasks 13.0.0 → 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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/app/services/foreman_tasks/troubleshooting_help_generator.rb +1 -1
  3. data/lib/foreman_tasks/version.rb +1 -1
  4. data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +8 -70
  5. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.js +115 -104
  6. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.scss +45 -0
  7. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskHelper.js +54 -0
  8. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +236 -131
  9. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +27 -33
  10. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +83 -67
  11. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskHelper.test.js +118 -1
  12. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskInfo.test.js +190 -20
  13. data/webpack/ForemanTasks/Components/TaskDetails/ExecutionDetails.js +65 -0
  14. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +48 -51
  15. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +0 -39
  16. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +0 -5
  17. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/ExecutionDetails.test.js +194 -0
  18. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +99 -0
  19. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +129 -8
  20. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.js +174 -0
  21. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.scss +5 -0
  22. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsPage.js +10 -33
  23. data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsHeader.test.js +142 -0
  24. data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +100 -88
  25. data/webpack/ForemanTasks/{Components/TaskDetails → Routes/ShowTaskDetails}/index.js +7 -8
  26. data/webpack/Routes/routes.js +1 -1
  27. data/webpack/Routes/routes.test.js +1 -1
  28. data/webpack/index.js +0 -5
  29. metadata +8 -2
@@ -1,36 +1,40 @@
1
- import React from 'react';
1
+ import React, { useState } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import {
4
+ ClipboardCopy,
5
+ ExpandableSection,
6
+ ExpandableSectionToggle,
7
+ Flex,
8
+ FlexItem,
4
9
  Grid,
5
10
  GridItem,
6
11
  Progress,
7
12
  ProgressVariant,
13
+ Split,
14
+ SplitItem,
15
+ Text,
16
+ TextVariants,
8
17
  } from '@patternfly/react-core';
9
- import { translate as __ } from 'foremanReact/common/I18n';
18
+ import capitalize from 'lodash/capitalize';
19
+ import { translate as __, sprintf } from 'foremanReact/common/I18n';
10
20
  import RelativeDateTime from 'foremanReact/components/common/dates/RelativeDateTime';
11
21
 
12
22
  import { taskResultIconEl } from '../../common/taskResultIcon';
23
+ import {
24
+ formatTaskDuration,
25
+ isDelayed,
26
+ parseUsernameLinkHref,
27
+ } from './TaskHelper';
13
28
 
14
- const isDelayed = ({ startAt, startedAt }) => {
15
- if (
16
- startAt == null ||
17
- startedAt == null ||
18
- startAt === '' ||
19
- startedAt === ''
20
- ) {
21
- return false;
22
- }
23
- const a = new Date(startAt);
24
- const b = new Date(startedAt);
25
- if (Number.isNaN(a.getTime()) || Number.isNaN(b.getTime())) {
26
- return false;
29
+ const formatResultLabel = result => {
30
+ if (!result || typeof result !== 'string') {
31
+ return __('N/A');
27
32
  }
28
- a.setMilliseconds(0);
29
- b.setMilliseconds(0);
30
- return a.getTime() !== b.getTime();
33
+
34
+ return capitalize(result);
31
35
  };
32
36
 
33
- const progressVariantForResult = result => {
37
+ const progressVariant = result => {
34
38
  switch (result) {
35
39
  case 'error':
36
40
  return ProgressVariant.danger;
@@ -43,135 +47,234 @@ const progressVariantForResult = result => {
43
47
  }
44
48
  };
45
49
 
50
+ const MetadataField = ({ title, value, className, labelOuiaId, span }) => (
51
+ <GridItem
52
+ {...(span ? { span } : { xl: 2, lg: 4, md: 6, sm: 12 })}
53
+ className={className}
54
+ >
55
+ <Flex
56
+ direction={{ default: 'column' }}
57
+ spaceItems={{ default: 'spaceItemsXs' }}
58
+ >
59
+ <FlexItem>
60
+ <Text ouiaId={labelOuiaId} component={TextVariants.small}>
61
+ {title}
62
+ </Text>
63
+ </FlexItem>
64
+ <FlexItem>{value}</FlexItem>
65
+ </Flex>
66
+ </GridItem>
67
+ );
68
+
69
+ MetadataField.propTypes = {
70
+ title: PropTypes.node.isRequired,
71
+ value: PropTypes.node.isRequired,
72
+ className: PropTypes.string,
73
+ labelOuiaId: PropTypes.string.isRequired,
74
+ span: PropTypes.number,
75
+ };
76
+
77
+ MetadataField.defaultProps = {
78
+ className: undefined,
79
+ span: undefined,
80
+ };
81
+
82
+ const copyableText = text =>
83
+ text ? (
84
+ <ClipboardCopy
85
+ variant="inline-compact"
86
+ hoverTip={__('Copy')}
87
+ clickTip={__('Copied')}
88
+ >
89
+ {text}
90
+ </ClipboardCopy>
91
+ ) : (
92
+ __('N/A')
93
+ );
94
+
46
95
  const TaskInfo = props => {
47
96
  const {
48
97
  action,
49
98
  endedAt,
99
+ externalId,
100
+ id,
101
+ label,
50
102
  result,
51
103
  startAt,
52
- startBefore,
53
104
  startedAt,
105
+ startBefore,
54
106
  state,
55
107
  help,
56
- output,
57
- errors,
58
108
  progress,
59
109
  username,
60
110
  usernamePath,
61
111
  } = props;
62
112
 
63
- const details = [
64
- [
65
- {
66
- title: 'Name',
67
- value: action || __('N/A'),
68
- className: 'details-name',
69
- },
70
- {
71
- title: 'Start at',
72
- value: <RelativeDateTime defaultValue={__('N/A')} date={startAt} />,
73
- },
74
- ],
75
- [
76
- {
77
- title: 'Result',
78
- value: (
79
- <span>
80
- {taskResultIconEl(state, result)} {result}
81
- </span>
82
- ),
83
- },
84
- {
85
- title: 'Started at',
86
- value: <RelativeDateTime defaultValue={__('N/A')} date={startedAt} />,
87
- },
88
- ],
89
- [
90
- {
91
- title: 'Triggered by',
92
- value: (usernamePath || '').includes('href') ? (
93
- <a href={usernamePath.split('"')[1]}>{username}</a>
94
- ) : (
95
- username || ''
96
- ),
97
- },
98
- {
99
- title: 'Ended at',
100
- value: <RelativeDateTime defaultValue={__('N/A')} date={endedAt} />,
101
- },
102
- ],
103
- [
104
- {
105
- title: 'Execution type',
106
- value: __(isDelayed(props) ? 'Delayed' : 'Immediate'),
107
- },
108
- {
109
- title: 'Start before',
110
- value: startBefore ? (
111
- <RelativeDateTime defaultValue={__('N/A')} date={startBefore} />
112
- ) : (
113
- '-'
114
- ),
115
- },
116
- ],
117
- ];
118
-
119
- const progVariant = progressVariantForResult(result);
113
+ const [isExpanded, setIsExpanded] = useState(false);
114
+ const baseId = id || 'task-info';
115
+ const contentId = `${baseId}-more-details-content`;
116
+ const toggleId = `${baseId}-more-details-toggle`;
117
+
118
+ const usernameHref = parseUsernameLinkHref(usernamePath)?.startsWith('/')
119
+ ? parseUsernameLinkHref(usernamePath)
120
+ : null;
121
+ const triggeredBy = usernameHref ? (
122
+ <a href={usernameHref}>{username}</a>
123
+ ) : (
124
+ username || ''
125
+ );
126
+
127
+ const progVariant = progressVariant(result);
120
128
 
121
129
  return (
122
- <Grid>
123
- <GridItem span={12} className="pf-v5-u-pb-lg" />
124
- {details.map((items, key) => (
125
- <React.Fragment key={key}>
126
- <GridItem md={2} sm={6}>
127
- <span className="list-group-item-heading">
128
- {__(items[0].title)}:
129
- </span>
130
- </GridItem>
131
- <GridItem md={5} sm={6} className={items[0].className}>
132
- <span>{items[0].value}</span>
133
- </GridItem>
134
- <GridItem md={2} sm={6}>
135
- <span className="list-group-item-heading">
136
- {__(items[1].title)}:
137
- </span>
138
- </GridItem>
139
- <GridItem md={3} sm={6} className={items[1].className}>
140
- {items[1].value}
141
- </GridItem>
142
- </React.Fragment>
143
- ))}
144
- <GridItem span={12} className="pf-v5-u-pb-lg" />
130
+ <Grid className="task-details-overview" hasGutter>
131
+ <MetadataField
132
+ labelOuiaId="task-info-metadata-result-label"
133
+ title={__('Result')}
134
+ value={
135
+ <>
136
+ {taskResultIconEl(state, result)}
137
+ <span> {formatResultLabel(result)}</span>
138
+ </>
139
+ }
140
+ />
141
+ <MetadataField
142
+ labelOuiaId="task-info-metadata-state-label"
143
+ title={__('State')}
144
+ value={capitalize(state) || __('N/A')}
145
+ />
146
+ <MetadataField
147
+ labelOuiaId="task-info-metadata-started-at-label"
148
+ title={__('Started at')}
149
+ value={<RelativeDateTime defaultValue={__('N/A')} date={startedAt} />}
150
+ />
151
+ <MetadataField
152
+ labelOuiaId="task-info-metadata-duration-label"
153
+ title={__('Duration')}
154
+ value={formatTaskDuration(startedAt, endedAt, state)}
155
+ />
156
+ <MetadataField
157
+ labelOuiaId="task-info-metadata-triggered-by-label"
158
+ title={__('Triggered by')}
159
+ value={triggeredBy}
160
+ />
161
+ <MetadataField
162
+ labelOuiaId="task-info-metadata-id-label"
163
+ title={__('Id')}
164
+ value={copyableText(id)}
165
+ />
166
+
145
167
  <GridItem span={12}>
146
- <span className="list-group-item-heading">{__('State')}: </span>
147
- {state}
168
+ <ExpandableSectionToggle
169
+ toggleId={toggleId}
170
+ contentId={contentId}
171
+ isExpanded={isExpanded}
172
+ onToggle={setIsExpanded}
173
+ >
174
+ {isExpanded ? __('Show less details') : __('Show more details')}
175
+ </ExpandableSectionToggle>
148
176
  </GridItem>
177
+
149
178
  <GridItem span={12}>
150
- <Progress
151
- value={progress}
152
- max={100}
153
- aria-label={`${progress}% ${__('Complete')}`}
154
- measureLocation="outside"
155
- {...(progVariant ? { variant: progVariant } : {})}
156
- />
179
+ <ExpandableSection
180
+ isExpanded={isExpanded}
181
+ isDetached
182
+ toggleId={toggleId}
183
+ contentId={contentId}
184
+ >
185
+ <Grid hasGutter>
186
+ <MetadataField
187
+ labelOuiaId="task-info-metadata-execution-type-label"
188
+ title={__('Execution type')}
189
+ value={__(isDelayed(props) ? 'Delayed' : 'Immediate')}
190
+ />
191
+ <MetadataField
192
+ labelOuiaId="task-info-metadata-label-field-label"
193
+ title={__('Label')}
194
+ value={label || action || __('N/A')}
195
+ className="details-name"
196
+ />
197
+ <MetadataField
198
+ labelOuiaId="task-info-metadata-start-at-label"
199
+ title={__('Start at')}
200
+ value={
201
+ <Flex direction={{ default: 'column' }}>
202
+ <FlexItem>
203
+ <RelativeDateTime defaultValue={__('N/A')} date={startAt} />
204
+ </FlexItem>
205
+ {startBefore && (
206
+ <FlexItem>
207
+ <span>({__('before')} </span>
208
+ <RelativeDateTime
209
+ defaultValue={__('N/A')}
210
+ date={startBefore}
211
+ />
212
+ <span>)</span>
213
+ </FlexItem>
214
+ )}
215
+ </Flex>
216
+ }
217
+ />
218
+ <MetadataField
219
+ labelOuiaId="task-info-metadata-ended-at-label"
220
+ title={__('Ended at')}
221
+ value={
222
+ <RelativeDateTime defaultValue={__('N/A')} date={endedAt} />
223
+ }
224
+ />
225
+ <MetadataField
226
+ labelOuiaId="task-info-metadata-external-id-label"
227
+ title={__('External Id')}
228
+ value={copyableText(externalId)}
229
+ />
230
+ {help && (
231
+ <MetadataField
232
+ labelOuiaId="task-info-metadata-help-text-label"
233
+ title={__('Troubleshooting')}
234
+ span={12}
235
+ value={<p dangerouslySetInnerHTML={{ __html: help }} />}
236
+ />
237
+ )}
238
+ </Grid>
239
+ </ExpandableSection>
157
240
  </GridItem>
158
- <GridItem span={12} className="pf-v5-u-pb-lg" />
159
- {help && (
160
- <GridItem span={12}>
161
- <b>{__('Troubleshooting')}</b>
162
- <p dangerouslySetInnerHTML={{ __html: help }} />
163
- </GridItem>
164
- )}
165
- {output && output.length > 0 && (
166
- <GridItem span={12}>
167
- <b>{__('Output:')}</b>
168
- <pre>{output}</pre>
169
- </GridItem>
170
- )}
171
- {errors && errors.length > 0 && (
241
+
242
+ {state !== 'stopped' && (
172
243
  <GridItem span={12}>
173
- <b>{__('Errors:')}</b>
174
- <pre>{errors}</pre>
244
+ <Flex
245
+ direction={{ default: 'column' }}
246
+ spaceItems={{ default: 'spaceItemsXs' }}
247
+ >
248
+ <FlexItem>
249
+ <Split hasGutter>
250
+ <SplitItem isFilled>
251
+ <Text
252
+ component={TextVariants.p}
253
+ ouiaId="task-info-running-state-summary"
254
+ >
255
+ {capitalize(state)}
256
+ </Text>
257
+ </SplitItem>
258
+ <SplitItem>
259
+ <Text
260
+ component={TextVariants.p}
261
+ ouiaId="task-info-running-progress-complete-text"
262
+ >
263
+ {sprintf(__('%s%% Complete'), progress)}
264
+ </Text>
265
+ </SplitItem>
266
+ </Split>
267
+ </FlexItem>
268
+ <FlexItem>
269
+ <Progress
270
+ value={progress}
271
+ max={100}
272
+ aria-label={sprintf(__('%s%% Complete'), progress)}
273
+ measureLocation="outside"
274
+ {...(progVariant ? { variant: progVariant } : {})}
275
+ />
276
+ </FlexItem>
277
+ </Flex>
175
278
  </GridItem>
176
279
  )}
177
280
  </Grid>
@@ -181,33 +284,35 @@ const TaskInfo = props => {
181
284
  TaskInfo.propTypes = {
182
285
  action: PropTypes.string,
183
286
  endedAt: PropTypes.string,
287
+ externalId: PropTypes.string,
288
+ id: PropTypes.string,
289
+ label: PropTypes.string,
184
290
  result: PropTypes.string,
185
291
  startAt: PropTypes.string,
186
292
  startBefore: PropTypes.string,
187
293
  startedAt: PropTypes.string,
188
294
  state: PropTypes.string,
189
295
  help: PropTypes.string,
190
- errors: PropTypes.array,
191
296
  progress: PropTypes.number,
192
297
  username: PropTypes.string,
193
298
  usernamePath: PropTypes.string,
194
- output: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]),
195
299
  };
196
300
 
197
301
  TaskInfo.defaultProps = {
198
302
  action: '',
199
303
  endedAt: '',
304
+ externalId: '',
305
+ id: '',
306
+ label: '',
200
307
  result: 'error',
201
308
  startAt: '',
202
309
  startBefore: '',
203
310
  startedAt: '',
204
311
  state: '',
205
312
  help: '',
206
- errors: [],
207
313
  progress: 0,
208
314
  username: '',
209
315
  usernamePath: '',
210
- output: '',
211
316
  };
212
317
 
213
318
  export default TaskInfo;
@@ -1,44 +1,38 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
3
4
  import '@testing-library/jest-dom';
4
5
 
5
6
  import Task from '../Task';
6
7
 
8
+ jest.mock('foremanReact/components/common/dates/RelativeDateTime', () => {
9
+ const RelativeDateTime = ({ date, defaultValue }) => (
10
+ <span>{date || defaultValue}</span>
11
+ );
12
+
13
+ return RelativeDateTime;
14
+ });
15
+
7
16
  describe('Task', () => {
8
- it('renders task controls from TaskButtons with minimal props', () => {
9
- render(
10
- <Task
11
- id="test"
12
- taskReloadStart={jest.fn()}
13
- taskProgressToggle={jest.fn()}
14
- />
15
- );
16
- expect(
17
- screen.getByRole('button', { name: /start auto-reloading/i })
18
- ).toBeInTheDocument();
19
- });
17
+ it('renders task overview metadata via TaskInfo', async () => {
18
+ render(<Task id="test" action="Refresh hosts" />);
20
19
 
21
- it('renders parent task and sub tasks links when provided', () => {
22
- render(
23
- <Task
24
- id="test"
25
- state="paused"
26
- hasSubTasks
27
- dynflowEnableConsole
28
- parentTask="parent-id"
29
- taskReload
30
- canEdit
31
- taskProgressToggle={jest.fn()}
32
- taskReloadStart={jest.fn()}
33
- />
34
- );
35
- expect(screen.getByRole('link', { name: /parent task/i })).toHaveAttribute(
36
- 'href',
37
- '/foreman_tasks/tasks/parent-id'
38
- );
39
- expect(screen.getByRole('link', { name: /sub tasks/i })).toHaveAttribute(
40
- 'href',
41
- '/foreman_tasks/tasks/test/sub_tasks'
20
+ expect(screen.getByText('Result')).toBeInTheDocument();
21
+ expect(screen.getByText('State')).toBeInTheDocument();
22
+ expect(screen.getByText('test')).toBeInTheDocument();
23
+
24
+ await userEvent.click(
25
+ screen.getByRole('button', { name: /show more details/i })
42
26
  );
27
+
28
+ expect(screen.getByText('Refresh hosts')).toBeInTheDocument();
29
+ expect(screen.getByText('Label')).toBeInTheDocument();
30
+ expect(screen.getByText('Execution type')).toBeInTheDocument();
31
+ expect(
32
+ screen.queryByRole('button', { name: /cancel/i })
33
+ ).not.toBeInTheDocument();
34
+ expect(
35
+ screen.queryByRole('button', { name: /^task actions$/i })
36
+ ).not.toBeInTheDocument();
43
37
  });
44
38
  });