foreman_remote_execution 13.0.0 → 13.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc +4 -1
  3. data/.github/workflows/release.yml +4 -2
  4. data/app/lib/actions/remote_execution/run_host_job.rb +0 -14
  5. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
  6. data/app/models/job_invocation_composer.rb +4 -3
  7. data/app/views/api/v2/job_invocations/base.json.rabl +5 -3
  8. data/app/views/templates/script/convert2rhel_analyze.erb +1 -12
  9. data/app/views/templates/script/package_action.erb +11 -1
  10. data/app/views/templates/script/puppet_run_once.erb +3 -3
  11. data/lib/foreman_remote_execution/version.rb +1 -1
  12. data/package.json +6 -6
  13. data/test/unit/job_invocation_composer_test.rb +31 -3
  14. data/webpack/JobInvocationDetail/JobInvocationConstants.js +10 -0
  15. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +38 -0
  16. data/webpack/JobInvocationDetail/JobInvocationOverview.js +13 -25
  17. data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +153 -0
  18. data/webpack/JobInvocationDetail/index.js +48 -10
  19. data/webpack/JobWizard/Footer.js +5 -1
  20. data/webpack/JobWizard/JobWizardPageRerun.js +3 -0
  21. data/webpack/JobWizard/StartsBeforeErrorAlert.js +1 -0
  22. data/webpack/JobWizard/index.js +1 -0
  23. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +8 -1
  24. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +7 -0
  25. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +16 -3
  26. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +5 -1
  27. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +8 -1
  28. data/webpack/JobWizard/steps/HostsAndInputs/index.js +7 -2
  29. data/webpack/JobWizard/steps/ReviewDetails/index.js +4 -0
  30. data/webpack/JobWizard/steps/Schedule/PurposeField.js +1 -0
  31. data/webpack/JobWizard/steps/Schedule/QueryType.js +2 -0
  32. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +1 -0
  33. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +2 -0
  34. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +1 -0
  35. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +1 -0
  36. data/webpack/JobWizard/steps/Schedule/ScheduleFuture.js +2 -0
  37. data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +6 -0
  38. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +3 -0
  39. data/webpack/JobWizard/steps/form/FormHelpers.js +1 -0
  40. data/webpack/JobWizard/steps/form/GroupedSelectField.js +1 -0
  41. data/webpack/JobWizard/steps/form/NumberInput.js +1 -0
  42. data/webpack/JobWizard/steps/form/ResourceSelect.js +1 -0
  43. data/webpack/JobWizard/steps/form/SearchSelect.js +1 -0
  44. data/webpack/JobWizard/steps/form/SelectField.js +1 -0
  45. data/webpack/JobWizard/steps/form/WizardTitle.js +6 -1
  46. data/webpack/__mocks__/foremanReact/routes/Hosts/constants.js +1 -0
  47. data/webpack/react_app/components/FeaturesDropdown/index.js +1 -0
  48. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +3 -0
  49. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +2 -1
  50. data/webpack/react_app/components/RegistrationExtension/RexInterface.js +1 -0
  51. data/webpack/react_app/components/RegistrationExtension/RexPull.js +1 -0
  52. data/webpack/react_app/components/RegistrationExtension/__tests__/__snapshots__/RexInterface.test.js.snap +1 -0
  53. metadata +9 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a5e47997164244116ccb9c37d8eb7f7ec32b36d953ddcba6763ba86afc84a0f
4
- data.tar.gz: ec69e4b0ef576ba474949648e164ba6d6bcd252bc4f9e501c7adaa8dc69b4da9
3
+ metadata.gz: 0c70b663aba31ca9cbe32da992a3bfa346232b535c720d578ade8a78772e0a38
4
+ data.tar.gz: 29b2f8d63af5a7a360b18db895805c3adc781042e06f928ac875b7a048f15090
5
5
  SHA512:
