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