Commit 037ac648 authored by Paul Gascou-Vaillancourt's avatar Paul Gascou-Vaillancourt Committed by Clement Ho

Add toggle to hide dismissed vulnerabilities

- Added a toggle that can be turned on to hide dismissed vulnerabilities
- Added specs for gl-toggle-vuex
- Updated specs for all changed files
parent e88d909d
<script>
import { GlToggle } from '@gitlab/ui';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
export default {
name: 'GlToggleVuex',
components: {
GlToggle,
},
props: {
stateProperty: {
type: String,
required: true,
},
storeModule: {
type: String,
required: false,
default: null,
},
setAction: {
type: String,
required: false,
default() {
return `set${capitalizeFirstCharacter(this.stateProperty)}`;
},
},
},
computed: {
value: {
get() {
const { state } = this.$store;
const { stateProperty, storeModule } = this;
return storeModule ? state[storeModule][stateProperty] : state[stateProperty];
},
set(value) {
const { stateProperty, storeModule, setAction } = this;
const action = storeModule ? `${storeModule}/${setAction}` : setAction;
this.$store.dispatch(action, { key: stateProperty, value });
},
},
},
};
</script>
<template>
<gl-toggle v-model="value">
<slot v-bind="{ value }"></slot>
</gl-toggle>
</template>
......@@ -100,6 +100,7 @@ export default {
});
}
this.setPipelineId(this.pipelineId);
this.setHideDismissedToggleInitialState();
this.setProjectsEndpoint(this.projectsEndpoint);
this.setVulnerabilitiesEndpoint(this.vulnerabilitiesEndpoint);
this.setVulnerabilitiesCountEndpoint(this.vulnerabilitiesCountEndpoint);
......@@ -131,7 +132,7 @@ export default {
'downloadPatch',
]),
...mapActions('projects', ['setProjectsEndpoint', 'fetchProjects']),
...mapActions('filters', ['lockFilter']),
...mapActions('filters', ['lockFilter', 'setHideDismissedToggleInitialState']),
emitVulnerabilitiesCountChanged(count) {
this.$emit('vulnerabilitiesCountChanged', count);
},
......
<script>
import { mapGetters } from 'vuex';
import DashboardFilter from './filter.vue';
import GlToggleVuex from '~/vue_shared/components/gl_toggle_vuex.vue';
export default {
components: {
DashboardFilter,
GlToggleVuex,
},
computed: {
...mapGetters({
......@@ -23,6 +25,15 @@ export default {
class="col-sm-6 col-md-4 col-lg-2 p-2 js-filter"
:filter-id="filter.id"
/>
<div class="ml-lg-auto p-2">
<strong>{{ s__('SecurityDashboard|Hide dismissed') }}</strong>
<gl-toggle-vuex
class="d-block mt-1 js-toggle"
store-module="filters"
state-property="hide_dismissed"
set-action="setToggleValue"
/>
</div>
</div>
</div>
</template>
......@@ -18,7 +18,8 @@ export default function configureModerator(store) {
});
break;
case `filters/${filtersMutationTypes.SET_ALL_FILTERS}`:
case `filters/${filtersMutationTypes.SET_FILTER}`: {
case `filters/${filtersMutationTypes.SET_FILTER}`:
case `filters/${filtersMutationTypes.SET_TOGGLE_VALUE}`: {
const activeFilters = store.getters['filters/activeFilters'];
store.dispatch('vulnerabilities/fetchVulnerabilities', activeFilters);
store.dispatch('vulnerabilities/fetchVulnerabilitiesCount', activeFilters);
......
import Tracking from '~/tracking';
import { getParameterValues } from '~/lib/utils/url_utility';
import { parseBoolean } from '~/lib/utils/common_utils';
import * as types from './mutation_types';
export const setFilter = ({ commit }, payload) => {
......@@ -23,6 +25,23 @@ export const lockFilter = ({ commit }, payload) => {
commit(types.HIDE_FILTER, payload);
};
export const setHideDismissedToggleInitialState = ({ commit }) => {
const [urlParam] = getParameterValues('hide_dismissed');
if (typeof urlParam !== 'undefined') {
const parsedParam = parseBoolean(urlParam);
commit(types.SET_TOGGLE_VALUE, { key: 'hide_dismissed', value: parsedParam });
}
};
export const setToggleValue = ({ commit }, { key, value }) => {
commit(types.SET_TOGGLE_VALUE, { key, value });
Tracking.event(document.body.dataset.page, 'set_toggle', {
label: key,
value,
});
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
// This is no longer needed after gitlab-ce#52179 is merged
export default () => {};
......@@ -26,11 +26,16 @@ export const getSelectedOptionNames = (state, getters) => filterId => {
* @returns Object
* e.g. { type: ['sast'], severity: ['high', 'medium'] }
*/
export const activeFilters = state =>
state.filters.reduce((acc, filter) => {
export const activeFilters = state => {
const filters = state.filters.reduce((acc, filter) => {
acc[filter.id] = [...Array.from(filter.selection)].filter(option => option !== 'all');
return acc;
}, {});
// hide_dismissed is hardcoded as it currently is an edge-case, more info in the MR:
// https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/15333#note_208301144
filters.hide_dismissed = state.hide_dismissed;
return filters;
};
export const visibleFilters = ({ filters }) => filters.filter(({ hidden }) => !hidden);
......
......@@ -2,3 +2,4 @@ export const SET_FILTER = 'SET_FILTER';
export const SET_FILTER_OPTIONS = 'SET_FILTER_OPTIONS';
export const SET_ALL_FILTERS = 'SET_ALL_FILTERS';
export const HIDE_FILTER = 'HIDE_FILTER';
export const SET_TOGGLE_VALUE = 'SET_TOGGLE_VALUE';
......@@ -53,4 +53,7 @@ export default {
hiddenFilter.hidden = true;
}
},
[types.SET_TOGGLE_VALUE](state, { key, value }) {
state[key] = value;
},
};
......@@ -34,4 +34,5 @@ export default () => ({
selection: new Set(['all']),
},
],
hide_dismissed: true,
});
---
title: Added a toggle to show/hide dismissed vulnerabilities in the security dashboard
merge_request: 15333
author:
type: added
import { shallowMount, createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { getParameterValues } from '~/lib/utils/url_utility';
import { TEST_HOST } from 'helpers/test_constants';
import SecurityDashboardApp from 'ee/security_dashboard/components/app.vue';
......@@ -19,6 +20,10 @@ const vulnerabilitiesEndpoint = `${TEST_HOST}/vulnerabilities`;
const vulnerabilitiesCountEndpoint = `${TEST_HOST}/vulnerabilities_summary`;
const vulnerabilitiesHistoryEndpoint = `${TEST_HOST}/vulnerabilities_history`;
jest.mock('~/lib/utils/url_utility', () => ({
getParameterValues: jest.fn().mockReturnValue([]),
}));
describe('Security Dashboard app', () => {
let wrapper;
let mock;
......@@ -162,4 +167,26 @@ describe('Security Dashboard app', () => {
expect(wrapper.find(Component).exists()).toBe(false);
});
});
describe('dismissed vulnerabilities', () => {
beforeEach(() => {
getParameterValues.mockImplementation(() => [true]);
setup();
});
afterEach(() => {
getParameterValues.mockRestore();
});
it('hides dismissed vulnerabilities by default', () => {
createComponent();
expect(wrapper.vm.$store.state.filters.hide_dismissed).toBe(true);
});
it('shows dismissed vulnerabilities if param is specified in URL', () => {
getParameterValues.mockImplementation(() => [false]);
createComponent();
expect(wrapper.vm.$store.state.filters.hide_dismissed).toBe(false);
});
});
});
......@@ -20,5 +20,9 @@ describe('Filter component', () => {
it('should display all filters', () => {
expect(vm.$el.querySelectorAll('.js-filter').length).toEqual(4);
});
it('should display "Hide dismissed vulnerabilities" toggle', () => {
expect(vm.$el.querySelectorAll('.js-toggle').length).toEqual(1);
});
});
});
......@@ -2,7 +2,7 @@ import testAction from 'spec/helpers/vuex_action_helper';
import createState from 'ee/security_dashboard/store/modules/filters/state';
import * as types from 'ee/security_dashboard/store/modules/filters/mutation_types';
import * as actions from 'ee/security_dashboard/store/modules/filters/actions';
import module, * as actions from 'ee/security_dashboard/store/modules/filters/actions';
describe('filters actions', () => {
describe('setFilter', () => {
......@@ -67,4 +67,55 @@ describe('filters actions', () => {
);
});
});
describe('setHideDismissedToggleInitialState', () => {
it('should not do anything if hide_dismissed param is not present', done => {
spyOnDependency(module, 'getParameterValues').and.returnValue([]);
const state = createState();
testAction(actions.setHideDismissedToggleInitialState, {}, state, [], [], done);
});
it('should commit the SET_TOGGLE_VALUE mutation if hide_dismissed param is present', done => {
const state = createState();
spyOnDependency(module, 'getParameterValues').and.returnValue([false]);
testAction(
actions.setHideDismissedToggleInitialState,
{},
state,
[
{
type: types.SET_TOGGLE_VALUE,
payload: {
key: 'hide_dismissed',
value: false,
},
},
],
[],
done,
);
});
});
describe('setToggleValue', () => {
it('should commit the SET_TOGGLE_VALUE mutation', done => {
const state = createState();
const payload = { key: 'foo', value: 'bar' };
testAction(
actions.setToggleValue,
payload,
state,
[
{
type: types.SET_TOGGLE_VALUE,
payload,
},
],
[],
done,
);
});
});
});
......@@ -74,4 +74,28 @@ describe('moderator', () => {
activeFilters,
);
});
it('triggers fetching vulnerabilities after "Hide dismissed" toggle changes', () => {
spyOn(store, 'dispatch');
const activeFilters = store.getters['filters/activeFilters'];
store.commit(`filters/${filtersMutationTypes.SET_TOGGLE_VALUE}`, {});
expect(store.dispatch).toHaveBeenCalledTimes(3);
expect(store.dispatch).toHaveBeenCalledWith(
'vulnerabilities/fetchVulnerabilities',
activeFilters,
);
expect(store.dispatch).toHaveBeenCalledWith(
'vulnerabilities/fetchVulnerabilitiesCount',
activeFilters,
);
expect(store.dispatch).toHaveBeenCalledWith(
'vulnerabilities/fetchVulnerabilitiesHistory',
activeFilters,
);
});
});
......@@ -13530,6 +13530,9 @@ msgstr ""
msgid "SecurityDashboard|Confidence"
msgstr ""
msgid "SecurityDashboard|Hide dismissed"
msgstr ""
msgid "SecurityDashboard|Monitor vulnerabilities in your code"
msgstr ""
......
import Vuex from 'vuex';
import GlToggleVuex from '~/vue_shared/components/gl_toggle_vuex.vue';
import { GlToggle } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('GlToggleVuex component', () => {
let wrapper;
let store;
const findButton = () => wrapper.find('button');
const createWrapper = (props = {}) => {
wrapper = mount(GlToggleVuex, {
localVue,
store,
propsData: {
stateProperty: 'toggleState',
...props,
},
sync: false,
});
};
beforeEach(() => {
store = new Vuex.Store({
state: {
toggleState: false,
},
actions: {
setToggleState: ({ commit }, { key, value }) => commit('setToggleState', { key, value }),
},
mutations: {
setToggleState: (state, { key, value }) => {
state[key] = value;
},
},
});
createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('renders gl-toggle', () => {
expect(wrapper.find(GlToggle).exists()).toBe(true);
});
it('properly computes default value for setAction', () => {
expect(wrapper.props('setAction')).toBe('setToggleState');
});
describe('without a store module', () => {
it('calls action with new value when value changes', () => {
jest.spyOn(store, 'dispatch');
findButton().trigger('click');
expect(store.dispatch).toHaveBeenCalledWith('setToggleState', {
key: 'toggleState',
value: true,
});
});
it('updates store property when value changes', () => {
findButton().trigger('click');
expect(store.state.toggleState).toBe(true);
});
});
describe('with a store module', () => {
beforeEach(() => {
store = new Vuex.Store({
modules: {
someModule: {
namespaced: true,
state: {
toggleState: false,
},
actions: {
setToggleState: ({ commit }, { key, value }) =>
commit('setToggleState', { key, value }),
},
mutations: {
setToggleState: (state, { key, value }) => {
state[key] = value;
},
},
},
},
});
createWrapper({
storeModule: 'someModule',
});
});
it('calls action with new value when value changes', () => {
jest.spyOn(store, 'dispatch');
findButton().trigger('click');
expect(store.dispatch).toHaveBeenCalledWith('someModule/setToggleState', {
key: 'toggleState',
value: true,
});
});
it('updates store property when value changes', () => {
findButton().trigger('click');
expect(store.state.someModule.toggleState).toBe(true);
});
});
});
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