diff --git a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue index 42f8cea872709450e04b97a415354ac536b759b8..355b4f71d99cb891f5bc69898ecb65de93015ccd 100644 --- a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue +++ b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue @@ -8,6 +8,7 @@ import { CI_RESOURCE_DETAILS_PAGE_NAME } from '../../router/constants'; export default { i18n: { + components: s__('CiCatalog|Components:'), unreleased: s__('CiCatalog|Unreleased'), releasedMessage: s__('CiCatalog|Released %{timeAgo} by %{author}'), }, @@ -34,8 +35,12 @@ export default { authorProfileUrl() { return this.latestVersion.author.webUrl; }, - resourceId() { - return cleanLeadingSeparator(this.resource.webPath); + componentNames() { + const components = this.resource.latestVersion?.components?.nodes; + return components?.map((component) => component.name).join(', ') || null; + }, + detailsPageHref() { + return decodeURIComponent(this.detailsPageResolved.href); }, detailsPageResolved() { return this.$router.resolve({ @@ -43,32 +48,35 @@ export default { params: { id: this.resourceId }, }); }, - detailsPageHref() { - return decodeURIComponent(this.detailsPageResolved.href); - }, entityId() { return getIdFromGraphQLId(this.resource.id); }, - starCount() { - return this.resource?.starCount || 0; + formattedDate() { + return formatDate(this.latestVersion?.releasedAt); }, - starCountText() { - return n__('Star', 'Stars', this.starCount); + hasComponents() { + return Boolean(this.componentNames); }, hasReleasedVersion() { return Boolean(this.latestVersion?.releasedAt); }, - formattedDate() { - return formatDate(this.latestVersion?.releasedAt); - }, latestVersion() { return this.resource?.latestVersion || {}; }, + name() { + return this.latestVersion?.name || this.$options.i18n.unreleased; + }, releasedAt() { return getTimeago().format(this.latestVersion?.releasedAt); }, - name() { - return this.latestVersion?.name || this.$options.i18n.unreleased; + resourceId() { + return cleanLeadingSeparator(this.resource.webPath); + }, + starCount() { + return this.resource?.starCount || 0; + }, + starCountText() { + return n__('Star', 'Stars', this.starCount); }, webPath() { return cleanLeadingSeparator(this.resource?.webPath); @@ -152,6 +160,14 @@ export default { </span> </div> </div> + <div + v-if="hasComponents" + data-testid="ci-resource-component-names" + class="gl-font-sm gl-mt-1" + > + <span class="gl-font-weight-bold"> • {{ $options.i18n.components }} </span> + <span class="gl-text-gray-900">{{ componentNames }}</span> + </div> </div> </li> </template> diff --git a/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql index 316308e96d7ea2174f06d40686542db4b97584b7..252bd6a574391b1dcd0f4ed821dedb559cbc15c3 100644 --- a/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql +++ b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql @@ -7,6 +7,12 @@ fragment CatalogResourceFields on CiCatalogResource { starCount latestVersion { id + components { + nodes { + id + name + } + } name path releasedAt diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d5d92d162084f75718339a361e8518a53b629b40..65515894d7036d4e5584b86ad305f4d37970ef87 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10484,6 +10484,9 @@ msgstr "" msgid "CiCatalog|Components" msgstr "" +msgid "CiCatalog|Components:" +msgstr "" + msgid "CiCatalog|Create a pipeline component repository and make reusing pipeline configurations faster and easier." msgstr "" diff --git a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js index 15add3f307ff8ec1ed93817250dfbb78c7d67a62..67d12b88b6aa716ea4ac3b3a33c6bf609c0342ab 100644 --- a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js +++ b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js @@ -18,6 +18,20 @@ describe('CiResourcesListItem', () => { const router = createRouter(); const resource = catalogSinglePageResponse.data.ciCatalogResources.nodes[0]; + const componentList = { + components: { + nodes: [ + { + id: 'gid://gitlab/Ci::Catalog::Resources::Component/2', + name: 'test-component', + }, + { + id: 'gid://gitlab/Ci::Catalog::Resources::Component/1', + name: 'component_two', + }, + ], + }, + }; const release = { author: { name: 'author', webUrl: '/user/1' }, releasedAt: Date.now(), @@ -42,6 +56,7 @@ describe('CiResourcesListItem', () => { const findAvatar = () => wrapper.findComponent(GlAvatar); const findBadge = () => wrapper.findComponent(GlBadge); + const findComponentNames = () => wrapper.findByTestId('ci-resource-component-names'); const findResourceName = () => wrapper.findByTestId('ci-resource-link'); const findResourceDescription = () => wrapper.findByText(defaultProps.resource.description); const findUserLink = () => wrapper.findByTestId('user-link'); @@ -82,6 +97,35 @@ describe('CiResourcesListItem', () => { }); }); + describe('components', () => { + describe('when there are no components', () => { + beforeEach(() => { + createComponent({ props: { resource: { ...resource, latestVersion: null } } }); + }); + + it('does not render the component names', () => { + expect(findComponentNames().exists()).toBe(false); + }); + }); + + describe('when there are components', () => { + beforeEach(() => { + createComponent({ + props: { resource: { ...resource, latestVersion: { ...componentList, ...release } } }, + }); + }); + + it('renders the component name template', () => { + expect(findComponentNames().exists()).toBe(true); + }); + + it('renders the correct component names', () => { + expect(findComponentNames().text()).toContain(componentList.components.nodes[0].name); + expect(findComponentNames().text()).toContain(componentList.components.nodes[1].name); + }); + }); + }); + describe('release time', () => { describe('when there is no release data', () => { beforeEach(() => { diff --git a/spec/frontend/ci/catalog/mock.js b/spec/frontend/ci/catalog/mock.js index c92564359901fa380f808e100e9d907cf025a923..2aac033b529f31eed027deae888d5de493587420 100644 --- a/spec/frontend/ci/catalog/mock.js +++ b/spec/frontend/ci/catalog/mock.js @@ -1,3 +1,49 @@ +const componentsDetailsMockData = { + __typename: 'CiComponentConnection', + nodes: [ + { + id: 'gid://gitlab/Ci::Component/1', + name: 'Ruby gal', + description: 'This is a pretty amazing component that does EVERYTHING ruby.', + includePath: 'gitlab.com/gitlab-org/ruby-gal@~latest', + inputs: [{ name: 'version', default: '1.0.0', required: true }], + }, + { + id: 'gid://gitlab/Ci::Component/2', + name: 'Javascript madness', + description: 'Adds some spice to your life.', + includePath: 'gitlab.com/gitlab-org/javascript-madness@~latest', + inputs: [ + { name: 'isFun', default: 'true', required: true }, + { name: 'RandomNumber', default: '10', required: false }, + ], + }, + { + id: 'gid://gitlab/Ci::Component/3', + name: 'Go go go', + description: 'When you write Go, you gotta go go go.', + includePath: 'gitlab.com/gitlab-org/go-go-go@~latest', + inputs: [{ name: 'version', default: '1.0.0', required: true }], + }, + ], +}; + +const componentsListMockData = { + nodes: [ + { + id: 'gid://gitlab/Ci::Catalog::Resources::Component/2', + name: 'test-component', + __typename: 'CiCatalogResourceComponent', + }, + { + id: 'gid://gitlab/Ci::Catalog::Resources::Component/1', + name: 'component_two', + __typename: 'CiCatalogResourceComponent', + }, + ], + __typename: 'CiCatalogResourceComponentConnection', +}; + export const emptyCatalogResponseBody = { data: { ciCatalogResources: { @@ -268,7 +314,13 @@ export const catalogSinglePageResponse = { name: 'Project-45 Name', description: 'A simple component', starCount: 0, - latestVersion: null, + latestVersion: { + id: 'gid://gitlab/Ci::Catalog::Resources::Version/2', + components: { + ...componentsListMockData, + }, + __typename: 'CiCatalogResourceVersion', + }, webPath: '/frontend-fixtures/project-45', __typename: 'CiCatalogResource', }, @@ -310,6 +362,7 @@ export const catalogSharedDataMock = { latestVersion: { __typename: 'Release', id: '3', + components: componentsListMockData, name: '1.0.0', path: 'path/to/release', releasedAt: Date.now(), @@ -378,6 +431,9 @@ const generateResourcesNodes = (count = 20, startId = 0) => { latestVersion: { __typename: 'Release', id: '3', + components: { + ...componentsListMockData, + }, name: '1.0.0', path: 'path/to/release', releasedAt: Date.now(), @@ -392,36 +448,6 @@ const generateResourcesNodes = (count = 20, startId = 0) => { export const mockCatalogResourceItem = generateResourcesNodes(1)[0]; -const componentsMockData = { - __typename: 'CiComponentConnection', - nodes: [ - { - id: 'gid://gitlab/Ci::Component/1', - name: 'Ruby gal', - description: 'This is a pretty amazing component that does EVERYTHING ruby.', - includePath: 'gitlab.com/gitlab-org/ruby-gal@~latest', - inputs: [{ name: 'version', default: '1.0.0', required: true }], - }, - { - id: 'gid://gitlab/Ci::Component/2', - name: 'Javascript madness', - description: 'Adds some spice to your life.', - includePath: 'gitlab.com/gitlab-org/javascript-madness@~latest', - inputs: [ - { name: 'isFun', default: 'true', required: true }, - { name: 'RandomNumber', default: '10', required: false }, - ], - }, - { - id: 'gid://gitlab/Ci::Component/3', - name: 'Go go go', - description: 'When you write Go, you gotta go go go.', - includePath: 'gitlab.com/gitlab-org/go-go-go@~latest', - inputs: [{ name: 'version', default: '1.0.0', required: true }], - }, - ], -}; - export const mockComponents = { data: { ciCatalogResource: { @@ -431,7 +457,7 @@ export const mockComponents = { latestVersion: { id: 'gid://gitlab/Version/1', components: { - ...componentsMockData, + ...componentsDetailsMockData, }, }, },