diff --git a/app/views/katello/api/v2/content_views/base.json.rabl b/app/views/katello/api/v2/content_views/base.json.rabl
index 696ba7ad75a..1906fd4d7b2 100644
--- a/app/views/katello/api/v2/content_views/base.json.rabl
+++ b/app/views/katello/api/v2/content_views/base.json.rabl
@@ -2,7 +2,7 @@ extends 'katello/api/v2/common/identifier'
extends 'katello/api/v2/common/org_reference'
attributes :composite
-attributes :component_ids
+attributes :component_ids, :duplicate_repositories_to_publish
attributes :default
attributes :version_count
attributes :latest_version, :latest_version_id
diff --git a/webpack/scenes/ContentViews/Details/ComponentContentViews/ComponentContentViewAddModal.js b/webpack/scenes/ContentViews/Details/ComponentContentViews/ComponentContentViewAddModal.js
index 0774e9f896e..52d88148606 100644
--- a/webpack/scenes/ContentViews/Details/ComponentContentViews/ComponentContentViewAddModal.js
+++ b/webpack/scenes/ContentViews/Details/ComponentContentViews/ComponentContentViewAddModal.js
@@ -13,8 +13,7 @@ import {
selectCVDetails,
selectCVDetailStatus,
} from '../../Details/ContentViewDetailSelectors';
-import { addComponent } from '../ContentViewDetailActions';
-import { CONTENT_VIEW_NEEDS_PUBLISH } from '../../ContentViewsConstants';
+import getContentViewDetails, { addComponent } from '../ContentViewDetailActions';
const ComponentContentViewAddModal = ({
cvId, componentCvId, componentId, latest, componentVersionId, show, setIsOpen,
@@ -67,12 +66,12 @@ const ComponentContentViewAddModal = ({
dispatch(addComponent({
compositeContentViewId: cvId,
components: getUpdateParams(),
- }, () => dispatch({ type: CONTENT_VIEW_NEEDS_PUBLISH })));
+ }, () => dispatch(getContentViewDetails(cvId))));
} else {
dispatch(addComponent({
compositeContentViewId: cvId,
components: getAddParams(),
- }, () => dispatch({ type: CONTENT_VIEW_NEEDS_PUBLISH })));
+ }, () => dispatch(getContentViewDetails(cvId))));
}
setIsOpen(false);
};
diff --git a/webpack/scenes/ContentViews/Details/ComponentContentViews/ComponentContentViewBulkAddModal.js b/webpack/scenes/ContentViews/Details/ComponentContentViews/ComponentContentViewBulkAddModal.js
index d78b42531d5..d235a655d44 100644
--- a/webpack/scenes/ContentViews/Details/ComponentContentViews/ComponentContentViewBulkAddModal.js
+++ b/webpack/scenes/ContentViews/Details/ComponentContentViews/ComponentContentViewBulkAddModal.js
@@ -8,7 +8,7 @@ import {
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
import { useDispatch } from 'react-redux';
import { translate as __ } from 'foremanReact/common/I18n';
-import { addComponent } from '../ContentViewDetailActions';
+import getContentViewDetails, { addComponent } from '../ContentViewDetailActions';
const ComponentContentViewBulkAddModal = ({ cvId, rowsToAdd, onClose }) => {
const dispatch = useDispatch();
@@ -37,7 +37,7 @@ const ComponentContentViewBulkAddModal = ({ cvId, rowsToAdd, onClose }) => {
dispatch(addComponent({
compositeContentViewId: cvId,
components: bulkAddParams(),
- }));
+ }, () => dispatch(getContentViewDetails(cvId))));
onClose();
};
diff --git a/webpack/scenes/ContentViews/Details/ComponentContentViews/ContentViewComponents.js b/webpack/scenes/ContentViews/Details/ComponentContentViews/ContentViewComponents.js
index a57c9f4127e..cdf8265098f 100644
--- a/webpack/scenes/ContentViews/Details/ComponentContentViews/ContentViewComponents.js
+++ b/webpack/scenes/ContentViews/Details/ComponentContentViews/ContentViewComponents.js
@@ -30,7 +30,7 @@ import AddedStatusLabel from '../../../../components/AddedStatusLabel';
import ComponentVersion from './ComponentVersion';
import ComponentEnvironments from './ComponentEnvironments';
import ContentViewIcon from '../../components/ContentViewIcon';
-import { ADDED, ALL_STATUSES, CONTENT_VIEW_NEEDS_PUBLISH, NOT_ADDED } from '../../ContentViewsConstants';
+import { ADDED, ALL_STATUSES, NOT_ADDED } from '../../ContentViewsConstants';
import SelectableDropdown from '../../../../components/SelectableDropdown/SelectableDropdown';
import '../../../../components/EditableTextInput/editableTextInput.scss';
import ComponentContentViewAddModal from './ComponentContentViewAddModal';
@@ -98,7 +98,7 @@ const ContentViewComponents = ({ cvId, details }) => {
dispatch(addComponent({
compositeContentViewId: cvId,
components: [{ latest: true, content_view_id: componentCvId }],
- }, () => dispatch({ type: CONTENT_VIEW_NEEDS_PUBLISH })));
+ }, () => dispatch(getContentViewDetails(cvId))));
}
}, [cvId, dispatch]);
@@ -108,7 +108,7 @@ const ContentViewComponents = ({ cvId, details }) => {
dispatch(removeComponent({
compositeContentViewId: cvId,
component_ids: componentIds,
- }, () => dispatch({ type: CONTENT_VIEW_NEEDS_PUBLISH })));
+ }, () => dispatch(getContentViewDetails(cvId))));
};
const addBulk = () => {
@@ -122,7 +122,7 @@ const ContentViewComponents = ({ cvId, details }) => {
dispatch(removeComponent({
compositeContentViewId: cvId,
component_ids: [componentIdToRemove],
- }, () => dispatch({ type: CONTENT_VIEW_NEEDS_PUBLISH })));
+ }, () => dispatch(getContentViewDetails(cvId))));
};
const toggleBulkAction = () => {
diff --git a/webpack/scenes/ContentViews/Details/ComponentContentViews/__tests__/compositeCVDetails.fixtures.json b/webpack/scenes/ContentViews/Details/ComponentContentViews/__tests__/compositeCVDetails.fixtures.json
new file mode 100644
index 00000000000..5feb78ebdff
--- /dev/null
+++ b/webpack/scenes/ContentViews/Details/ComponentContentViews/__tests__/compositeCVDetails.fixtures.json
@@ -0,0 +1,393 @@
+{
+ "content_host_count": 0,
+ "composite": true,
+ "component_ids": [
+ 5,
+ 7
+ ],
+ "duplicate_repositories_to_publish": [
+ {
+ "id": 1,
+ "name": "repo1",
+ "components": [
+ {
+ "content_view_name": "test",
+ "content_view_version": "2.0"
+ },
+ {
+ "content_view_name": "dev",
+ "content_view_version": "3.0"
+ }
+ ]
+ }
+ ],
+ "default": false,
+ "version_count": 1,
+ "latest_version": "1.0",
+ "latest_version_id": 10,
+ "auto_publish": false,
+ "solve_dependencies": false,
+ "import_only": false,
+ "generated_for": "none",
+ "related_cv_count": 2,
+ "related_composite_cvs": [],
+ "needs_publish": false,
+ "filtered": false,
+ "repository_ids": [
+ 39,
+ 50,
+ 51,
+ 52,
+ 49
+ ],
+ "id": 4,
+ "name": "dup_container",
+ "label": "dup_container",
+ "description": "",
+ "organization_id": 1,
+ "organization": {
+ "name": "Default Organization",
+ "label": "Default_Organization",
+ "id": 1
+ },
+ "created_at": "2023-09-15 13:19:51 -0400",
+ "updated_at": "2023-09-15 14:34:29 -0400",
+ "last_task": {
+ "id": "14bb82d0-fdea-4316-a4dc-0e3f914acf8e",
+ "started_at": "2023-09-15 14:34:28 -0400",
+ "result": "success",
+ "last_sync_words": "3 days"
+ },
+ "latest_version_environments": [
+ {
+ "id": 1,
+ "name": "Library",
+ "label": "Library"
+ }
+ ],
+ "repositories": [
+ {
+ "id": 39,
+ "name": "repo1",
+ "label": "repo1",
+ "content_type": "yum"
+ },
+ {
+ "id": 50,
+ "name": "repo1",
+ "label": "repo1",
+ "content_type": "yum"
+ },
+ {
+ "id": 51,
+ "name": "docker_repo",
+ "label": "docker_repo",
+ "content_type": "docker"
+ },
+ {
+ "id": 52,
+ "name": "python-pulp",
+ "label": "python-pulp",
+ "content_type": "python"
+ },
+ {
+ "id": 49,
+ "name": "test_ansible",
+ "label": "test_ansible",
+ "content_type": "ansible_collection"
+ }
+ ],
+ "versions": [
+ {
+ "id": 10,
+ "version": "1.0",
+ "published": "2023-09-15 14:34:29 -0400",
+ "description": "",
+ "environment_ids": [
+ 1
+ ],
+ "filters_applied": false,
+ "published_at_words": "3 days"
+ }
+ ],
+ "components": [
+ {
+ "id": 5,
+ "name": "test 2.0",
+ "content_view_id": 2,
+ "version": "2.0",
+ "environments": [
+ {
+ "id": 2,
+ "name": "dev",
+ "label": "dev"
+ },
+ {
+ "id": 3,
+ "name": "test",
+ "label": "test"
+ }
+ ],
+ "content_view": {
+ "id": 2,
+ "name": "test",
+ "label": "test",
+ "description": "",
+ "next_version": 4,
+ "latest_version": "3.0"
+ },
+ "repositories": [
+ {
+ "id": 39,
+ "name": "repo1",
+ "label": "repo1",
+ "description": null
+ }
+ ]
+ },
+ {
+ "id": 7,
+ "name": "dev 3.0",
+ "content_view_id": 3,
+ "version": "3.0",
+ "environments": [
+ {
+ "id": 2,
+ "name": "dev",
+ "label": "dev"
+ }
+ ],
+ "content_view": {
+ "id": 3,
+ "name": "dev",
+ "label": "dev",
+ "description": "",
+ "next_version": 5,
+ "latest_version": "4.0"
+ },
+ "repositories": [
+ {
+ "id": 50,
+ "name": "repo1",
+ "label": "repo1",
+ "description": null
+ },
+ {
+ "id": 51,
+ "name": "docker_repo",
+ "label": "docker_repo",
+ "description": null
+ },
+ {
+ "id": 52,
+ "name": "python-pulp",
+ "label": "python-pulp",
+ "description": null
+ },
+ {
+ "id": 49,
+ "name": "test_ansible",
+ "label": "test_ansible",
+ "description": null
+ }
+ ]
+ }
+ ],
+ "content_view_components": [
+ {
+ "latest": false,
+ "id": 2,
+ "created_at": "2023-09-15 13:28:32 -0400",
+ "updated_at": "2023-09-15 13:28:32 -0400",
+ "composite_content_view": {
+ "id": 6,
+ "name": "dup_container",
+ "label": "dup_container",
+ "description": "",
+ "next_version": 2,
+ "latest_version": "1.0",
+ "version_count": 1
+ },
+ "content_view": {
+ "id": 2,
+ "name": "test",
+ "label": "test",
+ "description": "",
+ "next_version": 4,
+ "latest_version": "3.0",
+ "version_count": 3
+ },
+ "content_view_version": {
+ "id": 5,
+ "name": "test 2.0",
+ "content_view_id": 2,
+ "version": "2.0",
+ "content_view": {
+ "id": 2,
+ "name": "test",
+ "label": "test",
+ "description": ""
+ },
+ "environments": [
+ {
+ "id": 2,
+ "name": "dev",
+ "label": "dev"
+ },
+ {
+ "id": 3,
+ "name": "test",
+ "label": "test"
+ }
+ ],
+ "repositories": [
+ {
+ "id": 39,
+ "name": "repo1",
+ "label": "repo1",
+ "description": null
+ }
+ ]
+ },
+ "component_content_view_versions": [
+ {
+ "id": 8,
+ "version": "3.0",
+ "description": "",
+ "published_at_words": "3 days"
+ },
+ {
+ "id": 5,
+ "version": "2.0",
+ "description": "",
+ "published_at_words": "4 days"
+ },
+ {
+ "id": 3,
+ "version": "1.0",
+ "description": "",
+ "published_at_words": "18 days"
+ }
+ ]
+ },
+ {
+ "latest": false,
+ "id": 1,
+ "created_at": "2023-09-15 13:28:32 -0400",
+ "updated_at": "2023-09-15 13:58:39 -0400",
+ "composite_content_view": {
+ "id": 6,
+ "name": "dup_container",
+ "label": "dup_container",
+ "description": "",
+ "next_version": 2,
+ "latest_version": "1.0",
+ "version_count": 1
+ },
+ "content_view": {
+ "id": 3,
+ "name": "dev",
+ "label": "dev",
+ "description": "",
+ "next_version": 5,
+ "latest_version": "4.0",
+ "version_count": 4
+ },
+ "content_view_version": {
+ "id": 7,
+ "name": "dev 3.0",
+ "content_view_id": 3,
+ "version": "3.0",
+ "content_view": {
+ "id": 3,
+ "name": "dev",
+ "label": "dev",
+ "description": ""
+ },
+ "environments": [
+ {
+ "id": 2,
+ "name": "dev",
+ "label": "dev"
+ }
+ ],
+ "repositories": [
+ {
+ "id": 50,
+ "name": "repo1",
+ "label": "repo1",
+ "description": null
+ },
+ {
+ "id": 51,
+ "name": "docker_repo",
+ "label": "docker_repo",
+ "description": null
+ },
+ {
+ "id": 52,
+ "name": "python-pulp",
+ "label": "python-pulp",
+ "description": null
+ },
+ {
+ "id": 49,
+ "name": "test_ansible",
+ "label": "test_ansible",
+ "description": null
+ }
+ ]
+ },
+ "component_content_view_versions": [
+ {
+ "id": 9,
+ "version": "4.0",
+ "description": "",
+ "published_at_words": "3 days"
+ },
+ {
+ "id": 7,
+ "version": "3.0",
+ "description": "",
+ "published_at_words": "4 days"
+ },
+ {
+ "id": 6,
+ "version": "2.0",
+ "description": "",
+ "published_at_words": "4 days"
+ },
+ {
+ "id": 2,
+ "version": "1.0",
+ "description": "",
+ "published_at_words": "18 days"
+ }
+ ]
+ }
+ ],
+ "activation_keys": [],
+ "hosts": [],
+ "next_version": "2.0",
+ "last_published": "2023-09-15 14:34:29 -0400",
+ "environments": [
+ {
+ "id": 1,
+ "label": "Library",
+ "name": "Library",
+ "activation_keys": [],
+ "hosts": [],
+ "permissions": {
+ "readable": true
+ }
+ }
+ ],
+ "permissions": {
+ "view_content_views": true,
+ "edit_content_views": true,
+ "destroy_content_views": true,
+ "publish_content_views": true,
+ "promote_or_remove_content_views": true
+ },
+ "errors": null
+}
\ No newline at end of file
diff --git a/webpack/scenes/ContentViews/Details/ComponentContentViews/__tests__/contentViewComponents.test.js b/webpack/scenes/ContentViews/Details/ComponentContentViews/__tests__/contentViewComponents.test.js
index 9814a004cab..2c0146cf5fe 100644
--- a/webpack/scenes/ContentViews/Details/ComponentContentViews/__tests__/contentViewComponents.test.js
+++ b/webpack/scenes/ContentViews/Details/ComponentContentViews/__tests__/contentViewComponents.test.js
@@ -8,12 +8,14 @@ import cvComponentData from './contentViewComponents.fixtures.json';
import cvUnpublishedComponentData from './unpublishedCVComponents.fixtures.json';
import cvPublishedComponentData from './publishedContentViewDetails.fixtures.json';
import cvDetails from '../../__tests__/contentViewDetails.fixtures.json';
+import compositeCvDetails from './compositeCVDetails.fixtures.json';
const renderOptions = { apiNamespace: `${CONTENT_VIEWS_KEY}_1` };
const cvComponentsWithoutSearch = api.getApiUrl('/content_views/4/content_view_components/show_all?per_page=20&page=1&status=All');
const cvComponents = api.getApiUrl('/content_views/4/content_view_components/show_all?per_page=20&page=1&search=&status=All');
const addComponentURL = api.getApiUrl('/content_views/4/content_view_components/add');
const publishedComponentDetailsURL = api.getApiUrl('/content_views/13');
+const cvDetailsPath = api.getApiUrl('/content_views/4');
const removeComponentURL = api.getApiUrl('/content_views/4/content_view_components/remove');
const autocompleteUrl = '/content_views/auto_complete_search';
const autocompleteQuery = {
@@ -208,6 +210,11 @@ test('Can add published component views to content view with modal', async (done
.put(addComponentURL, addComponentParams)
.reply(200, {});
+ const cvDetailsScope = nockInstance
+ .get(cvDetailsPath)
+ .query(true)
+ .reply(200, compositeCvDetails);
+
const {
getByText, getByLabelText, queryByLabelText, getAllByLabelText,
} = renderWithRedux(
@@ -234,7 +241,8 @@ test('Can add published component views to content view with modal', async (done
assertNockRequest(scope);
assertNockRequest(publishedComponentVersionsScope);
assertNockRequest(addComponentScope);
- assertNockRequest(returnScope, done);
+ assertNockRequest(returnScope);
+ assertNockRequest(cvDetailsScope, done);
});
test('Can add unpublished component views to content view', async (done) => {
@@ -256,6 +264,11 @@ test('Can add unpublished component views to content view', async (done) => {
.put(addComponentURL, addComponentParams)
.reply(200, {});
+ const cvDetailsScope = nockInstance
+ .get(cvDetailsPath)
+ .query(true)
+ .reply(200, compositeCvDetails);
+
const { getByText, getAllByLabelText } = renderWithRedux(
,
renderOptions,
@@ -270,7 +283,8 @@ test('Can add unpublished component views to content view', async (done) => {
assertNockRequest(autocompleteScope);
assertNockRequest(scope);
assertNockRequest(addComponentScope);
- assertNockRequest(returnScope, done);
+ assertNockRequest(returnScope);
+ assertNockRequest(cvDetailsScope, done);
});
test('Can remove component views from content view', async (done) => {
@@ -292,6 +306,11 @@ test('Can remove component views from content view', async (done) => {
.put(removeComponentURL, removeComponentParams)
.reply(200, {});
+ const cvDetailsScope = nockInstance
+ .get(cvDetailsPath)
+ .query(true)
+ .reply(200, compositeCvDetails);
+
const { getByText, getAllByLabelText } = renderWithRedux(
,
renderOptions,
@@ -306,7 +325,8 @@ test('Can remove component views from content view', async (done) => {
assertNockRequest(autocompleteScope);
assertNockRequest(scope);
assertNockRequest(removeComponentScope);
- assertNockRequest(returnScope, done);
+ assertNockRequest(returnScope);
+ assertNockRequest(cvDetailsScope, done);
});
test('Can bulk add component views to content view with modal', async (done) => {
@@ -328,6 +348,11 @@ test('Can bulk add component views to content view with modal', async (done) =>
.put(addComponentURL, addComponentParams)
.reply(200, {});
+ const cvDetailsScope = nockInstance
+ .get(cvDetailsPath)
+ .query(true)
+ .reply(200, compositeCvDetails);
+
const {
getAllByText, getByLabelText, queryByText, getAllByRole,
} = renderWithRedux(
@@ -360,5 +385,6 @@ test('Can bulk add component views to content view with modal', async (done) =>
assertNockRequest(autocompleteScope);
assertNockRequest(scope);
assertNockRequest(addComponentScope);
- assertNockRequest(returnScope, done);
+ assertNockRequest(returnScope);
+ assertNockRequest(cvDetailsScope, done);
});
diff --git a/webpack/scenes/ContentViews/Publish/CVPublishForm.js b/webpack/scenes/ContentViews/Publish/CVPublishForm.js
index 6f630d28729..299c4761337 100644
--- a/webpack/scenes/ContentViews/Publish/CVPublishForm.js
+++ b/webpack/scenes/ContentViews/Publish/CVPublishForm.js
@@ -17,7 +17,10 @@ const CVPublishForm = ({
description,
setDescription,
details: {
- name, composite, next_version: nextVersion, needs_publish: needsPublish,
+ name, composite,
+ next_version: nextVersion,
+ needs_publish: needsPublish,
+ duplicate_repositories_to_publish: duplicateRepos,
},
userCheckedItems,
setUserCheckedItems,
@@ -27,6 +30,7 @@ const CVPublishForm = ({
}) => {
const [alertDismissed, setAlertDismissed] = useState(false);
const [needsPublishAlertDismissed, setNeedsPublishAlertDismissed] = useState(false);
+ const [duplicateReposAlertDismissed, setDuplicateReposAlertDismissed] = useState(false);
const needsPublishLocal = useSelector(state => selectCVNeedsPublish(state));
const checkPromote = (checked) => {
@@ -61,6 +65,24 @@ const CVPublishForm = ({
{__('Newly published version will be the same as the previous version.')}
)
}
+ {!duplicateReposAlertDismissed && composite &&
+ (duplicateRepos !== null || duplicateRepos.length > 0) &&
+ (
+ setDuplicateReposAlertDismissed(true)}
+ />
+ }
+ style={{ marginBottom: '24px' }}
+ >
+ {__('Repositories common to the selected content view versions will merge, resulting in a composite content view that is a union of all content from each of the content view versions.')}
+ )
+ }
{__('A new version of ')}{composite ? : } {name}
{__(' will be created and automatically promoted to the ' +
'Library environment. You can promote to other environments as well. ')
@@ -142,6 +164,7 @@ CVPublishForm.propTypes = {
PropTypes.string,
]).isRequired,
needs_publish: PropTypes.bool,
+ duplicate_repositories_to_publish: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
}).isRequired,
};
diff --git a/webpack/scenes/ContentViews/Publish/__tests__/publishContentView.test.js b/webpack/scenes/ContentViews/Publish/__tests__/publishContentView.test.js
index 5b69ff97523..32312069b48 100644
--- a/webpack/scenes/ContentViews/Publish/__tests__/publishContentView.test.js
+++ b/webpack/scenes/ContentViews/Publish/__tests__/publishContentView.test.js
@@ -38,6 +38,48 @@ test('Can call API and show Wizard', async (done) => {
assertNockRequest(filterScope, done);
});
+test('Can show wizard with duplicate repository warning for composite CV', async (done) => {
+ const cvCompositeDetailsData = cvDetailData;
+ cvCompositeDetailsData.composite = true;
+ cvCompositeDetailsData.duplicate_repositories_to_publish = [
+ {
+ id: 1,
+ name: 'repo1',
+ components: [
+ {
+ content_view_name: 'test',
+ content_view_version: '2.0',
+ },
+ {
+ content_view_name: 'dev',
+ content_view_version: '3.0',
+ },
+ ],
+ },
+ ];
+ const scope = nockInstance
+ .get(environmentPathsPath)
+ .query(true)
+ .reply(200, environmentPathsData);
+ const filterScope = nockInstance
+ .get(cvFiltersPath)
+ .reply(200, contentViewFilterData);
+
+ const { getByText } = renderWithRedux( { }}
+ />);
+
+ await patientlyWaitFor(() => {
+ expect(getByText('Publish new version - 6.0')).toBeInTheDocument();
+ expect(getByText('Repositories common to the selected content view versions will merge, resulting in a composite content view that is a union of all content from each of the content view versions.')).toBeTruthy();
+ });
+
+ assertNockRequest(scope);
+ assertNockRequest(filterScope, done);
+});
+
test('Can show Wizard and show environment paths', async (done) => {
const scope = nockInstance
.get(environmentPathsPath)
diff --git a/webpack/scenes/ContentViews/__tests__/mockDetails.fixtures.json b/webpack/scenes/ContentViews/__tests__/mockDetails.fixtures.json
index e2ef9a36e81..731689eb829 100644
--- a/webpack/scenes/ContentViews/__tests__/mockDetails.fixtures.json
+++ b/webpack/scenes/ContentViews/__tests__/mockDetails.fixtures.json
@@ -12,5 +12,6 @@
"next_version": "1.0",
"id": "5",
"version_count": 0,
- "filtered": true
+ "filtered": true,
+ "duplicate_repositories_to_publish": []
}
\ No newline at end of file