foreman-tasks 12.2.4 → 13.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/foreman_tasks/tasks_controller.rb +0 -5
- data/app/services/foreman_tasks/troubleshooting_help_generator.rb +1 -1
- data/config/routes.rb +3 -2
- data/lib/foreman_tasks/engine.rb +2 -2
- data/lib/foreman_tasks/version.rb +1 -1
- data/test/controllers/tasks_controller_test.rb +0 -9
- data/test/foreman_tasks_test_helper.rb +2 -2
- data/test/integration/tasks_test.rb +17 -0
- data/test/test_plugin_helper.rb +8 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +8 -70
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.js +115 -104
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.scss +45 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskHelper.js +54 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +237 -177
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskSkeleton.js +1 -6
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +27 -35
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +83 -69
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskHelper.test.js +118 -1
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskInfo.test.js +190 -20
- data/webpack/ForemanTasks/Components/TaskDetails/ExecutionDetails.js +65 -0
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +76 -65
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +1 -44
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsConstants.js +2 -1
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +20 -10
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/ExecutionDetails.test.js +194 -0
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +100 -1
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +226 -18
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetailsSelectors.test.js +81 -0
- data/webpack/ForemanTasks/Components/common/taskResultIcon.js +53 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.js +174 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.scss +5 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsPage.js +51 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsHeader.test.js +142 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +277 -0
- data/webpack/ForemanTasks/{Components/TaskDetails → Routes/ShowTaskDetails}/index.js +13 -12
- data/webpack/Routes/routes.js +6 -0
- data/webpack/Routes/routes.test.js +16 -5
- data/webpack/index.js +0 -5
- metadata +16 -3
- data/app/views/foreman_tasks/tasks/show.html.erb +0 -18
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
|
|
4
|
+
import { translate as __, sprintf } from 'foremanReact/common/I18n';
|
|
5
|
+
import TaskDetails from '../../Components/TaskDetails/TaskDetails';
|
|
6
|
+
import TaskDetailsHeader from './TaskDetailsHeader';
|
|
7
|
+
|
|
8
|
+
const TaskDetailsPage = props => {
|
|
9
|
+
const { match, action } = props;
|
|
10
|
+
const { id } = match.params;
|
|
11
|
+
const headerText = action
|
|
12
|
+
? sprintf(__('Details of %s task'), action)
|
|
13
|
+
: __('Task Details');
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<PageLayout
|
|
17
|
+
customHeader={<TaskDetailsHeader {...props} />}
|
|
18
|
+
header={headerText}
|
|
19
|
+
searchable={false}
|
|
20
|
+
breadcrumbOptions={{
|
|
21
|
+
breadcrumbItems: [
|
|
22
|
+
{ caption: __('Tasks'), url: '/foreman_tasks/tasks' },
|
|
23
|
+
{ caption: action || id },
|
|
24
|
+
],
|
|
25
|
+
isSwitchable: true,
|
|
26
|
+
resource: {
|
|
27
|
+
nameField: 'action',
|
|
28
|
+
resourceUrl: '/foreman_tasks/api/tasks',
|
|
29
|
+
switcherItemUrl: '/foreman_tasks/tasks/:id',
|
|
30
|
+
},
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<TaskDetails {...props} />
|
|
34
|
+
</PageLayout>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
TaskDetailsPage.propTypes = {
|
|
39
|
+
match: PropTypes.shape({
|
|
40
|
+
params: PropTypes.shape({
|
|
41
|
+
id: PropTypes.string.isRequired,
|
|
42
|
+
}).isRequired,
|
|
43
|
+
}).isRequired,
|
|
44
|
+
action: PropTypes.string,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
TaskDetailsPage.defaultProps = {
|
|
48
|
+
action: '',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default TaskDetailsPage;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import { STATUS } from 'foremanReact/constants';
|
|
5
|
+
|
|
6
|
+
import TaskDetailsHeader from '../TaskDetailsHeader';
|
|
7
|
+
import { VIEW_FOREMAN_TASKS } from '../../../Components/TaskDetails/TaskDetailsConstants';
|
|
8
|
+
|
|
9
|
+
const mockUseForemanPermissions = jest.fn(
|
|
10
|
+
() => new Set(['view_foreman_tasks'])
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
|
|
14
|
+
...jest.requireActual('foremanReact/Root/Context/ForemanContext'),
|
|
15
|
+
useForemanPermissions: (...args) => mockUseForemanPermissions(...args),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const baseHeaderProps = {
|
|
19
|
+
id: 'test-id',
|
|
20
|
+
action: 'Refresh hosts',
|
|
21
|
+
state: 'running',
|
|
22
|
+
result: 'pending',
|
|
23
|
+
taskReload: false,
|
|
24
|
+
taskReloadStart: jest.fn(),
|
|
25
|
+
taskReloadStop: jest.fn(),
|
|
26
|
+
forceCancelTaskRequest: jest.fn(),
|
|
27
|
+
unlockTaskRequest: jest.fn(),
|
|
28
|
+
cancelTaskRequest: jest.fn(),
|
|
29
|
+
resumeTaskRequest: jest.fn(),
|
|
30
|
+
canEdit: true,
|
|
31
|
+
dynflowEnableConsole: true,
|
|
32
|
+
externalId: 'ext-99',
|
|
33
|
+
executionPlan: {},
|
|
34
|
+
apiStatus: STATUS.RESOLVED,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const expectHeaderActionsVisible = () => {
|
|
38
|
+
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
|
|
39
|
+
expect(
|
|
40
|
+
screen.getByRole('link', { name: /dynflow console/i })
|
|
41
|
+
).toBeInTheDocument();
|
|
42
|
+
expect(
|
|
43
|
+
screen.getByRole('button', { name: /^task actions$/i })
|
|
44
|
+
).toBeInTheDocument();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const expectHeaderActionsHidden = () => {
|
|
48
|
+
expect(
|
|
49
|
+
screen.queryByRole('button', { name: /cancel/i })
|
|
50
|
+
).not.toBeInTheDocument();
|
|
51
|
+
expect(
|
|
52
|
+
screen.queryByRole('link', { name: /dynflow console/i })
|
|
53
|
+
).not.toBeInTheDocument();
|
|
54
|
+
expect(
|
|
55
|
+
screen.queryByRole('button', { name: /^task actions$/i })
|
|
56
|
+
).not.toBeInTheDocument();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
describe('TaskDetailsHeader', () => {
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
mockUseForemanPermissions.mockImplementation(
|
|
62
|
+
() => new Set(['view_foreman_tasks'])
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
jest.clearAllMocks();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('renders action heading and exposes cancel, dynflow link, and task actions toggle', () => {
|
|
71
|
+
render(<TaskDetailsHeader {...baseHeaderProps} />);
|
|
72
|
+
|
|
73
|
+
expect(
|
|
74
|
+
screen.getByRole('heading', { level: 1, name: 'Refresh hosts' })
|
|
75
|
+
).toBeInTheDocument();
|
|
76
|
+
expectHeaderActionsVisible();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('shows running status icon next to the title when task state is running', () => {
|
|
80
|
+
render(
|
|
81
|
+
<TaskDetailsHeader
|
|
82
|
+
{...baseHeaderProps}
|
|
83
|
+
action="Running job"
|
|
84
|
+
state="running"
|
|
85
|
+
result="pending"
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(
|
|
90
|
+
screen.getByRole('heading', { level: 1, name: 'Running job' })
|
|
91
|
+
).toBeInTheDocument();
|
|
92
|
+
expect(screen.getByTitle('Running')).toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('shows error status icon next to the title when task stopped with error', () => {
|
|
96
|
+
render(
|
|
97
|
+
<TaskDetailsHeader
|
|
98
|
+
{...baseHeaderProps}
|
|
99
|
+
action="Failed job"
|
|
100
|
+
state="stopped"
|
|
101
|
+
result="error"
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(
|
|
106
|
+
screen.getByRole('heading', { level: 1, name: 'Failed job' })
|
|
107
|
+
).toBeInTheDocument();
|
|
108
|
+
expect(screen.getByTitle('Error')).toBeInTheDocument();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('uses generic title when action is unset', () => {
|
|
112
|
+
render(<TaskDetailsHeader {...baseHeaderProps} action="" />);
|
|
113
|
+
|
|
114
|
+
expect(
|
|
115
|
+
screen.getByRole('heading', { level: 1, name: 'Task Details' })
|
|
116
|
+
).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it.each([
|
|
120
|
+
[
|
|
121
|
+
`${VIEW_FOREMAN_TASKS} is absent`,
|
|
122
|
+
() => {
|
|
123
|
+
mockUseForemanPermissions.mockImplementation(() => new Set());
|
|
124
|
+
},
|
|
125
|
+
{},
|
|
126
|
+
],
|
|
127
|
+
[
|
|
128
|
+
'apiStatus is ERROR',
|
|
129
|
+
() => {},
|
|
130
|
+
{ apiStatus: STATUS.ERROR },
|
|
131
|
+
],
|
|
132
|
+
])('hides task actions when %s', (_label, setupPermissions, propOverrides) => {
|
|
133
|
+
setupPermissions();
|
|
134
|
+
|
|
135
|
+
render(<TaskDetailsHeader {...baseHeaderProps} {...propOverrides} />);
|
|
136
|
+
|
|
137
|
+
expect(
|
|
138
|
+
screen.getByRole('heading', { level: 1, name: 'Refresh hosts' })
|
|
139
|
+
).toBeInTheDocument();
|
|
140
|
+
expectHeaderActionsHidden();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { screen, within } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import { createMemoryHistory } from 'history';
|
|
5
|
+
import { Router } from 'react-router-dom';
|
|
6
|
+
import { IntlProvider } from 'react-intl';
|
|
7
|
+
|
|
8
|
+
import { rtlHelpers } from 'foremanReact/common/testHelpers';
|
|
9
|
+
import { STATUS } from 'foremanReact/constants';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
FOREMAN_TASK_DETAILS,
|
|
13
|
+
VIEW_FOREMAN_TASKS,
|
|
14
|
+
} from '../../../Components/TaskDetails/TaskDetailsConstants';
|
|
15
|
+
import TaskDetailsPage from '../index';
|
|
16
|
+
|
|
17
|
+
const { renderWithStore } = rtlHelpers;
|
|
18
|
+
|
|
19
|
+
jest.mock('../../../Components/TaskDetails/TaskDetailsActions', () => {
|
|
20
|
+
const actual = jest.requireActual(
|
|
21
|
+
'../../../Components/TaskDetails/TaskDetailsActions'
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
...actual,
|
|
26
|
+
taskReloadStart: jest.fn(() => () => undefined),
|
|
27
|
+
taskReloadStop: jest.fn(() => () => undefined),
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const mockUseForemanPermissions = jest.fn(
|
|
32
|
+
() => new Set(['view_foreman_tasks'])
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
|
|
36
|
+
...jest.requireActual('foremanReact/Root/Context/ForemanContext'),
|
|
37
|
+
useForemanPermissions: (...args) => mockUseForemanPermissions(...args),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const routerPropsBase = {
|
|
41
|
+
history: { push: jest.fn(), replace: jest.fn(), go: jest.fn() },
|
|
42
|
+
location: {
|
|
43
|
+
pathname: '/foreman_tasks/tasks/task-route-id',
|
|
44
|
+
search: '',
|
|
45
|
+
hash: '',
|
|
46
|
+
state: undefined,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const matchDefault = {
|
|
51
|
+
params: { id: 'task-route-id' },
|
|
52
|
+
path: '/foreman_tasks/tasks/:id',
|
|
53
|
+
url: '/foreman_tasks/tasks/task-route-id',
|
|
54
|
+
isExact: true,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const baseTaskPayload = {
|
|
58
|
+
action: '',
|
|
59
|
+
input: [],
|
|
60
|
+
output: {},
|
|
61
|
+
locks: [],
|
|
62
|
+
links: [],
|
|
63
|
+
depends_on: [],
|
|
64
|
+
blocks: [],
|
|
65
|
+
failed_steps: [],
|
|
66
|
+
running_steps: [],
|
|
67
|
+
execution_plan: {},
|
|
68
|
+
state: 'running',
|
|
69
|
+
result: '',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const createStoreForTaskPayload = (overrides, apiStatus = STATUS.RESOLVED) => ({
|
|
73
|
+
API: {
|
|
74
|
+
[FOREMAN_TASK_DETAILS]: {
|
|
75
|
+
response: { ...baseTaskPayload, ...overrides },
|
|
76
|
+
status: apiStatus,
|
|
77
|
+
payload: {},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const expectHeaderActionsHidden = () => {
|
|
83
|
+
expect(
|
|
84
|
+
screen.queryByRole('button', { name: /cancel/i })
|
|
85
|
+
).not.toBeInTheDocument();
|
|
86
|
+
expect(
|
|
87
|
+
screen.queryByRole('button', { name: /^task actions$/i })
|
|
88
|
+
).not.toBeInTheDocument();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const getHeaderTitleArea = title =>
|
|
92
|
+
screen.getByRole('heading', { level: 1, name: title }).parentElement
|
|
93
|
+
.parentElement;
|
|
94
|
+
|
|
95
|
+
const renderPage = (
|
|
96
|
+
apiPayloadOverrides = {},
|
|
97
|
+
propsOverrides = {},
|
|
98
|
+
apiStatus = STATUS.RESOLVED
|
|
99
|
+
) => {
|
|
100
|
+
const history = createMemoryHistory({
|
|
101
|
+
initialEntries: [`/foreman_tasks/tasks/${matchDefault.params.id}`],
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
window.history.pushState(
|
|
105
|
+
{},
|
|
106
|
+
'',
|
|
107
|
+
`/foreman_tasks/tasks/${matchDefault.params.id}`
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return renderWithStore(
|
|
111
|
+
<IntlProvider locale="en">
|
|
112
|
+
<Router history={history}>
|
|
113
|
+
<TaskDetailsPage
|
|
114
|
+
{...routerPropsBase}
|
|
115
|
+
history={history}
|
|
116
|
+
match={matchDefault}
|
|
117
|
+
{...propsOverrides}
|
|
118
|
+
/>
|
|
119
|
+
</Router>
|
|
120
|
+
</IntlProvider>,
|
|
121
|
+
createStoreForTaskPayload(apiPayloadOverrides, apiStatus)
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
describe('TaskDetailsPage', () => {
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
mockUseForemanPermissions.mockImplementation(
|
|
128
|
+
() => new Set(['view_foreman_tasks'])
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
afterEach(() => {
|
|
133
|
+
jest.clearAllMocks();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('permissions', () => {
|
|
137
|
+
it(`renders the task details chrome when ${VIEW_FOREMAN_TASKS} is present`, () => {
|
|
138
|
+
mockUseForemanPermissions.mockImplementation(
|
|
139
|
+
() => new Set(['edit_foreman_tasks', VIEW_FOREMAN_TASKS])
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
renderPage({ action: 'Run job' });
|
|
143
|
+
|
|
144
|
+
expect(
|
|
145
|
+
screen.getByRole('navigation', { name: 'Breadcrumb' })
|
|
146
|
+
).toBeInTheDocument();
|
|
147
|
+
expect(
|
|
148
|
+
screen.getByRole('heading', {
|
|
149
|
+
level: 1,
|
|
150
|
+
name: 'Run job',
|
|
151
|
+
})
|
|
152
|
+
).toBeInTheDocument();
|
|
153
|
+
expect(
|
|
154
|
+
screen.queryByRole('heading', { name: /Permission denied/i })
|
|
155
|
+
).not.toBeInTheDocument();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it(`shows ResourceLoadFailedEmptyState and lists ${VIEW_FOREMAN_TASKS} when it is absent`, () => {
|
|
159
|
+
mockUseForemanPermissions.mockImplementation(() => new Set());
|
|
160
|
+
|
|
161
|
+
renderPage({ action: 'Hidden task' });
|
|
162
|
+
|
|
163
|
+
expect(
|
|
164
|
+
screen.getByRole('heading', { name: /Permission denied/i })
|
|
165
|
+
).toBeInTheDocument();
|
|
166
|
+
expect(
|
|
167
|
+
screen.getByText(
|
|
168
|
+
/You do not have permission to view the task with id task-route-id/
|
|
169
|
+
)
|
|
170
|
+
).toBeInTheDocument();
|
|
171
|
+
expect(screen.getByText(VIEW_FOREMAN_TASKS)).toBeInTheDocument();
|
|
172
|
+
expect(
|
|
173
|
+
screen.getByRole('button', { name: /Back to tasks/i })
|
|
174
|
+
).toBeInTheDocument();
|
|
175
|
+
expect(
|
|
176
|
+
screen.getByRole('navigation', { name: 'Breadcrumb' })
|
|
177
|
+
).toBeInTheDocument();
|
|
178
|
+
expect(
|
|
179
|
+
screen.queryByRole('tab', { name: /^task$/i })
|
|
180
|
+
).not.toBeInTheDocument();
|
|
181
|
+
expectHeaderActionsHidden();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('denies access when user only has edit_foreman_tasks without view', () => {
|
|
185
|
+
mockUseForemanPermissions.mockImplementation(
|
|
186
|
+
() => new Set(['edit_foreman_tasks'])
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
renderPage({});
|
|
190
|
+
|
|
191
|
+
expect(
|
|
192
|
+
screen.getByRole('heading', { name: /Permission denied/i })
|
|
193
|
+
).toBeInTheDocument();
|
|
194
|
+
expect(screen.getByText(VIEW_FOREMAN_TASKS)).toBeInTheDocument();
|
|
195
|
+
expect(
|
|
196
|
+
screen.getByRole('navigation', { name: 'Breadcrumb' })
|
|
197
|
+
).toBeInTheDocument();
|
|
198
|
+
expect(
|
|
199
|
+
screen.queryByRole('tab', { name: /^task$/i })
|
|
200
|
+
).not.toBeInTheDocument();
|
|
201
|
+
expectHeaderActionsHidden();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('hides header actions when the task API returns an error', () => {
|
|
205
|
+
renderPage({ action: 'Failed load' }, {}, STATUS.ERROR);
|
|
206
|
+
|
|
207
|
+
expect(
|
|
208
|
+
screen.getByRole('heading', { name: /unable to load task/i })
|
|
209
|
+
).toBeInTheDocument();
|
|
210
|
+
expectHeaderActionsHidden();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('shows running status icon when action is unset', () => {
|
|
215
|
+
renderPage({});
|
|
216
|
+
|
|
217
|
+
expect(
|
|
218
|
+
screen.getByRole('heading', { level: 1, name: 'Task Details' })
|
|
219
|
+
).toBeInTheDocument();
|
|
220
|
+
|
|
221
|
+
expect(screen.getByRole('navigation', { name: 'Breadcrumb' })).toHaveTextContent(
|
|
222
|
+
'Tasks'
|
|
223
|
+
);
|
|
224
|
+
expect(
|
|
225
|
+
within(screen.getByRole('navigation', { name: 'Breadcrumb' })).getByText(
|
|
226
|
+
/task-route-id/
|
|
227
|
+
)
|
|
228
|
+
).toBeInTheDocument();
|
|
229
|
+
|
|
230
|
+
const titleArea = getHeaderTitleArea('Task Details');
|
|
231
|
+
|
|
232
|
+
expect(within(titleArea).getByTitle('Running')).toBeInTheDocument();
|
|
233
|
+
expect(within(titleArea).queryByTitle('Error')).not.toBeInTheDocument();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('uses task action for title and breadcrumb when loaded', () => {
|
|
237
|
+
renderPage({ action: 'Refresh hosts' });
|
|
238
|
+
|
|
239
|
+
expect(
|
|
240
|
+
screen.getByRole('heading', { level: 1, name: 'Refresh hosts' })
|
|
241
|
+
).toBeInTheDocument();
|
|
242
|
+
expect(
|
|
243
|
+
screen.getByRole('button', { name: /cancel/i })
|
|
244
|
+
).toBeInTheDocument();
|
|
245
|
+
|
|
246
|
+
expect(screen.getByRole('link', { name: /^Tasks$/ })).toHaveAttribute(
|
|
247
|
+
'href',
|
|
248
|
+
'/foreman_tasks/tasks'
|
|
249
|
+
);
|
|
250
|
+
expect(
|
|
251
|
+
within(screen.getByRole('navigation', { name: 'Breadcrumb' })).getByText(
|
|
252
|
+
'Refresh hosts'
|
|
253
|
+
)
|
|
254
|
+
).toBeInTheDocument();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('shows error status icon when task is stopped with error result', () => {
|
|
258
|
+
renderPage({
|
|
259
|
+
action: 'Some action',
|
|
260
|
+
state: 'stopped',
|
|
261
|
+
result: 'error',
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
expect(
|
|
265
|
+
screen.getByRole('heading', { level: 1, name: 'Some action' })
|
|
266
|
+
).toBeInTheDocument();
|
|
267
|
+
expect(
|
|
268
|
+
within(getHeaderTitleArea('Some action')).getByTitle('Error')
|
|
269
|
+
).toBeInTheDocument();
|
|
270
|
+
|
|
271
|
+
expect(
|
|
272
|
+
within(screen.getByRole('navigation', { name: 'Breadcrumb' })).getByText(
|
|
273
|
+
'Some action'
|
|
274
|
+
)
|
|
275
|
+
).toBeInTheDocument();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { bindActionCreators } from 'redux';
|
|
2
2
|
import { connect } from 'react-redux';
|
|
3
|
-
import
|
|
4
|
-
import * as
|
|
5
|
-
import * as taskActions from '../TaskActions';
|
|
3
|
+
import * as taskDetailsActions from '../../Components/TaskDetails/TaskDetailsActions';
|
|
4
|
+
import * as taskActions from '../../Components/TaskActions';
|
|
6
5
|
import {
|
|
7
6
|
selectEndedAt,
|
|
8
7
|
selectStartAt,
|
|
@@ -15,7 +14,6 @@ import {
|
|
|
15
14
|
selectResumable,
|
|
16
15
|
selectCancellable,
|
|
17
16
|
selectHelp,
|
|
18
|
-
selectErrors,
|
|
19
17
|
selectProgress,
|
|
20
18
|
selectUsername,
|
|
21
19
|
selectLabel,
|
|
@@ -33,14 +31,17 @@ import {
|
|
|
33
31
|
selectExternalId,
|
|
34
32
|
selectDynflowEnableConsole,
|
|
35
33
|
selectCanEdit,
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
selectAPIStatus,
|
|
35
|
+
selectAPIErrorMessage,
|
|
36
|
+
selectAPIErrorCode,
|
|
38
37
|
selectIsLoading,
|
|
39
38
|
selectDependsOn,
|
|
40
39
|
selectBlocks,
|
|
41
|
-
} from '
|
|
40
|
+
} from '../../Components/TaskDetails/TaskDetailsSelectors';
|
|
41
|
+
import TaskDetailsPage from './TaskDetailsPage';
|
|
42
42
|
|
|
43
|
-
const mapStateToProps = state => ({
|
|
43
|
+
const mapStateToProps = (state, ownProps) => ({
|
|
44
|
+
id: ownProps.match.params.id,
|
|
44
45
|
startAt: selectStartAt(state),
|
|
45
46
|
startBefore: selectStartBefore(state),
|
|
46
47
|
startedAt: selectStartedAt(state),
|
|
@@ -49,7 +50,6 @@ const mapStateToProps = state => ({
|
|
|
49
50
|
output: selectOutput(state),
|
|
50
51
|
resumable: selectResumable(state),
|
|
51
52
|
cancellable: selectCancellable(state),
|
|
52
|
-
errors: selectErrors(state),
|
|
53
53
|
progress: selectProgress(state),
|
|
54
54
|
username: selectUsername(state),
|
|
55
55
|
label: selectLabel(state),
|
|
@@ -70,8 +70,9 @@ const mapStateToProps = state => ({
|
|
|
70
70
|
externalId: selectExternalId(state),
|
|
71
71
|
dynflowEnableConsole: selectDynflowEnableConsole(state),
|
|
72
72
|
canEdit: selectCanEdit(state),
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
apiStatus: selectAPIStatus(state),
|
|
74
|
+
apiErrorMessage: selectAPIErrorMessage(state),
|
|
75
|
+
apiErrorCode: selectAPIErrorCode(state),
|
|
75
76
|
isLoading: selectIsLoading(state),
|
|
76
77
|
dependsOn: selectDependsOn(state),
|
|
77
78
|
blocks: selectBlocks(state),
|
|
@@ -80,4 +81,4 @@ const mapStateToProps = state => ({
|
|
|
80
81
|
const mapDispatchToProps = dispatch =>
|
|
81
82
|
bindActionCreators({ ...taskActions, ...taskDetailsActions }, dispatch);
|
|
82
83
|
|
|
83
|
-
export default connect(mapStateToProps, mapDispatchToProps)(
|
|
84
|
+
export default connect(mapStateToProps, mapDispatchToProps)(TaskDetailsPage);
|
data/webpack/Routes/routes.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import TasksTableIndexPage from '../ForemanTasks/Components/TasksTable/TasksIndexPage';
|
|
3
|
+
import TaskDetailsPage from '../ForemanTasks/Routes/ShowTaskDetails';
|
|
3
4
|
|
|
4
5
|
const ForemanTasksRoutes = [
|
|
5
6
|
{
|
|
@@ -7,6 +8,11 @@ const ForemanTasksRoutes = [
|
|
|
7
8
|
exact: true,
|
|
8
9
|
render: props => <TasksTableIndexPage {...props} />,
|
|
9
10
|
},
|
|
11
|
+
{
|
|
12
|
+
path: '/foreman_tasks/tasks/:id',
|
|
13
|
+
exact: true,
|
|
14
|
+
render: props => <TaskDetailsPage {...props} />,
|
|
15
|
+
},
|
|
10
16
|
{
|
|
11
17
|
path: '/foreman_tasks/tasks/:id/sub_tasks',
|
|
12
18
|
exact: true,
|
|
@@ -12,6 +12,14 @@ jest.mock(
|
|
|
12
12
|
}
|
|
13
13
|
);
|
|
14
14
|
|
|
15
|
+
jest.mock(
|
|
16
|
+
'../ForemanTasks/Routes/ShowTaskDetails',
|
|
17
|
+
() =>
|
|
18
|
+
function TaskDetailsPageStub() {
|
|
19
|
+
return <div data-testid="task-details-page-stub" />;
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
|
|
15
23
|
const routerProps = {
|
|
16
24
|
history: { push: jest.fn(), replace: jest.fn(), go: jest.fn() },
|
|
17
25
|
location: {
|
|
@@ -34,6 +42,7 @@ describe('ForemanTasks routes', () => {
|
|
|
34
42
|
ForemanTasksRoutes.map(({ path, exact }) => ({ path, exact }))
|
|
35
43
|
).toEqual([
|
|
36
44
|
{ path: '/foreman_tasks/tasks', exact: true },
|
|
45
|
+
{ path: '/foreman_tasks/tasks/:id', exact: true },
|
|
37
46
|
{ path: '/foreman_tasks/tasks/:id/sub_tasks', exact: true },
|
|
38
47
|
]);
|
|
39
48
|
});
|
|
@@ -53,9 +62,9 @@ describe('ForemanTasks routes', () => {
|
|
|
53
62
|
...routerProps,
|
|
54
63
|
match: {
|
|
55
64
|
...routerProps.match,
|
|
56
|
-
params: { id: '
|
|
57
|
-
path: '/foreman_tasks/tasks/:id
|
|
58
|
-
url: '/foreman_tasks/tasks/
|
|
65
|
+
params: { id: '99' },
|
|
66
|
+
path: '/foreman_tasks/tasks/:id',
|
|
67
|
+
url: '/foreman_tasks/tasks/99',
|
|
59
68
|
},
|
|
60
69
|
},
|
|
61
70
|
];
|
|
@@ -63,8 +72,10 @@ describe('ForemanTasks routes', () => {
|
|
|
63
72
|
ForemanTasksRoutes.forEach((route, index) => {
|
|
64
73
|
const { unmount } = render(route.render(propsByIndex[index]));
|
|
65
74
|
|
|
66
|
-
if (index ===
|
|
67
|
-
expect(
|
|
75
|
+
if (index === 1) {
|
|
76
|
+
expect(
|
|
77
|
+
screen.getByTestId('task-details-page-stub')
|
|
78
|
+
).toBeInTheDocument();
|
|
68
79
|
} else {
|
|
69
80
|
expect(
|
|
70
81
|
screen.getByTestId('tasks-table-index-stub')
|
data/webpack/index.js
CHANGED
|
@@ -3,13 +3,8 @@
|
|
|
3
3
|
/* eslint-disable import/extensions */
|
|
4
4
|
import componentRegistry from 'foremanReact/components/componentRegistry';
|
|
5
5
|
import TasksDashboard from './ForemanTasks/Components/TasksDashboard';
|
|
6
|
-
import TaskDetails from './ForemanTasks/Components/TaskDetails';
|
|
7
6
|
|
|
8
7
|
componentRegistry.register({
|
|
9
8
|
name: 'TasksDashboard',
|
|
10
9
|
type: TasksDashboard,
|
|
11
10
|
});
|
|
12
|
-
componentRegistry.register({
|
|
13
|
-
name: 'TaskDetails',
|
|
14
|
-
type: TaskDetails,
|
|
15
|
-
});
|