foreman-tasks 13.0.0 → 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.
- checksums.yaml +4 -4
- data/app/services/foreman_tasks/troubleshooting_help_generator.rb +1 -1
- data/lib/foreman_tasks/version.rb +1 -1
- data/webpack/ForemanTasks/Components/Chart/Chart.js +3 -1
- data/webpack/ForemanTasks/Components/TaskActions/TaskActionHelpers.js +8 -1
- data/webpack/ForemanTasks/Components/TaskActions/TaskActionsConstants.js +1 -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 +58 -1
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +236 -131
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +27 -33
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +83 -67
- 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 +71 -0
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +48 -51
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +1 -39
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsActions.js +6 -2
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsConstants.js +2 -0
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +2 -7
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/ExecutionDetails.test.js +241 -0
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +99 -0
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +129 -8
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/StoppedTasksCard.scss +0 -1
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/TasksDonutCard/TasksDonutCard.scss +1 -0
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksLabelsRow/TasksLabelsRow.scss +1 -0
- data/webpack/ForemanTasks/Components/TasksDashboard/TasksDashboardHelper.js +9 -4
- data/webpack/ForemanTasks/Components/TasksTable/TasksTableHelpers.js +2 -1
- data/webpack/ForemanTasks/Components/common/taskResultIcon.js +39 -29
- 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 +10 -33
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsHeader.test.js +142 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +100 -88
- data/webpack/ForemanTasks/{Components/TaskDetails → Routes/ShowTaskDetails}/index.js +7 -8
- data/webpack/Routes/routes.js +1 -1
- data/webpack/Routes/routes.test.js +1 -1
- data/webpack/index.js +0 -5
- metadata +9 -3
|
@@ -1,4 +1,5 @@
|
|
|
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(),
|
|
@@ -6,3 +7,101 @@ export const minProps = {
|
|
|
6
7
|
taskProgressToggle: jest.fn(),
|
|
7
8
|
apiStatus: 'RESOLVED',
|
|
8
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
|
+
],
|
|
107
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
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';
|
|
4
5
|
import { configureStore } from '@reduxjs/toolkit';
|
|
@@ -7,7 +8,13 @@ import { createMemoryHistory } from 'history';
|
|
|
7
8
|
import { Router } from 'react-router-dom';
|
|
8
9
|
|
|
9
10
|
import TaskDetails from '../TaskDetails';
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
fixtureFailedExecutionDetail,
|
|
13
|
+
fixtureWithDependencies,
|
|
14
|
+
fixtureWithOverviewMessages,
|
|
15
|
+
minProps,
|
|
16
|
+
taskDetailsWithExecutionTabDefaults,
|
|
17
|
+
} from './TaskDetails.fixtures';
|
|
11
18
|
import { VIEW_FOREMAN_TASKS } from '../TaskDetailsConstants';
|
|
12
19
|
|
|
13
20
|
const mockUseForemanPermissions = jest.fn(
|
|
@@ -45,6 +52,10 @@ describe('TaskDetails', () => {
|
|
|
45
52
|
);
|
|
46
53
|
});
|
|
47
54
|
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
jest.clearAllMocks();
|
|
57
|
+
});
|
|
58
|
+
|
|
48
59
|
it(`shows ResourceLoadFailedEmptyState when ${VIEW_FOREMAN_TASKS} is absent`, () => {
|
|
49
60
|
mockUseForemanPermissions.mockImplementation(() => new Set());
|
|
50
61
|
|
|
@@ -111,25 +122,135 @@ describe('TaskDetails', () => {
|
|
|
111
122
|
expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
|
|
112
123
|
});
|
|
113
124
|
|
|
114
|
-
it('shows skeleton
|
|
125
|
+
it('shows skeleton in the overview while loading', () => {
|
|
115
126
|
const { container } = renderTaskDetails({ isLoading: true });
|
|
116
127
|
expect(
|
|
117
128
|
container.querySelector('.react-loading-skeleton')
|
|
118
129
|
).toBeInTheDocument();
|
|
119
130
|
});
|
|
120
131
|
|
|
121
|
-
it('renders
|
|
132
|
+
it('renders four tabs with expected labels', () => {
|
|
122
133
|
renderTaskDetails();
|
|
123
|
-
expect(document.getElementById('task-details-tabs')).toBeInTheDocument();
|
|
124
|
-
expect(screen.getByRole('tab', { name: /^task$/i })).toBeInTheDocument();
|
|
125
134
|
expect(
|
|
126
|
-
screen.getByRole('tab', { name: /
|
|
135
|
+
screen.getByRole('tab', { name: /execution details/i })
|
|
127
136
|
).toBeInTheDocument();
|
|
128
|
-
expect(screen.getByRole('tab', { name: /errors/i })).toBeInTheDocument();
|
|
129
|
-
expect(screen.getByRole('tab', { name: /locks/i })).toBeInTheDocument();
|
|
130
137
|
expect(
|
|
131
138
|
screen.getByRole('tab', { name: /dependencies/i })
|
|
132
139
|
).toBeInTheDocument();
|
|
140
|
+
expect(screen.getByRole('tab', { name: /locks/i })).toBeInTheDocument();
|
|
133
141
|
expect(screen.getByRole('tab', { name: /raw/i })).toBeInTheDocument();
|
|
134
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
|
+
});
|
|
135
256
|
});
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { getURIQuery } from 'foremanReact/common/helpers';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
HOURS_PER_HALF_DAY,
|
|
5
|
+
HOURS_PER_DAY,
|
|
6
|
+
DAYS_PER_WEEK,
|
|
7
|
+
} from 'foremanReact/constants';
|
|
3
8
|
import {
|
|
4
9
|
TASKS_DASHBOARD_AVAILABLE_TIMES,
|
|
5
10
|
TASKS_DASHBOARD_QUERY_KEYS_TEXT,
|
|
@@ -16,13 +21,13 @@ export const getQueryValueText = value =>
|
|
|
16
21
|
export const timeToHoursNumber = time => {
|
|
17
22
|
switch (time) {
|
|
18
23
|
case TASKS_DASHBOARD_AVAILABLE_TIMES.H12:
|
|
19
|
-
return
|
|
24
|
+
return HOURS_PER_HALF_DAY;
|
|
20
25
|
case TASKS_DASHBOARD_AVAILABLE_TIMES.H24:
|
|
21
|
-
return
|
|
26
|
+
return HOURS_PER_DAY;
|
|
22
27
|
case TASKS_DASHBOARD_AVAILABLE_TIMES.WEEK:
|
|
23
|
-
return
|
|
28
|
+
return HOURS_PER_DAY * DAYS_PER_WEEK;
|
|
24
29
|
default:
|
|
25
|
-
return
|
|
30
|
+
return HOURS_PER_DAY;
|
|
26
31
|
}
|
|
27
32
|
};
|
|
28
33
|
|
|
@@ -2,6 +2,7 @@ import URI from 'urijs';
|
|
|
2
2
|
import { translate as __, documentLocale } from 'foremanReact/common/I18n';
|
|
3
3
|
import humanizeDuration from 'humanize-duration';
|
|
4
4
|
import { isoCompatibleDate } from 'foremanReact/common/helpers';
|
|
5
|
+
import { MS_PER_SECOND } from 'foremanReact/constants';
|
|
5
6
|
import { convertDashboardQuery } from '../TaskActions/TaskActionHelpers';
|
|
6
7
|
|
|
7
8
|
export const updateURlQuery = (query, history) => {
|
|
@@ -51,7 +52,7 @@ export const getDuration = (start, finish) => {
|
|
|
51
52
|
const duration = finishDate - startDate;
|
|
52
53
|
return {
|
|
53
54
|
text:
|
|
54
|
-
duration > 0 && duration <
|
|
55
|
+
duration > 0 && duration < MS_PER_SECOND
|
|
55
56
|
? __('Less than a second')
|
|
56
57
|
: humanizeDuration(duration, dateOptions),
|
|
57
58
|
};
|
|
@@ -5,49 +5,59 @@ import {
|
|
|
5
5
|
ExclamationCircleIcon,
|
|
6
6
|
ExclamationTriangleIcon,
|
|
7
7
|
QuestionCircleIcon,
|
|
8
|
+
InProgressIcon,
|
|
8
9
|
} from '@patternfly/react-icons';
|
|
9
10
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
10
11
|
|
|
12
|
+
const RESULT_ICONS = {
|
|
13
|
+
success: {
|
|
14
|
+
status: 'success',
|
|
15
|
+
title: () => __('Success'),
|
|
16
|
+
IconComponent: CheckCircleIcon,
|
|
17
|
+
},
|
|
18
|
+
error: {
|
|
19
|
+
status: 'danger',
|
|
20
|
+
title: () => __('Error'),
|
|
21
|
+
IconComponent: ExclamationCircleIcon,
|
|
22
|
+
},
|
|
23
|
+
warning: {
|
|
24
|
+
status: 'warning',
|
|
25
|
+
title: () => __('Warning'),
|
|
26
|
+
IconComponent: ExclamationTriangleIcon,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const UNKNOWN_RESULT_ICON = {
|
|
31
|
+
status: undefined,
|
|
32
|
+
title: () => __('Unknown'),
|
|
33
|
+
IconComponent: QuestionCircleIcon,
|
|
34
|
+
};
|
|
35
|
+
|
|
11
36
|
/**
|
|
12
37
|
* Icon reflecting task state/result (aligned with TaskInfo / tasks table).
|
|
13
38
|
*
|
|
14
39
|
* @param {string} state Dynflow task state (e.g. stopped, running).
|
|
15
40
|
* @param {string} [result] Result when stopped (success, error, warning, …).
|
|
41
|
+
* @param {boolean} isTitleIcon Whether the icon should be used as a title icon.
|
|
16
42
|
* @returns {React.ReactElement}
|
|
17
43
|
*/
|
|
18
|
-
export const taskResultIconEl = (state, result) => {
|
|
44
|
+
export const taskResultIconEl = (state, result, isTitleIcon = false) => {
|
|
45
|
+
const size = isTitleIcon ? 'lg' : 'md';
|
|
46
|
+
|
|
19
47
|
if (state && state !== 'stopped') {
|
|
20
48
|
return (
|
|
21
|
-
<Icon title={__('Running')}>
|
|
22
|
-
<
|
|
49
|
+
<Icon title={__('Running')} status="info" isInline size={size}>
|
|
50
|
+
<InProgressIcon />
|
|
23
51
|
</Icon>
|
|
24
52
|
);
|
|
25
53
|
}
|
|
26
54
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
}
|
|
55
|
+
const { status, title, IconComponent } =
|
|
56
|
+
RESULT_ICONS[result] || UNKNOWN_RESULT_ICON;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Icon title={title()} status={status} isInline size={size}>
|
|
60
|
+
<IconComponent />
|
|
61
|
+
</Icon>
|
|
62
|
+
);
|
|
53
63
|
};
|
|
@@ -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, true)}</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;
|
|
@@ -1,48 +1,20 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
-
import { useSelector } from 'react-redux';
|
|
4
|
-
import { Flex, FlexItem, TextContent, Text } from '@patternfly/react-core';
|
|
5
3
|
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
|
|
6
4
|
import { translate as __, sprintf } from 'foremanReact/common/I18n';
|
|
7
|
-
import TaskDetails from '../../Components/TaskDetails';
|
|
8
|
-
import
|
|
9
|
-
selectAction,
|
|
10
|
-
selectResult,
|
|
11
|
-
selectState,
|
|
12
|
-
} from '../../Components/TaskDetails/TaskDetailsSelectors';
|
|
13
|
-
import { taskResultIconEl } from '../../Components/common/taskResultIcon';
|
|
5
|
+
import TaskDetails from '../../Components/TaskDetails/TaskDetails';
|
|
6
|
+
import TaskDetailsHeader from './TaskDetailsHeader';
|
|
14
7
|
|
|
15
8
|
const TaskDetailsPage = props => {
|
|
16
|
-
const {
|
|
17
|
-
const
|
|
18
|
-
const taskState = useSelector(selectState);
|
|
19
|
-
const taskResult = useSelector(selectResult);
|
|
9
|
+
const { match, action } = props;
|
|
10
|
+
const { id } = match.params;
|
|
20
11
|
const headerText = action
|
|
21
12
|
? sprintf(__('Details of %s task'), action)
|
|
22
13
|
: __('Task Details');
|
|
23
14
|
|
|
24
|
-
const header = (
|
|
25
|
-
<Flex
|
|
26
|
-
component="span"
|
|
27
|
-
data-ouia-component-id="foreman-tasks-task-details-title-row"
|
|
28
|
-
display={{ default: 'inlineFlex' }}
|
|
29
|
-
alignItems={{ default: 'alignItemsCenter' }}
|
|
30
|
-
gap={{ default: 'gapSm' }}
|
|
31
|
-
>
|
|
32
|
-
<TextContent>
|
|
33
|
-
<Text ouiaId="breadcrumb_title" component="h1">
|
|
34
|
-
{headerText}
|
|
35
|
-
</Text>
|
|
36
|
-
</TextContent>
|
|
37
|
-
<FlexItem component="span">
|
|
38
|
-
{taskResultIconEl(taskState, taskResult)}
|
|
39
|
-
</FlexItem>
|
|
40
|
-
</Flex>
|
|
41
|
-
);
|
|
42
|
-
|
|
43
15
|
return (
|
|
44
16
|
<PageLayout
|
|
45
|
-
customHeader={
|
|
17
|
+
customHeader={<TaskDetailsHeader {...props} />}
|
|
46
18
|
header={headerText}
|
|
47
19
|
searchable={false}
|
|
48
20
|
breadcrumbOptions={{
|
|
@@ -69,6 +41,11 @@ TaskDetailsPage.propTypes = {
|
|
|
69
41
|
id: PropTypes.string.isRequired,
|
|
70
42
|
}).isRequired,
|
|
71
43
|
}).isRequired,
|
|
44
|
+
action: PropTypes.string,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
TaskDetailsPage.defaultProps = {
|
|
48
|
+
action: '',
|
|
72
49
|
};
|
|
73
50
|
|
|
74
51
|
export default TaskDetailsPage;
|