Commit 0e81fa09 authored by Savas Vedova's avatar Savas Vedova

Merge branch '284471-refactor-vulnerability-report-filters' into 'master'

Improve code readability and layout of vulnerability report filters

See merge request gitlab-org/gitlab!58488
parents 0fd9d62f c1a48644
...@@ -71,7 +71,7 @@ export default { ...@@ -71,7 +71,7 @@ export default {
</script> </script>
<template> <template>
<div class="dashboard-filter"> <div>
<strong data-testid="name">{{ name }}</strong> <strong data-testid="name">{{ name }}</strong>
<gl-dropdown <gl-dropdown
class="gl-mt-2 gl-w-full" class="gl-mt-2 gl-w-full"
......
...@@ -10,10 +10,11 @@ export default { ...@@ -10,10 +10,11 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
showSearchBox: { // Number of options that must exist for the search box to show.
type: Boolean, searchBoxShowThreshold: {
type: Number,
required: false, required: false,
default: false, default: 20,
}, },
loading: { loading: {
type: Boolean, type: Boolean,
...@@ -67,6 +68,9 @@ export default { ...@@ -67,6 +68,9 @@ export default {
return hasAllId ? [] : this.filter.defaultOptions; return hasAllId ? [] : this.filter.defaultOptions;
}, },
showSearchBox() {
return this.options.length >= this.searchBoxShowThreshold;
},
}, },
watch: { watch: {
selectedOptions() { selectedOptions() {
......
<script> <script>
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { import {
stateFilter, stateFilter,
severityFilter, severityFilter,
...@@ -11,9 +12,9 @@ import ActivityFilter from './filters/activity_filter.vue'; ...@@ -11,9 +12,9 @@ import ActivityFilter from './filters/activity_filter.vue';
import ScannerFilter from './filters/scanner_filter.vue'; import ScannerFilter from './filters/scanner_filter.vue';
import StandardFilter from './filters/standard_filter.vue'; import StandardFilter from './filters/standard_filter.vue';
const searchBoxOptionCount = 20; // Number of options before the search box is shown.
export default { export default {
components: { StandardFilter, ScannerFilter, ActivityFilter },
mixins: [glFeatureFlagsMixin()],
props: { props: {
projects: { type: Array, required: false, default: undefined }, projects: { type: Array, required: false, default: undefined },
}, },
...@@ -23,14 +24,19 @@ export default { ...@@ -23,14 +24,19 @@ export default {
}; };
}, },
computed: { computed: {
filters() { standardFilters() {
const filters = [stateFilter, severityFilter, scannerFilter, activityFilter]; return this.shouldShowCustomScannerFilter
? [stateFilter, severityFilter]
if (this.projects) { : [stateFilter, severityFilter, scannerFilter];
filters.push(getProjectFilter(this.projects)); },
} shouldShowProjectFilter() {
return Boolean(this.projects?.length);
return filters; },
shouldShowCustomScannerFilter() {
return this.glFeatures.customSecurityScanners;
},
projectFilter() {
return getProjectFilter(this.projects);
}, },
}, },
methods: { methods: {
...@@ -43,33 +49,34 @@ export default { ...@@ -43,33 +49,34 @@ export default {
emitFilterChange: debounce(function emit() { emitFilterChange: debounce(function emit() {
this.$emit('filterChange', this.filterQuery); this.$emit('filterChange', this.filterQuery);
}), }),
getFilterComponent({ id }) {
if (id === activityFilter.id) {
return ActivityFilter;
} else if (gon.features?.customSecurityScanners && id === scannerFilter.id) {
return ScannerFilter;
}
return StandardFilter;
},
}, },
searchBoxOptionCount, scannerFilter,
activityFilter,
}; };
</script> </script>
<template> <template>
<div class="dashboard-filters border-bottom bg-gray-light"> <div
<div class="row mx-0 p-2"> class="vulnerability-report-filters gl-p-5 gl-bg-gray-10 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
<component >
:is="getFilterComponent(filter)" <standard-filter
v-for="filter in filters" v-for="filter in standardFilters"
:key="filter.id" :key="filter.id"
class="col-sm-6 col-md-4 col-lg-2 p-2"
:filter="filter" :filter="filter"
:data-testid="filter.id" :data-testid="filter.id"
:show-search-box="filter.options.length >= $options.searchBoxOptionCount"
@filter-changed="updateFilterQuery" @filter-changed="updateFilterQuery"
/> />
</div> <scanner-filter
v-if="shouldShowCustomScannerFilter"
:filter="$options.scannerFilter"
@filter-changed="updateFilterQuery"
/>
<activity-filter :filter="$options.activityFilter" @filter-changed="updateFilterQuery" />
<standard-filter
v-if="shouldShowProjectFilter"
:filter="projectFilter"
:data-testid="projectFilter.id"
@filter-changed="updateFilterQuery"
/>
</div> </div>
</template> </template>
.vulnerability-report-filters {
@include gl-display-grid;
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
grid-gap: $gl-spacing-scale-5;
}
...@@ -96,13 +96,14 @@ describe('Standard Filter component', () => { ...@@ -96,13 +96,14 @@ describe('Standard Filter component', () => {
describe('search box', () => { describe('search box', () => {
it.each` it.each`
phrase | showSearchBox phrase | count | searchBoxShowThreshold
${'shows'} | ${true} ${'shows'} | ${5} | ${5}
${'does not show'} | ${false} ${'hides'} | ${7} | ${8}
`('$phrase search box if showSearchBox is $showSearchBox', ({ showSearchBox }) => { `('$phrase search box if there are $count options', ({ count, searchBoxShowThreshold }) => {
createWrapper({}, { showSearchBox }); createWrapper({ options: generateOptions(count) }, { searchBoxShowThreshold });
const shouldShow = count >= searchBoxShowThreshold;
expect(filterBody().props('showSearchBox')).toBe(showSearchBox);
expect(filterBody().props('showSearchBox')).toBe(shouldShow);
}); });
it('filters options when something is typed in the search box', async () => { it('filters options when something is typed in the search box', async () => {
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import ActivityFilter from 'ee/security_dashboard/components/filters/activity_filter.vue';
import ScannerFilter from 'ee/security_dashboard/components/filters/scanner_filter.vue'; import ScannerFilter from 'ee/security_dashboard/components/filters/scanner_filter.vue';
import StandardFilter from 'ee/security_dashboard/components/filters/standard_filter.vue'; import StandardFilter from 'ee/security_dashboard/components/filters/standard_filter.vue';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue'; import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
import { scannerFilter, getProjectFilter } from 'ee/security_dashboard/helpers';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('First class vulnerability filters component', () => { describe('First class vulnerability filters component', () => {
let wrapper; let wrapper;
...@@ -11,35 +14,41 @@ describe('First class vulnerability filters component', () => { ...@@ -11,35 +14,41 @@ describe('First class vulnerability filters component', () => {
{ id: 'gid://gitlab/Project/12', name: 'GitLab Com' }, { id: 'gid://gitlab/Project/12', name: 'GitLab Com' },
]; ];
const findFilters = () => wrapper.findAll(StandardFilter); const findStandardFilters = () => wrapper.findAllComponents(StandardFilter);
const findStateFilter = () => wrapper.find('[data-testid="state"]'); const findStandardScannerFilter = () => wrapper.findByTestId(scannerFilter.id);
const findProjectFilter = () => wrapper.find('[data-testid="projectId"]'); const findCustomScannerFilter = () => wrapper.findComponent(ScannerFilter);
const findActivityFilter = () => wrapper.findComponent(ActivityFilter);
const createComponent = (propsData) => { const findProjectFilter = () => wrapper.findByTestId(getProjectFilter([]).id);
return shallowMount(Filters, { propsData });
const createComponent = ({ props, provide } = {}) => {
return extendedWrapper(
shallowMount(Filters, {
propsData: props,
provide,
}),
);
}; };
beforeEach(() => {
gon.features = {};
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
}); });
it.each` it.each`
flagValue | expectedComponent | expectedName flagValue | isStandardFilterShown | isCustomFilterShown
${true} | ${ScannerFilter} | ${'ScannerFilter'} ${true} | ${false} | ${true}
${false} | ${StandardFilter} | ${'StandardFilter'} ${false} | ${true} | ${false}
`( `(
`renders $expectedName component when customSecurityScanners feature flag is $flagValue`, `renders correct scanner filter component when customSecurityScanners feature flag is $flagValue`,
({ flagValue, expectedComponent }) => { ({ flagValue, isStandardFilterShown, isCustomFilterShown }) => {
wrapper = createComponent(); wrapper = createComponent({
const filter = { id: 'reportType' }; provide: {
gon.features.customSecurityScanners = flagValue; glFeatures: { customSecurityScanners: flagValue },
},
});
expect(wrapper.vm.getFilterComponent(filter)).toEqual(expectedComponent); expect(findCustomScannerFilter().exists()).toBe(isCustomFilterShown);
expect(findStandardScannerFilter().exists()).toBe(isStandardFilterShown);
}, },
); );
...@@ -49,31 +58,28 @@ describe('First class vulnerability filters component', () => { ...@@ -49,31 +58,28 @@ describe('First class vulnerability filters component', () => {
}); });
it('should render the default filters', () => { it('should render the default filters', () => {
expect(findFilters()).toHaveLength(3); expect(findStandardFilters()).toHaveLength(3);
expect(findActivityFilter().exists()).toBe(true);
expect(findProjectFilter().exists()).toBe(false);
}); });
it('should emit filterChange when a filter is changed', () => { it('should emit filterChange when a filter is changed', () => {
const options = { foo: 'bar' }; const options = { foo: 'bar' };
findStateFilter().vm.$emit('filter-changed', options); findActivityFilter().vm.$emit('filter-changed', options);
expect(wrapper.emitted('filterChange')[0][0]).toEqual(options); expect(wrapper.emitted('filterChange')[0][0]).toEqual(options);
}); });
}); });
describe('when project filter is populated dynamically', () => { describe('when project filter is populated dynamically', () => {
beforeEach(() => { it('should not render the project filter if there are no options', async () => {
wrapper = createComponent(); wrapper = createComponent({ props: { projects: [] } });
});
it('should render the project filter with no options', async () => { expect(findProjectFilter().exists()).toBe(false);
wrapper.setProps({ projects: [] });
await wrapper.vm.$nextTick();
expect(findProjectFilter().props('filter').options).toHaveLength(0);
}); });
it('should render the project filter with the expected options', async () => { it('should render the project filter with the expected options', async () => {
wrapper.setProps({ projects }); wrapper = createComponent({ props: { projects } });
await wrapper.vm.$nextTick();
expect(findProjectFilter().props('filter').options).toEqual([ expect(findProjectFilter().props('filter').options).toEqual([
{ id: '11', name: projects[0].name }, { id: '11', name: projects[0].name },
......
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