foreman_remote_execution 4.6.0 → 5.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_ci.yml +7 -0
  3. data/.rubocop_todo.yml +1 -0
  4. data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
  5. data/app/controllers/job_invocations_controller.rb +1 -1
  6. data/app/controllers/ui_job_wizard_controller.rb +21 -2
  7. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  8. data/app/graphql/types/job_invocation.rb +16 -0
  9. data/app/graphql/types/job_invocation_input.rb +13 -0
  10. data/app/graphql/types/recurrence_input.rb +8 -0
  11. data/app/graphql/types/scheduling_input.rb +6 -0
  12. data/app/graphql/types/targeting_enum.rb +7 -0
  13. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +5 -1
  14. data/app/helpers/remote_execution_helper.rb +9 -3
  15. data/app/lib/actions/remote_execution/run_host_job.rb +10 -1
  16. data/app/lib/actions/remote_execution/run_hosts_job.rb +58 -4
  17. data/app/mailers/rex_job_mailer.rb +15 -0
  18. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +10 -0
  19. data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +6 -0
  20. data/app/models/host_proxy_invocation.rb +4 -0
  21. data/app/models/host_status/execution_status.rb +3 -3
  22. data/app/models/job_invocation.rb +12 -5
  23. data/app/models/job_invocation_composer.rb +25 -17
  24. data/app/models/job_template.rb +1 -1
  25. data/app/models/remote_execution_feature.rb +5 -1
  26. data/app/models/remote_execution_provider.rb +18 -2
  27. data/app/models/rex_mail_notification.rb +13 -0
  28. data/app/models/targeting.rb +7 -3
  29. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  30. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  31. data/app/views/job_invocations/index.html.erb +1 -1
  32. data/app/views/job_invocations/refresh.js.erb +1 -0
  33. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  34. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  35. data/app/views/template_invocations/show.html.erb +2 -1
  36. data/app/views/templates/ssh/module_action.erb +1 -0
  37. data/app/views/templates/ssh/power_action.erb +2 -0
  38. data/app/views/templates/ssh/puppet_run_once.erb +1 -0
  39. data/config/routes.rb +1 -0
  40. data/db/migrate/2021051713291621250977_add_host_proxy_invocations.rb +12 -0
  41. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  42. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  43. data/db/seeds.d/95-mail_notifications.rb +24 -0
  44. data/foreman_remote_execution.gemspec +2 -3
  45. data/lib/foreman_remote_execution/engine.rb +114 -8
  46. data/lib/foreman_remote_execution/version.rb +1 -1
  47. data/package.json +9 -7
  48. data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
  49. data/test/functional/cockpit_controller_test.rb +0 -1
  50. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  51. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  52. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  53. data/test/helpers/remote_execution_helper_test.rb +0 -1
  54. data/test/unit/actions/run_host_job_test.rb +21 -0
  55. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  56. data/test/unit/concerns/host_extensions_test.rb +40 -7
  57. data/test/unit/input_template_renderer_test.rb +1 -89
  58. data/test/unit/job_invocation_composer_test.rb +18 -18
  59. data/test/unit/job_invocation_report_template_test.rb +16 -13
  60. data/test/unit/job_invocation_test.rb +1 -1
  61. data/test/unit/job_template_effective_user_test.rb +0 -4
  62. data/test/unit/remote_execution_provider_test.rb +46 -4
  63. data/test/unit/targeting_test.rb +68 -1
  64. data/webpack/JobWizard/JobWizard.js +158 -24
  65. data/webpack/JobWizard/JobWizard.scss +93 -1
  66. data/webpack/JobWizard/JobWizardConstants.js +54 -0
  67. data/webpack/JobWizard/JobWizardSelectors.js +41 -0
  68. data/webpack/JobWizard/__tests__/fixtures.js +188 -3
  69. data/webpack/JobWizard/__tests__/integration.test.js +41 -106
  70. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  71. data/webpack/JobWizard/autofill.js +38 -0
  72. data/webpack/JobWizard/index.js +7 -0
  73. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +41 -10
  74. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +90 -0
  75. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +116 -55
  76. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +354 -16
  77. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +79 -246
  78. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
  79. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +123 -51
  80. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  81. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  82. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  83. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  84. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  85. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +100 -0
  86. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  87. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  88. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +53 -0
  89. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  90. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  91. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  92. data/webpack/JobWizard/steps/HostsAndInputs/index.js +214 -0
  93. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  94. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  95. data/webpack/JobWizard/steps/Schedule/QueryType.js +51 -0
  96. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  97. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  98. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  99. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  100. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +125 -0
  101. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  102. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +28 -0
  103. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +106 -0
  104. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
  105. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +32 -0
  106. data/webpack/JobWizard/steps/Schedule/index.js +178 -0
  107. data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
  108. data/webpack/JobWizard/steps/form/FormHelpers.js +5 -0
  109. data/webpack/JobWizard/steps/form/Formatter.js +181 -0
  110. data/webpack/JobWizard/steps/form/NumberInput.js +36 -0
  111. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  112. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  113. data/webpack/JobWizard/steps/form/SelectField.js +28 -5
  114. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  115. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  116. data/webpack/JobWizard/submit.js +120 -0
  117. data/webpack/JobWizard/validation.js +53 -0
  118. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  119. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  120. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  121. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  122. data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
  123. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  124. data/webpack/helpers.js +1 -0
  125. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
  126. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
  127. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
  128. data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
  129. data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
  130. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
  131. data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
  132. metadata +71 -16
  133. data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +0 -70
  134. data/app/models/setting/remote_execution.rb +0 -88
  135. data/test/models/orchestration/ssh_test.rb +0 -56
  136. data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -13
  137. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -32
  138. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -113
  139. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
  140. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
  141. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -37
  142. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +0 -23
  143. data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
