Commit 0d47287e authored by Phil Hughes's avatar Phil Hughes

Merge branch '214386-project-dropdown-filter' into 'master'

Add project filter

See merge request gitlab-org/gitlab!31444
parents 2bb589b2 f41fd4b4
......@@ -32,12 +32,16 @@ export default {
data() {
return {
filters: {},
projects: [],
};
},
methods: {
handleFilterChange(filters) {
this.filters = filters;
},
handleProjectsFetch(projects) {
this.projects = projects;
},
},
};
</script>
......@@ -45,13 +49,14 @@ export default {
<template>
<security-dashboard-layout>
<template #header>
<filters @filterChange="handleFilterChange" />
<filters :projects="projects" @filterChange="handleFilterChange" />
</template>
<group-security-vulnerabilities
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
:group-full-path="groupFullPath"
:filters="filters"
@projectFetch="handleProjectsFetch"
/>
<template #aside>
<vulnerability-severity :endpoint="vulnerableProjectsEndpoint" />
......
......@@ -51,6 +51,7 @@ export default {
},
update: ({ group }) => group.vulnerabilities.nodes,
result({ data }) {
this.$emit('projectFetch', data.group.projects.nodes);
this.pageInfo = data.group.vulnerabilities.pageInfo;
},
error() {
......
......@@ -52,6 +52,7 @@ export default {
data() {
return {
filters: {},
graphqlProjectList: [], // TODO: Rename me to projects once we back the project selector with GraphQL as well
showProjectSelector: false,
};
},
......@@ -94,6 +95,9 @@ export default {
toggleProjectSelector() {
this.showProjectSelector = !this.showProjectSelector;
},
handleProjectFetch(projects) {
this.graphqlProjectList = projects;
},
},
};
</script>
......@@ -111,7 +115,12 @@ export default {
>{{ toggleButtonProps.text }}</gl-button
>
</header>
<filters v-if="shouldShowDashboard" @filterChange="handleFilterChange" />
<filters
v-if="shouldShowDashboard"
:projects="graphqlProjectList"
@filterChange="handleFilterChange"
@projectFetch="handleProjectFetch"
/>
</template>
<instance-security-vulnerabilities
v-if="shouldShowDashboard"
......@@ -119,6 +128,7 @@ export default {
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
:filters="filters"
@projectFetch="handleProjectFetch"
/>
<gl-empty-state
v-else-if="shouldShowEmptyState"
......
......@@ -57,6 +57,7 @@ export default {
result({ data, loading }) {
this.isFirstResultLoading = loading;
this.pageInfo = data.vulnerabilities.pageInfo;
this.$emit('projectFetch', data.instanceSecurityDashboard.projects.nodes);
},
error() {
this.errorLoadingVulnerabilities = true;
......
<script>
import { initFirstClassVulnerabilityFilters } from 'ee/security_dashboard/constants';
import { initFirstClassVulnerabilityFilters, mapProjects } from 'ee/security_dashboard/helpers';
import { ALL } from 'ee/security_dashboard/store/modules/filters/constants';
import { setFilter } from 'ee/security_dashboard/store/modules/filters/utils';
import DashboardFilter from 'ee/security_dashboard/components/filter.vue';
......@@ -8,11 +8,27 @@ export default {
components: {
DashboardFilter,
},
props: {
projects: { type: Array, required: false, default: undefined },
},
data() {
return {
filters: initFirstClassVulnerabilityFilters(),
filters: initFirstClassVulnerabilityFilters(this.projects),
};
},
watch: {
/**
* Initially the project list empty. We fetch them dynamically from GraphQL while
* fetching the list of vulnerabilities. We display the project filter with the base
* option and when the projects are fetched we add them to the list.
*/
projects(newProjects, oldProjects) {
if (oldProjects.length === 0) {
const projectFilter = this.filters[3];
projectFilter.options = [projectFilter.options[0], ...mapProjects(this.projects)];
}
},
},
methods: {
setFilter(options) {
const selectedFilters = {};
......
import { s__ } from '~/locale';
import { ALL, BASE_FILTERS } from 'ee/security_dashboard/store/modules/filters/constants';
import { REPORT_TYPES, SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants';
import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants';
const parseOptions = obj =>
Object.entries(obj).map(([id, name]) => ({ id: id.toUpperCase(), name }));
export const initFirstClassVulnerabilityFilters = () => [
{
name: s__('SecurityReports|Status'),
id: 'state',
options: [
{ id: ALL, name: s__('VulnerabilityStatusTypes|All') },
...parseOptions(VULNERABILITY_STATES),
],
selection: new Set([ALL]),
},
{
name: s__('SecurityReports|Severity'),
id: 'severity',
options: [BASE_FILTERS.severity, ...parseOptions(SEVERITY_LEVELS)],
selection: new Set([ALL]),
},
{
name: s__('SecurityReports|Report type'),
id: 'reportType',
options: [BASE_FILTERS.report_type, ...parseOptions(REPORT_TYPES)],
selection: new Set([ALL]),
},
];
export default () => ({});
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "ee/vulnerabilities/graphql/vulnerability.fragment.graphql"
#import "./project.fragment.graphql"
query group(
$fullPath: ID!,
$after: String,
$first: Int,
$fullPath: ID!
$after: String
$first: Int
$projectId: [ID!]
$severity: [VulnerabilitySeverity!]
$reportType: [VulnerabilityReportType!]
$state: [VulnerabilityState!]
) {
group(fullPath: $fullPath) {
projects(hasVulnerabilities: true) {
nodes {
...Project
}
}
vulnerabilities(
after:$after,
first:$first,
after: $after
first: $first
severity: $severity
reportType: $reportType
state: $state
projectId: $projectId
){
nodes{
...Vulnerability
......
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "ee/vulnerabilities/graphql/vulnerability.fragment.graphql"
#import "./project.fragment.graphql"
query instance(
$after: String,
$after: String
$first: Int
$projectId: [ID!]
$severity: [VulnerabilitySeverity!]
$reportType: [VulnerabilityReportType!]
$state: [VulnerabilityState!]
) {
instanceSecurityDashboard {
projects{
nodes{
...Project
}
}
}
vulnerabilities(
after:$after,
first:$first,
after: $after
first: $first
severity: $severity
reportType: $reportType
state: $state
projectId: $projectId
) {
nodes {
...Vulnerability
......
import { s__ } from '~/locale';
import { ALL, BASE_FILTERS } from 'ee/security_dashboard/store/modules/filters/constants';
import { REPORT_TYPES, SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants';
import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants';
const parseOptions = obj =>
Object.entries(obj).map(([id, name]) => ({ id: id.toUpperCase(), name }));
export const mapProjects = projects =>
projects.map(p => ({ id: p.id.split('/').pop(), name: p.name }));
export const initFirstClassVulnerabilityFilters = projects => {
const filters = [
{
name: s__('SecurityReports|Status'),
id: 'state',
options: [
{ id: ALL, name: s__('VulnerabilityStatusTypes|All') },
...parseOptions(VULNERABILITY_STATES),
],
selection: new Set([ALL]),
},
{
name: s__('SecurityReports|Severity'),
id: 'severity',
options: [BASE_FILTERS.severity, ...parseOptions(SEVERITY_LEVELS)],
selection: new Set([ALL]),
},
{
name: s__('SecurityReports|Report type'),
id: 'reportType',
options: [BASE_FILTERS.report_type, ...parseOptions(REPORT_TYPES)],
selection: new Set([ALL]),
},
];
if (Array.isArray(projects)) {
filters.push({
name: s__('SecurityReports|Project'),
id: 'projectId',
options: [BASE_FILTERS.project_id, ...mapProjects(projects)],
selection: new Set([ALL]),
});
}
return filters;
};
export default () => ({});
---
title: Add project filter
merge_request: 31444
author:
type: added
......@@ -52,7 +52,15 @@ describe('First Class Group Dashboard Component', () => {
expect(findFilters().exists()).toBe(true);
});
it('it responds to the filterChange event', () => {
it('responds to the projectFetch event', () => {
const projects = [{ id: 1, name: 'GitLab Org' }];
findGroupVulnerabilities().vm.$listeners.projectFetch(projects);
return wrapper.vm.$nextTick(() => {
expect(findFilters().props('projects')).toEqual(projects);
});
});
it('responds to the filterChange event', () => {
const filters = { severity: 'critical' };
findFilters().vm.$listeners.filterChange(filters);
return wrapper.vm.$nextTick(() => {
......
......@@ -92,7 +92,15 @@ describe('First Class Instance Dashboard Component', () => {
expect(findFilters().exists()).toBe(true);
});
it('it responds to the filterChange event', () => {
it('responds to the projectFetch event', () => {
const projects = [{ id: 1, name: 'GitLab Org' }];
findInstanceVulnerabilities().vm.$listeners.projectFetch(projects);
return wrapper.vm.$nextTick(() => {
expect(findFilters().props('projects')).toEqual(projects);
});
});
it('responds to the filterChange event', () => {
const filters = { severity: 'critical' };
findFilters().vm.$listeners.filterChange(filters);
return wrapper.vm.$nextTick(() => {
......
import { shallowMount } from '@vue/test-utils';
import { initFirstClassVulnerabilityFilters } from 'ee/security_dashboard/constants';
import { initFirstClassVulnerabilityFilters } from 'ee/security_dashboard/helpers';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
import Filter from 'ee/security_dashboard/components/filter.vue';
describe('First class vulnerability filters component', () => {
let wrapper;
let filters;
const projects = [
{ id: 'gid://gitlab/Project/11', name: 'GitLab Org' },
{ id: 'gid://gitlab/Project/12', name: 'GitLab Com' },
];
const findFilters = () => wrapper.findAll(Filter);
const findFirstFilter = () => findFilters().at(0);
const findLastFilter = () => findFilters().at(filters.length - 1);
const createComponent = () => {
wrapper = shallowMount(Filters);
const createComponent = ({ propsData } = {}) => {
return shallowMount(Filters, { propsData });
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('on render', () => {
describe('on render without project filter', () => {
beforeEach(() => {
createComponent();
wrapper = createComponent();
filters = initFirstClassVulnerabilityFilters();
});
......@@ -41,29 +47,79 @@ describe('First class vulnerability filters component', () => {
expect(stub).toHaveBeenCalledWith(options);
});
});
describe('when setFilter is called', () => {
let filterId;
let optionId;
describe('when project filter is populated dynamically', () => {
beforeEach(() => {
filters = initFirstClassVulnerabilityFilters([]);
wrapper = createComponent({ propsData: { projects: [] } });
});
beforeEach(() => {
filterId = filters[0].id;
optionId = filters[0].options[1].id;
it('should render the project filter with one option', () => {
expect(findLastFilter().props('filter')).toEqual({
id: 'projectId',
name: 'Project',
options: [{ id: 'all', name: 'All projects' }],
selection: new Set(['all']),
});
});
wrapper.vm.setFilter({ filterId, optionId });
it('should set the projects dynamically', () => {
wrapper.setProps({ projects });
return wrapper.vm.$nextTick(() => {
expect(findLastFilter().props('filter')).toEqual(
expect.objectContaining({
options: [
{ id: 'all', name: 'All projects' },
{ id: '11', name: 'GitLab Org' },
{ id: '12', name: 'GitLab Com' },
],
}),
);
});
});
});
it('should set the filters locally', () => {
const expectedFilters = initFirstClassVulnerabilityFilters();
expectedFilters[0].selection = new Set([optionId]);
describe('when project filter is ready on mount', () => {
beforeEach(() => {
filters = initFirstClassVulnerabilityFilters([]);
wrapper = createComponent({ propsData: { projects } });
});
expect(wrapper.vm.filters).toEqual(expectedFilters);
});
it('should set the projects dynamically', () => {
expect(findLastFilter().props('filter')).toEqual(
expect.objectContaining({
options: [
{ id: 'all', name: 'All projects' },
{ id: '11', name: 'GitLab Org' },
{ id: '12', name: 'GitLab Com' },
],
}),
);
});
});
it('should emit selected filters when a filter is set', () => {
expect(wrapper.emitted().filterChange).toBeTruthy();
expect(wrapper.emitted().filterChange[0]).toEqual([{ [filterId]: [optionId] }]);
});
describe('when setFilter is called', () => {
let filterId;
let optionId;
beforeEach(() => {
filterId = filters[0].id;
optionId = filters[0].options[1].id;
wrapper = createComponent();
wrapper.vm.setFilter({ filterId, optionId });
});
it('should set the filters locally', () => {
const expectedFilters = initFirstClassVulnerabilityFilters();
expectedFilters[0].selection = new Set([optionId]);
expect(wrapper.vm.filters).toEqual(expectedFilters);
});
it('should emit selected filters when a filter is set', () => {
expect(wrapper.emitted().filterChange).toBeTruthy();
expect(wrapper.emitted().filterChange[0]).toEqual([{ [filterId]: [optionId] }]);
});
});
});
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