foreman_remote_execution 12.0.5 → 13.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.eslintrc +4 -1
- data/.github/workflows/js_ci.yml +1 -1
- data/.github/workflows/release.yml +4 -2
- data/.github/workflows/ruby_ci.yml +16 -81
- data/.packit.yaml +8 -3
- data/app/assets/javascripts/foreman_remote_execution/locale/de/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/en/foreman_remote_execution.js +22 -4
- data/app/assets/javascripts/foreman_remote_execution/locale/en_GB/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/es/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/fr/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/ja/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/ka/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/ko/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/pt_BR/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/ru/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/zh_CN/foreman_remote_execution.js +23 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/zh_TW/foreman_remote_execution.js +23 -14
- data/app/controllers/ui_job_wizard_controller.rb +1 -1
- data/app/helpers/job_invocations_helper.rb +1 -1
- data/app/helpers/remote_execution_helper.rb +2 -2
- data/app/lib/actions/remote_execution/proxy_action.rb +1 -1
- data/app/lib/actions/remote_execution/run_host_job.rb +9 -17
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
- data/app/models/host_status/execution_status.rb +2 -2
- data/app/models/job_invocation_composer.rb +4 -3
- data/app/views/api/v2/job_invocations/base.json.rabl +5 -3
- data/app/views/job_invocations/_preview_hosts_list.html.erb +1 -1
- data/app/views/job_invocations/show.html.erb +12 -5
- data/app/views/job_invocations/show.js.erb +8 -1
- data/app/views/job_invocations/welcome.html.erb +1 -1
- data/app/views/template_invocations/_refresh.js.erb +10 -4
- data/app/views/template_invocations/show.html.erb +2 -2
- 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/engine.rb +1 -1
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/locale/de/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/de/foreman_remote_execution.po +24 -6
- data/locale/en/foreman_remote_execution.po +24 -6
- data/locale/en_GB/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/en_GB/foreman_remote_execution.po +24 -6
- data/locale/es/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/es/foreman_remote_execution.po +24 -6
- data/locale/foreman_remote_execution.pot +170 -142
- data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/fr/foreman_remote_execution.po +24 -6
- data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ja/foreman_remote_execution.po +24 -6
- data/locale/ka/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ka/foreman_remote_execution.po +24 -6
- data/locale/ko/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ko/foreman_remote_execution.po +24 -6
- data/locale/pt_BR/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/pt_BR/foreman_remote_execution.po +24 -6
- data/locale/ru/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ru/foreman_remote_execution.po +24 -6
- data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/zh_CN/foreman_remote_execution.po +24 -6
- data/locale/zh_TW/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/zh_TW/foreman_remote_execution.po +24 -6
- data/package.json +7 -11
- data/test/functional/api/v2/job_invocations_controller_test.rb +7 -7
- data/test/functional/api/v2/template_invocations_controller_test.rb +3 -3
- data/test/helpers/remote_execution_helper_test.rb +8 -7
- data/test/unit/actions/run_host_job_test.rb +1 -1
- data/test/unit/actions/run_hosts_job_test.rb +11 -11
- data/test/unit/concerns/foreman_tasks_cleaner_extensions_test.rb +5 -5
- data/test/unit/concerns/host_extensions_test.rb +34 -34
- data/test/unit/concerns/nic_extensions_test.rb +1 -1
- data/test/unit/execution_task_status_mapper_test.rb +10 -10
- data/test/unit/input_template_renderer_test.rb +53 -49
- data/test/unit/job_invocation_composer_test.rb +109 -81
- data/test/unit/job_invocation_test.rb +25 -25
- data/test/unit/job_template_effective_user_test.rb +3 -3
- data/test/unit/job_template_test.rb +28 -28
- data/test/unit/remote_execution_feature_test.rb +14 -14
- data/test/unit/remote_execution_provider_test.rb +39 -39
- data/test/unit/renderer_scope_input_test.rb +6 -6
- data/test/unit/targeting_test.rb +32 -32
- 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/JobWizardConstants.js +4 -0
- data/webpack/JobWizard/JobWizardPageRerun.js +3 -0
- data/webpack/JobWizard/JobWizardSelectors.js +31 -3
- data/webpack/JobWizard/PermissionDenied.js +64 -0
- data/webpack/JobWizard/StartsBeforeErrorAlert.js +1 -0
- data/webpack/JobWizard/__tests__/fixtures.js +3 -3
- data/webpack/JobWizard/autofill.js +8 -4
- data/webpack/JobWizard/index.js +41 -1
- 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 +41 -7
- data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +7 -3
- data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +1 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +21 -7
- data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +1 -0
- data/webpack/JobWizard/steps/HostsAndInputs/index.js +27 -2
- data/webpack/JobWizard/steps/ReviewDetails/index.js +7 -2
- 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 +4 -1
- data/webpack/JobWizard/steps/form/SelectField.js +1 -0
- data/webpack/JobWizard/steps/form/WizardTitle.js +6 -1
- data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.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 +10 -6
@@ -13,15 +13,15 @@ class RendererScopeInputTest < ActiveSupport::TestCase
|
|
13
13
|
it 'caches the value under given key' do
|
14
14
|
i = 1
|
15
15
|
result = input.cached('some_key') { i }
|
16
|
-
|
16
|
+
assert_equal 1, result
|
17
17
|
|
18
18
|
i += 1
|
19
19
|
result = input.cached('some_key') { i }
|
20
|
-
|
20
|
+
assert_equal 1, result
|
21
21
|
|
22
22
|
i += 1
|
23
23
|
result = input.cached('different_key') { i }
|
24
|
-
|
24
|
+
assert_equal 3, result
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
@@ -35,15 +35,15 @@ class RendererScopeInputTest < ActiveSupport::TestCase
|
|
35
35
|
it 'does not cache the value' do
|
36
36
|
i = 1
|
37
37
|
result = input.cached('some_key') { i }
|
38
|
-
|
38
|
+
assert_equal 1, result
|
39
39
|
|
40
40
|
i += 1
|
41
41
|
result = input.cached('some_key') { i }
|
42
|
-
|
42
|
+
assert_equal 2, result
|
43
43
|
|
44
44
|
i += 1
|
45
45
|
result = input.cached('different_key') { i }
|
46
|
-
|
46
|
+
assert_equal 3, result
|
47
47
|
end
|
48
48
|
end
|
49
49
|
end
|
data/test/unit/targeting_test.rb
CHANGED
@@ -51,7 +51,7 @@ class TargetingTest < ActiveSupport::TestCase
|
|
51
51
|
targeting.resolve_hosts!
|
52
52
|
end
|
53
53
|
|
54
|
-
it {
|
54
|
+
it { assert_includes targeting.hosts, host }
|
55
55
|
end
|
56
56
|
|
57
57
|
context 'can delete a user' do
|
@@ -63,7 +63,7 @@ class TargetingTest < ActiveSupport::TestCase
|
|
63
63
|
|
64
64
|
it { assert_nil targeting.reload.user }
|
65
65
|
it do
|
66
|
-
|
66
|
+
assert_raises(Foreman::Exception) { targeting.resolve_hosts! }
|
67
67
|
end
|
68
68
|
end
|
69
69
|
|
@@ -74,7 +74,7 @@ class TargetingTest < ActiveSupport::TestCase
|
|
74
74
|
host.destroy
|
75
75
|
end
|
76
76
|
|
77
|
-
it {
|
77
|
+
it { assert_empty targeting.reload.hosts }
|
78
78
|
end
|
79
79
|
|
80
80
|
describe '#resolve_hosts!' do
|
@@ -100,9 +100,9 @@ class TargetingTest < ActiveSupport::TestCase
|
|
100
100
|
targeting.user = User.current
|
101
101
|
targeting.resolve_hosts!
|
102
102
|
|
103
|
-
targeting.hosts
|
104
|
-
targeting.hosts
|
105
|
-
targeting.hosts
|
103
|
+
assert_includes targeting.hosts, host
|
104
|
+
assert_includes targeting.hosts, second_host
|
105
|
+
assert_includes targeting.hosts, infra_host
|
106
106
|
end
|
107
107
|
end
|
108
108
|
|
@@ -115,9 +115,9 @@ class TargetingTest < ActiveSupport::TestCase
|
|
115
115
|
targeting.user = User.current
|
116
116
|
targeting.resolve_hosts!
|
117
117
|
|
118
|
-
targeting.hosts
|
119
|
-
targeting.hosts
|
120
|
-
targeting.hosts
|
118
|
+
assert_includes targeting.hosts, host
|
119
|
+
assert_includes targeting.hosts, second_host
|
120
|
+
refute_includes targeting.hosts, infra_host
|
121
121
|
end
|
122
122
|
end
|
123
123
|
end
|
@@ -136,14 +136,14 @@ class TargetingTest < ActiveSupport::TestCase
|
|
136
136
|
let(:query) { Targeting.build_query_from_hosts([ host.id, second_host.id, infra_host.id ]) }
|
137
137
|
|
138
138
|
it 'builds query using host names joining inside ^' do
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
Host.search_for(query)
|
145
|
-
Host.search_for(query)
|
146
|
-
Host.search_for(query)
|
139
|
+
assert_includes query, host.name
|
140
|
+
assert_includes query, second_host.name
|
141
|
+
assert_includes query, infra_host.name
|
142
|
+
assert_includes query, 'name ^'
|
143
|
+
|
144
|
+
assert_includes Host.search_for(query), host
|
145
|
+
assert_includes Host.search_for(query), second_host
|
146
|
+
assert_includes Host.search_for(query), infra_host
|
147
147
|
end
|
148
148
|
end
|
149
149
|
|
@@ -151,10 +151,10 @@ class TargetingTest < ActiveSupport::TestCase
|
|
151
151
|
let(:query) { Targeting.build_query_from_hosts([ host.id ]) }
|
152
152
|
|
153
153
|
it 'builds query using host name' do
|
154
|
-
|
155
|
-
Host.search_for(query)
|
156
|
-
Host.search_for(query)
|
157
|
-
Host.search_for(query)
|
154
|
+
assert_equal "name ^ (#{host.name})", query
|
155
|
+
assert_includes Host.search_for(query), host
|
156
|
+
refute_includes Host.search_for(query), second_host
|
157
|
+
refute_includes Host.search_for(query), infra_host
|
158
158
|
end
|
159
159
|
end
|
160
160
|
|
@@ -162,9 +162,9 @@ class TargetingTest < ActiveSupport::TestCase
|
|
162
162
|
let(:query) { Targeting.build_query_from_hosts([]) }
|
163
163
|
|
164
164
|
it 'builds query to find all hosts' do
|
165
|
-
Host.search_for(query)
|
166
|
-
Host.search_for(query)
|
167
|
-
Host.search_for(query)
|
165
|
+
assert_includes Host.search_for(query), host
|
166
|
+
assert_includes Host.search_for(query), second_host
|
167
|
+
assert_includes Host.search_for(query), infra_host
|
168
168
|
end
|
169
169
|
end
|
170
170
|
|
@@ -173,14 +173,14 @@ class TargetingTest < ActiveSupport::TestCase
|
|
173
173
|
|
174
174
|
it 'ignores the infrastructure host' do
|
175
175
|
query = Targeting.build_query_from_hosts([host.id, second_host.id, infra_host.id])
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
Host.search_for(query)
|
182
|
-
Host.search_for(query)
|
183
|
-
Host.search_for(query)
|
176
|
+
assert_includes query, host.name
|
177
|
+
assert_includes query, second_host.name
|
178
|
+
refute_includes query, infra_host.name
|
179
|
+
assert_includes query, 'name ^'
|
180
|
+
|
181
|
+
assert_includes Host.search_for(query), host
|
182
|
+
assert_includes Host.search_for(query), second_host
|
183
|
+
refute_includes Host.search_for(query), infra_host
|
184
184
|
end
|
185
185
|
end
|
186
186
|
end
|
@@ -5,3 +5,13 @@ export const STATUS = {
|
|
5
5
|
SUCCEEDED: 'succeeded',
|
6
6
|
FAILED: 'failed',
|
7
7
|
};
|
8
|
+
|
9
|
+
export const DATE_OPTIONS = {
|
10
|
+
day: 'numeric',
|
11
|
+
month: 'short',
|
12
|
+
year: 'numeric',
|
13
|
+
hour: '2-digit',
|
14
|
+
minute: '2-digit',
|
15
|
+
hour12: false,
|
16
|
+
timeZoneName: 'short',
|
17
|
+
};
|
@@ -0,0 +1,38 @@
|
|
1
|
+
.job-invocation-detail-page-section {
|
2
|
+
$chart_size: 105px;
|
3
|
+
|
4
|
+
.chart-donut {
|
5
|
+
height: $chart_size;
|
6
|
+
width: $chart_size;
|
7
|
+
margin-bottom: 10px;
|
8
|
+
}
|
9
|
+
|
10
|
+
.chart-legend {
|
11
|
+
height: $chart_size;
|
12
|
+
min-width: 270px;
|
13
|
+
|
14
|
+
.legend-title {
|
15
|
+
font-weight: bold;
|
16
|
+
font-size: var(--pf-global--FontSize--sm);
|
17
|
+
margin-left: 8px;
|
18
|
+
margin-bottom: 0;
|
19
|
+
}
|
20
|
+
|
21
|
+
.pf-c-description-list {
|
22
|
+
margin-left: 8px;
|
23
|
+
margin-top: 8px;
|
24
|
+
|
25
|
+
.pf-c-description-list__term .pf-c-description-list__text {
|
26
|
+
font-weight: normal;
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
.pf-c-divider {
|
32
|
+
max-height: $chart_size;
|
33
|
+
}
|
34
|
+
|
35
|
+
.job-overview {
|
36
|
+
height: $chart_size;
|
37
|
+
}
|
38
|
+
}
|
@@ -7,12 +7,15 @@ import {
|
|
7
7
|
DescriptionListGroup,
|
8
8
|
DescriptionListDescription,
|
9
9
|
} from '@patternfly/react-core';
|
10
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
10
11
|
import DefaultLoaderEmptyState from 'foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState';
|
11
|
-
import { translate as __, documentLocale } from 'foremanReact/common/I18n';
|
12
12
|
|
13
|
-
const JobInvocationOverview = ({
|
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 && (
|
@@ -5,6 +5,10 @@ export const JOB_TEMPLATES = 'JOB_TEMPLATES';
|
|
5
5
|
export const JOB_CATEGORIES = 'JOB_CATEGORIES';
|
6
6
|
export const JOB_TEMPLATE = 'JOB_TEMPLATE';
|
7
7
|
export const JOB_INVOCATION = 'JOB_INVOCATION';
|
8
|
+
export const CURRENT_PERMISSIONS = 'CURRENT_PERMISSIONS';
|
9
|
+
export const currentPermissionsUrl = foremanUrl(
|
10
|
+
'/api/v2/permissions/current_permissions'
|
11
|
+
);
|
8
12
|
export const templatesUrl = foremanUrl('/api/v2/job_templates');
|
9
13
|
|
10
14
|
export const repeatTypes = {
|
@@ -48,6 +48,7 @@ const JobWizardPageRerun = ({
|
|
48
48
|
searchable={false}
|
49
49
|
toolbarButtons={
|
50
50
|
<Button
|
51
|
+
ouiaId="job-wizard-rerun-old-form-button"
|
51
52
|
variant="link"
|
52
53
|
component="a"
|
53
54
|
href={`/old/job_invocations/${id}/rerun${search}`}
|
@@ -71,6 +72,7 @@ const JobWizardPageRerun = ({
|
|
71
72
|
<React.Fragment>
|
72
73
|
{jobOrganization?.id !== currentOrganization?.id && (
|
73
74
|
<Alert
|
75
|
+
ouiaId="job-wizard-alert-organization"
|
74
76
|
isInline
|
75
77
|
className="job-wizard-alert"
|
76
78
|
variant="warning"
|
@@ -85,6 +87,7 @@ const JobWizardPageRerun = ({
|
|
85
87
|
)}
|
86
88
|
{jobLocation?.id !== currentLocation?.id && (
|
87
89
|
<Alert
|
90
|
+
ouiaId="job-wizard-alert-location"
|
88
91
|
isInline
|
89
92
|
className="job-wizard-alert"
|
90
93
|
variant="warning"
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import URI from 'urijs';
|
2
|
+
import { get } from 'lodash';
|
2
3
|
import {
|
3
4
|
selectAPIResponse,
|
4
5
|
selectAPIStatus,
|
@@ -42,6 +43,18 @@ export const selectJobCategoriesStatus = state =>
|
|
42
43
|
export const selectCategoryError = state =>
|
43
44
|
selectAPIErrorMessage(state, JOB_CATEGORIES);
|
44
45
|
|
46
|
+
export const selectJobCategoriesMissingPermissions = state => {
|
47
|
+
const jobCategoriesResponse = selectJobCategoriesResponse(state);
|
48
|
+
return (
|
49
|
+
get(jobCategoriesResponse, [
|
50
|
+
'response',
|
51
|
+
'data',
|
52
|
+
'error',
|
53
|
+
'missing_permissions',
|
54
|
+
]) || []
|
55
|
+
);
|
56
|
+
};
|
57
|
+
|
45
58
|
export const selectAllTemplatesError = state =>
|
46
59
|
selectAPIErrorMessage(state, JOB_TEMPLATES);
|
47
60
|
|
@@ -60,11 +73,26 @@ export const selectAdvancedTemplateInputs = state =>
|
|
60
73
|
export const selectTemplateInputs = state =>
|
61
74
|
selectAPIResponse(state, JOB_TEMPLATE).template_inputs || [];
|
62
75
|
|
76
|
+
export const selectHostsResponse = state => selectAPIResponse(state, HOSTS_API);
|
77
|
+
|
63
78
|
export const selectHostCount = state =>
|
64
|
-
|
79
|
+
selectHostsResponse(state).subtotal || 0;
|
80
|
+
|
81
|
+
export const selectHosts = state => {
|
82
|
+
const hosts = selectHostsResponse(state).results || [];
|
83
|
+
return hosts.map(host => ({
|
84
|
+
name: host.name,
|
85
|
+
display_name: host.display_name,
|
86
|
+
}));
|
87
|
+
};
|
65
88
|
|
66
|
-
export const
|
67
|
-
|
89
|
+
export const selectHostsMissingPermissions = state => {
|
90
|
+
const hostsResponse = selectHostsResponse(state);
|
91
|
+
return (
|
92
|
+
get(hostsResponse, ['response', 'data', 'error', 'missing_permissions']) ||
|
93
|
+
[]
|
94
|
+
);
|
95
|
+
};
|
68
96
|
|
69
97
|
export const selectIsLoadingHosts = state =>
|
70
98
|
!selectAPIStatus(state, HOSTS_API) ||
|