foreman_rh_cloud 13.0.8 → 13.0.10

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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/inventory_upload/report_actions.rb +8 -1
  3. data/app/controllers/foreman_inventory_upload/accounts_controller.rb +82 -7
  4. data/app/controllers/foreman_inventory_upload/api/tasks_controller.rb +110 -0
  5. data/app/controllers/foreman_inventory_upload/reports_controller.rb +41 -17
  6. data/app/controllers/foreman_inventory_upload/uploads_controller.rb +0 -9
  7. data/config/routes.rb +4 -2
  8. data/db/migrate/20251209163012_drop_task_output_tables.foreman_rh_cloud.rb +24 -0
  9. data/lib/foreman_inventory_upload/async/generate_all_reports_job.rb +1 -1
  10. data/lib/foreman_inventory_upload/async/host_inventory_report_job.rb +39 -0
  11. data/lib/foreman_inventory_upload/async/queue_for_upload_job.rb +1 -23
  12. data/lib/foreman_inventory_upload/async/upload_report_direct_job.rb +171 -0
  13. data/lib/foreman_inventory_upload.rb +0 -4
  14. data/lib/foreman_rh_cloud/plugin.rb +1 -0
  15. data/lib/foreman_rh_cloud/version.rb +1 -1
  16. data/lib/inventory_sync/async/inventory_hosts_sync.rb +0 -2
  17. data/lib/tasks/rh_cloud_inventory.rake +4 -2
  18. data/package.json +1 -1
  19. data/test/controllers/accounts_controller_test.rb +10 -11
  20. data/test/controllers/insights_cloud/api/cloud_request_controller_test.rb +1 -2
  21. data/test/jobs/host_inventory_report_job_test.rb +161 -97
  22. data/test/jobs/queue_for_upload_job_test.rb +1 -12
  23. data/test/jobs/single_host_report_job_test.rb +36 -54
  24. data/test/jobs/upload_report_direct_job_test.rb +399 -0
  25. data/test/unit/rh_cloud_permissions_test.rb +2 -0
  26. data/webpack/ForemanInventoryUpload/Components/AccountList/AccountList.fixtures.js +6 -6
  27. data/webpack/ForemanInventoryUpload/Components/AccountList/AccountList.js +49 -34
  28. data/webpack/ForemanInventoryUpload/Components/AccountList/AccountListActions.js +2 -2
  29. data/webpack/ForemanInventoryUpload/Components/AccountList/Components/ListItem/ListItem.fixtures.js +4 -5
  30. data/webpack/ForemanInventoryUpload/Components/AccountList/Components/ListItem/ListItem.js +15 -7
  31. data/webpack/ForemanInventoryUpload/Components/AccountList/Components/ListItem/__tests__/__snapshots__/ListItem.test.js.snap +11 -11
  32. data/webpack/ForemanInventoryUpload/Components/AccountList/Components/ListItemStatus/ListItemStatus.fixtures.js +2 -2
  33. data/webpack/ForemanInventoryUpload/Components/AccountList/Components/ListItemStatus/ListItemStatus.js +10 -14
  34. data/webpack/ForemanInventoryUpload/Components/AccountList/Components/ListItemStatus/ListItemStatusHelper.js +9 -4
  35. data/webpack/ForemanInventoryUpload/Components/AccountList/Components/ListItemStatus/__tests__/__snapshots__/ListItemStatus.test.js.snap +4 -4
  36. data/webpack/ForemanInventoryUpload/Components/AccountList/__tests__/__snapshots__/AccountList.test.js.snap +15 -9
  37. data/webpack/ForemanInventoryUpload/Components/AccountList/__tests__/__snapshots__/AccountListActions.test.js.snap +7 -7
  38. data/webpack/ForemanInventoryUpload/Components/AccountList/__tests__/__snapshots__/AccountListReducer.test.js.snap +6 -6
  39. data/webpack/ForemanInventoryUpload/Components/AccountList/__tests__/__snapshots__/AccountListSelectors.test.js.snap +12 -12
  40. data/webpack/ForemanInventoryUpload/Components/Dashboard/Dashboard.js +37 -130
  41. data/webpack/ForemanInventoryUpload/Components/Dashboard/__tests__/Dashboard.test.js +60 -17
  42. data/webpack/ForemanInventoryUpload/Components/Dashboard/index.js +1 -34
  43. data/webpack/ForemanInventoryUpload/Components/InventoryFilter/__tests__/__snapshots__/integration.test.js.snap +0 -1
  44. data/webpack/ForemanInventoryUpload/Components/NavContainer/NavContainer.js +1 -26
  45. data/webpack/ForemanInventoryUpload/Components/PageHeader/PageHeader.js +24 -17
  46. data/webpack/ForemanInventoryUpload/Components/PageHeader/__tests__/PageHeader.test.js +178 -8
  47. data/webpack/ForemanInventoryUpload/Components/PageHeader/__tests__/__snapshots__/PageTitle.test.js.snap +2 -2
  48. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/ToolbarButtons/ToolbarButtons.js +3 -1
  49. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/ToolbarButtons/__tests__/ToolbarButtons.test.js +69 -51
  50. data/webpack/ForemanInventoryUpload/Components/TabHeader/TabHeader.js +22 -9
  51. data/webpack/ForemanInventoryUpload/Components/TabHeader/__tests__/TabHeader.test.js +67 -4
  52. data/webpack/ForemanInventoryUpload/Components/TaskHistory/TaskHistory.js +140 -0
  53. data/webpack/ForemanInventoryUpload/Components/TaskHistory/index.js +1 -0
  54. data/webpack/ForemanInventoryUpload/Components/TaskHistory/taskHistory.scss +40 -0
  55. data/webpack/ForemanInventoryUpload/Components/TaskProgress/TaskProgress.js +340 -0
  56. data/webpack/ForemanInventoryUpload/Components/TaskProgress/index.js +1 -0
  57. data/webpack/ForemanInventoryUpload/Components/TaskProgress/taskProgress.scss +8 -0
  58. data/webpack/ForemanInventoryUpload/ForemanInventoryHelpers.js +2 -2
  59. data/webpack/ForemanInventoryUpload/ForemanInventoryUploadReducers.js +0 -2
  60. data/webpack/ForemanInventoryUpload/__tests__/__snapshots__/ForemanInventoryHelpers.test.js.snap +1 -1
  61. data/webpack/ForemanRhCloudPages.js +0 -1
  62. data/webpack/InsightsCloudSync/Components/InsightsTable/__tests__/InsightsTable.test.js +11 -19
  63. data/webpack/InsightsCloudSync/Components/RemediationModal/RemediationHelpers.js +1 -2
  64. data/webpack/InsightsCloudSync/Components/RemediationModal/RemediationModal.js +2 -4
  65. data/webpack/InsightsVulnerabilityHostIndexExtensions/__tests__/CVECountCell.test.js +77 -22
  66. data/webpack/__mocks__/foremanReact/components/common/dates/RelativeDateTime.js +14 -0
  67. data/webpack/__tests__/ForemanRhCloudHelpers.test.js +5 -1
  68. metadata +13 -68
  69. data/app/models/task_output_line.rb +0 -2
  70. data/app/models/task_output_status.rb +0 -2
  71. data/lib/foreman_inventory_upload/async/generate_report_job.rb +0 -61
  72. data/lib/foreman_inventory_upload/async/progress_output.rb +0 -38
  73. data/lib/foreman_inventory_upload/async/shell_process.rb +0 -77
  74. data/lib/foreman_inventory_upload/async/upload_report_job.rb +0 -97
  75. data/lib/foreman_inventory_upload/scripts/uploader.sh.erb +0 -55
  76. data/test/controllers/reports_controller_test.rb +0 -21
  77. data/test/controllers/uploads_controller_test.rb +0 -21
  78. data/test/jobs/generate_report_job_test.rb +0 -146
  79. data/test/jobs/upload_report_job_test.rb +0 -38
  80. data/test/unit/shell_process_job_test.rb +0 -29
  81. data/webpack/ForemanInventoryUpload/Components/Dashboard/DashboardActions.js +0 -88
  82. data/webpack/ForemanInventoryUpload/Components/Dashboard/DashboardConstants.js +0 -9
  83. data/webpack/ForemanInventoryUpload/Components/Dashboard/DashboardReducer.js +0 -68
  84. data/webpack/ForemanInventoryUpload/Components/Dashboard/DashboardSelectors.js +0 -17
  85. data/webpack/ForemanInventoryUpload/Components/Dashboard/__tests__/DashboardActions.test.js +0 -51
  86. data/webpack/ForemanInventoryUpload/Components/Dashboard/__tests__/DashboardIntegration.test.js +0 -17
  87. data/webpack/ForemanInventoryUpload/Components/Dashboard/__tests__/DashboardReducer.test.js +0 -64
  88. data/webpack/ForemanInventoryUpload/Components/Dashboard/__tests__/DashboardSelectors.test.js +0 -46
  89. data/webpack/ForemanInventoryUpload/Components/Dashboard/__tests__/__snapshots__/Dashboard.test.js.snap +0 -36
  90. data/webpack/ForemanInventoryUpload/Components/Dashboard/__tests__/__snapshots__/DashboardActions.test.js.snap +0 -76
  91. data/webpack/ForemanInventoryUpload/Components/Dashboard/__tests__/__snapshots__/DashboardReducer.test.js.snap +0 -44
  92. data/webpack/ForemanInventoryUpload/Components/Dashboard/__tests__/__snapshots__/DashboardSelectors.test.js.snap +0 -42
  93. data/webpack/ForemanInventoryUpload/Components/FullScreenModal/FullScreenModal.fixtures.js +0 -0
  94. data/webpack/ForemanInventoryUpload/Components/FullScreenModal/FullScreenModal.js +0 -55
  95. data/webpack/ForemanInventoryUpload/Components/FullScreenModal/FullScreenModalHelper.js +0 -0
  96. data/webpack/ForemanInventoryUpload/Components/FullScreenModal/__tests__/FullScreenModal.test.js +0 -13
  97. data/webpack/ForemanInventoryUpload/Components/FullScreenModal/__tests__/__snapshots__/FullScreenModal.test.js.snap +0 -65
  98. data/webpack/ForemanInventoryUpload/Components/FullScreenModal/fullScreenModal.scss +0 -20
  99. data/webpack/ForemanInventoryUpload/Components/FullScreenModal/index.js +0 -1
  100. data/webpack/ForemanInventoryUpload/Components/PageHeader/__tests__/__snapshots__/PageHeader.test.js.snap +0 -36
  101. data/webpack/ForemanInventoryUpload/Components/ReportGenerate/ReportGenerate.fixtures.js +0 -18
  102. data/webpack/ForemanInventoryUpload/Components/ReportGenerate/ReportGenerate.js +0 -65
  103. data/webpack/ForemanInventoryUpload/Components/ReportGenerate/ReportGenerateHelper.js +0 -0
  104. data/webpack/ForemanInventoryUpload/Components/ReportGenerate/__tests__/ReportGenerate.test.js +0 -14
  105. data/webpack/ForemanInventoryUpload/Components/ReportGenerate/__tests__/__snapshots__/ReportGenerate.test.js.snap +0 -47
  106. data/webpack/ForemanInventoryUpload/Components/ReportGenerate/index.js +0 -1
  107. data/webpack/ForemanInventoryUpload/Components/ReportGenerate/reportGenerate.scss +0 -0
  108. data/webpack/ForemanInventoryUpload/Components/ReportUpload/ReportUpload.fixtures.js +0 -18
  109. data/webpack/ForemanInventoryUpload/Components/ReportUpload/ReportUpload.js +0 -46
  110. data/webpack/ForemanInventoryUpload/Components/ReportUpload/ReportUploadHelper.js +0 -0
  111. data/webpack/ForemanInventoryUpload/Components/ReportUpload/__tests__/ReportUpload.test.js +0 -14
  112. data/webpack/ForemanInventoryUpload/Components/ReportUpload/__tests__/__snapshots__/ReportUpload.test.js.snap +0 -47
  113. data/webpack/ForemanInventoryUpload/Components/ReportUpload/index.js +0 -1
  114. data/webpack/ForemanInventoryUpload/Components/ReportUpload/reportUpload.scss +0 -0
  115. data/webpack/ForemanInventoryUpload/Components/TabBody/TabBody.fixtures.js +0 -0
  116. data/webpack/ForemanInventoryUpload/Components/TabBody/TabBody.js +0 -31
  117. data/webpack/ForemanInventoryUpload/Components/TabBody/TabBodyHelper.js +0 -0
  118. data/webpack/ForemanInventoryUpload/Components/TabBody/__tests__/TabBody.test.js +0 -13
  119. data/webpack/ForemanInventoryUpload/Components/TabBody/__tests__/__snapshots__/TabBody.test.js.snap +0 -19
  120. data/webpack/ForemanInventoryUpload/Components/TabBody/index.js +0 -1
  121. data/webpack/ForemanInventoryUpload/Components/TabBody/tabBody.scss +0 -5
  122. data/webpack/ForemanInventoryUpload/Components/Terminal/Terminal.fixtures.js +0 -10
  123. data/webpack/ForemanInventoryUpload/Components/Terminal/Terminal.js +0 -110
  124. data/webpack/ForemanInventoryUpload/Components/Terminal/TerminalHelper.js +0 -6
  125. data/webpack/ForemanInventoryUpload/Components/Terminal/__tests__/Terminal.test.js +0 -34
  126. data/webpack/ForemanInventoryUpload/Components/Terminal/__tests__/__snapshots__/Terminal.test.js.snap +0 -98
  127. data/webpack/ForemanInventoryUpload/Components/Terminal/index.js +0 -1
  128. data/webpack/ForemanInventoryUpload/Components/Terminal/terminal.scss +0 -32
  129. data/webpack/InsightsCloudSync/Components/InsightsTable/__tests__/__snapshots__/InsightsTable.test.js.snap +0 -112
  130. data/webpack/__mocks__/foremanReact/common/hooks/API/APIHooks.js +0 -3
