Commit 4aaaa813 authored by Nathan Friend's avatar Nathan Friend Committed by Nicolò Maria Mezzopera

Show Release assets with type in accordion

This commit updates the "Assets" section on Release blocks to be
collapsible (collapsed by default) and to be grouped by asset type.
parent 00477e9d
<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 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 {
name: 'ReleaseBlockAssets',
components: {
GlLink,
GlButton,
GlCollapse,
GlIcon,
Icon,
GlBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
assets: {
type: Object,
required: true,
},
},
data() {
return {
isAssetsExpanded: true,
};
},
computed: {
hasAssets() {
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>
<template>
<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>
{{ __('Assets') }}
<span class="js-assets-count badge badge-pill">{{ assets.count }}</span>
......@@ -37,7 +159,9 @@ export default {
<gl-link v-gl-tooltip.bottom :title="__('Download asset')" :href="link.directAssetUrl">
<icon name="package" class="align-middle append-right-4 align-text-bottom" />
{{ 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>
</li>
</ul>
......@@ -61,5 +185,6 @@ export default {
</li>
</div>
</div>
</template>
</div>
</template>
......@@ -18341,15 +18341,24 @@ msgstr ""
msgid "ReleaseAssetLinkType|Image"
msgstr ""
msgid "ReleaseAssetLinkType|Images"
msgstr ""
msgid "ReleaseAssetLinkType|Other"
msgstr ""
msgid "ReleaseAssetLinkType|Package"
msgstr ""
msgid "ReleaseAssetLinkType|Packages"
msgstr ""
msgid "ReleaseAssetLinkType|Runbook"
msgstr ""
msgid "ReleaseAssetLinkType|Runbooks"
msgstr ""
msgid "Releases"
msgstr ""
......@@ -20992,6 +21001,9 @@ msgstr ""
msgid "Source code"
msgstr ""
msgid "Source code (%{fileExtension})"
msgstr ""
msgid "Source is not available"
msgstr ""
......@@ -22894,6 +22906,9 @@ msgstr ""
msgid "This license has already expired."
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."
msgstr ""
......
......@@ -26,6 +26,7 @@ RSpec.describe 'User views releases', :js do
expect(page).not_to have_content('Upcoming Release')
end
shared_examples 'asset link tests' do
context 'when there is a link as an asset' do
let!(:release_link) { create(:release_link, release: release, url: url ) }
let(:url) { "#{project.web_url}/-/jobs/1/artifacts/download" }
......@@ -36,7 +37,7 @@ RSpec.describe 'User views releases', :js do
page.within('.js-assets-list') do
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
......@@ -49,7 +50,7 @@ RSpec.describe 'User views releases', :js do
page.within('.js-assets-list') do
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
......@@ -61,11 +62,28 @@ RSpec.describe 'User views releases', :js do
visit project_releases_path(project)
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
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
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 = [
{
id: 50,
......@@ -150,6 +152,42 @@ export const pageInfoHeadersWithPagination = {
'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 = {
name: 'Bionic Beaver',
tag_name: '18.04',
......@@ -180,42 +218,7 @@ export const release2 = {
committer_email: 'jack@example.com',
committed_date: '2012-05-28T04:42:42-07:00',
},
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,
},
],
},
assets,
};
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