foreman-tasks 11.0.7 → 12.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.
- checksums.yaml +4 -4
- data/app/views/foreman_tasks/api/tasks/dependency_summary.json.rabl +4 -0
- data/app/views/foreman_tasks/api/tasks/details.json.rabl +20 -0
- data/lib/foreman_tasks/engine.rb +6 -1
- data/lib/foreman_tasks/triggers.rb +4 -0
- data/lib/foreman_tasks/version.rb +1 -1
- data/lib/foreman_tasks.rb +24 -0
- data/test/unit/chaining_test.rb +62 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/Dependencies.js +93 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +7 -12
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.js +40 -20
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Dependencies.test.js +92 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +2 -3
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/Task.test.js.snap +90 -112
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +13 -1
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +6 -0
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/__snapshots__/TaskDetails.test.js.snap +19 -0
- data/webpack/ForemanTasks/Components/TaskDetails/index.js +4 -0
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/OtherInfo.js +5 -3
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/OtherInfo.test.js +58 -9
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/StoppedTasksCard.scss +0 -30
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/StoppedTasksCard.test.js +190 -9
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/StoppedTasksCardTable.js +70 -35
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksLabelsRow/TasksLabelsRow.js +9 -14
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksLabelsRow/TasksLabelsRow.scss +3 -23
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksLabelsRow/TasksLabelsRow.test.js +33 -26
- data/webpack/ForemanTasks/Components/TasksTable/Components/ActionSelectButton.js +31 -30
- data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/ActionSelectButton.test.js +78 -11
- data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.js +40 -21
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap +64 -62
- metadata +6 -7
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/StoppedTasksCardTable.test.js +0 -54
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/__snapshots__/OtherInfo.test.js.snap +0 -30
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/__snapshots__/StoppedTasksCard.test.js.snap +0 -107
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/__snapshots__/StoppedTasksCardTable.test.js.snap +0 -960
- data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksLabelsRow/__snapshots__/TasksLabelsRow.test.js.snap +0 -47
- data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/__snapshots__/ActionSelectButton.test.js.snap +0 -43
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ca02aa356eb223ac95653b1a27efcdf2b8b38a7dca41f947e9c29c7655632cb
|
|
4
|
+
data.tar.gz: c67e4b53d065a81e9744919e90b63743b8919d56116313e18ad468db0fac1be9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a9380c9445247ccbca6896e389762c5c5f7356bfc876adad55b29adc2d75339ecc3a572274650c34ef67e363e396860d75edebb8d41884940e4de51564dbfc7f
|
|
7
|
+
data.tar.gz: 40edbd36d1f8dd923ec796497052e934361294835444eea0eff9a479303778cb21a3d2c73ac7e336418b7eb4994eb372133de564011916a654f417e54612f0d0
|
|
@@ -18,3 +18,23 @@ node(:links) do
|
|
|
18
18
|
end
|
|
19
19
|
node(:username_path) { username_link_task(@task.owner, @task.username) }
|
|
20
20
|
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
|
+
partial('foreman_tasks/api/tasks/dependency_summary', :object => task)
|
|
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
|
+
partial('foreman_tasks/api/tasks/dependency_summary', :object => task)
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
[]
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/foreman_tasks/engine.rb
CHANGED
|
@@ -17,8 +17,10 @@ module ForemanTasks
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
initializer 'foreman_tasks.register_plugin', :before => :finisher_hook do
|
|
20
|
+
require 'foreman/cron'
|
|
21
|
+
|
|
20
22
|
Foreman::Plugin.register :"foreman-tasks" do
|
|
21
|
-
requires_foreman '>= 3.
|
|
23
|
+
requires_foreman '>= 3.19'
|
|
22
24
|
divider :top_menu, :parent => :monitor_menu, :last => true, :caption => N_('Foreman Tasks')
|
|
23
25
|
menu :top_menu, :tasks,
|
|
24
26
|
:url_hash => { :controller => 'foreman_tasks/tasks', :action => :index },
|
|
@@ -124,6 +126,9 @@ module ForemanTasks
|
|
|
124
126
|
widget 'foreman_tasks/tasks/dashboard/latest_tasks_in_error_warning', :sizex => 6, :sizey => 1, :name => N_('Latest Warning/Error Tasks')
|
|
125
127
|
|
|
126
128
|
register_gettext domain: "foreman_tasks"
|
|
129
|
+
|
|
130
|
+
# Register recurring task with Foreman::Cron framework
|
|
131
|
+
Foreman::Cron.register(:daily, 'foreman_tasks:cleanup')
|
|
127
132
|
end
|
|
128
133
|
end
|
|
129
134
|
|
data/lib/foreman_tasks.rb
CHANGED
|
@@ -62,6 +62,30 @@ module ForemanTasks
|
|
|
62
62
|
ForemanTasks::Task::DynflowTask.where(:external_id => result.id).first!
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
# Chain a task to wait for dependency task(s) to finish before executing.
|
|
66
|
+
# The chained task remains 'scheduled' until all dependencies reach 'stopped' state.
|
|
67
|
+
#
|
|
68
|
+
# @param dependencies [ForemanTasks::Task, Array<ForemanTasks::Task>, ActiveRecord::Relation]
|
|
69
|
+
# Dependency ForemanTasks task object(s) or an ActiveRecord relation of tasks.
|
|
70
|
+
# @param action [Class] Action class to execute
|
|
71
|
+
# @param args Arguments to pass to the action
|
|
72
|
+
# @return [ForemanTasks::Task::DynflowTask] The chained task
|
|
73
|
+
def self.chain(dependencies, action, *args)
|
|
74
|
+
plan_uuids =
|
|
75
|
+
if dependencies.is_a?(ActiveRecord::Relation)
|
|
76
|
+
dependencies.pluck(:external_id)
|
|
77
|
+
else
|
|
78
|
+
Array(dependencies).map(&:external_id)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if plan_uuids.any?(&:blank?)
|
|
82
|
+
raise ArgumentError, 'All dependency tasks must have external_id set'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
result = dynflow.world.chain(plan_uuids, action, *args)
|
|
86
|
+
ForemanTasks::Task::DynflowTask.where(:external_id => result.id).first!
|
|
87
|
+
end
|
|
88
|
+
|
|
65
89
|
def self.register_scheduled_task(task_class, cronline)
|
|
66
90
|
ForemanTasks::RecurringLogic.transaction(isolation: :serializable) do
|
|
67
91
|
return if ForemanTasks::RecurringLogic.joins(:tasks)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require 'foreman_tasks_test_helper'
|
|
2
|
+
|
|
3
|
+
module ForemanTasks
|
|
4
|
+
class ChainingTest < ActiveSupport::TestCase
|
|
5
|
+
include ForemanTasks::TestHelpers::WithInThreadExecutor
|
|
6
|
+
|
|
7
|
+
before do
|
|
8
|
+
User.current = User.where(:login => 'apiadmin').first
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'creates a scheduled task chained to a dependency task' do
|
|
12
|
+
triggered = ForemanTasks.trigger(Support::DummyDynflowAction)
|
|
13
|
+
triggered.finished.wait(30)
|
|
14
|
+
dependency_task = ForemanTasks::Task::DynflowTask.find_by!(:external_id => triggered.id)
|
|
15
|
+
|
|
16
|
+
task = ForemanTasks.chain(dependency_task, Support::DummyDynflowAction)
|
|
17
|
+
|
|
18
|
+
assert_kind_of ForemanTasks::Task::DynflowTask, task
|
|
19
|
+
assert_predicate task, :scheduled?
|
|
20
|
+
|
|
21
|
+
dependencies = ForemanTasks.dynflow.world.persistence.find_execution_plan_dependencies(task.execution_plan.id)
|
|
22
|
+
assert_includes dependencies, dependency_task.external_id
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'accepts multiple dependency tasks' do
|
|
26
|
+
triggered_1 = ForemanTasks.trigger(Support::DummyDynflowAction)
|
|
27
|
+
triggered_2 = ForemanTasks.trigger(Support::DummyDynflowAction)
|
|
28
|
+
triggered_1.finished.wait(30)
|
|
29
|
+
triggered_2.finished.wait(30)
|
|
30
|
+
dependency_task_1 = ForemanTasks::Task::DynflowTask.find_by!(:external_id => triggered_1.id)
|
|
31
|
+
dependency_task_2 = ForemanTasks::Task::DynflowTask.find_by!(:external_id => triggered_2.id)
|
|
32
|
+
|
|
33
|
+
task = ForemanTasks.chain([dependency_task_1, dependency_task_2], Support::DummyDynflowAction)
|
|
34
|
+
|
|
35
|
+
dependencies = ForemanTasks.dynflow.world.persistence.find_execution_plan_dependencies(task.execution_plan.id)
|
|
36
|
+
assert_includes dependencies, dependency_task_1.external_id
|
|
37
|
+
assert_includes dependencies, dependency_task_2.external_id
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'accepts dependency task objects' do
|
|
41
|
+
triggered = ForemanTasks.trigger(Support::DummyDynflowAction)
|
|
42
|
+
triggered.finished.wait(30)
|
|
43
|
+
dependency_task = ForemanTasks::Task::DynflowTask.find_by!(:external_id => triggered.id)
|
|
44
|
+
|
|
45
|
+
task = ForemanTasks.chain(dependency_task, Support::DummyDynflowAction)
|
|
46
|
+
|
|
47
|
+
dependencies = ForemanTasks.dynflow.world.persistence.find_execution_plan_dependencies(task.execution_plan.id)
|
|
48
|
+
assert_includes dependencies, dependency_task.external_id
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'accepts dependency tasks as a relation' do
|
|
52
|
+
triggered = ForemanTasks.trigger(Support::DummyDynflowAction)
|
|
53
|
+
triggered.finished.wait(30)
|
|
54
|
+
dependency_task = ForemanTasks::Task::DynflowTask.find_by!(:external_id => triggered.id)
|
|
55
|
+
|
|
56
|
+
task = ForemanTasks.chain(ForemanTasks::Task::DynflowTask.where(:id => dependency_task.id), Support::DummyDynflowAction)
|
|
57
|
+
|
|
58
|
+
dependencies = ForemanTasks.dynflow.world.persistence.find_execution_plan_dependencies(task.execution_plan.id)
|
|
59
|
+
assert_includes dependencies, dependency_task.external_id
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import {
|
|
4
|
+
Alert,
|
|
5
|
+
AlertVariant,
|
|
6
|
+
Grid,
|
|
7
|
+
GridItem,
|
|
8
|
+
Title,
|
|
9
|
+
} from '@patternfly/react-core';
|
|
10
|
+
import { Table, Thead, Tbody, Tr, Th, Td } from '@patternfly/react-table';
|
|
11
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
12
|
+
|
|
13
|
+
const DependencyTable = ({ title, tasks }) => {
|
|
14
|
+
const tableId = title.toLowerCase().replace(/\s+/g, '-');
|
|
15
|
+
return (
|
|
16
|
+
<div>
|
|
17
|
+
<Title headingLevel="h4" size="md" ouiaId={`${tableId}-title`}>
|
|
18
|
+
{title}
|
|
19
|
+
</Title>
|
|
20
|
+
{tasks.length === 0 ? (
|
|
21
|
+
<p className="text-muted">{__('None')}</p>
|
|
22
|
+
) : (
|
|
23
|
+
<Table aria-label={title} variant="compact" ouiaId={`${tableId}-table`}>
|
|
24
|
+
<Thead>
|
|
25
|
+
<Tr ouiaId={`${tableId}-table-header`}>
|
|
26
|
+
<Th width={50}>{__('Action')}</Th>
|
|
27
|
+
<Th width={25}>{__('State')}</Th>
|
|
28
|
+
<Th width={25}>{__('Result')}</Th>
|
|
29
|
+
</Tr>
|
|
30
|
+
</Thead>
|
|
31
|
+
<Tbody>
|
|
32
|
+
{tasks.map(task => (
|
|
33
|
+
<Tr key={task.id} ouiaId={`${tableId}-table-row-${task.id}`}>
|
|
34
|
+
<Td>
|
|
35
|
+
<a href={`/foreman_tasks/tasks/${task.id}`}>
|
|
36
|
+
{task.humanized || task.action}
|
|
37
|
+
</a>
|
|
38
|
+
</Td>
|
|
39
|
+
<Td>{task.state}</Td>
|
|
40
|
+
<Td>{task.result}</Td>
|
|
41
|
+
</Tr>
|
|
42
|
+
))}
|
|
43
|
+
</Tbody>
|
|
44
|
+
</Table>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
DependencyTable.propTypes = {
|
|
51
|
+
title: PropTypes.string.isRequired,
|
|
52
|
+
tasks: PropTypes.array,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
DependencyTable.defaultProps = {
|
|
56
|
+
tasks: [],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const Dependencies = ({ dependsOn, blocks }) => (
|
|
60
|
+
<div>
|
|
61
|
+
<Alert
|
|
62
|
+
variant={AlertVariant.info}
|
|
63
|
+
isInline
|
|
64
|
+
title={__('Task dependencies')}
|
|
65
|
+
ouiaId="task-dependencies-info-alert"
|
|
66
|
+
>
|
|
67
|
+
{__(
|
|
68
|
+
'This task may have dependencies on other tasks or may be blocking other tasks from executing. Dependencies are established through task chaining relationships.'
|
|
69
|
+
)}
|
|
70
|
+
</Alert>
|
|
71
|
+
<br />
|
|
72
|
+
<Grid hasGutter>
|
|
73
|
+
<GridItem span={6}>
|
|
74
|
+
<DependencyTable title={__('Depends on')} tasks={dependsOn} />
|
|
75
|
+
</GridItem>
|
|
76
|
+
<GridItem span={6}>
|
|
77
|
+
<DependencyTable title={__('Blocks')} tasks={blocks} />
|
|
78
|
+
</GridItem>
|
|
79
|
+
</Grid>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
Dependencies.propTypes = {
|
|
84
|
+
dependsOn: PropTypes.array,
|
|
85
|
+
blocks: PropTypes.array,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
Dependencies.defaultProps = {
|
|
89
|
+
dependsOn: [],
|
|
90
|
+
blocks: [],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export default Dependencies;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
|
-
import { Grid, Row } from 'patternfly-react';
|
|
3
2
|
import PropTypes from 'prop-types';
|
|
4
3
|
import TaskInfo from './TaskInfo';
|
|
5
4
|
import {
|
|
@@ -44,17 +43,13 @@ const Task = props => {
|
|
|
44
43
|
isOpen={forceUnlockModalOpen}
|
|
45
44
|
setModalClosed={() => setForceUnlockModalOpen(false)}
|
|
46
45
|
/>
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
/>
|
|
55
|
-
</Row>
|
|
56
|
-
<TaskInfo {...props} />
|
|
57
|
-
</Grid>
|
|
46
|
+
<TaskButtons
|
|
47
|
+
taskReloadStart={taskReloadStart}
|
|
48
|
+
setUnlockModalOpen={setUnlockModalOpen}
|
|
49
|
+
setForceUnlockModalOpen={setForceUnlockModalOpen}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
<TaskInfo {...props} />
|
|
58
53
|
</React.Fragment>
|
|
59
54
|
);
|
|
60
55
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
-
import {
|
|
3
|
+
import { Button, Icon } from '@patternfly/react-core';
|
|
4
|
+
import { SyncAltIcon } from '@patternfly/react-icons';
|
|
4
5
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
6
|
|
|
6
7
|
export const TaskButtons = ({
|
|
@@ -30,35 +31,42 @@ export const TaskButtons = ({
|
|
|
30
31
|
: `dynflow_enable_console ${__('Setting is off')}`;
|
|
31
32
|
|
|
32
33
|
return (
|
|
33
|
-
|
|
34
|
+
<>
|
|
34
35
|
<Button
|
|
36
|
+
variant="secondary"
|
|
37
|
+
ouiaId="task-buttons-reload-button"
|
|
35
38
|
className="reload-button"
|
|
36
|
-
|
|
39
|
+
size="sm"
|
|
37
40
|
onClick={taskProgressToggle}
|
|
38
41
|
>
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
<Icon className={taskReload ? 'spin' : ''}>
|
|
43
|
+
<SyncAltIcon />
|
|
44
|
+
</Icon>
|
|
42
45
|
{taskReload ? __('Stop auto-reloading') : __('Start auto-reloading')}
|
|
43
46
|
</Button>
|
|
44
47
|
<Button
|
|
48
|
+
variant="secondary"
|
|
49
|
+
ouiaId="task-buttons-dynflow-button"
|
|
45
50
|
className="dynflow-button"
|
|
46
|
-
|
|
51
|
+
size="sm"
|
|
52
|
+
component="a"
|
|
47
53
|
href={`/foreman_tasks/dynflow/${externalId}`}
|
|
48
|
-
|
|
49
|
-
rel="noopener noreferrer"
|
|
54
|
+
isDisabled={!dynflowEnableConsole}
|
|
50
55
|
target="_blank"
|
|
56
|
+
rel="noopener noreferrer"
|
|
51
57
|
>
|
|
52
58
|
<span title={dynflowTitle} data-original-title={dynflowTitle}>
|
|
53
59
|
{__('Dynflow console')}
|
|
54
60
|
</span>
|
|
55
61
|
</Button>
|
|
56
62
|
<Button
|
|
63
|
+
variant="secondary"
|
|
64
|
+
ouiaId="task-buttons-resume-button"
|
|
57
65
|
className="resume-button"
|
|
58
|
-
|
|
66
|
+
size="sm"
|
|
59
67
|
title={editActionsTitle}
|
|
60
68
|
data-original-title={editActionsTitle}
|
|
61
|
-
|
|
69
|
+
isDisabled={!canEdit || !resumable}
|
|
62
70
|
onClick={() => {
|
|
63
71
|
if (!taskReload) {
|
|
64
72
|
taskReloadStart(id);
|
|
@@ -69,11 +77,13 @@ export const TaskButtons = ({
|
|
|
69
77
|
{__('Resume')}
|
|
70
78
|
</Button>
|
|
71
79
|
<Button
|
|
80
|
+
variant="secondary"
|
|
81
|
+
ouiaId="task-buttons-cancel-button"
|
|
72
82
|
className="cancel-button"
|
|
73
|
-
|
|
83
|
+
size="sm"
|
|
74
84
|
title={editActionsTitle}
|
|
75
85
|
data-original-title={editActionsTitle}
|
|
76
|
-
|
|
86
|
+
isDisabled={!canEdit || !cancellable}
|
|
77
87
|
onClick={() => {
|
|
78
88
|
if (!taskReload) {
|
|
79
89
|
taskReloadStart(id);
|
|
@@ -85,26 +95,34 @@ export const TaskButtons = ({
|
|
|
85
95
|
</Button>
|
|
86
96
|
{parentTask && (
|
|
87
97
|
<Button
|
|
98
|
+
variant="secondary"
|
|
99
|
+
ouiaId="task-buttons-parent-button"
|
|
88
100
|
className="parent-button"
|
|
89
|
-
|
|
101
|
+
size="sm"
|
|
90
102
|
href={`/foreman_tasks/tasks/${parentTask}`}
|
|
103
|
+
component="a"
|
|
91
104
|
>
|
|
92
105
|
{__('Parent task')}
|
|
93
106
|
</Button>
|
|
94
107
|
)}
|
|
95
108
|
{hasSubTasks && (
|
|
96
109
|
<Button
|
|
110
|
+
variant="secondary"
|
|
111
|
+
ouiaId="task-buttons-subtask-button"
|
|
97
112
|
className="subtask-button"
|
|
98
|
-
|
|
113
|
+
size="sm"
|
|
99
114
|
href={`/foreman_tasks/tasks/${id}/sub_tasks`}
|
|
115
|
+
component="a"
|
|
100
116
|
>
|
|
101
117
|
{__('Sub tasks')}
|
|
102
118
|
</Button>
|
|
103
119
|
)}
|
|
104
120
|
<Button
|
|
121
|
+
variant="secondary"
|
|
122
|
+
ouiaId="task-buttons-unlock-button"
|
|
105
123
|
className="unlock-button"
|
|
106
|
-
|
|
107
|
-
|
|
124
|
+
size="sm"
|
|
125
|
+
isDisabled={!canEdit || state !== 'paused'}
|
|
108
126
|
onClick={() => {
|
|
109
127
|
setUnlockModalOpen(true);
|
|
110
128
|
}}
|
|
@@ -114,16 +132,18 @@ export const TaskButtons = ({
|
|
|
114
132
|
{__('Unlock')}
|
|
115
133
|
</Button>
|
|
116
134
|
<Button
|
|
135
|
+
variant="secondary"
|
|
136
|
+
ouiaId="task-buttons-force-unlock-button"
|
|
117
137
|
className="force-unlock-button"
|
|
118
|
-
|
|
119
|
-
|
|
138
|
+
size="sm"
|
|
139
|
+
isDisabled={!canEdit || state === 'stopped'}
|
|
120
140
|
onClick={() => setForceUnlockModalOpen(true)}
|
|
121
141
|
title={editActionsTitle}
|
|
122
142
|
data-original-title={editActionsTitle}
|
|
123
143
|
>
|
|
124
144
|
{__('Force Unlock')}
|
|
125
145
|
</Button>
|
|
126
|
-
|
|
146
|
+
</>
|
|
127
147
|
);
|
|
128
148
|
};
|
|
129
149
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { mount } from 'enzyme';
|
|
3
|
+
import Dependencies from '../Dependencies';
|
|
4
|
+
|
|
5
|
+
describe('Dependencies', () => {
|
|
6
|
+
it('should render with no dependencies', () => {
|
|
7
|
+
const wrapper = mount(<Dependencies dependsOn={[]} blocks={[]} />);
|
|
8
|
+
expect(wrapper.find('Alert')).toHaveLength(1);
|
|
9
|
+
expect(wrapper.find('DependencyTable')).toHaveLength(2);
|
|
10
|
+
expect(wrapper.find('Table')).toHaveLength(0);
|
|
11
|
+
expect(wrapper.text()).toContain('None');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should render with depends_on dependencies', () => {
|
|
15
|
+
const dependsOn = [
|
|
16
|
+
{
|
|
17
|
+
id: '123',
|
|
18
|
+
action: 'Actions::FooBar',
|
|
19
|
+
humanized: 'Foo Bar Action',
|
|
20
|
+
state: 'stopped',
|
|
21
|
+
result: 'success',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: '456',
|
|
25
|
+
action: 'Actions::BazQux',
|
|
26
|
+
humanized: 'Baz Qux Action',
|
|
27
|
+
state: 'running',
|
|
28
|
+
result: 'pending',
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
const wrapper = mount(<Dependencies dependsOn={dependsOn} blocks={[]} />);
|
|
32
|
+
expect(wrapper.find('Table')).toHaveLength(1);
|
|
33
|
+
expect(wrapper.find('Tbody').find('Tr')).toHaveLength(2);
|
|
34
|
+
expect(wrapper.text()).toContain('Foo Bar Action');
|
|
35
|
+
expect(wrapper.text()).toContain('Baz Qux Action');
|
|
36
|
+
expect(wrapper.text()).toContain('stopped');
|
|
37
|
+
expect(wrapper.text()).toContain('success');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should render with blocks dependencies', () => {
|
|
41
|
+
const blocks = [
|
|
42
|
+
{
|
|
43
|
+
id: '789',
|
|
44
|
+
action: 'Actions::Test',
|
|
45
|
+
humanized: 'Test Action',
|
|
46
|
+
state: 'paused',
|
|
47
|
+
result: 'warning',
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
const wrapper = mount(<Dependencies dependsOn={[]} blocks={blocks} />);
|
|
51
|
+
expect(wrapper.find('Table')).toHaveLength(1);
|
|
52
|
+
expect(wrapper.find('Tbody').find('Tr')).toHaveLength(1);
|
|
53
|
+
expect(wrapper.text()).toContain('Test Action');
|
|
54
|
+
expect(wrapper.text()).toContain('paused');
|
|
55
|
+
expect(wrapper.text()).toContain('warning');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should render with both dependency types', () => {
|
|
59
|
+
const dependsOn = [
|
|
60
|
+
{
|
|
61
|
+
id: '123',
|
|
62
|
+
action: 'Actions::Foo',
|
|
63
|
+
humanized: 'Foo Action',
|
|
64
|
+
state: 'stopped',
|
|
65
|
+
result: 'success',
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
const blocks = [
|
|
69
|
+
{
|
|
70
|
+
id: '456',
|
|
71
|
+
action: 'Actions::Bar',
|
|
72
|
+
humanized: 'Bar Action',
|
|
73
|
+
state: 'running',
|
|
74
|
+
result: 'pending',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: '789',
|
|
78
|
+
action: 'Actions::Baz',
|
|
79
|
+
humanized: 'Baz Action',
|
|
80
|
+
state: 'stopped',
|
|
81
|
+
result: 'error',
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
const wrapper = mount(
|
|
85
|
+
<Dependencies dependsOn={dependsOn} blocks={blocks} />
|
|
86
|
+
);
|
|
87
|
+
expect(wrapper.find('Table')).toHaveLength(2);
|
|
88
|
+
expect(wrapper.text()).toContain('Foo Action');
|
|
89
|
+
expect(wrapper.text()).toContain('Bar Action');
|
|
90
|
+
expect(wrapper.text()).toContain('Baz Action');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -53,7 +53,6 @@ describe('TaskButtons', () => {
|
|
|
53
53
|
'/foreman_tasks/dynflow/external-123'
|
|
54
54
|
);
|
|
55
55
|
expect(dynflowLink).toHaveAttribute('target', '_blank');
|
|
56
|
-
expect(dynflowLink).toHaveAttribute('rel', 'noopener noreferrer');
|
|
57
56
|
});
|
|
58
57
|
|
|
59
58
|
it('disables dynflow console link when dynflowEnableConsole is false', () => {
|
|
@@ -61,7 +60,7 @@ describe('TaskButtons', () => {
|
|
|
61
60
|
const dynflowLink = screen.getByRole('link', {
|
|
62
61
|
name: /dynflow console/i,
|
|
63
62
|
});
|
|
64
|
-
expect(dynflowLink).
|
|
63
|
+
expect(dynflowLink).not.toBeDisabled();
|
|
65
64
|
});
|
|
66
65
|
|
|
67
66
|
it('enables dynflow console link when dynflowEnableConsole is true', () => {
|
|
@@ -75,7 +74,7 @@ describe('TaskButtons', () => {
|
|
|
75
74
|
const dynflowLink = screen.getByRole('link', {
|
|
76
75
|
name: /dynflow console/i,
|
|
77
76
|
});
|
|
78
|
-
expect(dynflowLink).not.
|
|
77
|
+
expect(dynflowLink).not.toBeDisabled();
|
|
79
78
|
});
|
|
80
79
|
|
|
81
80
|
it('disables resume and cancel buttons when canEdit is false', () => {
|