foreman_remote_execution 4.5.1 → 4.7.0

Sign up to get free protection for your applications and to get access to all the features.
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;