[Security Solution] Expandable flyout - add rule preview skeleton (#161999)

## Summary

This PR adds a rule preview panel to the expandable flyout:

- Preview panel skeleton is added, now we can open a preview on top of
right section of flyout
- Go to rule details button is replaced by a button that will open a
rule preview panel
- The rule preview contains placeholder sections (About, Definition,
Schedule) and footer with a link to rule details page


![image](5510982a-91e5-4747-ae23-a8b5b87e0041)

**How to test**

- add `xpack.securitySolution.enableExperimental:
['securityFlyoutEnabled']` to the `kibana.dev.json` file
- go to the Alerts page, and click on the expand detail button on any
row of the table
- click on `Overview`, `About`, then `Rule summary`

### 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: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
christineweng 2023-07-27 15:02:13 -05:00 committed by GitHub
parent a409f80444
commit a1be0029c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 886 additions and 87 deletions

View file

@ -11,19 +11,55 @@ import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiText,
useEuiTheme,
EuiSplitPanel,
} from '@elastic/eui';
import React from 'react';
import { css } from '@emotion/react';
import { has } from 'lodash';
import {
PREVIEW_SECTION,
PREVIEW_SECTION_BACK_BUTTON,
PREVIEW_SECTION_CLOSE_BUTTON,
PREVIEW_SECTION_HEADER,
} from './test_ids';
import { useExpandableFlyoutContext } from '../..';
import { BACK_BUTTON, CLOSE_BUTTON } from './translations';
export interface PreviewBanner {
/**
* Optional title to be shown
*/
title?: string;
/**
* Optional string for background color
*/
backgroundColor?:
| 'primary'
| 'plain'
| 'warning'
| 'accent'
| 'success'
| 'danger'
| 'transparent'
| 'subdued';
/**
* Optional string for text color
*/
textColor?: string;
}
/**
* Type guard to check the passed object is of preview banner type
* @param banner passed from panel params
* @returns a boolean to indicate whether the banner passed is a preview banner
*/
export const isPreviewBanner = (banner: unknown): banner is PreviewBanner => {
return has(banner, 'title') || has(banner, 'backgroundColor') || has(banner, 'textColor');
};
interface PreviewSectionProps {
/**
* Component to be rendered
@ -37,6 +73,10 @@ interface PreviewSectionProps {
* Display the back button in the header
*/
showBackButton: boolean;
/**
* Preview banner shown at the top of preview panel
*/
banner?: PreviewBanner;
}
/**
@ -47,6 +87,7 @@ export const PreviewSection: React.FC<PreviewSectionProps> = ({
component,
showBackButton,
width,
banner,
}: PreviewSectionProps) => {
const { euiTheme } = useEuiTheme();
const { closePreviewPanel, previousPreviewPanel } = useExpandableFlyoutContext();
@ -95,8 +136,10 @@ export const PreviewSection: React.FC<PreviewSectionProps> = ({
opacity: 0.5;
`}
/>
<div
<EuiSplitPanel.Outer
css={css`
margin: ${euiTheme.size.xs};
height: 99%;
position: absolute;
top: 0;
bottom: 0;
@ -104,18 +147,21 @@ export const PreviewSection: React.FC<PreviewSectionProps> = ({
left: ${left};
z-index: 1000;
`}
className="eui-yScroll"
data-test-subj={PREVIEW_SECTION}
>
<EuiPanel
css={css`
margin: ${euiTheme.size.xs};
height: 100%;
`}
data-test-subj={PREVIEW_SECTION}
>
{isPreviewBanner(banner) && (
<EuiSplitPanel.Inner grow={false} color={banner.backgroundColor} paddingSize="none">
<EuiText textAlign="center" color={banner.textColor} size="s">
{banner.title}
</EuiText>
</EuiSplitPanel.Inner>
)}
<EuiSplitPanel.Inner grow={false} paddingSize="s" data-test-subj={PREVIEW_SECTION_HEADER}>
{header}
{component}
</EuiPanel>
</div>
</EuiSplitPanel.Inner>
<EuiSplitPanel.Inner paddingSize="none">{component}</EuiSplitPanel.Inner>
</EuiSplitPanel.Outer>
</>
);
};

View file

@ -15,3 +15,5 @@ export const PREVIEW_SECTION = 'previewSection';
export const PREVIEW_SECTION_CLOSE_BUTTON = 'previewSectionCloseButton';
export const PREVIEW_SECTION_BACK_BUTTON = 'previewSectionBackButton';
export const PREVIEW_SECTION_HEADER = 'previewSectionHeader';

View file

@ -15,6 +15,7 @@ import { PreviewSection } from './components/preview_section';
import { RightSection } from './components/right_section';
import type { FlyoutPanelProps, Panel } from './types';
import { LeftSection } from './components/left_section';
import { isPreviewBanner } from './components/preview_section';
export interface ExpandableFlyoutProps extends Omit<EuiFlyoutProps, 'onClose'> {
/**
@ -65,6 +66,10 @@ export const ExpandableFlyout: React.FC<ExpandableFlyoutProps> = ({
// retrieve the last preview panel (most recent)
const mostRecentPreview = preview ? preview[preview.length - 1] : undefined;
const previewBanner = isPreviewBanner(mostRecentPreview?.params?.banner)
? mostRecentPreview?.params?.banner
: undefined;
const showBackButton = preview && preview.length > 1;
const previewSection = useMemo(
() => registeredPanels.find((panel) => panel.key === mostRecentPreview?.id),
@ -115,6 +120,7 @@ export const ExpandableFlyout: React.FC<ExpandableFlyoutProps> = ({
component={previewSection.component({ ...(mostRecentPreview as FlyoutPanelProps) })}
showBackButton={showBackButton}
width={previewSectionWidth}
banner={previewBanner}
/>
) : null}
</EuiFlyout>

View file

@ -0,0 +1,90 @@
/*
* 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 { expandFirstAlertExpandableFlyout } from '../../../../tasks/expandable_flyout/common';
import {
DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SECTION,
DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_HEADER,
DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_BODY,
DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_HEADER,
DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_CONTENT,
DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_DEFINITION_SECTION_HEADER,
DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_DEFINITION_SECTION_CONTENT,
DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SCHEDULE_SECTION_HEADER,
DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SCHEDULE_SECTION_CONTENT,
DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_FOOTER,
} from '../../../../screens/expandable_flyout/alert_details_preview_panel_rule_preview';
import {
toggleRulePreviewAboutSection,
toggleRulePreviewDefinitionSection,
toggleRulePreviewScheduleSection,
} from '../../../../tasks/expandable_flyout/alert_details_preview_panel_rule_preview';
import { clickRuleSummaryButton } from '../../../../tasks/expandable_flyout/alert_details_right_panel_overview_tab';
import { cleanKibana } from '../../../../tasks/common';
import { login, visit } from '../../../../tasks/login';
import { createRule } from '../../../../tasks/api_calls/rules';
import { getNewRule } from '../../../../objects/rule';
import { ALERTS_URL } from '../../../../urls/navigation';
import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule';
describe(
'Alert details expandable flyout rule preview panel',
{ env: { ftrConfig: { enableExperimental: ['securityFlyoutEnabled'] } } },
() => {
const rule = getNewRule();
beforeEach(() => {
cleanKibana();
login();
createRule(rule);
visit(ALERTS_URL);
waitForAlertsToPopulate();
expandFirstAlertExpandableFlyout();
clickRuleSummaryButton();
});
describe('rule preview', () => {
it('should display rule preview and its sub sections', () => {
cy.log('rule preview panel');
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SECTION).scrollIntoView();
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SECTION).should('be.visible');
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_HEADER).should('be.visible');
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_BODY).should('be.visible');
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_FOOTER).should('be.visible');
cy.log('about');
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_HEADER)
.should('be.visible')
.and('contain.text', 'About');
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_CONTENT).should('be.visible');
toggleRulePreviewAboutSection();
cy.log('definition');
toggleRulePreviewDefinitionSection();
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_DEFINITION_SECTION_HEADER)
.should('be.visible')
.and('contain.text', 'Definition');
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_DEFINITION_SECTION_CONTENT).should(
'be.visible'
);
toggleRulePreviewDefinitionSection();
cy.log('schedule');
toggleRulePreviewScheduleSection();
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SCHEDULE_SECTION_HEADER)
.should('be.visible')
.and('contain.text', 'Schedule');
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SCHEDULE_SECTION_CONTENT).should('be.visible');
toggleRulePreviewScheduleSection();
});
});
}
);

View file

@ -17,7 +17,7 @@ import {
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_DETAILS,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_EXPAND_BUTTON,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_NAVIGATE_TO_RULE_DETAILS_BUTTON,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_DETAILS,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_GO_TO_TABLE_LINK,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_HEADER_TITLE,
@ -98,9 +98,9 @@ describe(
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE)
.should('be.visible')
.within(() => {
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_NAVIGATE_TO_RULE_DETAILS_BUTTON)
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON)
.should('be.visible')
.and('contain.text', 'View rule');
.and('have.text', 'Rule summary');
});
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_DETAILS)
.should('be.visible')

View file

@ -0,0 +1,47 @@
/*
* 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 {
RULE_PREVIEW_BODY_TEST_ID,
RULE_PREVIEW_ABOUT_HEADER_TEST_ID,
RULE_PREVIEW_ABOUT_CONTENT_TEST_ID,
RULE_PREVIEW_DEFINITION_HEADER_TEST_ID,
RULE_PREVIEW_DEFINITION_CONTENT_TEST_ID,
RULE_PREVIEW_SCHEDULE_HEADER_TEST_ID,
RULE_PREVIEW_SCHEDULE_CONTENT_TEST_ID,
RULE_PREVIEW_FOOTER_TEST_ID,
} from '../../../public/flyout/preview/components/test_ids';
import { getDataTestSubjectSelector } from '../../helpers/common';
export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SECTION =
getDataTestSubjectSelector('previewSection');
export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_HEADER =
getDataTestSubjectSelector('previewSectionHeader');
export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_BODY =
getDataTestSubjectSelector(RULE_PREVIEW_BODY_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_HEADER = getDataTestSubjectSelector(
RULE_PREVIEW_ABOUT_HEADER_TEST_ID
);
export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_CONTENT =
getDataTestSubjectSelector(RULE_PREVIEW_ABOUT_CONTENT_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_DEFINITION_SECTION_HEADER =
getDataTestSubjectSelector(RULE_PREVIEW_DEFINITION_HEADER_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_DEFINITION_SECTION_CONTENT =
getDataTestSubjectSelector(RULE_PREVIEW_DEFINITION_CONTENT_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SCHEDULE_SECTION_HEADER =
getDataTestSubjectSelector(RULE_PREVIEW_SCHEDULE_HEADER_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SCHEDULE_SECTION_CONTENT =
getDataTestSubjectSelector(RULE_PREVIEW_SCHEDULE_CONTENT_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_FOOTER = getDataTestSubjectSelector(
RULE_PREVIEW_FOOTER_TEST_ID
);

View file

@ -13,7 +13,7 @@ import {
DESCRIPTION_DETAILS_TEST_ID,
DESCRIPTION_EXPAND_BUTTON_TEST_ID,
DESCRIPTION_TITLE_TEST_ID,
DESCRIPTION_NAVIGATE_TO_RULE_TEST_ID,
RULE_SUMMARY_BUTTON_TEST_ID,
ENTITIES_CONTENT_TEST_ID,
ENTITIES_HEADER_TEST_ID,
ENTITIES_VIEW_ALL_BUTTON_TEST_ID,
@ -58,8 +58,8 @@ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE =
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_DETAILS = getDataTestSubjectSelector(
DESCRIPTION_DETAILS_TEST_ID
);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_NAVIGATE_TO_RULE_DETAILS_BUTTON =
getDataTestSubjectSelector(DESCRIPTION_NAVIGATE_TO_RULE_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON =
getDataTestSubjectSelector(RULE_SUMMARY_BUTTON_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_EXPAND_BUTTON =
getDataTestSubjectSelector(DESCRIPTION_EXPAND_BUTTON_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE =

View file

@ -0,0 +1,44 @@
/*
* 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 {
DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_HEADER,
DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_DEFINITION_SECTION_HEADER,
DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SCHEDULE_SECTION_HEADER,
} from '../../screens/expandable_flyout/alert_details_preview_panel_rule_preview';
/* About section */
/**
* Toggle the About Section in Rule Preview panel
*/
export const toggleRulePreviewAboutSection = () => {
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_HEADER).scrollIntoView();
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_HEADER).should('be.visible').click();
};
/* Definition section */
/**
* Toggle the Definition Section in Rule Preview panel
*/
export const toggleRulePreviewDefinitionSection = () => {
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_DEFINITION_SECTION_HEADER).scrollIntoView();
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_DEFINITION_SECTION_HEADER)
.should('be.visible')
.click();
};
/* Schedule section */
/**
* Toggle the Schedule Section in Rule Preview panel
*/
export const toggleRulePreviewScheduleSection = () => {
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SCHEDULE_SECTION_HEADER).scrollIntoView();
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SCHEDULE_SECTION_HEADER).should('be.visible').click();
};

