foreman_remote_execution 4.6.0 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -8,6 +8,8 @@ export const SelectField = ({
8
8
  options,
9
9
  value,
10
10
  setValue,
11
+ labelIcon,
12
+ isRequired,
11
13
  ...props
12
14
  }) => {
13
15
  const onSelect = (event, selection) => {
@@ -15,8 +17,23 @@ export const SelectField = ({
15
17
  setIsOpen(false);
16
18
  };
17
19
  const [isOpen, setIsOpen] = useState(false);
20
+ const selectOptions = [
21
+ !isRequired && (
22
+ <SelectOption key={0} value="">
23
+ <p> </p>
24
+ </SelectOption>
25
+ ),
26
+ ...options.map((option, index) => (
27
+ <SelectOption key={index + 1} value={option} />
28
+ )),
29
+ ];
18
30
  return (
19
- <FormGroup label={label} fieldId={fieldId}>
31
+ <FormGroup
32
+ label={label}
33
+ fieldId={fieldId}
34
+ labelIcon={labelIcon}
35
+ isRequired={isRequired}
36
+ >
20
37
  <Select
21
38
  selections={value}
22
39
  onSelect={onSelect}
@@ -25,24 +42,30 @@ export const SelectField = ({
25
42
  className="without_select2"
26
43
  maxHeight="45vh"
27
44
  menuAppendTo={() => document.body}
45
+ placeholderText=" " // To prevent showing first option as selected
46
+ aria-labelledby={fieldId}
47
+ toggleAriaLabel={`${label} toggle`}
28
48
  {...props}
29
49
  >
30
- {options.map((option, index) => (
31
- <SelectOption key={index} value={option} />
32
- ))}
50
+ {selectOptions.filter(option => option)}
33
51
  </Select>
34
52
  </FormGroup>
35
53
  );
36
54
  };
37
55
  SelectField.propTypes = {
38
- label: PropTypes.string.isRequired,
56
+ label: PropTypes.string,
39
57
  fieldId: PropTypes.string.isRequired,
40
58
  options: PropTypes.array,
41
59
  value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
42
60
  setValue: PropTypes.func.isRequired,
61
+ labelIcon: PropTypes.node,
62
+ isRequired: PropTypes.bool,
43
63
  };
44
64
 
45
65
  SelectField.defaultProps = {
66
+ label: null,
46
67
  options: [],
68
+ labelIcon: null,
47
69
  value: null,
70
+ isRequired: false,
48
71
  };
@@ -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;
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, screen } from '@testing-library/react';
3
+ import { SearchSelect } from '../SearchSelect';
4
+
5
+ const apiKey = 'HOSTS_KEY';
6
+
7
+ describe('SearchSelect', () => {
8
+ it('too many', () => {
9
+ const onSearch = jest.fn();
10
+ render(
11
+ <SearchSelect
12
+ selected={['hosts1,host2']}
13
+ setSelected={jest.fn()}
14
+ apiKey={apiKey}
15
+ url="/hosts"
16
+ placeholderText="Test hosts"
17
+ useNameSearch={() => [
18
+ onSearch,
19
+ { results: ['host1', 'host2', 'host3'], subtotal: 101 },
20
+ false,
21
+ ]}
22
+ />
23
+ );
24
+ const openSelectbutton = screen.getByRole('button', {
25
+ name: 'typeahead select toggle',
26
+ });
27
+ fireEvent.click(openSelectbutton);
28
+ const tooMany = screen.queryAllByText(
29
+ 'You have %s results to display. Showing first %s results + 101 + 100'
30
+ );
31
+ expect(tooMany).toHaveLength(1);
32
+ });
33
+ });
@@ -0,0 +1,120 @@
1
+ import { post } from 'foremanReact/redux/API';
2
+ import { repeatTypes, JOB_INVOCATION } from './JobWizardConstants';
3
+ import { buildHostQuery } from './steps/HostsAndInputs/buildHostQuery';
4
+
5
+ export const submit = ({
6
+ jobTemplateID,
7
+ templateValues,
8
+ advancedValues,
9
+ scheduleValue,
10
+ selectedTargets,
11
+ hostsSearchQuery,
12
+ location,
13
+ organization,
14
+ dispatch,
15
+ }) => {
16
+ const {
17
+ repeatAmount,
18
+ repeatType,
19
+ repeatData,
20
+ startsAt,
21
+ startsBefore,
22
+ ends,
23
+ purpose,
24
+ } = scheduleValue;
25
+ const {
26
+ effectiveUserValue,
27
+ effectiveUserPassword,
28
+ description,
29
+ timeoutToKill,
30
+ isRandomizedOrdering,
31
+ timeSpan,
32
+ concurrencyLevel,
33
+ templateValues: advancedTemplateValues,
34
+ password,
35
+ keyPassphrase,
36
+ } = advancedValues;
37
+ const getCronLine = () => {
38
+ const [hour, minute] = repeatData.at
39
+ ? repeatData.at.split(':')
40
+ : [null, null];
41
+ switch (repeatType) {
42
+ case repeatTypes.cronline:
43
+ return repeatData.cronline;
44
+ case repeatTypes.monthly:
45
+ return `${minute} ${hour} ${repeatData.days} * *`;
46
+ case repeatTypes.weekly:
47
+ return `${minute} ${hour} * * ${repeatData.daysOfWeek}`;
48
+ case repeatTypes.daily:
49
+ return `${minute} ${hour} * * *`;
50
+ case repeatTypes.hourly:
51
+ return `${repeatData.minute} * * * *`;
52
+ case repeatTypes.noRepeat:
53
+ default:
54
+ return null;
55
+ }
56
+ };
57
+ const api = {
58
+ location,
59
+ organization,
60
+ job_invocation: {
61
+ job_template_id: jobTemplateID,
62
+ targeting_type: scheduleValue?.isTypeStatic
63
+ ? 'static_query'
64
+ : 'dynamic_query',
65
+ randomized_ordering: isRandomizedOrdering,
66
+ inputs: { ...templateValues, ...advancedTemplateValues },
67
+ ssh: {
68
+ effective_user: effectiveUserValue,
69
+ effective_user_password: effectiveUserPassword,
70
+ },
71
+ password,
72
+ key_passphrase: keyPassphrase,
73
+ recurrence:
74
+ repeatType !== repeatTypes.noRepeat
75
+ ? {
76
+ cron_line: getCronLine(),
77
+ max_iteration: repeatAmount,
78
+ end_time: ends.length ? new Date(ends).toISOString() : null,
79
+ purpose,
80
+ }
81
+ : null,
82
+ scheduling:
83
+ startsAt?.length || startsBefore?.length
84
+ ? {
85
+ start_at: startsAt?.length
86
+ ? new Date(startsAt).toISOString()
87
+ : null,
88
+ start_before: startsBefore?.length
89
+ ? new Date(startsBefore).toISOString()
90
+ : null,
91
+ }
92
+ : null,
93
+ concurrency_control: {
94
+ time_span: timeSpan,
95
+ concurrency_level: concurrencyLevel,
96
+ },
97
+ bookmark_id: null,
98
+ search_query:
99
+ buildHostQuery(selectedTargets, hostsSearchQuery) || 'name ~ *',
100
+ description_format: description,
101
+ execution_timeout_interval: timeoutToKill,
102
+ feature: '', // TODO add value after https://github.com/theforeman/foreman_remote_execution/pull/629
103
+ },
104
+ };
105
+
106
+ dispatch(
107
+ post({
108
+ key: JOB_INVOCATION,
109
+ url: '/api/job_invocations',
110
+ params: api,
111
+ handleSuccess: ({ data: { id } }) => {
112
+ window.location.href = `/job_invocations/${id}`;
113
+ },
114
+ errorToast: ({ response }) =>
115
+ response?.date?.error?.message ||
116
+ response?.message ||
117
+ response?.statusText,
118
+ })
119
+ );
120
+ };
@@ -0,0 +1,53 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useSelector } from 'react-redux';
3
+ import {
4
+ selectTemplateInputs,
5
+ selectAdvancedTemplateInputs,
6
+ } from './JobWizardSelectors';
7
+ import { isPositiveNumber, isValidDate } from './steps/form/FormHelpers';
8
+ import './JobWizard.scss';
9
+
10
+ export const useValidation = ({ advancedValues, templateValues }) => {
11
+ const [valid, setValid] = useState({});
12
+ const templateInputs = useSelector(selectTemplateInputs);
13
+ const advancedTemplateInputs = useSelector(selectAdvancedTemplateInputs);
14
+ useEffect(() => {
15
+ setValid({
16
+ hostsAndInputs: true,
17
+ advanced: true,
18
+ schedule: true,
19
+ });
20
+ const inputValidation = (inputs, values, setInvalid) => {
21
+ inputs.forEach(({ name, required, value_type: valueType }) => {
22
+ const value = values[name];
23
+ if (required && !value) {
24
+ setInvalid();
25
+ }
26
+ if (value && valueType === 'date') {
27
+ if (!isValidDate(value) && !isValidDate(new Date(value))) {
28
+ setInvalid();
29
+ }
30
+ }
31
+ });
32
+ };
33
+ inputValidation(templateInputs, templateValues, () =>
34
+ setValid(currValid => ({ ...currValid, hostsAndInputs: false }))
35
+ );
36
+
37
+ inputValidation(advancedTemplateInputs, advancedValues.templateValues, () =>
38
+ setValid(currValid => ({ ...currValid, advanced: false }))
39
+ );
40
+ [
41
+ advancedValues.timeoutToKill,
42
+ advancedValues.concurrencyLevel,
43
+ advancedValues.timeSpan,
44
+ ].forEach(value => {
45
+ if (value && !isPositiveNumber(value)) {
46
+ setValid(currValid => ({ ...currValid, advanced: false }));
47
+ }
48
+ });
49
+
50
+ // eslint-disable-next-line react-hooks/exhaustive-deps
51
+ }, [advancedValues, templateValues]);
52
+ return [valid, setValid];
53
+ };
@@ -0,0 +1,2 @@
1
+ export const useForemanOrganization = () => ({ id: 1 });
2
+ export const useForemanLocation = () => ({ id: 2 });
@@ -1 +1,3 @@
1
+ export const sprintf = (string, ...args) => [string, ...args].join(' + ');
1
2
  export const translate = s => s;
3
+ export const documentLocale = () => 'en';
@@ -0,0 +1 @@
1
+ export const resetData = payload => ({ type: 'REST_DATA', payload });
@@ -0,0 +1 @@
1
+ export const TRIGGERS = { INPUT_CHANGE: 'INPUT_CHANGE' };
@@ -1,2 +1,19 @@
1
- const SearchBar = () => jest.fn();
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ const SearchBar = ({ onChange }) => (
5
+ <input
6
+ className="foreman-search"
7
+ onChange={onChange}
8
+ placeholder="Filter..."
9
+ />
10
+ );
2
11
  export default SearchBar;
12
+
13
+ SearchBar.propTypes = {
14
+ onChange: PropTypes.func,
15
+ };
16
+
17
+ SearchBar.defaultProps = {
18
+ onChange: () => null,
19
+ };
@@ -0,0 +1 @@
1
+ export const selectRouterLocation = jest.fn(() => ({}));
@@ -0,0 +1 @@
1
+ export const noop = Function.prototype;
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ CheckCircleIcon,
5
+ ExclamationCircleIcon,
6
+ QuestionCircleIcon,
7
+ } from '@patternfly/react-icons';
8
+ import { JOB_SUCCESS_STATUS, JOB_ERROR_STATUS } from './constants';
9
+ import './styles.scss';
10
+
11
+ const JobStatusIcon = ({ status, children, ...props }) => {
12
+ switch (status) {
13
+ case JOB_SUCCESS_STATUS:
14
+ return (
15
+ <span className="job-success">
16
+ <CheckCircleIcon {...props} /> {children}
17
+ </span>
18
+ );
19
+ case JOB_ERROR_STATUS:
20
+ return (
21
+ <span className="job-error">
22
+ <ExclamationCircleIcon {...props} /> {children}
23
+ </span>
24
+ );
25
+ default:
26
+ return (
27
+ <span className="job-info">
28
+ <QuestionCircleIcon {...props} /> {children}
29
+ </span>
30
+ );
31
+ }
32
+ };
33
+
34
+ JobStatusIcon.propTypes = {
35
+ status: PropTypes.number,
36
+ children: PropTypes.string.isRequired,
37
+ };
38
+
39
+ JobStatusIcon.defaultProps = {
40
+ status: undefined,
41
+ };
42
+
43
+ export default JobStatusIcon;
@@ -1,76 +1,78 @@
1
- /* eslint-disable camelcase */
2
-
3
1
  import PropTypes from 'prop-types';
4
- import React from 'react';
5
- import Skeleton from 'react-loading-skeleton';
6
- import ElipsisWithTooltip from 'react-ellipsis-with-tooltip';
7
-
8
- import { Grid, GridItem } from '@patternfly/react-core';
9
- import {
10
- PropertiesSidePanel,
11
- PropertyItem,
12
- } from '@patternfly/react-catalog-view-extension';
13
- import { ArrowIcon, ErrorCircleOIcon, OkIcon } from '@patternfly/react-icons';
2
+ import React, { useState } from 'react';
14
3
 
15
- import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
16
- import CardItem from 'foremanReact/components/HostDetails/Templates/CardItem/CardTemplate';
17
- import RelativeDateTime from 'foremanReact/components/common/dates/RelativeDateTime';
4
+ import { DropdownItem, Tabs, Tab, TabTitleText } from '@patternfly/react-core';
5
+ import CardTemplate from 'foremanReact/components/HostDetails/Templates/CardItem/CardTemplate';
18
6
  import { translate as __ } from 'foremanReact/common/I18n';
19
- import './styles.css';
7
+ import { foremanUrl } from 'foremanReact/common/helpers';
20
8
 
21
- const RecentJobsCard = ({ hostDetails: { name } }) => {
22
- const jobsUrl =
23
- name && `/api/job_invocations?search=host%3D${name}&per_page=3`;
24
- const {
25
- response: { results: jobs },
26
- } = useAPI('get', jobsUrl);
9
+ import {
10
+ FINISHED_TAB,
11
+ RUNNING_TAB,
12
+ SCHEDULED_TAB,
13
+ JOB_BASE_URL,
14
+ } from './constants';
15
+ import RecentJobsTable from './RecentJobsTable';
27
16
 
28
- const iconMarkup = status => {
29
- if (status === 1) return <ErrorCircleOIcon color="#C9190B" />;
30
- return <OkIcon color="#3E8635" />;
31
- };
17
+ const RecentJobsCard = ({ hostDetails: { name, id } }) => {
18
+ const [activeTab, setActiveTab] = useState(FINISHED_TAB);
19
+
20
+ const handleTabClick = (evt, tabIndex) => setActiveTab(tabIndex);
32
21
 
33
22
  return (
34
- <CardItem
35
- header={
36
- <span>
37
- {__('Recent Jobs')}{' '}
38
- <a href={`/job_invocations?search=host+%3D+${name}`}>
39
- <ArrowIcon />
40
- </a>
41
- </span>
42
- }
23
+ <CardTemplate
24
+ overrideGridProps={{ xl2: 6, xl: 8, lg: 8, md: 12 }}
25
+ header={__('Recent jobs')}
26
+ dropdownItems={[
27
+ <DropdownItem
28
+ href={foremanUrl(`${JOB_BASE_URL}${name}`)}
29
+ key="link-to-all"
30
+ >
31
+ {__('View All Jobs')}
32
+ </DropdownItem>,
33
+ <DropdownItem
34
+ href={foremanUrl(
35
+ `${JOB_BASE_URL}${name}+and+status+%3D+failed+or+status%3D+succeeded`
36
+ )}
37
+ key="link-to-finished"
38
+ >
39
+ {__('View Finished Jobs')}
40
+ </DropdownItem>,
41
+ <DropdownItem
42
+ href={foremanUrl(`${JOB_BASE_URL}${name}+and+status+%3D+running`)}
43
+ key="link-to-running"
44
+ >
45
+ {__('View Running Jobs')}
46
+ </DropdownItem>,
47
+ <DropdownItem
48
+ href={foremanUrl(`${JOB_BASE_URL}${name}+and+status+%3D+queued`)}
49
+ key="link-to-scheduled"
50
+ >
51
+ {__('View Scheduled Jobs')}
52
+ </DropdownItem>,
53
+ ]}
43
54
  >
44
- <PropertiesSidePanel>
45
- {jobs?.map(({ status, status_label, id, start_at, description }) => (
46
- <PropertyItem
47
- key={id}
48
- label={
49
- description ? (
50
- <Grid>
51
- <GridItem span={8}>
52
- <ElipsisWithTooltip>{description}</ElipsisWithTooltip>
53
- </GridItem>
54
- <GridItem span={1}>{iconMarkup(status)}</GridItem>
55
- <GridItem span={3}>{status_label}</GridItem>
56
- </Grid>
57
- ) : (
58
- <Skeleton />
59
- )
60
- }
61
- value={
62
- start_at ? (
63
- <a href={`/job_invocations/${id}`}>
64
- <RelativeDateTime date={start_at} />
65
- </a>
66
- ) : (
67
- <Skeleton />
68
- )
69
- }
70
- />
71
- ))}
72
- </PropertiesSidePanel>
73
- </CardItem>
55
+ <Tabs mountOnEnter activeKey={activeTab} onSelect={handleTabClick}>
56
+ <Tab
57
+ eventKey={FINISHED_TAB}
58
+ title={<TabTitleText>{__('Finished')}</TabTitleText>}
59
+ >
60
+ <RecentJobsTable hostId={id} status="failed+or+status%3D+succeeded" />
61
+ </Tab>
62
+ <Tab
63
+ eventKey={RUNNING_TAB}
64
+ title={<TabTitleText>{__('Running')}</TabTitleText>}
65
+ >
66
+ <RecentJobsTable hostId={id} status="running" />
67
+ </Tab>
68
+ <Tab
69
+ eventKey={SCHEDULED_TAB}
70
+ title={<TabTitleText>{__('Scheduled')}</TabTitleText>}
71
+ >
72
+ <RecentJobsTable hostId={id} status="queued" />
73
+ </Tab>
74
+ </Tabs>
75
+ </CardTemplate>
74
76
  );
75
77
  };
76
78
 
@@ -79,5 +81,10 @@ export default RecentJobsCard;
79
81
  RecentJobsCard.propTypes = {
80
82
  hostDetails: PropTypes.shape({
81
83
  name: PropTypes.string,
82
- }).isRequired,
84
+ id: PropTypes.number,
85
+ }),
86
+ };
87
+
88
+ RecentJobsCard.defaultProps = {
89
+ hostDetails: {},
83
90
  };
