katello 5.0.0.rc2 → 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 (243) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/katello/locale/bn/katello.js +41 -2
  3. data/app/assets/javascripts/katello/locale/bn_IN/katello.js +41 -2
  4. data/app/assets/javascripts/katello/locale/ca/katello.js +41 -2
  5. data/app/assets/javascripts/katello/locale/cs/katello.js +41 -2
  6. data/app/assets/javascripts/katello/locale/cs_CZ/katello.js +42 -3
  7. data/app/assets/javascripts/katello/locale/de/katello.js +42 -3
  8. data/app/assets/javascripts/katello/locale/de_AT/katello.js +41 -2
  9. data/app/assets/javascripts/katello/locale/de_DE/katello.js +41 -2
  10. data/app/assets/javascripts/katello/locale/el/katello.js +41 -2
  11. data/app/assets/javascripts/katello/locale/en/katello.js +41 -2
  12. data/app/assets/javascripts/katello/locale/en_GB/katello.js +41 -2
  13. data/app/assets/javascripts/katello/locale/en_US/katello.js +41 -2
  14. data/app/assets/javascripts/katello/locale/es/katello.js +42 -3
  15. data/app/assets/javascripts/katello/locale/et_EE/katello.js +41 -2
  16. data/app/assets/javascripts/katello/locale/fr/katello.js +42 -3
  17. data/app/assets/javascripts/katello/locale/gl/katello.js +41 -2
  18. data/app/assets/javascripts/katello/locale/gu/katello.js +41 -2
  19. data/app/assets/javascripts/katello/locale/he_IL/katello.js +41 -2
  20. data/app/assets/javascripts/katello/locale/hi/katello.js +41 -2
  21. data/app/assets/javascripts/katello/locale/id/katello.js +41 -2
  22. data/app/assets/javascripts/katello/locale/it/katello.js +42 -3
  23. data/app/assets/javascripts/katello/locale/ja/katello.js +42 -3
  24. data/app/assets/javascripts/katello/locale/ka/katello.js +42 -3
  25. data/app/assets/javascripts/katello/locale/kn/katello.js +41 -2
  26. data/app/assets/javascripts/katello/locale/ko/katello.js +42 -3
  27. data/app/assets/javascripts/katello/locale/ml_IN/katello.js +41 -2
  28. data/app/assets/javascripts/katello/locale/mr/katello.js +41 -2
  29. data/app/assets/javascripts/katello/locale/nl_NL/katello.js +41 -2
  30. data/app/assets/javascripts/katello/locale/or/katello.js +41 -2
  31. data/app/assets/javascripts/katello/locale/pa/katello.js +41 -2
  32. data/app/assets/javascripts/katello/locale/pl/katello.js +41 -2
  33. data/app/assets/javascripts/katello/locale/pl_PL/katello.js +41 -2
  34. data/app/assets/javascripts/katello/locale/pt/katello.js +41 -2
  35. data/app/assets/javascripts/katello/locale/pt_BR/katello.js +42 -3
  36. data/app/assets/javascripts/katello/locale/ro/katello.js +41 -2
  37. data/app/assets/javascripts/katello/locale/ro_RO/katello.js +41 -2
  38. data/app/assets/javascripts/katello/locale/ru/katello.js +42 -3
  39. data/app/assets/javascripts/katello/locale/sl/katello.js +41 -2
  40. data/app/assets/javascripts/katello/locale/sv_SE/katello.js +41 -2
  41. data/app/assets/javascripts/katello/locale/ta/katello.js +41 -2
  42. data/app/assets/javascripts/katello/locale/ta_IN/katello.js +41 -2
  43. data/app/assets/javascripts/katello/locale/te/katello.js +41 -2
  44. data/app/assets/javascripts/katello/locale/tr/katello.js +41 -2
  45. data/app/assets/javascripts/katello/locale/vi/katello.js +41 -2
  46. data/app/assets/javascripts/katello/locale/vi_VN/katello.js +41 -2
  47. data/app/assets/javascripts/katello/locale/zh/katello.js +41 -2
  48. data/app/assets/javascripts/katello/locale/zh_CN/katello.js +42 -3
  49. data/app/assets/javascripts/katello/locale/zh_TW/katello.js +42 -3
  50. data/app/controllers/katello/concerns/api/v2/hosts_controller_extensions.rb +10 -10
  51. data/app/controllers/katello/concerns/registration_commands_controller_extensions.rb +1 -1
  52. data/app/models/katello/concerns/smart_proxy_extensions.rb +1 -1
  53. data/app/views/katello/api/v2/activation_keys/base.json.rabl +18 -18
  54. data/app/views/katello/api/v2/content_facet/base.json.rabl +19 -19
  55. data/app/views/katello/api/v2/content_view_environments/show.json.rabl +10 -10
  56. data/app/views/katello/api/v2/content_view_versions/base.json.rabl +2 -2
  57. data/db/migrate/20220929204746_add_content_view_environment_content_facet.rb +5 -5
  58. data/db/migrate/20240730163043_add_content_view_environment_activation_key.rb +5 -5
  59. data/db/migrate/20260402151630_add_content_view_environment_to_hostgroup_content_facet.rb +4 -4
  60. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/activation-keys/details/views/activation-key-info.html +1 -1
  61. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/activation-keys/new/views/activation-key-new.html +1 -1
  62. data/lib/katello/tasks/upgrades/4.20/clean_cve_with_no_cvv.rake +4 -4
  63. data/lib/katello/version.rb +1 -1
  64. data/lib/rubocop/cop/katello/cve_abbreviation.rb +82 -0
  65. data/locale/bn/LC_MESSAGES/katello.mo +0 -0
  66. data/locale/bn/katello.po +41 -2
  67. data/locale/bn_IN/LC_MESSAGES/katello.mo +0 -0
  68. data/locale/bn_IN/katello.po +41 -2
  69. data/locale/ca/LC_MESSAGES/katello.mo +0 -0
  70. data/locale/ca/katello.po +41 -2
  71. data/locale/cs/LC_MESSAGES/katello.mo +0 -0
  72. data/locale/cs/katello.po +41 -2
  73. data/locale/cs_CZ/LC_MESSAGES/katello.mo +0 -0
  74. data/locale/cs_CZ/katello.po +42 -3
  75. data/locale/de/LC_MESSAGES/katello.mo +0 -0
  76. data/locale/de/katello.po +42 -3
  77. data/locale/de_AT/LC_MESSAGES/katello.mo +0 -0
  78. data/locale/de_AT/katello.po +41 -2
  79. data/locale/de_DE/LC_MESSAGES/katello.mo +0 -0
  80. data/locale/de_DE/katello.po +41 -2
  81. data/locale/el/LC_MESSAGES/katello.mo +0 -0
  82. data/locale/el/katello.po +41 -2
  83. data/locale/en/LC_MESSAGES/katello.mo +0 -0
  84. data/locale/en/katello.po +41 -2
  85. data/locale/en_GB/LC_MESSAGES/katello.mo +0 -0
  86. data/locale/en_GB/katello.po +41 -2
  87. data/locale/en_US/LC_MESSAGES/katello.mo +0 -0
  88. data/locale/en_US/katello.po +41 -2
  89. data/locale/es/LC_MESSAGES/katello.mo +0 -0
  90. data/locale/es/katello.po +42 -3
  91. data/locale/et_EE/LC_MESSAGES/katello.mo +0 -0
  92. data/locale/et_EE/katello.po +41 -2
  93. data/locale/fr/LC_MESSAGES/katello.mo +0 -0
  94. data/locale/fr/katello.po +42 -3
  95. data/locale/gl/LC_MESSAGES/katello.mo +0 -0
  96. data/locale/gl/katello.po +41 -2
  97. data/locale/gu/LC_MESSAGES/katello.mo +0 -0
  98. data/locale/gu/katello.po +41 -2
  99. data/locale/he_IL/LC_MESSAGES/katello.mo +0 -0
  100. data/locale/he_IL/katello.po +41 -2
  101. data/locale/hi/LC_MESSAGES/katello.mo +0 -0
  102. data/locale/hi/katello.po +41 -2
  103. data/locale/id/LC_MESSAGES/katello.mo +0 -0
  104. data/locale/id/katello.po +41 -2
  105. data/locale/it/LC_MESSAGES/katello.mo +0 -0
  106. data/locale/it/katello.po +42 -3
  107. data/locale/ja/LC_MESSAGES/katello.mo +0 -0
  108. data/locale/ja/katello.po +42 -3
  109. data/locale/ka/LC_MESSAGES/katello.mo +0 -0
  110. data/locale/ka/katello.po +42 -3
  111. data/locale/katello.pot +315 -257
  112. data/locale/kn/LC_MESSAGES/katello.mo +0 -0
  113. data/locale/kn/katello.po +41 -2
  114. data/locale/ko/LC_MESSAGES/katello.mo +0 -0
  115. data/locale/ko/katello.po +42 -3
  116. data/locale/ml_IN/LC_MESSAGES/katello.mo +0 -0
  117. data/locale/ml_IN/katello.po +41 -2
  118. data/locale/mr/LC_MESSAGES/katello.mo +0 -0
  119. data/locale/mr/katello.po +41 -2
  120. data/locale/nl_NL/LC_MESSAGES/katello.mo +0 -0
  121. data/locale/nl_NL/katello.po +41 -2
  122. data/locale/or/LC_MESSAGES/katello.mo +0 -0
  123. data/locale/or/katello.po +41 -2
  124. data/locale/pa/LC_MESSAGES/katello.mo +0 -0
  125. data/locale/pa/katello.po +41 -2
  126. data/locale/pl/LC_MESSAGES/katello.mo +0 -0
  127. data/locale/pl/katello.po +41 -2
  128. data/locale/pl_PL/LC_MESSAGES/katello.mo +0 -0
  129. data/locale/pl_PL/katello.po +41 -2
  130. data/locale/pt/LC_MESSAGES/katello.mo +0 -0
  131. data/locale/pt/katello.po +41 -2
  132. data/locale/pt_BR/LC_MESSAGES/katello.mo +0 -0
  133. data/locale/pt_BR/katello.po +42 -3
  134. data/locale/ro/LC_MESSAGES/katello.mo +0 -0
  135. data/locale/ro/katello.po +41 -2
  136. data/locale/ro_RO/LC_MESSAGES/katello.mo +0 -0
  137. data/locale/ro_RO/katello.po +41 -2
  138. data/locale/ru/LC_MESSAGES/katello.mo +0 -0
  139. data/locale/ru/katello.po +42 -3
  140. data/locale/sl/LC_MESSAGES/katello.mo +0 -0
  141. data/locale/sl/katello.po +41 -2
  142. data/locale/sv_SE/LC_MESSAGES/katello.mo +0 -0
  143. data/locale/sv_SE/katello.po +41 -2
  144. data/locale/ta/LC_MESSAGES/katello.mo +0 -0
  145. data/locale/ta/katello.po +41 -2
  146. data/locale/ta_IN/LC_MESSAGES/katello.mo +0 -0
  147. data/locale/ta_IN/katello.po +41 -2
  148. data/locale/te/LC_MESSAGES/katello.mo +0 -0
  149. data/locale/te/katello.po +41 -2
  150. data/locale/tr/LC_MESSAGES/katello.mo +0 -0
  151. data/locale/tr/katello.po +41 -2
  152. data/locale/vi/LC_MESSAGES/katello.mo +0 -0
  153. data/locale/vi/katello.po +41 -2
  154. data/locale/vi_VN/LC_MESSAGES/katello.mo +0 -0
  155. data/locale/vi_VN/katello.po +41 -2
  156. data/locale/zh/LC_MESSAGES/katello.mo +0 -0
  157. data/locale/zh/katello.po +41 -2
  158. data/locale/zh_CN/LC_MESSAGES/katello.mo +0 -0
  159. data/locale/zh_CN/katello.po +42 -3
  160. data/locale/zh_TW/LC_MESSAGES/katello.mo +0 -0
  161. data/locale/zh_TW/katello.po +42 -3
  162. data/package.json +2 -2
  163. data/webpack/ForemanColumnExtensions/index.js +1 -1
  164. data/webpack/components/extensions/HostDetails/Cards/ContentViewDetailsCard/AssignHostCVModal.js +3 -3
  165. data/webpack/components/extensions/HostDetails/Cards/ContentViewDetailsCard/ContentViewDetailsCard.js +5 -5
  166. data/webpack/components/extensions/HostDetails/Cards/ContentViewDetailsCard/OrderableAssignments.js +5 -5
  167. data/webpack/components/extensions/HostDetails/Cards/ContentViewDetailsCard/__tests__/AssignHostCVModal.test.js +17 -17
  168. data/webpack/components/extensions/Hosts/BulkActions/BulkAssignCVEnvsModal/BulkAssignCVEnvsModal.js +4 -8
  169. data/webpack/components/extensions/Hosts/BulkActions/BulkAssignCVEnvsModal/__tests__/BulkAssignCVEnvsModal.test.js +1 -1
  170. data/webpack/components/extensions/Hosts/BulkActions/BulkAssignCVEnvsModal/index.js +2 -1
  171. data/webpack/components/extensions/Hosts/BulkActions/BulkChangeHostCollectionsModal/BulkChangeHostCollectionsModal.js +5 -10
  172. data/webpack/components/extensions/Hosts/BulkActions/BulkChangeHostCollectionsModal/__tests__/BulkChangeHostCollectionsModal.test.js +6 -24
  173. data/webpack/components/extensions/Hosts/BulkActions/BulkChangeHostCollectionsModal/index.js +2 -1
  174. data/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/04_ReviewFooter.js +2 -0
  175. data/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/BulkErrataWizard.js +2 -1
  176. data/webpack/components/extensions/Hosts/BulkActions/BulkManageTracesModal/BulkManageTracesModal.js +4 -0
  177. data/webpack/components/extensions/Hosts/BulkActions/BulkManageTracesModal/index.js +2 -1
  178. data/webpack/components/extensions/Hosts/BulkActions/BulkPackagesWizard/04_ReviewFooter.js +2 -0
  179. data/webpack/components/extensions/Hosts/BulkActions/BulkPackagesWizard/BulkPackagesWizard.js +2 -1
  180. data/webpack/components/extensions/Hosts/BulkActions/BulkRepositorySetsWizard/03_ReviewFooter.js +2 -0
  181. data/webpack/components/extensions/Hosts/BulkActions/BulkRepositorySetsWizard/BulkRepositorySetsWizard.js +2 -1
  182. data/webpack/components/extensions/Hosts/BulkActions/BulkSystemPurposeModal/BulkSystemPurposeModal.js +5 -0
  183. data/webpack/components/extensions/Hosts/BulkActions/BulkSystemPurposeModal/index.js +2 -1
  184. data/webpack/components/extensions/Hosts/BulkActions/__tests__/bulkSystemPurposeModal.test.js +6 -1
  185. data/webpack/components/extensions/RegistrationCommands/__tests__/ActivationKeys.test.js +6 -6
  186. data/webpack/components/extensions/RegistrationCommands/fields/ActivationKeys.js +1 -1
  187. data/webpack/containers/Application/config.js +1 -1
  188. data/webpack/eslint-rules/no-cve-abbreviation.js +121 -0
  189. data/webpack/global_index.js +6 -6
  190. data/webpack/redux/reducers/index.js +0 -2
  191. data/webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.js +2 -2
  192. data/webpack/scenes/ActivationKeys/Details/components/AssignAKCVModal.js +3 -3
  193. data/webpack/scenes/ActivationKeys/Details/components/CreateAKCVModal.js +1 -1
  194. data/webpack/scenes/ActivationKeys/Details/components/{CVEDetailsCard.js → CvEnvDetailsCard.js} +9 -9
  195. data/webpack/scenes/ActivationKeys/Details/components/{CVEDetailsCreateCard.js → CvEnvDetailsCreateCard.js} +10 -10
  196. data/webpack/scenes/ActivationKeys/Details/components/__tests__/{CVEDetailsCreateCard.test.js → CvEnvDetailsCreateCard.test.js} +8 -8
  197. data/webpack/scenes/ContainerImages/Booted/BootedContainerImagesPage.js +2 -2
  198. data/webpack/scenes/ContainerImages/ContainerImagesPage.js +59 -52
  199. data/webpack/scenes/ContainerImages/ContainerImagesPage.scss +27 -4
  200. data/webpack/scenes/ContainerImages/Synced/SyncedContainerImagesPage.js +1 -1
  201. data/webpack/scenes/ContentViews/Details/Versions/Delete/RemoveSteps/CVVersionRemoveReview.js +9 -9
  202. data/webpack/scenes/Hosts/ChangeContentSource/actions.js +2 -2
  203. data/webpack/scenes/Subscriptions/Manifest/CdnTabContent.js +44 -0
  204. data/webpack/scenes/Subscriptions/Manifest/ManageManifestModal.js +179 -307
  205. data/webpack/scenes/Subscriptions/Manifest/ManageManifestModalConstants.js +5 -0
  206. data/webpack/scenes/Subscriptions/Manifest/ManifestActions.js +64 -70
  207. data/webpack/scenes/Subscriptions/Manifest/ManifestConstants.js +5 -0
  208. data/webpack/scenes/Subscriptions/Manifest/ManifestHistoryContent.js +61 -0
  209. data/webpack/scenes/Subscriptions/Manifest/ManifestHistoryReducer.js +1 -1
  210. data/webpack/scenes/Subscriptions/Manifest/ManifestTabContent.js +357 -0
  211. data/webpack/scenes/Subscriptions/Manifest/__tests__/ManageManifestModal.test.js +129 -0
  212. data/webpack/scenes/Subscriptions/Manifest/__tests__/ManifestActions.test.js +69 -110
  213. data/webpack/scenes/Subscriptions/Manifest/__tests__/ManifestHistoryReducer.test.js +6 -2
  214. data/webpack/scenes/Subscriptions/Manifest/index.js +16 -1
  215. data/webpack/scenes/Subscriptions/SubscriptionActions.js +79 -145
  216. data/webpack/scenes/Subscriptions/SubscriptionConstants.js +4 -35
  217. data/webpack/scenes/Subscriptions/SubscriptionsColumns.js +90 -0
  218. data/webpack/scenes/Subscriptions/SubscriptionsPage.js +297 -217
  219. data/webpack/scenes/Subscriptions/SubscriptionsPage.scss +40 -30
  220. data/webpack/scenes/Subscriptions/UpstreamSubscriptions/UpstreamSubscriptionsActions.js +11 -8
  221. data/webpack/scenes/Subscriptions/UpstreamSubscriptions/UpstreamSubscriptionsConstants.js +0 -1
  222. data/webpack/scenes/Subscriptions/__tests__/SubscriptionsPage.test.js +104 -205
  223. data/webpack/scenes/Subscriptions/__tests__/subscriptions.fixtures.js +12 -8
  224. data/webpack/scenes/Subscriptions/components/SubscriptionsTable/SubscriptionsTable.js +245 -241
  225. data/webpack/scenes/Subscriptions/components/SubscriptionsTable/__tests__/SubscriptionsTable.test.js +61 -27
  226. data/webpack/scenes/Subscriptions/components/SubscriptionsTable/components/Table.js +487 -73
  227. data/webpack/scenes/Subscriptions/components/SubscriptionsToolbar/SubscriptionsToolbar.js +22 -4
  228. data/webpack/utils/contentViewEnvironmentLabel.js +3 -3
  229. data/webpack/utils/helpers.js +2 -2
  230. metadata +15 -20
  231. data/webpack/scenes/Subscriptions/Manifest/ManageManifestModal.scss +0 -39
  232. data/webpack/scenes/Subscriptions/Manifest/ManifestHistoryTableSchema.js +0 -47
  233. data/webpack/scenes/Subscriptions/SubscriptionReducer.js +0 -223
  234. data/webpack/scenes/Subscriptions/SubscriptionsSelectors.js +0 -31
  235. data/webpack/scenes/Subscriptions/__tests__/SubscriptionsActions.test.js +0 -297
  236. data/webpack/scenes/Subscriptions/__tests__/SubscriptionsReducer.test.js +0 -244
  237. data/webpack/scenes/Subscriptions/__tests__/SubscriptionsSelectors.test.js +0 -47
  238. data/webpack/scenes/Subscriptions/components/SubscriptionsTable/SubscriptionNameFormatter.js +0 -23
  239. data/webpack/scenes/Subscriptions/components/SubscriptionsTable/SubscriptionTypeFormatter.js +0 -32
  240. data/webpack/scenes/Subscriptions/components/SubscriptionsTable/SubscriptionsTableSchema.js +0 -169
  241. data/webpack/scenes/Subscriptions/components/SubscriptionsTable/__tests__/SubscriptionTypeFormatter.test.js +0 -41
  242. data/webpack/scenes/Subscriptions/components/SubscriptionsTable/index.js +0 -2
  243. data/webpack/scenes/Subscriptions/index.js +0 -57
