Commit 5adf8f0a authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '233536-group-project-security-status-report' into 'master'

Use GraphQL to get list of vulnerable projects for group

See merge request gitlab-org/gitlab!38878
parents e6e504ac b2cec84c
...@@ -42,6 +42,11 @@ export default { ...@@ -42,6 +42,11 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
groupFullPath: {
type: String,
required: false,
default: undefined,
},
}, },
data() { data() {
return { return {
...@@ -54,6 +59,11 @@ export default { ...@@ -54,6 +59,11 @@ export default {
query() { query() {
return this.query; return this.query;
}, },
variables() {
return {
fullPath: this.groupFullPath,
};
},
update(results) { update(results) {
return this.processRawData(results); return this.processRawData(results);
}, },
...@@ -102,7 +112,9 @@ export default { ...@@ -102,7 +112,9 @@ export default {
return mostSevereVulnerability; return mostSevereVulnerability;
}, },
processRawData(results) { processRawData(results) {
const { vulnerabilityGrades } = results.instanceSecurityDashboard; const { vulnerabilityGrades } = this.groupFullPath
? results.group
: results.instanceSecurityDashboard;
return vulnerabilityGrades.reduce((acc, v) => { return vulnerabilityGrades.reduce((acc, v) => {
acc[v.grade] = v.projects.nodes; acc[v.grade] = v.projects.nodes;
...@@ -152,8 +164,11 @@ export default { ...@@ -152,8 +164,11 @@ export default {
:disabled="shouldAccordionItemBeDisabled(severityGroup)" :disabled="shouldAccordionItemBeDisabled(severityGroup)"
:max-height="$options.accordionItemsContentMaxHeight" :max-height="$options.accordionItemsContentMaxHeight"
> >
<template #title="{ isExpanded, isDisabled }" <template #title="{ isExpanded, isDisabled }">
><h5 class="gl-display-flex gl-align-items-center gl-font-weight-normal gl-p-0 gl-m-0"> <h5
class="gl-display-flex gl-align-items-center gl-font-weight-normal gl-p-0 gl-m-0"
data-testid="vulnerability-severity-groups"
>
<span <span
v-gl-tooltip v-gl-tooltip
:title="severityGroup.description" :title="severityGroup.description"
......
<script> <script>
import VulnerabilityChart from './first_class_vulnerability_chart.vue'; import VulnerabilityChart from './first_class_vulnerability_chart.vue';
import VulnerabilitySeverity from './vulnerability_severity.vue'; import VulnerabilitySeverities from './first_class_vulnerability_severities.vue';
import vulnerabilityHistoryQuery from '../graphql/group_vulnerability_history.query.graphql'; import vulnerabilityHistoryQuery from '../graphql/group_vulnerability_history.query.graphql';
import vulnerabilityGradesQuery from '../graphql/group_vulnerability_grades.query.graphql';
import SecurityChartsLayout from './security_charts_layout.vue'; import SecurityChartsLayout from './security_charts_layout.vue';
export default { export default {
components: { components: {
SecurityChartsLayout, SecurityChartsLayout,
VulnerabilitySeverity, VulnerabilitySeverities,
VulnerabilityChart, VulnerabilityChart,
}, },
props: { props: {
...@@ -15,14 +16,11 @@ export default { ...@@ -15,14 +16,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
vulnerableProjectsEndpoint: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
vulnerabilityHistoryQuery, vulnerabilityHistoryQuery,
vulnerabilityGradesQuery,
}; };
}, },
}; };
...@@ -31,6 +29,6 @@ export default { ...@@ -31,6 +29,6 @@ export default {
<template> <template>
<security-charts-layout> <security-charts-layout>
<vulnerability-chart :query="vulnerabilityHistoryQuery" :group-full-path="groupFullPath" /> <vulnerability-chart :query="vulnerabilityHistoryQuery" :group-full-path="groupFullPath" />
<vulnerability-severity :endpoint="vulnerableProjectsEndpoint" /> <vulnerability-severities :query="vulnerabilityGradesQuery" :group-full-path="groupFullPath" />
</security-charts-layout> </security-charts-layout>
</template> </template>
...@@ -44,7 +44,6 @@ export default (el, dashboardType) => { ...@@ -44,7 +44,6 @@ export default (el, dashboardType) => {
} else if (dashboardType === DASHBOARD_TYPES.GROUP) { } else if (dashboardType === DASHBOARD_TYPES.GROUP) {
component = FirstClassGroupSecurityDashboard; component = FirstClassGroupSecurityDashboard;
props.groupFullPath = el.dataset.groupFullPath; props.groupFullPath = el.dataset.groupFullPath;
props.vulnerableProjectsEndpoint = el.dataset.vulnerableProjectsEndpoint;
} else if (dashboardType === DASHBOARD_TYPES.INSTANCE) { } else if (dashboardType === DASHBOARD_TYPES.INSTANCE) {
component = FirstClassInstanceSecurityDashboard; component = FirstClassInstanceSecurityDashboard;
} }
......
#import "ee/security_dashboard/graphql/project.fragment.graphql"
#import "./vulnerability_severities_count.fragment.graphql"
query groupVulnerabilityGrades($fullPath: ID!) {
group(fullPath: $fullPath) {
vulnerabilityGrades {
grade
projects {
nodes {
...Project
...VulnerabilitySeveritiesCount
}
}
}
}
}
...@@ -33,7 +33,6 @@ export default (el, dashboardType) => { ...@@ -33,7 +33,6 @@ export default (el, dashboardType) => {
if (dashboardType === DASHBOARD_TYPES.GROUP) { if (dashboardType === DASHBOARD_TYPES.GROUP) {
component = GroupSecurityCharts; component = GroupSecurityCharts;
props.groupFullPath = el.dataset.groupFullPath; props.groupFullPath = el.dataset.groupFullPath;
props.vulnerableProjectsEndpoint = el.dataset.vulnerableProjectsEndpoint;
} }
const router = createRouter(); const router = createRouter();
......
---
title: Change data source for Vulnerable Projects to GraphQL
merge_request: 38878
author:
type: performance
...@@ -13,7 +13,6 @@ describe('First Class Group Dashboard Component', () => { ...@@ -13,7 +13,6 @@ describe('First Class Group Dashboard Component', () => {
const dashboardDocumentation = 'dashboard-documentation'; const dashboardDocumentation = 'dashboard-documentation';
const emptyStateSvgPath = 'empty-state-path'; const emptyStateSvgPath = 'empty-state-path';
const groupFullPath = 'group-full-path'; const groupFullPath = 'group-full-path';
const vulnerableProjectsEndpoint = '/vulnerable/projects';
const vulnerabilitiesExportEndpoint = '/vulnerabilities/exports'; const vulnerabilitiesExportEndpoint = '/vulnerabilities/exports';
const findDashboardLayout = () => wrapper.find(SecurityDashboardLayout); const findDashboardLayout = () => wrapper.find(SecurityDashboardLayout);
...@@ -29,7 +28,6 @@ describe('First Class Group Dashboard Component', () => { ...@@ -29,7 +28,6 @@ describe('First Class Group Dashboard Component', () => {
dashboardDocumentation, dashboardDocumentation,
emptyStateSvgPath, emptyStateSvgPath,
groupFullPath, groupFullPath,
vulnerableProjectsEndpoint,
vulnerabilitiesExportEndpoint, vulnerabilitiesExportEndpoint,
}, },
data, data,
......
...@@ -19,10 +19,26 @@ describe('Vulnerability Severity component', () => { ...@@ -19,10 +19,26 @@ describe('Vulnerability Severity component', () => {
B: [projects[4]], B: [projects[4]],
A: [projects[5], projects[6]], A: [projects[5], projects[6]],
}; };
const responseData = {
vulnerabilityGrades: Object.entries(vulnerabilityGrades).map(([grade, gradeProjects]) => ({
grade,
projects: { nodes: gradeProjects },
})),
};
const findAccordionItemsText = () =>
wrapper
.findAll('[data-testid="vulnerability-severity-groups"]')
.wrappers.map(item => trimText(item.text()));
const createComponent = ({ $apollo, propsData, data }) => { const mockAccordianItemsText = () =>
Object.entries(vulnerabilityGrades).map(
([grade, relatedProjects]) =>
`${grade} ${n__('%d project', '%d projects', relatedProjects.length)}`,
);
const createComponent = ({ propsData, data }) => {
return shallowMount(VulnerabilitySeverity, { return shallowMount(VulnerabilitySeverity, {
$apollo,
propsData: { propsData: {
query: {}, query: {},
helpPagePath, helpPagePath,
...@@ -47,11 +63,37 @@ describe('Vulnerability Severity component', () => { ...@@ -47,11 +63,37 @@ describe('Vulnerability Severity component', () => {
wrapper = null; wrapper = null;
}); });
beforeEach(() => { describe('when loading the project severity component for group level dashboard', () => {
wrapper = createComponent({ data: () => ({ vulnerabilityGrades }) }); beforeEach(() => {
wrapper = createComponent({ propsData: { groupFullPath: 'gitlab-org' } });
});
it('should process the data returned from GraphQL properly', async () => {
wrapper.setData({ vulnerabilityGrades: wrapper.vm.processRawData({ group: responseData }) });
await wrapper.vm.$nextTick();
expect(findAccordionItemsText()).toEqual(mockAccordianItemsText());
});
});
describe('when loading the project severity component for instance level dashboard', () => {
beforeEach(() => {
wrapper = createComponent({});
});
it('should process the data returned from GraphQL properly', async () => {
wrapper.setData({
vulnerabilityGrades: wrapper.vm.processRawData({ instanceSecurityDashboard: responseData }),
});
await wrapper.vm.$nextTick();
expect(findAccordionItemsText()).toEqual(mockAccordianItemsText());
});
}); });
describe('for all cases', () => { describe('for all cases', () => {
beforeEach(() => {
wrapper = createComponent({});
});
it('has the link to the help page', () => { it('has the link to the help page', () => {
expect(findHelpLink().attributes('href')).toBe(helpPagePath); expect(findHelpLink().attributes('href')).toBe(helpPagePath);
}); });
...@@ -81,6 +123,7 @@ describe('Vulnerability Severity component', () => { ...@@ -81,6 +123,7 @@ describe('Vulnerability Severity component', () => {
let text; let text;
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ data: () => ({ vulnerabilityGrades }) });
accordion = findAccordionItemByGrade(grade); accordion = findAccordionItemByGrade(grade);
text = trimText(accordion.text()); text = trimText(accordion.text());
}); });
......
...@@ -3,7 +3,7 @@ import { TEST_HOST } from 'jest/helpers/test_constants'; ...@@ -3,7 +3,7 @@ import { TEST_HOST } from 'jest/helpers/test_constants';
import GroupSecurityCharts from 'ee/security_dashboard/components/group_security_charts.vue'; import GroupSecurityCharts from 'ee/security_dashboard/components/group_security_charts.vue';
import SecurityChartsLayout from 'ee/security_dashboard/components/security_charts_layout.vue'; import SecurityChartsLayout from 'ee/security_dashboard/components/security_charts_layout.vue';
import VulnerabilityChart from 'ee/security_dashboard/components/first_class_vulnerability_chart.vue'; import VulnerabilityChart from 'ee/security_dashboard/components/first_class_vulnerability_chart.vue';
import VulnerabilitySeverity from 'ee/security_dashboard/components/vulnerability_severity.vue'; import VulnerabilitySeverities from 'ee/security_dashboard/components/first_class_vulnerability_severities.vue';
jest.mock('ee/security_dashboard/graphql/group_vulnerability_history.query.graphql', () => ({})); jest.mock('ee/security_dashboard/graphql/group_vulnerability_history.query.graphql', () => ({}));
...@@ -11,15 +11,14 @@ describe('Group Security Charts component', () => { ...@@ -11,15 +11,14 @@ describe('Group Security Charts component', () => {
let wrapper; let wrapper;
const groupFullPath = `${TEST_HOST}/group/5`; const groupFullPath = `${TEST_HOST}/group/5`;
const vulnerableProjectsEndpoint = `${TEST_HOST}/group/5/projects`;
const findSecurityChartsLayoutComponent = () => wrapper.find(SecurityChartsLayout); const findSecurityChartsLayoutComponent = () => wrapper.find(SecurityChartsLayout);
const findVulnerabilityChart = () => wrapper.find(VulnerabilityChart); const findVulnerabilityChart = () => wrapper.find(VulnerabilityChart);
const findVulnerabilitySeverity = () => wrapper.find(VulnerabilitySeverity); const findVulnerabilitySeverities = () => wrapper.find(VulnerabilitySeverities);
const createWrapper = () => { const createWrapper = () => {
wrapper = shallowMount(GroupSecurityCharts, { wrapper = shallowMount(GroupSecurityCharts, {
propsData: { groupFullPath, vulnerableProjectsEndpoint }, propsData: { groupFullPath },
}); });
}; };
...@@ -35,13 +34,11 @@ describe('Group Security Charts component', () => { ...@@ -35,13 +34,11 @@ describe('Group Security Charts component', () => {
it('renders the default page', () => { it('renders the default page', () => {
const securityChartsLayout = findSecurityChartsLayoutComponent(); const securityChartsLayout = findSecurityChartsLayoutComponent();
const vulnerabilityChart = findVulnerabilityChart(); const vulnerabilityChart = findVulnerabilityChart();
const vulnerabilitySeverity = findVulnerabilitySeverity(); const vulnerabilitySeverities = findVulnerabilitySeverities();
expect(securityChartsLayout.exists()).toBe(true); expect(securityChartsLayout.exists()).toBe(true);
expect(vulnerabilityChart.props()).toEqual({ query: {}, groupFullPath }); expect(vulnerabilityChart.props()).toEqual({ query: {}, groupFullPath });
expect(vulnerabilitySeverity.props()).toEqual({ expect(vulnerabilitySeverities.exists()).toBe(true);
endpoint: vulnerableProjectsEndpoint, expect(vulnerabilitySeverities.props().groupFullPath).toEqual(groupFullPath);
helpPagePath: '',
});
}); });
}); });
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