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 {
} from '@gitlab/ui';
import Tracking from '~/tracking';
import PackageActivity from './activity.vue';
import PackageHistory from './package_history.vue';
import PackageInformation from './information.vue';
import PackageTitle from './package_title.vue';
import ConanInstallation from './conan_installation.vue';
......@@ -57,6 +58,7 @@ export default {
PackagesListLoader,
PackageListRow,
DependencyRow,
PackageHistory,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -66,6 +68,7 @@ export default {
trackingActions: { ...TrackingActions },
computed: {
...mapState([
'projectName',
'packageEntity',
'packageFiles',
'isLoading',
......@@ -74,6 +77,7 @@ export default {
'svgPath',
'npmPath',
'npmHelpPath',
'oneColumnView',
]),
installationComponent() {
switch (this.packageEntity.package_type) {
......@@ -219,29 +223,37 @@ export default {
<gl-tabs>
<gl-tab :title="__('Detail')">
<div class="row" data-qa-selector="package_information_content">
<div class="col-sm-6">
<package-information :information="packageInformation" />
<package-information
v-if="packageMetadata"
:heading="packageMetadataTitle"
:information="packageMetadata"
:show-copy="true"
/>
</div>
<template v-if="!oneColumnView">
<div
class="row"
data-qa-selector="package_information_content"
data-testid="old-package-info"
>
<div class="col-sm-6">
<package-information :information="packageInformation" />
<package-information
v-if="packageMetadata"
:heading="packageMetadataTitle"
:information="packageMetadata"
:show-copy="true"
/>
</div>
<div class="col-sm-6">
<component
:is="installationComponent"
v-if="installationComponent"
:name="packageEntity.name"
:registry-url="npmPath"
:help-url="npmHelpPath"
/>
<div class="col-sm-6">
<component
:is="installationComponent"
v-if="installationComponent"
:name="packageEntity.name"
:registry-url="npmPath"
:help-url="npmHelpPath"
/>
</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>
<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 { parseBoolean } from '~/lib/utils/common_utils';
import PackagesApp from './components/app.vue';
import Translate from '~/vue_shared/translate';
import createStore from './store';
......@@ -7,7 +8,7 @@ Vue.use(Translate);
export default () => {
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 canDelete = canDeleteStr === 'true';
......@@ -15,6 +16,7 @@ export default () => {
packageEntity,
packageFiles: packageEntity.package_files,
canDelete,
oneColumnView: parseBoolean(oneColumnView),
...rest,
});
......
......@@ -19,4 +19,6 @@
nuget_help_path: help_page_path('user/packages/nuget_repository/index'),
pypi_path: pypi_registry_url(@project.id),
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 ""
msgid "Package was removed"
msgstr ""
msgid "PackageRegistry|%{name} version %{version} was created %{datetime}"
msgstr ""
msgid "PackageRegistry|%{name} version %{version} was updated %{datetime}"
msgstr ""
msgid "PackageRegistry|Add Conan Remote"
msgstr ""
msgid "PackageRegistry|Add NuGet Source"
msgstr ""
msgid "PackageRegistry|Commit %{link} on branch %{branch}"
msgstr ""
msgid "PackageRegistry|Composer"
msgstr ""
......@@ -16897,6 +16906,12 @@ msgstr ""
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
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}"
msgstr ""
......
......@@ -62,14 +62,14 @@ exports[`PackageActivity render to match the default snapshot when there is a pi
<!---->
<gl-link-stub
href="../../commit/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
href="../../commit/sha-baz"
>
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
sha-baz
</gl-link-stub>
<clipboard-button-stub
cssclass="border-0 text-secondary py-0"
text="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
text="sha-baz"
title="Copy commit SHA"
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.
import NugetInstallation from '~/packages/details/components/nuget_installation.vue';
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 PackageActivity from '~/packages/details/components/activity.vue';
import {
conanPackage,
mavenPackage,
......@@ -39,6 +41,7 @@ describe('PackagesApp', () => {
packageEntity = mavenPackage,
packageFiles = mavenFiles,
isLoading = false,
oneColumnView = false,
} = {}) {
store = new Vuex.Store({
state: {
......@@ -50,6 +53,8 @@ describe('PackagesApp', () => {
emptySvgPath: 'empty-illustration',
npmPath: 'foo',
npmHelpPath: 'foo',
projectName: 'bar',
oneColumnView,
},
actions: {
fetchPackageVersions,
......@@ -93,6 +98,9 @@ describe('PackagesApp', () => {
const dependenciesCountBadge = () => wrapper.find('[data-testid="dependencies-badge"]');
const noDependenciesMessage = () => wrapper.find('[data-testid="no-dependencies-message"]');
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(() => {
wrapper.destroy();
......@@ -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 = {
export const mockPipelineInfo = {
id: 1,
ref: 'branch-name',
sha: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
sha: 'sha-baz',
user: {
name: 'foo',
},
......@@ -14,6 +14,7 @@ export const mockPipelineInfo = {
name: 'foo-project',
web_url: 'foo-project-link',
},
created_at: '2015-12-10',
};
export const mavenPackage = {
......
......@@ -24,14 +24,14 @@ exports[`publish_method renders 1`] = `
<gl-link-stub
class="mr-1"
href="../commit/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
href="../commit/sha-baz"
>
xxxxxxxx
sha-baz
</gl-link-stub>
<clipboard-button-stub
cssclass="border-0 text-secondary py-0 px-1"
text="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
text="sha-baz"
title="Copy commit SHA"
tooltipplacement="top"
/>
......
......@@ -21,6 +21,10 @@ end
RSpec.shared_examples 'package details link' do |property|
let(:package) { packages.first }
before do
stub_feature_flags(packages_details_one_column: false)
end
it 'navigates to the correct url' do
page.within(packages_table_selector) do
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