Commit 07a4d7d0 authored by Mark Florian's avatar Mark Florian Committed by Fatih Acet

Refactor Security Dashboard store

This refactoring is part of the [Instance Security Dashboard MVC][1], to
abstract away from the base Security Dashboard the logic for how the
list of projects to filter with is set. It:

* Adds a new `GroupSecurityDashboard` component that wraps the
  `SecurityDashboard` component.
* Moves the `projects` module bindings/calls from the
  `SecurityDashboard` component to the new `GroupSecurityDashboard`.
* Creates a new `projects` plugin for the Security Dashboard store which
  dynamically adds the `projects` module and sets up the appropriate
  store bindings between it and the `filters` module.
* Updates the entry point for the Group Security Dashboard to add the
  `projects` plugin to the Security Dashboard store.
* Moves the existing `mediator` and `sync_with_store` plugins into
  a `plugins` directory.
* Renames `moderator` to `mediator`.

[1]: https://gitlab.com/gitlab-org/gitlab/issues/6953
parent 8514991c
......@@ -25,11 +25,6 @@ export default {
type: String,
required: true,
},
projectsEndpoint: {
type: String,
required: false,
default: null,
},
vulnerabilitiesEndpoint: {
type: String,
required: true,
......@@ -62,7 +57,6 @@ export default {
},
computed: {
...mapState('vulnerabilities', ['modal', 'pageInfo']),
...mapState('projects', ['projects']),
...mapGetters('filters', ['activeFilters']),
canCreateIssue() {
const path = this.vulnerability.create_vulnerability_feedback_issue_path;
......@@ -106,14 +100,12 @@ export default {
if (this.showHideDismissedToggle) {
this.setHideDismissedToggleInitialState();
}
this.setProjectsEndpoint(this.projectsEndpoint);
this.setVulnerabilitiesEndpoint(this.vulnerabilitiesEndpoint);
this.setVulnerabilitiesCountEndpoint(this.vulnerabilitiesCountEndpoint);
this.setVulnerabilitiesHistoryEndpoint(this.vulnerabilitiesHistoryEndpoint);
this.fetchVulnerabilities({ ...this.activeFilters, page: this.pageInfo.page });
this.fetchVulnerabilitiesCount(this.activeFilters);
this.fetchVulnerabilitiesHistory(this.activeFilters);
this.fetchProjects();
},
methods: {
...mapActions('vulnerabilities', [
......@@ -136,7 +128,6 @@ export default {
'undoDismiss',
'downloadPatch',
]),
...mapActions('projects', ['setProjectsEndpoint', 'fetchProjects']),
...mapActions('filters', ['lockFilter', 'setHideDismissedToggleInitialState']),
emitVulnerabilitiesCountChanged(count) {
this.$emit('vulnerabilitiesCountChanged', count);
......
<script>
import { mapActions } from 'vuex';
import SecurityDashboard from './app.vue';
export default {
name: 'GroupSecurityDashboard',
components: {
SecurityDashboard,
},
props: {
dashboardDocumentation: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
projectsEndpoint: {
type: String,
required: true,
},
vulnerabilitiesEndpoint: {
type: String,
required: true,
},
vulnerabilitiesCountEndpoint: {
type: String,
required: true,
},
vulnerabilitiesHistoryEndpoint: {
type: String,
required: true,
},
vulnerabilityFeedbackHelpPath: {
type: String,
required: true,
},
},
created() {
this.setProjectsEndpoint(this.projectsEndpoint);
this.fetchProjects();
},
methods: {
...mapActions('projects', ['setProjectsEndpoint', 'fetchProjects']),
},
};
</script>
<template>
<security-dashboard
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
:vulnerabilities-endpoint="vulnerabilitiesEndpoint"
:vulnerabilities-count-endpoint="vulnerabilitiesCountEndpoint"
:vulnerabilities-history-endpoint="vulnerabilitiesHistoryEndpoint"
:vulnerability-feedback-help-path="vulnerabilityFeedbackHelpPath"
/>
</template>
import Vue from 'vue';
import GroupSecurityDashboardApp from './components/app.vue';
import GroupSecurityDashboardApp from './components/group_security_dashboard.vue';
import UnavailableState from './components/unavailable_state.vue';
import createStore from './store';
import router from './store/router';
import projectsPlugin from './store/plugins/projects';
export default function() {
const el = document.getElementById('js-group-security-dashboard');
......@@ -22,7 +23,7 @@ export default function() {
});
}
const store = createStore();
const store = createStore({ plugins: [projectsPlugin] });
return new Vue({
el,
store,
......
import Vue from 'vue';
import Vuex from 'vuex';
import router from './router';
import configureModerator from './moderator';
import syncWithRouter from './sync_with_router';
import mediator from './plugins/mediator';
import syncWithRouter from './plugins/sync_with_router';
import filters from './modules/filters/index';
import projects from './modules/projects/index';
import vulnerabilities from './modules/vulnerabilities/index';
Vue.use(Vuex);
export default () => {
export default ({ plugins = [] } = {}) => {
const store = new Vuex.Store({
modules: {
filters,
projects,
vulnerabilities,
},
plugins: [configureModerator, syncWithRouter(router)],
plugins: [mediator, syncWithRouter(router), ...plugins],
});
store.$router = router;
......
import * as filtersMutationTypes from './modules/filters/mutation_types';
import * as projectsMutationTypes from './modules/projects/mutation_types';
import { BASE_FILTERS } from './modules/filters/constants';
import * as filtersMutationTypes from '../modules/filters/mutation_types';
export default function configureModerator(store) {
store.subscribe(({ type, payload }) => {
export default store => {
store.subscribe(({ type }) => {
switch (type) {
case `projects/${projectsMutationTypes.RECEIVE_PROJECTS_SUCCESS}`:
store.dispatch('filters/setFilterOptions', {
filterId: 'project_id',
options: [
BASE_FILTERS.project_id,
...payload.projects.map(project => ({
name: project.name,
id: project.id.toString(),
})),
],
});
break;
case `filters/${filtersMutationTypes.SET_ALL_FILTERS}`:
case `filters/${filtersMutationTypes.SET_FILTER}`:
case `filters/${filtersMutationTypes.SET_TOGGLE_VALUE}`: {
......@@ -29,4 +15,4 @@ export default function configureModerator(store) {
default:
}
});
}
};
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(),
})),
],
});
}
});
};
import {
SET_VULNERABILITIES_HISTORY_DAY_RANGE,
RECEIVE_VULNERABILITIES_SUCCESS,
} from './modules/vulnerabilities/mutation_types';
} from '../modules/vulnerabilities/mutation_types';
/**
* Vuex store plugin to sync some Group Security Dashboard view settings with the URL.
......
......@@ -15,7 +15,6 @@ import createStore from 'ee/security_dashboard/store';
const localVue = createLocalVue();
const pipelineId = 123;
const projectsEndpoint = `${TEST_HOST}/projects`;
const vulnerabilitiesEndpoint = `${TEST_HOST}/vulnerabilities`;
const vulnerabilitiesCountEndpoint = `${TEST_HOST}/vulnerabilities_summary`;
const vulnerabilitiesHistoryEndpoint = `${TEST_HOST}/vulnerabilities_history`;
......@@ -27,14 +26,12 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('Security Dashboard app', () => {
let wrapper;
let mock;
let fetchProjectsSpy;
let lockFilterSpy;
let setPipelineIdSpy;
let store;
const setup = () => {
mock = new MockAdapter(axios);
fetchProjectsSpy = jest.fn();
lockFilterSpy = jest.fn();
setPipelineIdSpy = jest.fn();
};
......@@ -47,13 +44,11 @@ describe('Security Dashboard app', () => {
sync: false,
methods: {
lockFilter: lockFilterSpy,
fetchProjects: fetchProjectsSpy,
setPipelineId: setPipelineIdSpy,
},
propsData: {
dashboardDocumentation: '',
emptyStateSvgPath: '',
projectsEndpoint,
vulnerabilitiesEndpoint,
vulnerabilitiesCountEndpoint,
vulnerabilitiesHistoryEndpoint,
......@@ -95,10 +90,6 @@ describe('Security Dashboard app', () => {
expect(wrapper.vm.isLockedToProject).toBe(false);
});
it('fetches projects', () => {
expect(fetchProjectsSpy).toHaveBeenCalled();
});
it('does not lock project filters', () => {
expect(lockFilterSpy).not.toHaveBeenCalled();
});
......@@ -139,10 +130,6 @@ describe('Security Dashboard app', () => {
expect(wrapper.vm.isLockedToProject).toBe(true);
});
it('fetches projects', () => {
expect(fetchProjectsSpy).toHaveBeenCalled();
});
it('locks the filters to a given project', () => {
expect(lockFilterSpy).toHaveBeenCalledWith({
filterId: 'project_id',
......
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import GroupSecurityDashboard from 'ee/security_dashboard/components/group_security_dashboard.vue';
import SecurityDashboard from 'ee/security_dashboard/components/app.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
const dashboardDocumentation = '/help/docs';
const emptyStateSvgPath = '/svgs/empty/svg';
const projectsEndpoint = '/projects';
const vulnerabilitiesEndpoint = '/vulnerabilities';
const vulnerabilitiesCountEndpoint = '/vulnerabilities_summary';
const vulnerabilitiesHistoryEndpoint = '/vulnerabilities_history';
const vulnerabilityFeedbackHelpPath = '/vulnerabilities_feedback_help';
describe('Group Security Dashboard component', () => {
let store;
let wrapper;
const factory = () => {
store = new Vuex.Store({
modules: {
projects: {
namespaced: true,
actions: {
fetchProjects() {},
setProjectsEndpoint() {},
},
},
},
});
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMount(GroupSecurityDashboard, {
localVue,
store,
sync: false,
propsData: {
dashboardDocumentation,
emptyStateSvgPath,
projectsEndpoint,
vulnerabilitiesEndpoint,
vulnerabilitiesCountEndpoint,
vulnerabilitiesHistoryEndpoint,
vulnerabilityFeedbackHelpPath,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('on creation', () => {
beforeEach(() => {
factory();
});
it('dispatches the expected actions', () => {
expect(store.dispatch.mock.calls).toEqual([
['projects/setProjectsEndpoint', projectsEndpoint],
['projects/fetchProjects', undefined],
]);
});
it('renders the security dashboard', () => {
const dashboard = wrapper.find(SecurityDashboard);
expect(dashboard.exists()).toBe(true);
expect(dashboard.props()).toEqual(
expect.objectContaining({
dashboardDocumentation,
emptyStateSvgPath,
vulnerabilitiesEndpoint,
vulnerabilitiesCountEndpoint,
vulnerabilitiesHistoryEndpoint,
vulnerabilityFeedbackHelpPath,
}),
);
});
});
});
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],
}),
);
});
});
......@@ -32,7 +32,7 @@ describe('syncWithRouter', () => {
);
});
it("doesn't update the store if the URL update originated from the moderator", () => {
it("doesn't update the store if the URL update originated from the mediator", () => {
const query = { example: ['test'] };
jest.spyOn(store, 'commit').mockImplementation(noop);
......
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 { BASE_FILTERS } from 'ee/security_dashboard/store/modules/filters/constants';
describe('moderator', () => {
describe('mediator', () => {
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: [BASE_FILTERS.project_id, { name: 'foo', id: '1' }],
}),
);
});
it('triggers fetching vulnerabilities after one filter changes', () => {
spyOn(store, 'dispatch');
......
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