diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/get_filters.test.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/get_filters.test.ts new file mode 100644 index 0000000000000..39c88981a3698 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/get_filters.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { CSP_LATEST_FINDINGS_DATA_VIEW } from '../../../common/constants'; +import { createStubDataView } from '@kbn/data-views-plugin/common/stubs'; +import { DataView } from '@kbn/data-views-plugin/common'; +import { getFilters } from './get_filters'; + +describe('Get Filters', () => { + let dataViewMock: DataView; + const fieldKey = 'some_field_name'; + + beforeEach(() => { + dataViewMock = createStubDataView({ + spec: { + id: CSP_LATEST_FINDINGS_DATA_VIEW, + fields: { + a: { + searchable: false, + aggregatable: false, + name: fieldKey, + type: 'type', + }, + }, + }, + }); + }); + + it('negate an existing filter', () => { + const fields = { + dataView: dataViewMock, + field: fieldKey, + value: 'b', + }; + const initialFilters = getFilters({ + ...fields, + filters: [], + negate: false, + }); + + expect(initialFilters.length).toBe(1); + expect(initialFilters[0].meta.negate).toBe(false); + + const nextFilters = getFilters({ + ...fields, + filters: initialFilters, + negate: true, + }); + + expect(nextFilters.length).toBe(1); + expect(nextFilters[0].meta.negate).toBe(true); + }); +}); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/get_filters.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/get_filters.ts new file mode 100644 index 0000000000000..fec0efdcaba4c --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/get_filters.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + type Filter, + buildFilter, + FILTERS, + FilterStateStore, + compareFilters, + FilterCompareOptions, +} from '@kbn/es-query'; +import type { Serializable } from '@kbn/utility-types'; +import type { FindingsBaseProps } from './types'; + +const compareOptions: FilterCompareOptions = { + negate: false, +}; + +/** + * adds a new filter to a new filters array + * removes existing filter if negated filter is added + * + * @returns {Filter[]} a new array of filters to be added back to filterManager + */ +export const getFilters = ({ + filters: existingFilters, + dataView, + field, + value, + negate, +}: { + filters: Filter[]; + dataView: FindingsBaseProps['dataView']; + field: string; + value: Serializable; + negate: boolean; +}): Filter[] => { + const dataViewField = dataView.getFieldByName(field); + if (!dataViewField) return existingFilters; + + const phraseFilter = buildFilter( + dataView, + dataViewField, + FILTERS.PHRASE, + negate, + false, + value, + null, + FilterStateStore.APP_STATE + ); + + const nextFilters = [ + ...existingFilters.filter( + // Exclude existing filters that match the newly added 'phraseFilter' + (filter) => !compareFilters(filter, phraseFilter, compareOptions) + ), + phraseFilter, + ]; + + return nextFilters; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.tsx index c41b6835fb4ff..f72e4b61bd17c 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.tsx @@ -18,7 +18,7 @@ import type { FindingsBaseURLQuery } from '../types'; import { FindingsDistributionBar } from '../layout/findings_distribution_bar'; import { getFindingsPageSizeInfo, - addFilter, + getFilters, getPaginationQuery, getPaginationTableParams, useBaseEsQuery, @@ -119,7 +119,7 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { onAddFilter={(field, value, negate) => setUrlQuery({ pageIndex: 0, - filters: addFilter({ + filters: getFilters({ filters: urlQuery.filters, dataView, field, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx index e55af94202e3c..6ed71372c4580 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx @@ -17,7 +17,7 @@ import { FindingsByResourceQuery, useFindingsByResource } from './use_findings_b import { FindingsByResourceTable } from './findings_by_resource_table'; import { getFindingsPageSizeInfo, - addFilter, + getFilters, getPaginationQuery, getPaginationTableParams, useBaseEsQuery, @@ -143,7 +143,7 @@ const LatestFindingsByResource = ({ dataView }: FindingsBaseProps) => { onAddFilter={(field, value, negate) => setUrlQuery({ pageIndex: 0, - filters: addFilter({ + filters: getFilters({ filters: urlQuery.filters, dataView, field, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx index 1302dfafe6603..c3e23d2a865f1 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx @@ -19,7 +19,7 @@ import { useUrlQuery } from '../../../../common/hooks/use_url_query'; import type { FindingsBaseURLQuery, FindingsBaseProps, CspFinding } from '../../types'; import { getFindingsPageSizeInfo, - addFilter, + getFilters, getPaginationQuery, getPaginationTableParams, useBaseEsQuery, @@ -147,7 +147,7 @@ export const ResourceFindings = ({ dataView }: FindingsBaseProps) => { onAddFilter={(field, value, negate) => setUrlQuery({ pageIndex: 0, - filters: addFilter({ + filters: getFilters({ filters: urlQuery.filters, dataView, field, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/utils.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/utils.ts index 55d33ec15b83b..269e715bd2c06 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/utils.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/utils.ts @@ -5,15 +5,14 @@ * 2.0. */ -import { buildEsQuery, type Filter, buildFilter, FILTERS, FilterStateStore } from '@kbn/es-query'; +import { buildEsQuery } from '@kbn/es-query'; import { EuiBasicTableProps, Pagination } from '@elastic/eui'; import { useCallback, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import type { estypes } from '@elastic/elasticsearch'; -import type { Serializable } from '@kbn/utility-types'; import type { FindingsBaseProps, FindingsBaseURLQuery } from './types'; import { useKibana } from '../../common/hooks/use_kibana'; -import { isNonNullable } from '../../../common/utils/helpers'; +export { getFilters } from './get_filters'; const getBaseQuery = ({ dataView, query, filters }: FindingsBaseURLQuery & FindingsBaseProps) => { try { @@ -130,38 +129,6 @@ export const getAggregationCount = (buckets: estypes.AggregationsStringRareTerms }; }; -export const addFilter = ({ - filters, - dataView, - field, - value, - negate, -}: { - filters: Filter[]; - dataView: FindingsBaseProps['dataView']; - field: string; - value: Serializable; - negate: boolean; -}): Filter[] => { - const dataViewField = dataView.getFieldByName(field); - if (!dataViewField) return filters; - - const singleValue = Array.isArray(value) ? value[0] : value; - - const filter = buildFilter( - dataView, - dataViewField, - FILTERS.PHRASE, - negate, - false, - singleValue, - null, - FilterStateStore.APP_STATE - ); - - return [...filters, filter].filter(isNonNullable); -}; - const FIELDS_WITHOUT_KEYWORD_MAPPING = new Set([ '@timestamp', 'resource.sub_type',