foreman-tasks 12.2.4 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/foreman_tasks/tasks_controller.rb +0 -5
  3. data/config/routes.rb +3 -2
  4. data/lib/foreman_tasks/engine.rb +2 -2
  5. data/lib/foreman_tasks/version.rb +1 -1
  6. data/test/controllers/tasks_controller_test.rb +0 -9
  7. data/test/foreman_tasks_test_helper.rb +2 -2
  8. data/test/integration/tasks_test.rb +17 -0
  9. data/test/test_plugin_helper.rb +8 -0
  10. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +6 -51
  11. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskSkeleton.js +1 -6
  12. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +0 -2
  13. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +0 -2
  14. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +29 -15
  15. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +1 -5
  16. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsConstants.js +2 -1
  17. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +20 -5
  18. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +1 -1
  19. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +97 -10
  20. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetailsSelectors.test.js +81 -0
  21. data/webpack/ForemanTasks/Components/TaskDetails/index.js +6 -4
  22. data/webpack/ForemanTasks/Components/common/taskResultIcon.js +53 -0
  23. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsPage.js +74 -0
  24. data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +265 -0
  25. data/webpack/Routes/routes.js +6 -0
  26. data/webpack/Routes/routes.test.js +16 -5
  27. metadata +9 -2
  28. data/app/views/foreman_tasks/tasks/show.html.erb +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23f5d844abe59e29a92b15f62c0770e4c3c1b577a1315232c422d976fa77a07e
4
- data.tar.gz: 5d6316649f6cb75894b966301e5d8abefa579d472ddb33052d06e9d32210b869
3
+ metadata.gz: 9fa716da28a9b6bf38240aa02e47f9ab4a4ab6ba82f98c5d26f7d6066f3149e4
4
+ data.tar.gz: eb7ec125870236abf803a65c132b042fd4360f4de5d7d9a5b2bc718ba8672235
5
5
  SHA512:
6
- metadata.gz: 7b16c2a1cd40296f38805bddb2a96879ed09d4288e7dc03c8061ba6b561333c96f9e9aa213b583349bce844580b85cc24833ca49f617e67730d3e6b28edf4773
7
- data.tar.gz: 2b147d1a8a95389cb7c6a12c4fefea577cdec04f8c0ca66315b4411e62f968b1255b4a82438c014300bfd8d67eb0b9a0bea6fcaf277f1eb2d7058a8feaf5375f
6
+ metadata.gz: 0547b82e39eabef4f7205100733272e5bdb7b619721ea1a9b94259403c1be370a4ee34564825ccbca83ed16767e72634dd2a070f288ceed28627c3c58671d726
7
+ data.tar.gz: c27122956334571f9b1cc287e8ec6cf322f5f2931be69780539cc58a7f37cbf7f7c4c81800807fc81a79ca8213d1510c127552492ee942c21a1067eb09258a9e
@@ -6,11 +6,6 @@ module ForemanTasks
6
6
 
7
7
  before_action :find_dynflow_task, only: [:unlock, :force_unlock, :cancel, :abort, :cancel_step, :resume]
8
8
 
9
- def show
10
- @task = resource_base.find(params[:id])
11
- render :layout => !request.xhr?
12
- end
13
-
14
9
  def index
15
10
  params[:order] ||= 'started_at DESC'
16
11
  respond_with_tasks resource_base
data/config/routes.rb CHANGED
@@ -12,7 +12,7 @@ Foreman::Application.routes.draw do
12
12
  end
13
13
  end
14
14
 
15
- resources :tasks, :only => [:show] do
15
+ resources :tasks, :only => [] do
16
16
  collection do
17
17
  get 'auto_complete_search'
18
18
  get '/summary/:recent_timeframe', action: 'summary'
@@ -27,7 +27,7 @@ Foreman::Application.routes.draw do
27
27
  post :cancel_step
28
28
  end
29
29
  end
30
- resources :tasks, :only => [:show], constraints: ->(req) { req.format == :csv } do
30
+ resources :tasks, :only => [], constraints: ->(req) { req.format == :csv } do
31
31
  member do
32
32
  get :sub_tasks
33
33
  end
@@ -35,6 +35,7 @@ Foreman::Application.routes.draw do
35
35
  resources :tasks, :only => [:index], constraints: ->(req) { req.format == :csv }
36
36
 
37
37
  match '/tasks', to: '/react#index', via: :get
38
+ match '/tasks/:id', to: '/react#index', via: :get, :as => :task
38
39
  match '/tasks/:id/sub_tasks', to: '/react#index', via: :get
39
40
 
40
41
  namespace :api do
@@ -20,7 +20,7 @@ module ForemanTasks
20
20
  require 'foreman/cron'
21
21
 
22
22
  Foreman::Plugin.register :"foreman-tasks" do
23
- requires_foreman '>= 3.19'
23
+ requires_foreman '>= 5.0'
24
24
  register_global_js_file 'global'
25
25
  divider :top_menu, :parent => :monitor_menu, :last => true, :caption => N_('Foreman Tasks')
26
26
  menu :top_menu, :tasks,
@@ -36,7 +36,7 @@ module ForemanTasks
36
36
  :last => true
37
37
 
38
38
  security_block :foreman_tasks do |_map|