@@ -0,0 +1,140 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ DataList,
5
+ DataListItem,
6
+ DataListItemRow,
7
+ DataListItemCells,
8
+ DataListCell,
9
+ EmptyState,
10
+ EmptyStateIcon,
11
+ EmptyStateBody,
12
+ Title,
13
+ } from '@patternfly/react-core';
14
+ import {
15
+ CheckCircleIcon,
16
+ ExclamationCircleIcon,
17
+ ExclamationTriangleIcon,
18
+ } from '@patternfly/react-icons';
19
+ import { translate as __ } from 'foremanReact/common/I18n';
20
+ import RelativeDateTime from 'foremanReact/components/common/dates/RelativeDateTime';
21
+ import './taskHistory.scss';
22
+
23
+ const TaskHistory = ({ tasks, title }) => {
24
+ if (!tasks || tasks.length === 0) {
25
+ return (
26
+ <EmptyState>
27
+ <EmptyStateIcon icon={CheckCircleIcon} />
28
+ <Title headingLevel="h4" size="lg" ouiaId="task-history-empty-title">
29
+ {__('No task history')}
30
+ </Title>
31
+ <EmptyStateBody>
32
+ {__('Previous tasks will appear here.')}
33
+ </EmptyStateBody>
34
+ </EmptyState>
35
+ );
36
+ }
37
+
38
+ const getResultIcon = result => {
39
+ if (result === 'success') {
40
+ return <CheckCircleIcon className="task-history-icon-success" />;
41
+ }
42
+ if (result === 'error') {
43
+ return <ExclamationCircleIcon className="task-history-icon-error" />;
44
+ }
45
+ if (result === 'warning') {
46
+ return <ExclamationTriangleIcon className="task-history-icon-warning" />;
47
+ }
48
+ return null;
49
+ };
50
+
51
+ const formatDuration = seconds => {
52
+ if (!Number.isFinite(seconds) || seconds < 0) {
53
+ return __('N/A');
54
+ }
55
+ const mins = Math.floor(seconds / 60);
56
+ const secs = Math.floor(seconds % 60);
57
+ if (mins > 0) {
58
+ return `${mins}m ${secs}s`;
59
+ }
60
+ return `${secs}s`;
61
+ };
62
+
63
+ const getResultLabel = result => {
64
+ if (result === 'success') return __('Success');
65
+ if (result === 'error') return __('Failed');
66
+ if (result === 'warning') return __('Warning');
67
+ return result || __('Unknown');
68
+ };
69
+
70
+ return (
71
+ <div className="task-history-container">
72
+ {title && (
73
+ <Title headingLevel="h3" size="md" ouiaId="task-history-title">
74
+ {title}
75
+ </Title>
76
+ )}
77
+ <DataList aria-label="task history" className="task-history-list">
78
+ {tasks.map(task => (
79
+ <DataListItem key={task.id} aria-labelledby={`task-${task.id}`}>
80
+ <DataListItemRow>
81
+ <DataListItemCells
82
+ dataListCells={[
83
+ <DataListCell key="icon" className="task-history-icon-cell">
84
+ {getResultIcon(task.result)}
85
+ </DataListCell>,
86
+ <DataListCell key="time" className="task-history-time-cell">
87
+ <RelativeDateTime date={task.started_at} />
88
+ </DataListCell>,
89
+ <DataListCell
90
+ key="result"
91
+ className="task-history-result-cell"
92
+ >
93
+ {getResultLabel(task.result)}
94
+ </DataListCell>,
95
+ <DataListCell
96
+ key="duration"
97
+ className="task-history-duration-cell"
98
+ >
99
+ {formatDuration(task.duration)}
100
+ </DataListCell>,
101
+ <DataListCell key="link" className="task-history-link-cell">
102
+ <a href={`/foreman_tasks/tasks/${task.id}`}>
103
+ {__('Details')}
104
+ </a>
105
+ </DataListCell>,
106
+ ]}
107
+ />
108
+ </DataListItemRow>
109
+ </DataListItem>
110
+ ))}
111
+ </DataList>
112
+ </div>
113
+ );
114
+ };
115
+
116
+ TaskHistory.propTypes = {
117
+ tasks: PropTypes.arrayOf(
118
+ PropTypes.shape({
119
+ id: PropTypes.string.isRequired,
120
+ label: PropTypes.string,
121
+ action: PropTypes.string,
122
+ state: PropTypes.string,
123
+ result: PropTypes.string,
124
+ progress: PropTypes.number,
125
+ started_at: PropTypes.string,
126
+ ended_at: PropTypes.string,
127
+ duration: PropTypes.number,
128
+ humanized: PropTypes.object,
129
+ report_file_path: PropTypes.string,
130
+ })
131
+ ),
132
+ title: PropTypes.string,
133
+ };
134
+
135
+ TaskHistory.defaultProps = {
136
+ tasks: [],
137
+ title: null,
138
+ };
139
+
140
+ export default TaskHistory;
@@ -0,0 +1 @@
1
+ export { default } from './TaskHistory';
@@ -0,0 +1,40 @@
1
+ .task-history-container {
2
+ margin-top: 2rem;
3
+
4
+ .task-history-list {
5
+ margin-top: 1rem;
6
+ }
7
+
8
+ .task-history-icon-cell {
9
+ width: 40px;
10
+ }
11
+
12
+ .task-history-time-cell {
13
+ flex: 2;
14
+ }
15
+
16
+ .task-history-result-cell {
17
+ flex: 1;
18
+ }
19
+
20
+ .task-history-duration-cell {
21
+ flex: 1;
22
+ }
23
+
24
+ .task-history-link-cell {
25
+ flex: 1;
26
+ text-align: right;
27
+ }
28
+
29
+ .task-history-icon-success {
30
+ color: var(--pf-global--success-color--100);
31
+ }
32
+
33
+ .task-history-icon-error {
34
+ color: var(--pf-global--danger-color--100);
35
+ }
36
+
37
+ .task-history-icon-warning {
38
+ color: var(--pf-global--warning-color--100);
39
+ }
40
+ }
@@ -0,0 +1,340 @@
1
+ /* eslint-disable max-lines */
2
+ import React, { useState, useEffect } from 'react';
3
+ import PropTypes from 'prop-types';
4
+ import {
5
+ Progress,
6
+ ProgressVariant,
7
+ Card,
8
+ CardTitle,
9
+ CardBody,
10
+ DescriptionList,
11
+ DescriptionListGroup,
12
+ DescriptionListTerm,
13
+ DescriptionListDescription,
14
+ Button,
15
+ EmptyState,
16
+ EmptyStateIcon,
17
+ EmptyStateBody,
18
+ Title,
19
+ Flex,
20
+ FlexItem,
21
+ Tooltip,
22
+ } from '@patternfly/react-core';
23
+ import { ClockIcon, DownloadIcon } from '@patternfly/react-icons';
24
+ import { translate as __ } from 'foremanReact/common/I18n';
25
+ import RelativeDateTime from 'foremanReact/components/common/dates/RelativeDateTime';
26
+ import { API } from 'foremanReact/redux/API';
27
+ import { addToast } from 'foremanReact/components/ToastsList';
28
+ import { useDispatch, useSelector } from 'react-redux';
29
+ import { inventoryUrl } from '../../ForemanInventoryHelpers';
30
+ import { selectSubscriptionConnectionEnabled } from '../InventorySettings/InventorySettingsSelectors';
31
+ import { useIopConfig } from '../../../common/Hooks/ConfigHooks';
32
+ import './taskProgress.scss';
33
+
34
+ const TaskProgress = ({
35
+ task,
36
+ title,
37
+ emptyMessage,
38
+ organizationId,
39
+ taskType,
40
+ onTaskStart,
41
+ }) => {
42
+ const dispatch = useDispatch();
43
+ const [isLoading, setIsLoading] = useState(false);
44
+ const [lastTaskId, setLastTaskId] = useState(null);
45
+
46
+ // Clear lastTaskId when the task data updates to match it
47
+ useEffect(() => {
48
+ if (lastTaskId && task?.id === lastTaskId) {
49
+ setLastTaskId(null);
50
+ }
51
+ // eslint-disable-next-line react-hooks/exhaustive-deps
52
+ }, [task?.id, lastTaskId]);
53
+
54
+ // Track when a new task starts so we can show optimistic UI
55
+ // Show optimistic UI if we've started a new task but haven't received the updated task data yet
56
+ const isStartingNewTask = lastTaskId && task?.id !== lastTaskId;
57
+
58
+ // Check IoP mode and subscription connection settings
59
+ const subscriptionConnectionEnabled = useSelector(
60
+ selectSubscriptionConnectionEnabled
61
+ );
62
+ const isIop = Boolean(useIopConfig());
63
+ const isUploadDisabled = !isIop && !subscriptionConnectionEnabled;
64
+ const uploadDisabledTooltip = __(
65
+ 'Upload is disabled because subscription connection is not enabled. Enable it in Administer > Settings > Content.'
66
+ );
67
+
68
+ // Helper to render action buttons for generate tasks
69
+ const renderGenerateButtons = (buttonsDisabled, ouiaIdSuffix = '') => {
70
+ if (taskType !== 'generate' || !organizationId) return null;
71
+
72
+ return (
73
+ <>
74
+ <FlexItem>
75
+ {isUploadDisabled ? (
76
+ <Tooltip content={uploadDisabledTooltip}>
77
+ <span>
78
+ <Button
79
+ ouiaId={`generate-and-upload-disabled-button${ouiaIdSuffix}`}
80
+ variant="primary"
81
+ onClick={() => handleGenerateReport(false)}
82
+ isDisabled
83
+ >
84
+ {__('Generate and upload report')}
85
+ </Button>
86
+ </span>
87
+ </Tooltip>
88
+ ) : (
89
+ <Button
90
+ ouiaId={`generate-and-upload-button${ouiaIdSuffix}`}
91
+ variant="primary"
92
+ onClick={() => handleGenerateReport(false)}
93
+ isDisabled={buttonsDisabled}
94
+ >
95
+ {__('Generate and upload report')}
96
+ </Button>
97
+ )}
98
+ </FlexItem>
99
+ <FlexItem>
100
+ <Button
101
+ ouiaId={`generate-report-button${ouiaIdSuffix}`}
102
+ variant="secondary"
103
+ onClick={() => handleGenerateReport(true)}
104
+ isDisabled={buttonsDisabled}
105
+ >
106
+ {__('Generate report')}
107
+ </Button>
108
+ </FlexItem>
109
+ </>
110
+ );
111
+ };
112
+
113
+ const handleGenerateReport = async disconnected => {
114
+ setIsLoading(true);
115
+ try {
116
+ const { data } = await API.post(
117
+ inventoryUrl(`${organizationId}/reports`),
118
+ {
119
+ disconnected,
120
+ }
121
+ );
122
+
123
+ // Validate API response
124
+ if (!data || !data.id) {
125
+ throw new Error(__('Invalid response from server'));
126
+ }
127
+
128
+ // Update last task ID only if it's a new task
129
+ if (data.id !== task?.id) {
130
+ setLastTaskId(data.id);
131
+ }
132
+
133
+ // Trigger immediate poll to update UI faster
134
+ if (onTaskStart) {
135
+ onTaskStart();
136
+ }
137
+
138
+ // Use Foreman's toast notification with task link
139
+ const message = disconnected
140
+ ? __('Report generation started')
141
+ : __('Report generation and upload started');
142
+
143
+ dispatch(
144
+ addToast({
145
+ type: 'success',
146
+ message,
147
+ link: (
148
+ <a href={`/foreman_tasks/tasks/${data.id}`}>{__('View task')}</a>
149
+ ),
150
+ })
151
+ );
152
+ } catch (error) {
153
+ dispatch(
154
+ addToast({
155
+ sticky: true,
156
+ type: 'error',
157
+ message: error.message,
158
+ })
159
+ );
160
+ } finally {
161
+ setIsLoading(false);
162
+ }
163
+ };
164
+
165
+ const handleDownloadReport = () => {
166
+ window.location.href = `/api/v2/organizations/${organizationId}/rh_cloud/report`;
167
+ };
168
+
169
+ if (!task) {
170
+ return (
171
+ <EmptyState>
172
+ <EmptyStateIcon icon={ClockIcon} />
173
+ <Title headingLevel="h4" size="lg" ouiaId="task-progress-empty-title">
174
+ {__('No recent tasks')}
175
+ </Title>
176
+ <EmptyStateBody>
177
+ {emptyMessage || __('No tasks have been run yet.')}
178
+ </EmptyStateBody>
179
+ <Flex className="task-progress-actions">
180
+ {renderGenerateButtons(isLoading)}
181
+ </Flex>
182
+ </EmptyState>
183
+ );
184
+ }
185
+
186
+ const getProgressVariant = () => {
187
+ if (task.state !== 'stopped') return ProgressVariant.info;
188
+ if (task.result === 'success') return ProgressVariant.success;
189
+ if (task.result === 'error') return ProgressVariant.danger;
190
+ if (task.result === 'warning') return ProgressVariant.warning;
191
+ return ProgressVariant.info;
192
+ };
193
+
194
+ const getStateLabel = () => {
195
+ if (task.state === 'running') return __('Running');
196
+ if (task.state === 'paused') return __('Paused');
197
+ if (task.state === 'stopped') {
198
+ if (task.result === 'success') return __('Completed');
199
+ if (task.result === 'error') return __('Failed');
200
+ if (task.result === 'warning') return __('Completed with warnings');
201
+ return __('Stopped');
202
+ }
203
+ return task.state;
204
+ };
205
+
206
+ const formatDuration = seconds => {
207
+ if (
208
+ seconds === null ||
209
+ seconds === undefined ||
210
+ typeof seconds !== 'number' ||
211
+ Number.isNaN(seconds) ||
212
+ seconds < 0
213
+ ) {
214
+ return '';
215
+ }
216
+ const mins = Math.floor(seconds / 60);
217
+ const secs = Math.floor(seconds % 60);
218
+ if (mins > 0) {
219
+ return `${mins}m ${secs}s`;
220
+ }
221
+ return `${secs}s`;
222
+ };
223
+
224
+ const isTaskRunning = task.state === 'running' || task.state === 'paused';
225
+
226
+ // Buttons should be disabled if loading, task is running, or we're waiting for new task data
227
+ const areButtonsDisabled = isLoading || isTaskRunning || isStartingNewTask;
228
+
229
+ // Show 100% for completed tasks, otherwise use reported progress
230
+ // If starting a new task, show 0% to give immediate feedback
231
+ let progressValue = 0;
232
+ if (isStartingNewTask) {
233
+ progressValue = 0;
234
+ } else if (task.state === 'stopped') {
235
+ progressValue = 100;
236
+ } else {
237
+ progressValue = task.progress || 0;
238
+ }
239
+
240
+ // Override state label and variant when starting new task
241
+ const displayStateLabel = isStartingNewTask ? __('Running') : getStateLabel();
242
+ const displayVariant = isStartingNewTask
243
+ ? ProgressVariant.info
244
+ : getProgressVariant();
245
+
246
+ return (
247
+ <Card className="task-progress-card" ouiaId="task-progress-card">
248
+ {title && <CardTitle>{title}</CardTitle>}
249
+ <CardBody>
250
+ <Progress
251
+ value={progressValue}
252
+ title={displayStateLabel}
253
+ variant={displayVariant}
254
+ measureLocation="outside"
255
+ aria-label="task-progress"
256
+ />
257
+ <DescriptionList
258
+ isHorizontal
259
+ className="task-progress-details"
260
+ ouiaId="task-progress-details"
261
+ >
262
+ <DescriptionListGroup>
263
+ <DescriptionListTerm>{__('Started')}</DescriptionListTerm>
264
+ <DescriptionListDescription>
265
+ {!isStartingNewTask && task.started_at && (
266
+ <RelativeDateTime date={task.started_at} />
267
+ )}
268
+ </DescriptionListDescription>
269
+ </DescriptionListGroup>
270
+ <DescriptionListGroup>
271
+ <DescriptionListTerm>{__('Duration')}</DescriptionListTerm>
272
+ <DescriptionListDescription>
273
+ {!isStartingNewTask && formatDuration(task.duration)}
274
+ </DescriptionListDescription>
275
+ </DescriptionListGroup>
276
+ <DescriptionListGroup>
277
+ <DescriptionListTerm>{__('Report saved to')}</DescriptionListTerm>
278
+ <DescriptionListDescription>
279
+ {!isStartingNewTask && task.report_file_path}
280
+ </DescriptionListDescription>
281
+ </DescriptionListGroup>
282
+ </DescriptionList>
283
+ <Flex className="task-progress-actions">
284
+ <FlexItem>
285
+ <Button
286
+ ouiaId="view-task-details-button"
287
+ component="a"
288
+ href={`/foreman_tasks/tasks/${task.id}`}
289
+ variant="link"
290
+ isInline
291
+ >
292
+ {__('View task details')}
293
+ </Button>
294
+ </FlexItem>
295
+ <FlexItem>
296
+ <Button
297
+ ouiaId="download-report-button"
298
+ variant="secondary"
299
+ onClick={handleDownloadReport}
300
+ icon={<DownloadIcon />}
301
+ isDisabled={!task.report_file_path || areButtonsDisabled}
302
+ >
303
+ {__('Download report')}
304
+ </Button>
305
+ </FlexItem>
306
+ {renderGenerateButtons(areButtonsDisabled, '-task-view')}
307
+ </Flex>
308
+ </CardBody>
309
+ </Card>
310
+ );
311
+ };
312
+
313
+ TaskProgress.propTypes = {
314
+ task: PropTypes.shape({
315
+ id: PropTypes.string.isRequired,
316
+ state: PropTypes.string.isRequired,
317
+ result: PropTypes.string,
318
+ progress: PropTypes.number,
319
+ started_at: PropTypes.string,
320
+ ended_at: PropTypes.string,
321
+ duration: PropTypes.number,
322
+ report_file_path: PropTypes.string,
323
+ }),
324
+ title: PropTypes.string,
325
+ emptyMessage: PropTypes.string,
326
+ organizationId: PropTypes.number,
327
+ taskType: PropTypes.oneOf(['generate', 'upload']),
328
+ onTaskStart: PropTypes.func,
329
+ };
330
+
331
+ TaskProgress.defaultProps = {
332
+ task: null,
333
+ title: null,
334
+ emptyMessage: null,
335
+ organizationId: null,
336
+ taskType: null,
337
+ onTaskStart: null,
338
+ };
339
+
340
+ export default TaskProgress;
@@ -0,0 +1 @@
1
+ export { default } from './TaskProgress';
@@ -0,0 +1,8 @@
1
+ .task-progress-card {
2
+ margin-bottom: 1rem;
3
+
4
+ .task-progress-details {
5
+ margin-top: 1rem;
6
+ margin-bottom: 1rem;
7
+ }
8
+ }
@@ -7,13 +7,13 @@ export const inventoryUrl = path =>
7
7
  export const getInventoryDocsUrl = () =>
