Commit e10b129e 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 history component to package details page

See merge request gitlab-org/gitlab!38045
parents 7da1f298 a6f18ea4
...@@ -14,6 +14,7 @@ import { ...@@ -14,6 +14,7 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import PackageActivity from './activity.vue'; import PackageActivity from './activity.vue';
import PackageHistory from './package_history.vue';
import PackageInformation from './information.vue'; import PackageInformation from './information.vue';
import PackageTitle from './package_title.vue'; import PackageTitle from './package_title.vue';
import ConanInstallation from './conan_installation.vue'; import ConanInstallation from './conan_installation.vue';
...@@ -57,6 +58,7 @@ export default { ...@@ -57,6 +58,7 @@ export default {
PackagesListLoader, PackagesListLoader,
PackageListRow, PackageListRow,
DependencyRow, DependencyRow,
PackageHistory,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -66,6 +68,7 @@ export default { ...@@ -66,6 +68,7 @@ export default {
trackingActions: { ...TrackingActions }, trackingActions: { ...TrackingActions },
computed: { computed: {
...mapState([ ...mapState([
'projectName',
'packageEntity', 'packageEntity',
'packageFiles', 'packageFiles',
'isLoading', 'isLoading',
...@@ -74,6 +77,7 @@ export default { ...@@ -74,6 +77,7 @@ export default {
'svgPath', 'svgPath',
'npmPath', 'npmPath',
'npmHelpPath', 'npmHelpPath',
'oneColumnView',
]), ]),
installationComponent() { installationComponent() {
switch (this.packageEntity.package_type) { switch (this.packageEntity.package_type) {
...@@ -219,29 +223,37 @@ export default { ...@@ -219,29 +223,37 @@ export default {
<gl-tabs> <gl-tabs>
<gl-tab :title="__('Detail')"> <gl-tab :title="__('Detail')">
<div class="row" data-qa-selector="package_information_content"> <template v-if="!oneColumnView">
<div class="col-sm-6"> <div
<package-information :information="packageInformation" /> class="row"
<package-information data-qa-selector="package_information_content"
v-if="packageMetadata" data-testid="old-package-info"
:heading="packageMetadataTitle" >
:information="packageMetadata" <div class="col-sm-6">
:show-copy="true" <package-information :information="packageInformation" />
/> <package-information
</div> v-if="packageMetadata"
:heading="packageMetadataTitle"
:information="packageMetadata"
:show-copy="true"
/>
</div>
<div class="col-sm-6"> <div class="col-sm-6">
<component <component
:is="installationComponent" :is="installationComponent"
v-if="installationComponent" v-if="installationComponent"
:name="packageEntity.name" :name="packageEntity.name"
:registry-url="npmPath" :registry-url="npmPath"
:help-url="npmHelpPath" :help-url="npmHelpPath"
/> />
</div>
</div> </div>
</div>
<package-activity /> <package-activity />
</template>
<package-history v-else :package-entity="packageEntity" :project-name="projectName" />
<h3 class="gl-font-lg">{{ __('Files') }}</h3> <h3 class="gl-font-lg">{{ __('Files') }}</h3>
<gl-table <gl-table
......
<script>
import { GlIcon } from '@gitlab/ui';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
export default {
name: 'HistoryElement',
components: {
GlIcon,
TimelineEntryItem,
},
props: {
icon: {
type: String,
required: true,
},
},
};
</script>
<template>
<timeline-entry-item class="system-note note-wrapper gl-my-6!">
<div class="timeline-icon">
<gl-icon :name="icon" />
</div>
<div class="timeline-content">
<div class="note-header">
<span>
<slot></slot>
</span>
</div>
<div class="note-body"></div>
</div>
</timeline-entry-item>
</template>
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import HistoryElement from './history_element.vue';
export default {
name: 'PackageHistory',
i18n: {
createdOn: s__('PackageRegistry|%{name} version %{version} was created %{datetime}'),
updatedAtText: s__('PackageRegistry|%{name} version %{version} was updated %{datetime}'),
commitText: s__('PackageRegistry|Commit %{link} on branch %{branch}'),
pipelineText: s__('PackageRegistry|Pipeline %{link} triggered %{datetime} by %{author}'),
publishText: s__('PackageRegistry|Published to the %{project} Package Registry %{datetime}'),
},
components: {
GlLink,
GlSprintf,
HistoryElement,
TimeAgoTooltip,
},
props: {
packageEntity: {
type: Object,
required: true,
},
projectName: {
type: String,
required: true,
},
},
data() {
return {
showDescription: false,
};
},
computed: {
packagePipeline() {
return this.packageEntity.pipeline?.id ? this.packageEntity.pipeline : null;
},
},
};
</script>
<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">
<history-element icon="clock" data-testid="created-on">
<gl-sprintf :message="$options.i18n.createdOn">
<template #name>
<strong>{{ packageEntity.name }}</strong>
</template>
<template #version>
<strong>{{ packageEntity.version }}</strong>
</template>
<template #datetime>
<time-ago-tooltip :time="packageEntity.created_at" />
</template>
</gl-sprintf>
</history-element>
<history-element icon="pencil" data-testid="updated-at">
<gl-sprintf :message="$options.i18n.updatedAtText">
<template #name>
<strong>{{ packageEntity.name }}</strong>
</template>
<template #version>
<strong>{{ packageEntity.version }}</strong>
</template>
<template #datetime>
<time-ago-tooltip :time="packageEntity.updated_at" />
</template>
</gl-sprintf>
</history-element>
<template v-if="packagePipeline">
<history-element icon="commit" data-testid="commit">
<gl-sprintf :message="$options.i18n.commitText">
<template #link>
<gl-link :href="`../../commit/${packagePipeline.sha}`">{{
packagePipeline.sha
}}</gl-link>
</template>
<template #branch>
<strong>{{ packagePipeline.ref }}</strong>
</template>
</gl-sprintf>
</history-element>
<history-element icon="pipeline" data-testid="pipeline">
<gl-sprintf :message="$options.i18n.pipelineText">
<template #link>
<gl-link :href="`../../pipelines/${packagePipeline.id}`"
>#{{ packagePipeline.id }}</gl-link
>
</template>
<template #datetime>
<time-ago-tooltip :time="packagePipeline.created_at" />
</template>
<template #author>{{ packagePipeline.user.name }}</template>
</gl-sprintf>
</history-element>
</template>
<history-element icon="package" data-testid="published">
<gl-sprintf :message="$options.i18n.publishText">
<template #project>
<strong>{{ projectName }}</strong>
</template>
<template #datetime>
<time-ago-tooltip :time="packageEntity.created_at" />
</template>
</gl-sprintf>
</history-element>
</ul>
</div>
</template>
import Vue from 'vue'; import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import PackagesApp from './components/app.vue'; import PackagesApp from './components/app.vue';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import createStore from './store'; import createStore from './store';
...@@ -7,7 +8,7 @@ Vue.use(Translate); ...@@ -7,7 +8,7 @@ Vue.use(Translate);
export default () => { export default () => {
const el = document.querySelector('#js-vue-packages-detail'); const el = document.querySelector('#js-vue-packages-detail');
const { package: packageJson, canDelete: canDeleteStr, ...rest } = el.dataset; const { package: packageJson, canDelete: canDeleteStr, oneColumnView, ...rest } = el.dataset;
const packageEntity = JSON.parse(packageJson); const packageEntity = JSON.parse(packageJson);
const canDelete = canDeleteStr === 'true'; const canDelete = canDeleteStr === 'true';
...@@ -15,6 +16,7 @@ export default () => { ...@@ -15,6 +16,7 @@ export default () => {
packageEntity, packageEntity,
packageFiles: packageEntity.package_files, packageFiles: packageEntity.package_files,
canDelete, canDelete,
oneColumnView: parseBoolean(oneColumnView),
...rest, ...rest,
}); });
......
...@@ -19,4 +19,6 @@ ...@@ -19,4 +19,6 @@
nuget_help_path: help_page_path('user/packages/nuget_repository/index'), nuget_help_path: help_page_path('user/packages/nuget_repository/index'),
pypi_path: pypi_registry_url(@project.id), pypi_path: pypi_registry_url(@project.id),
pypi_setup_path: package_registry_project_url(@project.id, :pypi), pypi_setup_path: package_registry_project_url(@project.id, :pypi),
pypi_help_path: help_page_path('user/packages/pypi_repository/index') } } pypi_help_path: help_page_path('user/packages/pypi_repository/index'),
project_name: @project.name,
one_column_view: Feature.enabled?(:packages_details_one_column, @project).to_s } }
...@@ -16774,12 +16774,21 @@ msgstr "" ...@@ -16774,12 +16774,21 @@ msgstr ""
msgid "Package was removed" msgid "Package was removed"
msgstr "" msgstr ""
msgid "PackageRegistry|%{name} version %{version} was created %{datetime}"
msgstr ""
msgid "PackageRegistry|%{name} version %{version} was updated %{datetime}"
msgstr ""
msgid "PackageRegistry|Add Conan Remote" msgid "PackageRegistry|Add Conan Remote"
msgstr "" msgstr ""
msgid "PackageRegistry|Add NuGet Source" msgid "PackageRegistry|Add NuGet Source"
msgstr "" msgstr ""
msgid "PackageRegistry|Commit %{link} on branch %{branch}"
msgstr ""
msgid "PackageRegistry|Composer" msgid "PackageRegistry|Composer"
msgstr "" msgstr ""
...@@ -16897,6 +16906,12 @@ msgstr "" ...@@ -16897,6 +16906,12 @@ msgstr ""
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}" msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr "" msgstr ""
msgid "PackageRegistry|Pipeline %{link} triggered %{datetime} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the %{project} Package Registry %{datetime}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}" msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr "" msgstr ""
......
...@@ -62,14 +62,14 @@ exports[`PackageActivity render to match the default snapshot when there is a pi ...@@ -62,14 +62,14 @@ exports[`PackageActivity render to match the default snapshot when there is a pi
<!----> <!---->
<gl-link-stub <gl-link-stub
href="../../commit/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" href="../../commit/sha-baz"
> >
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx sha-baz
</gl-link-stub> </gl-link-stub>
<clipboard-button-stub <clipboard-button-stub
cssclass="border-0 text-secondary py-0" cssclass="border-0 text-secondary py-0"
text="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" text="sha-baz"
title="Copy commit SHA" title="Copy commit SHA"
tooltipplacement="top" tooltipplacement="top"
/> />
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`History Element renders the correct markup 1`] = `
<li
class="timeline-entry system-note note-wrapper gl-my-6!"
>
<div
class="timeline-entry-inner"
>
<div
class="timeline-icon"
>
<gl-icon-stub
name="pencil"
size="16"
/>
</div>
<div
class="timeline-content"
>
<div
class="note-header"
>
<span>
<div
data-testid="default-slot"
/>
</span>
</div>
<div
class="note-body"
/>
</div>
</div>
</li>
`;
...@@ -16,6 +16,8 @@ import ConanInstallation from '~/packages/details/components/conan_installation. ...@@ -16,6 +16,8 @@ import ConanInstallation from '~/packages/details/components/conan_installation.
import NugetInstallation from '~/packages/details/components/nuget_installation.vue'; import NugetInstallation from '~/packages/details/components/nuget_installation.vue';
import PypiInstallation from '~/packages/details/components/pypi_installation.vue'; import PypiInstallation from '~/packages/details/components/pypi_installation.vue';
import DependencyRow from '~/packages/details/components/dependency_row.vue'; import DependencyRow from '~/packages/details/components/dependency_row.vue';
import PackageHistory from '~/packages/details/components/package_history.vue';
import PackageActivity from '~/packages/details/components/activity.vue';
import { import {
conanPackage, conanPackage,
mavenPackage, mavenPackage,
...@@ -39,6 +41,7 @@ describe('PackagesApp', () => { ...@@ -39,6 +41,7 @@ describe('PackagesApp', () => {
packageEntity = mavenPackage, packageEntity = mavenPackage,
packageFiles = mavenFiles, packageFiles = mavenFiles,
isLoading = false, isLoading = false,
oneColumnView = false,
} = {}) { } = {}) {
store = new Vuex.Store({ store = new Vuex.Store({
state: { state: {
...@@ -50,6 +53,8 @@ describe('PackagesApp', () => { ...@@ -50,6 +53,8 @@ describe('PackagesApp', () => {
emptySvgPath: 'empty-illustration', emptySvgPath: 'empty-illustration',
npmPath: 'foo', npmPath: 'foo',
npmHelpPath: 'foo', npmHelpPath: 'foo',
projectName: 'bar',
oneColumnView,
}, },
actions: { actions: {
fetchPackageVersions, fetchPackageVersions,
...@@ -93,6 +98,9 @@ describe('PackagesApp', () => { ...@@ -93,6 +98,9 @@ describe('PackagesApp', () => {
const dependenciesCountBadge = () => wrapper.find('[data-testid="dependencies-badge"]'); const dependenciesCountBadge = () => wrapper.find('[data-testid="dependencies-badge"]');
const noDependenciesMessage = () => wrapper.find('[data-testid="no-dependencies-message"]'); const noDependenciesMessage = () => wrapper.find('[data-testid="no-dependencies-message"]');
const dependencyRows = () => wrapper.findAll(DependencyRow); const dependencyRows = () => wrapper.findAll(DependencyRow);
const findPackageHistory = () => wrapper.find(PackageHistory);
const findPackageActivity = () => wrapper.find(PackageActivity);
const findOldPackageInfo = () => wrapper.find('[data-testid="old-package-info"]');
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -286,4 +294,31 @@ describe('PackagesApp', () => { ...@@ -286,4 +294,31 @@ describe('PackagesApp', () => {
); );
}); });
}); });
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);
});
},
);
});
}); });
import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import component from '~/packages/details/components/history_element.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
describe('History Element', () => {
let wrapper;
const defaultProps = {
icon: 'pencil',
};
const mountComponent = () => {
wrapper = shallowMount(component, {
propsData: { ...defaultProps },
stubs: {
TimelineEntryItem,
},
slots: {
default: '<div data-testid="default-slot"></div>',
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findTimelineEntry = () => wrapper.find(TimelineEntryItem);
const findGlIcon = () => wrapper.find(GlIcon);
const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
it('renders the correct markup', () => {
mountComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('has a default slot', () => {
mountComponent();
expect(findDefaultSlot().exists()).toBe(true);
});
it('has a timeline entry', () => {
mountComponent();
expect(findTimelineEntry().exists()).toBe(true);
});
it('has an icon', () => {
mountComponent();
const icon = findGlIcon();
expect(icon.exists()).toBe(true);
expect(icon.attributes('name')).toBe(defaultProps.icon);
});
});
import { shallowMount } from '@vue/test-utils';
import { GlLink, GlSprintf } from '@gitlab/ui';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import component from '~/packages/details/components/package_history.vue';
import { mavenPackage, mockPipelineInfo } from '../../mock_data';
describe('Package History', () => {
let wrapper;
const defaultProps = {
projectName: 'baz project',
packageEntity: { ...mavenPackage },
};
const mountComponent = props => {
wrapper = shallowMount(component, {
propsData: { ...defaultProps, ...props },
stubs: {
HistoryElement: '<div data-testid="history-element"><slot></slot></div>',
GlSprintf,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findHistoryElement = testId => wrapper.find(`[data-testid="${testId}"]`);
const findElementLink = container => container.find(GlLink);
const findElementTimeAgo = container => container.find(TimeAgoTooltip);
const findTitle = () => wrapper.find('[data-testid="title"]');
const findTimeline = () => wrapper.find('[data-testid="timeline"]');
it('has the correct title', () => {
mountComponent();
const title = findTitle();
expect(title.exists()).toBe(true);
expect(title.text()).toBe('History');
});
it('has a timeline container', () => {
mountComponent();
const title = findTimeline();
expect(title.exists()).toBe(true);
expect(title.classes()).toEqual(
expect.arrayContaining(['timeline', 'main-notes-list', 'notes']),
);
});
describe.each`
name | icon | text | timeAgoTooltip | link
${'created-on'} | ${'clock'} | ${'Test package version 1.0.0 was created'} | ${mavenPackage.created_at} | ${null}
${'updated-at'} | ${'pencil'} | ${'Test package version 1.0.0 was updated'} | ${mavenPackage.updated_at} | ${null}
${'commit'} | ${'commit'} | ${'Commit sha-baz on branch branch-name'} | ${null} | ${'../../commit/sha-baz'}
${'pipeline'} | ${'pipeline'} | ${'Pipeline #1 triggered by foo'} | ${mockPipelineInfo.created_at} | ${'../../pipelines/1'}
${'published'} | ${'package'} | ${'Published to the baz project Package Registry'} | ${mavenPackage.created_at} | ${null}
`('history element $name', ({ name, icon, text, timeAgoTooltip, link }) => {
let element;
beforeEach(() => {
mountComponent({ packageEntity: { ...mavenPackage, pipeline: mockPipelineInfo } });
element = findHistoryElement(name);
});
it('has the correct icon', () => {
expect(element.props('icon')).toBe(icon);
});
it('has the correct text', () => {
expect(element.text()).toBe(text);
});
it('time-ago tooltip', () => {
const timeAgo = findElementTimeAgo(element);
const exist = Boolean(timeAgoTooltip);
expect(timeAgo.exists()).toBe(exist);
if (exist) {
expect(timeAgo.props('time')).toBe(timeAgoTooltip);
}
});
it('link', () => {
const linkElement = findElementLink(element);
const exist = Boolean(link);
expect(linkElement.exists()).toBe(exist);
if (exist) {
expect(linkElement.attributes('href')).toBe(link);
}
});
});
describe('when pipelineInfo is missing', () => {
it.each(['commit', 'pipeline'])('%s history element is hidden', name => {
mountComponent();
expect(findHistoryElement(name).exists()).toBe(false);
});
});
});
...@@ -6,7 +6,7 @@ const _links = { ...@@ -6,7 +6,7 @@ const _links = {
export const mockPipelineInfo = { export const mockPipelineInfo = {
id: 1, id: 1,
ref: 'branch-name', ref: 'branch-name',
sha: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', sha: 'sha-baz',
user: { user: {
name: 'foo', name: 'foo',
}, },
...@@ -14,6 +14,7 @@ export const mockPipelineInfo = { ...@@ -14,6 +14,7 @@ export const mockPipelineInfo = {
name: 'foo-project', name: 'foo-project',
web_url: 'foo-project-link', web_url: 'foo-project-link',
}, },
created_at: '2015-12-10',
}; };
export const mavenPackage = { export const mavenPackage = {
......
...@@ -24,14 +24,14 @@ exports[`publish_method renders 1`] = ` ...@@ -24,14 +24,14 @@ exports[`publish_method renders 1`] = `
<gl-link-stub <gl-link-stub
class="mr-1" class="mr-1"
href="../commit/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" href="../commit/sha-baz"
> >
xxxxxxxx sha-baz
</gl-link-stub> </gl-link-stub>
<clipboard-button-stub <clipboard-button-stub
cssclass="border-0 text-secondary py-0 px-1" cssclass="border-0 text-secondary py-0 px-1"
text="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" text="sha-baz"
title="Copy commit SHA" title="Copy commit SHA"
tooltipplacement="top" tooltipplacement="top"
/> />
......
...@@ -21,6 +21,10 @@ end ...@@ -21,6 +21,10 @@ end
RSpec.shared_examples 'package details link' do |property| RSpec.shared_examples 'package details link' do |property|
let(:package) { packages.first } let(:package) { packages.first }
before do
stub_feature_flags(packages_details_one_column: false)
end
it 'navigates to the correct url' do it 'navigates to the correct url' do
page.within(packages_table_selector) do page.within(packages_table_selector) do
click_link package.name click_link package.name
......
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