diff --git a/ee/app/assets/javascripts/security_dashboard/components/filter.vue b/ee/app/assets/javascripts/security_dashboard/components/filter.vue index e848f3915f0c21f480949948bb1047e812d69a94..59328d6b495a1e47dc316b07d14545d3476a12c2 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/filter.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/filter.vue @@ -1,12 +1,12 @@ @@ -45,7 +58,7 @@ export default { 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 ""