Commit 3ca6eda9 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch 'nfriend-show-asset-type-on-releases-page' into 'master'

Show link asset types on Releases page

Closes #208795

See merge request gitlab-org/gitlab!33643
parents 03d106ed 4aaaa813
<script> <script>
import { GlTooltipDirective, GlLink } from '@gitlab/ui'; import { GlTooltipDirective, GlLink, GlButton, GlCollapse, GlIcon, GlBadge } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ASSET_LINK_TYPE } from '../constants';
import { __, s__, sprintf } from '~/locale';
import { difference } from 'lodash';
export default { export default {
name: 'ReleaseBlockAssets', name: 'ReleaseBlockAssets',
components: { components: {
GlLink, GlLink,
GlButton,
GlCollapse,
GlIcon,
Icon, Icon,
GlBadge,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
assets: { assets: {
type: Object, type: Object,
required: true, required: true,
}, },
}, },
data() {
return {
isAssetsExpanded: true,
};
},
computed: { computed: {
hasAssets() { hasAssets() {
return Boolean(this.assets.count); return Boolean(this.assets.count);
}, },
imageLinks() {
return this.linksForType(ASSET_LINK_TYPE.IMAGE);
},
packageLinks() {
return this.linksForType(ASSET_LINK_TYPE.PACKAGE);
},
runbookLinks() {
return this.linksForType(ASSET_LINK_TYPE.RUNBOOK);
},
otherLinks() {
return difference(this.assets.links, [
...this.imageLinks,
...this.packageLinks,
...this.runbookLinks,
]);
},
sections() {
return [
{
links: this.assets.sources.map(s => ({
url: s.url,
name: sprintf(__('Source code (%{fileExtension})'), { fileExtension: s.format }),
})),
iconName: 'doc-code',
},
{
title: s__('ReleaseAssetLinkType|Images'),
links: this.imageLinks,
iconName: 'container-image',
},
{
title: s__('ReleaseAssetLinkType|Packages'),
links: this.packageLinks,
iconName: 'package',
},
{
title: s__('ReleaseAssetLinkType|Runbooks'),
links: this.runbookLinks,
iconName: 'book',
},
{
title: s__('ReleaseAssetLinkType|Other'),
links: this.otherLinks,
iconName: 'link',
},
].filter(section => section.links.length > 0);
},
},
methods: {
toggleAssetsExpansion() {
this.isAssetsExpanded = !this.isAssetsExpanded;
}, },
linksForType(type) {
return this.assets.links.filter(l => l.linkType === type);
},
},
externalLinkTooltipText: __('This link points to external content'),
}; };
</script> </script>
<template> <template>
<div class="card-text prepend-top-default"> <div class="card-text prepend-top-default">
<template v-if="glFeatures.releaseAssetLinkType">
<gl-button
data-testid="accordion-button"
variant="link"
class="gl-font-weight-bold"
@click="toggleAssetsExpansion"
>
<gl-icon
name="chevron-right"
class="gl-transition-medium"
:class="{ 'gl-rotate-90': isAssetsExpanded }"
/>
{{ __('Assets') }}
<gl-badge size="sm" variant="neutral" class="gl-display-inline-block">{{
assets.count
}}</gl-badge>
</gl-button>
<gl-collapse v-model="isAssetsExpanded">
<div class="gl-pl-6 gl-pt-3 js-assets-list">
<template v-for="(section, index) in sections">
<h5 v-if="section.title" :key="`section-header-${index}`" class="gl-mb-2">
{{ section.title }}
</h5>
<ul :key="`section-body-${index}`" class="list-unstyled gl-m-0">
<li v-for="link in section.links" :key="link.url">
<gl-link
:href="link.directAssetUrl || link.url"
class="gl-display-flex gl-align-items-center gl-line-height-24"
>
<gl-icon
:name="section.iconName"
class="gl-mr-2 gl-flex-shrink-0 gl-flex-grow-0"
/>
{{ link.name }}
<gl-icon
v-if="link.external"
v-gl-tooltip
name="external-link"
:aria-label="$options.externalLinkTooltipText"
:title="$options.externalLinkTooltipText"
data-testid="external-link-indicator"
class="gl-ml-2 gl-flex-shrink-0 gl-flex-grow-0 gl-text-gray-600"
/>
</gl-link>
</li>
</ul>
</template>
</div>
</gl-collapse>
</template>
<template v-else>
<b> <b>
{{ __('Assets') }} {{ __('Assets') }}
<span class="js-assets-count badge badge-pill">{{ assets.count }}</span> <span class="js-assets-count badge badge-pill">{{ assets.count }}</span>
...@@ -37,7 +159,9 @@ export default { ...@@ -37,7 +159,9 @@ export default {
<gl-link v-gl-tooltip.bottom :title="__('Download asset')" :href="link.directAssetUrl"> <gl-link v-gl-tooltip.bottom :title="__('Download asset')" :href="link.directAssetUrl">
<icon name="package" class="align-middle append-right-4 align-text-bottom" /> <icon name="package" class="align-middle append-right-4 align-text-bottom" />
{{ link.name }} {{ link.name }}
<span v-if="link.external">{{ __('(external source)') }}</span> <span v-if="link.external" data-testid="external-link-indicator">{{
__('(external source)')
}}</span>
</gl-link> </gl-link>
</li> </li>
</ul> </ul>
...@@ -61,5 +185,6 @@ export default { ...@@ -61,5 +185,6 @@ export default {
</li> </li>
</div> </div>
</div> </div>
</template>
</div> </div>
</template> </template>
...@@ -18341,15 +18341,24 @@ msgstr "" ...@@ -18341,15 +18341,24 @@ msgstr ""
msgid "ReleaseAssetLinkType|Image" msgid "ReleaseAssetLinkType|Image"
msgstr "" msgstr ""
msgid "ReleaseAssetLinkType|Images"
msgstr ""
msgid "ReleaseAssetLinkType|Other" msgid "ReleaseAssetLinkType|Other"
msgstr "" msgstr ""
msgid "ReleaseAssetLinkType|Package" msgid "ReleaseAssetLinkType|Package"
msgstr "" msgstr ""
msgid "ReleaseAssetLinkType|Packages"
msgstr ""
msgid "ReleaseAssetLinkType|Runbook" msgid "ReleaseAssetLinkType|Runbook"
msgstr "" msgstr ""
msgid "ReleaseAssetLinkType|Runbooks"
msgstr ""
msgid "Releases" msgid "Releases"
msgstr "" msgstr ""
...@@ -20992,6 +21001,9 @@ msgstr "" ...@@ -20992,6 +21001,9 @@ msgstr ""
msgid "Source code" msgid "Source code"
msgstr "" msgstr ""
msgid "Source code (%{fileExtension})"
msgstr ""
msgid "Source is not available" msgid "Source is not available"
msgstr "" msgstr ""
...@@ -22894,6 +22906,9 @@ msgstr "" ...@@ -22894,6 +22906,9 @@ msgstr ""
msgid "This license has already expired." msgid "This license has already expired."
msgstr "" msgstr ""
msgid "This link points to external content"
msgstr ""
msgid "This may expose confidential information as the selected fork is in another namespace that can have other members." msgid "This may expose confidential information as the selected fork is in another namespace that can have other members."
msgstr "" msgstr ""
......
...@@ -26,6 +26,7 @@ RSpec.describe 'User views releases', :js do ...@@ -26,6 +26,7 @@ RSpec.describe 'User views releases', :js do
expect(page).not_to have_content('Upcoming Release') expect(page).not_to have_content('Upcoming Release')
end end
shared_examples 'asset link tests' do
context 'when there is a link as an asset' do context 'when there is a link as an asset' do
let!(:release_link) { create(:release_link, release: release, url: url ) } let!(:release_link) { create(:release_link, release: release, url: url ) }
let(:url) { "#{project.web_url}/-/jobs/1/artifacts/download" } let(:url) { "#{project.web_url}/-/jobs/1/artifacts/download" }
...@@ -36,7 +37,7 @@ RSpec.describe 'User views releases', :js do ...@@ -36,7 +37,7 @@ RSpec.describe 'User views releases', :js do
page.within('.js-assets-list') do page.within('.js-assets-list') do
expect(page).to have_link release_link.name, href: direct_asset_link expect(page).to have_link release_link.name, href: direct_asset_link
expect(page).not_to have_content('(external source)') expect(page).not_to have_css('[data-testid="external-link-indicator"]')
end end
end end
...@@ -49,7 +50,7 @@ RSpec.describe 'User views releases', :js do ...@@ -49,7 +50,7 @@ RSpec.describe 'User views releases', :js do
page.within('.js-assets-list') do page.within('.js-assets-list') do
expect(page).to have_link release_link.name, href: direct_asset_link expect(page).to have_link release_link.name, href: direct_asset_link
expect(page).not_to have_content('(external source)') expect(page).not_to have_css('[data-testid="external-link-indicator"]')
end end
end end
end end
...@@ -61,11 +62,28 @@ RSpec.describe 'User views releases', :js do ...@@ -61,11 +62,28 @@ RSpec.describe 'User views releases', :js do
visit project_releases_path(project) visit project_releases_path(project)
page.within('.js-assets-list') do page.within('.js-assets-list') do
expect(page).to have_content('(external source)') expect(page).to have_css('[data-testid="external-link-indicator"]')
end end
end end
end end
end end
end
context 'when the release_asset_link_type feature flag is enabled' do
before do
stub_feature_flags(release_asset_link_type: true)
end
it_behaves_like 'asset link tests'
end
context 'when the release_asset_link_type feature flag is disabled' do
before do
stub_feature_flags(release_asset_link_type: false)
end
it_behaves_like 'asset link tests'
end
context 'with an upcoming release' do context 'with an upcoming release' do
let(:tomorrow) { Time.zone.now + 1.day } let(:tomorrow) { Time.zone.now + 1.day }
......
import { mount } from '@vue/test-utils';
import { GlCollapse } from '@gitlab/ui';
import ReleaseBlockAssets from '~/releases/components/release_block_assets.vue';
import { ASSET_LINK_TYPE } from '~/releases/constants';
import { trimText } from 'helpers/text_helper';
import { assets } from '../mock_data';
describe('Release block assets', () => {
let wrapper;
let defaultProps;
// A map of types to the expected section heading text
const sections = {
[ASSET_LINK_TYPE.IMAGE]: 'Images',
[ASSET_LINK_TYPE.PACKAGE]: 'Packages',
[ASSET_LINK_TYPE.RUNBOOK]: 'Runbooks',
[ASSET_LINK_TYPE.OTHER]: 'Other',
};
const createComponent = (propsData = defaultProps) => {
wrapper = mount(ReleaseBlockAssets, {
provide: {
glFeatures: { releaseAssetLinkType: true },
},
propsData,
});
};
const findSectionHeading = type =>
wrapper.findAll('h5').filter(h5 => h5.text() === sections[type]);
beforeEach(() => {
defaultProps = { assets };
});
describe('with default props', () => {
beforeEach(() => createComponent());
const findAccordionButton = () => wrapper.find('[data-testid="accordion-button"]');
it('renders an "Assets" accordion with the asset count', () => {
const accordionButton = findAccordionButton();
expect(accordionButton.exists()).toBe(true);
expect(trimText(accordionButton.text())).toBe('Assets 5');
});
it('renders the accordion as expanded by default', () => {
const accordion = wrapper.find(GlCollapse);
expect(accordion.exists()).toBe(true);
expect(accordion.isVisible()).toBe(true);
});
it('renders sources with the expected text and URL', () => {
defaultProps.assets.sources.forEach(s => {
const sourceLink = wrapper.find(`li>a[href="${s.url}"]`);
expect(sourceLink.exists()).toBe(true);
expect(sourceLink.text()).toBe(`Source code (${s.format})`);
});
});
it('renders a heading for each assets type (except sources)', () => {
Object.keys(sections).forEach(type => {
const sectionHeadings = findSectionHeading(type);
expect(sectionHeadings).toHaveLength(1);
});
});
it('renders asset links with the expected text and URL', () => {
defaultProps.assets.links.forEach(l => {
const sourceLink = wrapper.find(`li>a[href="${l.directAssetUrl}"]`);
expect(sourceLink.exists()).toBe(true);
expect(sourceLink.text()).toBe(l.name);
});
});
});
describe("when a release doesn't have a link with a certain asset type", () => {
const typeToExclude = ASSET_LINK_TYPE.IMAGE;
beforeEach(() => {
defaultProps.assets.links = defaultProps.assets.links.filter(
l => l.linkType !== typeToExclude,
);
createComponent(defaultProps);
});
it('does not render a section heading if there are no links of that type', () => {
const sectionHeadings = findSectionHeading(typeToExclude);
expect(sectionHeadings).toHaveLength(0);
});
});
describe('external vs internal links', () => {
const containsExternalSourceIndicator = () =>
wrapper.contains('[data-testid="external-link-indicator"]');
describe('when a link is external', () => {
beforeEach(() => {
defaultProps.assets.sources = [];
defaultProps.assets.links = [
{
...defaultProps.assets.links[0],
external: true,
},
];
createComponent(defaultProps);
});
it('renders the link with an "external source" indicator', () => {
expect(containsExternalSourceIndicator()).toBe(true);
});
});
describe('when a link is internal', () => {
beforeEach(() => {
defaultProps.assets.sources = [];
defaultProps.assets.links = [
{
...defaultProps.assets.links[0],
external: false,
},
];
createComponent(defaultProps);
});
it('renders the link without the "external source" indicator', () => {
expect(containsExternalSourceIndicator()).toBe(false);
});
});
});
});
import { ASSET_LINK_TYPE } from '~/releases/constants';
export const milestones = [ export const milestones = [
{ {
id: 50, id: 50,
...@@ -150,6 +152,42 @@ export const pageInfoHeadersWithPagination = { ...@@ -150,6 +152,42 @@ export const pageInfoHeadersWithPagination = {
'X-TOTAL-PAGES': '2', 'X-TOTAL-PAGES': '2',
}; };
export const assets = {
count: 5,
sources: [
{
format: 'zip',
url: 'https://example.gitlab.com/path/to/zip',
},
],
links: [
{
linkType: ASSET_LINK_TYPE.IMAGE,
url: 'https://example.gitlab.com/path/to/image',
directAssetUrl: 'https://example.gitlab.com/path/to/image',
name: 'Example image link',
},
{
linkType: ASSET_LINK_TYPE.PACKAGE,
url: 'https://example.gitlab.com/path/to/package',
directAssetUrl: 'https://example.gitlab.com/path/to/package',
name: 'Example package link',
},
{
linkType: ASSET_LINK_TYPE.RUNBOOK,
url: 'https://example.gitlab.com/path/to/runbook',
directAssetUrl: 'https://example.gitlab.com/path/to/runbook',
name: 'Example runbook link',
},
{
linkType: ASSET_LINK_TYPE.OTHER,
url: 'https://example.gitlab.com/path/to/link',
directAssetUrl: 'https://example.gitlab.com/path/to/link',
name: 'Example link',
},
],
};
export const release2 = { export const release2 = {
name: 'Bionic Beaver', name: 'Bionic Beaver',
tag_name: '18.04', tag_name: '18.04',
...@@ -180,42 +218,7 @@ export const release2 = { ...@@ -180,42 +218,7 @@ export const release2 = {
committer_email: 'jack@example.com', committer_email: 'jack@example.com',
committed_date: '2012-05-28T04:42:42-07:00', committed_date: '2012-05-28T04:42:42-07:00',
}, },
assets: { assets,
count: 6,
sources: [
{
format: 'zip',
url: 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.zip',
},
{
format: 'tar.gz',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.gz',
},
{
format: 'tar.bz2',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.bz2',
},
{
format: 'tar',
url: 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar',
},
],
links: [
{
name: 'release-18.04.dmg',
url: 'https://my-external-hosting.example.com/scrambled-url/',
external: true,
},
{
name: 'binary-linux-amd64',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50',
external: false,
},
],
},
}; };
export const releases = [release, release2]; export const releases = [release, release2];
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