Commit 9041c27c authored by Sam Beckham's avatar Sam Beckham Committed by Phil Hughes

Add modals and actions for the vulnerabilities in the Group security dashboard

parent c3a5223b
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
import { spriteIcon } from '~/lib/utils/common_utils';
import Tabs from '~/vue_shared/components/tabs/tabs';
import Tab from '~/vue_shared/components/tabs/tab.vue';
import IssueModal from 'ee/vue_shared/security_reports/components/modal.vue';
import SecurityDashboardTable from './security_dashboard_table.vue';
import VulnerabilityCountList from './vulnerability_count_list.vue';
import SvgBlankState from '~/pipelines/components/blank_state.vue';
......@@ -16,6 +18,7 @@ export default {
},
components: {
Icon,
IssueModal,
SecurityDashboardTable,
SvgBlankState,
Tab,
......@@ -46,7 +49,7 @@ export default {
},
computed: {
...mapGetters('vulnerabilities', ['vulnerabilitiesCountByReportType']),
...mapState('vulnerabilities', ['hasError']),
...mapState('vulnerabilities', ['hasError', 'modal']),
sastCount() {
return this.vulnerabilitiesCountByReportType('sast');
},
......@@ -66,7 +69,7 @@ export default {
<span class="vertical-align-middle">${s__(
'Security Reports|Security Dashboard Documentation',
)}</span>
${gl.utils.spriteIcon('external-link', 's16 vertical-align-middle')}
${spriteIcon('external-link', 's16 vertical-align-middle')}
</a>
`,
html: true,
......@@ -83,6 +86,9 @@ export default {
'setVulnerabilitiesCountEndpoint',
'setVulnerabilitiesEndpoint',
'fetchVulnerabilitiesCount',
'createIssue',
'dismissVulnerability',
'undoDismissal',
]),
},
};
......@@ -90,6 +96,7 @@ export default {
<template>
<div>
<div class="flash-container"></div>
<svg-blank-state
v-if="hasError"
:svg-path="errorStateSvgPath"
......@@ -110,7 +117,8 @@ export default {
</span>
<span
v-popover="popoverOptions"
class="text-muted ml-1"
class="text-muted prepend-left-4"
:aria-label="__('help')"
>
<icon
name="question"
......@@ -124,6 +132,14 @@ export default {
/>
</tab>
</tabs>
<issue-modal
:modal="modal"
:can-create-issue-permission="true"
:can-create-feedback-permission="true"
@createNewIssue="createIssue({ vulnerability: modal.vulnerability })"
@dismissIssue="dismissVulnerability({ vulnerability: modal.vulnerability })"
@revertDismissIssue="undoDismissal({ vulnerability: modal.vulnerability })"
/>
</div>
</div>
</template>
<script>
import Icon from '~/vue_shared/components/icon.vue';
export default {
name: 'SecurityDashboardActionButtons',
components: {
Icon,
},
props: {
vulnerability: {
type: Object,
required: true,
},
},
methods: {
openModal() {
// TODO: Open the modal
},
newIssue() {
this.$store.dispatch('newIssue', this.vulnerability);
},
dismissVulnerability() {
this.$store.dispatch('dismissVulnerability', this.vulnerability);
},
},
};
</script>
<template>
<div>
<button
:aria-label="s__('Reports|More info')"
class="btn btn-secondary js-more-info"
type="button"
@click="openModal()"
>
<icon
name="external-link"
/>
</button>
<button
:aria-label="s__('Reports|New Issue')"
class="btn btn-inverted btn-info js-new-issue"
type="button"
@click="newIssue()"
>
<icon
name="issue-new"
/>
</button>
<button
:aria-label="s__('Reports|Dismiss Vulnerability')"
class="btn btn-inverted btn-remove js-dismiss-vulnerability"
type="button"
@click="dismissVulnerability()"
>
<icon
name="cancel"
/>
</button>
</div>
</template>
......@@ -19,7 +19,7 @@ export default {
this.fetchVulnerabilities();
},
methods: {
...mapActions('vulnerabilities', ['fetchVulnerabilities']),
...mapActions('vulnerabilities', ['fetchVulnerabilities', 'openModal']),
},
};
</script>
......@@ -63,6 +63,7 @@ export default {
v-for="vulnerability in vulnerabilities"
:key="vulnerability.id"
:vulnerability="vulnerability"
@openModal="openModal({ vulnerability })"
/>
<pagination
......
<script>
import { mapActions } from 'vuex';
import { GlSkeletonLoading } 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 VulnerabilityActionButtons from './vulnerability_action_buttons.vue';
import VulnerabilityIssueLink from './vulnerability_issue_link.vue';
export default {
name: 'SecurityDashboardTableRow',
components: {
SeverityBadge,
SecurityDashboardActionButtons,
GlSkeletonLoading,
VulnerabilityActionButtons,
VulnerabilityIssueLink,
},
props: {
......@@ -31,17 +32,26 @@ export default {
severity() {
return this.vulnerability.severity || ' ';
},
projectNamespace() {
projectFullName() {
const { project } = this.vulnerability;
return project && project.full_name ? project.full_name : null;
return project && project.full_name;
},
isDismissed() {
return this.vulnerability.dismissal_feedback;
return Boolean(this.vulnerability.dismissal_feedback);
},
hasIssue() {
return this.vulnerability.issue_feedback;
return Boolean(this.vulnerability.issue_feedback);
},
canDismissVulnerability() {
return Boolean(this.vulnerability.vulnerability_feedback_url);
},
canCreateIssue() {
return this.canDismissVulnerability && !this.hasIssue;
},
},
methods: {
...mapActions('vulnerabilities', ['openModal']),
},
};
</script>
......@@ -73,8 +83,11 @@ export default {
:lines="2"
/>
<div v-else>
<strike v-if="isDismissed">{{ vulnerability.name }}</strike>
<span v-else>{{ vulnerability.name }}</span>
<span
class="js-vulnerability-info"
:class="{ strikethrough: isDismissed }"
@click="openModal({ vulnerability })"
>{{ vulnerability.name }}</span>
<vulnerability-issue-link
v-if="hasIssue"
:issue="vulnerability.issue_feedback"
......@@ -82,9 +95,9 @@ export default {
/>
<br />
<span
v-if="projectNamespace"
v-if="projectFullName"
class="vulnerability-namespace">
{{ projectNamespace }}
{{ projectFullName }}
</span>
</div>
</div>
......@@ -98,12 +111,10 @@ export default {
{{ s__('Reports|Confidence') }}
</div>
<div class="table-mobile-content text-capitalize">
<strike v-if="isDismissed">{{ confidence }}</strike>
<span v-else>{{ confidence }}</span>
<span :class="{ strikethrough: isDismissed }">{{ confidence }}</span>
</div>
</div>
<!-- This is hidden till we can hook up the actions
<div class="table-section section-20">
<div
class="table-mobile-header"
......@@ -112,12 +123,14 @@ export default {
{{ s__('Reports|Actions') }}
</div>
<div class="table-mobile-content vulnerabilities-action-buttons">
<security-dashboard-action-buttons
<vulnerability-action-buttons
:vulnerability="vulnerability"
:can-create-issue="canCreateIssue"
:can-dismiss-vulnerability="canDismissVulnerability"
:is-dismissed="isDismissed"
/>
</div>
</div>
-->
</div>
</template>
......
<script>
import { mapActions, mapState } from 'vuex';
import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
name: 'SecurityDashboardActionButtons',
components: {
Icon,
LoadingButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
vulnerability: {
type: Object,
required: true,
},
canCreateIssue: {
type: Boolean,
required: false,
default: false,
},
canDismissVulnerability: {
type: Boolean,
required: false,
default: false,
},
isDismissed: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState('vulnerabilities', ['isCreatingIssue', 'isDismissingVulnerability']),
},
methods: {
...mapActions('vulnerabilities', [
'openModal',
'createIssue',
'dismissVulnerability',
'undoDismissal',
]),
handleCreateIssue() {
const { vulnerability } = this;
this.createIssue({ vulnerability, flashError: true });
},
handleDismissVulnerability() {
const { vulnerability } = this;
this.dismissVulnerability({ vulnerability, flashError: true });
},
handleUndoDismissal() {
const { vulnerability } = this;
this.undoDismissal({ vulnerability, flashError: true });
},
},
};
</script>
<template>
<div>
<button
key="more-info"
v-gl-tooltip
:aria-label="s__('Security Reports|More info')"
:title="s__('Security Reports|More info')"
class="btn btn-secondary js-more-info"
type="button"
@click="openModal({ vulnerability })"
>
<icon name="information" />
</button>
<loading-button
v-if="canCreateIssue"
key="create-issue"
v-gl-tooltip
:aria-label="s__('Security Reports|New Issue')"
:loading="isCreatingIssue"
:title="s__('Security Reports|New Issue')"
container-class="btn btn-inverted btn-success js-create-issue"
type="button"
@click="handleCreateIssue"
>
<icon name="issue-new" />
</loading-button>
<template v-if="canDismissVulnerability">
<loading-button
v-if="isDismissed"
key="undo-dismissal"
:label="s__('Security Reports|Undo Dismissal')"
:loading="isDismissingVulnerability"
container-class="btn btn-inverted btn-warning js-undo-dismissal"
type="button"
@click="handleUndoDismissal"
/>
<loading-button
v-else
key="dismiss-vulnerability"
v-gl-tooltip
:aria-label="s__('Security Reports|Dismiss Vulnerability')"
:loading="isDismissingVulnerability"
:title="s__('Security Reports|Dismiss Vulnerability')"
container-class="btn btn-inverted btn-warning js-dismiss-vulnerability"
type="button"
@click="handleDismissVulnerability"
>
<icon name="cancel" />
</loading-button>
</template>
</div>
</template>
<script>
import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
export default {
name: 'VulnerabilityIssueLink',
......@@ -8,7 +8,7 @@ export default {
Icon,
},
directives: {
Tooltip,
GlTooltip: GlTooltipDirective,
},
props: {
issue: {
......@@ -31,7 +31,7 @@ export default {
<template>
<div class="d-inline">
<icon
v-tooltip
v-gl-tooltip
name="issues"
css-classes="text-success vertical-align-middle"
:title="s__('Security Dashboard|Issue Created')"
......
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import createFlash from '~/flash';
export const setVulnerabilitiesEndpoint = ({ commit }, endpoint) => {
commit(types.SET_VULNERABILITIES_ENDPOINT, endpoint);
......@@ -30,17 +33,19 @@ export const requestVulnerabilitiesCount = ({ commit }) => {
commit(types.REQUEST_VULNERABILITIES_COUNT);
};
export const receiveVulnerabilitiesCountSuccess = ({ commit }, response) => {
commit(types.RECEIVE_VULNERABILITIES_COUNT_SUCCESS, response.data);
export const receiveVulnerabilitiesCountSuccess = ({ commit }, { data }) => {
commit(types.RECEIVE_VULNERABILITIES_COUNT_SUCCESS, data);
};
export const receiveVulnerabilitiesCountError = ({ commit }) => {
commit(types.RECEIVE_VULNERABILITIES_COUNT_ERROR);
};
export const fetchVulnerabilities = ({ state, dispatch }, page = 1) => {
export const fetchVulnerabilities = ({ state, dispatch }, pageNumber) => {
dispatch('requestVulnerabilities');
const page = pageNumber || (state.pageInfo && state.pageInfo.page) || 1;
axios({
method: 'GET',
url: state.vulnerabilitiesEndpoint,
......@@ -59,10 +64,10 @@ export const requestVulnerabilities = ({ commit }) => {
commit(types.REQUEST_VULNERABILITIES);
};
export const receiveVulnerabilitiesSuccess = ({ commit }, response = {}) => {
const normalizedHeaders = normalizeHeaders(response.headers);
export const receiveVulnerabilitiesSuccess = ({ commit }, { headers, data }) => {
const normalizedHeaders = normalizeHeaders(headers);
const pageInfo = parseIntPagination(normalizedHeaders);
const vulnerabilities = response.data;
const vulnerabilities = data;
commit(types.RECEIVE_VULNERABILITIES_SUCCESS, { pageInfo, vulnerabilities });
};
......@@ -71,4 +76,117 @@ export const receiveVulnerabilitiesError = ({ commit }) => {
commit(types.RECEIVE_VULNERABILITIES_ERROR);
};
export const openModal = ({ commit }, payload = {}) => {
$('#modal-mrwidget-security-issue').modal('show');
commit(types.SET_MODAL_DATA, payload);
};
export const createIssue = ({ dispatch }, { vulnerability, flashError }) => {
dispatch('requestCreateIssue');
axios
.post(vulnerability.vulnerability_feedback_url, {
vulnerability_feedback: {
feedback_type: 'issue',
category: vulnerability.report_type,
project_fingerprint: vulnerability.project_fingerprint,
vulnerability_data: {
...vulnerability,
category: vulnerability.report_type,
},
},
})
.then(({ data }) => {
dispatch('receiveCreateIssueSuccess', data);
})
.catch(() => {
dispatch('receiveCreateIssueError', { flashError });
});
};
export const requestCreateIssue = ({ commit }) => {
commit(types.REQUEST_CREATE_ISSUE);
};
export const receiveCreateIssueSuccess = ({ commit }, payload) => {
commit(types.RECEIVE_CREATE_ISSUE_SUCCESS, payload);
};
export const receiveCreateIssueError = ({ commit }) => {
commit(types.RECEIVE_CREATE_ISSUE_ERROR);
createFlash(s__('Security Reports|There was an error creating the issue.'));
};
export const dismissVulnerability = ({ dispatch }, { vulnerability, flashError }) => {
dispatch('requestDismissVulnerability');
axios
.post(vulnerability.vulnerability_feedback_url, {
vulnerability_feedback: {
feedback_type: 'dismissal',
category: vulnerability.report_type,
project_fingerprint: vulnerability.project_fingerprint,
vulnerability_data: {
...vulnerability,
category: vulnerability.report_type,
},
},
})
.then(({ data }) => {
const { id } = vulnerability;
dispatch('receiveDismissVulnerabilitySuccess', { id, data });
})
.catch(() => {
dispatch('receiveDismissVulnerabilityError', { flashError });
});
};
export const requestDismissVulnerability = ({ commit }) => {
commit(types.REQUEST_DISMISS_VULNERABILITY);
};
export const receiveDismissVulnerabilitySuccess = ({ commit }, payload) => {
commit(types.RECEIVE_DISMISS_VULNERABILITY_SUCCESS, payload);
};
export const receiveDismissVulnerabilityError = ({ commit }, { flashError }) => {
commit(types.RECEIVE_DISMISS_VULNERABILITY_ERROR);
if (flashError) {
createFlash(s__('Security Reports|There was an error dismissing the issue.'));
}
};
export const undoDismissal = ({ dispatch }, { vulnerability, flashError }) => {
const { vulnerability_feedback_url, dismissal_feedback } = vulnerability;
// eslint-disable-next-line camelcase
const url = `${vulnerability_feedback_url}/${dismissal_feedback.id}`;
dispatch('requestUndoDismissal');
axios
.delete(url)
.then(() => {
const { id } = vulnerability;
dispatch('receiveUndoDismissalSuccess', { id });
})
.catch(() => {
dispatch('receiveUndoDismissalError', { flashError });
});
};
export const requestUndoDismissal = ({ commit }) => {
commit(types.REQUEST_UNDO_DISMISSAL);
};
export const receiveUndoDismissalSuccess = ({ commit }, payload) => {
commit(types.RECEIVE_UNDO_DISMISSAL_SUCCESS, payload);
};
export const receiveUndoDismissalError = ({ commit }, { flashError }) => {
commit(types.RECEIVE_UNDO_DISMISSAL_ERROR);
if (flashError) {
createFlash(s__('Security Reports|There was an error undoing this dismissal.'));
}
};
export default () => {};
......@@ -7,3 +7,17 @@ export const SET_VULNERABILITIES_COUNT_ENDPOINT = 'SET_VULNERABILITIES_COUNT_END
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';
export const SET_MODAL_DATA = 'SET_MODAL_DATA';
export const REQUEST_CREATE_ISSUE = 'REQUEST_CREATE_ISSUE';
export const RECEIVE_CREATE_ISSUE_SUCCESS = 'RECEIVE_CREATE_ISSUE_SUCCESS';
export const RECEIVE_CREATE_ISSUE_ERROR = 'RECEIVE_CREATE_ISSUE_ERROR';
export const REQUEST_DISMISS_VULNERABILITY = 'REQUEST_DISMISS_VULNERABILITY';
export const RECEIVE_DISMISS_VULNERABILITY_SUCCESS = 'RECEIVE_DISMISS_VULNERABILITY_SUCCESS';
export const RECEIVE_DISMISS_VULNERABILITY_ERROR = 'RECEIVE_DISMISS_VULNERABILITY_ERROR';
export const REQUEST_UNDO_DISMISSAL = 'REQUEST_UNDO_DISMISSAL';
export const RECEIVE_UNDO_DISMISSAL_SUCCESS = 'RECEIVE_UNDO_DISMISSAL_SUCCESS';
export const RECEIVE_UNDO_DISMISSAL_ERROR = 'RECEIVE_UNDO_DISMISSAL_ERROR';
import Vue from 'vue';
import { s__ } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import * as types from './mutation_types';
export default {
......@@ -32,4 +35,90 @@ export default {
state.isLoadingVulnerabilitiesCount = false;
state.hasError = true;
},
[types.SET_MODAL_DATA](state, payload) {
const { vulnerability } = payload;
Vue.set(state.modal, 'title', vulnerability.name);
Vue.set(state.modal.data.description, 'value', vulnerability.description);
Vue.set(
state.modal.data.project,
'value',
vulnerability.project && vulnerability.project.full_name,
);
Vue.set(
state.modal.data.project,
'url',
vulnerability.project && vulnerability.project.full_path,
);
Vue.set(state.modal.data.file, 'value', vulnerability.location && vulnerability.location.file);
Vue.set(
state.modal.data.identifiers,
'value',
vulnerability.identifiers.length && vulnerability.identifiers,
);
Vue.set(state.modal.data.severity, 'value', vulnerability.severity);
Vue.set(state.modal.data.confidence, 'value', vulnerability.confidence);
Vue.set(state.modal.data.solution, 'value', vulnerability.solution);
Vue.set(state.modal.data.links, 'value', vulnerability.links);
Vue.set(state.modal.data.instances, 'value', vulnerability.instances);
Vue.set(state.modal, 'vulnerability', vulnerability);
Vue.set(state.modal.vulnerability, 'hasIssue', Boolean(vulnerability.issue_feedback));
Vue.set(state.modal, 'error', null);
},
[types.REQUEST_CREATE_ISSUE](state) {
state.isCreatingIssue = true;
Vue.set(state.modal, 'isCreatingNewIssue', true);
Vue.set(state.modal, 'error', null);
},
[types.RECEIVE_CREATE_ISSUE_SUCCESS](state, payload) {
// We don't cancel the loading state here because we're navigating away from the page
visitUrl(payload.issue_url);
},
[types.RECEIVE_CREATE_ISSUE_ERROR](state) {
state.isCreatingIssue = false;
Vue.set(state.modal, 'isCreatingNewIssue', false);
Vue.set(state.modal, 'error', 'There was an error creating the issue');
},
[types.REQUEST_DISMISS_VULNERABILITY](state) {
state.isDismissingVulnerability = true;
Vue.set(state.modal, 'isDismissingVulnerability', true);
Vue.set(state.modal, 'error', null);
},
[types.RECEIVE_DISMISS_VULNERABILITY_SUCCESS](state, payload) {
const vulnerability = state.vulnerabilities.find(vuln => vuln.id === payload.id);
vulnerability.dismissal_feedback = payload.data;
state.isDismissingVulnerability = false;
Vue.set(state.modal, 'isDismissingVulnerability', false);
Vue.set(state.modal.vulnerability, 'isDismissed', true);
},
[types.RECEIVE_DISMISS_VULNERABILITY_ERROR](state) {
state.isDismissingVulnerability = false;
Vue.set(state.modal, 'isDismissingVulnerability', false);
Vue.set(
state.modal,
'error',
s__('Security Reports|There was an error dismissing the vulnerability.'),
);
},
[types.REQUEST_UNDO_DISMISSAL](state) {
state.isDismissingVulnerability = true;
Vue.set(state.modal, 'isDismissingVulnerability', true);
Vue.set(state.modal, 'error', null);
},
[types.RECEIVE_UNDO_DISMISSAL_SUCCESS](state, payload) {
const vulnerability = state.vulnerabilities.find(vuln => vuln.id === payload.id);
vulnerability.dismissal_feedback = null;
state.isDismissingVulnerability = false;
Vue.set(state.modal, 'isDismissingVulnerability', false);
Vue.set(state.modal.vulnerability, 'isDismissed', false);
},
[types.RECEIVE_UNDO_DISMISSAL_ERROR](state) {
state.isDismissingVulnerability = false;
Vue.set(state.modal, 'isDismissingVulnerability', false);
Vue.set(
state.modal,
'error',
s__('Security Reports|There was an error undoing the dismissal.'),
);
},
};
import { s__ } from '~/locale';
export default () => ({
hasError: false,
isLoadingVulnerabilities: true,
......@@ -7,4 +9,26 @@ export default () => ({
vulnerabilitiesCount: {},
vulnerabilitiesCountEndpoint: null,
vulnerabilitiesEndpoint: null,
activeVulnerability: null,
modal: {
data: {
description: { text: s__('Vulnerability|Description') },
project: {
text: s__('Vulnerability|Project'),
isLink: true,
},
file: { text: s__('Vulnerability|File') },
identifiers: { text: s__('Vulnerability|Identifiers') },
severity: { text: s__('Vulnerability|Severity') },
confidence: { text: s__('Vulnerability|Confidence') },
solution: { text: s__('Vulnerability|Solution') },
links: { text: s__('Vulnerability|Links') },
instances: { text: s__('Vulnerability|Instances') },
},
vulnerability: {},
isCreatingNewIssue: false,
isDismissingVulnerability: false,
},
isCreatingIssue: false,
isDismissingVulnerability: false,
});
---
title: Add modals and actions to the vulnerabilities in the Group security dashboard
merge_request: 7910
author:
type: added
import Vue from 'vue';
import Vuex from 'vuex';
import component from 'ee/security_dashboard/components/security_dashboard_action_buttons.vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
describe('Security Dashboard Action Buttons', () => {
let vm;
let props;
let actions;
beforeEach(() => {
props = { vulnerability: { id: 123 } };
actions = {
newIssue: jasmine.createSpy('newIssue'),
dismissVulnerability: jasmine.createSpy('dismissVulnerability'),
};
const Component = Vue.extend(component);
const store = new Vuex.Store({ actions });
vm = mountComponentWithStore(Component, { props, store });
});
afterEach(() => {
vm.$destroy();
});
it('should render three buttons', () => {
expect(vm.$el.querySelectorAll('.btn')).toHaveLength(3);
});
describe('More Info Button', () => {
it('should render the More info button', () => {
expect(vm.$el.querySelector('.js-more-info')).not.toBeNull();
});
});
describe('New Issue Button', () => {
it('should render the New Issue button', () => {
expect(vm.$el.querySelector('.js-new-issue')).not.toBeNull();
});
it('should trigger the `newIssue` action when clicked', () => {
vm.$el.querySelector('.js-new-issue').click();
expect(actions.newIssue).toHaveBeenCalledTimes(1);
});
});
describe('Dismiss Vulnerability Button', () => {
it('should render the Dismiss Vulnerability button', () => {
expect(vm.$el.querySelector('.js-dismiss-vulnerability')).not.toBeNull();
});
it('should trigger the `dismissVulnerability` action when clicked', () => {
vm.$el.querySelector('.js-dismiss-vulnerability').click();
expect(actions.dismissVulnerability).toHaveBeenCalledTimes(1);
});
});
});
import Vue from 'vue';
import component from 'ee/security_dashboard/components/security_dashboard_table_row.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import createStore from 'ee/security_dashboard/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import mockDataVulnerabilities from '../store/vulnerabilities/data/mock_data_vulnerabilities.json';
describe('Security Dashboard Table Row', () => {
let vm;
let props;
const store = createStore();
const Component = Vue.extend(component);
afterEach(() => {
vm.$destroy();
});
describe('when loading', () => {
beforeEach(() => {
props = { isLoading: true };
vm = mountComponent(Component, props);
vm = mountComponentWithStore(Component, { store, props });
});
afterEach(() => {
vm.$destroy();
});
it('should display the skeleton loader', () => {
......@@ -33,16 +36,15 @@ describe('Security Dashboard Table Row', () => {
});
describe('when loaded', () => {
beforeEach(() => {
const vulnerability = {
severity: 'high',
name: 'Test vulnerability',
confidence: 'medium',
project: { full_name: 'project name' },
};
const vulnerability = mockDataVulnerabilities[0];
beforeEach(() => {
props = { vulnerability };
vm = mountComponent(Component, props);
vm = mountComponentWithStore(Component, { store, props });
});
afterEach(() => {
vm.$destroy();
});
it('should not display the skeleton loader', () => {
......@@ -55,22 +57,34 @@ describe('Security Dashboard Table Row', () => {
);
});
it('should render the name', () => {
expect(vm.$el.querySelectorAll('.table-mobile-content')[1].textContent).toContain(
props.vulnerability.name,
);
});
it('should render the project namespace', () => {
expect(vm.$el.querySelectorAll('.table-mobile-content')[1].textContent).toContain(
props.vulnerability.project.full_name,
);
});
it('should render the confidence', () => {
expect(vm.$el.querySelectorAll('.table-mobile-content')[2].textContent).toContain(
props.vulnerability.confidence,
);
});
describe('the project name', () => {
it('should render the name', () => {
expect(vm.$el.querySelectorAll('.table-mobile-content')[1].textContent).toContain(
props.vulnerability.name,
);
});
it('should render the project namespace', () => {
expect(vm.$el.querySelectorAll('.table-mobile-content')[1].textContent).toContain(
props.vulnerability.project.full_name,
);
});
it('should fire the openModal action when clicked', () => {
spyOn(vm.$store, 'dispatch');
vm.$el.querySelector('.js-vulnerability-info').click();
expect(vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/openModal', {
vulnerability,
});
});
});
});
});
import Vue from 'vue';
import MockAdapater from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import component from 'ee/security_dashboard/components/security_dashboard_table.vue';
import createStore from 'ee/security_dashboard/store';
import mockDataVulnerabilities from 'ee/security_dashboard/store/modules/vulnerabilities/mock_data_vulnerabilities.json';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import waitForPromises from 'spec/helpers/wait_for_promises';
import { resetStore } from '../helpers';
import mockDataVulnerabilities from '../store/vulnerabilities/data/mock_data_vulnerabilities.json';
describe('Security Dashboard Table', () => {
const Component = Vue.extend(component);
......
import Vue from 'vue';
import component from 'ee/security_dashboard/components/vulnerability_action_buttons.vue';
import createStore from 'ee/security_dashboard/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
import mockDataVulnerabilities from '../store/vulnerabilities/data/mock_data_vulnerabilities.json';
describe('Security Dashboard Action Buttons', () => {
const Component = Vue.extend(component);
let vm;
let store;
let props;
beforeEach(() => {
store = createStore();
});
afterEach(() => {
vm.$destroy();
resetStore(store);
});
describe('with a fresh vulnerability', () => {
beforeEach(() => {
props = {
vulnerability: mockDataVulnerabilities[0],
canCreateIssue: true,
canDismissVulnerability: true,
};
vm = mountComponentWithStore(Component, { store, props });
spyOn(vm.$store, 'dispatch').and.returnValue(Promise.resolve());
});
afterEach(() => {
vm.$destroy();
});
it('should render three buttons', () => {
expect(vm.$el.querySelectorAll('.btn')).toHaveLength(3);
});
describe('More Info Button', () => {
let button;
beforeEach(() => {
button = vm.$el.querySelector('.js-more-info');
});
it('should render the More info button', () => {
expect(button).not.toBeNull();
});
it('should emit an `openModal` event when clicked', () => {
button.click();
expect(vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/openModal', {
vulnerability: mockDataVulnerabilities[0],
});
});
});
describe('Create Issue Button', () => {
let button;
beforeEach(() => {
button = vm.$el.querySelector('.js-create-issue');
});
it('should render the create issue button', () => {
expect(button).not.toBeNull();
});
it('should emit an `createIssue` event when clicked', () => {
button.click();
expect(vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/createIssue', {
vulnerability: mockDataVulnerabilities[0],
flashError: true,
});
});
});
describe('Dismiss Vulnerability Button', () => {
let button;
beforeEach(() => {
button = vm.$el.querySelector('.js-dismiss-vulnerability');
});
it('should render the dismiss vulnerability button', () => {
expect(button).not.toBeNull();
});
it('should emit an `dismissVulnerability` event when clicked', () => {
button.click();
expect(vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/dismissVulnerability', {
vulnerability: mockDataVulnerabilities[0],
flashError: true,
});
});
});
});
describe('with a vulnerbility that has an issue', () => {
beforeEach(() => {
props = {
vulnerability: mockDataVulnerabilities[3],
};
vm = mountComponentWithStore(Component, { store, props });
});
afterEach(() => {
vm.$destroy();
});
it('should only render one button', () => {
expect(vm.$el.querySelectorAll('.btn')).toHaveLength(1);
});
it('should not render the create issue button', () => {
expect(vm.$el.querySelector('.js-create-issue')).toBeNull();
});
});
describe('with a vulnerbility that has been dismissed', () => {
beforeEach(() => {
props = {
vulnerability: mockDataVulnerabilities[2],
};
vm = mountComponentWithStore(Component, { store, props });
});
afterEach(() => {
vm.$destroy();
});
it('should only render one button', () => {
expect(vm.$el.querySelectorAll('.btn')).toHaveLength(1);
});
it('should not render the dismiss vulnerability button', () => {
expect(vm.$el.querySelector('.js-dismiss-vulnerability')).toBeNull();
});
});
});
......@@ -32,6 +32,7 @@
},
"dismissal_feedback": null,
"issue_feedback": null,
"vulnerability_feedback_url": "https://example.com/vulnerability_feedback",
"description": "The cipher does not provide data integrity update 1",
"solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
"location": {
......@@ -81,6 +82,7 @@
},
"dismissal_feedback": null,
"issue_feedback": null,
"vulnerability_feedback_url": "https://example.com/vulnerability_feedback",
"description": "The cipher does not provide data integrity update 1",
"solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
"location": {
......@@ -152,6 +154,7 @@
"project_fingerprint": "4e5b6966dd100170b4b1ad599c7058cce91b57b4"
},
"issue_feedback": null,
"vulnerability_feedback_url": "https://example.com/vulnerability_feedback",
"description": "The cipher does not provide data integrity update 1",
"solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
"location": {
......@@ -224,6 +227,7 @@
"branch": "master",
"project_fingerprint": "4e5b6966dd100170b4b1ad599c7058cce91b57b4"
},
"vulnerability_feedback_url": "https://example.com/vulnerability_feedback",
"description": "The cipher does not provide data integrity update 1",
"solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
"location": {
......@@ -318,6 +322,7 @@
"branch": "master",
"project_fingerprint": "4e5b6966dd100170b4b1ad599c7058cce91b57b4"
},
"vulnerability_feedback_url": "https://example.com/vulnerability_feedback",
"description": "The cipher does not provide data integrity update 1",
"solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
"location": {
......@@ -367,6 +372,7 @@
},
"dismissal_feedback": null,
"issue_feedback": null,
"vulnerability_feedback_url": "https://example.com/vulnerability_feedback",
"description": "The cipher does not provide data integrity update 1",
"solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
"location": {
......
......@@ -6661,13 +6661,13 @@ msgstr ""
msgid "Reports|%{failedString} and %{resolvedString}"
msgstr ""
msgid "Reports|Class"
msgid "Reports|Actions"
msgstr ""
msgid "Reports|Confidence"
msgid "Reports|Class"
msgstr ""
msgid "Reports|Dismiss Vulnerability"
msgid "Reports|Confidence"
msgstr ""
msgid "Reports|Execution time"
......@@ -6676,12 +6676,6 @@ msgstr ""
msgid "Reports|Failure"
msgstr ""
msgid "Reports|More info"
msgstr ""
msgid "Reports|New Issue"
msgstr ""
msgid "Reports|Severity"
msgstr ""
......@@ -7017,12 +7011,39 @@ msgstr ""
msgid "Security Reports|At this time, the security dashboard only supports SAST."
msgstr ""
msgid "Security Reports|Dismiss Vulnerability"
msgstr ""
msgid "Security Reports|More info"
msgstr ""
msgid "Security Reports|New Issue"
msgstr ""
msgid "Security Reports|Security Dashboard Documentation"
msgstr ""
msgid "Security Reports|There was an error creating the issue."
msgstr ""
msgid "Security Reports|There was an error dismissing the issue."
msgstr ""
msgid "Security Reports|There was an error dismissing the vulnerability."
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 "Security Reports|There was an error undoing the dismissal."
msgstr ""
msgid "Security Reports|There was an error undoing this dismissal."
msgstr ""
msgid "Security Reports|Undo Dismissal"
msgstr ""
msgid "SecurityDashboard| The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
......@@ -8696,6 +8717,33 @@ msgstr ""
msgid "VisibilityLevel|Unknown"
msgstr ""
msgid "Vulnerability|Confidence"
msgstr ""
msgid "Vulnerability|Description"
msgstr ""
msgid "Vulnerability|File"
msgstr ""
msgid "Vulnerability|Identifiers"
msgstr ""
msgid "Vulnerability|Instances"
msgstr ""
msgid "Vulnerability|Links"
msgstr ""
msgid "Vulnerability|Project"
msgstr ""
msgid "Vulnerability|Severity"
msgstr ""
msgid "Vulnerability|Solution"
msgstr ""
msgid "Want to see the data? Please ask an administrator for access."
msgstr ""
......@@ -9353,6 +9401,9 @@ msgstr ""
msgid "from"
msgstr ""
msgid "help"
msgstr ""
msgid "here"
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