foreman_remote_execution 12.0.5 → 13.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc +4 -1
  3. data/.github/workflows/js_ci.yml +1 -1
  4. data/.github/workflows/release.yml +4 -2
  5. data/.github/workflows/ruby_ci.yml +16 -81
  6. data/.packit.yaml +8 -3
  7. data/app/assets/javascripts/foreman_remote_execution/locale/de/foreman_remote_execution.js +23 -14
  8. data/app/assets/javascripts/foreman_remote_execution/locale/en/foreman_remote_execution.js +22 -4
  9. data/app/assets/javascripts/foreman_remote_execution/locale/en_GB/foreman_remote_execution.js +23 -14
  10. data/app/assets/javascripts/foreman_remote_execution/locale/es/foreman_remote_execution.js +23 -14
  11. data/app/assets/javascripts/foreman_remote_execution/locale/fr/foreman_remote_execution.js +23 -14
  12. data/app/assets/javascripts/foreman_remote_execution/locale/ja/foreman_remote_execution.js +23 -14
  13. data/app/assets/javascripts/foreman_remote_execution/locale/ka/foreman_remote_execution.js +23 -14
  14. data/app/assets/javascripts/foreman_remote_execution/locale/ko/foreman_remote_execution.js +23 -14
  15. data/app/assets/javascripts/foreman_remote_execution/locale/pt_BR/foreman_remote_execution.js +23 -14
  16. data/app/assets/javascripts/foreman_remote_execution/locale/ru/foreman_remote_execution.js +23 -14
  17. data/app/assets/javascripts/foreman_remote_execution/locale/zh_CN/foreman_remote_execution.js +23 -14
  18. data/app/assets/javascripts/foreman_remote_execution/locale/zh_TW/foreman_remote_execution.js +23 -14
  19. data/app/controllers/ui_job_wizard_controller.rb +1 -1
  20. data/app/helpers/job_invocations_helper.rb +1 -1
  21. data/app/helpers/remote_execution_helper.rb +2 -2
  22. data/app/lib/actions/remote_execution/proxy_action.rb +1 -1
  23. data/app/lib/actions/remote_execution/run_host_job.rb +9 -17
  24. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
  25. data/app/models/host_status/execution_status.rb +2 -2
  26. data/app/models/job_invocation_composer.rb +4 -3
  27. data/app/views/api/v2/job_invocations/base.json.rabl +5 -3
  28. data/app/views/job_invocations/_preview_hosts_list.html.erb +1 -1
  29. data/app/views/job_invocations/show.html.erb +12 -5
  30. data/app/views/job_invocations/show.js.erb +8 -1
  31. data/app/views/job_invocations/welcome.html.erb +1 -1
  32. data/app/views/template_invocations/_refresh.js.erb +10 -4
  33. data/app/views/template_invocations/show.html.erb +2 -2
  34. data/app/views/templates/script/convert2rhel_analyze.erb +1 -12
  35. data/app/views/templates/script/package_action.erb +11 -1
  36. data/app/views/templates/script/puppet_run_once.erb +3 -3
  37. data/lib/foreman_remote_execution/engine.rb +1 -1
  38. data/lib/foreman_remote_execution/version.rb +1 -1
  39. data/locale/de/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  40. data/locale/de/foreman_remote_execution.po +24 -6
  41. data/locale/en/foreman_remote_execution.po +24 -6
  42. data/locale/en_GB/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  43. data/locale/en_GB/foreman_remote_execution.po +24 -6
  44. data/locale/es/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  45. data/locale/es/foreman_remote_execution.po +24 -6
  46. data/locale/foreman_remote_execution.pot +170 -142
  47. data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  48. data/locale/fr/foreman_remote_execution.po +24 -6
  49. data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  50. data/locale/ja/foreman_remote_execution.po +24 -6
  51. data/locale/ka/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  52. data/locale/ka/foreman_remote_execution.po +24 -6
  53. data/locale/ko/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  54. data/locale/ko/foreman_remote_execution.po +24 -6
  55. data/locale/pt_BR/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  56. data/locale/pt_BR/foreman_remote_execution.po +24 -6
  57. data/locale/ru/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  58. data/locale/ru/foreman_remote_execution.po +24 -6
  59. data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  60. data/locale/zh_CN/foreman_remote_execution.po +24 -6
  61. data/locale/zh_TW/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  62. data/locale/zh_TW/foreman_remote_execution.po +24 -6
  63. data/package.json +7 -11
  64. data/test/functional/api/v2/job_invocations_controller_test.rb +7 -7
  65. data/test/functional/api/v2/template_invocations_controller_test.rb +3 -3
  66. data/test/helpers/remote_execution_helper_test.rb +8 -7
  67. data/test/unit/actions/run_host_job_test.rb +1 -1
  68. data/test/unit/actions/run_hosts_job_test.rb +11 -11
  69. data/test/unit/concerns/foreman_tasks_cleaner_extensions_test.rb +5 -5
  70. data/test/unit/concerns/host_extensions_test.rb +34 -34
  71. data/test/unit/concerns/nic_extensions_test.rb +1 -1
  72. data/test/unit/execution_task_status_mapper_test.rb +10 -10
  73. data/test/unit/input_template_renderer_test.rb +53 -49
  74. data/test/unit/job_invocation_composer_test.rb +109 -81
  75. data/test/unit/job_invocation_test.rb +25 -25
  76. data/test/unit/job_template_effective_user_test.rb +3 -3
  77. data/test/unit/job_template_test.rb +28 -28
  78. data/test/unit/remote_execution_feature_test.rb +14 -14
  79. data/test/unit/remote_execution_provider_test.rb +39 -39
  80. data/test/unit/renderer_scope_input_test.rb +6 -6
  81. data/test/unit/targeting_test.rb +32 -32
  82. data/webpack/JobInvocationDetail/JobInvocationConstants.js +10 -0
  83. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +38 -0
  84. data/webpack/JobInvocationDetail/JobInvocationOverview.js +13 -25
  85. data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +153 -0
  86. data/webpack/JobInvocationDetail/index.js +48 -10
  87. data/webpack/JobWizard/Footer.js +5 -1
  88. data/webpack/JobWizard/JobWizardConstants.js +4 -0
  89. data/webpack/JobWizard/JobWizardPageRerun.js +3 -0
  90. data/webpack/JobWizard/JobWizardSelectors.js +31 -3
  91. data/webpack/JobWizard/PermissionDenied.js +64 -0
  92. data/webpack/JobWizard/StartsBeforeErrorAlert.js +1 -0
  93. data/webpack/JobWizard/__tests__/fixtures.js +3 -3
  94. data/webpack/JobWizard/autofill.js +8 -4
  95. data/webpack/JobWizard/index.js +41 -1
  96. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +8 -1
  97. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +7 -0
  98. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +41 -7
  99. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +7 -3
  100. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +1 -0
  101. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +21 -7
  102. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +1 -0
  103. data/webpack/JobWizard/steps/HostsAndInputs/index.js +27 -2
  104. data/webpack/JobWizard/steps/ReviewDetails/index.js +7 -2
  105. data/webpack/JobWizard/steps/Schedule/PurposeField.js +1 -0
  106. data/webpack/JobWizard/steps/Schedule/QueryType.js +2 -0
  107. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +1 -0
  108. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +2 -0
  109. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +1 -0
  110. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +1 -0
  111. data/webpack/JobWizard/steps/Schedule/ScheduleFuture.js +2 -0
  112. data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +6 -0
  113. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +3 -0
  114. data/webpack/JobWizard/steps/form/FormHelpers.js +1 -0
  115. data/webpack/JobWizard/steps/form/GroupedSelectField.js +1 -0
  116. data/webpack/JobWizard/steps/form/NumberInput.js +1 -0
  117. data/webpack/JobWizard/steps/form/ResourceSelect.js +1 -0
  118. data/webpack/JobWizard/steps/form/SearchSelect.js +4 -1
  119. data/webpack/JobWizard/steps/form/SelectField.js +1 -0
  120. data/webpack/JobWizard/steps/form/WizardTitle.js +6 -1
  121. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +2 -0
  122. data/webpack/__mocks__/foremanReact/routes/Hosts/constants.js +1 -0
  123. data/webpack/react_app/components/FeaturesDropdown/index.js +1 -0
  124. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +3 -0
  125. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +2 -1
  126. data/webpack/react_app/components/RegistrationExtension/RexInterface.js +1 -0
  127. data/webpack/react_app/components/RegistrationExtension/RexPull.js +1 -0
  128. data/webpack/react_app/components/RegistrationExtension/__tests__/__snapshots__/RexInterface.test.js.snap +1 -0
  129. metadata +10 -6
@@ -13,15 +13,15 @@ class RendererScopeInputTest < ActiveSupport::TestCase
13
13
  it 'caches the value under given key' do
14
14
  i = 1
15
15
  result = input.cached('some_key') { i }
16
- _(result).must_equal 1
16
+ assert_equal 1, result
17
17
 
18
18
  i += 1
19
19
  result = input.cached('some_key') { i }
20
- _(result).must_equal 1
20
+ assert_equal 1, result
21
21
 
22
22
  i += 1
23
23
  result = input.cached('different_key') { i }
24
- _(result).must_equal 3
24
+ assert_equal 3, result
25
25
  end
26
26
  end
27
27
 
@@ -35,15 +35,15 @@ class RendererScopeInputTest < ActiveSupport::TestCase
35
35
  it 'does not cache the value' do
36
36
  i = 1
37
37
  result = input.cached('some_key') { i }
38
- _(result).must_equal 1
38
+ assert_equal 1, result
39
39
 
40
40
  i += 1
41
41
  result = input.cached('some_key') { i }
42
- _(result).must_equal 2
42
+ assert_equal 2, result
43
43
 
44
44
  i += 1
45
45
  result = input.cached('different_key') { i }
46
- _(result).must_equal 3
46
+ assert_equal 3, result
47
47
  end
