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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc +4 -1
  3. data/.github/workflows/release.yml +4 -2
  4. data/app/assets/javascripts/foreman_remote_execution/locale/de/foreman_remote_execution.js +1000 -973
  5. data/app/assets/javascripts/foreman_remote_execution/locale/en/foreman_remote_execution.js +553 -526
  6. data/app/assets/javascripts/foreman_remote_execution/locale/en_GB/foreman_remote_execution.js +584 -557
  7. data/app/assets/javascripts/foreman_remote_execution/locale/es/foreman_remote_execution.js +1043 -1016
  8. data/app/assets/javascripts/foreman_remote_execution/locale/fr/foreman_remote_execution.js +1047 -1020
  9. data/app/assets/javascripts/foreman_remote_execution/locale/ja/foreman_remote_execution.js +1041 -1014
  10. data/app/assets/javascripts/foreman_remote_execution/locale/ka/foreman_remote_execution.js +1015 -988
  11. data/app/assets/javascripts/foreman_remote_execution/locale/ko/foreman_remote_execution.js +886 -859
  12. data/app/assets/javascripts/foreman_remote_execution/locale/pt_BR/foreman_remote_execution.js +1047 -1020
  13. data/app/assets/javascripts/foreman_remote_execution/locale/ru/foreman_remote_execution.js +901 -874
  14. data/app/assets/javascripts/foreman_remote_execution/locale/zh_CN/foreman_remote_execution.js +1041 -1014
  15. data/app/assets/javascripts/foreman_remote_execution/locale/zh_TW/foreman_remote_execution.js +886 -859
  16. data/app/lib/actions/remote_execution/run_host_job.rb +0 -14
  17. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
  18. data/app/models/job_invocation_composer.rb +4 -3
  19. data/app/views/api/v2/job_invocations/base.json.rabl +5 -3
  20. data/app/views/templates/script/convert2rhel_analyze.erb +1 -12
  21. data/app/views/templates/script/package_action.erb +15 -2
  22. data/app/views/templates/script/puppet_run_once.erb +3 -3
  23. data/lib/foreman_remote_execution/version.rb +1 -1
  24. data/locale/de/foreman_remote_execution.po +27 -0
  25. data/locale/en/foreman_remote_execution.po +27 -0
  26. data/locale/en_GB/foreman_remote_execution.po +27 -0
  27. data/locale/es/foreman_remote_execution.po +27 -0
  28. data/locale/foreman_remote_execution.pot +177 -141
  29. data/locale/fr/foreman_remote_execution.po +27 -0
  30. data/locale/ja/foreman_remote_execution.po +27 -0
  31. data/locale/ka/foreman_remote_execution.po +27 -0
  32. data/locale/ko/foreman_remote_execution.po +27 -0
  33. data/locale/pt_BR/foreman_remote_execution.po +27 -0
  34. data/locale/ru/foreman_remote_execution.po +27 -0
  35. data/locale/zh_CN/foreman_remote_execution.po +27 -0
  36. data/locale/zh_TW/foreman_remote_execution.po +27 -0
  37. data/package.json +6 -6
  38. data/test/unit/job_invocation_composer_test.rb +31 -3
  39. data/webpack/JobInvocationDetail/JobInvocationConstants.js +10 -0
  40. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +38 -0
  41. data/webpack/JobInvocationDetail/JobInvocationOverview.js +13 -25
  42. data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +153 -0
  43. data/webpack/JobInvocationDetail/index.js +48 -10
  44. data/webpack/JobWizard/Footer.js +5 -1
  45. data/webpack/JobWizard/JobWizardPageRerun.js +4 -1
  46. data/webpack/JobWizard/StartsBeforeErrorAlert.js +1 -0
  47. data/webpack/JobWizard/index.js +1 -0
  48. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +8 -1
  49. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +7 -0
  50. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +16 -3
  51. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +11 -5
  52. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +8 -1
  53. data/webpack/JobWizard/steps/HostsAndInputs/index.js +7 -2
  54. data/webpack/JobWizard/steps/ReviewDetails/index.js +4 -0
  55. data/webpack/JobWizard/steps/Schedule/PurposeField.js +1 -0
  56. data/webpack/JobWizard/steps/Schedule/QueryType.js +2 -0
  57. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +1 -0
  58. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +2 -0
  59. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +1 -0
  60. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +1 -0
  61. data/webpack/JobWizard/steps/Schedule/ScheduleFuture.js +2 -0
  62. data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +6 -0
  63. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +3 -0
  64. data/webpack/JobWizard/steps/form/FormHelpers.js +1 -0
  65. data/webpack/JobWizard/steps/form/GroupedSelectField.js +1 -0
  66. data/webpack/JobWizard/steps/form/NumberInput.js +1 -0
  67. data/webpack/JobWizard/steps/form/ResourceSelect.js +1 -0
  68. data/webpack/JobWizard/steps/form/SearchSelect.js +1 -0
  69. data/webpack/JobWizard/steps/form/SelectField.js +1 -0
  70. data/webpack/JobWizard/steps/form/WizardTitle.js +6 -1
  71. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  72. data/webpack/__mocks__/foremanReact/routes/Hosts/constants.js +1 -0
  73. data/webpack/react_app/components/FeaturesDropdown/index.js +1 -0
  74. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +3 -0
  75. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +2 -1
  76. data/webpack/react_app/components/RegistrationExtension/RexInterface.js +1 -0
  77. data/webpack/react_app/components/RegistrationExtension/RexPull.js +1 -0
  78. data/webpack/react_app/components/RegistrationExtension/__tests__/__snapshots__/RexInterface.test.js.snap +1 -0
  79. 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 => trying_job_template_1.job_category,
