Commit 2f07fec8 authored by Dave Pisek's avatar Dave Pisek

Add dependencies licenses to security dashboard

This commit adds the frontend changes needed to display dependencies
licenses on the security dashboard table.

It also adds spacing in between the ependencies-table columns.
parent facdbd2b
......@@ -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