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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/app/views/foreman_tasks/api/tasks/dependency_summary.json.rabl +4 -0
  3. data/app/views/foreman_tasks/api/tasks/details.json.rabl +20 -0
  4. data/lib/foreman_tasks/engine.rb +6 -1
  5. data/lib/foreman_tasks/triggers.rb +4 -0
  6. data/lib/foreman_tasks/version.rb +1 -1
  7. data/lib/foreman_tasks.rb +24 -0
  8. data/test/unit/chaining_test.rb +62 -0
  9. data/webpack/ForemanTasks/Components/TaskDetails/Components/Dependencies.js +93 -0
  10. data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +7 -12
  11. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.js +40 -20
  12. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Dependencies.test.js +92 -0
  13. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +2 -3
  14. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/Task.test.js.snap +90 -112
  15. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +13 -1
  16. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +6 -0
  17. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/__snapshots__/TaskDetails.test.js.snap +19 -0
  18. data/webpack/ForemanTasks/Components/TaskDetails/index.js +4 -0
  19. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/OtherInfo.js +5 -3
  20. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/OtherInfo.test.js +58 -9
  21. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/StoppedTasksCard.scss +0 -30
  22. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/StoppedTasksCard.test.js +190 -9
  23. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/StoppedTasksCardTable.js +70 -35
  24. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksLabelsRow/TasksLabelsRow.js +9 -14
  25. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksLabelsRow/TasksLabelsRow.scss +3 -23
  26. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksLabelsRow/TasksLabelsRow.test.js +33 -26
  27. data/webpack/ForemanTasks/Components/TasksTable/Components/ActionSelectButton.js +31 -30
  28. data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/ActionSelectButton.test.js +78 -11
  29. data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.js +40 -21
  30. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap +64 -62
  31. metadata +6 -7
  32. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/StoppedTasksCardTable.test.js +0 -54
  33. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/__snapshots__/OtherInfo.test.js.snap +0 -30
  34. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/__snapshots__/StoppedTasksCard.test.js.snap +0 -107
  35. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/__snapshots__/StoppedTasksCardTable.test.js.snap +0 -960
  36. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksLabelsRow/__snapshots__/TasksLabelsRow.test.js.snap +0 -47
  37. 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: c64888d93b687e1d29a9b7baaeefc47dd42a8cc82383b6b42cce34e69731cbe3
4
- data.tar.gz: 2166a6056f5d8553b7b61345908860d745e8dd465508bc4011251e2d2938ece2
3
+ metadata.gz: 7ca02aa356eb223ac95653b1a27efcdf2b8b38a7dca41f947e9c29c7655632cb
4
+ data.tar.gz: c67e4b53d065a81e9744919e90b63743b8919d56116313e18ad468db0fac1be9
5
5
  SHA512:
6
- metadata.gz: bfce2a4d918298cec79b2954f3411fa7d6947a020f95f4b62137a12e0b81d5019dff5c317706bae0572d64728bfed5707e49a75793aa43221d2beb95f901a992
7
- data.tar.gz: fc7f80734a1e145bfee7530252288a9037266c8b1da0577394a776cd3c503459b2420b35c3c4725c27ee16858978237f990b714650c32301ff7ea0a8f456716e
6
+ metadata.gz: a9380c9445247ccbca6896e389762c5c5f7356bfc876adad55b29adc2d75339ecc3a572274650c34ef67e363e396860d75edebb8d41884940e4de51564dbfc7f
7
+ data.tar.gz: 40edbd36d1f8dd923ec796497052e934361294835444eea0eff9a479303778cb21a3d2c73ac7e336418b7eb4994eb372133de564011916a654f417e54612f0d0
@@ -0,0 +1,4 @@
1
+ object @task
2
+
3
+ attributes :id, :action, :state, :result
4
+ node(:humanized) { @task.humanized[:action] }
@@ -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
@@ -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.15'
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
 
@@ -25,5 +25,9 @@ module ForemanTasks
25
25
  def delay(action, delay_options, *args)
26
26
  foreman_tasks.delay(action, delay_options, *args)
27
27
  end
28
+
29
+ def chain(dependencies, action, *args)
30
+ foreman_tasks.chain(dependencies, action, *args)
31
+ end
28
32
  end
29
33
  end
@@ -1,3 +1,3 @@
1
1
  module ForemanTasks
2
- VERSION = '11.0.7'.freeze
2
+ VERSION = '12.0.0'.freeze
3
3
  end
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
- <Grid>
48
- <Row>
49
- <TaskButtons
50
- taskReloadStart={taskReloadStart}
51
- setUnlockModalOpen={setUnlockModalOpen}
52
- setForceUnlockModalOpen={setForceUnlockModalOpen}
53
- {...props}
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 { Col, Button } from 'patternfly-react';
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
- <Col xs={12}>
34
+ <>
34
35
  <Button
36
+ variant="secondary"
37
+ ouiaId="task-buttons-reload-button"
35
38
  className="reload-button"
36
- bsSize="small"
39
+ size="sm"
37
40
  onClick={taskProgressToggle}
38
41
  >
39
- <span
40
- className={`glyphicon glyphicon-refresh ${taskReload ? 'spin' : ''}`}
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
- bsSize="small"
51
+ size="sm"
52
+ component="a"
47
53
  href={`/foreman_tasks/dynflow/${externalId}`}
48
- disabled={!dynflowEnableConsole}
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
- bsSize="small"
66
+ size="sm"
59
67
  title={editActionsTitle}
60
68
  data-original-title={editActionsTitle}
61
- disabled={!canEdit || !resumable}
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
- bsSize="small"
83
+ size="sm"
74
84
  title={editActionsTitle}
75
85
  data-original-title={editActionsTitle}
76
- disabled={!canEdit || !cancellable}
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
- bsSize="small"
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
- bsSize="small"
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
- bsSize="small"
107
- disabled={!canEdit || state !== 'paused'}
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
- bsSize="small"
119
- disabled={!canEdit || state === 'stopped'}
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
- </Col>
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).toHaveClass('disabled');
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.toHaveClass('disabled');
77
+ expect(dynflowLink).not.toBeDisabled();
79
78
  });
80
79
 
81
80
  it('disables resume and cancel buttons when canEdit is false', () => {