foreman_remote_execution 4.7.0 → 4.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +1 -0
  3. data/app/controllers/api/v2/job_invocations_controller.rb +7 -1
  4. data/app/lib/actions/remote_execution/run_host_job.rb +2 -1
  5. data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
  6. data/app/mailers/rex_job_mailer.rb +15 -0
  7. data/app/models/job_invocation.rb +4 -0
  8. data/app/models/job_invocation_composer.rb +20 -12
  9. data/app/models/remote_execution_provider.rb +18 -2
  10. data/app/models/rex_mail_notification.rb +13 -0
  11. data/app/models/setting/remote_execution.rb +7 -1
  12. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  13. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  14. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  15. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  16. data/app/views/template_invocations/show.html.erb +2 -1
  17. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  18. data/db/seeds.d/95-mail_notifications.rb +24 -0
  19. data/foreman_remote_execution.gemspec +1 -1
  20. data/lib/foreman_remote_execution/engine.rb +1 -0
  21. data/lib/foreman_remote_execution/version.rb +1 -1
  22. data/package.json +6 -6
  23. data/test/functional/api/v2/job_invocations_controller_test.rb +10 -0
  24. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  25. data/test/unit/job_invocation_report_template_test.rb +15 -12
  26. data/test/unit/remote_execution_provider_test.rb +46 -0
  27. data/webpack/JobWizard/JobWizard.js +53 -20
  28. data/webpack/JobWizard/JobWizard.scss +33 -4
  29. data/webpack/JobWizard/JobWizardConstants.js +17 -0
  30. data/webpack/JobWizard/__tests__/fixtures.js +8 -0
  31. data/webpack/JobWizard/__tests__/integration.test.js +3 -7
  32. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +16 -5
  33. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
  34. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +29 -14
  35. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +4 -2
  36. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
  37. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +25 -0
  38. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  39. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +37 -0
  40. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +50 -0
  41. data/webpack/JobWizard/steps/HostsAndInputs/index.js +66 -0
  42. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
  43. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +36 -21
  44. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +155 -0
  45. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +9 -8
  46. data/webpack/JobWizard/steps/Schedule/index.js +89 -28
  47. data/webpack/JobWizard/steps/form/DateTimePicker.js +93 -0
  48. data/webpack/JobWizard/steps/form/Formatter.js +10 -9
  49. data/webpack/JobWizard/steps/form/NumberInput.js +2 -0
  50. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  51. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +2 -1
  52. metadata +18 -4
@@ -1,41 +1,102 @@
1
- import React, { useState } from 'react';
2
- import { Title, Button, Form } from '@patternfly/react-core';
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Button, Form } from '@patternfly/react-core';
3
4
  import { translate as __ } from 'foremanReact/common/I18n';
4
5
  import { ScheduleType } from './ScheduleType';
5
6
  import { RepeatOn } from './RepeatOn';
6
7
  import { QueryType } from './QueryType';
7
8
  import { StartEndDates } from './StartEndDates';
8
- import { repeatTypes } from '../../JobWizardConstants';
9
+ import { WIZARD_TITLES } from '../../JobWizardConstants';
10
+ import { WizardTitle } from '../form/WizardTitle';
9
11
 
