Skip to content
代码片段 群组 项目
未验证 提交 d1d8005e 编辑于 作者: Savas Vedova's avatar Savas Vedova 提交者: GitLab
浏览文件

Display capped results for large datasets

Use capped results instead of showing the real numbers in
Vulnerability Report pages. This is a breaking change.

EE: true
Changelog: changed
上级 1e58bbc4
No related branches found
No related tags found
无相关合并请求
显示
135 个添加14 个删除
......@@ -13,3 +13,5 @@ export const UNKNOWN = 'unknown';
* All vulnerability severities in decreasing order.
*/
export const SEVERITIES = [CRITICAL, HIGH, MEDIUM, LOW, INFO, UNKNOWN];
export const SEVERITY_COUNT_LIMIT = 1000;
......@@ -5,7 +5,7 @@ import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import countsQuery from 'ee/security_dashboard/graphql/queries/vulnerability_severities_count.query.graphql';
import { SEVERITIES } from '~/vulnerabilities/constants';
import { SEVERITIES, SEVERITY_COUNT_LIMIT } from '~/vulnerabilities/constants';
export default {
components: { GlCard, GlSkeletonLoader, SeverityBadge },
......@@ -32,6 +32,7 @@ export default {
isProject: this.dashboardType === DASHBOARD_TYPES.PROJECT,
isGroup: this.dashboardType === DASHBOARD_TYPES.GROUP,
isInstance: this.dashboardType === DASHBOARD_TYPES.INSTANCE,
capped: true,
...this.filters,
};
},
......@@ -66,6 +67,11 @@ export default {
this.$emit('counts-changed', this.severityCounts);
},
},
methods: {
formattedCounts(counts) {
return counts > SEVERITY_COUNT_LIMIT ? `${SEVERITY_COUNT_LIMIT}+` : counts;
},
},
};
</script>
......@@ -84,7 +90,7 @@ export default {
<template #default>
<gl-skeleton-loader v-if="isLoadingCounts" :equal-width-lines="true" :lines="1" />
<span v-else class="gl-font-size-h2">{{ count }}</span>
<span v-else class="gl-font-size-h2">{{ formattedCounts(count) }}</span>
</template>
</gl-card>
</div>
......
......@@ -407,6 +407,7 @@ export default {
isProject: this.isProjectVulnerabilityReport,
isGroup: this.isGroupVulnerabilityReport,
isInstance: this.isInstanceVulnerabilityReport,
capped: true,
...this.graphqlFilters,
...this.formatGroupFilters(value),
};
......@@ -562,6 +563,7 @@ export default {
class="gl-ml-3"
:show-single-severity="computeShowSingleSeverityProp(group.value)"
:highlights="groupCounts[group.value]"
capped
/>
</div>
<vulnerability-list-graphql
......
<script>
import { sumBy } from 'lodash';
import { GlTab, GlBadge } from '@gitlab/ui';
import { SEVERITY_COUNT_LIMIT } from '~/vulnerabilities/constants';
import VulnerabilityReport from './vulnerability_report.vue';
export default {
......@@ -30,12 +30,19 @@ export default {
},
data() {
return {
counts: undefined,
counts: [],
isCapped: false,
};
},
computed: {
countsSum() {
return sumBy(this.counts, (x) => x.count);
const hasCapped = this.counts.some(({ count }) => count > SEVERITY_COUNT_LIMIT);
if (hasCapped) {
return `${SEVERITY_COUNT_LIMIT}+`;
}
return this.counts.reduce((sum, { count }) => sum + count, 0);
},
},
methods: {
......
......@@ -17,6 +17,7 @@ query vulnerabilitySeveritiesCount(
$isGroup: Boolean = false
$isProject: Boolean = false
$isInstance: Boolean = false
$capped: Boolean = false
$dismissalReason: [VulnerabilityDismissalReason!]
$owaspTopTen: [VulnerabilityOwaspTop10!]
) {
......@@ -33,6 +34,7 @@ query vulnerabilitySeveritiesCount(
hasMergeRequest: $hasMergeRequest
hasRemediations: $hasRemediations
dismissalReason: $dismissalReason
capped: $capped
) {
...VulnerabilitySeveritiesCount
}
......@@ -52,6 +54,7 @@ query vulnerabilitySeveritiesCount(
hasRemediations: $hasRemediations
dismissalReason: $dismissalReason
owaspTopTen: $owaspTopTen
capped: $capped
) {
...VulnerabilitySeveritiesCount
}
......@@ -72,6 +75,7 @@ query vulnerabilitySeveritiesCount(
image: $image
dismissalReason: $dismissalReason
owaspTopTen: $owaspTopTen
capped: $capped
) {
...VulnerabilitySeveritiesCount
}
......
<script>
import { GlSprintf } from '@gitlab/ui';
import { CRITICAL, HIGH, MEDIUM, LOW, INFO, UNKNOWN } from '~/vulnerabilities/constants';
import {
CRITICAL,
HIGH,
MEDIUM,
LOW,
INFO,
UNKNOWN,
SEVERITY_COUNT_LIMIT,
} from '~/vulnerabilities/constants';
import { s__ } from '~/locale';
import { SEVERITY_CLASS_NAME_MAP } from './constants';
......@@ -30,22 +38,49 @@ export default {
validate: (highlights) =>
[CRITICAL, HIGH].every((requiredField) => typeof highlights[requiredField] !== 'undefined'),
},
capped: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
criticalSeverity() {
return this.highlights[CRITICAL];
return this.formattedCounts(this.highlights[CRITICAL]);
},
highSeverity() {
return this.highlights[HIGH];
return this.formattedCounts(this.highlights[HIGH]);
},
otherSeverity() {
if (typeof this.highlights.other !== 'undefined') {
return this.highlights.other;
return this.formattedCounts(this.highlights.other);
}
let totalCounts = 0;
let isCapped = false;
[MEDIUM, LOW, INFO, UNKNOWN].forEach((severity) => {
const count = this.highlights[severity];
if (count) {
totalCounts += count;
}
if (this.capped && count > SEVERITY_COUNT_LIMIT) {
isCapped = true;
}
});
return isCapped ? this.formattedCounts(totalCounts) : totalCounts;
},
},
methods: {
formattedCounts(count) {
if (this.capped) {
return count > SEVERITY_COUNT_LIMIT ? `${SEVERITY_COUNT_LIMIT}+` : count;
}
return Object.keys(this.highlights).reduce((total, key) => {
return [MEDIUM, LOW, INFO, UNKNOWN].includes(key) ? total + this.highlights[key] : total;
}, 0);
return count;
},
},
cssClass: SEVERITY_CLASS_NAME_MAP,
......@@ -55,7 +90,7 @@ export default {
<template>
<div class="gl-font-sm">
<strong v-if="showSingleSeverity" :class="$options.cssClass[showSingleSeverity]">{{
highlights[showSingleSeverity]
formattedCounts(highlights[showSingleSeverity])
}}</strong>
<gl-sprintf v-else :message="$options.i18n.highlights">
<template #critical="{ content }"
......
......@@ -148,4 +148,27 @@ describe('Vulnerability counts component', () => {
expect(findCardWithSeverity(severity).text()).toContain('0');
});
});
describe('capped counts', () => {
const cappedCounts = { critical: 1001, high: 2, medium: 1001, low: 4, info: 3, unknown: 6 };
it.each`
dashboardType
${DASHBOARD_TYPES.PROJECT}
${DASHBOARD_TYPES.GROUP}
${DASHBOARD_TYPES.INSTANCE}
`('should display capped results for %dashboardType', async ({ dashboardType }) => {
const handler = getCountsRequestHandler({ data: cappedCounts, dashboardType });
createWrapper({ countsHandler: handler, dashboardType });
await waitForPromises();
['critical', 'medium'].forEach((severity) => {
expect(findCardWithSeverity(severity).text()).toContain('1000+');
});
['high', 'low', 'info', 'unknown'].forEach((severity) => {
expect(findCardWithSeverity(severity).text()).toContain(cappedCounts[severity].toString());
});
});
});
});
......@@ -62,6 +62,15 @@ describe('Vulnerability report tab component', () => {
expect(findBadge().text()).toBe('16');
});
it('shows the correct capped sum when the vulnerability report updates the counts', async () => {
createWrapper();
findReport().vm.$emit('counts-changed', [{ count: 1001 }, { count: 3 }, { count: 5 }]);
await nextTick();
// If any count is higher than the cap, we show the cap.
expect(findBadge().text()).toBe('1000+');
});
it('shows the header slot content', () => {
const headerSlot = 'header slot content';
createWrapper({ headerSlot });
......
......@@ -5,11 +5,12 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('MR Widget Security Reports - Summary Highlights', () => {
let wrapper;
const createComponent = ({ highlights, showSingleSeverity } = {}) => {
const createComponent = ({ highlights, showSingleSeverity, capped } = {}) => {
wrapper = shallowMountExtended(SummaryHighlights, {
propsData: {
highlights,
showSingleSeverity,
capped,
},
stubs: { GlSprintf },
});
......@@ -66,4 +67,36 @@ describe('MR Widget Security Reports - Summary Highlights', () => {
expect(wrapper.text()).toBe(count.toString());
},
);
it('shows capped results when capped property is true', () => {
const others = { medium: 50, low: 1001, unknown: 20 };
createComponent({
capped: true,
highlights: {
critical: 1001,
high: 20,
...others,
},
});
expect(wrapper.text()).toContain('1000+ critical');
expect(wrapper.text()).toContain('20 high');
expect(wrapper.text()).toContain('1000+ others');
});
it('shows capped results when `other` is specified and capped property is true', () => {
createComponent({
capped: true,
highlights: {
critical: 1001,
high: 20,
other: 1001,
},
});
expect(wrapper.text()).toContain('1000+ critical');
expect(wrapper.text()).toContain('20 high');
expect(wrapper.text()).toContain('1000+ others');
});
});
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册