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>
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 { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { __ } from '~/locale';
export default {
name: 'PackageTitle',
components: {
GlAvatar,
GlIcon,
GlLink,
GlSprintf,
......@@ -19,7 +21,7 @@ export default {
mixins: [timeagoMixin],
computed: {
...mapState(['packageEntity', 'packageFiles']),
...mapGetters(['packageTypeDisplay', 'packagePipeline']),
...mapGetters(['packageTypeDisplay', 'packagePipeline', 'packageIcon']),
hasTagsToDisplay() {
return Boolean(this.packageEntity.tags && this.packageEntity.tags.length);
},
......@@ -27,34 +29,49 @@ export default {
return numberToHumanSize(this.packageFiles.reduce((acc, p) => acc + p.size, 0));
},
},
i18n: {
packageInfo: __('v%{version} published %{timeAgo}'),
},
};
</script>
<template>
<div class="flex-column">
<h1 class="gl-font-size-20-deprecated-no-really-do-not-use-me gl-mt-3 append-bottom-4">
{{ packageEntity.name }}
</h1>
<div class="gl-flex-direction-column">
<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 }}
</h1>
<div class="gl-display-flex gl-align-items-center text-secondary">
<gl-icon name="eye" class="gl-mr-3" />
<gl-sprintf message="v%{version} published %{timeAgo}">
<template #version>
{{ packageEntity.version }}
</template>
<div class="gl-display-flex gl-align-items-center text-secondary">
<gl-icon name="eye" class="gl-mr-3" />
<gl-sprintf :message="$options.i18n.packageInfo">
<template #version>
{{ packageEntity.version }}
</template>
<template #timeAgo>
<span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
&nbsp;{{ timeFormatted(packageEntity.created_at) }}
</span>
</template>
</gl-sprintf>
<template #timeAgo>
<span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
&nbsp;{{ timeFormatted(packageEntity.created_at) }}
</span>
</template>
</gl-sprintf>
</div>
</div>
</div>
<div class="gl-display-flex flex-wrap gl-align-items-center append-bottom-8">
<div v-if="packageTypeDisplay" class="gl-display-flex align-items-center gl-mr-5">
<div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mb-3">
<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" />
<span ref="package-type" class="font-weight-bold">{{ packageTypeDisplay }}</span>
<span data-testid="package-type" class="gl-font-weight-bold">{{ packageTypeDisplay }}</span>
</div>
<div v-if="hasTagsToDisplay" class="gl-display-flex gl-align-items-center gl-mr-5">
......@@ -64,9 +81,9 @@ export default {
<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-link
ref="pipeline-project"
data-testid="pipeline-project"
:href="packagePipeline.project.web_url"
class="font-weight-bold text-truncate"
class="gl-font-weight-bold text-truncate"
>
{{ packagePipeline.project.name }}
</gl-link>
......@@ -74,13 +91,13 @@ export default {
<div
v-if="packagePipeline"
ref="package-ref"
data-testid="package-ref"
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<gl-icon name="branch" class="text-secondary gl-mr-3" />
<span
v-gl-tooltip
class="font-weight-bold text-truncate mw-xs"
class="gl-font-weight-bold text-truncate mw-xs"
:title="packagePipeline.ref"
>{{ packagePipeline.ref }}</span
>
......@@ -88,7 +105,7 @@ export default {
<div class="gl-display-flex gl-align-items-center gl-mr-5">
<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>
......
import { generateConanRecipe } from '../utils';
import { PackageType } from '../../shared/constants';
import { getPackageTypeLabel } from '../../shared/utils';
import { NpmManager } from '../constants';
......@@ -10,6 +11,14 @@ export const packageTypeDisplay = ({ packageEntity }) => {
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 }) => {
const recipe = generateConanRecipe(packageEntity);
......
---
title: Adds NuGet package icon to package details page
merge_request: 33701
author:
type: added
......@@ -2,35 +2,45 @@
exports[`PackageTitle renders with tags 1`] = `
<div
class="flex-column"
class="gl-flex-direction-column"
>
<h1
class="gl-font-size-20-deprecated-no-really-do-not-use-me gl-mt-3 append-bottom-4"
>
Test package
</h1>
<div
class="gl-display-flex gl-align-items-center text-secondary"
class="gl-display-flex"
>
<gl-icon-stub
class="gl-mr-3"
name="eye"
size="16"
/>
<!---->
<gl-sprintf-stub
message="v%{version} published %{timeAgo}"
/>
<div
class="gl-display-flex gl-flex-direction-column"
>
<h1
class="gl-font-size-h1 gl-mt-3 gl-mb-2"
>
Test package
</h1>
<div
class="gl-display-flex gl-align-items-center text-secondary"
>
<gl-icon-stub
class="gl-mr-3"
name="eye"
size="16"
/>
<gl-sprintf-stub
message="v%{version} published %{timeAgo}"
/>
</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
class="gl-display-flex align-items-center gl-mr-5"
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<gl-icon-stub
class="text-secondary gl-mr-3"
......@@ -39,7 +49,8 @@ exports[`PackageTitle renders with tags 1`] = `
/>
<span
class="font-weight-bold"
class="gl-font-weight-bold"
data-testid="package-type"
>
maven
</span>
......@@ -68,7 +79,8 @@ exports[`PackageTitle renders with tags 1`] = `
/>
<span
class="font-weight-bold"
class="gl-font-weight-bold"
data-testid="package-size"
>
300 bytes
</span>
......@@ -79,35 +91,45 @@ exports[`PackageTitle renders with tags 1`] = `
exports[`PackageTitle renders without tags 1`] = `
<div
class="flex-column"
class="gl-flex-direction-column"
>
<h1
class="gl-font-size-20-deprecated-no-really-do-not-use-me gl-mt-3 append-bottom-4"
>
Test package
</h1>
<div
class="gl-display-flex gl-align-items-center text-secondary"
class="gl-display-flex"
>
<gl-icon-stub
class="gl-mr-3"
name="eye"
size="16"
/>
<!---->
<gl-sprintf-stub
message="v%{version} published %{timeAgo}"
/>
<div
class="gl-display-flex gl-flex-direction-column"
>
<h1
class="gl-font-size-h1 gl-mt-3 gl-mb-2"
>
Test package
</h1>
<div
class="gl-display-flex gl-align-items-center text-secondary"
>
<gl-icon-stub
class="gl-mr-3"
name="eye"
size="16"
/>
<gl-sprintf-stub
message="v%{version} published %{timeAgo}"
/>
</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
class="gl-display-flex align-items-center gl-mr-5"
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<gl-icon-stub
class="text-secondary gl-mr-3"
......@@ -116,7 +138,8 @@ exports[`PackageTitle renders without tags 1`] = `
/>
<span
class="font-weight-bold"
class="gl-font-weight-bold"
data-testid="package-type"
>
maven
</span>
......@@ -138,7 +161,8 @@ exports[`PackageTitle renders without tags 1`] = `
/>
<span
class="font-weight-bold"
class="gl-font-weight-bold"
data-testid="package-size"
>
300 bytes
</span>
......
......@@ -19,7 +19,11 @@ describe('PackageTitle', () => {
let wrapper;
let store;
function createComponent(packageEntity = mavenPackage, packageFiles = mavenFiles) {
function createComponent({
packageEntity = mavenPackage,
packageFiles = mavenFiles,
icon = null,
} = {}) {
store = new Vuex.Store({
state: {
packageEntity,
......@@ -28,6 +32,7 @@ describe('PackageTitle', () => {
getters: {
packageTypeDisplay: ({ packageEntity: { package_type: type } }) => type,
packagePipeline: ({ packageEntity: { pipeline = null } }) => pipeline,
packageIcon: () => icon,
},
});
......@@ -37,10 +42,11 @@ describe('PackageTitle', () => {
});
}
const packageType = () => wrapper.find({ ref: 'package-type' });
const packageSize = () => wrapper.find({ ref: 'package-size' });
const pipelineProject = () => wrapper.find({ ref: 'pipeline-project' });
const packageRef = () => wrapper.find({ ref: 'package-ref' });
const packageIcon = () => wrapper.find('[data-testid="package-icon"]');
const packageType = () => wrapper.find('[data-testid="package-type"]');
const packageSize = () => wrapper.find('[data-testid="package-size"]');
const pipelineProject = () => wrapper.find('[data-testid="pipeline-project"]');
const packageRef = () => wrapper.find('[data-testid="package-ref"]');
const packageTags = () => wrapper.find(PackageTags);
afterEach(() => {
......@@ -55,12 +61,34 @@ describe('PackageTitle', () => {
});
it('with tags', () => {
createComponent({ ...mavenPackage, tags: mockTags });
createComponent({ packageEntity: { ...mavenPackage, tags: mockTags } });
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`
packageEntity | expectedResult
${conanPackage} | ${'conan'}
......@@ -68,7 +96,7 @@ describe('PackageTitle', () => {
${npmPackage} | ${'npm'}
${nugetPackage} | ${'nuget'}
`(`package type`, ({ packageEntity, expectedResult }) => {
beforeEach(() => createComponent(packageEntity));
beforeEach(() => createComponent({ packageEntity }));
it(`${packageEntity.package_type} should render from Vuex getters ${expectedResult}`, () => {
expect(packageType().text()).toBe(expectedResult);
......@@ -77,7 +105,7 @@ describe('PackageTitle', () => {
describe('calculates the package size', () => {
it('correctly calulates when there is only 1 file', () => {
createComponent(npmPackage, npmFiles);
createComponent({ packageEntity: npmPackage, packageFiles: npmFiles });
expect(packageSize().text()).toBe('200 bytes');
});
......@@ -92,8 +120,10 @@ describe('PackageTitle', () => {
describe('package tags', () => {
it('displays the package-tags component when the package has tags', () => {
createComponent({
...npmPackage,
tags: mockTags,
packageEntity: {
...npmPackage,
tags: mockTags,
},
});
expect(packageTags().exists()).toBe(true);
......@@ -114,7 +144,7 @@ describe('PackageTitle', () => {
});
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().text()).toBe(npmPackage.pipeline.ref);
......@@ -129,7 +159,7 @@ describe('PackageTitle', () => {
});
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().attributes('href')).toBe(npmPackage.pipeline.project.web_url);
......
......@@ -3,6 +3,7 @@ import {
conanSetupCommand,
packagePipeline,
packageTypeDisplay,
packageIcon,
mavenInstallationXml,
mavenInstallationCommand,
mavenSetupXml,
......@@ -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', () => {
it('gets the correct conanInstallationCommand', () => {
setupState({ packageEntity: conanPackage });
......
......@@ -118,6 +118,9 @@ export const nugetPackage = {
updated_at: '2015-12-10',
version: '1.0.0',
dependency_links: Object.values(dependencyLinks),
nuget_metadatum: {
icon_url: 'fake-icon',
},
};
export const pypiPackage = {
......
......@@ -27393,6 +27393,9 @@ msgstr ""
msgid "uses Kubernetes clusters to deploy your code!"
msgstr ""
msgid "v%{version} published %{timeAgo}"
msgstr ""
msgid "verify ownership"
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