48
48
  end
49
49
  end
@@ -51,7 +51,7 @@ class TargetingTest < ActiveSupport::TestCase
51
51
  targeting.resolve_hosts!
52
52
  end
53
53
 
54
- it { _(targeting.hosts).must_include(host) }
54
+ it { assert_includes targeting.hosts, host }
55
55
  end
56
56
 
57
57
  context 'can delete a user' do
@@ -63,7 +63,7 @@ class TargetingTest < ActiveSupport::TestCase
63
63
 
64
64
  it { assert_nil targeting.reload.user }
65
65
  it do
66
- -> { targeting.resolve_hosts! }.must_raise(Foreman::Exception)
66
+ assert_raises(Foreman::Exception) { targeting.resolve_hosts! }
67
67
  end
68
68
  end
69
69
 
@@ -74,7 +74,7 @@ class TargetingTest < ActiveSupport::TestCase
74
74
  host.destroy
75
75
  end
76
76
 
77
- it { _(targeting.reload.hosts).must_be_empty }
77
+ it { assert_empty targeting.reload.hosts }
78
78
  end
79
79
 
80
80
  describe '#resolve_hosts!' do
@@ -100,9 +100,9 @@ class TargetingTest < ActiveSupport::TestCase
100
100
  targeting.user = User.current
101
101
  targeting.resolve_hosts!
102
102
 
103
- targeting.hosts.must_include host
104
- targeting.hosts.must_include second_host
105
- targeting.hosts.must_include infra_host
103
+ assert_includes targeting.hosts, host
104
+ assert_includes targeting.hosts, second_host
105
+ assert_includes targeting.hosts, infra_host
106
106
  end
107
107
  end
108
108
 
@@ -115,9 +115,9 @@ class TargetingTest < ActiveSupport::TestCase
115
115
  targeting.user = User.current
116
116
  targeting.resolve_hosts!
117
117
 
118
- targeting.hosts.must_include host
119
- targeting.hosts.must_include second_host
120
- targeting.hosts.wont_include infra_host
118
+ assert_includes targeting.hosts, host
119
+ assert_includes targeting.hosts, second_host
120
+ refute_includes targeting.hosts, infra_host
121
121
  end
122
122
  end
123
123
  end
@@ -136,14 +136,14 @@ class TargetingTest < ActiveSupport::TestCase
136
136
  let(:query) { Targeting.build_query_from_hosts([ host.id, second_host.id, infra_host.id ]) }
137
137
 
138
138
  it 'builds query using host names joining inside ^' do
139
- _(query).must_include host.name
140
- _(query).must_include second_host.name
141
- _(query).must_include infra_host.name
142
- _(query).must_include 'name ^'
143
-
144
- Host.search_for(query).must_include host
145
- Host.search_for(query).must_include second_host
146
- Host.search_for(query).must_include infra_host
139
+ assert_includes query, host.name
140
+ assert_includes query, second_host.name
141
+ assert_includes query, infra_host.name
142
+ assert_includes query, 'name ^'
143
+
144
+ assert_includes Host.search_for(query), host
145
+ assert_includes Host.search_for(query), second_host
146
+ assert_includes Host.search_for(query), infra_host
147
147
  end
148
148
  end
149
149
 