10
- const Schedule = () => {
11
- const [repeatType, setRepeatType] = useState(repeatTypes.noRepeat);
12
- const [repeatAmount, setRepeatAmount] = useState('');
13
- const [starts, setStarts] = useState('');
14
- const [ends, setEnds] = useState('');
12
+ const Schedule = ({ scheduleValue, setScheduleValue }) => {
13
+ const { repeatType, repeatAmount, starts, ends, isNeverEnds } = scheduleValue;
15
14
 
16
15
  return (
17
- <Form className="schedule-tab">
18
- <Title headingLevel="h2">{__('Schedule')}</Title>
19
- <ScheduleType />
16
+ <>
17
+ <WizardTitle title={WIZARD_TITLES.schedule} />
18
+ <Form className="schedule-tab">
19
+ <ScheduleType
20
+ isFuture={scheduleValue.isFuture}
21
+ setIsFuture={newValue => {
22
+ if (!newValue) {
23
+ // if schedule type is execute now
24
+ setScheduleValue(current => ({
25
+ ...current,
26
+ starts: '',
27
+ }));
28
+ }
29
+ setScheduleValue(current => ({
30
+ ...current,
31
+ isFuture: newValue,
32
+ }));
33
+ }}
34
+ />
20
35
 
21
- <RepeatOn
22
- repeatType={repeatType}
23
- setRepeatType={setRepeatType}
24
- repeatAmount={repeatAmount}
25
- setRepeatAmount={setRepeatAmount}
26
- />
27
- <StartEndDates
28
- starts={starts}
29
- setStarts={setStarts}
30
- ends={ends}
31
- setEnds={setEnds}
32
- />
33
- <Button variant="link" className="advanced-scheduling-button" isInline>
34
- {__('Advanced scheduling')}
35
- </Button>
36
- <QueryType />
37
- </Form>
36
+ <RepeatOn
37
+ repeatType={repeatType}
38
+ setRepeatType={newValue => {
39
+ setScheduleValue(current => ({
40
+ ...current,
41
+ repeatType: newValue,
42
+ }));
43
+ }}
44
+ repeatAmount={repeatAmount}
45
+ setRepeatAmount={newValue => {
46
+ setScheduleValue(current => ({
47
+ ...current,
48
+ repeatAmount: newValue,
49
+ }));
50
+ }}
51
+ />
52
+ <StartEndDates
53
+ starts={starts}
54
+ setStarts={newValue => {
55
+ if (!scheduleValue.isFuture) {
56
+ setScheduleValue(current => ({
57
+ ...current,
58
+ isFuture: true,
59
+ }));
60
+ }
61
+ setScheduleValue(current => ({
62
+ ...current,
63
+ starts: newValue,
64
+ }));
65
+ }}
66
+ ends={ends}
67
+ setEnds={newValue => {
68
+ setScheduleValue(current => ({
69
+ ...current,
70
+ ends: newValue,
71
+ }));
72
+ }}
73
+ isNeverEnds={isNeverEnds}
74
+ setIsNeverEnds={newValue => {
75
+ setScheduleValue(current => ({
76
+ ...current,
77
+ isNeverEnds: newValue,
78
+ }));
79
+ }}
80
+ />
81
+ <Button variant="link" className="advanced-scheduling-button" isInline>
82
+ {__('Advanced scheduling')}
83
+ </Button>
84
+ <QueryType />
85
+ </Form>
86
+ </>
38
87
  );
39
88
  };
40
89
 
90
+ Schedule.propTypes = {
91
+ scheduleValue: PropTypes.shape({
92
+ repeatType: PropTypes.string.isRequired,
93
+ repeatAmount: PropTypes.string,
94
+ starts: PropTypes.string,
95
+ ends: PropTypes.string,
96
+ isFuture: PropTypes.bool,
97
+ isNeverEnds: PropTypes.bool,
98
+ }).isRequired,
99
+ setScheduleValue: PropTypes.func.isRequired,
100
+ };
101
+
41
102
  export default Schedule;
@@ -0,0 +1,93 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { DatePicker, TimePicker } from '@patternfly/react-core';
4
+ import { debounce } from 'lodash';
5
+ import { translate as __ } from 'foremanReact/common/I18n';
6
+
7
+ export const DateTimePicker = ({ dateTime, setDateTime, isDisabled }) => {
8
+ const dateFormat = date =>
9
+ `${date.getFullYear()}/${(date.getMonth() + 1)
10
+ .toString()
11
+ .padStart(2, '0')}/${date
12
+ .getDate()
13
+ .toString()
14
+ .padStart(2, '0')}`;
15
+
16
+ const dateObject = dateTime ? new Date(dateTime) : new Date();
17
+ const formattedDate = dateTime ? dateFormat(dateObject) : '';
18
+ const dateParse = date =>
19
+ new Date(`${date} ${dateObject.getHours()}:${dateObject.getMinutes()}`);
20
+
21
+ const isValidDate = date => date && !Number.isNaN(date.getTime());
22
+
23
+ const isValidTime = time => {
24
+ if (!time) return false;
25
+ const split = time.split(':');
26
+ if (!(split[0].length === 2 && split[1].length === 2)) return false;
27
+ if (isValidDate(new Date(`${formattedDate} ${time}`))) return true;
28
+ if (!formattedDate.length && isValidDate(new Date(`01/01/2020 ${time}`))) {
29
+ const today = new Date();
30
+ today.setHours(split[0]);
31
+ today.setMinutes(split[1]);
32
+ setDateTime(today.toString());
33
+ }
34
+ return false;
35
+ };
36
+
37
+ const onDateChange = newDate => {
38
+ const parsedNewDate = new Date(newDate);
39
+
40
+ if (isValidDate(parsedNewDate)) {
41
+ parsedNewDate.setHours(dateObject.getHours());
42
+ parsedNewDate.setMinutes(dateObject.getMinutes());
43
+ setDateTime(parsedNewDate.toString());
44
+ }
45
+ };
46
+
47
+ const onTimeChange = newTime => {
48
+ if (isValidTime(newTime)) {
49
+ const parsedNewTime = new Date(`${formattedDate} ${newTime}`);
50
+ setDateTime(parsedNewTime.toString());
51
+ }
52
+ };
53
+ return (
54
+ <>
55
+ <DatePicker
56
+ value={formattedDate}
57
+ placeholder="yyyy/mm/dd"
58
+ onChange={debounce(onDateChange, 1000, {
59
+ leading: false,
60
+ trailing: true,
61
+ })}
62
+ dateFormat={dateFormat}
63
+ dateParse={dateParse}
64
+ isDisabled={isDisabled}
65
+ invalidFormatText={__('Invalid date')}
66
+ />
67
+ <TimePicker
68
+ className="time-picker"
69
+ time={dateTime ? dateObject.toString() : ''}
70
+ inputProps={dateTime ? {} : { value: '' }}
71
+ placeholder="hh:mm"
72
+ onChange={debounce(onTimeChange, 1000, {
73
+ leading: false,
74
+ trailing: true,
75
+ })}
76
+ is24Hour
77
+ isDisabled={isDisabled}
78
+ invalidFormatErrorMessage={__('Invalid time format')}
79
+ menuAppendTo={() => document.body}
80
+ />
81
+ </>
82
+ );
83
+ };
84
+
85
+ DateTimePicker.propTypes = {
86
+ dateTime: PropTypes.string,
87
+ setDateTime: PropTypes.func.isRequired,
88
+ isDisabled: PropTypes.bool,
89
+ };
90
+ DateTimePicker.defaultProps = {
91
+ dateTime: null,
92
+ isDisabled: false,
93
+ };
@@ -22,11 +22,12 @@ const TemplateSearchField = ({
22
22
  setValue({ ...values, [name]: searchQuery });
23
23
  // eslint-disable-next-line react-hooks/exhaustive-deps
24
24
  }, [searchQuery]);
25
+ const id = name.replace(/ /g, '-');
25
26
  return (
26
27
  <FormGroup
27
28
  label={name}
28
29
  labelIcon={helpLabel(labelText, name)}
29
- fieldId={name}
30
+ fieldId={id}
30
31
  isRequired={required}
31
32
  className="foreman-search-field"
32
33
  >
@@ -54,16 +55,16 @@ export const formatter = (input, values, setValue) => {
54
55
  const { name, required, hidden_value: hidden } = input;
55
56
  const labelText = input.description;
56
57
  const value = values[name];
57
-
58
+ const id = name.replace(/ /g, '-');
58
59
  if (isSelectType) {
59
60
  const options = input.options.split(/\r?\n/).map(option => option.trim());
60
61
  return (
61
62
  <SelectField
62
63
  aria-label={name}
63
- key={name}
64
+ key={id}
64
65
  isRequired={required}
65
66
  label={name}
66
- fieldId={name}
67
+ fieldId={id}
67
68
  options={options}
68
69
  labelIcon={helpLabel(labelText, name)}
69
70
  value={value}
@@ -77,7 +78,7 @@ export const formatter = (input, values, setValue) => {
77
78
  key={name}
78
79
  label={name}
79
80
  labelIcon={helpLabel(labelText, name)}
80
- fieldId={name}
81
+ fieldId={id}
81
82
  isRequired={required}
82
83
  >
83
84
  <TextArea
@@ -85,7 +86,7 @@ export const formatter = (input, values, setValue) => {
85
86
  className={hidden ? 'masked-input' : null}
86
87
  required={required}
87
88
  rows={2}
88
- id={name}
89
+ id={id}
89
90
  value={value}
90
91
  onChange={newValue => setValue({ ...values, [name]: newValue })}
91
92
  />
@@ -98,7 +99,7 @@ export const formatter = (input, values, setValue) => {
98
99
  key={name}
99
100
  label={name}
100
101
  labelIcon={helpLabel(labelText, name)}
101
- fieldId={name}
102
+ fieldId={id}
102
103
  isRequired={required}
103
104
  >
104
105
  <TextInput
@@ -106,7 +107,7 @@ export const formatter = (input, values, setValue) => {
106
107
  placeholder="YYYY-mm-dd HH:MM"
107
108
  className={hidden ? 'masked-input' : null}
108
109
  required={required}
109
- id={name}
110
+ id={id}
110
111
  type="text"
111
112
  value={value}
112
113
  onChange={newValue => setValue({ ...values, [name]: newValue })}
@@ -119,7 +120,7 @@ export const formatter = (input, values, setValue) => {
119
120
  // TODO: get text from redux autocomplete
120
121
  return (
121
122
  <TemplateSearchField
122
- key={name}
123
+ key={id}
123
124
  name={name}
124
125
  defaultValue={value}
125
126
  controller={controller}
@@ -5,6 +5,7 @@ import { translate as __ } from 'foremanReact/common/I18n';
5
5
 
6
6
  export const NumberInput = ({ formProps, inputProps }) => {
7
7
  const [validated, setValidated] = useState();
8
+ const name = inputProps.id.replace(/-/g, ' ');
8
9
  return (
9
10
  <FormGroup
10
11
  {...formProps}
@@ -12,6 +13,7 @@ export const NumberInput = ({ formProps, inputProps }) => {
12
13
  validated={validated}
13
14
  >
14
15
  <TextInput
16
+ aria-label={name}
15
17
  type="text"
16
18
  {...inputProps}
17
19
  onChange={newValue => {
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Title } from '@patternfly/react-core';
4
+
5
+ export const WizardTitle = ({ title, ...props }) => (
6
+ <Title headingLevel="h2" className="wizard-title" {...props}>
7
+ {title}
8
+ </Title>
9
+ );
10
+
11
+ WizardTitle.propTypes = {
12
+ title: PropTypes.string.isRequired,
13
+ };
14
+ export default WizardTitle;
@@ -21,7 +21,8 @@ const RecentJobsCard = ({ hostDetails: { name, id } }) => {
21
21
 
22
22
  return (
23
23
  <CardTemplate
24
- header={__('Recent Jobs')}
24
+ overrideGridProps={{ xl: 8, lg: 8, md: 12 }}
25
+ header={__('Recent jobs')}
25
26
  dropdownItems={[
26
27
  <DropdownItem
27
28
  href={foremanUrl(`${JOB_BASE_URL}${name}`)}
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_remote_execution
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.7.0
4
+ version: 4.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Remote Execution team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-08-03 00:00:00.000000000 Z
11
+ date: 2021-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deface
@@ -50,14 +50,14 @@ dependencies:
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: 5.0.0
53
+ version: 5.1.0
54
54
  type: :runtime
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: 5.0.0
60
+ version: 5.1.0
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: factory_bot_rails
63
63
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +150,7 @@ files:
150
150
  - app/lib/foreman_remote_execution/renderer/scope/input.rb
151
151
  - app/lib/proxy_api/remote_execution_ssh.rb
152
152
  - app/mailers/.gitkeep
153
+ - app/mailers/rex_job_mailer.rb
153
154
  - app/models/concerns/api/v2/interfaces_controller_extensions.rb
154
155
  - app/models/concerns/foreman_remote_execution/bookmark_extensions.rb
155
156
  - app/models/concerns/foreman_remote_execution/errors_flattener.rb
@@ -177,6 +178,7 @@ files:
177
178
  - app/models/job_template_effective_user.rb
178
179
  - app/models/remote_execution_feature.rb
179
180
  - app/models/remote_execution_provider.rb
181
+ - app/models/rex_mail_notification.rb
180
182
  - app/models/setting/remote_execution.rb
181
183
  - app/models/ssh_execution_provider.rb
182
184
  - app/models/target_remote_execution_proxy.rb
@@ -216,6 +218,7 @@ files:
216
218
  - app/views/api/v2/template_invocations/base.json.rabl
217
219
  - app/views/api/v2/template_invocations/template_invocations.json.rabl
218
220
  - app/views/dashboard/.gitkeep
221
+ - app/views/dashboard/_latest-jobs.html.erb
219
222
  - app/views/job_invocation_task_groups/_job_invocation_task_groups.html.erb
220
223
  - app/views/job_invocations/_card_results.html.erb
221
224
  - app/views/job_invocations/_card_schedule.html.erb
@@ -249,6 +252,8 @@ files:
249
252
  - app/views/remote_execution_features/_form.html.erb
250
253
  - app/views/remote_execution_features/index.html.erb
251
254
  - app/views/remote_execution_features/show.html.erb
255
+ - app/views/rex_job_mailer/job_finished.html.erb
256
+ - app/views/rex_job_mailer/job_finished.text.erb
252
257
  - app/views/template_inputs/_foreign_input_set_form.html.erb
253
258
  - app/views/template_invocations/_output_line_set.html.erb
254
259
  - app/views/template_invocations/_refresh.js.erb
@@ -318,6 +323,7 @@ files:
318
323
  - db/seeds.d/60-ssh_proxy_feature.rb
319
324
  - db/seeds.d/70-job_templates.rb
320
325
  - db/seeds.d/90-bookmarks.rb
326
+ - db/seeds.d/95-mail_notifications.rb
321
327
  - extra/cockpit/cockpit.conf.example
322
328
  - extra/cockpit/foreman-cockpit-session
323
329
  - extra/cockpit/foreman-cockpit.service
@@ -405,17 +411,25 @@ files:
405
411
  - webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js
406
412
  - webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js
407
413
  - webpack/JobWizard/steps/CategoryAndTemplate/index.js
414
+ - webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js
415
+ - webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js
416
+ - webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js
417
+ - webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js
418
+ - webpack/JobWizard/steps/HostsAndInputs/index.js
408
419
  - webpack/JobWizard/steps/Schedule/QueryType.js
409
420
  - webpack/JobWizard/steps/Schedule/RepeatOn.js
410
421
  - webpack/JobWizard/steps/Schedule/ScheduleType.js
411
422
  - webpack/JobWizard/steps/Schedule/StartEndDates.js
423
+ - webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js
412
424
  - webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js
413
425
  - webpack/JobWizard/steps/Schedule/index.js
426
+ - webpack/JobWizard/steps/form/DateTimePicker.js
414
427
  - webpack/JobWizard/steps/form/FormHelpers.js
415
428
  - webpack/JobWizard/steps/form/Formatter.js
416
429
  - webpack/JobWizard/steps/form/GroupedSelectField.js
417
430
  - webpack/JobWizard/steps/form/NumberInput.js
418
431
  - webpack/JobWizard/steps/form/SelectField.js
432
+ - webpack/JobWizard/steps/form/WizardTitle.js
419
433
  - webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example
420
434
  - webpack/Routes/routes.js
421
435
  - webpack/__mocks__/foremanReact/common/I18n.js