658
- :job_template_id => trying_job_template_1.id,
659
+ { :job_category => trying_job_template_5.job_category,
660
+ :job_template_id => trying_job_template_5.id,
659
661
  :targeting_type => 'static_query',
660
662
  :search_query => 'some hosts',
661
- :inputs => {input1.name => 'some_value'}}
663
+ :inputs => {input5.name => 'some_value'}}
662
664
  end
663
665
 
664
666
  it 'finds the inputs by name' do
667
+ assert composer.save!
668
+ values = composer.pattern_template_invocations.first.input_values
669
+ assert_equal 1, values.count
670
+ assert_equal 'some_value', values.first.value
671
+ end
672
+
673
+ it 'can be forced to be empty' do
674
+ params[:inputs] = {input5.name => ''}
675
+ assert composer.save!
676
+ values = composer.pattern_template_invocations.first.input_values
677
+ assert_equal 1, values.count
678
+ assert_equal '', values.first.value
679
+ end
680
+ end
681
+
682
+ context 'with inputs and default values' do
683
+ let(:params) do
684
+ { :job_category => trying_job_template_5.job_category,
685
+ :job_template_id => trying_job_template_5.id,
686
+ :targeting_type => 'static_query',
687
+ :search_query => 'some hosts',
688
+ :inputs => {}}
689
+ end
690
+
691
+ it 'uses the default input values' do
692
+ input5 # Force the factory to be materialized
665
693
  assert composer.save!
666
694
  assert_equal 1, composer.pattern_template_invocations.first.input_values.collect.count
667
695
  end
@@ -5,3 +5,13 @@ export const STATUS = {
5
5
  SUCCEEDED: 'succeeded',
6
6
  FAILED: 'failed',
7
7
  };
