diff --git a/ee/app/assets/javascripts/security_dashboard/components/app.vue b/ee/app/assets/javascripts/security_dashboard/components/app.vue index 60d3d10f0e79a411d54c2f7eff26edad9b6f9050..c2f23eb899dd4e991b9e37a32f2d18bda5f596b7 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/app.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/app.vue @@ -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); diff --git a/ee/app/assets/javascripts/security_dashboard/components/group_security_dashboard.vue b/ee/app/assets/javascripts/security_dashboard/components/group_security_dashboard.vue new file mode 100644 index 0000000000000000000000000000000000000000..b37d58bd7bf8e72e62f872fadd1fd0bfff6aff66 --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/components/group_security_dashboard.vue @@ -0,0 +1,59 @@ + + + diff --git a/ee/app/assets/javascripts/security_dashboard/index.js b/ee/app/assets/javascripts/security_dashboard/index.js index c8f150d5b526fac5e1ef26d7b80768484e800f96..3e2a664c97d7449579756ba91fbca13e28fa29e9 100644 --- a/ee/app/assets/javascripts/security_dashboard/index.js +++ b/ee/app/assets/javascripts/security_dashboard/index.js @@ -1,8 +1,9 @@ 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, diff --git a/ee/app/assets/javascripts/security_dashboard/store/index.js b/ee/app/assets/javascripts/security_dashboard/store/index.js index 2d9e64e3a2e94481aebd6a473d73eacf16490daf..41f8125b50a9a0b0be7aff2ee58d3cef24ca6b73 100644 --- a/ee/app/assets/javascripts/security_dashboard/store/index.js +++ b/ee/app/assets/javascripts/security_dashboard/store/index.js @@ -1,22 +1,20 @@ 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; diff --git a/ee/app/assets/javascripts/security_dashboard/store/moderator.js b/ee/app/assets/javascripts/security_dashboard/store/moderator.js deleted file mode 100644 index 0328a3907867df5a17f11d7cfe1f9498700c44f4..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/security_dashboard/store/moderator.js +++ /dev/null @@ -1,32 +0,0 @@ -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.subscribe(({ type, payload }) => { - 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}`: { - const activeFilters = store.getters['filters/activeFilters']; - store.dispatch('vulnerabilities/fetchVulnerabilities', activeFilters); - store.dispatch('vulnerabilities/fetchVulnerabilitiesCount', activeFilters); - store.dispatch('vulnerabilities/fetchVulnerabilitiesHistory', activeFilters); - break; - } - default: - } - }); -} diff --git a/ee/app/assets/javascripts/security_dashboard/store/plugins/mediator.js b/ee/app/assets/javascripts/security_dashboard/store/plugins/mediator.js new file mode 100644 index 0000000000000000000000000000000000000000..5871e351e5f55a4595380ce286ecbd0778b17d0e --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/store/plugins/mediator.js @@ -0,0 +1,18 @@ +import * as filtersMutationTypes from '../modules/filters/mutation_types'; + +export default store => { + store.subscribe(({ type }) => { + switch (type) { + case `filters/${filtersMutationTypes.SET_ALL_FILTERS}`: + case `filters/${filtersMutationTypes.SET_FILTER}`: + case `filters/${filtersMutationTypes.SET_TOGGLE_VALUE}`: { + const activeFilters = store.getters['filters/activeFilters']; + store.dispatch('vulnerabilities/fetchVulnerabilities', activeFilters); + store.dispatch('vulnerabilities/fetchVulnerabilitiesCount', activeFilters); + store.dispatch('vulnerabilities/fetchVulnerabilitiesHistory', activeFilters); + break; + } + default: + } + }); +}; diff --git a/ee/app/assets/javascripts/security_dashboard/store/plugins/projects.js b/ee/app/assets/javascripts/security_dashboard/store/plugins/projects.js new file mode 100644 index 0000000000000000000000000000000000000000..ff3a2943974185bec8290692fd0a831021682ba7 --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/store/plugins/projects.js @@ -0,0 +1,22 @@ +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(), + })), + ], + }); + } + }); +}; diff --git a/ee/app/assets/javascripts/security_dashboard/store/sync_with_router.js b/ee/app/assets/javascripts/security_dashboard/store/plugins/sync_with_router.js similarity index 96% rename from ee/app/assets/javascripts/security_dashboard/store/sync_with_router.js rename to ee/app/assets/javascripts/security_dashboard/store/plugins/sync_with_router.js index db943fed712107c65bd4bddd24e70a66bd2475dd..09bc3d003ab32222851d92a1f691d30588f83f68 100644 --- a/ee/app/assets/javascripts/security_dashboard/store/sync_with_router.js +++ b/ee/app/assets/javascripts/security_dashboard/store/plugins/sync_with_router.js @@ -1,7 +1,7 @@ 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. diff --git a/ee/spec/frontend/security_dashboard/components/app_spec.js b/ee/spec/frontend/security_dashboard/components/app_spec.js index 98d8e323496c18626b0cae9e7bd1b09faaaea3fc..c0870204d20369f2b8a33285d8dd69b5b8ab7fb5 100644 --- a/ee/spec/frontend/security_dashboard/components/app_spec.js +++ b/ee/spec/frontend/security_dashboard/components/app_spec.js @@ -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', diff --git a/ee/spec/frontend/security_dashboard/components/group_security_dashboard_spec.js b/ee/spec/frontend/security_dashboard/components/group_security_dashboard_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..a6f9dba9c40bbec8c49cfca9f2b8b95d056e6526 --- /dev/null +++ b/ee/spec/frontend/security_dashboard/components/group_security_dashboard_spec.js @@ -0,0 +1,82 @@ +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, + }), + ); + }); + }); +}); diff --git a/ee/spec/frontend/security_dashboard/store/plugins/projects_spec.js b/ee/spec/frontend/security_dashboard/store/plugins/projects_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..0467a35b56409cfad6fb457f94849b328b5cb034 --- /dev/null +++ b/ee/spec/frontend/security_dashboard/store/plugins/projects_spec.js @@ -0,0 +1,38 @@ +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], + }), + ); + }); +}); diff --git a/ee/spec/frontend/security_dashboard/store/sync_with_router_spec.js b/ee/spec/frontend/security_dashboard/store/plugins/sync_with_router_spec.js similarity index 99% rename from ee/spec/frontend/security_dashboard/store/sync_with_router_spec.js rename to ee/spec/frontend/security_dashboard/store/plugins/sync_with_router_spec.js index ed87c00ee49551b875296a7313e9a7da97eaf999..6395ca282659aa4410f95910f48d580bb1a0c2c5 100644 --- a/ee/spec/frontend/security_dashboard/store/sync_with_router_spec.js +++ b/ee/spec/frontend/security_dashboard/store/plugins/sync_with_router_spec.js @@ -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); diff --git a/ee/spec/javascripts/security_dashboard/store/moderator_spec.js b/ee/spec/javascripts/security_dashboard/store/plugins/mediator_spec.js similarity index 75% rename from ee/spec/javascripts/security_dashboard/store/moderator_spec.js rename to ee/spec/javascripts/security_dashboard/store/plugins/mediator_spec.js index 261a8bdd34e734ba75f02ba42d73aa5aab3fc55e..98ae5c21136480badf4678a7c7f0492556a09847 100644 --- a/ee/spec/javascripts/security_dashboard/store/moderator_spec.js +++ b/ee/spec/javascripts/security_dashboard/store/plugins/mediator_spec.js @@ -1,32 +1,13 @@ 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');