{{ filter.name }}
-
+
{{ selectedOptionText.firstOption }}
@@ -57,20 +70,56 @@ export default {
-
+ {{ filter.name }}
+
+
+
+
+
+
+
+
+
+
diff --git a/ee/app/assets/javascripts/security_dashboard/store/moderator.js b/ee/app/assets/javascripts/security_dashboard/store/moderator.js
index 0d3acab79e919fe23a0ac547a1947685f0edc4cb..b84d20891cdda7e45008123082453ab7b6b31547 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/moderator.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/moderator.js
@@ -1,6 +1,7 @@
import * as vulnerabilitiesMutationTypes from './modules/vulnerabilities/mutation_types';
import * as filtersMutationTypes from './modules/filters/mutation_types';
import * as projectsMutationTypes from './modules/projects/mutation_types';
+import { BASE_FILTERS } from './modules/filters/constants';
export default function configureModerator(store) {
store.$router.beforeEach((to, from, next) => {
@@ -19,10 +20,7 @@ export default function configureModerator(store) {
store.dispatch('filters/setFilterOptions', {
filterId: 'project_id',
options: [
- {
- name: 'All',
- id: 'all',
- },
+ BASE_FILTERS.project_id,
...payload.projects.map(project => ({
name: project.name,
id: project.id.toString(),
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/constants.js b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/constants.js
index 28b508d12a2a1bd2c0bab2dfa32b8a19948d5f75..fcde0a8d93ee53b8de82c19ab55cf8fa7f8e9d32 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/constants.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/constants.js
@@ -27,3 +27,18 @@ export const REPORT_TYPES = {
dependency_scanning: s__('ciReport|Dependency Scanning'),
sast: s__('ciReport|SAST'),
};
+
+export const BASE_FILTERS = {
+ severity: {
+ name: s__('ciReport|All severities'),
+ id: 'all',
+ },
+ report_type: {
+ name: s__('ciReport|All report types'),
+ id: 'all',
+ },
+ project_id: {
+ name: s__('ciReport|All projects'),
+ id: 'all',
+ },
+};
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/state.js b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/state.js
index 446be736b95bf420ca16d52cc0c8e04260145c15..f84bb0a836f6ed6bf5053c7c90471090c8f1dc5f 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/state.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/state.js
@@ -1,46 +1,25 @@
-import { SEVERITY_LEVELS, REPORT_TYPES } from './constants';
+import { SEVERITY_LEVELS, REPORT_TYPES, BASE_FILTERS } from './constants';
+
+const optionsObjectToArray = obj => Object.entries(obj).map(([id, name]) => ({ id, name }));
export default () => ({
filters: [
{
name: 'Severity',
id: 'severity',
- options: [
- {
- name: 'All',
- id: 'all',
- },
- ...Object.entries(SEVERITY_LEVELS).map(severity => {
- const [id, name] = severity;
- return { id, name };
- }),
- ],
+ options: [BASE_FILTERS.severity, ...optionsObjectToArray(SEVERITY_LEVELS)],
selection: new Set(['all']),
},
{
name: 'Report type',
id: 'report_type',
- options: [
- {
- name: 'All',
- id: 'all',
- },
- ...Object.entries(REPORT_TYPES).map(type => {
- const [id, name] = type;
- return { id, name };
- }),
- ],
+ options: [BASE_FILTERS.report_type, ...optionsObjectToArray(REPORT_TYPES)],
selection: new Set(['all']),
},
{
name: 'Project',
id: 'project_id',
- options: [
- {
- name: 'All',
- id: 'all',
- },
- ],
+ options: [BASE_FILTERS.project_id],
selection: new Set(['all']),
},
],
diff --git a/ee/changelogs/unreleased/9253-add-full-feature-dropdowns-to-group-security-dashboard-filters.yml b/ee/changelogs/unreleased/9253-add-full-feature-dropdowns-to-group-security-dashboard-filters.yml
new file mode 100644
index 0000000000000000000000000000000000000000..021be46b1f94d0635ebf592df969880f59ffdd96
--- /dev/null
+++ b/ee/changelogs/unreleased/9253-add-full-feature-dropdowns-to-group-security-dashboard-filters.yml
@@ -0,0 +1,5 @@
+---
+title: Make editing the filters in the Group Security Dashboard easier.
+merge_request: 10138
+author:
+type: fixed
diff --git a/ee/spec/frontend/security_dashboard/store/filters/getters_spec.js b/ee/spec/frontend/security_dashboard/store/filters/getters_spec.js
index 0fc6fb6a2d5c57cb3bba1ef51fdb12f227b04c95..8f6e131188f7a6a62fdd35c09194bf9e508b7df4 100644
--- a/ee/spec/frontend/security_dashboard/store/filters/getters_spec.js
+++ b/ee/spec/frontend/security_dashboard/store/filters/getters_spec.js
@@ -1,5 +1,6 @@
import createState from 'ee/security_dashboard/store/modules/filters/state';
import * as getters from 'ee/security_dashboard/store/modules/filters/getters';
+import { BASE_FILTERS } from 'ee/security_dashboard/store/modules/filters/constants';
describe('filters module getters', () => {
const mockedGetters = state => {
@@ -28,13 +29,13 @@ describe('filters module getters', () => {
describe('getSelectedOptions', () => {
describe('with one selected option', () => {
- it('should return "All" as the selected option', () => {
+ it('should return the base filter as the selected option', () => {
const selectedOptions = getters.getSelectedOptions(state, mockedGetters(state))(
'report_type',
);
expect(selectedOptions).toHaveLength(1);
- expect(selectedOptions[0].name).toEqual('All');
+ expect(selectedOptions[0].name).toBe(BASE_FILTERS.report_type.name);
});
});
@@ -57,12 +58,13 @@ describe('filters module getters', () => {
});
describe('getSelectedOptionNames', () => {
- it('should return "All" as the selected option', () => {
+ it('should return the base filter as the selected option', () => {
const selectedOptionNames = getters.getSelectedOptionNames(state, mockedGetters(state))(
'severity',
);
- expect(selectedOptionNames).toEqual({ firstOption: 'All', extraOptionCount: '' });
+ expect(selectedOptionNames.firstOption).toBe(BASE_FILTERS.severity.name);
+ expect(selectedOptionNames.extraOptionCount).toBe('');
});
it('should return the correct message when multiple filters are selected', () => {
diff --git a/ee/spec/javascripts/security_dashboard/components/filter_spec.js b/ee/spec/javascripts/security_dashboard/components/filter_spec.js
index 82312bbe8989d55b90b3f1144e80f30e508cd4d9..4b74fd64e232f3c01bb0ee2a6bf981c08b5502dd 100644
--- a/ee/spec/javascripts/security_dashboard/components/filter_spec.js
+++ b/ee/spec/javascripts/security_dashboard/components/filter_spec.js
@@ -6,8 +6,36 @@ import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper
describe('Filter component', () => {
let vm;
let props;
- const store = createStore();
- const Component = Vue.extend(component);
+ let store;
+ let Component;
+
+ function isDropdownOpen() {
+ const toggleButton = vm.$el.querySelector('.dropdown-toggle');
+ return toggleButton.getAttribute('aria-expanded') === 'true';
+ }
+
+ function setProjectsCount(count) {
+ const projects = new Array(count).fill(null).map((_, i) => ({
+ name: i.toString(),
+ id: i.toString(),
+ }));
+
+ store.dispatch('filters/setFilterOptions', {
+ filterId: 'project_id',
+ options: projects,
+ });
+ }
+
+ const findSearchInput = () => vm.$refs.searchBox && vm.$refs.searchBox.$el.querySelector('input');
+
+ beforeEach(() => {
+ store = createStore();
+ Component = Vue.extend(component);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
describe('severity', () => {
beforeEach(() => {
@@ -15,10 +43,6 @@ describe('Filter component', () => {
vm = mountComponentWithStore(Component, { store, props });
});
- afterEach(() => {
- vm.$destroy();
- });
-
it('should display all 8 severity options', () => {
expect(vm.$el.querySelectorAll('.dropdown-item').length).toEqual(8);
});
@@ -30,5 +54,68 @@ describe('Filter component', () => {
it('should display "Severity" as the option name', () => {
expect(vm.$el.querySelector('.js-name').textContent).toContain('Severity');
});
+
+ it('should not have a search box', () => {
+ expect(findSearchInput()).not.toEqual(jasmine.any(HTMLElement));
+ });
+
+ it('should not be open', () => {
+ expect(isDropdownOpen()).toBe(false);
+ });
+
+ describe('when the dropdown is open', () => {
+ beforeEach(done => {
+ vm.$el.querySelector('.dropdown-toggle').click();
+ vm.$nextTick(done);
+ });
+
+ it('should keep the menu open after clicking on an item', done => {
+ expect(isDropdownOpen()).toBe(true);
+ vm.$el.querySelector('.dropdown-item').click();
+ vm.$nextTick(() => {
+ expect(isDropdownOpen()).toBe(true);
+ done();
+ });
+ });
+
+ it('should close the menu when the close button is clicked', done => {
+ expect(isDropdownOpen()).toBe(true);
+ vm.$refs.close.click();
+ vm.$nextTick(() => {
+ expect(isDropdownOpen()).toBe(false);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('Project', () => {
+ describe('when there are lots of projects', () => {
+ const lots = 30;
+ beforeEach(done => {
+ props = { filterId: 'project_id', dashboardDocumentation: '' };
+ vm = mountComponentWithStore(Component, { store, props });
+ setProjectsCount(lots);
+ vm.$nextTick(done);
+ });
+
+ it('should display a search box', () => {
+ expect(findSearchInput()).toEqual(jasmine.any(HTMLElement));
+ });
+
+ it(`should show all projects`, () => {
+ expect(vm.$el.querySelectorAll('.dropdown-item').length).toBe(lots);
+ });
+
+ it('should show only matching projects when a search term is entered', done => {
+ const input = findSearchInput();
+ input.value = '0';
+ input.dispatchEvent(new Event('input'));
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.dropdown-item').length).toBe(3);
+ done();
+ });
+ });
+ });
});
});
diff --git a/ee/spec/javascripts/security_dashboard/store/moderator_spec.js b/ee/spec/javascripts/security_dashboard/store/moderator_spec.js
index e6c05a0a969033147aac43532a05d64e8ba3318b..d47a551175dd5652810468efcd6a15f0d6981df7 100644
--- a/ee/spec/javascripts/security_dashboard/store/moderator_spec.js
+++ b/ee/spec/javascripts/security_dashboard/store/moderator_spec.js
@@ -2,6 +2,7 @@ import createStore from 'ee/security_dashboard/store/index';
import * as projectsMutationTypes from 'ee/security_dashboard/store/modules/projects/mutation_types';
import * as filtersMutationTypes from 'ee/security_dashboard/store/modules/filters/mutation_types';
import * as vulnerabilitiesMutationTypes from 'ee/security_dashboard/store/modules/vulnerabilities/mutation_types';
+import { BASE_FILTERS } from 'ee/security_dashboard/store/modules/filters/constants';
describe('moderator', () => {
let store;
@@ -22,7 +23,7 @@ describe('moderator', () => {
'filters/setFilterOptions',
Object({
filterId: 'project_id',
- options: [{ name: 'All', id: 'all' }, { name: 'foo', id: '1' }],
+ options: [BASE_FILTERS.project_id, { name: 'foo', id: '1' }],
}),
);
});
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index af10e123ee18566d2c73fea09c44838c83e12e75..a71f5fd9f8204e486bc218fb0bd48ce26cc915e4 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -12392,6 +12392,15 @@ msgstr ""
msgid "ciReport|(is loading, errors when loading results)"
msgstr ""
+msgid "ciReport|All projects"
+msgstr ""
+
+msgid "ciReport|All report types"
+msgstr ""
+
+msgid "ciReport|All severities"
+msgstr ""
+
msgid "ciReport|Class"
msgstr ""