Commit 7ebbf830 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '210346-add-counts-to-graphql-dashboard' into 'master'

Add a GraphQL based counts component

Closes #210346

See merge request gitlab-org/gitlab!28414
parents c8557573 d599ea23
<script>
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import VulnerabilitiesCountList from 'ee/security_dashboard/components/vulnerability_count_list.vue';
import ProjectVulnerabilitiesApp from 'ee/vulnerabilities/components/project_vulnerabilities_app.vue';
export default {
components: {
SecurityDashboardLayout,
ProjectVulnerabilitiesApp,
VulnerabilitiesCountList,
},
props: {
dashboardDocumentation: {
......@@ -26,6 +28,9 @@ export default {
<template>
<security-dashboard-layout>
<template #header>
<vulnerabilities-count-list :project-full-path="projectFullPath" />
</template>
<project-vulnerabilities-app
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
......
......@@ -6,7 +6,7 @@ import Filters from './filters.vue';
import SecurityDashboardLayout from './security_dashboard_layout.vue';
import SecurityDashboardTable from './security_dashboard_table.vue';
import VulnerabilityChart from './vulnerability_chart.vue';
import VulnerabilityCountList from './vulnerability_count_list.vue';
import VulnerabilityCountList from './vulnerability_count_list_vuex.vue';
import VulnerabilitySeverity from './vulnerability_severity.vue';
import LoadingError from './loading_error.vue';
......
<script>
import { mapGetters, mapState } from 'vuex';
import VulnerabilityCount from './vulnerability_count.vue';
import { CRITICAL, HIGH, MEDIUM, LOW } from '../store/modules/vulnerabilities/constants';
const SEVERITIES = [CRITICAL, HIGH, MEDIUM, LOW];
import vulnerabilitySeveritiesCountQuery from '../graphql/project_vulnerability_severities_count.graphql';
import VulnerabilityCountListLayout from './vulnerability_count_list_layout.vue';
export default {
components: {
VulnerabilityCount,
VulnerabilityCountListLayout,
},
props: {
projectFullPath: {
type: String,
required: true,
},
},
data: () => ({
queryError: false,
vulnerabilitiesCount: {},
}),
computed: {
...mapGetters('vulnerabilities', ['dashboardCountError', 'dashboardError']),
...mapState('vulnerabilities', ['isLoadingVulnerabilitiesCount', 'vulnerabilitiesCount']),
counts() {
return SEVERITIES.map(severity => {
const count = this.vulnerabilitiesCount[severity] || 0;
return { severity, count };
});
isLoading() {
return this.$apollo.queries.vulnerabilitiesCount.loading;
},
},
apollo: {
vulnerabilitiesCount: {
query: vulnerabilitySeveritiesCountQuery,
variables() {
return { fullPath: this.projectFullPath };
},
update: ({ project }) => project.vulnerabilitySeveritiesCount,
result() {
this.queryError = false;
},
error() {
this.queryError = true;
},
},
},
};
</script>
<template>
<div class="vulnerabilities-count-list mb-5 mt-4">
<div class="flash-container">
<div v-if="dashboardError" class="flash-alert">
<div class="flash-text container-fluid container-limited limit-container-width">
{{
s__(
'Security Dashboard|Error fetching the dashboard data. Please check your network connection and try again.',
)
}}
</div>
</div>
</div>
<div class="row">
<div v-for="count in counts" :key="count.severity" class="col-md col-sm-6 js-count">
<vulnerability-count
:severity="count.severity"
:count="count.count"
:is-loading="isLoadingVulnerabilitiesCount"
/>
</div>
</div>
<div class="flash-container">
<div v-if="dashboardCountError" class="flash-alert">
<div class="flash-text container-fluid container-limited limit-container-width">
{{
s__(
'Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again.',
)
}}
</div>
</div>
</div>
</div>
<vulnerability-count-list-layout
:show-error="queryError"
:is-loading="isLoading"
:vulnerabilities-count="vulnerabilitiesCount"
/>
</template>
<script>
import { GlAlert } from '@gitlab/ui';
import { CRITICAL, HIGH, MEDIUM, LOW } from '../store/modules/vulnerabilities/constants';
import VulnerabilityCount from './vulnerability_count.vue';
const SEVERITIES = [CRITICAL, HIGH, MEDIUM, LOW];
export default {
components: {
VulnerabilityCount,
GlAlert,
},
props: {
showError: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
vulnerabilitiesCount: {
type: Object,
required: true,
},
},
data() {
return {
showAlert: this.showError,
};
},
computed: {
counts() {
return SEVERITIES.map(severity => ({
severity,
count: this.vulnerabilitiesCount[severity] || 0,
}));
},
},
methods: {
onErrorDismiss() {
this.showAlert = false;
},
},
};
</script>
<template>
<div class="vulnerabilities-count-list mb-5 mt-4">
<gl-alert v-if="showAlert" class="mb-4" variant="danger" @dismiss="onErrorDismiss">
{{
s__(
'Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again.',
)
}}
</gl-alert>
<div class="row">
<div v-for="count in counts" :key="count.severity" class="col-md col-sm-6">
<vulnerability-count
:severity="count.severity"
:count="count.count"
:is-loading="isLoading"
/>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapState } from 'vuex';
import VulnerabilityCountListLayout from './vulnerability_count_list_layout.vue';
export default {
components: {
VulnerabilityCountListLayout,
},
computed: {
...mapGetters('vulnerabilities', ['dashboardCountError']),
...mapState('vulnerabilities', ['isLoadingVulnerabilitiesCount', 'vulnerabilitiesCount']),
},
};
</script>
<template>
<vulnerability-count-list-layout
:show-error="dashboardCountError"
:is-loading="isLoadingVulnerabilitiesCount"
:vulnerabilities-count="vulnerabilitiesCount"
/>
</template>
query vulnerabilitySeveritiesCount($fullPath: ID!) {
project(fullPath: $fullPath) {
vulnerabilitySeveritiesCount {
critical
high
low
medium
}
}
}
......@@ -8,7 +8,7 @@ import IssueModal from 'ee/vue_shared/security_reports/components/modal.vue';
import SecurityDashboardTable from 'ee/security_dashboard/components/security_dashboard_table.vue';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import VulnerabilityChart from 'ee/security_dashboard/components/vulnerability_chart.vue';
import VulnerabilityCountList from 'ee/security_dashboard/components/vulnerability_count_list.vue';
import VulnerabilityCountList from 'ee/security_dashboard/components/vulnerability_count_list_vuex.vue';
import VulnerabilitySeverity from 'ee/security_dashboard/components/vulnerability_severity.vue';
import LoadingError from 'ee/security_dashboard/components/loading_error.vue';
......
import { shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
import VulnerabilityCountListLayout from 'ee/security_dashboard/components/vulnerability_count_list_layout.vue';
import VulnerabilityCount from 'ee/security_dashboard/components/vulnerability_count.vue';
describe('Vulnerabilities count list component', () => {
let wrapper;
const findAlert = () => wrapper.find(GlAlert);
const findVulnerability = () => wrapper.findAll(VulnerabilityCount);
const createWrapper = ({ propsData } = {}) => {
return shallowMount(VulnerabilityCountListLayout, {
propsData,
stubs: {
GlAlert,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('when loading', () => {
it('passes the isLoading prop to the counts', () => {
wrapper = createWrapper({ propsData: { isLoading: true, vulnerabilitiesCount: {} } });
findVulnerability().wrappers.forEach(component => {
expect(component.props('isLoading')).toBe(true);
});
});
});
describe('when loaded and has a list of vulnerability counts', () => {
const vulnerabilitiesCount = { critical: 5, medium: 3 };
beforeEach(() => {
wrapper = createWrapper({ propsData: { vulnerabilitiesCount } });
});
it('sets the isLoading prop false and passes it down', () => {
findVulnerability().wrappers.forEach(component => {
expect(component.props('isLoading')).toBe(false);
});
});
it('shows the counts', () => {
const vulnerabilites = findVulnerability();
const critical = vulnerabilites.at(0);
const high = vulnerabilites.at(1);
const medium = vulnerabilites.at(2);
expect(critical.props('severity')).toBe('critical');
expect(critical.props('count')).toBe(5);
expect(high.props('severity')).toBe('high');
expect(high.props('count')).toBe(0);
expect(medium.props('severity')).toBe('medium');
expect(medium.props('count')).toBe(3);
});
});
describe('when loaded and has an error', () => {
it('shows the error message', () => {
wrapper = createWrapper({ propsData: { showError: true, vulnerabilitiesCount: {} } });
expect(findAlert().text()).toBe(
'Error fetching the vulnerability counts. Please check your network connection and try again.',
);
});
});
});
import Vue from 'vue';
import component from 'ee/security_dashboard/components/vulnerability_count_list.vue';
import createStore from 'ee/security_dashboard/store';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
import mockData from '../store/modules/vulnerabilities/data/mock_data_vulnerabilities_count.json';
describe('Vulnerability Count List', () => {
const Component = Vue.extend(component);
const store = createStore();
let vm;
beforeEach(() => {
store.dispatch('vulnerabilities/receiveVulnerabilitiesCountSuccess', { data: mockData });
vm = mountComponentWithStore(Component, { store });
});
import { shallowMount } from '@vue/test-utils';
import VulnerabilityCountList from 'ee/security_dashboard/components/vulnerability_count_list.vue';
import VulnerabilityCountListLayout from 'ee/security_dashboard/components/vulnerability_count_list_layout.vue';
describe('Vulnerabilities count list component', () => {
let wrapper;
const findVulnerabilityLayout = () => wrapper.find(VulnerabilityCountListLayout);
const createWrapper = ({ query } = {}) => {
return shallowMount(VulnerabilityCountList, {
propsData: {
projectFullPath: '/root/security-project',
},
mocks: {
$apollo: { queries: { vulnerabilitiesCount: query } },
},
});
};
afterEach(() => {
vm.$destroy();
resetStore(store);
wrapper.destroy();
});
it('should fetch the counts for each severity', () => {
const firstCount = vm.$el.querySelector('.js-count');
describe('when loading', () => {
it('passes down to the loading indicator', () => {
wrapper = createWrapper({ query: { loading: true } });
expect(findVulnerabilityLayout().props('isLoading')).toBe(true);
});
});
describe('when counts are loaded', () => {
beforeEach(() => {
wrapper = createWrapper({ query: { loading: false } });
wrapper.setData({
vulnerabilitiesCount: {
critical: 5,
high: 3,
low: 19,
},
});
});
expect(firstCount.textContent).toContain('Critical');
expect(firstCount.textContent).toContain(mockData.critical);
it('sets the loading indicator false and passes it down', () => {
expect(findVulnerabilityLayout().props('isLoading')).toBe(false);
});
it('should load the vulnerabilities and pass them down to the layout', () => {
expect(findVulnerabilityLayout().props('vulnerabilitiesCount')).toEqual({
critical: 5,
high: 3,
low: 19,
});
});
});
it('should render a counter for each severity', () => {
expect(vm.$el.querySelectorAll('.js-count')).toHaveLength(vm.counts.length);
describe('when there is an error', () => {
beforeEach(() => {
wrapper = createWrapper({ query: {} });
wrapper.setData({ queryError: true });
});
it('should tell the layout to display an error', () => {
expect(findVulnerabilityLayout().props('showError')).toBe(true);
});
});
});
import { shallowMount } from '@vue/test-utils';
import VulnerabilityCountListVuex from 'ee/security_dashboard/components/vulnerability_count_list_vuex.vue';
import createStore from 'ee/security_dashboard/store';
import VulnerabilityCountListLayout from 'ee/security_dashboard/components/vulnerability_count_list_layout.vue';
import { resetStore } from '../helpers';
import mockData from '../store/modules/vulnerabilities/data/mock_data_vulnerabilities_count.json';
describe('Vulnerability Count List', () => {
const projectFullPath = 'root/security-imports';
const store = createStore();
let wrapper;
const findVulnerabilityCountListLayout = () => wrapper.find(VulnerabilityCountListLayout);
beforeEach(() => {
wrapper = shallowMount(VulnerabilityCountListVuex, {
store,
propsData: {
projectFullPath,
},
});
});
afterEach(() => {
wrapper.destroy();
resetStore(store);
});
it('should pass down the data to the layout', () => {
store.dispatch('vulnerabilities/receiveVulnerabilitiesCountSuccess', { data: mockData });
return wrapper.vm.$nextTick(() => {
const layout = findVulnerabilityCountListLayout();
expect(layout.props('isLoading')).toBe(false);
expect(layout.props('showError')).toBe(false);
expect(layout.props('vulnerabilitiesCount')).toEqual(mockData);
});
});
it('should pass down the loading flag when vulnerabilities are loading', () => {
store.dispatch('vulnerabilities/requestVulnerabilitiesCount');
return wrapper.vm.$nextTick(() => {
const layout = findVulnerabilityCountListLayout();
expect(layout.props('isLoading')).toBe(true);
expect(layout.props('showError')).toBe(false);
expect(layout.props('vulnerabilitiesCount')).toEqual({});
});
});
it('should pass down the error flag when vulnerabilities are loading', () => {
store.dispatch('vulnerabilities/receiveVulnerabilitiesCountError');
return wrapper.vm.$nextTick(() => {
const layout = findVulnerabilityCountListLayout();
expect(layout.props('isLoading')).toBe(false);
expect(layout.props('showError')).toBe(true);
expect(layout.props('vulnerabilitiesCount')).toEqual({});
});
});
});
......@@ -17646,9 +17646,6 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
msgid "Security Dashboard|Error fetching the dashboard data. Please check your network connection and try again."
msgstr ""
msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
......
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