@@ -1,16 +1,238 @@
1
- import React from 'react';
1
+ import React, { useMemo, useCallback, useEffect, useRef } from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { Table as PFtable } from 'patternfly-react';
4
- import { translate as __ } from 'foremanReact/common/I18n';
5
- import classNames from 'classnames';
6
- import { createSubscriptionsTableSchema } from '../SubscriptionsTableSchema';
7
- import { Table as ForemanTable, TableBody as ForemanTableBody } from '../../../../../components/pf3Table';
3
+ import {
4
+ Button,
5
+ Checkbox,
6
+ FormGroup,
7
+ FormHelperText,
8
+ HelperText,
9
+ HelperTextItem,
10
+ Spinner,
11
+ TextInput,
12
+ ToolbarItem,
13
+ } from '@patternfly/react-core';
14
+ import { AngleDownIcon, AngleRightIcon } from '@patternfly/react-icons';
15
+ import { Tr, Td } from '@patternfly/react-table';
16
+ import TableIndexPage from 'foremanReact/components/PF4/TableIndexPage/TableIndexPage';
17
+ import { Table as TableIndexTable } from 'foremanReact/components/PF4/TableIndexPage/Table/Table';
18
+ import {
19
+ useTableIndexAPIResponse,
20
+ useSetParamsAndApiAndSearch,
21
+ } from 'foremanReact/components/PF4/TableIndexPage/Table/TableIndexHooks';
22
+ import { translate as __, sprintf } from 'foremanReact/common/I18n';
23
+ import { STATUS } from 'foremanReact/constants';
24
+ import { noop } from 'foremanReact/common/helpers';
25
+ import EmptyState from 'foremanReact/components/common/EmptyState';
26
+ import { orgId } from '../../../../../services/api';
27
+ import { SUBSCRIPTIONS_TABLE_KEY } from '../../../SubscriptionConstants';
28
+ import { validateQuantity } from '../../../SubscriptionValidations';
29
+ import { getEntitlementsDisplayValue } from '../SubscriptionsTableHelpers';
30
+
31
+ const SUBSCRIPTIONS_API_URL = '/katello/api/v2/subscriptions';
32
+
33
+ const renderEntitlementsCell = (rowData, rowIndex, inlineEditController) => {
34
+ const additionalData = { rowData, rowIndex };
35
+ const {
36
+ hasChanged, onChange, onActivate, isEditing,
37
+ } = inlineEditController;
38
+ const value = rowData.quantity;
39
+
40
+ if (isEditing(additionalData)) {
41
+ const {
42
+ upstreamAvailable, upstreamAvailableLoaded, maxQuantity,
43
+ } = rowData;
44
+
45
+ const className = hasChanged(additionalData)
46
+ ? 'editable editing changed'
47
+ : 'editable editing';
48
+
49
+ let maxMessage;
50
+ if (maxQuantity && upstreamAvailableLoaded && (upstreamAvailable !== undefined)) {
51
+ maxMessage = (upstreamAvailable < 0)
52
+ ? __('Unlimited')
53
+ : sprintf(__('Max %(maxQuantity)s'), { maxQuantity });
54
+ }
55
+
56
+ const validation = validateQuantity(value, maxQuantity);
57
+
58
+ const formGroup = !upstreamAvailableLoaded ? (
59
+ <Spinner size="sm" />
60
+ ) : (
61
+ <FormGroup>
62
+ <TextInput
63
+ type="text"
64
+ defaultValue={value}
65
+ validated={validation.state}
66
+ onChange={(_event, newValue) => onChange(newValue, additionalData)}
67
+ ouiaId={`edit-entitlements-${rowData.id ?? rowIndex}`}
68
+ />
69
+ <FormHelperText>
70
+ <HelperText>
71
+ <HelperTextItem variant={validation.state}>
72
+ {maxMessage}
73
+ <div className="validationMessages">
74
+ {validation.message}
75
+ </div>
76
+ </HelperTextItem>
77
+ </HelperText>
78
+ </FormHelperText>
79
+ </FormGroup>
80
+ );
81
+
82
+ return (
83
+ <Td key="quantity" dataLabel={__('Entitlements')} className={className}>
84
+ {formGroup}
85
+ </Td>
86
+ );
87
+ }
88
+
89
+ const displayValue = getEntitlementsDisplayValue({
90
+ quantity: value,
91
+ collapsible: rowData.collapsible,
92
+ });
93
+ const editable = (typeof displayValue === 'number' && !rowData.expired);
94
+
95
+ return (
96
+ <Td key="quantity" dataLabel={__('Entitlements')} className={editable ? 'editable' : undefined}>
97
+ {editable ? (
98
+ <div
99
+ onClick={() => onActivate(additionalData)}
100
+ onKeyDown={(e) => {
101
+ if (e.key === 'Enter' || e.key === ' ') {
102
+ e.preventDefault();
103
+ onActivate(additionalData);
104
+ }
105
+ }}
106
+ className="input"
107
+ role="button"
108
+ aria-label={__('Edit entitlements')}
109
+ tabIndex={0}
110
+ >
111
+ {displayValue}
112
+ </div>
113
+ ) : displayValue}
114
+ </Td>
115
+ );
116
+ };
117
+
118
+ const renderSubscriptionRows = ({
119
+ columnKeys,
120
+ columns,
121
+ rows,
122
+ bodyMessage,
123
+ groupingController,
124
+ selectionController,
125
+ selectionEnabled,
126
+ inlineEditController,
127
+ }) => {
128
+ if (bodyMessage) {
129
+ return (
130
+ <Tr ouiaId="subscriptions-table-body-message">
131
+ <Td colSpan={columnKeys.length}>{bodyMessage}</Td>
132
+ </Tr>
133
+ );
134
+ }
135
+
136
+ return rows.map((rowData, rowIndex) => {
137
+ const additionalData = { rowData, rowIndex };
138
+ const isGenericRow = rowData.collapsible;
139
+ const shouldShowCollapse = groupingController.isCollapseable(additionalData);
140
+ const disabled = !selectionEnabled || !rowData.upstream_pool_id;
141
+
142
+ return (
143
+ <Tr
144
+ key={rowData.id ?? rowIndex}
145
+ ouiaId={`subscriptions-table-row-${rowData.id ?? rowIndex}`}
146
+ className={!groupingController.isCollapsed(additionalData) ? 'open-grouped-row' : ''}
147
+ >
148
+ {columnKeys.map((key) => {
149
+ if (key === 'select') {
150
+ return (
151
+ <Td key="select" dataLabel={__('Select row')} className="subscriptions-table-select">
152
+ <div className="subscriptions-table-select-cell">
153
+ {shouldShowCollapse && (
154
+ <Button
155
+ ouiaId={`collapse-subscription-group-${rowData.id ?? rowIndex}`}
156
+ variant="plain"
157
+ className="collapse-subscription-group-button"
158
+ onClick={() => groupingController.toggle(additionalData)}
159
+ aria-label={
160
+ groupingController.isCollapsed(additionalData)
161
+ ? __('Expand group')
162
+ : __('Collapse group')
163
+ }
164
+ icon={
165
+ groupingController.isCollapsed(additionalData)
166
+ ? <AngleRightIcon />
167
+ : <AngleDownIcon />
168
+ }
169
+ />
170
+ )}
171
+ {!isGenericRow && (
172
+ <Checkbox
173
+ ouiaId={`select-subscription-row-${rowData.id ?? rowIndex}`}
174
+ id={`select${rowIndex}`}
175
+ isChecked={selectionController.isSelected(additionalData)}
176
+ onChange={() => selectionController.selectRow(additionalData)}
177
+ isDisabled={disabled}
178
+ aria-label={__('Select row')}
179
+ />
180
+ )}
181
+ </div>
182
+ </Td>
183
+ );
184
+ }
185
+
186
+ if (key === 'quantity') {
187
+ return renderEntitlementsCell(rowData, rowIndex, inlineEditController);
188
+ }
189
+
190
+ const column = columns[key];
191
+ const content = column?.wrapper ? column.wrapper(rowData) : rowData[key];
192
+
193
+ return (
194
+ <Td key={key} dataLabel={column?.title}>
195
+ {content}
196
+ </Td>
197
+ );
198
+ })}
199
+ </Tr>
200
+ );
201
+ });
202
+ };
203
+
204
+ const extractMissingPermissions = (status, response) => {
205
+ if (status !== STATUS.ERROR) {
206
+ return undefined;
207
+ }
208
+
209
+ const error = response;
210
+ const errors = error?.response?.data?.errors;
211
+ const explicitMissingPermissions =
212
+ (Array.isArray(errors) && errors[0]?.missing_permissions) ||
213
+ error?.response?.data?.missing_permissions;
214
+ const statusCode = error?.response?.status;
215
+ let errorMessages = [];
216
+ if (error?.response?.data?.displayMessage) {
217
+ errorMessages = [error.response.data.displayMessage];
218
+ } else if (Array.isArray(errors)) {
219
+ errorMessages = errors.filter(e => typeof e === 'string');
220
+ }
221
+
222
+ let missingPermissions = explicitMissingPermissions;
223
+ if (!missingPermissions && (statusCode === 403 || statusCode === 404)) {
224
+ missingPermissions = errorMessages.length > 0 ? errorMessages : ['view_subscriptions'];
225
+ }
226
+ return missingPermissions;
227
+ };
8
228
 
