Commit 86d3dcae authored by Simon Knox's avatar Simon Knox

Merge branch '210327-fix-pipeline-security-tab-filters' into 'master'

Fix pipeline security tab filters not showing

See merge request gitlab-org/gitlab!47294
parents f3b3cc3f 0432a5f6
---
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],
}),
);
});
});
...@@ -24036,9 +24036,6 @@ msgstr "" ...@@ -24036,9 +24036,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