katello 3.18.5 → 4.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of katello might be problematic. Click here for more details.

Files changed (239) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/katello/katello.scss +72 -0
  3. data/app/controllers/katello/api/registry/registry_proxies_controller.rb +6 -4
  4. data/app/controllers/katello/api/rhsm/candlepin_dynflow_proxy_controller.rb +0 -19
  5. data/app/controllers/katello/api/rhsm/candlepin_proxies_controller.rb +1 -3
  6. data/app/controllers/katello/api/v2/content_credentials_controller.rb +24 -24
  7. data/app/controllers/katello/api/v2/content_export_incrementals_controller.rb +1 -1
  8. data/app/controllers/katello/api/v2/content_exports_controller.rb +4 -0
  9. data/app/controllers/katello/api/v2/content_views_controller.rb +2 -2
  10. data/app/controllers/katello/api/v2/host_subscriptions_controller.rb +2 -3
  11. data/app/controllers/katello/api/v2/repositories_controller.rb +3 -19
  12. data/app/controllers/katello/api/v2/simple_content_access_controller.rb +34 -0
  13. data/app/controllers/katello/api/v2/subscriptions_controller.rb +1 -1
  14. data/app/controllers/katello/api/v2/upstream_subscriptions_controller.rb +8 -4
  15. data/app/controllers/katello/concerns/api/v2/authorization.rb +1 -14
  16. data/app/controllers/katello/concerns/authorization/api/v2/content_views_controller.rb +1 -1
  17. data/app/helpers/katello/sync_management_helper.rb +0 -2
  18. data/app/lib/actions/candlepin/environment/create.rb +1 -1
  19. data/app/lib/actions/candlepin/environment/set_content.rb +1 -1
  20. data/app/lib/actions/katello/activation_key/create.rb +9 -11
  21. data/app/lib/actions/katello/capsule_content/sync.rb +8 -8
  22. data/app/lib/actions/katello/capsule_content/sync_capsule.rb +9 -0
  23. data/app/lib/actions/katello/check_matching_content.rb +17 -0
  24. data/app/lib/actions/katello/content_view/environment_create.rb +6 -8
  25. data/app/lib/actions/katello/content_view/publish.rb +1 -1
  26. data/app/lib/actions/katello/content_view_version/incremental_update.rb +11 -7
  27. data/app/lib/actions/katello/host/hypervisors_update.rb +4 -4
  28. data/app/lib/actions/katello/organization/create.rb +3 -5
  29. data/app/lib/actions/katello/organization/destroy.rb +1 -1
  30. data/app/lib/actions/katello/organization/manifest_delete.rb +3 -5
  31. data/app/lib/actions/katello/organization/manifest_import.rb +1 -1
  32. data/app/lib/actions/katello/organization/manifest_refresh.rb +1 -1
  33. data/app/lib/actions/katello/orphan_cleanup/remove_orphans.rb +1 -1
  34. data/app/lib/actions/katello/repository/check_matching_content.rb +3 -1
  35. data/app/lib/actions/katello/repository/clone_contents.rb +8 -11
  36. data/app/lib/actions/katello/repository/create.rb +0 -8
  37. data/app/lib/actions/katello/repository/filtered_index_content.rb +3 -0
  38. data/app/lib/actions/katello/repository/index_content.rb +1 -0
  39. data/app/lib/actions/katello/repository/multi_clone_contents.rb +9 -12
  40. data/app/lib/actions/katello/repository/sync.rb +1 -5
  41. data/app/lib/actions/katello/repository/update.rb +0 -8
  42. data/app/lib/actions/middleware/execute_if_contents_changed.rb +4 -1
  43. data/app/lib/actions/pulp/orchestration/repository/refresh_repos.rb +0 -6
  44. data/app/lib/actions/pulp3/capsule_content/refresh_distribution.rb +3 -3
  45. data/app/lib/actions/pulp3/content_guard/refresh_all_distributions.rb +1 -2
  46. data/app/lib/actions/pulp3/content_migration_presenter.rb +2 -5
  47. data/app/lib/actions/pulp3/orchestration/repository/refresh_repos.rb +1 -6
  48. data/app/lib/katello/concerns/base_template_scope_extensions.rb +8 -0
  49. data/app/lib/katello/errors.rb +1 -1
  50. data/app/lib/katello/event_daemon/monitor.rb +53 -0
  51. data/app/lib/katello/event_daemon/runner.rb +99 -0
  52. data/app/lib/katello/logging.rb +32 -0
  53. data/app/lib/katello/messaging/connection.rb +1 -7
  54. data/app/lib/katello/util/pulpcore_content_filters.rb +1 -1
  55. data/app/lib/katello/validators/content_view_puppet_module_validator.rb +1 -1
  56. data/app/models/katello/activation_key.rb +2 -2
  57. data/app/models/katello/candlepin/repository_mapper.rb +1 -1
  58. data/app/models/katello/concerns/hostgroup_extensions.rb +2 -4
  59. data/app/models/katello/concerns/organization_extensions.rb +2 -2
  60. data/app/models/katello/concerns/pulp_database_unit.rb +0 -12
  61. data/app/models/katello/concerns/redhat_extensions.rb +8 -9
  62. data/app/models/katello/concerns/smart_proxy_extensions.rb +24 -0
  63. data/app/models/katello/content_view.rb +5 -1
  64. data/app/models/katello/content_view_environment.rb +2 -2
  65. data/app/models/katello/content_view_puppet_environment.rb +2 -2
  66. data/app/models/katello/content_view_version.rb +2 -1
  67. data/app/models/katello/content_view_version_export_history.rb +20 -0
  68. data/app/models/katello/erratum.rb +3 -1
  69. data/app/models/katello/file_unit.rb +0 -4
  70. data/app/models/katello/glue/candlepin/pool.rb +2 -0
  71. data/app/models/katello/glue/pulp/repo.rb +0 -6
  72. data/app/models/katello/glue/pulp/repos.rb +1 -22
  73. data/app/models/katello/host/content_facet.rb +31 -9
  74. data/app/models/katello/ping.rb +19 -39
  75. data/app/models/katello/pool.rb +5 -0
  76. data/app/models/katello/product.rb +3 -3
  77. data/app/models/katello/repository.rb +3 -3
  78. data/app/presenters/katello/host_subscription_presenter.rb +3 -4
  79. data/app/presenters/katello/host_subscriptions_presenter.rb +24 -0
  80. data/app/services/katello/applicability/applicable_content_helper.rb +6 -8
  81. data/app/services/katello/candlepin_event_listener.rb +11 -19
  82. data/app/services/katello/event_monitor/poller_thread.rb +2 -11
  83. data/app/services/katello/pulp/repository.rb +2 -4
  84. data/app/services/katello/pulp/smart_proxy_repository.rb +0 -15
  85. data/app/services/katello/pulp3/api/core.rb +0 -14
  86. data/app/services/katello/pulp3/erratum.rb +1 -2
  87. data/app/services/katello/pulp3/migration.rb +9 -83
  88. data/app/services/katello/pulp3/migration_plan.rb +5 -54
  89. data/app/services/katello/pulp3/migration_switchover.rb +5 -36
  90. data/app/services/katello/pulp3/repository/apt.rb +2 -1
  91. data/app/services/katello/pulp3/repository/yum.rb +2 -11
  92. data/app/services/katello/pulp3/repository.rb +13 -34
  93. data/app/services/katello/pulp3/rpm.rb +1 -5
  94. data/app/services/katello/pulp3/task.rb +5 -8
  95. data/app/services/katello/pulp3/task_group.rb +5 -13
  96. data/app/services/katello/repository_type.rb +1 -1
  97. data/app/views/foreman/smart_proxies/_content_tab.html.erb +4 -47
  98. data/app/views/foreman/smart_proxies/show.html.erb +1 -1
  99. data/app/views/katello/api/v2/capsule_content/sync_status.json.rabl +22 -25
  100. data/app/views/katello/api/v2/content_view_version_export_histories/show.json.rabl +1 -0
  101. data/app/views/katello/api/v2/organizations/show.json.rabl +7 -9
  102. data/app/views/katello/sync_management/_products.html.erb +1 -1
  103. data/app/views/overrides/organizations/_edit_override.html.erb +1 -4
  104. data/app/views/overrides/smart_proxies/_environment_tab.html.erb +1 -1
  105. data/app/views/overrides/smart_proxies/_environment_tab_pane.html.erb +1 -1
  106. data/config/katello.yaml.example +0 -3
  107. data/config/routes/api/v2.rb +8 -10
  108. data/db/migrate/20191204214919_add_content_view_version_counts.rb +0 -1
  109. data/db/migrate/20200514092553_move_katello_fields_from_hostgroups.katello.rb +1 -1
  110. data/db/migrate/20210119162528_delete_puppet_and_ostree_repos.rb +56 -0
  111. data/db/migrate/20210128231228_add_type_and_from_cvv_to_cvv_export_history.rb +14 -0
  112. data/db/migrate/20210201163238_migrate_background_download_policy_to_migrate.rb +7 -0
  113. data/db/seeds.d/104-proxy.rb +1 -1
  114. data/db/seeds.d/111-upgrade_tasks.rb +2 -1
  115. data/engines/bastion/app/assets/javascripts/bastion/auth/authorization.service.js +1 -1
  116. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/capsule-content/capsule-content.controller.js +1 -1
  117. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/content-hosts/content-host-register-os-client.directive.js +17 -0
  118. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/content-hosts/details/views/content-host-info.html +2 -2
  119. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/content-hosts/views/content-hosts.html +2 -2
  120. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/content-hosts/views/register-client.html +11 -4
  121. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/content-hosts/views/register-deb-client.html +38 -0
  122. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/content-hosts/views/register-oracle-client.html +5 -0
  123. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/content-hosts/views/register-sles-client.html +28 -0
  124. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/content-hosts/views/register.html +14 -11
  125. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/content-views/details/filters/views/package-filter-details.html +2 -2
  126. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/products/details/repositories/details/repository-details-info.controller.js +2 -2
  127. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/products/details/repositories/details/views/repository-info.html +1 -1
  128. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/products/details/repositories/download-policy.service.js +0 -1
  129. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/products/details/repositories/new/views/new-repository.html +1 -1
  130. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/products/products.controller.js +2 -2
  131. data/lib/katello/engine.rb +4 -5
  132. data/lib/katello/middleware/event_daemon.rb +1 -1
  133. data/lib/katello/permission_creator.rb +3 -2
  134. data/lib/katello/plugin.rb +2 -2
  135. data/lib/katello/tasks/jenkins.rake +1 -1
  136. data/lib/katello/tasks/pulp3_content_switchover.rake +20 -31
  137. data/lib/katello/tasks/pulp3_migration.rake +25 -45
  138. data/lib/katello/tasks/pulp3_migration_abort.rake +0 -8
  139. data/lib/katello/tasks/pulp3_migration_stats.rake +3 -46
  140. data/lib/katello/tasks/upgrades/4.0/remove_ostree_puppet_content.rake +16 -0
  141. data/lib/katello/version.rb +1 -1
  142. data/lib/proxy_api/container_gateway.rb +21 -0
  143. data/locale/bn/katello.edit.po +0 -0
  144. data/locale/cs/katello.edit.po +0 -0
  145. data/locale/de/katello.edit.po +0 -0
  146. data/locale/en/katello.edit.po +0 -0
  147. data/locale/es/katello.edit.po +0 -0
  148. data/locale/fr/katello.edit.po +0 -0
  149. data/locale/gu/katello.edit.po +0 -0
  150. data/locale/hi/katello.edit.po +0 -0
  151. data/locale/it/katello.edit.po +0 -0
  152. data/locale/ja/katello.edit.po +0 -0
  153. data/locale/kn/katello.edit.po +0 -0
  154. data/locale/ko/katello.edit.po +0 -0
  155. data/locale/mr/katello.edit.po +0 -0
  156. data/locale/or/katello.edit.po +0 -0
  157. data/locale/pa/katello.edit.po +0 -0
  158. data/locale/pt/katello.edit.po +0 -0
  159. data/locale/pt_BR/katello.edit.po +0 -0
  160. data/locale/ru/katello.edit.po +0 -0
  161. data/locale/ta/katello.edit.po +0 -0
  162. data/locale/te/katello.edit.po +0 -0
  163. data/locale/zh_CN/katello.edit.po +0 -0
  164. data/locale/zh_TW/katello.edit.po +0 -0
  165. data/package.json +1 -1
  166. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext.js +0 -1
  167. data/webpack/__mocks__/foremanReact/components/Pagination/PaginationHooks.js +2 -0
  168. data/webpack/__mocks__/foremanReact/components/common/dates/LongDateTime.js +5 -0
  169. data/webpack/{scenes/ContentViews/Details/Repositories → components/SelectableDropdown}/SelectableDropdown.js +20 -3
  170. data/webpack/components/SelectableDropdown/__tests__/SelectableDropdown.test.js +45 -0
  171. data/webpack/components/SelectableDropdown/index.js +3 -0
  172. data/webpack/components/Table/TableWrapper.js +2 -1
  173. data/webpack/components/Table/helpers.js +14 -0
  174. data/webpack/components/TypeAhead/pf4Search/TypeAheadSearch.js +0 -1
  175. data/webpack/containers/Application/overrides.scss +6 -0
  176. data/webpack/index.js +6 -0
  177. data/webpack/redux/actions/RedHatRepositories/helpers.js +5 -5
  178. data/webpack/scenes/ContentViews/ContentViewsActions.js +31 -2
  179. data/webpack/scenes/ContentViews/ContentViewsConstants.js +5 -1
  180. data/webpack/scenes/ContentViews/Copy/ContentViewCopySelectors.js +16 -0
  181. data/webpack/scenes/ContentViews/Copy/CopyContentViewForm.js +77 -0
  182. data/webpack/scenes/ContentViews/Copy/CopyContentViewModal.js +44 -0
  183. data/webpack/scenes/ContentViews/Copy/__tests__/contentViewCopyResult.fixtures.json +42 -0
  184. data/webpack/scenes/ContentViews/Copy/__tests__/copyContentView.test.js +39 -0
  185. data/webpack/scenes/ContentViews/Copy/index.js +4 -0
  186. data/webpack/scenes/ContentViews/Create/ContentViewCreateSelectors.js +16 -0
  187. data/webpack/scenes/ContentViews/Create/ContentViewFormComponents.js +58 -0
  188. data/webpack/scenes/ContentViews/Create/CreateContentViewForm.js +175 -0
  189. data/webpack/scenes/ContentViews/Create/CreateContentViewModal.js +27 -0
  190. data/webpack/scenes/ContentViews/Create/__tests__/contentViewCreateResult.fixtures.json +42 -0
  191. data/webpack/scenes/ContentViews/Create/__tests__/createContentView.test.js +92 -0
  192. data/webpack/scenes/ContentViews/Create/index.js +4 -0
  193. data/webpack/scenes/ContentViews/Details/ContentViewDetailActions.js +16 -0
  194. data/webpack/scenes/ContentViews/Details/ContentViewDetailSelectors.js +20 -1
  195. data/webpack/scenes/ContentViews/Details/ContentViewDetails.js +17 -7
  196. data/webpack/scenes/ContentViews/Details/ContentViewInfo.js +19 -13
  197. data/webpack/scenes/ContentViews/Details/Filters/ContentType.js +40 -0
  198. data/webpack/scenes/ContentViews/Details/Filters/ContentViewFilters.js +124 -0
  199. data/webpack/scenes/ContentViews/Details/Filters/__tests__/contentViewFilters.fixtures.json +134 -0
  200. data/webpack/scenes/ContentViews/Details/Filters/__tests__/contentViewFilters.test.js +92 -0
  201. data/webpack/scenes/ContentViews/Details/Repositories/ContentViewRepositories.js +44 -25
  202. data/webpack/scenes/ContentViews/Details/Repositories/__tests__/contentViewDetailRepos.test.js +17 -7
  203. data/webpack/scenes/ContentViews/Details/__tests__/contentViewDetail.test.js +24 -0
  204. data/webpack/scenes/ContentViews/Table/ContentViewsTable.js +53 -3
  205. data/webpack/scenes/ContentViews/Table/tableDataGenerator.js +4 -3
  206. data/webpack/scenes/ContentViews/__tests__/contentViewPage.test.js +41 -0
  207. data/webpack/scenes/ContentViews/helpers.js +13 -0
  208. data/webpack/scenes/SmartProxy/Content.js +17 -0
  209. data/webpack/scenes/SmartProxy/SmartProxyContentActions.js +11 -0
  210. data/webpack/scenes/SmartProxy/SmartProxyContentConstants.js +3 -0
  211. data/webpack/scenes/SmartProxy/SmartProxyContentSelectors.js +16 -0
  212. data/webpack/scenes/SmartProxy/SmartProxyContentTable.js +152 -0
  213. data/webpack/scenes/SmartProxy/__tests__/SmartProxyContentResult.fixtures.json +140 -0
  214. data/webpack/scenes/SmartProxy/__tests__/SmartProxyContentTest.js +38 -0
  215. data/webpack/scenes/SmartProxy/index.js +4 -0
  216. data/webpack/scenes/Subscriptions/Manifest/ManageManifestModal.js +2 -7
  217. data/webpack/scenes/Subscriptions/Manifest/ManifestActions.js +3 -3
  218. data/webpack/scenes/Subscriptions/Manifest/__tests__/ManifestActions.test.js +2 -2
  219. data/webpack/scenes/Subscriptions/Manifest/__tests__/SimpleContentAccess.test.js +2 -2
  220. data/webpack/scenes/Subscriptions/Manifest/index.js +0 -1
  221. data/webpack/test-utils/react-testing-lib-wrapper.js +5 -2
  222. data/webpack/utils/helpers.js +3 -0
  223. metadata +103 -63
  224. data/app/controllers/katello/api/v2/gpg_keys_controller.rb +0 -114
  225. data/app/lib/actions/pulp3/content_migration_reset.rb +0 -22
  226. data/app/lib/katello/util/hostgroup_facets_helper.rb +0 -126
  227. data/app/overrides/disable_turbolinks_on_proxies_index.rb +0 -5
  228. data/app/services/katello/event_daemon.rb +0 -135
  229. data/app/services/katello/pulp/content_counts_calculator.rb +0 -60
  230. data/db/migrate/20210201165835_add_migration_missing_content.rb +0 -12
  231. data/db/migrate/20210420140050_add_pulp3_hrefs_to_content_types_deb.rb +0 -5
  232. data/lib/katello/tasks/check_config.rake +0 -11
  233. data/lib/katello/tasks/fix_hostgroup_facets.rake +0 -8
  234. data/lib/katello/tasks/pulp3_migration_approve_corrupted.rake +0 -21
  235. data/lib/katello/tasks/pulp3_migration_reset.rake +0 -26
  236. data/lib/katello/tasks/reports.rake +0 -7
  237. data/lib/katello/tasks/upgrades/3.10/update_gpg_key_urls.rake +0 -32
  238. data/webpack/components/TypeAhead/pf4Search/TypeAheadSearch.scss +0 -5
  239. data/webpack/scenes/ContentViews/Table/actionResolver.js +0 -28
