Skip to content
Snippets Groups Projects
Commit fa682861 authored by David Pisek's avatar David Pisek 3️⃣
Browse files

Merge branch '365502-display-summary-highlights' into 'master'

Display summary highlights

See merge request !95568
parents d5cb6990 8766f656
No related branches found
No related tags found
2 merge requests!96059Draft: Add GraphQL query for deployment details,!95568Display summary highlights
Pipeline #616825209 passed
......@@ -13,6 +13,9 @@ export default {
apiFuzzing: s__('ciReport|API fuzzing'),
securityScanning: s__('ciReport|Security scanning'),
error: s__('ciReport|Security reports failed loading results'),
highlights: s__(
'ciReport|%{criticalStart}critical%{criticalEnd}, %{highStart}high%{highEnd} and %{otherStart}others%{otherEnd}',
),
noNewVulnerabilities: s__(
'ciReport|%{scanner} detected no %{boldStart}new%{boldEnd} potential vulnerabilities',
),
......
<script>
import axios from '~/lib/utils/axios_utils';
import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue';
import { CRITICAL, HIGH } from '~/vulnerabilities/constants';
import SummaryText from './summary_text.vue';
import SummaryHighlights from './summary_highlights.vue';
import i18n from './i18n';
export default {
......@@ -9,6 +11,7 @@ export default {
components: {
MrWidget,
SummaryText,
SummaryHighlights,
},
i18n,
props: {
......@@ -41,6 +44,37 @@ export default {
}, 0);
},
highlights() {
if (!this.vulnerabilities.collapsed) {
return {};
}
const highlights = {
[HIGH]: 0,
[CRITICAL]: 0,
other: 0,
};
// The data we receive from the API is something like:
// [
// { scanner: "SAST", added: [{ id: 15, severity: 'critical' }] },
// { scanner: "DAST", added: [{ id: 15, severity: 'high' }] },
// ...
// ]
return this.vulnerabilities.collapsed
.flatMap((vuln) => vuln.added)
.reduce((acc, vuln) => {
if (vuln.severity === HIGH) {
acc[HIGH] += 1;
} else if (vuln.severity === CRITICAL) {
acc[CRITICAL] += 1;
} else {
acc.other += 1;
}
return acc;
}, highlights);
},
totalNewVulnerabilities() {
if (!this.vulnerabilities.collapsed) {
return 0;
......@@ -100,6 +134,10 @@ export default {
>
<template #summary>
<summary-text :total-new-vulnerabilities="totalNewVulnerabilities" :is-loading="isLoading" />
<summary-highlights
v-if="!isLoading && totalNewVulnerabilities > 0"
:highlights="highlights"
/>
</template>
<template #content>
<!-- complex content will go here, otherwise we can use the structured :content property. -->
......
<script>
import { GlSprintf } from '@gitlab/ui';
import { CRITICAL, HIGH } from '~/vulnerabilities/constants';
import i18n from './i18n';
export default {
components: {
GlSprintf,
},
i18n,
props: {
highlights: {
type: Object,
required: true,
validate: (highlights) =>
[CRITICAL, HIGH, 'other'].every(
(requiredField) => typeof highlights[requiredField] !== 'undefined',
),
},
},
computed: {
criticalSeverity() {
return this.highlights[CRITICAL];
},
highSeverity() {
return this.highlights[HIGH];
},
otherSeverity() {
return this.highlights.other;
},
},
};
</script>
<template>
<div>
<gl-sprintf :message="$options.i18n.highlights">
<template #critical="{ content }"
><strong class="gl-text-red-800">{{ criticalSeverity }} {{ content }}</strong></template
>
<template #high="{ content }"
><strong class="gl-text-red-600">{{ highSeverity }} {{ content }}</strong></template
>
<template #other="{ content }"
><strong>{{ otherSeverity }} {{ content }}</strong></template
>
</gl-sprintf>
</div>
</template>
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MR Widget Security Reports - Summary Highlights should display the summary highlights properly 1`] = `"<div><strong class=\\"gl-text-red-800\\">10 critical</strong>, <strong class=\\"gl-text-red-600\\">20 high</strong> and <strong>60 others</strong></div>"`;
......@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import MRSecurityWidget from 'ee/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue';
import SummaryText from 'ee/vue_merge_request_widget/extensions/security_reports/summary_text.vue';
import SummaryHighlights from 'ee/vue_merge_request_widget/extensions/security_reports/summary_highlights.vue';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
import axios from '~/lib/utils/axios_utils';
......@@ -22,6 +23,7 @@ describe('MR Widget Security Reports', () => {
const findWidget = () => wrapper.findComponent(Widget);
const findSummaryText = () => wrapper.findComponent(SummaryText);
const findSummaryHighlights = () => wrapper.findComponent(SummaryHighlights);
beforeEach(() => {
mockAxios = new MockAdapter(axios);
......@@ -48,7 +50,7 @@ describe('MR Widget Security Reports', () => {
});
});
it('fetchCollapsedData - returns an empty list of endpoints when the paths are missing from the MR data', () => {
it('fetchCollapsedData - returns an empty list of endpoints', () => {
expect(wrapper.vm.fetchCollapsedData().length).toBe(0);
});
......@@ -57,6 +59,11 @@ describe('MR Widget Security Reports', () => {
findWidget().vm.$emit('is-loading', true);
await nextTick();
expect(findSummaryText().props()).toMatchObject({ isLoading: true });
expect(findSummaryHighlights().exists()).toBe(false);
});
it('does not display the summary highlights component', () => {
expect(findSummaryHighlights().exists()).toBe(false);
});
it('should not be collapsible', () => {
......@@ -71,13 +78,19 @@ describe('MR Widget Security Reports', () => {
};
const mockWithData = () => {
mockAxios
.onGet(reportEndpoints.sastComparisonPath)
.replyOnce(200, { added: [{ id: 1 }, { id: 2 }] });
mockAxios.onGet(reportEndpoints.sastComparisonPath).replyOnce(200, {
added: [
{ id: 1, severity: 'critical' },
{ id: 2, severity: 'high' },
],
});
mockAxios
.onGet(reportEndpoints.dastComparisonPath)
.replyOnce(200, { added: [{ id: 5 }, { id: 3 }] });
mockAxios.onGet(reportEndpoints.dastComparisonPath).replyOnce(200, {
added: [
{ id: 5, severity: 'low' },
{ id: 3, severity: 'unknown' },
],
});
};
it('computes the total number of new potential vulnerabilities correctly', async () => {
......@@ -90,6 +103,9 @@ describe('MR Widget Security Reports', () => {
await waitForPromises();
expect(findSummaryText().props()).toMatchObject({ totalNewVulnerabilities: 4 });
expect(findSummaryHighlights().props()).toMatchObject({
highlights: { critical: 1, high: 1, other: 2 },
});
});
it('tells the widget to be collapsible only if there is data', async () => {
......
import { GlSprintf } from '@gitlab/ui';
import SummaryHighlights from 'ee/vue_merge_request_widget/extensions/security_reports/summary_highlights.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('MR Widget Security Reports - Summary Highlights', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMountExtended(SummaryHighlights, {
propsData: {
highlights: {
critical: 10,
high: 20,
other: 60,
},
},
stubs: { GlSprintf },
});
};
it('should display the summary highlights properly', () => {
createComponent();
expect(wrapper.html()).toMatchSnapshot();
});
});
......@@ -45840,6 +45840,9 @@ msgid_plural "changes"
msgstr[0] ""
msgstr[1] ""
 
msgid "ciReport|%{criticalStart}critical%{criticalEnd}, %{highStart}high%{highEnd} and %{otherStart}others%{otherEnd}"
msgstr ""
msgid "ciReport|%{danger_start}%{degradedNum} degraded%{danger_end}, %{same_start}%{sameNum} same%{same_end}, and %{success_start}%{improvedNum} improved%{success_end}"
msgstr ""
 
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment