Commit 0432a5f6 authored by Daniel Tian's avatar Daniel Tian

Fix pipeline security tab filters

Fix the dropdown filters not showing on the security tab of the
pipeline details page
parent 1aa7f24b
---
title: Fix pipeline security tab filters not showing
merge_request: 47294
author:
type: fixed
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { severityFilter, scannerFilter } from 'ee/security_dashboard/helpers';
import { GlToggle } from '@gitlab/ui';
import StandardFilter from './filters/standard_filter.vue'; import StandardFilter from './filters/standard_filter.vue';
import GlToggleVuex from '~/vue_shared/components/gl_toggle_vuex.vue'; import { DISMISSAL_STATES } from '../store/modules/filters/constants';
export default { export default {
components: { components: {
StandardFilter, StandardFilter,
GlToggleVuex, GlToggle,
}, },
data: () => ({
filterConfigs: [severityFilter, scannerFilter],
}),
computed: { computed: {
...mapGetters('filters', ['visibleFilters']), ...mapState('filters', ['filters']),
hideDismissed() {
return this.filters.scope === DISMISSAL_STATES.DISMISSED;
},
}, },
methods: { methods: {
...mapActions('filters', ['setFilter']), ...mapActions('filters', ['setFilter', 'toggleHideDismissed']),
}, },
}; };
</script> </script>
...@@ -21,21 +29,20 @@ export default { ...@@ -21,21 +29,20 @@ export default {
<div class="dashboard-filters border-bottom bg-gray-light"> <div class="dashboard-filters border-bottom bg-gray-light">
<div class="row mx-0 p-2"> <div class="row mx-0 p-2">
<standard-filter <standard-filter
v-for="filter in visibleFilters" v-for="filter in filterConfigs"
:key="filter.id" :key="filter.id"
class="col-sm-6 col-md-4 col-lg-2 p-2 js-filter" class="col-sm-6 col-md-4 col-lg-2 p-2 js-filter"
:filter="filter" :filter="filter"
@setFilter="setFilter" @filter-changed="setFilter"
/> />
<div class="gl-display-flex ml-lg-auto p-2"> <div class="gl-display-flex ml-lg-auto p-2">
<slot name="buttons"></slot> <slot name="buttons"></slot>
<div class="pl-md-6"> <div class="pl-md-6">
<strong>{{ s__('SecurityReports|Hide dismissed') }}</strong> <strong>{{ s__('SecurityReports|Hide dismissed') }}</strong>
<gl-toggle-vuex <gl-toggle
class="d-block mt-1 js-toggle" class="d-block mt-1 js-toggle"
store-module="filters" :value="hideDismissed"
state-property="hideDismissed" @change="toggleHideDismissed"
set-action="setToggleValue"
/> />
</div> </div>
</div> </div>
......
...@@ -46,7 +46,7 @@ export default { ...@@ -46,7 +46,7 @@ export default {
); );
}, },
routeQueryIds() { routeQueryIds() {
const ids = this.$route.query[this.filter.id] || []; const ids = this.$route?.query[this.filter.id] || [];
return Array.isArray(ids) ? ids : [ids]; return Array.isArray(ids) ? ids : [ids];
}, },
routeQueryOptions() { routeQueryOptions() {
...@@ -83,9 +83,9 @@ export default { ...@@ -83,9 +83,9 @@ export default {
this.updateRouteQuery(); this.updateRouteQuery();
}, },
updateRouteQuery() { updateRouteQuery() {
const query = { query: { ...this.$route.query, ...this.queryObject } }; const query = { query: { ...this.$route?.query, ...this.queryObject } };
// To avoid a console error, don't update the querystring if it's the same as the current one. // To avoid a console error, don't update the querystring if it's the same as the current one.
if (!isEqual(this.routeQueryIds, this.queryObject[this.filter.id])) { if (this.$router && !isEqual(this.routeQueryIds, this.queryObject[this.filter.id])) {
this.$router.push(query); this.$router.push(query);
} }
}, },
......
...@@ -23,7 +23,7 @@ export default { ...@@ -23,7 +23,7 @@ export default {
'pageInfo', 'pageInfo',
'vulnerabilities', 'vulnerabilities',
]), ]),
...mapGetters('filters', ['activeFilters']), ...mapState('filters', ['filters']),
...mapGetters('vulnerabilities', [ ...mapGetters('vulnerabilities', [
'dashboardListError', 'dashboardListError',
'hasSelectedAllVulnerabilities', 'hasSelectedAllVulnerabilities',
...@@ -49,7 +49,7 @@ export default { ...@@ -49,7 +49,7 @@ export default {
'selectAllVulnerabilities', 'selectAllVulnerabilities',
]), ]),
fetchPage(page) { fetchPage(page) {
this.fetchVulnerabilities({ ...this.activeFilters, page }); this.fetchVulnerabilities({ ...this.filters, page });
}, },
handleSelectAll() { handleSelectAll() {
return this.hasSelectedAllVulnerabilities return this.hasSelectedAllVulnerabilities
......
<script> <script>
import { isUndefined } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import IssueModal from 'ee/vue_shared/security_reports/components/modal.vue'; import IssueModal from 'ee/vue_shared/security_reports/components/modal.vue';
import Filters from './filters.vue'; import Filters from './filters.vue';
...@@ -29,12 +28,6 @@ export default { ...@@ -29,12 +28,6 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
lockToProject: {
type: Object,
required: false,
default: null,
validator: project => !isUndefined(project.id),
},
pipelineId: { pipelineId: {
type: Number, type: Number,
required: false, required: false,
...@@ -56,7 +49,7 @@ export default { ...@@ -56,7 +49,7 @@ export default {
'isCreatingMergeRequest', 'isCreatingMergeRequest',
]), ]),
...mapState('pipelineJobs', ['projectId']), ...mapState('pipelineJobs', ['projectId']),
...mapGetters('filters', ['activeFilters']), ...mapState('filters', ['filters']),
...mapGetters('vulnerabilities', ['loadingVulnerabilitiesFailedWithRecognizedErrorCode']), ...mapGetters('vulnerabilities', ['loadingVulnerabilitiesFailedWithRecognizedErrorCode']),
...mapGetters('pipelineJobs', ['hasFuzzingArtifacts', 'fuzzingJobsWithArtifact']), ...mapGetters('pipelineJobs', ['hasFuzzingArtifacts', 'fuzzingJobsWithArtifact']),
canCreateIssue() { canCreateIssue() {
...@@ -74,9 +67,6 @@ export default { ...@@ -74,9 +67,6 @@ export default {
vulnerability() { vulnerability() {
return this.modal.vulnerability; return this.modal.vulnerability;
}, },
isLockedToProject() {
return this.lockToProject !== null;
},
shouldShowAside() { shouldShowAside() {
return this.shouldShowChart; return this.shouldShowChart;
}, },
...@@ -85,18 +75,11 @@ export default { ...@@ -85,18 +75,11 @@ export default {
}, },
}, },
created() { created() {
if (this.isLockedToProject) {
this.lockFilter({
filterId: 'project_id',
optionId: this.lockToProject.id,
});
}
this.setPipelineId(this.pipelineId); this.setPipelineId(this.pipelineId);
this.setHideDismissedToggleInitialState();
this.setVulnerabilitiesEndpoint(this.vulnerabilitiesEndpoint); this.setVulnerabilitiesEndpoint(this.vulnerabilitiesEndpoint);
this.setVulnerabilitiesHistoryEndpoint(this.vulnerabilitiesHistoryEndpoint); this.setVulnerabilitiesHistoryEndpoint(this.vulnerabilitiesHistoryEndpoint);
this.fetchVulnerabilities({ ...this.activeFilters, page: this.pageInfo.page }); this.fetchVulnerabilities({ ...this.filters, page: this.pageInfo.page });
this.fetchVulnerabilitiesHistory(this.activeFilters); this.fetchVulnerabilitiesHistory(this.filters);
this.fetchPipelineJobs(); this.fetchPipelineJobs();
}, },
methods: { methods: {
......
import Tracking from '~/tracking';
import { getParameterValues } from '~/lib/utils/url_utility';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { ALL } from './constants';
import { hasValidSelection } from './utils';
export const setFilter = ({ commit }, { filterId, optionId, lazy = false }) => { export const setFilter = ({ commit }, filter) => {
commit(types.SET_FILTER, { filterId, optionId, lazy }); commit(types.SET_FILTER, filter);
Tracking.event(document.body.dataset.page, 'set_filter', {
label: filterId,
value: optionId,
});
};
export const setFilterOptions = ({ commit, state }, { filterId, options, lazy = false }) => {
commit(types.SET_FILTER_OPTIONS, { filterId, options });
const { selection } = state.filters.find(({ id }) => id === filterId);
if (!hasValidSelection({ selection, options })) {
commit(types.SET_FILTER, { filterId, optionId: ALL, lazy });
}
};
export const setAllFilters = ({ commit }, payload) => {
commit(types.SET_ALL_FILTERS, payload);
}; };
export const lockFilter = ({ commit }, payload) => { export const toggleHideDismissed = ({ commit }) => {
commit(types.SET_FILTER, payload); commit(types.TOGGLE_HIDE_DISMISSED);
commit(types.HIDE_FILTER, payload);
};
export const setHideDismissedToggleInitialState = ({ commit }) => {
const [urlParam] = getParameterValues('scope');
const showDismissed = urlParam === 'all';
commit(types.SET_TOGGLE_VALUE, { key: 'hideDismissed', value: !showDismissed });
};
export const setToggleValue = ({ commit }, { key, value }) => {
commit(types.SET_TOGGLE_VALUE, { key, value });
Tracking.event(document.body.dataset.page, 'set_toggle', {
label: key,
value,
});
}; };
...@@ -5,6 +5,10 @@ export const STATE = { ...@@ -5,6 +5,10 @@ export const STATE = {
DETECTED: 'DETECTED', DETECTED: 'DETECTED',
CONFIRMED: 'CONFIRMED', CONFIRMED: 'CONFIRMED',
}; };
export const DISMISSAL_STATES = {
DISMISSED: 'dismissed',
ALL: 'all',
};
export const BASE_FILTERS = { export const BASE_FILTERS = {
state: { state: {
......
import { isBaseFilterOption } from './utils';
/**
* Loops through all the filters and returns all the active ones
* stripping out base filter options.
* @returns Object
* e.g. { type: ['sast'], severity: ['high', 'medium'] }
*/
export const activeFilters = state => {
const filters = state.filters.reduce((acc, filter) => {
acc[filter.id] = [...Array.from(filter.selection)].filter(id => !isBaseFilterOption(id));
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.scope = state.hideDismissed ? 'dismissed' : 'all';
return filters;
};
export const visibleFilters = ({ filters }) => filters.filter(({ hidden }) => !hidden);
import * as actions from './actions'; import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
import state from './state'; import state from './state';
export default { export default {
namespaced: true, namespaced: true,
actions, actions,
getters,
mutations, mutations,
state, state,
}; };
export const SET_FILTER = 'SET_FILTER'; export const SET_FILTER = 'SET_FILTER';
export const SET_FILTER_OPTIONS = 'SET_FILTER_OPTIONS'; export const TOGGLE_HIDE_DISMISSED = 'TOGGLE_HIDE_DISMISSED';
export const SET_ALL_FILTERS = 'SET_ALL_FILTERS';
export const HIDE_FILTER = 'HIDE_FILTER';
export const SET_TOGGLE_VALUE = 'SET_TOGGLE_VALUE';
import { mapValues } from 'lodash';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { ALL } from './constants'; import { DISMISSAL_STATES } from './constants';
import { setFilter } from './utils'; import Tracking from '~/tracking';
export default { export default {
[types.SET_ALL_FILTERS](state, payload = {}) { [types.SET_FILTER](state, filter) {
state.filters = state.filters.map(filter => { // Convert the filter key to snake case and the selected option IDs to lower case. The API
// If the payload is empty, we fall back to an empty selection // endpoint needs them to be in this format.
const selectedOptions = (payload && payload[filter.id]) || []; const convertedFilter = mapValues(convertObjectPropsToSnakeCase(filter), array =>
array.map(element => element.toLowerCase()),
);
const selection = Array.isArray(selectedOptions) state.filters = { ...state.filters, ...convertedFilter };
? new Set(selectedOptions)
: new Set([selectedOptions]);
// This prevents us from selecting nothing at all const [label, value] = Object.values(filter);
if (selection.size === 0) { Tracking.event(document.body.dataset.page, 'set_filter', { label, value });
selection.add(ALL);
}
return { ...filter, selection };
});
state.hideDismissed = payload.scope !== 'all';
},
[types.SET_FILTER](state, payload) {
state.filters = setFilter(state.filters, payload);
},
[types.SET_FILTER_OPTIONS](state, payload) {
const { filterId, options } = payload;
state.filters.find(filter => filter.id === filterId).options = options;
}, },
[types.HIDE_FILTER](state, { filterId }) { [types.TOGGLE_HIDE_DISMISSED](state) {
const hiddenFilter = state.filters.find(({ id }) => id === filterId); const scope =
if (hiddenFilter) { state.filters.scope === DISMISSAL_STATES.DISMISSED
hiddenFilter.hidden = true; ? DISMISSAL_STATES.ALL
} : DISMISSAL_STATES.DISMISSED;
},
[types.SET_TOGGLE_VALUE](state, { key, value }) { state.filters = { ...state.filters, scope };
state[key] = value;
Tracking.event(document.body.dataset.page, 'set_toggle', { label: 'scope', value: scope });
}, },
}; };
import { s__ } from '~/locale';
import { BASE_FILTERS } from './constants';
import { SEVERITY_LEVELS, REPORT_TYPES } from '../../constants';
const optionsObjectToArray = obj => Object.entries(obj).map(([id, name]) => ({ id, name }));
export default () => ({ export default () => ({
filters: [ filters: {
{ scope: 'dismissed',
name: s__('SecurityReports|Severity'),
id: 'severity',
options: [BASE_FILTERS.severity, ...optionsObjectToArray(SEVERITY_LEVELS)],
hidden: false,
selection: new Set([BASE_FILTERS.severity.id]),
}, },
{
name: s__('SecurityReports|Scanner'),
id: 'report_type',
options: [BASE_FILTERS.report_type, ...optionsObjectToArray(REPORT_TYPES)],
hidden: false,
selection: new Set([BASE_FILTERS.report_type.id]),
},
{
name: s__('SecurityReports|Project'),
id: 'project_id',
options: [BASE_FILTERS.project_id],
hidden: false,
selection: new Set([BASE_FILTERS.project_id.id]),
},
],
hideDismissed: true,
}); });
import { isSubset } from '~/lib/utils/set';
import { ALL } from './constants';
export const isBaseFilterOption = id => id === ALL;
/**
* Returns whether or not the given state filter has a valid selection,
* considering its available options.
* @param {Object} filter The filter from the state to check.
* @returns boolean
*/
export const hasValidSelection = ({ selection, options }) =>
isSubset(selection, new Set(options.map(({ id }) => id)));
/**
* Takes a filter array and a selected payload.
* It then either adds or removes that option from the appropriate selected filter.
* With a few extra exceptions around the `ALL` special case.
* @param {Array} filters the filters to mutate
* @param {Object} payload
* @param {String} payload.optionId the ID of the option that was just selected
* @param {String} payload.filterId the ID of the filter that the selected option belongs to
* @returns {Array} the mutated filters array
*/
export const setFilter = (filters, { optionId, filterId }) =>
filters.map(filter => {
if (filter.id === filterId) {
const { selection } = filter;
if (optionId === ALL) {
selection.clear();
} else if (selection.has(optionId)) {
selection.delete(optionId);
} else {
selection.delete(ALL);
selection.add(optionId);
}
if (selection.size === 0) {
selection.add(ALL);
}
return {
...filter,
selection,
};
}
return filter;
});
import * as filtersMutationTypes from '../modules/filters/mutation_types'; import { SET_FILTER, TOGGLE_HIDE_DISMISSED } from '../modules/filters/mutation_types';
import * as vulnerabilitiesMutationTypes from '../modules/vulnerabilities/mutation_types';
const refreshTypes = [`filters/${SET_FILTER}`, `filters/${TOGGLE_HIDE_DISMISSED}`];
export default store => { export default store => {
const refreshVulnerabilities = payload => { const refreshVulnerabilities = payload => {
...@@ -7,27 +8,9 @@ export default store => { ...@@ -7,27 +8,9 @@ export default store => {
store.dispatch('vulnerabilities/fetchVulnerabilitiesHistory', payload); store.dispatch('vulnerabilities/fetchVulnerabilitiesHistory', payload);
}; };
store.subscribe(({ type, payload = {} }) => { store.subscribe(({ type }) => {
switch (type) { if (refreshTypes.includes(type)) {
// SET_ALL_FILTERS mutations are triggered by navigation events, in such case we refreshVulnerabilities(store.state.filters.filters);
// want to preserve the page number that was set in the sync_with_router plugin
case `filters/${filtersMutationTypes.SET_ALL_FILTERS}`:
refreshVulnerabilities({
...store.getters['filters/activeFilters'],
page: store.state.vulnerabilities.pageInfo.page,
});
break;
// These mutations happen when users interact with the UI,
// in that case we want to reset the page number
case `vulnerabilities/${vulnerabilitiesMutationTypes.RECEIVE_DISMISS_SELECTED_VULNERABILITIES_SUCCESS}`:
case `filters/${filtersMutationTypes.SET_FILTER}`:
case `filters/${filtersMutationTypes.SET_TOGGLE_VALUE}`: {
if (!payload.lazy) {
refreshVulnerabilities(store.getters['filters/activeFilters']);
}
break;
}
default:
} }
}); });
}; };
import projectSelectorModule from '../modules/project_selector';
import * as projectSelectorMutationTypes from '../modules/project_selector/mutation_types';
import { BASE_FILTERS } from '../modules/filters/constants';
export default store => {
store.registerModule('projectSelector', projectSelectorModule());
store.subscribe(({ type, payload }) => {
if (type === `projectSelector/${projectSelectorMutationTypes.RECEIVE_PROJECTS_SUCCESS}`) {
store.dispatch('filters/setFilterOptions', {
filterId: 'project_id',
options: [
BASE_FILTERS.project_id,
...payload.map(({ name, id }) => ({
name,
id: id.toString(),
})),
],
lazy: true,
});
}
});
};
import projectsModule from '../modules/projects';
import * as projectsMutationTypes from '../modules/projects/mutation_types';
import { BASE_FILTERS } from '../modules/filters/constants';
export default store => {
store.registerModule('projects', projectsModule);
store.subscribe(({ type, payload }) => {
if (type === `projects/${projectsMutationTypes.RECEIVE_PROJECTS_SUCCESS}`) {
store.dispatch('filters/setFilterOptions', {
filterId: 'project_id',
options: [
BASE_FILTERS.project_id,
...payload.projects.map(({ name, id }) => ({
name,
id: id.toString(),
})),
],
});
}
});
};
...@@ -38,7 +38,7 @@ describe('Filter component', () => { ...@@ -38,7 +38,7 @@ describe('Filter component', () => {
}); });
it('should display all filters', () => { it('should display all filters', () => {
expect(wrapper.findAll('.js-filter')).toHaveLength(3); expect(wrapper.findAll('.js-filter')).toHaveLength(2);
}); });
it('should display "Hide dismissed vulnerabilities" toggle', () => { it('should display "Hide dismissed vulnerabilities" toggle', () => {
......
...@@ -88,7 +88,6 @@ describe('Pipeline Security Dashboard component', () => { ...@@ -88,7 +88,6 @@ describe('Pipeline Security Dashboard component', () => {
const dashboard = wrapper.find(SecurityDashboard); const dashboard = wrapper.find(SecurityDashboard);
expect(dashboard.exists()).toBe(true); expect(dashboard.exists()).toBe(true);
expect(dashboard.props()).toMatchObject({ expect(dashboard.props()).toMatchObject({
lockToProject: { id: projectId },
pipelineId, pipelineId,
vulnerabilitiesEndpoint, vulnerabilitiesEndpoint,
}); });
......
...@@ -11,7 +11,6 @@ import VulnerabilityChart from 'ee/security_dashboard/components/vulnerability_c ...@@ -11,7 +11,6 @@ import VulnerabilityChart from 'ee/security_dashboard/components/vulnerability_c
import LoadingError from 'ee/security_dashboard/components/loading_error.vue'; import LoadingError from 'ee/security_dashboard/components/loading_error.vue';
import createStore from 'ee/security_dashboard/store'; import createStore from 'ee/security_dashboard/store';
import { getParameterValues } from '~/lib/utils/url_utility';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
const pipelineId = 123; const pipelineId = 123;
...@@ -25,7 +24,6 @@ jest.mock('~/lib/utils/url_utility', () => ({ ...@@ -25,7 +24,6 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('Security Dashboard component', () => { describe('Security Dashboard component', () => {
let wrapper; let wrapper;
let mock; let mock;
let lockFilterSpy;
let setPipelineIdSpy; let setPipelineIdSpy;
let fetchPipelineJobsSpy; let fetchPipelineJobsSpy;
let store; let store;
...@@ -37,7 +35,6 @@ describe('Security Dashboard component', () => { ...@@ -37,7 +35,6 @@ describe('Security Dashboard component', () => {
SecurityDashboardLayout, SecurityDashboardLayout,
}, },
methods: { methods: {
lockFilter: lockFilterSpy,
setPipelineId: setPipelineIdSpy, setPipelineId: setPipelineIdSpy,
fetchPipelineJobs: fetchPipelineJobsSpy, fetchPipelineJobs: fetchPipelineJobsSpy,
}, },
...@@ -53,7 +50,6 @@ describe('Security Dashboard component', () => { ...@@ -53,7 +50,6 @@ describe('Security Dashboard component', () => {
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
lockFilterSpy = jest.fn();
setPipelineIdSpy = jest.fn(); setPipelineIdSpy = jest.fn();
fetchPipelineJobsSpy = jest.fn(); fetchPipelineJobsSpy = jest.fn();
store = createStore(); store = createStore();
...@@ -83,14 +79,6 @@ describe('Security Dashboard component', () => { ...@@ -83,14 +79,6 @@ describe('Security Dashboard component', () => {
expect(wrapper.find(VulnerabilityChart).exists()).toBe(true); expect(wrapper.find(VulnerabilityChart).exists()).toBe(true);
}); });
it('does not lock to a project', () => {
expect(wrapper.vm.isLockedToProject).toBe(false);
});
it('does not lock project filters', () => {
expect(lockFilterSpy).not.toHaveBeenCalled();
});
it('sets the pipeline id', () => { it('sets the pipeline id', () => {
expect(setPipelineIdSpy).toHaveBeenCalledWith(pipelineId); expect(setPipelineIdSpy).toHaveBeenCalledWith(pipelineId);
}); });
...@@ -155,28 +143,6 @@ describe('Security Dashboard component', () => { ...@@ -155,28 +143,6 @@ describe('Security Dashboard component', () => {
); );
}); });
describe('with project lock', () => {
const project = {
id: 123,
};
beforeEach(() => {
createComponent({
lockToProject: project,
});
});
it('locks to a given project', () => {
expect(wrapper.vm.isLockedToProject).toBe(true);
});
it('locks the filters to a given project', () => {
expect(lockFilterSpy).toHaveBeenCalledWith({
filterId: 'project_id',
optionId: project.id,
});
});
});
describe.each` describe.each`
endpointProp | Component endpointProp | Component
${'vulnerabilitiesHistoryEndpoint'} | ${VulnerabilityChart} ${'vulnerabilitiesHistoryEndpoint'} | ${VulnerabilityChart}
...@@ -192,19 +158,6 @@ describe('Security Dashboard component', () => { ...@@ -192,19 +158,6 @@ describe('Security Dashboard component', () => {
}); });
}); });
describe('dismissed vulnerabilities', () => {
it.each`
description | getParameterValuesReturnValue | expected
${'hides dismissed vulnerabilities by default'} | ${[]} | ${true}
${'shows dismissed vulnerabilities if scope param is "all"'} | ${['all']} | ${false}
${'hides dismissed vulnerabilities if scope param is "dismissed"'} | ${['dismissed']} | ${true}
`('$description', ({ getParameterValuesReturnValue, expected }) => {
getParameterValues.mockImplementation(() => getParameterValuesReturnValue);
createComponent();
expect(wrapper.vm.$store.state.filters.hideDismissed).toBe(expected);
});
});
describe('on error', () => { describe('on error', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
......
...@@ -2,9 +2,7 @@ import testAction from 'helpers/vuex_action_helper'; ...@@ -2,9 +2,7 @@ import testAction from 'helpers/vuex_action_helper';
import createState from 'ee/security_dashboard/store/modules/filters/state'; import createState from 'ee/security_dashboard/store/modules/filters/state';
import * as types from 'ee/security_dashboard/store/modules/filters/mutation_types'; import * as types from 'ee/security_dashboard/store/modules/filters/mutation_types';
import * as actions from 'ee/security_dashboard/store/modules/filters/actions'; import * as actions from 'ee/security_dashboard/store/modules/filters/actions';
import { ALL } from 'ee/security_dashboard/store/modules/filters/constants';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { getParameterValues } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
getParameterValues: jest.fn().mockReturnValue([]), getParameterValues: jest.fn().mockReturnValue([]),
...@@ -16,199 +14,28 @@ describe('filters actions', () => { ...@@ -16,199 +14,28 @@ describe('filters actions', () => {
}); });
describe('setFilter', () => { describe('setFilter', () => {
it('should commit the SET_FILTER mutuation', done => { it('should commit the SET_FILTER mutuation', () => {
const state = createState(); const state = createState();
const payload = { filterId: 'report_type', optionId: 'sast' }; const payload = { reportType: ['sast'] };
testAction( return testAction(actions.setFilter, payload, state, [
actions.setFilter,
payload,
state,
[
{
type: types.SET_FILTER,
payload: { ...payload, lazy: false },
},
],
[],
done,
);
});
it('should commit the SET_FILTER mutuation passing through lazy = true', done => {
const state = createState();
const payload = { filterId: 'report_type', optionId: 'sast', lazy: true };
testAction(
actions.setFilter,
payload,
state,
[
{ {
type: types.SET_FILTER, type: types.SET_FILTER,
payload, payload,
}, },
], ]);
[],
done,
);
}); });
}); });
describe('setFilterOptions', () => { describe('toggleHideDismissed', () => {
it('should commit the SET_FILTER_OPTIONS mutuation', done => { it('should commit the TOGGLE_HIDE_DISMISSED mutation', () => {
const state = createState(); const state = createState();
const payload = { filterId: 'project_id', options: [{ id: ALL }] };
testAction( return testAction(actions.toggleHideDismissed, undefined, state, [
actions.setFilterOptions,
payload,
state,
[
{ {
type: types.SET_FILTER_OPTIONS, type: types.TOGGLE_HIDE_DISMISSED,
payload,
},
],
[],
done,
);
});
it('should commit the SET_FILTER_OPTIONS and SET_FILTER mutation when filter selection is invalid', done => {
const state = createState();
const payload = { filterId: 'project_id', options: [{ id: 'foo' }] };
testAction(
actions.setFilterOptions,
payload,
state,
[
{
type: types.SET_FILTER_OPTIONS,
payload,
},
{
type: types.SET_FILTER,
payload: expect.objectContaining({
filterId: 'project_id',
optionId: ALL,
}),
},
],
[],
done,
);
});
it('should commit the SET_FILTER_OPTIONS and SET_FILTER mutation when filter selection is invalid, passing the lazy flag', done => {
const state = createState();
const payload = { filterId: 'project_id', options: [{ id: 'foo' }] };
testAction(
actions.setFilterOptions,
{ ...payload, lazy: true },
state,
[
{
type: types.SET_FILTER_OPTIONS,
payload,
},
{
type: types.SET_FILTER,
payload: {
filterId: 'project_id',
optionId: ALL,
lazy: true,
},
},
],
[],
done,
);
});
});
describe('setAllFilters', () => {
it('should commit the SET_ALL_FILTERS mutuation', done => {
const state = createState();
const payload = { project_id: ['12', '15'] };
testAction(
actions.setAllFilters,
payload,
state,
[
{
type: types.SET_ALL_FILTERS,
payload,
},
],
[],
done,
);
});
});
describe('setHideDismissedToggleInitialState', () => {
[
{
description: 'should set hideDismissed to true if scope param is not present',
returnValue: [],
hideDismissedValue: true,
},
{
description: 'should set hideDismissed to false if scope param is "all"',
returnValue: ['all'],
hideDismissedValue: false,
},
{
description: 'should set hideDismissed to true if scope param is "dismissed"',
returnValue: ['dismissed'],
hideDismissedValue: true,
},
].forEach(testCase => {
it(testCase.description, done => {
getParameterValues.mockReturnValue(testCase.returnValue);
const state = createState();
testAction(
actions.setHideDismissedToggleInitialState,
{},
state,
[
{
type: types.SET_TOGGLE_VALUE,
payload: {
key: 'hideDismissed',
value: testCase.hideDismissedValue,
},
},
],
[],
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,
);
}); });
}); });
}); });
import createState from 'ee/security_dashboard/store/modules/filters/state';
import * as getters from 'ee/security_dashboard/store/modules/filters/getters';
describe('filters module getters', () => {
let state;
beforeEach(() => {
state = createState();
});
describe('activeFilters', () => {
it('should return no severity filters', () => {
const activeFilters = getters.activeFilters(state);
expect(activeFilters.severity).toHaveLength(0);
});
it('should return multiple dummy filters"', () => {
const dummyFilter = {
id: 'dummy',
options: [{ id: 'one' }, { id: 'two' }],
selection: new Set(['one', 'two']),
};
state.filters.push(dummyFilter);
const activeFilters = getters.activeFilters(state);
expect(activeFilters.dummy).toHaveLength(2);
});
});
});
import createState from 'ee/security_dashboard/store/modules/filters/state'; import createState from 'ee/security_dashboard/store/modules/filters/state';
import * as types from 'ee/security_dashboard/store/modules/filters/mutation_types'; import {
SET_FILTER,
TOGGLE_HIDE_DISMISSED,
} from 'ee/security_dashboard/store/modules/filters/mutation_types';
import mutations from 'ee/security_dashboard/store/modules/filters/mutations'; import mutations from 'ee/security_dashboard/store/modules/filters/mutations';
import { ALL } from 'ee/security_dashboard/store/modules/filters/constants'; import { severityFilter } from 'ee/security_dashboard/helpers';
const criticalOption = severityFilter.options.find(x => x.id === 'CRITICAL');
const highOption = severityFilter.options.find(x => x.id === 'HIGH');
describe('filters module mutations', () => { describe('filters module mutations', () => {
let state; let state;
let severityFilter;
let criticalOption;
let highOption;
beforeEach(() => { beforeEach(() => {
state = createState(); state = createState();
[severityFilter] = state.filters;
[, criticalOption, highOption] = severityFilter.options;
}); });
describe('SET_FILTER', () => { describe('SET_FILTER', () => {
beforeEach(() => { it.each`
mutations[types.SET_FILTER](state, { options | expected
filterId: severityFilter.id, ${[]} | ${[]}
optionId: criticalOption.id, ${[criticalOption.id]} | ${[criticalOption.id.toLowerCase()]}
}); ${[criticalOption.id, highOption.id]} | ${[criticalOption.id.toLowerCase(), highOption.id.toLowerCase()]}
}); `('sets the filter to $options', ({ options, expected }) => {
mutations[SET_FILTER](state, { [severityFilter.id]: options });
it('should make critical the selected option', () => {
expect(state.filters[0].selection).toEqual(new Set(['critical']));
});
it('should set to `all` if no option is selected', () => {
mutations[types.SET_FILTER](state, {
filterId: severityFilter.id,
optionId: criticalOption.id,
});
expect(state.filters[0].selection).toEqual(new Set([ALL]));
});
describe('on subsequent changes', () => {
it('should add "high" to the selected options', () => {
mutations[types.SET_FILTER](state, {
filterId: severityFilter.id,
optionId: highOption.id,
});
expect(state.filters[0].selection).toEqual(new Set(['high', 'critical']));
});
});
});
describe('SET_ALL_FILTERS', () => { expect(state.filters[severityFilter.id]).toEqual(expected);
it('should set options if they are a single string', () => {
mutations[types.SET_ALL_FILTERS](state, { [severityFilter.id]: criticalOption.id });
const expected = new Set([criticalOption.id]);
expect(state.filters[0].selection).toEqual(expected);
});
it('should set options if they are given as an array', () => {
mutations[types.SET_ALL_FILTERS](state, {
[severityFilter.id]: [criticalOption.id, highOption.id],
}); });
const expected = new Set([criticalOption.id, highOption.id]); it('sets multiple filters correctly with the right casing', () => {
const filter1 = { oneWord: ['ABC', 'DEF'] };
expect(state.filters[0].selection).toEqual(expected); const filter2 = { twoWords: ['123', '456'] };
}); const filter3 = { threeTotalWords: ['Abc123', 'dEF456'] };
it('should set options to `all` if no payload is given', () => {
mutations[types.SET_ALL_FILTERS](state);
const expected = new Set([ALL]);
state.filters.forEach(filter => { mutations[SET_FILTER](state, filter1);
expect(filter.selection).toEqual(expected); mutations[SET_FILTER](state, filter2);
}); mutations[SET_FILTER](state, filter3);
});
it('should set options to `all` if payload contains an empty array', () => { expect(state.filters).toMatchObject({
mutations[types.SET_ALL_FILTERS](state, { one_word: ['abc', 'def'],
[severityFilter.id]: [], two_words: ['123', '456'],
three_total_words: ['abc123', 'def456'],
}); });
const expected = new Set([ALL]);
expect(state.filters[0].selection).toEqual(expected);
}); });
}); });
describe('SET_FILTER_OPTIONS', () => { describe('TOGGLE_HIDE_DISMISSED', () => {
const options = [{ id: 0, name: 'c' }, { id: 3, name: 'c' }]; it('toggles scope filter', () => {
const toggleAndCheck = expected => {
beforeEach(() => { mutations[TOGGLE_HIDE_DISMISSED](state);
const filterId = severityFilter.id; expect(state.filters.scope).toBe(expected);
};
mutations[types.SET_FILTER_OPTIONS](state, { filterId, options });
});
it('should add all the options to the type filter', () => { toggleAndCheck('all');
expect(severityFilter.options).toEqual(options); toggleAndCheck('dismissed');
toggleAndCheck('all');
}); });
}); });
}); });
import { ALL } from 'ee/security_dashboard/store/modules/filters/constants';
import { hasValidSelection, setFilter } from 'ee/security_dashboard/store/modules/filters/utils';
describe('filters module utils', () => {
describe('hasValidSelection', () => {
describe.each`
selection | options | expected
${[]} | ${[]} | ${true}
${[]} | ${['foo']} | ${true}
${['foo']} | ${['foo']} | ${true}
${['foo']} | ${['foo', 'bar']} | ${true}
${['bar', 'foo']} | ${['foo', 'bar']} | ${true}
${['foo']} | ${[]} | ${false}
${['foo']} | ${['bar']} | ${false}
${['foo', 'bar']} | ${['foo']} | ${false}
`('given selection $selection and options $options', ({ selection, options, expected }) => {
let filter;
beforeEach(() => {
filter = {
selection,
options: options.map(id => ({ id })),
};
});
it(`return ${expected}`, () => {
expect(hasValidSelection(filter)).toBe(expected);
});
});
});
describe('setFilter', () => {
const filterId = 'foo';
const option1 = 'bar';
const option2 = 'baz';
const initFilters = (initiallySelected = [ALL]) => [
{ id: filterId, selection: new Set(initiallySelected) },
];
let filters;
let filter;
describe('when ALL is initially selected', () => {
beforeEach(() => {
filters = initFilters();
});
describe('when a valid filter is passed', () => {
beforeEach(() => {
[filter] = setFilter(filters, { filterId, optionId: option1 });
});
it('should select the passed option', () => {
expect(filter.selection.has(option1)).toBe(true);
});
it('should remove the `ALL` option', () => {
expect(filter.selection.has(ALL)).toBe(false);
});
});
describe('when an invalid filter is passed ', () => {
beforeEach(() => {
[filter] = setFilter(filters, { filterId: 'baz', optionId: option1 });
});
it('should not select the passed option', () => {
expect(filter.selection.has(option1)).toBe(false);
});
it('should not remove the `ALL` option', () => {
expect(filter.selection.has(ALL)).toBe(true);
});
});
});
describe('when an option is initially selected', () => {
beforeEach(() => {
filters = initFilters([option1]);
});
describe('when the selected option is passed', () => {
beforeEach(() => {
[filter] = setFilter(filters, { filterId, optionId: option1 });
});
it('should remove the passed option', () => {
expect(filter.selection.has(option1)).toBe(false);
});
it('should select the `ALL` option', () => {
expect(filter.selection.has(ALL)).toBe(true);
});
});
describe('when another option is passed ', () => {
beforeEach(() => {
[filter] = setFilter(filters, { filterId, optionId: option2 });
});
it('should not remove the initially selected option', () => {
expect(filter.selection.has(option1)).toBe(true);
});
it('should add the passed selected option', () => {
expect(filter.selection.has(option2)).toBe(true);
});
it('should not select the `ALL` option', () => {
expect(filter.selection.has(ALL)).toBe(false);
});
});
});
describe('when two options are initially selected', () => {
beforeEach(() => {
filters = initFilters([option1, option2]);
});
describe('when a selected option is passed', () => {
beforeEach(() => {
[filter] = setFilter(filters, { filterId, optionId: option1 });
});
it('should remove the passed option', () => {
expect(filter.selection.has(option1)).toBe(false);
});
it('should not remove the other option', () => {
expect(filter.selection.has(option2)).toBe(true);
});
it('should not select the `ALL` option', () => {
expect(filter.selection.has(ALL)).toBe(false);
});
});
});
});
});
import createStore from 'ee/security_dashboard/store/index'; import createStore from 'ee/security_dashboard/store/index';
import * as filtersMutationTypes from 'ee/security_dashboard/store/modules/filters/mutation_types'; import {
import * as vulnerabilityMutationTypes from 'ee/security_dashboard/store/modules/vulnerabilities/mutation_types'; SET_FILTER,
TOGGLE_HIDE_DISMISSED,
} from 'ee/security_dashboard/store/modules/filters/mutation_types';
function expectRefreshDispatches(store, payload) { function expectRefreshDispatches(store, payload) {
expect(store.dispatch).toHaveBeenCalledTimes(2); expect(store.dispatch).toHaveBeenCalledTimes(2);
...@@ -20,51 +22,24 @@ describe('mediator', () => { ...@@ -20,51 +22,24 @@ describe('mediator', () => {
}); });
it('triggers fetching vulnerabilities after one filter changes', () => { it('triggers fetching vulnerabilities after one filter changes', () => {
store.commit(`filters/${filtersMutationTypes.SET_FILTER}`, {}); store.commit(`filters/${SET_FILTER}`, {});
const activeFilters = store.getters['filters/activeFilters'];
expectRefreshDispatches(store, activeFilters); expectRefreshDispatches(store, store.state.filters.filters);
}); });
it('does not fetch vulnerabilities after one filter changes with lazy = true', () => { it('triggers fetching vulnerabilities after multiple filters change', () => {
store.commit(`filters/${filtersMutationTypes.SET_FILTER}`, { lazy: true }); const filters = {
filter1: ['abc', 'def'],
expect(store.dispatch).not.toHaveBeenCalled(); filter2: ['123', '456'],
});
it('triggers fetching vulnerabilities after filters change', () => {
const payload = {
...store.getters['filters/activeFilters'],
page: store.state.vulnerabilities.pageInfo.page,
}; };
store.commit(`filters/${SET_FILTER}`, filters);
store.commit(`filters/${filtersMutationTypes.SET_ALL_FILTERS}`, {}); expectRefreshDispatches(store, expect.objectContaining(filters));
expectRefreshDispatches(store, payload);
});
it('triggers fetching vulnerabilities multiple vulnerabilities have been dismissed', () => {
const activeFilters = store.getters['filters/activeFilters'];
store.commit(
`vulnerabilities/${vulnerabilityMutationTypes.RECEIVE_DISMISS_SELECTED_VULNERABILITIES_SUCCESS}`,
{},
);
expectRefreshDispatches(store, activeFilters);
}); });
it('triggers fetching vulnerabilities after "Hide dismissed" toggle changes', () => { it('triggers fetching vulnerabilities after "Hide dismissed" toggle changes', () => {
const activeFilters = store.getters['filters/activeFilters']; store.commit(`filters/${TOGGLE_HIDE_DISMISSED}`);
store.commit(`filters/${filtersMutationTypes.SET_TOGGLE_VALUE}`, {});
expectRefreshDispatches(store, activeFilters);
});
it('does not fetch vulnerabilities after "Hide dismissed" toggle changes with lazy = true', () => {
store.commit(`filters/${filtersMutationTypes.SET_TOGGLE_VALUE}`, { lazy: true });
expect(store.dispatch).not.toHaveBeenCalled(); expectRefreshDispatches(store, store.state.filters.filters);
}); });
}); });
import Vuex from 'vuex';
import createStore from 'ee/security_dashboard/store';
import { BASE_FILTERS } from 'ee/security_dashboard/store/modules/filters/constants';
import projectSelectorModule from 'ee/security_dashboard/store/modules/project_selector';
import projectSelectorPlugin from 'ee/security_dashboard/store/plugins/project_selector';
import * as projectSelectorMutationTypes from 'ee/security_dashboard/store/modules/projects/mutation_types';
describe('project selector plugin', () => {
let store;
beforeEach(() => {
jest.spyOn(Vuex.Store.prototype, 'registerModule');
store = createStore({ plugins: [projectSelectorPlugin] });
});
it('registers the project selector module on the store', () => {
expect(Vuex.Store.prototype.registerModule).toHaveBeenCalledTimes(1);
expect(Vuex.Store.prototype.registerModule).toHaveBeenCalledWith(
'projectSelector',
projectSelectorModule(),
);
});
it('sets project filter options with lazy = true after projects have been received', () => {
jest.spyOn(store, 'dispatch').mockImplementation();
const projects = [{ name: 'foo', id: '1' }];
store.commit(
`projectSelector/${projectSelectorMutationTypes.RECEIVE_PROJECTS_SUCCESS}`,
projects,
);
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith('filters/setFilterOptions', {
filterId: 'project_id',
options: [BASE_FILTERS.project_id, ...projects],
lazy: true,
});
});
});
import Vuex from 'vuex';
import createStore from 'ee/security_dashboard/store';
import projectsModule from 'ee/security_dashboard/store/modules/projects';
import projectsPlugin from 'ee/security_dashboard/store/plugins/projects';
import * as projectsMutationTypes from 'ee/security_dashboard/store/modules/projects/mutation_types';
import { BASE_FILTERS } from 'ee/security_dashboard/store/modules/filters/constants';
describe('projects plugin', () => {
let store;
beforeEach(() => {
jest.spyOn(Vuex.Store.prototype, 'registerModule');
store = createStore({ plugins: [projectsPlugin] });
});
it('registers the projects module on the store', () => {
expect(Vuex.Store.prototype.registerModule).toHaveBeenCalledTimes(1);
expect(Vuex.Store.prototype.registerModule).toHaveBeenCalledWith('projects', projectsModule);
});
it('sets project filter options after projects have been received', () => {
jest.spyOn(store, 'dispatch').mockImplementation();
const projectOption = { name: 'foo', id: '1' };
store.commit(`projects/${projectsMutationTypes.RECEIVE_PROJECTS_SUCCESS}`, {
projects: [{ ...projectOption, irrelevantProperty: 'foobar' }],
});
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith(
'filters/setFilterOptions',
Object({
filterId: 'project_id',
options: [BASE_FILTERS.project_id, projectOption],
}),
);
});
});
...@@ -23972,9 +23972,6 @@ msgstr "" ...@@ -23972,9 +23972,6 @@ msgstr ""
msgid "SecurityReports|Scan details" msgid "SecurityReports|Scan details"
msgstr "" msgstr ""
msgid "SecurityReports|Scanner"
msgstr ""
msgid "SecurityReports|Security Dashboard" msgid "SecurityReports|Security Dashboard"
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