@@ -0,0 +1,98 @@
1
+ import PropTypes from 'prop-types';
2
+ import React from 'react';
3
+ import {
4
+ DataList,
5
+ DataListItem,
6
+ DataListItemRow,
7
+ DataListItemCells,
8
+ DataListCell,
9
+ DataListWrapModifier,
10
+ Text,
11
+ Bullseye,
12
+ } from '@patternfly/react-core';
13
+ import { STATUS } from 'foremanReact/constants';
14
+
15
+ import RelativeDateTime from 'foremanReact/components/common/dates/RelativeDateTime';
16
+ import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
17
+ import SkeletonLoader from 'foremanReact/components/common/SkeletonLoader';
18
+ import { translate as __ } from 'foremanReact/common/I18n';
19
+ import { foremanUrl } from 'foremanReact/common/helpers';
20
+
21
+ import JobStatusIcon from './JobStatusIcon';
22
+ import { JOB_API_URL, JOBS_IN_CARD } from './constants';
23
+
24
+ const RecentJobsTable = ({ status, hostId }) => {
25
+ const jobsUrl =
26
+ hostId &&
27
+ foremanUrl(
28
+ `${JOB_API_URL}${hostId}+and+status%3D${status}&per_page=${JOBS_IN_CARD}`
29
+ );
30
+ const {
31
+ response: { results: jobs },
32
+ status: responseStatus,
33
+ } = useAPI('get', jobsUrl);
34
+
35
+ return (
36
+ <DataList aria-label="recent-jobs-table" isCompact>
37
+ <SkeletonLoader
38
+ skeletonProps={{ count: 3 }}
39
+ status={responseStatus || STATUS.PENDING}
40
+ emptyState={
41
+ <Bullseye>
42
+ <Text style={{ marginTop: '20px' }} component="p">
43
+ {__('No results found')}
44
+ </Text>
45
+ </Bullseye>
46
+ }
47
+ >
48
+ {jobs?.length &&
49
+ jobs.map(
50
+ ({
51
+ status: jobStatus,
52
+ status_label: label,
53
+ id,
54
+ start_at: startAt,
55
+ description,
56
+ }) => (
57
+ <DataListItem key={id}>
58
+ <DataListItemRow>
59
+ <DataListItemCells
60
+ dataListCells={[
61
+ <DataListCell
62
+ wrapModifier={DataListWrapModifier.truncate}
63
+ key={`name-${id}`}
64
+ >
65
+ <a href={foremanUrl(`/job_invocations/${id}`)}>
66
+ {description}
67
+ </a>
68
+ </DataListCell>,
69
+ <DataListCell key={`date-${id}`}>
70
+ <RelativeDateTime date={startAt} />
71
+ </DataListCell>,
72
+ <DataListCell key={`status-${id}`}>
73
+ <JobStatusIcon status={jobStatus}>
74
+ {label}
75
+ </JobStatusIcon>
76
+ </DataListCell>,
77
+ ]}
78
+ />
79
+ </DataListItemRow>
80
+ </DataListItem>
81
+ )
82
+ )}
83
+ </SkeletonLoader>
84
+ </DataList>
85
+ );
86
+ };
87
+
88
+ RecentJobsTable.propTypes = {
89
+ hostId: PropTypes.number,
90
+ status: PropTypes.string,
91
+ };
92
+
93
+ RecentJobsTable.defaultProps = {
94
+ hostId: undefined,
95
+ status: STATUS.PENDING,
96
+ };
97
+
98
+ export default RecentJobsTable;
@@ -1 +1,12 @@
1
1
  export const HOST_DETAILS_JOBS = 'HOST_DETAILS_JOBS';