6
- metadata.gz: e3cf4ecf6c5834264479776dad8342b039964664ab2ff9b388da168c9ad5337d7b2f013d435bf64e6aedc15929a0147e882d068883b1d1dabaeac82a09193f72
7
- data.tar.gz: c41da5c789687b7374a4412c12d3a20befeda3a46d1ab087d12da91a053c82a1fd6d63164d4a11043cb4931c899012e2da9b57c1f250893ed1ec79a2db8e03a1
6
+ metadata.gz: 30c819eda158efaabec5b9aa47f01fd61bf9b92ff595f20e0646f5e91eae4ad1b4c9a5e05ffb7d0ecc442c19c211ed6b46e8eccaba831f36eef36886c11cd155
7
+ data.tar.gz: 2b60fb76b97d97b7491eb87b2c1c454d024b5be5ffa48de1acad41575eb9fd761a2d70aecda133b5d7f4604150c88524ca7606185b2fd2d0a86760eb4c9a9433
data/.eslintrc CHANGED
@@ -6,5 +6,8 @@
6
6
  "extends": [
7
7
  "plugin:@theforeman/foreman/core",
8
8
  "plugin:@theforeman/foreman/plugins"
9
- ]
9
+ ],
10
+ "rules":{
11
+ "@theforeman/rules/require-ouiaid": ["error"]
12
+ }
10
13
  }
@@ -1,8 +1,10 @@
1
1
  name: Release
2
2
 
3
3
  on:
4
- create:
5
- ref_type: tag
4
+ push:
5
+ # Pattern matched against refs/tags
6
+ tags:
7
+ - '**'
6
8
 
7
9
  jobs:
8
10
  release:
@@ -204,20 +204,6 @@ module Actions
204
204
  host.refresh_global_status!
205
205
  end
206
206
 
207
- def delegated_output
208
- if input[:delegated_action_id]
209
- super
210
- elsif phase?(Present)
211
- # for compatibility with old actions
212
- old_action = all_planned_actions.first
213
- if old_action
214
- old_action.output.fetch('proxy_output', {})
215
- else
216
- {}
217
- end
218
- end
219
- end
220
-
221
207
  def verify_permissions(host, template_invocation)
222
208
  raise _('User can not execute job on host %s') % host.name unless User.current.can?(:view_hosts, host)
223
209
  raise _('User can not execute this job template') unless User.current.can?(:view_job_templates, template_invocation.template)
@@ -152,3 +152,11 @@ module ForemanRemoteExecution
152
152
  end
153
153
  end
154
154
  end
155
+
156
+ module Host
157
+ class Managed
158
+ class Jail < Safemode::Jail
159
+ allow :execution_interface
160
+ end
161
+ end
162
+ end
@@ -569,9 +569,10 @@ class JobInvocationComposer
569
569
  # builds input values for a given templates id based on params
570
570
  # omits inputs that belongs to unavailable templates
571
571
  def build_input_values_for(template_invocation, job_template_base)
572
- template_invocation.input_values = job_template_base.fetch('input_values', {}).map do |attributes|
573
- input = template_invocation.template.template_inputs_with_foreign.find { |i| i.id.to_s == attributes[:template_input_id].to_s }
574
- input ? input.template_invocation_input_values.build(attributes) : nil
572
+ template_invocation.input_values = template_invocation.template.template_inputs_with_foreign.map do |input|
573
+ attributes = job_template_base.fetch('input_values', {}).find { |i| i[:template_input_id].to_s == input.id.to_s }
574
+ attributes = { "template_input_id" => input.id, "value" => input.default } if attributes.nil? && input.default
575
+ attributes ? input.template_invocation_input_values.build(attributes) : nil
575
576
  end.compact
576
577
  template_invocation.provider_input_values.build job_template_base.fetch('provider_input_values', [])
577
578
  end
@@ -8,11 +8,13 @@ node do |invocation|
8
8
  :template_id => pattern_template&.template_id,
9
9
  :template_name => pattern_template&.template_name,
10
10
  :effective_user => pattern_template&.effective_user,
