Commit 2e077f28 authored by Mark Florian's avatar Mark Florian Committed by Phil Hughes

Persist vulnerabilities page number in GSD URL

This follows straight-forwardly from [previous work][1] to persist some
state of the Group Security Dashboard to the URL, and vice versa.

[1]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9108
parent 9b7b1493
......@@ -47,7 +47,7 @@ export default {
},
},
computed: {
...mapState('vulnerabilities', ['modal']),
...mapState('vulnerabilities', ['modal', 'pageInfo']),
...mapState('projects', ['projects']),
...mapGetters('filters', ['activeFilters']),
canCreateIssuePermission() {
......@@ -67,7 +67,7 @@ export default {
this.setVulnerabilitiesEndpoint(this.vulnerabilitiesEndpoint);
this.setVulnerabilitiesCountEndpoint(this.vulnerabilitiesCountEndpoint);
this.setVulnerabilitiesHistoryEndpoint(this.vulnerabilitiesHistoryEndpoint);
this.fetchVulnerabilities(this.activeFilters);
this.fetchVulnerabilities({ ...this.activeFilters, page: this.pageInfo.page });
this.fetchVulnerabilitiesCount(this.activeFilters);
this.fetchVulnerabilitiesHistory(this.activeFilters);
this.fetchProjects();
......
......@@ -2,6 +2,7 @@ import Vue from 'vue';
import Vuex from 'vuex';
import router from './router';
import configureModerator from './moderator';
import syncWithRouter from './sync_with_router';
import filters from './modules/filters/index';
import projects from './modules/projects/index';
import vulnerabilities from './modules/vulnerabilities/index';
......@@ -15,11 +16,10 @@ export default () => {
projects,
vulnerabilities,
},
plugins: [configureModerator, syncWithRouter(router)],
});
store.$router = router;
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 { BASE_FILTERS } from './modules/filters/constants';
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 }) => {
switch (type) {
case `projects/${projectsMutationTypes.RECEIVE_PROJECTS_SUCCESS}`:
......@@ -36,15 +25,6 @@ export default function configureModerator(store) {
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:
}
});
......
......@@ -45,6 +45,10 @@ export const receiveVulnerabilitiesCountError = ({ commit }) => {
commit(types.RECEIVE_VULNERABILITIES_COUNT_ERROR);
};
export const setVulnerabilitiesPage = ({ commit }, page) => {
commit(types.SET_VULNERABILITIES_PAGE, page);
};
export const fetchVulnerabilities = ({ state, dispatch }, params = {}) => {
if (!state.vulnerabilitiesEndpoint) {
return;
......
export const SET_VULNERABILITIES_ENDPOINT = 'SET_VULNERABILITIES_ENDPOINT';
export const SET_VULNERABILITIES_PAGE = 'SET_VULNERABILITIES_PAGE';
export const REQUEST_VULNERABILITIES = 'REQUEST_VULNERABILITIES';
export const RECEIVE_VULNERABILITIES_SUCCESS = 'RECEIVE_VULNERABILITIES_SUCCESS';
export const RECEIVE_VULNERABILITIES_ERROR = 'RECEIVE_VULNERABILITIES_ERROR';
......
......@@ -24,6 +24,9 @@ export default {
[types.SET_VULNERABILITIES_COUNT_ENDPOINT](state, payload) {
state.vulnerabilitiesCountEndpoint = payload;
},
[types.SET_VULNERABILITIES_PAGE](state, payload) {
state.pageInfo = { ...state.pageInfo, page: payload };
},
[types.REQUEST_VULNERABILITIES_COUNT](state) {
state.isLoadingVulnerabilitiesCount = true;
state.errorLoadingVulnerabilitiesCount = false;
......
import {
SET_VULNERABILITIES_HISTORY_DAY_RANGE,
RECEIVE_VULNERABILITIES_SUCCESS,
} from './modules/vulnerabilities/mutation_types';
/**
* Vuex store plugin to sync some Group Security Dashboard view settings with the URL.
*/
export default router => store => {
let syncingRouter = false;
const MUTATION_TYPES = [
`vulnerabilities/${SET_VULNERABILITIES_HISTORY_DAY_RANGE}`,
`vulnerabilities/${RECEIVE_VULNERABILITIES_SUCCESS}`,
];
// Update store from routing events
router.beforeEach((to, from, next) => {
const updatedFromState = (to.params && to.params.updatedFromState) || false;
if (to.name === 'dashboard' && !updatedFromState) {
syncingRouter = true;
store.dispatch(`filters/setAllFilters`, to.query);
const page = parseInt(to.query.page, 10);
if (Number.isFinite(page)) {
store.dispatch(`vulnerabilities/setVulnerabilitiesPage`, page);
}
const dayRange = parseInt(to.query.days, 10);
if (Number.isFinite(dayRange)) {
store.dispatch(`vulnerabilities/setVulnerabilitiesHistoryDayRange`, dayRange);
}
syncingRouter = false;
}
next();
});
// Update router from store mutations
store.subscribe(({ type }) => {
if (!syncingRouter && MUTATION_TYPES.includes(type)) {
const activeFilters = store.getters['filters/activeFilters'];
const { page } = store.state.vulnerabilities.pageInfo;
const days = store.state.vulnerabilities.vulnerabilitiesHistoryDayRange;
store.$router.push({
name: 'dashboard',
query: { ...activeFilters, page, days },
params: { updatedFromState: true },
});
}
});
};
---
title: Persist in the URL the page and day range of vulnerabilities viewed in the
Group Security Dashboard.
merge_request: 10402
author:
type: added
import createStore from 'ee/security_dashboard/store/index';
import * as vulnerabilitiesMutationTypes from 'ee/security_dashboard/store/modules/vulnerabilities/mutation_types';
import { DAYS } from 'ee/security_dashboard/store/modules/vulnerabilities/constants';
describe('syncWithRouter', () => {
let store;
const noop = () => {};
beforeEach(() => {
store = createStore();
});
it('updates store after URL changes', () => {
const page = 3;
const days = DAYS.SIXTY;
const query = { example: ['test'], page, days };
jest.spyOn(store, 'dispatch');
const routerPush = store.$router.push.bind(store.$router);
jest.spyOn(store.$router, 'push');
routerPush({ name: 'dashboard', query });
// Assert no implicit synchronous recursive calls occurred
expect(store.$router.push).not.toHaveBeenCalled();
expect(store.dispatch).toHaveBeenCalledWith(`filters/setAllFilters`, query);
expect(store.dispatch).toHaveBeenCalledWith(`vulnerabilities/setVulnerabilitiesPage`, page);
expect(store.dispatch).toHaveBeenCalledWith(
`vulnerabilities/setVulnerabilitiesHistoryDayRange`,
days,
);
});
it("doesn't update the store if the URL update originated from the moderator", () => {
const query = { example: ['test'] };
jest.spyOn(store, 'commit').mockImplementation(noop);
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'];
const page = 2;
jest.spyOn(store.$router, 'push').mockImplementation(noop);
store.commit(
`vulnerabilities/${vulnerabilitiesMutationTypes.RECEIVE_VULNERABILITIES_SUCCESS}`,
{ pageInfo: { page } },
);
expect(store.$router.push).toHaveBeenCalledTimes(1);
expect(store.$router.push).toHaveBeenCalledWith({
name: 'dashboard',
query: expect.objectContaining({ ...activeFilters, page }),
params: { updatedFromState: true },
});
});
it('it updates the route after changing the vulnerability history day range', () => {
const days = DAYS.SIXTY;
jest.spyOn(store.$router, 'push').mockImplementation(noop);
store.commit(
`vulnerabilities/${vulnerabilitiesMutationTypes.SET_VULNERABILITIES_HISTORY_DAY_RANGE}`,
days,
);
expect(store.$router.push).toHaveBeenCalledTimes(1);
expect(store.$router.push).toHaveBeenCalledWith({
name: 'dashboard',
query: expect.objectContaining({ days }),
params: { updatedFromState: true },
});
});
});
......@@ -49,6 +49,7 @@ describe('Security Dashboard Table', () => {
beforeEach(() => {
store.commit(`vulnerabilities/${RECEIVE_VULNERABILITIES_SUCCESS}`, {
vulnerabilities: mockDataVulnerabilities,
pageInfo: {},
});
vm = mountComponentWithStore(Component, { store, props });
});
......@@ -62,7 +63,10 @@ describe('Security Dashboard Table', () => {
describe('with no vulnerabilties', () => {
beforeEach(() => {
store.commit(`vulnerabilities/${RECEIVE_VULNERABILITIES_SUCCESS}`, { vulnerabilities: [] });
store.commit(`vulnerabilities/${RECEIVE_VULNERABILITIES_SUCCESS}`, {
vulnerabilities: [],
pageInfo: {},
});
vm = mountComponentWithStore(Component, { store, props });
});
......
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';
import { BASE_FILTERS } from 'ee/security_dashboard/store/modules/filters/constants';
describe('moderator', () => {
......@@ -75,45 +74,4 @@ describe('moderator', () => {
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 },
});
});
});
});
......@@ -325,6 +325,27 @@ describe('vulnerabilities actions', () => {
);
});
});
describe('setVulnerabilitiesPage', () => {
it('should commit the correct mutuation', done => {
const state = initialState;
const page = 3;
testAction(
actions.setVulnerabilitiesPage,
page,
state,
[
{
type: types.SET_VULNERABILITIES_PAGE,
payload: page,
},
],
[],
done,
);
});
});
});
describe('openModal', () => {
......
......@@ -16,6 +16,17 @@ describe('vulnerabilities module mutations', () => {
});
});
describe('SET_VULNERABILITIES_PAGE', () => {
const page = 3;
it(`should set pageInfo.page to ${page}`, () => {
const state = createState();
mutations[types.SET_VULNERABILITIES_PAGE](state, page);
expect(state.pageInfo.page).toEqual(page);
});
});
describe('REQUEST_VULNERABILITIES', () => {
let state;
......
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