From de5cb6ede2e1b8483ef1edb711736f4b0a84e1d9 Mon Sep 17 00:00:00 2001 From: christineweng <18648970+christineweng@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:00:02 -0500 Subject: [PATCH] [Security Solution][Alert Flyout] Convert rule name to rule flyout and enable rule previews (#191764) ## Summary This PR converts rule name in alert table to be a flyout (consistent with host name and user name) and enables rule preview whenever rule name is present. This PR also moved the rule details component into its own `rule_details` folder to be independent of the `document_details` flyout. Dependency: https://github.com/elastic/kibana/pull/190560 to be merged first New behavior: - Rule link in alert table opens rule flyout - Clicking the rule title goes to rule details page - Clicking rule name in alert flyout opens rule preview https://github.com/user-attachments/assets/857aa894-6253-4041-873a-18d6e8a003b6 ### 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 --- ...correlations_details_alerts_table.test.tsx | 26 +++ .../correlations_details_alerts_table.tsx | 34 +++- .../components/alert_description.test.tsx | 12 +- .../right/components/alert_description.tsx | 23 +-- .../table_field_value_cell.test.tsx | 8 + .../components/table_field_value_cell.tsx | 12 ++ .../document_details/right/tabs/table_tab.tsx | 27 ++- .../rule_overview/components/footer.test.tsx | 49 ------ .../components/rule_overview.test.tsx | 142 ---------------- .../components/rule_overview.tsx | 154 ------------------ .../components/rule_title.test.tsx | 54 ------ .../rule_overview/components/rule_title.tsx | 65 -------- .../rule_overview/components/test_ids.ts | 46 ------ .../rule_overview/context.tsx | 58 ------- .../document_details/rule_overview/index.tsx | 38 ----- .../rule_overview/mocks/mock_context.ts | 15 -- .../shared/constants/panel_keys.ts | 1 - .../security_solution/public/flyout/index.tsx | 16 +- .../hooks/use_rule_details.test.ts | 66 ++++++++ .../rule_details/hooks/use_rule_details.tsx | 56 +++++++ .../rule_details/preview/footer.test.tsx | 44 +++++ .../preview}/footer.tsx | 36 ++-- .../flyout/rule_details/preview/test_ids.ts | 15 ++ .../rule_details/right/content.test.tsx | 98 +++++++++++ .../flyout/rule_details/right/content.tsx | 145 +++++++++++++++++ .../flyout/rule_details/right/header.test.tsx | 60 +++++++ .../flyout/rule_details/right/header.tsx | 90 ++++++++++ .../flyout/rule_details/right/index.test.tsx | 104 ++++++++++++ .../flyout/rule_details/right/index.tsx | 68 ++++++++ .../flyout/rule_details/right/test_ids.ts | 40 +++++ .../shared/components/preview_link.test.tsx | 56 ++++++- .../flyout/shared/components/preview_link.tsx | 43 ++++- .../renderers/formatted_field_helpers.tsx | 35 +++- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - ...lert_details_preview_panel_rule_preview.ts | 22 +-- 37 files changed, 1054 insertions(+), 707 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/footer.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/rule_overview.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/rule_overview.tsx delete mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/rule_title.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/rule_title.tsx delete mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/test_ids.ts delete mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/context.tsx delete mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/mocks/mock_context.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/rule_details/hooks/use_rule_details.test.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/rule_details/hooks/use_rule_details.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.test.tsx rename x-pack/plugins/security_solution/public/flyout/{document_details/rule_overview/components => rule_details/preview}/footer.tsx (51%) create mode 100644 x-pack/plugins/security_solution/public/flyout/rule_details/preview/test_ids.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/rule_details/right/content.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/rule_details/right/content.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/rule_details/right/header.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/rule_details/right/header.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/rule_details/right/index.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/rule_details/right/test_ids.ts diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.test.tsx index ddf05af5d390857..6fd56c3aa5195a9 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.test.tsx @@ -17,6 +17,7 @@ import { mockContextValue } from '../../shared/mocks/mock_context'; import { DocumentDetailsPreviewPanelKey } from '../../shared/constants/panel_keys'; import { ALERT_PREVIEW_BANNER } from '../../preview/constants'; import { DocumentDetailsContext } from '../../shared/context'; +import { RulePreviewPanelKey, RULE_PREVIEW_BANNER } from '../../../rule_details/right'; jest.mock('../hooks/use_paginated_alerts'); jest.mock('../../../../common/hooks/use_experimental_features'); @@ -59,6 +60,7 @@ describe('CorrelationsDetailsAlertsTable', () => { 'kibana.alert.rule.name': ['Rule1'], 'kibana.alert.reason': ['Reason1'], 'kibana.alert.severity': ['Severity1'], + 'kibana.alert.rule.uuid': ['uuid1'], }, }, { @@ -69,6 +71,7 @@ describe('CorrelationsDetailsAlertsTable', () => { 'kibana.alert.rule.name': ['Rule2'], 'kibana.alert.reason': ['Reason2'], 'kibana.alert.severity': ['Severity2'], + 'kibana.alert.rule.uuid': ['uuid2'], }, }, ], @@ -124,4 +127,27 @@ describe('CorrelationsDetailsAlertsTable', () => { }, }); }); + + it('opens rule preview when feature flag is on and isPreview is false', () => { + mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); + const { getAllByTestId } = renderCorrelationsTable(mockContextValue); + + expect(getAllByTestId(`${TEST_ID}RulePreview`).length).toBe(2); + + getAllByTestId(`${TEST_ID}RulePreview`)[0].click(); + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ + id: RulePreviewPanelKey, + params: { + ruleId: 'uuid1', + banner: RULE_PREVIEW_BANNER, + isPreviewMode: true, + }, + }); + }); + + it('does not render preview link when feature flag is on and isPreview is true', () => { + mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); + const { queryByTestId } = renderCorrelationsTable({ ...mockContextValue, isPreview: true }); + expect(queryByTestId(`${TEST_ID}RulePreview`)).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.tsx index ea16fcfa80e9395..20663b56dab3945 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/correlations_details_alerts_table.tsx @@ -24,6 +24,8 @@ import { InvestigateInTimelineButton } from '../../../../common/components/event import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations'; import { getDataProvider } from '../../../../common/components/event_details/use_action_cell_data_provider'; import { AlertPreviewButton } from '../../../shared/components/alert_preview_button'; +import { PreviewLink } from '../../../shared/components/preview_link'; +import { useDocumentDetailsContext } from '../../shared/context'; export const TIMESTAMP_DATE_FORMAT = 'MMM D, YYYY @ HH:mm:ss.SSS'; const dataProviderLimit = 5; @@ -82,6 +84,8 @@ export const CorrelationsDetailsAlertsTable: FC>) => { if (page) { @@ -160,7 +164,6 @@ export const CorrelationsDetailsAlertsTable: FC ), truncateText: true, - render: (value: string) => ( - - {value} - - ), + render: (row: Record) => { + const ruleName = row[ALERT_RULE_NAME] as string; + const ruleId = row['kibana.alert.rule.uuid'] as string; + return ( + + {isPreviewEnabled ? ( + + {ruleName} + + ) : ( + {ruleName} + )} + + ); + }, }, { field: ALERT_REASON, @@ -209,7 +229,7 @@ export const CorrelationsDetailsAlertsTable: FC { @@ -150,13 +150,11 @@ describe('', () => { getByTestId(RULE_SUMMARY_BUTTON_TEST_ID).click(); expect(flyoutContextValue.openPreviewPanel).toHaveBeenCalledWith({ - id: DocumentDetailsRuleOverviewPanelKey, + id: RulePreviewPanelKey, params: { - id: panelContext.eventId, - indexName: panelContext.indexName, - scopeId: panelContext.scopeId, - banner: RULE_OVERVIEW_BANNER, + banner: RULE_PREVIEW_BANNER, ruleId: ruleUuid.values[0], + isPreviewMode: true, }, }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx index ca01ac08d66a551..2d2dfddbbbabeff 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx @@ -21,43 +21,32 @@ import { ALERT_DESCRIPTION_TITLE_TEST_ID, RULE_SUMMARY_BUTTON_TEST_ID, } from './test_ids'; -import { DocumentDetailsRuleOverviewPanelKey } from '../../shared/constants/panel_keys'; - -export const RULE_OVERVIEW_BANNER = { - title: i18n.translate('xpack.securitySolution.flyout.right.about.description.rulePreviewTitle', { - defaultMessage: 'Preview rule details', - }), - backgroundColor: 'warning', - textColor: 'warning', -}; +import { RULE_PREVIEW_BANNER, RulePreviewPanelKey } from '../../../rule_details/right'; /** * Displays the rule description of a signal document. */ export const AlertDescription: FC = () => { const { telemetry } = useKibana().services; - const { dataFormattedForFieldBrowser, scopeId, eventId, indexName, isPreview } = - useDocumentDetailsContext(); + const { dataFormattedForFieldBrowser, scopeId, isPreview } = useDocumentDetailsContext(); const { isAlert, ruleDescription, ruleName, ruleId } = useBasicDataFromDetailsData( dataFormattedForFieldBrowser ); const { openPreviewPanel } = useExpandableFlyoutApi(); const openRulePreview = useCallback(() => { openPreviewPanel({ - id: DocumentDetailsRuleOverviewPanelKey, + id: RulePreviewPanelKey, params: { - id: eventId, - indexName, - scopeId, - banner: RULE_OVERVIEW_BANNER, ruleId, + banner: RULE_PREVIEW_BANNER, + isPreviewMode: true, }, }); telemetry.reportDetailsFlyoutOpened({ location: scopeId, panel: 'preview', }); - }, [eventId, openPreviewPanel, indexName, scopeId, ruleId, telemetry]); + }, [openPreviewPanel, scopeId, ruleId, telemetry]); const viewRule = useMemo( () => ( diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.test.tsx index 16f72108913eb6a..8de7c909548b00f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.test.tsx @@ -84,6 +84,8 @@ describe('TableFieldValueCell', () => { data={hostIpData} eventId={eventId} values={hostIpValues} + ruleId="ruleId" + isPreview={false} /> @@ -106,6 +108,8 @@ describe('TableFieldValueCell', () => { eventId={eventId} fieldFromBrowserField={undefined} // <-- no metadata values={hostIpValues} + ruleId="ruleId" + isPreview={false} /> @@ -152,6 +156,8 @@ describe('TableFieldValueCell', () => { eventId={eventId} fieldFromBrowserField={messageFieldFromBrowserField} values={messageValues} + ruleId="ruleId" + isPreview={false} /> @@ -188,6 +194,8 @@ describe('TableFieldValueCell', () => { eventId={eventId} fieldFromBrowserField={hostIpFieldFromBrowserField} // <-- metadata values={hostIpValues} + ruleId="ruleId" + isPreview={false} /> diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.tsx index e72b9755f3ca95a..a0095bb8eadf0d9 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.tsx @@ -34,6 +34,14 @@ export interface FieldValueCellProps { * Field retrieved from the BrowserField */ fieldFromBrowserField?: Partial; + /** + * Id of the rule + */ + ruleId: string; + /** + * Whether the preview link is in rule preview + */ + isPreview: boolean; /** * Value of the link field if it exists. Allows to navigate to other pages like host, user, network... */ @@ -53,8 +61,10 @@ export const TableFieldValueCell = memo( data, eventId, fieldFromBrowserField, + ruleId, getLinkValue, values, + isPreview, }: FieldValueCellProps) => { const isPreviewEnabled = !useIsExperimentalFeatureEnabled('entityAlertPreviewDisabled'); if (values == null) { @@ -87,6 +97,8 @@ export const TableFieldValueCell = memo( field={data.field} value={value} scopeId={scopeId} + ruleId={ruleId} + isPreview={isPreview} data-test-subj={`${FLYOUT_TABLE_PREVIEW_LINK_FIELD_TEST_ID}-${i}`} /> ) : ( diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/table_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/table_tab.tsx index 9bd1cf26bc83d44..e271fd2dae6cd73 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/table_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/table_tab.tsx @@ -26,6 +26,7 @@ import type { EventFieldsData } from '../../../../common/components/event_detail import { CellActions } from '../../shared/components/cell_actions'; import { useDocumentDetailsContext } from '../../shared/context'; import { isInTableScope, isTimelineScope } from '../../../../helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; const COUNT_PER_PAGE_OPTIONS = [25, 50, 100]; @@ -76,13 +77,28 @@ export type ColumnsProvider = (providerOptions: { * Maintain backwards compatibility // TODO remove when possible */ scopeId: string; + /** + * Id of the rule + */ + ruleId: string; + /** + * Whether the preview link is in preview mode + */ + isPreview: boolean; /** * Value of the link field if it exists. Allows to navigate to other pages like host, user, network... */ getLinkValue: (field: string) => string | null; }) => Array>; -export const getColumns: ColumnsProvider = ({ browserFields, eventId, scopeId, getLinkValue }) => [ +export const getColumns: ColumnsProvider = ({ + browserFields, + eventId, + scopeId, + getLinkValue, + ruleId, + isPreview, +}) => [ { field: 'field', name: ( @@ -113,6 +129,8 @@ export const getColumns: ColumnsProvider = ({ browserFields, eventId, scopeId, g eventId={eventId} fieldFromBrowserField={fieldFromBrowserField} getLinkValue={getLinkValue} + ruleId={ruleId} + isPreview={isPreview} values={values} /> @@ -127,8 +145,9 @@ export const getColumns: ColumnsProvider = ({ browserFields, eventId, scopeId, g export const TableTab = memo(() => { const smallFontSize = useEuiFontSize('xs').fontSize; - const { browserFields, dataFormattedForFieldBrowser, eventId, scopeId } = + const { browserFields, dataFormattedForFieldBrowser, eventId, scopeId, isPreview } = useDocumentDetailsContext(); + const { ruleId } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); const [pagination, setPagination] = useState<{ pageIndex: number }>({ pageIndex: 0, @@ -199,8 +218,10 @@ export const TableTab = memo(() => { eventId, scopeId, getLinkValue, + ruleId, + isPreview, }), - [browserFields, eventId, scopeId, getLinkValue] + [browserFields, eventId, scopeId, getLinkValue, ruleId, isPreview] ); return ( diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/footer.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/footer.test.tsx deleted file mode 100644 index b2c9b895bc0ce33..000000000000000 --- a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/footer.test.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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_context'; -import { RuleOverviewPanelContext } from '../context'; -import { RULE_OVERVIEW_FOOTER_TEST_ID, RULE_OVERVIEW_NAVIGATE_TO_RULE_TEST_ID } from './test_ids'; -import { RuleFooter } from './footer'; -import { useRuleDetailsLink } from '../../shared/hooks/use_rule_details_link'; - -jest.mock('../../shared/hooks/use_rule_details_link'); - -const renderRulePreviewFooter = (contextValue: RuleOverviewPanelContext) => - render( - - - - - - ); - -describe('', () => { - it('renders rule details link correctly when ruleId is available', () => { - (useRuleDetailsLink as jest.Mock).mockReturnValue('rule_details_link'); - - const { getByTestId } = renderRulePreviewFooter(mockContextValue); - - expect(getByTestId(RULE_OVERVIEW_FOOTER_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(RULE_OVERVIEW_NAVIGATE_TO_RULE_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(RULE_OVERVIEW_NAVIGATE_TO_RULE_TEST_ID)).toHaveTextContent( - 'Show full rule details' - ); - }); - - it('should not render rule details link when ruleId is not available', () => { - (useRuleDetailsLink as jest.Mock).mockReturnValue(null); - - const { queryByTestId } = renderRulePreviewFooter(mockContextValue); - - expect(queryByTestId(RULE_OVERVIEW_FOOTER_TEST_ID)).not.toBeInTheDocument(); - expect(queryByTestId(RULE_OVERVIEW_NAVIGATE_TO_RULE_TEST_ID)).not.toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/rule_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/rule_overview.test.tsx deleted file mode 100644 index f55937a9ebafacd..000000000000000 --- a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/rule_overview.test.tsx +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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 { act, render } from '@testing-library/react'; -import { RuleOverview } from './rule_overview'; -import { RuleOverviewPanelContext } from '../context'; -import { mockContextValue } from '../mocks/mock_context'; -import { ThemeProvider } from 'styled-components'; -import { getMockTheme } from '../../../../common/lib/kibana/kibana_react.mock'; -import { TestProviders } from '../../../../common/mock'; -import { TestProvider } from '@kbn/expandable-flyout/src/test/provider'; -import { useRuleWithFallback } from '../../../../detection_engine/rule_management/logic/use_rule_with_fallback'; -import { getStepsData } from '../../../../detections/pages/detection_engine/rules/helpers'; -import { - mockAboutStepRule, - mockDefineStepRule, - mockScheduleStepRule, -} from '../../../../detection_engine/rule_management_ui/components/rules_table/__mocks__/mock'; -import { useGetSavedQuery } from '../../../../detections/pages/detection_engine/rules/use_get_saved_query'; -import { - RULE_OVERVIEW_BODY_TEST_ID, - RULE_OVERVIEW_ABOUT_HEADER_TEST_ID, - RULE_OVERVIEW_ABOUT_CONTENT_TEST_ID, - RULE_OVERVIEW_DEFINITION_HEADER_TEST_ID, - RULE_OVERVIEW_DEFINITION_CONTENT_TEST_ID, - RULE_OVERVIEW_SCHEDULE_HEADER_TEST_ID, - RULE_OVERVIEW_SCHEDULE_CONTENT_TEST_ID, - RULE_OVERVIEW_ACTIONS_HEADER_TEST_ID, - RULE_OVERVIEW_ACTIONS_CONTENT_TEST_ID, - RULE_OVERVIEW_LOADING_TEST_ID, -} from './test_ids'; - -jest.mock('../../../../common/lib/kibana'); - -const mockUseRuleWithFallback = useRuleWithFallback as jest.Mock; -jest.mock('../../../../detection_engine/rule_management/logic/use_rule_with_fallback'); - -const mockGetStepsData = getStepsData as jest.Mock; -jest.mock('../../../../detections/pages/detection_engine/rules/helpers'); - -const mockUseGetSavedQuery = useGetSavedQuery as jest.Mock; -jest.mock('../../../../detections/pages/detection_engine/rules/use_get_saved_query'); - -const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } }); - -const contextValue = { - ...mockContextValue, - ruleId: 'rule id', -}; - -const renderRulePreview = () => - render( - - - - - - - - - - ); - -const NO_DATA_MESSAGE = 'There was an error displaying data.'; - -describe('', () => { - afterEach(() => { - jest.clearAllMocks(); - mockUseGetSavedQuery.mockReturnValue({ isSavedQueryLoading: false, savedQueryBar: null }); - }); - - it('should render rule preview and its sub sections', async () => { - mockUseRuleWithFallback.mockReturnValue({ - rule: { name: 'rule name', description: 'rule description' }, - }); - mockGetStepsData.mockReturnValue({ - aboutRuleData: mockAboutStepRule(), - defineRuleData: mockDefineStepRule(), - scheduleRuleData: mockScheduleStepRule(), - ruleActionsData: { actions: ['action'] }, - }); - - const { getByTestId } = renderRulePreview(); - - await act(async () => { - expect(getByTestId(RULE_OVERVIEW_BODY_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(RULE_OVERVIEW_ABOUT_HEADER_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(RULE_OVERVIEW_ABOUT_HEADER_TEST_ID)).toHaveTextContent('About'); - expect(getByTestId(RULE_OVERVIEW_ABOUT_CONTENT_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(RULE_OVERVIEW_DEFINITION_HEADER_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(RULE_OVERVIEW_DEFINITION_HEADER_TEST_ID)).toHaveTextContent('Definition'); - expect(getByTestId(RULE_OVERVIEW_DEFINITION_CONTENT_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(RULE_OVERVIEW_SCHEDULE_HEADER_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(RULE_OVERVIEW_SCHEDULE_HEADER_TEST_ID)).toHaveTextContent('Schedule'); - expect(getByTestId(RULE_OVERVIEW_SCHEDULE_CONTENT_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(RULE_OVERVIEW_ACTIONS_HEADER_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(RULE_OVERVIEW_ACTIONS_HEADER_TEST_ID)).toHaveTextContent('Actions'); - expect(getByTestId(RULE_OVERVIEW_ACTIONS_CONTENT_TEST_ID)).toBeInTheDocument(); - }); - }); - - it('should not render actions if action is not available', async () => { - mockUseRuleWithFallback.mockReturnValue({ - rule: { name: 'rule name', description: 'rule description' }, - }); - mockGetStepsData.mockReturnValue({ - aboutRuleData: mockAboutStepRule(), - defineRuleData: mockDefineStepRule(), - scheduleRuleData: mockScheduleStepRule(), - }); - const { queryByTestId } = renderRulePreview(); - - await act(async () => { - expect(queryByTestId(RULE_OVERVIEW_ACTIONS_HEADER_TEST_ID)).not.toBeInTheDocument(); - expect(queryByTestId(RULE_OVERVIEW_ACTIONS_CONTENT_TEST_ID)).not.toBeInTheDocument(); - }); - }); - - it('should render loading spinner when rule is loading', async () => { - mockUseRuleWithFallback.mockReturnValue({ loading: true, rule: null }); - mockGetStepsData.mockReturnValue({}); - const { getByTestId } = renderRulePreview(); - await act(async () => { - expect(getByTestId(RULE_OVERVIEW_LOADING_TEST_ID)).toBeInTheDocument(); - }); - }); - - it('should not render rule preview when rule is null', async () => { - mockUseRuleWithFallback.mockReturnValue({}); - mockGetStepsData.mockReturnValue({}); - const { queryByTestId, getByText } = renderRulePreview(); - await act(async () => { - expect(queryByTestId(RULE_OVERVIEW_BODY_TEST_ID)).not.toBeInTheDocument(); - expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/rule_overview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/rule_overview.tsx deleted file mode 100644 index 2e615012418991a..000000000000000 --- a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/rule_overview.tsx +++ /dev/null @@ -1,154 +0,0 @@ -/* - * 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 { EuiText, EuiHorizontalRule, EuiSpacer, EuiPanel } from '@elastic/eui'; -import { css } from '@emotion/css'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { FlyoutLoading, FlyoutError } from '@kbn/security-solution-common'; -import { useRuleOverviewPanelContext } from '../context'; -import { ExpandableSection } from '../../right/components/expandable_section'; -import { useRuleWithFallback } from '../../../../detection_engine/rule_management/logic/use_rule_with_fallback'; -import { getStepsData } from '../../../../detections/pages/detection_engine/rules/helpers'; -import { RuleTitle } from './rule_title'; -import { RuleAboutSection } from '../../../../detection_engine/rule_management/components/rule_details/rule_about_section'; -import { RuleScheduleSection } from '../../../../detection_engine/rule_management/components/rule_details/rule_schedule_section'; -import { RuleDefinitionSection } from '../../../../detection_engine/rule_management/components/rule_details/rule_definition_section'; -import { StepRuleActionsReadOnly } from '../../../../detection_engine/rule_creation/components/step_rule_actions'; -import { - RULE_OVERVIEW_BODY_TEST_ID, - RULE_OVERVIEW_ABOUT_TEST_ID, - RULE_OVERVIEW_DEFINITION_TEST_ID, - RULE_OVERVIEW_SCHEDULE_TEST_ID, - RULE_OVERVIEW_ACTIONS_TEST_ID, - RULE_OVERVIEW_LOADING_TEST_ID, -} from './test_ids'; -import type { RuleResponse } from '../../../../../common/api/detection_engine'; - -const panelViewStyle = css` - dt { - font-size: 90% !important; - } - text-overflow: ellipsis; - .euiFlexGroup { - flex-wrap: inherit; - } - - .euiFlexItem { - inline-size: inherit; - flex-basis: inherit; - } -`; - -/** - * Rule summary on a preview panel on top of the right section of expandable flyout - */ -export const RuleOverview = memo(() => { - const { ruleId } = useRuleOverviewPanelContext(); - const [rule, setRule] = useState(null); - const { - rule: maybeRule, - loading: ruleLoading, - isExistingRule, - } = useRuleWithFallback(ruleId ?? ''); - - // persist rule until refresh is complete - useEffect(() => { - if (maybeRule != null) { - setRule(maybeRule); - } - }, [maybeRule]); - - const { ruleActionsData } = - rule != null ? getStepsData({ rule, detailsView: true }) : { ruleActionsData: null }; - - const hasNotificationActions = Boolean(ruleActionsData?.actions?.length); - const hasResponseActions = Boolean(ruleActionsData?.responseActions?.length); - const hasActions = ruleActionsData != null && (hasNotificationActions || hasResponseActions); - - return ruleLoading ? ( - - ) : rule ? ( - - - - - - } - expanded - data-test-subj={RULE_OVERVIEW_ABOUT_TEST_ID} - > - {rule.description} - - - - - - } - expanded={false} - data-test-subj={RULE_OVERVIEW_DEFINITION_TEST_ID} - > - - - - - } - expanded={false} - data-test-subj={RULE_OVERVIEW_SCHEDULE_TEST_ID} - > - - - - {hasActions && ( - - } - expanded={false} - data-test-subj={RULE_OVERVIEW_ACTIONS_TEST_ID} - > - - - )} - - ) : ( - - - - ); -}); - -RuleOverview.displayName = 'RuleOverview'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/rule_title.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/rule_title.test.tsx deleted file mode 100644 index 9de5d568572e3be..000000000000000 --- a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/rule_title.test.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 type { RuleTitleProps } from './rule_title'; -import { RuleTitle } from './rule_title'; -import { TestProvider as ExpandableFlyoutTestProvider } from '@kbn/expandable-flyout/src/test/provider'; -import { TestProviders } from '../../../../common/mock'; -import type { Rule } from '../../../../detection_engine/rule_management/logic'; -import { - RULE_OVERVIEW_TITLE_TEST_ID, - RULE_OVERVIEW_RULE_CREATED_BY_TEST_ID, - RULE_OVERVIEW_RULE_UPDATED_BY_TEST_ID, - RULE_OVERVIEW_RULE_TITLE_SUPPRESSED_TEST_ID, -} from './test_ids'; - -const defaultProps = { - rule: { id: 'id' } as Rule, - isSuppressed: false, -}; - -const renderRuleOverviewTitle = (props: RuleTitleProps) => - render( - - - - - - ); - -describe('', () => { - it('should render title and its components', () => { - const { getByTestId, queryByTestId } = renderRuleOverviewTitle(defaultProps); - - expect(getByTestId(RULE_OVERVIEW_TITLE_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(RULE_OVERVIEW_RULE_CREATED_BY_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(RULE_OVERVIEW_RULE_UPDATED_BY_TEST_ID)).toBeInTheDocument(); - expect(queryByTestId(RULE_OVERVIEW_RULE_TITLE_SUPPRESSED_TEST_ID)).not.toBeInTheDocument(); - }); - - it('should render deleted rule badge', () => { - const props = { - ...defaultProps, - isSuppressed: true, - }; - const { getByTestId } = renderRuleOverviewTitle(props); - expect(getByTestId(RULE_OVERVIEW_RULE_TITLE_SUPPRESSED_TEST_ID)).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/rule_title.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/rule_title.tsx deleted file mode 100644 index 780c8b15f773062..000000000000000 --- a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/rule_title.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 { EuiTitle, EuiText, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; -import { DELETED_RULE } from '../../../../detection_engine/rule_details_ui/pages/rule_details/translations'; -import { CreatedBy, UpdatedBy } from '../../../../detections/components/rules/rule_info'; -import { - RULE_OVERVIEW_TITLE_TEST_ID, - RULE_OVERVIEW_RULE_CREATED_BY_TEST_ID, - RULE_OVERVIEW_RULE_UPDATED_BY_TEST_ID, - RULE_OVERVIEW_RULE_TITLE_SUPPRESSED_TEST_ID, -} from './test_ids'; -import type { RuleResponse } from '../../../../../common/api/detection_engine'; - -export interface RuleTitleProps { - /** - * Rule object that represents relevant information about a rule - */ - rule: RuleResponse; - /** - * Flag to indicate if rule is suppressed - */ - isSuppressed: boolean; -} - -/** - * Title component that shows basic information of a rule. This is displayed above rule overview body - */ -export const RuleTitle: React.FC = ({ rule, isSuppressed }) => { - return ( -
- -
{rule.name}
-
- {isSuppressed && ( - <> - - - {DELETED_RULE} - - - )} - - - - - - - - - - - - - -
- ); -}; - -RuleTitle.displayName = 'RuleTitle'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/test_ids.ts deleted file mode 100644 index b48e2c930075010..000000000000000 --- a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/test_ids.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 { PREFIX } from '../../../shared/test_ids'; -import { CONTENT_TEST_ID, HEADER_TEST_ID } from '../../right/components/expandable_section'; - -const RULE_OVERVIEW_TEST_ID = `${PREFIX}RuleOverview` as const; -export const RULE_OVERVIEW_TITLE_TEST_ID = `${RULE_OVERVIEW_TEST_ID}RuleOverviewTitle` as const; -export const RULE_OVERVIEW_RULE_TITLE_SUPPRESSED_TEST_ID = - `${RULE_OVERVIEW_TITLE_TEST_ID}Suppressed` as const; -export const RULE_OVERVIEW_RULE_CREATED_BY_TEST_ID = - `${RULE_OVERVIEW_TEST_ID}CreatedByText` as const; -export const RULE_OVERVIEW_RULE_UPDATED_BY_TEST_ID = - `${RULE_OVERVIEW_TEST_ID}UpdatedByText` as const; -export const RULE_OVERVIEW_BODY_TEST_ID = `${RULE_OVERVIEW_TEST_ID}Body` as const; - -export const RULE_OVERVIEW_ABOUT_TEST_ID = `${RULE_OVERVIEW_TEST_ID}AboutSection` as const; -export const RULE_OVERVIEW_ABOUT_HEADER_TEST_ID = RULE_OVERVIEW_ABOUT_TEST_ID + HEADER_TEST_ID; -export const RULE_OVERVIEW_ABOUT_CONTENT_TEST_ID = RULE_OVERVIEW_ABOUT_TEST_ID + CONTENT_TEST_ID; - -export const RULE_OVERVIEW_DEFINITION_TEST_ID = - `${RULE_OVERVIEW_TEST_ID}DefinitionSection` as const; -export const RULE_OVERVIEW_DEFINITION_HEADER_TEST_ID = - RULE_OVERVIEW_DEFINITION_TEST_ID + HEADER_TEST_ID; -export const RULE_OVERVIEW_DEFINITION_CONTENT_TEST_ID = - RULE_OVERVIEW_DEFINITION_TEST_ID + CONTENT_TEST_ID; - -export const RULE_OVERVIEW_SCHEDULE_TEST_ID = `${RULE_OVERVIEW_TEST_ID}ScheduleSection` as const; -export const RULE_OVERVIEW_SCHEDULE_HEADER_TEST_ID = - RULE_OVERVIEW_SCHEDULE_TEST_ID + HEADER_TEST_ID; -export const RULE_OVERVIEW_SCHEDULE_CONTENT_TEST_ID = - RULE_OVERVIEW_SCHEDULE_TEST_ID + CONTENT_TEST_ID; - -export const RULE_OVERVIEW_ACTIONS_TEST_ID = `${RULE_OVERVIEW_TEST_ID}ActionsSection` as const; -export const RULE_OVERVIEW_ACTIONS_HEADER_TEST_ID = RULE_OVERVIEW_ACTIONS_TEST_ID + HEADER_TEST_ID; -export const RULE_OVERVIEW_ACTIONS_CONTENT_TEST_ID = - RULE_OVERVIEW_ACTIONS_TEST_ID + CONTENT_TEST_ID; - -export const RULE_OVERVIEW_LOADING_TEST_ID = `${RULE_OVERVIEW_TEST_ID}Loading` as const; -export const RULE_OVERVIEW_FOOTER_TEST_ID = `${RULE_OVERVIEW_TEST_ID}Footer` as const; -export const RULE_OVERVIEW_NAVIGATE_TO_RULE_TEST_ID = - `${RULE_OVERVIEW_FOOTER_TEST_ID}LinkToRuleDetails` as const; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/context.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/context.tsx deleted file mode 100644 index cae182b839e6d9d..000000000000000 --- a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/context.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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, memo, useContext, useMemo } from 'react'; -import { FlyoutError } from '@kbn/security-solution-common'; -import type { RuleOverviewPanelProps } from '.'; - -export interface RuleOverviewPanelContext { - /** - * Rule id if preview is rule details - */ - ruleId: string; -} - -export const RuleOverviewPanelContext = createContext( - undefined -); - -export type RuleOverviewPanelProviderProps = { - /** - * React components to render - */ - children: React.ReactNode; -} & Partial; - -export const RuleOverviewPanelProvider = memo( - ({ ruleId, children }: RuleOverviewPanelProviderProps) => { - const contextValue = useMemo(() => (ruleId ? { ruleId } : undefined), [ruleId]); - - if (!contextValue) { - return ; - } - - return ( - - {children} - - ); - } -); - -RuleOverviewPanelProvider.displayName = 'RuleOverviewPanelProvider'; - -export const useRuleOverviewPanelContext = (): RuleOverviewPanelContext => { - const contextValue = useContext(RuleOverviewPanelContext); - - if (!contextValue) { - throw new Error( - 'RuleOverviewPanelContext can only be used within RuleOverviewPanelContext provider' - ); - } - - return contextValue; -}; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/index.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/index.tsx deleted file mode 100644 index b978a1697bbd56e..000000000000000 --- a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 type { FlyoutPanelProps } from '@kbn/expandable-flyout'; -import { FlyoutBody } from '@kbn/security-solution-common'; -import type { DocumentDetailsRuleOverviewPanelKey } from '../shared/constants/panel_keys'; -import { RuleOverview } from './components/rule_overview'; -import { RuleFooter } from './components/footer'; - -export interface RuleOverviewPanelProps extends FlyoutPanelProps { - key: typeof DocumentDetailsRuleOverviewPanelKey; - params: { - ruleId: string; - }; -} - -/** - * Displays a rule overview panel - */ -export const RuleOverviewPanel: React.FC = memo(() => { - return ( - <> - -
- -
-
- - - ); -}); - -RuleOverviewPanel.displayName = 'RuleOverviewPanel'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/mocks/mock_context.ts b/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/mocks/mock_context.ts deleted file mode 100644 index 4b800e338d85bdf..000000000000000 --- a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/mocks/mock_context.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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 { RuleOverviewPanelContext } from '../context'; - -/** - * Mock contextValue for rule overview panel context - */ -export const mockContextValue: RuleOverviewPanelContext = { - ruleId: 'rule id', -}; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts index a57cbf85fa784ee..b3f6cd3343ef1a6 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts @@ -11,4 +11,3 @@ export const DocumentDetailsPreviewPanelKey = 'document-details-preview' as cons export const DocumentDetailsIsolateHostPanelKey = 'document-details-isolate-host' as const; export const DocumentDetailsAlertReasonPanelKey = 'document-details-alert-reason' as const; -export const DocumentDetailsRuleOverviewPanelKey = 'document-details-rule-overview' as const; diff --git a/x-pack/plugins/security_solution/public/flyout/index.tsx b/x-pack/plugins/security_solution/public/flyout/index.tsx index 8437777a1e89159..d137e4464108009 100644 --- a/x-pack/plugins/security_solution/public/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/index.tsx @@ -16,7 +16,6 @@ import { DocumentDetailsRightPanelKey, DocumentDetailsPreviewPanelKey, DocumentDetailsAlertReasonPanelKey, - DocumentDetailsRuleOverviewPanelKey, } from './document_details/shared/constants/panel_keys'; import type { IsolateHostPanelProps } from './document_details/isolate_host'; import { IsolateHostPanel } from './document_details/isolate_host'; @@ -29,9 +28,8 @@ import { PreviewPanel } from './document_details/preview'; import type { AlertReasonPanelProps } from './document_details/alert_reason'; import { AlertReasonPanel } from './document_details/alert_reason'; import { AlertReasonPanelProvider } from './document_details/alert_reason/context'; -import type { RuleOverviewPanelProps } from './document_details/rule_overview'; -import { RuleOverviewPanel } from './document_details/rule_overview'; -import { RuleOverviewPanelProvider } from './document_details/rule_overview/context'; +import type { RulePanelExpandableFlyoutProps } from './rule_details/right'; +import { RulePanel, RulePanelKey, RulePreviewPanelKey } from './rule_details/right'; import type { UserPanelExpandableFlyoutProps } from './entity_details/user_right'; import { UserPanel, UserPanelKey, UserPreviewPanelKey } from './entity_details/user_right'; import type { UserDetailsPanelProps } from './entity_details/user_details_left'; @@ -80,11 +78,13 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels'] ), }, { - key: DocumentDetailsRuleOverviewPanelKey, + key: RulePanelKey, + component: (props) => , + }, + { + key: RulePreviewPanelKey, component: (props) => ( - - - + ), }, { diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/hooks/use_rule_details.test.ts b/x-pack/plugins/security_solution/public/flyout/rule_details/hooks/use_rule_details.test.ts new file mode 100644 index 000000000000000..41370334ed6a113 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/hooks/use_rule_details.test.ts @@ -0,0 +1,66 @@ +/* + * 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 { RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; +import type { UseRuleDetailsParams, UseRuleDetailsResult } from './use_rule_details'; +import { useRuleDetails } from './use_rule_details'; +import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback'; + +const mockUseRuleWithFallback = useRuleWithFallback as jest.Mock; +jest.mock('../../../detection_engine/rule_management/logic/use_rule_with_fallback'); + +const initialProps: UseRuleDetailsParams = { + ruleId: 'ruleId', +}; + +describe('useRuleDetails', () => { + let hookResult: RenderHookResult; + + it('should return loading as true when the rule is loading', () => { + mockUseRuleWithFallback.mockReturnValue({ + rule: null, + loading: true, + isExistingRule: false, + }); + hookResult = renderHook((props: UseRuleDetailsParams) => useRuleDetails(props), { + initialProps, + }); + expect(hookResult.result.current.loading).toBe(true); + expect(hookResult.result.current.isExistingRule).toBe(false); + expect(hookResult.result.current.rule).toBe(null); + }); + + it('should return empty rule when no rule is found', () => { + mockUseRuleWithFallback.mockReturnValue({ + rule: null, + loading: false, + isExistingRule: false, + }); + hookResult = renderHook((props: UseRuleDetailsParams) => useRuleDetails(props), { + initialProps, + }); + expect(hookResult.result.current.loading).toBe(false); + expect(hookResult.result.current.isExistingRule).toBe(false); + expect(hookResult.result.current.rule).toBe(null); + }); + + it('should return rule data when rule is loaded', () => { + mockUseRuleWithFallback.mockReturnValue({ + rule: { id: 'ruleId' }, + loading: false, + isExistingRule: true, + }); + + hookResult = renderHook((props: UseRuleDetailsParams) => useRuleDetails(props), { + initialProps, + }); + expect(hookResult.result.current.loading).toBe(false); + expect(hookResult.result.current.isExistingRule).toBe(true); + expect(hookResult.result.current.rule).toEqual({ id: 'ruleId' }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/hooks/use_rule_details.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/hooks/use_rule_details.tsx new file mode 100644 index 000000000000000..33f093acbe2c354 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/hooks/use_rule_details.tsx @@ -0,0 +1,56 @@ +/* + * 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 { useEffect, useMemo, useState } from 'react'; +import type { RuleResponse } from '../../../../common/api/detection_engine'; +import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback'; + +export interface UseRuleDetailsParams { + /** + * Id of the rule + */ + ruleId: string; +} + +export interface UseRuleDetailsResult { + /** + * Whether the rule exists + */ + isExistingRule: boolean; + /** + * Whether the data is loading + */ + loading: boolean; + /** + * Rule object that represents relevant information about a rule + */ + rule: RuleResponse | null; +} + +/** + * Hook to retrieve rule details for rule details flyout + */ +export const useRuleDetails = ({ ruleId }: UseRuleDetailsParams): UseRuleDetailsResult => { + const [rule, setRule] = useState(null); + const { rule: maybeRule, loading, isExistingRule } = useRuleWithFallback(ruleId ?? ''); + + // persist rule until refresh is complete + useEffect(() => { + if (maybeRule != null) { + setRule(maybeRule); + } + }, [maybeRule]); + + return useMemo( + () => ({ + rule, + loading, + isExistingRule, + }), + [loading, isExistingRule, rule] + ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.test.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.test.tsx new file mode 100644 index 000000000000000..f1e276011ca268f --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.test.tsx @@ -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 { render } from '@testing-library/react'; +import React from 'react'; +import { RULE_PREVIEW_FOOTER_TEST_ID, RULE_PREVIEW_OPEN_RULE_FLYOUT_TEST_ID } from './test_ids'; +import { PreviewFooter } from './footer'; +import { mockFlyoutApi } from '../../document_details/shared/mocks/mock_flyout_context'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { RulePanelKey } from '../right'; + +jest.mock('@kbn/expandable-flyout'); + +const renderRulePreviewFooter = () => render(); + +describe('', () => { + beforeAll(() => { + jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); + }); + + it('should render rule details link correctly when ruleId is available', () => { + const { getByTestId } = renderRulePreviewFooter(); + + expect(getByTestId(RULE_PREVIEW_FOOTER_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RULE_PREVIEW_OPEN_RULE_FLYOUT_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RULE_PREVIEW_OPEN_RULE_FLYOUT_TEST_ID)).toHaveTextContent( + 'Show full rule details' + ); + }); + + it('should open rule flyout when clicked', () => { + const { getByTestId } = renderRulePreviewFooter(); + + getByTestId(RULE_PREVIEW_OPEN_RULE_FLYOUT_TEST_ID).click(); + + expect(mockFlyoutApi.openFlyout).toHaveBeenCalledWith({ + right: { id: RulePanelKey, params: { ruleId: 'ruleid' } }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/footer.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.tsx similarity index 51% rename from x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/footer.tsx rename to x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.tsx index aca0d23027a6197..1774c37d9e53513 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/rule_overview/components/footer.tsx +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.tsx @@ -5,29 +5,39 @@ * 2.0. */ -import React, { memo } from 'react'; +import React, { memo, useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FlyoutFooter } from '@kbn/security-solution-common'; -import { useRuleOverviewPanelContext } from '../context'; -import { RULE_OVERVIEW_FOOTER_TEST_ID, RULE_OVERVIEW_NAVIGATE_TO_RULE_TEST_ID } from './test_ids'; -import { useRuleDetailsLink } from '../../shared/hooks/use_rule_details_link'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { RULE_PREVIEW_FOOTER_TEST_ID, RULE_PREVIEW_OPEN_RULE_FLYOUT_TEST_ID } from './test_ids'; +import { RulePanelKey } from '../right'; /** * Footer in rule preview panel */ -export const RuleFooter = memo(() => { - const { ruleId } = useRuleOverviewPanelContext(); - const href = useRuleDetailsLink({ ruleId }); +export const PreviewFooter = memo(({ ruleId }: { ruleId: string }) => { + const { openFlyout } = useExpandableFlyoutApi(); - return href ? ( - + const openRuleFlyout = useCallback(() => { + openFlyout({ + right: { + id: RulePanelKey, + params: { + ruleId, + }, + }, + }); + }, [openFlyout, ruleId]); + + return ( + {i18n.translate('xpack.securitySolution.flyout.preview.rule.viewDetailsLabel', { defaultMessage: 'Show full rule details', @@ -36,7 +46,7 @@ export const RuleFooter = memo(() => { - ) : null; + ); }); -RuleFooter.displayName = 'RuleFooter'; +PreviewFooter.displayName = 'PreviewFooter'; diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/preview/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/rule_details/preview/test_ids.ts new file mode 100644 index 000000000000000..7cb938cbdce7de3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/preview/test_ids.ts @@ -0,0 +1,15 @@ +/* + * 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 { PREFIX } from '../../shared/test_ids'; + +const RULE_PREVIEW_TEST_ID = `${PREFIX}RulePreviewPanel` as const; + +export const RULE_PREVIEW_FOOTER_TEST_ID = `${RULE_PREVIEW_TEST_ID}Footer` as const; + +export const RULE_PREVIEW_OPEN_RULE_FLYOUT_TEST_ID = + `${RULE_PREVIEW_FOOTER_TEST_ID}OpenRuleFlyout` as const; diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/right/content.test.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/right/content.test.tsx new file mode 100644 index 000000000000000..1a65f9127358973 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/right/content.test.tsx @@ -0,0 +1,98 @@ +/* + * 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 { PanelContent } from './content'; +import { ThemeProvider } from 'styled-components'; +import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock'; +import { TestProviders } from '../../../common/mock'; +import { useRuleDetails } from '../hooks/use_rule_details'; +import { getStepsData } from '../../../detections/pages/detection_engine/rules/helpers'; +import { + mockAboutStepRule, + mockDefineStepRule, + mockScheduleStepRule, +} from '../../../detection_engine/rule_management_ui/components/rules_table/__mocks__/mock'; +import type { RuleResponse } from '../../../../common/api/detection_engine'; +import { + BODY_TEST_ID, + ABOUT_HEADER_TEST_ID, + ABOUT_CONTENT_TEST_ID, + DEFINITION_HEADER_TEST_ID, + DEFINITION_CONTENT_TEST_ID, + SCHEDULE_HEADER_TEST_ID, + SCHEDULE_CONTENT_TEST_ID, + ACTIONS_HEADER_TEST_ID, + ACTIONS_CONTENT_TEST_ID, +} from './test_ids'; + +const mockUseRuleDetails = useRuleDetails as jest.Mock; +jest.mock('../hooks/use_rule_details'); + +const mockGetStepsData = getStepsData as jest.Mock; +jest.mock('../../../detections/pages/detection_engine/rules/helpers'); + +const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } }); +const rule = { name: 'rule name', description: 'rule description' } as RuleResponse; + +const renderRulePreview = () => + render( + + + + + + ); + +describe('', () => { + afterEach(() => { + jest.clearAllMocks(); + mockUseRuleDetails.mockReturnValue({ + rule, + loading: false, + isExistingRule: true, + }); + }); + + it('should render rule preview and its sub sections', () => { + mockGetStepsData.mockReturnValue({ + aboutRuleData: mockAboutStepRule(), + defineRuleData: mockDefineStepRule(), + scheduleRuleData: mockScheduleStepRule(), + ruleActionsData: { actions: ['action'] }, + }); + + const { getByTestId } = renderRulePreview(); + + expect(getByTestId(BODY_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(ABOUT_HEADER_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(ABOUT_HEADER_TEST_ID)).toHaveTextContent('About'); + expect(getByTestId(ABOUT_CONTENT_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(DEFINITION_HEADER_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(DEFINITION_HEADER_TEST_ID)).toHaveTextContent('Definition'); + expect(getByTestId(DEFINITION_CONTENT_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(SCHEDULE_HEADER_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(SCHEDULE_HEADER_TEST_ID)).toHaveTextContent('Schedule'); + expect(getByTestId(SCHEDULE_CONTENT_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(ACTIONS_HEADER_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(ACTIONS_HEADER_TEST_ID)).toHaveTextContent('Actions'); + expect(getByTestId(ACTIONS_CONTENT_TEST_ID)).toBeInTheDocument(); + }); + + it('should not render actions if action is not available', () => { + mockGetStepsData.mockReturnValue({ + aboutRuleData: mockAboutStepRule(), + defineRuleData: mockDefineStepRule(), + scheduleRuleData: mockScheduleStepRule(), + }); + const { queryByTestId } = renderRulePreview(); + + expect(queryByTestId(ACTIONS_HEADER_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(ACTIONS_CONTENT_TEST_ID)).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/right/content.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/right/content.tsx new file mode 100644 index 000000000000000..2778fc9c7ca2237 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/right/content.tsx @@ -0,0 +1,145 @@ +/* + * 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 { EuiText, EuiHorizontalRule, EuiSpacer, EuiPanel } from '@elastic/eui'; +import { css } from '@emotion/css'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { FlyoutBody } from '@kbn/security-solution-common'; +import { ExpandableSection } from '../../document_details/right/components/expandable_section'; +import { RuleAboutSection } from '../../../detection_engine/rule_management/components/rule_details/rule_about_section'; +import { RuleScheduleSection } from '../../../detection_engine/rule_management/components/rule_details/rule_schedule_section'; +import { RuleDefinitionSection } from '../../../detection_engine/rule_management/components/rule_details/rule_definition_section'; +import { StepRuleActionsReadOnly } from '../../../detection_engine/rule_creation/components/step_rule_actions'; +import { getStepsData } from '../../../detections/pages/detection_engine/rules/helpers'; +import { + BODY_TEST_ID, + ABOUT_TEST_ID, + DEFINITION_TEST_ID, + SCHEDULE_TEST_ID, + ACTIONS_TEST_ID, +} from './test_ids'; +import type { RuleResponse } from '../../../../common/api/detection_engine'; + +const panelViewStyle = css` + dt { + font-size: 90% !important; + } + text-overflow: ellipsis; + .euiFlexGroup { + flex-wrap: inherit; + } + + .euiFlexItem { + inline-size: inherit; + flex-basis: inherit; + } +`; + +export interface RuleDetailsProps { + /** + * Rule object that represents relevant information about a rule + */ + rule: RuleResponse; +} + +/** + * Rule details content on the right section of expandable flyout + */ +export const PanelContent = memo(({ rule }: RuleDetailsProps) => { + const { ruleActionsData } = + rule != null ? getStepsData({ rule, detailsView: true }) : { ruleActionsData: null }; + + const hasNotificationActions = Boolean(ruleActionsData?.actions?.length); + const hasResponseActions = Boolean(ruleActionsData?.responseActions?.length); + const hasActions = ruleActionsData != null && (hasNotificationActions || hasResponseActions); + + return ( + + + + } + expanded + data-test-subj={ABOUT_TEST_ID} + > + {rule.description} + + + + + + } + expanded={false} + data-test-subj={DEFINITION_TEST_ID} + > + + + + + } + expanded={false} + data-test-subj={SCHEDULE_TEST_ID} + > + + + + {hasActions && ruleActionsData != null && ( + + } + expanded={false} + data-test-subj={ACTIONS_TEST_ID} + > + + + )} + + + ); +}); + +PanelContent.displayName = 'PanelContent'; diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/right/header.test.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/right/header.test.tsx new file mode 100644 index 000000000000000..06919bac4a9ab57 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/right/header.test.tsx @@ -0,0 +1,60 @@ +/* + * 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 type { PanelHeaderProps } from './header'; +import { PanelHeader } from './header'; +import { TestProvider as ExpandableFlyoutTestProvider } from '@kbn/expandable-flyout/src/test/provider'; +import { TestProviders } from '../../../common/mock'; +import { useRuleDetailsLink } from '../../document_details/shared/hooks/use_rule_details_link'; +import type { Rule } from '../../../detection_engine/rule_management/logic'; +import { + RULE_TITLE_TEST_ID, + RULE_CREATED_BY_TEST_ID, + RULE_UPDATED_BY_TEST_ID, + RULE_TITLE_SUPPRESSED_TEST_ID, + NAVIGATE_TO_RULE_DETAILS_PAGE_TEST_ID, +} from './test_ids'; + +jest.mock('../../document_details/shared/hooks/use_rule_details_link'); +const defaultProps = { + rule: { id: 'id', name: 'rule' } as Rule, + isSuppressed: false, +}; + +const renderRuleTitle = (props: PanelHeaderProps) => + render( + + + + + + ); + +describe('', () => { + it('should render title and its components', () => { + (useRuleDetailsLink as jest.Mock).mockReturnValue('rule_details_link'); + const { getByTestId, queryByTestId } = renderRuleTitle(defaultProps); + + expect(getByTestId(RULE_TITLE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(NAVIGATE_TO_RULE_DETAILS_PAGE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RULE_CREATED_BY_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RULE_UPDATED_BY_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(RULE_TITLE_SUPPRESSED_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should render deleted rule badge', () => { + (useRuleDetailsLink as jest.Mock).mockReturnValue('rule_details_link'); + const props = { + ...defaultProps, + isSuppressed: true, + }; + const { getByTestId } = renderRuleTitle(props); + expect(getByTestId(RULE_TITLE_SUPPRESSED_TEST_ID)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/right/header.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/right/header.tsx new file mode 100644 index 000000000000000..3dbbcc6b5b25929 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/right/header.tsx @@ -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 React from 'react'; +import { + EuiTitle, + EuiText, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiLink, +} from '@elastic/eui'; +import { FlyoutHeader, FlyoutTitle } from '@kbn/security-solution-common'; +import { DELETED_RULE } from '../../../detection_engine/rule_details_ui/pages/rule_details/translations'; +import { CreatedBy, UpdatedBy } from '../../../detections/components/rules/rule_info'; +import { + RULE_TITLE_TEST_ID, + RULE_CREATED_BY_TEST_ID, + RULE_UPDATED_BY_TEST_ID, + RULE_TITLE_SUPPRESSED_TEST_ID, + NAVIGATE_TO_RULE_DETAILS_PAGE_TEST_ID, +} from './test_ids'; +import type { RuleResponse } from '../../../../common/api/detection_engine'; +import { useRuleDetailsLink } from '../../document_details/shared/hooks/use_rule_details_link'; + +export interface PanelHeaderProps { + /** + * Rule object that represents relevant information about a rule + */ + rule: RuleResponse; + /** + * Flag to indicate if rule is suppressed + */ + isSuppressed: boolean; +} + +/** + * Title component that shows basic information of a rule. This is displayed above rule overview body + */ +export const PanelHeader: React.FC = ({ rule, isSuppressed }) => { + const href = useRuleDetailsLink({ ruleId: rule.id }); + + return ( + + {href ? ( + + + + ) : ( + +
{rule.name}
+
+ )} + + {isSuppressed && ( + <> + + + {DELETED_RULE} + + + )} + + + + + + + + + + + + + +
+ ); +}; + +PanelHeader.displayName = 'PanelHeader'; diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx new file mode 100644 index 000000000000000..1ce755575450c55 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx @@ -0,0 +1,104 @@ +/* + * 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 { ThemeProvider } from 'styled-components'; +import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock'; +import { TestProviders } from '../../../common/mock'; +// import { TestProvider } from '@kbn/expandable-flyout/src/test/provider'; +import { RulePanel } from '.'; +import { getStepsData } from '../../../detections/pages/detection_engine/rules/helpers'; +import { useRuleDetails } from '../hooks/use_rule_details'; +import { + mockAboutStepRule, + mockDefineStepRule, + mockScheduleStepRule, +} from '../../../detection_engine/rule_management_ui/components/rules_table/__mocks__/mock'; +import type { RuleResponse } from '../../../../common/api/detection_engine'; +import { BODY_TEST_ID, LOADING_TEST_ID } from './test_ids'; +import { RULE_PREVIEW_FOOTER_TEST_ID } from '../preview/test_ids'; + +const mockUseRuleDetails = useRuleDetails as jest.Mock; +jest.mock('../hooks/use_rule_details'); + +const mockGetStepsData = getStepsData as jest.Mock; +jest.mock('../../../detections/pages/detection_engine/rules/helpers'); + +const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } }); +const rule = { name: 'rule name', description: 'rule description' } as RuleResponse; +const ERROR_MESSAGE = 'There was an error displaying data.'; + +const renderRulePanel = (isPreviewMode = false) => + render( + + + + + + ); + +describe('', () => { + it('should render rule details and its sub sections', () => { + mockUseRuleDetails.mockReturnValue({ + rule, + loading: false, + isExistingRule: true, + }); + mockGetStepsData.mockReturnValue({ + aboutRuleData: mockAboutStepRule(), + defineRuleData: mockDefineStepRule(), + scheduleRuleData: mockScheduleStepRule(), + ruleActionsData: { actions: ['action'] }, + }); + + const { getByTestId, queryByTestId, queryByText } = renderRulePanel(); + + expect(getByTestId(BODY_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(LOADING_TEST_ID)).not.toBeInTheDocument(); + expect(queryByText(ERROR_MESSAGE)).not.toBeInTheDocument(); + }); + + it('should render loading spinner when rule is loading', () => { + mockUseRuleDetails.mockReturnValue({ + rule: null, + loading: true, + isExistingRule: true, + }); + mockGetStepsData.mockReturnValue({}); + const { getByTestId } = renderRulePanel(); + + expect(getByTestId(LOADING_TEST_ID)).toBeInTheDocument(); + }); + + it('should render error message when rule is null', () => { + mockUseRuleDetails.mockReturnValue({ + rule: null, + loading: false, + isExistingRule: true, + }); + mockGetStepsData.mockReturnValue({}); + const { queryByTestId, getByText } = renderRulePanel(); + + expect(queryByTestId(BODY_TEST_ID)).not.toBeInTheDocument(); + expect(getByText(ERROR_MESSAGE)).toBeInTheDocument(); + }); + + it('should render preview footer when isPreviewMode is true', () => { + mockUseRuleDetails.mockReturnValue({ + rule, + loading: false, + isExistingRule: true, + }); + mockGetStepsData.mockReturnValue({}); + const { getByTestId } = renderRulePanel(true); + + // await act(async () => { + expect(getByTestId(RULE_PREVIEW_FOOTER_TEST_ID)).toBeInTheDocument(); + // }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.tsx new file mode 100644 index 000000000000000..958a2d42651866f --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.tsx @@ -0,0 +1,68 @@ +/* + * 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 type { FlyoutPanelProps } from '@kbn/expandable-flyout'; +import { FlyoutLoading, FlyoutError, FlyoutNavigation } from '@kbn/security-solution-common'; +import { i18n } from '@kbn/i18n'; +import { PanelContent } from './content'; +import { PanelHeader } from './header'; +import { PreviewFooter } from '../preview/footer'; +import { useRuleDetails } from '../hooks/use_rule_details'; +import { LOADING_TEST_ID } from './test_ids'; + +export interface RulePanelExpandableFlyoutProps extends FlyoutPanelProps { + key: 'rule-panel' | 'rule-preview-panel'; + params: { + ruleId: string; + isPreviewMode?: boolean; + }; +} + +export const RulePanelKey: RulePanelExpandableFlyoutProps['key'] = 'rule-panel'; +export const RulePreviewPanelKey: RulePanelExpandableFlyoutProps['key'] = 'rule-preview-panel'; + +export const RULE_PREVIEW_BANNER = { + title: i18n.translate('xpack.securitySolution.flyout.right.rule.rulePreviewTitle', { + defaultMessage: 'Preview rule details', + }), + backgroundColor: 'warning', + textColor: 'warning', +}; + +export interface RulePanelProps extends Record { + /** + * Rule ID + */ + ruleId: string; + /** + * If in preview mode, show preview banner and footer + */ + isPreviewMode?: boolean; +} + +/** + * Displays a rule overview panel + */ +export const RulePanel = memo(({ ruleId, isPreviewMode }: RulePanelProps) => { + const { rule, loading, isExistingRule } = useRuleDetails({ ruleId }); + + return loading ? ( + + ) : rule ? ( + <> + + + + {isPreviewMode && } + + ) : ( + + ); +}); + +RulePanel.displayName = 'RulePanel'; diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/right/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/rule_details/right/test_ids.ts new file mode 100644 index 000000000000000..0e0073535efba71 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/right/test_ids.ts @@ -0,0 +1,40 @@ +/* + * 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 { PREFIX } from '../../shared/test_ids'; +import { + CONTENT_TEST_ID, + HEADER_TEST_ID, +} from '../../document_details/right/components/expandable_section'; + +const RULE_PANEL_TEST_ID = `${PREFIX}RulePanel` as const; +export const RULE_TITLE_TEST_ID = `${RULE_PANEL_TEST_ID}Title` as const; +export const NAVIGATE_TO_RULE_DETAILS_PAGE_TEST_ID = + `${RULE_PANEL_TEST_ID}LinkToRuleDetailsPage` as const; + +export const RULE_TITLE_SUPPRESSED_TEST_ID = `${RULE_TITLE_TEST_ID}Suppressed` as const; +export const RULE_CREATED_BY_TEST_ID = `${RULE_PANEL_TEST_ID}CreatedByText` as const; +export const RULE_UPDATED_BY_TEST_ID = `${RULE_PANEL_TEST_ID}UpdatedByText` as const; +export const BODY_TEST_ID = `${RULE_PANEL_TEST_ID}Body` as const; + +export const ABOUT_TEST_ID = `${RULE_PANEL_TEST_ID}AboutSection` as const; +export const ABOUT_HEADER_TEST_ID = ABOUT_TEST_ID + HEADER_TEST_ID; +export const ABOUT_CONTENT_TEST_ID = ABOUT_TEST_ID + CONTENT_TEST_ID; + +export const DEFINITION_TEST_ID = `${RULE_PANEL_TEST_ID}DefinitionSection` as const; +export const DEFINITION_HEADER_TEST_ID = DEFINITION_TEST_ID + HEADER_TEST_ID; +export const DEFINITION_CONTENT_TEST_ID = DEFINITION_TEST_ID + CONTENT_TEST_ID; + +export const SCHEDULE_TEST_ID = `${RULE_PANEL_TEST_ID}ScheduleSection` as const; +export const SCHEDULE_HEADER_TEST_ID = SCHEDULE_TEST_ID + HEADER_TEST_ID; +export const SCHEDULE_CONTENT_TEST_ID = SCHEDULE_TEST_ID + CONTENT_TEST_ID; + +export const ACTIONS_TEST_ID = `${RULE_PANEL_TEST_ID}ActionsSection` as const; +export const ACTIONS_HEADER_TEST_ID = ACTIONS_TEST_ID + HEADER_TEST_ID; +export const ACTIONS_CONTENT_TEST_ID = ACTIONS_TEST_ID + CONTENT_TEST_ID; + +export const LOADING_TEST_ID = `${RULE_PANEL_TEST_ID}Loading` as const; diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.test.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.test.tsx index 6ffc418c7e54c49..c1dfc456f40ce39 100644 --- a/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.test.tsx @@ -17,6 +17,7 @@ import { HOST_PREVIEW_BANNER } from '../../document_details/right/components/hos import { UserPreviewPanelKey } from '../../entity_details/user_right'; import { USER_PREVIEW_BANNER } from '../../document_details/right/components/user_entity_overview'; import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../network_details'; +import { RulePreviewPanelKey, RULE_PREVIEW_BANNER } from '../../rule_details/right'; import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock'; const mockedTelemetry = createTelemetryServiceMock(); @@ -43,7 +44,13 @@ jest.mock('@kbn/expandable-flyout', () => ({ const renderPreviewLink = (field: string, value: string, dataTestSuj?: string) => render( - + ); @@ -111,6 +118,49 @@ describe('', () => { }, }); }); + + it('should render a link to open rule preview', () => { + const { getByTestId } = renderPreviewLink('kibana.alert.rule.name', 'ruleId', 'rule-link'); + getByTestId('rule-link').click(); + + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ + id: RulePreviewPanelKey, + params: { + ruleId: 'ruleId', + banner: RULE_PREVIEW_BANNER, + isPreviewMode: true, + }, + }); + }); + + it('should not render a link when ruleId is not provided', () => { + const { queryByTestId } = render( + + + + ); + expect(queryByTestId('rule-link')).not.toBeInTheDocument(); + }); + + it('should not render a link when rule name is rendered in rule preview', () => { + const { queryByTestId } = render( + + + + ); + expect(queryByTestId('rule-link')).not.toBeInTheDocument(); + }); }); describe('hasPreview', () => { @@ -122,6 +172,10 @@ describe('hasPreview', () => { expect(hasPreview('user.name')).toBe(true); }); + it('should return true if field is rule.id', () => { + expect(hasPreview('kibana.alert.rule.name')).toBe(true); + }); + it('should return true if field type is source.ip', () => { expect(hasPreview('source.ip')).toBe(true); expect(hasPreview('destination.ip')).toBe(true); diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx index ceb1f216703c7b8..fc51a4e64e6c02c 100644 --- a/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx +++ b/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx @@ -13,6 +13,7 @@ import { getEcsField } from '../../document_details/right/components/table_field import { HOST_NAME_FIELD_NAME, USER_NAME_FIELD_NAME, + SIGNAL_RULE_NAME_FIELD_NAME, IP_FIELD_TYPE, } from '../../../timelines/components/timeline/body/renderers/constants'; import { useKibana } from '../../../common/lib/kibana'; @@ -22,12 +23,13 @@ import { HOST_PREVIEW_BANNER } from '../../document_details/right/components/hos import { UserPreviewPanelKey } from '../../entity_details/user_right'; import { USER_PREVIEW_BANNER } from '../../document_details/right/components/user_entity_overview'; import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../network_details'; +import { RulePreviewPanelKey, RULE_PREVIEW_BANNER } from '../../rule_details/right'; + +const PREVIEW_FIELDS = [HOST_NAME_FIELD_NAME, USER_NAME_FIELD_NAME, SIGNAL_RULE_NAME_FIELD_NAME]; // Helper function to check if the field has a preview link export const hasPreview = (field: string) => - field === HOST_NAME_FIELD_NAME || - field === USER_NAME_FIELD_NAME || - getEcsField(field)?.type === IP_FIELD_TYPE; + PREVIEW_FIELDS.includes(field) || getEcsField(field)?.type === IP_FIELD_TYPE; interface PreviewParams { id: string; @@ -35,7 +37,12 @@ interface PreviewParams { } // Helper get function to get the preview parameters -const getPreviewParams = (value: string, field: string, scopeId: string): PreviewParams | null => { +const getPreviewParams = ( + value: string, + field: string, + scopeId: string, + ruleId?: string +): PreviewParams | null => { if (getEcsField(field)?.type === IP_FIELD_TYPE) { return { id: NetworkPanelKey, @@ -48,6 +55,9 @@ const getPreviewParams = (value: string, field: string, scopeId: string): Previe }, }; } + if (field === SIGNAL_RULE_NAME_FIELD_NAME && !ruleId) { + return null; + } switch (field) { case HOST_NAME_FIELD_NAME: return { @@ -59,6 +69,11 @@ const getPreviewParams = (value: string, field: string, scopeId: string): Previe id: UserPreviewPanelKey, params: { userName: value, scopeId, banner: USER_PREVIEW_BANNER }, }; + case SIGNAL_RULE_NAME_FIELD_NAME: + return { + id: RulePreviewPanelKey, + params: { ruleId, banner: RULE_PREVIEW_BANNER, isPreviewMode: true }, + }; default: return null; } @@ -78,6 +93,14 @@ interface PreviewLinkProps { * Scope id to use for the preview panel */ scopeId: string; + /** + * Rule id to use for the preview panel + */ + ruleId?: string; + /** + * Whether the preview link is in preview mode + */ + isPreview?: boolean; /** * Optional data-test-subj value */ @@ -95,6 +118,8 @@ export const PreviewLink: FC = ({ field, value, scopeId, + ruleId, + isPreview, children, 'data-test-subj': dataTestSubj = FLYOUT_PREVIEW_LINK_TEST_ID, }) => { @@ -102,7 +127,7 @@ export const PreviewLink: FC = ({ const { telemetry } = useKibana().services; const onClick = useCallback(() => { - const previewParams = getPreviewParams(value, field, scopeId); + const previewParams = getPreviewParams(value, field, scopeId, ruleId); if (previewParams) { openPreviewPanel({ id: previewParams.id, @@ -113,12 +138,18 @@ export const PreviewLink: FC = ({ panel: 'preview', }); } - }, [field, scopeId, value, telemetry, openPreviewPanel]); + }, [field, scopeId, value, telemetry, openPreviewPanel, ruleId]); + // If the field is not previewable, do not render link if (!hasPreview(field)) { return <>{children ?? value}; } + // If the field is rule.id, and the ruleId is not provided or currently in rule preview, do not render link + if (field === SIGNAL_RULE_NAME_FIELD_NAME && (!ruleId || isPreview)) { + return <>{children ?? value}; + } + return ( {children ?? value} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx index c0457fffabc3689..d41c0238ce59286 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx @@ -9,17 +9,15 @@ import type { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; import { EuiLink, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiToolTip } from '@elastic/eui'; import { isString, isEmpty } from 'lodash/fp'; import type { SyntheticEvent } from 'react'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useContext } from 'react'; import styled from 'styled-components'; - +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { DefaultDraggable } from '../../../../../common/components/draggables'; import { getEmptyTagValue } from '../../../../../common/components/empty_value'; import { getRuleDetailsUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { TruncatableText } from '../../../../../common/components/truncatable_text'; - import { isUrlInvalid } from '../../../../../common/utils/validators'; import endPointSvg from '../../../../../common/utils/logo_endpoint/64_color.svg'; - import * as i18n from './translations'; import { SecurityPageName } from '../../../../../app/types'; import { useFormatUrl } from '../../../../../common/components/link_to'; @@ -27,6 +25,8 @@ import { useKibana } from '../../../../../common/lib/kibana'; import { APP_UI_ID } from '../../../../../../common/constants'; import { LinkAnchor } from '../../../../../common/components/links'; import { GenericLinkButton } from '../../../../../common/components/links/helpers'; +import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context'; +import { RulePanelKey } from '../../../../../flyout/rule_details/right'; const EventModuleFlexItem = styled(EuiFlexItem)` width: 100%; @@ -67,21 +67,38 @@ export const RenderRuleName: React.FC = ({ title, value, }) => { + const { openRightPanel } = useExpandableFlyoutApi(); + const eventContext = useContext(StatefulEventContext); + const ruleName = `${value}`; const ruleId = linkValue; const { search } = useFormatUrl(SecurityPageName.rules); const { navigateToApp, getUrlForApp } = useKibana().services.application; + const isInTimelineContext = + ruleName && eventContext?.enableHostDetailsFlyout && eventContext?.timelineID; + const goToRuleDetails = useCallback( (ev: React.SyntheticEvent) => { ev.preventDefault(); - navigateToApp(APP_UI_ID, { - deepLinkId: SecurityPageName.rules, - path: getRuleDetailsUrl(ruleId ?? '', search), - openInNewTab, + + if (!eventContext || !isInTimelineContext) { + navigateToApp(APP_UI_ID, { + deepLinkId: SecurityPageName.rules, + path: getRuleDetailsUrl(ruleId ?? '', search), + openInNewTab, + }); + return; + } + + openRightPanel({ + id: RulePanelKey, + params: { + ruleId, + }, }); }, - [navigateToApp, ruleId, search, openInNewTab] + [navigateToApp, ruleId, search, openInNewTab, openRightPanel, eventContext, isInTimelineContext] ); const href = useMemo( diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index dd4d2709bd4cf71..583b571bbc73ffe 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -39118,7 +39118,6 @@ "xpack.securitySolution.flyout.preview.rule.viewDetailsLabel": "Afficher l’ensemble des détails de la règle", "xpack.securitySolution.flyout.right.about.description.documentTitle": "Description du document", "xpack.securitySolution.flyout.right.about.description.noRuleDescription": "Il n'existe pas de description pour cette règle.", - "xpack.securitySolution.flyout.right.about.description.rulePreviewTitle": "Afficher les détails de la règle", "xpack.securitySolution.flyout.right.about.description.ruleSummaryButtonAriaLabel": "Afficher le résumé de la règle", "xpack.securitySolution.flyout.right.about.description.ruleSummaryButtonLabel": "Afficher le résumé de la règle", "xpack.securitySolution.flyout.right.about.description.ruleTitle": "Description de la règle", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1e6b0cb9dab3517..70d5a3b861b9921 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -39101,7 +39101,6 @@ "xpack.securitySolution.flyout.preview.rule.viewDetailsLabel": "完全なルール詳細を表示", "xpack.securitySolution.flyout.right.about.description.documentTitle": "ドキュメント説明", "xpack.securitySolution.flyout.right.about.description.noRuleDescription": "このルールに関する説明はありません。", - "xpack.securitySolution.flyout.right.about.description.rulePreviewTitle": "ルール詳細をプレビュー", "xpack.securitySolution.flyout.right.about.description.ruleSummaryButtonAriaLabel": "ルール概要を表示", "xpack.securitySolution.flyout.right.about.description.ruleSummaryButtonLabel": "ルール概要を表示", "xpack.securitySolution.flyout.right.about.description.ruleTitle": "ルールの説明", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a2dd46f2f7c18b9..5ba58da581710aa 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -39145,7 +39145,6 @@ "xpack.securitySolution.flyout.preview.rule.viewDetailsLabel": "显示完整规则详情", "xpack.securitySolution.flyout.right.about.description.documentTitle": "文档描述", "xpack.securitySolution.flyout.right.about.description.noRuleDescription": "此规则没有订阅。", - "xpack.securitySolution.flyout.right.about.description.rulePreviewTitle": "预览规则详情", "xpack.securitySolution.flyout.right.about.description.ruleSummaryButtonAriaLabel": "显示规则摘要", "xpack.securitySolution.flyout.right.about.description.ruleSummaryButtonLabel": "显示规则摘要", "xpack.securitySolution.flyout.right.about.description.ruleTitle": "规则描述", diff --git a/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_preview_panel_rule_preview.ts b/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_preview_panel_rule_preview.ts index 1c511e090103f5c..d6e800517fdcbe2 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_preview_panel_rule_preview.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_preview_panel_rule_preview.ts @@ -8,30 +8,30 @@ import { getDataTestSubjectSelector } from '../../helpers/common'; export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_TITLE = getDataTestSubjectSelector( - 'securitySolutionFlyoutRuleOverviewRuleOverviewTitle' + 'securitySolutionFlyoutRulePanelTitle' ); export const DOCUMENT_DETAILS_FLYOUT_CREATED_BY = getDataTestSubjectSelector( - 'securitySolutionFlyoutRuleOverviewCreatedByText' + 'securitySolutionFlyoutRulePanelCreatedByText' ); export const DOCUMENT_DETAILS_FLYOUT_UPDATED_BY = getDataTestSubjectSelector( - 'securitySolutionFlyoutRuleOverviewUpdatedByText' + 'securitySolutionFlyoutRulePanelUpdatedByText' ); export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_HEADER = getDataTestSubjectSelector( - 'securitySolutionFlyoutRuleOverviewAboutSectionHeader' + 'securitySolutionFlyoutRulePanelAboutSectionHeader' ); export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_CONTENT = - getDataTestSubjectSelector('securitySolutionFlyoutRuleOverviewAboutSectionContent'); + getDataTestSubjectSelector('securitySolutionFlyoutRulePanelAboutSectionContent'); export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_DEFINITION_SECTION_HEADER = - getDataTestSubjectSelector('securitySolutionFlyoutRuleOverviewDefinitionSectionHeader'); + getDataTestSubjectSelector('securitySolutionFlyoutRulePanelDefinitionSectionHeader'); export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_DEFINITION_SECTION_CONTENT = - getDataTestSubjectSelector('securitySolutionFlyoutRuleOverviewDefinitionSectionContent'); + getDataTestSubjectSelector('securitySolutionFlyoutRulePanelDefinitionSectionContent'); export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SCHEDULE_SECTION_HEADER = - getDataTestSubjectSelector('securitySolutionFlyoutRuleOverviewScheduleSectionHeader'); + getDataTestSubjectSelector('securitySolutionFlyoutRulePanelScheduleSectionHeader'); export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SCHEDULE_SECTION_CONTENT = - getDataTestSubjectSelector('securitySolutionFlyoutRuleOverviewScheduleSectionContent'); + getDataTestSubjectSelector('securitySolutionFlyoutRulePanelScheduleSectionContent'); export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_FOOTER = getDataTestSubjectSelector( - 'securitySolutionFlyoutRuleOverviewFooter' + 'securitySolutionFlyoutRulePreviewPanelFooter' ); export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_FOOTER_LINK = getDataTestSubjectSelector( - 'securitySolutionFlyoutRuleOverviewFooterLinkToRuleDetails' + 'securitySolutionFlyoutRulePreviewPanelFooterOpenRuleFlyout' );