11
- :succeeded => invocation_count(invocation, :output_key => :success_count),
12
- :failed => invocation_count(invocation, :output_key => :failed_count),
13
- :pending => invocation_count(invocation, :output_key => :pending_count),
11
+ :succeeded => invocation.progress_report[:success],
12
+ :failed => invocation.progress_report[:error],
13
+ :pending => invocation.progress_report[:pending],
14
+ :cancelled => invocation.progress_report[:cancelled],
14
15
  :total => invocation_count(invocation, :output_key => :total_count),
15
16
  :missing => invocation.missing_hosts_count,
17
+ :total_hosts => invocation.total_hosts_count,
16
18
  }
17
19
  end
18
20
 
@@ -1,15 +1,6 @@
1
1
  <%#
2
2
  name: Convert2RHEL analyze
3
3
  snippet: false
4
- template_inputs:
5
- - name: Data telemetry
6
- required: false
7
- input_type: user
8
- options: "yes\r\nno"
9
- advanced: false
10
- value_type: plain
11
- default: 'yes'
12
- hidden_value: false
13
4
  model: JobTemplate
14
5
  job_category: Convert 2 RHEL
15
6
  provider_type: script
@@ -25,9 +16,7 @@ if ! rpm -q convert2rhel &> /dev/null; then
25
16
  yum install -y convert2rhel
26
17
  fi
27
18
 
28
- <% if input('Data telemetry') != "yes" -%>
29
- export CONVERT2RHEL_DISABLE_TELEMETRY=1
30
- <% end -%>
19
+ export CONVERT2RHEL_THROUGH_FOREMAN=1
31
20
 
32
21
  /usr/bin/convert2rhel analyze -y
33
22
 
@@ -115,9 +115,19 @@ handle_zypp_res_codes () {
115
115
  end
116
116
  end
117
117
  -%>
118
+ OUTFILE=$(mktemp)
119
+ trap "rm -f $OUTFILE" EXIT
120
+ set -o pipefail
118
121
  export DEBIAN_FRONTEND=noninteractive
119
122
  apt-get -y update
120
- apt-get -o APT::Get::Upgrade-Allow-New="true" -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y <%= input("options") %> <%= action %> <%= input("package") %>
123
+ LANG=C apt-get -o APT::Get::Upgrade-Allow-New="true" -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y <%= input("options") %> <%= action %> <%= input("package") %> 2>&1 | tee $OUTFILE
124
+ RETVAL=$?
125
+ if grep -q "Unable to locate" $OUTFILE; then
126
+ true
127
+ else
128
+ setReturnValue() { RETVAL=$1; return $RETVAL; }
129
+ setReturnValue $RETVAL
130
+ fi
121
131
  <% elsif package_manager == 'zypper' -%>
122
132
  <%-
123
133
  if action == "group install"
@@ -12,7 +12,7 @@ template_inputs:
12
12
  required: false
13
13
  feature: puppet_run_host
14
14
  %>
15
- <% if @host.operatingsystem.family == 'Debian' -%>
16
- export PATH=/opt/puppetlabs/bin:$PATH
17
- <% end -%>
15
+ if [ -f /etc/profile.d/puppet-agent.sh ]; then
16
+ . /etc/profile.d/puppet-agent.sh
17
+ fi
18
18
  puppet agent --onetime --no-usecacheonfailure --no-daemonize <%= input("puppet_options") %>
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '13.0.0'.freeze
2
+ VERSION = '13.1.0'.freeze
3
3
  end
data/package.json CHANGED
@@ -19,11 +19,11 @@
19
19
  },
20
20
  "devDependencies": {
21
21
  "@babel/core": "^7.7.0",
22
- "@theforeman/builder": "^12.0.1",
23
- "@theforeman/eslint-plugin-foreman": "^12.0.1",
24
- "@theforeman/eslint-plugin-rules": "^12.0.2",
25
- "@theforeman/test": "^12.0.1",
26
- "@theforeman/vendor-dev": "^12.0.1",
22
+ "@theforeman/builder": ">= 12.0.1",
23
+ "@theforeman/eslint-plugin-foreman": ">= 12.0.1",
24
+ "@theforeman/eslint-plugin-rules": ">= 12.0.2",
25
+ "@theforeman/test": ">= 12.0.1",
26
+ "@theforeman/vendor-dev": ">= 12.0.1",
27
27
  "babel-eslint": "^10.0.0",
28
28
  "eslint": "^6.8.0",
29
29
  "prettier": "^1.19.1",
@@ -32,6 +32,6 @@
32
32
  "graphql": "^15.5.0"
33
33
  },
