diff --git a/app/models/katello/concerns/smart_proxy_extensions.rb b/app/models/katello/concerns/smart_proxy_extensions.rb index b01d46fd54c..8ba281c460e 100644 --- a/app/models/katello/concerns/smart_proxy_extensions.rb +++ b/app/models/katello/concerns/smart_proxy_extensions.rb @@ -145,25 +145,14 @@ def update_content_counts! translated_counts[::Katello::Pulp3::PulpContentUnit.katello_name_from_pulpcore_name(name, repo)] = count end end - new_content_counts[:content_view_versions][repo.content_view_version_id] ||= { repositories: {}, cv_version_content_counts: {}} - new_content_counts[:content_view_versions][repo.content_view_version_id][:repositories][repo.id] = translated_counts - new_content_counts = aggregated_cv_version_count!(repo.content_view_version_id, new_content_counts, translated_counts) + new_content_counts[:content_view_versions][repo.content_view_version_id] ||= { repositories: {}} + # Store counts on capsule of archived repos which are reused across environment copies + # of the archived repo corresponding to each environment CV version is promoted to. + new_content_counts[:content_view_versions][repo.content_view_version_id][:repositories][repo.content_view_version.archived_repos.find_by(library_instance_id: repo.library_instance_id)&.id] = translated_counts end update(content_counts: new_content_counts) end - def aggregated_cv_version_count!(cv_version_id, cvv_content_counts, repo_counts) - repo_counts.keys.each do |content_type| - cvv_content_counts[:content_view_versions][cv_version_id][:cv_version_content_counts][content_type] = - if cvv_content_counts[:content_view_versions][cv_version_id][:cv_version_content_counts][content_type] - cvv_content_counts[:content_view_versions][cv_version_id][:cv_version_content_counts][content_type] + repo_counts[content_type] - else - repo_counts[content_type] - end - end - cvv_content_counts - end - def sync_container_gateway if has_feature?(::SmartProxy::CONTAINER_GATEWAY_FEATURE) update_container_repo_list diff --git a/app/services/katello/pulp3/ansible_collection.rb b/app/services/katello/pulp3/ansible_collection.rb index 4529c77034e..5245f9b387f 100644 --- a/app/services/katello/pulp3/ansible_collection.rb +++ b/app/services/katello/pulp3/ansible_collection.rb @@ -2,7 +2,7 @@ module Katello module Pulp3 class AnsibleCollection < PulpContentUnit include LazyAccessor - PULPCORE_CONTENT_TYPE = "ansible.collection".freeze + PULPCORE_CONTENT_TYPE = "ansible.collection_version".freeze def self.content_api PulpAnsibleClient::ContentCollectionVersionsApi.new(Katello::Pulp3::Api::AnsibleCollection.new(SmartProxy.pulp_primary!).api_client) diff --git a/app/views/foreman/smart_proxies/_content_tab.html.erb b/app/views/foreman/smart_proxies/_content_tab.html.erb index f627beb4e06..233c548d18f 100644 --- a/app/views/foreman/smart_proxies/_content_tab.html.erb +++ b/app/views/foreman/smart_proxies/_content_tab.html.erb @@ -1,5 +1,3 @@ <%= javascript_include_tag *webpack_asset_paths('katello', extension: 'js') %> - -
<% @smartProxyId= @smart_proxy.id %> <%= react_component('Content', smartProxyId: @smartProxyId,) %> diff --git a/app/views/katello/api/v2/capsule_content/sync_status.json.rabl b/app/views/katello/api/v2/capsule_content/sync_status.json.rabl index e499e14b328..d93d10048b1 100644 --- a/app/views/katello/api/v2/capsule_content/sync_status.json.rabl +++ b/app/views/katello/api/v2/capsule_content/sync_status.json.rabl @@ -27,7 +27,8 @@ child @lifecycle_environments => :lifecycle_environments do if @capsule.has_feature?(SmartProxy::PULP_NODE_FEATURE) || @capsule.has_feature?(SmartProxy::PULP3_FEATURE) node :counts do |env| { - :content_views => env.content_views.non_default.count + :content_views => env.content_views.non_default.count, + :content_counts => @capsule.content_counts } end @@ -35,6 +36,7 @@ child @lifecycle_environments => :lifecycle_environments do env.content_views.ignore_generated.map do |content_view| attributes = { :id => content_view.id, + :cvv_id => ::Katello::ContentViewVersion.in_environment(env).find_by(:content_view => content_view)&.id, :label => content_view.label, :name => content_view.name, :composite => content_view.composite, @@ -44,7 +46,13 @@ child @lifecycle_environments => :lifecycle_environments do :counts => { :repositories => ::Katello::ContentViewVersion.in_environment(env).find_by(:content_view => content_view)&.archived_repos&.count }, - :content_counts => @capsule.content_counts + :repositories => ::Katello::ContentViewVersion.in_environment(env)&.find_by(:content_view => content_view)&.archived_repos&.map do |repo| + { + :id => repo.id, + :name => repo.name, + :library_id => repo.library_instance_id + } + end } attributes end diff --git a/test/models/concerns/smart_proxy_extensions_test.rb b/test/models/concerns/smart_proxy_extensions_test.rb index 5f86d594d35..0dc795fcb45 100644 --- a/test/models/concerns/smart_proxy_extensions_test.rb +++ b/test/models/concerns/smart_proxy_extensions_test.rb @@ -25,6 +25,7 @@ def test_update_content_counts yum_service = yum_repo.backend_service(@proxy).with_mirror_adapter yum_repo.expects(:backend_service).with(@proxy).once.returns(yum_service) yum_service.expects(:count_by_pulpcore_type).with(::Katello::Pulp3::Srpm).once.returns(1) + yum_repo.update(library_instance_id: yum_repo.id) yum_counts = { "rpm.advisory" => {count: 4, href: 'href'}, "rpm.package" => {count: 32, href: 'href'}, @@ -36,6 +37,7 @@ def test_update_content_counts yum_service.expects(:latest_content_counts).once.returns(yum_counts) file_repo = katello_repositories(:pulp3_file_1) + file_repo.update library_instance_id: file_repo.id file_service = file_repo.backend_service(@proxy).with_mirror_adapter file_repo.expects(:backend_service).with(@proxy).once.returns(file_service) file_counts = { @@ -44,6 +46,7 @@ def test_update_content_counts file_service.expects(:latest_content_counts).once.returns(file_counts) ansible_repo = katello_repositories(:pulp3_ansible_collection_1) + ansible_repo.update library_instance_id: ansible_repo.id ansible_service = ansible_repo.backend_service(@proxy).with_mirror_adapter ansible_repo.expects(:backend_service).with(@proxy).once.returns(ansible_service) ansible_counts = { @@ -52,6 +55,7 @@ def test_update_content_counts ansible_service.expects(:latest_content_counts).once.returns(ansible_counts) container_repo = katello_repositories(:pulp3_docker_1) + container_repo.update library_instance_id: container_repo.id container_repo.docker_manifest_lists << ::Katello::DockerManifestList.create(pulp_id: 'manifester-lister') container_service = container_repo.backend_service(@proxy).with_mirror_adapter container_repo.expects(:backend_service).with(@proxy).once.returns(container_service) @@ -64,6 +68,7 @@ def test_update_content_counts container_service.expects(:latest_content_counts).once.returns(container_counts) ostree_repo = katello_repositories(:pulp3_ostree_1) + ostree_repo.update library_instance_id: ostree_repo.id ostree_service = ostree_repo.backend_service(@proxy).with_mirror_adapter ostree_repo.expects(:backend_service).with(@proxy).once.returns(ostree_service) ostree_counts = { @@ -72,6 +77,7 @@ def test_update_content_counts ostree_service.expects(:latest_content_counts).once.returns(ostree_counts) deb_repo = katello_repositories(:pulp3_deb_1) + deb_repo.update library_instance_id: deb_repo.id deb_service = deb_repo.backend_service(@proxy).with_mirror_adapter deb_repo.expects(:backend_service).with(@proxy).once.returns(deb_service) deb_counts = { @@ -80,15 +86,17 @@ def test_update_content_counts deb_service.expects(:latest_content_counts).once.returns(deb_counts) python_repo = katello_repositories(:pulp3_python_1) + python_repo.update library_instance_id: python_repo.id python_service = python_repo.backend_service(@proxy).with_mirror_adapter python_repo.expects(:backend_service).with(@proxy).once.returns(python_service) python_counts = { "python.python" => {count: 42, href: 'href'} } python_service.expects(:latest_content_counts).once.returns(python_counts) - repos = [yum_repo, file_repo, ansible_repo, container_repo, ostree_repo, deb_repo, python_repo] + yum_repo.content_view_version.expects(:archived_repos).returns(::Katello::Repository.where(id: [yum_repo, file_repo, ansible_repo, container_repo, + ostree_repo, deb_repo, python_repo])) ::Katello::SmartProxyHelper.any_instance.expects(:repositories_available_to_capsule).once.returns(repos) @proxy.update_content_counts! counts = @proxy.content_counts @@ -97,29 +105,11 @@ def test_update_content_counts { "repositories" => { yum_repo.id.to_s => { "erratum" => 4, "srpm" => 1, "rpm" => 31, "rpm.modulemd" => 7, "rpm.modulemd_defaults" => 3, "package_group" => 7, "rpm.packagecategory" => 1 }, file_repo.id.to_s => { "file" => 100 }, - ansible_repo.id.to_s => { "ansible_collection" => 802 }, + ansible_repo.id.to_s => { "ansible.collection" => 802 }, container_repo.id.to_s => { "container.blob" => 30, "docker_manifest_list" => 1, "docker_manifest" => 9, "docker_tag" => 5 }, ostree_repo.id.to_s => {"ostree_ref" => 30 }, deb_repo.id.to_s => { "deb" => 987 }, python_repo.id.to_s => { "python_package" => 42 } - }, - "cv_version_content_counts" => - { "erratum" => 4, - "srpm" => 1, - "rpm" => 31, - "rpm.modulemd" => 7, - "rpm.modulemd_defaults" => 3, - "package_group" => 7, - "rpm.packagecategory" => 1, - "file" => 100, - "ansible_collection" => 802, - "container.blob" => 30, - "docker_manifest_list" => 1, - "docker_manifest" => 9, - "docker_tag" => 5, - "ostree_ref" => 30, - "deb" => 987, - "python_package" => 42 } } } diff --git a/webpack/components/Table/TableWrapper.js b/webpack/components/Table/TableWrapper.js index 8d5b346dfc6..b04503e9a95 100644 --- a/webpack/components/Table/TableWrapper.js +++ b/webpack/components/Table/TableWrapper.js @@ -45,6 +45,7 @@ const TableWrapper = ({ emptySearchBody, hideSearch, alwaysHideToolbar, + hidePagination, nodesBelowSearch, bookmarkController, readOnlyBookmarks, @@ -59,7 +60,7 @@ const TableWrapper = ({ const { pageRowCount } = getPageStats({ total, page, perPage }); const unresolvedStatus = !!allTableProps?.status && allTableProps.status !== STATUS.RESOLVED; const unresolvedStatusOrNoRows = unresolvedStatus || pageRowCount === 0; - const showPagination = !unresolvedStatusOrNoRows; + const showPagination = !unresolvedStatusOrNoRows && !hidePagination; const filtersAreActive = activeFilters?.length && !isEqual(new Set(activeFilters), new Set(allTableProps.defaultFilters)); const hideToolbar = alwaysHideToolbar || (!searchQuery && !filtersAreActive && @@ -308,6 +309,7 @@ TableWrapper.propTypes = { emptySearchBody: PropTypes.string, hideSearch: PropTypes.bool, alwaysHideToolbar: PropTypes.bool, + hidePagination: PropTypes.bool, nodesBelowSearch: PropTypes.node, bookmarkController: PropTypes.string, readOnlyBookmarks: PropTypes.bool, @@ -338,6 +340,7 @@ TableWrapper.defaultProps = { emptySearchBody: __('Try changing your search settings.'), hideSearch: false, alwaysHideToolbar: false, + hidePagination: false, nodesBelowSearch: null, bookmarkController: undefined, readOnlyBookmarks: false, diff --git a/webpack/scenes/Content/ContentConfig.js b/webpack/scenes/Content/ContentConfig.js index b7dfb1e6d4a..addcc592db3 100644 --- a/webpack/scenes/Content/ContentConfig.js +++ b/webpack/scenes/Content/ContentConfig.js @@ -16,6 +16,7 @@ export default [ singularLowercase: __('Python package'), pluralLabel: 'python_packages', singularLabel: 'python_package', + capsuleCountLabel: 'python_package', }, columnHeaders: [ { title: __('Name'), getProperty: unit => ({unit?.name}) }, @@ -81,6 +82,7 @@ export default [ singularLowercase: __('OSTree ref'), pluralLabel: 'ostree_refs', singularLabel: 'ostree_ref', + capsuleCountLabel: 'ostree_ref', }, columnHeaders: [ { title: __('Name'), getProperty: unit => ({unit?.name}) }, @@ -142,6 +144,7 @@ export default [ singularLowercase: __('Ansible collection'), pluralLabel: 'ansible_collections', singularLabel: 'ansible_collection', + capsuleCountLabel: 'ansible_collection', }, columnHeaders: [ { title: __('Name'), getProperty: unit => ({unit?.name}) }, diff --git a/webpack/scenes/SmartProxy/AdditionalCapsuleContent.js b/webpack/scenes/SmartProxy/AdditionalCapsuleContent.js new file mode 100644 index 00000000000..963dd47b50e --- /dev/null +++ b/webpack/scenes/SmartProxy/AdditionalCapsuleContent.js @@ -0,0 +1,93 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ContentConfig from '../Content/ContentConfig'; + +const AdditionalCapsuleContent = ({ counts }) => { + const { + deb: debPackageCount = 0, + docker_manifest: dockerManifestCount = 0, + docker_tag: dockerTagCount = 0, + file: fileCount = 0, + erratum: errataCount = 0, + package_group: packageGroup = 0, + 'rpm.modulemd': moduleStreamCount = 0, + } = counts; + + const contentConfigTypes = ContentConfig.filter(({ names: { capsuleCountLabel } }) => + !!counts[`${capsuleCountLabel}`]) + .map(({ + names: { + capsuleCountLabel, pluralLowercase, + }, + }) => { + const countParam = `${capsuleCountLabel}`; + const count = counts[countParam]; + return { + pluralLowercase, + count, + }; + }); + + return ( + <> + {errataCount > 0 && + <> + {`${errataCount} Errata`}
+ + } + {moduleStreamCount > 0 && + <> + {`${moduleStreamCount} Module streams`}
+ + } + {packageGroup > 0 && + <> + {`${packageGroup} Package groups`}
+ + } + {dockerTagCount > 0 && + <> + {`${dockerTagCount} Container tags`}
+ + } + {dockerManifestCount > 0 && + <> + {`${dockerManifestCount} Container manifests`}
+ + } + {fileCount > 0 && + <> + {`${fileCount} Files`}
+ + } + {debPackageCount > 0 && + <> + {`${debPackageCount} Debian packages`}
+ } + {contentConfigTypes?.length > 0 && + contentConfigTypes.map(({ count, pluralLowercase }) => ( + + {`${count} ${pluralLowercase}`}
+
)) + } + + ); +}; + +AdditionalCapsuleContent.propTypes = { + counts: PropTypes.shape({ + deb: PropTypes.number, + docker_manifest: PropTypes.number, + docker_tag: PropTypes.number, + file: PropTypes.number, + erratum: PropTypes.number, + package_group: PropTypes.number, + 'rpm.modulemd': PropTypes.number, + }), +}; + +AdditionalCapsuleContent.defaultProps = { + counts: {}, +}; + +export default AdditionalCapsuleContent; diff --git a/webpack/scenes/SmartProxy/Content.js b/webpack/scenes/SmartProxy/Content.js index f6f1ca748c4..57c096d0cd1 100644 --- a/webpack/scenes/SmartProxy/Content.js +++ b/webpack/scenes/SmartProxy/Content.js @@ -1,9 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; -import SmartProxyContentTable from './SmartProxyContentTable'; +import SmartProxyExpandableTable from './SmartProxyExpandableTable'; const Content = ({ smartProxyId }) => ( - + ); Content.propTypes = { diff --git a/webpack/scenes/SmartProxy/ExpandableCvDetails.js b/webpack/scenes/SmartProxy/ExpandableCvDetails.js new file mode 100644 index 00000000000..31a63adc64e --- /dev/null +++ b/webpack/scenes/SmartProxy/ExpandableCvDetails.js @@ -0,0 +1,95 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { TableComposable, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; +import { CheckCircleIcon, TimesCircleIcon } from '@patternfly/react-icons'; +import LongDateTime from 'foremanReact/components/common/dates/LongDateTime'; +import { urlBuilder } from 'foremanReact/common/urlHelpers'; +import ContentViewIcon from '../ContentViews/components/ContentViewIcon'; +import { useSet } from '../../components/Table/TableHooks'; +import ExpandedSmartProxyRepositories from './ExpandedSmartProxyRepositories'; + +const ExpandableCvDetails = ({ contentViews, counts }) => { + const columnHeaders = [ + __('Content view'), + __('Last published'), + __('Synced to smart proxy'), + ]; + const { content_counts: contentCounts } = counts; + const expandedTableRows = useSet([]); + const tableRowIsExpanded = id => expandedTableRows.has(id); + + return ( + + + + + {columnHeaders.map(col => ( + + {col} + + ))} + + + {contentViews.map((cv, rowIndex) => { + const { + id, name: cvName, composite, up_to_date: upToDate, cvv_id: version, repositories, + } = cv; + const upToDateVal = upToDate ? : ; + const isExpanded = tableRowIsExpanded(version); + return ( + + + expandedTableRows.onToggle(isOpen, version), + }} + /> + + {cvName}} + /> + + + {upToDateVal} + + + + + + + + ); + })} + + + + ); +}; + +ExpandableCvDetails.propTypes = { + contentViews: PropTypes.arrayOf(PropTypes.shape({})), + counts: PropTypes.shape({ + content_counts: PropTypes.shape({ + content_view_versions: PropTypes.shape({}), + }), + }), +}; + +ExpandableCvDetails.defaultProps = { + contentViews: [], + counts: {}, +}; + +export default ExpandableCvDetails; diff --git a/webpack/scenes/SmartProxy/ExpandedSmartProxyRepositories.js b/webpack/scenes/SmartProxy/ExpandedSmartProxyRepositories.js new file mode 100644 index 00000000000..c7ef1520f6a --- /dev/null +++ b/webpack/scenes/SmartProxy/ExpandedSmartProxyRepositories.js @@ -0,0 +1,56 @@ +/* eslint-disable react/no-array-index-key */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { DataList, DataListItem, DataListItemRow, DataListItemCells, DataListCell } from '@patternfly/react-core'; +import AdditionalCapsuleContent from './AdditionalCapsuleContent'; + +const ExpandedSmartProxyRepositories = ({ contentCounts, repositories }) => { + const getRepositoryNameById = id => (repositories.find(repo => + Number(repo.id) === Number(id)) || {}).name; + + const dataListCellLists = (repo) => { + const cellList = []; + /* eslint-disable max-len */ + cellList.push({getRepositoryNameById(repo)}); + cellList.push({contentCounts[repo].rpm ? `${contentCounts[repo].rpm} Packages` : 'N/A'}); + cellList.push(); + /* eslint-enable max-len */ + return cellList; + }; + return ( + + + + + {__('Repositories')} + , + {__('Packages')}, + {__('Additional Content')}, + ]} + /> + + + {Object.keys(contentCounts).map((repo, index) => ( + + + + + + ))} + + ); +}; + +ExpandedSmartProxyRepositories.propTypes = { + contentCounts: PropTypes.shape({}), + repositories: PropTypes.arrayOf(PropTypes.shape({})), +}; + +ExpandedSmartProxyRepositories.defaultProps = { + contentCounts: {}, + repositories: [{}], +}; + +export default ExpandedSmartProxyRepositories; diff --git a/webpack/scenes/SmartProxy/SmartProxyExpandableTable.js b/webpack/scenes/SmartProxy/SmartProxyExpandableTable.js new file mode 100644 index 00000000000..e90663acf60 --- /dev/null +++ b/webpack/scenes/SmartProxy/SmartProxyExpandableTable.js @@ -0,0 +1,111 @@ +import React, { useState, useCallback } from 'react'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { Thead, Tbody, Th, Tr, Td } from '@patternfly/react-table'; +import getSmartProxyContent from './SmartProxyContentActions'; +import { + selectSmartProxyContent, + selectSmartProxyContentStatus, + selectSmartProxyContentError, +} from './SmartProxyContentSelectors'; +import { useSet } from '../../components/Table/TableHooks'; +import TableWrapper from '../../components/Table/TableWrapper'; +import ExpandableCvDetails from './ExpandableCvDetails'; + +const SmartProxyExpandableTable = ({ smartProxyId }) => { + const response = useSelector(selectSmartProxyContent); + const status = useSelector(selectSmartProxyContentStatus); + const error = useSelector(selectSmartProxyContentError); + const [searchQuery, updateSearchQuery] = useState(''); + const expandedTableRows = useSet([]); + const tableRowIsExpanded = id => expandedTableRows.has(id); + let metadata = {}; + const { + lifecycle_environments: results, + } = response; + if (results) { + metadata = { total: results.length, subtotal: results.length }; + } + const columnHeaders = [ + __('Environment'), + ]; + const fetchItems = useCallback(() => getSmartProxyContent({ smartProxyId }), [smartProxyId]); + + const emptyContentTitle = __('No content views yet'); + const emptyContentBody = __('You currently have no content views to display'); + const emptySearchTitle = __('No matching content views found'); + const emptySearchBody = __('Try changing your search settings.'); + const alwaysHideToolbar = true; + const hidePagination = true; + return ( + + + + + {columnHeaders.map(col => ( + + {col} + + ))} + + + { + results?.map((env, rowIndex) => { + const { + name, id, content_views: contentViews, counts, + } = env; + const isExpanded = tableRowIsExpanded(id); + return ( + + + + expandedTableRows.onToggle(isOpen, id), + }} + /> + {name} + + + + + + + + ); + }) + } + + ); +}; + +SmartProxyExpandableTable.propTypes = { + smartProxyId: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, // The API can sometimes return strings + ]).isRequired, +}; + +export default SmartProxyExpandableTable; diff --git a/webpack/scenes/SmartProxy/__tests__/SmartProxyContentTest.js b/webpack/scenes/SmartProxy/__tests__/SmartProxyContentTest.js index 3c78f51e90d..58a7c4e97bb 100644 --- a/webpack/scenes/SmartProxy/__tests__/SmartProxyContentTest.js +++ b/webpack/scenes/SmartProxy/__tests__/SmartProxyContentTest.js @@ -3,7 +3,7 @@ import { renderWithRedux, patientlyWaitFor } from 'react-testing-lib-wrapper'; import { nockInstance, assertNockRequest } from '../../../test-utils/nockWrapper'; import api from '../../../services/api'; -import SmartProxyContentTable from '../SmartProxyContentTable'; +import SmartProxyExpandableTable from '../SmartProxyExpandableTable'; const smartProxyContentData = require('./SmartProxyContentResult.fixtures.json'); @@ -11,7 +11,7 @@ const smartProxyContentPath = api.getApiUrl('/capsules/1/content/sync'); const smartProxyContent = { ...smartProxyContentData }; -const contentTable = ; +const contentTable = ; test('Can display Smart proxy content table', async (done) => { const detailsScope = nockInstance @@ -19,13 +19,12 @@ test('Can display Smart proxy content table', async (done) => { .query(true) .reply(200, smartProxyContent); - const { getByText, getAllByLabelText } = renderWithRedux(contentTable); + const { getByText, getAllByText, getAllByLabelText } = renderWithRedux(contentTable); await patientlyWaitFor(() => expect(getByText('Environment')).toBeInTheDocument()); - expect(getByText('Content view')).toBeInTheDocument(); - expect(getByText('Type')).toBeInTheDocument(); - expect(getByText('Last published')).toBeInTheDocument(); - expect(getByText('Repositories')).toBeInTheDocument(); - expect(getByText('Synced to smart proxy')).toBeInTheDocument(); + expect(getAllByText('Content view')[0]).toBeInTheDocument(); + expect(getAllByText('Last published')[0]).toBeInTheDocument(); + expect(getAllByText('Repositories')[0]).toBeInTheDocument(); + expect(getAllByText('Synced to smart proxy')[0]).toBeInTheDocument(); expect(getAllByLabelText('Details')[0]).toHaveAttribute('aria-expanded', 'false'); getAllByLabelText('Details')[0].click(); expect(getAllByLabelText('Details')[0]).toHaveAttribute('aria-expanded', 'true');