Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] [Security Solution] Document details flyout - update insight KPI count (#196617) #196780

Merged
merged 1 commit into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import type { FC, PropsWithChildren } from 'react';
import React, { useCallback } from 'react';
import { EuiButton, EuiButtonEmpty } from '@elastic/eui';
import type { IconType } from '@elastic/eui';
import type { IconType, EuiButtonEmptyProps } from '@elastic/eui';
import type { Filter } from '@kbn/es-query';
import { useDispatch, useSelector } from 'react-redux';

Expand All @@ -34,6 +34,7 @@ export interface InvestigateInTimelineButtonProps {
isDisabled?: boolean;
iconType?: IconType;
children?: React.ReactNode;
flush?: EuiButtonEmptyProps['flush'];
}

export const InvestigateInTimelineButton: FC<
Expand All @@ -46,6 +47,7 @@ export const InvestigateInTimelineButton: FC<
timeRange,
keepDataView,
iconType,
flush,
...rest
}) => {
const dispatch = useDispatch();
Expand Down Expand Up @@ -118,7 +120,7 @@ export const InvestigateInTimelineButton: FC<
<EuiButtonEmpty
aria-label={ACTION_INVESTIGATE_IN_TIMELINE}
onClick={configureAndOpenTimeline}
flush="right"
flush={flush ?? 'right'}
size="xs"
iconType={iconType}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ describe('AlertCountInsight', () => {
const { getByTestId } = renderAlertCountInsight();
expect(getByTestId(testId)).toBeInTheDocument();
expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument();
expect(getByTestId(`${testId}-count`)).toHaveTextContent('177');
});

it('renders loading spinner if data is being fetched', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ import {
getIsAlertsBySeverityData,
getSeverityColor,
} from '../../../../detections/components/alerts_kpis/severity_level_panel/helpers';
import { FormattedCount } from '../../../../common/components/formatted_number';
import { InvestigateInTimelineButton } from '../../../../common/components/event_details/investigate_in_timeline_button';
import { getDataProvider } from '../../../../common/components/event_details/use_action_cell_data_provider';

const ENTITY_ALERT_COUNT_ID = 'entity-alert-count';
const SEVERITIES = ['unknown', 'low', 'medium', 'high', 'critical'];

interface AlertCountInsightProps {
/**
Expand All @@ -39,7 +43,7 @@ interface AlertCountInsightProps {
}

/*
* Displays a distribution bar with the count of critical alerts for a given entity
* Displays a distribution bar with the total alert count for a given entity
*/
export const AlertCountInsight: React.FC<AlertCountInsightProps> = ({
name,
Expand All @@ -56,22 +60,27 @@ export const AlertCountInsight: React.FC<AlertCountInsightProps> = ({
uniqueQueryId,
signalIndexName: null,
});
const dataProviders = useMemo(
() => [getDataProvider(fieldName, `timeline-indicator-${fieldName}-${name}`, name)],
[fieldName, name]
);

const data = useMemo(() => (getIsAlertsBySeverityData(items) ? items : []), [items]);

const alertStats = useMemo(() => {
return data.map((item) => ({
key: item.key,
count: item.value,
color: getSeverityColor(item.key),
}));
}, [data]);

const count = useMemo(
() => data.filter((item) => item.key === 'critical')[0]?.value ?? 0,
const alertStats = useMemo(
() =>
data
.map((item) => ({
key: item.key,
count: item.value,
color: getSeverityColor(item.key),
}))
.sort((a, b) => SEVERITIES.indexOf(a.key) - SEVERITIES.indexOf(b.key)),
[data]
);

const totalAlertCount = useMemo(() => data.reduce((acc, item) => acc + item.value, 0), [data]);

if (!isLoading && items.length === 0) return null;

return (
Expand All @@ -87,7 +96,17 @@ export const AlertCountInsight: React.FC<AlertCountInsightProps> = ({
/>
}
stats={alertStats}
count={count}
count={
<div data-test-subj={`${dataTestSubj}-count`}>
<InvestigateInTimelineButton
asEmptyButton={true}
dataProviders={dataProviders}
flush={'both'}
>
<FormattedCount count={totalAlertCount} />
</InvestigateInTimelineButton>
</div>
}
direction={direction}
data-test-subj={`${dataTestSubj}-distribution-bar`}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { InsightDistributionBar } from './insight_distribution_bar';
import { TestProviders } from '../../../../common/mock';

const title = 'test title';
const count = 10;
const count = <div data-test-subj="test-count">{'100'}</div>;
const testId = 'test-id';
const stats = [
{
Expand All @@ -35,7 +35,7 @@ describe('<InsightDistributionBar />', () => {
);
expect(getByTestId(testId)).toBeInTheDocument();
expect(getByText(title)).toBeInTheDocument();
expect(getByTestId(`${testId}-badge`)).toHaveTextContent(`${count}`);
expect(getByTestId('test-count')).toBeInTheDocument();
expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React from 'react';
import React, { useMemo } from 'react';
import { css } from '@emotion/css';
import {
EuiFlexGroup,
Expand All @@ -17,7 +17,6 @@ import {
type EuiFlexGroupProps,
} from '@elastic/eui';
import { DistributionBar } from '@kbn/security-solution-distribution-bar';
import { FormattedCount } from '../../../../common/components/formatted_number';

export interface InsightDistributionBarProps {
/**
Expand All @@ -31,7 +30,7 @@ export interface InsightDistributionBarProps {
/**
* Count to be displayed in the badge
*/
count: number;
count: React.ReactNode;
/**
* Flex direction of the component
*/
Expand All @@ -53,34 +52,53 @@ export const InsightDistributionBar: React.FC<InsightDistributionBarProps> = ({
const { euiTheme } = useEuiTheme();
const xsFontSize = useEuiFontSize('xs').fontSize;

const barComponent = useMemo(
() => (
<EuiFlexGroup gutterSize="xs" responsive={false}>
<EuiFlexItem>
<DistributionBar
stats={stats}
hideLastTooltip
data-test-subj={`${dataTestSubj}-distribution-bar`}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} data-test-subj={`${dataTestSubj}-badge`}>
<EuiBadge color="hollow">{count}</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
),
[stats, count, dataTestSubj]
);

return (
<EuiFlexGroup direction={direction} data-test-subj={dataTestSubj} responsive={false}>
<EuiFlexItem>
<EuiFlexGroup
direction={direction}
data-test-subj={dataTestSubj}
responsive={false}
gutterSize="s"
>
<EuiFlexItem grow={false}>
<EuiText
css={css`
font-size: ${xsFontSize};
font-weight: ${euiTheme.font.weight.bold};
min-width: 115px;
`}
>
{title}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="xs" responsive={false}>
<EuiFlexItem>
<DistributionBar
stats={stats}
hideLastTooltip
data-test-subj={`${dataTestSubj}-distribution-bar`}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} data-test-subj={`${dataTestSubj}-badge`}>
<EuiBadge color="hollow">
<FormattedCount count={count} />
</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{direction === 'column' ? (
<EuiFlexItem
css={css`
margin-top: -${euiTheme.size.base};
`}
>
{barComponent}
</EuiFlexItem>
) : (
<EuiFlexItem>{barComponent}</EuiFlexItem>
)}
</EuiFlexGroup>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,87 @@ import { render } from '@testing-library/react';
import { TestProviders } from '../../../../common/mock';
import { MisconfigurationsInsight } from './misconfiguration_insight';
import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
import { DocumentDetailsContext } from '../context';
import { mockFlyoutApi } from '../mocks/mock_flyout_context';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { mockContextValue } from '../mocks/mock_context';
import { HostPreviewPanelKey } from '../../../entity_details/host_right';
import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview';
import { UserPreviewPanelKey } from '../../../entity_details/user_right';
import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview';

jest.mock('@kbn/expandable-flyout');
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');

const fieldName = 'host.name';
const name = 'test host';
const hostName = 'test host';
const userName = 'test user';
const testId = 'test';

const renderMisconfigurationsInsight = () => {
const renderMisconfigurationsInsight = (fieldName: 'host.name' | 'user.name', value: string) => {
return render(
<TestProviders>
<MisconfigurationsInsight name={name} fieldName={fieldName} data-test-subj={testId} />
<DocumentDetailsContext.Provider value={mockContextValue}>
<MisconfigurationsInsight name={value} fieldName={fieldName} data-test-subj={testId} />
</DocumentDetailsContext.Provider>
</TestProviders>
);
};

describe('MisconfigurationsInsight', () => {
beforeEach(() => {
jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi);
});

it('renders', () => {
(useMisconfigurationPreview as jest.Mock).mockReturnValue({
data: { count: { passed: 1, failed: 2 } },
});
const { getByTestId } = renderMisconfigurationsInsight();
const { getByTestId } = renderMisconfigurationsInsight('host.name', hostName);
expect(getByTestId(testId)).toBeInTheDocument();
expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument();
});

it('renders null if no misconfiguration data found', () => {
(useMisconfigurationPreview as jest.Mock).mockReturnValue({});
const { container } = renderMisconfigurationsInsight();
const { container } = renderMisconfigurationsInsight('host.name', hostName);
expect(container).toBeEmptyDOMElement();
});

describe('should open entity flyout when clicking on badge', () => {
it('should open host entity flyout when clicking on host badge', () => {
(useMisconfigurationPreview as jest.Mock).mockReturnValue({
data: { count: { passed: 1, failed: 2 } },
});
const { getByTestId } = renderMisconfigurationsInsight('host.name', hostName);
expect(getByTestId(`${testId}-count`)).toHaveTextContent('3');

getByTestId(`${testId}-count`).click();
expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({
id: HostPreviewPanelKey,
params: {
hostName,
banner: HOST_PREVIEW_BANNER,
scopeId: mockContextValue.scopeId,
},
});
});

it('should open user entity flyout when clicking on user badge', () => {
(useMisconfigurationPreview as jest.Mock).mockReturnValue({
data: { count: { passed: 2, failed: 3 } },
});
const { getByTestId } = renderMisconfigurationsInsight('user.name', userName);
expect(getByTestId(`${testId}-count`)).toHaveTextContent('5');

getByTestId(`${testId}-count`).click();
expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({
id: UserPreviewPanelKey,
params: {
userName,
banner: USER_PREVIEW_BANNER,
scopeId: mockContextValue.scopeId,
},
});
});
});
});
Loading