8
+
9
+ export const DATE_OPTIONS = {
10
+ day: 'numeric',
11
+ month: 'short',
12
+ year: 'numeric',
13
+ hour: '2-digit',
14
+ minute: '2-digit',
15
+ hour12: false,
16
+ timeZoneName: 'short',
17
+ };
@@ -0,0 +1,38 @@
1
+ .job-invocation-detail-page-section {
2
+ $chart_size: 105px;
3
+
4
+ .chart-donut {
5
+ height: $chart_size;
6
+ width: $chart_size;
7
+ margin-bottom: 10px;
8
+ }
9
+
10
+ .chart-legend {
11
+ height: $chart_size;
12
+ min-width: 270px;
13
+
14
+ .legend-title {
15
+ font-weight: bold;
16
+ font-size: var(--pf-global--FontSize--sm);
17
+ margin-left: 8px;
18
+ margin-bottom: 0;
19
+ }
20
+
21
+ .pf-c-description-list {
22
+ margin-left: 8px;
23
+ margin-top: 8px;
24
+
25
+ .pf-c-description-list__term .pf-c-description-list__text {
26
+ font-weight: normal;
27
+ }
28
+ }
29
+ }
30
+
31
+ .pf-c-divider {
32
+ max-height: $chart_size;
33
+ }
34
+
35
+ .job-overview {
36
+ height: $chart_size;
37
+ }
38
+ }
@@ -7,12 +7,15 @@ import {
7
7
  DescriptionListGroup,
8
8
  DescriptionListDescription,
9
9
  } from '@patternfly/react-core';
10
+ import { translate as __ } from 'foremanReact/common/I18n';
10
11
  import DefaultLoaderEmptyState from 'foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState';
11
- import { translate as __, documentLocale } from 'foremanReact/common/I18n';
12
12
 
