Commit a7601b55 authored by Phil Hughes's avatar Phil Hughes

Merge branch '8893-persist-security-dashboard-state-in-url-ee' into 'master'

Persist Group Level Security Dashboard state in URL

See merge request gitlab-org/gitlab-ee!9108
parents 930d2475 f52c9926
...@@ -64,7 +64,9 @@ export default { ...@@ -64,7 +64,9 @@ export default {
this.setVulnerabilitiesEndpoint(this.vulnerabilitiesEndpoint); this.setVulnerabilitiesEndpoint(this.vulnerabilitiesEndpoint);
this.setVulnerabilitiesCountEndpoint(this.vulnerabilitiesCountEndpoint); this.setVulnerabilitiesCountEndpoint(this.vulnerabilitiesCountEndpoint);
this.setVulnerabilitiesHistoryEndpoint(this.vulnerabilitiesHistoryEndpoint); this.setVulnerabilitiesHistoryEndpoint(this.vulnerabilitiesHistoryEndpoint);
this.fetchVulnerabilitiesCount(); this.fetchVulnerabilities(this.activeFilters);
this.fetchVulnerabilitiesCount(this.activeFilters);
this.fetchVulnerabilitiesHistory(this.activeFilters);
this.fetchProjects(); this.fetchProjects();
}, },
methods: { methods: {
...@@ -80,18 +82,13 @@ export default { ...@@ -80,18 +82,13 @@ export default {
'setVulnerabilitiesHistoryEndpoint', 'setVulnerabilitiesHistoryEndpoint',
]), ]),
...mapActions('projects', ['setProjectsEndpoint', 'fetchProjects']), ...mapActions('projects', ['setProjectsEndpoint', 'fetchProjects']),
filterChange() {
this.fetchVulnerabilities(this.activeFilters);
this.fetchVulnerabilitiesCount(this.activeFilters);
this.fetchVulnerabilitiesHistory(this.activeFilters);
},
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<filters :dashboard-documentation="dashboardDocumentation" @change="filterChange" /> <filters :dashboard-documentation="dashboardDocumentation" />
<vulnerability-count-list /> <vulnerability-count-list />
<h4 class="my-4">{{ __('Vulnerability Chart') }}</h4> <h4 class="my-4">{{ __('Vulnerability Chart') }}</h4>
<vulnerability-chart /> <vulnerability-chart />
......
...@@ -26,6 +26,9 @@ export default { ...@@ -26,6 +26,9 @@ export default {
filter() { filter() {
return this.getFilter(this.filterId); return this.getFilter(this.filterId);
}, },
selection() {
return this.getFilter(this.filterId).selection;
},
selectedOptionText() { selectedOptionText() {
return this.getSelectedOptionNames(this.filterId) || '-'; return this.getSelectedOptionNames(this.filterId) || '-';
}, },
...@@ -37,7 +40,9 @@ export default { ...@@ -37,7 +40,9 @@ export default {
filterId: this.filterId, filterId: this.filterId,
optionId: option.id, optionId: option.id,
}); });
this.$emit('change'); },
isSelected(option) {
return this.selection.has(option.id);
}, },
}, },
}; };
...@@ -57,11 +62,11 @@ export default { ...@@ -57,11 +62,11 @@ export default {
@click="clickFilter(option)" @click="clickFilter(option)"
> >
<icon <icon
v-if="option.selected" v-if="isSelected(option)"
class="vertical-align-middle js-check" class="vertical-align-middle js-check"
name="mobile-issue-close" name="mobile-issue-close"
/> />
<span class="vertical-align-middle" :class="{ 'prepend-left-20': !option.selected }">{{ <span class="vertical-align-middle" :class="{ 'prepend-left-20': !isSelected(option) }">{{
option.name option.name
}}</span> }}</span>
</gl-dropdown-item> </gl-dropdown-item>
......
...@@ -27,7 +27,6 @@ export default { ...@@ -27,7 +27,6 @@ export default {
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-id="filter.id" :filter-id="filter.id"
:dashboard-documentation="dashboardDocumentation" :dashboard-documentation="dashboardDocumentation"
@change="$emit('change')"
/> />
</div> </div>
</div> </div>
......
...@@ -43,9 +43,6 @@ export default { ...@@ -43,9 +43,6 @@ export default {
return this.pageInfo && this.pageInfo.total; return this.pageInfo && this.pageInfo.total;
}, },
}, },
created() {
this.fetchVulnerabilities();
},
methods: { methods: {
...mapActions('vulnerabilities', ['fetchVulnerabilities', 'openModal']), ...mapActions('vulnerabilities', ['fetchVulnerabilities', 'openModal']),
fetchPage(page) { fetchPage(page) {
......
<script> <script>
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { mapState, mapActions } from 'vuex'; import { mapState } from 'vuex';
import { GlChart } from '@gitlab/ui/dist/charts'; import { GlChart } from '@gitlab/ui/dist/charts';
import ChartTooltip from './vulnerability_chart_tooltip.vue'; import ChartTooltip from './vulnerability_chart_tooltip.vue';
...@@ -142,11 +142,7 @@ export default { ...@@ -142,11 +142,7 @@ export default {
}; };
}, },
}, },
created() {
this.fetchVulnerabilitiesHistory();
},
methods: { methods: {
...mapActions('vulnerabilities', ['fetchVulnerabilitiesHistory']),
renderTooltip(params, ticket, callback) { renderTooltip(params, ticket, callback) {
this.tooltipTitle = dateFormat(params[0].axisValue, 'd mmmm'); this.tooltipTitle = dateFormat(params[0].axisValue, 'd mmmm');
this.tooltipEntries = params; this.tooltipEntries = params;
......
import Vue from 'vue'; import Vue from 'vue';
import GroupSecurityDashboardApp from './components/app.vue'; import GroupSecurityDashboardApp from './components/app.vue';
import store from './store'; import createStore from './store';
import router from './store/router';
export default () => { export default () => {
const el = document.getElementById('js-group-security-dashboard'); const el = document.getElementById('js-group-security-dashboard');
const store = createStore();
return new Vue({ return new Vue({
el, el,
store, store,
router,
components: { components: {
GroupSecurityDashboardApp, GroupSecurityDashboardApp,
}, },
......
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import router from './router';
import configureModerator from './moderator'; import configureModerator from './moderator';
import filters from './modules/filters/index'; import filters from './modules/filters/index';
import projects from './modules/projects/index'; import projects from './modules/projects/index';
...@@ -7,14 +8,18 @@ import vulnerabilities from './modules/vulnerabilities/index'; ...@@ -7,14 +8,18 @@ import vulnerabilities from './modules/vulnerabilities/index';
Vue.use(Vuex); Vue.use(Vuex);
const store = new Vuex.Store({ export default () => {
modules: { const store = new Vuex.Store({
filters, modules: {
projects, filters,
vulnerabilities, projects,
}, vulnerabilities,
}); },
});
configureModerator(store); store.$router = router;
export default () => store; configureModerator(store);
return store;
};
import * as vulnerabilitiesMutationTypes from './modules/vulnerabilities/mutation_types';
import * as filtersMutationTypes from './modules/filters/mutation_types';
import * as projectsMutationTypes from './modules/projects/mutation_types'; import * as projectsMutationTypes from './modules/projects/mutation_types';
export default function configureModerator(store) { export default function configureModerator(store) {
store.$router.beforeEach((to, from, next) => {
const updatedFromState = (to.params && to.params.updatedFromState) || false;
if (to.name === 'dashboard' && !updatedFromState) {
store.dispatch(`filters/setAllFilters`, to.query);
}
next();
});
store.subscribe(({ type, payload }) => { store.subscribe(({ type, payload }) => {
switch (type) { switch (type) {
case `projects/${projectsMutationTypes.RECEIVE_PROJECTS_SUCCESS}`: case `projects/${projectsMutationTypes.RECEIVE_PROJECTS_SUCCESS}`:
return store.dispatch('filters/setFilterOptions', { store.dispatch('filters/setFilterOptions', {
filterId: 'project_id', filterId: 'project_id',
options: [ options: [
{ {
name: 'All', name: 'All',
id: 'all', id: 'all',
selected: true,
}, },
...payload.projects.map(project => ({ ...payload.projects.map(project => ({
name: project.name, name: project.name,
id: project.id.toString(), id: project.id.toString(),
selected: false,
})), })),
], ],
}); });
break;
case `filters/${filtersMutationTypes.SET_ALL_FILTERS}`:
case `filters/${filtersMutationTypes.SET_FILTER}`: {
const activeFilters = store.getters['filters/activeFilters'];
store.dispatch('vulnerabilities/fetchVulnerabilities', activeFilters);
store.dispatch('vulnerabilities/fetchVulnerabilitiesCount', activeFilters);
store.dispatch('vulnerabilities/fetchVulnerabilitiesHistory', activeFilters);
break;
}
case `vulnerabilities/${vulnerabilitiesMutationTypes.RECEIVE_VULNERABILITIES_SUCCESS}`: {
const activeFilters = store.getters['filters/activeFilters'];
store.$router.push({
name: 'dashboard',
query: activeFilters,
params: { updatedFromState: true },
});
break;
}
default: default:
return null;
} }
}); });
} }
...@@ -14,6 +14,10 @@ export const setFilterOptions = ({ commit }, payload) => { ...@@ -14,6 +14,10 @@ export const setFilterOptions = ({ commit }, payload) => {
commit(types.SET_FILTER_OPTIONS, payload); commit(types.SET_FILTER_OPTIONS, payload);
}; };
export const setAllFilters = ({ commit }, payload) => {
commit(types.SET_ALL_FILTERS, payload);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
// This is no longer needed after gitlab-ce#52179 is merged // This is no longer needed after gitlab-ce#52179 is merged
export default () => {}; export default () => {};
...@@ -2,11 +2,10 @@ import { sprintf, __ } from '~/locale'; ...@@ -2,11 +2,10 @@ import { sprintf, __ } from '~/locale';
export const getFilter = state => filterId => state.filters.find(filter => filter.id === filterId); export const getFilter = state => filterId => state.filters.find(filter => filter.id === filterId);
export const getSelectedOptions = (state, getters) => filterId => export const getSelectedOptions = (state, getters) => filterId => {
getters.getFilter(filterId).options.filter(option => option.selected); const filter = getters.getFilter(filterId);
return filter.options.filter(option => filter.selection.has(option.id));
export const getSelectedOptionIds = (state, getters) => filterId => };
getters.getSelectedOptions(filterId).map(option => option.id);
export const getSelectedOptionNames = (state, getters) => filterId => { export const getSelectedOptionNames = (state, getters) => filterId => {
const selectedOptions = getters.getSelectedOptions(filterId); const selectedOptions = getters.getSelectedOptions(filterId);
...@@ -21,22 +20,17 @@ export const getSelectedOptionNames = (state, getters) => filterId => { ...@@ -21,22 +20,17 @@ export const getSelectedOptionNames = (state, getters) => filterId => {
: firstOption; : firstOption;
}; };
export const getFilterIds = state => state.filters.map(filter => filter.id);
/** /**
* Loops through all the filters and returns all the active ones * Loops through all the filters and returns all the active ones
* stripping out any that are set to 'all' * stripping out any that are set to 'all'
* @returns Object * @returns Object
* e.g. { type: ['sast'], severity: ['high', 'medium'] } * e.g. { type: ['sast'], severity: ['high', 'medium'] }
*/ */
export const activeFilters = (state, getters) => export const activeFilters = state =>
getters.getFilterIds.reduce( state.filters.reduce((acc, filter) => {
(result, filterId) => ({ acc[filter.id] = [...filter.selection].filter(option => option !== 'all');
...result, return acc;
[filterId]: getters.getSelectedOptionIds(filterId).filter(option => option !== 'all'), }, {});
}),
{},
);
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
// This is no longer needed after gitlab-ce#52179 is merged // This is no longer needed after gitlab-ce#52179 is merged
......
export const SET_FILTER = 'SET_FILTER'; export const SET_FILTER = 'SET_FILTER';
export const SET_FILTER_OPTIONS = 'SET_FILTER_OPTIONS'; export const SET_FILTER_OPTIONS = 'SET_FILTER_OPTIONS';
export const SET_ALL_FILTERS = 'SET_ALL_FILTERS';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
[types.SET_ALL_FILTERS](state, payload = {}) {
state.filters = state.filters.map(filter => {
// If the payload is empty, we fall back to an empty selection
const selectedOptions = (payload && payload[filter.id]) || [];
const selection = Array.isArray(selectedOptions)
? new Set(selectedOptions)
: new Set([selectedOptions]);
// This prevents us from selecting nothing at all
if (selection.size === 0) {
selection.add('all');
}
return { ...filter, selection };
});
},
[types.SET_FILTER](state, payload) { [types.SET_FILTER](state, payload) {
const { filterId, optionId } = payload; const { filterId, optionId } = payload;
const activeFilter = state.filters.find(filter => filter.id === filterId); const activeFilter = state.filters.find(filter => filter.id === filterId);
if (activeFilter) { if (activeFilter) {
let activeOptions; let selection = new Set(activeFilter.selection);
if (optionId === 'all') { if (optionId === 'all') {
activeOptions = activeFilter.options.map(option => ({ selection = new Set(['all']);
...option,
selected: option.id === optionId,
}));
} else { } else {
activeOptions = activeFilter.options.map(option => { selection.delete('all');
if (option.id === 'all') { if (selection.has(optionId)) {
return { selection.delete(optionId);
...option, } else {
selected: false, selection.add(optionId);
}; }
}
if (option.id === optionId) {
return {
...option,
selected: !option.selected,
};
}
return option;
});
} }
// This prevents us from selecting nothing at all // This prevents us from selecting nothing at all
if (!activeOptions.find(option => option.selected)) { if (selection.size === 0) {
activeOptions[0].selected = true; selection.add('all');
} }
activeFilter.selection = selection;
activeFilter.options = activeOptions;
} }
}, },
[types.SET_FILTER_OPTIONS](state, payload) { [types.SET_FILTER_OPTIONS](state, payload) {
......
...@@ -9,13 +9,13 @@ export default () => ({ ...@@ -9,13 +9,13 @@ export default () => ({
{ {
name: 'All', name: 'All',
id: 'all', id: 'all',
selected: true,
}, },
...Object.entries(SEVERITIES).map(severity => { ...Object.entries(SEVERITIES).map(severity => {
const [id, name] = severity; const [id, name] = severity;
return { id, name }; return { id, name };
}), }),
], ],
selection: new Set(['all']),
}, },
{ {
name: 'Report type', name: 'Report type',
...@@ -24,13 +24,13 @@ export default () => ({ ...@@ -24,13 +24,13 @@ export default () => ({
{ {
name: 'All', name: 'All',
id: 'all', id: 'all',
selected: true,
}, },
...Object.entries(REPORT_TYPES).map(type => { ...Object.entries(REPORT_TYPES).map(type => {
const [id, name] = type; const [id, name] = type;
return { id, name }; return { id, name };
}), }),
], ],
selection: new Set(['all']),
}, },
{ {
name: 'Project', name: 'Project',
...@@ -39,9 +39,9 @@ export default () => ({ ...@@ -39,9 +39,9 @@ export default () => ({
{ {
name: 'All', name: 'All',
id: 'all', id: 'all',
selected: true,
}, },
], ],
selection: new Set(['all']),
}, },
], ],
}); });
...@@ -14,6 +14,9 @@ export const setVulnerabilitiesCountEndpoint = ({ commit }, endpoint) => { ...@@ -14,6 +14,9 @@ export const setVulnerabilitiesCountEndpoint = ({ commit }, endpoint) => {
}; };
export const fetchVulnerabilitiesCount = ({ state, dispatch }, params = {}) => { export const fetchVulnerabilitiesCount = ({ state, dispatch }, params = {}) => {
if (!state.vulnerabilitiesCountEndpoint) {
return;
}
dispatch('requestVulnerabilitiesCount'); dispatch('requestVulnerabilitiesCount');
axios({ axios({
...@@ -43,6 +46,9 @@ export const receiveVulnerabilitiesCountError = ({ commit }) => { ...@@ -43,6 +46,9 @@ export const receiveVulnerabilitiesCountError = ({ commit }) => {
}; };
export const fetchVulnerabilities = ({ state, dispatch }, params = {}) => { export const fetchVulnerabilities = ({ state, dispatch }, params = {}) => {
if (!state.vulnerabilitiesEndpoint) {
return;
}
dispatch('requestVulnerabilities'); dispatch('requestVulnerabilities');
axios({ axios({
...@@ -208,6 +214,9 @@ export const setVulnerabilitiesHistoryEndpoint = ({ commit }, endpoint) => { ...@@ -208,6 +214,9 @@ export const setVulnerabilitiesHistoryEndpoint = ({ commit }, endpoint) => {
}; };
export const fetchVulnerabilitiesHistory = ({ state, dispatch }, params = {}) => { export const fetchVulnerabilitiesHistory = ({ state, dispatch }, params = {}) => {
if (!state.vulnerabilitiesHistoryEndpoint) {
return;
}
dispatch('requestVulnerabilitiesHistory'); dispatch('requestVulnerabilitiesHistory');
axios({ axios({
......
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
// Unfortunately Vue Router doesn't work without at least a fake component
// If you do only data handling
const EmptyRouterComponent = {
render(createElement) {
return createElement('div');
},
};
const routes = [{ path: '/', name: 'dashboard', component: EmptyRouterComponent }];
const router = new VueRouter({
mode: 'history',
base: window.location.pathname,
routes,
});
export default router;
---
title: Persist Group Level Security Dashboard state in URL
merge_request: 9108
author:
type: added
import Vue from 'vue'; import Vue from 'vue';
import MockAdapater from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import component from 'ee/security_dashboard/components/security_dashboard_table.vue'; import component from 'ee/security_dashboard/components/security_dashboard_table.vue';
import createStore from 'ee/security_dashboard/store'; import createStore from 'ee/security_dashboard/store';
import { TEST_HOST } from 'spec/test_constants'; import { TEST_HOST } from 'spec/test_constants';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import waitForPromises from 'spec/helpers/wait_for_promises';
import {
RECEIVE_VULNERABILITIES_ERROR,
RECEIVE_VULNERABILITIES_SUCCESS,
REQUEST_VULNERABILITIES,
} from 'ee/security_dashboard/store/modules/vulnerabilities/mutation_types';
import { resetStore } from '../helpers'; import { resetStore } from '../helpers';
import mockDataVulnerabilities from '../store/vulnerabilities/data/mock_data_vulnerabilities.json'; import mockDataVulnerabilities from '../store/vulnerabilities/data/mock_data_vulnerabilities.json';
...@@ -19,11 +22,9 @@ describe('Security Dashboard Table', () => { ...@@ -19,11 +22,9 @@ describe('Security Dashboard Table', () => {
emptyStateSvgPath: TEST_HOST, emptyStateSvgPath: TEST_HOST,
}; };
let store; let store;
let mock;
let vm; let vm;
beforeEach(() => { beforeEach(() => {
mock = new MockAdapater(axios);
store = createStore(); store = createStore();
store.state.vulnerabilities.vulnerabilitiesEndpoint = vulnerabilitiesEndpoint; store.state.vulnerabilities.vulnerabilitiesEndpoint = vulnerabilitiesEndpoint;
}); });
...@@ -31,12 +32,11 @@ describe('Security Dashboard Table', () => { ...@@ -31,12 +32,11 @@ describe('Security Dashboard Table', () => {
afterEach(() => { afterEach(() => {
resetStore(store); resetStore(store);
vm.$destroy(); vm.$destroy();
mock.restore();
}); });
describe('while loading', () => { describe('while loading', () => {
beforeEach(() => { beforeEach(() => {
store.dispatch('vulnerabilities/requestVulnerabilities'); store.commit(`vulnerabilities/${REQUEST_VULNERABILITIES}`);
vm = mountComponentWithStore(Component, { store, props }); vm = mountComponentWithStore(Component, { store, props });
}); });
...@@ -47,60 +47,42 @@ describe('Security Dashboard Table', () => { ...@@ -47,60 +47,42 @@ describe('Security Dashboard Table', () => {
describe('with a list of vulnerabilities', () => { describe('with a list of vulnerabilities', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(vulnerabilitiesEndpoint).replyOnce(200, mockDataVulnerabilities); store.commit(`vulnerabilities/${RECEIVE_VULNERABILITIES_SUCCESS}`, {
vulnerabilities: mockDataVulnerabilities,
});
vm = mountComponentWithStore(Component, { store, props }); vm = mountComponentWithStore(Component, { store, props });
}); });
it('should render a row for each vulnerability', done => { it('should render a row for each vulnerability', () => {
waitForPromises() expect(vm.$el.querySelectorAll('.vulnerabilities-row')).toHaveLength(
.then(() => { mockDataVulnerabilities.length,
expect(vm.$el.querySelectorAll('.vulnerabilities-row')).toHaveLength( );
mockDataVulnerabilities.length,
);
done();
})
.catch(done.fail);
}); });
}); });
describe('with no vulnerabilties', () => { describe('with no vulnerabilties', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(vulnerabilitiesEndpoint).replyOnce(200, []); store.commit(`vulnerabilities/${RECEIVE_VULNERABILITIES_SUCCESS}`, { vulnerabilities: [] });
vm = mountComponentWithStore(Component, { store, props }); vm = mountComponentWithStore(Component, { store, props });
}); });
it('should render the empty state', done => { it('should render the empty state', () => {
waitForPromises() expect(vm.$el.querySelector('.empty-state')).not.toBeNull();
.then(() => {
expect(vm.$el.querySelector('.empty-state')).not.toBeNull();
done();
})
.catch(done.fail);
}); });
}); });
describe('on error', () => { describe('on error', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(vulnerabilitiesEndpoint).replyOnce(404, []); store.commit(`vulnerabilities/${RECEIVE_VULNERABILITIES_ERROR}`);
vm = mountComponentWithStore(Component, { store, props }); vm = mountComponentWithStore(Component, { store, props });
}); });
it('should not render the empty state', done => { it('should not render the empty state', () => {
waitForPromises() expect(vm.$el.querySelector('.empty-state')).toBeNull();
.then(() => {
expect(vm.$el.querySelector('.empty-state')).toBeNull();
done();
})
.catch(done.fail);
}); });
it('should render the error alert', done => { it('should render the error alert', () => {
waitForPromises() expect(vm.$el.querySelector('.flash-alert')).not.toBeNull();
.then(() => {
expect(vm.$el.querySelector('.flash-alert')).not.toBeNull();
done();
})
.catch(done.fail);
}); });
}); });
}); });
...@@ -46,4 +46,25 @@ describe('filters actions', () => { ...@@ -46,4 +46,25 @@ describe('filters actions', () => {
); );
}); });
}); });
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,
);
});
});
}); });
...@@ -6,15 +6,10 @@ describe('filters module getters', () => { ...@@ -6,15 +6,10 @@ describe('filters module getters', () => {
const getFilter = filterId => getters.getFilter(state)(filterId); const getFilter = filterId => getters.getFilter(state)(filterId);
const getSelectedOptions = filterId => const getSelectedOptions = filterId =>
getters.getSelectedOptions(state, { getFilter })(filterId); getters.getSelectedOptions(state, { getFilter })(filterId);
const getSelectedOptionIds = filterId =>
getters.getSelectedOptionIds(state, { getSelectedOptions })(filterId);
const getFilterIds = getters.getFilterIds(state);
return { return {
getFilter, getFilter,
getSelectedOptions, getSelectedOptions,
getSelectedOptionIds,
getFilterIds,
}; };
}; };
let state; let state;
...@@ -49,7 +44,8 @@ describe('filters module getters', () => { ...@@ -49,7 +44,8 @@ describe('filters module getters', () => {
filters: [ filters: [
{ {
id: 'severity', id: 'severity',
options: [{ id: 'critical', selected: true }, { id: 'high', selected: true }], options: [{ id: 'critical' }, { id: 'high' }],
selection: new Set(['critical', 'high']),
}, },
], ],
}; };
...@@ -60,20 +56,6 @@ describe('filters module getters', () => { ...@@ -60,20 +56,6 @@ describe('filters module getters', () => {
}); });
}); });
describe('getSelectedOptionIds', () => {
it('should return "one" as the selcted dummy ID', () => {
const dummyFilter = {
id: 'dummy',
options: [{ id: 'one', selected: true }, { id: 'anotherone', selected: false }],
};
state.filters.push(dummyFilter);
const selectedOptionIds = getters.getSelectedOptionIds(state, mockedGetters(state))('dummy');
expect(selectedOptionIds).toHaveLength(1);
expect(selectedOptionIds[0]).toEqual('one');
});
});
describe('getSelectedOptionNames', () => { describe('getSelectedOptionNames', () => {
it('should return "All" as the selected option', () => { it('should return "All" as the selected option', () => {
const selectedOptionNames = getters.getSelectedOptionNames(state, mockedGetters(state))( const selectedOptionNames = getters.getSelectedOptionNames(state, mockedGetters(state))(
...@@ -88,7 +70,8 @@ describe('filters module getters', () => { ...@@ -88,7 +70,8 @@ describe('filters module getters', () => {
filters: [ filters: [
{ {
id: 'severity', id: 'severity',
options: [{ name: 'Critical', selected: true }, { name: 'High', selected: true }], options: [{ name: 'Critical', id: 1 }, { name: 'High', id: 2 }],
selection: new Set([1, 2]),
}, },
], ],
}; };
...@@ -110,7 +93,8 @@ describe('filters module getters', () => { ...@@ -110,7 +93,8 @@ describe('filters module getters', () => {
it('should return multiple dummy filters"', () => { it('should return multiple dummy filters"', () => {
const dummyFilter = { const dummyFilter = {
id: 'dummy', id: 'dummy',
options: [{ id: 'one', selected: true }, { id: 'anotherone', selected: true }], options: [{ id: 'one' }, { id: 'two' }],
selection: new Set(['one', 'two']),
}; };
state.filters.push(dummyFilter); state.filters.push(dummyFilter);
const activeFilters = getters.activeFilters(state, mockedGetters(state)); const activeFilters = getters.activeFilters(state, mockedGetters(state));
......
...@@ -3,17 +3,19 @@ import * as types from 'ee/security_dashboard/store/modules/filters/mutation_typ ...@@ -3,17 +3,19 @@ import * as types from 'ee/security_dashboard/store/modules/filters/mutation_typ
import mutations from 'ee/security_dashboard/store/modules/filters/mutations'; import mutations from 'ee/security_dashboard/store/modules/filters/mutations';
describe('filters module mutations', () => { describe('filters module mutations', () => {
describe('SET_FILTER', () => { let state;
let state; let severityFilter;
let severityFilter; let criticalOption;
let criticalOption; let highOption;
let highOption;
beforeEach(() => { beforeEach(() => {
state = createState(); state = createState();
[severityFilter] = state.filters; [severityFilter] = state.filters;
[, criticalOption, highOption] = severityFilter.options; [, criticalOption, highOption] = severityFilter.options;
});
describe('SET_FILTER', () => {
beforeEach(() => {
mutations[types.SET_FILTER](state, { mutations[types.SET_FILTER](state, {
filterId: severityFilter.id, filterId: severityFilter.id,
optionId: criticalOption.id, optionId: criticalOption.id,
...@@ -21,11 +23,16 @@ describe('filters module mutations', () => { ...@@ -21,11 +23,16 @@ describe('filters module mutations', () => {
}); });
it('should make critical the selected option', () => { it('should make critical the selected option', () => {
expect(state.filters[0].options[1].selected).toEqual(true); expect(state.filters[0].selection).toEqual(new Set(['critical']));
}); });
it('should remove ALL as the selected option', () => { it('should set to `all` if no option is selected', () => {
expect(state.filters[0].options[0].selected).toEqual(false); mutations[types.SET_FILTER](state, {
filterId: severityFilter.id,
optionId: criticalOption.id,
});
expect(state.filters[0].selection).toEqual(new Set(['all']));
}); });
describe('on subsequent changes', () => { describe('on subsequent changes', () => {
...@@ -35,27 +42,62 @@ describe('filters module mutations', () => { ...@@ -35,27 +42,62 @@ describe('filters module mutations', () => {
optionId: highOption.id, optionId: highOption.id,
}); });
expect(state.filters[0].options[1].selected).toEqual(true); expect(state.filters[0].selection).toEqual(new Set(['high', 'critical']));
expect(state.filters[0].options[2].selected).toEqual(true); });
});
});
describe('SET_ALL_FILTERS', () => {
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]);
expect(state.filters[0].selection).toEqual(expected);
});
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 => {
expect(filter.selection).toEqual(expected);
});
});
it('should set options to `all` if payload contains an empty array', () => {
mutations[types.SET_ALL_FILTERS](state, {
[severityFilter.id]: [],
});
const expected = new Set(['all']);
expect(state.filters[0].selection).toEqual(expected);
}); });
}); });
describe('SET_FILTER_OPTIONS', () => { describe('SET_FILTER_OPTIONS', () => {
let state;
let firstFilter;
const options = [{ id: 0, name: 'c' }, { id: 3, name: 'c' }]; const options = [{ id: 0, name: 'c' }, { id: 3, name: 'c' }];
beforeEach(() => { beforeEach(() => {
state = createState(); const filterId = severityFilter.id;
[firstFilter] = state.filters;
const filterId = firstFilter.id;
mutations[types.SET_FILTER_OPTIONS](state, { filterId, options }); mutations[types.SET_FILTER_OPTIONS](state, { filterId, options });
}); });
it('should add all the options to the type filter', () => { it('should add all the options to the type filter', () => {
expect(firstFilter.options).toEqual(options); expect(severityFilter.options).toEqual(options);
}); });
}); });
}); });
import createStore from 'ee/security_dashboard/store/index';
import * as projectsMutationTypes from 'ee/security_dashboard/store/modules/projects/mutation_types';
import * as filtersMutationTypes from 'ee/security_dashboard/store/modules/filters/mutation_types';
import * as vulnerabilitiesMutationTypes from 'ee/security_dashboard/store/modules/vulnerabilities/mutation_types';
describe('moderator', () => {
let store;
beforeEach(() => {
store = createStore();
});
it('sets project filter options after projects have been received', () => {
spyOn(store, 'dispatch');
store.commit(`projects/${projectsMutationTypes.RECEIVE_PROJECTS_SUCCESS}`, {
projects: [{ name: 'foo', id: 1, otherProp: 'foobar' }],
});
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith(
'filters/setFilterOptions',
Object({
filterId: 'project_id',
options: [{ name: 'All', id: 'all' }, { name: 'foo', id: '1' }],
}),
);
});
it('triggers fetching vulnerabilities after one filter changes', () => {
spyOn(store, 'dispatch');
const activeFilters = store.getters['filters/activeFilters'];
store.commit(`filters/${filtersMutationTypes.SET_FILTER}`, {});
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,
);
});
it('triggers fetching vulnerabilities after filters change', () => {
spyOn(store, 'dispatch');
const activeFilters = store.getters['filters/activeFilters'];
store.commit(`filters/${filtersMutationTypes.SET_ALL_FILTERS}`, {});
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,
);
});
describe('routing', () => {
it('updates store after URL changes', () => {
const query = { example: ['test'] };
spyOn(store, 'dispatch');
store.$router.push({ name: 'dashboard', query });
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith(`filters/setAllFilters`, query);
});
it("doesn't update the store if the URL update originated from the moderator", () => {
const query = { example: ['test'] };
spyOn(store, 'commit');
store.$router.push({ name: 'dashboard', query, params: { updatedFromState: true } });
expect(store.commit).toHaveBeenCalledTimes(0);
});
it('it updates the route after a successful vulnerability retrieval', () => {
const activeFilters = store.getters['filters/activeFilters'];
spyOn(store.$router, 'push');
store.commit(
`vulnerabilities/${vulnerabilitiesMutationTypes.RECEIVE_VULNERABILITIES_SUCCESS}`,
{},
);
expect(store.$router.push).toHaveBeenCalledTimes(1);
expect(store.$router.push).toHaveBeenCalledWith({
name: 'dashboard',
query: activeFilters,
params: { updatedFromState: true },
});
});
});
});
...@@ -11,7 +11,7 @@ import mockDataVulnerabilities from './data/mock_data_vulnerabilities.json'; ...@@ -11,7 +11,7 @@ import mockDataVulnerabilities from './data/mock_data_vulnerabilities.json';
import mockDataVulnerabilitiesCount from './data/mock_data_vulnerabilities_count.json'; import mockDataVulnerabilitiesCount from './data/mock_data_vulnerabilities_count.json';
import mockDataVulnerabilitiesHistory from './data/mock_data_vulnerabilities_history.json'; import mockDataVulnerabilitiesHistory from './data/mock_data_vulnerabilities_history.json';
describe('vulnerabiliites count actions', () => { describe('vulnerabilities count actions', () => {
const data = mockDataVulnerabilitiesCount; const data = mockDataVulnerabilitiesCount;
const params = { filters: { type: ['sast'] } }; const params = { filters: { type: ['sast'] } };
const filteredData = mockDataVulnerabilitiesCount.sast; const filteredData = mockDataVulnerabilitiesCount.sast;
...@@ -37,7 +37,7 @@ describe('vulnerabiliites count actions', () => { ...@@ -37,7 +37,7 @@ describe('vulnerabiliites count actions', () => {
}); });
}); });
describe('fetchVulnerabilitesCount', () => { describe('fetchVulnerabilitiesCount', () => {
let mock; let mock;
const state = initialState; const state = initialState;
...@@ -112,7 +112,7 @@ describe('vulnerabiliites count actions', () => { ...@@ -112,7 +112,7 @@ describe('vulnerabiliites count actions', () => {
}); });
}); });
describe('requestVulnerabilitesCount', () => { describe('requestVulnerabilitiesCount', () => {
it('should commit the request mutation', done => { it('should commit the request mutation', done => {
const state = initialState; const state = initialState;
...@@ -127,7 +127,7 @@ describe('vulnerabiliites count actions', () => { ...@@ -127,7 +127,7 @@ describe('vulnerabiliites count actions', () => {
}); });
}); });
describe('receiveVulnerabilitesCountSuccess', () => { describe('receiveVulnerabilitiesCountSuccess', () => {
it('should commit the success mutation', done => { it('should commit the success mutation', done => {
const state = initialState; const state = initialState;
...@@ -142,7 +142,7 @@ describe('vulnerabiliites count actions', () => { ...@@ -142,7 +142,7 @@ describe('vulnerabiliites count actions', () => {
}); });
}); });
describe('receivetVulnerabilitesCountError', () => { describe('receiveVulnerabilitiesCountError', () => {
it('should commit the error mutation', done => { it('should commit the error mutation', done => {
const state = initialState; const state = initialState;
...@@ -708,12 +708,12 @@ describe('vulnerabilities history actions', () => { ...@@ -708,12 +708,12 @@ describe('vulnerabilities history actions', () => {
}); });
}); });
describe('fetchVulnerabilitesTimeline', () => { describe('fetchVulnerabilitiesTimeline', () => {
let mock; let mock;
const state = initialState; const state = initialState;
beforeEach(() => { beforeEach(() => {
state.vulnerabilitiesCountEndpoint = `${TEST_HOST}/vulnerabilitIES_HISTORY.json`; state.vulnerabilitiesHistoryEndpoint = `${TEST_HOST}/vulnerabilitIES_HISTORY.json`;
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
}); });
...@@ -786,7 +786,7 @@ describe('vulnerabilities history actions', () => { ...@@ -786,7 +786,7 @@ describe('vulnerabilities history actions', () => {
}); });
}); });
describe('requestVulnerabilitesTimeline', () => { describe('requestVulnerabilitiesTimeline', () => {
it('should commit the request mutation', done => { it('should commit the request mutation', done => {
const state = initialState; const state = initialState;
...@@ -801,7 +801,7 @@ describe('vulnerabilities history actions', () => { ...@@ -801,7 +801,7 @@ describe('vulnerabilities history actions', () => {
}); });
}); });
describe('receiveVulnerabilitesTimelineSuccess', () => { describe('receiveVulnerabilitiesTimelineSuccess', () => {
it('should commit the success mutation', done => { it('should commit the success mutation', done => {
const state = initialState; const state = initialState;
...@@ -816,7 +816,7 @@ describe('vulnerabilities history actions', () => { ...@@ -816,7 +816,7 @@ describe('vulnerabilities history actions', () => {
}); });
}); });
describe('receivetVulnerabilitesTimelineError', () => { describe('receiveVulnerabilitiesTimelineError', () => {
it('should commit the error mutation', done => { it('should commit the error mutation', done => {
const state = initialState; const state = initialState;
......
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