[Security Solution] expandable flyout - add status to flyout header (#161942)

This commit is contained in:
Philippe Oberti 2023-07-18 09:23:13 +02:00 committed by GitHub
parent f7faa8217b
commit 68b8ac3fef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 241 additions and 10 deletions

View file

@ -43,6 +43,7 @@ import {
DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE_VALUE,
DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY,
DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY_VALUE,
DOCUMENT_DETAILS_FLYOUT_HEADER_STATUS,
DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE,
DOCUMENT_DETAILS_FLYOUT_JSON_TAB,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB,
@ -85,6 +86,8 @@ describe(
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_CHAT_BUTTON).should('be.visible');
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_STATUS).should('be.visible');
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE).should('be.visible');
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE_VALUE)
.should('be.visible')

View file

@ -20,6 +20,7 @@ import {
FLYOUT_HEADER_RISK_SCORE_VALUE_TEST_ID,
FLYOUT_HEADER_SEVERITY_TITLE_TEST_ID,
FLYOUT_HEADER_SEVERITY_VALUE_TEST_ID,
FLYOUT_HEADER_STATUS_BUTTON_TEST_ID,
FLYOUT_HEADER_TITLE_TEST_ID,
} from '../../../public/flyout/right/components/test_ids';
@ -42,6 +43,9 @@ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB =
getDataTestSubjectSelector(OVERVIEW_TAB_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB = getDataTestSubjectSelector(TABLE_TAB_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_JSON_TAB = getDataTestSubjectSelector(JSON_TAB_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_HEADER_STATUS = getDataTestSubjectSelector(
FLYOUT_HEADER_STATUS_BUTTON_TEST_ID
);
export const DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE = getDataTestSubjectSelector(
FLYOUT_HEADER_RISK_SCORE_TITLE_TEST_ID
);

View file

@ -91,6 +91,7 @@ exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = `
<button
aria-label="Click to change alert status"
class="euiBadge c3 emotion-euiBadge-primary-clickable"
data-test-subj="rule-status-badge"
>
<span
class="euiBadge__content emotion-euiBadge__content"

View file

@ -7,6 +7,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { RightPanelContext } from '../context';
import {
FLYOUT_HEADER_CHAT_BUTTON_TEST_ID,
@ -21,7 +22,7 @@ import moment from 'moment-timezone';
import { useDateFormat, useTimeZone } from '../../../common/lib/kibana';
import { mockDataFormattedForFieldBrowser, mockGetFieldsData } from '../mocks/mock_context';
import { useAssistant } from '../hooks/use_assistant';
import { MockAssistantProvider } from '../../../common/mock/mock_assistant_provider';
import { TestProvidersComponent } from '../../../common/mock';
jest.mock('../../../common/lib/kibana');
jest.mock('../hooks/use_assistant');
@ -31,13 +32,17 @@ moment.tz.setDefault('UTC');
const dateFormat = 'MMM D, YYYY @ HH:mm:ss.SSS';
const flyoutContextValue = {} as unknown as ExpandableFlyoutContext;
const renderHeader = (contextValue: RightPanelContext) =>
render(
<MockAssistantProvider>
<RightPanelContext.Provider value={contextValue}>
<HeaderTitle />
</RightPanelContext.Provider>
</MockAssistantProvider>
<TestProvidersComponent>
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<RightPanelContext.Provider value={contextValue}>
<HeaderTitle />
</RightPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
</TestProvidersComponent>
);
describe('<HeaderTitle />', () => {

View file

@ -10,6 +10,7 @@ import React, { memo } from 'react';
import { NewChatById } from '@kbn/elastic-assistant';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
import { isEmpty } from 'lodash';
import { DocumentStatus } from './status';
import { useAssistant } from '../hooks/use_assistant';
import {
ALERT_SUMMARY_CONVERSATION_ID,
@ -67,10 +68,17 @@ export const HeaderTitle: FC = memo(() => {
</EuiFlexItem>
</EuiFlexGroup>
</EuiTitle>
<EuiSpacer size="m" />
{timestamp && <PreferenceFormattedDate value={new Date(timestamp)} />}
<EuiSpacer size="m" />
<EuiFlexGroup direction="row" gutterSize="l">
<EuiSpacer size="xs" />
<EuiFlexGroup direction="row" gutterSize="m">
<EuiFlexItem grow={false}>
<DocumentStatus />
</EuiFlexItem>
<EuiFlexItem grow={false}>
{timestamp && <PreferenceFormattedDate value={new Date(timestamp)} />}
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
<EuiFlexGroup direction="row" gutterSize="m">
<EuiFlexItem grow={false}>
<DocumentSeverity />
</EuiFlexItem>

View file

@ -0,0 +1,59 @@
/*
* 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 type { Story } from '@storybook/react';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { StorybookProviders } from '../../../common/mock/storybook_providers';
import { DocumentStatus } from './status';
import { RightPanelContext } from '../context';
import { mockBrowserFields, mockDataFormattedForFieldBrowser } from '../mocks/mock_context';
export default {
component: DocumentStatus,
title: 'Flyout/Status',
};
const flyoutContextValue = {
closeFlyout: () => {},
} as unknown as ExpandableFlyoutContext;
export const Default: Story<void> = () => {
const contextValue = {
eventId: 'eventId',
browserFields: mockBrowserFields,
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
scopeId: 'alerts-page',
} as unknown as RightPanelContext;
return (
<StorybookProviders>
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<RightPanelContext.Provider value={contextValue}>
<DocumentStatus />
</RightPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
</StorybookProviders>
);
};
export const Empty: Story<void> = () => {
const contextValue = {
eventId: 'eventId',
browserFields: {},
dataFormattedForFieldBrowser: [],
scopeId: 'scopeId',
} as unknown as RightPanelContext;
return (
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<RightPanelContext.Provider value={contextValue}>
<DocumentStatus />
</RightPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
);
};

View file

@ -0,0 +1,75 @@
/*
* 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 { render } from '@testing-library/react';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { RightPanelContext } from '../context';
import { DocumentStatus } from './status';
import { mockDataFormattedForFieldBrowser } from '../mocks/mock_context';
import { TestProviders } from '../../../common/mock';
import { useAlertsActions } from '../../../detections/components/alerts_table/timeline_actions/use_alerts_actions';
import { FLYOUT_HEADER_STATUS_BUTTON_TEST_ID } from './test_ids';
jest.mock('../../../detections/components/alerts_table/timeline_actions/use_alerts_actions');
const flyoutContextValue = {
closeFlyout: jest.fn(),
} as unknown as ExpandableFlyoutContext;
const renderStatus = (contextValue: RightPanelContext) =>
render(
<TestProviders>
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<RightPanelContext.Provider value={contextValue}>
<DocumentStatus />
</RightPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
</TestProviders>
);
const actionItem = {
key: 'key',
name: 'name',
'data-test-subj': 'data-test-subj',
};
(useAlertsActions as jest.Mock).mockReturnValue({
actionItems: [actionItem],
});
describe('<DocumentStatus />', () => {
it('should render status information', () => {
const contextValue = {
eventId: 'eventId',
browserFields: {},
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
scopeId: 'scopeId',
} as unknown as RightPanelContext;
const { getByTestId, getByText } = renderStatus(contextValue);
expect(getByTestId(FLYOUT_HEADER_STATUS_BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByText('open')).toBeInTheDocument();
getByTestId(FLYOUT_HEADER_STATUS_BUTTON_TEST_ID).click();
expect(getByTestId('data-test-subj')).toBeInTheDocument();
});
it('should render empty component', () => {
const contextValue = {
eventId: 'eventId',
browserFields: {},
dataFormattedForFieldBrowser: [],
scopeId: 'scopeId',
} as unknown as RightPanelContext;
const { container } = renderStatus(contextValue);
expect(container).toBeEmptyDOMElement();
});
});

View file

@ -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 { FC } from 'react';
import React, { useMemo } from 'react';
import { find } from 'lodash/fp';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import type {
EnrichedFieldInfo,
EnrichedFieldInfoWithValues,
} from '../../../common/components/event_details/types';
import { SIGNAL_STATUS_FIELD_NAME } from '../../../timelines/components/timeline/body/renderers/constants';
import { StatusPopoverButton } from '../../../common/components/event_details/overview/status_popover_button';
import { useRightPanelContext } from '../context';
import { getEnrichedFieldInfo } from '../../../common/components/event_details/helpers';
/**
* Checks if the field info has data to convert EnrichedFieldInfo into EnrichedFieldInfoWithValues
*/
function hasData(fieldInfo?: EnrichedFieldInfo): fieldInfo is EnrichedFieldInfoWithValues {
return !!fieldInfo && Array.isArray(fieldInfo.values);
}
/**
* Document details status displayed in flyout right section header
*/
export const DocumentStatus: FC = () => {
const { closeFlyout } = useExpandableFlyoutContext();
const { eventId, browserFields, dataFormattedForFieldBrowser, scopeId } = useRightPanelContext();
const statusData = useMemo(() => {
const item = find(
{ field: SIGNAL_STATUS_FIELD_NAME, category: 'kibana' },
dataFormattedForFieldBrowser
);
return (
item &&
getEnrichedFieldInfo({
eventId,
contextId: scopeId,
scopeId,
browserFields: browserFields || {},
item,
})
);
}, [browserFields, dataFormattedForFieldBrowser, eventId, scopeId]);
if (!statusData || !hasData(statusData)) return null;
return (
<StatusPopoverButton
eventId={eventId}
contextId={scopeId}
enrichedFieldInfo={statusData}
scopeId={scopeId}
handleOnEventClosed={closeFlyout}
/>
);
};
DocumentStatus.displayName = 'DocumentStatus';

View file

@ -14,6 +14,7 @@ export const EXPAND_DETAILS_BUTTON_TEST_ID =
'securitySolutionDocumentDetailsFlyoutHeaderExpandDetailButton';
export const COLLAPSE_DETAILS_BUTTON_TEST_ID =
'securitySolutionDocumentDetailsFlyoutHeaderCollapseDetailButton';
export const FLYOUT_HEADER_STATUS_BUTTON_TEST_ID = 'rule-status-badge';
export const FLYOUT_HEADER_SEVERITY_TITLE_TEST_ID =
'securitySolutionAlertDetailsFlyoutHeaderSeverityTitle';
export const FLYOUT_HEADER_SEVERITY_VALUE_TEST_ID = 'severity';

View file

@ -31,6 +31,13 @@ export const SEVERITY_TITLE = i18n.translate(
}
);
export const STATUS_TITLE = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.statusTitle',
{
defaultMessage: 'Status',
}
);
export const RISK_SCORE_TITLE = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.riskScoreTitle',
{

View file

@ -77,6 +77,8 @@ export const RightPanelProvider = ({
children,
}: RightPanelProviderProps) => {
const currentSpaceId = useSpaceId();
// TODO Replace getAlertIndexAlias way to retrieving the eventIndex with the GET /_alias
// https://github.com/elastic/kibana/issues/113063
const eventIndex = indexName ? getAlertIndexAlias(indexName, currentSpaceId) ?? indexName : '';
const [{ pageName }] = useRouteSpy();
const sourcererScope =

View file

@ -57,6 +57,7 @@ const RuleStatusComponent: React.FC<Props> = ({
onClickAriaLabel={onClickAriaLabel}
iconType={iconType}
iconSide={iconSide}
data-test-subj="rule-status-badge"
>
{value}
</StyledEuiBadge>