13
- const JobInvocationOverview = ({ data }) => {
13
+ const JobInvocationOverview = ({
14
+ data,
15
+ isAlreadyStarted,
16
+ formattedStartDate,
17
+ }) => {
14
18
  const {
15
- start_at: startAt,
16
19
  ssh_user: sshUser,
17
20
  template_id: templateId,
18
21
  template_name: templateName,
@@ -22,27 +25,6 @@ const JobInvocationOverview = ({ data }) => {
22
25
  const canEditJobTemplates = permissions
23
26
  ? permissions.edit_job_templates
24
27
  : false;
25
- const dateOptions = {
26
- day: 'numeric',
27
- month: 'short',
28
- year: 'numeric',
29
- hour: '2-digit',
30
- minute: '2-digit',
31
- hour12: false,
32
- timeZoneName: 'short',
33
- };
34
- let formattedStartDate = __('Not yet');
35
-
36
- if (startAt) {
37
- // Ensures date string compatibility across browsers
38
- const convertedDate = new Date(startAt.replace(/[-.]/g, '/'));
39
- if (convertedDate.getTime() <= new Date().getTime()) {
40
- formattedStartDate = convertedDate.toLocaleString(
41
- documentLocale(),
42
- dateOptions
43
- );
44
- }
45
- }
46
28
 
47
29
  return (
48
30
  <DescriptionList
@@ -63,7 +45,7 @@ const JobInvocationOverview = ({ data }) => {
63
45
  <DescriptionListGroup>
64
46
  <DescriptionListTerm>{__('Started at:')}</DescriptionListTerm>
65
47
  <DescriptionListDescription>
66
- {formattedStartDate}
48
+ {isAlreadyStarted ? formattedStartDate : __('Not yet')}
67
49
  </DescriptionListDescription>
68
50
  </DescriptionListGroup>
69
51
  <DescriptionListGroup>
@@ -99,6 +81,12 @@ const JobInvocationOverview = ({ data }) => {
99
81
 
100
82
  JobInvocationOverview.propTypes = {
101
83
  data: PropTypes.object.isRequired,
84
+ isAlreadyStarted: PropTypes.bool.isRequired,
85
+ formattedStartDate: PropTypes.string,
86
+ };
87
+
88
+ JobInvocationOverview.defaultProps = {
89
+ formattedStartDate: undefined,
102
90
  };
103
91
 
104
92
  export default JobInvocationOverview;
@@ -0,0 +1,153 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __, sprintf } from 'foremanReact/common/I18n';
4
+ import {
5
+ ChartDonut,
6
+ ChartLabel,
7
+ ChartLegend,
8
+ ChartTooltip,
9
+ } from '@patternfly/react-charts';
10
+ import {
11
+ DescriptionList,
12
+ DescriptionListTerm,
13
+ DescriptionListGroup,
14
+ DescriptionListDescription,
15
+ FlexItem,
16
+ Text,
17
+ } from '@patternfly/react-core';
18
+ import {
19
+ global_palette_green_500 as successedColor,
20
+ global_palette_red_100 as failedColor,
21
+ global_palette_blue_300 as inProgressColor,
22
+ global_palette_black_600 as canceledColor,
23
+ global_palette_black_500 as emptyChartDonut,
24
+ } from '@patternfly/react-tokens';
25
+ import DefaultLoaderEmptyState from 'foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState';
26
+ import './JobInvocationDetail.scss';
27
+
28
+ const JobInvocationSystemStatusChart = ({
29
+ data,
30
+ isAlreadyStarted,
31
+ formattedStartDate,
32
+ }) => {
33
+ const {
34
+ succeeded,
35
+ failed,
36
+ pending,
37
+ cancelled,
38
+ total,
39
+ total_hosts: totalHosts, // includes scheduled
40
+ } = data;
41
+ const chartData = [
42
+ { title: __('Succeeded:'), count: succeeded, color: successedColor.value },
43
+ { title: __('Failed:'), count: failed, color: failedColor.value },
44
+ { title: __('In Progress:'), count: pending, color: inProgressColor.value },
45
+ { title: __('Canceled:'), count: cancelled, color: canceledColor.value },
46
+ ];
47
+ const chartDonutTitle = () => {
48
+ if (total > 0) return `${succeeded.toString()}/${total}`;
49
+ if (totalHosts > 0) return `0/${totalHosts}`;
50
+ return '0';
51
+ };
52
+ const chartSize = 105;
53
+ const [legendWidth, setLegendWidth] = useState(270);
54
+
55
+ // Calculates chart legend width based on its content
56
+ useEffect(() => {
57
+ const legendContainer = document.querySelector('.chart-legend');
58
+ if (legendContainer) {
59
+ const rectElement = legendContainer.querySelector('rect');
60
+ if (rectElement) {
61
+ const rectWidth = parseFloat(rectElement.getAttribute('width'));
62
+ setLegendWidth(rectWidth);
63
+ }
64
+ }
65
+ }, [isAlreadyStarted, data]);
66
+
67
+ return (
68
+ <>
69
+ <FlexItem className="chart-donut">
70
+ <ChartDonut
71
+ allowTooltip
72
+ constrainToVisibleArea
73
+ data={
74
+ total > 0
75
+ ? chartData.map(d => ({
76
+ label: sprintf(__(`${d.title} ${d.count} hosts`)),
77
+ y: d.count,
78
+ }))
79
+ : [{ label: sprintf(__(`Scheduled: ${totalHosts} hosts`)), y: 1 }]
80
+ }
81
+ colorScale={
82
+ total > 0 ? chartData.map(d => d.color) : [emptyChartDonut.value]
83
+ }
84
+ labelComponent={
85
+ <ChartTooltip pointerLength={0} constrainToVisibleArea />
86
+ }
87
+ title={chartDonutTitle}
88
+ titleComponent={
89
+ // inline style overrides PatternFly default styling
90
+ <ChartLabel style={{ fontSize: '20px' }} />
91
+ }
92
+ subTitle={__('Systems')}
93
+ subTitleComponent={
94
+ // inline style overrides PatternFly default styling
95
+ <ChartLabel
96
+ style={{ fontSize: '12px', fill: canceledColor.value }}
97
+ />
98
+ }
99
+ padding={{
100
+ bottom: 0,
101
+ left: 0,
102
+ right: 0,
103
+ top: 0,
104
+ }}
105
+ width={chartSize}
106
+ height={chartSize}
107
+ />
108
+ </FlexItem>
109
+ <FlexItem className="chart-legend">
110
+ <Text ouiaId="legend-title" className="legend-title">
111
+ {__('System status')}
112
+ </Text>
113
+ {isAlreadyStarted ? (
114
+ <ChartLegend
115
+ orientation="vertical"
116
+ itemsPerRow={2}
117
+ gutter={25}
118
+ rowGutter={7}
119
+ padding={{ left: 15 }}
120
+ data={chartData.map(d => ({
121
+ name: `${d.title} ${d.count}`,
122
+ symbol: { type: 'circle' },
123
+ }))}
124
+ colorScale={chartData.map(d => d.color)}
125
+ width={legendWidth}
126
+ height={chartSize}
127
+ />
128
+ ) : (
129
+ <DescriptionList>
130
+ <DescriptionListGroup>
131
+ <DescriptionListTerm>{__('Scheduled at:')}</DescriptionListTerm>
132
+ <DescriptionListDescription>
133
+ {formattedStartDate || <DefaultLoaderEmptyState />}
134
+ </DescriptionListDescription>
135
+ </DescriptionListGroup>
136
+ </DescriptionList>
137
+ )}
138
+ </FlexItem>
139
+ </>
140
+ );
141
+ };
142
+
143
+ JobInvocationSystemStatusChart.propTypes = {
144
+ data: PropTypes.object.isRequired,
145
+ isAlreadyStarted: PropTypes.bool.isRequired,
146
+ formattedStartDate: PropTypes.string,
147
+ };
148
+
149
+ JobInvocationSystemStatusChart.defaultProps = {
150
+ formattedStartDate: undefined,
151
+ };
152
+
153
+ export default JobInvocationSystemStatusChart;
@@ -1,14 +1,20 @@
1
1
  import React, { useEffect } from 'react';
2
2
  import { useSelector, useDispatch } from 'react-redux';
3
3
  import PropTypes from 'prop-types';
4
- import { Divider, PageSection, Flex, FlexItem } from '@patternfly/react-core';
5
- import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { Divider, PageSection, Flex } from '@patternfly/react-core';
5
+ import { translate as __, documentLocale } from 'foremanReact/common/I18n';
6
6
  import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
7
7
  import { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
8
8
  import { getData } from './JobInvocationActions';
9
9
  import { selectItems } from './JobInvocationSelectors';
10
10
  import JobInvocationOverview from './JobInvocationOverview';
11
- import { JOB_INVOCATION_KEY, STATUS } from './JobInvocationConstants';
11
+ import JobInvocationSystemStatusChart from './JobInvocationSystemStatusChart';
12
+ import {
13
+ JOB_INVOCATION_KEY,
14
+ STATUS,
15
+ DATE_OPTIONS,
16
+ } from './JobInvocationConstants';
17
+ import './JobInvocationDetail.scss';
12
18
 
13
19
  const JobInvocationDetailPage = ({
14
20
  match: {
@@ -17,11 +23,28 @@ const JobInvocationDetailPage = ({
17
23
  }) => {
18
24
  const dispatch = useDispatch();
19
25
  const items = useSelector(selectItems);
20
- const { description, status_label: statusLabel, task } = items;
26
+ const {
27
+ description,
28
+ status_label: statusLabel,
29
+ task,
30
+ start_at: startAt,
31
+ } = items;
21
32
  const finished =
22
33
  statusLabel === STATUS.FAILED || statusLabel === STATUS.SUCCEEDED;
23
34
  const autoRefresh = task?.state === STATUS.PENDING || false;
24
35
 
36
+ let isAlreadyStarted = false;
37
+ let formattedStartDate;
38
+ if (startAt) {
39
+ // Ensures date string compatibility across browsers
40
+ const convertedDate = new Date(startAt.replace(/[-.]/g, '/'));
41
+ isAlreadyStarted = convertedDate.getTime() <= new Date().getTime();
42
+ formattedStartDate = convertedDate.toLocaleString(
43
+ documentLocale(),
44
+ DATE_OPTIONS
45
+ );
46
+ }
47
+
25
48
  useEffect(() => {
26
49
  dispatch(getData(`/api/job_invocations/${id}`));
27
50
  if (finished && !autoRefresh) {
@@ -48,17 +71,32 @@ const JobInvocationDetailPage = ({
48
71
  searchable={false}
49
72
  >
50
73
  <React.Fragment>
51
- <PageSection isFilled variant="light">
52
- <Flex>
53
- <FlexItem> </FlexItem>
74
+ <PageSection
75
+ className="job-invocation-detail-page-section"
76
+ isFilled
77
+ variant="light"
78
+ >
79
+ <Flex alignItems={{ default: 'alignItemsFlexStart' }}>
80
+ <JobInvocationSystemStatusChart
81
+ data={items}
82
+ isAlreadyStarted={isAlreadyStarted}
83
+ formattedStartDate={formattedStartDate}
84
+ />
54
85
  <Divider
55
86
  orientation={{
56
87
  default: 'vertical',
57
88
  }}
58
89
  />
59
- <FlexItem>
60
- <JobInvocationOverview data={items} />
61
- </FlexItem>
90
+ <Flex
91
+ className="job-overview"
92
+ alignItems={{ default: 'alignItemsCenter' }}
93
+ >
94
+ <JobInvocationOverview
95
+ data={items}
96
+ isAlreadyStarted={isAlreadyStarted}
97
+ formattedStartDate={formattedStartDate}
98
+ />
99
+ </Flex>
62
100
  </Flex>
63
101
  </PageSection>
64
102
  </React.Fragment>
@@ -26,6 +26,7 @@ export const Footer = ({ canSubmit, onSave }) => {
26
26
  return (
27
27
  <>
28
28
  <Button
29
+ ouiaId="next-footer"
29
30
  variant="primary"
30
31
  type="submit"
31
32
  onClick={onNext}
@@ -36,6 +37,7 @@ export const Footer = ({ canSubmit, onSave }) => {
36
37
  : __('Next')}
37
38
  </Button>
38
39
  <Button
40
+ ouiaId="back-footer"
39
41
  variant="secondary"
40
42
  onClick={onBack}
41
43
  isDisabled={
@@ -54,6 +56,7 @@ export const Footer = ({ canSubmit, onSave }) => {
54
56
  }
55
57
  >
56
58
  <Button
59
+ ouiaId="run-on-selected-hosts-footer"
57
60
  variant="tertiary"
58
61
  onClick={onSave}
59
62
  isAriaDisabled={!canSubmit}
@@ -74,6 +77,7 @@ export const Footer = ({ canSubmit, onSave }) => {
74
77
  }
75
78
  >
76
79
  <Button
80
+ ouiaId="skip-to-review-footer"
77
81
  variant="tertiary"
78
82
  onClick={() => goToStepByName(WIZARD_TITLES.review)}
79
83
  isAriaDisabled={!canSubmit}
@@ -82,7 +86,7 @@ export const Footer = ({ canSubmit, onSave }) => {
82
86
  {__('Skip to review')}
83
87
  </Button>
84
88
  </Tooltip>
85
- <Button variant="link" onClick={onClose}>
89
+ <Button ouiaId="cancel-footer" variant="link" onClick={onClose}>
86
90
  {__('Cancel')}
87
91
  </Button>
88
92
  {isSubmitting && (
@@ -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 old form')}
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"
@@ -5,6 +5,7 @@ import { translate as __ } from 'foremanReact/common/I18n';
5
5
  export const StartsBeforeErrorAlert = () => (
6
6
  <>
7
7
  <Alert
8
+ ouiaId="starts-before-error-alert"
8
9
  variant="danger"
9
10
  title={__("'Starts before' date must in the future")}
10
11
  >
@@ -59,6 +59,7 @@ const JobWizardPage = ({ location: { search } }) => {
59
59
  searchable={false}
60
60
  toolbarButtons={
61
61
  <Button
62
+ ouiaId="legacy-form"
62
63
  variant="link"
63
64
  component="a"
64
65
  href={`/old/job_invocations/new${search}`}
@@ -56,7 +56,12 @@ export const DescriptionField = ({
56
56
  }
57
57
  fieldId="description"
58
58
  helperText={
59
- <Button variant="link" isInline onClick={togglePreview}>
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 component={TextVariants.p}>{__('All fields are required.')}</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 variant="warning" title={__('Access denied')}>
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 variant="danger" title={__('Errors:')}>
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}