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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/foreman_tasks/tasks_controller.rb +0 -5
  3. data/app/services/foreman_tasks/troubleshooting_help_generator.rb +1 -1
  4. data/config/routes.rb +3 -2
  5. data/lib/foreman_tasks/engine.rb +2 -2
  6. data/lib/foreman_tasks/version.rb +1 -1
  7. data/test/controllers/tasks_controller_test.rb +0 -9
  8. data/test/foreman_tasks_test_helper.rb +2 -2
  9. data/test/integration/tasks_test.rb +17 -0
  10. data/test/test_plugin_helper.rb +8 -0
  11. data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +8 -70
  12. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.js +115 -104
  13. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.scss +45 -0
  14. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskHelper.js +54 -0
  15. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +237 -177
  16. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskSkeleton.js +1 -6
  17. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +27 -35
  18. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +83 -69
  19. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskHelper.test.js +118 -1
  20. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskInfo.test.js +190 -20
  21. data/webpack/ForemanTasks/Components/TaskDetails/ExecutionDetails.js +65 -0
  22. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +76 -65
  23. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +1 -44
  24. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsConstants.js +2 -1
  25. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +20 -10
  26. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/ExecutionDetails.test.js +194 -0
  27. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +100 -1
  28. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +226 -18
  29. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetailsSelectors.test.js +81 -0
  30. data/webpack/ForemanTasks/Components/common/taskResultIcon.js +53 -0
  31. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.js +174 -0
  32. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.scss +5 -0
  33. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsPage.js +51 -0
  34. data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsHeader.test.js +142 -0
  35. data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +277 -0
  36. data/webpack/ForemanTasks/{Components/TaskDetails → Routes/ShowTaskDetails}/index.js +13 -12
  37. data/webpack/Routes/routes.js +6 -0
  38. data/webpack/Routes/routes.test.js +16 -5
  39. data/webpack/index.js +0 -5
  40. metadata +16 -3
  41. data/app/views/foreman_tasks/tasks/show.html.erb +0 -18
@@ -1,8 +1,107 @@
1
1
  export const minProps = {
2
+ id: 'a15dd820-32f1-4ced-9ab7-c0fab8234c47',
2
3
  cancelStep: jest.fn(),
3
4
  taskReload: false,
4
5
  taskReloadStop: jest.fn(),
5
6
  taskReloadStart: jest.fn(),
6
7
  taskProgressToggle: jest.fn(),
7
- status: 'RESOLVED',
8
+ apiStatus: 'RESOLVED',
9
+ };
10
+
11
+ /** Props for snapshots that exercise the Execution details tab alongside the overview. */
12
+ export const taskDetailsWithExecutionTabDefaults = {
13
+ ...minProps,
14
+ locks: [
15
+ {
16
+ exclusive: false,
17
+ resource_type: 'User',
18
+ resource_id: 4,
19
+ link: null,
20
+ },
21
+ ],
22
+ links: [],
23
+ executionPlan: { state: 'stopped', cancellable: false },
24
+ failedSteps: [],
25
+ runningSteps: [],
26
+ dependsOn: [],
27
+ blocks: [],
28
+ action: 'Refresh foo',
29
+ state: 'stopped',
30
+ result: 'success',
31
+ progress: 100,
32
+ startAt: '',
33
+ startedAt: '',
34
+ endedAt: '',
35
+ startBefore: '',
36
+ };
37
+
38
+ const SAMPLE_FAILED_STEP = {
39
+ error: {
40
+ exception_class: 'RuntimeError',
41
+ message:
42
+ 'Action Actions::Katello::EventQueue::Monitor is already active',
43
+ backtrace: [
44
+ "/home/example/gems/dynflow-1.2.3/lib/dynflow/action/singleton.rb:15:in `rescue in singleton_lock!'",
45
+ ],
46
+ },
47
+ action_class: 'Actions::Katello::EventQueue::Monitor',
48
+ state: 'error',
49
+ input:
50
+ '{"locale"=>"en",\n "current_request_id"=>nil,\n "current_user_id"=>4}\n',
51
+ output: '{}\n',
52
+ };
53
+
54
+ const SAMPLE_RUNNING_STEP = {
55
+ cancellable: false,
56
+ id: 1,
57
+ action_class: 'Actions::DynflowExample',
58
+ state: 'running',
59
+ input: '{}',
60
+ output: '{}',
61
+ };
62
+
63
+ const SAMPLE_DEPENDS_ON = [
64
+ {
65
+ id: '123',
66
+ action: 'Actions::FooBar',
67
+ humanized: 'Foo Bar Action',
68
+ state: 'stopped',
69
+ result: 'success',
70
+ },
71
+ ];
72
+
73
+ export const fixtureFailedExecutionDetail = {
74
+ ...taskDetailsWithExecutionTabDefaults,
75
+ result: 'error',
76
+ state: 'stopped',
77
+ failedSteps: [SAMPLE_FAILED_STEP],
78
+ };
79
+
80
+ export const fixtureRunningExecutionDetail = {
81
+ ...taskDetailsWithExecutionTabDefaults,
82
+ state: 'running',
83
+ runningSteps: [SAMPLE_RUNNING_STEP],
84
+ };
85
+
86
+ export const fixtureWithOverviewMessages = {
87
+ ...taskDetailsWithExecutionTabDefaults,
88
+ help: '<strong>See logs</strong> for more.',
89
+ output: {
90
+ messages: ['partial output'],
91
+ result: 'error',
92
+ },
93
+ };
94
+
95
+ export const fixtureWithDependencies = {
96
+ ...taskDetailsWithExecutionTabDefaults,
97
+ dependsOn: SAMPLE_DEPENDS_ON,
98
+ blocks: [
99
+ {
100
+ id: '789',
101
+ action: 'Actions::Test',
102
+ humanized: 'Test Action',
103
+ state: 'paused',
104
+ result: 'warning',
105
+ },
106
+ ],
8
107
  };
@@ -1,48 +1,256 @@
1
1
  import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
2
3
  import { render, screen } from '@testing-library/react';
3
4
  import '@testing-library/jest-dom';
5
+ import { configureStore } from '@reduxjs/toolkit';
6
+ import { Provider } from 'react-redux';
7
+ import { createMemoryHistory } from 'history';
8
+ import { Router } from 'react-router-dom';
4
9
 
5
10
  import TaskDetails from '../TaskDetails';
6
- import { minProps } from './TaskDetails.fixtures';
11
+ import {
12
+ fixtureFailedExecutionDetail,
13
+ fixtureWithDependencies,
14
+ fixtureWithOverviewMessages,
15
+ minProps,
16
+ taskDetailsWithExecutionTabDefaults,
17
+ } from './TaskDetails.fixtures';
18
+ import { VIEW_FOREMAN_TASKS } from '../TaskDetailsConstants';
19
+
20
+ const mockUseForemanPermissions = jest.fn(
21
+ () => new Set(['view_foreman_tasks'])
22
+ );
23
+
24
+ jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
25
+ ...jest.requireActual('foremanReact/Root/Context/ForemanContext'),
26
+ useForemanPermissions: (...args) => mockUseForemanPermissions(...args),
27
+ }));
7
28
 
8
29
  delete window.location;
9
30
  window.location = new URL(
10
31
  'https://foreman.com/foreman_tasks/tasks/a15dd820-32f1-4ced-9ab7-c0fab8234c47/'
11
32
  );
12
33
 
