Commit 133cedea authored by David Pisek's avatar David Pisek Committed by Kushal Pandya

Add projectSelector module to security dashboard

This commit adds a new vuex store module to the security dashboard.

It is largely based off an existing store module
(ee/app/assets/javascripts/vue_shared/dashboards/store) but includes
some changes that aim to make it more generic.
parent f3ddb255
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { __, s__, sprintf } from '~/locale';
import * as types from './mutation_types';
const API_MINIMUM_QUERY_LENGTH = 3;
export const toggleSelectedProject = ({ commit, state }, project) => {
const isProject = ({ id }) => id === project.id;
if (state.selectedProjects.some(isProject)) {
commit(types.DESELECT_PROJECT, project);
} else {
commit(types.SELECT_PROJECT, project);
}
};
export const clearSearchResults = ({ commit }) => {
commit(types.CLEAR_SEARCH_RESULTS);
};
export const setSearchQuery = ({ commit }, query) => {
commit(types.SET_SEARCH_QUERY, query);
};
export const setProjectEndpoints = ({ commit }, endpoints) => {
commit(types.SET_PROJECT_ENDPOINTS, endpoints);
};
export const addProjects = ({ state, dispatch }) => {
dispatch('requestAddProjects');
return axios
.post(state.projectEndpoints.add, {
project_ids: state.selectedProjects.map(p => p.id),
})
.then(response => dispatch('receiveAddProjectsSuccess', response.data))
.catch(() => dispatch('receiveAddProjectsError'));
};
export const requestAddProjects = ({ commit }) => {
commit(types.REQUEST_ADD_PROJECTS);
};
export const receiveAddProjectsSuccess = ({ commit, dispatch, state }, data) => {
const { added, invalid } = data;
commit(types.RECEIVE_ADD_PROJECTS_SUCCESS);
if (invalid.length) {
const [firstProject, secondProject, ...rest] = state.selectedProjects
.filter(project => invalid.includes(project.id))
.map(project => project.name);
const translationValues = {
firstProject,
secondProject,
rest: rest.join(', '),
};
let invalidProjects;
if (rest.length > 0) {
invalidProjects = sprintf(
s__('SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}'),
translationValues,
);
} else if (secondProject) {
invalidProjects = sprintf(
s__('SecurityDashboard|%{firstProject} and %{secondProject}'),
translationValues,
);
} else {
invalidProjects = firstProject;
}
createFlash(
sprintf(s__('SecurityDashboard|Unable to add %{invalidProjects}'), {
invalidProjects,
}),
);
}
if (added.length) {
dispatch('fetchProjects');
}
};
export const receiveAddProjectsError = ({ commit }) => {
commit(types.RECEIVE_ADD_PROJECTS_ERROR);
createFlash(__('Something went wrong, unable to add projects to dashboard'));
};
export const fetchProjects = ({ state, dispatch }) => {
dispatch('requestProjects');
return axios
.get(state.projectEndpoints.list)
.then(({ data }) => {
dispatch('receiveProjectsSuccess', 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);
createFlash(__('Something went wrong, unable to get projects'));
};
export const removeProject = ({ dispatch }, removePath) => {
dispatch('requestRemoveProject');
return axios
.delete(removePath)
.then(() => {
dispatch('receiveRemoveProjectSuccess');
})
.catch(() => dispatch('receiveRemoveProjectError'));
};
export const requestRemoveProject = ({ commit }) => {
commit(types.REQUEST_REMOVE_PROJECT);
};
export const receiveRemoveProjectSuccess = ({ commit, dispatch }) => {
commit(types.RECEIVE_REMOVE_PROJECT_SUCCESS);
dispatch('fetchProjects');
};
export const receiveRemoveProjectError = ({ commit }) => {
commit(types.RECEIVE_REMOVE_PROJECT_ERROR);
createFlash(__('Something went wrong, unable to remove project'));
};
export const fetchSearchResults = ({ state, dispatch }) => {
const { searchQuery } = state;
dispatch('requestSearchResults');
if (!searchQuery || searchQuery.length < API_MINIMUM_QUERY_LENGTH) {
return dispatch('setMinimumQueryMessage');
}
return Api.projects(searchQuery, {})
.then(results => dispatch('receiveSearchResultsSuccess', results))
.catch(() => dispatch('receiveSearchResultsError'));
};
export const requestSearchResults = ({ commit }) => {
commit(types.REQUEST_SEARCH_RESULTS);
};
export const receiveSearchResultsSuccess = ({ commit }, results) => {
commit(types.RECEIVE_SEARCH_RESULTS_SUCCESS, results);
};
export const receiveSearchResultsError = ({ commit }) => {
commit(types.RECEIVE_SEARCH_RESULTS_ERROR);
};
export const setMinimumQueryMessage = ({ commit }) => {
commit(types.SET_MINIMUM_QUERY_MESSAGE);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
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_PROJECT_ENDPOINTS = 'SET_PROJECT_ENDPOINTS';
export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
export const SELECT_PROJECT = 'SELECT_PROJECT';
export const DESELECT_PROJECT = 'DESELECT_PROJECT';
export const REQUEST_ADD_PROJECTS = 'REQUEST_ADD_PROJECTS';
export const RECEIVE_ADD_PROJECTS_SUCCESS = 'RECEIVE_ADD_PROJECTS_SUCCESS';
export const RECEIVE_ADD_PROJECTS_ERROR = 'RECEIVE_ADD_PROJECTS_ERROR';
export const REQUEST_REMOVE_PROJECT = 'REQUEST_REMOVE_PROJECT';
export const RECEIVE_REMOVE_PROJECT_SUCCESS = 'RECEIVE_REMOVE_PROJECT_SUCCESS';
export const RECEIVE_REMOVE_PROJECT_ERROR = 'RECEIVE_REMOVE_PROJECT_ERROR';
export const REQUEST_PROJECTS = 'REQUEST_PROJECTS';
export const RECEIVE_PROJECTS_SUCCESS = 'RECEIVE_PROJECTS_SUCCESS';
export const RECEIVE_PROJECTS_ERROR = 'RECEIVE_PROJECTS_ERROR';
export const CLEAR_SEARCH_RESULTS = 'CLEAR_SEARCH_RESULTS';
export const REQUEST_SEARCH_RESULTS = 'REQUEST_SEARCH_RESULTS';
export const RECEIVE_SEARCH_RESULTS_SUCCESS = 'RECEIVE_SEARCH_RESULTS_SUCCESS';
export const RECEIVE_SEARCH_RESULTS_ERROR = 'RECEIVE_SEARCH_RESULTS_ERROR';
export const SET_MINIMUM_QUERY_MESSAGE = 'SET_MINIMUM_QUERY_MESSAGE';
import * as types from './mutation_types';
export default {
[types.SET_PROJECT_ENDPOINTS](state, endpoints) {
state.projectEndpoints.add = endpoints.add;
state.projectEndpoints.list = endpoints.list;
},
[types.SET_SEARCH_QUERY](state, query) {
state.searchQuery = query;
},
[types.SELECT_PROJECT](state, project) {
if (!state.selectedProjects.some(p => p.id === project.id)) {
state.selectedProjects.push(project);
}
},
[types.DESELECT_PROJECT](state, project) {
state.selectedProjects = state.selectedProjects.filter(p => p.id !== project.id);
},
[types.REQUEST_ADD_PROJECTS](state) {
state.isAddingProjects = true;
},
[types.RECEIVE_ADD_PROJECTS_SUCCESS](state) {
state.isAddingProjects = false;
},
[types.RECEIVE_ADD_PROJECTS_ERROR](state) {
state.isAddingProjects = false;
},
[types.REQUEST_REMOVE_PROJECT](state) {
state.isRemovingProject = true;
},
[types.RECEIVE_REMOVE_PROJECT_SUCCESS](state) {
state.isRemovingProject = false;
},
[types.RECEIVE_REMOVE_PROJECT_ERROR](state) {
state.isRemovingProject = false;
},
[types.REQUEST_PROJECTS](state) {
state.isLoadingProjects = true;
},
[types.RECEIVE_PROJECTS_SUCCESS](state, projects) {
state.projects = projects;
state.isLoadingProjects = false;
},
[types.RECEIVE_PROJECTS_ERROR](state) {
state.projects = [];
state.isLoadingProjects = false;
},
[types.CLEAR_SEARCH_RESULTS](state) {
state.projectSearchResults = [];
state.selectedProjects = [];
},
[types.REQUEST_SEARCH_RESULTS](state) {
state.messages.minimumQuery = false;
state.searchCount += 1;
},
[types.RECEIVE_SEARCH_RESULTS_SUCCESS](state, results) {
state.projectSearchResults = results;
state.messages.noResults = state.projectSearchResults.length === 0;
state.messages.searchError = false;
state.messages.minimumQuery = false;
state.searchCount = Math.max(0, state.searchCount - 1);
},
[types.RECEIVE_SEARCH_RESULTS_ERROR](state) {
state.projectSearchResults = [];
state.messages.noResults = false;
state.messages.searchError = true;
state.messages.minimumQuery = false;
state.searchCount = Math.max(0, state.searchCount - 1);
},
[types.SET_MINIMUM_QUERY_MESSAGE](state) {
state.projectSearchResults = [];
state.messages.noResults = false;
state.messages.searchError = false;
state.messages.minimumQuery = true;
state.searchCount = Math.max(0, state.searchCount - 1);
},
};
export default () => ({
inputValue: '',
isLoadingProjects: false,
isAddingProjects: false,
isRemovingProject: false,
projectEndpoints: {
list: null,
add: null,
},
searchQuery: '',
projects: [],
projectSearchResults: [],
selectedProjects: [],
messages: {
noResults: false,
searchError: false,
minimumQuery: false,
},
searchCount: 0,
});
import createState from 'ee/security_dashboard/store/modules/project_selector/state';
import mutations from 'ee/security_dashboard/store/modules/project_selector/mutations';
import * as types from 'ee/security_dashboard/store/modules/project_selector/mutation_types';
describe('projectsSelector mutations', () => {
let state;
beforeEach(() => {
state = createState();
});
describe('SET_PROJECT_ENDPOINTS', () => {
it('sets "projectEndpoints.list" and "projectEndpoints.add"', () => {
const payload = { list: 'list', add: 'add' };
state.projectEndpoints = {};
mutations[types.SET_PROJECT_ENDPOINTS](state, payload);
expect(state.projectEndpoints.list).toBe(payload.list);
expect(state.projectEndpoints.add).toBe(payload.add);
});
});
describe('SET_SEARCH_QUERY', () => {
it('sets "searchQuery" to be the given payload', () => {
const payload = 'searchQuery';
state.searchQuery = '';
mutations[types.SET_SEARCH_QUERY](state, payload);
expect(state.searchQuery).toBe(payload);
});
});
describe('SELECT_PROJECT', () => {
it('adds the given project to "selectedProjects"', () => {
const payload = {};
state.selectedProjects = [];
mutations[types.SELECT_PROJECT](state, payload);
expect(state.selectedProjects[0]).toBe(payload);
});
it('prevents projects from being added to "selectedProjects" twice', () => {
const payload1 = { id: 1 };
const payload2 = { id: 2 };
mutations[types.SELECT_PROJECT](state, payload1);
mutations[types.SELECT_PROJECT](state, payload1);
expect(state.selectedProjects).toHaveLength(1);
mutations[types.SELECT_PROJECT](state, payload2);
expect(state.selectedProjects).toHaveLength(2);
});
});
describe('DESELECT_PROJECT', () => {
it('removes the project with the given id from "selectedProjects"', () => {
state.selectedProjects = [{ id: 1 }, { id: 2 }];
const payload = { id: 1 };
mutations[types.DESELECT_PROJECT](state, payload);
expect(state.selectedProjects).toHaveLength(1);
expect(state.selectedProjects[0].id).toBe(2);
});
});
describe('REQUEST_ADD_PROJECTS', () => {
it('sets "isAddingProjects" to be true', () => {
state.isAddingProjects = false;
mutations[types.REQUEST_ADD_PROJECTS](state);
expect(state.isAddingProjects).toBe(true);
});
});
describe('RECEIVE_ADD_PROJECTS_SUCCESS', () => {
it('sets "isAddingProjects" to be true', () => {
state.isAddingProjects = true;
mutations[types.RECEIVE_ADD_PROJECTS_SUCCESS](state);
expect(state.isAddingProjects).toBe(false);
});
});
describe('RECEIVE_ADD_PROJECTS_ERROR', () => {
it('sets "isAddingProjects" to be true', () => {
state.isAddingProjects = true;
mutations[types.RECEIVE_ADD_PROJECTS_ERROR](state);
expect(state.isAddingProjects).toBe(false);
});
});
describe('REQUEST_REMOVE_PROJECT', () => {
it('sets "isRemovingProjects" to be true', () => {
state.isRemovingProject = false;
mutations[types.REQUEST_REMOVE_PROJECT](state);
expect(state.isRemovingProject).toBe(true);
});
});
describe('RECEIVE_REMOVE_PROJECT_SUCCESS', () => {
it('sets "isRemovingProjects" to be true', () => {
state.isRemovingProject = true;
mutations[types.RECEIVE_REMOVE_PROJECT_SUCCESS](state);
expect(state.isRemovingProject).toBe(false);
});
});
describe('RECEIVE_REMOVE_PROJECT_ERROR', () => {
it('sets "isRemovingProjects" to be true', () => {
state.isRemovingProject = true;
mutations[types.RECEIVE_REMOVE_PROJECT_ERROR](state);
expect(state.isRemovingProject).toBe(false);
});
});
describe('REQUEST_PROJECTS', () => {
it('sets "isLoadingProjects" to be true', () => {
state.isLoadingProjects = false;
mutations[types.REQUEST_PROJECTS](state);
expect(state.isLoadingProjects).toBe(true);
});
});
describe('RECEIVE_PROJECTS_SUCCESS', () => {
it('sets "projects" to be the payload', () => {
const payload = [];
state.projects = [];
mutations[types.RECEIVE_PROJECTS_SUCCESS](state, payload);
expect(state.projects).toBe(payload);
});
it('sets "isLoadingProjects" to be false', () => {
state.isLoadingProjects = true;
mutations[types.RECEIVE_PROJECTS_SUCCESS](state, []);
expect(state.isLoadingProjects).toBe(false);
});
});
describe('RECEIVE_PROJECTS_ERROR', () => {
it('sets "projects" to be an empty array', () => {
state.projects = [];
mutations[types.RECEIVE_PROJECTS_ERROR](state);
expect(state.projects).toEqual([]);
});
it('sets "isLoadingProjects" to be false', () => {
state.isLoadingProjects = true;
mutations[types.RECEIVE_PROJECTS_ERROR](state);
expect(state.isLoadingProjects).toBe(false);
});
});
describe('CLEAR_SEARCH_RESULTS', () => {
it('sets "projectSearchResults" to be an empty array', () => {
state.projectSearchResults = [''];
mutations[types.CLEAR_SEARCH_RESULTS](state);
expect(state.projectSearchResults).toHaveLength(0);
});
it('sets "selectedProjects" to be an empty array', () => {
state.selectedProjects = [''];
mutations[types.CLEAR_SEARCH_RESULTS](state);
expect(state.selectedProjects).toHaveLength(0);
});
});
describe('REQUEST_SEARCH_RESULTS', () => {
it('sets "messages.minimumQuery" to be false', () => {
state.messages.minimumQuery = true;
mutations[types.REQUEST_SEARCH_RESULTS](state);
expect(state.messages.minimumQuery).toBe(false);
});
it('increments "searchCount" by one', () => {
state.searchCount = 0;
mutations[types.REQUEST_SEARCH_RESULTS](state);
expect(state.searchCount).toBe(1);
});
});
describe('RECEIVE_SEARCH_RESULTS_SUCCESS', () => {
it('sets "projectSearchResults" to be the payload', () => {
const payload = [];
mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](state, payload);
expect(state.projectSearchResults).toBe(payload);
});
it('sets "messages.noResults" to be false if the payload is not empty', () => {
state.messages.noResults = true;
mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](state, ['']);
expect(state.messages.noResults).toBe(false);
});
it('sets "messages.searchError" to be false', () => {
state.messages.searchError = true;
mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](state, ['']);
expect(state.messages.searchError).toBe(false);
});
it('sets "messages.minimumQuery" to be false', () => {
state.messages.minimumQuery = true;
mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](state, ['']);
expect(state.messages.minimumQuery).toBe(false);
});
it('decrements "searchCount" by one', () => {
state.searchCount = 1;
mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](state, ['']);
expect(state.searchCount).toBe(0);
});
it('does not decrement "searchCount" into negative', () => {
state.searchCount = 0;
mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](state, ['']);
expect(state.searchCount).toBe(0);
});
});
describe('RECEIVE_SEARCH_RESULTS_ERROR', () => {
it('sets "projectSearchResult" to be empty', () => {
state.projectSearchResults = [''];
mutations[types.RECEIVE_SEARCH_RESULTS_ERROR](state);
expect(state.projectSearchResults).toHaveLength(0);
});
it('sets "messages.noResults" to be false', () => {
state.messages.noResults = true;
mutations[types.RECEIVE_SEARCH_RESULTS_ERROR](state);
expect(state.messages.noResults).toBe(false);
});
it('sets "messages.searchError" to be true', () => {
state.messages.searchError = false;
mutations[types.RECEIVE_SEARCH_RESULTS_ERROR](state);
expect(state.messages.searchError).toBe(true);
});
it('sets "messages.minimumQuery" to be false', () => {
state.messages.minimumQuery = true;
mutations[types.RECEIVE_SEARCH_RESULTS_ERROR](state);
expect(state.messages.minimumQuery).toBe(false);
});
it('decrements "searchCount" by one', () => {
state.searchCount = 1;
mutations[types.RECEIVE_SEARCH_RESULTS_ERROR](state);
expect(state.searchCount).toBe(0);
});
it('does not decrement "searchCount" into negative', () => {
state.searchCount = 0;
mutations[types.RECEIVE_SEARCH_RESULTS_ERROR](state);
expect(state.searchCount).toBe(0);
});
});
describe('SET_MINIMUM_QUERY_MESSAGE', () => {
it('sets "projectSearchResult" to be an empty array', () => {
state.projectSearchResults = [''];
mutations[types.SET_MINIMUM_QUERY_MESSAGE](state);
expect(state.projectSearchResults).toHaveLength(0);
});
it('sets "messages.noResults" to be false', () => {
state.messages.noResults = true;
mutations[types.SET_MINIMUM_QUERY_MESSAGE](state);
expect(state.messages.noResults).toBe(false);
});
it('sets "messages.searchError" to be false', () => {
state.messages.searchError = true;
mutations[types.SET_MINIMUM_QUERY_MESSAGE](state);
expect(state.messages.searchError).toBe(false);
});
it('sets "messages.minimumQuery" to true', () => {
state.messages.minimumQuery = false;
mutations[types.SET_MINIMUM_QUERY_MESSAGE](state);
expect(state.messages.minimumQuery).toBe(true);
});
it('does not decrement "searchCount" into negative', () => {
state.searchCount = 0;
mutations[types.RECEIVE_SEARCH_RESULTS_ERROR](state);
expect(state.searchCount).toBe(0);
});
});
});
import createState from 'ee/security_dashboard/store/modules/project_selector/state';
describe('projectsSelector default state', () => {
const state = createState();
it('has "inputValue" set to be an empty string', () => {
expect(state.inputValue).toBe('');
});
it('has "isLoadingProjects" set to be false', () => {
expect(state.isLoadingProjects).toBe(false);
});
it('has "isAddingProjects" set to be false', () => {
expect(state.isAddingProjects).toBe(false);
});
it('has "isRemovingProject" set to be false', () => {
expect(state.isRemovingProject).toBe(false);
});
it('has all "projectEndpoints" set to be null', () => {
expect(state.projectEndpoints.list).toBe(null);
expect(state.projectEndpoints.add).toBe(null);
});
it('has "searchQuery" set to an empty string', () => {
expect(state.searchQuery).toBe('');
});
it('has "projects" set to be an empty array', () => {
expect(state.projects).toEqual([]);
});
it('has "projectSearchResults" set to be an empty array', () => {
expect(state.projectSearchResults).toEqual([]);
});
it('has "selectedProjects" set to be an empty array', () => {
expect(state.selectedProjects).toEqual([]);
});
it('has all "messages" set to be false', () => {
expect(state.messages.noResults).toBe(false);
expect(state.messages.searchError).toBe(false);
expect(state.messages.minimumQuery).toBe(false);
});
it('has "searchCount" set to be 0', () => {
expect(state.searchCount).toBe(0);
});
});
......@@ -14039,6 +14039,12 @@ msgstr ""
msgid "SecurityDashboard| The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
msgstr ""
msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
msgid "SecurityDashboard|Confidence"
msgstr ""
......@@ -14060,6 +14066,9 @@ msgstr ""
msgid "SecurityDashboard|Severity"
msgstr ""
msgid "SecurityDashboard|Unable to add %{invalidProjects}"
msgstr ""
msgid "See metrics"
msgstr ""
......@@ -14785,6 +14794,9 @@ msgstr ""
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
msgid "Something went wrong, unable to add projects to dashboard"
msgstr ""
msgid "Something went wrong, unable to get projects"
msgstr ""
......
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