2
+ export const FINISHED_TAB = 0;
3
+ export const RUNNING_TAB = 1;
4
+ export const SCHEDULED_TAB = 2;
5
+
6
+ export const JOB_SUCCESS_STATUS = 0;
7
+ export const JOB_ERROR_STATUS = 1;
8
+
9
+ export const JOB_BASE_URL = '/job_invocations?search=host+%3D+';
10
+ export const JOB_API_URL =
11
+ '/api/job_invocations?order=start_at+DESC&search=targeted_host_id%3D';
12
+ export const JOBS_IN_CARD = 3;
@@ -0,0 +1,11 @@
1
+ .job-success {
2
+ color: var(--pf-global--success-color--100);
3
+ }
4
+
5
+ .job-error {
6
+ color: var(--pf-global--danger-color--100);
7
+ }
8
+
9
+ .job-info {
10
+ color: var(--pf-global--info-color--200);
11
+ }
@@ -27,6 +27,7 @@ exports[`TargetingHostsPage renders 1`] = `
27
27
  "controller": "hosts",
28
28
  }
29
29
  }
30
+ onChange={[Function]}
30
31
  onSearch={[Function]}
31
32
  />
32
33
  </Col>
@@ -6,6 +6,6 @@ export default () =>
6
6
  addGlobalFill(
7
7
  'details-cards',
8
8
  'rex-host-details-latest-jobs',
9
- <RecentJobsCard />,
9
+ <RecentJobsCard key="rex-host-details-latest-jobs" />,
10
10
  1000
11
11
  );