@@ -0,0 +1,124 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { shallowEqual, useSelector } from 'react-redux';
3
+ import { Label } from '@patternfly/react-core';
4
+ import { TableVariant } from '@patternfly/react-table';
5
+ import { STATUS } from 'foremanReact/constants';
6
+ import LongDateTime from 'foremanReact/components/common/dates/LongDateTime';
7
+ import { translate as __ } from 'foremanReact/common/I18n';
8
+ import PropTypes from 'prop-types';
9
+
10
+ import TableWrapper from '../../../../components/Table/TableWrapper';
11
+ import onSelect from '../../../../components/Table/helpers';
12
+ import { getContentViewFilters } from '../ContentViewDetailActions';
13
+ import {
14
+ selectCVFilters,
15
+ selectCVFiltersStatus,
16
+ selectCVFiltersError,
17
+ } from '../ContentViewDetailSelectors';
18
+ import { truncate } from '../../../../utils/helpers';
19
+ import ContentType from './ContentType';
20
+
21
+ // won't be needed when details pages are built, linking to old pages for now
22
+ const cvFilterUrl = (cvId, filterId, type, errataByDate) => {
23
+ const repoType = type === 'docker' ? 'docker' : 'yum';
24
+ const filterType = errataByDate ? 'errata_by_date' : type;
25
+ const base = `/content_views/${cvId}/repositories/${repoType}/filters/${filterId}`;
26
+ const endings = {
27
+ rpm: '/package/details',
28
+ package_group: '/package-group/list',
29
+ erratum: '/errata/list',
30
+ errata_by_date: '/errata/date_type',
31
+ modulemd: '/module-stream/list',
32
+ docker: '/docker/details',
33
+ };
34
+
35
+ return base + endings[filterType];
36
+ };
37
+
38
+ const ContentViewFilters = ({ cvId }) => {
39
+ const response = useSelector(state => selectCVFilters(state, cvId), shallowEqual);
40
+ const status = useSelector(state => selectCVFiltersStatus(state, cvId), shallowEqual);
41
+ const error = useSelector(state => selectCVFiltersError(state, cvId), shallowEqual);
42
+ const [rows, setRows] = useState([]);
43
+ const [metadata, setMetadata] = useState({});
44
+ const [searchQuery, updateSearchQuery] = useState('');
45
+ const loading = status === STATUS.PENDING;
46
+
47
+ const columnHeaders = [
48
+ __('Name'),
49
+ __('Description'),
50
+ __('Updated'),
51
+ __('Content type'),
52
+ __('Inclusion type'),
53
+ ];
54
+
55
+ const buildRows = (results) => {
56
+ const newRows = [];
57
+ results.forEach((filter) => {
58
+ let errataByDate = false;
59
+ const {
60
+ id, name, type, description, updated_at: updatedAt, inclusion,
61
+ } = filter;
62
+ if (filter.type === 'erratum' && filter.rules[0].types) errataByDate = true;
63
+
64
+ const cells = [
65
+ { title: <a href={cvFilterUrl(cvId, id, type, errataByDate)}>{name}</a> },
66
+ truncate(description || ''),
67
+ { title: <LongDateTime date={updatedAt} showRelativeTimeTooltip /> },
68
+ { title: <ContentType type={type} errataByDate={errataByDate} /> },
69
+ {
70
+ title: (
71
+ <Label color={inclusion && 'blue'}>
72
+ {inclusion ? 'Include' : 'Exclude'}
73
+ </Label>),
74
+ },
75
+ ];
76
+
77
+ newRows.push({ cells });
78
+ });
79
+ return newRows;
80
+ };
81
+
82
+ useEffect(() => {
83
+ const { results, ...meta } = response;
84
+ setMetadata(meta);
85
+
86
+ if (!loading && results) {
87
+ const newRows = buildRows(results);
88
+ setRows(newRows);
89
+ }
90
+ }, [JSON.stringify(response)]);
91
+
92
+ const emptyContentTitle = __("You currently don't have any filters for this content view.");
93
+ const emptyContentBody = __("Add filters using the 'Add filter' button above."); // needs link
94
+ const emptySearchTitle = __('No matching filters found');
95
+ const emptySearchBody = __('Try changing your search settings.');
96
+
97
+ return (
98
+ <TableWrapper
99
+ {...{
100
+ rows,
101
+ metadata,
102
+ emptyContentTitle,
103
+ emptyContentBody,
104
+ emptySearchTitle,
105
+ emptySearchBody,
106
+ searchQuery,
107
+ updateSearchQuery,
108
+ error,
109
+ status,
110
+ }}
111
+ onSelect={onSelect(rows, setRows)}
112
+ cells={columnHeaders}
113
+ variant={TableVariant.compact}
114
+ autocompleteEndpoint="/content_view_filters/auto_complete_search"
115
+ fetchItems={params => getContentViewFilters(cvId, params)}
116
+ />);
117
+ };
118
+
119
+
120
+ ContentViewFilters.propTypes = {
121
+ cvId: PropTypes.number.isRequired,
122
+ };
123
+
124
+ export default ContentViewFilters;
@@ -0,0 +1,134 @@
1
+ {
2
+ "total":6,
3
+ "subtotal":6,
4
+ "page":"1",
5
+ "per_page":"20",
6
+ "error":null,
7
+ "search":null,
8
+ "sort":{
9
+ "by":"name",
10
+ "order":"asc"
11
+ },
12
+ "results":[
13
+ {
14
+ "inclusion":true,
15
+ "id":4,
16
+ "name":"af1",
17
+ "description":"A really great filter",
18
+ "created_at":"2020-12-07 09:58:36 -0500",
19
+ "updated_at":"2020-12-07 10:42:05 -0500",
20
+ "content_view":{
21
+ },
22
+ "repositories":[
23
+
24
+ ],
25
+ "type":"rpm",
26
+ "rules":[
27
+
28
+ ]
29
+ },
30
+ {
31
+ "inclusion":false,
32
+ "id":5,
33
+ "name":"Another filter",
34
+ "description":"hey",
35
+ "created_at":"2020-12-07 12:59:12 -0500",
36
+ "updated_at":"2020-12-07 12:59:12 -0500",
37
+ "content_view":{
38
+ },
39
+ "repositories":[
40
+
41
+ ],
42
+ "type":"package_group",
43
+ "rules":[
44
+
45
+ ]
46
+ },
47
+ {
48
+ "inclusion":true,
49
+ "id":6,
50
+ "name":"f2",
51
+ "description":null,
52
+ "created_at":"2020-12-08 09:26:11 -0500",
53
+ "updated_at":"2020-12-08 09:26:11 -0500",
54
+ "content_view":{
55
+ },
56
+ "repositories":[
57
+
58
+ ],
59
+ "type":"erratum",
60
+ "rules":[
61
+ {
62
+ "content_view_filter_id":6,
63
+ "errata_id":"RHEA-2012:0059",
64
+ "date_type":"updated",
65
+ "id":3,
66
+ "created_at":"2020-12-08 10:32:25 -0500",
67
+ "updated_at":"2020-12-08 10:32:25 -0500"
68
+ }
69
+ ]
70
+ },
71
+ {
72
+ "inclusion":false,
73
+ "id":7,
74
+ "name":"f3",
75
+ "description":null,
76
+ "created_at":"2020-12-08 09:26:27 -0500",
77
+ "updated_at":"2020-12-08 09:26:27 -0500",
78
+ "content_view":{
79
+ },
80
+ "repositories":[
81
+
82
+ ],
83
+ "type":"erratum",
84
+ "rules":[
85
+ {
86
+ "content_view_filter_id":7,
87
+ "types":[
88
+ "security",
89
+ "enhancement",
90
+ "bugfix"
91
+ ],
92
+ "date_type":"updated",
93
+ "id":2,
94
+ "created_at":"2020-12-08 09:26:27 -0500",
95
+ "updated_at":"2020-12-08 09:26:27 -0500"
96
+ }
97
+ ]
98
+ },
99
+ {
100
+ "inclusion":false,
101
+ "id":8,
102
+ "name":"f4",
103
+ "description":null,
104
+ "created_at":"2020-12-08 09:26:51 -0500",
105
+ "updated_at":"2020-12-08 09:26:51 -0500",
106
+ "content_view":{
107
+ },
108
+ "repositories":[
109
+
110
+ ],
111
+ "type":"modulemd",
112
+ "rules":[
113
+
114
+ ]
115
+ },
116
+ {
117
+ "inclusion":true,
118
+ "id":9,
119
+ "name":"f5",
120
+ "description":null,
121
+ "created_at":"2020-12-08 09:32:53 -0500",
122
+ "updated_at":"2020-12-08 09:32:53 -0500",
123
+ "content_view":{
124
+ },
125
+ "repositories":[
126
+
127
+ ],
128
+ "type":"docker",
129
+ "rules":[
130
+
131
+ ]
132
+ }
133
+ ]
134
+ }
@@ -0,0 +1,92 @@
1
+ import React from 'react';
2
+ import { renderWithRedux, patientlyWaitFor, fireEvent } from 'react-testing-lib-wrapper';
3
+
4
+ import api from '../../../../../services/api';
5
+ import nock, { nockInstance, assertNockRequest, mockAutocomplete, mockSetting } from '../../../../../test-utils/nockWrapper';
6
+ import ContentViewFilters from '../ContentViewFilters';
7
+ import CONTENT_VIEWS_KEY from '../../../ContentViewsConstants';
8
+
9
+ const cvFilterFixtures = require('./contentViewFilters.fixtures.json');
10
+
11
+ const cvFilters = api.getApiUrl('/content_view_filters');
12
+ const autocompleteUrl = '/content_view_filters/auto_complete_search';
13
+ const renderOptions = { apiNamespace: `${CONTENT_VIEWS_KEY}_1` };
14
+
15
+ let firstFilter;
16
+ let lastFilter;
17
+ let searchDelayScope;
18
+ let autoSearchScope;
19
+ beforeEach(() => {
20
+ const { results } = cvFilterFixtures;
21
+ [firstFilter] = results;
22
+ [lastFilter] = results.slice(-1);
23
+ searchDelayScope = mockSetting(nockInstance, 'autosearch_delay', 500);
24
+ // Autosearch can cause some asynchronous issues with the typing timeout, using basic search
25
+ autoSearchScope = mockSetting(nockInstance, 'autosearch_while_typing', false);
26
+ });
27
+
28
+ afterEach(() => {
29
+ assertNockRequest(searchDelayScope);
30
+ assertNockRequest(autoSearchScope);
31
+ nock.cleanAll();
32
+ });
33
+
34
+ test('Can call API and show filters on page load', async (done) => {
35
+ const { name, description } = firstFilter;
36
+ const autocompleteScope = mockAutocomplete(nockInstance, autocompleteUrl);
37
+
38
+ const scope = nockInstance
39
+ .get(cvFilters)
40
+ .query(true)
41
+ .reply(200, cvFilterFixtures);
42
+
43
+ const { getByText, queryByText } =
44
+ renderWithRedux(<ContentViewFilters cvId={1} />, renderOptions);
45
+
46
+ // Nothing will show at first, page is loading
47
+ expect(queryByText(name)).toBeNull();
48
+ await patientlyWaitFor(() => {
49
+ expect(getByText(name)).toBeInTheDocument();
50
+ expect(getByText(description)).toBeInTheDocument();
51
+ expect(getByText(lastFilter.name)).toBeInTheDocument();
52
+ });
53
+
54
+ assertNockRequest(autocompleteScope);
55
+ assertNockRequest(scope, done);
56
+ });
57
+
58
+ test('Can search for filter', async (done) => {
59
+ const { name, description } = firstFilter;
60
+ const searchQueryMatcher = actualParams => actualParams?.search?.includes(name);
61
+ const autocompleteScope = mockAutocomplete(nockInstance, autocompleteUrl);
62
+ const withSearchScope = mockAutocomplete(nockInstance, autocompleteUrl, searchQueryMatcher);
63
+ const initialScope = nockInstance
64
+ .get(cvFilters)
65
+ .query(true)
66
+ .reply(200, cvFilterFixtures);
67
+ const searchResultScope = nockInstance
68
+ .get(cvFilters)
69
+ .query(searchQueryMatcher)
70
+ .reply(200, { results: [firstFilter] });
71
+
72
+ const { queryByText, getByLabelText, getByText } = renderWithRedux(
73
+ <ContentViewFilters cvId={1} />,
74
+ renderOptions,
75
+ );
76
+
77
+ // Looking for description because the name is in the search bar and could match
78
+ await patientlyWaitFor(() => expect(getByText(description)).toBeInTheDocument());
79
+ // Search for a filter by name
80
+ fireEvent.change(getByLabelText(/text input for search/i), { target: { value: name } });
81
+ getByLabelText(/search button/i).click();
82
+ // Only the first filter should be showing, not the last one
83
+ await patientlyWaitFor(() => {
84
+ expect(getByText(description)).toBeInTheDocument();
85
+ expect(queryByText(lastFilter.name)).not.toBeInTheDocument();
86
+ });
87
+
88
+ assertNockRequest(autocompleteScope);
89
+ assertNockRequest(withSearchScope);
90
+ assertNockRequest(initialScope);
91
+ assertNockRequest(searchResultScope, done);
92
+ });
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect } from 'react';
2
- import { useSelector, shallowEqual } from 'react-redux';
2
+ import { useSelector, shallowEqual, useDispatch } from 'react-redux';
3
3
  import { Bullseye, Split, SplitItem } from '@patternfly/react-core';
