foreman_remote_execution 13.0.0 → 13.1.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/.eslintrc +4 -1
- data/.github/workflows/release.yml +4 -2
- data/app/lib/actions/remote_execution/run_host_job.rb +0 -14
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
- data/app/models/job_invocation_composer.rb +4 -3
- data/app/views/api/v2/job_invocations/base.json.rabl +5 -3
- data/app/views/templates/script/convert2rhel_analyze.erb +1 -12
- data/app/views/templates/script/package_action.erb +11 -1
- data/app/views/templates/script/puppet_run_once.erb +3 -3
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +6 -6
- data/test/unit/job_invocation_composer_test.rb +31 -3
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +10 -0
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +38 -0
- data/webpack/JobInvocationDetail/JobInvocationOverview.js +13 -25
- data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +153 -0
- data/webpack/JobInvocationDetail/index.js +48 -10
- data/webpack/JobWizard/Footer.js +5 -1
- data/webpack/JobWizard/JobWizardPageRerun.js +3 -0
- data/webpack/JobWizard/StartsBeforeErrorAlert.js +1 -0
- data/webpack/JobWizard/index.js +1 -0
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +8 -1
- data/webpack/JobWizard/steps/AdvancedFields/Fields.js +7 -0
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +16 -3
- data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +5 -1
- data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +8 -1
- data/webpack/JobWizard/steps/HostsAndInputs/index.js +7 -2
- data/webpack/JobWizard/steps/ReviewDetails/index.js +4 -0
- data/webpack/JobWizard/steps/Schedule/PurposeField.js +1 -0
- data/webpack/JobWizard/steps/Schedule/QueryType.js +2 -0
- data/webpack/JobWizard/steps/Schedule/RepeatCron.js +1 -0
- data/webpack/JobWizard/steps/Schedule/RepeatHour.js +2 -0
- data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +1 -0
- data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +1 -0
- data/webpack/JobWizard/steps/Schedule/ScheduleFuture.js +2 -0
- data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +6 -0
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +3 -0
- data/webpack/JobWizard/steps/form/FormHelpers.js +1 -0
- data/webpack/JobWizard/steps/form/GroupedSelectField.js +1 -0
- data/webpack/JobWizard/steps/form/NumberInput.js +1 -0
- data/webpack/JobWizard/steps/form/ResourceSelect.js +1 -0
- data/webpack/JobWizard/steps/form/SearchSelect.js +1 -0
- data/webpack/JobWizard/steps/form/SelectField.js +1 -0
- data/webpack/JobWizard/steps/form/WizardTitle.js +6 -1
- data/webpack/__mocks__/foremanReact/routes/Hosts/constants.js +1 -0
- data/webpack/react_app/components/FeaturesDropdown/index.js +1 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +3 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +2 -1
- data/webpack/react_app/components/RegistrationExtension/RexInterface.js +1 -0
- data/webpack/react_app/components/RegistrationExtension/RexPull.js +1 -0
- data/webpack/react_app/components/RegistrationExtension/__tests__/__snapshots__/RexInterface.test.js.snap +1 -0
- metadata +9 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0c70b663aba31ca9cbe32da992a3bfa346232b535c720d578ade8a78772e0a38
|
4
|
+
data.tar.gz: 29b2f8d63af5a7a360b18db895805c3adc781042e06f928ac875b7a048f15090
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 30c819eda158efaabec5b9aa47f01fd61bf9b92ff595f20e0646f5e91eae4ad1b4c9a5e05ffb7d0ecc442c19c211ed6b46e8eccaba831f36eef36886c11cd155
|
7
|
+
data.tar.gz: 2b60fb76b97d97b7491eb87b2c1c454d024b5be5ffa48de1acad41575eb9fd761a2d70aecda133b5d7f4604150c88524ca7606185b2fd2d0a86760eb4c9a9433
|
data/.eslintrc
CHANGED
@@ -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)
|
@@ -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 =
|
573
|
-
|
574
|
-
input
|
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 =>
|
12
|
-
:failed =>
|
13
|
-
:pending =>
|
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
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
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") %>
|
data/package.json
CHANGED
@@ -19,11 +19,11 @@
|
|
19
19
|
},
|
20
20
|
"devDependencies": {
|
21
21
|
"@babel/core": "^7.7.0",
|
22
|
-
"@theforeman/builder": "
|
23
|
-
"@theforeman/eslint-plugin-foreman": "
|
24
|
-
"@theforeman/eslint-plugin-rules": "
|
25
|
-
"@theforeman/test": "
|
26
|
-
"@theforeman/vendor-dev": "
|
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": "
|
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 =>
|
658
|
-
:job_template_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 => {
|
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 = ({
|
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
|
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
|
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 {
|
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
|
52
|
-
|
53
|
-
|
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
|
-
<
|
60
|
-
|
61
|
-
|
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>
|
data/webpack/JobWizard/Footer.js
CHANGED
@@ -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 && (
|