34
+ const store = configureStore({ reducer: state => state || {} });
35
+
36
+ function renderTaskDetails(props = {}) {
37
+ const history = createMemoryHistory();
38
+
39
+ return render(
40
+ <Router history={history}>
41
+ <Provider store={store}>
42
+ <TaskDetails {...minProps} {...props} />
43
+ </Provider>
44
+ </Router>
45
+ );
46
+ }
47
+
13
48
  describe('TaskDetails', () => {
14
- it('shows error message when status is ERROR', () => {
15
- render(
16
- <TaskDetails
17
- {...minProps}
18
- status="ERROR"
19
- APIerror={{ message: 'some-error' }}
20
- />
49
+ beforeEach(() => {
50
+ mockUseForemanPermissions.mockImplementation(
51
+ () => new Set(['view_foreman_tasks'])
21
52
  );
53
+ });
54
+
55
+ afterEach(() => {
56
+ jest.clearAllMocks();
57
+ });
58
+
59
+ it(`shows ResourceLoadFailedEmptyState when ${VIEW_FOREMAN_TASKS} is absent`, () => {
60
+ mockUseForemanPermissions.mockImplementation(() => new Set());
61
+
62
+ renderTaskDetails();
63
+
64
+ expect(
65
+ screen.getByRole('heading', { name: /permission denied/i })
66
+ ).toBeInTheDocument();
67
+ expect(screen.getByText(VIEW_FOREMAN_TASKS)).toBeInTheDocument();
68
+ expect(
69
+ screen.getByRole('button', { name: /back to tasks/i })
70
+ ).toBeInTheDocument();
71
+ expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
72
+ });
73
+
74
+ it('shows ResourceLoadFailedEmptyState when apiStatus is ERROR', () => {
75
+ renderTaskDetails({
76
+ apiStatus: 'ERROR',
77
+ apiErrorMessage: 'some-error',
78
+ });
79
+
80
+ expect(
81
+ screen.getByRole('heading', { name: /unable to load task/i })
82
+ ).toBeInTheDocument();
83
+ expect(
84
+ screen.getByText('Server returned: some-error')
85
+ ).toBeInTheDocument();
86
+ expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
87
+ });
88
+
89
+ it('shows permission denied when apiErrorCode is 403', () => {
90
+ renderTaskDetails({
91
+ apiStatus: 'ERROR',
92
+ apiErrorCode: 403,
93
+ apiErrorMessage: 'Forbidden',
94
+ });
95
+
96
+ expect(
97
+ screen.getByRole('heading', { name: /permission denied/i })
98
+ ).toBeInTheDocument();
99
+ expect(screen.getByText('view_foreman_tasks')).toBeInTheDocument();
100
+ expect(
101
+ screen.getByText('Server returned: Forbidden')
102
+ ).toBeInTheDocument();
103
+ expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
104
+ });
105
+
106
+ it('shows not found messaging when apiErrorCode is 404', () => {
107
+ renderTaskDetails({
108
+ apiStatus: 'ERROR',
109
+ apiErrorCode: 404,
110
+ apiErrorMessage: 'Task missing',
111
+ });
112
+
22
113
  expect(
23
- screen.getByText(/could not receive data: some-error/i)
114
+ screen.getByRole('heading', { name: /unable to load task/i })
24
115
  ).toBeInTheDocument();
116
+ expect(
117
+ screen.getByText(/could not be found/i)
118
+ ).toBeInTheDocument();
119
+ expect(
120
+ screen.getByText('Server returned: Task missing')
121
+ ).toBeInTheDocument();
122
+ expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
25
123
  });
26
124
 
27
- it('shows skeleton while loading on the Task tab', () => {
28
- const { container } = render(<TaskDetails {...minProps} isLoading />);
125
+ it('shows skeleton in the overview while loading', () => {
126
+ const { container } = renderTaskDetails({ isLoading: true });
29
127
  expect(
30
128
  container.querySelector('.react-loading-skeleton')
31
129
  ).toBeInTheDocument();
32
130
  });
33
131
 
34
- it('renders six tabs with expected labels', () => {
35
- render(<TaskDetails {...minProps} />);
36
- expect(document.getElementById('task-details-tabs')).toBeInTheDocument();
37
- expect(screen.getByRole('tab', { name: /^task$/i })).toBeInTheDocument();
132
+ it('renders four tabs with expected labels', () => {
133
+ renderTaskDetails();
38
134
  expect(
39
- screen.getByRole('tab', { name: /running steps/i })
135
+ screen.getByRole('tab', { name: /execution details/i })
40
136
  ).toBeInTheDocument();
41
- expect(screen.getByRole('tab', { name: /errors/i })).toBeInTheDocument();
42
- expect(screen.getByRole('tab', { name: /locks/i })).toBeInTheDocument();
43
137
  expect(
44
138
  screen.getByRole('tab', { name: /dependencies/i })
45
139
  ).toBeInTheDocument();
140
+ expect(screen.getByRole('tab', { name: /locks/i })).toBeInTheDocument();
46
141
  expect(screen.getByRole('tab', { name: /raw/i })).toBeInTheDocument();
47
142
  });
143
+
144
+ it('exposes stable ouiaIds on tabs for automation', () => {
145
+ renderTaskDetails();
146
+
147
+ expect(
148
+ document.querySelector(
149
+ '[data-ouia-component-id="task-details-tab-execution-details"]'
150
+ )
151
+ ).toBeInTheDocument();
152
+ expect(
153
+ document.querySelector(
154
+ '[data-ouia-component-id="task-details-tab-dependencies"]'
155
+ )
156
+ ).toBeInTheDocument();
157
+ expect(
158
+ document.querySelector(
159
+ '[data-ouia-component-id="task-details-tab-locks"]'
160
+ )
161
+ ).toBeInTheDocument();
162
+ expect(
163
+ document.querySelector('[data-ouia-component-id="task-details-tab-raw"]')
164
+ ).toBeInTheDocument();
165
+ });
166
+
167
+ it('shows failed step errors on the default execution details tab', () => {
168
+ renderTaskDetails({
169
+ ...fixtureFailedExecutionDetail,
170
+ executionPlan: fixtureFailedExecutionDetail.executionPlan,
171
+ failedSteps: fixtureFailedExecutionDetail.failedSteps,
172
+ });
173
+
174
+ expect(
175
+ screen.getByRole('tab', {
176
+ name: /action actions::katello::eventqueue::monitor is already active/i,
177
+ })
178
+ ).toBeInTheDocument();
179
+ expect(screen.getByLabelText(/failed task errors/i)).toBeInTheDocument();
180
+ });
181
+
182
+ it('starts task reload on mount and stops on unmount', () => {
183
+ const taskReloadStart = jest.fn();
184
+ const taskReloadStop = jest.fn();
185
+
186
+ const { unmount } = renderTaskDetails({
187
+ ...taskDetailsWithExecutionTabDefaults,
188
+ taskReloadStart,
189
+ taskReloadStop,
190
+ });
191
+
192
+ expect(taskReloadStart).toHaveBeenCalledWith(
193
+ 'a15dd820-32f1-4ced-9ab7-c0fab8234c47'
194
+ );
195
+ unmount();
196
+ expect(taskReloadStop).toHaveBeenCalled();
197
+ });
198
+
199
+ it('shows execution details panel on the default tab when loaded', () => {
200
+ renderTaskDetails({ ...taskDetailsWithExecutionTabDefaults });
201
+
202
+ expect(
203
+ screen.getByRole('heading', { name: /^no errors found$/i })
204
+ ).toBeInTheDocument();
205
+ });
206
+
207
+ it('disables tabs and hides execution panel while loading', () => {
208
+ renderTaskDetails({
209
+ ...taskDetailsWithExecutionTabDefaults,
210
+ isLoading: true,
211
+ });
212
+
213
+ expect(
214
+ screen.queryByRole('heading', { name: /^no errors found$/i })
215
+ ).not.toBeInTheDocument();
216
+ expect(
217
+ screen.getByRole('tab', { name: /execution details/i })
218
+ ).toBeDisabled();
219
+ expect(screen.getByRole('tab', { name: /dependencies/i })).toBeDisabled();
220
+ expect(screen.getByRole('tab', { name: /locks/i })).toBeDisabled();
221
+ expect(screen.getByRole('tab', { name: /raw/i })).toBeDisabled();
222
+ });
223
+
224
+ it('renders troubleshooting help in overview after expanding details', async () => {
225
+ renderTaskDetails({ ...fixtureWithOverviewMessages });
226
+
227
+ await userEvent.click(
228
+ screen.getByRole('button', { name: /show more details/i })
229
+ );
230
+
231
+ expect(screen.getByText('Troubleshooting')).toBeInTheDocument();
232
+ expect(screen.getByText('See logs')).toBeInTheDocument();
233
+ });
234
+
235
+ it('renders raw output when the Raw tab is selected', async () => {
236
+ renderTaskDetails({ ...fixtureWithOverviewMessages });
237
+
238
+ await userEvent.click(screen.getByRole('tab', { name: /raw/i }));
239
+
240
+ expect(screen.getByText(/raw output/i)).toBeInTheDocument();
241
+ expect(screen.getByText(/partial output/i)).toBeInTheDocument();
242
+ });
243
+
244
+ it('renders dependency tables when the Dependencies tab is selected', async () => {
245
+ renderTaskDetails({ ...fixtureWithDependencies });
246
+
247
+ await userEvent.click(screen.getByRole('tab', { name: /dependencies/i }));
248
+
249
+ expect(
250
+ screen.getByRole('link', { name: 'Foo Bar Action' })
251
+ ).toBeInTheDocument();
252
+ expect(
253
+ screen.getByRole('link', { name: 'Test Action' })
254
+ ).toBeInTheDocument();
255
+ });
48
256
  });
@@ -0,0 +1,81 @@
1
+ import { STATUS } from 'foremanReact/constants';
2
+
3
+ import { FOREMAN_TASK_DETAILS } from '../TaskDetailsConstants';
4
+ import {
5
+ selectAPIError,
6
+ selectAPIErrorCode,
7
+ selectAPIErrorMessage,
8
+ } from '../TaskDetailsSelectors';
9
+
10
+ const axiosErrorWithApiBody = {
11
+ message: 'Request failed with status code 404',
12
+ response: {
13
+ status: 404,
14
+ data: {
15
+ error: {
16
+ message: 'Task not found',
17
+ },
18
+ },
19
+ },
20
+ };
21
+
22
+ const plainError = {
23
+ message: 'Network Error',
24
+ };
25
+
26
+ const forbiddenError = {
27
+ message: 'Request failed with status code 403',
28
+ response: {
29
+ status: 403,
30
+ data: {
31
+ error: {
32
+ message: 'Forbidden',
33
+ },
34
+ },
35
+ },
36
+ };
37
+
38
+ const buildState = (response, status = STATUS.ERROR) => ({
39
+ API: {
40
+ [FOREMAN_TASK_DETAILS]: {
41
+ payload: {},
42
+ response,
43
+ status,
44
+ },
45
+ },
46
+ });
47
+
48
+ describe('TaskDetailsSelectors - API error selectors', () => {
49
+ it('selectAPIError returns null when API status is not ERROR', () => {
50
+ expect(selectAPIError(buildState({}, STATUS.RESOLVED))).toBeNull();
51
+ });
52
+
53
+ it('selectAPIError returns the stored response when API status is ERROR', () => {
54
+ expect(selectAPIError(buildState(plainError))).toEqual(plainError);
55
+ });
56
+
57
+ it('selectAPIErrorMessage prefers the API error body message', () => {
58
+ expect(
59
+ selectAPIErrorMessage(buildState(axiosErrorWithApiBody))
60
+ ).toBe('Task not found');
61
+ });
62
+
63
+ it('selectAPIErrorMessage falls back to the top-level error message', () => {
64
+ expect(selectAPIErrorMessage(buildState(plainError))).toBe('Network Error');
65
+ });
66
+
67
+ it('selectAPIErrorMessage returns undefined when there is no API error', () => {
68
+ expect(
69
+ selectAPIErrorMessage(buildState({}, STATUS.RESOLVED))
70
+ ).toBeUndefined();
71
+ });
72
+
73
+ it('selectAPIErrorCode returns the HTTP status from the error response', () => {
74
+ expect(selectAPIErrorCode(buildState(axiosErrorWithApiBody))).toBe(404);
75
+ expect(selectAPIErrorCode(buildState(forbiddenError))).toBe(403);
76
+ });
77
+
78
+ it('selectAPIErrorCode returns undefined when the error response has no status', () => {
79
+ expect(selectAPIErrorCode(buildState(plainError))).toBeUndefined();
80
+ });
81
+ });
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import { Icon } from '@patternfly/react-core';
3
+ import {
4
+ CheckCircleIcon,
5
+ ExclamationCircleIcon,
6
+ ExclamationTriangleIcon,
7
+ QuestionCircleIcon,
8
+ } from '@patternfly/react-icons';
9
+ import { translate as __ } from 'foremanReact/common/I18n';
10
+
11
+ /**
12
+ * Icon reflecting task state/result (aligned with TaskInfo / tasks table).
13
+ *
14
+ * @param {string} state Dynflow task state (e.g. stopped, running).
15
+ * @param {string} [result] Result when stopped (success, error, warning, …).
16
+ * @returns {React.ReactElement}
17
+ */
18
+ export const taskResultIconEl = (state, result) => {
19
+ if (state && state !== 'stopped') {
20
+ return (
21
+ <Icon title={__('Running')}>
22
+ <QuestionCircleIcon />
23
+ </Icon>
24
+ );
25
+ }
26
+
27
+ switch (result) {
28
+ case 'success':
29
+ return (
30
+ <Icon status="success" title={__('Success')}>
31
+ <CheckCircleIcon />
32
+ </Icon>
33
+ );
34
+ case 'error':
35
+ return (
36
+ <Icon status="danger" title={__('Error')}>
37
+ <ExclamationCircleIcon />
38
+ </Icon>
39
+ );
40
+ case 'warning':
41
+ return (
42
+ <Icon status="warning" title={__('Warning')}>
43
+ <ExclamationTriangleIcon />
44
+ </Icon>
45
+ );
46
+ default:
47
+ return (
48
+ <Icon title={__('Unknown')}>
49
+ <QuestionCircleIcon />
50
+ </Icon>
51
+ );
52
+ }
53
+ };
@@ -0,0 +1,174 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ Flex,
5
+ FlexItem,
6
+ Split,
7
+ SplitItem,
8
+ Title,
9
+ } from '@patternfly/react-core';
10
+ import { translate as __ } from 'foremanReact/common/I18n';
11
+ import { STATUS } from 'foremanReact/constants';
12
+ import { usePermissions } from 'foremanReact/common/hooks/Permissions/permissionHooks';
13
+ import { TaskButtons } from '../../Components/TaskDetails/Components/TaskButtons';
14
+ import { VIEW_FOREMAN_TASKS } from '../../Components/TaskDetails/TaskDetailsConstants';
15
+ import {
16
+ ForceUnlockConfirmationModal,
17
+ UnlockConfirmationModal,
18
+ } from '../../Components/common/ClickConfirmation';
19
+ import { taskResultIconEl } from '../../Components/common/taskResultIcon';
20
+ import './TaskDetailsHeader.scss';
21
+
22
+ const TitleComponent = ({ action, state, result }) => (
23
+ <Flex
24
+ alignItems={{ default: 'alignItemsCenter' }}
25
+ spaceItems={{ default: 'spaceItemsXs' }}
26
+ >
27
+ <FlexItem>
28
+ <Title
29
+ headingLevel="h1"
30
+ className="foreman-tasks-task-details-header__title"
31
+ ouiaId="task-overview-title"
32
+ >
33
+ {action || __('Task Details')}
34
+ </Title>
35
+ </FlexItem>
36
+ <FlexItem>{taskResultIconEl(state, result)}</FlexItem>
37
+ </Flex>
38
+ );
39
+
40
+ TitleComponent.propTypes = {
41
+ action: PropTypes.string,
42
+ state: PropTypes.string,
43
+ result: PropTypes.string,
44
+ };
45
+
46
+ TitleComponent.defaultProps = {
47
+ action: '',
48
+ state: '',
49
+ result: '',
50
+ };
51
+
52
+ const TaskDetailsHeader = props => {
53
+ const {
54
+ taskReload,
55
+ id,
56
+ forceCancelTaskRequest,
57
+ unlockTaskRequest,
58
+ action,
59
+ taskReloadStart,
60
+ taskReloadStop,
61
+ executionPlan,
62
+ apiStatus,
63
+ } = props;
64
+ const hasViewPermission = usePermissions([VIEW_FOREMAN_TASKS]);
65
+ const showActions = hasViewPermission && apiStatus !== STATUS.ERROR;
66
+ const resumable = executionPlan ? executionPlan.state === 'paused' : false;
67
+ const cancellable = executionPlan ? executionPlan.cancellable : false;
68
+
69
+ const forceUnlock = () => {
70
+ if (!taskReload) {
71
+ taskReloadStart(id);
72
+ }
73
+
74
+ forceCancelTaskRequest(id, action);
75
+ };
76
+
77
+ const unlock = () => {
78
+ if (!taskReload) {
79
+ taskReloadStart(id);
80
+ }
81
+
82
+ unlockTaskRequest(id, action);
83
+ };
84
+
85
+ const taskProgressToggle = () => {
86
+ if (taskReload) {
87
+ taskReloadStop();
88
+ } else {
89
+ taskReloadStart(id);
90
+ }
91
+ };
92
+
93
+ const [unlockModalOpen, setUnlockModalOpen] = useState(false);
94
+ const [forceUnlockModalOpen, setForceUnlockModalOpen] = useState(false);
95
+
96
+ return (
97
+ <React.Fragment>
98
+ {showActions && (
99
+ <>
100
+ <UnlockConfirmationModal
101
+ onClick={unlock}
102
+ isOpen={unlockModalOpen}
103
+ setModalClosed={() => setUnlockModalOpen(false)}
104
+ />
105
+ <ForceUnlockConfirmationModal
106
+ onClick={forceUnlock}
107
+ isOpen={forceUnlockModalOpen}
108
+ setModalClosed={() => setForceUnlockModalOpen(false)}
109
+ />
110
+ </>
111
+ )}
112
+ <Split
113
+ className="foreman-tasks-task-details-header"
114
+ hasGutter
115
+ isWrappable
116
+ data-ouia-component-id="foreman-tasks-task-details-title-row"
117
+ >
118
+ <SplitItem isFilled>
119
+ <TitleComponent
120
+ action={props.action}
121
+ state={props.state}
122
+ result={props.result}
123
+ />
124
+ </SplitItem>
125
+ {showActions && (
126
+ <SplitItem>
127
+ <TaskButtons
128
+ {...props}
129
+ taskReloadStart={taskReloadStart}
130
+ taskProgressToggle={taskProgressToggle}
131
+ setUnlockModalOpen={setUnlockModalOpen}
132
+ setForceUnlockModalOpen={setForceUnlockModalOpen}
133
+ resumable={resumable}
134
+ cancellable={cancellable}
135
+ />
136
+ </SplitItem>
137
+ )}
138
+ </Split>
139
+ </React.Fragment>
140
+ );
141
+ };
142
+
143
+ TaskDetailsHeader.propTypes = {
144
+ taskReload: PropTypes.bool,
145
+ id: PropTypes.string,
146
+ forceCancelTaskRequest: PropTypes.func,
147
+ unlockTaskRequest: PropTypes.func,
148
+ action: PropTypes.string,
149
+ state: PropTypes.string,
150
+ result: PropTypes.string,
151
+ taskReloadStart: PropTypes.func,
152
+ taskReloadStop: PropTypes.func,
153
+ executionPlan: PropTypes.shape({
154
+ state: PropTypes.string,
155
+ cancellable: PropTypes.bool,
156
+ }),
157
+ apiStatus: PropTypes.oneOf(Object.keys(STATUS)),
158
+ };
159
+
160
+ TaskDetailsHeader.defaultProps = {
161
+ taskReload: false,
162
+ id: '',
163
+ forceCancelTaskRequest: () => null,
164
+ unlockTaskRequest: () => null,
165
+ action: '',
166
+ state: '',
167
+ result: '',
168
+ taskReloadStart: () => null,
169
+ taskReloadStop: () => null,
170
+ executionPlan: {},
171
+ apiStatus: STATUS.RESOLVED,
172
+ };
173
+
174
+ export default TaskDetailsHeader;
@@ -0,0 +1,5 @@
1
+ .foreman-tasks-task-details-header {
2
+ .foreman-tasks-task-details-header__title {
3
+ margin: 0;
4
+ }
5
+ }