9
229
  const Table = ({
10
230
  emptyState,
11
231
  tableColumns,
12
- subscriptions,
13
- loadSubscriptions,
232
+ columns,
233
+ searchQuery,
234
+ organizationId,
235
+ availableQuantities,
14
236
  selectionController,
15
237
  inlineEditController,
16
238
  rows,
@@ -18,105 +240,297 @@ const Table = ({
18
240
  selectionEnabled,
19
241
  groupedSubscriptions,
20
242
  toggleSubscriptionGroup,
243
+ customHeader,
244
+ customToolbar,
245
+ onApiResponse,
246
+ onRefreshReady,
21
247
  }) => {
22
- const allSubscriptionResults = subscriptions.results;
248
+ const lastNotifiedRef = useRef(null);
249
+
250
+ const persistentParams = useMemo(() => ({
251
+ organization_id: organizationId || orgId(),
252
+ ...(searchQuery ? { search: searchQuery } : {}),
253
+ }), [organizationId, searchQuery]);
254
+
255
+ const defaultParams = useMemo(() => ({
256
+ page: 1,
257
+ per_page: 20,
258
+ ...persistentParams,
259
+ }), [persistentParams]);
260
+
261
+ const apiOptions = useMemo(() => ({ key: SUBSCRIPTIONS_TABLE_KEY }), []);
262
+
263
+ const originalResponse = useTableIndexAPIResponse({
264
+ apiUrl: SUBSCRIPTIONS_API_URL,
265
+ apiOptions,
266
+ defaultParams,
267
+ });
268
+
269
+ const {
270
+ response: apiResponse = {},
271
+ status = STATUS.PENDING,
272
+ setAPIOptions,
273
+ } = originalResponse;
274
+
275
+ const wrappedSetAPIOptions = useCallback((options) => {
276
+ setAPIOptions({
277
+ ...options,
278
+ params: {
279
+ ...persistentParams,
280
+ ...options?.params,
281
+ },
282
+ });
283
+ }, [setAPIOptions, persistentParams]);
284
+
285
+ // Refetch when org or search changes
286
+ useEffect(() => {
287
+ wrappedSetAPIOptions({ ...apiOptions, params: defaultParams });
288
+ }, [searchQuery, organizationId]); // eslint-disable-line react-hooks/exhaustive-deps
289
+
290
+ useEffect(() => {
291
+ if (onRefreshReady) {
292
+ onRefreshReady(() => wrappedSetAPIOptions({ ...apiOptions, params: defaultParams }));
293
+ }
294
+ }, [onRefreshReady, wrappedSetAPIOptions, apiOptions, defaultParams]);
295
+
296
+ useEffect(() => {
297
+ if (!onApiResponse) {
298
+ return;
299
+ }
300
+ const quantitiesKey = JSON.stringify(availableQuantities);
301
+ const notifyKey = `${status}:${apiResponse?.page}:${apiResponse?.subtotal}:${(apiResponse?.results || []).length}:${quantitiesKey}`;
302
+ if (lastNotifiedRef.current === notifyKey) {
303
+ return;
304
+ }
305
+ lastNotifiedRef.current = notifyKey;
306
+
307
+ const missingPermissions = extractMissingPermissions(status, apiResponse);
308
+ onApiResponse({
309
+ results: apiResponse.results || [],
310
+ page: Number(apiResponse.page) || 1,
311
+ perPage: Number(apiResponse.per_page) || 20,
312
+ itemCount: Number(apiResponse.subtotal) || 0,
313
+ loading: status === STATUS.PENDING,
314
+ searchIsActive: !!searchQuery,
315
+ availableQuantities,
316
+ missingPermissions,
317
+ activePermissions: {
318
+ can_import_manifest: apiResponse.can_import_manifest,
319
+ can_delete_manifest: apiResponse.can_delete_manifest,
320
+ can_manage_subscription_allocations: apiResponse.can_manage_subscription_allocations,
321
+ can_edit_organizations: apiResponse.can_edit_organizations,
322
+ },
323
+ });
324
+ }, [apiResponse, status, searchQuery, availableQuantities, onApiResponse]);
23
325
 
24
326
  let bodyMessage;
25
- if (allSubscriptionResults.length === 0 && subscriptions.searchIsActive) {
327
+ if ((apiResponse.results || []).length === 0 && searchQuery) {
26
328
  bodyMessage = __('No subscriptions match your search criteria.');
27
329
  }
28
330
 
29
331
  const groupingController = {
30
- isCollapseable: ({ rowData }) =>
31
- // the group contains more than one subscription
32
- rowData.collapsible,
33
- isCollapsed: ({ rowData }) => !groupedSubscriptions[rowData.product_id].open,
332
+ isCollapseable: ({ rowData }) => rowData.collapsible,
333
+ isCollapsed: ({ rowData }) => !groupedSubscriptions[rowData.product_id]?.open,
34
334
  toggle: ({ rowData }) => toggleSubscriptionGroup(rowData.product_id),
35
335
  };
36
336
 
37
337
  const collapsibleGroup =
38
- Object.values(groupedSubscriptions).some(v => v.subscriptions.length > 1);
338
+ Object.values(groupedSubscriptions || {}).some(v => v.subscriptions.length > 1);
39
339
 
40
- const alwaysDisplayColumns = [];
340
+ const showSelectColumn = selectionEnabled || collapsibleGroup;
41
341
 
42
- if (selectionEnabled || collapsibleGroup) {
43
- alwaysDisplayColumns.push('select');
44
- }
342
+ const visibleColumns = useMemo(() => {
343
+ const nextColumns = {};
344
+
345
+ if (showSelectColumn) {
346
+ nextColumns.select = {
347
+ title: (
348
+ <div className="subscriptions-table-select-cell">
349
+ <Checkbox
350
+ ouiaId="select-all-subscriptions"
351
+ id="selectAll"
352
+ aria-label={__('Select all rows')}
353
+ isChecked={selectionController.allRowsSelected()}
354
+ onChange={() => selectionController.selectAllRows()}
355
+ isDisabled={!selectionEnabled}
356
+ />
357
+ </div>
358
+ ),
359
+ };
360
+ }
45
361
 
46
- const columnsDefinition = createSubscriptionsTableSchema(
47
- inlineEditController,
48
- selectionController,
49
- groupingController,
50
- selectionEnabled,
51
- ).filter(column => tableColumns.includes(column.property) ||
52
- alwaysDisplayColumns.includes(column.property));
53
-
54
- const onPaginationChange = (pagination) => {
55
- loadSubscriptions({
56
- ...pagination,
362
+ Object.keys(columns).forEach((key) => {
363
+ if (tableColumns.includes(key)) {
364
+ nextColumns[key] = columns[key];
365
+ }
57
366
  });
58
- };
59
367
 
368
+ return nextColumns;
369
+ }, [columns, tableColumns, showSelectColumn, selectionController, selectionEnabled]);
370
+
371
+ const columnKeys = useMemo(() => Object.keys(visibleColumns), [visibleColumns]);
372
+
373
+ const replacementResponse = useMemo(() => ({
374
+ response: {
375
+ results: rows,
376
+ subtotal: apiResponse.subtotal,
377
+ // TableIndexPage shows top pagination when total > 0; keep only bottom pagination
378
+ total: 0,
379
+ page: apiResponse.page || 1,
380
+ per_page: apiResponse.per_page || 20,
381
+ },
382
+ status: status === STATUS.PENDING ? STATUS.PENDING : STATUS.RESOLVED,
383
+ setAPIOptions: wrappedSetAPIOptions,
384
+ }), [rows, apiResponse, status, wrappedSetAPIOptions]);
385
+
386
+ const response = useTableIndexAPIResponse({
387
+ replacementResponse,
388
+ apiUrl: SUBSCRIPTIONS_API_URL,
389
+ apiOptions,
390
+ defaultParams,
391
+ });
392
+
393
+ const {
394
+ response: {
395
+ page,
396
+ per_page: perPage,
397
+ subtotal,
398
+ } = {},
399
+ setAPIOptions: setAPIOptionsFromResponse,
400
+ } = response;
401
+
402
+ const { setParamsAndAPI, params } = useSetParamsAndApiAndSearch({
403
+ defaultParams: {
404
+ page: page || defaultParams.page,
405
+ per_page: perPage || defaultParams.per_page,
406
+ ...persistentParams,
407
+ },
408
+ apiOptions,
409
+ setAPIOptions: setAPIOptionsFromResponse,
410
+ pushToHistory: false,
411
+ });
412
+
413
+ const isPending = status === STATUS.PENDING;
414
+ const showCustomEmptyState = !isPending && rows.length === 0 && !bodyMessage && emptyState;
415
+ const customEmptyState = showCustomEmptyState ? (
416
+ <Tr ouiaId="table-empty">
417
+ <Td colSpan={100}>
418
+ <EmptyState {...emptyState} />
419
+ </Td>
420
+ </Tr>
421
+ ) : null;
422
+
423
+ const customToolbarItems = editing ? (
424
+ <>
425
+ <ToolbarItem>
426
+ <Button
427
+ ouiaId="confirm-subscription-edit-button"
428
+ variant="primary"
429
+ onClick={inlineEditController.onConfirm}
430
+ >
431
+ {__('Confirm')}
432
+ </Button>
433
+ </ToolbarItem>
434
+ <ToolbarItem>
435
+ <Button
436
+ ouiaId="cancel-subscription-edit-button"
437
+ variant="link"
438
+ onClick={inlineEditController.onCancel}
439
+ >
440
+ {__('Cancel')}
441
+ </Button>
442
+ </ToolbarItem>
443
+ </>
444
+ ) : null;
60
445
 
61
446
  return (
62
- <ForemanTable
63
- columns={columnsDefinition}
64
- emptyState={emptyState}
65
- bodyMessage={bodyMessage}
66
- rows={rows}
67
- components={{
68
- header: {
69
- row: PFtable.TableInlineEditHeaderRow,
70
- },
71
- }}
72
- itemCount={subscriptions.itemCount}
73
- pagination={subscriptions.pagination}
74
- onPaginationChange={onPaginationChange}
75
- inlineEdit
447
+ <TableIndexPage
448
+ header={__('Subscriptions')}
449
+ customHeader={customHeader}
450
+ beforeToolbarComponent={customToolbar}
451
+ apiUrl={SUBSCRIPTIONS_API_URL}
452
+ apiOptions={apiOptions}
453
+ columns={visibleColumns}
454
+ replacementResponse={response}
455
+ searchable={false}
456
+ creatable={false}
457
+ updateParamsByUrl={false}
458
+ customToolbarItems={customToolbarItems}
459
+ ouiaId="subscriptions-table"
76
460
  >
77
- <PFtable.Header
78
- onRow={() => ({
79
- role: 'row',
80
- isEditing: () => editing,
81
- onCancel: () => inlineEditController.onCancel(),
82
- onConfirm: () => inlineEditController.onConfirm(),
461
+ <TableIndexTable
462
+ columns={visibleColumns}
463
+ results={rows}
464
+ customEmptyState={customEmptyState}
465
+ emptyMessage={bodyMessage}
466
+ params={{
467
+ ...params,
468
+ page: page || params.page,
469
+ perPage: perPage || params.per_page,
470
+ }}
471
+ setParams={setParamsAndAPI}
472
+ itemCount={subtotal ?? apiResponse.subtotal}
473
+ refreshData={noop}
474
+ url={SUBSCRIPTIONS_API_URL}
475
+ isPending={isPending}
476
+ isEmbedded
477
+ ouiaId="subscriptions-table"
478
+ >
479
+ {renderSubscriptionRows({
480
+ columnKeys,
481
+ columns: visibleColumns,
482
+ rows,
483
+ bodyMessage,
484
+ groupingController,
485
+ selectionController,
486
+ selectionEnabled,
487
+ inlineEditController,
83
488
  })}
84
- />
85
- <ForemanTableBody
86
- columns={columnsDefinition}
87
- rows={rows}
88
- rowKey="id"
89
- message={bodyMessage}
90
- onRow={rowData => ({
91
- className: classNames({ 'open-grouped-row': !groupingController.isCollapsed({ rowData }) }),
92
- })}
93
- />
94
- </ForemanTable>
489
+ </TableIndexTable>
490
+ </TableIndexPage>
95
491
  );
96
492
  };
97
493
 
98
494
  Table.propTypes = {
99
495
  emptyState: PropTypes.shape({}).isRequired,
100
496
  tableColumns: PropTypes.arrayOf(PropTypes.string).isRequired,
101
- subscriptions: PropTypes.shape({
102
- searchIsActive: PropTypes.bool,
103
- itemCount: PropTypes.number,
104
- pagination: PropTypes.shape({}),
105
- // Disabling rule as existing code failed due to an eslint-plugin-react update
106
- // eslint-disable-next-line react/forbid-prop-types
107
- results: PropTypes.array,
108
- }).isRequired,
109
- loadSubscriptions: PropTypes.func.isRequired,
497
+ columns: PropTypes.objectOf(PropTypes.shape({
498
+ title: PropTypes.node,
499
+ wrapper: PropTypes.func,
500
+ })).isRequired,
501
+ searchQuery: PropTypes.string,
502
+ organizationId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
503
+ availableQuantities: PropTypes.shape({}),
110
504
  toggleSubscriptionGroup: PropTypes.func.isRequired,
111
- selectionController: PropTypes.shape({}).isRequired,
505
+ selectionController: PropTypes.shape({
506
+ allRowsSelected: PropTypes.func,
507
+ selectAllRows: PropTypes.func,
508
+ isSelected: PropTypes.func,
509
+ selectRow: PropTypes.func,
510
+ }).isRequired,
112
511
  inlineEditController: PropTypes.shape({
113
512
  onCancel: PropTypes.func,
114
513
  onConfirm: PropTypes.func,
115
514
  }).isRequired,
116
- groupedSubscriptions: PropTypes.shape({}).isRequired,
515
+ groupedSubscriptions: PropTypes.shape({}),
117
516
  editing: PropTypes.bool.isRequired,
118
517
  rows: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
119
518
  selectionEnabled: PropTypes.bool.isRequired,
519
+ customHeader: PropTypes.node,
520
+ customToolbar: PropTypes.node,
521
+ onApiResponse: PropTypes.func,
522
+ onRefreshReady: PropTypes.func,
523
+ };
524
+
525
+ Table.defaultProps = {
526
+ searchQuery: '',
527
+ organizationId: undefined,
528
+ availableQuantities: null,
529
+ customHeader: undefined,
530
+ customToolbar: undefined,
531
+ groupedSubscriptions: {},
532
+ onApiResponse: undefined,
533
+ onRefreshReady: undefined,
120
534
  };
121
535
 
122
536
  export default Table;
@@ -32,6 +32,7 @@ const SubscriptionsToolbar = ({
32
32
  disableAddButton,
33
33
  autocompleteQueryParams,
34
34
  updateSearchQuery,
35
+ searchQuery,
35
36
  onDeleteButtonClick,
36
37
  onSearch,
37
38
  onManageManifestButtonClick,
@@ -71,22 +72,37 @@ const SubscriptionsToolbar = ({
71
72
  hasPreference,
72
73
  };
73
74
 
75
+ const searchProps = getControllerSearchProps(
76
+ '/katello/api/v2/subscriptions',
77
+ 'searchBar-katello_subscriptions',
78
+ true,
79
+ autocompleteQueryParams,
80
+ );
81
+
74
82
  return (
75
83
  <Toolbar id="subscriptions-toolbar" ouiaId="subscriptions-toolbar">
76
- <ToolbarContent>
77
- <ToolbarGroup variant="filter-group" className="subscriptions-filter-group">
84
+ <ToolbarContent className="subscriptions-toolbar-content">
85
+ <ToolbarGroup
86
+ variant="filter-group"
87
+ className="subscriptions-filter-group"
88
+ >
78
89
  <ToolbarItem variant="search-filter">
79
90
  <SearchBar
80
91
  data={{
81
- ...getControllerSearchProps('/katello/api/v2/subscriptions', 'searchBar-katello_subscriptions', true, autocompleteQueryParams),
92
+ ...searchProps,
82
93
  controller: 'katello_subscriptions',
94
+ autocomplete: {
95
+ ...searchProps.autocomplete,
96
+ searchQuery,
97
+ },
83
98
  }}
99
+ initialQuery={searchQuery}
84
100
  onSearch={onSearch}
85
101
  onSearchChange={updateSearchQuery}
86
102
  />
87
103
  </ToolbarItem>
88
104
  </ToolbarGroup>
89
- <ToolbarGroup>
105
+ <ToolbarGroup variant="button-group">
90
106
  {canManageSubscriptionAllocations &&
91
107
  <ToolbarItem>
92
108
  <LinkContainer
@@ -204,6 +220,7 @@ SubscriptionsToolbar.propTypes = {
204
220
  disableAddButton: PropTypes.bool,
205
221
  autocompleteQueryParams: PropTypes.shape({}),
206
222
  updateSearchQuery: PropTypes.func,
223
+ searchQuery: PropTypes.string,
207
224
  onSearch: PropTypes.func,
208
225
  onDeleteButtonClick: PropTypes.func,
209
226
  onManageManifestButtonClick: PropTypes.func,
@@ -223,6 +240,7 @@ SubscriptionsToolbar.defaultProps = {
223
240
  disableAddButton: false,
224
241
  autocompleteQueryParams: undefined,
225
242
  updateSearchQuery: noop,
243
+ searchQuery: '',
226
244
  onSearch: noop,
227
245
  onDeleteButtonClick: noop,
228
246
  onManageManifestButtonClick: noop,
@@ -8,8 +8,8 @@
8
8
  *
9
9
  * @param {Object} assignment - Assignment object with lifecycle environment
10
10
  * and content view
11
- * @param {Object} assignment.cveLabel - Pre-computed label from API
12
- * (if available)
11
+ * @param {Object} assignment.cvEnvLabel - Pre-computed label from API
12
+ * (if available)
13
13
  * @param {Array|Object} assignment.selectedEnv - Selected lifecycle
14
14
  * environment (array or object)
15
15
  * @param {Object} assignment.environment - Lifecycle environment object
@@ -19,7 +19,7 @@
19
19
  */
20
20
  const buildContentViewEnvironmentLabel = (assignment) => {
21
21
  // If assignment has pre-computed label from API, use it
22
- if (assignment.cveLabel) return assignment.cveLabel;
22
+ if (assignment.cvEnvLabel) return assignment.cvEnvLabel;
23
23
 
24
24
  // Get lifecycle environment - support both selectedEnv array and direct environment property
25
25
  const env = assignment.selectedEnv?.[0] || assignment.environment;