Commit 4ff2e634 authored by Kushal Pandya's avatar Kushal Pandya

Add license report information to MR widget

parent 682abe40
......@@ -20,8 +20,10 @@ export default {
return {
isLoadingCodequality: false,
isLoadingPerformance: false,
isLoadingLicenseReport: false,
loadingCodequalityFailed: false,
loadingPerformanceFailed: false,
loadingLicenseReportFailed: false,
};
},
computed: {
......@@ -32,6 +34,10 @@ export default {
const { codeclimate } = this.mr;
return codeclimate && codeclimate.head_path && codeclimate.base_path;
},
shouldRenderLicenseReport() {
const { licenseManagement } = this.mr;
return licenseManagement && licenseManagement.head_path && licenseManagement.base_path;
},
hasCodequalityIssues() {
return (
this.mr.codeclimateMetrics &&
......@@ -49,6 +55,10 @@ export default {
(this.mr.performanceMetrics.neutral && this.mr.performanceMetrics.neutral.length > 0))
);
},
hasLicenseReportIssues() {
const { licenseReport } = this.mr;
return licenseReport && licenseReport.length > 0;
},
shouldRenderPerformance() {
const { performance } = this.mr;
return performance && performance.head_path && performance.base_path;
......@@ -111,6 +121,18 @@ export default {
return text.join('');
},
licenseReportText() {
const { licenseReport } = this.mr;
if (licenseReport.length > 0) {
return sprintf(s__('ciReport|License management detected %{licenseInfo}'), {
licenseInfo: n__('%d new license', '%d new licenses', licenseReport.length),
});
}
return s__('ciReport|License management detected no new licenses');
},
codequalityStatus() {
return this.checkReportStatus(this.isLoadingCodequality, this.loadingCodequalityFailed);
},
......@@ -118,6 +140,10 @@ export default {
performanceStatus() {
return this.checkReportStatus(this.isLoadingPerformance, this.loadingPerformanceFailed);
},
licenseReportStatus() {
return this.checkReportStatus(this.isLoadingLicenseReport, this.loadingLicenseReportFailed);
},
},
created() {
if (this.shouldRenderCodeQuality) {
......@@ -127,6 +153,10 @@ export default {
if (this.shouldRenderPerformance) {
this.fetchPerformance();
}
if (this.shouldRenderLicenseReport) {
this.fetchLicenseReport();
}
},
methods: {
fetchCodeQuality() {
......@@ -166,6 +196,22 @@ export default {
});
},
fetchLicenseReport() {
const { head_path, base_path } = this.mr.licenseManagement;
this.isLoadingLicenseReport = true;
Promise.all([this.service.fetchReport(head_path), this.service.fetchReport(base_path)])
.then(values => {
this.mr.parseLicenseReportMetrics(values[0], values[1]);
this.isLoadingLicenseReport = false;
})
.catch(() => {
this.isLoadingLicenseReport = false;
this.loadingLicenseReportFailed = true;
});
},
translateText(type) {
return {
error: sprintf(s__('ciReport|Failed to load %{reportName} report'), {
......@@ -243,6 +289,17 @@ export default {
:vulnerability-feedback-help-path="mr.vulnerabilityFeedbackHelpPath"
:pipeline-id="mr.securityReportsPipelineId"
/>
<report-section
class="js-license-report-widget mr-widget-border-top"
v-if="shouldRenderLicenseReport"
type="license"
:status="licenseReportStatus"
:loading-text="translateText('license management').loading"
:error-text="translateText('license management').error"
:success-text="licenseReportText"
:unresolved-issues="mr.licenseReport"
:has-issues="hasLicenseReportIssues"
/>
<div class="mr-widget-section">
<component
:is="componentName"
......
......@@ -22,6 +22,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.initCodeclimate(data);
this.initPerformanceReport(data);
this.initLicenseReport(data);
}
setData(data) {
......@@ -67,6 +68,11 @@ export default class MergeRequestStore extends CEMergeRequestStore {
};
}
initLicenseReport(data) {
this.licenseManagement = data.license_management;
this.licenseReport = [];
}
compareCodeclimateMetrics(headIssues, baseIssues, headBlobPath, baseBlobPath) {
const parsedHeadIssues = MergeRequestStore.parseCodeclimateMetrics(headIssues, headBlobPath);
const parsedBaseIssues = MergeRequestStore.parseCodeclimateMetrics(baseIssues, baseBlobPath);
......@@ -127,6 +133,44 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.performanceMetrics = { improved, degraded, neutral };
}
parseLicenseReportMetrics(headMetrics, baseMetrics) {
const headLicenses = headMetrics.licenses;
const headDependencies = headMetrics.dependencies;
const baseLicenses = baseMetrics.licenses;
if (headLicenses.length > 0 && headDependencies.length > 0) {
const report = {};
const knownLicenses = baseLicenses.map(license => license.name);
const newLicenses = [];
headLicenses.forEach(license => {
if (knownLicenses.indexOf(license.name) === -1) {
report[license.name] = {
name: license.name,
count: license.count,
url: '',
packages: [],
};
newLicenses.push(license.name);
}
});
headDependencies.forEach(dependencyItem => {
const licenseName = dependencyItem.license.name;
if (newLicenses.indexOf(licenseName) > -1) {
if (!report[licenseName].url) {
report[licenseName].url = dependencyItem.license.url;
}
report[licenseName].packages.push(dependencyItem.dependency);
}
});
this.licenseReport = newLicenses.map(licenseName => report[licenseName]);
}
}
// normalize performance metrics by indexing on performance subject and metric name
static normalizePerformanceMetrics(performanceData) {
const indexedSubjects = {};
......
......@@ -44,6 +44,11 @@ export default {
isFullReportVisible: false,
};
},
computed: {
unresolvedIssuesStatus() {
return this.type === 'license' ? 'neutral' : 'failed';
},
},
methods: {
openFullReport() {
this.isFullReportVisible = true;
......@@ -59,7 +64,7 @@ export default {
class="js-mr-code-new-issues"
v-if="unresolvedIssues.length"
:type="type"
status="failed"
:status="unresolvedIssuesStatus"
:issues="unresolvedIssues"
/>
......
......@@ -2,6 +2,7 @@
import Icon from '~/vue_shared/components/icon.vue';
import PerformanceIssue from 'ee/vue_merge_request_widget/components/performance_issue_body.vue';
import CodequalityIssue from 'ee/vue_merge_request_widget/components/codequality_issue_body.vue';
import LicenseIssue from 'ee/vue_merge_request_widget/components/license_issue_body.vue';
import SastIssue from './sast_issue_body.vue';
import SastContainerIssue from './sast_container_issue_body.vue';
import DastIssue from './dast_issue_body.vue';
......@@ -17,13 +18,14 @@ export default {
DastIssue,
PerformanceIssue,
CodequalityIssue,
LicenseIssue,
},
props: {
issues: {
type: Array,
required: true,
},
// security || codequality || performance || docker || dast
// security || codequality || performance || docker || dast || license
type: {
type: String,
required: true,
......@@ -59,6 +61,9 @@ export default {
isTypePerformance() {
return this.type === 'performance';
},
isTypeLicense() {
return this.type === 'license';
},
isTypeSast() {
return this.type === SAST;
},
......@@ -89,6 +94,13 @@ export default {
}"
>
<icon
v-if="isTypeLicense"
name="status_created_borderless"
css-classes="prepend-left-4"
:size="24"
/>
<icon
v-else
:name="iconName"
:size="32"
/>
......@@ -120,6 +132,11 @@ export default {
v-else-if="isTypePerformance"
:issue="issue"
/>
<license-issue
v-else-if="isTypeLicense"
:issue="issue"
/>
</li>
</ul>
</div>
......
......@@ -56,6 +56,19 @@
list-style: none;
padding: 0 1px;
margin: 0;
.license-item {
line-height: $gl-padding-24;
.license-dependencies {
color: $gl-text-color-tertiary;
}
.btn-show-all-packages {
line-height: $gl-btn-line-height;
margin-bottom: 2px;
}
}
}
.report-block-list-icon {
......
......@@ -6,7 +6,14 @@ import MRWidgetService from 'ee/vue_merge_request_widget/services/mr_widget_serv
import MRWidgetStore from 'ee/vue_merge_request_widget/stores/mr_widget_store';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import state from 'ee/vue_shared/security_reports/store/state';
import mockData, { baseIssues, headIssues, basePerformance, headPerformance } from './mock_data';
import mockData, {
baseIssues,
headIssues,
basePerformance,
headPerformance,
licenseBaseIssues,
licenseHeadIssues,
} from './mock_data';
import {
sastIssues,
......@@ -654,6 +661,106 @@ describe('ee merge request widget options', () => {
});
});
describe('license management report', () => {
beforeEach(() => {
gl.mrWidgetData = {
...mockData,
license_management: {
head_path: 'head.json',
base_path: 'base.json',
},
};
Component.mr = new MRWidgetStore(gl.mrWidgetData);
Component.service = new MRWidgetService({});
});
describe('when it is loading', () => {
it('should render loading indicator', () => {
mock.onGet('head.json').reply(200, licenseHeadIssues);
mock.onGet('base.json').reply(200, licenseBaseIssues);
vm = mountComponent(Component);
expect(
removeBreakLine(vm.$el.querySelector('.js-license-report-widget').textContent),
).toContain('Loading license management report');
});
});
describe('with successful request', () => {
beforeEach(() => {
mock.onGet('head.json').reply(200, licenseHeadIssues);
mock.onGet('base.json').reply(200, licenseBaseIssues);
vm = mountComponent(Component);
});
it('should render report overview', done => {
setTimeout(() => {
expect(
removeBreakLine(
vm.$el.querySelector('.js-license-report-widget .js-code-text').textContent,
),
).toEqual('License management detected 1 new license');
done();
}, 0);
});
it('should render report issues list in section body', done => {
setTimeout(() => {
const sectionBodyEl = vm.$el.querySelector(
'.js-license-report-widget .js-report-section-container',
);
expect(sectionBodyEl).not.toBeNull();
expect(sectionBodyEl.querySelectorAll('li.report-block-list-issue').length).toBe(
licenseHeadIssues.licenses.length - 1,
);
done();
}, 0);
});
});
describe('with empty successful request', () => {
beforeEach(() => {
mock.onGet('head.json').reply(200, licenseBaseIssues);
mock.onGet('base.json').reply(200, licenseBaseIssues);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('should render report overview', done => {
setTimeout(() => {
expect(
removeBreakLine(
vm.$el.querySelector('.js-license-report-widget .js-code-text').textContent,
),
).toEqual('License management detected no new licenses');
done();
}, 0);
});
});
describe('with failed request', () => {
beforeEach(() => {
mock.onGet('head.json').reply(500, {});
mock.onGet('base.json').reply(500, {});
vm = mountComponent(Component);
});
it('should render error indicator', done => {
setTimeout(() => {
expect(
removeBreakLine(
vm.$el.querySelector('.js-license-report-widget .js-code-text').textContent,
),
).toContain('Failed to load license management report');
done();
}, 0);
});
});
});
describe('computed', () => {
describe('shouldRenderApprovals', () => {
it('should return false when no approvals', () => {
......
......@@ -224,8 +224,10 @@ export default {
base_path: 'blob_path',
head_path: 'blob_path',
},
vulnerability_feedback_help_path: '/help/user/project/merge_requests/index#interacting-with-security-reports-ultimate',
merge_commit_path: 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
vulnerability_feedback_help_path:
'/help/user/project/merge_requests/index#interacting-with-security-reports-ultimate',
merge_commit_path:
'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
};
// Codeclimate
export const headIssues = [
......@@ -396,3 +398,141 @@ export const codequalityParsedIssues = [
urlPath: 'foo/Gemfile.lock',
},
];
export const licenseBaseIssues = {
licenses: [
{
count: 1,
name: 'MIT',
},
],
dependencies: [
{
license: {
name: 'MIT',
url: 'http://opensource.org/licenses/mit-license',
},
dependency: {
name: 'bundler',
url: 'http://bundler.io',
description: 'The best way to manage your application\'s dependencies',
pathes: [
'.',
],
},
},
],
};
export const licenseHeadIssues = {
licenses: [
{
count: 3,
name: 'New BSD',
},
{
count: 1,
name: 'MIT',
},
],
dependencies: [
{
license: {
name: 'New BSD',
url: 'http://opensource.org/licenses/BSD-3-Clause',
},
dependency: {
name: 'pg',
url: 'https://bitbucket.org/ged/ruby-pg',
description:
'Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]',
pathes: ['.'],
},
},
{
license: {
name: 'New BSD',
url: 'http://opensource.org/licenses/BSD-3-Clause',
},
dependency: {
name: 'puma',
url: 'http://puma.io',
description:
'Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications',
pathes: ['.'],
},
},
{
license: {
name: 'New BSD',
url: 'http://opensource.org/licenses/BSD-3-Clause',
},
dependency: {
name: 'foo',
url: 'http://foo.io',
description:
'Foo is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications',
pathes: ['.'],
},
},
{
license: {
name: 'MIT',
url: 'http://opensource.org/licenses/mit-license',
},
dependency: {
name: 'execjs',
url: 'https://github.com/rails/execjs',
description: 'Run JavaScript code from Ruby',
pathes: [
'.',
],
},
},
],
};
export const licenseReport = [
{
name: 'New BSD',
count: 5,
url: 'http://opensource.org/licenses/BSD-3-Clause',
packages: [
{
name: 'pg',
url: 'https://bitbucket.org/ged/ruby-pg',
description:
'Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]',
pathes: ['.'],
},
{
name: 'puma',
url: 'http://puma.io',
description:
'Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications',
pathes: ['.'],
},
{
name: 'foo',
url: 'https://bitbucket.org/ged/ruby-pg',
description:
'Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]',
pathes: ['.'],
},
{
name: 'bar',
url: 'http://puma.io',
description:
'Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications',
pathes: ['.'],
},
{
name: 'baz',
url: 'https://bitbucket.org/ged/ruby-pg',
description:
'Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]',
pathes: ['.'],
},
],
},
];
......@@ -5,6 +5,8 @@ import mockData, {
baseIssues,
parsedBaseIssues,
parsedHeadIssues,
licenseBaseIssues,
licenseHeadIssues,
} from '../mock_data';
describe('MergeRequestStore', () => {
......@@ -95,6 +97,26 @@ describe('MergeRequestStore', () => {
});
});
describe('parseLicenseReportMetrics', () => {
it('should parse the received issues', () => {
store.parseLicenseReportMetrics(licenseHeadIssues, licenseBaseIssues);
expect(store.licenseReport[0].name).toBe(licenseHeadIssues.licenses[0].name);
expect(store.licenseReport[0].url).toBe(licenseHeadIssues.dependencies[0].license.url);
});
it('should ommit issues from base report', () => {
const knownLicenseName = licenseBaseIssues.licenses[0].name;
store.parseLicenseReportMetrics(licenseHeadIssues, licenseBaseIssues);
expect(store.licenseReport.length).toBe(licenseHeadIssues.licenses.length - 1);
expect(store.licenseReport[0].packages.length).toBe(
licenseHeadIssues.dependencies.length - 1,
);
store.licenseReport.forEach(license => {
expect(license.name).not.toBe(knownLicenseName);
});
});
});
describe('isNothingToMergeState', () => {
it('returns true when nothingToMerge', () => {
store.state = stateKey.nothingToMerge;
......
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