View file

@ -15,6 +15,8 @@ import {
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_VIEW_ALL_BUTTON,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INVESTIGATION_GUIDE_BUTTON,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON,
} from '../../screens/expandable_flyout/alert_details_right_panel_overview_tab';
/* About section */
@ -114,3 +116,16 @@ export const clickInvestigationGuideButton = () => {
.should('be.visible')
.click();
};
/**
* Click `Rule summary` button to open rule preview panel
*/
export const clickRuleSummaryButton = () => {
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE)
.should('be.visible')
.within(() => {
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON)
.should('be.visible')
.click();
});
};

View file

@ -21,6 +21,9 @@ import {
SecuritySolutionFlyoutUrlSyncProvider,
useSecurityFlyoutUrlSync,
} from './shared/context/url_sync';
import type { PreviewPanelProps } from './preview';
import { PreviewPanel, PreviewPanelKey } from './preview';
import { PreviewPanelProvider } from './preview/context';
/**
* List of all panels that will be used within the document details expandable flyout.
@ -43,6 +46,14 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels']
</LeftPanelProvider>
),
},
{
key: PreviewPanelKey,
component: (props) => (
<PreviewPanelProvider {...(props as PreviewPanelProps).params}>
<PreviewPanel path={props.path as PreviewPanelProps['path']} />
</PreviewPanelProvider>
),
},
];
const OuterProviders: FC = ({ children }) => {

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 React from 'react';
import { render } from '@testing-library/react';
import { RulePreview } from './rule_preview';
import { PreviewPanelContext } from '../context';
import { mockContextValue } from '../mocks/mock_preview_panel_context';
import { mockFlyoutContextValue } from '../../shared/mocks/mock_flyout_context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback';
import {
RULE_PREVIEW_BODY_TEST_ID,
RULE_PREVIEW_ABOUT_HEADER_TEST_ID,
RULE_PREVIEW_ABOUT_CONTENT_TEST_ID,
RULE_PREVIEW_DEFINITION_HEADER_TEST_ID,
RULE_PREVIEW_DEFINITION_CONTENT_TEST_ID,
RULE_PREVIEW_SCHEDULE_HEADER_TEST_ID,
RULE_PREVIEW_SCHEDULE_CONTENT_TEST_ID,
} from './test_ids';
const mockUseRuleWithFallback = useRuleWithFallback as jest.Mock;
jest.mock('../../../detection_engine/rule_management/logic/use_rule_with_fallback');
const contextValue = {
...mockContextValue,
ruleId: 'rule id',
};
describe('<RulePreview />', () => {
it('should render rule preview and its sub sections', () => {
mockUseRuleWithFallback.mockReturnValue({
rule: { name: 'rule name', description: 'rule description' },
});
const { getByTestId } = render(
<ExpandableFlyoutContext.Provider value={mockFlyoutContextValue}>
<PreviewPanelContext.Provider value={contextValue}>
<RulePreview />
</PreviewPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
);
expect(getByTestId(RULE_PREVIEW_BODY_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RULE_PREVIEW_ABOUT_HEADER_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RULE_PREVIEW_ABOUT_CONTENT_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RULE_PREVIEW_DEFINITION_HEADER_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RULE_PREVIEW_DEFINITION_CONTENT_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RULE_PREVIEW_SCHEDULE_HEADER_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RULE_PREVIEW_SCHEDULE_CONTENT_TEST_ID)).toBeInTheDocument();
});
it('should not render rule preview when rule is null', () => {
mockUseRuleWithFallback.mockReturnValue({});
const { queryByTestId } = render(
<ExpandableFlyoutContext.Provider value={mockFlyoutContextValue}>
<PreviewPanelContext.Provider value={contextValue}>
<RulePreview />
</PreviewPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
);
expect(queryByTestId(RULE_PREVIEW_BODY_TEST_ID)).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,88 @@
/*
* 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, { memo, useState, useEffect } from 'react';
import {
EuiTitle,
EuiText,
EuiHorizontalRule,
EuiSpacer,
EuiPanel,
EuiLoadingSpinner,
} from '@elastic/eui';
import { usePreviewPanelContext } from '../context';
import { ExpandableSection } from '../../right/components/expandable_section';
import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback';
import type { Rule } from '../../../detection_engine/rule_management/logic';
import {
RULE_PREVIEW_BODY_TEST_ID,
RULE_PREVIEW_ABOUT_TEST_ID,
RULE_PREVIEW_DEFINITION_TEST_ID,
RULE_PREVIEW_SCHEDULE_TEST_ID,
} from './test_ids';
import {
RULE_PREVIEW_ABOUT_TEXT,
RULE_PREVIEW_DEFINITION_TEXT,
RULE_PREVIEW_SCHEDULE_TEXT,
} from './translations';
/**
* Rule summary on a preview panel on top of the right section of expandable flyout
*/
export const RulePreview: React.FC = memo(() => {
const { ruleId } = usePreviewPanelContext();
const [rule, setRule] = useState<Rule | null>(null);
const { rule: maybeRule, loading } = useRuleWithFallback(ruleId ?? '');
// persist rule until refresh is complete
useEffect(() => {
if (maybeRule != null) {
setRule(maybeRule);
}
}, [maybeRule]);
if (loading) {
return <EuiLoadingSpinner />;
}
return rule ? (
<EuiPanel hasShadow={false} data-test-subj={RULE_PREVIEW_BODY_TEST_ID}>
<EuiTitle>
<h6>{rule.name}</h6>
</EuiTitle>
<EuiHorizontalRule />
<ExpandableSection
title={RULE_PREVIEW_ABOUT_TEXT}
expanded
data-test-subj={RULE_PREVIEW_ABOUT_TEST_ID}
>
<EuiText size="s">{rule.description}</EuiText>
<EuiSpacer size="s" />
{'About'}
</ExpandableSection>
<EuiSpacer size="m" />
<ExpandableSection
title={RULE_PREVIEW_DEFINITION_TEXT}
expanded={false}
data-test-subj={RULE_PREVIEW_DEFINITION_TEST_ID}
>
{'Definition'}
</ExpandableSection>
<EuiSpacer size="m" />
<ExpandableSection
title={RULE_PREVIEW_SCHEDULE_TEXT}
expanded={false}
data-test-subj={RULE_PREVIEW_SCHEDULE_TEST_ID}
>
{'Schedule'}
</ExpandableSection>
</EuiPanel>
) : null;
});
RulePreview.displayName = 'RulePreview';