8
8
  foremanUrl(
9
9
  `/links/manual/?root_url=${URI.encode(
10
- 'https://access.redhat.com/documentation/en-us/red_hat_insights/2023/html/red_hat_insights_remediations_guide/host-communication-with-insights_red-hat-insights-remediation-guide#uploading-satellite-host-inventory-to-insights_configuring-satellite-cloud-connector'
10
+ 'https://docs.redhat.com/en/documentation/red_hat_lightspeed/1-latest/html-single/red_hat_lightspeed_remediations_guide/index'
11
11
  )}`
12
12
  );
13
13
 
14
14
  export const getActionsHistoryUrl = () =>
15
15
  foremanUrl(
16
- '/foreman_tasks/tasks?search=label+%3D+ForemanInventoryUpload%3A%3AAsync%3A%3AGenerateReportJob+or+label+%3D+ForemanInventoryUpload%3A%3AAsync%3A%3AGenerateAllReportsJob&page=1'
16
+ '/foreman_tasks/tasks?search=label+%3D+ForemanInventoryUpload%3A%3AAsync%3A%3AHostInventoryReportJob+or+label+%3D+ForemanInventoryUpload%3A%3AAsync%3A%3AGenerateAllReportsJob&page=1'
17
17
  );
18
18
 
19
19
  export const isExitCodeLoading = exitCode => {
@@ -1,12 +1,10 @@
1
1
  import { combineReducers } from 'redux';
2
2
  import { reducers as accountListReducers } from './Components/AccountList';
3
- import { reducers as dashboardReducers } from './Components/Dashboard';
4
3
  import { reducers as filterReducers } from './Components/InventoryFilter';
5
4
 
6
5
  export default {
7
6
  inventoryUpload: combineReducers({
8
7
  ...accountListReducers,
9
- ...dashboardReducers,
10
8
  ...filterReducers,
11
9
  }),
12
10
  };
@@ -2,4 +2,4 @@
2
2
 
3
3
  exports[`ForemanInventoryUpload helpers should return inventory Url 1`] = `"/foreman_inventory_upload/test_path"`;
4
4
 
5
- exports[`ForemanInventoryUpload helpers should return inventory docs url 1`] = `"/links/manual/?root_url=https%3A%2F%2Faccess.redhat.com%2Fdocumentation%2Fen-us%2Fred_hat_insights%2F2023%2Fhtml%2Fred_hat_insights_remediations_guide%2Fhost-communication-with-insights_red-hat-insights-remediation-guide%23uploading-satellite-host-inventory-to-insights_configuring-satellite-cloud-connector"`;
5
+ exports[`ForemanInventoryUpload helpers should return inventory docs url 1`] = `"/links/manual/?root_url=https%3A%2F%2Fdocs.redhat.com%2Fen%2Fdocumentation%2Fred_hat_lightspeed%2F1-latest%2Fhtml-single%2Fred_hat_lightspeed_remediations_guide%2Findex"`;
@@ -1,4 +1,3 @@
1
- /* eslint-disable spellcheck/spell-checker */
2
1
  import React from 'react';
3
2
  import componentRegistry from 'foremanReact/components/componentRegistry';
4
3
  import { registerRoutes as foremanRegisterRoutes } from 'foremanReact/routes/RoutingService';
@@ -1,26 +1,18 @@
1
- import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
2
-
1
+ import React from 'react';
2
+ import { rtlHelpers } from 'foremanReact/common/rtlTestHelpers';
3
3
  import InsightsTable from '../InsightsTable';
4
4
  import { tableProps } from './fixtures';
5
5
 
6
- jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
7
- useForemanContext: () => ({
8
- metadata: {
9
- foreman_rh_cloud: {
10
- iop: true,
11
- },
12
- },
13
- }),
14
- useForemanSettings: () => ({
15
- perPage: 20,
16
- }),
17
- }));
6
+ jest.mock('../../../../common/Hooks/ConfigHooks');
18
7
 