@@ -151,10 +151,10 @@ class TargetingTest < ActiveSupport::TestCase
151
151
  let(:query) { Targeting.build_query_from_hosts([ host.id ]) }
152
152
 
153
153
  it 'builds query using host name' do
154
- _(query).must_equal "name ^ (#{host.name})"
155
- Host.search_for(query).must_include host
156
- Host.search_for(query).wont_include second_host
157
- Host.search_for(query).wont_include infra_host
154
+ assert_equal "name ^ (#{host.name})", query
155
+ assert_includes Host.search_for(query), host
156
+ refute_includes Host.search_for(query), second_host
157
+ refute_includes Host.search_for(query), infra_host
158
158
  end
159
159
  end
160
160
 
@@ -162,9 +162,9 @@ class TargetingTest < ActiveSupport::TestCase
162
162
  let(:query) { Targeting.build_query_from_hosts([]) }
163
163
 
164
164
  it 'builds query to find all hosts' do
165
- Host.search_for(query).must_include host
166
- Host.search_for(query).must_include second_host
167
- Host.search_for(query).must_include infra_host
165
+ assert_includes Host.search_for(query), host
166
+ assert_includes Host.search_for(query), second_host
167
+ assert_includes Host.search_for(query), infra_host
168
168
  end
169
169
  end
170
170
 
@@ -173,14 +173,14 @@ class TargetingTest < ActiveSupport::TestCase
173
173
 
174
174
  it 'ignores the infrastructure host' do
175
175
  query = Targeting.build_query_from_hosts([host.id, second_host.id, infra_host.id])
176
- _(query).must_include host.name
177
- _(query).must_include second_host.name
178
- _(query).wont_include infra_host.name
179
- _(query).must_include 'name ^'
180
-
181
- Host.search_for(query).must_include host
182
- Host.search_for(query).must_include second_host
183
- Host.search_for(query).wont_include infra_host
176
+ assert_includes query, host.name
177
+ assert_includes query, second_host.name
178
+ refute_includes query, infra_host.name
179
+ assert_includes query, 'name ^'
180
+
181
+ assert_includes Host.search_for(query), host
182
+ assert_includes Host.search_for(query), second_host
183
+ refute_includes Host.search_for(query), infra_host
184
184
  end
185
185
  end
186
186
  end
@@ -5,3 +5,13 @@ export const STATUS = {
5
5
  SUCCEEDED: 'succeeded',
6
6
  FAILED: 'failed',
7
7
  };
8
+
9
+ export const DATE_OPTIONS = {
10
+ day: 'numeric',
11
+ month: 'short',
12
+ year: 'numeric',
13
+ hour: '2-digit',
14
+ minute: '2-digit',
15
+ hour12: false,
16
+ timeZoneName: 'short',
17
+ };
@@ -0,0 +1,38 @@
1
+ .job-invocation-detail-page-section {
2
+ $chart_size: 105px;
3
+
4
+ .chart-donut {
5
+ height: $chart_size;
6
+ width: $chart_size;
7
+ margin-bottom: 10px;
8
+ }
9
+
10
+ .chart-legend {
11
+ height: $chart_size;
12
+ min-width: 270px;
13
+
14
+ .legend-title {
15
+ font-weight: bold;
16
+ font-size: var(--pf-global--FontSize--sm);
17
+ margin-left: 8px;
18
+ margin-bottom: 0;
19
+ }
20
+
21
+ .pf-c-description-list {
22
+ margin-left: 8px;
23
+ margin-top: 8px;
24
+
25
+ .pf-c-description-list__term .pf-c-description-list__text {
26
+ font-weight: normal;
27
+ }
28
+ }
29
+ }
30
+
31
+ .pf-c-divider {
32
+ max-height: $chart_size;
33
+ }
34
+
35
+ .job-overview {
36
+ height: $chart_size;
37
+ }
38
+ }
@@ -7,12 +7,15 @@ import {
7
7
  DescriptionListGroup,
8
8
  DescriptionListDescription,
9
9
  } from '@patternfly/react-core';
10
+ import { translate as __ } from 'foremanReact/common/I18n';
10
11
  import DefaultLoaderEmptyState from 'foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState';
11
- import { translate as __, documentLocale } from 'foremanReact/common/I18n';
12
12
 
