Commit e7678707 authored by Filipa Lacerda's avatar Filipa Lacerda

Adds summary report in the pipeline widget

Adds badge to show number of vulnerabilities
parent c8a55646
<script>
import { sprintf, n__, __ } from '~/locale';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
export default {
name: 'SastSummaryReport',
components: {
ciIcon,
},
props: {
unresolvedIssues: {
type: Array,
required: false,
default: () => ([]),
},
link: {
type: String,
required: true,
},
},
computed: {
summarySastText() {
if (this.unresolvedIssues.length) {
return n__(
sprintf('SAST detected %{link}', {
link: `<a href=${this.link} class="prepend-left-5">%d security vulnerability</a>`,
}, false),
sprintf('SAST detected %{link}', {
link: `<a href=${this.link} class="prepend-left-5">%d security vulnerabilities</a>`,
}, false),
this.unresolvedIssues.length,
);
}
return sprintf(__('SAST detected %{link} '), {
link: `<a href=${this.link} class="prepend-left-5">no security vulnerabilities</a>`,
}, false);
},
statusIcon() {
if (this.unresolvedIssues) {
return {
group: 'warning',
icon: 'status_warning',
};
}
return {
group: 'success',
icon: 'status_success',
};
},
},
};
</script>
<template>
<div class="well-segment flex">
<ci-icon
:status="statusIcon"
class="flex flex-align-self-center"
/>
<span
class="prepend-left-10 flex flex-align-self-center"
v-html="summarySastText"
>
</span>
</div>
</template>
import Vue from 'vue'; import Vue from 'vue';
import Flash from '~/flash'; import Flash from '~/flash';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import { __ } from '~/locale';
import PipelinesMediator from './pipeline_details_mediator'; import PipelinesMediator from './pipeline_details_mediator';
import pipelineGraph from './components/graph/graph_component.vue'; import pipelineGraph from './components/graph/graph_component.vue';
import pipelineHeader from './components/header_component.vue'; import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub'; import eventHub from './event_hub';
import SecurityReportApp from './components/security_reports/security_report_app.vue'; import SecurityReportApp from './components/security_reports/security_report_app.vue';
import SastSummaryWidget from './components/security_reports/sast_report_summary_widget.vue';
Vue.use(Translate); Vue.use(Translate);
...@@ -76,8 +78,46 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -76,8 +78,46 @@ document.addEventListener('DOMContentLoaded', () => {
*/ */
const securityTab = document.getElementById('js-security-report-app'); const securityTab = document.getElementById('js-security-report-app');
const sastSummary = document.querySelector('.js-sast-summary');
if (securityTab) { // They are being rendered under the same condition
if (securityTab && sastSummary) {
const datasetOptions = securityTab.dataset;
const endpoint = datasetOptions.endpoint;
const blobPath = datasetOptions.blobPath;
mediator.fetchSastReport(endpoint, blobPath)
.then(() => {
// update the badge
document.querySelector('.js-sast-counter').textContent = mediator.store.state.sast.securityReports.newIssues.length;
})
.catch(() => {
Flash(__('Something when wrong while fetching SAST.'));
});
// Widget summary
// eslint-disable-next-line no-new
new Vue({
el: sastSummary,
components: {
SastSummaryWidget,
},
data() {
return {
mediator,
};
},
render(createElement) {
return createElement('sast-summary-widget', {
props: {
unresolvedIssues: this.mediator.store.state.securityReports.sast.newIssues,
link: sastSummary.dataset.tabPath,
},
});
},
});
// Tab content
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el: securityTab, el: securityTab,
...@@ -85,16 +125,10 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -85,16 +125,10 @@ document.addEventListener('DOMContentLoaded', () => {
SecurityReportApp, SecurityReportApp,
}, },
data() { data() {
const datasetOptions = this.$options.el.dataset;
return { return {
endpoint: datasetOptions.endpoint,
blobPath: datasetOptions.blobPath,
mediator, mediator,
}; };
}, },
created() {
this.mediator.fetchSastReport(this.endpoint, this.blobPath);
},
render(createElement) { render(createElement) {
return createElement('security-report-app', { return createElement('security-report-app', {
props: { props: {
......
...@@ -61,13 +61,10 @@ export default class pipelinesMediator { ...@@ -61,13 +61,10 @@ export default class pipelinesMediator {
* EE only * EE only
*/ */
fetchSastReport(endpoint, blobPath) { fetchSastReport(endpoint, blobPath) {
PipelineService.getSecurityReport(endpoint) return PipelineService.getSecurityReport(endpoint)
.then(response => response.json()) .then(response => response.json())
.then((data) => { .then((data) => {
this.store.storeSastReport(data, blobPath); this.store.storeSastReport(data, blobPath);
})
.catch(() => {
Flash(__('Something when wrong while fetching SAST.'));
}); });
} }
} }
#js-pipeline-header-vue.pipeline-header-container #js-pipeline-header-vue.pipeline-header-container
- sast_artifact = @pipeline.sast_artifact
- sast_tab_path = security_project_pipeline_path(@project, @pipeline)
- if @commit.present? - if @commit.present?
.commit-box .commit-box
...@@ -33,3 +35,6 @@ ...@@ -33,3 +35,6 @@
%span.js-details-content.hide %span.js-details-content.hide
= link_to @pipeline.sha, project_commit_path(@project, @pipeline.sha), class: "commit-sha commit-hash-full" = link_to @pipeline.sha, project_commit_path(@project, @pipeline.sha), class: "commit-sha commit-hash-full"
= clipboard_button(text: @pipeline.sha, title: "Copy commit SHA to clipboard") = clipboard_button(text: @pipeline.sha, title: "Copy commit SHA to clipboard")
- if sast_artifact
.js-sast-summary{ data: { tab_path: sast_tab_path }}
\ No newline at end of file
...@@ -19,8 +19,9 @@ ...@@ -19,8 +19,9 @@
%span.badge.js-failures-counter= failed_builds.count %span.badge.js-failures-counter= failed_builds.count
- if sast_artifact - if sast_artifact
%li.js-security-tab-link %li.js-security-tab-link
= link_to _("Security report"), security_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-security', action: 'security', toggle: 'tab' }, class: 'security-tab' = link_to security_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-security', action: 'security', toggle: 'tab' }, class: 'security-tab' do
= _("Security report")
%span.badge.js-sast-counter
.tab-content .tab-content
#js-tab-pipeline.tab-pane #js-tab-pipeline.tab-pane
......
...@@ -177,7 +177,7 @@ ...@@ -177,7 +177,7 @@
<p <p
v-if="type === 'docker' && infoText" v-if="type === 'docker' && infoText"
v-html="infoText" v-html="infoText"
class="js-mr-code-quality-info report-block-info" class="js-mr-code-quality-info prepend-left-10 report-block-info"
> >
</p> </p>
......
...@@ -39,7 +39,7 @@ export default { ...@@ -39,7 +39,7 @@ export default {
translateText(type) { translateText(type) {
return { return {
error: sprintf(s__('ciReport|Failed to load %{reportName} report'), { reportName: type }), error: sprintf(s__('ciReport|Failed to load %{reportName} report'), { reportName: type }),
loading: sprintf(s__('ciReport|Loading %{report} report'), { reportName: type }), loading: sprintf(s__('ciReport|Loading %{reportName} report'), { reportName: type }),
}; };
}, },
......
...@@ -3,11 +3,7 @@ ...@@ -3,11 +3,7 @@
border-top: 1px solid $gray-darker; border-top: 1px solid $gray-darker;
padding: $gl-padding-top; padding: $gl-padding-top;
background-color: $gray-light; background-color: $gray-light;
margin: $gl-padding -$gl-padding -$gl-padding; margin: $gl-padding #{-$gl-padding} #{-$gl-padding};
}
.report-block-info {
padding-left: 10px;
} }
.report-block-dast-code { .report-block-dast-code {
...@@ -51,3 +47,14 @@ ...@@ -51,3 +47,14 @@
} }
} }
} }
.pipeline-tab-content {
.space-children,
.space-children > * {
display: flex;
}
.media {
align-items: center;
}
}
\ No newline at end of file
import Vue from 'vue';
import reportSummary from '~/pipelines/components/security_reports/sast_report_summary_widget.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import { parsedSastIssuesHead } from '../../vue_shared/security_reports/mock_data';
describe('SAST report summary widget', () => {
let vm;
let Component;
beforeEach(() => {
Component = Vue.extend(reportSummary);
});
afterEach(() => {
vm.$destroy();
});
describe('with vulnerabilities', () => {
beforeEach(() => {
vm = mountComponent(Component, {
unresolvedIssues: parsedSastIssuesHead,
link: 'group/project/pipelines/2/security',
});
});
it('renders summary text with link for the security tab', () => {
expect(vm.$el.textContent.trim()).toEqual('SAST detected 2 security vulnerabilities');
expect(vm.$el.querySelector('a').getAttribute('href')).toEqual('group/project/pipelines/2/security');
});
});
describe('without vulnerabilities', () => {
beforeEach(() => {
vm = mountComponent(Component, {
link: 'group/project/pipelines/2/security',
});
});
it('render summary text with link for the security tab', () => {
expect(vm.$el.textContent.trim()).toEqual('SAST detected no security vulnerabilities');
expect(vm.$el.querySelector('a').getAttribute('href')).toEqual('group/project/pipelines/2/security');
});
});
});
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