Commit 1eb24b98 authored by Clement Ho's avatar Clement Ho

Merge branch...

Merge branch '10536-add-license-information-to-the-dependency-list-based-on-current-license-rules' into 'master'

Add License information to the Dependency List

See merge request gitlab-org/gitlab-ee!14905
parents 51db1086 2f07fec8
......@@ -26,13 +26,14 @@ export default {
data() {
const tableSections = [
{ className: 'section-20', label: s__('Dependencies|Component') },
{ className: 'section-15', label: s__('Dependencies|Version') },
{ className: 'section-10', label: s__('Dependencies|Version') },
{ className: 'section-20', label: s__('Dependencies|Packager') },
{ className: 'flex-grow-1', label: s__('Dependencies|Location') },
{ className: 'section-15', label: s__('Dependencies|Location') },
{ className: 'section-15', label: s__('Dependencies|License') },
];
if (this.dependencyListVulnerabilities) {
tableSections.unshift({ className: 'section-15', label: s__('Dependencies|Status') });
tableSections.unshift({ className: 'section-20', label: s__('Dependencies|Status') });
}
return { tableSections };
......
<script>
import { GlButton, GlSkeletonLoading } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import DependencyLicenseLinks from './dependency_license_links.vue';
import DependencyVulnerability from './dependency_vulnerability.vue';
import { MAX_DISPLAYED_VULNERABILITIES_PER_DEPENDENCY } from './constants';
export default {
name: 'DependenciesTableRow',
components: {
DependencyLicenseLinks,
DependencyVulnerability,
GlButton,
GlSkeletonLoading,
......@@ -82,7 +84,8 @@ export default {
class="d-flex flex-column justify-content-center h-auto"
/>
<div v-else class="d-md-flex align-items-baseline">
<div class="table-section section-15 section-wrap">
<!-- status-->
<div class="table-section section-20 section-wrap pr-md-3">
<div class="table-mobile-header" role="rowheader">{{ s__('Dependencies|Status') }}</div>
<div class="table-mobile-content">
<gl-button
......@@ -106,29 +109,41 @@ export default {
</div>
</div>
<div class="table-section section-20 section-wrap">
<!-- name-->
<div class="table-section section-20 section-wrap pr-md-3">
<div class="table-mobile-header" role="rowheader">
{{ s__('Dependencies|Component') }}
</div>
<div class="table-mobile-content">{{ dependency.name }}</div>
</div>
<div class="table-section section-15">
<!-- version -->
<div class="table-section section-10 pr-md-3">
<div class="table-mobile-header" role="rowheader">{{ s__('Dependencies|Version') }}</div>
<div class="table-mobile-content">{{ dependency.version }}</div>
</div>
<div class="table-section section-20 section-wrap">
<!-- packager -->
<div class="table-section section-20 section-wrap pr-md-3">
<div class="table-mobile-header" role="rowheader">{{ s__('Dependencies|Packager') }}</div>
<div class="table-mobile-content">{{ dependency.packager }}</div>
</div>
<div class="table-section flex-grow-1 section-wrap">
<!-- location -->
<div class="table-section section-15 section-wrap pr-md-3">
<div class="table-mobile-header" role="rowheader">{{ s__('Dependencies|Location') }}</div>
<div class="table-mobile-content">
<a :href="dependency.location.blob_path">{{ dependency.location.path }}</a>
</div>
</div>
<!-- license -->
<div class="table-section section-15 section-wrap">
<div class="table-mobile-header" role="rowheader">{{ s__('Dependencies|License') }}</div>
<div class="table-mobile-content">
<dependency-license-links :licenses="dependency.licenses" :title="dependency.name" />
</div>
</div>
</div>
<ul v-if="isExpanded" class="d-none d-md-block list-unstyled mb-1">
......@@ -154,27 +169,39 @@ export default {
class="d-flex flex-column justify-content-center"
/>
<template v-else>
<div class="table-section section-20 section-wrap">
<!-- name-->
<div class="table-section section-20 section-wrap pr-md-3">
<div class="table-mobile-header" role="rowheader">{{ s__('Dependencies|Component') }}</div>
<div class="table-mobile-content">{{ dependency.name }}</div>
</div>
<div class="table-section section-15">
<!-- version -->
<div class="table-section section-10 pr-md-3">
<div class="table-mobile-header" role="rowheader">{{ s__('Dependencies|Version') }}</div>
<div class="table-mobile-content">{{ dependency.version }}</div>
</div>
<div class="table-section section-20 section-wrap">
<!-- packager -->
<div class="table-section section-20 section-wrap pr-md-3">
<div class="table-mobile-header" role="rowheader">{{ s__('Dependencies|Packager') }}</div>
<div class="table-mobile-content">{{ dependency.packager }}</div>
</div>
<div class="table-section flex-grow-1 section-wrap">
<!-- location -->
<div class="table-section section-15 section-wrap pr-md-3">
<div class="table-mobile-header" role="rowheader">{{ s__('Dependencies|Location') }}</div>
<div class="table-mobile-content">
<a :href="dependency.location.blob_path">{{ dependency.location.path }}</a>
</div>
</div>
<!-- license -->
<div class="table-section section-15">
<div class="table-mobile-header" role="rowheader">{{ s__('Dependencies|License') }}</div>
<div class="table-mobile-content">
<dependency-license-links :licenses="dependency.licenses" :title="dependency.name" />
</div>
</div>
</template>
</div>
</template>
<script>
import { uniqueId } from 'underscore';
import { sprintf, s__ } from '~/locale';
import { GlButton, GlLink, GlModal, GlModalDirective, GlIntersperse } from '@gitlab/ui';
// If there are more licenses than this count, a counter will be displayed for the remaining licenses
// eg.: VISIBLE_LICENSE_COUNT = 2; licenses = ['MIT', 'GNU', 'GPL'] -> 'MIT, GNU and 1 more'
const VISIBLE_LICENSES_COUNT = 2;
const MODAL_ID_PREFIX = 'dependency-license-link-modal-';
export default {
components: {
GlIntersperse,
GlButton,
GlLink,
GlModal,
},
directives: {
GlModalDirective,
},
props: {
title: {
type: String,
required: true,
},
licenses: {
type: Array,
required: true,
},
},
computed: {
allLicenses() {
return Array.isArray(this.licenses) ? this.licenses : [];
},
visibleLicenses() {
return this.allLicenses.slice(0, VISIBLE_LICENSES_COUNT);
},
remainingLicensesCount() {
return this.allLicenses.length - VISIBLE_LICENSES_COUNT;
},
hasLicensesInModal() {
return this.remainingLicensesCount > 0;
},
lastSeparator() {
return ` ${s__('SeriesFinalConjunction|and')} `;
},
modalId() {
return uniqueId(MODAL_ID_PREFIX);
},
modalActionText() {
return s__('Modal|Close');
},
modalButtonText() {
const { remainingLicensesCount } = this;
return sprintf(s__('Dependencies|%{remainingLicensesCount} more'), {
remainingLicensesCount,
});
},
},
};
</script>
<template>
<div>
<gl-intersperse :last-separator="lastSeparator" class="js-license-links-license-list">
<span
v-for="license in visibleLicenses"
:key="license.name"
class="js-license-links-license-list-item"
>
<gl-link v-if="license.url" :href="license.url" target="_blank">{{ license.name }}</gl-link>
<template v-else>{{ license.name }}</template>
</span>
<gl-button
v-if="hasLicensesInModal"
v-gl-modal-directive="modalId"
variant="link"
class="align-baseline js-license-links-modal-trigger"
>{{ modalButtonText }}</gl-button
>
</gl-intersperse>
<div class="js-license-links-modal">
<gl-modal
v-if="hasLicensesInModal"
:title="title"
:modal-id="modalId"
:ok-title="modalActionText"
ok-only
ok-variant="secondary"
>
<h5>{{ __('Licenses') }}</h5>
<ul class="list-unstyled">
<li v-for="license in licenses" :key="license.name" class="js-license-links-modal-item">
<gl-link v-if="license.url" :href="license.url" target="_blank">{{
license.name
}}</gl-link>
<span v-else>{{ license.name }}</span>
</li>
</ul>
</gl-modal>
</div>
</div>
</template>
---
title: Add License information to the Dependency List based on current license rules
merge_request: 14905
author:
type: added
......@@ -8,7 +8,7 @@ exports[`DependenciesTableRow component given the dependencyListVulnerabilities
class="d-md-flex align-items-baseline"
>
<div
class="table-section section-15 section-wrap"
class="table-section section-20 section-wrap pr-md-3"
>
<div
class="table-mobile-header"
......@@ -36,7 +36,7 @@ exports[`DependenciesTableRow component given the dependencyListVulnerabilities
</div>
<div
class="table-section section-20 section-wrap"
class="table-section section-20 section-wrap pr-md-3"
>
<div
class="table-mobile-header"
......@@ -55,7 +55,7 @@ exports[`DependenciesTableRow component given the dependencyListVulnerabilities
</div>
<div
class="table-section section-15"
class="table-section section-10 pr-md-3"
>
<div
class="table-mobile-header"
......@@ -72,7 +72,7 @@ exports[`DependenciesTableRow component given the dependencyListVulnerabilities
</div>
<div
class="table-section section-20 section-wrap"
class="table-section section-20 section-wrap pr-md-3"
>
<div
class="table-mobile-header"
......@@ -89,7 +89,7 @@ exports[`DependenciesTableRow component given the dependencyListVulnerabilities
</div>
<div
class="table-section flex-grow-1 section-wrap"
class="table-section section-15 section-wrap pr-md-3"
>
<div
class="table-mobile-header"
......@@ -108,6 +108,26 @@ exports[`DependenciesTableRow component given the dependencyListVulnerabilities
</a>
</div>
</div>
<div
class="table-section section-15 section-wrap"
>
<div
class="table-mobile-header"
role="rowheader"
>
License
</div>
<div
class="table-mobile-content"
>
<dependencylicenselinks-stub
licenses=""
title="left-pad"
/>
</div>
</div>
</div>
<!---->
......@@ -122,7 +142,7 @@ exports[`DependenciesTableRow component given the dependencyListVulnerabilities
class="d-md-flex align-items-baseline"
>
<div
class="table-section section-15 section-wrap"
class="table-section section-20 section-wrap pr-md-3"
>
<div
class="table-mobile-header"
......@@ -152,7 +172,7 @@ exports[`DependenciesTableRow component given the dependencyListVulnerabilities
</div>
<div
class="table-section section-20 section-wrap"
class="table-section section-20 section-wrap pr-md-3"
>
<div
class="table-mobile-header"
......@@ -171,7 +191,7 @@ exports[`DependenciesTableRow component given the dependencyListVulnerabilities
</div>
<div
class="table-section section-15"
class="table-section section-10 pr-md-3"
>
<div
class="table-mobile-header"
......@@ -188,7 +208,7 @@ exports[`DependenciesTableRow component given the dependencyListVulnerabilities
</div>
<div
class="table-section section-20 section-wrap"
class="table-section section-20 section-wrap pr-md-3"
>
<div
class="table-mobile-header"
......@@ -205,7 +225,7 @@ exports[`DependenciesTableRow component given the dependencyListVulnerabilities
</div>
<div
class="table-section flex-grow-1 section-wrap"
class="table-section section-15 section-wrap pr-md-3"
>
<div
class="table-mobile-header"
......@@ -224,6 +244,26 @@ exports[`DependenciesTableRow component given the dependencyListVulnerabilities
</a>
</div>
</div>
<div
class="table-section section-15 section-wrap"
>
<div
class="table-mobile-header"
role="rowheader"
>
License
</div>
<div
class="table-mobile-content"
>
<dependencylicenselinks-stub
licenses=""
title="left-pad"
/>
</div>
</div>
</div>
<!---->
......@@ -261,7 +301,7 @@ exports[`DependenciesTableRow component when a dependency is loaded matches the
class="gl-responsive-table-row p-2"
>
<div
class="table-section section-20 section-wrap"
class="table-section section-20 section-wrap pr-md-3"
>
<div
class="table-mobile-header"
......@@ -278,7 +318,7 @@ exports[`DependenciesTableRow component when a dependency is loaded matches the
</div>
<div
class="table-section section-15"
class="table-section section-10 pr-md-3"
>
<div
class="table-mobile-header"
......@@ -295,7 +335,7 @@ exports[`DependenciesTableRow component when a dependency is loaded matches the
</div>
<div
class="table-section section-20 section-wrap"
class="table-section section-20 section-wrap pr-md-3"
>
<div
class="table-mobile-header"
......@@ -312,7 +352,7 @@ exports[`DependenciesTableRow component when a dependency is loaded matches the
</div>
<div
class="table-section flex-grow-1 section-wrap"
class="table-section section-15 section-wrap pr-md-3"
>
<div
class="table-mobile-header"
......@@ -331,6 +371,26 @@ exports[`DependenciesTableRow component when a dependency is loaded matches the
</a>
</div>
</div>
<div
class="table-section section-15"
>
<div
class="table-mobile-header"
role="rowheader"
>
License
</div>
<div
class="table-mobile-content"
>
<dependencylicenselinks-stub
licenses=""
title="left-pad"
/>
</div>
</div>
</div>
`;
......
......@@ -15,7 +15,7 @@ exports[`DependenciesTable component given a list of dependencies (loaded) match
</div>
<div
class="table-section section-15"
class="table-section section-10"
role="rowheader"
>
......@@ -31,13 +31,21 @@ exports[`DependenciesTable component given a list of dependencies (loaded) match
</div>
<div
class="table-section flex-grow-1"
class="table-section section-15"
role="rowheader"
>
Location
</div>
<div
class="table-section section-15"
role="rowheader"
>
License
</div>
</div>
<dependenciestablerow-stub
......@@ -64,7 +72,7 @@ exports[`DependenciesTable component given a list of dependencies (loading) matc
</div>
<div
class="table-section section-15"
class="table-section section-10"
role="rowheader"
>
......@@ -80,13 +88,21 @@ exports[`DependenciesTable component given a list of dependencies (loading) matc
</div>
<div
class="table-section flex-grow-1"
class="table-section section-15"
role="rowheader"
>
Location
</div>
<div
class="table-section section-15"
role="rowheader"
>
License
</div>
</div>
<dependenciestablerow-stub
......@@ -115,7 +131,7 @@ exports[`DependenciesTable component given an empty list of dependencies matches
</div>
<div
class="table-section section-15"
class="table-section section-10"
role="rowheader"
>
......@@ -131,13 +147,21 @@ exports[`DependenciesTable component given an empty list of dependencies matches
</div>
<div
class="table-section flex-grow-1"
class="table-section section-15"
role="rowheader"
>
Location
</div>
<div
class="table-section section-15"
role="rowheader"
>
License
</div>
</div>
</div>
......@@ -150,7 +174,7 @@ exports[`DependenciesTable component given the dependencyListVulnerabilities fla
role="row"
>
<div
class="table-section section-15"
class="table-section section-20"
role="rowheader"
>
......@@ -166,7 +190,7 @@ exports[`DependenciesTable component given the dependencyListVulnerabilities fla
</div>
<div
class="table-section section-15"
class="table-section section-10"
role="rowheader"
>
......@@ -182,13 +206,21 @@ exports[`DependenciesTable component given the dependencyListVulnerabilities fla
</div>
<div
class="table-section flex-grow-1"
class="table-section section-15"
role="rowheader"
>
Location
</div>
<div
class="table-section section-15"
role="rowheader"
>
License
</div>
</div>
<dependenciestablerow-stub
......@@ -207,7 +239,7 @@ exports[`DependenciesTable component given the dependencyListVulnerabilities fla
role="row"
>
<div
class="table-section section-15"
class="table-section section-20"
role="rowheader"
>
......@@ -223,7 +255,7 @@ exports[`DependenciesTable component given the dependencyListVulnerabilities fla
</div>
<div
class="table-section section-15"
class="table-section section-10"
role="rowheader"
>
......@@ -239,13 +271,21 @@ exports[`DependenciesTable component given the dependencyListVulnerabilities fla
</div>
<div
class="table-section flex-grow-1"
class="table-section section-15"
role="rowheader"
>
Location
</div>
<div
class="table-section section-15"
role="rowheader"
>
License
</div>
</div>
<dependenciestablerow-stub
......@@ -266,7 +306,7 @@ exports[`DependenciesTable component given the dependencyListVulnerabilities fla
role="row"
>
<div
class="table-section section-15"
class="table-section section-20"
role="rowheader"
>
......@@ -282,7 +322,7 @@ exports[`DependenciesTable component given the dependencyListVulnerabilities fla
</div>
<div
class="table-section section-15"
class="table-section section-10"
role="rowheader"
>
......@@ -298,13 +338,21 @@ exports[`DependenciesTable component given the dependencyListVulnerabilities fla
</div>
<div
class="table-section flex-grow-1"
class="table-section section-15"
role="rowheader"
>
Location
</div>
<div
class="table-section section-15"
role="rowheader"
>
License
</div>
</div>
</div>
......
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlModal, GlLink, GlIntersperse } from '@gitlab/ui';
import DependenciesLicenseLinks from 'ee/dependencies/components/dependency_license_links.vue';
describe('DependencyLicenseLinks component', () => {
// data helpers
const createLicenses = n => [...Array(n).keys()].map(i => ({ name: `license ${i + 1}` }));
const addUrls = (licenses, numLicensesWithUrls = Infinity) =>
licenses.map((ls, i) => ({
...ls,
...(i < numLicensesWithUrls ? { url: `license ${i + 1}` } : {}),
}));
// wrapper / factory
let wrapper;
const factory = ({ numLicenses, numLicensesWithUrl = 0, title = 'test-dependency' } = {}) => {
const licenses = addUrls(createLicenses(numLicenses), numLicensesWithUrl);
const localVue = createLocalVue();
wrapper = shallowMount(localVue.extend(DependenciesLicenseLinks), {
localVue,
propsData: {
licenses,
title,
},
});
};
// query helpers
const jsTestClassSelector = name => `.js-license-links-${name}`;
const findLicensesList = () => wrapper.find(jsTestClassSelector('license-list'));
const findLicenseListItems = () => wrapper.findAll(jsTestClassSelector('license-list-item'));
const findModal = () => wrapper.find(jsTestClassSelector('modal'));
const findModalItem = () => wrapper.findAll(jsTestClassSelector('modal-item'));
const findModalTrigger = () => wrapper.find(jsTestClassSelector('modal-trigger'));
afterEach(() => {
wrapper.destroy();
});
it('intersperses the list of licenses correctly', () => {
factory();
const intersperseInstance = wrapper.find(GlIntersperse);
expect(intersperseInstance.exists()).toBe(true);
expect(intersperseInstance.attributes('lastseparator')).toBe(' and ');
});
it.each([3, 5, 8, 13])('limits the number of visible licenses to 2', numLicenses => {
factory({ numLicenses });
expect(findLicenseListItems().length).toBe(2);
});
it.each`
numLicenses | numLicensesWithUrl | expectedNumVisibleLinks | expectedNumModalLinks
${2} | ${2} | ${2} | ${0}
${3} | ${2} | ${2} | ${2}
${5} | ${2} | ${2} | ${2}
${2} | ${1} | ${1} | ${0}
${3} | ${1} | ${1} | ${1}
${5} | ${0} | ${0} | ${0}
`(
'contains the correct number of links given $numLicenses licenses where $numLicensesWithUrl contain a url',
({ numLicenses, numLicensesWithUrl, expectedNumVisibleLinks, expectedNumModalLinks }) => {
factory({ numLicenses, numLicensesWithUrl });
expect(findLicensesList().findAll(GlLink).length).toBe(expectedNumVisibleLinks);
expect(findModal().findAll(GlLink).length).toBe(expectedNumModalLinks);
},
);
it('sets all links to open in new windows/tabs', () => {
factory({ numLicenses: 8, numLicensesWithUrl: 8 });
const links = wrapper.findAll(GlLink);
links.wrappers.forEach(link => {
expect(link.attributes('target')).toBe('_blank');
});
});
it.each`
numLicenses | expectedNumExceedingLicenses
${3} | ${1}
${5} | ${3}
${8} | ${6}
`(
'shows the number of licenses that are included in the modal',
({ numLicenses, expectedNumExceedingLicenses }) => {
factory({ numLicenses });
expect(findModalTrigger().text()).toBe(`${expectedNumExceedingLicenses} more`);
},
);
it.each`
numLicenses | expectedNumModals
${0} | ${0}
${1} | ${0}
${2} | ${0}
${3} | ${1}
${5} | ${1}
${8} | ${1}
`(
'contains $expectedNumModals modal when $numLicenses licenses are given',
({ numLicenses, expectedNumModals }) => {
factory({ numLicenses, expectedNumModals });
expect(wrapper.findAll(GlModal).length).toBe(expectedNumModals);
},
);
it('opens the modal when the trigger gets clicked', () => {
factory({ numLicenses: 3 });
const modalId = wrapper.find(GlModal).props('modalId');
const modalTrigger = findModalTrigger();
const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
modalTrigger.trigger('click');
expect(rootEmit.mock.calls[0]).toContain(modalId);
});
it('assigns a unique modal-id to each of its instances', () => {
const numLicenses = 4;
const usedModalIds = [];
while (usedModalIds.length < 10) {
factory({ numLicenses });
const modalId = wrapper.find(GlModal).props('modalId');
expect(usedModalIds).not.toContain(modalId);
usedModalIds.push(modalId);
}
});
it('uses the title as the modal-title', () => {
const title = 'test-dependency';
factory({ numLicenses: 3, title });
expect(wrapper.find(GlModal).attributes('title')).toEqual(title);
});
it('assigns the correct action button text to the modal', () => {
factory({ numLicenses: 3 });
expect(wrapper.find(GlModal).attributes('ok-title')).toEqual('Close');
});
it.each`
numLicenses | expectedLicensesInModal
${1} | ${0}
${2} | ${0}
${3} | ${3}
${5} | ${5}
${8} | ${8}
`('contains the correct modal content', ({ numLicenses, expectedLicensesInModal }) => {
factory({ numLicenses });
expect(findModalItem().length).toBe(expectedLicensesInModal);
});
});
......@@ -6,6 +6,7 @@ export const makeDependency = (changes = {}) => ({
blob_path: '/a-group/a-project/blob/da39a3ee5e6b4b0d3255bfef95601890afd80709/yarn.lock',
path: 'yarn.lock',
},
licenses: [],
...changes,
});
......
......@@ -4765,6 +4765,9 @@ msgid_plural "Dependencies|%d vulnerabilities"
msgstr[0] ""
msgstr[1] ""
msgid "Dependencies|%{remainingLicensesCount} more"
msgstr ""
msgid "Dependencies|All"
msgstr ""
......@@ -4780,6 +4783,9 @@ msgstr ""
msgid "Dependencies|Job failed to generate the dependency list"
msgstr ""
msgid "Dependencies|License"
msgstr ""
msgid "Dependencies|Location"
msgstr ""
......@@ -13692,6 +13698,9 @@ msgstr ""
msgid "September"
msgstr ""
msgid "SeriesFinalConjunction|and"
msgstr ""
msgid "Server supports batch API only, please update your Git LFS client to version 1.0.1 and up."
msgstr ""
......
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