mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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

**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:
parent
a409f80444
commit
a1be0029c3
31 changed files with 886 additions and 87 deletions
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
);
|
|
@ -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 =
|
||||
|
|
|
@ -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();
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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';
|
|
@ -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' }
|
||||
);
|
|
@ -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;
|
||||
};
|
|
@ -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';
|
|
@ -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: '',
|
||||
};
|
|
@ -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 />,
|
||||
},
|
||||
];
|
|
@ -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' }
|
||||
);
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "可視化",
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue