foreman_remote_execution 4.5.6 → 5.0.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 (118) 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/ui_job_wizard_controller.rb +16 -4
  6. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  7. data/app/graphql/types/job_invocation.rb +16 -0
  8. data/app/graphql/types/job_invocation_input.rb +13 -0
  9. data/app/graphql/types/recurrence_input.rb +8 -0
  10. data/app/graphql/types/scheduling_input.rb +6 -0
  11. data/app/graphql/types/targeting_enum.rb +7 -0
  12. data/app/lib/actions/remote_execution/run_host_job.rb +6 -1
  13. data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
  14. data/app/mailers/rex_job_mailer.rb +15 -0
  15. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
  16. data/app/models/job_invocation.rb +4 -0
  17. data/app/models/job_invocation_composer.rb +21 -13
  18. data/app/models/job_template.rb +1 -1
  19. data/app/models/remote_execution_provider.rb +17 -2
  20. data/app/models/rex_mail_notification.rb +13 -0
  21. data/app/models/targeting.rb +2 -2
  22. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  23. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  24. data/app/views/job_invocations/refresh.js.erb +1 -0
  25. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  26. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  27. data/app/views/template_invocations/show.html.erb +2 -1
  28. data/config/routes.rb +1 -0
  29. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  30. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  31. data/db/seeds.d/95-mail_notifications.rb +24 -0
  32. data/foreman_remote_execution.gemspec +2 -4
  33. data/lib/foreman_remote_execution/engine.rb +114 -6
  34. data/lib/foreman_remote_execution/version.rb +1 -1
  35. data/package.json +6 -6
  36. data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
  37. data/test/functional/cockpit_controller_test.rb +0 -1
  38. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  39. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  40. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  41. data/test/helpers/remote_execution_helper_test.rb +0 -1
  42. data/test/unit/actions/run_host_job_test.rb +21 -0
  43. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  44. data/test/unit/concerns/host_extensions_test.rb +40 -7
  45. data/test/unit/input_template_renderer_test.rb +1 -89
  46. data/test/unit/job_invocation_composer_test.rb +4 -17
  47. data/test/unit/job_invocation_report_template_test.rb +16 -13
  48. data/test/unit/job_template_effective_user_test.rb +0 -4
  49. data/test/unit/remote_execution_provider_test.rb +34 -4
  50. data/test/unit/targeting_test.rb +68 -1
  51. data/webpack/JobWizard/JobWizard.js +106 -15
  52. data/webpack/JobWizard/JobWizard.scss +73 -39
  53. data/webpack/JobWizard/JobWizardConstants.js +36 -0
  54. data/webpack/JobWizard/JobWizardSelectors.js +32 -0
  55. data/webpack/JobWizard/__tests__/fixtures.js +81 -6
  56. data/webpack/JobWizard/__tests__/integration.test.js +26 -15
  57. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  58. data/webpack/JobWizard/autofill.js +38 -0
  59. data/webpack/JobWizard/index.js +7 -0
  60. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +7 -4
  61. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
  62. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +216 -12
  63. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
  64. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +1 -0
  65. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  66. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  67. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  68. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  69. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  70. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +82 -7
  71. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  72. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +7 -4
  73. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  74. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  75. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  76. data/webpack/JobWizard/steps/HostsAndInputs/index.js +182 -34
  77. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  78. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  79. data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
  80. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  81. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  82. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  83. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  84. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
  85. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  86. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
  87. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +78 -23
  88. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
  89. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +20 -10
  90. data/webpack/JobWizard/steps/Schedule/index.js +153 -19
  91. data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
  92. data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
  93. data/webpack/JobWizard/steps/form/Formatter.js +39 -8
  94. data/webpack/JobWizard/steps/form/NumberInput.js +3 -2
  95. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  96. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  97. data/webpack/JobWizard/steps/form/SelectField.js +14 -3
  98. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  99. data/webpack/JobWizard/submit.js +120 -0
  100. data/webpack/JobWizard/validation.js +53 -0
  101. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  102. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  103. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  104. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  105. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  106. data/webpack/helpers.js +1 -0
  107. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
  108. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
  109. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
  110. data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
  111. data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
  112. data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
  113. metadata +56 -23
  114. data/app/models/setting/remote_execution.rb +0 -88
  115. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
  116. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +0 -37
  117. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
  118. data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
@@ -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' };
@@ -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
+ }
@@ -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
  );