Skip to content

Commit

Permalink
[Security Solution][Alert details] - refactor UI on insights (#197349)
Browse files Browse the repository at this point in the history
## Summary

This PR refactors the Insights section of the expandable flyout for
alerts and events.

The changes are applied to the following section:
- Threat Intelligence: when the user clicks on the number, we open the
left section to the Insights Threat Intelligence tab
- Correlations: when the user clicks on the number, we open the left
section to the Insights Correlations tab
- Prevalence: no user interactions

When in preview mode, none of the number are clickable and the buttons
are disabled.

#### New UI

| Normal flyout  | Preview flyout |
| ------------- | ------------- |
| ![Screenshot 2024-10-22 at 6 01
38 PM](https://github.com/user-attachments/assets/de179a2b-c8ab-42f6-b5b7-839dae0139d5)
| ![Screenshot 2024-10-22 at 6 01
54 PM](https://github.com/user-attachments/assets/63ed125e-5e3b-4c4c-a10e-7cc01d291660)
|

#### UX flows to expand the flyout


https://github.com/user-attachments/assets/30031a12-c2f3-47e6-a783-5b9482359ee5

elastic/security-team#7033

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
  • Loading branch information
PhilippeOberti authored Oct 30, 2024
1 parent fa0f397 commit 9b9eb7e
Show file tree
Hide file tree
Showing 25 changed files with 806 additions and 512 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {
CORRELATIONS_RELATED_CASES_TEST_ID,
CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID,
CORRELATIONS_TEST_ID,
SUMMARY_ROW_VALUE_TEST_ID,
SUMMARY_ROW_BUTTON_TEST_ID,
SUMMARY_ROW_TEXT_TEST_ID,
} from './test_ids';
import { useShowRelatedAlertsByAncestry } from '../../shared/hooks/use_show_related_alerts_by_ancestry';
import { useShowRelatedAlertsBySameSourceEvent } from '../../shared/hooks/use_show_related_alerts_by_same_source_event';
Expand Down Expand Up @@ -58,17 +59,32 @@ const TITLE_LINK_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(CORRELATIO
const TITLE_ICON_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(CORRELATIONS_TEST_ID);
const TITLE_TEXT_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(CORRELATIONS_TEST_ID);

const SUPPRESSED_ALERTS_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID);
const RELATED_ALERTS_BY_ANCESTRY_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(
const SUPPRESSED_ALERTS_TEXT_TEST_ID = SUMMARY_ROW_TEXT_TEST_ID(
CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID
);
const SUPPRESSED_ALERTS_VALUE_TEST_ID = SUMMARY_ROW_BUTTON_TEST_ID(
CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID
);
const RELATED_ALERTS_BY_ANCESTRY_TEXT_TEST_ID = SUMMARY_ROW_TEXT_TEST_ID(
CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID
);
const RELATED_ALERTS_BY_ANCESTRY_VALUE_TEST_ID = SUMMARY_ROW_BUTTON_TEST_ID(
CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID
);
const RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(
const RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEXT_TEST_ID = SUMMARY_ROW_TEXT_TEST_ID(
CORRELATIONS_RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID
);
const RELATED_ALERTS_BY_SESSION_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(
const RELATED_ALERTS_BY_SAME_SOURCE_EVENT_VALUE_TEST_ID = SUMMARY_ROW_BUTTON_TEST_ID(
CORRELATIONS_RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID
);
const RELATED_ALERTS_BY_SESSION_TEXT_TEST_ID = SUMMARY_ROW_TEXT_TEST_ID(
CORRELATIONS_RELATED_ALERTS_BY_SESSION_TEST_ID
);
const RELATED_ALERTS_BY_SESSION_VALUE_TEST_ID = SUMMARY_ROW_BUTTON_TEST_ID(
CORRELATIONS_RELATED_ALERTS_BY_SESSION_TEST_ID
);
const RELATED_CASES_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(CORRELATIONS_RELATED_CASES_TEST_ID);
const RELATED_CASES_TEXT_TEST_ID = SUMMARY_ROW_TEXT_TEST_ID(CORRELATIONS_RELATED_CASES_TEST_ID);
const RELATED_CASES_VALUE_TEST_ID = SUMMARY_ROW_BUTTON_TEST_ID(CORRELATIONS_RELATED_CASES_TEST_ID);

const panelContextValue = {
eventId: 'event id',
Expand Down Expand Up @@ -193,11 +209,16 @@ describe('<CorrelationsOverview />', () => {
});

const { getByTestId, queryByText } = render(renderCorrelationsOverview(panelContextValue));
expect(getByTestId(RELATED_ALERTS_BY_ANCESTRY_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RELATED_ALERTS_BY_SESSION_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RELATED_CASES_TEST_ID)).toBeInTheDocument();
expect(getByTestId(SUPPRESSED_ALERTS_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RELATED_ALERTS_BY_ANCESTRY_TEXT_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RELATED_ALERTS_BY_ANCESTRY_VALUE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEXT_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_VALUE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RELATED_ALERTS_BY_SESSION_TEXT_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RELATED_ALERTS_BY_SESSION_VALUE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RELATED_CASES_TEXT_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RELATED_CASES_VALUE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(SUPPRESSED_ALERTS_TEXT_TEST_ID)).toBeInTheDocument();
expect(getByTestId(SUPPRESSED_ALERTS_VALUE_TEST_ID)).toBeInTheDocument();
expect(queryByText(NO_DATA_MESSAGE)).not.toBeInTheDocument();
});

Expand All @@ -215,11 +236,18 @@ describe('<CorrelationsOverview />', () => {
jest.mocked(useShowSuppressedAlerts).mockReturnValue({ show: false, alertSuppressionCount: 0 });

const { getByText, queryByTestId } = render(renderCorrelationsOverview(panelContextValue));
expect(queryByTestId(RELATED_ALERTS_BY_ANCESTRY_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_ALERTS_BY_SESSION_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_CASES_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(SUPPRESSED_ALERTS_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_ALERTS_BY_ANCESTRY_TEXT_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_ALERTS_BY_ANCESTRY_VALUE_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEXT_TEST_ID)).not.toBeInTheDocument();
expect(
queryByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_VALUE_TEST_ID)
).not.toBeInTheDocument();
expect(queryByTestId(RELATED_ALERTS_BY_SESSION_TEXT_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_ALERTS_BY_SESSION_VALUE_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_CASES_TEXT_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_CASES_VALUE_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(SUPPRESSED_ALERTS_TEXT_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(SUPPRESSED_ALERTS_VALUE_TEST_ID)).not.toBeInTheDocument();
expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export const CorrelationsOverview: React.FC = () => {
data-test-subj={CORRELATIONS_TEST_ID}
>
{canShowAtLeastOneInsight ? (
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexGroup direction="column" gutterSize="s">
{showSuppressedAlerts && (
<SuppressedAlerts alertSuppressionCount={alertSuppressionCount} ruleType={ruleType} />
)}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,74 +9,147 @@ import React from 'react';
import { render } from '@testing-library/react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { InsightsSummaryRow } from './insights_summary_row';
import { useDocumentDetailsContext } from '../../shared/context';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys';
import { LeftPanelInsightsTab } from '../../left';

jest.mock('@kbn/expandable-flyout');
jest.mock('../../shared/context');

const mockOpenLeftPanel = jest.fn();
const scopeId = 'scopeId';
const eventId = 'eventId';
const indexName = 'indexName';

const testId = 'test';
const iconTestId = `${testId}Icon`;
const textTestId = `${testId}Text`;
const buttonTestId = `${testId}Button`;
const valueTestId = `${testId}Value`;
const colorTestId = `${testId}Color`;
const loadingTestId = `${testId}Loading`;

describe('<InsightsSummaryRow />', () => {
it('should render by default', () => {
beforeEach(() => {
jest.clearAllMocks();

(useDocumentDetailsContext as jest.Mock).mockReturnValue({
eventId,
indexName,
scopeId,
isPreviewMode: false,
});
(useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel });
});

it('should render loading skeleton if loading is true', () => {
const { getByTestId } = render(
<InsightsSummaryRow
loading={true}
text={'text'}
value={<div>{'value for this'}</div>}
data-test-subj={testId}
/>
);

expect(getByTestId(loadingTestId)).toBeInTheDocument();
});

it('should only render null when error is true', () => {
const { container } = render(
<InsightsSummaryRow error={true} text={'text'} value={<div>{'value for this'}</div>} />
);

expect(container).toBeEmptyDOMElement();
});

it('should render the value component', () => {
const { getByTestId, queryByTestId } = render(
<IntlProvider locale="en">
<InsightsSummaryRow
icon={'image'}
value={1}
text={'this is a test for red'}
color={'rgb(189,39,30)'}
value={<div>{'value for this'}</div>}
data-test-subj={testId}
/>
</IntlProvider>
);

expect(getByTestId(iconTestId)).toBeInTheDocument();
expect(getByTestId(valueTestId)).toHaveTextContent('1 this is a test for red');
expect(getByTestId(colorTestId)).toBeInTheDocument();
expect(getByTestId(textTestId)).toHaveTextContent('this is a test for red');
expect(getByTestId(valueTestId)).toHaveTextContent('value for this');
expect(queryByTestId(buttonTestId)).not.toBeInTheDocument();
});

it('should render loading skeletton if loading is true', () => {
const { getByTestId } = render(
<InsightsSummaryRow loading={true} icon={'image'} text={'text'} data-test-subj={testId} />
it('should render the value as EuiBadge and EuiButtonEmpty', () => {
const { getByTestId, queryByTestId } = render(
<IntlProvider locale="en">
<InsightsSummaryRow text={'this is a test for red'} value={2} data-test-subj={testId} />
</IntlProvider>
);

expect(getByTestId(loadingTestId)).toBeInTheDocument();
expect(getByTestId(textTestId)).toHaveTextContent('this is a test for red');
expect(getByTestId(buttonTestId)).toHaveTextContent('2');
expect(queryByTestId(valueTestId)).not.toBeInTheDocument();
});

it('should only render null when error is true', () => {
const { container } = render(<InsightsSummaryRow error={true} icon={'image'} text={'text'} />);
it('should render big numbers formatted correctly', () => {
const { getByTestId } = render(
<IntlProvider locale="en">
<InsightsSummaryRow text={'this is a test for red'} value={2000} data-test-subj={testId} />
</IntlProvider>
);

expect(container).toBeEmptyDOMElement();
expect(getByTestId(buttonTestId)).toHaveTextContent('2k');
});

it('should handle big number in a compact notation', () => {
it('should open the expanded section to the correct tab when the number is clicked', () => {
const { getByTestId } = render(
<IntlProvider locale="en">
<InsightsSummaryRow
icon={'image'}
value={160000}
text={'this is a test for red'}
color={'rgb(189,39,30)'}
value={2}
expandedSubTab={'subTab'}
data-test-subj={testId}
/>
</IntlProvider>
);
getByTestId(buttonTestId).click();

expect(getByTestId(valueTestId)).toHaveTextContent('160k this is a test for red');
expect(mockOpenLeftPanel).toHaveBeenCalledWith({
id: DocumentDetailsLeftPanelKey,
path: {
tab: LeftPanelInsightsTab,
subTab: 'subTab',
},
params: {
id: eventId,
indexName,
scopeId,
},
});
});

it(`should not show the colored dot if color isn't provided`, () => {
const { queryByTestId } = render(
it('should disabled the click when in preview mode', () => {
(useDocumentDetailsContext as jest.Mock).mockReturnValue({
eventId,
indexName,
scopeId,
isPreviewMode: true,
});

const { getByTestId } = render(
<IntlProvider locale="en">
<InsightsSummaryRow
icon={'image'}
value={160000}
text={'this is a test for no color'}
text={'this is a test for red'}
value={2}
expandedSubTab={'subTab'}
data-test-subj={testId}
/>
</IntlProvider>
);
const button = getByTestId(buttonTestId);

expect(button).toHaveAttribute('disabled');

expect(queryByTestId(colorTestId)).not.toBeInTheDocument();
button.click();
expect(mockOpenLeftPanel).not.toHaveBeenCalled();
});
});
Loading

0 comments on commit 9b9eb7e

Please sign in to comment.