Commit e6516886 authored by Savas Vedova's avatar Savas Vedova

Merge branch '338790-reusable-report-component' into 'master'

Add vulnerability tabs and reusable report component

See merge request gitlab-org/gitlab!74384
parents d87ac214 df0ffd27
<script>
import ReportNotConfiguredGroup from '../shared/empty_states/report_not_configured_group.vue';
import VulnerabilityReportTabs from '../shared/vulnerability_report/vulnerability_report_tabs.vue';
import groupVulnerabilitiesQuery from '../../graphql/queries/group_vulnerabilities.query.graphql';
export default {
components: {
ReportNotConfiguredGroup,
VulnerabilityReportTabs,
},
inject: ['hasProjects'],
groupVulnerabilitiesQuery,
};
</script>
<template>
<report-not-configured-group v-if="!hasProjects" />
<vulnerability-report-tabs
v-else
:query="$options.groupVulnerabilitiesQuery"
show-project-filter
/>
</template>
<script>
import vulnerabilitiesQuery from 'ee/security_dashboard/graphql/queries/group_vulnerabilities.query.graphql';
import VulnerabilityReportHeader from '../shared/vulnerability_report/vulnerability_report_header.vue';
import ReportNotConfiguredGroup from '../shared/empty_states/report_not_configured_group.vue';
import VulnerabilityCounts from '../shared/vulnerability_report/vulnerability_counts.vue';
import VulnerabilityListGraphql from '../shared/vulnerability_report/vulnerability_list_graphql.vue';
import VulnerabilityFilters from '../shared/vulnerability_report/vulnerability_filters.vue';
import { FIELDS, FILTERS } from '../shared/vulnerability_report/constants';
export default {
components: {
VulnerabilityCounts,
VulnerabilityListGraphql,
VulnerabilityFilters,
ReportNotConfiguredGroup,
VulnerabilityReportHeader,
},
inject: ['canAdminVulnerability', 'hasProjects'],
data() {
return {
filters: undefined,
};
},
computed: {
fields() {
return [
// Add the checkbox field if the user can use the bulk select feature.
...[this.canAdminVulnerability ? FIELDS.CHECKBOX : []],
FIELDS.DETECTED,
FIELDS.STATUS,
FIELDS.SEVERITY,
FIELDS.DESCRIPTION,
FIELDS.IDENTIFIER,
FIELDS.TOOL,
FIELDS.ACTIVITY,
];
},
},
methods: {
updateFilters(filters) {
this.filters = filters;
},
},
filtersToShow: [
FILTERS.STATUS,
FILTERS.SEVERITY,
FILTERS.TOOL_SIMPLE,
FILTERS.ACTIVITY,
FILTERS.PROJECT,
],
vulnerabilitiesQuery,
};
</script>
<template>
<report-not-configured-group v-if="!hasProjects" />
<div v-else>
<vulnerability-report-header />
<vulnerability-counts class="gl-mt-7" :filters="filters" />
<vulnerability-filters
:filters="$options.filtersToShow"
class="security-dashboard-filters gl-mt-7"
@filters-changed="updateFilters"
/>
<vulnerability-list-graphql
class="gl-mt-6"
:query="$options.vulnerabilitiesQuery"
:fields="fields"
:filters="filters"
show-project-namespace
/>
</div>
</template>
...@@ -3,10 +3,17 @@ import { ...@@ -3,10 +3,17 @@ import {
stateFilter, stateFilter,
severityFilter, severityFilter,
activityFilter, activityFilter,
simpleScannerFilter, simpleScannerFilterNoClusterImage,
vendorScannerFilter, vendorScannerFilterNoClusterImage,
getProjectFilter, getProjectFilter,
} from 'ee/security_dashboard/helpers'; } from 'ee/security_dashboard/helpers';
import { REPORT_TYPES_NO_CLUSTER_IMAGE } from 'ee/security_dashboard/store/constants';
import { REPORT_TYPE_CLUSTER_IMAGE_SCANNING } from '~/vue_shared/security_reports/constants';
export const REPORT_TAB = {
DEVELOPMENT: 'DEVELOPMENT',
OPERATIONAL: 'OPERATIONAL',
};
export const FIELDS = { export const FIELDS = {
CHECKBOX: { CHECKBOX: {
...@@ -60,7 +67,37 @@ export const FILTERS = { ...@@ -60,7 +67,37 @@ export const FILTERS = {
STATUS: stateFilter, STATUS: stateFilter,
SEVERITY: severityFilter, SEVERITY: severityFilter,
ACTIVITY: activityFilter, ACTIVITY: activityFilter,
TOOL_SIMPLE: simpleScannerFilter, TOOL_SIMPLE: simpleScannerFilterNoClusterImage,
TOOL_VENDOR: vendorScannerFilter, TOOL_VENDOR: vendorScannerFilterNoClusterImage,
PROJECT: getProjectFilter(), PROJECT: getProjectFilter(),
}; };
export const FIELD_PRESETS = {
DEVELOPMENT: [
FIELDS.DETECTED,
FIELDS.STATUS,
FIELDS.SEVERITY,
FIELDS.DESCRIPTION,
FIELDS.IDENTIFIER,
FIELDS.TOOL,
FIELDS.ACTIVITY,
],
OPERATIONAL: [
FIELDS.DETECTED,
FIELDS.STATUS,
FIELDS.SEVERITY,
FIELDS.DESCRIPTION,
FIELDS.CLUSTER,
FIELDS.ACTIVITY,
],
};
export const FILTER_PRESETS = {
DEVELOPMENT: [FILTERS.STATUS, FILTERS.SEVERITY, FILTERS.TOOL_SIMPLE, FILTERS.ACTIVITY],
OPERATIONAL: [FILTERS.STATUS, FILTERS.SEVERITY, FILTERS.ACTIVITY],
};
export const REPORT_TYPE_PRESETS = {
DEVELOPMENT: Object.keys(REPORT_TYPES_NO_CLUSTER_IMAGE).map((type) => type.toUpperCase()),
OPERATIONAL: [REPORT_TYPE_CLUSTER_IMAGE_SCANNING.toUpperCase()],
};
<script>
import VulnerabilityCounts from './vulnerability_counts.vue';
import VulnerabilityListGraphql from './vulnerability_list_graphql.vue';
import VulnerabilityFilters from './vulnerability_filters.vue';
import {
FIELDS,
FILTERS,
FIELD_PRESETS,
FILTER_PRESETS,
REPORT_TAB,
REPORT_TYPE_PRESETS,
} from './constants';
export default {
components: {
VulnerabilityCounts,
VulnerabilityListGraphql,
VulnerabilityFilters,
},
inject: ['canAdminVulnerability'],
props: {
type: {
type: String,
required: true,
},
query: {
type: Object,
required: true,
},
showProjectFilter: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
graphqlFilters: undefined,
};
},
computed: {
filtersToShow() {
return [...FILTER_PRESETS[this.type], ...(this.showProjectFilter ? [FILTERS.PROJECT] : [])];
},
fieldsToShow() {
return [
// Add the checkbox field if the user can use the bulk select feature.
...(this.canAdminVulnerability ? [FIELDS.CHECKBOX] : []),
...FIELD_PRESETS[this.type],
];
},
},
methods: {
updateGraphqlFilters(graphqlFilters) {
this.graphqlFilters = graphqlFilters;
if (this.type === REPORT_TAB.DEVELOPMENT && (graphqlFilters.reportType?.length || 0) <= 0) {
this.graphqlFilters.reportType = REPORT_TYPE_PRESETS.DEVELOPMENT;
} else if (this.type === REPORT_TAB.OPERATIONAL) {
this.graphqlFilters.reportType = REPORT_TYPE_PRESETS.OPERATIONAL;
}
},
},
};
</script>
<template>
<div>
<vulnerability-counts class="gl-mt-7" :filters="graphqlFilters" />
<vulnerability-filters
:filters="filtersToShow"
class="security-dashboard-filters gl-mt-7"
@filters-changed="updateGraphqlFilters"
/>
<vulnerability-list-graphql
class="gl-mt-6"
:query="query"
:fields="fieldsToShow"
:filters="graphqlFilters"
:show-project-namespace="showProjectFilter"
/>
</div>
</template>
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import SurveyRequestBanner from '../survey_request_banner.vue';
import VulnerabilityReportHeader from './vulnerability_report_header.vue';
import VulnerabilityReport from './vulnerability_report.vue';
import { REPORT_TAB } from './constants';
export default {
components: {
GlTabs,
GlTab,
SurveyRequestBanner,
VulnerabilityReportHeader,
VulnerabilityReport,
},
props: {
query: {
type: Object,
required: true,
},
showProjectFilter: {
type: Boolean,
required: false,
default: false,
},
},
i18n: {
operationalTabParameter: REPORT_TAB.OPERATIONAL.toLowerCase(),
developmentTab: s__('SecurityReports|Development vulnerabilities'),
operationalTab: s__('SecurityReports|Operational vulnerabilities'),
},
REPORT_TAB,
};
</script>
<template>
<div>
<survey-request-banner class="gl-mt-5" />
<vulnerability-report-header />
<gl-tabs class="gl-mt-5" sync-active-tab-with-query-params>
<gl-tab :title="$options.i18n.developmentTab" lazy>
<vulnerability-report
:type="$options.REPORT_TAB.DEVELOPMENT"
:query="query"
:show-project-filter="showProjectFilter"
/>
</gl-tab>
<gl-tab
:title="$options.i18n.operationalTab"
:query-param-value="$options.i18n.operationalTabParameter"
lazy
>
<vulnerability-report
:type="$options.REPORT_TAB.OPERATIONAL"
:query="query"
:show-project-filter="showProjectFilter"
/>
</gl-tab>
</gl-tabs>
</div>
</template>
import isPlainObject from 'lodash/isPlainObject'; import isPlainObject from 'lodash/isPlainObject';
import { REPORT_TYPES, SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants'; import {
REPORT_TYPES,
REPORT_TYPES_NO_CLUSTER_IMAGE,
SEVERITY_LEVELS,
} from 'ee/security_dashboard/store/constants';
import { BASE_FILTERS } from 'ee/security_dashboard/store/modules/filters/constants'; import { BASE_FILTERS } from 'ee/security_dashboard/store/modules/filters/constants';
import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type'; import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type';
import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants'; import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants';
...@@ -55,6 +59,14 @@ export const simpleScannerFilter = { ...@@ -55,6 +59,14 @@ export const simpleScannerFilter = {
defaultOptions: [], defaultOptions: [],
}; };
export const simpleScannerFilterNoClusterImage = {
name: s__('SecurityReports|Tool'),
id: 'reportType',
options: parseOptions(REPORT_TYPES_NO_CLUSTER_IMAGE),
allOption: BASE_FILTERS.report_type,
defaultOptions: [],
};
// This is used on the project-level report. It's used by the scanner filter that shows a list of // This is used on the project-level report. It's used by the scanner filter that shows a list of
// scan types (DAST, SAST, etc) that's grouped by vendor. // scan types (DAST, SAST, etc) that's grouped by vendor.
export const vendorScannerFilter = { export const vendorScannerFilter = {
...@@ -65,6 +77,16 @@ export const vendorScannerFilter = { ...@@ -65,6 +77,16 @@ export const vendorScannerFilter = {
defaultOptions: [], defaultOptions: [],
}; };
export const vendorScannerFilterNoClusterImage = {
name: s__('SecurityReports|Tool'),
id: 'scanner',
options: Object.keys(REPORT_TYPES_NO_CLUSTER_IMAGE).map((x) =>
createScannerOption(DEFAULT_SCANNER, x),
),
allOption: BASE_FILTERS.report_type,
defaultOptions: [],
};
export const activityOptions = { export const activityOptions = {
NO_ACTIVITY: { id: 'NO_ACTIVITY', name: s__('SecurityReports|No activity') }, NO_ACTIVITY: { id: 'NO_ACTIVITY', name: s__('SecurityReports|No activity') },
WITH_ISSUES: { id: 'WITH_ISSUES', name: s__('SecurityReports|With issues') }, WITH_ISSUES: { id: 'WITH_ISSUES', name: s__('SecurityReports|With issues') },
......
...@@ -11,9 +11,8 @@ export const SEVERITY_LEVELS = { ...@@ -11,9 +11,8 @@ export const SEVERITY_LEVELS = {
info: s__('severity|Info'), info: s__('severity|Info'),
}; };
export const REPORT_TYPES = { export const REPORT_TYPES_NO_CLUSTER_IMAGE = {
container_scanning: s__('ciReport|Container Scanning'), container_scanning: s__('ciReport|Container Scanning'),
cluster_image_scanning: s__('ciReport|Cluster Image Scanning'),
dast: s__('ciReport|DAST'), dast: s__('ciReport|DAST'),
dependency_scanning: s__('ciReport|Dependency Scanning'), dependency_scanning: s__('ciReport|Dependency Scanning'),
sast: s__('ciReport|SAST'), sast: s__('ciReport|SAST'),
...@@ -22,6 +21,10 @@ export const REPORT_TYPES = { ...@@ -22,6 +21,10 @@ export const REPORT_TYPES = {
api_fuzzing: s__('ciReport|API Fuzzing'), api_fuzzing: s__('ciReport|API Fuzzing'),
}; };
export const REPORT_TYPES = {
...REPORT_TYPES_NO_CLUSTER_IMAGE,
cluster_image_scanning: s__('ciReport|Cluster Image Scanning'),
};
export const DASHBOARD_TYPES = { export const DASHBOARD_TYPES = {
PROJECT: 'project', PROJECT: 'project',
PIPELINE: 'pipeline', PIPELINE: 'pipeline',
......
...@@ -6,7 +6,7 @@ import VulnerabilityReport from './components/shared/vulnerability_report.vue'; ...@@ -6,7 +6,7 @@ import VulnerabilityReport from './components/shared/vulnerability_report.vue';
import apolloProvider from './graphql/provider'; import apolloProvider from './graphql/provider';
import createRouter from './router'; import createRouter from './router';
import createStore from './store'; import createStore from './store';
import GroupVulnerabilityReport from './components/group/vulnerability_report_development.vue'; import GroupVulnerabilityReport from './components/group/group_vulnerability_report.vue';
export default (el, dashboardType) => { export default (el, dashboardType) => {
if (!el) { if (!el) {
......
import { shallowMount } from '@vue/test-utils';
import GroupVulnerabilityReport from 'ee/security_dashboard/components/group/group_vulnerability_report.vue';
import VulnerabilityReportTabs from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_report_tabs.vue';
import ReportNotConfiguredGroup from 'ee/security_dashboard/components/shared/empty_states/report_not_configured_group.vue';
describe('Group vulnerability report component', () => {
let wrapper;
const createWrapper = ({ hasProjects = true } = {}) => {
wrapper = shallowMount(GroupVulnerabilityReport, {
provide: { hasProjects },
});
};
const findReportNotConfiguredGroup = () => wrapper.findComponent(ReportNotConfiguredGroup);
const findVulnerabilityReportTabs = () => wrapper.findComponent(VulnerabilityReportTabs);
afterEach(() => {
wrapper.destroy();
});
it('shows the report not configured component if there are no projects', () => {
createWrapper({ hasProjects: false });
expect(findReportNotConfiguredGroup().exists()).toBe(true);
expect(findVulnerabilityReportTabs().exists()).toBe(false);
});
it('shows the vulnerability report tabs component if there are projects', () => {
createWrapper({ hasProjects: true });
expect(findReportNotConfiguredGroup().exists()).toBe(false);
expect(findVulnerabilityReportTabs().exists()).toBe(true);
});
});
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import VulnerabilityListGraphql from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_list_graphql.vue'; import VulnerabilityListGraphql from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_list_graphql.vue';
import VulnerabilityReportDevelopment from 'ee/security_dashboard/components/group/vulnerability_report_development.vue'; import VulnerabilityReport from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_report.vue';
import VulnerabilityCounts from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_counts.vue'; import VulnerabilityCounts from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_counts.vue';
import VulnerabilityFilters from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_filters.vue'; import VulnerabilityFilters from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_filters.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import projectVulnerabilitiesQuery from 'ee/security_dashboard/graphql/queries/project_vulnerabilities.query.graphql';
import {
FIELD_PRESETS,
FIELDS,
REPORT_TAB,
REPORT_TYPE_PRESETS,
} from 'ee/security_dashboard/components/shared/vulnerability_report/constants';
describe('Vulnerability counts component', () => { describe('Vulnerability report component', () => {
let wrapper; let wrapper;
const createWrapper = () => { const createWrapper = ({
wrapper = shallowMountExtended(VulnerabilityReportDevelopment, { type = REPORT_TAB.DEVELOPMENT,
showProjectFilter = false,
canAdminVulnerability = false,
} = {}) => {
wrapper = shallowMount(VulnerabilityReport, {
propsData: {
type,
query: projectVulnerabilitiesQuery,
showProjectFilter,
},
provide: { provide: {
canAdminVulnerability: true, canAdminVulnerability,
hasProjects: true,
}, },
}); });
}; };
...@@ -30,11 +45,65 @@ describe('Vulnerability counts component', () => { ...@@ -30,11 +45,65 @@ describe('Vulnerability counts component', () => {
createWrapper(); createWrapper();
const data = { a: 1 }; const data = { a: 1 };
findVulnerabilityFilters().vm.$emit('filters-changed', data); findVulnerabilityFilters().vm.$emit('filters-changed', data);
await nextTick(); await nextTick();
expect(findVulnerabilityCounts().props('filters')).toBe(data); expect(findVulnerabilityCounts().props('filters')).toBe(data);
expect(findVulnerabilityListGraphql().props('filters')).toBe(data); expect(findVulnerabilityListGraphql().props('filters')).toBe(data);
}); });
it('will filter by everything except cluster image scanning results for the development report', async () => {
createWrapper({ type: REPORT_TAB.DEVELOPMENT });
findVulnerabilityFilters().vm.$emit('filters-changed', {});
await nextTick();
expect(findVulnerabilityListGraphql().props('filters').reportType).toBe(
REPORT_TYPE_PRESETS.DEVELOPMENT,
);
});
it('will filter by cluster image scanning results for the operational report', async () => {
createWrapper({ type: REPORT_TAB.OPERATIONAL });
findVulnerabilityFilters().vm.$emit('filters-changed', {});
await nextTick();
expect(findVulnerabilityListGraphql().props('filters').reportType).toBe(
REPORT_TYPE_PRESETS.OPERATIONAL,
);
});
});
describe('vulnerability list GraphQL component', () => {
it('gets passed the query prop', () => {
createWrapper();
expect(findVulnerabilityListGraphql().props('query')).toBe(projectVulnerabilitiesQuery);
});
it.each`
type | expectedFields
${REPORT_TAB.DEVELOPMENT} | ${FIELD_PRESETS.DEVELOPMENT}
${REPORT_TAB.OPERATIONAL} | ${FIELD_PRESETS.OPERATIONAL}
`('gets passed the expected fields prop for the $type report', ({ type, expectedFields }) => {
createWrapper({ type });
expect(findVulnerabilityListGraphql().props('fields')).toEqual(expectedFields);
});
it('gets passed the checkbox field if the user can admin vulnerability', () => {
createWrapper({ canAdminVulnerability: true });
expect(findVulnerabilityListGraphql().props('fields')).toContainEqual(FIELDS.CHECKBOX);
});
it.each([true, false])(
'gets passed the expected value for the should show project namespace prop',
(showProjectFilter) => {
createWrapper({ showProjectFilter });
expect(findVulnerabilityListGraphql().props('showProjectNamespace')).toBe(
showProjectFilter,
);
},
);
}); });
}); });
import { shallowMount } from '@vue/test-utils';
import { GlTabs, GlTab } from '@gitlab/ui';
import VulnerabilityReportTabs from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_report_tabs.vue';
import VulnerabilityReport from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_report.vue';
import SurveyRequestBanner from 'ee/security_dashboard/components/shared/survey_request_banner.vue';
import projectVulnerabilitiesQuery from 'ee/security_dashboard/graphql/queries/project_vulnerabilities.query.graphql';
describe('Vulnerability report tabs component', () => {
let wrapper;
const createWrapper = ({ showProjectFilter = false } = {}) => {
wrapper = shallowMount(VulnerabilityReportTabs, {
propsData: {
query: projectVulnerabilitiesQuery,
showProjectFilter,
},
});
};
const findVulnerabilityReports = () => wrapper.findAllComponents(VulnerabilityReport);
afterEach(() => {
wrapper.destroy();
});
describe('survey request banner', () => {
it('shows the survey request banner', () => {
createWrapper();
expect(wrapper.findComponent(SurveyRequestBanner).exists()).toBe(true);
});
});
describe('tabs', () => {
it('renders 2 tabs', () => {
createWrapper();
expect(wrapper.findComponent(GlTabs).exists()).toBe(true);
const tabs = wrapper.findAllComponents(GlTab);
expect(tabs).toHaveLength(2);
expect(tabs.at(0).attributes('title')).toBe('Development vulnerabilities');
expect(tabs.at(1).attributes('title')).toBe('Operational vulnerabilities');
});
});
describe('vulnerability report components', () => {
it('gets passed the query prop', () => {
createWrapper();
const reports = findVulnerabilityReports();
reports.wrappers.forEach((report) => {
expect(report.props('query')).toBe(projectVulnerabilitiesQuery);
});
});
it.each([true, false])('gets passed %s for the showProjectFilter prop', (showProjectFilter) => {
createWrapper({ showProjectFilter });
const reports = findVulnerabilityReports();
reports.wrappers.forEach((report) => {
expect(report.props('showProjectFilter')).toBe(showProjectFilter);
});
});
});
});
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