19
- const fixtures = {
20
- 'render with Props': tableProps,
21
- };
8
+ const { renderWithStore } = rtlHelpers;
22
9
 
23
10
  describe('InsightsTable', () => {
24
- describe('rendering', () =>
25
- testComponentSnapshotsWithFixtures(InsightsTable, fixtures));
11
+ afterEach(() => {
12
+ jest.clearAllMocks();
13
+ });
14
+
15
+ it('renders without crashing', () => {
16
+ renderWithStore(<InsightsTable {...tableProps} />);
17
+ });
26
18
  });
@@ -25,11 +25,11 @@ export const modifyRows = (
25
25
  ).map(({ id, host_id, hostname, title, resolutions, reboot }) => {
26
26
  hostsIdsToSubmit.add(host_id);
27
27
  const selectedResolution = resolutions[0]?.id;
28
- /* eslint-disable spellcheck/spell-checker */
29
28
 
30
29
  // For IoP:
31
30
  // All of the values will be plain strings
32
31
  // {
32
+ // eslint-disable-next-line spellcheck/spell-checker
33
33
  // hit_id: "c7c6727e-2966-4f7c-87f1-20ef14db7a2d", <-- this refers to a host by insights ID
34
34
  // rule_id: "hardening_ssh_client_alive|OPENSSH_HARDENING_CLIENT_ALIVE",
35
35
  // resolution_type: "less_secure",
@@ -41,7 +41,6 @@ export const modifyRows = (
41
41
  // rule_id refers to an InsightsRule
42
42
  // resolution_type and resolution_id both refer to an InsightsResolution (InsightsHit.find(xx).rule.resolutions)
43
43
 
44
- /* eslint-enable spellcheck/spell-checker */
45
44
  resolutionToSubmit.push({
46
45
  hit_id: isIop ? host_id : id,
47
46
  rule_id: id,
@@ -18,11 +18,10 @@ import TableEmptyState from '../../../common/table/EmptyState';
18
18
  import './RemediationModal.scss';
19
19
  import { useIopConfig } from '../../../common/Hooks/ConfigHooks';
20
20
 
21
- /* eslint-disable spellcheck/spell-checker */
22
-
23
21
  // Sample iopData:
24
22
  // const iopTestData = Immutable([
25
23
  // {
24
+ // eslint-disable-next-line spellcheck/spell-checker
26
25
  // hostid: 'c7c6727e-2966-4f7c-87f1-20ef14db7a2d',
27
26
  // host_name: 'advisor-test.local',
28
27
  // rulename: 'hardening_cryptopol_krb5|NO_CPOL_KRB5',
@@ -38,6 +37,7 @@ import { useIopConfig } from '../../../common/Hooks/ConfigHooks';
38
37
  // description: 'Decreased security: krb5 crypto-policies overridden',
39
38
  // },
40
39
  // {
40
+ // eslint-disable-next-line spellcheck/spell-checker
41
41
  // hostid: 'c7c6727e-2966-4f7c-87f1-20ef14db7a2d',
42
42
  // host_name: 'advisor-test.local',
43
43
  // rulename: 'hardening_logging_auditd|HARDENING_LOGGING_5_AUDITD',
@@ -54,8 +54,6 @@ import { useIopConfig } from '../../../common/Hooks/ConfigHooks';
54
54
  // },
55
55
  // ]);
56
56
 
57
- /* eslint-enable spellcheck/spell-checker */
58
-
59
57
  const RemediationModal = ({
60
58
  iopData,
61
59
  selectedIds,