4
4
  import { TableVariant, fitContent } from '@patternfly/react-table';
5
5
  import { STATUS } from 'foremanReact/constants';
@@ -8,37 +8,47 @@ import { urlBuilder } from 'foremanReact/common/urlHelpers';
8
8
  import PropTypes from 'prop-types';
9
9
 
10
10
  import TableWrapper from '../../../../components/Table/TableWrapper';
11
- import { getContentViewRepositories } from '../ContentViewDetailActions';
12
- import { selectCVRepos, selectCVReposStatus, selectCVReposError } from '../ContentViewDetailSelectors';
11
+ import onSelect from '../../../../components/Table/helpers';
12
+ import { getContentViewRepositories, getRepositoryTypes } from '../ContentViewDetailActions';
13
+ import {
14
+ selectCVRepos,
15
+ selectCVReposStatus,
16
+ selectCVReposError,
17
+ selectRepoTypes,
18
+ selectRepoTypesStatus,
19
+ } from '../ContentViewDetailSelectors';
13
20
  import { ADDED, NOT_ADDED, ALL_STATUSES } from '../../ContentViewsConstants';
14
21
  import ContentCounts from './ContentCounts';
15
22
  import LastSync from './LastSync';
16
23
  import RepoAddedStatus from './RepoAddedStatus';
