katello 4.3.0 → 4.3.1

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/katello/api/v2/repositories_controller.rb +2 -1
  3. data/app/controllers/katello/remote_execution_controller.rb +5 -4
  4. data/app/lib/actions/katello/content_view/publish.rb +5 -0
  5. data/app/lib/actions/katello/content_view_version/incremental_update.rb +17 -3
  6. data/app/lib/actions/pulp3/content_view_version/import.rb +7 -0
  7. data/app/lib/actions/pulp3/orchestration/content_view_version/import.rb +7 -5
  8. data/app/lib/actions/pulp3/repository/copy_content.rb +1 -1
  9. data/app/lib/actions/pulp3/repository/save_artifact.rb +1 -0
  10. data/app/lib/katello/resources/cdn.rb +1 -1
  11. data/app/models/katello/concerns/pulp_database_unit.rb +1 -1
  12. data/app/models/katello/docker_meta_tag.rb +1 -1
  13. data/app/models/katello/repository.rb +5 -0
  14. data/app/services/cert/rhsm_client.rb +1 -5
  15. data/app/services/katello/pulp3/content_view_version/import.rb +11 -2
  16. data/app/services/katello/pulp3/erratum.rb +9 -1
  17. data/app/services/katello/pulp3/generic_content_unit.rb +2 -1
  18. data/app/services/katello/pulp3/pulp_content_unit.rb +5 -7
  19. data/app/services/katello/pulp3/repository/yum.rb +10 -2
  20. data/app/services/katello/pulp3/repository.rb +11 -4
  21. data/app/views/foreman/job_templates/install_errata.erb +6 -9
  22. data/app/views/foreman/job_templates/install_errata_by_search_query.erb +26 -0
  23. data/db/migrate/20211019192121_create_cdn_configuration.katello.rb +11 -2
  24. data/db/seeds.d/111-upgrade_tasks.rb +2 -1
  25. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/packages/packages.controller.js +1 -0
  26. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/products/details/repositories/details/repository-details-manage-content.controller.js +2 -3
  27. data/lib/katello/plugin.rb +1 -0
  28. data/lib/katello/repository_types/ostree.rb +2 -0
  29. data/lib/katello/tasks/content_view_import_only.rake +34 -0
  30. data/lib/katello/tasks/upgrades/4.4/publish_import_cvvs.rake +17 -0
  31. data/lib/katello/version.rb +1 -1
  32. data/webpack/components/WithOrganization/withOrganization.js +1 -0
  33. data/webpack/components/extensions/HostDetails/Tabs/RemoteExecutionActions.js +2 -2
  34. data/webpack/components/extensions/HostDetails/Tabs/RemoteExecutionConstants.js +1 -1
  35. data/webpack/components/extensions/HostDetails/Tabs/__tests__/errataTab.test.js +5 -5
  36. data/webpack/components/extensions/HostDetails/Tabs/customizedRexUrlHelpers.js +1 -1
  37. data/webpack/scenes/Content/ContentConfig.js +55 -5
  38. data/webpack/scenes/Content/ContentPage.js +1 -1
  39. data/webpack/scenes/Content/Details/ContentDetails.js +1 -1
  40. data/webpack/scenes/Content/Details/ContentInfo.js +1 -1
  41. data/webpack/scenes/Content/Details/ContentRepositories.js +1 -1
  42. data/webpack/scenes/Content/Table/ContentTable.js +1 -1
  43. data/webpack/scenes/ContentViews/ContentViewsConstants.js +2 -1
  44. data/webpack/scenes/ContentViews/Details/ContentViewDetailActions.js +11 -10
  45. data/webpack/scenes/ContentViews/Details/ContentViewDetailSelectors.js +5 -5
  46. data/webpack/scenes/ContentViews/Details/Repositories/ContentCounts.js +1 -1
  47. data/webpack/scenes/ContentViews/Details/Versions/ContentViewVersionContent.js +16 -17
  48. data/webpack/scenes/ContentViews/Details/Versions/ContentViewVersions.js +1 -1
  49. data/webpack/scenes/ContentViews/Details/Versions/VersionDetails/ContentViewVersionDetailConfig.js +30 -34
  50. data/webpack/scenes/ContentViews/Details/Versions/VersionDetails/ContentViewVersionDetails.js +8 -8
  51. data/webpack/scenes/ContentViews/Details/Versions/VersionDetails/ContentViewVersionRepositoryCell.js +1 -1
  52. data/webpack/scenes/ContentViews/Details/Versions/__tests__/contentViewVersions.test.js +1 -1
  53. metadata +11 -2
