Commit 5045686e authored by Daniel Tian's avatar Daniel Tian Committed by Savas Vedova

Remove deprecated project filter on vulnerability report

parent 70a1dc76
---
name: vuln_report_new_project_filter
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55745
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334380
milestone: '14.3'
type: development
group: group::threat insights
default_enabled: true
......@@ -6,13 +6,12 @@ import {
vendorScannerFilter,
simpleScannerFilter,
activityFilter,
getProjectFilter,
projectFilter,
} from 'ee/security_dashboard/helpers';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ActivityFilter from './activity_filter.vue';
import ProjectFilter from './project_filter.vue';
import ProjectFilterDeprecated from './project_filter_deprecated.vue';
import ScannerFilter from './scanner_filter.vue';
import SimpleFilter from './simple_filter.vue';
......@@ -22,13 +21,9 @@ export default {
ScannerFilter,
ActivityFilter,
ProjectFilter,
ProjectFilterDeprecated,
},
mixins: [glFeatureFlagsMixin()],
inject: ['dashboardType'],
props: {
projects: { type: Array, required: false, default: undefined },
},
data() {
return {
filterQuery: {},
......@@ -50,12 +45,6 @@ export default {
shouldShowProjectFilter() {
return this.isGroupDashboard || this.isInstanceDashboard;
},
shouldShowNewProjectFilter() {
return this.glFeatures.vulnReportNewProjectFilter && this.shouldShowProjectFilter;
},
projectFilter() {
return getProjectFilter(this.projects);
},
},
methods: {
updateFilterQuery(query) {
......@@ -77,6 +66,7 @@ export default {
vendorScannerFilter,
simpleScannerFilter,
activityFilter,
projectFilter,
};
</script>
......@@ -111,14 +101,8 @@ export default {
/>
<project-filter
v-if="shouldShowNewProjectFilter"
:filter="projectFilter"
@filter-changed="updateFilterQuery"
/>
<project-filter-deprecated
v-else-if="shouldShowProjectFilter"
:filter="projectFilter"
:data-testid="projectFilter.id"
v-if="shouldShowProjectFilter"
:filter="$options.projectFilter"
@filter-changed="updateFilterQuery"
/>
</div>
......
......@@ -8,11 +8,11 @@ import {
import { escapeRegExp, has, xorBy } from 'lodash';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import createFlash from '~/flash';
import { convertToGraphQLIds } from '~/graphql_shared/utils';
import { convertToGraphQLIds, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale';
import groupProjectsQuery from '../../../graphql/queries/group_projects.query.graphql';
import instanceProjectsQuery from '../../../graphql/queries/instance_projects.query.graphql';
import { mapProjects, PROJECT_LOADING_ERROR_MESSAGE } from '../../../helpers';
import { PROJECT_LOADING_ERROR_MESSAGE } from '../../../helpers';
import FilterBody from './filter_body.vue';
import FilterItem from './filter_item.vue';
import SimpleFilter from './simple_filter.vue';
......@@ -21,6 +21,9 @@ const SEARCH_TERM_MINIMUM_LENGTH = 3;
const SELECTED_PROJECTS_MAX_COUNT = 100;
const PROJECT_ENTITY_NAME = 'Project';
const mapProjects = (projects = []) =>
projects.map((p) => ({ id: getIdFromGraphQLId(p.id).toString(), name: p.name }));
export default {
components: {
FilterBody,
......
<script>
import FilterBody from './filter_body.vue';
import FilterItem from './filter_item.vue';
import SimpleFilter from './simple_filter.vue';
const SHOW_SEARCH_BOX_THRESHOLD = 20;
export default {
components: { FilterBody, FilterItem },
extends: SimpleFilter,
data() {
return {
searchTerm: '',
};
},
computed: {
filteredOptions() {
return this.options.filter((option) =>
option.name.toLowerCase().includes(this.searchTerm.toLowerCase()),
);
},
showSearchBox() {
return this.options.length >= SHOW_SEARCH_BOX_THRESHOLD;
},
},
};
</script>
<template>
<filter-body
v-model.trim="searchTerm"
:name="filter.name"
:selected-options="selectedOptionsOrAll"
:show-search-box="showSearchBox"
>
<filter-item
v-if="filter.allOption && !searchTerm.length"
:is-checked="isNoOptionsSelected"
:text="filter.allOption.name"
data-testid="allOption"
@click="deselectAllOptions"
/>
<filter-item
v-for="option in filteredOptions"
:key="option.id"
:is-checked="isSelected(option)"
:text="option.name"
:data-testid="`${filter.id}:${option.id}`"
@click="toggleOption(option)"
/>
</filter-body>
</template>
<script>
import { GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { GlLink, GlSprintf } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { PortalTarget } from 'portal-vue';
import groupProjectsQuery from 'ee/security_dashboard/graphql/queries/group_projects.query.graphql';
import instanceProjectsQuery from 'ee/security_dashboard/graphql/queries/instance_projects.query.graphql';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
......@@ -39,7 +37,6 @@ export default {
PortalTarget,
ProjectPipelineStatus,
GlLink,
GlLoadingIcon,
GlSprintf,
VulnerabilitiesCountList,
},
......@@ -55,23 +52,7 @@ export default {
autoFixDocumentation: { default: undefined },
dashboardDocumentation: { default: undefined },
pipeline: { default: undefined },
},
apollo: {
projects: {
query() {
return this.isGroup ? groupProjectsQuery : instanceProjectsQuery;
},
variables() {
return this.isGroup ? { fullPath: this.groupFullPath } : {};
},
update(data) {
return this.isGroup ? data.group.projects.nodes : data.instance.projects.nodes;
},
skip() {
// Only run this query on the group and instance-level dashboards.
return !(this.isGroup || this.isInstance);
},
},
hasProjects: { default: undefined },
},
data() {
const shouldShowAutoFixUserCallout =
......@@ -81,14 +62,10 @@ export default {
return {
filters: null,
projects: [],
shouldShowAutoFixUserCallout,
};
},
computed: {
projectsWereFetched() {
return !this.$apollo.queries.projects?.loading;
},
isGroup() {
return this.dashboardType === DASHBOARD_TYPES.GROUP;
},
......@@ -115,7 +92,7 @@ export default {
}
// Group and Instance Dashboards
return this.projects.length > 0;
return this.hasProjects;
},
shouldShowPipelineStatus() {
return this.isProject && Boolean(this.pipeline);
......@@ -143,8 +120,7 @@ export default {
<template>
<div>
<gl-loading-icon v-if="!projectsWereFetched" size="lg" class="gl-mt-6" />
<template v-else-if="!isDashboardConfigured">
<template v-if="!isDashboardConfigured">
<survey-request-banner v-if="shouldShowSurvey" class="gl-mt-5" />
<report-not-configured-group v-if="isGroup" />
<report-not-configured-instance v-else-if="isInstance" />
......@@ -182,7 +158,7 @@ export default {
<vulnerabilities-count-list :filters="filters" />
</template>
<template #sticky>
<filters :projects="projects" @filterChange="handleFilterChange" />
<filters @filterChange="handleFilterChange" />
</template>
<group-vulnerabilities v-if="isGroup" :filters="filters" />
<instance-vulnerabilities v-else-if="isInstance" :filters="filters" />
......
......@@ -3,9 +3,9 @@ import {
stateFilter,
severityFilter,
activityFilter,
projectFilter,
simpleScannerFilterNoClusterImage,
vendorScannerFilterNoClusterImage,
getProjectFilter,
} from 'ee/security_dashboard/helpers';
import { REPORT_TYPES_NO_CLUSTER_IMAGE } from 'ee/security_dashboard/store/constants';
import { REPORT_TYPE_CLUSTER_IMAGE_SCANNING } from '~/vue_shared/security_reports/constants';
......@@ -70,7 +70,7 @@ export const FILTERS = {
ACTIVITY: activityFilter,
TOOL_SIMPLE: simpleScannerFilterNoClusterImage,
TOOL_VENDOR: vendorScannerFilterNoClusterImage,
PROJECT: getProjectFilter(),
PROJECT: projectFilter,
};
export const FIELD_PRESETS = {
......
......@@ -2,7 +2,6 @@
import { debounce, cloneDeep, isEqual } from 'lodash';
import ActivityFilter from '../filters/activity_filter.vue';
import ProjectFilter from '../filters/project_filter.vue';
import ProjectFilterDeprecated from '../filters/project_filter_deprecated.vue';
import ScannerFilter from '../filters/scanner_filter.vue';
import SimpleFilter from '../filters/simple_filter.vue';
import { FILTERS } from './constants';
......@@ -15,7 +14,6 @@ export default {
ScannerFilter,
ActivityFilter,
ProjectFilter,
ProjectFilterDeprecated,
},
props: {
filters: {
......
......@@ -7,7 +7,6 @@ import {
import { BASE_FILTERS } from 'ee/security_dashboard/store/modules/filters/constants';
import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type';
import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import { s__, __ } from '~/locale';
import { DEFAULT_SCANNER } from './constants';
......@@ -15,9 +14,6 @@ import { DEFAULT_SCANNER } from './constants';
const parseOptions = (obj) =>
Object.entries(obj).map(([id, name]) => ({ id: id.toUpperCase(), name }));
export const mapProjects = (projects = []) =>
projects.map((p) => ({ id: getIdFromGraphQLId(p.id).toString(), name: p.name }));
const stateOptions = parseOptions(VULNERABILITY_STATES);
const defaultStateOptions = stateOptions.filter((x) => ['DETECTED', 'CONFIRMED'].includes(x.id));
......@@ -101,14 +97,12 @@ export const activityFilter = {
defaultOptions: [],
};
export const getProjectFilter = (projects) => {
return {
export const projectFilter = {
name: s__('SecurityReports|Project'),
id: 'projectId',
options: mapProjects(projects),
options: [],
allOption: BASE_FILTERS.project_id,
defaultOptions: [],
};
};
/**
......
......@@ -7,7 +7,6 @@ module Groups
before_action do
push_frontend_feature_flag(:vulnerability_management_survey, type: :ops, default_enabled: :yaml)
push_frontend_feature_flag(:vuln_report_new_project_filter, current_user, default_enabled: :yaml)
push_frontend_feature_flag(:operational_vulnerabilities, current_user, default_enabled: :yaml)
end
......
......@@ -6,7 +6,6 @@ module Security
before_action do
push_frontend_feature_flag(:vulnerability_management_survey, type: :ops, default_enabled: :yaml)
push_frontend_feature_flag(:vuln_report_new_project_filter, current_user, default_enabled: :yaml)
push_frontend_feature_flag(:operational_vulnerabilities, current_user, default_enabled: :yaml)
end
end
......
......@@ -4,29 +4,22 @@ import Filters from 'ee/security_dashboard/components/shared/filters/filters_lay
import ProjectFilter from 'ee/security_dashboard/components/shared/filters/project_filter.vue';
import ScannerFilter from 'ee/security_dashboard/components/shared/filters/scanner_filter.vue';
import SimpleFilter from 'ee/security_dashboard/components/shared/filters/simple_filter.vue';
import { getProjectFilter, simpleScannerFilter } from 'ee/security_dashboard/helpers';
import { simpleScannerFilter } from 'ee/security_dashboard/helpers';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('First class vulnerability filters component', () => {
let wrapper;
const projects = [
{ id: 'gid://gitlab/Project/11', name: 'GitLab Org' },
{ id: 'gid://gitlab/Project/12', name: 'GitLab Com' },
];
const findSimpleFilters = () => wrapper.findAllComponents(SimpleFilter);
const findSimpleScannerFilter = () => wrapper.findByTestId(simpleScannerFilter.id);
const findVendorScannerFilter = () => wrapper.findComponent(ScannerFilter);
const findActivityFilter = () => wrapper.findComponent(ActivityFilter);
const findProjectFilter = () => wrapper.findByTestId(getProjectFilter([]).id);
const findNewProjectFilter = () => wrapper.findComponent(ProjectFilter);
const findProjectFilter = () => wrapper.findComponent(ProjectFilter);
const createComponent = ({ props, provide } = {}) => {
const createComponent = ({ provide } = {}) => {
return extendedWrapper(
shallowMount(Filters, {
propsData: props,
provide: {
dashboardType: DASHBOARD_TYPES.PROJECT,
...provide,
......@@ -73,37 +66,6 @@ describe('First class vulnerability filters component', () => {
expect(findProjectFilter().exists()).toBe(isShown);
},
);
it('should render the project filter with the expected options', () => {
wrapper = createComponent({
provide: { dashboardType: DASHBOARD_TYPES.GROUP },
props: { projects },
});
expect(findProjectFilter().props('filter').options).toEqual([
{ id: '11', name: projects[0].name },
{ id: '12', name: projects[1].name },
]);
});
it.each`
featureFlag | isProjectFilterShown | isNewProjectFilterShown
${false} | ${true} | ${false}
${true} | ${false} | ${true}
`(
'should show the correct project filter when vulnReportNewProjectFilter feature flag is $featureFlag',
({ featureFlag, isProjectFilterShown, isNewProjectFilterShown }) => {
wrapper = createComponent({
provide: {
dashboardType: DASHBOARD_TYPES.GROUP,
glFeatures: { vulnReportNewProjectFilter: featureFlag },
},
});
expect(findProjectFilter().exists()).toBe(isProjectFilterShown);
expect(findNewProjectFilter().exists()).toBe(isNewProjectFilterShown);
},
);
});
describe('activity filter', () => {
......
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import FilterBody from 'ee/security_dashboard/components/shared/filters/filter_body.vue';
import FilterItem from 'ee/security_dashboard/components/shared/filters/filter_item.vue';
import ProjectFilterDeprecated from 'ee/security_dashboard/components/shared/filters/project_filter_deprecated.vue';
import { getProjectFilter } from 'ee/security_dashboard/helpers';
const generateProjects = (length) =>
Array.from({ length }, (_, i) => ({ id: i + 1, name: `Option ${i + 1}` }));
describe('Project Filter Deprecated component', () => {
let wrapper;
const createWrapper = ({ projects }) => {
wrapper = shallowMount(ProjectFilterDeprecated, {
propsData: { filter: getProjectFilter(projects) },
});
};
const dropdownItems = () => wrapper.findAllComponents(FilterItem);
const filterBody = () => wrapper.findComponent(FilterBody);
afterEach(() => {
wrapper.destroy();
});
describe('search box', () => {
it.each`
phrase | count | shouldShow
${'shows'} | ${20} | ${true}
${'hides'} | ${15} | ${false}
`('$phrase search box if there are $count options', ({ count, shouldShow }) => {
createWrapper({ projects: generateProjects(count) });
expect(filterBody().props('showSearchBox')).toBe(shouldShow);
});
it('filters options when something is typed in the search box', async () => {
const projects = generateProjects(11);
const expectedProjectNames = ['Option 1', 'Option 10', 'Option 11'];
createWrapper({ projects });
filterBody().vm.$emit('input', '1');
await nextTick();
expect(dropdownItems()).toHaveLength(3);
expect(dropdownItems().wrappers.map((x) => x.props('text'))).toEqual(expectedProjectNames);
});
});
});
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Cookies from 'js-cookie';
import { PortalTarget } from 'portal-vue';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import GroupVulnerabilities from 'ee/security_dashboard/components/group/group_vulnerabilities.vue';
import InstanceVulnerabilities from 'ee/security_dashboard/components/instance/instance_vulnerabilities.vue';
import ProjectVulnerabilities from 'ee/security_dashboard/components/project/project_vulnerabilities.vue';
......@@ -17,11 +15,7 @@ import SurveyRequestBanner from 'ee/security_dashboard/components/shared/survey_
import VulnerabilitiesCountList from 'ee/security_dashboard/components/shared/vulnerability_count_list.vue';
import VulnerabilityReport from 'ee/security_dashboard/components/shared/vulnerability_report.vue';
import VulnerabilityReportLayout from 'ee/security_dashboard/components/shared/vulnerability_report_layout.vue';
import groupProjectsQuery from 'ee/security_dashboard/graphql/queries/group_projects.query.graphql';
import instanceProjectsQuery from 'ee/security_dashboard/graphql/queries/instance_projects.query.graphql';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import createApolloProvider from 'helpers/mock_apollo_helper';
import { mockVulnerableProjectsInstance, mockVulnerableProjectsGroup } from '../../mock_data';
describe('Vulnerability Report', () => {
let wrapper;
......@@ -34,7 +28,6 @@ describe('Vulnerability Report', () => {
const findCsvExportButton = () => wrapper.findComponent(CsvExportButton);
const findGroupEmptyState = () => wrapper.findComponent(ReportNotConfiguredGroup);
const findInstanceEmptyState = () => wrapper.findComponent(ReportNotConfiguredInstance);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findFilters = () => wrapper.findComponent(Filters);
const findVulnerabilitiesCountList = () => wrapper.findComponent(VulnerabilitiesCountList);
const findProjectPipelineStatus = () => wrapper.findComponent(ProjectPipelineStatus);
......@@ -42,20 +35,15 @@ describe('Vulnerability Report', () => {
const findHeader = () => wrapper.find('h2');
const createWrapper = ({ data = {}, mocks, propsData, provide, apolloProvider }) => {
const createWrapper = ({ data = {}, mocks, provide }) => {
const localVue = createLocalVue();
if (apolloProvider) {
localVue.use(VueApollo);
}
return shallowMount(VulnerabilityReport, {
localVue,
apolloProvider,
data: () => data,
mocks,
propsData,
provide: {
hasProjects: true,
...provide,
},
stubs: { VulnerabilityReportLayout },
......@@ -72,9 +60,6 @@ describe('Vulnerability Report', () => {
provide: {
dashboardType: DASHBOARD_TYPES.INSTANCE,
},
apolloProvider: createApolloProvider([
[instanceProjectsQuery, jest.fn().mockResolvedValue(mockVulnerableProjectsInstance())],
]),
});
});
......@@ -118,9 +103,6 @@ describe('Vulnerability Report', () => {
dashboardType: DASHBOARD_TYPES.INSTANCE,
},
data: { filters },
apolloProvider: createApolloProvider([
[instanceProjectsQuery, jest.fn().mockResolvedValue(mockVulnerableProjectsInstance())],
]),
});
});
......@@ -144,9 +126,6 @@ describe('Vulnerability Report', () => {
groupFullPath: 'gitlab-org',
dashboardType: DASHBOARD_TYPES.GROUP,
},
apolloProvider: createApolloProvider([
[groupProjectsQuery, jest.fn().mockResolvedValue(mockVulnerableProjectsGroup())],
]),
});
});
......@@ -167,15 +146,8 @@ describe('Vulnerability Report', () => {
provide: {
groupFullPath: 'gitlab-org',
dashboardType: DASHBOARD_TYPES.GROUP,
hasProjects: false,
},
apolloProvider: createApolloProvider([
[
groupProjectsQuery,
jest
.fn()
.mockResolvedValue({ data: { group: { id: 'group-1', projects: { nodes: [] } } } }),
],
]),
});
});
......@@ -194,29 +166,6 @@ describe('Vulnerability Report', () => {
});
});
describe('when loading projects', () => {
beforeEach(() => {
wrapper = createWrapper({
provide: { dashboardType: DASHBOARD_TYPES.INSTANCE },
mocks: { $apollo: { queries: { projects: { loading: true } } } },
});
});
it('renders the loading icon', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
it('does not render the export button, vulnerabilities count list, or header', () => {
expect(findCsvExportButton().exists()).toBe(false);
expect(findVulnerabilitiesCountList().exists()).toBe(false);
expect(findHeader().exists()).toBe(false);
});
it('should not show the survey request banner', () => {
expect(findSurveyRequestBanner().exists()).toBe(false);
});
});
describe('when initialized - project level', () => {
const createProjectWrapper = ({ securityAutoFix } = {}) =>
createWrapper({
......@@ -226,7 +175,6 @@ describe('Vulnerability Report', () => {
pipeline: { id: '591' },
glFeatures: { securityAutoFix },
},
apolloProvider: createApolloProvider(),
});
it('does not show user callout when feature flag is disabled', () => {
......@@ -271,7 +219,6 @@ describe('Vulnerability Report', () => {
dashboardType: DASHBOARD_TYPES.PROJECT,
pipeline: null,
},
apolloProvider: createApolloProvider(),
});
});
......
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