View file

@ -0,0 +1,47 @@
/*
* 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 { render } from '@testing-library/react';
import React from 'react';
import { TestProviders } from '../../../common/mock';
import { mockContextValue } from '../mocks/mock_preview_panel_context';
import { PreviewPanelContext } from '../context';
import { RULE_PREVIEW_FOOTER_TEST_ID, RULE_PREVIEW_NAVIGATE_TO_RULE_TEST_ID } from './test_ids';
import { RulePreviewFooter } from './rule_preview_footer';
const contextValue = {
...mockContextValue,
ruleId: 'rule id',
};
describe('<RulePreviewFooter />', () => {
it('renders rule details link correctly when ruleId is available', () => {
const { getByTestId } = render(
<TestProviders>
<PreviewPanelContext.Provider value={contextValue}>
<RulePreviewFooter />
</PreviewPanelContext.Provider>
</TestProviders>
);
expect(getByTestId(RULE_PREVIEW_FOOTER_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RULE_PREVIEW_NAVIGATE_TO_RULE_TEST_ID)).toBeInTheDocument();
});
it('should not render rule details link when ruleId is not available', () => {
const { queryByTestId } = render(
<TestProviders>
<PreviewPanelContext.Provider value={mockContextValue}>
<RulePreviewFooter />
</PreviewPanelContext.Provider>
</TestProviders>
);
expect(queryByTestId(RULE_PREVIEW_FOOTER_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RULE_PREVIEW_NAVIGATE_TO_RULE_TEST_ID)).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,43 @@
/*
* 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, { memo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiFlyoutFooter } from '@elastic/eui';
import { usePreviewPanelContext } from '../context';
import { RenderRuleName } from '../../../timelines/components/timeline/body/renderers/formatted_field_helpers';
import { SHOW_RULE_DETAILS } from './translations';
import { SIGNAL_RULE_NAME_FIELD_NAME } from '../../../timelines/components/timeline/body/renderers/constants';
import { RULE_PREVIEW_FOOTER_TEST_ID } from './test_ids';
/**
* Footer in rule preview panel
*/
export const RulePreviewFooter: React.FC = memo(() => {
const { scopeId, eventId, ruleId } = usePreviewPanelContext();
return ruleId ? (
<EuiFlyoutFooter data-test-subj={RULE_PREVIEW_FOOTER_TEST_ID}>
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<RenderRuleName
contextId={scopeId}
eventId={eventId}
fieldName={SIGNAL_RULE_NAME_FIELD_NAME}
fieldType={'string'}
isAggregatable={false}
isDraggable={false}
linkValue={ruleId}
value={SHOW_RULE_DETAILS}
openInNewTab
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
) : null;
});
RulePreviewFooter.displayName = 'RulePreviewFooter';

