diff --git a/src/platform/packages/shared/kbn-grouping/src/components/grouping.mock.tsx b/src/platform/packages/shared/kbn-grouping/src/components/grouping.mock.tsx index 787a31ec5fd8a..ca706746498e0 100644 --- a/src/platform/packages/shared/kbn-grouping/src/components/grouping.mock.tsx +++ b/src/platform/packages/shared/kbn-grouping/src/components/grouping.mock.tsx @@ -130,6 +130,9 @@ export const mockGroupingProps = { unitsCountWithoutNull: { value: 14, }, + nullGroupItems: { + doc_count: 1, + }, }, groupingId: 'test-grouping-id', isLoading: false, diff --git a/src/platform/packages/shared/kbn-grouping/src/components/grouping.test.tsx b/src/platform/packages/shared/kbn-grouping/src/components/grouping.test.tsx index c76be444fdd27..b215e80786afc 100644 --- a/src/platform/packages/shared/kbn-grouping/src/components/grouping.test.tsx +++ b/src/platform/packages/shared/kbn-grouping/src/components/grouping.test.tsx @@ -207,6 +207,7 @@ describe('Grouping', () => { ); expect(screen.getByTestId('group-count').textContent).toBe('3 groups'); }); + it('calls custom groupsUnit callback correctly', () => { // Provide a custom groupsUnit function in testProps const customGroupsUnit = jest.fn( @@ -223,5 +224,59 @@ describe('Grouping', () => { expect(customGroupsUnit).toHaveBeenCalledWith(3, testProps.selectedGroup, true); expect(screen.getByTestId('group-count').textContent).toBe('3 custom units'); }); + + it('calls custom groupsUnit callback with hasNullGroup = false and null group in current page', () => { + const customGroupsUnit = jest.fn( + (n, parentSelectedGroup, hasNullGroup) => `${n} custom units` + ); + + const customProps = { + ...testProps, + groupsUnit: customGroupsUnit, + data: { + ...testProps.data, + nullGroupItems: { + ...testProps.data.nullGroupItems, + doc_count: 0, + }, + }, + }; + + render( + + + + ); + + expect(customGroupsUnit).toHaveBeenCalledWith(3, testProps.selectedGroup, true); + expect(screen.getByTestId('group-count').textContent).toBe('3 custom units'); + }); + }); + + it('calls custom groupsUnit callback with hasNullGroup = true and no null group in current page', () => { + const customGroupsUnit = jest.fn((n, parentSelectedGroup, hasNullGroup) => `${n} custom units`); + + const customProps = { + ...testProps, + groupsUnit: customGroupsUnit, + data: { + ...testProps.data, + groupByFields: { + ...testProps.data.groupByFields, + buckets: testProps?.data?.groupByFields?.buckets?.map( + (bucket, index) => (index === 2 ? { ...bucket, isNullGroup: undefined } : bucket) as any + ), + }, + }, + }; + + render( + + + + ); + + expect(customGroupsUnit).toHaveBeenCalledWith(3, testProps.selectedGroup, true); + expect(screen.getByTestId('group-count').textContent).toBe('3 custom units'); }); }); diff --git a/src/platform/packages/shared/kbn-grouping/src/components/grouping.tsx b/src/platform/packages/shared/kbn-grouping/src/components/grouping.tsx index ad655438440a0..b9a1f98480323 100644 --- a/src/platform/packages/shared/kbn-grouping/src/components/grouping.tsx +++ b/src/platform/packages/shared/kbn-grouping/src/components/grouping.tsx @@ -103,13 +103,15 @@ const GroupingComponent = ({ const groupCount = useMemo(() => data?.groupsCount?.value ?? 0, [data?.groupsCount?.value]); const groupCountText = useMemo(() => { - const hasNullGroup = + const hasNullGroupInCurrentPage = data?.groupByFields?.buckets?.some( (groupBucket: GroupingBucket) => groupBucket.isNullGroup ) || false; - return `${groupsUnit(groupCount, selectedGroup, hasNullGroup)}`; - }, [data?.groupByFields?.buckets, groupCount, groupsUnit, selectedGroup]); + const hasNullGroup = Boolean(data?.nullGroupItems?.doc_count); + + return `${groupsUnit(groupCount, selectedGroup, hasNullGroupInCurrentPage || hasNullGroup)}`; + }, [data?.groupByFields?.buckets, data?.nullGroupItems, groupCount, groupsUnit, selectedGroup]); const groupPanels = useMemo( () => diff --git a/src/platform/packages/shared/kbn-grouping/src/components/types.ts b/src/platform/packages/shared/kbn-grouping/src/components/types.ts index bbaef504eb434..43a77e8a60704 100644 --- a/src/platform/packages/shared/kbn-grouping/src/components/types.ts +++ b/src/platform/packages/shared/kbn-grouping/src/components/types.ts @@ -37,6 +37,9 @@ export interface RootAggregation { unitsCountWithoutNull?: { value?: number | null; }; + nullGroupItems?: { + doc_count?: number; + }; } export type ParsedRootAggregation = RootAggregation & { diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts index 8612c754ac35c..fe80dda0155ed 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts @@ -47,7 +47,7 @@ export const MISCONFIGURATIONS_GROUPS_UNIT = ( }); default: return i18n.translate('xpack.csp.findings.groupUnit', { - values: { groupCount: totalCount }, + values: { groupCount }, defaultMessage: `{groupCount} {groupCount, plural, =1 {group} other {groups}}`, }); } diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.test.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.test.tsx new file mode 100644 index 0000000000000..2a8a878497f41 --- /dev/null +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.test.tsx @@ -0,0 +1,122 @@ +/* + * 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 React from 'react'; +import { renderHook } from '@testing-library/react'; +import { useLatestFindingsGrouping } from './use_latest_findings_grouping'; +import { useCloudSecurityGrouping } from '../../../components/cloud_security_grouping'; +import { useDataViewContext } from '../../../common/contexts/data_view_context'; +import { useGetCspBenchmarkRulesStatesApi } from '@kbn/cloud-security-posture/src/hooks/use_get_benchmark_rules_state_api'; +import { getGroupingQuery } from '@kbn/grouping'; +import { useGroupedFindings } from './use_grouped_findings'; + +jest.mock('../../../components/cloud_security_grouping'); +jest.mock('../../../common/contexts/data_view_context'); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_get_benchmark_rules_state_api'); +jest.mock('@kbn/grouping', () => ({ + getGroupingQuery: jest.fn().mockImplementation((params) => { + return { + query: { bool: {} }, + }; + }), + parseGroupingQuery: jest.fn().mockReturnValue({}), +})); +jest.mock('./use_grouped_findings'); + +describe('useLatestFindingsGrouping', () => { + const mockGroupPanelRenderer = ( + selectedGroup: string, + fieldBucket: any, + nullGroupMessage?: string, + isLoading?: boolean + ) =>
Mock Group Panel Renderer
; + const mockGetGroupStats = jest.fn(); + + beforeEach(() => { + (useCloudSecurityGrouping as jest.Mock).mockReturnValue({ + grouping: { selectedGroups: ['cloud.account.id'] }, + }); + (useDataViewContext as jest.Mock).mockReturnValue({ dataView: {} }); + (useGetCspBenchmarkRulesStatesApi as jest.Mock).mockReturnValue({ data: {} }); + (useGroupedFindings as jest.Mock).mockReturnValue({ + data: {}, + isFetching: false, + }); + }); + + it('calls getGroupingQuery with correct rootAggregations', () => { + renderHook(() => + useLatestFindingsGrouping({ + groupPanelRenderer: mockGroupPanelRenderer, + getGroupStats: mockGetGroupStats, + groupingLevel: 0, + groupFilters: [], + selectedGroup: 'cloud.account.id', + }) + ); + + expect(getGroupingQuery).toHaveBeenCalledWith( + expect.objectContaining({ + rootAggregations: [ + { + failedFindings: { + filter: { + term: { + 'result.evaluation': { value: 'failed' }, + }, + }, + }, + passedFindings: { + filter: { + term: { + 'result.evaluation': { value: 'passed' }, + }, + }, + }, + nullGroupItems: { + missing: { field: 'cloud.account.id' }, + }, + }, + ], + }) + ); + }); + + it('calls getGroupingQuery without nullGroupItems when selectedGroup is "none"', () => { + renderHook(() => + useLatestFindingsGrouping({ + groupPanelRenderer: mockGroupPanelRenderer, + getGroupStats: mockGetGroupStats, + groupingLevel: 0, + groupFilters: [], + selectedGroup: 'none', + }) + ); + + expect(getGroupingQuery).toHaveBeenCalledWith( + expect.objectContaining({ + rootAggregations: [ + { + failedFindings: { + filter: { + term: { + 'result.evaluation': { value: 'failed' }, + }, + }, + }, + passedFindings: { + filter: { + term: { + 'result.evaluation': { value: 'passed' }, + }, + }, + }, + }, + ], + }) + ); + }); +}); diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx index f591115792e08..fd277348d3dee 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx @@ -230,6 +230,11 @@ export const useLatestFindingsGrouping = ({ }, }, }, + ...(!isNoneGroup([currentSelectedGroup]) && { + nullGroupItems: { + missing: { field: currentSelectedGroup }, + }, + }), }, ], }); diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx index 1d73b21f083a5..43b22b60bcfac 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx @@ -187,6 +187,15 @@ export const useLatestVulnerabilitiesGrouping = ({ sort: [{ groupByField: { order: 'desc' } }], statsAggregations: getAggregationsByGroupField(currentSelectedGroup), runtimeMappings: getRuntimeMappingsByGroupField(currentSelectedGroup), + rootAggregations: [ + { + ...(!isNoneGroup([currentSelectedGroup]) && { + nullGroupItems: { + missing: { field: currentSelectedGroup }, + }, + }), + }, + ], }); const { data, isFetching } = useGroupedVulnerabilities({ diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/translations.ts b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/translations.ts index 2864ff7f29005..33fe66d61c81b 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/translations.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/vulnerabilities/translations.ts @@ -44,7 +44,7 @@ export const VULNERABILITIES_GROUPS_UNIT = ( }); default: return i18n.translate('xpack.csp.vulnerabilities.groupUnit', { - values: { groupCount: totalCount }, + values: { groupCount }, defaultMessage: `{groupCount} {groupCount, plural, =1 {group} other {groups}}`, }); }