-
+
{{ s__('SecurityDashboard|Add or remove projects from your dashboard') }}
-
+
-
+
{{ s__('SecurityDashboard|Add projects') }}
@@ -91,8 +74,12 @@ export default {
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 d7f4cf556db2c89d55ffa0775d5c27ca6ba4df91..a21ee78cba3996880cc32b722bd4d8c53928481e 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
@@ -1,18 +1,25 @@
import Tracking from '~/tracking';
import { getParameterValues } from '~/lib/utils/url_utility';
import * as types from './mutation_types';
+import { ALL } from './constants';
+import { hasValidSelection } from './utils';
-export const setFilter = ({ commit }, payload) => {
- commit(types.SET_FILTER, payload);
+export const setFilter = ({ commit }, { filterId, optionId, lazy = false }) => {
+ commit(types.SET_FILTER, { filterId, optionId, lazy });
Tracking.event(document.body.dataset.page, 'set_filter', {
- label: payload.filterId,
- value: payload.optionId,
+ label: filterId,
+ value: optionId,
});
};
-export const setFilterOptions = ({ commit }, payload) => {
- commit(types.SET_FILTER_OPTIONS, payload);
+export const setFilterOptions = ({ commit, state }, { filterId, options, lazy = false }) => {
+ commit(types.SET_FILTER_OPTIONS, { filterId, options });
+
+ const { selection } = state.filters.find(({ id }) => id === filterId);
+ if (!hasValidSelection({ selection, options })) {
+ commit(types.SET_FILTER, { filterId, optionId: ALL, lazy });
+ }
};
export const setAllFilters = ({ commit }, payload) => {
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/utils.js b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/utils.js
index d7269527218583808702991eaeb4311c4ed84f1b..3b549ad2262f0e9569a8fcc4555197cc988d6900 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/utils.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/utils.js
@@ -1,4 +1,13 @@
+import { isSubset } from '~/lib/utils/set';
import { ALL } from './constants';
-// eslint-disable-next-line import/prefer-default-export
export const isBaseFilterOption = id => id === ALL;
+
+/**
+ * Returns whether or not the given state filter has a valid selection,
+ * considering its available options.
+ * @param {Object} filter The filter from the state to check.
+ * @returns boolean
+ */
+export const hasValidSelection = ({ selection, options }) =>
+ isSubset(selection, new Set(options.map(({ id }) => id)));
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/actions.js b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/actions.js
index f016f1a1a9ae1eea8b8d234e205e197c897a78ed..d8143e05da17a5f5501019b0bea76d047c26a7e3 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/actions.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/actions.js
@@ -36,7 +36,8 @@ export const addProjects = ({ state, dispatch }) => {
project_ids: state.selectedProjects.map(p => p.id),
})
.then(response => dispatch('receiveAddProjectsSuccess', response.data))
- .catch(() => dispatch('receiveAddProjectsError'));
+ .catch(() => dispatch('receiveAddProjectsError'))
+ .finally(() => dispatch('clearSearchResults'));
};
export const requestAddProjects = ({ commit }) => {
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/getters.js b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/getters.js
new file mode 100644
index 0000000000000000000000000000000000000000..c12bf3f19bc4c2992a5e270cb56d5377dff9036d
--- /dev/null
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/getters.js
@@ -0,0 +1,7 @@
+export const canAddProjects = ({ isAddingProjects, selectedProjects }) =>
+ !isAddingProjects && selectedProjects.length > 0;
+
+export const isSearchingProjects = ({ searchCount }) => searchCount > 0;
+
+export const isUpdatingProjects = ({ isAddingProjects, isLoadingProjects, isRemovingProject }) =>
+ isAddingProjects || isLoadingProjects || isRemovingProject;
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/index.js b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/index.js
index 636092ce1a9aab25cda419c7e2c9bb1d6145029e..3e2701435b377175b61dcc3b582b86894c311a9d 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/index.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/index.js
@@ -1,10 +1,12 @@
import state from './state';
import mutations from './mutations';
import * as actions from './actions';
+import * as getters from './getters';
export default () => ({
namespaced: true,
state,
mutations,
actions,
+ getters,
});
diff --git a/ee/app/assets/javascripts/security_dashboard/store/plugins/mediator.js b/ee/app/assets/javascripts/security_dashboard/store/plugins/mediator.js
index 460e994da3b3b3a94d9207a5a3ac05f57c38c468..e0fe496468642a421be1c1294bcb87692c56302f 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/plugins/mediator.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/plugins/mediator.js
@@ -7,7 +7,7 @@ export default store => {
store.dispatch('vulnerabilities/fetchVulnerabilitiesHistory', payload);
};
- store.subscribe(({ type }) => {
+ store.subscribe(({ type, payload }) => {
switch (type) {
// SET_ALL_FILTERS mutations are triggered by navigation events, in such case we
// want to preserve the page number that was set in the sync_with_router plugin
@@ -21,7 +21,9 @@ export default store => {
// in that case we want to reset the page number
case `filters/${filtersMutationTypes.SET_FILTER}`:
case `filters/${filtersMutationTypes.SET_TOGGLE_VALUE}`: {
- refreshVulnerabilities(store.getters['filters/activeFilters']);
+ if (!payload.lazy) {
+ refreshVulnerabilities(store.getters['filters/activeFilters']);
+ }
break;
}
default:
diff --git a/ee/app/assets/javascripts/security_dashboard/store/plugins/project_selector.js b/ee/app/assets/javascripts/security_dashboard/store/plugins/project_selector.js
new file mode 100644
index 0000000000000000000000000000000000000000..3cc6bd6fb1ff92621e23ad3915a7cbe14cab6802
--- /dev/null
+++ b/ee/app/assets/javascripts/security_dashboard/store/plugins/project_selector.js
@@ -0,0 +1,23 @@
+import projectSelectorModule from '../modules/project_selector';
+import * as projectSelectorMutationTypes from '../modules/project_selector/mutation_types';
+import { BASE_FILTERS } from '../modules/filters/constants';
+
+export default store => {
+ store.registerModule('projectSelector', projectSelectorModule());
+
+ store.subscribe(({ type, payload }) => {
+ if (type === `projectSelector/${projectSelectorMutationTypes.RECEIVE_PROJECTS_SUCCESS}`) {
+ store.dispatch('filters/setFilterOptions', {
+ filterId: 'project_id',
+ options: [
+ BASE_FILTERS.project_id,
+ ...payload.map(({ name, id }) => ({
+ name,
+ id: id.toString(),
+ })),
+ ],
+ lazy: true,
+ });
+ }
+ });
+};
diff --git a/ee/spec/frontend/security_dashboard/components/instance_security_dashboard_spec.js b/ee/spec/frontend/security_dashboard/components/instance_security_dashboard_spec.js
index a41e58ff063499114591c8b7f83de51bc6339e85..d1f239eafa3ccdbf64fc893062a422341322da38 100644
--- a/ee/spec/frontend/security_dashboard/components/instance_security_dashboard_spec.js
+++ b/ee/spec/frontend/security_dashboard/components/instance_security_dashboard_spec.js
@@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import InstanceSecurityDashboard from 'ee/security_dashboard/components/instance_security_dashboard.vue';
import SecurityDashboard from 'ee/security_dashboard/components/app.vue';
+import ProjectManager from 'ee/security_dashboard/components/project_manager.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -10,7 +11,8 @@ localVue.use(Vuex);
const dashboardDocumentation = '/help/docs';
const emptyStateSvgPath = '/svgs/empty.svg';
const emptyDashboardStateSvgPath = '/svgs/empty-dash.svg';
-const projectsEndpoint = '/projects';
+const projectAddEndpoint = '/projects/add';
+const projectListEndpoint = '/projects/list';
const vulnerabilitiesEndpoint = '/vulnerabilities';
const vulnerabilitiesCountEndpoint = '/vulnerabilities_summary';
const vulnerabilitiesHistoryEndpoint = '/vulnerabilities_history';
@@ -24,11 +26,11 @@ describe('Instance Security Dashboard component', () => {
const factory = ({ projects = [] } = {}) => {
store = new Vuex.Store({
modules: {
- projects: {
+ projectSelector: {
namespaced: true,
actions: {
fetchProjects() {},
- setProjectsEndpoint() {},
+ setProjectEndpoints() {},
},
state: {
projects,
@@ -53,7 +55,8 @@ describe('Instance Security Dashboard component', () => {
dashboardDocumentation,
emptyStateSvgPath,
emptyDashboardStateSvgPath,
- projectsEndpoint,
+ projectAddEndpoint,
+ projectListEndpoint,
vulnerabilitiesEndpoint,
vulnerabilitiesCountEndpoint,
vulnerabilitiesHistoryEndpoint,
@@ -85,6 +88,7 @@ describe('Instance Security Dashboard component', () => {
expect(wrapper.find(GlEmptyState).exists()).toBe(false);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(SecurityDashboard).exists()).toBe(false);
+ expect(wrapper.find(ProjectManager).exists()).toBe(true);
};
afterEach(() => {
@@ -98,8 +102,14 @@ describe('Instance Security Dashboard component', () => {
it('dispatches the expected actions', () => {
expect(store.dispatch.mock.calls).toEqual([
- ['projects/setProjectsEndpoint', projectsEndpoint],
- ['projects/fetchProjects', undefined],
+ [
+ 'projectSelector/setProjectEndpoints',
+ {
+ add: projectAddEndpoint,
+ list: projectListEndpoint,
+ },
+ ],
+ ['projectSelector/fetchProjects', undefined],
]);
});
@@ -108,6 +118,7 @@ describe('Instance Security Dashboard component', () => {
expect(wrapper.find(GlEmptyState).exists()).toBe(false);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find(SecurityDashboard).exists()).toBe(false);
+ expect(wrapper.find(ProjectManager).exists()).toBe(false);
});
});
@@ -121,6 +132,7 @@ describe('Instance Security Dashboard component', () => {
expect(findProjectSelectorToggleButton().exists()).toBe(true);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(SecurityDashboard).exists()).toBe(false);
+ expect(wrapper.find(ProjectManager).exists()).toBe(false);
expectComponentWithProps(GlEmptyState, {
svgPath: emptyStateSvgPath,
@@ -146,6 +158,7 @@ describe('Instance Security Dashboard component', () => {
expect(findProjectSelectorToggleButton().exists()).toBe(true);
expect(wrapper.find(GlEmptyState).exists()).toBe(false);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find(ProjectManager).exists()).toBe(false);
expectComponentWithProps(SecurityDashboard, {
dashboardDocumentation,
diff --git a/ee/spec/frontend/security_dashboard/components/project_list_spec.js b/ee/spec/frontend/security_dashboard/components/project_list_spec.js
index 81ffe80da763ffb76ef17d140fec590f54fe1212..21956512fdb3b17f39d0a1ad0b04f1780c7ae03d 100644
--- a/ee/spec/frontend/security_dashboard/components/project_list_spec.js
+++ b/ee/spec/frontend/security_dashboard/components/project_list_spec.js
@@ -1,6 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlBadge, GlButton } from '@gitlab/ui';
+import { GlBadge, GlButton, GlLoadingIcon } from '@gitlab/ui';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
import ProjectList from 'ee/security_dashboard/components/project_list.vue';
@@ -14,12 +14,13 @@ const generateMockProjects = (projectsCount, mockProject = {}) =>
describe('Project List component', () => {
let wrapper;
- const factory = ({ projects = [], stubs = {} } = {}) => {
+ const factory = ({ projects = [], stubs = {}, showLoadingIndicator = false } = {}) => {
wrapper = shallowMount(ProjectList, {
stubs,
localVue,
propsData: {
projects,
+ showLoadingIndicator,
},
sync: false,
});
@@ -39,6 +40,18 @@ describe('Project List component', () => {
);
});
+ it('does not show a loading indicator when showLoadingIndicator = false', () => {
+ factory();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ });
+
+ it('shows a loading indicator when showLoadingIndicator = true', () => {
+ factory({ showLoadingIndicator: true });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
it.each([0, 1, 2])(
'renders a list of projects and displays a count of how many there are',
projectsCount => {
diff --git a/ee/spec/frontend/security_dashboard/components/project_manager_spec.js b/ee/spec/frontend/security_dashboard/components/project_manager_spec.js
index f5e8bd1c0e554f7ececb52f0f86d0bdd35f6e3ef..4fe205946e6ad2fc193de1a46aa294826c122d6a 100644
--- a/ee/spec/frontend/security_dashboard/components/project_manager_spec.js
+++ b/ee/spec/frontend/security_dashboard/components/project_manager_spec.js
@@ -3,7 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import createDefaultState from 'ee/security_dashboard/store/modules/project_selector/state';
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
import ProjectManager from 'ee/security_dashboard/components/project_manager.vue';
@@ -17,7 +17,12 @@ describe('Project Manager component', () => {
let store;
let wrapper;
- const factory = ({ stateOverrides = {} } = {}) => {
+ const factory = ({
+ state = {},
+ canAddProjects = false,
+ isSearchingProjects = false,
+ isUpdatingProjects = false,
+ } = {}) => {
storeOptions = {
modules: {
projectSelector: {
@@ -30,9 +35,14 @@ describe('Project Manager component', () => {
toggleSelectedProject: jest.fn(),
removeProject: jest.fn(),
},
+ getters: {
+ canAddProjects: jest.fn().mockReturnValue(canAddProjects),
+ isSearchingProjects: jest.fn().mockReturnValue(isSearchingProjects),
+ isUpdatingProjects: jest.fn().mockReturnValue(isUpdatingProjects),
+ },
state: {
...createDefaultState(),
- ...stateOverrides,
+ ...state,
},
},
},
@@ -51,7 +61,6 @@ describe('Project Manager component', () => {
const getMockActionDispatchedPayload = actionName => getMockAction(actionName).mock.calls[0][1];
const getAddProjectsButton = () => wrapper.find(GlButton);
- const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
const getProjectList = () => wrapper.find(ProjectList);
const getProjectSelector = () => wrapper.find(ProjectSelector);
@@ -87,18 +96,11 @@ describe('Project Manager component', () => {
expect(getAddProjectsButton().attributes('disabled')).toBe('true');
});
- it.each`
- actionName | payload
- ${'addProjects'} | ${undefined}
- ${'clearSearchResults'} | ${undefined}
- `(
- 'dispatches the correct actions when the add-projects button has been clicked',
- ({ actionName, payload }) => {
- getAddProjectsButton().vm.$emit('click');
+ it('dispatches the addProjects when the "Add projects" button has been clicked', () => {
+ getAddProjectsButton().vm.$emit('click');
- expect(getMockActionDispatchedPayload(actionName)).toBe(payload);
- },
- );
+ expect(getMockAction('addProjects')).toHaveBeenCalled();
+ });
it('contains a project-list component', () => {
expect(getProjectList().exists()).toBe(true);
@@ -116,26 +118,26 @@ describe('Project Manager component', () => {
});
});
- describe('given the state changes', () => {
+ describe('given the store state', () => {
it.each`
- state | projectSelectorPropName | expectedPropValue
- ${{ searchCount: 1 }} | ${'showLoadingIndicator'} | ${true}
- ${{ selectedProjects: ['bar'] }} | ${'selectedProjects'} | ${['bar']}
- ${{ projectSearchResults: ['foo'] }} | ${'projectSearchResults'} | ${['foo']}
- ${{ messages: { noResults: true } }} | ${'showNoResultsMessage'} | ${true}
- ${{ messages: { searchError: true } }} | ${'showSearchErrorMessage'} | ${true}
- ${{ messages: { minimumQuery: true } }} | ${'showMinimumSearchQueryMessage'} | ${true}
+ config | projectSelectorPropName | expectedPropValue
+ ${{ isSearchingProjects: true }} | ${'showLoadingIndicator'} | ${true}
+ ${{ state: { selectedProjects: ['bar'] } }} | ${'selectedProjects'} | ${['bar']}
+ ${{ state: { projectSearchResults: ['foo'] } }} | ${'projectSearchResults'} | ${['foo']}
+ ${{ state: { messages: { noResults: true } } }} | ${'showNoResultsMessage'} | ${true}
+ ${{ state: { messages: { searchError: true } } }} | ${'showSearchErrorMessage'} | ${true}
+ ${{ state: { messages: { minimumQuery: true } } }} | ${'showMinimumSearchQueryMessage'} | ${true}
`(
- 'passes the correct prop-values to the project-selector',
- ({ state, projectSelectorPropName, expectedPropValue }) => {
- factory({ stateOverrides: state });
+ 'passes $projectSelectorPropName = $expectedPropValue to the project-selector',
+ ({ config, projectSelectorPropName, expectedPropValue }) => {
+ factory(config);
expect(getProjectSelector().props(projectSelectorPropName)).toEqual(expectedPropValue);
},
);
- it('enables the add-projects button when at least one projects is selected', () => {
- factory({ stateOverrides: { selectedProjects: [{}] } });
+ it('enables the add-projects button when projects can be added', () => {
+ factory({ canAddProjects: true });
expect(getAddProjectsButton().attributes('disabled')).toBe(undefined);
});
@@ -143,21 +145,18 @@ describe('Project Manager component', () => {
it('passes the list of projects to the project-list component', () => {
const projects = [{}];
- factory({ stateOverrides: { projects } });
+ factory({ state: { projects } });
expect(getProjectList().props('projects')).toBe(projects);
});
- it('toggles the loading icon when a project is being added', () => {
- factory({ stateOverrides: { isAddingProjects: false } });
-
- expect(getLoadingIcon().exists()).toBe(false);
-
- store.state.projectSelector.isAddingProjects = true;
+ it.each([false, true])(
+ 'passes showLoadingIndicator = %p to the project-list component',
+ isUpdatingProjects => {
+ factory({ isUpdatingProjects });
- return wrapper.vm.$nextTick().then(() => {
- expect(getLoadingIcon().exists()).toBe(true);
- });
- });
+ expect(getProjectList().props('showLoadingIndicator')).toBe(isUpdatingProjects);
+ },
+ );
});
});
diff --git a/ee/spec/frontend/security_dashboard/store/filters/utils_spec.js b/ee/spec/frontend/security_dashboard/store/filters/utils_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..ebde83ec0cd9b6ea644b6c70859e3e0e9eee0dc9
--- /dev/null
+++ b/ee/spec/frontend/security_dashboard/store/filters/utils_spec.js
@@ -0,0 +1,29 @@
+import { hasValidSelection } from 'ee/security_dashboard/store/modules/filters/utils';
+
+describe('filters module utils', () => {
+ describe('hasValidSelection', () => {
+ describe.each`
+ selection | options | expected
+ ${[]} | ${[]} | ${true}
+ ${[]} | ${['foo']} | ${true}
+ ${['foo']} | ${['foo']} | ${true}
+ ${['foo']} | ${['foo', 'bar']} | ${true}
+ ${['bar', 'foo']} | ${['foo', 'bar']} | ${true}
+ ${['foo']} | ${[]} | ${false}
+ ${['foo']} | ${['bar']} | ${false}
+ ${['foo', 'bar']} | ${['foo']} | ${false}
+ `('given selection $selection and options $options', ({ selection, options, expected }) => {
+ let filter;
+ beforeEach(() => {
+ filter = {
+ selection,
+ options: options.map(id => ({ id })),
+ };
+ });
+
+ it(`return ${expected}`, () => {
+ expect(hasValidSelection(filter)).toBe(expected);
+ });
+ });
+ });
+});
diff --git a/ee/spec/frontend/security_dashboard/store/modules/project_selector/actions_spec.js b/ee/spec/frontend/security_dashboard/store/modules/project_selector/actions_spec.js
index 56b1086200c83f4d1b628ac7fcc0af107e7fb918..a78fe967392748a217d9386cad60b7fb582b1423 100644
--- a/ee/spec/frontend/security_dashboard/store/modules/project_selector/actions_spec.js
+++ b/ee/spec/frontend/security_dashboard/store/modules/project_selector/actions_spec.js
@@ -91,6 +91,9 @@ describe('projectSelector actions', () => {
type: 'receiveAddProjectsSuccess',
payload: mockResponse,
},
+ {
+ type: 'clearSearchResults',
+ },
],
);
});
@@ -103,7 +106,11 @@ describe('projectSelector actions', () => {
null,
state,
[],
- [{ type: 'requestAddProjects' }, { type: 'receiveAddProjectsError' }],
+ [
+ { type: 'requestAddProjects' },
+ { type: 'receiveAddProjectsError' },
+ { type: 'clearSearchResults' },
+ ],
);
});
});
diff --git a/ee/spec/frontend/security_dashboard/store/modules/project_selector/getters_spec.js b/ee/spec/frontend/security_dashboard/store/modules/project_selector/getters_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..69f28b539c82550d3dd8dca22eb6befe5690fad2
--- /dev/null
+++ b/ee/spec/frontend/security_dashboard/store/modules/project_selector/getters_spec.js
@@ -0,0 +1,73 @@
+import createState from 'ee/security_dashboard/store/modules/project_selector/state';
+import * as getters from 'ee/security_dashboard/store/modules/project_selector/getters';
+
+describe('project selector module getters', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState();
+ });
+
+ describe('canAddProjects', () => {
+ describe.each`
+ isAddingProjects | selectedProjectCount | expected
+ ${true} | ${0} | ${false}
+ ${true} | ${1} | ${false}
+ ${false} | ${0} | ${false}
+ ${false} | ${1} | ${true}
+ `(
+ 'given isAddingProjects = $isAddingProjects and $selectedProjectCount selected projects',
+ ({ isAddingProjects, selectedProjectCount, expected }) => {
+ beforeEach(() => {
+ state = {
+ ...state,
+ isAddingProjects,
+ selectedProjects: Array(selectedProjectCount).fill({}),
+ };
+ });
+
+ it(`returns ${expected}`, () => {
+ expect(getters.canAddProjects(state)).toBe(expected);
+ });
+ },
+ );
+ });
+
+ describe('isSearchingProjects', () => {
+ describe.each`
+ searchCount | expected
+ ${0} | ${false}
+ ${1} | ${true}
+ ${2} | ${true}
+ `('given searchCount = $searchCount', ({ searchCount, expected }) => {
+ beforeEach(() => {
+ state = { ...state, searchCount };
+ });
+
+ it(`returns ${expected}`, () => {
+ expect(getters.isSearchingProjects(state)).toBe(expected);
+ });
+ });
+ });
+
+ describe('isUpdatingProjects', () => {
+ describe.each`
+ isAddingProjects | isRemovingProject | isLoadingProjects | expected
+ ${false} | ${false} | ${false} | ${false}
+ ${true} | ${false} | ${false} | ${true}
+ ${false} | ${true} | ${false} | ${true}
+ ${false} | ${false} | ${true} | ${true}
+ `(
+ 'given isAddingProjects = $isAddingProjects, isRemovingProject = $isRemovingProject, isLoadingProjects = $isLoadingProjects',
+ ({ isAddingProjects, isRemovingProject, isLoadingProjects, expected }) => {
+ beforeEach(() => {
+ state = { ...state, isAddingProjects, isRemovingProject, isLoadingProjects };
+ });
+
+ it(`returns ${expected}`, () => {
+ expect(getters.isUpdatingProjects(state)).toBe(expected);
+ });
+ },
+ );
+ });
+});
diff --git a/ee/spec/frontend/security_dashboard/store/plugins/project_selector_spec.js b/ee/spec/frontend/security_dashboard/store/plugins/project_selector_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..6b16168f4b37f8df44490df812ac7aa2159034ab
--- /dev/null
+++ b/ee/spec/frontend/security_dashboard/store/plugins/project_selector_spec.js
@@ -0,0 +1,40 @@
+import Vuex from 'vuex';
+import createStore from 'ee/security_dashboard/store';
+import { BASE_FILTERS } from 'ee/security_dashboard/store/modules/filters/constants';
+import projectSelectorModule from 'ee/security_dashboard/store/modules/project_selector';
+import projectSelectorPlugin from 'ee/security_dashboard/store/plugins/project_selector';
+import * as projectSelectorMutationTypes from 'ee/security_dashboard/store/modules/projects/mutation_types';
+
+describe('project selector plugin', () => {
+ let store;
+
+ beforeEach(() => {
+ jest.spyOn(Vuex.Store.prototype, 'registerModule');
+ store = createStore({ plugins: [projectSelectorPlugin] });
+ });
+
+ it('registers the project selector module on the store', () => {
+ expect(Vuex.Store.prototype.registerModule).toHaveBeenCalledTimes(1);
+ expect(Vuex.Store.prototype.registerModule).toHaveBeenCalledWith(
+ 'projectSelector',
+ projectSelectorModule(),
+ );
+ });
+
+ it('sets project filter options with lazy = true after projects have been received', () => {
+ jest.spyOn(store, 'dispatch').mockImplementation();
+ const projects = [{ name: 'foo', id: '1' }];
+
+ store.commit(
+ `projectSelector/${projectSelectorMutationTypes.RECEIVE_PROJECTS_SUCCESS}`,
+ projects,
+ );
+
+ expect(store.dispatch).toHaveBeenCalledTimes(1);
+ expect(store.dispatch).toHaveBeenCalledWith('filters/setFilterOptions', {
+ filterId: 'project_id',
+ options: [BASE_FILTERS.project_id, ...projects],
+ lazy: true,
+ });
+ });
+});
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 041162737e20ff6bd01d7752779c91896b6812e8..6fec50fd811cc7a97b8fe7487bdee543b4156c1a 100644
--- a/ee/spec/javascripts/security_dashboard/store/filters/actions_spec.js
+++ b/ee/spec/javascripts/security_dashboard/store/filters/actions_spec.js
@@ -3,6 +3,7 @@ import Tracking from '~/tracking';
import createState from 'ee/security_dashboard/store/modules/filters/state';
import * as types from 'ee/security_dashboard/store/modules/filters/mutation_types';
import module, * as actions from 'ee/security_dashboard/store/modules/filters/actions';
+import { ALL } from 'ee/security_dashboard/store/modules/filters/constants';
describe('filters actions', () => {
beforeEach(() => {
@@ -12,7 +13,26 @@ describe('filters actions', () => {
describe('setFilter', () => {
it('should commit the SET_FILTER mutuation', done => {
const state = createState();
- const payload = { filterId: 'type', optionId: 'sast' };
+ const payload = { filterId: 'report_type', optionId: 'sast' };
+
+ testAction(
+ actions.setFilter,
+ payload,
+ state,
+ [
+ {
+ type: types.SET_FILTER,
+ payload: { ...payload, lazy: false },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('should commit the SET_FILTER mutuation passing through lazy = true', done => {
+ const state = createState();
+ const payload = { filterId: 'report_type', optionId: 'sast', lazy: true };
testAction(
actions.setFilter,
@@ -33,7 +53,26 @@ describe('filters actions', () => {
describe('setFilterOptions', () => {
it('should commit the SET_FILTER_OPTIONS mutuation', done => {
const state = createState();
- const payload = { filterId: 'project', options: [] };
+ const payload = { filterId: 'project_id', options: [{ id: ALL }] };
+
+ testAction(
+ actions.setFilterOptions,
+ payload,
+ state,
+ [
+ {
+ type: types.SET_FILTER_OPTIONS,
+ payload,
+ },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('should commit the SET_FILTER_OPTIONS and SET_FILTER mutation when filter selection is invalid', done => {
+ const state = createState();
+ const payload = { filterId: 'project_id', options: [{ id: 'foo' }] };
testAction(
actions.setFilterOptions,
@@ -44,6 +83,40 @@ describe('filters actions', () => {
type: types.SET_FILTER_OPTIONS,
payload,
},
+ {
+ type: types.SET_FILTER,
+ payload: jasmine.objectContaining({
+ filterId: 'project_id',
+ optionId: ALL,
+ }),
+ },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('should commit the SET_FILTER_OPTIONS and SET_FILTER mutation when filter selection is invalid, passing the lazy flag', done => {
+ const state = createState();
+ const payload = { filterId: 'project_id', options: [{ id: 'foo' }] };
+
+ testAction(
+ actions.setFilterOptions,
+ { ...payload, lazy: true },
+ state,
+ [
+ {
+ type: types.SET_FILTER_OPTIONS,
+ payload,
+ },
+ {
+ type: types.SET_FILTER,
+ payload: {
+ filterId: 'project_id',
+ optionId: ALL,
+ lazy: true,
+ },
+ },
],
[],
done,
diff --git a/ee/spec/javascripts/security_dashboard/store/plugins/mediator_spec.js b/ee/spec/javascripts/security_dashboard/store/plugins/mediator_spec.js
index aa14edd3ffd8fb0955ad9b614337739449a897e8..514b9db12757f5264495d585012c9b562db20fa6 100644
--- a/ee/spec/javascripts/security_dashboard/store/plugins/mediator_spec.js
+++ b/ee/spec/javascripts/security_dashboard/store/plugins/mediator_spec.js
@@ -6,11 +6,10 @@ describe('mediator', () => {
beforeEach(() => {
store = createStore();
+ spyOn(store, 'dispatch');
});
it('triggers fetching vulnerabilities after one filter changes', () => {
- spyOn(store, 'dispatch');
-
const activeFilters = store.getters['filters/activeFilters'];
store.commit(`filters/${filtersMutationTypes.SET_FILTER}`, {});
@@ -32,9 +31,13 @@ describe('mediator', () => {
);
});
- it('triggers fetching vulnerabilities after filters change', () => {
- spyOn(store, 'dispatch');
+ it('does not fetch vulnerabilities after one filter changes with lazy = true', () => {
+ store.commit(`filters/${filtersMutationTypes.SET_FILTER}`, { lazy: true });
+
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+ it('triggers fetching vulnerabilities after filters change', () => {
const payload = {
...store.getters['filters/activeFilters'],
page: store.state.vulnerabilities.pageInfo.page,
@@ -57,8 +60,6 @@ describe('mediator', () => {
});
it('triggers fetching vulnerabilities after "Hide dismissed" toggle changes', () => {
- spyOn(store, 'dispatch');
-
const activeFilters = store.getters['filters/activeFilters'];
store.commit(`filters/${filtersMutationTypes.SET_TOGGLE_VALUE}`, {});
@@ -79,4 +80,10 @@ describe('mediator', () => {
activeFilters,
);
});
+
+ it('does not fetch vulnerabilities after "Hide dismissed" toggle changes with lazy = true', () => {
+ store.commit(`filters/${filtersMutationTypes.SET_TOGGLE_VALUE}`, { lazy: true });
+
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
});
diff --git a/spec/frontend/lib/utils/set_spec.js b/spec/frontend/lib/utils/set_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..7636a1c634cfd33fde050cf5ee36919f00e3c016
--- /dev/null
+++ b/spec/frontend/lib/utils/set_spec.js
@@ -0,0 +1,19 @@
+import { isSubset } from '~/lib/utils/set';
+
+describe('utils/set', () => {
+ describe('isSubset', () => {
+ it.each`
+ subset | superset | expected
+ ${new Set()} | ${new Set()} | ${true}
+ ${new Set()} | ${new Set([1])} | ${true}
+ ${new Set([1])} | ${new Set([1])} | ${true}
+ ${new Set([1, 3])} | ${new Set([1, 2, 3])} | ${true}
+ ${new Set([1])} | ${new Set()} | ${false}
+ ${new Set([1])} | ${new Set([2])} | ${false}
+ ${new Set([7, 8, 9])} | ${new Set([1, 2, 3])} | ${false}
+ ${new Set([1, 2, 3, 4])} | ${new Set([1, 2, 3])} | ${false}
+ `('isSubset($subset, $superset) === $expected', ({ subset, superset, expected }) => {
+ expect(isSubset(subset, superset)).toBe(expected);
+ });
+ });
+});