34
34
  "peerDependencies": {
35
- "@theforeman/vendor": "^12.0.1"
35
+ "@theforeman/vendor": ">= 12.0.1"
36
36
  }
37
37
  }
@@ -35,6 +35,7 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
35
35
  let(:trying_job_template_1) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_1', :name => 'trying1', :provider_type => 'SSH') }
36
36
  let(:trying_job_template_2) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_2', :name => 'trying2', :provider_type => 'Mcollective') }
37
37
  let(:trying_job_template_3) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_1', :name => 'trying3', :provider_type => 'SSH') }
38
+ let(:trying_job_template_5) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_5', :name => 'trying5', :provider_type => 'SSH') }
38
39
  let(:unauthorized_job_template_1) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_1', :name => 'unauth1', :provider_type => 'SSH') }
39
40
  let(:unauthorized_job_template_2) { FactoryBot.create(:job_template, :job_category => 'unauthorized_job_template_2', :name => 'unauth2', :provider_type => 'Ansible') }
40
41
 
@@ -44,6 +45,7 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
44
45
  let(:input2) { FactoryBot.create(:template_input, :template => trying_job_template_3, :input_type => 'user') }
45
46
  let(:input3) { FactoryBot.create(:template_input, :template => trying_job_template_1, :input_type => 'user', :required => true) }
46
47
  let(:input4) { FactoryBot.create(:template_input, :template => provider_inputs_job_template, :input_type => 'user') }
48
+ let(:input5) { FactoryBot.create(:template_input, :template => trying_job_template_5, :input_type => 'user', :default => 'value') }
47
49
  let(:unauthorized_input1) { FactoryBot.create(:template_input, :template => unauthorized_job_template_1, :input_type => 'user') }
48
50
 
49
51
  let(:ansible_params) { { } }
@@ -654,14 +656,40 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
654
656
 
655
657
  context 'with with inputs' do
656
658
  let(:params) do
657
- { :job_category => trying_job_template_1.job_category,
658
- :job_template_id => trying_job_template_1.id,
659
+ { :job_category => trying_job_template_5.job_category,
660
+ :job_template_id => trying_job_template_5.id,
659
661
  :targeting_type => 'static_query',
660
662
  :search_query => 'some hosts',
661
- :inputs => {input1.name => 'some_value'}}
663
+ :inputs => {input5.name => 'some_value'}}
662
664
  end
663
665
 
664
666
  it 'finds the inputs by name' do
667
+ assert composer.save!
668
+ values = composer.pattern_template_invocations.first.input_values
669
+ assert_equal 1, values.count
670
+ assert_equal 'some_value', values.first.value
671
+ end
672
+
673
+ it 'can be forced to be empty' do
674
+ params[:inputs] = {input5.name => ''}
675
+ assert composer.save!
676
+ values = composer.pattern_template_invocations.first.input_values
677
+ assert_equal 1, values.count
678
+ assert_equal '', values.first.value
679
+ end
680
+ end
681
+
682
+ context 'with inputs and default values' do
683
+ let(:params) do
684
+ { :job_category => trying_job_template_5.job_category,
685
+ :job_template_id => trying_job_template_5.id,
686
+ :targeting_type => 'static_query',
687
+ :search_query => 'some hosts',
688
+ :inputs => {}}
689
+ end
690
+
691
+ it 'uses the default input values' do
692
+ input5 # Force the factory to be materialized
665
693
  assert composer.save!
666
694
  assert_equal 1, composer.pattern_template_invocations.first.input_values.collect.count
667
695
  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 && (