@@ -1,249 +1,82 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
- exports[`AdvancedFields rendring 1`] = `
4
- <Provider
5
- store={
6
- Object {
7
- "clearActions": [Function],
8
- "dispatch": [Function],
9
- "getActions": [Function],
10
- "getState": [Function],
11
- "replaceReducer": [Function],
12
- "subscribe": [Function],
13
- }
14
- }
15
- >
16
- <AdvancedFields
17
- advancedValues={Object {}}
18
- setAdvancedValues={[MockFunction]}
19
- >
20
- <Title
21
- className="advanced-fields-title"
22
- headingLevel="h2"
23
- >
24
- <h2
25
- className="pf-c-title pf-m-xl advanced-fields-title"
26
- >
27
- Advanced Fields
28
- </h2>
29
- </Title>
30
- <Form>
31
- <form
32
- className="pf-c-form"
33
- noValidate={true}
34
- >
35
- <EffectiveUserField
36
- setValue={[Function]}
37
- value=""
38
- >
39
- <mockConstructor
40
- fieldId="effective-user"
41
- label="Effective user"
42
- labelIcon={
43
- <Popover
44
- aria-label="help-text"
45
- bodyContent="A user to be used for executing the script. If it differs from the SSH user, su or sudo is used to switch the accounts."
46
- id="effective-user-help"
47
- >
48
- <button
49
- aria-label="open-help-tooltip-button"
50
- className="pf-c-form__group-label-help"
51
- onClick={[Function]}
52
- >
53
- <HelpIcon
54
- color="currentColor"
55
- noVerticalAlign={true}
56
- size="sm"
57
- />
58
- </button>
59
- </Popover>
60
- }
61
- >
62
- <div />
63
- </mockConstructor>
64
- </EffectiveUserField>
65
- <TimeoutToKillField
66
- setValue={[Function]}
67
- value=""
68
- >
69
- <mockConstructor
70
- fieldId="timeout-to-kill"
71
- label="Timeout to kill"
72
- labelIcon={
73
- <Popover
74
- aria-label="help-text"
75
- bodyContent="Time in seconds from the start on the remote host after which the job should be killed."
76
- id="timeout-to-kill-help"
77
- >
78
- <button
79
- aria-label="open-help-tooltip-button"
80
- className="pf-c-form__group-label-help"
81
- onClick={[Function]}
82
- >
83
- <HelpIcon
84
- color="currentColor"
85
- noVerticalAlign={true}
86
- size="sm"
87
- />
88
- </button>
89
- </Popover>
90
- }
91
- >
92
- <div />
93
- </mockConstructor>
94
- </TimeoutToKillField>
95
- <PasswordField
96
- setValue={[Function]}
97
- value=""
98
- >
99
- <mockConstructor
100
- fieldId="password"
101
- label="Password"
102
- labelIcon={
103
- <Popover
104
- aria-label="help-text"
105
- bodyContent="Password is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution."
106
- id="password-help"
107
- >
108
- <button
109
- aria-label="open-help-tooltip-button"
110
- className="pf-c-form__group-label-help"
111
- onClick={[Function]}
112
- >
113
- <HelpIcon
114
- color="currentColor"
115
- noVerticalAlign={true}
116
- size="sm"
117
- />
118
- </button>
119
- </Popover>
120
- }
121
- >
122
- <div />
123
- </mockConstructor>
124
- </PasswordField>
125
- <KeyPassphraseField
126
- setValue={[Function]}
127
- value=""
128
- >
129
- <mockConstructor
130
- fieldId="key-passphrase"
131
- label="Private key passphrase"
132
- labelIcon={
133
- <Popover
134
- aria-label="help-text"
135
- bodyContent="Key passphrase is only applicable for SSH provider. Other providers ignore this field. Passphrase is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution."
136
- id="key-passphrase-help"
137
- >
138
- <button
139
- aria-label="open-help-tooltip-button"
140
- className="pf-c-form__group-label-help"
141
- onClick={[Function]}
142
- >
143
- <HelpIcon
144
- color="currentColor"
145
- noVerticalAlign={true}
146
- size="sm"
147
- />
148
- </button>
149
- </Popover>
150
- }
151
- >
152
- <div />
153
- </mockConstructor>
154
- </KeyPassphraseField>
155
- <EffectiveUserPasswordField
156
- setValue={[Function]}
157
- value=""
158
- >
159
- <mockConstructor
160
- fieldId="effective-user-password"
161
- label="Effective user password"
162
- labelIcon={
163
- <Popover
164
- aria-label="help-text"
165
- bodyContent="Effective user password is only applicable for SSH provider. Other providers ignore this field. Password is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution."
166
- id="effective-user-password-help"
167
- >
168
- <button
169
- aria-label="open-help-tooltip-button"
170
- className="pf-c-form__group-label-help"
171
- onClick={[Function]}
172
- >
173
- <HelpIcon
174
- color="currentColor"
175
- noVerticalAlign={true}
176
- size="sm"
177
- />
178
- </button>
179
- </Popover>
180
- }
181
- >
182
- <div />
183
- </mockConstructor>
184
- </EffectiveUserPasswordField>
185
- <ConcurrencyLevelField
186
- setValue={[Function]}
187
- value=""
188
- >
189
- <mockConstructor
190
- fieldId="concurrency-level"
191
- label="Concurrency level"
192
- labelIcon={
193
- <Popover
194
- aria-label="help-text"
195
- bodyContent="Run at most N tasks at a time. If this is set and proxy batch triggering is enabled, then tasks are triggered on the smart proxy in batches of size 1."
196
- id="concurrency-level-help"
197
- >
198
- <button
199
- aria-label="open-help-tooltip-button"
200
- className="pf-c-form__group-label-help"
201
- onClick={[Function]}
202
- >
203
- <HelpIcon
204
- color="currentColor"
205
- noVerticalAlign={true}
206
- size="sm"
207
- />
208
- </button>
209
- </Popover>
210
- }
211
- >
212
- <div />
213
- </mockConstructor>
214
- </ConcurrencyLevelField>
215
- <TimeSpanLevelField
216
- setValue={[Function]}
217
- value=""
218
- >
219
- <mockConstructor
220
- fieldId="time-span"
221
- label="Time span"
222
- labelIcon={
223
- <Popover
224
- aria-label="help-text"
225
- bodyContent="Distribute execution over N seconds. If this is set and proxy batch triggering is enabled, then tasks are triggered on the smart proxy in batches of size 1."
226
- id="time-span-help"
227
- >
228
- <button
229
- aria-label="open-help-tooltip-button"
230
- className="pf-c-form__group-label-help"
231
- onClick={[Function]}
232
- >
233
- <HelpIcon
234
- color="currentColor"
235
- noVerticalAlign={true}
236
- size="sm"
237
- />
238
- </button>
239
- </Popover>
240
- }
241
- >
242
- <div />
243
- </mockConstructor>
244
- </TimeSpanLevelField>
245
- </form>
246
- </Form>
247
- </AdvancedFields>
248
- </Provider>
3
+ exports[`AdvancedFields search resources action: resource search 1`] = `
4
+ Array [
5
+ Object {
6
+ "key": "JOB_CATEGORIES",
7
+ "type": "get",
8
+ "url": "/ui_job_wizard/categories",
9
+ },
10
+ Object {
11
+ "key": "JOB_TEMPLATES",
12
+ "type": "get",
13
+ "url": URI {
14
+ "_deferred_build": true,
15
+ "_parts": Object {
16
+ "duplicateQueryParameters": false,
17
+ "escapeQuerySpace": true,
18
+ "fragment": null,
19
+ "hostname": null,
20
+ "password": null,
21
+ "path": "foreman/api/v2/job_templates",
22
+ "port": null,
23
+ "preventInvalidHostname": false,
24
+ "protocol": null,
25
+ "query": "search=job_category%3D%22Ansible+Commands%22&per_page=all",
26
+ "urn": null,
27
+ "username": null,
28
+ },
29
+ "_string": "",
30
+ },
31
+ },
32
+ Object {
33
+ "key": "JOB_TEMPLATE",
34
+ "type": "get",
35
+ "url": "/ui_job_wizard/template/178",
36
+ },
37
+ Object {
38
+ "key": "ForemanTasksTask",
39
+ "type": "get",
40
+ "url": URI {
41
+ "_deferred_build": true,
42
+ "_parts": Object {
43
+ "duplicateQueryParameters": false,
44
+ "escapeQuerySpace": true,
45
+ "fragment": null,
46
+ "hostname": null,
47
+ "password": null,
48
+ "path": "/ui_job_wizard/resources",
49
+ "port": null,
50
+ "preventInvalidHostname": false,
51
+ "protocol": null,
52
+ "query": "resource=ForemanTasks%3A%3ATask&name=",
53
+ "urn": null,
54
+ "username": null,
55
+ },
56
+ "_string": "",
57
+ },
58
+ },
59
+ Object {
60
+ "key": "ForemanTasksTask",
61
+ "type": "get",
62
+ "url": URI {
63
+ "_deferred_build": true,
64
+ "_parts": Object {
65
+ "duplicateQueryParameters": false,
66
+ "escapeQuerySpace": true,
67
+ "fragment": null,
68
+ "hostname": null,
69
+ "password": null,
70
+ "path": "/ui_job_wizard/resources",
71
+ "port": null,
72
+ "preventInvalidHostname": false,
73
+ "protocol": null,
74
+ "query": "resource=ForemanTasks%3A%3ATask&name=some+search",
75
+ "urn": null,
76
+ "username": null,
77
+ },
78
+ "_string": "",
79
+ },
80
+ },
81
+ ]
249
82
  `;
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { Title, Text, TextVariants, Form, Alert } from '@patternfly/react-core';
3
+ import { Text, TextVariants, Form, Alert } from '@patternfly/react-core';
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
5
  import { SelectField } from '../form/SelectField';
6
6
  import { GroupedSelectField } from '../form/GroupedSelectField';
7
+ import { WizardTitle } from '../form/WizardTitle';
8
+ import { WIZARD_TITLES } from '../../JobWizardConstants';
7
9
 
8
10
  export const CategoryAndTemplate = ({
9
11
  jobCategories,
@@ -40,7 +42,7 @@ export const CategoryAndTemplate = ({
40
42
  const isError = !!(categoryError || allTemplatesError || templateError);
41
43
  return (
42
44
  <>
43
- <Title headingLevel="h2">{__('Category and Template')}</Title>
45
+ <WizardTitle title={WIZARD_TITLES.categoryAndTemplate} />
44
46
  <Text component={TextVariants.p}>{__('All fields are required.')}</Text>
45
47
  <Form>
46
48
  <SelectField
@@ -51,6 +53,7 @@ export const CategoryAndTemplate = ({
51
53
  value={selectedCategory}
52
54
  placeholderText={categoryError ? __('Error') : ''}
53
55
  isDisabled={!!categoryError}
56
+ isRequired
54
57
  />
55
58
  <GroupedSelectField
56
59
  label={__('Job template')}
@@ -1,52 +1,124 @@
1
- import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
2
- import { CategoryAndTemplate } from './CategoryAndTemplate';
3
-
4
- const baseProps = {
5
- setJobTemplate: jest.fn(),
6
- selectedTemplateID: 190,
7
- setCategory: jest.fn(),
8
- };
9
- const fixtures = {
10
- 'renders with props': {
11
- ...baseProps,
12
- jobCategories: [
13
- 'Commands',
14
- 'Ansible Playbook',
15
- 'Ansible Galaxy',
16
- 'Ansible Roles Installation',
17
- ],
18
- jobTemplates: [
19
- {
20
- id: 190,
21
- name: 'ab Run Command - SSH Default clone',
22
- job_category: 'Commands',
23
- provider_type: 'SSH',
24
- snippet: false,
25
- },
26
- {
27
- id: 168,
28
- name: 'Ansible Roles - Ansible Default',
29
- job_category: 'Ansible Playbook',
30
- provider_type: 'Ansible',
31
- snippet: false,
32
- },
33
- {
34
- id: 170,
35
- name: 'Ansible Roles - Install from git',
36
- job_category: 'Ansible Roles Installation',
37
- provider_type: 'Ansible',
38
- snippet: false,
39
- },
40
- ],
41
- selectedCategory: 'I am a category',
42
- },
43
- 'render with error': {
44
- ...baseProps,
45
- errors: { allTemplatesError: 'I have an error' },
46
- },
47
- };
48
-
49
- describe('CategoryAndTemplate', () => {
50
- describe('rendering', () =>
51
- testComponentSnapshotsWithFixtures(CategoryAndTemplate, fixtures));
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import { fireEvent, screen, render, act } from '@testing-library/react';
4
+ import * as api from 'foremanReact/redux/API';
5
+ import { JobWizard } from '../../JobWizard';
6
+ import * as selectors from '../../JobWizardSelectors';
7
+ import { testSetup, mockApi } from '../../__tests__/fixtures';
8
+ import { WIZARD_TITLES } from '../../JobWizardConstants';
9
+
10
+ const store = testSetup(selectors, api);
11
+ mockApi(api);
12
+ jest.spyOn(selectors, 'selectCategoryError');
13
+ jest.spyOn(selectors, 'selectAllTemplatesError');
14
+ jest.spyOn(selectors, 'selectTemplateError');
15
+
16
+ describe('Category And Template', () => {
17
+ it('should select ', async () => {
18
+ selectors.selectCategoryError.mockImplementation(() => null);
19
+ selectors.selectAllTemplatesError.mockImplementation(() => null);
20
+ selectors.selectTemplateError.mockImplementation(() => null);
21
+ render(
22
+ <Provider store={store}>
23
+ <JobWizard />
24
+ </Provider>
25
+ );
26
+
27
+ expect(screen.queryAllByLabelText('Error')).toHaveLength(0);
28
+ expect(screen.queryAllByLabelText('failed')).toHaveLength(0);
29
+ // Category
30
+ fireEvent.click(
31
+ screen.getByLabelText('Ansible Commands', { selector: 'button' })
32
+ );
33
+ await act(async () => {
34
+ await fireEvent.click(screen.getByText('Puppet'));
35
+ });
36
+ fireEvent.click(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)[0]); // to remove focus
37
+ expect(
38
+ screen.queryAllByLabelText('Ansible Commands', { selector: 'button' })
39
+ ).toHaveLength(0);
40
+ expect(
41
+ screen.queryAllByLabelText('Puppet', { selector: 'button' })
42
+ ).toHaveLength(1);
43
+
44
+ // Template
45
+ fireEvent.click(
46
+ screen.getByDisplayValue('template1', { selector: 'button' })
47
+ );
48
+ await act(async () => {
49
+ await fireEvent.click(screen.getByText('template2'));
50
+ });
51
+ fireEvent.click(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)[0]); // to remove focus
52
+ expect(
53
+ screen.queryAllByDisplayValue('template1', { selector: 'button' })
54
+ ).toHaveLength(0);
55
+ expect(
56
+ screen.queryAllByDisplayValue('template2', { selector: 'button' })
57
+ ).toHaveLength(1);
58
+ });
59
+ it('category error ', async () => {
60
+ selectors.selectCategoryError.mockImplementation(() => 'category error');
61
+ selectors.selectAllTemplatesError.mockImplementation(() => null);
62
+ selectors.selectTemplateError.mockImplementation(() => null);
63
+ render(
64
+ <Provider store={store}>
65
+ <JobWizard />
66
+ </Provider>
67
+ );
68
+
69
+ expect(
70
+ screen.queryAllByText('Categories list failed with:', { exact: false })
71
+ ).toHaveLength(1);
72
+
73
+ expect(
74
+ screen.queryAllByText('Templates list failed with:', { exact: false })
75
+ ).toHaveLength(0);
76
+ expect(
77
+ screen.queryAllByText('Template failed with:', { exact: false })
78
+ ).toHaveLength(0);
79
+ });
80
+ it('templates error ', async () => {
81
+ selectors.selectCategoryError.mockImplementation(() => null);
82
+ selectors.selectAllTemplatesError.mockImplementation(
83
+ () => 'templates error'
84
+ );
85
+ selectors.selectTemplateError.mockImplementation(() => null);
86
+ render(
87
+ <Provider store={store}>
88
+ <JobWizard />
89
+ </Provider>
90
+ );
91
+
92
+ expect(
93
+ screen.queryAllByText('Categories list failed with:', { exact: false })
94
+ ).toHaveLength(0);
95
+
96
+ expect(
97
+ screen.queryAllByText('Templates list failed with:', { exact: false })
98
+ ).toHaveLength(1);
99
+ expect(
100
+ screen.queryAllByText('Template failed with:', { exact: false })
101
+ ).toHaveLength(0);
102
+ });
103
+ it('template error ', async () => {
104
+ selectors.selectCategoryError.mockImplementation(() => null);
105
+ selectors.selectAllTemplatesError.mockImplementation(() => null);
106
+ selectors.selectTemplateError.mockImplementation(() => 'template error');
107
+ render(
108
+ <Provider store={store}>
109
+ <JobWizard />
110
+ </Provider>
111
+ );
112
+
113
+ expect(
114
+ screen.queryAllByText('Categories list failed with:', { exact: false })
115
+ ).toHaveLength(0);
116
+
117
+ expect(
118
+ screen.queryAllByText('Templates list failed with:', { exact: false })
119
+ ).toHaveLength(0);
120
+ expect(
121
+ screen.queryAllByText('Template failed with:', { exact: false })
122
+ ).toHaveLength(1);
123
+ });
52
124
  });
@@ -54,10 +54,11 @@ const ConnectedCategoryAndTemplate = ({
54
54
  search: `job_category="${category}"`,
55
55
  per_page: 'all',
56
56
  }),
57
- handleSuccess: response =>
57
+ handleSuccess: response => {
58
58
  setJobTemplate(
59
59
  Number(filterJobTemplates(response?.data?.results)[0]?.id) || null
60
- ),
60
+ );
61
+ },
61
62
  })
62
63
  );
63
64
  }
@@ -0,0 +1,62 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useSelector } from 'react-redux';
4
+ import URI from 'urijs';
5
+ import { List, ListItem, Modal, Button } from '@patternfly/react-core';
6
+ import { translate as __, sprintf } from 'foremanReact/common/I18n';
7
+ import { foremanUrl } from 'foremanReact/common/helpers';
8
+ import { selectHosts, selectHostCount } from '../../JobWizardSelectors';
9
+ import { HOSTS_TO_PREVIEW_AMOUNT } from '../../JobWizardConstants';
10
+
11
+ export const HostPreviewModal = ({ isOpen, setIsOpen, searchQuery }) => {
12
+ const hosts = useSelector(selectHosts);
13
+ const hostsCount = useSelector(selectHostCount);
14
+ const url = new URI(foremanUrl('/hosts'));
15
+
16
+ return (
17
+ <Modal
18
+ title={__('Preview Hosts')}
19
+ isOpen={isOpen}
20
+ onClose={() => setIsOpen(false)}
21
+ appendTo={() => document.getElementsByClassName('job-wizard')[0]}
22
+ >
23
+ <List isPlain>
24
+ {hosts.map(host => (
25
+ <ListItem key={host}>
26
+ <Button
27
+ component="a"
28
+ href={foremanUrl(`/hosts/${host}`)}
29
+ variant="link"
30
+ target="_blank"
31
+ rel="noreferrer"
32
+ >
33
+ {host}
34
+ </Button>
35
+ </ListItem>
36
+ ))}
37
+ {hostsCount > HOSTS_TO_PREVIEW_AMOUNT && (
38
+ <ListItem>
39
+ <Button
40
+ component="a"
41
+ href={url.addSearch({ search: searchQuery })}
42
+ variant="link"
43
+ target="_blank"
44
+ rel="noreferrer"
45
+ >
46
+ {sprintf(
47
+ __('...and %s more'),
48
+ hostsCount - HOSTS_TO_PREVIEW_AMOUNT
49
+ )}
50
+ </Button>
51
+ </ListItem>
52
+ )}
53
+ </List>
54
+ </Modal>
55
+ );
56
+ };
57
+
58
+ HostPreviewModal.propTypes = {
59
+ isOpen: PropTypes.bool.isRequired,
60
+ setIsOpen: PropTypes.func.isRequired,
61
+ searchQuery: PropTypes.string.isRequired,
62
+ };
@@ -0,0 +1,54 @@
1
+ import React, { useEffect } from 'react';
2
+ import { useSelector, useDispatch } from 'react-redux';
3
+ import PropTypes from 'prop-types';
4
+ import SearchBar from 'foremanReact/components/SearchBar';
5
+ import { getControllerSearchProps } from 'foremanReact/constants';
6
+ import { getResults } from 'foremanReact/components/AutoComplete/AutoCompleteActions';
7
+ import { TRIGGERS } from 'foremanReact/components/AutoComplete/AutoCompleteConstants';
8
+ import { hostsController, hostQuerySearchID } from '../../JobWizardConstants';
9
+ import { noop } from '../../../helpers';
10
+
11
+ export const HostSearch = ({ value, setValue }) => {
12
+ const searchQuery = useSelector(
13
+ state => state.autocomplete?.hostsSearch?.searchQuery
14
+ );
15
+ useEffect(() => {
16
+ setValue(searchQuery || '');
17
+ }, [setValue, searchQuery]);
18
+ const dispatch = useDispatch();
19
+ const setSearch = newSearchQuery => {
20
+ dispatch(
21
+ getResults({
22
+ url: '/hosts/auto_complete_search',
23
+ searchQuery: newSearchQuery,
24
+ controller: 'hostsController',
25
+ trigger: TRIGGERS.INPUT_CHANGE,
26
+ id: hostQuerySearchID,
27
+ })
28
+ );
29
+ };
30
+
31
+ const props = getControllerSearchProps(hostsController, hostQuerySearchID);
32
+ return (
33
+ <div className="foreman-search-field">
34
+ <SearchBar
35
+ data={{
36
+ ...props,
37
+ autocomplete: {
38
+ id: hostQuerySearchID,
39
+ url: '/hosts/auto_complete_search',
40
+ useKeyShortcuts: true,
41
+ },
42
+ }}
43
+ onSearch={noop}
44
+ initialQuery={value}
45
+ onBookmarkClick={search => setSearch(search)}
46
+ />
47
+ </div>
48
+ );
49
+ };
50
+
51
+ HostSearch.propTypes = {
52
+ value: PropTypes.string.isRequired,
53
+ setValue: PropTypes.func.isRequired,
54
+ };