Skip to content

Commit 9603331

Browse files
ianballouadamruzicka
authored andcommitted
Fixes #38991 - Add chained task dependencies in the UI
1 parent f8e543d commit 9603331

File tree

7 files changed

+191
-4
lines changed

7 files changed

+191
-4
lines changed

app/views/foreman_tasks/api/tasks/details.json.rabl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,23 @@ node(:links) do
1818
end
1919
node(:username_path) { username_link_task(@task.owner, @task.username) }
2020
node(:dynflow_enable_console) { Setting['dynflow_enable_console'] }
21+
node(:depends_on) do
22+
if @task.execution_plan
23+
dynflow_uuids = ForemanTasks.dynflow.world.persistence.find_execution_plan_dependencies(@task.execution_plan.id)
24+
ForemanTasks::Task.where(external_id: dynflow_uuids).map do |task|
25+
{ id: task.id, action: task.action, humanized: task.humanized[:action] }
26+
end
27+
else
28+
[]
29+
end
30+
end
31+
node(:blocks) do
32+
if @task.execution_plan
33+
dynflow_uuids = ForemanTasks.dynflow.world.persistence.find_blocked_execution_plans(@task.execution_plan.id)
34+
ForemanTasks::Task.where(external_id: dynflow_uuids).map do |task|
35+
{ id: task.id, action: task.action, humanized: task.humanized[:action] }
36+
end
37+
else
38+
[]
39+
end
40+
end
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import {
4+
Alert,
5+
AlertVariant,
6+
Grid,
7+
GridItem,
8+
List,
9+
ListItem,
10+
Title,
11+
} from '@patternfly/react-core';
12+
import { translate as __ } from 'foremanReact/common/I18n';
13+
14+
const DependencyList = ({ title, tasks }) => {
15+
if (!tasks || tasks.length === 0) {
16+
return (
17+
<div>
18+
<Title headingLevel="h4" size="md">
19+
{title}
20+
</Title>
21+
<p className="text-muted">{__('None')}</p>
22+
</div>
23+
);
24+
}
25+
26+
return (
27+
<div>
28+
<Title headingLevel="h4" size="md">
29+
{title}
30+
</Title>
31+
<List isPlain>
32+
{tasks.map((task, index) => (
33+
<ListItem key={index}>
34+
<a href={`/foreman_tasks/tasks/${task.id}`}>
35+
{task.humanized || task.action}
36+
</a>
37+
</ListItem>
38+
))}
39+
</List>
40+
</div>
41+
);
42+
};
43+
44+
DependencyList.propTypes = {
45+
title: PropTypes.string.isRequired,
46+
tasks: PropTypes.array,
47+
};
48+
49+
DependencyList.defaultProps = {
50+
tasks: [],
51+
};
52+
53+
const Dependencies = ({ dependsOn, blocks }) => (
54+
<div>
55+
<Alert variant={AlertVariant.info} isInline title={__('Task dependencies')}>
56+
{__(
57+
'This task may have dependencies on other tasks or may be blocking other tasks from executing. Dependencies are established through task chaining relationships.'
58+
)}
59+
</Alert>
60+
<br />
61+
<Grid hasGutter>
62+
<GridItem span={6}>
63+
<DependencyList title={__('Depends on')} tasks={dependsOn} />
64+
</GridItem>
65+
<GridItem span={6}>
66+
<DependencyList title={__('Blocks')} tasks={blocks} />
67+
</GridItem>
68+
</Grid>
69+
</div>
70+
);
71+
72+
Dependencies.propTypes = {
73+
dependsOn: PropTypes.array,
74+
blocks: PropTypes.array,
75+
};
76+
77+
Dependencies.defaultProps = {
78+
dependsOn: [],
79+
blocks: [],
80+
};
81+
82+
export default Dependencies;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from 'react';
2+
import { shallow } from 'enzyme';
3+
import Dependencies from '../Dependencies';
4+
5+
describe('Dependencies', () => {
6+
it('should render with no dependencies', () => {
7+
const wrapper = shallow(<Dependencies dependsOn={[]} blocks={[]} />);
8+
expect(wrapper.find('Alert')).toHaveLength(1);
9+
expect(wrapper.find('DependencyList')).toHaveLength(2);
10+
});
11+
12+
it('should render with depends_on dependencies', () => {
13+
const dependsOn = [
14+
{ id: '123', action: 'Actions::FooBar', humanized: 'Foo Bar Action' },
15+
{ id: '456', action: 'Actions::BazQux', humanized: 'Baz Qux Action' },
16+
];
17+
const wrapper = shallow(<Dependencies dependsOn={dependsOn} blocks={[]} />);
18+
expect(
19+
wrapper
20+
.find('DependencyList')
21+
.at(0)
22+
.prop('tasks')
23+
).toEqual(dependsOn);
24+
});
25+
26+
it('should render with blocks dependencies', () => {
27+
const blocks = [
28+
{ id: '789', action: 'Actions::Test', humanized: 'Test Action' },
29+
];
30+
const wrapper = shallow(<Dependencies dependsOn={[]} blocks={blocks} />);
31+
expect(
32+
wrapper
33+
.find('DependencyList')
34+
.at(1)
35+
.prop('tasks')
36+
).toEqual(blocks);
37+
});
38+
39+
it('should render with both dependency types', () => {
40+
const dependsOn = [
41+
{ id: '123', action: 'Actions::Foo', humanized: 'Foo Action' },
42+
];
43+
const blocks = [
44+
{ id: '456', action: 'Actions::Bar', humanized: 'Bar Action' },
45+
{ id: '789', action: 'Actions::Baz', humanized: 'Baz Action' },
46+
];
47+
const wrapper = shallow(
48+
<Dependencies dependsOn={dependsOn} blocks={blocks} />
49+
);
50+
expect(wrapper.find('DependencyList')).toHaveLength(2);
51+
expect(
52+
wrapper
53+
.find('DependencyList')
54+
.at(0)
55+
.prop('tasks')
56+
).toEqual(dependsOn);
57+
expect(
58+
wrapper
59+
.find('DependencyList')
60+
.at(1)
61+
.prop('tasks')
62+
).toEqual(blocks);
63+
});
64+
});

webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import RunningSteps from './Components/RunningSteps';
99
import Errors from './Components/Errors';
1010
import Locks from './Components/Locks';
1111
import Raw from './Components/Raw';
12+
import Dependencies from './Components/Dependencies';
1213
import { getTaskID } from './TasksDetailsHelper';
1314
import { TaskSkeleton } from './Components/TaskSkeleton';
1415

@@ -20,6 +21,8 @@ const TaskDetails = ({
2021
runningSteps,
2122
locks,
2223
links,
24+
dependsOn,
25+
blocks,
2326
cancelStep,
2427
taskReloadStart,
2528
taskReloadStop,
@@ -90,7 +93,10 @@ const TaskDetails = ({
9093
<Tab eventKey={4} disabled={isLoading} title={__('Locks')}>
9194
<Locks locks={locks.concat(links)} />
9295
</Tab>
93-
<Tab eventKey={5} disabled={isLoading} title={__('Raw')}>
96+
<Tab eventKey={5} disabled={isLoading} title={__('Dependencies')}>
97+
<Dependencies dependsOn={dependsOn} blocks={blocks} />
98+
</Tab>
99+
<Tab eventKey={6} disabled={isLoading} title={__('Raw')}>
94100
<Raw
95101
id={id}
96102
label={props.label}
@@ -116,9 +122,12 @@ TaskDetails.propTypes = {
116122
taskReloadStop: PropTypes.func.isRequired,
117123
taskReloadStart: PropTypes.func.isRequired,
118124
links: PropTypes.array,
125+
dependsOn: PropTypes.array,
126+
blocks: PropTypes.array,
119127
...Task.propTypes,
120128
...Errors.propTypes,
121129
...Locks.propTypes,
130+
...Dependencies.propTypes,
122131
...Raw.propTypes,
123132
};
124133
TaskDetails.defaultProps = {
@@ -127,10 +136,13 @@ TaskDetails.defaultProps = {
127136
APIerror: null,
128137
status: STATUS.PENDING,
129138
links: [],
139+
dependsOn: [],
140+
blocks: [],
130141
...Task.defaultProps,
131142
...RunningSteps.defaultProps,
132143
...Errors.defaultProps,
133144
...Locks.defaultProps,
145+
...Dependencies.defaultProps,
134146
...Raw.defaultProps,
135147
};
136148

webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,9 @@ export const selectAPIError = state =>
109109
export const selectIsLoading = state =>
110110
!!selectAPIByKey(state, FOREMAN_TASK_DETAILS).response &&
111111
selectStatus(state) === STATUS.PENDING;
112+
113+
export const selectDependsOn = state =>
114+
selectTaskDetailsResponse(state).depends_on || [];
115+
116+
export const selectBlocks = state =>
117+
selectTaskDetailsResponse(state).blocks || [];

webpack/ForemanTasks/Components/TaskDetails/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import {
3636
selectStatus,
3737
selectAPIError,
3838
selectIsLoading,
39+
selectDependsOn,
40+
selectBlocks,
3941
} from './TaskDetailsSelectors';
4042

4143
const mapStateToProps = state => ({
@@ -71,6 +73,8 @@ const mapStateToProps = state => ({
7173
status: selectStatus(state),
7274
APIerror: selectAPIError(state),
7375
isLoading: selectIsLoading(state),
76+
dependsOn: selectDependsOn(state),
77+
blocks: selectBlocks(state),
7478
});
7579

7680
const mapDispatchToProps = dispatch =>

webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/__test__/index.test.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,15 @@ jest.mock('../../../TasksTableSelectors', () => ({
4444
}));
4545

4646
// Create a mock store
47-
const createMockStore = (initialState = {}) => {
48-
return configureStore({
47+
const createMockStore = (initialState = {}) =>
48+
configureStore({
4949
reducer: {
5050
foremanTasks: (state = initialState, action) => state,
5151
},
5252
preloadedState: {
5353
foremanTasks: initialState,
5454
},
5555
});
56-
};
5756

5857
// Test wrapper component
5958
const TestWrapper = ({ children, store }) => (

0 commit comments

Comments
 (0)