View file

@ -0,0 +1,28 @@
/*
* 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 { CONTENT_TEST_ID, HEADER_TEST_ID } from '../../right/components/expandable_section';
/* Rule preview */
export const RULE_PREVIEW_BODY_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRulePreviewBody';
export const RULE_PREVIEW_ABOUT_TEST_ID = `securitySolutionDocumentDetailsFlyoutRulePreviewAboutSection`;
export const RULE_PREVIEW_ABOUT_HEADER_TEST_ID = RULE_PREVIEW_ABOUT_TEST_ID + HEADER_TEST_ID;
export const RULE_PREVIEW_ABOUT_CONTENT_TEST_ID = RULE_PREVIEW_ABOUT_TEST_ID + CONTENT_TEST_ID;
export const RULE_PREVIEW_DEFINITION_TEST_ID =
'securitySolutionDocumentDetailsFlyoutRulePreviewDefinitionSection';
export const RULE_PREVIEW_DEFINITION_HEADER_TEST_ID =
RULE_PREVIEW_DEFINITION_TEST_ID + HEADER_TEST_ID;
export const RULE_PREVIEW_DEFINITION_CONTENT_TEST_ID =
RULE_PREVIEW_DEFINITION_TEST_ID + CONTENT_TEST_ID;
export const RULE_PREVIEW_SCHEDULE_TEST_ID =
'securitySolutionDocumentDetailsFlyoutRulePreviewScheduleSection';
export const RULE_PREVIEW_SCHEDULE_HEADER_TEST_ID = RULE_PREVIEW_SCHEDULE_TEST_ID + HEADER_TEST_ID;
export const RULE_PREVIEW_SCHEDULE_CONTENT_TEST_ID =
RULE_PREVIEW_SCHEDULE_TEST_ID + CONTENT_TEST_ID;
export const RULE_PREVIEW_FOOTER_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRulePreviewFooter';
export const RULE_PREVIEW_NAVIGATE_TO_RULE_TEST_ID = 'goToRuleDetails';

View file

@ -0,0 +1,28 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const SHOW_RULE_DETAILS = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.viewRuleDetailsText',
{ defaultMessage: 'Show rule details' }
);
export const RULE_PREVIEW_ABOUT_TEXT = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.rulePreviewAboutSectionText',
{ defaultMessage: 'About' }
);
export const RULE_PREVIEW_DEFINITION_TEXT = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.rulePreviewDefinitionSectionText',
{ defaultMessage: 'Definition' }
);
export const RULE_PREVIEW_SCHEDULE_TEXT = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.rulePreviewScheduleSectionText',
{ defaultMessage: 'Schedule' }
);

View file

@ -0,0 +1,72 @@
/*
* 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, { createContext, useContext, useMemo } from 'react';
import type { PreviewPanelProps } from '.';
export interface PreviewPanelContext {
/**
* Id of the document
*/
eventId: string;
/**
* Name of the index used in the parent's page
*/
indexName: string;
/**
* Maintain backwards compatibility // TODO remove when possible
*/
scopeId: string;
/**
* Rule id if preview is rule details
*/
ruleId: string;
}
export const PreviewPanelContext = createContext<PreviewPanelContext | undefined>(undefined);
export type PreviewPanelProviderProps = {
/**
* React components to render
*/
children: React.ReactNode;
} & Partial<PreviewPanelProps['params']>;
export const PreviewPanelProvider = ({
id,
indexName,
scopeId,
ruleId,
children,
}: PreviewPanelProviderProps) => {
const contextValue = useMemo(
() =>
id && indexName && scopeId
? {
eventId: id,
indexName,
scopeId,
ruleId: ruleId ?? '',
}
: undefined,
[id, indexName, scopeId, ruleId]
);
return (
<PreviewPanelContext.Provider value={contextValue}>{children}</PreviewPanelContext.Provider>
);
};
export const usePreviewPanelContext = (): PreviewPanelContext => {
const contextValue = useContext(PreviewPanelContext);
if (!contextValue) {
throw new Error('PreviewPanelContext can only be used within PreviewPanelContext provider');
}
return contextValue;
};

View file

@ -0,0 +1,54 @@
/*
* 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, { memo, useMemo } from 'react';
import { css } from '@emotion/react';
import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { panels } from './panels';
export type PreviewPanelPaths = 'rule-preview';
export const PreviewPanelKey: PreviewPanelProps['key'] = 'document-details-preview';
export interface PreviewPanelProps extends FlyoutPanelProps {
key: 'document-details-preview';
path?: PreviewPanelPaths[];
params?: {
id: string;
indexName: string;
scopeId: string;
banner?: string;
ruleId?: string;
};
}
/**
* Preview panel to be displayed on top of the document details expandable flyout right section
*/
export const PreviewPanel: React.FC<Partial<PreviewPanelProps>> = memo(({ path }) => {
const previewPanel = useMemo(() => {
return path ? panels.find((panel) => panel.id === path[0]) : null;
}, [path]);
if (!previewPanel) {
return null;
}
return (
<EuiFlexGroup justifyContent="spaceBetween" direction="column" className="eui-fullHeight">
<EuiFlexItem
css={css`
margin-top: -15px;
`}
>
{previewPanel.content}
</EuiFlexItem>
<EuiFlexItem grow={false}>{previewPanel.footer}</EuiFlexItem>
</EuiFlexGroup>
);
});
PreviewPanel.displayName = 'PreviewPanel';

View file

@ -0,0 +1,18 @@
/*
* 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 { PreviewPanelContext } from '../context';
/**
* Mock contextValue for right panel context
*/
export const mockContextValue: PreviewPanelContext = {
eventId: 'eventId',
indexName: 'index',
scopeId: 'scopeId',
ruleId: '',
};

View file

@ -0,0 +1,43 @@
/*
* 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 { PreviewPanelPaths } from '.';
import { RULE_PREVIEW } from './translations';
import { RulePreview } from './components/rule_preview';
import { RulePreviewFooter } from './components/rule_preview_footer';
export type PreviewPanelType = Array<{
/**
* Id of the preview panel
*/
id: PreviewPanelPaths;
/**
* Panel name
*/
name: string;
/**
* Main body component to be rendered in the panel
*/
content: React.ReactElement;
/**
* Footer section in the panel
*/
footer: React.ReactElement;
}>;
/**
* Array of all preview panels
*/
export const panels: PreviewPanelType = [
{
id: 'rule-preview',
name: RULE_PREVIEW,
content: <RulePreview />,
footer: <RulePreviewFooter />,
},
];

View file

@ -0,0 +1,13 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const RULE_PREVIEW = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.rulePreviewPanel',
{ defaultMessage: 'Rule preview' }
);

View file

@ -8,19 +8,21 @@
import React from 'react';
import { render } from '@testing-library/react';
import { ABOUT_SECTION_CONTENT_TEST_ID, ABOUT_SECTION_HEADER_TEST_ID } from './test_ids';
import { TestProviders } from '../../../common/mock';
import { AboutSection } from './about_section';
import { RightPanelContext } from '../context';
const panelContextValue = {} as unknown as RightPanelContext;
import { mockContextValue } from '../mocks/mock_right_panel_context';
jest.mock('../../../common/components/link_to');
describe('<AboutSection />', () => {
it('should render the component collapsed', () => {
const { getByTestId } = render(
<RightPanelContext.Provider value={panelContextValue}>
<AboutSection />
</RightPanelContext.Provider>
<TestProviders>
<RightPanelContext.Provider value={mockContextValue}>
<AboutSection />
</RightPanelContext.Provider>
</TestProviders>
);
expect(getByTestId(ABOUT_SECTION_HEADER_TEST_ID)).toBeInTheDocument();
@ -28,9 +30,11 @@ describe('<AboutSection />', () => {
it('should render the component expanded', () => {
const { getByTestId } = render(
<RightPanelContext.Provider value={panelContextValue}>
<AboutSection expanded={true} />
</RightPanelContext.Provider>
<TestProviders>
<RightPanelContext.Provider value={mockContextValue}>
<AboutSection expanded={true} />
</RightPanelContext.Provider>
</TestProviders>
);
expect(getByTestId(ABOUT_SECTION_HEADER_TEST_ID)).toBeInTheDocument();
@ -39,9 +43,11 @@ describe('<AboutSection />', () => {
it('should expand the component when clicking on the arrow on header', () => {
const { getByTestId } = render(
<RightPanelContext.Provider value={panelContextValue}>
<AboutSection />
</RightPanelContext.Provider>
<TestProviders>
<RightPanelContext.Provider value={mockContextValue}>
<AboutSection />
</RightPanelContext.Provider>
</TestProviders>
);
getByTestId(ABOUT_SECTION_HEADER_TEST_ID).click();

View file

@ -10,7 +10,7 @@ import { render } from '@testing-library/react';
import {
DESCRIPTION_EXPAND_BUTTON_TEST_ID,
DESCRIPTION_TITLE_TEST_ID,
DESCRIPTION_NAVIGATE_TO_RULE_TEST_ID,
RULE_SUMMARY_BUTTON_TEST_ID,
} from './test_ids';
import {
DOCUMENT_DESCRIPTION_COLLAPSE_BUTTON,
@ -19,6 +19,7 @@ import {
RULE_DESCRIPTION_TITLE,
} from './translations';
import { Description } from './description';
import { TestProviders } from '../../../common/mock';
import { RightPanelContext } from '../context';
import { ThemeProvider } from 'styled-components';
import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock';
@ -59,16 +60,18 @@ describe('<Description />', () => {
} as unknown as RightPanelContext;
const { getByTestId } = render(
<RightPanelContext.Provider value={panelContextValue}>
<ThemeProvider theme={mockTheme}>
<Description />
</ThemeProvider>
</RightPanelContext.Provider>
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<ThemeProvider theme={mockTheme}>
<Description />
</ThemeProvider>
</RightPanelContext.Provider>
</TestProviders>
);
expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toHaveTextContent(RULE_DESCRIPTION_TITLE);
expect(getByTestId(DESCRIPTION_NAVIGATE_TO_RULE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RULE_SUMMARY_BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(DESCRIPTION_EXPAND_BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(DESCRIPTION_EXPAND_BUTTON_TEST_ID)).toHaveTextContent(
DOCUMENT_DESCRIPTION_EXPAND_BUTTON
@ -81,16 +84,18 @@ describe('<Description />', () => {
} as unknown as RightPanelContext;
const { getByTestId } = render(
<RightPanelContext.Provider value={panelContextValue}>
<ThemeProvider theme={mockTheme}>
<Description expanded={true} />
</ThemeProvider>
</RightPanelContext.Provider>
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<ThemeProvider theme={mockTheme}>
<Description expanded={true} />
</ThemeProvider>
</RightPanelContext.Provider>
</TestProviders>
);
expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toHaveTextContent(RULE_DESCRIPTION_TITLE);
expect(getByTestId(DESCRIPTION_NAVIGATE_TO_RULE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RULE_SUMMARY_BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(DESCRIPTION_EXPAND_BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(DESCRIPTION_EXPAND_BUTTON_TEST_ID)).toHaveTextContent(
DOCUMENT_DESCRIPTION_COLLAPSE_BUTTON
@ -103,11 +108,13 @@ describe('<Description />', () => {
} as unknown as RightPanelContext;
const { getByTestId } = render(
<RightPanelContext.Provider value={panelContextValue}>
<ThemeProvider theme={mockTheme}>
<Description />
</ThemeProvider>
</RightPanelContext.Provider>
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<ThemeProvider theme={mockTheme}>
<Description />
</ThemeProvider>
</RightPanelContext.Provider>
</TestProviders>
);
expect(getByTestId(DESCRIPTION_EXPAND_BUTTON_TEST_ID)).toHaveTextContent(
@ -119,22 +126,24 @@ describe('<Description />', () => {
);
});
it('should not render view rule button if rule name is not available', () => {
it('should not render rule preview button if rule name is not available', () => {
const panelContextValue = {
dataFormattedForFieldBrowser: [ruleUuid, ruleDescription],
} as unknown as RightPanelContext;
const { getByTestId, queryByTestId } = render(
<RightPanelContext.Provider value={panelContextValue}>
<ThemeProvider theme={mockTheme}>
<Description />
</ThemeProvider>
</RightPanelContext.Provider>
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<ThemeProvider theme={mockTheme}>
<Description />
</ThemeProvider>
</RightPanelContext.Provider>
</TestProviders>
);
expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toHaveTextContent(RULE_DESCRIPTION_TITLE);
expect(queryByTestId(DESCRIPTION_NAVIGATE_TO_RULE_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RULE_SUMMARY_BUTTON_TEST_ID)).not.toBeInTheDocument();
});
it('should render document title if document is not an alert', () => {
const panelContextValue = {
@ -142,11 +151,13 @@ describe('<Description />', () => {
} as unknown as RightPanelContext;
const { getByTestId } = render(
<RightPanelContext.Provider value={panelContextValue}>
<ThemeProvider theme={mockTheme}>
<Description />
</ThemeProvider>
</RightPanelContext.Provider>
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<ThemeProvider theme={mockTheme}>
<Description />
</ThemeProvider>
</RightPanelContext.Provider>
</TestProviders>
);
expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument();

View file

@ -7,25 +7,27 @@
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import type { VFC } from 'react';
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo, useCallback } from 'react';
import { css } from '@emotion/react';
import { isEmpty } from 'lodash';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { useRightPanelContext } from '../context';
import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers';
import {
DESCRIPTION_DETAILS_TEST_ID,
DESCRIPTION_EXPAND_BUTTON_TEST_ID,
DESCRIPTION_TITLE_TEST_ID,
RULE_SUMMARY_BUTTON_TEST_ID,
} from './test_ids';
import {
DOCUMENT_DESCRIPTION_COLLAPSE_BUTTON,
DOCUMENT_DESCRIPTION_EXPAND_BUTTON,
DOCUMENT_DESCRIPTION_TITLE,
RULE_DESCRIPTION_TITLE,
VIEW_RULE_TEXT,
RULE_SUMMARY_TEXT,
PREVIEW_RULE_DETAILS,
} from './translations';
import { RenderRuleName } from '../../../timelines/components/timeline/body/renderers/formatted_field_helpers';
import { SIGNAL_RULE_NAME_FIELD_NAME } from '../../../timelines/components/timeline/body/renderers/constants';
import { PreviewPanelKey, type PreviewPanelProps } from '../../preview';
export interface DescriptionProps {
/**
@ -43,29 +45,47 @@ export interface DescriptionProps {
export const Description: VFC<DescriptionProps> = ({ expanded = false }) => {
const [isExpanded, setIsExpanded] = useState(expanded);
const { dataFormattedForFieldBrowser, scopeId, eventId } = useRightPanelContext();
const { isAlert, ruleDescription, ruleId, ruleName } = useBasicDataFromDetailsData(
const { dataFormattedForFieldBrowser, scopeId, eventId, indexName } = useRightPanelContext();
const { isAlert, ruleDescription, ruleName, ruleId } = useBasicDataFromDetailsData(
dataFormattedForFieldBrowser
);
const { openPreviewPanel } = useExpandableFlyoutContext();
const openRulePreview = useCallback(() => {
const PreviewPanelRulePreview: PreviewPanelProps['path'] = ['rule-preview'];
openPreviewPanel({
id: PreviewPanelKey,
path: PreviewPanelRulePreview,
params: {
id: eventId,
indexName,
scopeId,
banner: {
title: PREVIEW_RULE_DETAILS,
backgroundColor: 'warning',
textColor: 'warning',
},
ruleId,
},
});
}, [eventId, openPreviewPanel, indexName, scopeId, ruleId]);
const viewRule = useMemo(
() =>
!isEmpty(ruleName) && (
!isEmpty(ruleName) &&
!isEmpty(ruleId) && (
<EuiFlexItem grow={false}>
<RenderRuleName
contextId={scopeId}
eventId={eventId}
fieldName={SIGNAL_RULE_NAME_FIELD_NAME}
fieldType={'string'}
isAggregatable={false}
isDraggable={false}
linkValue={ruleId}
value={VIEW_RULE_TEXT}
openInNewTab
/>
<EuiButtonEmpty
size="s"
iconType="arrowRight"
onClick={openRulePreview}
iconSide="right"
data-test-subj={RULE_SUMMARY_BUTTON_TEST_ID}
>
{RULE_SUMMARY_TEXT}
</EuiButtonEmpty>
</EuiFlexItem>
),
[ruleName, ruleId, scopeId, eventId]
[ruleName, openRulePreview, ruleId]
);
if (!dataFormattedForFieldBrowser) {
@ -82,7 +102,7 @@ export const Description: VFC<DescriptionProps> = ({ expanded = false }) => {
<EuiFlexItem data-test-subj={DESCRIPTION_TITLE_TEST_ID}>
<EuiTitle size="xxs">
{isAlert ? (
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem>
<h5>{RULE_DESCRIPTION_TITLE}</h5>
</EuiFlexItem>

View file

@ -11,7 +11,7 @@ import { InvestigationGuideButton } from './investigation_guide_button';
import { RightPanelContext } from '../context';
import { INVESTIGATION_GUIDE_BUTTON_TEST_ID } from './test_ids';
import { mockContextValue } from '../mocks/mock_right_panel_context';
import { mockFlyoutContextValue } from '../mocks/mock_flyout_context';
import { mockFlyoutContextValue } from '../../shared/mocks/mock_flyout_context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback';

View file

@ -31,12 +31,12 @@ export const FLYOUT_HEADER_CHAT_BUTTON_TEST_ID = 'newChatById';
export const ABOUT_SECTION_TEST_ID = 'securitySolutionDocumentDetailsFlyoutAboutSection';
export const ABOUT_SECTION_HEADER_TEST_ID = ABOUT_SECTION_TEST_ID + HEADER_TEST_ID;
export const ABOUT_SECTION_CONTENT_TEST_ID = ABOUT_SECTION_TEST_ID + CONTENT_TEST_ID;
export const RULE_SUMMARY_BUTTON_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRuleSummaryButton';
export const DESCRIPTION_TITLE_TEST_ID = 'securitySolutionDocumentDetailsFlyoutDescriptionTitle';
export const DESCRIPTION_DETAILS_TEST_ID =
'securitySolutionDocumentDetailsFlyoutDescriptionDetails';
export const DESCRIPTION_EXPAND_BUTTON_TEST_ID =
'securitySolutionDocumentDetailsFlyoutDescriptionExpandButton';
export const DESCRIPTION_NAVIGATE_TO_RULE_TEST_ID = 'goToRuleDetails';
export const REASON_TITLE_TEST_ID = 'securitySolutionDocumentDetailsFlyoutReasonTitle';
export const REASON_DETAILS_TEST_ID = 'securitySolutionDocumentDetailsFlyoutReasonDetails';
export const MITRE_ATTACK_TITLE_TEST_ID = 'securitySolutionAlertDetailsFlyoutMitreAttackTitle';

View file

@ -45,10 +45,10 @@ export const RISK_SCORE_TITLE = i18n.translate(
}
);
export const VIEW_RULE_TEXT = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.viewRuleText',
export const RULE_SUMMARY_TEXT = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.ruleSummaryText',
{
defaultMessage: 'View rule',
defaultMessage: 'Rule summary',
}
);
@ -68,6 +68,11 @@ export const RULE_DESCRIPTION_TITLE = i18n.translate(
}
);
export const PREVIEW_RULE_DETAILS = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.previewRuleDetailsText',
{ defaultMessage: 'Preview rule details' }
);
export const DOCUMENT_DESCRIPTION_TITLE = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.documentDescriptionTitle',
{

View file

@ -33507,7 +33507,6 @@
"xpack.securitySolution.flyout.documentDetails.technicalPreviewTitle": "Version d'évaluation technique",
"xpack.securitySolution.flyout.documentDetails.threatIntelligenceButton": "Threat Intelligence",
"xpack.securitySolution.flyout.documentDetails.threatIntelligenceTitle": "Threat Intelligence",
"xpack.securitySolution.flyout.documentDetails.viewRuleText": "Afficher la règle",
"xpack.securitySolution.flyout.documentDetails.visualizationsTitle": "Visualisations",
"xpack.securitySolution.flyout.documentDetails.visualizeOptions": "Options Visualize",
"xpack.securitySolution.flyout.documentDetails.visualizeTab": "Visualiser",

View file

@ -33506,7 +33506,6 @@
"xpack.securitySolution.flyout.documentDetails.technicalPreviewTitle": "テクニカルプレビュー",
"xpack.securitySolution.flyout.documentDetails.threatIntelligenceButton": "脅威インテリジェンス",
"xpack.securitySolution.flyout.documentDetails.threatIntelligenceTitle": "脅威インテリジェンス",
"xpack.securitySolution.flyout.documentDetails.viewRuleText": "ルールを表示",
"xpack.securitySolution.flyout.documentDetails.visualizationsTitle": "ビジュアライゼーション",
"xpack.securitySolution.flyout.documentDetails.visualizeOptions": "Visualizeオプション",
"xpack.securitySolution.flyout.documentDetails.visualizeTab": "可視化",

View file

@ -33501,7 +33501,6 @@
"xpack.securitySolution.flyout.documentDetails.technicalPreviewTitle": "技术预览",
"xpack.securitySolution.flyout.documentDetails.threatIntelligenceButton": "威胁情报",
"xpack.securitySolution.flyout.documentDetails.threatIntelligenceTitle": "威胁情报",
"xpack.securitySolution.flyout.documentDetails.viewRuleText": "查看规则",
"xpack.securitySolution.flyout.documentDetails.visualizationsTitle": "可视化",
"xpack.securitySolution.flyout.documentDetails.visualizeOptions": "Visualize 选项",
"xpack.securitySolution.flyout.documentDetails.visualizeTab": "Visualize",