Commit 65af2d47 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch...

Merge branch '227582-package-detail-ui-update-the-package-detail-main-body-to-include-a-history-install-commands' into 'master'

Add metadata component to Package details page

See merge request gitlab-org/gitlab!38285
parents 633a509e 31f03145
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import DetailsRow from '~/registry/shared/components/details_row.vue';
import { generateConanRecipe } from '../utils';
import { PackageType } from '../../shared/constants';
export default {
i18n: {
sourceText: s__('PackageRegistry|Source project located at %{link}'),
licenseText: s__('PackageRegistry|License information located at %{link}'),
recipeText: s__('PackageRegistry|Recipe: %{recipe}'),
appGroup: s__('PackageRegistry|App group: %{group}'),
appName: s__('PackageRegistry|App name: %{name}'),
},
components: {
DetailsRow,
GlLink,
GlSprintf,
},
props: {
packageEntity: {
type: Object,
required: true,
},
},
computed: {
conanRecipe() {
return generateConanRecipe(this.packageEntity);
},
showMetadata() {
const visibilityConditions = {
[PackageType.NUGET]: this.packageEntity.nuget_metadatum,
[PackageType.CONAN]: this.packageEntity.conan_metadatum,
[PackageType.MAVEN]: this.packageEntity.maven_metadatum,
};
return visibilityConditions[this.packageEntity.package_type];
},
},
};
</script>
<template>
<div v-if="showMetadata">
<h3 class="gl-font-lg gl-mt-5" data-testid="title">{{ __('Additional Metadata') }}</h3>
<div class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base" data-testid="main">
<template v-if="packageEntity.nuget_metadatum">
<details-row icon="project" padding="gl-p-4" dashed data-testid="nuget-source">
<gl-sprintf :message="$options.i18n.sourceText">
<template #link>
<gl-link :href="packageEntity.nuget_metadatum.project_url" target="_blank">{{
packageEntity.nuget_metadatum.project_url
}}</gl-link>
</template>
</gl-sprintf>
</details-row>
<details-row icon="license" padding="gl-p-4" data-testid="nuget-license">
<gl-sprintf :message="$options.i18n.licenseText">
<template #link>
<gl-link :href="packageEntity.nuget_metadatum.license_url" target="_blank">{{
packageEntity.nuget_metadatum.license_url
}}</gl-link>
</template>
</gl-sprintf>
</details-row>
</template>
<details-row
v-else-if="packageEntity.conan_metadatum"
icon="information-o"
padding="gl-p-4"
data-testid="conan-recipe"
>
<gl-sprintf :message="$options.i18n.recipeText">
<template #recipe>{{ conanRecipe }}</template>
</gl-sprintf>
</details-row>
<template v-else-if="packageEntity.maven_metadatum">
<details-row icon="information-o" padding="gl-p-4" dashed data-testid="maven-app">
<gl-sprintf :message="$options.i18n.appName">
<template #name>
<strong>{{ packageEntity.maven_metadatum.app_name }}</strong>
</template>
</gl-sprintf>
</details-row>
<details-row icon="information-o" padding="gl-p-4" data-testid="maven-group">
<gl-sprintf :message="$options.i18n.appGroup">
<template #group>
<strong>{{ packageEntity.maven_metadatum.app_group }}</strong>
</template>
</gl-sprintf>
</details-row>
</template>
</div>
</div>
</template>
......@@ -12,6 +12,7 @@ import {
GlTable,
GlSprintf,
} from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import Tracking from '~/tracking';
import PackageActivity from './activity.vue';
import PackageHistory from './package_history.vue';
......@@ -25,6 +26,7 @@ import PypiInstallation from './pypi_installation.vue';
import PackagesListLoader from '../../shared/components/packages_list_loader.vue';
import PackageListRow from '../../shared/components/package_list_row.vue';
import DependencyRow from './dependency_row.vue';
import AdditionalMetadata from './additional_metadata.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import FileIcon from '~/vue_shared/components/file_icon.vue';
......@@ -32,7 +34,6 @@ import { generatePackageInfo } from '../utils';
import { __, s__ } from '~/locale';
import { PackageType, TrackingActions } from '../../shared/constants';
import { packageTypeToTrackCategory } from '../../shared/utils';
import { mapActions, mapState } from 'vuex';
export default {
name: 'PackagesApp',
......@@ -59,6 +60,7 @@ export default {
PackageListRow,
DependencyRow,
PackageHistory,
AdditionalMetadata,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -253,9 +255,12 @@ export default {
<package-activity />
</template>
<package-history v-else :package-entity="packageEntity" :project-name="projectName" />
<template v-else>
<package-history :package-entity="packageEntity" :project-name="projectName" />
<additional-metadata :package-entity="packageEntity" />
</template>
<h3 class="gl-font-lg">{{ __('Files') }}</h3>
<h3 class="gl-font-lg gl-mt-5">{{ __('Files') }}</h3>
<gl-table
:fields="$options.filesTableHeaderFields"
:items="filesTableRows"
......
......@@ -19,7 +19,7 @@ export default {
</script>
<template>
<timeline-entry-item class="system-note note-wrapper gl-my-6!">
<timeline-entry-item class="system-note note-wrapper gl-mb-6!">
<div class="timeline-icon">
<gl-icon :name="icon" />
</div>
......
......@@ -44,8 +44,8 @@ export default {
<template>
<div class="issuable-discussion">
<h3 class="gl-ml-6" data-testid="title">{{ __('History') }}</h3>
<ul class="timeline main-notes-list notes gl-my-4" data-testid="timeline">
<h3 class="gl-font-lg gl-my-3" data-testid="title">{{ __('History') }}</h3>
<ul class="timeline main-notes-list notes gl-mb-4" data-testid="timeline">
<history-element icon="clock" data-testid="created-on">
<gl-sprintf :message="$options.i18n.createdOn">
<template #name>
......
......@@ -7,7 +7,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
import DeleteButton from '../delete_button.vue';
import ListItem from '../list_item.vue';
import DetailsRow from './details_row.vue';
import DetailsRow from '~/registry/shared/components/details_row.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
DIGEST_LABEL,
......
......@@ -10,13 +10,29 @@ export default {
type: String,
required: true,
},
padding: {
type: String,
default: 'gl-py-2',
required: false,
},
dashed: {
type: Boolean,
default: false,
required: false,
},
},
computed: {
borderClass() {
return this.dashed ? 'gl-border-b-solid gl-border-gray-100 gl-border-b-1' : '';
},
},
};
</script>
<template>
<div
class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-py-2 gl-word-break-all"
class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-word-break-all"
:class="[padding, borderClass]"
>
<gl-icon :name="icon" class="gl-mr-4" />
<span>
......
......@@ -1629,6 +1629,9 @@ msgstr ""
msgid "Adding new applications is disabled in your GitLab instance. Please contact your GitLab administrator to get the permission"
msgstr ""
msgid "Additional Metadata"
msgstr ""
msgid "Additional minutes"
msgstr ""
......@@ -16923,6 +16926,12 @@ msgstr ""
msgid "PackageRegistry|Add NuGet Source"
msgstr ""
msgid "PackageRegistry|App group: %{group}"
msgstr ""
msgid "PackageRegistry|App name: %{name}"
msgstr ""
msgid "PackageRegistry|Commit %{link} on branch %{branch}"
msgstr ""
......@@ -17013,6 +17022,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
msgid "PackageRegistry|License information located at %{link}"
msgstr ""
msgid "PackageRegistry|Manually Published"
msgstr ""
......@@ -17055,6 +17067,9 @@ msgstr ""
msgid "PackageRegistry|PyPi"
msgstr ""
msgid "PackageRegistry|Recipe: %{recipe}"
msgstr ""
msgid "PackageRegistry|Registry Setup"
msgstr ""
......@@ -17064,6 +17079,9 @@ msgstr ""
msgid "PackageRegistry|Sorry, your filter produced no results"
msgstr ""
msgid "PackageRegistry|Source project located at %{link}"
msgstr ""
msgid "PackageRegistry|There are no %{packageType} packages yet"
msgstr ""
......
......@@ -2,7 +2,7 @@
exports[`History Element renders the correct markup 1`] = `
<li
class="timeline-entry system-note note-wrapper gl-my-6!"
class="timeline-entry system-note note-wrapper gl-mb-6!"
>
<div
class="timeline-entry-inner"
......
import { shallowMount } from '@vue/test-utils';
import { GlLink, GlSprintf } from '@gitlab/ui';
import DetailsRow from '~/registry/shared/components/details_row.vue';
import component from '~/packages/details/components/additional_metadata.vue';
import { mavenPackage, conanPackage, nugetPackage, npmPackage } from '../../mock_data';
describe('Package Additional Metadata', () => {
let wrapper;
const defaultProps = {
packageEntity: { ...mavenPackage },
};
const mountComponent = props => {
wrapper = shallowMount(component, {
propsData: { ...defaultProps, ...props },
stubs: {
DetailsRow,
GlSprintf,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findTitle = () => wrapper.find('[data-testid="title"]');
const findMainArea = () => wrapper.find('[data-testid="main"]');
const findNugetSource = () => wrapper.find('[data-testid="nuget-source"]');
const findNugetLicense = () => wrapper.find('[data-testid="nuget-license"]');
const findConanRecipe = () => wrapper.find('[data-testid="conan-recipe"]');
const findMavenApp = () => wrapper.find('[data-testid="maven-app"]');
const findMavenGroup = () => wrapper.find('[data-testid="maven-group"]');
const findElementLink = container => container.find(GlLink);
it('has the correct title', () => {
mountComponent();
const title = findTitle();
expect(title.exists()).toBe(true);
expect(title.text()).toBe('Additional Metadata');
});
describe.each`
packageEntity | visible | metadata
${mavenPackage} | ${true} | ${'maven_metadatum'}
${conanPackage} | ${true} | ${'conan_metadatum'}
${nugetPackage} | ${true} | ${'nuget_metadatum'}
${npmPackage} | ${false} | ${null}
`('Component visibility', ({ packageEntity, visible, metadata }) => {
it(`Is ${visible} that the component markup is visible when the package is ${packageEntity.package_type}`, () => {
mountComponent({ packageEntity });
expect(findTitle().exists()).toBe(visible);
expect(findMainArea().exists()).toBe(visible);
});
it(`The component is hidden if ${metadata} is missing`, () => {
mountComponent({ packageEntity: { ...packageEntity, [metadata]: null } });
expect(findTitle().exists()).toBe(false);
expect(findMainArea().exists()).toBe(false);
});
});
describe('nuget metadata', () => {
beforeEach(() => {
mountComponent({ packageEntity: nugetPackage });
});
it.each`
name | finderFunction | text | link | icon
${'source'} | ${findNugetSource} | ${'Source project located at project-foo-url'} | ${'project_url'} | ${'project'}
${'license'} | ${findNugetLicense} | ${'License information located at license-foo-url'} | ${'license_url'} | ${'license'}
`('$name element', ({ finderFunction, text, link, icon }) => {
const element = finderFunction();
expect(element.exists()).toBe(true);
expect(element.text()).toBe(text);
expect(element.props('icon')).toBe(icon);
expect(findElementLink(element).attributes('href')).toBe(nugetPackage.nuget_metadatum[link]);
});
});
describe('conan metadata', () => {
beforeEach(() => {
mountComponent({ packageEntity: conanPackage });
});
it.each`
name | finderFunction | text | icon
${'recipe'} | ${findConanRecipe} | ${'Recipe: conan-package/1.0.0@conan+conan-package/stable'} | ${'information-o'}
`('$name element', ({ finderFunction, text, icon }) => {
const element = finderFunction();
expect(element.exists()).toBe(true);
expect(element.text()).toBe(text);
expect(element.props('icon')).toBe(icon);
});
});
describe('maven metadata', () => {
beforeEach(() => {
mountComponent();
});
it.each`
name | finderFunction | text | icon
${'app'} | ${findMavenApp} | ${'App name: test-app'} | ${'information-o'}
${'group'} | ${findMavenGroup} | ${'App group: com.test.app'} | ${'information-o'}
`('$name element', ({ finderFunction, text, icon }) => {
const element = finderFunction();
expect(element.exists()).toBe(true);
expect(element.text()).toBe(text);
expect(element.props('icon')).toBe(icon);
});
});
});
......@@ -17,6 +17,7 @@ import NugetInstallation from '~/packages/details/components/nuget_installation.
import PypiInstallation from '~/packages/details/components/pypi_installation.vue';
import DependencyRow from '~/packages/details/components/dependency_row.vue';
import PackageHistory from '~/packages/details/components/package_history.vue';
import AdditionalMetadata from '~/packages/details/components/additional_metadata.vue';
import PackageActivity from '~/packages/details/components/activity.vue';
import {
conanPackage,
......@@ -99,6 +100,7 @@ describe('PackagesApp', () => {
const noDependenciesMessage = () => wrapper.find('[data-testid="no-dependencies-message"]');
const dependencyRows = () => wrapper.findAll(DependencyRow);
const findPackageHistory = () => wrapper.find(PackageHistory);
const findAdditionalMetadata = () => wrapper.find(AdditionalMetadata);
const findPackageActivity = () => wrapper.find(PackageActivity);
const findOldPackageInfo = () => wrapper.find('[data-testid="old-package-info"]');
......@@ -295,30 +297,38 @@ describe('PackagesApp', () => {
});
});
it('package history has the right props', () => {
createComponent({ oneColumnView: true });
expect(findPackageHistory().props('packageEntity')).toEqual(wrapper.vm.packageEntity);
expect(findPackageHistory().props('projectName')).toEqual(wrapper.vm.projectName);
});
it('additional metadata has the right props', () => {
createComponent({ oneColumnView: true });
expect(findAdditionalMetadata().props('packageEntity')).toEqual(wrapper.vm.packageEntity);
});
describe('one column layout feature flag', () => {
describe.each`
oneColumnView | history | oldInfo | activity
${true} | ${true} | ${false} | ${false}
${false} | ${false} | ${true} | ${true}
`(
'with oneColumnView set to $oneColumnView',
({ oneColumnView, history, oldInfo, activity }) => {
beforeEach(() => {
createComponent({ oneColumnView });
});
it('package history', () => {
expect(findPackageHistory().exists()).toBe(history);
});
it('old info block', () => {
expect(findOldPackageInfo().exists()).toBe(oldInfo);
});
it('package activity', () => {
expect(findPackageActivity().exists()).toBe(activity);
});
},
);
describe.each([true, false])('with oneColumnView set to %s', oneColumnView => {
beforeEach(() => {
createComponent({ oneColumnView });
});
it(`is ${oneColumnView} that package history is visible`, () => {
expect(findPackageHistory().exists()).toBe(oneColumnView);
});
it(`is ${oneColumnView} that additional metadata is visible`, () => {
expect(findAdditionalMetadata().exists()).toBe(oneColumnView);
});
it(`is ${!oneColumnView} that old info block is visible`, () => {
expect(findOldPackageInfo().exists()).toBe(!oneColumnView);
});
it(`is ${!oneColumnView} that package activity is visible`, () => {
expect(findPackageActivity().exists()).toBe(!oneColumnView);
});
});
});
});
......@@ -5,7 +5,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import component from '~/registry/explorer/components/details_page/tags_list_row.vue';
import DeleteButton from '~/registry/explorer/components/delete_button.vue';
import DetailsRow from '~/registry/explorer/components/details_page/details_row.vue';
import DetailsRow from '~/registry/shared/components/details_row.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
REMOVE_TAG_BUTTON_DISABLE_TOOLTIP,
......
import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import component from '~/registry/explorer/components/details_page/details_row.vue';
import component from '~/registry/shared/components/details_row.vue';
describe('DetailsRow', () => {
let wrapper;
......@@ -8,10 +8,11 @@ describe('DetailsRow', () => {
const findIcon = () => wrapper.find(GlIcon);
const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
const mountComponent = () => {
const mountComponent = props => {
wrapper = shallowMount(component, {
propsData: {
icon: 'clock',
...props,
},
slots: {
default: '<div data-testid="default-slot"></div>',
......@@ -24,20 +25,47 @@ describe('DetailsRow', () => {
wrapper = null;
});
it('contains an icon', () => {
it('has a default slot', () => {
mountComponent();
expect(findIcon().exists()).toBe(true);
expect(findDefaultSlot().exists()).toBe(true);
});
it('icon has the correct props', () => {
mountComponent();
expect(findIcon().props()).toMatchObject({
name: 'clock',
describe('icon prop', () => {
it('contains an icon', () => {
mountComponent();
expect(findIcon().exists()).toBe(true);
});
it('icon has the correct props', () => {
mountComponent();
expect(findIcon().props()).toMatchObject({
name: 'clock',
});
});
});
it('has a default slot', () => {
mountComponent();
expect(findDefaultSlot().exists()).toBe(true);
describe('padding prop', () => {
it('padding has a default', () => {
mountComponent();
expect(wrapper.classes('gl-py-2')).toBe(true);
});
it('is reflected in the template', () => {
mountComponent({ padding: 'gl-py-4' });
expect(wrapper.classes('gl-py-4')).toBe(true);
});
});
describe('dashed prop', () => {
const borderClasses = ['gl-border-b-solid', 'gl-border-gray-100', 'gl-border-b-1'];
it('by default component has no border', () => {
mountComponent();
expect(wrapper.classes).not.toEqual(expect.arrayContaining(borderClasses));
});
it('has a border when dashed is true', () => {
mountComponent({ dashed: true });
expect(wrapper.classes()).toEqual(expect.arrayContaining(borderClasses));
});
});
});
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