Commit 09340785 authored by Daniel Tian's avatar Daniel Tian Committed by Savas Vedova

Add new group vulnerability report and vulnerability counts components

parent a42a6d5e
<script>
import countsQuery from 'ee/security_dashboard/graphql/queries/vulnerability_severities_count.query.graphql';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import VulnerabilityCounts from '../shared/vulnerability_counts.vue';
export default {
components: { VulnerabilityCounts },
inject: ['groupFullPath'],
data() {
return {
counts: {},
};
},
apollo: {
counts: {
query: countsQuery,
variables() {
return {
fullPath: this.groupFullPath,
isGroup: true,
};
},
update({ group }) {
return group.vulnerabilitySeveritiesCount;
},
error() {
createFlash({
message: s__(
'SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again.',
),
});
},
},
},
computed: {
isCountsLoading() {
return this.$apollo.queries.counts.loading;
},
},
};
</script>
<template>
<vulnerability-counts :counts="counts" :is-loading="isCountsLoading" />
</template>
<script>
import { GlCard, GlSkeletonLoading } from '@gitlab/ui';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import { SEVERITIES } from 'ee/security_dashboard/store/modules/vulnerabilities/constants';
export default {
components: { GlCard, GlSkeletonLoading, SeverityBadge },
props: {
counts: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
severityCounts() {
return SEVERITIES.map((severity) => ({
severity,
count: this.counts[severity] || 0,
}));
},
},
};
</script>
<template>
<div class="vulnerability-counts gl-font-weight-bold gl-text-center">
<gl-card
v-for="{ severity, count } in severityCounts"
:key="severity"
:data-testid="severity"
header-class="gl-p-3"
body-class="gl-font-size-h2"
>
<template #header>
<severity-badge :severity="severity" class="gl-text-center!" />
</template>
<template #default>
<gl-skeleton-loading
v-if="isLoading"
:lines="1"
class="gl-display-flex gl-align-items-center"
/>
<span v-else>{{ count }}</span>
</template>
</gl-card>
</div>
</template>
......@@ -6,6 +6,7 @@ import VulnerabilityReport from './components/shared/vulnerability_report.vue';
import apolloProvider from './graphql/provider';
import createRouter from './router';
import createStore from './store';
import GroupVulnerabilityReport from './components/group/vulnerability_report_development.vue';
export default (el, dashboardType) => {
if (!el) {
......@@ -100,6 +101,11 @@ export default (el, dashboardType) => {
const router = createRouter();
const store = createStore({ dashboardType });
const component =
gon.features?.operationalVulnerabilities && dashboardType === DASHBOARD_TYPES.GROUP
? GroupVulnerabilityReport
: VulnerabilityReport;
return new Vue({
el,
store,
......@@ -107,7 +113,7 @@ export default (el, dashboardType) => {
apolloProvider,
provide,
render(createElement) {
return createElement(VulnerabilityReport);
return createElement(component);
},
});
};
......@@ -3,3 +3,9 @@
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
grid-gap: $gl-spacing-scale-5;
}
.vulnerability-counts {
@include gl-display-grid;
grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
grid-gap: $gl-spacing-scale-7;
}
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { nextTick } from 'vue';
import VulnerabilityReportDevelopment from 'ee/security_dashboard/components/group/vulnerability_report_development.vue';
import VulnerabilityCounts from 'ee/security_dashboard/components/shared/vulnerability_counts.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import countsQuery from 'ee/security_dashboard/graphql/queries/vulnerability_severities_count.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
const localVue = createLocalVue();
localVue.use(VueApollo);
const counts = { critical: 1, high: 2, info: 3, low: 4, medium: 5, unknown: 6 };
const groupFullPath = 'path';
const countsRequestHandler = jest.fn().mockResolvedValue({
data: {
group: {
vulnerabilitySeveritiesCount: counts,
},
},
});
describe('Vulnerability counts component', () => {
let wrapper;
const createWrapper = ({ queries }) => {
wrapper = shallowMountExtended(VulnerabilityReportDevelopment, {
localVue,
apolloProvider: createMockApollo(queries),
provide: { groupFullPath },
});
};
const findVulnerabilityCounts = () => wrapper.findComponent(VulnerabilityCounts);
afterEach(() => {
wrapper.destroy();
countsRequestHandler.mockClear();
});
describe('vulnerability counts query', () => {
it('calls the counts query once with the expected data', () => {
createWrapper({ queries: [[countsQuery, countsRequestHandler]] });
expect(countsRequestHandler).toHaveBeenCalledTimes(1);
expect(countsRequestHandler).toHaveBeenCalledWith(
expect.objectContaining({
isGroup: true,
fullPath: groupFullPath,
}),
);
});
it('passes the isLoading prop with the expected values', async () => {
createWrapper({ queries: [[countsQuery, countsRequestHandler]] });
expect(findVulnerabilityCounts().props('isLoading')).toBe(true);
await nextTick();
expect(findVulnerabilityCounts().props('isLoading')).toBe(false);
});
it('passes the results of the counts query to the vulnerability counts component', async () => {
createWrapper({ queries: [[countsQuery, countsRequestHandler]] });
await nextTick();
expect(findVulnerabilityCounts().props('counts')).toMatchObject(counts);
});
});
});
import { GlCard, GlSkeletonLoading } from '@gitlab/ui';
import VulnerabilityCounts from 'ee/security_dashboard/components/shared/vulnerability_counts.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import {
CRITICAL,
HIGH,
MEDIUM,
INFO,
LOW,
UNKNOWN,
SEVERITIES,
} from 'ee/security_dashboard/store/modules/vulnerabilities/constants';
import { SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants';
describe('Vulnerability counts component', () => {
let wrapper;
const createWrapper = (props) => {
wrapper = mountExtended(VulnerabilityCounts, {
propsData: props,
});
};
const findCards = () => wrapper.findAllComponents(GlCard);
const findCardWithSeverity = (severity) => wrapper.findByTestId(severity);
afterEach(() => {
wrapper.destroy();
});
it('should show a skeleton loading component for each count when the isLoading prop is true', () => {
createWrapper({ isLoading: true, counts: {} });
findCards().wrappers.forEach((card) => {
expect(card.findComponent(GlSkeletonLoading).exists()).toBe(true);
});
});
it('should show a card for each severity with the correct count', () => {
const counts = { [CRITICAL]: 1, [HIGH]: 2, [MEDIUM]: 3, [LOW]: 4, [INFO]: 5, [UNKNOWN]: 6 };
createWrapper({ counts });
// Check that there are exactly the same number of cards as there are severities.
expect(findCards()).toHaveLength(Object.keys(counts).length);
Object.entries(counts).forEach(([severity, count]) => {
const cardText = findCardWithSeverity(severity).text();
expect(cardText).toContain(SEVERITY_LEVELS[severity]);
expect(cardText).toContain(count);
});
});
it('should show zero for the count when there is no value for that severity', () => {
createWrapper({ counts: {} });
SEVERITIES.forEach((severity) => {
expect(findCardWithSeverity(severity).text()).toContain('0');
});
});
});
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