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.
- 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 && (
|