@@ -2,5 +2,5 @@ export const REX_JOB_INVOCATIONS_KEY = 'REX_JOB_INVOCATIONS';
2
2
  export const REX_FEATURES = {
3
3
  KATELLO_PACKAGE_INSTALL: 'katello_package_install',
4
4
  KATELLO_HOST_TRACER_RESOLVE: 'katello_host_tracer_resolve',
5
- KATELLO_HOST_ERRATA_INSTALL: 'katello_errata_install',
5
+ KATELLO_HOST_ERRATA_INSTALL_BY_SEARCH: 'katello_errata_install_by_search',
6
6
  };
@@ -981,9 +981,9 @@ test('Can bulk apply via remote execution', async (done) => {
981
981
  // eslint-disable-next-line camelcase
982
982
  const jobInvocationBody = ({ job_invocation: { inputs, feature, search_query } }) =>
983
983
  inputs[ERRATA_SEARCH_QUERY] === `errata_id ^ (${results[0].errata_id},${results[1].errata_id})` &&
984
- feature === REX_FEATURES.KATELLO_HOST_ERRATA_INSTALL &&
985
- // eslint-disable-next-line camelcase
986
- search_query === `name ^ (${hostName})`;
984
+ feature === REX_FEATURES.KATELLO_HOST_ERRATA_INSTALL_BY_SEARCH &&
985
+ // eslint-disable-next-line camelcase
986
+ search_query === `name ^ (${hostName})`;
987
987
 
988
988
  const resolveErrataScope = nockInstance
989
989
  .post(jobInvocations, jobInvocationBody)
@@ -1076,7 +1076,7 @@ test('Can apply errata in bulk via customized remote execution', async (done) =>
1076
1076
  getByLabelText('Select row 0').click();
1077
1077
  getByLabelText('Select row 1').click();
1078
1078
  const errata = `${results[0].errata_id},${results[1].errata_id}`;
1079
- const feature = REX_FEATURES.KATELLO_HOST_ERRATA_INSTALL;
1079
+ const feature = REX_FEATURES.KATELLO_HOST_ERRATA_INSTALL_BY_SEARCH;
1080
1080
  const actionMenu = getByLabelText('bulk_actions');
1081
1081
  actionMenu.click();
1082
1082
  const viaRexAction = queryByText('Apply via customized remote execution');
@@ -1176,7 +1176,7 @@ test('Can apply a single erratum to the host via customized remote execution', a
1176
1176
  const mockErrata = makeMockErrata({});
1177
1177
  const { results } = mockErrata;
1178
1178
  const { errata_id: errataId } = results[0];
1179
- const feature = REX_FEATURES.KATELLO_HOST_ERRATA_INSTALL;
1179
+ const feature = REX_FEATURES.KATELLO_HOST_ERRATA_INSTALL_BY_SEARCH;
1180
1180
  const scope = nockInstance
1181
1181
  .get(hostErrata)
1182
1182
  .query(defaultQuery)
@@ -29,6 +29,6 @@ export const resolveTraceUrl = ({ hostname, search }) => createJob({
29
29
 
30
30
  export const errataInstallUrl = ({ hostname, search }) => createJob({
31
31
  hostname,
32
- feature: REX_FEATURES.KATELLO_HOST_ERRATA_INSTALL,
32
+ feature: REX_FEATURES.KATELLO_HOST_ERRATA_INSTALL_BY_SEARCH,
33
33
  inputs: { [ERRATA_SEARCH_QUERY]: search },
34
34
  });
@@ -1,5 +1,4 @@
1
1
  import React from 'react';
2
- import { Link } from 'react-router-dom';
3
2
  import { urlBuilder } from 'foremanReact/common/urlHelpers';
4
3
  import { translate as __ } from 'foremanReact/common/I18n';
5
4
  import ContentInfo from './Details/ContentInfo';
@@ -7,7 +6,8 @@ import LastSync from '../ContentViews/Details/Repositories/LastSync';
7
6
  import ContentRepositories from './Details/ContentRepositories';
8
7
  import ContentCounts from '../ContentViews/Details/Repositories/ContentCounts';
9
8
 
10
- export default () => [
9
+ // Keep in mind when editing this file that the ContentViewVersionDetailConfig consumes this array.
10
+ export default [
11
11
  {
12
12
  names: {
13
13
  pluralTitle: __('Python Packages'),
@@ -18,7 +18,7 @@ export default () => [
18
18
  singularLabel: 'python_package',
19
19
  },
20
20
  columnHeaders: [
21
- { title: __('Name'), getProperty: unit => (<Link to={urlBuilder(`content/python_packages/${unit?.id}`, '')}>{unit?.name}</Link>) },
21
+ { title: __('Name'), getProperty: unit => (<a href={urlBuilder(`content/python_packages/${unit?.id}`, '')}>{unit?.name}</a>) },
22
22
  { title: __('Version'), getProperty: unit => unit?.version },
23
23
  { title: __('Filename'), getProperty: unit => unit?.filename },
24
24
  ],
@@ -82,6 +82,57 @@ export default () => [
82
82
  pluralLabel: 'ostree_refs',
83
83
  singularLabel: 'ostree_ref',
84
84
  },
85
+ columnHeaders: [
86
+ { title: __('Name'), getProperty: unit => (<a href={urlBuilder(`content/ostree_refs/${unit?.id}`, '')}>{unit?.name}</a>) },
87
+ { title: __('Version'), getProperty: unit => unit?.version },
88
+ ],
89
+ tabs: [
90
+ {
91
+ tabKey: 'details',
92
+ title: __('Details'),
93
+ getContent: (contentType, id, tabKey) => <ContentInfo {...{ contentType, id, tabKey }} />,
94
+ columnHeaders: [
95
+ { title: __('Name'), getProperty: unit => unit?.name },
96
+ { title: __('Version'), getProperty: unit => unit?.version },
97
+ ],
98
+ },
99
+ {
100
+ tabKey: 'repositories',
101
+ title: __('Repositories'),
102
+ getContent: (contentType, id, tabKey) =>
103
+ <ContentRepositories {...{ contentType, id, tabKey }} />,
104
+ columnHeaders: [
105
+ {
106
+ title: __('Name'),
107
+ getProperty: unit =>
108
+ <a href={urlBuilder(`products/${unit?.product.id}/repositories/${unit?.id}`, '')}>{unit?.name}</a>,
109
+ },
110
+ {
111
+ title: __('Product'),
112
+ getProperty: unit =>
113
+ <a href={urlBuilder(`products/${unit?.product.id}`, '')}>{unit?.product.name}</a>,
114
+ },
115
+ {
116
+ title: __('Sync Status'),
117
+ getProperty: unit =>
118
+ (<LastSync
119
+ startedAt={unit?.started_at}
120
+ lastSyncWords={unit?.last_sync_words}
121
+ lastSync={unit?.last_sync}
122
+ />),
123
+ },
124
+ {
125
+ title: __('Content Count'),
126
+ getProperty: unit =>
127
+ (<ContentCounts
128
+ productId={unit.product.id}
129
+ repoId={unit.id}
130
+ counts={unit.content_counts}
131
+ />),
132
+ },
133
+ ],
134
+ },
135
+ ],
85
136
  },
86
137
  {
87
138
  names: {
@@ -93,11 +144,10 @@ export default () => [
93
144
  singularLabel: 'ansible_collection',
94
145
  },
95
146
  columnHeaders: [
96
- { title: __('Name'), getProperty: unit => (<Link to={urlBuilder(`content/ansible_collections/${unit?.id}`, '')}>{unit?.name}</Link>) },
147
+ { title: __('Name'), getProperty: unit => (<a href={urlBuilder(`content/ansible_collections/${unit?.id}`, '')}>{unit?.name}</a>) },
97
148
  { title: __('Author'), getProperty: unit => unit?.namespace },
98
149
  { title: __('Version'), getProperty: unit => unit?.version },
99
150
  { title: __('Checksum'), getProperty: unit => unit?.checksum },
100
-
101
151
  ],
102
152
  tabs: [
103
153
  {
@@ -27,7 +27,7 @@ const ContentPage = () => {
27
27
  const types = {};
28
28
  contentTypesResponse.forEach((type) => {
29
29
  if (type.generic_browser) {
30
- const typeConfig = ContentConfig().find(config =>
30
+ const typeConfig = ContentConfig.find(config =>
31
31
  config.names.singularLabel === type.label);
32
32
  if (typeConfig) {
33
33
  const { names } = typeConfig;
@@ -16,7 +16,7 @@ const ContentDetails = () => {
16
16
 
17
17
  const { id, content_type: contentType } = useParams();
18
18
  const contentId = Number(id);
19
- const config = ContentConfig().find(type =>
19
+ const config = ContentConfig.find(type =>
20
20
  type.names.pluralLabel === contentType);
21
21
  const { pluralTitle, pluralLabel } = config.names;
22
22
 
@@ -19,7 +19,7 @@ const ContentInfo = ({ contentType, id, tabKey }) => {
19
19
  const detailsResponse = useSelector(selectContentDetails);
20
20
  const detailsStatus = useSelector(selectContentDetailsStatus);
21
21
 
22
- const config = contentConfig().find(type => type.names.pluralLabel === contentType);
22
+ const config = contentConfig.find(type => type.names.pluralLabel === contentType);
23
23
  const { columnHeaders } = config.tabs.find(header => header.tabKey === tabKey);
24
24
 
25
25
  useEffect(() => {
@@ -20,7 +20,7 @@ const ContentRepositories = ({ contentType, id, tabKey }) => {
20
20
  const [searchQuery, updateSearchQuery] = useState('');
21
21
  const { results, ...metadata } = response;
22
22
 
23
- const config = contentConfig().find(type => type.names.pluralLabel === contentType);
23
+ const config = contentConfig.find(type => type.names.pluralLabel === contentType);
24
24
  const typeSingularLabel = config.names.singularLabel;
25
25
  const { columnHeaders } = config.tabs.find(header => header.tabKey === tabKey);
26
26
 
@@ -18,7 +18,7 @@ const ContentTable = ({
18
18
  const [searchQuery, updateSearchQuery] = useState('');
19
19
  const { results, ...metadata } = response;
20
20
 
21
- const { columnHeaders } = contentConfig().find(type =>
21
+ const { columnHeaders } = contentConfig.find(type =>
22
22
  type.names.singularLabel === contentTypes[selectedContentType][0]);
23
23
 
24
24
  return (
@@ -1,4 +1,5 @@
1
1
  import { translate as __ } from 'foremanReact/common/I18n';
2
+ import { toUpper } from 'lodash';
2
3
 
3
4
  const CONTENT_VIEWS_KEY = 'CONTENT_VIEWS';
4
5
  export const CREATE_CONTENT_VIEW_KEY = 'CONTENT_VIEW_CREATE';
@@ -22,11 +23,11 @@ export const RPM_PACKAGE_GROUPS_CONTENT = 'RPM_PACKAGE_GROUPS_CONTENT';
22
23
  export const REPOSITORY_CONTENT = 'REPOSITORY_CONTENT';
23
24
  export const ERRATA_CONTENT = 'ERRATA_CONTENT';
24
25
  export const DOCKER_TAGS_CONTENT = 'DOCKER_TAGS_CONTENT';
25
- export const ANSIBLE_COLLECTIONS_CONTENT = 'ANSIBLE_COLLECTIONS_CONTENT';
26
26
  export const MODULE_STREAMS_CONTENT = 'MODULE_STREAMS_CONTENT';
27
27
  export const DEB_PACKAGES_CONTENT = 'DEB_PACKAGES_CONTENT';
28
28
  export const RPM_PACKAGES_CONTENT = 'RPM_PACKAGES_CONTENT';
29
29
  export const FILE_CONTENT = 'FILE_CONTENT';
30
+ export const generatedContentKey = pluralLabel => `${toUpper(pluralLabel)}_CONTENT`;
30
31
  export const cvDetailsKey = cvId => `${CONTENT_VIEWS_KEY}_${cvId}`;
31
32
  export const cvDetailsRepoKey = cvId => `${CONTENT_VIEWS_KEY}_REPOSITORIES_${cvId}`;
32
33
  export const cvFilterRepoKey = filterId => `CV_FILTER_REPOSITORIES_${filterId}`;
@@ -1,6 +1,7 @@
1
1
  import { API_OPERATIONS, APIActions, get, put, post } from 'foremanReact/redux/API';
2
2
  import { addToast } from 'foremanReact/redux/actions/toasts';
3
3
  import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { lowerCase } from 'lodash';
4
5
  import {
5
6
  RPM_PACKAGES_CONTENT,
6
7
  RPM_PACKAGE_GROUPS_CONTENT,
@@ -45,8 +46,8 @@ import {
45
46
  ERRATA_CONTENT,
46
47
  MODULE_STREAMS_CONTENT,
47
48
  DEB_PACKAGES_CONTENT,
48
- ANSIBLE_COLLECTIONS_CONTENT,
49
49
  DOCKER_TAGS_CONTENT,
50
+ generatedContentKey,
50
51
  } from '../ContentViewsConstants';
51
52
  import api, { foremanApi, orgId } from '../../../services/api';
52
53
  import { getResponseErrorMsgs, apiError } from '../../../utils/helpers';
@@ -70,6 +71,14 @@ const cvUpdateSuccess = (response, dispatch) => {
70
71
  }));
71
72
  };
72
73
 
74
+ export const getContent = (pluralLabel, params) => get({
75
+ type: API_OPERATIONS.GET,
76
+ key: generatedContentKey(pluralLabel),
77
+ url: api.getApiUrl(`/${pluralLabel}`),
78
+ params,
79
+ errorToast: error => __(`Something went wrong while fetching ${lowerCase(pluralLabel)}! ${getResponseErrorMsgs(error.response)}`),
80
+ });
81
+
73
82
  export const getRPMPackages = params => get({
74
83
  type: API_OPERATIONS.GET,
75
84
  key: RPM_PACKAGES_CONTENT,
@@ -135,14 +144,6 @@ export const getDockerTags = params => get({
135
144
  errorToast: error => __(`Something went wrong while getting docker tags! ${getResponseErrorMsgs(error.response)}`),
136
145
  });
137
146
 
138
- export const getAnsibleCollections = params => get({
139
- type: API_OPERATIONS.GET,
140
- key: ANSIBLE_COLLECTIONS_CONTENT,
141
- url: api.getApiUrl('/ansible_collections'),
142
- params,
143
- errorToast: error => __(`Something went wrong while getting ansible collections! ${getResponseErrorMsgs(error.response)}`),
144
- });
145
-
146
147
  export const getErrata = params => get({
147
148
  type: API_OPERATIONS.GET,
148
149
  key: ERRATA_CONTENT,
@@ -176,7 +177,7 @@ export const getRepositories = params => get({
176
177
  });
177
178
 
178
179
  export const getContentViewRepositories = (cvId, params, status) => {
179
- const apiParams = { ...params };
180
+ const apiParams = { organization_id: orgId(), ...params };
180
181
  let apiUrl = `/content_views/${cvId}/repositories`;
181
182
 
182
183
  if (status === ALL_STATUSES) {
@@ -33,8 +33,8 @@ import {
33
33
  ERRATA_CONTENT,
34
34
  MODULE_STREAMS_CONTENT,
35
35
  DEB_PACKAGES_CONTENT,
36
- ANSIBLE_COLLECTIONS_CONTENT,
37
36
  DOCKER_TAGS_CONTENT,
37
+ generatedContentKey,
38
38
  } from '../ContentViewsConstants';
39
39
  import { pollTaskKey } from '../../Tasks/helpers';
40
40
 
@@ -131,11 +131,11 @@ export const selectCVFilterRules = (state, filterId) =>
131
131
  export const selectCVFilterRulesStatus = (state, filterId) =>
132
132
  selectAPIStatus(state, cvFilterRulesKey(filterId)) || STATUS.PENDING;
133
133
 
134
- export const selectAnsibleCollections = state =>
135
- selectAPIResponse(state, ANSIBLE_COLLECTIONS_CONTENT);
134
+ export const selectContent = (pluralLabel, state) =>
135
+ selectAPIResponse(state, generatedContentKey(pluralLabel));
136
136
 
137
- export const selectAnsibleCollectionsStatus = state =>
138
- selectAPIStatus(state, ANSIBLE_COLLECTIONS_CONTENT) || STATUS.PENDING;
137
+ export const selectContentStatus = (pluralLabel, state) =>
138
+ selectAPIStatus(state, generatedContentKey(pluralLabel)) || STATUS.PENDING;
139
139
 
140
140
  export const selectDockerTags = state =>
141
141
  selectAPIResponse(state, DOCKER_TAGS_CONTENT);
@@ -41,7 +41,7 @@ const ContentCounts = ({ productId, repoId, counts }) => {
41
41
  Object.keys(counts).forEach((type) => {
42
42
  const count = counts[type];
43
43
  let info = repoLabels[type];
44
- const config = ContentConfig().find(typeConfig =>
44
+ const config = ContentConfig.find(typeConfig =>
45
45
  typeConfig.names.singularLabel === type);
46
46
 
47
47
  if (config) {
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
+ import { camelCase } from 'lodash';
3
4
  import { Link } from 'react-router-dom';
4
5
  import { translate as __ } from 'foremanReact/common/I18n';
5
6
  import { urlBuilder } from 'foremanReact/common/urlHelpers';
@@ -17,15 +18,16 @@ const ContentViewVersionContent = ({ cvId, versionId, cvVersion }) => {
17
18
  } = cvVersion;
18
19
 
19
20
 
20
- const genericContentTypes = ContentConfig().filter(({ names: { singularLabel } }) => {
21
- // Ansible Collections has a tab in version details so it's handled separately below.
22
- if (singularLabel === 'ansible_collection') return false;
23
- const countLabel = `${singularLabel}_count`;
24
- return !!cvVersion[countLabel];
25
- }).map(({ names: { singularLabel, singularLowercase, pluralLowercase } }) => {
21
+ const contentConfigTypes = ContentConfig.filter(({ names: { singularLabel } }) =>
22
+ !!cvVersion[`${singularLabel}_count`]).map(({
23
+ names: {
24
+ singularLabel, singularLowercase, pluralLowercase, pluralLabel,
25
+ },
26
+ }) => {
26
27
  const countParam = `${singularLabel}_count`;
27
28
  const count = cvVersion[countParam];
28
29
  return {
30
+ pluralLabel,
29
31
  label: count > 1 ? pluralLowercase : singularLowercase,
30
32
  count,
31
33
  };
@@ -34,7 +36,7 @@ const ContentViewVersionContent = ({ cvId, versionId, cvVersion }) => {
34
36
  const noCounts =
35
37
  !Number(debCount) && !Number(dockerManifestCount) && !Number(dockerTagCount) &&
36
38
  !Number(fileCount) && !Number(moduleStreamCount) && !Number(ansibleCollectionCount) &&
37
- !genericContentTypes?.length;
39
+ !contentConfigTypes?.length;
38
40
 
39
41
  if (noCounts) {
40
42
  return <InactiveText text={__('No content')} />;
@@ -69,16 +71,13 @@ const ContentViewVersionContent = ({ cvId, versionId, cvVersion }) => {
69
71
  <a href={urlBuilder(`content_views/${cvId}#/versions/${versionId}/files`, '')}>{`${fileCount} Files`}</a><br />
70
72
  </>
71
73
  }
72
- {ansibleCollectionCount > 0 &&
73
- <>
74
- <a href={urlBuilder(`content_views/${cvId}#/versions/${versionId}/ansibleCollections`, '')}>{`${ansibleCollectionCount} Collections`}</a><br />
75
- </>
76
- }
77
- {genericContentTypes?.length > 0 &&
78
- genericContentTypes.map(({ label, count }) => (
79
- <span key={label} style={{ whiteSpace: 'pre-line' }}>
80
- {`${count} ${label}`}
81
- </span>))
74
+ {contentConfigTypes?.length > 0 &&
75
+ contentConfigTypes.map(({ label, count, pluralLabel }) => (
76
+ <React.Fragment key={label}>
77
+ <a href={urlBuilder(`content_views/${cvId}#/versions/${versionId}/${camelCase(pluralLabel)}`, '')}>
78
+ {`${count} ${label}`}
79
+ </a><br />
80
+ </React.Fragment>))
82
81
  }
83
82
  </>
84
83
  );
@@ -92,7 +92,7 @@ const ContentViewVersions = ({ cvId, details }) => {
92
92
  { title: <ContentViewVersionEnvironments {...{ environments }} /> },
93
93
  {
94
94
  title: Number(packageCount) ?
95
- <a href={urlBuilder(`content_views/${cvId}#/versions/${versionId}/packages`, '')}>{packageCount}</a> :
95
+ <a href={urlBuilder(`content_views/${cvId}#/versions/${versionId}/rpmPackages`, '')}>{packageCount}</a> :
96
96
  <InactiveText text={__('No packages')} />,
97
97
  },
98
98
  { title: <ContentViewVersionErrata {...{ cvId, versionId, errataCounts }} /> },
@@ -3,14 +3,13 @@ import PropTypes from 'prop-types';
3
3
  import { translate as __ } from 'foremanReact/common/I18n';
4
4
  import { urlBuilder } from 'foremanReact/common/urlHelpers';
5
5
  import LongDateTime from 'foremanReact/components/common/dates/LongDateTime';
6
- import { startCase } from 'lodash';
6
+ import { startCase, camelCase } from 'lodash';
7
7
  import {
8
8
  BugIcon,
9
9
  SecurityIcon,
10
10
  EnhancementIcon,
11
11
  } from '@patternfly/react-icons';
12
12
  import {
13
- getAnsibleCollections,
14
13
  getContentViewVersions,
15
14
  getDebPackages,
16
15
  getDockerTags,
@@ -20,10 +19,9 @@ import {
20
19
  getPackageGroups,
21
20
  getRepositories,
22
21
  getRPMPackages,
22
+ getContent,
23
23
  } from '../../ContentViewDetailActions';
24
24
  import {
25
- selectAnsibleCollections,
26
- selectAnsibleCollectionsStatus,
27
25
  selectCVVersions,
28
26
  selectCVVersionsStatus,
29
27
  selectDebPackages,
@@ -42,11 +40,15 @@ import {
42
40
  selectRPMPackageGroupsStatus,
43
41
  selectRPMPackages,
44
42
  selectRPMPackagesStatus,
43
+ selectContent,
44
+ selectContentStatus,
45
45
  } from '../../ContentViewDetailSelectors';
46
46
  import ContentViewVersionRepositoryCell from './ContentViewVersionRepositoryCell';
47
+ import ContentConfig from '../../../../Content/ContentConfig';
47
48
 
48
49
  export const TableType = PropTypes.shape({
49
50
  name: PropTypes.string,
51
+ route: PropTypes.string,
50
52
  getCountKey: PropTypes.func,
51
53
  repoType: PropTypes.string,
52
54
  responseSelector: PropTypes.func,
@@ -64,6 +66,7 @@ export const TableType = PropTypes.shape({
64
66
  export default ({ cvId, versionId }) => [
65
67
  {
66
68
  name: __('Components'),
69
+ route: 'components',
67
70
  getCountKey: item => item?.component_view_count,
68
71
  responseSelector: state => selectCVVersions(state, cvId),
69
72
  statusSelector: state => selectCVVersionsStatus(state, cvId),
@@ -91,6 +94,7 @@ export default ({ cvId, versionId }) => [
91
94
  },
92
95
  {
93
96
  name: __('Repositories'),
97
+ route: 'repositories',
94
98
  getCountKey: item => item?.repositories?.length,
95
99
  responseSelector: state => selectRepositories(state),
96
100
  statusSelector: state => selectRepositoriesStatus(state),
@@ -127,6 +131,7 @@ export default ({ cvId, versionId }) => [
127
131
  },
128
132
  {
129
133
  name: __('RPM Packages'),
134
+ route: 'rpmPackages',
130
135
  repoType: 'yum',
131
136
  getCountKey: item => item?.rpm_count,
132
137
  responseSelector: state => selectRPMPackages(state),
@@ -148,6 +153,7 @@ export default ({ cvId, versionId }) => [
148
153
  },
149
154
  {
150
155
  name: __('RPM Package Groups'),
156
+ route: 'rpmPackageGroups',
151
157
  repoType: 'yum',
152
158
  getCountKey: item => item?.package_group_count,
153
159
  responseSelector: state => selectRPMPackageGroups(state),
@@ -161,6 +167,7 @@ export default ({ cvId, versionId }) => [
161
167
  },
162
168
  {
163
169
  name: __('Files'),
170
+ route: 'files',
164
171
  repoType: 'file',
165
172
  getCountKey: item => item?.file_count,
166
173
  responseSelector: state => selectFiles(state),
@@ -180,6 +187,7 @@ export default ({ cvId, versionId }) => [
180
187
  },
181
188
  {
182
189
  name: __('Errata'),
190
+ route: 'errata',
183
191
  repoType: 'yum',
184
192
  getCountKey: item => item?.erratum_count,
185
193
  responseSelector: state => selectErrata(state),
@@ -233,6 +241,7 @@ export default ({ cvId, versionId }) => [
233
241
  },
234
242
  {
235
243
  name: __('Module Streams'),
244
+ route: 'moduleStreams',
236
245
  repoType: 'yum',
237
246
  getCountKey: item => item?.module_stream_count,
238
247
  responseSelector: state => selectModuleStreams(state),
@@ -255,6 +264,7 @@ export default ({ cvId, versionId }) => [
255
264
  },
256
265
  {
257
266
  name: __('Deb Packages'),
267
+ route: 'debPackages',
258
268
  repoType: 'deb',
259
269
  getCountKey: item => item?.deb_count,
260
270
  responseSelector: state => selectDebPackages(state),
@@ -273,38 +283,9 @@ export default ({ cvId, versionId }) => [
273
283
  { title: __('Architecture'), getProperty: item => item?.architecture },
274
284
  ],
275
285
  },
276
- {
277
- name: __('Ansible Collections'),
278
- repoType: 'ansible_collection',
279
- getCountKey: item => item?.ansible_collection_count,
280
- responseSelector: state => selectAnsibleCollections(state),
281
- statusSelector: state => selectAnsibleCollectionsStatus(state),
282
- autocompleteEndpoint: `/ansible_collections/auto_complete_search?content_view_version_id=${versionId}`,
283
- fetchItems: params => getAnsibleCollections({ content_view_version_id: versionId, ...params }),
284
- columnHeaders: [
285
- {
286
- title: __('Name'),
287
- getProperty: item => (
288
- <a href={urlBuilder(`ansible_collections/${item?.id}`, '')}>
289
- {item?.name}
290
- </a>),
291
- },
292
- {
293
- title: __('Author'),
294
- getProperty: item => item?.namespace,
295
- },
296
- {
297
- title: __('Version'),
298
- getProperty: item => item?.version,
299
- },
300
- {
301
- title: __('Checksum'),
302
- getProperty: item => item?.checksum,
303
- },
304
- ],
305
- },
306
286
  {
307
287
  name: __('Docker Tags'),
288
+ route: 'dockerTags',
308
289
  repoType: 'docker',
309
290
  getCountKey: item => item?.docker_tag_count,
310
291
  responseSelector: state => selectDockerTags(state),
@@ -329,4 +310,19 @@ export default ({ cvId, versionId }) => [
329
310
  { title: __('Product Name'), getProperty: item => item?.product?.name },
330
311
  ],
331
312
  },
313
+ ...ContentConfig.map(({
314
+ names: { pluralTitle, pluralLabel, singularLabel },
315
+ columnHeaders,
316
+ }) => ({
317
+ name: pluralTitle,
318
+ route: camelCase(pluralLabel),
319
+ repoType: singularLabel,
320
+ getCountKey: item => item[`${singularLabel}_count`],
321
+ responseSelector: state => selectContent(pluralLabel, state),
322
+ statusSelector: state => selectContentStatus(pluralLabel, state),
323
+ autocompleteEndpoint: `/${pluralLabel}/auto_complete_search?content_view_version_id=${versionId}`,
324
+ fetchItems: params =>
325
+ getContent(pluralLabel, { content_view_version_id: versionId, ...params }),
326
+ columnHeaders,
327
+ })),
332
328
  ];
@@ -3,7 +3,7 @@ import useDeepCompareEffect from 'use-deep-compare-effect';
3
3
  import { useParams, Route, useHistory, useLocation, Redirect, Switch } from 'react-router-dom';
4
4
  import { useDispatch, useSelector, shallowEqual } from 'react-redux';
5
5
  import { STATUS } from 'foremanReact/constants';
6
- import { isEmpty, camelCase, first } from 'lodash';
6
+ import { isEmpty, first } from 'lodash';
7
7
  import { Grid, Tabs, Tab, TabTitleText, Label } from '@patternfly/react-core';
8
8
  import { number, shape } from 'prop-types';
9
9
  import './ContentViewVersionDetails.scss';
@@ -68,7 +68,7 @@ const ContentViewVersionDetails = ({ cvId, details }) => {
68
68
  const filteredTableConfigs = tableConfigs.filter(({ getCountKey }) => !!getCountKey(response));
69
69
  const { repositories } = versionDetails;
70
70
  const showTabs = filteredTableConfigs.length > 0 && repositories;
71
- const getCurrentActiveKey = tab ?? camelCase(first(filteredTableConfigs)?.name);
71
+ const getCurrentActiveKey = tab ?? first(filteredTableConfigs)?.route;
72
72
 
73
73
  return (
74
74
  <Grid>
@@ -84,10 +84,10 @@ const ContentViewVersionDetails = ({ cvId, details }) => {
84
84
  onSelect={onSelect}
85
85
  isVertical
86
86
  >
87
- {filteredTableConfigs.map(({ name, getCountKey }) => (
87
+ {filteredTableConfigs.map(({ route, name, getCountKey }) => (
88
88
  <Tab
89
- key={name}
90
- eventKey={camelCase(name)}
89
+ key={route}
90
+ eventKey={route}
91
91
  title={
92
92
  <>
93
93
  <TabTitleText>{name}</TabTitleText>
@@ -100,9 +100,9 @@ const ContentViewVersionDetails = ({ cvId, details }) => {
100
100
  <Switch>
101
101
  {filteredTableConfigs.map(config => (
102
102
  <Route
103
- key={camelCase(config.name)}
103
+ key={config.route}
104
104
  exact
105
- path={`/versions/:versionId([0-9]+)/${camelCase(config.name)}`}
105
+ path={`/versions/:versionId([0-9]+)/${config.route}`}
106
106
  >
107
107
  <ContentViewVersionDetailsTable
108
108
  tableConfig={config}
@@ -111,7 +111,7 @@ const ContentViewVersionDetails = ({ cvId, details }) => {
111
111
  </Route>))
112
112
  }
113
113
  <Redirect
114
- to={`/versions/${versionId}/${camelCase(first(filteredTableConfigs).name)}`}
114
+ to={`/versions/${versionId}/${first(filteredTableConfigs).route}`}
115
115
  />
116
116
  </Switch>
117
117
  </div>
@@ -67,7 +67,7 @@ const ContentViewVersionRepositoryCell = ({
67
67
  },
68
68
  };
69
69
 
70
- ContentConfig().forEach((type) => {
70
+ ContentConfig.forEach((type) => {
71
71
  CONTENT_COUNTS[type.names.singularLabel] = {
72
72
  name: type.names.pluralLowercase,
73
73
  url: `products/${id}/repositories/${libraryInstanceId}/content/${type.names.pluralLabel}`,
@@ -122,7 +122,7 @@ test('Can show package and erratas and link to list page', async () => {
122
122
 
123
123
  await patientlyWaitFor(() => {
124
124
  expect(getAllByText(8)[0].closest('a'))
125
- .toHaveAttribute('href', '/content_views/5#/versions/11/packages/');
125
+ .toHaveAttribute('href', '/content_views/5#/versions/11/rpmPackages/');
126
126
  expect(getAllByText(15)[0].closest('a'))
127
127
  .toHaveAttribute('href', '/content_views/5#/versions/11/errata/');
128
128
  expect(getByText(5)).toBeInTheDocument();