Commit 586f688a authored by samdbeckham's avatar samdbeckham Committed by Kamil Trzciński

Connects the FE and API for the security dashboard

- Adds actions to get the endpoints from the BE
- Adds Error states
- Adds Popover explanations for upcoming features
- Adds Dismissed issue styling
- Adds Feedbacked issue styling
- Updates the dummy data
- Updates some of the data structures to account for changes
parent 98821eb1
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapActions, mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
import Tabs from '~/vue_shared/components/tabs/tabs';
import Tab from '~/vue_shared/components/tabs/tab.vue';
import SecurityDashboardTable from './security_dashboard_table.vue';
import VulnerabilityCountList from './vulnerability_count_list.vue';
import SvgBlankState from '~/pipelines/components/blank_state.vue';
import Icon from '~/vue_shared/components/icon.vue';
import popover from '~/vue_shared/directives/popover';
export default {
name: 'SecurityDashboardApp',
directives: {
popover,
},
components: {
Tabs,
Tab,
Icon,
SecurityDashboardTable,
SvgBlankState,
Tab,
Tabs,
VulnerabilityCountList,
},
props: {
dashboardDocumentation: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
errorStateSvgPath: {
type: String,
required: true,
},
vulnerabilitiesEndpoint: {
type: String,
required: true,
},
vulnerabilitiesCountEndpoint: {
type: String,
required: true,
},
},
computed: {
...mapGetters('vulnerabilities', ['vulnerabilitiesCountByReportType']),
...mapState('vulnerabilities', ['hasError']),
sastCount() {
return this.vulnerabilitiesCountByReportType('sast');
},
popoverOptions() {
return {
trigger: 'click',
placement: 'right',
title: s__(
'Security Reports|At this time, the security dashboard only supports SAST. More analyzers are coming soon.',
),
content: `
<a
title="${s__('Security Reports|Security Dashboard Roadmap')}"
href="${this.dashboardDocumentation}"
target="_blank"
rel="noopener
noreferrer"
>
<span class="vertical-align-middle">${s__(
'Security Reports|Security Dashboard Roadmap',
)}</span>
${gl.utils.spriteIcon('external-link', 's16 vertical-align-middle')}
</a>
`,
html: true,
};
},
},
created() {
this.setVulnerabilitiesEndpoint(this.vulnerabilitiesEndpoint);
this.setVulnerabilitiesCountEndpoint(this.vulnerabilitiesCountEndpoint);
this.fetchVulnerabilitiesCount();
},
methods: {
...mapActions('vulnerabilities', ['fetchVulnerabilitiesCount']),
...mapActions('vulnerabilities', [
'setVulnerabilitiesCountEndpoint',
'setVulnerabilitiesEndpoint',
'fetchVulnerabilitiesCount',
]),
},
};
</script>
<template>
<div>
<svg-blank-state
v-if="hasError"
:svg-path="errorStateSvgPath"
:message="s__(`Security Reports|There was an error fetching the dashboard.
Please try again in a few moments or contact your support team.`)"
/>
<div v-else>
<vulnerability-count-list />
<tabs stop-propagation>
<tab active>
<template slot="title">
{{ __('SAST') }}
<span>{{ __('SAST') }}</span>
<span
v-if="sastCount"
class="badge badge-pill">
class="badge badge-pill"
>
{{ sastCount }}
</span>
<span
v-popover="popoverOptions"
class="text-muted ml-1"
>
<icon
name="question"
class="vertical-align-middle"
/>
</span>
</template>
<security-dashboard-table/>
<security-dashboard-table
:empty-state-svg-path="emptyStateSvgPath"
/>
</tab>
</tabs>
</div>
</div>
</template>
......@@ -2,6 +2,7 @@
import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import SecurityDashboardActionButtons from './security_dashboard_action_buttons.vue';
import VulnerabilityIssueLink from './vulnerability_issue_link.vue';
export default {
name: 'SecurityDashboardTableRow',
......@@ -9,6 +10,7 @@ export default {
SeverityBadge,
SecurityDashboardActionButtons,
SkeletonLoading,
VulnerabilityIssueLink,
},
props: {
vulnerability: {
......@@ -31,7 +33,13 @@ export default {
},
projectNamespace() {
const { project } = this.vulnerability;
return project && project.name_with_namespace ? project.name_with_namespace : null;
return project && project.full_name ? project.full_name : null;
},
isDismissed() {
return this.vulnerability.dismissal_feedback;
},
hasIssue() {
return this.vulnerability.issue_feedback;
},
},
};
......@@ -65,7 +73,13 @@ export default {
:lines="2"
/>
<div v-else>
<span>{{ vulnerability.description }}</span>
<strike v-if="isDismissed">{{ vulnerability.name }}</strike>
<span v-else>{{ vulnerability.name }}</span>
<vulnerability-issue-link
v-if="hasIssue"
:issue="vulnerability.issue_feedback"
:project-name="vulnerability.project.name"
/>
<br />
<span
v-if="projectNamespace"
......@@ -84,7 +98,8 @@ export default {
{{ s__('Reports|Confidence') }}
</div>
<div class="table-mobile-content text-capitalize">
{{ confidence }}
<strike v-if="isDismissed">{{ confidence }}</strike>
<span v-else>{{ confidence }}</span>
</div>
</div>
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'VulnerabilityIssueLink',
components: {
Icon,
},
directives: {
Tooltip,
},
props: {
issue: {
type: Object,
required: true,
},
projectName: {
type: String,
required: true,
},
},
computed: {
linkText() {
return `${this.projectName}#${this.issue.issue_id}`;
},
},
};
</script>
<template>
<div class="d-inline">
<icon
v-tooltip
name="issues"
css-classes="text-success vertical-align-middle"
:title="s__('Security Dashboard|Issue Created')"
/>
<a
:href="issue.issue_url"
>{{ linkText }}</a>
</div>
</template>
......@@ -12,7 +12,15 @@ export default () => {
GroupSecurityDashboardApp,
},
render(createElement) {
return createElement('group-security-dashboard-app');
return createElement('group-security-dashboard-app', {
props: {
dashboardDocumentation: el.dataset.dashboardDocumentation,
errorStateSvgPath: el.dataset.errorStateSvgPath,
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
vulnerabilitiesEndpoint: el.dataset.vulnerabilitiesEndpoint,
vulnerabilitiesCountEndpoint: el.dataset.vulnerabilitiesSummaryEndpoint,
},
});
},
});
};
......@@ -2,6 +2,14 @@ import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
export const setVulnerabilitiesEndpoint = ({ commit }, endpoint) => {
commit(types.SET_VULNERABILITIES_ENDPOINT, endpoint);
};
export const setVulnerabilitiesCountEndpoint = ({ commit }, endpoint) => {
commit(types.SET_VULNERABILITIES_COUNT_ENDPOINT, endpoint);
};
export const fetchVulnerabilitiesCount = ({ state, dispatch }) => {
dispatch('requestVulnerabilitiesCount');
......
export const SET_VULNERABILITIES_ENDPOINT = 'SET_VULNERABILITIES_ENDPOINT';
export const REQUEST_VULNERABILITIES = 'REQUEST_VULNERABILITIES';
export const RECEIVE_VULNERABILITIES_SUCCESS = 'RECEIVE_VULNERABILITIES_SUCCESS';
export const RECEIVE_VULNERABILITIES_ERROR = 'RECEIVE_VULNERABILITIES_ERROR';
export const SET_VULNERABILITIES_COUNT_ENDPOINT = 'SET_VULNERABILITIES_COUNT_ENDPOINT';
export const REQUEST_VULNERABILITIES_COUNT = 'REQUEST_VULNERABILITIES_COUNT';
export const RECEIVE_VULNERABILITIES_COUNT_SUCCESS = 'RECEIVE_VULNERABILITIES_COUNT_SUCCESS';
export const RECEIVE_VULNERABILITIES_COUNT_ERROR = 'RECEIVE_VULNERABILITIES_COUNT_ERROR';
import * as types from './mutation_types';
export default {
[types.SET_VULNERABILITIES_ENDPOINT](state, payload) {
state.vulnerabilitiesEndpoint = payload;
},
[types.REQUEST_VULNERABILITIES](state) {
state.isLoadingVulnerabilities = true;
state.hasError = false;
},
[types.RECEIVE_VULNERABILITIES_SUCCESS](state, payload) {
state.isLoadingVulnerabilities = false;
state.errorLoadingVulnerabilities = false;
state.pageInfo = payload.pageInfo;
state.vulnerabilities = payload.vulnerabilities;
},
[types.RECEIVE_VULNERABILITIES_ERROR](state) {
state.isLoadingVulnerabilities = false;
state.errorLoadingVulnerabilities = true;
state.hasError = true;
},
[types.SET_VULNERABILITIES_COUNT_ENDPOINT](state, payload) {
state.vulnerabilitiesCountEndpoint = payload;
},
[types.REQUEST_VULNERABILITIES_COUNT](state) {
state.isLoadingVulnerabilitiesCount = true;
state.hasError = false;
},
[types.RECEIVE_VULNERABILITIES_COUNT_SUCCESS](state, payload) {
state.isLoadingVulnerabilitiesCount = false;
state.errorLoadingVulnerabilities = false;
state.vulnerabilitiesCount = payload;
},
[types.RECEIVE_VULNERABILITIES_COUNT_ERROR](state) {
state.isLoadingVulnerabilitiesCount = false;
state.errorLoadingVulnerabilities = true;
state.hasError = true;
},
};
export default () => ({
isLoadingVulnerabilities: false,
isLoadingVulnerabilitiesCount: false,
hasError: false,
isLoadingVulnerabilities: true,
isLoadingVulnerabilitiesCount: true,
pageInfo: {},
vulnerabilities: [],
vulnerabilitiesCount: {},
errorLoadingVulnerabilities: false,
vulnerabilitiesCountEndpoint: null,
vulnerabilitiesEndpoint: null,
});
- breadcrumb_title _("Security Dashboard")
- page_title _("Security Dashboard")
#js-group-security-dashboard
#js-group-security-dashboard{ data: { vulnerabilities_endpoint: group_security_vulnerabilities_path(@group),
vulnerabilities_summary_endpoint: summary_group_security_vulnerabilities_path(@group),
dashboard_documentation: help_page_path('user/group/security_dashboard'),
empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
error_state_svg_path: image_path('illustrations/security-dashboard-api-error-empty-state.svg') } }
---
title: Connects the Group Security Dashboard API and Frontend
merge_request: 7793
author:
type: other
......@@ -36,9 +36,9 @@ describe('Security Dashboard Table Row', () => {
beforeEach(() => {
const vulnerability = {
severity: 'high',
description: 'Test vulnerability',
name: 'Test vulnerability',
confidence: 'medium',
project: { name_with_namespace: 'project name' },
project: { full_name: 'project name' },
};
props = { vulnerability };
......@@ -55,15 +55,15 @@ describe('Security Dashboard Table Row', () => {
);
});
it('should render the description', () => {
it('should render the name', () => {
expect(vm.$el.querySelectorAll('.table-mobile-content')[1].textContent).toContain(
props.vulnerability.description,
props.vulnerability.name,
);
});
it('should render the project namespace', () => {
expect(vm.$el.querySelectorAll('.table-mobile-content')[1].textContent).toContain(
props.vulnerability.project.name_with_namespace,
props.vulnerability.project.full_name,
);
});
......
import Vue from 'vue';
import component from 'ee/security_dashboard/components/vulnerability_issue_link.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Vulnerability Issue Link component', () => {
const Component = Vue.extend(component);
let vm;
let props;
beforeEach(() => {
const issue = {
issue_id: 1,
issue_url: 'https://gitlab.com',
};
const projectName = 'Project Name';
props = { issue, projectName };
vm = mountComponent(Component, props);
});
afterEach(() => {
vm.$destroy();
});
it('should render the severity label', () => {
expect(vm.$el.textContent).toContain(`${props.projectName}#${props.issue.issue_id}`);
});
it('should link to the issue', () => {
const link = vm.$el.querySelector('a');
expect(link.href).toMatch(props.issue.issue_url);
});
});
......@@ -234,4 +234,46 @@ describe('vulnerabilities actions', () => {
);
});
});
describe('setVulnerabilitiesEndpoint', () => {
it('should commit the correct mutuation', done => {
const state = initialState;
const endpoint = 'fakepath.json';
testAction(
actions.setVulnerabilitiesEndpoint,
endpoint,
state,
[
{
type: types.SET_VULNERABILITIES_ENDPOINT,
payload: endpoint,
},
],
[],
done,
);
});
});
describe('setVulnerabilitiesCountEndpoint', () => {
it('should commit the correct mutuation', done => {
const state = initialState;
const endpoint = 'fakepath.json';
testAction(
actions.setVulnerabilitiesCountEndpoint,
endpoint,
state,
[
{
type: types.SET_VULNERABILITIES_COUNT_ENDPOINT,
payload: endpoint,
},
],
[],
done,
);
});
});
});
......@@ -3,14 +3,35 @@ import * as types from 'ee/security_dashboard/store/modules/vulnerabilities/muta
import mutations from 'ee/security_dashboard/store/modules/vulnerabilities/mutations';
describe('vulnerabilities module mutations', () => {
describe('REQUEST_VULNERABILITIES', () => {
it('should set `isLoadingVulnerabilities` to `true`', () => {
describe('SET_VULNERABILITIES_ENDPOINT', () => {
it('should set `vulnerabilitiesEndpoint` to `fakepath.json`', () => {
const state = initialState;
const endpoint = 'fakepath.json';
mutations[types.SET_VULNERABILITIES_ENDPOINT](state, endpoint);
expect(state.vulnerabilitiesEndpoint).toEqual(endpoint);
});
});
describe('REQUEST_VULNERABILITIES', () => {
let state;
beforeEach(() => {
state = {
...initialState,
hasError: true,
};
mutations[types.REQUEST_VULNERABILITIES](state);
});
it('should set `isLoadingVulnerabilities` to `true`', () => {
expect(state.isLoadingVulnerabilities).toBeTruthy();
});
it('should set `hasError` to `false`', () => {
expect(state.hasError).toBeFalsy();
});
});
describe('RECEIVE_VULNERABILITIES_SUCCESS', () => {
......@@ -30,10 +51,6 @@ describe('vulnerabilities module mutations', () => {
expect(state.isLoadingVulnerabilities).toBeFalsy();
});
it('should set `errorLoadingData` to `false`', () => {
expect(state.errorLoadingData).toBeFalsy();
});
it('should set `pageInfo`', () => {
expect(state.pageInfo).toBe(payload.pageInfo);
});
......@@ -53,14 +70,35 @@ describe('vulnerabilities module mutations', () => {
});
});
describe('REQUEST_VULNERABILITIES_COUNT', () => {
it('should set `isLoadingVulnerabilitiesCount` to `true`', () => {
describe('SET_VULNERABILITIES_COUNT_ENDPOINT', () => {
it('should set `vulnerabilitiesCountEndpoint` to `fakepath.json`', () => {
const state = initialState;
const endpoint = 'fakepath.json';
mutations[types.SET_VULNERABILITIES_COUNT_ENDPOINT](state, endpoint);
expect(state.vulnerabilitiesCountEndpoint).toEqual(endpoint);
});
});
describe('REQUEST_VULNERABILITIES_COUNT', () => {
let state;
beforeEach(() => {
state = {
...initialState,
hasError: true,
};
mutations[types.REQUEST_VULNERABILITIES_COUNT](state);
});
it('should set `isLoadingVulnerabilitiesCount` to `true`', () => {
expect(state.isLoadingVulnerabilitiesCount).toBeTruthy();
});
it('should set `hasError` to `false`', () => {
expect(state.hasError).toBeFalsy();
});
});
describe('RECEIVE_VULNERABILITIES_COUNT_SUCCESS', () => {
......@@ -77,10 +115,6 @@ describe('vulnerabilities module mutations', () => {
expect(state.isLoadingVulnerabilitiesCount).toBeFalsy();
});
it('should set `errorLoadingData` to `false`', () => {
expect(state.errorLoadingData).toBeFalsy();
});
it('should set `vulnerabilitiesCount`', () => {
expect(state.vulnerabilitiesCount).toBe(payload);
});
......
......@@ -6868,6 +6868,18 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
msgid "Security Dashboard|Issue Created"
msgstr ""
msgid "Security Reports|At this time, the security dashboard only supports SAST. More analyzers are coming soon."
msgstr ""
msgid "Security Reports|Security Dashboard Roadmap"
msgstr ""
msgid "Security Reports|There was an error fetching the dashboard. Please try again in a few moments or contact your support team."
msgstr ""
msgid "SecurityDashboard| The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
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