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> <script>
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue'; 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'; import ProjectVulnerabilitiesApp from 'ee/vulnerabilities/components/project_vulnerabilities_app.vue';
export default { export default {
components: { components: {
SecurityDashboardLayout, SecurityDashboardLayout,
ProjectVulnerabilitiesApp, ProjectVulnerabilitiesApp,
VulnerabilitiesCountList,
}, },
props: { props: {
dashboardDocumentation: { dashboardDocumentation: {
...@@ -26,6 +28,9 @@ export default { ...@@ -26,6 +28,9 @@ export default {
<template> <template>
<security-dashboard-layout> <security-dashboard-layout>
<template #header>
<vulnerabilities-count-list :project-full-path="projectFullPath" />
</template>
<project-vulnerabilities-app <project-vulnerabilities-app
:dashboard-documentation="dashboardDocumentation" :dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath" :empty-state-svg-path="emptyStateSvgPath"
......
...@@ -6,7 +6,7 @@ import Filters from './filters.vue'; ...@@ -6,7 +6,7 @@ import Filters from './filters.vue';
import SecurityDashboardLayout from './security_dashboard_layout.vue'; import SecurityDashboardLayout from './security_dashboard_layout.vue';
import SecurityDashboardTable from './security_dashboard_table.vue'; import SecurityDashboardTable from './security_dashboard_table.vue';
import VulnerabilityChart from './vulnerability_chart.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 VulnerabilitySeverity from './vulnerability_severity.vue';
import LoadingError from './loading_error.vue'; import LoadingError from './loading_error.vue';
......
<script> <script>
import { mapGetters, mapState } from 'vuex'; import vulnerabilitySeveritiesCountQuery from '../graphql/project_vulnerability_severities_count.graphql';
import VulnerabilityCount from './vulnerability_count.vue'; import VulnerabilityCountListLayout from './vulnerability_count_list_layout.vue';
import { CRITICAL, HIGH, MEDIUM, LOW } from '../store/modules/vulnerabilities/constants';
const SEVERITIES = [CRITICAL, HIGH, MEDIUM, LOW];
export default { export default {
components: { components: {
VulnerabilityCount, VulnerabilityCountListLayout,
},
props: {
projectFullPath: {
type: String,
required: true,
},
}, },
data: () => ({
queryError: false,
vulnerabilitiesCount: {},
}),
computed: { computed: {
...mapGetters('vulnerabilities', ['dashboardCountError', 'dashboardError']), isLoading() {
...mapState('vulnerabilities', ['isLoadingVulnerabilitiesCount', 'vulnerabilitiesCount']), return this.$apollo.queries.vulnerabilitiesCount.loading;
counts() { },
return SEVERITIES.map(severity => { },
const count = this.vulnerabilitiesCount[severity] || 0; apollo: {
return { severity, count }; vulnerabilitiesCount: {
}); query: vulnerabilitySeveritiesCountQuery,
variables() {
return { fullPath: this.projectFullPath };
},
update: ({ project }) => project.vulnerabilitySeveritiesCount,
result() {
this.queryError = false;
},
error() {
this.queryError = true;
},
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="vulnerabilities-count-list mb-5 mt-4"> <vulnerability-count-list-layout
<div class="flash-container"> :show-error="queryError"
<div v-if="dashboardError" class="flash-alert"> :is-loading="isLoading"
<div class="flash-text container-fluid container-limited limit-container-width"> :vulnerabilities-count="vulnerabilitiesCount"
{{ />
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>
</template> </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'; ...@@ -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 SecurityDashboardTable from 'ee/security_dashboard/components/security_dashboard_table.vue';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue'; import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import VulnerabilityChart from 'ee/security_dashboard/components/vulnerability_chart.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 VulnerabilitySeverity from 'ee/security_dashboard/components/vulnerability_severity.vue';
import LoadingError from 'ee/security_dashboard/components/loading_error.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 { shallowMount } from '@vue/test-utils';
import component from 'ee/security_dashboard/components/vulnerability_count_list.vue'; import VulnerabilityCountList from 'ee/security_dashboard/components/vulnerability_count_list.vue';
import createStore from 'ee/security_dashboard/store'; import VulnerabilityCountListLayout from 'ee/security_dashboard/components/vulnerability_count_list_layout.vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import { resetStore } from '../helpers'; describe('Vulnerabilities count list component', () => {
import mockData from '../store/modules/vulnerabilities/data/mock_data_vulnerabilities_count.json'; let wrapper;
describe('Vulnerability Count List', () => { const findVulnerabilityLayout = () => wrapper.find(VulnerabilityCountListLayout);
const Component = Vue.extend(component);
const store = createStore(); const createWrapper = ({ query } = {}) => {
let vm; return shallowMount(VulnerabilityCountList, {
propsData: {
beforeEach(() => { projectFullPath: '/root/security-project',
store.dispatch('vulnerabilities/receiveVulnerabilitiesCountSuccess', { data: mockData }); },
vm = mountComponentWithStore(Component, { store }); mocks: {
}); $apollo: { queries: { vulnerabilitiesCount: query } },
},
});
};
afterEach(() => { afterEach(() => {
vm.$destroy(); wrapper.destroy();
resetStore(store);
}); });
it('should fetch the counts for each severity', () => { describe('when loading', () => {
const firstCount = vm.$el.querySelector('.js-count'); 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'); it('sets the loading indicator false and passes it down', () => {
expect(firstCount.textContent).toContain(mockData.critical); 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', () => { describe('when there is an error', () => {
expect(vm.$el.querySelectorAll('.js-count')).toHaveLength(vm.counts.length); 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 "" ...@@ -17646,9 +17646,6 @@ msgstr ""
msgid "Security Dashboard" msgid "Security Dashboard"
msgstr "" 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." msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr "" 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