[Security Solution] Use new expandable flyout in alert preview (#167902)

## Summary

This PR updates the alert preview in Create rule -> Rule preview to use
the new expandable alert flyout:

- Switched timeline wrapper to be visible on create rule page. This
allows us to keep all the timeline navigation in the new expandable
alert flyout
- Disabled alert specific components, when flyout is open in create
rule:
   - Alert status is not shown
   - Rule summary preview is disabled
   - Title link to rule details page is removed
   - Exclude filter in/filter out hover actions in highlighted fields
- New placeholder text for investigation guide and response: we should
not show link to documentation when user is setting up a rule

With feature flag on:


a45e930e-f1e8-4899-aef4-1aa0c3dc3330



**How to test**
- Add `xpack.securitySolution.enableExperimental:
['expandableFlyoutInCreateRuleEnabled' ]` to `kibana.yml.dev`
- Go to Rules page -> Detection rules (SIEM) => Create rule
- Pick a rule type and populate the query, click `Continue`
- On the right hand side, click `Refresh`, some alerts should appear in
the table
- Click expand on a row

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [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

---------

Co-authored-by: Nikita Indik <nikita.indik@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
christineweng 2023-11-27 10:04:32 -06:00 committed by GitHub
parent 192519d01f
commit f5648d9585
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 325 additions and 94 deletions

View file

@ -70,8 +70,14 @@ export const allowedExperimentalValues = Object.freeze({
* Enables top charts on Alerts Page
*/
alertsPageChartsEnabled: true,
/**
* Enables the alert type column in KPI visualizations on Alerts Page
*/
alertTypeEnabled: false,
/**
* Enables expandable flyout in create rule page, alert preview
*/
expandableFlyoutInCreateRuleEnabled: false,
/*
* Enables new Set of filters on the Alerts page.
*

View file

@ -24,6 +24,7 @@ import { getMappedNonEcsValue } from '../../../../timelines/components/timeline/
import type { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy';
import type { ColumnHeaderOptions, OnRowSelected } from '../../../../../common/types/timeline';
import { TimelineId } from '../../../../../common/types';
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
type Props = EuiDataGridCellValueElementProps & {
columnHeaders: ColumnHeaderOptions[];
@ -73,6 +74,9 @@ const RowActionComponent = ({
const dispatch = useDispatch();
const [isSecurityFlyoutEnabled] = useUiSetting$<boolean>(ENABLE_EXPANDABLE_FLYOUT_SETTING);
const isExpandableFlyoutInCreateRuleEnabled = useIsExperimentalFeatureEnabled(
'expandableFlyoutInCreateRuleEnabled'
);
const columnValues = useMemo(
() =>
@ -89,6 +93,13 @@ const RowActionComponent = ({
[columnHeaders, timelineNonEcsData]
);
let showExpandableFlyout: boolean;
if (tableId === TableId.rulePreview) {
showExpandableFlyout = isSecurityFlyoutEnabled && isExpandableFlyoutInCreateRuleEnabled;
} else {
showExpandableFlyout = isSecurityFlyoutEnabled;
}
const handleOnEventDetailPanelOpened = useCallback(() => {
const updatedExpandedDetail: ExpandedDetailType = {
panelView: 'eventDetail',
@ -98,9 +109,7 @@ const RowActionComponent = ({
},
};
// TODO remove when https://github.com/elastic/security-team/issues/7760 is merged
// excluding rule preview page as some sections in new flyout are not applicable when user is creating a new rule
if (isSecurityFlyoutEnabled && tableId !== TableId.rulePreview) {
if (showExpandableFlyout) {
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
@ -133,7 +142,7 @@ const RowActionComponent = ({
})
);
}
}, [dispatch, eventId, indexName, isSecurityFlyoutEnabled, openFlyout, tabType, tableId]);
}, [dispatch, eventId, indexName, openFlyout, tabType, tableId, showExpandableFlyout]);
const Action = controlColumn.rowCellRender;

View file

@ -83,7 +83,7 @@ describe('use show timeline', () => {
});
it('hides timeline for blacklist routes', async () => {
mockUseLocation.mockReturnValueOnce({ pathname: '/rules/create' });
mockUseLocation.mockReturnValueOnce({ pathname: '/rules/add_rules' });
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useShowTimeline());
await waitForNextUpdate();

View file

@ -18,6 +18,7 @@ jest.mock('../../shared/hooks/use_investigation_guide');
const NO_DATA_TEXT =
"There's no investigation guide for this rule. Edit the rule's settingsExternal link(opens in a new tab or window) to add one.";
const PREVIEW_MESSAGE = 'Investigation guide is not available in alert preview.';
const renderInvestigationGuide = (context: LeftPanelContext = mockContextValue) => (
<TestProviders>
@ -76,4 +77,15 @@ describe('<InvestigationGuide />', () => {
const { getByTestId } = render(renderInvestigationGuide());
expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toHaveTextContent(NO_DATA_TEXT);
});
it('should render preview message when flyout is in preview', () => {
(useInvestigationGuide as jest.Mock).mockReturnValue({
loading: false,
error: true,
});
const { getByTestId } = render(
renderInvestigationGuide({ ...mockContextValue, isPreview: true })
);
expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toHaveTextContent(PREVIEW_MESSAGE);
});
});

View file

@ -18,7 +18,7 @@ import { FlyoutLoading } from '../../../shared/components/flyout_loading';
* Renders a message saying the guide hasn't been set up or the full investigation guide.
*/
export const InvestigationGuide: React.FC = () => {
const { dataFormattedForFieldBrowser } = useLeftPanelContext();
const { dataFormattedForFieldBrowser, isPreview } = useLeftPanelContext();
const { loading, error, basicAlertData, ruleNote } = useInvestigationGuide({
dataFormattedForFieldBrowser,
@ -26,7 +26,12 @@ export const InvestigationGuide: React.FC = () => {
return (
<div data-test-subj={INVESTIGATION_GUIDE_TEST_ID}>
{loading ? (
{isPreview ? (
<FormattedMessage
id="xpack.securitySolution.flyout.left.investigation.previewMessage"
defaultMessage="Investigation guide is not available in alert preview."
/>
) : loading ? (
<FlyoutLoading data-test-subj={INVESTIGATION_GUIDE_LOADING_TEST_ID} />
) : !error && basicAlertData.ruleId && ruleNote ? (
<InvestigationGuideView

View file

@ -61,6 +61,7 @@ jest.mock('../../../../common/lib/kibana', () => {
const NO_DATA_MESSAGE =
"There are no response actions defined for this event. To add some, edit the rule's settings and set up response actionsExternal link(opens in a new tab or window).";
const PREVIEW_MESSAGE = 'Response is not available in alert preview.';
const defaultContextValue = {
dataAsNestedObject: {
@ -139,4 +140,10 @@ describe('<ResponseDetails />', () => {
expect(wrapper.getByTestId(RESPONSE_DETAILS_TEST_ID)).toHaveTextContent(NO_DATA_MESSAGE);
});
it('should render preview message if flyout is in preview', () => {
const wrapper = renderResponseDetails({ ...defaultContextValue, isPreview: true });
expect(wrapper.getByTestId(RESPONSE_DETAILS_TEST_ID)).toBeInTheDocument();
expect(wrapper.getByTestId(RESPONSE_DETAILS_TEST_ID)).toHaveTextContent(PREVIEW_MESSAGE);
});
});

View file

@ -24,7 +24,7 @@ const ExtendedFlyoutWrapper = styled.div`
* Automated response actions results, displayed in the document details expandable flyout left section under the Insights tab, Response tab
*/
export const ResponseDetails: React.FC = () => {
const { searchHit, dataAsNestedObject } = useLeftPanelContext();
const { searchHit, dataAsNestedObject, isPreview } = useLeftPanelContext();
const endpointResponseActionsEnabled = useIsExperimentalFeatureEnabled(
'endpointResponseActionsEnabled'
);
@ -40,19 +40,28 @@ export const ResponseDetails: React.FC = () => {
return (
<div data-test-subj={RESPONSE_DETAILS_TEST_ID}>
<EuiTitle size="xxxs">
<h5>
<FormattedMessage
id="xpack.securitySolution.flyout.left.response.responseTitle"
defaultMessage="Responses"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
{isPreview ? (
<FormattedMessage
id="xpack.securitySolution.flyout.left.response.previewMessage"
defaultMessage="Response is not available in alert preview."
/>
) : (
<>
<EuiTitle size="xxxs">
<h5>
<FormattedMessage
id="xpack.securitySolution.flyout.left.response.responseTitle"
defaultMessage="Responses"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<ExtendedFlyoutWrapper>
{endpointResponseActionsEnabled ? responseActionsView?.content : osqueryView?.content}
</ExtendedFlyoutWrapper>
<ExtendedFlyoutWrapper>
{endpointResponseActionsEnabled ? responseActionsView?.content : osqueryView?.content}
</ExtendedFlyoutWrapper>
</>
)}
</div>
);
};

View file

@ -8,6 +8,7 @@
import type { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
import React, { createContext, memo, useContext, useMemo } from 'react';
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import { TableId } from '@kbn/securitysolution-data-table';
import { useEventDetails } from '../shared/hooks/use_event_details';
import { FlyoutError } from '../../shared/components/flyout_error';
import { FlyoutLoading } from '../../shared/components/flyout_loading';
@ -54,6 +55,10 @@ export interface LeftPanelContext {
* Retrieves searchHit values for the provided field
*/
getFieldsData: GetFieldsData;
/**
* Boolean to indicate whether it is a preview flyout
*/
isPreview: boolean;
}
export const LeftPanelContext = createContext<LeftPanelContext | undefined>(undefined);
@ -97,6 +102,7 @@ export const LeftPanelProvider = memo(
searchHit,
investigationFields: maybeRule?.investigation_fields?.field_names ?? [],
getFieldsData,
isPreview: scopeId === TableId.rulePreview,
}
: undefined,
[

View file

@ -25,4 +25,5 @@ export const mockContextValue: LeftPanelContext = {
searchHit: mockSearchHit,
dataAsNestedObject: mockDataAsNestedObject,
investigationFields: [],
isPreview: false,
};

View file

@ -53,10 +53,10 @@ const panelContextValue = {
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
};
const renderAnalyzerPreview = () =>
const renderAnalyzerPreview = (context = panelContextValue) =>
render(
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<RightPanelContext.Provider value={context}>
<AnalyzerPreviewContainer />
</RightPanelContext.Provider>
</TestProviders>
@ -117,7 +117,7 @@ describe('AnalyzerPreviewContainer', () => {
).toHaveTextContent(NO_ANALYZER_MESSAGE);
});
it('should navigate to left section Visualize tab when clicking on title', () => {
it('should navigate to analyzer in timeline when clicking on title', () => {
(isInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true);
(useAlertPrevalenceFromProcessTree as jest.Mock).mockReturnValue({
loading: false,
@ -136,4 +136,24 @@ describe('AnalyzerPreviewContainer', () => {
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID)).click();
expect(investigateInTimelineAlertClick).toHaveBeenCalled();
});
it('should not navigate to analyzer when in preview and clicking on title', () => {
(isInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true);
(useAlertPrevalenceFromProcessTree as jest.Mock).mockReturnValue({
loading: false,
error: false,
alertIds: ['alertid'],
statsNodes: mock.mockStatsNodes,
});
(useInvestigateInTimeline as jest.Mock).mockReturnValue({
investigateInTimelineAlertClick: jest.fn(),
});
const { queryByTestId } = renderAnalyzerPreview({ ...panelContextValue, isPreview: true });
expect(
queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
).not.toBeInTheDocument();
const { investigateInTimelineAlertClick } = useInvestigateInTimeline({});
expect(investigateInTimelineAlertClick).not.toHaveBeenCalled();
});
});

View file

@ -27,7 +27,7 @@ const timelineId = 'timeline-1';
* Analyzer preview under Overview, Visualizations. It shows a tree representation of analyzer.
*/
export const AnalyzerPreviewContainer: React.FC = () => {
const { dataAsNestedObject } = useRightPanelContext();
const { dataAsNestedObject, isPreview } = useRightPanelContext();
// decide whether to show the analyzer preview or not
const isEnabled = isInvestigateInResolverActionEnabled(dataAsNestedObject);
@ -64,17 +64,18 @@ export const AnalyzerPreviewContainer: React.FC = () => {
/>
),
iconType: 'timeline',
...(isEnabled && {
link: {
callback: goToAnalyzerTab,
tooltip: (
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.analyzerPreview.analyzerPreviewTooltip"
defaultMessage="Show analyzer graph"
/>
),
},
}),
...(isEnabled &&
!isPreview && {
link: {
callback: goToAnalyzerTab,
tooltip: (
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.analyzerPreview.analyzerPreviewTooltip"
defaultMessage="Show analyzer graph"
/>
),
},
}),
}}
data-test-subj={ANALYZER_PREVIEW_TEST_ID}
>

View file

@ -111,6 +111,15 @@ describe('<Description />', () => {
expect(getByTestId(RULE_SUMMARY_BUTTON_TEST_ID)).toHaveAttribute('disabled');
});
it('should render rule preview button as disabled if flyout is in preview', () => {
const { getByTestId } = renderDescription({
...panelContextValue([{ ...ruleUuid, values: [] }, ruleName, ruleDescription]),
isPreview: true,
});
expect(getByTestId(RULE_SUMMARY_BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RULE_SUMMARY_BUTTON_TEST_ID)).toHaveAttribute('disabled');
});
it('should open preview panel when clicking on button', () => {
const panelContext = panelContextValue([ruleUuid, ruleDescription, ruleName]);

View file

@ -31,7 +31,8 @@ import {
* If the document is an alert we show the rule description. If the document is of another type, we show -.
*/
export const Description: FC = () => {
const { dataFormattedForFieldBrowser, scopeId, eventId, indexName } = useRightPanelContext();
const { dataFormattedForFieldBrowser, scopeId, eventId, indexName, isPreview } =
useRightPanelContext();
const { isAlert, ruleDescription, ruleName, ruleId } = useBasicDataFromDetailsData(
dataFormattedForFieldBrowser
);
@ -75,7 +76,7 @@ export const Description: FC = () => {
defaultMessage: 'Show rule summary',
}
)}
disabled={isEmpty(ruleName) || isEmpty(ruleId)}
disabled={isEmpty(ruleName) || isEmpty(ruleId) || isPreview}
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.about.description.ruleSummaryButtonLabel"
@ -84,7 +85,7 @@ export const Description: FC = () => {
</EuiButtonEmpty>
</EuiFlexItem>
),
[ruleName, openRulePreview, ruleId]
[ruleName, openRulePreview, ruleId, isPreview]
);
const alertRuleDescription =

View file

@ -13,6 +13,7 @@ import {
RISK_SCORE_VALUE_TEST_ID,
SEVERITY_VALUE_TEST_ID,
FLYOUT_HEADER_TITLE_TEST_ID,
STATUS_BUTTON_TEST_ID,
} from './test_ids';
import { HeaderTitle } from './header_title';
import moment from 'moment-timezone';
@ -56,6 +57,7 @@ describe('<HeaderTitle />', () => {
expect(getByTestId(FLYOUT_HEADER_TITLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RISK_SCORE_VALUE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(SEVERITY_VALUE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(STATUS_BUTTON_TEST_ID)).toBeInTheDocument();
});
it('should render rule name in the title if document is an alert', () => {
@ -82,4 +84,32 @@ describe('<HeaderTitle />', () => {
expect(getByTestId(FLYOUT_HEADER_TITLE_TEST_ID)).toHaveTextContent('Event details');
});
it('should not render document status if document is not an alert', () => {
const contextValue = {
...mockContextValue,
dataFormattedForFieldBrowser: [
{
category: 'kibana',
field: 'kibana.alert.rule.name',
values: [],
originalValue: [],
isObjectArray: false,
},
],
} as unknown as RightPanelContext;
const { queryByTestId } = renderHeader(contextValue);
expect(queryByTestId(STATUS_BUTTON_TEST_ID)).not.toBeInTheDocument();
});
it('should not render document status if flyout is open in preview', () => {
const contextValue = {
...mockContextValue,
isPreview: true,
} as unknown as RightPanelContext;
const { queryByTestId } = renderHeader(contextValue);
expect(queryByTestId(STATUS_BUTTON_TEST_ID)).not.toBeInTheDocument();
});
});

View file

@ -25,33 +25,40 @@ import { FlyoutTitle } from '../../../shared/components/flyout_title';
* Document details flyout right section header
*/
export const HeaderTitle: FC = memo(() => {
const { dataFormattedForFieldBrowser, eventId, scopeId } = useRightPanelContext();
const { dataFormattedForFieldBrowser, eventId, scopeId, isPreview } = useRightPanelContext();
const { isAlert, ruleName, timestamp, ruleId } = useBasicDataFromDetailsData(
dataFormattedForFieldBrowser
);
const ruleTitle = useMemo(
() => (
<RenderRuleName
contextId={scopeId}
eventId={eventId}
fieldName={SIGNAL_RULE_NAME_FIELD_NAME}
fieldType={'string'}
isAggregatable={false}
isDraggable={false}
linkValue={ruleId}
value={ruleName}
openInNewTab
>
() =>
isPreview ? (
<FlyoutTitle
title={ruleName}
iconType={'warning'}
isLink
data-test-subj={FLYOUT_HEADER_TITLE_TEST_ID}
/>
</RenderRuleName>
),
[ruleName, ruleId, eventId, scopeId]
) : (
<RenderRuleName
contextId={scopeId}
eventId={eventId}
fieldName={SIGNAL_RULE_NAME_FIELD_NAME}
fieldType={'string'}
isAggregatable={false}
isDraggable={false}
linkValue={ruleId}
value={ruleName}
openInNewTab
>
<FlyoutTitle
title={ruleName}
iconType={'warning'}
isLink
data-test-subj={FLYOUT_HEADER_TITLE_TEST_ID}
/>
</RenderRuleName>
),
[ruleName, ruleId, eventId, scopeId, isPreview]
);
const eventTitle = (
@ -76,9 +83,11 @@ export const HeaderTitle: FC = memo(() => {
</div>
<EuiSpacer size="m" />
<EuiFlexGroup direction="row" gutterSize="m" responsive={false}>
<EuiFlexItem grow={false}>
<DocumentStatus />
</EuiFlexItem>
{isAlert && !isPreview && (
<EuiFlexItem grow={false}>
<DocumentStatus />
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<RiskScore />
</EuiFlexItem>

View file

@ -15,6 +15,7 @@ import { convertHighlightedFieldsToTableRow } from '../../shared/utils/highlight
import { useRuleWithFallback } from '../../../../detection_engine/rule_management/logic/use_rule_with_fallback';
import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers';
import { HighlightedFieldsCell } from './highlighted_fields_cell';
import { SecurityCellActionType } from '../../../../actions/constants';
import {
CellActionsMode,
SecurityCellActions,
@ -42,6 +43,10 @@ export interface HighlightedFieldsTableRow {
* Maintain backwards compatibility // TODO remove when possible
*/
scopeId: string;
/**
* Boolean to indicate this field is shown in a preview
*/
isPreview: boolean;
};
}
@ -71,6 +76,7 @@ const columns: Array<EuiBasicTableColumn<HighlightedFieldsTableRow>> = [
field: string;
values: string[] | null | undefined;
scopeId: string;
isPreview: boolean;
}) => (
<SecurityCellActions
data={{
@ -82,6 +88,11 @@ const columns: Array<EuiBasicTableColumn<HighlightedFieldsTableRow>> = [
visibleCellActions={6}
sourcererScopeId={getSourcererScopeId(description.scopeId)}
metadata={{ scopeId: description.scopeId }}
disabledActionTypes={
description.isPreview
? [SecurityCellActionType.FILTER, SecurityCellActionType.TOGGLE_COLUMN]
: []
}
>
<HighlightedFieldsCell values={description.values} field={description.field} />
</SecurityCellActions>
@ -93,7 +104,7 @@ const columns: Array<EuiBasicTableColumn<HighlightedFieldsTableRow>> = [
* Component that displays the highlighted fields in the right panel under the Investigation section.
*/
export const HighlightedFields: FC = () => {
const { dataFormattedForFieldBrowser, scopeId } = useRightPanelContext();
const { dataFormattedForFieldBrowser, scopeId, isPreview } = useRightPanelContext();
const { ruleId } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
const { loading, rule: maybeRule } = useRuleWithFallback(ruleId);
@ -102,8 +113,8 @@ export const HighlightedFields: FC = () => {
investigationFields: maybeRule?.investigation_fields?.field_names ?? [],
});
const items = useMemo(
() => convertHighlightedFieldsToTableRow(highlightedFields, scopeId),
[highlightedFields, scopeId]
() => convertHighlightedFieldsToTableRow(highlightedFields, scopeId, isPreview),
[highlightedFields, scopeId, isPreview]
);
return (

View file

@ -24,6 +24,7 @@ import { LeftPanelInvestigationTab, DocumentDetailsLeftPanelKey } from '../../le
jest.mock('../../shared/hooks/use_investigation_guide');
const NO_DATA_MESSAGE = 'Investigation guideTheres no investigation guide for this rule.';
const PREVIEW_MESSAGE = 'Investigation guide is not available in alert preview.';
const renderInvestigationGuide = () =>
render(
@ -97,6 +98,21 @@ describe('<InvestigationGuide />', () => {
expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toHaveTextContent(NO_DATA_MESSAGE);
});
it('should render preview message when flyout is in preview', () => {
const { queryByTestId, getByTestId } = render(
<IntlProvider locale="en">
<ExpandableFlyoutContext.Provider value={mockFlyoutContextValue}>
<RightPanelContext.Provider value={{ ...mockContextValue, isPreview: true }}>
<InvestigationGuide />
</RightPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
</IntlProvider>
);
expect(queryByTestId(INVESTIGATION_GUIDE_BUTTON_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toHaveTextContent(PREVIEW_MESSAGE);
});
it('should navigate to investigation guide when clicking on button', () => {
(useInvestigationGuide as jest.Mock).mockReturnValue({
loading: false,

View file

@ -24,7 +24,8 @@ import {
*/
export const InvestigationGuide: React.FC = () => {
const { openLeftPanel } = useExpandableFlyoutContext();
const { eventId, indexName, scopeId, dataFormattedForFieldBrowser } = useRightPanelContext();
const { eventId, indexName, scopeId, dataFormattedForFieldBrowser, isPreview } =
useRightPanelContext();
const { loading, error, basicAlertData, ruleNote } = useInvestigationGuide({
dataFormattedForFieldBrowser,
@ -56,7 +57,12 @@ export const InvestigationGuide: React.FC = () => {
</h5>
</EuiTitle>
</EuiFlexItem>
{loading ? (
{isPreview ? (
<FormattedMessage
id="xpack.securitySolution.flyout.right.investigation.investigationGuide.previewMessage"
defaultMessage="Investigation guide is not available in alert preview."
/>
) : loading ? (
<EuiSkeletonText
data-test-subj={INVESTIGATION_GUIDE_LOADING_TEST_ID}
contentAriaLabel={i18n.translate(

View file

@ -13,6 +13,8 @@ import { RightPanelContext } from '../context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { ResponseSection } from './response_section';
const PREVIEW_MESSAGE = 'Response is not available in alert preview.';
const flyoutContextValue = {} as unknown as ExpandableFlyoutContext;
const panelContextValue = {} as unknown as RightPanelContext;
@ -47,4 +49,18 @@ describe('<ResponseSection />', () => {
getByTestId(RESPONSE_SECTION_HEADER_TEST_ID).click();
expect(getByTestId(RESPONSE_SECTION_CONTENT_TEST_ID)).toBeInTheDocument();
});
it('should render preview message if flyout is in preview', () => {
const { getByTestId } = render(
<IntlProvider locale="en">
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<RightPanelContext.Provider value={{ ...panelContextValue, isPreview: true }}>
<ResponseSection />
</RightPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
</IntlProvider>
);
getByTestId(RESPONSE_SECTION_HEADER_TEST_ID).click();
expect(getByTestId(RESPONSE_SECTION_CONTENT_TEST_ID)).toHaveTextContent(PREVIEW_MESSAGE);
});
});

View file

@ -10,6 +10,7 @@ import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { ResponseButton } from './response_button';
import { ExpandableSection } from './expandable_section';
import { useRightPanelContext } from '../context';
import { RESPONSE_SECTION_TEST_ID } from './test_ids';
export interface ResponseSectionProps {
/**
@ -22,6 +23,7 @@ export interface ResponseSectionProps {
* Most bottom section of the overview tab. It contains a summary of the response tab.
*/
export const ResponseSection: VFC<ResponseSectionProps> = ({ expanded = false }) => {
const { isPreview } = useRightPanelContext();
return (
<ExpandableSection
expanded={expanded}
@ -33,7 +35,14 @@ export const ResponseSection: VFC<ResponseSectionProps> = ({ expanded = false })
}
data-test-subj={RESPONSE_SECTION_TEST_ID}
>
<ResponseButton />
{isPreview ? (
<FormattedMessage
id="xpack.securitySolution.flyout.right.response.previewMessage"
defaultMessage="Response is not available in alert preview."
/>
) : (
<ResponseButton />
)}
</ExpandableSection>
);
};

View file

@ -37,7 +37,7 @@ const ValueContainer: FC<{ text?: ReactElement }> = ({ text, children }) => (
* Renders session preview under Visualizations section in the flyout right EuiPanel
*/
export const SessionPreview: FC = () => {
const { eventId, scopeId } = useRightPanelContext();
const { eventId, scopeId, isPreview } = useRightPanelContext();
const { processName, userName, startAt, ruleName, ruleId, workdir, command } = useProcessData();
const { euiTheme } = useEuiTheme();
@ -100,13 +100,13 @@ export const SessionPreview: FC = () => {
fieldType={'string'}
isAggregatable={false}
isDraggable={false}
linkValue={ruleId}
linkValue={!isPreview ? ruleId : null}
value={ruleName}
/>
</ValueContainer>
)
);
}, [ruleName, ruleId, scopeId, eventId]);
}, [ruleName, ruleId, scopeId, eventId, isPreview]);
const commandFragment = useMemo(() => {
return (

View file

@ -40,10 +40,10 @@ const sessionViewConfig = {
sessionStartTime: 'sessionStartTime',
};
const renderSessionPreview = () =>
const renderSessionPreview = (context = panelContextValue) =>
render(
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<RightPanelContext.Provider value={context}>
<SessionPreviewContainer />
</RightPanelContext.Provider>
</TestProviders>
@ -121,4 +121,31 @@ describe('SessionPreviewContainer', () => {
).not.toHaveTextContent(NO_DATA_MESSAGE);
expect(queryByTestId(SESSION_PREVIEW_TEST_ID)).not.toBeInTheDocument();
});
it('should not render link to session viewer if flyout is open in preview', () => {
(useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
const { getByTestId, queryByTestId } = renderSessionPreview({
...panelContextValue,
isPreview: true,
});
expect(getByTestId(SESSION_PREVIEW_TEST_ID)).toBeInTheDocument();
expect(
queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(SESSION_PREVIEW_TEST_ID))
).not.toBeInTheDocument();
expect(
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(SESSION_PREVIEW_TEST_ID))
).toBeInTheDocument();
expect(
getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(SESSION_PREVIEW_TEST_ID))
).toBeInTheDocument();
expect(
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(SESSION_PREVIEW_TEST_ID))
).toBeInTheDocument();
expect(
queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(SESSION_PREVIEW_TEST_ID))
).not.toBeInTheDocument();
});
});

View file

@ -29,7 +29,7 @@ const timelineId = 'timeline-1';
* Checks if the SessionView component is available, if so render it or else render an error message
*/
export const SessionPreviewContainer: FC = () => {
const { dataAsNestedObject, getFieldsData } = useRightPanelContext();
const { dataAsNestedObject, getFieldsData, isPreview } = useRightPanelContext();
// decide whether to show the session view or not
const sessionViewConfig = useSessionPreview({ getFieldsData });
@ -122,17 +122,18 @@ export const SessionPreviewContainer: FC = () => {
/>
),
iconType: 'timeline',
...(isEnabled && {
link: {
callback: goToSessionViewTab,
tooltip: (
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.sessionPreview.sessionPreviewTooltip"
defaultMessage="Show session viewer"
/>
),
},
}),
...(isEnabled &&
!isPreview && {
link: {
callback: goToSessionViewTab,
tooltip: (
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.sessionPreview.sessionPreviewTooltip"
defaultMessage="Show session viewer"
/>
),
},
}),
}}
data-test-subj={SESSION_PREVIEW_TEST_ID}
>

View file

@ -8,6 +8,7 @@
import type { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
import React, { createContext, memo, useContext, useMemo } from 'react';
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import { TableId } from '@kbn/securitysolution-data-table';
import { useEventDetails } from '../shared/hooks/use_event_details';
import { FlyoutError } from '../../shared/components/flyout_error';
@ -59,6 +60,10 @@ export interface RightPanelContext {
* Retrieves searchHit values for the provided field
*/
getFieldsData: GetFieldsData;
/**
* Boolean to indicate whether it is a preview flyout
*/
isPreview: boolean;
}
export const RightPanelContext = createContext<RightPanelContext | undefined>(undefined);
@ -104,6 +109,7 @@ export const RightPanelProvider = memo(
investigationFields: maybeRule?.investigation_fields?.field_names ?? [],
refetchFlyoutData,
getFieldsData,
isPreview: scopeId === TableId.rulePreview,
}
: undefined,
[

View file

@ -15,10 +15,17 @@ import { useHostIsolationTools } from '../../../timelines/components/side_panel/
import { DEFAULT_DARK_MODE } from '../../../../common/constants';
import { useUiSetting } from '../../../common/lib/kibana';
interface PanelFooterProps {
/**
* Boolean that indicates whether flyout is in preview and action should be hidden
*/
isPreview: boolean;
}
/**
*
*/
export const PanelFooter: FC = () => {
export const PanelFooter: FC<PanelFooterProps> = ({ isPreview }) => {
const { closeFlyout, openRightPanel } = useExpandableFlyoutContext();
const {
eventId,
@ -48,7 +55,7 @@ export const PanelFooter: FC = () => {
[eventId, indexName, openRightPanel, scopeId, showHostIsolationPanel]
);
return (
return !isPreview ? (
<EuiPanel
hasShadow={false}
borderRadius="none"
@ -68,5 +75,5 @@ export const PanelFooter: FC = () => {
refetchFlyoutData={refetchFlyoutData}
/>
</EuiPanel>
);
) : null;
};

View file

@ -37,7 +37,7 @@ export interface RightPanelProps extends FlyoutPanelProps {
*/
export const RightPanel: FC<Partial<RightPanelProps>> = memo(({ path }) => {
const { openRightPanel } = useExpandableFlyoutContext();
const { eventId, getFieldsData, indexName, scopeId } = useRightPanelContext();
const { eventId, getFieldsData, indexName, scopeId, isPreview } = useRightPanelContext();
// for 8.10, we only render the flyout in its expandable mode if the document viewed is of type signal
const documentIsSignal = getField(getFieldsData('event.kind')) === EventKind.signal;
@ -72,7 +72,7 @@ export const RightPanel: FC<Partial<RightPanelProps>> = memo(({ path }) => {
setSelectedTabId={setSelectedTabId}
/>
<PanelContent tabs={tabsDisplayed} selectedTabId={selectedTabId} />
<PanelFooter />
<PanelFooter isPreview={isPreview} />
</>
);
});

View file

@ -26,4 +26,5 @@ export const mockContextValue: RightPanelContext = {
searchHit: mockSearchHit,
investigationFields: [],
refetchFlyoutData: jest.fn(),
isPreview: false,
};

View file

@ -23,7 +23,7 @@ const FLYOUT_FOOTER_HEIGHT = 72;
* Json view displayed in the document details expandable flyout right section
*/
export const JsonTab: FC = memo(() => {
const { searchHit } = useRightPanelContext();
const { searchHit, isPreview } = useRightPanelContext();
const jsonValue = JSON.stringify(searchHit, null, 2);
const flexGroupElement = useRef<HTMLDivElement>(null);
@ -31,19 +31,20 @@ export const JsonTab: FC = memo(() => {
useEffect(() => {
const topPosition = flexGroupElement?.current?.getBoundingClientRect().top || 0;
const footerOffset = isPreview ? 0 : FLYOUT_FOOTER_HEIGHT;
const height =
window.innerHeight -
topPosition -
COPY_TO_CLIPBOARD_BUTTON_HEIGHT -
FLYOUT_BODY_PADDING -
FLYOUT_FOOTER_HEIGHT;
footerOffset;
if (height === 0) {
return;
}
setEditorHeight(height);
}, [setEditorHeight]);
}, [setEditorHeight, isPreview]);
return (
<EuiFlexGroup

View file

@ -11,6 +11,7 @@ import {
} from './highlighted_fields_helpers';
const scopeId = 'scopeId';
const isPreview = false;
describe('convertHighlightedFieldsToTableRow', () => {
it('should convert highlighted fields to a table row', () => {
@ -19,13 +20,14 @@ describe('convertHighlightedFieldsToTableRow', () => {
values: ['host-1'],
},
};
expect(convertHighlightedFieldsToTableRow(highlightedFields, scopeId)).toEqual([
expect(convertHighlightedFieldsToTableRow(highlightedFields, scopeId, isPreview)).toEqual([
{
field: 'host.name',
description: {
field: 'host.name',
values: ['host-1'],
scopeId: 'scopeId',
isPreview,
},
},
]);
@ -38,13 +40,14 @@ describe('convertHighlightedFieldsToTableRow', () => {
values: ['host-1'],
},
};
expect(convertHighlightedFieldsToTableRow(highlightedFields, scopeId)).toEqual([
expect(convertHighlightedFieldsToTableRow(highlightedFields, scopeId, isPreview)).toEqual([
{
field: 'host.name-override',
description: {
field: 'host.name-override',
values: ['host-1'],
scopeId: 'scopeId',
isPreview,
},
},
]);

View file

@ -16,7 +16,8 @@ import type { HighlightedFieldsTableRow } from '../../right/components/highlight
*/
export const convertHighlightedFieldsToTableRow = (
highlightedFields: UseHighlightedFieldsResult,
scopeId: string
scopeId: string,
isPreview: boolean
): HighlightedFieldsTableRow[] => {
const fieldNames = Object.keys(highlightedFields);
return fieldNames.map((fieldName) => {
@ -30,6 +31,7 @@ export const convertHighlightedFieldsToTableRow = (
field,
values,
scopeId,
isPreview,
},
};
});

View file

@ -65,7 +65,7 @@ export const links: LinkItem = {
title: CREATE_NEW_RULE,
path: RULES_CREATE_PATH,
skipUrlState: true,
hideTimeline: true,
hideTimeline: false,
},
],
},