foreman_remote_execution 13.0.0 → 13.1.1
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/assets/javascripts/foreman_remote_execution/locale/de/foreman_remote_execution.js +1000 -973
- data/app/assets/javascripts/foreman_remote_execution/locale/en/foreman_remote_execution.js +553 -526
- data/app/assets/javascripts/foreman_remote_execution/locale/en_GB/foreman_remote_execution.js +584 -557
- data/app/assets/javascripts/foreman_remote_execution/locale/es/foreman_remote_execution.js +1043 -1016
- data/app/assets/javascripts/foreman_remote_execution/locale/fr/foreman_remote_execution.js +1047 -1020
- data/app/assets/javascripts/foreman_remote_execution/locale/ja/foreman_remote_execution.js +1041 -1014
- data/app/assets/javascripts/foreman_remote_execution/locale/ka/foreman_remote_execution.js +1015 -988
- data/app/assets/javascripts/foreman_remote_execution/locale/ko/foreman_remote_execution.js +886 -859
- data/app/assets/javascripts/foreman_remote_execution/locale/pt_BR/foreman_remote_execution.js +1047 -1020
- data/app/assets/javascripts/foreman_remote_execution/locale/ru/foreman_remote_execution.js +901 -874
- data/app/assets/javascripts/foreman_remote_execution/locale/zh_CN/foreman_remote_execution.js +1041 -1014
- data/app/assets/javascripts/foreman_remote_execution/locale/zh_TW/foreman_remote_execution.js +886 -859
- 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 +15 -2
- data/app/views/templates/script/puppet_run_once.erb +3 -3
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/locale/de/foreman_remote_execution.po +27 -0
- data/locale/en/foreman_remote_execution.po +27 -0
- data/locale/en_GB/foreman_remote_execution.po +27 -0
- data/locale/es/foreman_remote_execution.po +27 -0
- data/locale/foreman_remote_execution.pot +177 -141
- data/locale/fr/foreman_remote_execution.po +27 -0
- data/locale/ja/foreman_remote_execution.po +27 -0
- data/locale/ka/foreman_remote_execution.po +27 -0
- data/locale/ko/foreman_remote_execution.po +27 -0
- data/locale/pt_BR/foreman_remote_execution.po +27 -0
- data/locale/ru/foreman_remote_execution.po +27 -0
- data/locale/zh_CN/foreman_remote_execution.po +27 -0
- data/locale/zh_TW/foreman_remote_execution.po +27 -0
- 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 +4 -1
- 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 +11 -5
- 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/Root/Context/ForemanContext/index.js +2 -0
- 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
@@ -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 && (
|
@@ -48,11 +48,12 @@ 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}`}
|
54
55
|
>
|
55
|
-
{__('Use
|
56
|
+
{__('Use legacy form')}
|
56
57
|
</Button>
|
57
58
|
}
|
58
59
|
>
|
@@ -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"
|
data/webpack/JobWizard/index.js
CHANGED
@@ -56,7 +56,12 @@ export const DescriptionField = ({
|
|
56
56
|
}
|
57
57
|
fieldId="description"
|
58
58
|
helperText={
|
59
|
-
<Button
|
59
|
+
<Button
|
60
|
+
ouiaId="description-preview-button"
|
61
|
+
variant="link"
|
62
|
+
isInline
|
63
|
+
onClick={togglePreview}
|
64
|
+
>
|
60
65
|
{isPreview
|
61
66
|
? __('Edit job description template')
|
62
67
|
: __('Preview job description')}
|
@@ -68,6 +73,7 @@ export const DescriptionField = ({
|
|
68
73
|
<div>
|
69
74
|
{/* div wrapper so the tooltip will be shown in chrome */}
|
70
75
|
<TextInput
|
76
|
+
ouiaId="description-preview"
|
71
77
|
aria-label="description preview"
|
72
78
|
id="description-preview"
|
73
79
|
value={generatedDesc}
|
@@ -77,6 +83,7 @@ export const DescriptionField = ({
|
|
77
83
|
</Tooltip>
|
78
84
|
) : (
|
79
85
|
<TextInput
|
86
|
+
ouiaId="description-edit"
|
80
87
|
aria-label="description edit"
|
81
88
|
type="text"
|
82
89
|
autoComplete="description"
|
@@ -19,6 +19,7 @@ export const EffectiveUserField = ({ value, setValue, defaultValue }) => (
|
|
19
19
|
labelInfo={<ResetDefault setValue={setValue} defaultValue={defaultValue} />}
|
20
20
|
>
|
21
21
|
<TextInput
|
22
|
+
ouiaId="effective-user"
|
22
23
|
aria-label="effective user"
|
23
24
|
autoComplete="effective-user"
|
24
25
|
id="effective-user"
|
@@ -86,6 +87,7 @@ export const PasswordField = ({ value, setValue }) => (
|
|
86
87
|
fieldId="job-password"
|
87
88
|
>
|
88
89
|
<TextInput
|
90
|
+
ouiaId="job-password"
|
89
91
|
aria-label="job password"
|
90
92
|
autoComplete="new-password" // to prevent firefox from autofilling the user password
|
91
93
|
id="job-password"
|
@@ -109,6 +111,7 @@ export const KeyPassphraseField = ({ value, setValue }) => (
|
|
109
111
|
fieldId="key-passphrase"
|
110
112
|
>
|
111
113
|
<TextInput
|
114
|
+
ouiaId="key-passphrase"
|
112
115
|
aria-label="key passphrase"
|
113
116
|
autoComplete="key-passphrase"
|
114
117
|
id="key-passphrase"
|
@@ -132,6 +135,7 @@ export const EffectiveUserPasswordField = ({ value, setValue }) => (
|
|
132
135
|
fieldId="effective-user-password"
|
133
136
|
>
|
134
137
|
<TextInput
|
138
|
+
ouiaId="effective-user-password"
|
135
139
|
aria-label="effective userpassword"
|
136
140
|
autoComplete="effective-user-password"
|
137
141
|
id="effective-user-password"
|
@@ -186,6 +190,7 @@ export const ExecutionOrderingField = ({ isRandomizedOrdering, setValue }) => (
|
|
186
190
|
isInline
|
187
191
|
>
|
188
192
|
<Radio
|
193
|
+
ouiaId="execution-order-alphabetical"
|
189
194
|
aria-label="execution order alphabetical"
|
190
195
|
isChecked={!isRandomizedOrdering}
|
191
196
|
name="execution-order"
|
@@ -194,6 +199,7 @@ export const ExecutionOrderingField = ({ isRandomizedOrdering, setValue }) => (
|
|
194
199
|
label={__('Alphabetical')}
|
195
200
|
/>
|
196
201
|
<Radio
|
202
|
+
ouiaId="execution-order-randomized"
|
197
203
|
aria-label="execution order randomized"
|
198
204
|
isChecked={isRandomizedOrdering}
|
199
205
|
name="execution-order"
|
@@ -216,6 +222,7 @@ export const SSHUserField = ({ value, setValue, defaultValue }) => (
|
|
216
222
|
labelInfo={<ResetDefault setValue={setValue} defaultValue={defaultValue} />}
|
217
223
|
>
|
218
224
|
<TextInput
|
225
|
+
ouiaId="ssh-user"
|
219
226
|
aria-label="ssh user"
|
220
227
|
autoComplete="ssh-user"
|
221
228
|
id="ssh-user"
|
@@ -71,7 +71,12 @@ export const CategoryAndTemplate = ({
|
|
71
71
|
return (
|
72
72
|
<>
|
73
73
|
<WizardTitle title={WIZARD_TITLES.categoryAndTemplate} />
|
74
|
-
<Text
|
74
|
+
<Text
|
75
|
+
ouiaId="category-and-template-required-fields"
|
76
|
+
component={TextVariants.p}
|
77
|
+
>
|
78
|
+
{__('All fields are required.')}
|
79
|
+
</Text>
|
75
80
|
<Form>
|
76
81
|
<SelectField
|
77
82
|
label={__('Job category')}
|
@@ -95,7 +100,11 @@ export const CategoryAndTemplate = ({
|
|
95
100
|
placeholderText={allTemplatesError ? __('Not available') : ''}
|
96
101
|
/>
|
97
102
|
{!isEmpty(missingPermissions) && (
|
98
|
-
<Alert
|
103
|
+
<Alert
|
104
|
+
ouiaId="category-and-template-access-denied"
|
105
|
+
variant="warning"
|
106
|
+
title={__('Access denied')}
|
107
|
+
>
|
99
108
|
<span>
|
100
109
|
{__(
|
101
110
|
`Missing the required permissions: ${missingPermissions.join(
|
@@ -106,7 +115,11 @@ export const CategoryAndTemplate = ({
|
|
106
115
|
</Alert>
|
107
116
|
)}
|
108
117
|
{isError && (
|
109
|
-
<Alert
|
118
|
+
<Alert
|
119
|
+
variant="danger"
|
120
|
+
title={__('Errors:')}
|
121
|
+
ouiaId="category-and-template-errors"
|
122
|
+
>
|
110
123
|
{categoryError && isEmpty(missingPermissions) && (
|
111
124
|
<span>
|
112
125
|
{__('Categories list failed with:')} {categoryError}
|