Commit 5cc6af9f authored by samdbeckham's avatar samdbeckham

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
parent acad50ce
...@@ -24,6 +24,10 @@ export default { ...@@ -24,6 +24,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
projectsEndpoint: {
type: String,
required: true,
},
vulnerabilitiesEndpoint: { vulnerabilitiesEndpoint: {
type: String, type: String,
required: true, required: true,
...@@ -43,13 +47,16 @@ export default { ...@@ -43,13 +47,16 @@ export default {
}, },
computed: { computed: {
...mapState('vulnerabilities', ['modal']), ...mapState('vulnerabilities', ['modal']),
...mapState('projects', ['projects']),
...mapGetters('filters', ['activeFilters']), ...mapGetters('filters', ['activeFilters']),
}, },
created() { created() {
this.setProjectsEndpoint(this.projectsEndpoint);
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.fetchVulnerabilitiesCount();
this.fetchProjects();
}, },
methods: { methods: {
...mapActions('vulnerabilities', [ ...mapActions('vulnerabilities', [
...@@ -63,6 +70,7 @@ export default { ...@@ -63,6 +70,7 @@ export default {
'setVulnerabilitiesEndpoint', 'setVulnerabilitiesEndpoint',
'setVulnerabilitiesHistoryEndpoint', 'setVulnerabilitiesHistoryEndpoint',
]), ]),
...mapActions('projects', ['setProjectsEndpoint', 'fetchProjects']),
filterChange() { filterChange() {
this.fetchVulnerabilities(this.activeFilters); this.fetchVulnerabilities(this.activeFilters);
this.fetchVulnerabilitiesCount(this.activeFilters); this.fetchVulnerabilitiesCount(this.activeFilters);
......
...@@ -16,6 +16,7 @@ export default () => { ...@@ -16,6 +16,7 @@ export default () => {
props: { props: {
dashboardDocumentation: el.dataset.dashboardDocumentation, dashboardDocumentation: el.dataset.dashboardDocumentation,
emptyStateSvgPath: el.dataset.emptyStateSvgPath, emptyStateSvgPath: el.dataset.emptyStateSvgPath,
projectsEndpoint: el.dataset.projectsEndpoint,
vulnerabilityFeedbackHelpPath: el.dataset.vulnerabilityFeedbackHelpPath, vulnerabilityFeedbackHelpPath: el.dataset.vulnerabilityFeedbackHelpPath,
vulnerabilitiesEndpoint: el.dataset.vulnerabilitiesEndpoint, vulnerabilitiesEndpoint: el.dataset.vulnerabilitiesEndpoint,
vulnerabilitiesCountEndpoint: el.dataset.vulnerabilitiesSummaryEndpoint, vulnerabilitiesCountEndpoint: el.dataset.vulnerabilitiesSummaryEndpoint,
......
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import vulnerabilities from './modules/vulnerabilities/index'; import configureModerator from './moderator';
import filters from './modules/filters/index'; import filters from './modules/filters/index';
import projects from './modules/projects/index';
import vulnerabilities from './modules/vulnerabilities/index';
Vue.use(Vuex); Vue.use(Vuex);
export default () => const store = new Vuex.Store({
new Vuex.Store({ modules: {
modules: { filters,
vulnerabilities, projects,
filters, vulnerabilities,
}, },
}); });
configureModerator(store);
export default () => store;
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;
}
});
}
...@@ -4,6 +4,10 @@ export const setFilter = ({ commit }, payload) => { ...@@ -4,6 +4,10 @@ export const setFilter = ({ commit }, payload) => {
commit(types.SET_FILTER, 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 // 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 () => {};
export const SET_FILTER = 'SET_FILTER'; export const SET_FILTER = 'SET_FILTER';
export const SET_FILTER_OPTIONS = 'SET_FILTER_OPTIONS';
// This is here because es-lint requires a default export when there are less than two named exports
export default SET_FILTER;
...@@ -41,4 +41,8 @@ export default { ...@@ -41,4 +41,8 @@ export default {
activeFilter.options = activeOptions; activeFilter.options = activeOptions;
} }
}, },
[types.SET_FILTER_OPTIONS](state, payload) {
const { filterId, options } = payload;
state.filters.find(filter => filter.id === filterId).options = options;
},
}; };
...@@ -32,5 +32,16 @@ export default () => ({ ...@@ -32,5 +32,16 @@ export default () => ({
}), }),
], ],
}, },
{
name: 'Project',
id: 'project',
options: [
{
name: 'All',
id: 'all',
selected: true,
},
],
},
], ],
}); });
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 () => {};
import state from './state';
import mutations from './mutations';
import * as actions from './actions';
export default {
namespaced: true,
state,
mutations,
actions,
};
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';
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;
},
};
export default () => ({
endpoint: '',
projects: [],
isLoadingProjects: false,
errorLoadingProjects: false,
});
- breadcrumb_title _("Security Dashboard") - breadcrumb_title _("Security Dashboard")
- page_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), #js-group-security-dashboard{ data: { vulnerabilities_endpoint: group_security_vulnerabilities_path(@group),
vulnerabilities_summary_endpoint: summary_group_security_vulnerabilities_path(@group), vulnerabilities_summary_endpoint: summary_group_security_vulnerabilities_path(@group),
vulnerabilities_history_endpoint: history_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"), 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'), empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
dashboard_documentation: help_page_path('user/group/security_dashboard/index') } } dashboard_documentation: help_page_path('user/group/security_dashboard/index') } }
...@@ -18,8 +18,8 @@ describe('Filter component', () => { ...@@ -18,8 +18,8 @@ describe('Filter component', () => {
vm.$destroy(); vm.$destroy();
}); });
it('should display both filters', () => { it('should display all filters', () => {
expect(vm.$el.querySelectorAll('.js-filter').length).toEqual(2); expect(vm.$el.querySelectorAll('.js-filter').length).toEqual(3);
}); });
}); });
}); });
...@@ -25,4 +25,25 @@ describe('filters actions', () => { ...@@ -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,
);
});
});
}); });
...@@ -58,16 +58,14 @@ describe('filters module getters', () => { ...@@ -58,16 +58,14 @@ describe('filters module getters', () => {
}); });
describe('getSelectedOptionIds', () => { describe('getSelectedOptionIds', () => {
it('should return "one" as the selcted project ID', () => { it('should return "one" as the selcted dummy ID', () => {
const state = createState(); const state = createState();
const projectFilter = { const dummyFilter = {
id: 'project', id: 'dummy',
options: [{ id: 'one', selected: true }, { id: 'anotherone', selected: false }], options: [{ id: 'one', selected: true }, { id: 'anotherone', selected: false }],
}; };
state.filters.push(projectFilter); state.filters.push(dummyFilter);
const selectedOptionIds = getters.getSelectedOptionIds(state, mockedGetters(state))( const selectedOptionIds = getters.getSelectedOptionIds(state, mockedGetters(state))('dummy');
'project',
);
expect(selectedOptionIds).toHaveLength(1); expect(selectedOptionIds).toHaveLength(1);
expect(selectedOptionIds[0]).toEqual('one'); expect(selectedOptionIds[0]).toEqual('one');
...@@ -109,16 +107,16 @@ describe('filters module getters', () => { ...@@ -109,16 +107,16 @@ describe('filters module getters', () => {
expect(activeFilters.severity).toHaveLength(0); expect(activeFilters.severity).toHaveLength(0);
}); });
it('should return multiple project filters"', () => { it('should return multiple dummy filters"', () => {
const state = createState(); const state = createState();
const projectFilter = { const dummyFilter = {
id: 'project', id: 'dummy',
options: [{ id: 'one', selected: true }, { id: 'anotherone', selected: true }], options: [{ id: 'one', selected: true }, { id: 'anotherone', selected: true }],
}; };
state.filters.push(projectFilter); state.filters.push(dummyFilter);
const activeFilters = getters.activeFilters(state, mockedGetters(state)); const activeFilters = getters.activeFilters(state, mockedGetters(state));
expect(activeFilters.project).toHaveLength(2); expect(activeFilters.dummy).toHaveLength(2);
}); });
}); });
}); });
...@@ -40,4 +40,22 @@ describe('filters module mutations', () => { ...@@ -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);
});
});
}); });
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,
);
});
});
});
[
{
"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
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();
});
});
});
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