Commit 3e981e51 authored by Nathan Friend's avatar Nathan Friend

Merge branch '213598-multi-dismiss-frontend-selection-summary' into 'master'

Create new selection_summary component for GraphQL

See merge request gitlab-org/gitlab!29288
parents 383c0d19 2571637d
......@@ -3,7 +3,7 @@ import { mapActions, mapState, mapGetters } from 'vuex';
import { GlEmptyState, GlFormCheckbox } from '@gitlab/ui';
import Pagination from '~/vue_shared/components/pagination_links.vue';
import SecurityDashboardTableRow from './security_dashboard_table_row.vue';
import SelectionSummary from './selection_summary.vue';
import SelectionSummary from './selection_summary_vuex.vue';
export default {
name: 'SecurityDashboardTable',
......
<script>
import { __, n__ } from '~/locale';
import { mapActions, mapGetters } from 'vuex';
import { GlDeprecatedButton, GlFormSelect } from '@gitlab/ui';
import { s__, n__ } from '~/locale';
import { GlNewButton, GlFormSelect } from '@gitlab/ui';
import toast from '~/vue_shared/plugins/global_toast';
import createFlash from '~/flash';
import dismissVulnerability from '../graphql/dismissVulnerability.graphql';
const REASON_NONE = __('[No reason]');
const REASON_WONT_FIX = __("Won't fix / Accept risk");
const REASON_FALSE_POSITIVE = __('False positive');
const REASON_NONE = s__('Security Reports|[No reason]');
const REASON_WONT_FIX = s__("Security Reports|Won't fix / Accept risk");
const REASON_FALSE_POSITIVE = s__('Security Reports|False positive');
export default {
name: 'SelectionSummary',
components: {
GlDeprecatedButton,
GlNewButton,
GlFormSelect,
},
data: () => ({
dismissalReason: null,
}),
props: {
selectedVulnerabilities: {
type: Array,
required: true,
},
},
data() {
return {
dismissalReason: null,
};
},
computed: {
...mapGetters('vulnerabilities', ['selectedVulnerabilitiesCount']),
selectedVulnerabilitiesCount() {
return this.selectedVulnerabilities.length;
},
canDismissVulnerability() {
return this.dismissalReason && this.selectedVulnerabilitiesCount > 0;
return Boolean(this.dismissalReason && this.selectedVulnerabilitiesCount > 0);
},
message() {
return n__(
......@@ -30,21 +42,43 @@ export default {
},
},
methods: {
...mapActions('vulnerabilities', ['dismissSelectedVulnerabilities']),
dismissalSuccessMessage() {
return n__(
'%d vulnerability dismissed',
'%d vulnerabilities dismissed',
this.selectedVulnerabilities.length,
);
},
handleDismiss() {
if (!this.canDismissVulnerability) {
return;
}
if (!this.canDismissVulnerability) return;
this.dismissSelectedVulnerabilities();
},
dismissSelectedVulnerabilities() {
// TODO: Batch vulnerability dismissal with https://gitlab.com/gitlab-org/gitlab/-/issues/214376
const promises = this.selectedVulnerabilities.map(vulnerability =>
this.$apollo.mutate({
mutation: dismissVulnerability,
variables: { id: vulnerability.id, comment: this.dismissalReason },
}),
);
if (this.dismissalReason === REASON_NONE) {
this.dismissSelectedVulnerabilities();
} else {
this.dismissSelectedVulnerabilities({ comment: this.dismissalReason });
}
Promise.all(promises)
.then(() => {
toast(this.dismissalSuccessMessage());
this.$emit('deselect-all-vulnerabilities');
this.$emit('refetch-vulnerabilities');
})
.catch(() => {
createFlash(
s__('Security Reports|There was an error dismissing the vulnerabilities.'),
'alert',
);
});
},
},
dismissalReasons: [
{ value: null, text: __('Select a reason') },
{ value: null, text: s__('Security Reports|Select a reason') },
REASON_FALSE_POSITIVE,
REASON_WONT_FIX,
REASON_NONE,
......@@ -55,15 +89,21 @@ export default {
<template>
<div class="card">
<form class="card-body d-flex align-items-center" @submit.prevent="handleDismiss">
<span>{{ message }}</span>
<span ref="dismiss-message">{{ message }}</span>
<gl-form-select
v-model="dismissalReason"
class="mx-3 w-auto"
:options="$options.dismissalReasons"
/>
<gl-deprecated-button type="submit" variant="close" :disabled="!canDismissVulnerability">{{
__('Dismiss Selected')
}}</gl-deprecated-button>
<gl-new-button
type="submit"
class="js-no-auto-disable"
category="secondary"
variant="warning"
:disabled="!canDismissVulnerability"
>
{{ s__('Security Reports|Dismiss Selected') }}
</gl-new-button>
</form>
</div>
</template>
<script>
import { __, n__ } from '~/locale';
import { mapActions, mapGetters } from 'vuex';
import { GlDeprecatedButton, GlFormSelect } from '@gitlab/ui';
const REASON_NONE = __('[No reason]');
const REASON_WONT_FIX = __("Won't fix / Accept risk");
const REASON_FALSE_POSITIVE = __('False positive');
export default {
name: 'SelectionSummary',
components: {
GlDeprecatedButton,
GlFormSelect,
},
data: () => ({
dismissalReason: null,
}),
computed: {
...mapGetters('vulnerabilities', ['selectedVulnerabilitiesCount']),
canDismissVulnerability() {
return this.dismissalReason && this.selectedVulnerabilitiesCount > 0;
},
message() {
return n__(
'Dismiss %d selected vulnerability as',
'Dismiss %d selected vulnerabilities as',
this.selectedVulnerabilitiesCount,
);
},
},
methods: {
...mapActions('vulnerabilities', ['dismissSelectedVulnerabilities']),
handleDismiss() {
if (!this.canDismissVulnerability) {
return;
}
if (this.dismissalReason === REASON_NONE) {
this.dismissSelectedVulnerabilities();
} else {
this.dismissSelectedVulnerabilities({ comment: this.dismissalReason });
}
},
},
dismissalReasons: [
{ value: null, text: __('Select a reason') },
REASON_FALSE_POSITIVE,
REASON_WONT_FIX,
REASON_NONE,
],
};
</script>
<template>
<div class="card">
<form class="card-body d-flex align-items-center" @submit.prevent="handleDismiss">
<span>{{ message }}</span>
<gl-form-select
v-model="dismissalReason"
class="mx-3 w-auto"
:options="$options.dismissalReasons"
/>
<gl-deprecated-button type="submit" variant="close" :disabled="!canDismissVulnerability">{{
__('Dismiss Selected')
}}</gl-deprecated-button>
</form>
</div>
</template>
mutation ($id: ID!, $comment: String! ) {
dismissVulnerability(input: {id: $id, comment: $comment}) {
errors
vulnerability {
id
state
}
}
}
import { mount } from '@vue/test-utils';
import SelectionSummary from 'ee/security_dashboard/components/selection_summary.vue';
import { GlFormSelect, GlNewButton } from '@gitlab/ui';
import createFlash from '~/flash';
import toast from '~/vue_shared/plugins/global_toast';
jest.mock('~/flash');
jest.mock('~/vue_shared/plugins/global_toast');
describe('Selection Summary component', () => {
let wrapper;
let spyMutate;
const defaultData = {
dismissalReason: null,
};
const defaultMocks = {
$apollo: {
mutate: jest.fn().mockResolvedValue(),
},
};
const dismissButton = () => wrapper.find(GlNewButton);
const dismissMessage = () => wrapper.find({ ref: 'dismiss-message' });
const formSelect = () => wrapper.find(GlFormSelect);
const createComponent = ({ props = {}, data = defaultData, mocks = defaultMocks }) => {
spyMutate = mocks.$apollo.mutate;
wrapper = mount(SelectionSummary, {
mocks: {
...defaultMocks,
...mocks,
},
propsData: {
selectedVulnerabilities: [],
...props,
},
data: () => data,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when vulnerabilities are selected', () => {
describe('it renders correctly', () => {
beforeEach(() => {
createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }] } });
});
it('returns the right message for one selected vulnerabilities', () => {
expect(dismissMessage().text()).toBe('Dismiss 1 selected vulnerability as');
});
it('returns the right message for greater than one selected vulnerabilities', () => {
createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }, { id: 'id_1' }] } });
expect(dismissMessage().text()).toBe('Dismiss 2 selected vulnerabilities as');
});
});
describe('dismiss button', () => {
beforeEach(() => {
createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }] } });
});
it('should have the button disabled if an option is not selected', () => {
expect(dismissButton().attributes('disabled')).toBe('disabled');
});
it('should have the button enabled if a vulnerability is selected and an option is selected', () => {
createComponent({ props: { selectedVulnerabilities: [{ id: 'id_0' }] } });
expect(wrapper.vm.dismissalReason).toBe(null);
expect(wrapper.findAll('option').length).toBe(4);
formSelect()
.findAll('option')
.at(1)
.setSelected();
formSelect().trigger('change');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.dismissalReason).toEqual(expect.any(String));
expect(dismissButton().attributes('disabled')).toBe(undefined);
});
});
});
describe('clicking the dismiss vulnerability button', () => {
beforeEach(() => {
createComponent({
props: { selectedVulnerabilities: [{ id: 'id_0' }, { id: 'id_1' }] },
data: { dismissalReason: 'Will Not Fix' },
});
});
it('should make an API request for each vulnerability', () => {
dismissButton().trigger('submit');
expect(spyMutate).toHaveBeenCalledTimes(2);
});
it('should show toast with the right message if all calls were successful', () => {
dismissButton().trigger('submit');
setImmediate(() => {
// return wrapper.vm.$nextTick().then(() => {
expect(toast).toHaveBeenCalledWith('2 vulnerabilities dismissed');
});
});
it('should show flash with the right message if some calls failed', () => {
createComponent({
props: { selectedVulnerabilities: [{ id: 'id_0' }, { id: 'id_1' }] },
data: { dismissalReason: 'Will Not Fix' },
mocks: { $apollo: { mutate: jest.fn().mockRejectedValue() } },
});
dismissButton().trigger('submit');
setImmediate(() => {
expect(createFlash).toHaveBeenCalledWith(
'There was an error dismissing the vulnerabilities.',
'alert',
);
});
});
});
});
describe('when vulnerabilities are not selected', () => {
beforeEach(() => {
createComponent({});
});
it('should have the button disabled', () => {
expect(dismissButton().attributes().disabled).toBe('disabled');
});
});
});
......@@ -17958,6 +17958,9 @@ msgstr ""
msgid "Security Reports|Create issue"
msgstr ""
msgid "Security Reports|Dismiss Selected"
msgstr ""
msgid "Security Reports|Dismiss vulnerability"
msgstr ""
......@@ -17970,6 +17973,9 @@ msgstr ""
msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
msgid "Security Reports|False positive"
msgstr ""
msgid "Security Reports|Learn more about setting up your dashboard"
msgstr ""
......@@ -17982,6 +17988,9 @@ msgstr ""
msgid "Security Reports|Security reports can only be accessed by authorized users."
msgstr ""
msgid "Security Reports|Select a reason"
msgstr ""
msgid "Security Reports|There was an error adding the comment."
msgstr ""
......@@ -18009,12 +18018,18 @@ msgstr ""
msgid "Security Reports|Undo dismiss"
msgstr ""
msgid "Security Reports|Won't fix / Accept risk"
msgstr ""
msgid "Security Reports|You do not have sufficient permissions to access this report"
msgstr ""
msgid "Security Reports|You must sign in as an authorized user to see this report"
msgstr ""
msgid "Security Reports|[No reason]"
msgstr ""
msgid "Security configuration help link"
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