13
- const JobInvocationOverview = ({ data }) => {
13
+ const JobInvocationOverview = ({
14
+ data,
15
+ isAlreadyStarted,
16
+ formattedStartDate,
17
+ }) => {
14
18
  const {
15
- start_at: startAt,
16
19
  ssh_user: sshUser,
17
20
  template_id: templateId,
18
21
  template_name: templateName,
@@ -22,27 +25,6 @@ const JobInvocationOverview = ({ data }) => {
22
25
  const canEditJobTemplates = permissions
23
26
  ? permissions.edit_job_templates
24
27
  : false;
25
- const dateOptions = {
26
- day: 'numeric',
27
- month: 'short',
28
- year: 'numeric',
29
- hour: '2-digit',
30
- minute: '2-digit',
31
- hour12: false,
32
- timeZoneName: 'short',
33
- };
34
- let formattedStartDate = __('Not yet');
35
-
36
- if (startAt) {
37
- // Ensures date string compatibility across browsers
38
- const convertedDate = new Date(startAt.replace(/[-.]/g, '/'));
39
- if (convertedDate.getTime() <= new Date().getTime()) {
40
- formattedStartDate = convertedDate.toLocaleString(
41
- documentLocale(),
42
- dateOptions
43
- );
44
- }
45
- }
46
28
 
47
29
  return (
48
30
  <DescriptionList
@@ -63,7 +45,7 @@ const JobInvocationOverview = ({ data }) => {
63
45
  <DescriptionListGroup>
64
46
  <DescriptionListTerm>{__('Started at:')}</DescriptionListTerm>
65
47
  <DescriptionListDescription>
66
- {formattedStartDate}
48
+ {isAlreadyStarted ? formattedStartDate : __('Not yet')}
67
49
  </DescriptionListDescription>
68
50
  </DescriptionListGroup>
69
51
  <DescriptionListGroup>
@@ -99,6 +81,12 @@ const JobInvocationOverview = ({ data }) => {
99
81
 
100
82
  JobInvocationOverview.propTypes = {
101
83
  data: PropTypes.object.isRequired,
84
+ isAlreadyStarted: PropTypes.bool.isRequired,
85
+ formattedStartDate: PropTypes.string,
86
+ };
87
+
88
+ JobInvocationOverview.defaultProps = {
89
+ formattedStartDate: undefined,
102
90
  };
103
91
 
104
92
  export default JobInvocationOverview;
@@ -0,0 +1,153 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __, sprintf } from 'foremanReact/common/I18n';
4
+ import {
5
+ ChartDonut,
6
+ ChartLabel,
7
+ ChartLegend,
8
+ ChartTooltip,
9
+ } from '@patternfly/react-charts';
10
+ import {
11
+ DescriptionList,
12
+ DescriptionListTerm,
13
+ DescriptionListGroup,
14
+ DescriptionListDescription,
15
+ FlexItem,
16
+ Text,
17
+ } from '@patternfly/react-core';
18
+ import {
19
+ global_palette_green_500 as successedColor,
20
+ global_palette_red_100 as failedColor,
21
+ global_palette_blue_300 as inProgressColor,
22
+ global_palette_black_600 as canceledColor,
23
+ global_palette_black_500 as emptyChartDonut,
24
+ } from '@patternfly/react-tokens';
25
+ import DefaultLoaderEmptyState from 'foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState';
26
+ import './JobInvocationDetail.scss';
27
+
28
+ const JobInvocationSystemStatusChart = ({
29
+ data,
30
+ isAlreadyStarted,
31
+ formattedStartDate,
32
+ }) => {
33
+ const {
34
+ succeeded,
35
+ failed,
36
+ pending,
37
+ cancelled,
38
+ total,
39
+ total_hosts: totalHosts, // includes scheduled
40
+ } = data;
41
+ const chartData = [
42
+ { title: __('Succeeded:'), count: succeeded, color: successedColor.value },
43
+ { title: __('Failed:'), count: failed, color: failedColor.value },
44
+ { title: __('In Progress:'), count: pending, color: inProgressColor.value },
45
+ { title: __('Canceled:'), count: cancelled, color: canceledColor.value },
46
+ ];
47
+ const chartDonutTitle = () => {
48
+ if (total > 0) return `${succeeded.toString()}/${total}`;
49
+ if (totalHosts > 0) return `0/${totalHosts}`;
50
+ return '0';
51
+ };
52
+ const chartSize = 105;
53
+ const [legendWidth, setLegendWidth] = useState(270);
54
+
55
+ // Calculates chart legend width based on its content
56
+ useEffect(() => {
57
+ const legendContainer = document.querySelector('.chart-legend');
58
+ if (legendContainer) {
59
+ const rectElement = legendContainer.querySelector('rect');
60
+ if (rectElement) {
61
+ const rectWidth = parseFloat(rectElement.getAttribute('width'));
62
+ setLegendWidth(rectWidth);
63
+ }
64
+ }
65
+ }, [isAlreadyStarted, data]);
66
+
67
+ return (
68
+ <>
69
+ <FlexItem className="chart-donut">
70
+ <ChartDonut
71
+ allowTooltip
72
+ constrainToVisibleArea
73
+ data={
74
+ total > 0
75
+ ? chartData.map(d => ({
76
+ label: sprintf(__(`${d.title} ${d.count} hosts`)),
77
+ y: d.count,
78
+ }))
79
+ : [{ label: sprintf(__(`Scheduled: ${totalHosts} hosts`)), y: 1 }]
80
+ }
81
+ colorScale={
82
+ total > 0 ? chartData.map(d => d.color) : [emptyChartDonut.value]
83
+ }
84
+ labelComponent={
85
+ <ChartTooltip pointerLength={0} constrainToVisibleArea />
86
+ }
87
+ title={chartDonutTitle}
88
+ titleComponent={
89
+ // inline style overrides PatternFly default styling
90
+ <ChartLabel style={{ fontSize: '20px' }} />
91
+ }
92
+ subTitle={__('Systems')}
93
+ subTitleComponent={
94
+ // inline style overrides PatternFly default styling
95
+ <ChartLabel
96
+ style={{ fontSize: '12px', fill: canceledColor.value }}
97
+ />
98
+ }
99
+ padding={{
100
+ bottom: 0,
101
+ left: 0,
102
+ right: 0,
103
+ top: 0,
104
+ }}
105
+ width={chartSize}
106
+ height={chartSize}
107
+ />
108
+ </FlexItem>
109
+ <FlexItem className="chart-legend">
110
+ <Text ouiaId="legend-title" className="legend-title">
111
+ {__('System status')}
112
+ </Text>
113
+ {isAlreadyStarted ? (
114
+ <ChartLegend
115
+ orientation="vertical"
116
+ itemsPerRow={2}
117
+ gutter={25}
118
+ rowGutter={7}
119
+ padding={{ left: 15 }}
120
+ data={chartData.map(d => ({
121
+ name: `${d.title} ${d.count}`,
122
+ symbol: { type: 'circle' },
123
+ }))}
124
+ colorScale={chartData.map(d => d.color)}
125
+ width={legendWidth}
126
+ height={chartSize}
127
+ />
128
+ ) : (
129
+ <DescriptionList>
130
+ <DescriptionListGroup>
131
+ <DescriptionListTerm>{__('Scheduled at:')}</DescriptionListTerm>
132
+ <DescriptionListDescription>
133
+ {formattedStartDate || <DefaultLoaderEmptyState />}
134
+ </DescriptionListDescription>
135
+ </DescriptionListGroup>
136
+ </DescriptionList>
137
+ )}
138
+ </FlexItem>
139
+ </>
140
+ );
141
+ };
142
+
143
+ JobInvocationSystemStatusChart.propTypes = {
144
+ data: PropTypes.object.isRequired,
145
+ isAlreadyStarted: PropTypes.bool.isRequired,
146
+ formattedStartDate: PropTypes.string,
147
+ };
148
+
149
+ JobInvocationSystemStatusChart.defaultProps = {
150
+ formattedStartDate: undefined,
151
+ };
152
+
153
+ export default JobInvocationSystemStatusChart;
@@ -1,14 +1,20 @@
1
1
  import React, { useEffect } from 'react';
2
2
  import { useSelector, useDispatch } from 'react-redux';
3
3
  import PropTypes from 'prop-types';
4
- import { Divider, PageSection, Flex, FlexItem } from '@patternfly/react-core';
5
- import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { Divider, PageSection, Flex } from '@patternfly/react-core';
5
+ import { translate as __, documentLocale } from 'foremanReact/common/I18n';
6
6
  import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
7
7
  import { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
8
8
  import { getData } from './JobInvocationActions';
9
9
  import { selectItems } from './JobInvocationSelectors';
10
10
  import JobInvocationOverview from './JobInvocationOverview';
11
- import { JOB_INVOCATION_KEY, STATUS } from './JobInvocationConstants';
11
+ import JobInvocationSystemStatusChart from './JobInvocationSystemStatusChart';
12
+ import {
13
+ JOB_INVOCATION_KEY,
14
+ STATUS,
15
+ DATE_OPTIONS,
16
+ } from './JobInvocationConstants';
17
+ import './JobInvocationDetail.scss';
12
18
 
13
19
  const JobInvocationDetailPage = ({
14
20
  match: {
@@ -17,11 +23,28 @@ const JobInvocationDetailPage = ({
17
23
  }) => {
18
24
  const dispatch = useDispatch();
19
25
  const items = useSelector(selectItems);
20
- const { description, status_label: statusLabel, task } = items;
26
+ const {
27
+ description,
28
+ status_label: statusLabel,
29
+ task,
30
+ start_at: startAt,
31
+ } = items;
21
32
  const finished =
22
33
  statusLabel === STATUS.FAILED || statusLabel === STATUS.SUCCEEDED;
23
34
  const autoRefresh = task?.state === STATUS.PENDING || false;
24
35
 
36
+ let isAlreadyStarted = false;
37
+ let formattedStartDate;
38
+ if (startAt) {
39
+ // Ensures date string compatibility across browsers
40
+ const convertedDate = new Date(startAt.replace(/[-.]/g, '/'));
41
+ isAlreadyStarted = convertedDate.getTime() <= new Date().getTime();
42
+ formattedStartDate = convertedDate.toLocaleString(
43
+ documentLocale(),
44
+ DATE_OPTIONS
45
+ );
46
+ }
47
+
25
48
  useEffect(() => {
26
49
  dispatch(getData(`/api/job_invocations/${id}`));
27
50
  if (finished && !autoRefresh) {
@@ -48,17 +71,32 @@ const JobInvocationDetailPage = ({
48
71
  searchable={false}
49
72
  >
50
73
  <React.Fragment>
51
- <PageSection isFilled variant="light">
52
- <Flex>
53
- <FlexItem> </FlexItem>
74
+ <PageSection
75
+ className="job-invocation-detail-page-section"
76
+ isFilled
77
+ variant="light"
78
+ >
79
+ <Flex alignItems={{ default: 'alignItemsFlexStart' }}>
80
+ <JobInvocationSystemStatusChart
81
+ data={items}
82
+ isAlreadyStarted={isAlreadyStarted}
83
+ formattedStartDate={formattedStartDate}
84
+ />
54
85
  <Divider
55
86
  orientation={{
56
87
  default: 'vertical',
57
88
  }}
58
89
  />
59
- <FlexItem>
60
- <JobInvocationOverview data={items} />
61
- </FlexItem>
90
+ <Flex
91
+ className="job-overview"
92
+ alignItems={{ default: 'alignItemsCenter' }}
93
+ >
94
+ <JobInvocationOverview
95
+ data={items}
96
+ isAlreadyStarted={isAlreadyStarted}
97
+ formattedStartDate={formattedStartDate}
98
+ />
99
+ </Flex>
62
100
  </Flex>
63
101
  </PageSection>
64
102
  </React.Fragment>
@@ -26,6 +26,7 @@ export const Footer = ({ canSubmit, onSave }) => {
26
26
  return (
27
27
  <>
28
28
  <Button
29
+ ouiaId="next-footer"
29
30
  variant="primary"
30
31
  type="submit"
31
32
  onClick={onNext}
@@ -36,6 +37,7 @@ export const Footer = ({ canSubmit, onSave }) => {
36
37
  : __('Next')}
37
38
  </Button>
38
39
  <Button
40
+ ouiaId="back-footer"
39
41
  variant="secondary"
40
42
  onClick={onBack}
41
43
  isDisabled={
@@ -54,6 +56,7 @@ export const Footer = ({ canSubmit, onSave }) => {
54
56
  }
55
57
  >
56
58
  <Button
59
+ ouiaId="run-on-selected-hosts-footer"
57
60
  variant="tertiary"
58
61
  onClick={onSave}
59
62
  isAriaDisabled={!canSubmit}
@@ -74,6 +77,7 @@ export const Footer = ({ canSubmit, onSave }) => {
74
77
  }
75
78
  >
76
79
  <Button
80
+ ouiaId="skip-to-review-footer"
77
81
  variant="tertiary"
78
82
  onClick={() => goToStepByName(WIZARD_TITLES.review)}
79
83
  isAriaDisabled={!canSubmit}
@@ -82,7 +86,7 @@ export const Footer = ({ canSubmit, onSave }) => {
82
86
  {__('Skip to review')}
83
87
  </Button>
84
88
  </Tooltip>
85
- <Button variant="link" onClick={onClose}>
89
+ <Button ouiaId="cancel-footer" variant="link" onClick={onClose}>
86
90
  {__('Cancel')}
87
91
  </Button>
88
92
  {isSubmitting && (
@@ -5,6 +5,10 @@ export const JOB_TEMPLATES = 'JOB_TEMPLATES';
5
5
  export const JOB_CATEGORIES = 'JOB_CATEGORIES';
6
6
  export const JOB_TEMPLATE = 'JOB_TEMPLATE';
7
7
  export const JOB_INVOCATION = 'JOB_INVOCATION';
8
+ export const CURRENT_PERMISSIONS = 'CURRENT_PERMISSIONS';
9
+ export const currentPermissionsUrl = foremanUrl(
10
+ '/api/v2/permissions/current_permissions'
11
+ );
8
12
  export const templatesUrl = foremanUrl('/api/v2/job_templates');
9
13
 
10
14
  export const repeatTypes = {
@@ -48,6 +48,7 @@ const JobWizardPageRerun = ({
48
48
  searchable={false}
49
49
  toolbarButtons={
50
50
  <Button
51
+ ouiaId="job-wizard-rerun-old-form-button"
51
52
  variant="link"
52
53
  component="a"
53
54
  href={`/old/job_invocations/${id}/rerun${search}`}
@@ -71,6 +72,7 @@ const JobWizardPageRerun = ({
71
72
  <React.Fragment>
72
73
  {jobOrganization?.id !== currentOrganization?.id && (
73
74
  <Alert
75
+ ouiaId="job-wizard-alert-organization"
74
76
  isInline
75
77
  className="job-wizard-alert"
76
78
  variant="warning"
@@ -85,6 +87,7 @@ const JobWizardPageRerun = ({
85
87
  )}
86
88
  {jobLocation?.id !== currentLocation?.id && (
87
89
  <Alert
90
+ ouiaId="job-wizard-alert-location"
88
91
  isInline
89
92
  className="job-wizard-alert"
90
93
  variant="warning"
@@ -1,4 +1,5 @@
1
1
  import URI from 'urijs';
2
+ import { get } from 'lodash';
2
3
  import {
3
4
  selectAPIResponse,
4
5
  selectAPIStatus,
@@ -42,6 +43,18 @@ export const selectJobCategoriesStatus = state =>
42
43
  export const selectCategoryError = state =>
43
44
  selectAPIErrorMessage(state, JOB_CATEGORIES);
44
45
 
46
+ export const selectJobCategoriesMissingPermissions = state => {
47
+ const jobCategoriesResponse = selectJobCategoriesResponse(state);
48
+ return (
49
+ get(jobCategoriesResponse, [
50
+ 'response',
51
+ 'data',
52
+ 'error',
53
+ 'missing_permissions',
54
+ ]) || []
55
+ );
56
+ };
57
+
45
58
  export const selectAllTemplatesError = state =>
46
59
  selectAPIErrorMessage(state, JOB_TEMPLATES);
47
60
 
@@ -60,11 +73,26 @@ export const selectAdvancedTemplateInputs = state =>
60
73
  export const selectTemplateInputs = state =>
61
74
  selectAPIResponse(state, JOB_TEMPLATE).template_inputs || [];
62
75
 
76
+ export const selectHostsResponse = state => selectAPIResponse(state, HOSTS_API);
77
+
63
78
  export const selectHostCount = state =>
64
- selectAPIResponse(state, HOSTS_API).subtotal || 0;
79
+ selectHostsResponse(state).subtotal || 0;
80
+
81
+ export const selectHosts = state => {
82
+ const hosts = selectHostsResponse(state).results || [];
83
+ return hosts.map(host => ({
84
+ name: host.name,
85
+ display_name: host.display_name,
86
+ }));
87
+ };
65
88
 
66
- export const selectHosts = state =>
67
- (selectAPIResponse(state, HOSTS_API).results || []).map(host => host.name);
89
+ export const selectHostsMissingPermissions = state => {
90
+ const hostsResponse = selectHostsResponse(state);
91
+ return (
92
+ get(hostsResponse, ['response', 'data', 'error', 'missing_permissions']) ||
93
+ []
94
+ );
95
+ };
68
96
 
69
97
  export const selectIsLoadingHosts = state =>
70
98
  !selectAPIStatus(state, HOSTS_API) ||