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 "" ...@@ -14039,6 +14039,12 @@ msgstr ""
msgid "SecurityDashboard| The security dashboard displays the latest security report. Use it to find and fix vulnerabilities." msgid "SecurityDashboard| The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr "" msgstr ""
msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
msgstr ""
msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
msgid "SecurityDashboard|Confidence" msgid "SecurityDashboard|Confidence"
msgstr "" msgstr ""
...@@ -14060,6 +14066,9 @@ msgstr "" ...@@ -14060,6 +14066,9 @@ msgstr ""
msgid "SecurityDashboard|Severity" msgid "SecurityDashboard|Severity"
msgstr "" msgstr ""
msgid "SecurityDashboard|Unable to add %{invalidProjects}"
msgstr ""
msgid "See metrics" msgid "See metrics"
msgstr "" msgstr ""
...@@ -14785,6 +14794,9 @@ msgstr "" ...@@ -14785,6 +14794,9 @@ msgstr ""
msgid "Something went wrong, unable to add %{project} to dashboard" msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr "" msgstr ""
msgid "Something went wrong, unable to add projects to dashboard"
msgstr ""
msgid "Something went wrong, unable to get projects" msgid "Something went wrong, unable to get projects"
msgstr "" 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