39
- permission :view_foreman_tasks, { :'foreman_tasks/tasks' => [:auto_complete_search, :sub_tasks, :index, :summary, :summary_sub_tasks, :show],
39
+ permission :view_foreman_tasks, { :'foreman_tasks/tasks' => [:auto_complete_search, :sub_tasks, :index, :summary, :summary_sub_tasks],
40
40
  :'foreman_tasks/api/tasks' => [:bulk_search, :show, :index, :summary, :summary_sub_tasks, :details, :sub_tasks] }, :resource_type => 'ForemanTasks::Task'
41
41
  permission :edit_foreman_tasks, { :'foreman_tasks/tasks' => [:resume, :unlock, :force_unlock, :cancel_step, :cancel, :abort],
42
42
  :'foreman_tasks/api/tasks' => [:bulk_resume, :bulk_cancel, :bulk_stop] }, :resource_type => 'ForemanTasks::Task'
@@ -1,3 +1,3 @@
1
1
  module ForemanTasks
2
- VERSION = '12.2.4'.freeze
2
+ VERSION = '13.0.0'.freeze
3
3
  end
@@ -82,15 +82,6 @@ module ForemanTasks
82
82
  assert_include response.body.lines[1], 'Some action'
83
83
  end
84
84
 
85
- describe 'show' do
86
- it 'does not allow user without permissions to see task details' do
87
- setup_user('view', 'foreman_tasks', 'owner.id = current_user')
88
- get :show, params: { id: FactoryBot.create(:some_task).id },
89
- session: set_session_user(User.current)
90
- assert_response :not_found
91
- end
92
- end
93
-
94
85
  describe 'index' do
95
86
  it 'shows duration column' do
96
87
  task = ForemanTasks::Task.select_duration.find(FactoryBot.create(:some_task).id)
@@ -9,8 +9,8 @@ require_relative './support/history_tasks_builder'
9
9
  require 'dynflow/testing'
10
10
  require 'foreman_tasks/test_helpers'
11
11
 
12
- FactoryBot.definition_file_paths = ["#{ForemanTasks::Engine.root}/test/factories"]
13
- FactoryBot.find_definitions
12
+ FactoryBot.definition_file_paths << "#{ForemanTasks::Engine.root}/test/factories"
13
+ FactoryBot.reload
14
14
 
15
15
  ForemanTasks.dynflow.require!
16
16
  ForemanTasks.dynflow.config.disable_active_record_actions = true
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_plugin_helper'
4
+ require 'integration_test_helper'
5
+
6
+ class TasksIntegrationTest < IntegrationTestWithJavascript
7
+ test 'does not allow user without permissions to see task details' do
8
+ setup_user('view', 'foreman_tasks', 'owner.id = current_user')
9
+ task = FactoryBot.create(:some_task)
10
+ set_request_user(User.current)
11
+
12
+ visit "/foreman_tasks/tasks/#{task.id}"
13
+
14
+ assert_selector 'h5', text: /Unable to load task/i
15
+ assert_no_selector '#task-details-tabs'
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This calls the main test_helper in Foreman-core
4
+ require 'test_helper'
5
+
6
+ # Add plugin to FactoryBot's paths
7
+ FactoryBot.definition_file_paths << File.join(File.dirname(__FILE__), 'factories')
8
+ FactoryBot.reload
@@ -5,17 +5,12 @@ import {
5
5
  GridItem,
6
6
  Progress,
7
7
  ProgressVariant,
8
- Icon,
9
8
  } from '@patternfly/react-core';
10
- import {
11
- CheckCircleIcon,
12
- ExclamationCircleIcon,
13
- ExclamationTriangleIcon,
14
- QuestionCircleIcon,
15
- } from '@patternfly/react-icons';
16
9
  import { translate as __ } from 'foremanReact/common/I18n';
17
10
  import RelativeDateTime from 'foremanReact/components/common/dates/RelativeDateTime';
18
11
 
12
+ import { taskResultIconEl } from '../../common/taskResultIcon';
13
+
19
14
  const isDelayed = ({ startAt, startedAt }) => {
20
15
  if (
21
16
  startAt == null ||
@@ -35,41 +30,6 @@ const isDelayed = ({ startAt, startedAt }) => {
35
30
  return a.getTime() !== b.getTime();
36
31
  };
37
32
 
38
- const resultIconEl = (state, result) => {
39
- if (state !== 'stopped')
40
- return (
41
- <Icon>
42
- <QuestionCircleIcon />
43
- </Icon>
44
- );
45
- switch (result) {
46
- case 'success':
47
- return (
48
- <Icon status="success">
49
- <CheckCircleIcon />
50
- </Icon>
51
- );
52
- case 'error':
53
- return (
54
- <Icon status="danger">
55
- <ExclamationCircleIcon />
56
- </Icon>
57
- );
58
- case 'warning':
59
- return (
60
- <Icon status="warning">
61
- <ExclamationTriangleIcon />
62
- </Icon>
63
- );
64
- default:
65
- return (
66
- <Icon>
67
- <QuestionCircleIcon />
68
- </Icon>
69
- );
70
- }
71
- };
72
-
73
33
  const progressVariantForResult = result => {
74
34
  switch (result) {
75
35
  case 'error':
@@ -117,7 +77,7 @@ const TaskInfo = props => {
117
77
  title: 'Result',
118
78
  value: (
119
79
  <span>
120
- {resultIconEl(state, result)} {result}
80
+ {taskResultIconEl(state, result)} {result}
121
81
  </span>
122
82
  ),
123
83
  },
@@ -182,14 +142,9 @@ const TaskInfo = props => {
182
142
  </React.Fragment>
183
143
  ))}
184
144
  <GridItem span={12} className="pf-v5-u-pb-lg" />
185
- <GridItem span={6}>
186
- <div className="progress-description">
187
- <span className="list-group-item-heading">{__('State')}: </span>
188
- {state}
189
- </div>
190
- </GridItem>
191
- <GridItem span={6} className="progress-label-top-right">
192
- <span>{`${progress}% ${__('Complete')}`}</span>
145
+ <GridItem span={12}>
146
+ <span className="list-group-item-heading">{__('State')}: </span>
147
+ {state}
193
148
  </GridItem>
194
149
  <GridItem span={12}>
195
150
  <Progress
@@ -27,12 +27,7 @@ export const TaskSkeleton = () => {
27
27
  </React.Fragment>
28
28
  ))}
29
29
  <GridItem span={12} className="pf-v5-u-pb-lg" />
30
- <GridItem span={6}>
31
- <div className="progress-description">
32
- <Skeleton />
33
- </div>
34
- </GridItem>
35
- <GridItem span={6} className="progress-label-top-right">
30
+ <GridItem span={12}>
36
31
  <Skeleton />
37
32
  </GridItem>
38
33
  <GridItem span={12}>
@@ -1,7 +1,6 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom';
4
- import { STATUS } from 'foremanReact/constants';
5
4
 
6
5
  import Task from '../Task';
7
6
 
@@ -29,7 +28,6 @@ describe('Task', () => {
29
28
  parentTask="parent-id"
30
29
  taskReload
31
30
  canEdit
32
- status={STATUS.RESOLVED}
33
31
  taskProgressToggle={jest.fn()}
34
32
  taskReloadStart={jest.fn()}
35
33
  />
@@ -1,7 +1,6 @@
1
1
  import React from 'react';
2
2
  import { render, screen, fireEvent } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom';
4
- import { STATUS } from 'foremanReact/constants';
5
4
  import { TaskButtons } from '../TaskButtons';
6
5
 
7
6
  const setUnlockModalOpen = jest.fn();
@@ -169,7 +168,6 @@ describe('TaskButtons', () => {
169
168
  resumeTaskRequest,
170
169
  taskProgressToggle,
171
170
  taskReloadStart,
172
- status: STATUS.RESOLVED,
173
171
  canEdit: true,
174
172
  resumable: true,
175
173
  cancellable: true,
@@ -1,15 +1,17 @@
1
1
  import React, { useEffect, useState } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { Tabs, Tab, TabTitleText } from '@patternfly/react-core';
4
- import { translate as __, sprintf } from 'foremanReact/common/I18n';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
5
  import { STATUS } from 'foremanReact/constants';
6
- import MessageBox from 'foremanReact/components/common/MessageBox';
6
+ import { usePermissions } from 'foremanReact/common/hooks/Permissions/permissionHooks';
7
+ import { ResourceLoadFailedEmptyState } from 'foremanReact/components/common/EmptyState';
7
8
  import Task from './Components/Task';
8
9
  import RunningSteps from './Components/RunningSteps';
9
10
  import Errors from './Components/Errors';
10
11
  import Locks from './Components/Locks';
11
12
  import Raw from './Components/Raw';
12
13
  import Dependencies from './Components/Dependencies';
14
+ import { TASKS_PATH, VIEW_FOREMAN_TASKS } from './TaskDetailsConstants';
13
15
  import { getTaskID } from './TasksDetailsHelper';
14
16
  import { TaskSkeleton } from './Components/TaskSkeleton';
15
17
 
@@ -26,12 +28,15 @@ const TaskDetails = ({
26
28
  cancelStep,
27
29
  taskReloadStart,
28
30
  taskReloadStop,
29
- APIerror,
31
+ apiStatus,
32
+ apiErrorMessage,
33
+ apiErrorCode,
30
34
  ...props
31
35
  }) => {
32
36
  const id = getTaskID();
33
- const { taskReload, status, isLoading, result } = props;
37
+ const { taskReload, isLoading, result } = props;
34
38
  const [activeTabKey, setActiveTabKey] = useState(1);
39
+ const hasViewPermission = usePermissions([VIEW_FOREMAN_TASKS]);
35
40
 
36
41
  useEffect(() => {
37
42
  taskReloadStart(id);
@@ -48,15 +53,25 @@ const TaskDetails = ({
48
53
  }
49
54
  };
50
55
 
51
- if (status === STATUS.ERROR) {
56
+ if (apiStatus === STATUS.ERROR || !hasViewPermission) {
52
57
  return (
53
- <MessageBox
54
- key="task-details-error"
55
- icontype="error-circle-o"
56
- msg={sprintf(__('Could not receive data: %s'), APIerror?.message)}
58
+ <ResourceLoadFailedEmptyState
59
+ resourceLabel={__('task')}
60
+ resourceId={id}
61
+ httpStatus={apiErrorCode}
62
+ errorMessage={apiErrorMessage}
63
+ viewPermissions={['view_foreman_tasks']}
64
+ requiredPermissions={['view_foreman_tasks']}
65
+ ouiaIdPrefix="task-details-empty-state"
66
+ primaryAction={{
67
+ label: __('Back to tasks'),
68
+ url: TASKS_PATH,
69
+ ouiaId: 'task-details-empty-state-tasks-list',
70
+ }}
57
71
  />
58
72
  );
59
73
  }
74
+
60
75
  const resumable = executionPlan ? executionPlan.state === 'paused' : false;
61
76
  const cancellable = executionPlan ? executionPlan.cancellable : false;
62
77
  const lockRecords = locks.concat(links);
@@ -66,13 +81,12 @@ const TaskDetails = ({
66
81
  cancellable,
67
82
  resumable,
68
83
  id,
69
- status,
70
84
  taskProgressToggle,
71
85
  taskReloadStart,
72
86
  };
73
87
 
74
88
  return (
75
- <div className="task-details-react well">
89
+ <div className="task-details-react">
76
90
  <Tabs
77
91
  id="task-details-tabs"
78
92
  ouiaId="task-details-tabs"
@@ -159,8 +173,9 @@ TaskDetails.propTypes = {
159
173
  runningSteps: PropTypes.array,
160
174
  cancelStep: PropTypes.func.isRequired,
161
175
  taskReload: PropTypes.bool.isRequired,
162
- status: PropTypes.oneOf(Object.keys(STATUS)),
163
- APIerror: PropTypes.object,
176
+ apiStatus: PropTypes.oneOf(Object.keys(STATUS)),
177
+ apiErrorMessage: PropTypes.string,
178
+ apiErrorCode: PropTypes.number,
164
179
  taskReloadStop: PropTypes.func.isRequired,
165
180
  taskReloadStart: PropTypes.func.isRequired,
166
181
  links: PropTypes.array,
@@ -175,8 +190,7 @@ TaskDetails.propTypes = {
175
190
  TaskDetails.defaultProps = {
176
191
  label: '',
177
192
  runningSteps: [],
178
- APIerror: null,
179
- status: STATUS.PENDING,
193
+ apiErrorMessage: '',
180
194
  links: [],
181
195
  dependsOn: [],
182
196
  blocks: [],
@@ -1,10 +1,6 @@
1
1
  .task-details-react {
2
- .progress-label-top-right {
3
- font-size: 11px;
4
- text-align: right;
5
- }
6
2
  a,
7
- button {
3
+ #pf-tab-section-1-task-details-tabs > button {
8
4
  margin-right: 3px;
9
5
  }
10
6
  .container {
@@ -1,4 +1,5 @@
1
1
  export const FOREMAN_TASK_DETAILS = 'FOREMAN_TASK_DETAILS';
2
2
  export const FOREMAN_TASK_DETAILS_SUCCESS = 'FOREMAN_TASK_DETAILS_SUCCESS';
3
-
3
+ export const TASKS_PATH = '/foreman_tasks/tasks';
4
4
  export const TASK_STEP_CANCEL = 'TASK_STEP_CANCEL';
5
+ export const VIEW_FOREMAN_TASKS = 'view_foreman_tasks';
@@ -1,7 +1,8 @@
1
1
  /* eslint-disable camelcase */
2
2
  import {
3
3
  selectAPIResponse,
4
- selectAPIByKey,
4
+ selectAPIStatus as selectAPIStatusByKey,
5
+ selectAPIError as selectAPIErrorByKey,
5
6
  } from 'foremanReact/redux/API/APISelectors';
6
7
  import { selectDoesIntervalExist } from 'foremanReact/redux/middlewares/IntervalMiddleware/IntervalSelectors';
7
8
  import { STATUS } from 'foremanReact/constants';
@@ -101,14 +102,28 @@ export const selectDynflowEnableConsole = state =>
101
102
  export const selectCanEdit = state =>
102
103
  selectTaskDetailsResponse(state).can_edit || false;
103
104
 
104
- export const selectStatus = state => selectTaskDetailsResponse(state).status;
105
+ export const selectAPIStatus = state =>
106
+ selectAPIStatusByKey(state, FOREMAN_TASK_DETAILS);
105
107
 
106
108
  export const selectAPIError = state =>
107
- selectTaskDetailsResponse(state)?.APIerror;
109
+ selectAPIErrorByKey(state, FOREMAN_TASK_DETAILS);
110
+
111
+ export const selectAPIErrorMessage = state => {
112
+ const apiError = selectAPIError(state);
113
+
114
+ if (apiError?.response?.data?.error) {
115
+ return apiError.response.data.error.message;
116
+ }
117
+
118
+ return apiError?.message;
119
+ };
120
+
121
+ export const selectAPIErrorCode = state =>
122
+ selectAPIError(state)?.response?.status;
108
123
 
109
124
  export const selectIsLoading = state =>
110
- !!selectAPIByKey(state, FOREMAN_TASK_DETAILS).response &&
111
- selectStatus(state) === STATUS.PENDING;
125
+ !Object.keys(selectTaskDetailsResponse(state) ?? {}).length &&
126
+ selectAPIStatus(state) === STATUS.PENDING;
112
127
 
113
128
  export const selectDependsOn = state =>
114
129
  selectTaskDetailsResponse(state).depends_on || [];
@@ -4,5 +4,5 @@ export const minProps = {
4
4
  taskReloadStop: jest.fn(),
5
5
  taskReloadStart: jest.fn(),
6
6
  taskProgressToggle: jest.fn(),
7
- status: 'RESOLVED',
7
+ apiStatus: 'RESOLVED',
8
8
  };
@@ -1,38 +1,125 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom';
4
+ import { configureStore } from '@reduxjs/toolkit';
5
+ import { Provider } from 'react-redux';
6
+ import { createMemoryHistory } from 'history';
7
+ import { Router } from 'react-router-dom';
4
8
 
5
9
  import TaskDetails from '../TaskDetails';
6
10
  import { minProps } from './TaskDetails.fixtures';
11
+ import { VIEW_FOREMAN_TASKS } from '../TaskDetailsConstants';
12
+
13
+ const mockUseForemanPermissions = jest.fn(
14
+ () => new Set(['view_foreman_tasks'])
15
+ );
16
+
17
+ jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
18
+ ...jest.requireActual('foremanReact/Root/Context/ForemanContext'),
19
+ useForemanPermissions: (...args) => mockUseForemanPermissions(...args),
20
+ }));
7
21
 
8
22
  delete window.location;
9
23
  window.location = new URL(
10
24
  'https://foreman.com/foreman_tasks/tasks/a15dd820-32f1-4ced-9ab7-c0fab8234c47/'
11
25
  );
12
26
 
27
+ const store = configureStore({ reducer: state => state || {} });
28
+
29
+ function renderTaskDetails(props = {}) {
30
+ const history = createMemoryHistory();
31
+
32
+ return render(
33
+ <Router history={history}>
34
+ <Provider store={store}>
35
+ <TaskDetails {...minProps} {...props} />
36
+ </Provider>
37
+ </Router>
38
+ );
39
+ }
40
+
13
41
  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
- />
42
+ beforeEach(() => {
43
+ mockUseForemanPermissions.mockImplementation(
44
+ () => new Set(['view_foreman_tasks'])
21
45
  );
46
+ });
47
+
48
+ it(`shows ResourceLoadFailedEmptyState when ${VIEW_FOREMAN_TASKS} is absent`, () => {
49
+ mockUseForemanPermissions.mockImplementation(() => new Set());
50
+
51
+ renderTaskDetails();
52
+
53
+ expect(
54
+ screen.getByRole('heading', { name: /permission denied/i })
55
+ ).toBeInTheDocument();
56
+ expect(screen.getByText(VIEW_FOREMAN_TASKS)).toBeInTheDocument();
57
+ expect(
58
+ screen.getByRole('button', { name: /back to tasks/i })
59
+ ).toBeInTheDocument();
60
+ expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
61
+ });
62
+
63
+ it('shows ResourceLoadFailedEmptyState when apiStatus is ERROR', () => {
64
+ renderTaskDetails({
65
+ apiStatus: 'ERROR',
66
+ apiErrorMessage: 'some-error',
67
+ });
68
+
69
+ expect(
70
+ screen.getByRole('heading', { name: /unable to load task/i })
71
+ ).toBeInTheDocument();
72
+ expect(
73
+ screen.getByText('Server returned: some-error')
74
+ ).toBeInTheDocument();
75
+ expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
76
+ });
77
+
78
+ it('shows permission denied when apiErrorCode is 403', () => {
79
+ renderTaskDetails({
80
+ apiStatus: 'ERROR',
81
+ apiErrorCode: 403,
82
+ apiErrorMessage: 'Forbidden',
83
+ });
84
+
85
+ expect(
86
+ screen.getByRole('heading', { name: /permission denied/i })
87
+ ).toBeInTheDocument();
88
+ expect(screen.getByText('view_foreman_tasks')).toBeInTheDocument();
89
+ expect(
90
+ screen.getByText('Server returned: Forbidden')
91
+ ).toBeInTheDocument();
92
+ expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
93
+ });
94
+
95
+ it('shows not found messaging when apiErrorCode is 404', () => {
96
+ renderTaskDetails({
97
+ apiStatus: 'ERROR',
98
+ apiErrorCode: 404,
99
+ apiErrorMessage: 'Task missing',
100
+ });
101
+
102
+ expect(
103
+ screen.getByRole('heading', { name: /unable to load task/i })
104
+ ).toBeInTheDocument();
105
+ expect(
106
+ screen.getByText(/could not be found/i)
107
+ ).toBeInTheDocument();
22
108
  expect(
23
- screen.getByText(/could not receive data: some-error/i)
109
+ screen.getByText('Server returned: Task missing')
24
110
  ).toBeInTheDocument();
111
+ expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
25
112
  });
26
113
 
27
114
  it('shows skeleton while loading on the Task tab', () => {
28
- const { container } = render(<TaskDetails {...minProps} isLoading />);
115
+ const { container } = renderTaskDetails({ isLoading: true });
29
116
  expect(
30
117
  container.querySelector('.react-loading-skeleton')
31
118
  ).toBeInTheDocument();
32
119
  });
33
120
 
34
121
  it('renders six tabs with expected labels', () => {
35
- render(<TaskDetails {...minProps} />);
122
+ renderTaskDetails();
36
123
  expect(document.getElementById('task-details-tabs')).toBeInTheDocument();
37
124
  expect(screen.getByRole('tab', { name: /^task$/i })).toBeInTheDocument();
38
125
  expect(
@@ -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
+ });
@@ -33,8 +33,9 @@ import {
33
33
  selectExternalId,
34
34
  selectDynflowEnableConsole,
35
35
  selectCanEdit,
36
- selectStatus,
37
- selectAPIError,
36
+ selectAPIStatus,
37
+ selectAPIErrorMessage,
38
+ selectAPIErrorCode,
38
39
  selectIsLoading,
39
40
  selectDependsOn,
40
41
  selectBlocks,
@@ -70,8 +71,9 @@ const mapStateToProps = state => ({
70
71
  externalId: selectExternalId(state),
71
72
  dynflowEnableConsole: selectDynflowEnableConsole(state),
72
73
  canEdit: selectCanEdit(state),
73
- status: selectStatus(state),
74
- APIerror: selectAPIError(state),
74
+ apiStatus: selectAPIStatus(state),
75
+ apiErrorMessage: selectAPIErrorMessage(state),
76
+ apiErrorCode: selectAPIErrorCode(state),
75
77
  isLoading: selectIsLoading(state),
76
78
  dependsOn: selectDependsOn(state),
77
79
  blocks: selectBlocks(state),
@@ -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,74 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useSelector } from 'react-redux';
4
+ import { Flex, FlexItem, TextContent, Text } from '@patternfly/react-core';
5
+ import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
6
+ 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';
14
+
15
+ const TaskDetailsPage = props => {
16
+ const { id } = props.match.params;
17
+ const action = useSelector(selectAction);
18
+ const taskState = useSelector(selectState);
19
+ const taskResult = useSelector(selectResult);
20
+ const headerText = action
21
+ ? sprintf(__('Details of %s task'), action)
22
+ : __('Task Details');
23
+
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
+ return (
44
+ <PageLayout
45
+ customHeader={header}
46
+ header={headerText}
47
+ searchable={false}
48
+ breadcrumbOptions={{
49
+ breadcrumbItems: [
50
+ { caption: __('Tasks'), url: '/foreman_tasks/tasks' },
51
+ { caption: action || id },
52
+ ],
53
+ isSwitchable: true,
54
+ resource: {
55
+ nameField: 'action',
56
+ resourceUrl: '/foreman_tasks/api/tasks',
57
+ switcherItemUrl: '/foreman_tasks/tasks/:id',
58
+ },
59
+ }}
60
+ >
61
+ <TaskDetails {...props} />
62
+ </PageLayout>
63
+ );
64
+ };
65
+
66
+ TaskDetailsPage.propTypes = {
67
+ match: PropTypes.shape({
68
+ params: PropTypes.shape({
69
+ id: PropTypes.string.isRequired,
70
+ }).isRequired,
71
+ }).isRequired,
72
+ };
73
+
74
+ export default TaskDetailsPage;
@@ -0,0 +1,265 @@
1
+ import React from 'react';
2
+ import { render, screen, within } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import { combineReducers, configureStore } from '@reduxjs/toolkit';
5
+ import { createMemoryHistory } from 'history';
6
+ import { Router } from 'react-router-dom';
7
+ import { IntlProvider } from 'react-intl';
8
+ import { Provider } from 'react-redux';
9
+
10
+ import breadcrumbBarReducer from 'foremanReact/components/BreadcrumbBar/BreadcrumbBarReducer';
11
+ import { STATUS } from 'foremanReact/constants';
12
+ import intervalsReducer from 'foremanReact/redux/middlewares/IntervalMiddleware/IntervalReducer';
13
+
14
+ import { FOREMAN_TASK_DETAILS, VIEW_FOREMAN_TASKS } from '../../../Components/TaskDetails/TaskDetailsConstants';
15
+ import TaskDetailsPage from '../TaskDetailsPage';
16
+
17
+ const mockUseForemanPermissions = jest.fn(
18
+ () => new Set(['view_foreman_tasks'])
19
+ );
20
+
21
+ jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
22
+ ...jest.requireActual('foremanReact/Root/Context/ForemanContext'),
23
+ useForemanPermissions: (...args) => mockUseForemanPermissions(...args),
24
+ }));
25
+
26
+ const TASK_DETAILS_TITLE_ROW_OUIA_ID = 'foreman-tasks-task-details-title-row';
27
+
28
+ const routerPropsBase = {
29
+ history: { push: jest.fn(), replace: jest.fn(), go: jest.fn() },
30
+ location: {
31
+ pathname: '/foreman_tasks/tasks/task-route-id',
32
+ search: '',
33
+ hash: '',
34
+ state: undefined,
35
+ },
36
+ };
37
+
38
+ const matchDefault = {
39
+ params: { id: 'task-route-id' },
40
+ path: '/foreman_tasks/tasks/:id',
41
+ url: '/foreman_tasks/tasks/task-route-id',
42
+ isExact: true,
43
+ };
44
+
45
+ const baseTaskPayload = {
46
+ action: '',
47
+ input: [],
48
+ output: {},
49
+ locks: [],
50
+ links: [],
51
+ depends_on: [],
52
+ blocks: [],
53
+ failed_steps: [],
54
+ running_steps: [],
55
+ execution_plan: {},
56
+ state: 'running',
57
+ result: '',
58
+ };
59
+
60
+ const createStoreForTaskPayload = overrides => ({
61
+ API: {
62
+ [FOREMAN_TASK_DETAILS]: {
63
+ response: { ...baseTaskPayload, ...overrides },
64
+ status: STATUS.RESOLVED,
65
+ payload: {},
66
+ },
67
+ },
68
+ });
69
+
70
+ const rootReducer = combineReducers({
71
+ API: (state = {}, action) => state,
72
+ intervals: intervalsReducer,
73
+ breadcrumbBar: breadcrumbBarReducer,
74
+ foremanTasks: (state = {}, action) => state,
75
+ });
76
+
77
+ const renderPage = (apiPayloadOverrides = {}, propsOverrides = {}) => {
78
+ const history = createMemoryHistory({
79
+ initialEntries: [`/foreman_tasks/tasks/${matchDefault.params.id}`],
80
+ });
81
+ const store = configureStore({
82
+ reducer: rootReducer,
83
+ preloadedState: createStoreForTaskPayload(apiPayloadOverrides),
84
+ middleware: getDefaultMiddleware =>
85
+ getDefaultMiddleware({
86
+ immutableCheck: false,
87
+ serializableCheck: false,
88
+ }),
89
+ });
90
+
91
+ window.history.pushState(
92
+ {},
93
+ '',
94
+ `/foreman_tasks/tasks/${matchDefault.params.id}`
95
+ );
96
+
97
+ return render(
98
+ <Router history={history}>
99
+ <Provider store={store}>
100
+ <IntlProvider locale="en">
101
+ <TaskDetailsPage
102
+ {...routerPropsBase}
103
+ history={history}
104
+ match={matchDefault}
105
+ {...propsOverrides}
106
+ />
107
+ </IntlProvider>
108
+ </Provider>
109
+ </Router>
110
+ );
111
+ };
112
+
113
+ const breadcrumbTitleHeadings = () =>
114
+ screen.getAllByRole('heading', { level: 1 }).filter(
115
+ heading => heading.getAttribute('data-ouia-component-id') === 'breadcrumb_title'
116
+ );
117
+
118
+ /**
119
+ * Title row (`customHeader` root `Flex`): same OUIA pattern as `Locks.test.js`.
120
+ */
121
+ const taskDetailsTitleRegion = container => {
122
+ const el = container.querySelector(
123
+ `[data-ouia-component-id="${TASK_DETAILS_TITLE_ROW_OUIA_ID}"]`
124
+ );
125
+
126
+ expect(el).toBeTruthy();
127
+
128
+ return el;
129
+ };
130
+
131
+ describe('TaskDetailsPage', () => {
132
+ beforeEach(() => {
133
+ mockUseForemanPermissions.mockImplementation(() => new Set(['view_foreman_tasks']));
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: /Details of Run job task/,
151
+ })
152
+ ).toBeInTheDocument();
153
+ expect(screen.queryByRole('heading', { name: /Permission denied/i })).not.toBeInTheDocument();
154
+ expect(mockUseForemanPermissions).toHaveBeenCalled();
155
+ });
156
+
157
+ it(`shows ResourceLoadFailedEmptyState and lists ${VIEW_FOREMAN_TASKS} when it is absent`, () => {
158
+ mockUseForemanPermissions.mockImplementation(() => new Set());
159
+
160
+ renderPage({ action: 'Hidden task' });
161
+
162
+ expect(
163
+ screen.getByRole('heading', { name: /Permission denied/i })
164
+ ).toBeInTheDocument();
165
+ expect(
166
+ screen.getByText(
167
+ /You do not have permission to view the task with id task-route-id/
168
+ )
169
+ ).toBeInTheDocument();
170
+ expect(screen.getByText(VIEW_FOREMAN_TASKS)).toBeInTheDocument();
171
+ expect(
172
+ screen.getByRole('button', { name: /Back to tasks/i })
173
+ ).toBeInTheDocument();
174
+ expect(
175
+ screen.getByRole('navigation', { name: 'Breadcrumb' })
176
+ ).toBeInTheDocument();
177
+ expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
178
+ });
179
+
180
+ it('denies access when user only has edit_foreman_tasks without view', () => {
181
+ mockUseForemanPermissions.mockImplementation(
182
+ () => new Set(['edit_foreman_tasks'])
183
+ );
184
+
185
+ renderPage({});
186
+
187
+ expect(
188
+ screen.getByRole('heading', { name: /Permission denied/i })
189
+ ).toBeInTheDocument();
190
+ expect(screen.getByText(VIEW_FOREMAN_TASKS)).toBeInTheDocument();
191
+ expect(
192
+ screen.getByRole('navigation', { name: 'Breadcrumb' })
193
+ ).toBeInTheDocument();
194
+ expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
195
+ });
196
+ });
197
+
198
+
199
+ const expectToolbarHeadingText = substring => {
200
+ const headings = breadcrumbTitleHeadings();
201
+
202
+ expect(headings.length).toBeGreaterThan(0);
203
+
204
+ headings.forEach(heading => {
205
+ expect(heading).toHaveTextContent(substring);
206
+ });
207
+ };
208
+
209
+ it('shows generic title and breadcrumb from route id when action is unset', () => {
210
+ const page = renderPage({});
211
+ expectToolbarHeadingText('Task Details');
212
+
213
+ expect(screen.getByRole('navigation', { name: 'Breadcrumb' })).toHaveTextContent(
214
+ 'Tasks'
215
+ );
216
+ expect(
217
+ within(screen.getByRole('navigation', { name: 'Breadcrumb' })).getByText(
218
+ /task-route-id/
219
+ )
220
+ ).toBeInTheDocument();
221
+
222
+ const titleRegion = taskDetailsTitleRegion(page.container);
223
+
224
+ expect(
225
+ within(titleRegion).getAllByRole('img', { hidden: true }).length
226
+ ).toBeGreaterThan(0);
227
+ expect(titleRegion.querySelector('[class*="danger"]')).toBeNull();
228
+ });
229
+
230
+ it('uses task action for title and breadcrumb when loaded', () => {
231
+ renderPage({ action: 'Refresh hosts' });
232
+
233
+ expectToolbarHeadingText('Details of Refresh hosts task');
234
+
235
+ expect(screen.getByRole('link', { name: /^Tasks$/ })).toHaveAttribute(
236
+ 'href',
237
+ '/foreman_tasks/tasks'
238
+ );
239
+ expect(
240
+ within(screen.getByRole('navigation', { name: 'Breadcrumb' })).getByText(
241
+ 'Refresh hosts'
242
+ )
243
+ ).toBeInTheDocument();
244
+ });
245
+
246
+ it('shows error status styling in the heading when task is stopped with error result', () => {
247
+ const page = renderPage({
248
+ action: 'Some action',
249
+ state: 'stopped',
250
+ result: 'error',
251
+ });
252
+
253
+ expectToolbarHeadingText('Details of Some action task');
254
+
255
+ expect(
256
+ taskDetailsTitleRegion(page.container).querySelector('[class*="danger"]')
257
+ ).toBeTruthy();
258
+
259
+ expect(
260
+ within(screen.getByRole('navigation', { name: 'Breadcrumb' })).getByText(
261
+ 'Some action'
262
+ )
263
+ ).toBeInTheDocument();
264
+ });
265
+ });
@@ -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/TaskDetailsPage';
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/TaskDetailsPage',
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: '7' },
57
- path: '/foreman_tasks/tasks/:id/sub_tasks',
58
- url: '/foreman_tasks/tasks/7/sub_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 === 2) {
67
- expect(screen.getByTestId('show-task-stub')).toBeInTheDocument();
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')
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman-tasks
3
3
  version: !ruby/object:Gem::Version
4
- version: 12.2.4
4
+ version: 13.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Nečas
@@ -219,7 +219,6 @@ files:
219
219
  - app/views/foreman_tasks/tasks/_lock_card.html.erb
220
220
  - app/views/foreman_tasks/tasks/dashboard/_latest_tasks_in_error_warning.html.erb
221
221
  - app/views/foreman_tasks/tasks/dashboard/_tasks_status.html.erb
222
- - app/views/foreman_tasks/tasks/show.html.erb
223
222
  - app/views/tasks_mailer/long_tasks.html.erb
224
223
  - app/views/tasks_mailer/long_tasks.text.erb
225
224
  - babel.config.js
@@ -331,6 +330,7 @@ files:
331
330
  - test/graphql/queries/tasks_query_test.rb
332
331
  - test/helpers/foreman_tasks/foreman_tasks_helper_test.rb
333
332
  - test/helpers/foreman_tasks/tasks_helper_test.rb
333
+ - test/integration/tasks_test.rb
334
334
  - test/lib/actions/middleware/keep_current_request_id_test.rb
335
335
  - test/lib/actions/middleware/keep_current_taxonomies_test.rb
336
336
  - test/lib/actions/middleware/keep_current_timezone_test.rb
@@ -343,6 +343,7 @@ files:
343
343
  - test/support/dummy_task_group.rb
344
344
  - test/support/history_tasks_builder.rb
345
345
  - test/tasks/generate_task_actions_test.rb
346
+ - test/test_plugin_helper.rb
346
347
  - test/unit/actions/action_with_sub_plans_test.rb
347
348
  - test/unit/actions/bulk_action_test.rb
348
349
  - test/unit/actions/proxy_action_test.rb
@@ -398,6 +399,7 @@ files:
398
399
  - webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js
399
400
  - webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js
400
401
  - webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetailsActions.test.js
402
+ - webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetailsSelectors.test.js
401
403
  - webpack/ForemanTasks/Components/TaskDetails/index.js
402
404
  - webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/PausedTasksCard/PausedTasksCard.js
403
405
  - webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/PausedTasksCard/PausedTasksCard.test.js
@@ -471,9 +473,12 @@ files:
471
473
  - webpack/ForemanTasks/Components/common/ClickConfirmation/index.js
472
474
  - webpack/ForemanTasks/Components/common/ToastsHelpers/ToastTypesConstants.js
473
475
  - webpack/ForemanTasks/Components/common/ToastsHelpers/index.js
476
+ - webpack/ForemanTasks/Components/common/taskResultIcon.js
474
477
  - webpack/ForemanTasks/Components/common/urlHelpers.js
475
478
  - webpack/ForemanTasks/ForemanTasksReducers.js
476
479
  - webpack/ForemanTasks/ForemanTasksSelectors.js
480
+ - webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsPage.js
481
+ - webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js
477
482
  - webpack/Routes/routes.js
478
483
  - webpack/Routes/routes.test.js
479
484
  - webpack/global_index.js
@@ -519,6 +524,7 @@ test_files:
519
524
  - test/graphql/queries/tasks_query_test.rb
520
525
  - test/helpers/foreman_tasks/foreman_tasks_helper_test.rb
521
526
  - test/helpers/foreman_tasks/tasks_helper_test.rb
527
+ - test/integration/tasks_test.rb
522
528
  - test/lib/actions/middleware/keep_current_request_id_test.rb
523
529
  - test/lib/actions/middleware/keep_current_taxonomies_test.rb
524
530
  - test/lib/actions/middleware/keep_current_timezone_test.rb
@@ -531,6 +537,7 @@ test_files:
531
537
  - test/support/dummy_task_group.rb
532
538
  - test/support/history_tasks_builder.rb
533
539
  - test/tasks/generate_task_actions_test.rb
540
+ - test/test_plugin_helper.rb
534
541
  - test/unit/actions/action_with_sub_plans_test.rb
535
542
  - test/unit/actions/bulk_action_test.rb
536
543
  - test/unit/actions/proxy_action_test.rb
@@ -1,18 +0,0 @@
1
- <% stylesheet 'foreman_tasks/foreman_tasks' %>
2
- <% content_for(:javascripts) do %>
3
- <%= webpacked_plugins_js_for :'foreman-tasks' %>
4
- <% end %>
5
- <% content_for(:stylesheets) do %>
6
- <%= webpacked_plugins_css_for :'foreman-tasks' %>
7
- <% end %>
8
-
9
- <% title _("Details of %s task") % @task.to_s %>
10
-
11
- <%= breadcrumbs(
12
- items: breadcrumb_items,
13
- name_field: 'action',
14
- resource_url: foreman_tasks_api_tasks_path,
15
- switcher_item_url: foreman_tasks_task_path(:id => ':id')
16
- ) %>
17
-
18
- <%= react_component('TaskDetails') %>