Commit 5b6fafe9 authored by Illya Klymov's avatar Illya Klymov

Merge branch '36422-add-nuget-package-icon-to-package-details' into 'master'

Add NuGet package icon to package details page

See merge request gitlab-org/gitlab!33701
parents 1b0d7e52 24febb8c
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import { GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; import { GlAvatar, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import PackageTags from '../../shared/components/package_tags.vue'; import PackageTags from '../../shared/components/package_tags.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { __ } from '~/locale';
export default { export default {
name: 'PackageTitle', name: 'PackageTitle',
components: { components: {
GlAvatar,
GlIcon, GlIcon,
GlLink, GlLink,
GlSprintf, GlSprintf,
...@@ -19,7 +21,7 @@ export default { ...@@ -19,7 +21,7 @@ export default {
mixins: [timeagoMixin], mixins: [timeagoMixin],
computed: { computed: {
...mapState(['packageEntity', 'packageFiles']), ...mapState(['packageEntity', 'packageFiles']),
...mapGetters(['packageTypeDisplay', 'packagePipeline']), ...mapGetters(['packageTypeDisplay', 'packagePipeline', 'packageIcon']),
hasTagsToDisplay() { hasTagsToDisplay() {
return Boolean(this.packageEntity.tags && this.packageEntity.tags.length); return Boolean(this.packageEntity.tags && this.packageEntity.tags.length);
}, },
...@@ -27,18 +29,31 @@ export default { ...@@ -27,18 +29,31 @@ export default {
return numberToHumanSize(this.packageFiles.reduce((acc, p) => acc + p.size, 0)); return numberToHumanSize(this.packageFiles.reduce((acc, p) => acc + p.size, 0));
}, },
}, },
i18n: {
packageInfo: __('v%{version} published %{timeAgo}'),
},
}; };
</script> </script>
<template> <template>
<div class="flex-column"> <div class="gl-flex-direction-column">
<h1 class="gl-font-size-20-deprecated-no-really-do-not-use-me gl-mt-3 append-bottom-4"> <div class="gl-display-flex">
<gl-avatar
v-if="packageIcon"
:src="packageIcon"
shape="rect"
class="gl-align-self-center gl-mr-4"
data-testid="package-icon"
/>
<div class="gl-display-flex gl-flex-direction-column">
<h1 class="gl-font-size-h1 gl-mt-3 gl-mb-2">
{{ packageEntity.name }} {{ packageEntity.name }}
</h1> </h1>
<div class="gl-display-flex gl-align-items-center text-secondary"> <div class="gl-display-flex gl-align-items-center text-secondary">
<gl-icon name="eye" class="gl-mr-3" /> <gl-icon name="eye" class="gl-mr-3" />
<gl-sprintf message="v%{version} published %{timeAgo}"> <gl-sprintf :message="$options.i18n.packageInfo">
<template #version> <template #version>
{{ packageEntity.version }} {{ packageEntity.version }}
</template> </template>
...@@ -50,11 +65,13 @@ export default { ...@@ -50,11 +65,13 @@ export default {
</template> </template>
</gl-sprintf> </gl-sprintf>
</div> </div>
</div>
</div>
<div class="gl-display-flex flex-wrap gl-align-items-center append-bottom-8"> <div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mb-3">
<div v-if="packageTypeDisplay" class="gl-display-flex align-items-center gl-mr-5"> <div v-if="packageTypeDisplay" class="gl-display-flex gl-align-items-center gl-mr-5">
<gl-icon name="package" class="text-secondary gl-mr-3" /> <gl-icon name="package" class="text-secondary gl-mr-3" />
<span ref="package-type" class="font-weight-bold">{{ packageTypeDisplay }}</span> <span data-testid="package-type" class="gl-font-weight-bold">{{ packageTypeDisplay }}</span>
</div> </div>
<div v-if="hasTagsToDisplay" class="gl-display-flex gl-align-items-center gl-mr-5"> <div v-if="hasTagsToDisplay" class="gl-display-flex gl-align-items-center gl-mr-5">
...@@ -64,9 +81,9 @@ export default { ...@@ -64,9 +81,9 @@ export default {
<div v-if="packagePipeline" class="gl-display-flex gl-align-items-center gl-mr-5"> <div v-if="packagePipeline" class="gl-display-flex gl-align-items-center gl-mr-5">
<gl-icon name="review-list" class="text-secondary gl-mr-3" /> <gl-icon name="review-list" class="text-secondary gl-mr-3" />
<gl-link <gl-link
ref="pipeline-project" data-testid="pipeline-project"
:href="packagePipeline.project.web_url" :href="packagePipeline.project.web_url"
class="font-weight-bold text-truncate" class="gl-font-weight-bold text-truncate"
> >
{{ packagePipeline.project.name }} {{ packagePipeline.project.name }}
</gl-link> </gl-link>
...@@ -74,13 +91,13 @@ export default { ...@@ -74,13 +91,13 @@ export default {
<div <div
v-if="packagePipeline" v-if="packagePipeline"
ref="package-ref" data-testid="package-ref"
class="gl-display-flex gl-align-items-center gl-mr-5" class="gl-display-flex gl-align-items-center gl-mr-5"
> >
<gl-icon name="branch" class="text-secondary gl-mr-3" /> <gl-icon name="branch" class="text-secondary gl-mr-3" />
<span <span
v-gl-tooltip v-gl-tooltip
class="font-weight-bold text-truncate mw-xs" class="gl-font-weight-bold text-truncate mw-xs"
:title="packagePipeline.ref" :title="packagePipeline.ref"
>{{ packagePipeline.ref }}</span >{{ packagePipeline.ref }}</span
> >
...@@ -88,7 +105,7 @@ export default { ...@@ -88,7 +105,7 @@ export default {
<div class="gl-display-flex gl-align-items-center gl-mr-5"> <div class="gl-display-flex gl-align-items-center gl-mr-5">
<gl-icon name="disk" class="text-secondary gl-mr-3" /> <gl-icon name="disk" class="text-secondary gl-mr-3" />
<span ref="package-size" class="font-weight-bold">{{ totalSize }}</span> <span data-testid="package-size" class="gl-font-weight-bold">{{ totalSize }}</span>
</div> </div>
</div> </div>
</div> </div>
......
import { generateConanRecipe } from '../utils'; import { generateConanRecipe } from '../utils';
import { PackageType } from '../../shared/constants';
import { getPackageTypeLabel } from '../../shared/utils'; import { getPackageTypeLabel } from '../../shared/utils';
import { NpmManager } from '../constants'; import { NpmManager } from '../constants';
...@@ -10,6 +11,14 @@ export const packageTypeDisplay = ({ packageEntity }) => { ...@@ -10,6 +11,14 @@ export const packageTypeDisplay = ({ packageEntity }) => {
return getPackageTypeLabel(packageEntity.package_type); return getPackageTypeLabel(packageEntity.package_type);
}; };
export const packageIcon = ({ packageEntity }) => {
if (packageEntity.package_type === PackageType.NUGET) {
return packageEntity.nuget_metadatum?.icon_url || null;
}
return null;
};
export const conanInstallationCommand = ({ packageEntity }) => { export const conanInstallationCommand = ({ packageEntity }) => {
const recipe = generateConanRecipe(packageEntity); const recipe = generateConanRecipe(packageEntity);
......
---
title: Adds NuGet package icon to package details page
merge_request: 33701
author:
type: added
...@@ -2,10 +2,18 @@ ...@@ -2,10 +2,18 @@
exports[`PackageTitle renders with tags 1`] = ` exports[`PackageTitle renders with tags 1`] = `
<div <div
class="flex-column" class="gl-flex-direction-column"
> >
<div
class="gl-display-flex"
>
<!---->
<div
class="gl-display-flex gl-flex-direction-column"
>
<h1 <h1
class="gl-font-size-20-deprecated-no-really-do-not-use-me gl-mt-3 append-bottom-4" class="gl-font-size-h1 gl-mt-3 gl-mb-2"
> >
Test package Test package
...@@ -25,12 +33,14 @@ exports[`PackageTitle renders with tags 1`] = ` ...@@ -25,12 +33,14 @@ exports[`PackageTitle renders with tags 1`] = `
message="v%{version} published %{timeAgo}" message="v%{version} published %{timeAgo}"
/> />
</div> </div>
</div>
</div>
<div <div
class="gl-display-flex flex-wrap gl-align-items-center append-bottom-8" class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mb-3"
> >
<div <div
class="gl-display-flex align-items-center gl-mr-5" class="gl-display-flex gl-align-items-center gl-mr-5"
> >
<gl-icon-stub <gl-icon-stub
class="text-secondary gl-mr-3" class="text-secondary gl-mr-3"
...@@ -39,7 +49,8 @@ exports[`PackageTitle renders with tags 1`] = ` ...@@ -39,7 +49,8 @@ exports[`PackageTitle renders with tags 1`] = `
/> />
<span <span
class="font-weight-bold" class="gl-font-weight-bold"
data-testid="package-type"
> >
maven maven
</span> </span>
...@@ -68,7 +79,8 @@ exports[`PackageTitle renders with tags 1`] = ` ...@@ -68,7 +79,8 @@ exports[`PackageTitle renders with tags 1`] = `
/> />
<span <span
class="font-weight-bold" class="gl-font-weight-bold"
data-testid="package-size"
> >
300 bytes 300 bytes
</span> </span>
...@@ -79,10 +91,18 @@ exports[`PackageTitle renders with tags 1`] = ` ...@@ -79,10 +91,18 @@ exports[`PackageTitle renders with tags 1`] = `
exports[`PackageTitle renders without tags 1`] = ` exports[`PackageTitle renders without tags 1`] = `
<div <div
class="flex-column" class="gl-flex-direction-column"
> >
<div
class="gl-display-flex"
>
<!---->
<div
class="gl-display-flex gl-flex-direction-column"
>
<h1 <h1
class="gl-font-size-20-deprecated-no-really-do-not-use-me gl-mt-3 append-bottom-4" class="gl-font-size-h1 gl-mt-3 gl-mb-2"
> >
Test package Test package
...@@ -102,12 +122,14 @@ exports[`PackageTitle renders without tags 1`] = ` ...@@ -102,12 +122,14 @@ exports[`PackageTitle renders without tags 1`] = `
message="v%{version} published %{timeAgo}" message="v%{version} published %{timeAgo}"
/> />
</div> </div>
</div>
</div>
<div <div
class="gl-display-flex flex-wrap gl-align-items-center append-bottom-8" class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mb-3"
> >
<div <div
class="gl-display-flex align-items-center gl-mr-5" class="gl-display-flex gl-align-items-center gl-mr-5"
> >
<gl-icon-stub <gl-icon-stub
class="text-secondary gl-mr-3" class="text-secondary gl-mr-3"
...@@ -116,7 +138,8 @@ exports[`PackageTitle renders without tags 1`] = ` ...@@ -116,7 +138,8 @@ exports[`PackageTitle renders without tags 1`] = `
/> />
<span <span
class="font-weight-bold" class="gl-font-weight-bold"
data-testid="package-type"
> >
maven maven
</span> </span>
...@@ -138,7 +161,8 @@ exports[`PackageTitle renders without tags 1`] = ` ...@@ -138,7 +161,8 @@ exports[`PackageTitle renders without tags 1`] = `
/> />
<span <span
class="font-weight-bold" class="gl-font-weight-bold"
data-testid="package-size"
> >
300 bytes 300 bytes
</span> </span>
......
...@@ -19,7 +19,11 @@ describe('PackageTitle', () => { ...@@ -19,7 +19,11 @@ describe('PackageTitle', () => {
let wrapper; let wrapper;
let store; let store;
function createComponent(packageEntity = mavenPackage, packageFiles = mavenFiles) { function createComponent({
packageEntity = mavenPackage,
packageFiles = mavenFiles,
icon = null,
} = {}) {
store = new Vuex.Store({ store = new Vuex.Store({
state: { state: {
packageEntity, packageEntity,
...@@ -28,6 +32,7 @@ describe('PackageTitle', () => { ...@@ -28,6 +32,7 @@ describe('PackageTitle', () => {
getters: { getters: {
packageTypeDisplay: ({ packageEntity: { package_type: type } }) => type, packageTypeDisplay: ({ packageEntity: { package_type: type } }) => type,
packagePipeline: ({ packageEntity: { pipeline = null } }) => pipeline, packagePipeline: ({ packageEntity: { pipeline = null } }) => pipeline,
packageIcon: () => icon,
}, },
}); });
...@@ -37,10 +42,11 @@ describe('PackageTitle', () => { ...@@ -37,10 +42,11 @@ describe('PackageTitle', () => {
}); });
} }
const packageType = () => wrapper.find({ ref: 'package-type' }); const packageIcon = () => wrapper.find('[data-testid="package-icon"]');
const packageSize = () => wrapper.find({ ref: 'package-size' }); const packageType = () => wrapper.find('[data-testid="package-type"]');
const pipelineProject = () => wrapper.find({ ref: 'pipeline-project' }); const packageSize = () => wrapper.find('[data-testid="package-size"]');
const packageRef = () => wrapper.find({ ref: 'package-ref' }); const pipelineProject = () => wrapper.find('[data-testid="pipeline-project"]');
const packageRef = () => wrapper.find('[data-testid="package-ref"]');
const packageTags = () => wrapper.find(PackageTags); const packageTags = () => wrapper.find(PackageTags);
afterEach(() => { afterEach(() => {
...@@ -55,12 +61,34 @@ describe('PackageTitle', () => { ...@@ -55,12 +61,34 @@ describe('PackageTitle', () => {
}); });
it('with tags', () => { it('with tags', () => {
createComponent({ ...mavenPackage, tags: mockTags }); createComponent({ packageEntity: { ...mavenPackage, tags: mockTags } });
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
}); });
describe('package icon', () => {
const fakeSrc = 'a-fake-src';
it('shows an icon when provided one from vuex', () => {
createComponent({ icon: fakeSrc });
expect(packageIcon().exists()).toBe(true);
});
it('has the correct src attribute', () => {
createComponent({ icon: fakeSrc });
expect(packageIcon().props('src')).toBe(fakeSrc);
});
it('does not show an icon when not provided one', () => {
createComponent();
expect(packageIcon().exists()).toBe(false);
});
});
describe.each` describe.each`
packageEntity | expectedResult packageEntity | expectedResult
${conanPackage} | ${'conan'} ${conanPackage} | ${'conan'}
...@@ -68,7 +96,7 @@ describe('PackageTitle', () => { ...@@ -68,7 +96,7 @@ describe('PackageTitle', () => {
${npmPackage} | ${'npm'} ${npmPackage} | ${'npm'}
${nugetPackage} | ${'nuget'} ${nugetPackage} | ${'nuget'}
`(`package type`, ({ packageEntity, expectedResult }) => { `(`package type`, ({ packageEntity, expectedResult }) => {
beforeEach(() => createComponent(packageEntity)); beforeEach(() => createComponent({ packageEntity }));
it(`${packageEntity.package_type} should render from Vuex getters ${expectedResult}`, () => { it(`${packageEntity.package_type} should render from Vuex getters ${expectedResult}`, () => {
expect(packageType().text()).toBe(expectedResult); expect(packageType().text()).toBe(expectedResult);
...@@ -77,7 +105,7 @@ describe('PackageTitle', () => { ...@@ -77,7 +105,7 @@ describe('PackageTitle', () => {
describe('calculates the package size', () => { describe('calculates the package size', () => {
it('correctly calulates when there is only 1 file', () => { it('correctly calulates when there is only 1 file', () => {
createComponent(npmPackage, npmFiles); createComponent({ packageEntity: npmPackage, packageFiles: npmFiles });
expect(packageSize().text()).toBe('200 bytes'); expect(packageSize().text()).toBe('200 bytes');
}); });
...@@ -92,8 +120,10 @@ describe('PackageTitle', () => { ...@@ -92,8 +120,10 @@ describe('PackageTitle', () => {
describe('package tags', () => { describe('package tags', () => {
it('displays the package-tags component when the package has tags', () => { it('displays the package-tags component when the package has tags', () => {
createComponent({ createComponent({
packageEntity: {
...npmPackage, ...npmPackage,
tags: mockTags, tags: mockTags,
},
}); });
expect(packageTags().exists()).toBe(true); expect(packageTags().exists()).toBe(true);
...@@ -114,7 +144,7 @@ describe('PackageTitle', () => { ...@@ -114,7 +144,7 @@ describe('PackageTitle', () => {
}); });
it('correctly shows the package ref if there is one', () => { it('correctly shows the package ref if there is one', () => {
createComponent(npmPackage); createComponent({ packageEntity: npmPackage });
expect(packageRef().contains('gl-icon-stub')).toBe(true); expect(packageRef().contains('gl-icon-stub')).toBe(true);
expect(packageRef().text()).toBe(npmPackage.pipeline.ref); expect(packageRef().text()).toBe(npmPackage.pipeline.ref);
...@@ -129,7 +159,7 @@ describe('PackageTitle', () => { ...@@ -129,7 +159,7 @@ describe('PackageTitle', () => {
}); });
it('correctly shows the pipeline project if there is one', () => { it('correctly shows the pipeline project if there is one', () => {
createComponent(npmPackage); createComponent({ packageEntity: npmPackage });
expect(pipelineProject().text()).toBe(npmPackage.pipeline.project.name); expect(pipelineProject().text()).toBe(npmPackage.pipeline.project.name);
expect(pipelineProject().attributes('href')).toBe(npmPackage.pipeline.project.web_url); expect(pipelineProject().attributes('href')).toBe(npmPackage.pipeline.project.web_url);
......
...@@ -3,6 +3,7 @@ import { ...@@ -3,6 +3,7 @@ import {
conanSetupCommand, conanSetupCommand,
packagePipeline, packagePipeline,
packageTypeDisplay, packageTypeDisplay,
packageIcon,
mavenInstallationXml, mavenInstallationXml,
mavenInstallationCommand, mavenInstallationCommand,
mavenSetupXml, mavenSetupXml,
...@@ -104,6 +105,28 @@ describe('Getters PackageDetails Store', () => { ...@@ -104,6 +105,28 @@ describe('Getters PackageDetails Store', () => {
}); });
}); });
describe('packageIcon', () => {
describe('nuget packages', () => {
it('should return nuget package icon', () => {
setupState({ packageEntity: nugetPackage });
expect(packageIcon(state)).toBe(nugetPackage.nuget_metadatum.icon_url);
});
it('should return null when nuget package does not have an icon', () => {
setupState({ packageEntity: { ...nugetPackage, nuget_metadatum: {} } });
expect(packageIcon(state)).toBe(null);
});
});
it('should not find icons for other package types', () => {
setupState({ packageEntity: npmPackage });
expect(packageIcon(state)).toBe(null);
});
});
describe('conan string getters', () => { describe('conan string getters', () => {
it('gets the correct conanInstallationCommand', () => { it('gets the correct conanInstallationCommand', () => {
setupState({ packageEntity: conanPackage }); setupState({ packageEntity: conanPackage });
......
...@@ -118,6 +118,9 @@ export const nugetPackage = { ...@@ -118,6 +118,9 @@ export const nugetPackage = {
updated_at: '2015-12-10', updated_at: '2015-12-10',
version: '1.0.0', version: '1.0.0',
dependency_links: Object.values(dependencyLinks), dependency_links: Object.values(dependencyLinks),
nuget_metadatum: {
icon_url: 'fake-icon',
},
}; };
export const pypiPackage = { export const pypiPackage = {
......
...@@ -27393,6 +27393,9 @@ msgstr "" ...@@ -27393,6 +27393,9 @@ msgstr ""
msgid "uses Kubernetes clusters to deploy your code!" msgid "uses Kubernetes clusters to deploy your code!"
msgstr "" msgstr ""
msgid "v%{version} published %{timeAgo}"
msgstr ""
msgid "verify ownership" msgid "verify ownership"
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment