foreman_remote_execution 4.5.1 → 4.7.0

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_ci.yml +7 -0
  3. data/app/controllers/ui_job_wizard_controller.rb +7 -0
  4. data/app/graphql/types/job_invocation.rb +16 -0
  5. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +5 -1
  6. data/app/helpers/remote_execution_helper.rb +9 -3
  7. data/app/lib/actions/remote_execution/run_hosts_job.rb +1 -1
  8. data/app/models/job_invocation_composer.rb +3 -3
  9. data/app/models/job_template.rb +1 -1
  10. data/app/models/remote_execution_feature.rb +5 -1
  11. data/app/models/remote_execution_provider.rb +1 -1
  12. data/app/views/templates/ssh/module_action.erb +1 -0
  13. data/app/views/templates/ssh/power_action.erb +2 -0
  14. data/app/views/templates/ssh/puppet_run_once.erb +1 -0
  15. data/foreman_remote_execution.gemspec +2 -4
  16. data/lib/foreman_remote_execution/engine.rb +3 -0
  17. data/lib/foreman_remote_execution/version.rb +1 -1
  18. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  19. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  20. data/test/unit/concerns/host_extensions_test.rb +4 -4
  21. data/test/unit/input_template_renderer_test.rb +1 -89
  22. data/test/unit/job_invocation_composer_test.rb +1 -12
  23. data/webpack/JobWizard/JobWizard.js +28 -8
  24. data/webpack/JobWizard/JobWizard.scss +39 -0
  25. data/webpack/JobWizard/JobWizardConstants.js +10 -0
  26. data/webpack/JobWizard/JobWizardSelectors.js +9 -0
  27. data/webpack/JobWizard/__tests__/fixtures.js +104 -2
  28. data/webpack/JobWizard/__tests__/integration.test.js +13 -85
  29. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +21 -4
  30. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +67 -0
  31. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +73 -59
  32. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +135 -16
  33. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +23 -0
  34. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +122 -51
  35. data/webpack/JobWizard/steps/Schedule/QueryType.js +48 -0
  36. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +61 -0
  37. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +25 -0
  38. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +51 -0
  39. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +22 -0
  40. data/webpack/JobWizard/steps/Schedule/index.js +41 -0
  41. data/webpack/JobWizard/steps/form/FormHelpers.js +1 -0
  42. data/webpack/JobWizard/steps/form/Formatter.js +149 -0
  43. data/webpack/JobWizard/steps/form/NumberInput.js +33 -0
  44. data/webpack/JobWizard/steps/form/SelectField.js +14 -2
  45. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +76 -0
  46. data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
  47. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
  48. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +72 -66
  49. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
  50. data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
  51. data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
  52. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
  53. data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
  54. metadata +23 -27
  55. data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -13
  56. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -32
  57. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +0 -249
  58. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -113
  59. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
  60. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
  61. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -37
  62. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +0 -23
  63. 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) => {
@@ -16,7 +18,12 @@ export const SelectField = ({
16
18
  };
17
19
  const [isOpen, setIsOpen] = useState(false);
18
20
  return (
19
- <FormGroup label={label} fieldId={fieldId}>
21
+ <FormGroup
22
+ label={label}
23
+ fieldId={fieldId}
24
+ labelIcon={labelIcon}
25
+ isRequired={isRequired}
26
+ >
20
27
  <Select
21
28
  selections={value}
22
29
  onSelect={onSelect}
@@ -35,14 +42,19 @@ export const SelectField = ({
35
42
  );
36
43
  };
37
44
  SelectField.propTypes = {
38
- label: PropTypes.string.isRequired,
45
+ label: PropTypes.string,
39
46
  fieldId: PropTypes.string.isRequired,
40
47
  options: PropTypes.array,
41
48
  value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
42
49
  setValue: PropTypes.func.isRequired,
50
+ labelIcon: PropTypes.node,
51
+ isRequired: PropTypes.bool,
43
52
  };
44
53
 
45
54
  SelectField.defaultProps = {
55
+ label: null,
46
56
  options: [],
57
+ labelIcon: null,
47
58
  value: null,
59
+ isRequired: false,
48
60
  };
@@ -0,0 +1,76 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import configureMockStore from 'redux-mock-store';
4
+ import * as patternfly from '@patternfly/react-core';
5
+ import { mount, shallow } from '@theforeman/test';
6
+ import { formatter } from '../Formatter';
7
+
8
+ jest.spyOn(patternfly, 'Select');
9
+ jest.spyOn(patternfly, 'SelectOption');
10
+ jest.spyOn(patternfly, 'FormGroup');
11
+ patternfly.Select.mockImplementation(props => <div props={props} />);
12
+ patternfly.SelectOption.mockImplementation(props => <div props={props} />);
13
+ patternfly.FormGroup.mockImplementation(props => <div props={props} />);
14
+ const mockStore = configureMockStore([]);
15
+ const store = mockStore({});
16
+
17
+ describe('formatter', () => {
18
+ it('render date input', () => {
19
+ const props = {
20
+ name: 'date adv',
21
+ required: false,
22
+ options: '',
23
+ advanced: true,
24
+ value_type: 'date',
25
+ resource_type: 'ansible_roles',
26
+ default: '',
27
+ hidden_value: false,
28
+ };
29
+ expect(shallow(formatter(props, {}, jest.fn()))).toMatchSnapshot();
30
+ });
31
+ it('render text input', () => {
32
+ const props = {
33
+ name: 'plain adv hidden',
34
+ required: true,
35
+ description: 'some Description',
36
+ options: '',
37
+ advanced: true,
38
+ value_type: 'plain',
39
+ resource_type: 'ansible_roles',
40
+ default: 'Default val',
41
+ hidden_value: true,
42
+ };
43
+ expect(shallow(formatter(props, {}, jest.fn()))).toMatchSnapshot();
44
+ });
45
+ it('render select input', () => {
46
+ const props = {
47
+ name: 'adv plain search',
48
+ required: false,
49
+ input_type: 'user',
50
+ options: 'option 1\r\noption 2\r\noption 3\r\noption 4',
51
+ advanced: true,
52
+ value_type: 'plain',
53
+ resource_type: 'ansible_roles',
54
+ default: '',
55
+ hidden_value: false,
56
+ };
57
+ expect(shallow(formatter(props, {}, jest.fn()))).toMatchSnapshot();
58
+ });
59
+ it('render search input', () => {
60
+ const props = {
61
+ name: 'search adv',
62
+ required: false,
63
+ options: '',
64
+ advanced: true,
65
+ value_type: 'search',
66
+ resource_type: 'foreman_tasks/tasks',
67
+ default: '',
68
+ hidden_value: false,
69
+ };
70
+ expect(
71
+ mount(
72
+ <Provider store={store}>{formatter(props, {}, jest.fn())}</Provider>
73
+ )
74
+ ).toMatchSnapshot();
75
+ });
76
+ });
@@ -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,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,77 @@
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
+ header={__('Recent Jobs')}
25
+ dropdownItems={[
26
+ <DropdownItem
27
+ href={foremanUrl(`${JOB_BASE_URL}${name}`)}
28
+ key="link-to-all"
29
+ >
30
+ {__('View All Jobs')}
31
+ </DropdownItem>,
32
+ <DropdownItem
33
+ href={foremanUrl(
34
+ `${JOB_BASE_URL}${name}+and+status+%3D+failed+or+status%3D+succeeded`
35
+ )}
36
+ key="link-to-finished"
37
+ >
38
+ {__('View Finished Jobs')}
39
+ </DropdownItem>,
40
+ <DropdownItem
41
+ href={foremanUrl(`${JOB_BASE_URL}${name}+and+status+%3D+running`)}
42
+ key="link-to-running"
43
+ >
44
+ {__('View Running Jobs')}
45
+ </DropdownItem>,
46
+ <DropdownItem
47
+ href={foremanUrl(`${JOB_BASE_URL}${name}+and+status+%3D+queued`)}
48
+ key="link-to-scheduled"
49
+ >
50
+ {__('View Scheduled Jobs')}
51
+ </DropdownItem>,
52
+ ]}
43
53
  >
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>
54
+ <Tabs mountOnEnter activeKey={activeTab} onSelect={handleTabClick}>
55
+ <Tab
56
+ eventKey={FINISHED_TAB}
57
+ title={<TabTitleText>{__('Finished')}</TabTitleText>}
58
+ >
59
+ <RecentJobsTable hostId={id} status="failed+or+status%3D+succeeded" />
60
+ </Tab>
61
+ <Tab
62
+ eventKey={RUNNING_TAB}
63
+ title={<TabTitleText>{__('Running')}</TabTitleText>}
64
+ >
65
+ <RecentJobsTable hostId={id} status="running" />
66
+ </Tab>
67
+ <Tab
68
+ eventKey={SCHEDULED_TAB}
69
+ title={<TabTitleText>{__('Scheduled')}</TabTitleText>}
70
+ >
71
+ <RecentJobsTable hostId={id} status="queued" />
72
+ </Tab>
73
+ </Tabs>
74
+ </CardTemplate>
74
75
  );
75
76
  };
76
77
 
@@ -79,5 +80,10 @@ export default RecentJobsCard;
79
80
  RecentJobsCard.propTypes = {
80
81
  hostDetails: PropTypes.shape({
81
82
  name: PropTypes.string,
82
- }).isRequired,
83
+ id: PropTypes.number,
84
+ }),
85
+ };
86
+
87
+ RecentJobsCard.defaultProps = {
88
+ hostDetails: {},
83
89
  };
@@ -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;