From 5cc6af9faebc64b260c20bbda7101a01556976d0 Mon Sep 17 00:00:00 2001 From: samdbeckham Date: Wed, 19 Dec 2018 17:14:07 +0000 Subject: [PATCH 1/5] Adds project filtering to the GSD - Adds a new module specifically for fetching projects - Pulls in the group projects endpoint - Creates a mediator pattern to orchestrate the vuex modules - Adds a "project" filter --- .../security_dashboard/components/app.vue | 8 ++ .../javascripts/security_dashboard/index.js | 1 + .../security_dashboard/store/index.js | 22 +-- .../security_dashboard/store/moderator.js | 26 ++++ .../store/modules/filters/actions.js | 4 + .../store/modules/filters/mutation_types.js | 4 +- .../store/modules/filters/mutations.js | 4 + .../store/modules/filters/state.js | 11 ++ .../store/modules/projects/actions.js | 40 ++++++ .../store/modules/projects/index.js | 10 ++ .../store/modules/projects/mutation_types.js | 4 + .../store/modules/projects/mutations.js | 19 +++ .../store/modules/projects/state.js | 6 + .../groups/security/dashboard/show.html.haml | 3 + .../components/filters_spec.js | 4 +- .../store/filters/actions_spec.js | 21 +++ .../store/filters/getters_spec.js | 22 ++- .../store/filters/mutations_spec.js | 18 +++ .../store/projects/actions_spec.js | 132 ++++++++++++++++++ .../store/projects/data/mock_data.json | 39 ++++++ .../store/projects/mutations_spec.js | 72 ++++++++++ 21 files changed, 445 insertions(+), 25 deletions(-) create mode 100644 ee/app/assets/javascripts/security_dashboard/store/moderator.js create mode 100644 ee/app/assets/javascripts/security_dashboard/store/modules/projects/actions.js create mode 100644 ee/app/assets/javascripts/security_dashboard/store/modules/projects/index.js create mode 100644 ee/app/assets/javascripts/security_dashboard/store/modules/projects/mutation_types.js create mode 100644 ee/app/assets/javascripts/security_dashboard/store/modules/projects/mutations.js create mode 100644 ee/app/assets/javascripts/security_dashboard/store/modules/projects/state.js create mode 100644 ee/spec/javascripts/security_dashboard/store/projects/actions_spec.js create mode 100644 ee/spec/javascripts/security_dashboard/store/projects/data/mock_data.json create mode 100644 ee/spec/javascripts/security_dashboard/store/projects/mutations_spec.js diff --git a/ee/app/assets/javascripts/security_dashboard/components/app.vue b/ee/app/assets/javascripts/security_dashboard/components/app.vue index cd1804fe1312e7..748e462b53ebf0 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/app.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/app.vue @@ -24,6 +24,10 @@ export default { type: String, required: true, }, + projectsEndpoint: { + type: String, + required: true, + }, vulnerabilitiesEndpoint: { type: String, required: true, @@ -43,13 +47,16 @@ export default { }, computed: { ...mapState('vulnerabilities', ['modal']), + ...mapState('projects', ['projects']), ...mapGetters('filters', ['activeFilters']), }, created() { + this.setProjectsEndpoint(this.projectsEndpoint); this.setVulnerabilitiesEndpoint(this.vulnerabilitiesEndpoint); this.setVulnerabilitiesCountEndpoint(this.vulnerabilitiesCountEndpoint); this.setVulnerabilitiesHistoryEndpoint(this.vulnerabilitiesHistoryEndpoint); this.fetchVulnerabilitiesCount(); + this.fetchProjects(); }, methods: { ...mapActions('vulnerabilities', [ @@ -63,6 +70,7 @@ export default { 'setVulnerabilitiesEndpoint', 'setVulnerabilitiesHistoryEndpoint', ]), + ...mapActions('projects', ['setProjectsEndpoint', 'fetchProjects']), filterChange() { this.fetchVulnerabilities(this.activeFilters); this.fetchVulnerabilitiesCount(this.activeFilters); diff --git a/ee/app/assets/javascripts/security_dashboard/index.js b/ee/app/assets/javascripts/security_dashboard/index.js index d421fc1569a174..4a555d5d31f7c4 100644 --- a/ee/app/assets/javascripts/security_dashboard/index.js +++ b/ee/app/assets/javascripts/security_dashboard/index.js @@ -16,6 +16,7 @@ export default () => { props: { dashboardDocumentation: el.dataset.dashboardDocumentation, emptyStateSvgPath: el.dataset.emptyStateSvgPath, + projectsEndpoint: el.dataset.projectsEndpoint, vulnerabilityFeedbackHelpPath: el.dataset.vulnerabilityFeedbackHelpPath, vulnerabilitiesEndpoint: el.dataset.vulnerabilitiesEndpoint, vulnerabilitiesCountEndpoint: el.dataset.vulnerabilitiesSummaryEndpoint, diff --git a/ee/app/assets/javascripts/security_dashboard/store/index.js b/ee/app/assets/javascripts/security_dashboard/store/index.js index 6f51dfde52bce1..2fc3fc755cc1ac 100644 --- a/ee/app/assets/javascripts/security_dashboard/store/index.js +++ b/ee/app/assets/javascripts/security_dashboard/store/index.js @@ -1,14 +1,20 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import vulnerabilities from './modules/vulnerabilities/index'; +import configureModerator from './moderator'; import filters from './modules/filters/index'; +import projects from './modules/projects/index'; +import vulnerabilities from './modules/vulnerabilities/index'; Vue.use(Vuex); -export default () => - new Vuex.Store({ - modules: { - vulnerabilities, - filters, - }, - }); +const store = new Vuex.Store({ + modules: { + filters, + projects, + vulnerabilities, + }, +}); + +configureModerator(store); + +export default () => store; diff --git a/ee/app/assets/javascripts/security_dashboard/store/moderator.js b/ee/app/assets/javascripts/security_dashboard/store/moderator.js new file mode 100644 index 00000000000000..909e5ec6ee4592 --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/store/moderator.js @@ -0,0 +1,26 @@ +import * as projectsMutationTypes from './modules/projects/mutation_types'; + +export default function configureModerator(store) { + store.subscribe(({ type, payload }) => { + switch (type) { + case `projects/${projectsMutationTypes.RECEIVE_PROJECTS_SUCCESS}`: + return store.dispatch('filters/setFilterOptions', { + filterId: 'project', + options: [ + { + name: 'All', + id: 'all', + selected: true, + }, + ...payload.projects.map(project => ({ + name: project.name, + id: project.id.toString(), + selected: false, + })), + ], + }); + default: + return null; + } + }); +} diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/actions.js b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/actions.js index 2b06257ba68e96..21153d0e380cf2 100644 --- a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/actions.js +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/actions.js @@ -4,6 +4,10 @@ export const setFilter = ({ commit }, payload) => { commit(types.SET_FILTER, payload); }; +export const setFilterOptions = ({ commit }, payload) => { + commit(types.SET_FILTER_OPTIONS, payload); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests // This is no longer needed after gitlab-ce#52179 is merged export default () => {}; diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutation_types.js b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutation_types.js index a92b48b9e2aacf..4a880d1a6bb629 100644 --- a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutation_types.js +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutation_types.js @@ -1,4 +1,2 @@ export const SET_FILTER = 'SET_FILTER'; - -// This is here because es-lint requires a default export when there are less than two named exports -export default SET_FILTER; +export const SET_FILTER_OPTIONS = 'SET_FILTER_OPTIONS'; diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutations.js b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutations.js index 634628b856f1e7..b02c3010dba9f0 100644 --- a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutations.js +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutations.js @@ -41,4 +41,8 @@ export default { activeFilter.options = activeOptions; } }, + [types.SET_FILTER_OPTIONS](state, payload) { + const { filterId, options } = payload; + state.filters.find(filter => filter.id === filterId).options = options; + }, }; diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/state.js b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/state.js index 38068d044bbf0f..ce266cd769443c 100644 --- a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/state.js +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/state.js @@ -32,5 +32,16 @@ export default () => ({ }), ], }, + { + name: 'Project', + id: 'project', + options: [ + { + name: 'All', + id: 'all', + selected: true, + }, + ], + }, ], }); diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/actions.js b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/actions.js new file mode 100644 index 00000000000000..799703074f9769 --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/actions.js @@ -0,0 +1,40 @@ +import axios from '~/lib/utils/axios_utils'; +import * as types from './mutation_types'; + +export const setProjectsEndpoint = ({ commit }, endpoint) => { + commit(types.SET_PROJECTS_ENDPOINT, endpoint); +}; + +export const fetchProjects = ({ state, dispatch }) => { + dispatch('requestProjects'); + + axios({ + method: 'GET', + url: state.projectsEndpoint, + }) + .then(response => { + const { data } = response; + dispatch('receiveProjectsSuccess', { data }); + }) + .catch(() => { + dispatch('receiveProjectsError'); + }); +}; + +export const requestProjects = ({ commit }) => { + commit(types.REQUEST_PROJECTS); +}; + +export const receiveProjectsSuccess = ({ commit }, { data }) => { + const projects = data; + + commit(types.RECEIVE_PROJECTS_SUCCESS, { projects }); +}; + +export const receiveProjectsError = ({ commit }) => { + commit(types.RECEIVE_PROJECTS_ERROR); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +// This is no longer needed after gitlab-ce#52179 is merged +export default () => {}; diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/index.js b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/index.js new file mode 100644 index 00000000000000..68c81bb45096f1 --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/index.js @@ -0,0 +1,10 @@ +import state from './state'; +import mutations from './mutations'; +import * as actions from './actions'; + +export default { + namespaced: true, + state, + mutations, + actions, +}; diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/mutation_types.js b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/mutation_types.js new file mode 100644 index 00000000000000..7fcf0ea6a49bde --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/mutation_types.js @@ -0,0 +1,4 @@ +export const SET_PROJECTS_ENDPOINT = 'SET_PROJECTS_ENDPOINT'; +export const REQUEST_PROJECTS = 'REQUEST_PROJECTS'; +export const RECEIVE_PROJECTS_SUCCESS = 'RECEIVE_PROJECTS_SUCCESS'; +export const RECEIVE_PROJECTS_ERROR = 'RECEIVE_PROJECTS_ERROR'; diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/mutations.js b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/mutations.js new file mode 100644 index 00000000000000..d176e5700f6db6 --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/mutations.js @@ -0,0 +1,19 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_PROJECTS_ENDPOINT](state, payload) { + state.projectsEndpoint = payload; + }, + [types.REQUEST_PROJECTS](state) { + state.isLoadingProjects = true; + state.errorLoadingProjects = false; + }, + [types.RECEIVE_PROJECTS_SUCCESS](state, payload) { + state.projects = payload.projects; + state.isLoadingProjects = false; + }, + [types.RECEIVE_PROJECTS_ERROR](state) { + state.isLoadingProjects = false; + state.errorLoadingProjects = true; + }, +}; diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/state.js b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/state.js new file mode 100644 index 00000000000000..f31b49f35f65ac --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/state.js @@ -0,0 +1,6 @@ +export default () => ({ + endpoint: '', + projects: [], + isLoadingProjects: false, + errorLoadingProjects: false, +}); diff --git a/ee/app/views/groups/security/dashboard/show.html.haml b/ee/app/views/groups/security/dashboard/show.html.haml index b5a5f078557574..5cf0ecf35c1fdd 100644 --- a/ee/app/views/groups/security/dashboard/show.html.haml +++ b/ee/app/views/groups/security/dashboard/show.html.haml @@ -1,9 +1,12 @@ - breadcrumb_title _("Security Dashboard") - page_title _("Security Dashboard") +-# TODO: Use a more sensible way of getting this endpoint +- group_projects_path = "/api/v4/groups/#{@group.id}/projects" #js-group-security-dashboard{ data: { vulnerabilities_endpoint: group_security_vulnerabilities_path(@group), vulnerabilities_summary_endpoint: summary_group_security_vulnerabilities_path(@group), vulnerabilities_history_endpoint: history_group_security_vulnerabilities_path(@group), + projects_endpoint: group_projects_path, vulnerability_feedback_help_path: help_page_path("user/project/merge_requests/index", anchor: "interacting-with-security-reports-ultimate"), empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'), dashboard_documentation: help_page_path('user/group/security_dashboard/index') } } diff --git a/ee/spec/javascripts/security_dashboard/components/filters_spec.js b/ee/spec/javascripts/security_dashboard/components/filters_spec.js index ea00a30a102c88..8fd344d7659f31 100644 --- a/ee/spec/javascripts/security_dashboard/components/filters_spec.js +++ b/ee/spec/javascripts/security_dashboard/components/filters_spec.js @@ -18,8 +18,8 @@ describe('Filter component', () => { vm.$destroy(); }); - it('should display both filters', () => { - expect(vm.$el.querySelectorAll('.js-filter').length).toEqual(2); + it('should display all filters', () => { + expect(vm.$el.querySelectorAll('.js-filter').length).toEqual(3); }); }); }); diff --git a/ee/spec/javascripts/security_dashboard/store/filters/actions_spec.js b/ee/spec/javascripts/security_dashboard/store/filters/actions_spec.js index 03d00185d2307d..a4d74acc68183a 100644 --- a/ee/spec/javascripts/security_dashboard/store/filters/actions_spec.js +++ b/ee/spec/javascripts/security_dashboard/store/filters/actions_spec.js @@ -25,4 +25,25 @@ describe('filters actions', () => { ); }); }); + + describe('setFilterOptions', () => { + it('should commit the SET_FILTER_OPTIONS mutuation', done => { + const state = createState(); + const payload = { filterId: 'project', options: [] }; + + testAction( + actions.setFilterOptions, + payload, + state, + [ + { + type: types.SET_FILTER_OPTIONS, + payload, + }, + ], + [], + done, + ); + }); + }); }); diff --git a/ee/spec/javascripts/security_dashboard/store/filters/getters_spec.js b/ee/spec/javascripts/security_dashboard/store/filters/getters_spec.js index 76fcfb8f00486b..16983b94eabcf9 100644 --- a/ee/spec/javascripts/security_dashboard/store/filters/getters_spec.js +++ b/ee/spec/javascripts/security_dashboard/store/filters/getters_spec.js @@ -58,16 +58,14 @@ describe('filters module getters', () => { }); describe('getSelectedOptionIds', () => { - it('should return "one" as the selcted project ID', () => { + it('should return "one" as the selcted dummy ID', () => { const state = createState(); - const projectFilter = { - id: 'project', + const dummyFilter = { + id: 'dummy', options: [{ id: 'one', selected: true }, { id: 'anotherone', selected: false }], }; - state.filters.push(projectFilter); - const selectedOptionIds = getters.getSelectedOptionIds(state, mockedGetters(state))( - 'project', - ); + state.filters.push(dummyFilter); + const selectedOptionIds = getters.getSelectedOptionIds(state, mockedGetters(state))('dummy'); expect(selectedOptionIds).toHaveLength(1); expect(selectedOptionIds[0]).toEqual('one'); @@ -109,16 +107,16 @@ describe('filters module getters', () => { expect(activeFilters.severity).toHaveLength(0); }); - it('should return multiple project filters"', () => { + it('should return multiple dummy filters"', () => { const state = createState(); - const projectFilter = { - id: 'project', + const dummyFilter = { + id: 'dummy', options: [{ id: 'one', selected: true }, { id: 'anotherone', selected: true }], }; - state.filters.push(projectFilter); + state.filters.push(dummyFilter); const activeFilters = getters.activeFilters(state, mockedGetters(state)); - expect(activeFilters.project).toHaveLength(2); + expect(activeFilters.dummy).toHaveLength(2); }); }); }); diff --git a/ee/spec/javascripts/security_dashboard/store/filters/mutations_spec.js b/ee/spec/javascripts/security_dashboard/store/filters/mutations_spec.js index 6bca9e13c61c17..aff84fdaf8b49e 100644 --- a/ee/spec/javascripts/security_dashboard/store/filters/mutations_spec.js +++ b/ee/spec/javascripts/security_dashboard/store/filters/mutations_spec.js @@ -40,4 +40,22 @@ describe('filters module mutations', () => { }); }); }); + + describe('SET_FILTER_OPTIONS', () => { + let state; + let firstFilter; + const options = [{ id: 0, name: 'c' }, { id: 3, name: 'c' }]; + + beforeEach(() => { + state = createState(); + [firstFilter] = state.filters; + const filterId = firstFilter.id; + + mutations[types.SET_FILTER_OPTIONS](state, { filterId, options }); + }); + + it('should add all the options to the type filter', () => { + expect(firstFilter.options).toEqual(options); + }); + }); }); diff --git a/ee/spec/javascripts/security_dashboard/store/projects/actions_spec.js b/ee/spec/javascripts/security_dashboard/store/projects/actions_spec.js new file mode 100644 index 00000000000000..d9333c35230b02 --- /dev/null +++ b/ee/spec/javascripts/security_dashboard/store/projects/actions_spec.js @@ -0,0 +1,132 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import testAction from 'spec/helpers/vuex_action_helper'; +import { TEST_HOST } from 'spec/test_constants'; + +import createState from 'ee/security_dashboard/store/modules/projects/state'; +import * as types from 'ee/security_dashboard/store/modules/projects/mutation_types'; +import * as actions from 'ee/security_dashboard/store/modules/projects/actions'; + +import mockData from './data/mock_data.json'; + +describe('projects actions', () => { + const data = mockData; + const endpoint = `${TEST_HOST}/projects.json`; + + describe('fetchProjects', () => { + let mock; + const state = createState(); + + beforeEach(() => { + state.projectsEndpoint = endpoint; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('on success', () => { + beforeEach(() => { + mock.onGet(state.projectsEndpoint).replyOnce(200, data); + }); + + it('should dispatch the request and success actions', done => { + testAction( + actions.fetchProjects, + {}, + state, + [], + [ + { type: 'requestProjects' }, + { + type: 'receiveProjectsSuccess', + payload: { data }, + }, + ], + done, + ); + }); + }); + + describe('on error', () => { + beforeEach(() => { + mock.onGet(state.projectsEndpoint).replyOnce(404, {}); + }); + + it('should dispatch the request and error actions', done => { + testAction( + actions.fetchProjects, + {}, + state, + [], + [{ type: 'requestProjects' }, { type: 'receiveProjectsError' }], + done, + ); + }); + }); + }); + + describe('receiveProjectsSuccess', () => { + it('should commit the success mutation', done => { + const state = createState(); + + testAction( + actions.receiveProjectsSuccess, + { data }, + state, + [ + { + type: types.RECEIVE_PROJECTS_SUCCESS, + payload: { projects: data }, + }, + ], + [], + done, + ); + }); + }); + + describe('receiveProjectsError', () => { + it('should commit the error mutation', done => { + const state = createState(); + + testAction( + actions.receiveProjectsError, + {}, + state, + [{ type: types.RECEIVE_PROJECTS_ERROR }], + [], + done, + ); + }); + }); + + describe('requestProjects', () => { + it('should commit the request mutation', done => { + const state = createState(); + + testAction(actions.requestProjects, {}, state, [{ type: types.REQUEST_PROJECTS }], [], done); + }); + }); + + describe('setProjectsEndpoint', () => { + it('should commit the correct mutuation', done => { + const state = createState(); + + testAction( + actions.setProjectsEndpoint, + endpoint, + state, + [ + { + type: types.SET_PROJECTS_ENDPOINT, + payload: endpoint, + }, + ], + [], + done, + ); + }); + }); +}); diff --git a/ee/spec/javascripts/security_dashboard/store/projects/data/mock_data.json b/ee/spec/javascripts/security_dashboard/store/projects/data/mock_data.json new file mode 100644 index 00000000000000..9dfa3614a9ea4b --- /dev/null +++ b/ee/spec/javascripts/security_dashboard/store/projects/data/mock_data.json @@ -0,0 +1,39 @@ +[ + { + "id": 9, + "description": "foo", + "default_branch": "master", + "tag_list": [], + "archived": false, + "visibility": "internal", + "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git", + "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git", + "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate", + "name": "Html5 Boilerplate", + "name_with_namespace": "Experimental / Html5 Boilerplate", + "path": "html5-boilerplate", + "path_with_namespace": "h5bp/html5-boilerplate", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "jobs_enabled": true, + "snippets_enabled": true, + "created_at": "2016-04-05T21:40:50.169Z", + "last_activity_at": "2016-04-06T16:52:08.432Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 5, + "name": "Experimental", + "path": "h5bp", + "kind": "group" + }, + "avatar_url": null, + "star_count": 1, + "forks_count": 0, + "open_issues_count": 3, + "public_jobs": true, + "shared_with_groups": [], + "request_access_enabled": false + } +] \ No newline at end of file diff --git a/ee/spec/javascripts/security_dashboard/store/projects/mutations_spec.js b/ee/spec/javascripts/security_dashboard/store/projects/mutations_spec.js new file mode 100644 index 00000000000000..be76d00b98164d --- /dev/null +++ b/ee/spec/javascripts/security_dashboard/store/projects/mutations_spec.js @@ -0,0 +1,72 @@ +import createState from 'ee/security_dashboard/store/modules/projects/state'; +import * as types from 'ee/security_dashboard/store/modules/projects/mutation_types'; +import mutations from 'ee/security_dashboard/store/modules/projects/mutations'; +import mockData from './data/mock_data.json'; + +describe('projects module mutations', () => { + describe('SET_PROJECTS_ENDPOINT', () => { + it('should set `projectsEndpoint` to `fakepath.json`', () => { + const state = createState(); + const endpoint = 'fakepath.json'; + + mutations[types.SET_PROJECTS_ENDPOINT](state, endpoint); + + expect(state.projectsEndpoint).toEqual(endpoint); + }); + }); + + describe('REQUEST_PROJECTS', () => { + let state; + + beforeEach(() => { + state = { + ...createState(), + errorLoadingProjects: true, + }; + mutations[types.REQUEST_PROJECTS](state); + }); + + it('should set `isLoadingProjects` to `true`', () => { + expect(state.isLoadingProjects).toBeTruthy(); + }); + + it('should set `errorLoadingProjects` to `false`', () => { + expect(state.errorLoadingProjects).toBeFalsy(); + }); + }); + + describe('RECEIVE_PROJECTS_SUCCESS', () => { + let payload; + let state; + + beforeEach(() => { + payload = { + projects: mockData, + }; + state = createState(); + mutations[types.RECEIVE_PROJECTS_SUCCESS](state, payload); + }); + + it('should set `isLoadingProjects` to `false`', () => { + expect(state.isLoadingProjects).toBeFalsy(); + }); + + it('should set `pageInfo`', () => { + expect(state.pageInfo).toBe(payload.pageInfo); + }); + + it('should set `projects`', () => { + expect(state.projects).toBe(payload.projects); + }); + }); + + describe('RECEIVE_PROJECTS_ERROR', () => { + it('should set `isLoadingProjects` to `false`', () => { + const state = createState(); + + mutations[types.RECEIVE_PROJECTS_ERROR](state); + + expect(state.isLoadingProjects).toBeFalsy(); + }); + }); +}); -- GitLab From faa8258c93851078d9146b86400e0e9ac64f97ba Mon Sep 17 00:00:00 2001 From: samdbeckham Date: Fri, 4 Jan 2019 10:26:14 +0000 Subject: [PATCH 2/5] Adds changes from @leipert review - Adds a better way of getting the projects endpoint --- ee/app/views/groups/security/dashboard/show.html.haml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ee/app/views/groups/security/dashboard/show.html.haml b/ee/app/views/groups/security/dashboard/show.html.haml index 5cf0ecf35c1fdd..300a3e4783744a 100644 --- a/ee/app/views/groups/security/dashboard/show.html.haml +++ b/ee/app/views/groups/security/dashboard/show.html.haml @@ -1,12 +1,10 @@ - breadcrumb_title _("Security Dashboard") - page_title _("Security Dashboard") --# TODO: Use a more sensible way of getting this endpoint -- group_projects_path = "/api/v4/groups/#{@group.id}/projects" #js-group-security-dashboard{ data: { vulnerabilities_endpoint: group_security_vulnerabilities_path(@group), vulnerabilities_summary_endpoint: summary_group_security_vulnerabilities_path(@group), vulnerabilities_history_endpoint: history_group_security_vulnerabilities_path(@group), - projects_endpoint: group_projects_path, + projects_endpoint: expose_url(api_v4_groups_projects_path(id: @group.id)), vulnerability_feedback_help_path: help_page_path("user/project/merge_requests/index", anchor: "interacting-with-security-reports-ultimate"), empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'), dashboard_documentation: help_page_path('user/group/security_dashboard/index') } } -- GitLab From bb879b2b94cba7e361ea339bfc893e3c5ce791b7 Mon Sep 17 00:00:00 2001 From: samdbeckham Date: Fri, 4 Jan 2019 11:54:16 +0000 Subject: [PATCH 3/5] Adds a changelog entry for 8362 --- ee/changelogs/unreleased/6240-project-filter-for-gsd.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 ee/changelogs/unreleased/6240-project-filter-for-gsd.yml diff --git a/ee/changelogs/unreleased/6240-project-filter-for-gsd.yml b/ee/changelogs/unreleased/6240-project-filter-for-gsd.yml new file mode 100644 index 00000000000000..50bec364860623 --- /dev/null +++ b/ee/changelogs/unreleased/6240-project-filter-for-gsd.yml @@ -0,0 +1,5 @@ +--- +title: Adds project filtering to the GSD +merge_request: 8944 +author: +type: added -- GitLab From fbee16a31fdf506f8195b09801959f63e727872c Mon Sep 17 00:00:00 2001 From: samdbeckham Date: Fri, 4 Jan 2019 13:51:37 +0000 Subject: [PATCH 4/5] Adds changes from @kushalpandya review - improves tests with beforeEach and stricter value checks - improves actions to reduce additional desctructuring - uses vue.$options to improve the report_type_popover i18n --- .../components/report_type_popover.vue | 8 ++++---- .../store/modules/projects/actions.js | 9 +++------ .../store/filters/getters_spec.js | 15 +++++++-------- .../store/projects/actions_spec.js | 4 ++-- .../store/projects/mutations_spec.js | 8 ++++---- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/ee/app/assets/javascripts/security_dashboard/components/report_type_popover.vue b/ee/app/assets/javascripts/security_dashboard/components/report_type_popover.vue index d415376598a1bb..820b34fe44d865 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/report_type_popover.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/report_type_popover.vue @@ -1,5 +1,6 @@ @@ -33,12 +35,10 @@ export default { v-if="dashboardDocumentation" target="_blank" rel="noopener noreferrer" - :title="s__('Security Reports|Security dashboard documentation')" + :title="$options.linkTitle" :href="dashboardDocumentation" > - {{ - s__('Security Reports|Security dashboard documentation') - }} + {{ $options.linkTitle }} diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/actions.js b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/actions.js index 799703074f9769..73b1d574de84f5 100644 --- a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/actions.js +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/actions.js @@ -12,9 +12,8 @@ export const fetchProjects = ({ state, dispatch }) => { method: 'GET', url: state.projectsEndpoint, }) - .then(response => { - const { data } = response; - dispatch('receiveProjectsSuccess', { data }); + .then(({ data }) => { + dispatch('receiveProjectsSuccess', { projects: data }); }) .catch(() => { dispatch('receiveProjectsError'); @@ -25,9 +24,7 @@ export const requestProjects = ({ commit }) => { commit(types.REQUEST_PROJECTS); }; -export const receiveProjectsSuccess = ({ commit }, { data }) => { - const projects = data; - +export const receiveProjectsSuccess = ({ commit }, { projects }) => { commit(types.RECEIVE_PROJECTS_SUCCESS, { projects }); }; diff --git a/ee/spec/javascripts/security_dashboard/store/filters/getters_spec.js b/ee/spec/javascripts/security_dashboard/store/filters/getters_spec.js index 16983b94eabcf9..a1f6c2bc15fa5e 100644 --- a/ee/spec/javascripts/security_dashboard/store/filters/getters_spec.js +++ b/ee/spec/javascripts/security_dashboard/store/filters/getters_spec.js @@ -17,10 +17,14 @@ describe('filters module getters', () => { getFilterIds, }; }; + let state; + + beforeEach(() => { + state = createState(); + }); describe('getFilter', () => { it('should return the type filter information', () => { - const state = createState(); const typeFilter = getters.getFilter(state)('report_type'); expect(typeFilter.name).toEqual('Report type'); @@ -30,7 +34,6 @@ describe('filters module getters', () => { describe('getSelectedOptions', () => { describe('with one selected option', () => { it('should return "All" as the selected option', () => { - const state = createState(); const selectedOptions = getters.getSelectedOptions(state, mockedGetters(state))( 'report_type', ); @@ -42,7 +45,7 @@ describe('filters module getters', () => { describe('with multiple selected options', () => { it('should return both "High" and "Critical" ', () => { - const state = { + state = { filters: [ { id: 'severity', @@ -59,7 +62,6 @@ describe('filters module getters', () => { describe('getSelectedOptionIds', () => { it('should return "one" as the selcted dummy ID', () => { - const state = createState(); const dummyFilter = { id: 'dummy', options: [{ id: 'one', selected: true }, { id: 'anotherone', selected: false }], @@ -74,7 +76,6 @@ describe('filters module getters', () => { describe('getSelectedOptionNames', () => { it('should return "All" as the selected option', () => { - const state = createState(); const selectedOptionNames = getters.getSelectedOptionNames(state, mockedGetters(state))( 'severity', ); @@ -83,7 +84,7 @@ describe('filters module getters', () => { }); it('should return the correct message when multiple filters are selected', () => { - const state = { + state = { filters: [ { id: 'severity', @@ -101,14 +102,12 @@ describe('filters module getters', () => { describe('activeFilters', () => { it('should return no severity filters', () => { - const state = createState(); const activeFilters = getters.activeFilters(state, mockedGetters(state)); expect(activeFilters.severity).toHaveLength(0); }); it('should return multiple dummy filters"', () => { - const state = createState(); const dummyFilter = { id: 'dummy', options: [{ id: 'one', selected: true }, { id: 'anotherone', selected: true }], diff --git a/ee/spec/javascripts/security_dashboard/store/projects/actions_spec.js b/ee/spec/javascripts/security_dashboard/store/projects/actions_spec.js index d9333c35230b02..d3ee398ffc46c8 100644 --- a/ee/spec/javascripts/security_dashboard/store/projects/actions_spec.js +++ b/ee/spec/javascripts/security_dashboard/store/projects/actions_spec.js @@ -41,7 +41,7 @@ describe('projects actions', () => { { type: 'requestProjects' }, { type: 'receiveProjectsSuccess', - payload: { data }, + payload: { projects: data }, }, ], done, @@ -73,7 +73,7 @@ describe('projects actions', () => { testAction( actions.receiveProjectsSuccess, - { data }, + { projects: data }, state, [ { diff --git a/ee/spec/javascripts/security_dashboard/store/projects/mutations_spec.js b/ee/spec/javascripts/security_dashboard/store/projects/mutations_spec.js index be76d00b98164d..df2b21dea2166e 100644 --- a/ee/spec/javascripts/security_dashboard/store/projects/mutations_spec.js +++ b/ee/spec/javascripts/security_dashboard/store/projects/mutations_spec.js @@ -27,11 +27,11 @@ describe('projects module mutations', () => { }); it('should set `isLoadingProjects` to `true`', () => { - expect(state.isLoadingProjects).toBeTruthy(); + expect(state.isLoadingProjects).toBe(true); }); it('should set `errorLoadingProjects` to `false`', () => { - expect(state.errorLoadingProjects).toBeFalsy(); + expect(state.errorLoadingProjects).toBe(false); }); }); @@ -48,7 +48,7 @@ describe('projects module mutations', () => { }); it('should set `isLoadingProjects` to `false`', () => { - expect(state.isLoadingProjects).toBeFalsy(); + expect(state.isLoadingProjects).toBe(false); }); it('should set `pageInfo`', () => { @@ -66,7 +66,7 @@ describe('projects module mutations', () => { mutations[types.RECEIVE_PROJECTS_ERROR](state); - expect(state.isLoadingProjects).toBeFalsy(); + expect(state.isLoadingProjects).toBe(false); }); }); }); -- GitLab From 9f55b6aaeedd5730658688d05343f39f23bcdec0 Mon Sep 17 00:00:00 2001 From: samdbeckham Date: Mon, 7 Jan 2019 10:53:36 +0000 Subject: [PATCH 5/5] Adds changes from @ayufan review - Adds whitespace to end of projects mock --- .../security_dashboard/store/projects/data/mock_data.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/spec/javascripts/security_dashboard/store/projects/data/mock_data.json b/ee/spec/javascripts/security_dashboard/store/projects/data/mock_data.json index 9dfa3614a9ea4b..e834a0c58f1c61 100644 --- a/ee/spec/javascripts/security_dashboard/store/projects/data/mock_data.json +++ b/ee/spec/javascripts/security_dashboard/store/projects/data/mock_data.json @@ -36,4 +36,4 @@ "shared_with_groups": [], "request_access_enabled": false } -] \ No newline at end of file +] -- GitLab