diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts index 03517383ecc3f..ae8ddb48488c1 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts @@ -137,7 +137,7 @@ export const useCloudPostureDataTable = ({ return { setUrlQuery, sort: urlQuery.sort, - filters: urlQuery.filters, + filters: urlQuery.filters || [], query: baseEsQuery.query ? baseEsQuery.query : { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.handlers.mock.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.handlers.mock.ts index 38e4edf46f77a..c5fb197583dd9 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.handlers.mock.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.handlers.mock.ts @@ -11,6 +11,15 @@ import { isArray } from 'lodash'; import { http, HttpResponse } from 'msw'; import { v4 as uuidV4 } from 'uuid'; +export const generateMultipleCspFindings = ( + option: { count: number; failedCount?: number } = { count: 1, failedCount: 0 } +) => { + const failedCount = option.failedCount || 0; + return Array.from({ length: option?.count }, (_, i) => { + return generateCspFinding(i.toString(), i < failedCount ? 'failed' : 'passed'); + }); +}; + export const generateCspFinding = ( id: string, evaluation: 'failed' | 'passed' = 'passed' @@ -211,25 +220,36 @@ export const bsearchFindingsHandler = (findings: CspFinding[]) => filter[0]?.bool?.should?.[0]?.term?.['rule.section']?.value !== undefined; if (hasRuleSectionQuerySearchTerm) { - const filteredFindingJson = findings.filter((finding) => { + const filteredFindings = findings.filter((finding) => { const termValue = (filter[0].bool?.should as estypes.QueryDslQueryContainer[])?.[0]?.term?.[ 'rule.section' ]?.value; return finding.rule.section === termValue; }); - return HttpResponse.json(getFindingsBsearchResponse(filteredFindingJson)); + return HttpResponse.json(getFindingsBsearchResponse(filteredFindings)); } const hasRuleSectionFilter = isArray(filter) && filter?.[0]?.match_phrase?.['rule.section'] !== undefined; if (hasRuleSectionFilter) { - const filteredFindingJson = findings.filter((finding) => { + const filteredFindings = findings.filter((finding) => { return finding.rule.section === filter?.[0]?.match_phrase?.['rule.section']; }); - return HttpResponse.json(getFindingsBsearchResponse(filteredFindingJson)); + return HttpResponse.json(getFindingsBsearchResponse(filteredFindings)); + } + + const hasResultEvaluationFilter = + isArray(filter) && filter?.[0]?.match_phrase?.['result.evaluation'] !== undefined; + + if (hasResultEvaluationFilter) { + const filteredFindings = findings.filter((finding) => { + return finding.result.evaluation === filter?.[0]?.match_phrase?.['result.evaluation']; + }); + + return HttpResponse.json(getFindingsBsearchResponse(filteredFindings)); } return HttpResponse.json(getFindingsBsearchResponse(findings)); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.test.tsx index 324eddd1fd8fc..9ff1b40d49c70 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.test.tsx @@ -22,6 +22,7 @@ import * as statusHandlers from '../../../server/routes/status/status.handlers.m import { bsearchFindingsHandler, generateCspFinding, + generateMultipleCspFindings, rulesGetStatesHandler, } from './configurations.handlers.mock'; @@ -247,4 +248,109 @@ describe('', () => { expect(screen.getByText(finding2.resource.name)).toBeInTheDocument(); }); }); + + describe('DistributionBar', () => { + it('renders the distribution bar', async () => { + server.use(statusHandlers.indexedHandler); + server.use( + bsearchFindingsHandler( + generateMultipleCspFindings({ + count: 10, + failedCount: 3, + }) + ) + ); + + renderFindingsPage(); + + // Loading while checking the status API + expect(screen.getByText(/loading/i)).toBeInTheDocument(); + + await waitFor(() => expect(screen.getByText(/10 findings/i)).toBeInTheDocument()); + + screen.getByRole('button', { + name: /passed findings: 7/i, + }); + screen.getByRole('button', { + name: /failed findings: 3/i, + }); + + // Assert that the distribution bar has the correct percentages rendered + expect(screen.getByTestId('distribution_bar_passed')).toHaveStyle('flex: 7'); + expect(screen.getByTestId('distribution_bar_failed')).toHaveStyle('flex: 3'); + }); + + it('filters by passed findings when clicking on the passed findings button', async () => { + server.use(statusHandlers.indexedHandler); + server.use( + bsearchFindingsHandler( + generateMultipleCspFindings({ + count: 2, + failedCount: 1, + }) + ) + ); + + renderFindingsPage(); + + // Loading while checking the status API + expect(screen.getByText(/loading/i)).toBeInTheDocument(); + + await waitFor(() => expect(screen.getByText(/2 findings/i)).toBeInTheDocument()); + + const passedFindingsButton = screen.getByRole('button', { + name: /passed findings: 1/i, + }); + userEvent.click(passedFindingsButton); + + await waitFor(() => expect(screen.getByText(/1 findings/i)).toBeInTheDocument()); + + screen.getByRole('button', { + name: /passed findings: 1/i, + }); + screen.getByRole('button', { + name: /failed findings: 0/i, + }); + + // Assert that the distribution bar has the correct percentages rendered + expect(screen.getByTestId('distribution_bar_passed')).toHaveStyle('flex: 1'); + expect(screen.getByTestId('distribution_bar_failed')).toHaveStyle('flex: 0'); + }, 10000); + it('filters by failed findings when clicking on the failed findings button', async () => { + server.use(statusHandlers.indexedHandler); + server.use( + bsearchFindingsHandler( + generateMultipleCspFindings({ + count: 2, + failedCount: 1, + }) + ) + ); + + renderFindingsPage(); + + // Loading while checking the status API + expect(screen.getByText(/loading/i)).toBeInTheDocument(); + + await waitFor(() => expect(screen.getByText(/2 findings/i)).toBeInTheDocument()); + + const failedFindingsButton = screen.getByRole('button', { + name: /failed findings: 1/i, + }); + userEvent.click(failedFindingsButton); + + await waitFor(() => expect(screen.getByText(/1 findings/i)).toBeInTheDocument()); + + screen.getByRole('button', { + name: /passed findings: 0/i, + }); + screen.getByRole('button', { + name: /failed findings: 1/i, + }); + + // Assert that the distribution bar has the correct percentages rendered + expect(screen.getByTestId('distribution_bar_passed')).toHaveStyle('flex: 0'); + expect(screen.getByTestId('distribution_bar_failed')).toHaveStyle('flex: 1'); + }, 10000); + }); }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_distribution_bar.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_distribution_bar.tsx index 35f6fd008d4b9..56ca9687551d8 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_distribution_bar.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_distribution_bar.tsx @@ -11,7 +11,6 @@ import { EuiBadge, EuiSpacer, EuiFlexGroup, - EuiFlexItem, useEuiTheme, EuiTextColor, } from '@elastic/eui'; @@ -28,6 +27,14 @@ interface Props { distributionOnClick: (evaluation: Evaluation) => void; } +const I18N_PASSED_FINDINGS = i18n.translate('xpack.csp.findings.distributionBar.totalPassedLabel', { + defaultMessage: 'Passed Findings', +}); + +const I18N_FAILED_FINDINGS = i18n.translate('xpack.csp.findings.distributionBar.totalFailedLabel', { + defaultMessage: 'Failed Findings', +}); + export const CurrentPageOfTotal = ({ pageEnd, pageStart, @@ -60,42 +67,21 @@ export const FindingsDistributionBar = (props: Props) => ( ); - -const Counters = (props: Props) => ( - - - - - - - -); - -const PassedFailedCounters = ({ passed, failed }: Pick) => { +const Counters = ({ passed, failed }: Pick) => { const { euiTheme } = useEuiTheme(); + return ( -
- - -
+ {I18N_PASSED_FINDINGS} + {getAbbreviatedNumber(passed)} + {I18N_FAILED_FINDINGS} + {getAbbreviatedNumber(failed)} + ); }; @@ -121,6 +107,7 @@ const DistributionBar: React.FC> = ({ distributionOnClick(RULE_PASSED); }} data-test-subj="distribution_bar_passed" + aria-label={`${I18N_PASSED_FINDINGS}: ${passed}`} /> > = ({ distributionOnClick(RULE_FAILED); }} data-test-subj="distribution_bar_failed" + aria-label={`${I18N_FAILED_FINDINGS}: ${failed}`} /> ); @@ -144,25 +132,18 @@ const DistributionBarPart = ({ color: string; distributionOnClick: () => void; ['data-test-subj']: string; + ['aria-label']: string; }) => (