Commit eaf38723 authored by Tim Zallmann's avatar Tim Zallmann

Merge branch '6240-project-filter-for-gsd' into 'master'

Adds project filtering to the GSD

See merge request gitlab-org/gitlab-ee!8944
parents 9b85d7c0 9f55b6aa
...@@ -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);
......
<script> <script>
import { GlPopover } from '@gitlab/ui'; import { GlPopover } from '@gitlab/ui';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
export default { export default {
...@@ -13,6 +14,7 @@ export default { ...@@ -13,6 +14,7 @@ export default {
required: true, required: true,
}, },
}, },
linkTitle: s__('Security Reports|Security dashboard documentation'),
}; };
</script> </script>
...@@ -33,12 +35,10 @@ export default { ...@@ -33,12 +35,10 @@ export default {
v-if="dashboardDocumentation" v-if="dashboardDocumentation"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
:title="s__('Security Reports|Security dashboard documentation')" :title="$options.linkTitle"
:href="dashboardDocumentation" :href="dashboardDocumentation"
> >
<span class="vertical-align-middle">{{ <span class="vertical-align-middle">{{ $options.linkTitle }}</span>
s__('Security Reports|Security dashboard documentation')
}}</span>
<icon name="external-link" :size="16" class="vertical-align-middle" /> <icon name="external-link" :size="16" class="vertical-align-middle" />
</a> </a>
</gl-popover> </gl-popover>
......
...@@ -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: {
vulnerabilities,
filters, filters,
projects,
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(({ data }) => {
dispatch('receiveProjectsSuccess', { projects: data });
})
.catch(() => {
dispatch('receiveProjectsError');
});
};
export const requestProjects = ({ commit }) => {
commit(types.REQUEST_PROJECTS);
};
export const receiveProjectsSuccess = ({ commit }, { projects }) => {
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,
});
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
#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: 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"), 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') } }
---
title: Adds project filtering to the GSD
merge_request: 8944
author:
type: added
...@@ -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,
);
});
});
}); });
...@@ -17,10 +17,14 @@ describe('filters module getters', () => { ...@@ -17,10 +17,14 @@ describe('filters module getters', () => {
getFilterIds, getFilterIds,
}; };
}; };
let state;
beforeEach(() => {
state = createState();
});
describe('getFilter', () => { describe('getFilter', () => {
it('should return the type filter information', () => { it('should return the type filter information', () => {
const state = createState();
const typeFilter = getters.getFilter(state)('report_type'); const typeFilter = getters.getFilter(state)('report_type');
expect(typeFilter.name).toEqual('Report type'); expect(typeFilter.name).toEqual('Report type');
...@@ -30,7 +34,6 @@ describe('filters module getters', () => { ...@@ -30,7 +34,6 @@ describe('filters module getters', () => {
describe('getSelectedOptions', () => { describe('getSelectedOptions', () => {
describe('with one selected option', () => { describe('with one selected option', () => {
it('should return "All" as the selected option', () => { it('should return "All" as the selected option', () => {
const state = createState();
const selectedOptions = getters.getSelectedOptions(state, mockedGetters(state))( const selectedOptions = getters.getSelectedOptions(state, mockedGetters(state))(
'report_type', 'report_type',
); );
...@@ -42,7 +45,7 @@ describe('filters module getters', () => { ...@@ -42,7 +45,7 @@ describe('filters module getters', () => {
describe('with multiple selected options', () => { describe('with multiple selected options', () => {
it('should return both "High" and "Critical" ', () => { it('should return both "High" and "Critical" ', () => {
const state = { state = {
filters: [ filters: [
{ {
id: 'severity', id: 'severity',
...@@ -58,16 +61,13 @@ describe('filters module getters', () => { ...@@ -58,16 +61,13 @@ 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 dummyFilter = {
const projectFilter = { id: 'dummy',
id: 'project',
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');
...@@ -76,7 +76,6 @@ describe('filters module getters', () => { ...@@ -76,7 +76,6 @@ describe('filters module getters', () => {
describe('getSelectedOptionNames', () => { describe('getSelectedOptionNames', () => {
it('should return "All" as the selected option', () => { it('should return "All" as the selected option', () => {
const state = createState();
const selectedOptionNames = getters.getSelectedOptionNames(state, mockedGetters(state))( const selectedOptionNames = getters.getSelectedOptionNames(state, mockedGetters(state))(
'severity', 'severity',
); );
...@@ -85,7 +84,7 @@ describe('filters module getters', () => { ...@@ -85,7 +84,7 @@ describe('filters module getters', () => {
}); });
it('should return the correct message when multiple filters are selected', () => { it('should return the correct message when multiple filters are selected', () => {
const state = { state = {
filters: [ filters: [
{ {
id: 'severity', id: 'severity',
...@@ -103,22 +102,20 @@ describe('filters module getters', () => { ...@@ -103,22 +102,20 @@ describe('filters module getters', () => {
describe('activeFilters', () => { describe('activeFilters', () => {
it('should return no severity filters', () => { it('should return no severity filters', () => {
const state = createState();
const activeFilters = getters.activeFilters(state, mockedGetters(state)); const activeFilters = getters.activeFilters(state, mockedGetters(state));
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 dummyFilter = {
const projectFilter = { id: 'dummy',
id: 'project',
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: { projects: 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,
{ projects: 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
}
]
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).toBe(true);
});
it('should set `errorLoadingProjects` to `false`', () => {
expect(state.errorLoadingProjects).toBe(false);
});
});
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).toBe(false);
});
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).toBe(false);
});
});
});
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