17
24
  import RepoIcon from './RepoIcon';
18
- import SelectableDropdown from './SelectableDropdown';
25
+ import SelectableDropdown from '../../../../components/SelectableDropdown';
26
+ import { capitalize } from '../../../../utils/helpers';
19
27
 
20
28
  const allRepositories = 'All repositories';
21
29
 
22
- // checkbox_name: API_name
23
- const repoTypes = {
24
- [allRepositories]: 'all',
25
- 'Yum repositories': 'yum',
26
- 'File repositories': 'file',
27
- 'Container repositories': 'docker',
28
- 'OSTree repositories': 'ostree', // ostree is deprecated?
30
+ // Add any exceptions to the display names here
31
+ // [API_value]: displayed_value
32
+ const repoTypeNames = {
33
+ docker: 'Container',
34
+ ostree: 'OSTree',
29
35
  };
30
36
 
31
37
  const ContentViewRepositories = ({ cvId }) => {
38
+ const dispatch = useDispatch();
32
39
  const response = useSelector(state => selectCVRepos(state, cvId), shallowEqual);
33
40
  const status = useSelector(state => selectCVReposStatus(state, cvId), shallowEqual);
34
41
  const error = useSelector(state => selectCVReposError(state, cvId), shallowEqual);
42
+ const repoTypesResponse = useSelector(state => selectRepoTypes(state), shallowEqual);
43
+ const repoTypesStatus = useSelector(state => selectRepoTypesStatus(state), shallowEqual);
35
44
 
36
45
  const [rows, setRows] = useState([]);
37
46
  const [metadata, setMetadata] = useState({});
38
47
  const [searchQuery, updateSearchQuery] = useState('');
39
48
  const [typeSelected, setTypeSelected] = useState(allRepositories);
40
49
  const [statusSelected, setStatusSelected] = useState(ALL_STATUSES);
41
-
50
+ // repoTypes object format: [displayed_value]: API_value
51
+ const [repoTypes, setRepoTypes] = useState({});
42
52
 
43
53
  const columnHeaders = [
44
54
  { title: __('Type'), transforms: [fitContent] },
@@ -80,18 +90,6 @@ const ContentViewRepositories = ({ cvId }) => {
80
90
  return newRows;
81
91
  };
82
92
 
83
- const onSelect = (_event, isSelected, rowId) => {
84
- let newRows;
85
- if (rowId === -1) {
86
- newRows = rows.map(row => ({ ...row, selected: isSelected }));
87
- } else {
88
- newRows = [...rows];
89
- newRows[rowId].selected = isSelected;
90
- }
91
-
92
- setRows(newRows);
93
- };
94
-
95
93
  const getCVReposWithOptions = (params = {}) => {
96
94
  const allParams = { ...params };
97
95
  if (typeSelected !== 'All repositories') allParams.content_type = repoTypes[typeSelected];
@@ -109,6 +107,25 @@ const ContentViewRepositories = ({ cvId }) => {
109
107
  }
110
108
  }, [JSON.stringify(response)]);
111
109
 
110
+ useEffect(() => {
111
+ dispatch(getRepositoryTypes());
112
+ }, []);
113
+
114
+ // Get repo type filter selections dynamically from the API
115
+ useEffect(() => {
116
+ if (repoTypesStatus === STATUS.RESOLVED && repoTypesResponse) {
117
+ const allRepoTypes = {};
118
+ allRepoTypes[allRepositories] = 'all';
119
+ repoTypesResponse.forEach((type) => {
120
+ const { name } = type;
121
+ const typeFullName = Object.prototype.hasOwnProperty.call(repoTypeNames, name) ?
122
+ repoTypeNames[name] : capitalize(name);
123
+ allRepoTypes[`${typeFullName} Repositories`] = name;
124
+ });
125
+ setRepoTypes(allRepoTypes);
126
+ }
127
+ }, [JSON.stringify(repoTypesResponse), repoTypesStatus]);
128
+
112
129
  const emptyContentTitle = __("You currently don't have any repositories to add to this content view.");
113
130
  const emptyContentBody = __('Please add some repositories.'); // needs link
114
131
  const emptySearchTitle = __('No matching repositories found');
@@ -121,7 +138,6 @@ const ContentViewRepositories = ({ cvId }) => {
121
138
  {...{
122
139
  rows,
123
140
  metadata,
124
- onSelect,
125
141
  emptyContentTitle,
126
142
  emptyContentBody,
127
143
  emptySearchTitle,
@@ -132,6 +148,7 @@ const ContentViewRepositories = ({ cvId }) => {
132
148
  status,
133
149
  activeFilters,
134
150
  }}
151
+ onSelect={onSelect(rows, setRows)}
135
152
  cells={columnHeaders}
136
153
  variant={TableVariant.compact}
137
154
  autocompleteEndpoint="/repositories/auto_complete_search"
@@ -146,6 +163,8 @@ const ContentViewRepositories = ({ cvId }) => {
146
163
  selected={typeSelected}
147
164
  setSelected={setTypeSelected}
148
165
  placeholderText="Type"
166
+ loading={repoTypesStatus === STATUS.PENDING}
167
+ error={repoTypesStatus === STATUS.ERROR}
149
168
  />
150
169
  </SplitItem>
151
170
  <SplitItem>
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { renderWithRedux, patientlyWaitFor, fireEvent } from 'react-testing-lib-wrapper';
2
+ import { renderWithRedux, patientlyWaitFor, patientlyWaitForRemoval, fireEvent } from 'react-testing-lib-wrapper';
3
3
 
4
4
  import nock, { nockInstance, assertNockRequest, mockAutocomplete, mockSetting } from '../../../../../test-utils/nockWrapper';
5
5
  import api from '../../../../../services/api';
@@ -12,15 +12,22 @@ const autocompleteUrl = '/repositories/auto_complete_search';
12
12
  const renderOptions = { apiNamespace: `${CONTENT_VIEWS_KEY}_1` };
13
13
  const cvAllRepos = api.getApiUrl('/content_views/1/repositories/show_all');
14
14
  const cvRepos = api.getApiUrl('/content_views/1/repositories');
15
+ const repoTypesResponse = [{ name: 'deb' }, { name: 'docker' }, { name: 'file' }, { name: 'ostree' }, { name: 'puppet' }, { name: 'yum' }];
15
16
 
16
17
  let firstRepo;
17
18
  let searchDelayScope;
18
19
  let autoSearchScope;
20
+
19
21
  beforeEach(() => {
20
22
  const { results } = repoData;
21
23
  [firstRepo] = results;
22
24
  searchDelayScope = mockSetting(nockInstance, 'autosearch_delay', 500);
23
25
  autoSearchScope = mockSetting(nockInstance, 'autosearch_while_typing', true);
26
+ nockInstance
27
+ .persist() // match any query to this endpoint, gets cleaned up by `nock.cleanAll()`
28
+ .get(api.getApiUrl('/repositories/repository_types'))
29
+ .query(true)
30
+ .reply(200, repoTypesResponse);
24
31
  });
25
32
 
26
33
  afterEach(() => {
@@ -47,7 +54,6 @@ test('Can call API and show repositories on page load', async (done) => {
47
54
  // Assert that the repo name is now showing on the screen, but wait for it to appear.
48
55
  await patientlyWaitFor(() => expect(getByText(firstRepo.name)).toBeTruthy());
49
56
 
50
-
51
57
  assertNockRequest(autocompleteScope);
52
58
  assertNockRequest(scope, done);
53
59
  });
@@ -57,23 +63,27 @@ test('Can filter by repository type', async (done) => {
57
63
 
58
64
  const allTypesScope = nockInstance
59
65
  .get(cvAllRepos)
60
- .query(true)
66
+ .query(queryObj => !queryObj.content_type) // no content_type param to match all repos
61
67
  .reply(200, repoData);
62
68
 
63
- // With the yum checkbox unchecked, we can expect the query params to not include 'yum'
64
69
  const noYumScope = nockInstance
65
70
  .get(cvAllRepos)
66
71
  .query(queryObj => queryObj.content_type === 'yum')
67
72
  .reply(200, repoData);
68
73
 
69
- const { getByLabelText } = renderWithRedux(<ContentViewRepositories cvId={1} />, renderOptions);
74
+ const { getByLabelText, getByText } =
75
+ renderWithRedux(<ContentViewRepositories cvId={1} />, renderOptions);
70
76
 
77
+ await patientlyWaitForRemoval(() => getByLabelText('Type spinner'));
71
78
  // Patternfly's Select component makes it hard to attach a label, the existing options aren't
72
- // working as expected, so querying by container label and getting first button to open dropdown
79
+ // working as expected, so querying by container label and getting the button
73
80
  const toggleContainer = getByLabelText('select Type container');
74
81
  const toggleButton = toggleContainer.querySelector('button');
75
82
  fireEvent.click(toggleButton); // Open type dropdown
76
- fireEvent.click(getByLabelText('select Yum repositories')); // select yum repos
83
+ const selectYum = getByLabelText('select Yum Repositories');
84
+ fireEvent.click(selectYum); // select yum repos
85
+ await patientlyWaitForRemoval(() => getByText('Loading'));
86
+ await patientlyWaitFor(() => expect(getByText(firstRepo.name)).toBeTruthy());
77
87
 
78
88
  assertNockRequest(autocompleteScope);
79
89
  assertNockRequest(allTypesScope);
@@ -13,6 +13,7 @@ const cvDetailsPath = api.getApiUrl('/content_views/1');
13
13
 
14
14
  // The Repositories tab will load in the background, prevent this by mocking
15
15
  jest.mock('../Repositories/ContentViewRepositories.js', () => () => 'mocked!');
16
+ jest.mock('../Filters/ContentViewFilters.js', () => () => 'mocked!');
16
17
 
17
18
  test('Can call API and show details on page load', async (done) => {
18
19
  const { label, name, description } = cvDetailData;
@@ -98,7 +99,30 @@ test('Can edit boolean details such as solve dependencies', async (done) => {
98
99
  fireEvent.click(getByLabelText(checkboxLabel));
99
100
  await patientlyWaitFor(() => expect(getByLabelText(checkboxLabel).checked).toBeTruthy());
100
101
 
102
+ const disabledImportLabel = /import_only_switch/;
103
+ expect(getByLabelText(disabledImportLabel)).toBeInTheDocument();
104
+ expect(getByLabelText(disabledImportLabel)).toHaveAttribute('disabled');
105
+
101
106
  assertNockRequest(getscope);
102
107
  assertNockRequest(updatescope);
103
108
  assertNockRequest(afterUpdateScope, done);
104
109
  });
110
+
111
+ test('Can link to view tasks', async () => {
112
+ const scope = nockInstance
113
+ .get(cvDetailsPath)
114
+ .query(true)
115
+ .reply(200, cvDetailData);
116
+
117
+ const { getByText } = renderWithRedux(
118
+ <ContentViewDetails match={{ params: { id: 1 } }} />,
119
+ renderOptions,
120
+ );
121
+
122
+ await patientlyWaitFor(() => {
123
+ expect(getByText(/view tasks/i).closest('a'))
124
+ .toHaveAttribute('href', '/foreman_tasks/tasks?search=resource_type%3D+Katello%3A%3AContentView+resource_id%3D1');
125
+ });
126
+
127
+ assertNockRequest(scope);
128
+ });
@@ -2,11 +2,12 @@ import React, { useState, useEffect } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { translate as __ } from 'foremanReact/common/I18n';
4
4
  import { STATUS } from 'foremanReact/constants';
5
-
5
+ import { Button } from '@patternfly/react-core';
6
6
  import TableWrapper from '../../../components/Table/TableWrapper';
7
7
  import tableDataGenerator from './tableDataGenerator';
8
- import actionResolver from './actionResolver';
9
8
  import getContentViews from '../ContentViewsActions';
9
+ import CreateContentViewModal from '../Create/CreateContentViewModal';
10
+ import CopyContentViewModal from '../Copy/CopyContentViewModal';
10
11
 
11
12
  const ContentViewTable = ({ response, status, error }) => {
12
13
  const [table, setTable] = useState({ rows: [], columns: [] });
@@ -14,6 +15,13 @@ const ContentViewTable = ({ response, status, error }) => {
14
15
  const [searchQuery, updateSearchQuery] = useState('');
15
16
  const { results, ...metadata } = response;
16
17
  const loadingResponse = status === STATUS.PENDING;
18
+ const [isModalOpen, setIsModalOpen] = useState(false);
19
+ const [copy, setCopy] = useState(false);
20
+ const [actionableCvId, setActionableCvId] = useState('');
21
+ const [actionableCvName, setActionableCvName] = useState('');
22
+ function openForm() {
23
+ setIsModalOpen(true);
24
+ }
17
25
 
18
26
  useEffect(
19
27
  () => {
@@ -59,6 +67,38 @@ const ContentViewTable = ({ response, status, error }) => {
59
67
  setTable(prevTable => ({ ...prevTable, rows }));
60
68
  };
61
69
 
70
+ const actionResolver = (rowData, { _rowIndex }) => {
71
+ // don't show actions for the expanded parts
72
+ if (rowData.parent || rowData.compoundParent || rowData.noactions) return null;
73
+
74
+ // printing to the console for now until these are hooked up
75
+ /* eslint-disable no-console */
76
+ return [
77
+ {
78
+ title: 'Publish and Promote',
79
+ onClick: (_event, rowId, rowInfo) => {
80
+ console.log(`clicked on row ${JSON.stringify(rowInfo)}`);
81
+ },
82
+ },
83
+ {
84
+ title: 'Promote',
85
+ onClick: (_event, rowId, rowInfo) => console.log(`clicked on row ${rowInfo.cvName}`),
86
+ },
87
+ {
88
+ title: 'Copy',
89
+ onClick: (_event, rowId, rowInfo) => {
90
+ setCopy(true);
91
+ setActionableCvId(rowInfo.cvId.toString());
92
+ setActionableCvName(rowInfo.cvName);
93
+ },
94
+ },
95
+ {
96
+ title: 'Delete',
97
+ onClick: (_event, rowId, _rowInfo) => console.log(`clicked on row ${rowId}`),
98
+ },
99
+ ];
100
+ /* eslint-enable no-console */
101
+ };
62
102
  // Prevents flash of "No Content" before rows are loaded
63
103
  const tableStatus = () => {
64
104
  if (typeof results === 'undefined') return status; // will handle errored state
@@ -99,7 +139,17 @@ const ContentViewTable = ({ response, status, error }) => {
99
139
  canSelectAll={false}
100
140
  cells={columns}
101
141
  autocompleteEndpoint="/content_views/auto_complete_search"
102
- />
142
+ >
143
+ <React.Fragment>
144
+ <Button onClick={openForm} variant="primary" aria-label="create_content_view">
145
+ Create content view
146
+ </Button>
147
+ <CreateContentViewModal show={isModalOpen} setIsOpen={setIsModalOpen} aria-label="create_content_view_modal" />
148
+ </React.Fragment>
149
+ <React.Fragment>
150
+ <CopyContentViewModal cvId={actionableCvId} cvName={actionableCvName} show={copy} setIsOpen={setCopy} aria-label="copy_content_view_modal" />
151
+ </React.Fragment>
152
+ </TableWrapper>
103
153
  );
104
154
  };
105
155