From f1da4a1d90f4d7cbb847b7cc6a633a5b2a771482 Mon Sep 17 00:00:00 2001 From: christineweng Date: Fri, 28 Jul 2023 13:56:37 -0500 Subject: [PATCH 1/4] add rule preview contents --- .../src/components/preview_section.tsx | 34 ++--- ...t_details_preview_panel_rule_preview.cy.ts | 18 ++- ...lert_details_preview_panel_rule_preview.ts | 17 +++ .../alert_details_right_panel_overview_tab.ts | 1 + .../rules/description_step/helpers.tsx | 15 +- .../rules/description_step/index.tsx | 21 +++ .../rules/step_about_rule/index.tsx | 9 +- .../rules/step_define_rule/index.tsx | 3 + .../rules/step_schedule_rule/index.tsx | 9 +- .../preview/components/rule_preview.test.tsx | 125 +++++++++++++++-- .../preview/components/rule_preview.tsx | 121 +++++++++++----- .../components/rule_preview_title.test.tsx | 73 ++++++++++ .../preview/components/rule_preview_title.tsx | 131 ++++++++++++++++++ .../flyout/preview/components/test_ids.ts | 14 ++ .../flyout/preview/components/translations.ts | 15 ++ .../public/flyout/preview/context.tsx | 18 ++- .../preview/hooks/use_rule_switch.test.tsx | 101 ++++++++++++++ .../flyout/preview/hooks/use_rule_switch.ts | 77 ++++++++++ .../public/flyout/preview/index.tsx | 17 +-- .../mocks/mock_preview_panel_context.ts | 1 + .../public/flyout/preview/panels.tsx | 2 +- 21 files changed, 737 insertions(+), 85 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview_title.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview_title.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/preview/hooks/use_rule_switch.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/preview/hooks/use_rule_switch.ts diff --git a/packages/kbn-expandable-flyout/src/components/preview_section.tsx b/packages/kbn-expandable-flyout/src/components/preview_section.tsx index 72aa87e388150..60e86ef4195bc 100644 --- a/packages/kbn-expandable-flyout/src/components/preview_section.tsx +++ b/packages/kbn-expandable-flyout/src/components/preview_section.tsx @@ -17,13 +17,12 @@ import { } 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, + PREVIEW_SECTION, } from './test_ids'; import { useExpandableFlyoutContext } from '../..'; import { BACK_BUTTON, CLOSE_BUTTON } from './translations'; @@ -124,28 +123,21 @@ export const PreviewSection: React.FC = ({ ); return ( - <> -
+
= ({ {component} - +
); }; diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_rule_preview.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_rule_preview.cy.ts index 1795a2e623802..80177f5e89397 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_rule_preview.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_rule_preview.cy.ts @@ -9,6 +9,10 @@ import { expandFirstAlertExpandableFlyout } from '../../../../tasks/expandable_f import { DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SECTION, DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_HEADER, + DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_TITLE, + DOCUMENT_DETAILS_FLYOUT_CREATED_BY, + DOCUMENT_DETAILS_FLYOUT_UPDATED_BY, + DOCUMENT_DETAILS_FLYOUT_RULE_SWITCH, DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_BODY, DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_HEADER, DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_CONTENT, @@ -52,10 +56,16 @@ describe( 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('title'); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_TITLE).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_TITLE).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_CREATED_BY).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_UPDATED_BY).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_SWITCH).should('not.be.disabled'); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_SWITCH).should('be.enabled'); cy.log('about'); @@ -84,6 +94,10 @@ describe( .and('contain.text', 'Schedule'); cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SCHEDULE_SECTION_CONTENT).should('be.visible'); toggleRulePreviewScheduleSection(); + + cy.log('footer'); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_FOOTER).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_FOOTER).should('be.visible'); }); }); } diff --git a/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_preview_panel_rule_preview.ts b/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_preview_panel_rule_preview.ts index af278e6e5dd0f..23a0c2e91ca18 100644 --- a/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_preview_panel_rule_preview.ts +++ b/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_preview_panel_rule_preview.ts @@ -6,6 +6,9 @@ */ import { + RULE_PREVIEW_TITLE_TEST_ID, + RULE_PREVIEW_RULE_CREATED_BY_TEST_ID, + RULE_PREVIEW_RULE_UPDATED_BY_TEST_ID, RULE_PREVIEW_BODY_TEST_ID, RULE_PREVIEW_ABOUT_HEADER_TEST_ID, RULE_PREVIEW_ABOUT_CONTENT_TEST_ID, @@ -23,6 +26,20 @@ export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SECTION = export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_HEADER = getDataTestSubjectSelector('previewSectionHeader'); +export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_TITLE = getDataTestSubjectSelector( + RULE_PREVIEW_TITLE_TEST_ID +); + +export const DOCUMENT_DETAILS_FLYOUT_CREATED_BY = getDataTestSubjectSelector( + RULE_PREVIEW_RULE_CREATED_BY_TEST_ID +); + +export const DOCUMENT_DETAILS_FLYOUT_UPDATED_BY = getDataTestSubjectSelector( + RULE_PREVIEW_RULE_UPDATED_BY_TEST_ID +); + +export const DOCUMENT_DETAILS_FLYOUT_RULE_SWITCH = getDataTestSubjectSelector('ruleSwitch'); + export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_BODY = getDataTestSubjectSelector(RULE_PREVIEW_BODY_TEST_ID); diff --git a/x-pack/plugins/security_solution/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts b/x-pack/plugins/security_solution/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts index b8324353f2013..86afe040c2cc1 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts @@ -121,6 +121,7 @@ export const clickInvestigationGuideButton = () => { * Click `Rule summary` button to open rule preview panel */ export const clickRuleSummaryButton = () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE).scrollIntoView(); cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE) .should('be.visible') .within(() => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index 6cfc11acedbef..d3d79b76b1d02 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -236,6 +236,13 @@ const OverrideColumn = styled(EuiFlexItem)` text-overflow: ellipsis; `; +const OverrideValueColumn = styled(EuiFlexItem)` + width: 30px; + max-width: 30px; + overflow: hidden; + text-overflow: ellipsis; +`; + export const buildSeverityDescription = (severity: AboutStepSeverity): ListItems[] => [ { title: i18nSeverity.DEFAULT_SEVERITY, @@ -248,7 +255,7 @@ export const buildSeverityDescription = (severity: AboutStepSeverity): ListItems return { title: index === 0 ? i18nSeverity.SEVERITY_MAPPING : '', description: ( - + {`${severityItem.field}:`} - + {defaultToEmptyTag(severityItem.value)} - + @@ -293,7 +300,7 @@ export const buildRiskScoreDescription = (riskScore: AboutStepRiskScore): ListIt return { title: index === 0 ? i18nRiskScore.RISK_SCORE_MAPPING : '', description: ( - + ` + dt { + font-size: ${theme.eui.euiFontSizeXS} !important; + } + text-overflow: ellipsis; + `} +`; + interface StepRuleDescriptionProps { columns?: 'multi' | 'single' | 'singleSplit'; data: unknown; indexPatterns?: DataViewBase; schema: FormSchema; + isInPanelView?: boolean; } export const StepRuleDescriptionComponent = ({ @@ -80,6 +90,7 @@ export const StepRuleDescriptionComponent = ({ columns = 'multi', indexPatterns, schema, + isInPanelView, }: StepRuleDescriptionProps) => { const kibana = useKibana(); const license = useLicense(); @@ -126,6 +137,16 @@ export const StepRuleDescriptionComponent = ({ ); } + if (isInPanelView) { + return ( + + + + + + ); + } + return ( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index cd2d8a3fa6e51..62b101aa4e76d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -57,6 +57,7 @@ interface StepAboutRuleReadOnlyProps { addPadding: boolean; descriptionColumns: 'multi' | 'single' | 'singleSplit'; defaultValues: AboutStepRule; + isInPanelView?: boolean; } const ThreeQuartersContainer = styled.div` @@ -367,10 +368,16 @@ const StepAboutRuleReadOnlyComponent: FC = ({ addPadding, defaultValues: data, descriptionColumns, + isInPanelView = false, }) => { return ( - + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 08d9775c0b2e8..585e9dcbd6719 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -116,6 +116,7 @@ interface StepDefineRuleReadOnlyProps { descriptionColumns: 'multi' | 'single' | 'singleSplit'; defaultValues: DefineStepRule; indexPattern: DataViewBase; + isInPanelView?: boolean; } export const MyLabelButton = styled(EuiButtonEmpty)` @@ -908,6 +909,7 @@ const StepDefineRuleReadOnlyComponent: FC = ({ defaultValues: data, descriptionColumns, indexPattern, + isInPanelView = false, }) => { const dataForDescription: Partial = getStepDataDataSource(data); @@ -918,6 +920,7 @@ const StepDefineRuleReadOnlyComponent: FC = ({ schema={filterRuleFieldsForType(schema, data.ruleType)} data={filterRuleFieldsForType(dataForDescription, data.ruleType)} indexPatterns={indexPattern} + isInPanelView={isInPanelView} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx index a4971a66972e7..00693dfe2c598 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx @@ -27,6 +27,7 @@ interface StepScheduleRuleReadOnlyProps { addPadding: boolean; descriptionColumns: 'multi' | 'single' | 'singleSplit'; defaultValues: ScheduleStepRule; + isInPanelView?: boolean; } const StepScheduleRuleComponent: FC = ({ @@ -69,10 +70,16 @@ const StepScheduleRuleReadOnlyComponent: FC = ({ addPadding, defaultValues: data, descriptionColumns, + isInPanelView = false, }) => { return ( - + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview.test.tsx index a0dc9c64d76d9..409d51ed64596 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview.test.tsx @@ -12,7 +12,19 @@ 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 { ThemeProvider } from 'styled-components'; +import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock'; +import { TestProviders } from '../../../common/mock'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; +import { useAppToastsMock } from '../../../common/hooks/use_app_toasts.mock'; import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback'; +import { getStepsData } from '../../../detections/pages/detection_engine/rules/helpers'; +import { useRuleSwitch } from '../hooks/use_rule_switch'; +import { + mockAboutStepRule, + mockDefineStepRule, + mockScheduleStepRule, +} from '../../../detection_engine/rule_management_ui/components/rules_table/__mocks__/mock'; import { RULE_PREVIEW_BODY_TEST_ID, RULE_PREVIEW_ABOUT_HEADER_TEST_ID, @@ -21,27 +33,73 @@ import { RULE_PREVIEW_DEFINITION_CONTENT_TEST_ID, RULE_PREVIEW_SCHEDULE_HEADER_TEST_ID, RULE_PREVIEW_SCHEDULE_CONTENT_TEST_ID, + RULE_PREVIEW_ACTIONS_HEADER_TEST_ID, + RULE_PREVIEW_ACTIONS_CONTENT_TEST_ID, + RULE_PREVIEW_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 mockUseRuleSwitch = useRuleSwitch as jest.Mock; +jest.mock('../hooks/use_rule_switch'); + +jest.mock('../../../detection_engine/rule_management/logic/use_start_ml_jobs', () => ({ + useStartMlJobs: jest.fn().mockReturnValue({ startMlJobs: jest.fn() }), +})); + +jest.mock('../../../common/hooks/use_app_toasts'); +const useAppToastsValueMock = useAppToastsMock.create(); + +const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } }); + const contextValue = { ...mockContextValue, ruleId: 'rule id', }; + describe('', () => { + beforeEach(() => { + (useAppToasts as jest.Mock).mockReturnValue(useAppToastsValueMock); + mockUseRuleSwitch.mockReturnValue({ + tooltipText: '', + userInfoLoading: false, + isButtonDisabled: false, + isRuleEnabled: true, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + it('should render rule preview and its sub sections', () => { mockUseRuleWithFallback.mockReturnValue({ rule: { name: 'rule name', description: 'rule description' }, }); + mockGetStepsData.mockReturnValue({ + aboutRuleData: mockAboutStepRule(), + defineRuleData: mockDefineStepRule(), + scheduleRuleData: mockScheduleStepRule(), + ruleActionsData: { actions: ['action'] }, + }); const { getByTestId } = render( - - - - - + + + + + + + + + ); + 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(); @@ -49,16 +107,63 @@ describe('', () => { 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(); + expect(getByTestId(RULE_PREVIEW_ACTIONS_HEADER_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RULE_PREVIEW_ACTIONS_CONTENT_TEST_ID)).toBeInTheDocument(); + }); + + it('should not render actions if action is not available', () => { + mockUseRuleWithFallback.mockReturnValue({ + rule: { name: 'rule name', description: 'rule description' }, + }); + mockGetStepsData.mockReturnValue({ + aboutRuleData: mockAboutStepRule(), + defineRuleData: mockDefineStepRule(), + scheduleRuleData: mockScheduleStepRule(), + }); + const { queryByTestId } = render( + + + + + + + + + + ); + + expect(queryByTestId(RULE_PREVIEW_ACTIONS_HEADER_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(RULE_PREVIEW_ACTIONS_CONTENT_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should render loading spinner when rule is loading', () => { + mockUseRuleWithFallback.mockReturnValue({ loading: true, rule: null }); + const { getByTestId } = render( + + + + + + + + + + ); + expect(getByTestId(RULE_PREVIEW_LOADING_TEST_ID)).toBeInTheDocument(); }); it('should not render rule preview when rule is null', () => { mockUseRuleWithFallback.mockReturnValue({}); const { queryByTestId } = render( - - - - - + + + + + + + + + ); expect(queryByTestId(RULE_PREVIEW_BODY_TEST_ID)).not.toBeInTheDocument(); }); diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview.tsx b/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview.tsx index 8090fe263beb5..1559555eaa38e 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview.tsx @@ -4,40 +4,40 @@ * 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 { EuiText, EuiHorizontalRule, EuiSpacer, EuiPanel, EuiLoadingSpinner } from '@elastic/eui'; +import type { Rule } from '../../../detection_engine/rule_management/logic'; 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 { getStepsData } from '../../../detections/pages/detection_engine/rules/helpers'; +import { RulePreviewTitle } from './rule_preview_title'; +import { useRuleSwitch } from '../hooks/use_rule_switch'; +import { StepAboutRuleReadOnly } from '../../../detections/components/rules/step_about_rule'; +import { StepDefineRuleReadOnly } from '../../../detections/components/rules/step_define_rule'; +import { StepScheduleRuleReadOnly } from '../../../detections/components/rules/step_schedule_rule'; +import { StepRuleActionsReadOnly } from '../../../detections/components/rules/step_rule_actions'; import { RULE_PREVIEW_BODY_TEST_ID, RULE_PREVIEW_ABOUT_TEST_ID, RULE_PREVIEW_DEFINITION_TEST_ID, RULE_PREVIEW_SCHEDULE_TEST_ID, + RULE_PREVIEW_ACTIONS_TEST_ID, + RULE_PREVIEW_LOADING_TEST_ID, } from './test_ids'; -import { - RULE_PREVIEW_ABOUT_TEXT, - RULE_PREVIEW_DEFINITION_TEXT, - RULE_PREVIEW_SCHEDULE_TEXT, -} from './translations'; +import * as i18n 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 { ruleId, indexPattern } = usePreviewPanelContext(); const [rule, setRule] = useState(null); - - const { rule: maybeRule, loading } = useRuleWithFallback(ruleId ?? ''); + const { + rule: maybeRule, + loading: ruleLoading, + isExistingRule, + } = useRuleWithFallback(ruleId ?? ''); // persist rule until refresh is complete useEffect(() => { @@ -46,42 +46,95 @@ export const RulePreview: React.FC = memo(() => { } }, [maybeRule]); - if (loading) { - return ; - } + const { userInfoLoading, tooltipText, isButtonDisabled, isRuleEnabled } = useRuleSwitch({ + rule, + isExistingRule, + }); + + const { aboutRuleData, defineRuleData, scheduleRuleData, ruleActionsData } = + rule != null + ? getStepsData({ rule, detailsView: true }) + : { + aboutRuleData: null, + defineRuleData: null, + scheduleRuleData: null, + ruleActionsData: null, + }; + + const hasNotificationActions = ruleActionsData != null && ruleActionsData.actions.length > 0; + const hasResponseActions = + ruleActionsData != null && (ruleActionsData.responseActions || []).length > 0; + const hasActions = hasNotificationActions || hasResponseActions; return rule ? ( - - -
{rule.name}
-
- + + + {rule.description} - {'About'} + {aboutRuleData && ( + + )} - + - {'Definition'} + {defineRuleData && ( + + )} - + - {'Schedule'} + {scheduleRuleData && ( + + )} + + {hasActions && ( + + + + )} + ) : ruleLoading || userInfoLoading ? ( + ) : null; }); diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview_title.test.tsx b/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview_title.test.tsx new file mode 100644 index 0000000000000..ec74268b7a73c --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview_title.test.tsx @@ -0,0 +1,73 @@ +/* + * 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 { RulePreviewTitle } from './rule_preview_title'; +import { mockFlyoutContextValue } from '../../shared/mocks/mock_flyout_context'; +import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; +import { TestProviders } from '../../../common/mock'; +import type { Rule } from '../../../detection_engine/rule_management/logic'; +import { + RULE_PREVIEW_TITLE_TEST_ID, + RULE_PREVIEW_RULE_SWITCH_TEST_ID, + RULE_PREVIEW_RULE_CREATED_BY_TEST_ID, + RULE_PREVIEW_RULE_UPDATED_BY_TEST_ID, +} from './test_ids'; + +jest.mock('../../../detection_engine/rule_management/logic/use_start_ml_jobs', () => ({ + useStartMlJobs: jest.fn().mockReturnValue({ startMlJobs: jest.fn() }), +})); + +const defaultProps = { + rule: { id: 'id' } as Rule, + isButtonDisabled: false, + isRuleEnabled: true, +}; + +describe('', () => { + it('should render title and its components', () => { + const { getByTestId } = render( + + + + + + ); + expect(getByTestId(RULE_PREVIEW_TITLE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RULE_PREVIEW_RULE_CREATED_BY_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RULE_PREVIEW_RULE_UPDATED_BY_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RULE_PREVIEW_RULE_SWITCH_TEST_ID)).toBeInTheDocument(); + }); + + it('should render updated by and created correctly', () => { + const { getByTestId } = render( + + + + + + ); + expect(getByTestId(RULE_PREVIEW_RULE_CREATED_BY_TEST_ID)).toHaveTextContent( + 'Created by: test on Jan 1, 2023 @ 22:01:00.000' + ); + expect(getByTestId(RULE_PREVIEW_RULE_UPDATED_BY_TEST_ID)).toHaveTextContent( + 'Updated by: elastic on Feb 1, 2023 @ 22:01:00.000' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview_title.tsx b/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview_title.tsx new file mode 100644 index 0000000000000..51b4f5281faa9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview_title.tsx @@ -0,0 +1,131 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { EuiTitle, EuiText, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { Rule } from '../../../detection_engine/rule_management/logic'; +import { FormattedDate } from '../../../common/components/formatted_date'; +import { RuleSwitch } from '../../../detections/components/rules/rule_switch'; +import { useStartMlJobs } from '../../../detection_engine/rule_management/logic/use_start_ml_jobs'; +import { + RULE_PREVIEW_TITLE_TEST_ID, + RULE_PREVIEW_RULE_CREATED_BY_TEST_ID, + RULE_PREVIEW_RULE_UPDATED_BY_TEST_ID, +} from './test_ids'; +import * as i18n from './translations'; + +interface RulePreviewTitleProps { + /** + * Rule object that represents relevant information about a rule + */ + rule: Rule; + /** + * Tooltip text that explains why a user does not have permission to rule + */ + tooltipText?: string; + /** + * Boolean that indivates whether the rule switch button is isabled + */ + isButtonDisabled: boolean; + /** + * Boolean that indivates whether the rule switch shoud be enabled + */ + isRuleEnabled: boolean; +} + +/** + * Title component that shows basic information of a rule. This is displayed above rule preview body in rule preview panel + */ +export const RulePreviewTitle: React.FC = ({ + rule, + tooltipText, + isButtonDisabled, + isRuleEnabled, +}) => { + const { startMlJobs } = useStartMlJobs(); + const startMlJobsIfNeeded = useCallback( + () => startMlJobs(rule?.machine_learning_job_id), + [rule, startMlJobs] + ); + + const createdBy = useMemo( + () => ( + + + ), + }} + /> + + ), + [rule] + ); + + const updatedBy = useMemo( + () => ( + + + ), + }} + /> + + ), + [rule] + ); + + const enableRule = useMemo( + () => ( + + + + + + {i18n.ENABLE_RULE_TEXT} + + + ), + [rule, tooltipText, isButtonDisabled, isRuleEnabled, startMlJobsIfNeeded] + ); + + return ( +
+ +
{rule.name}
+
+ + + {createdBy} + {updatedBy} + {enableRule} + +
+ ); +}; + +RulePreviewTitle.displayName = 'RulePreviewTitle'; diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts index 175992ba45200..1119b285b03df 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts @@ -9,6 +9,13 @@ import { CONTENT_TEST_ID, HEADER_TEST_ID } from '../../right/components/expandab /* Rule preview */ +export const RULE_PREVIEW_TITLE_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRulePreviewTitle'; +export const RULE_PREVIEW_RULE_SWITCH_TEST_ID = 'ruleSwitch'; +export const RULE_PREVIEW_RULE_CREATED_BY_TEST_ID = + 'securitySolutionDocumentDetailsFlyoutRulePreviewCreatedByText'; +export const RULE_PREVIEW_RULE_UPDATED_BY_TEST_ID = + 'securitySolutionDocumentDetailsFlyoutRulePreviewUpdatedByText'; + 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; @@ -24,5 +31,12 @@ export const RULE_PREVIEW_SCHEDULE_TEST_ID = 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_ACTIONS_TEST_ID = + 'securitySolutionDocumentDetailsFlyoutRulePreviewActionsSection'; +export const RULE_PREVIEW_ACTIONS_HEADER_TEST_ID = RULE_PREVIEW_ACTIONS_TEST_ID + HEADER_TEST_ID; +export const RULE_PREVIEW_ACTIONS_CONTENT_TEST_ID = RULE_PREVIEW_ACTIONS_TEST_ID + CONTENT_TEST_ID; +export const RULE_PREVIEW_LOADING_TEST_ID = + 'securitySolutionDocumentDetailsFlyoutRulePreviewLoadingSpinner'; +export const RULE_PREVIEW_HEADER_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRulePreviewHeader'; export const RULE_PREVIEW_FOOTER_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRulePreviewFooter'; export const RULE_PREVIEW_NAVIGATE_TO_RULE_TEST_ID = 'goToRuleDetails'; diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts index dce205f9f142f..717ccb04f76f4 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts @@ -26,3 +26,18 @@ export const RULE_PREVIEW_SCHEDULE_TEXT = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.rulePreviewScheduleSectionText', { defaultMessage: 'Schedule' } ); + +export const RULE_PREVIEW_ACTIONS_TEXT = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.rulePreviewActionsSectionText', + { defaultMessage: 'Actions' } +); + +export const ENABLE_RULE_TEXT = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.rulePreviewEnableRuleText', + { defaultMessage: 'Enable' } +); + +export const UNKNOWN_TEXT = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.rulePreviewUnknownText', + { defaultMessage: 'Unknown' } +); diff --git a/x-pack/plugins/security_solution/public/flyout/preview/context.tsx b/x-pack/plugins/security_solution/public/flyout/preview/context.tsx index 134036221fdf2..521303635c25d 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/preview/context.tsx @@ -6,7 +6,12 @@ */ import React, { createContext, useContext, useMemo } from 'react'; +import type { DataViewBase } from '@kbn/es-query'; import type { PreviewPanelProps } from '.'; +import { useRouteSpy } from '../../common/utils/route/use_route_spy'; +import { SecurityPageName } from '../../../common/constants'; +import { SourcererScopeName } from '../../common/store/sourcerer/model'; +import { useSourcererDataView } from '../../common/containers/sourcerer'; export interface PreviewPanelContext { /** @@ -25,6 +30,10 @@ export interface PreviewPanelContext { * Rule id if preview is rule details */ ruleId: string; + /** + * Index pattern for rule details + */ + indexPattern: DataViewBase; } export const PreviewPanelContext = createContext(undefined); @@ -43,6 +52,12 @@ export const PreviewPanelProvider = ({ ruleId, children, }: PreviewPanelProviderProps) => { + const [{ pageName }] = useRouteSpy(); + const sourcererScope = + pageName === SecurityPageName.detections + ? SourcererScopeName.detections + : SourcererScopeName.default; + const sourcererDataView = useSourcererDataView(sourcererScope); const contextValue = useMemo( () => id && indexName && scopeId @@ -51,9 +66,10 @@ export const PreviewPanelProvider = ({ indexName, scopeId, ruleId: ruleId ?? '', + indexPattern: sourcererDataView.indexPattern, } : undefined, - [id, indexName, scopeId, ruleId] + [id, indexName, scopeId, ruleId, sourcererDataView.indexPattern] ); return ( diff --git a/x-pack/plugins/security_solution/public/flyout/preview/hooks/use_rule_switch.test.tsx b/x-pack/plugins/security_solution/public/flyout/preview/hooks/use_rule_switch.test.tsx new file mode 100644 index 0000000000000..8c3be18dc2e47 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/preview/hooks/use_rule_switch.test.tsx @@ -0,0 +1,101 @@ +/* + * 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 { Rule } from '../../../detection_engine/rule_management/logic'; +import type { UseRuleSwitchParams, UseRuleSwitchResult } from './use_rule_switch'; +import { useRuleSwitch } from './use_rule_switch'; +import { useHasMlPermissions } from '../../../detection_engine/rule_management_ui/components/rules_table/use_has_ml_permissions'; +import { useUserData } from '../../../detections/components/user_info'; +import { canEditRuleWithActions, hasUserCRUDPermission } from '../../../common/utils/privileges'; + +const mockUseUserData = useUserData as jest.Mock; +jest.mock('../../../detections/components/user_info'); + +const mockCanEditRuleWithActions = canEditRuleWithActions as jest.Mock; +const mockHasUserCRUDPermission = hasUserCRUDPermission as jest.Mock; +jest.mock('../../../common/utils/privileges'); + +const mockUseHasMlPermissions = useHasMlPermissions as jest.Mock; +jest.mock( + '../../../detection_engine/rule_management_ui/components/rules_table/use_has_ml_permissions' +); + +jest.mock( + '../../../detection_engine/rule_management_ui/components/rules_table/use_has_actions_privileges' +); + +const defaultProps = { + rule: {} as Rule, + isExistingRule: true, +}; + +const mlProps = { + rule: { type: 'machine_learning' } as unknown as Rule, + isExistingRule: true, +}; + +describe('useRuleSwitch', () => { + let hookResult: RenderHookResult; + + it('should return correct userInfoLoading', () => { + mockUseUserData.mockReturnValue([{ loading: true }]); + hookResult = renderHook(() => useRuleSwitch(defaultProps)); + expect(hookResult.result.current.userInfoLoading).toEqual(true); + + mockUseUserData.mockReturnValue([{ loading: false }]); + hookResult = renderHook(() => useRuleSwitch(defaultProps)); + expect(hookResult.result.current.userInfoLoading).toEqual(false); + }); + + describe('should return correct button state if rule is ml rule', () => { + beforeEach(() => { + mockCanEditRuleWithActions.mockReturnValue(true); + mockUseUserData.mockReturnValue([{ loading: false, canUserCRUD: true }]); + mockHasUserCRUDPermission.mockReturnValue(true); + }); + + it('button should not be disabled with ml permission', () => { + mockUseHasMlPermissions.mockReturnValue(true); + hookResult = renderHook(() => useRuleSwitch(mlProps)); + expect(hookResult.result.current.isButtonDisabled).toEqual(false); + }); + + it('button should be disabled without ml permission', () => { + mockUseHasMlPermissions.mockReturnValue(false); + hookResult = renderHook(() => useRuleSwitch(mlProps)); + expect(hookResult.result.current.isButtonDisabled).toEqual(true); + }); + }); + + describe('should return correct button state if rule is not ml rule', () => { + beforeEach(() => { + mockUseUserData.mockReturnValue([{ loading: false, canUserCRUD: true }]); + mockCanEditRuleWithActions.mockReturnValue(true); + mockHasUserCRUDPermission.mockReturnValue(true); + mockUseHasMlPermissions.mockReturnValue(false); + }); + + it('should disable button if rule is not an existing rule', () => { + hookResult = renderHook(() => useRuleSwitch({ ...defaultProps, isExistingRule: false })); + expect(hookResult.result.current.isButtonDisabled).toEqual(true); + }); + + it('should disable button if user cannot edut rule with actions', () => { + mockCanEditRuleWithActions.mockReturnValue(false); + hookResult = renderHook(() => useRuleSwitch(defaultProps)); + expect(hookResult.result.current.isButtonDisabled).toEqual(true); + }); + + it('should disable button if user does not have CRUD permission', () => { + mockHasUserCRUDPermission.mockReturnValue(false); + hookResult = renderHook(() => useRuleSwitch(defaultProps)); + expect(hookResult.result.current.isButtonDisabled).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/preview/hooks/use_rule_switch.ts b/x-pack/plugins/security_solution/public/flyout/preview/hooks/use_rule_switch.ts new file mode 100644 index 0000000000000..84fc3edb788b3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/preview/hooks/use_rule_switch.ts @@ -0,0 +1,77 @@ +/* + * 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 { useHasActionsPrivileges } from '../../../detection_engine/rule_management_ui/components/rules_table/use_has_actions_privileges'; +import { useHasMlPermissions } from '../../../detection_engine/rule_management_ui/components/rules_table/use_has_ml_permissions'; +import { isMlRule } from '../../../../common/machine_learning/helpers'; +import { useUserData } from '../../../detections/components/user_info'; +import { + canEditRuleWithActions, + explainLackOfPermission, + hasUserCRUDPermission, +} from '../../../common/utils/privileges'; +import type { Rule } from '../../../detection_engine/rule_management/logic'; +export interface UseRuleSwitchParams { + /** + * Rule object that represents relevant information about a rule + */ + rule: Rule | null; + /** + * Boolean that indicates whether the rule currently exists + */ + isExistingRule: boolean; +} + +export interface UseRuleSwitchResult { + /** + * Boolean of wheather useUserData is loading + */ + userInfoLoading: boolean; + /** + * Tooltip text that explains why a user does not have permission to rule + */ + tooltipText?: string; + /** + * Boolean that indivates whether the rule switch button is isabled + */ + isButtonDisabled: boolean; + /** + * Boolean that indivates whether the rule switch shoud be enabled + */ + isRuleEnabled: boolean; +} + +/** + * This hook is used to retrieved data for rule switch + * @param rule data + * @param isExistingrule is this rule an existing rule + */ +export const useRuleSwitch = ({ + rule, + isExistingRule, +}: UseRuleSwitchParams): UseRuleSwitchResult => { + const hasMlPermissions = useHasMlPermissions(); + const hasActionsPrivileges = useHasActionsPrivileges(); + const [{ loading: userInfoLoading, canUserCRUD }] = useUserData(); + + const tooltipText = explainLackOfPermission( + rule, + hasMlPermissions, + hasActionsPrivileges, + canUserCRUD + ); + + return { + userInfoLoading, + tooltipText, + isButtonDisabled: + !isExistingRule || + !canEditRuleWithActions(rule, hasActionsPrivileges) || + !hasUserCRUDPermission(canUserCRUD) || + (isMlRule(rule?.type) && !hasMlPermissions), + isRuleEnabled: isExistingRule && (rule?.enabled ?? false), + }; +}; diff --git a/x-pack/plugins/security_solution/public/flyout/preview/index.tsx b/x-pack/plugins/security_solution/public/flyout/preview/index.tsx index a75e6b4890541..f06157e408a45 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/preview/index.tsx @@ -6,7 +6,6 @@ */ 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'; @@ -21,7 +20,6 @@ export interface PreviewPanelProps extends FlyoutPanelProps { id: string; indexName: string; scopeId: string; - banner?: string; ruleId?: string; }; } @@ -38,14 +36,13 @@ export const PreviewPanel: React.FC> = memo(({ path } return null; } return ( - - - {previewPanel.content} - + + {previewPanel.content} {previewPanel.footer} ); diff --git a/x-pack/plugins/security_solution/public/flyout/preview/mocks/mock_preview_panel_context.ts b/x-pack/plugins/security_solution/public/flyout/preview/mocks/mock_preview_panel_context.ts index b9a5140ce20d3..4e9f9cc43d8ba 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/mocks/mock_preview_panel_context.ts +++ b/x-pack/plugins/security_solution/public/flyout/preview/mocks/mock_preview_panel_context.ts @@ -15,4 +15,5 @@ export const mockContextValue: PreviewPanelContext = { indexName: 'index', scopeId: 'scopeId', ruleId: '', + indexPattern: { fields: [], title: 'test index' }, }; diff --git a/x-pack/plugins/security_solution/public/flyout/preview/panels.tsx b/x-pack/plugins/security_solution/public/flyout/preview/panels.tsx index 7813e92bf702a..b9aee26bdd577 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/panels.tsx +++ b/x-pack/plugins/security_solution/public/flyout/preview/panels.tsx @@ -27,7 +27,7 @@ export type PreviewPanelType = Array<{ /** * Footer section in the panel */ - footer: React.ReactElement; + footer?: React.ReactElement; }>; /** From 0a1c401ce98e21ca613ff3e6ee6812d90558a71d Mon Sep 17 00:00:00 2001 From: christineweng Date: Tue, 8 Aug 2023 17:17:26 -0500 Subject: [PATCH 2/4] fix pr comments --- .../src/components/preview_section.tsx | 2 +- ...t_details_preview_panel_rule_preview.cy.ts | 3 - ...lert_details_preview_panel_rule_preview.ts | 2 - .../pages/rule_details/index.tsx | 31 ++--- .../rules/description_step/index.tsx | 18 ++- .../components/rules/rule_info/index.test.tsx | 70 +++++++++++ .../components/rules/rule_info/index.tsx | 79 ++++++++++++ .../rules/rule_info/translations.ts | 15 +++ .../rules/step_about_rule/index.tsx | 2 +- .../rules/step_define_rule/index.tsx | 2 +- .../rules/step_schedule_rule/index.tsx | 2 +- .../preview/components/rule_preview.test.tsx | 21 +--- .../preview/components/rule_preview.tsx | 94 ++++++-------- .../components/rule_preview_title.test.tsx | 35 ------ .../preview/components/rule_preview_title.tsx | 117 ++++-------------- .../flyout/preview/components/test_ids.ts | 1 - .../flyout/preview/components/translations.ts | 5 - .../preview/hooks/use_rule_switch.test.tsx | 101 --------------- .../flyout/preview/hooks/use_rule_switch.ts | 77 ------------ 19 files changed, 247 insertions(+), 430 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/rule_info/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/flyout/preview/hooks/use_rule_switch.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/flyout/preview/hooks/use_rule_switch.ts diff --git a/packages/kbn-expandable-flyout/src/components/preview_section.tsx b/packages/kbn-expandable-flyout/src/components/preview_section.tsx index 60e86ef4195bc..1bb3f84d1b5f5 100644 --- a/packages/kbn-expandable-flyout/src/components/preview_section.tsx +++ b/packages/kbn-expandable-flyout/src/components/preview_section.tsx @@ -137,7 +137,7 @@ export const PreviewSection: React.FC = ({ css={css` margin: ${euiTheme.size.xs}; height: 99%; - box-shadow: 0px 0px 5px 5px #999999; + box-shadow: 0px 0px 5px 5px ${euiTheme.colors.darkShade}; `} className="eui-yScroll" data-test-subj={PREVIEW_SECTION} diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_rule_preview.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_rule_preview.cy.ts index 80177f5e89397..116162983f0b2 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_rule_preview.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_rule_preview.cy.ts @@ -12,7 +12,6 @@ import { DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_TITLE, DOCUMENT_DETAILS_FLYOUT_CREATED_BY, DOCUMENT_DETAILS_FLYOUT_UPDATED_BY, - DOCUMENT_DETAILS_FLYOUT_RULE_SWITCH, DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_BODY, DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_HEADER, DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_CONTENT, @@ -64,8 +63,6 @@ describe( cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_TITLE).should('be.visible'); cy.get(DOCUMENT_DETAILS_FLYOUT_CREATED_BY).should('be.visible'); cy.get(DOCUMENT_DETAILS_FLYOUT_UPDATED_BY).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_SWITCH).should('not.be.disabled'); - cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_SWITCH).should('be.enabled'); cy.log('about'); diff --git a/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_preview_panel_rule_preview.ts b/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_preview_panel_rule_preview.ts index 23a0c2e91ca18..5ccb58e1ef969 100644 --- a/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_preview_panel_rule_preview.ts +++ b/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_preview_panel_rule_preview.ts @@ -38,8 +38,6 @@ export const DOCUMENT_DETAILS_FLYOUT_UPDATED_BY = getDataTestSubjectSelector( RULE_PREVIEW_RULE_UPDATED_BY_TEST_ID ); -export const DOCUMENT_DETAILS_FLYOUT_RULE_SWITCH = getDataTestSubjectSelector('ruleSwitch'); - export const DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_BODY = getDataTestSubjectSelector(RULE_PREVIEW_BODY_TEST_ID); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx index bebd373b3c2ab..eb0a3c24db15c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx @@ -22,7 +22,6 @@ import type { Filter } from '@kbn/es-query'; import { i18n as i18nTranslate } from '@kbn/i18n'; import { Routes, Route } from '@kbn/shared-ux-router'; -import { FormattedMessage } from '@kbn/i18n-react'; import { noop, omit } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useParams } from 'react-router-dom'; @@ -53,7 +52,6 @@ import { import { useKibana } from '../../../../common/lib/kibana'; import type { UpdateDateRange } from '../../../../common/components/charts/common'; import { FiltersGlobal } from '../../../../common/components/filters_global'; -import { FormattedDate } from '../../../../common/components/formatted_date'; import { getDetectionEngineUrl, getRuleDetailsTabUrl, @@ -81,6 +79,7 @@ import { getStepsData, redirectToDetections, } from '../../../../detections/pages/detection_engine/rules/helpers'; +import { CreatedBy, UpdatedBy } from '../../../../detections/components/rules/rule_info'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { inputsSelectors } from '../../../../common/store/inputs'; import { setAbsoluteRangeDatePicker } from '../../../../common/store/inputs/actions'; @@ -468,32 +467,16 @@ const RuleDetailsPageComponent: React.FC = ({ () => rule ? ( [ - - ), - }} + createdBy={rule?.created_by} + createdAt={rule?.created_at} />, rule?.updated_by != null ? ( - - ), - }} + updatedBy={rule?.updated_by} + updatedAt={rule?.updated_at} /> ) : ( '' diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 686a7058d5aa6..d4f7c026d369e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -9,7 +9,7 @@ import { EuiDescriptionList, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEmpty, chunk, get, pick, isNumber } from 'lodash/fp'; import React, { memo, useState } from 'react'; import styled from 'styled-components'; - +import { css } from '@emotion/css'; import type { ThreatMapping, Threats, Type } from '@kbn/securitysolution-io-ts-alerting-types'; import type { DataViewBase, Filter } from '@kbn/es-query'; import { FilterStateStore } from '@kbn/es-query'; @@ -68,13 +68,11 @@ const DescriptionListContainer = styled(EuiDescriptionList)` } `; -export const DescriptionListPanelContainer = styled(EuiDescriptionList)` - ${({ theme }) => ` - dt { - font-size: ${theme.eui.euiFontSizeXS} !important; - } - text-overflow: ellipsis; - `} +const panelViewStyle = css` + dt { + font-size: 90% !important; + } + text-overflow: ellipsis; `; interface StepRuleDescriptionProps { @@ -82,7 +80,7 @@ interface StepRuleDescriptionProps { data: unknown; indexPatterns?: DataViewBase; schema: FormSchema; - isInPanelView?: boolean; + isInPanelView?: boolean; // Option to show description list in smaller font } export const StepRuleDescriptionComponent = ({ @@ -141,7 +139,7 @@ export const StepRuleDescriptionComponent = ({ return ( - + ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.test.tsx new file mode 100644 index 0000000000000..6b070f2d4285a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.test.tsx @@ -0,0 +1,70 @@ +/* + * 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 { CreatedBy, UpdatedBy } from '.'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; + +describe('Rule related info', () => { + describe('', () => { + it('should render created correctly when by and date are passed', () => { + const { getByTestId } = render( + + + + ); + expect(getByTestId('createdBy')).toHaveTextContent( + 'Created by: test on Jan 1, 2023 @ 22:01:00.000' + ); + }); + + it('should render created unknown when created by is not available', () => { + const { getByTestId } = render( + + + + ); + expect(getByTestId('createdBy')).toHaveTextContent( + 'Created by: Unknown on Jan 1, 2023 @ 22:01:00.000' + ); + }); + }); + describe('', () => { + it('should render updated by correctly when by and date are passed', () => { + const { getByTestId } = render( + + + + ); + expect(getByTestId('updatedBy')).toHaveTextContent( + 'Updated by: test on Jan 1, 2023 @ 22:01:00.000' + ); + }); + + it('should render updated by correctly when updated by is not available', () => { + const { getByTestId } = render( + + + + ); + expect(getByTestId('updatedBy')).toHaveTextContent( + 'Updated by: Unknown on Jan 1, 2023 @ 22:01:00.000' + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.tsx new file mode 100644 index 0000000000000..02d8b631a7c94 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.tsx @@ -0,0 +1,79 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import { UNKNOWN_TEXT } from './translations'; +import { FormattedDate } from '../../../../common/components/formatted_date'; + +interface CreatedByProps { + id: string; + createdBy?: string; + createdAt?: string; + ['data-test-subj']?: string; +} + +/** + * Created by and created at text that are shown on rule details + */ +export const CreatedBy: React.FC = ({ + id, + createdBy, + createdAt, + 'data-test-subj': dataTestSubj, +}) => { + return ( +
+ + ), + }} + /> +
+ ); +}; + +CreatedBy.displayName = 'CreatedBy'; + +interface UpdatedByProps { + id: string; + updatedBy?: string; + updatedAt?: string; + ['data-test-subj']?: string; +} + +/** + * Updated by and updated at text that are shown on rule details + */ +export const UpdatedBy: React.FC = ({ + id, + updatedBy, + updatedAt, + 'data-test-subj': dataTestSubj, +}) => { + return ( +
+ + ), + }} + /> +
+ ); +}; + +UpdatedBy.displayName = 'UpdatedBy'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/translations.ts new file mode 100644 index 0000000000000..5a17540b0c90d --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/translations.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 { i18n } from '@kbn/i18n'; + +export const UNKNOWN_TEXT = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleInfo.UnknownText', + { + defaultMessage: 'Unknown', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 62b101aa4e76d..7052c29ce7881 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -57,7 +57,7 @@ interface StepAboutRuleReadOnlyProps { addPadding: boolean; descriptionColumns: 'multi' | 'single' | 'singleSplit'; defaultValues: AboutStepRule; - isInPanelView?: boolean; + isInPanelView?: boolean; // Option to show description list in smaller font } const ThreeQuartersContainer = styled.div` diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 585e9dcbd6719..779fe5f35a820 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -116,7 +116,7 @@ interface StepDefineRuleReadOnlyProps { descriptionColumns: 'multi' | 'single' | 'singleSplit'; defaultValues: DefineStepRule; indexPattern: DataViewBase; - isInPanelView?: boolean; + isInPanelView?: boolean; // Option to show description list in smaller font } export const MyLabelButton = styled(EuiButtonEmpty)` diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx index 00693dfe2c598..30699d60912cb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx @@ -27,7 +27,7 @@ interface StepScheduleRuleReadOnlyProps { addPadding: boolean; descriptionColumns: 'multi' | 'single' | 'singleSplit'; defaultValues: ScheduleStepRule; - isInPanelView?: boolean; + isInPanelView?: boolean; // Option to show description list in smaller font } const StepScheduleRuleComponent: FC = ({ diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview.test.tsx index 409d51ed64596..fc9b8616d5fd7 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview.test.tsx @@ -15,11 +15,8 @@ import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; import { ThemeProvider } from 'styled-components'; import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock'; import { TestProviders } from '../../../common/mock'; -import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -import { useAppToastsMock } from '../../../common/hooks/use_app_toasts.mock'; import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback'; import { getStepsData } from '../../../detections/pages/detection_engine/rules/helpers'; -import { useRuleSwitch } from '../hooks/use_rule_switch'; import { mockAboutStepRule, mockDefineStepRule, @@ -46,16 +43,6 @@ jest.mock('../../../detection_engine/rule_management/logic/use_rule_with_fallbac const mockGetStepsData = getStepsData as jest.Mock; jest.mock('../../../detections/pages/detection_engine/rules/helpers'); -const mockUseRuleSwitch = useRuleSwitch as jest.Mock; -jest.mock('../hooks/use_rule_switch'); - -jest.mock('../../../detection_engine/rule_management/logic/use_start_ml_jobs', () => ({ - useStartMlJobs: jest.fn().mockReturnValue({ startMlJobs: jest.fn() }), -})); - -jest.mock('../../../common/hooks/use_app_toasts'); -const useAppToastsValueMock = useAppToastsMock.create(); - const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } }); const contextValue = { @@ -65,13 +52,7 @@ const contextValue = { describe('', () => { beforeEach(() => { - (useAppToasts as jest.Mock).mockReturnValue(useAppToastsValueMock); - mockUseRuleSwitch.mockReturnValue({ - tooltipText: '', - userInfoLoading: false, - isButtonDisabled: false, - isRuleEnabled: true, - }); + // (useAppToasts as jest.Mock).mockReturnValue(useAppToastsValueMock); }); afterEach(() => { diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview.tsx b/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview.tsx index 1559555eaa38e..0a78f6a29141f 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview.tsx @@ -12,7 +12,6 @@ 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 { RulePreviewTitle } from './rule_preview_title'; -import { useRuleSwitch } from '../hooks/use_rule_switch'; import { StepAboutRuleReadOnly } from '../../../detections/components/rules/step_about_rule'; import { StepDefineRuleReadOnly } from '../../../detections/components/rules/step_define_rule'; import { StepScheduleRuleReadOnly } from '../../../detections/components/rules/step_schedule_rule'; @@ -33,11 +32,7 @@ import * as i18n from './translations'; export const RulePreview: React.FC = memo(() => { const { ruleId, indexPattern } = usePreviewPanelContext(); const [rule, setRule] = useState(null); - const { - rule: maybeRule, - loading: ruleLoading, - isExistingRule, - } = useRuleWithFallback(ruleId ?? ''); + const { rule: maybeRule, loading: ruleLoading } = useRuleWithFallback(ruleId ?? ''); // persist rule until refresh is complete useEffect(() => { @@ -46,11 +41,6 @@ export const RulePreview: React.FC = memo(() => { } }, [maybeRule]); - const { userInfoLoading, tooltipText, isButtonDisabled, isRuleEnabled } = useRuleSwitch({ - rule, - isExistingRule, - }); - const { aboutRuleData, defineRuleData, scheduleRuleData, ruleActionsData } = rule != null ? getStepsData({ rule, detailsView: true }) @@ -61,19 +51,13 @@ export const RulePreview: React.FC = memo(() => { ruleActionsData: null, }; - const hasNotificationActions = ruleActionsData != null && ruleActionsData.actions.length > 0; - const hasResponseActions = - ruleActionsData != null && (ruleActionsData.responseActions || []).length > 0; - const hasActions = hasNotificationActions || hasResponseActions; + const hasNotificationActions = Boolean(ruleActionsData?.actions?.length); + const hasResponseActions = Boolean(ruleActionsData?.responseActions?.length); + const hasActions = ruleActionsData != null && (hasNotificationActions || hasResponseActions); return rule ? ( - + { )} - - {defineRuleData && ( - - )} - - - - {scheduleRuleData && ( - - )} - - + {defineRuleData && ( + <> + + + + + + )} + {scheduleRuleData && ( + <> + + + + + + )} {hasActions && ( { )} - ) : ruleLoading || userInfoLoading ? ( + ) : ruleLoading ? ( ) : null; }); diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview_title.test.tsx b/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview_title.test.tsx index ec74268b7a73c..589d0d4e3b456 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview_title.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview_title.test.tsx @@ -14,19 +14,12 @@ import { TestProviders } from '../../../common/mock'; import type { Rule } from '../../../detection_engine/rule_management/logic'; import { RULE_PREVIEW_TITLE_TEST_ID, - RULE_PREVIEW_RULE_SWITCH_TEST_ID, RULE_PREVIEW_RULE_CREATED_BY_TEST_ID, RULE_PREVIEW_RULE_UPDATED_BY_TEST_ID, } from './test_ids'; -jest.mock('../../../detection_engine/rule_management/logic/use_start_ml_jobs', () => ({ - useStartMlJobs: jest.fn().mockReturnValue({ startMlJobs: jest.fn() }), -})); - const defaultProps = { rule: { id: 'id' } as Rule, - isButtonDisabled: false, - isRuleEnabled: true, }; describe('', () => { @@ -41,33 +34,5 @@ describe('', () => { expect(getByTestId(RULE_PREVIEW_TITLE_TEST_ID)).toBeInTheDocument(); expect(getByTestId(RULE_PREVIEW_RULE_CREATED_BY_TEST_ID)).toBeInTheDocument(); expect(getByTestId(RULE_PREVIEW_RULE_UPDATED_BY_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(RULE_PREVIEW_RULE_SWITCH_TEST_ID)).toBeInTheDocument(); - }); - - it('should render updated by and created correctly', () => { - const { getByTestId } = render( - - - - - - ); - expect(getByTestId(RULE_PREVIEW_RULE_CREATED_BY_TEST_ID)).toHaveTextContent( - 'Created by: test on Jan 1, 2023 @ 22:01:00.000' - ); - expect(getByTestId(RULE_PREVIEW_RULE_UPDATED_BY_TEST_ID)).toHaveTextContent( - 'Updated by: elastic on Feb 1, 2023 @ 22:01:00.000' - ); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview_title.tsx b/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview_title.tsx index 51b4f5281faa9..6f1e671487035 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview_title.tsx +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/rule_preview_title.tsx @@ -4,115 +4,27 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useCallback, useMemo } from 'react'; -import { EuiTitle, EuiText, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import { EuiTitle, EuiText, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { Rule } from '../../../detection_engine/rule_management/logic'; -import { FormattedDate } from '../../../common/components/formatted_date'; -import { RuleSwitch } from '../../../detections/components/rules/rule_switch'; -import { useStartMlJobs } from '../../../detection_engine/rule_management/logic/use_start_ml_jobs'; +import { CreatedBy, UpdatedBy } from '../../../detections/components/rules/rule_info'; import { RULE_PREVIEW_TITLE_TEST_ID, RULE_PREVIEW_RULE_CREATED_BY_TEST_ID, RULE_PREVIEW_RULE_UPDATED_BY_TEST_ID, } from './test_ids'; -import * as i18n from './translations'; interface RulePreviewTitleProps { /** * Rule object that represents relevant information about a rule */ rule: Rule; - /** - * Tooltip text that explains why a user does not have permission to rule - */ - tooltipText?: string; - /** - * Boolean that indivates whether the rule switch button is isabled - */ - isButtonDisabled: boolean; - /** - * Boolean that indivates whether the rule switch shoud be enabled - */ - isRuleEnabled: boolean; } /** * Title component that shows basic information of a rule. This is displayed above rule preview body in rule preview panel */ -export const RulePreviewTitle: React.FC = ({ - rule, - tooltipText, - isButtonDisabled, - isRuleEnabled, -}) => { - const { startMlJobs } = useStartMlJobs(); - const startMlJobsIfNeeded = useCallback( - () => startMlJobs(rule?.machine_learning_job_id), - [rule, startMlJobs] - ); - - const createdBy = useMemo( - () => ( - - - ), - }} - /> - - ), - [rule] - ); - - const updatedBy = useMemo( - () => ( - - - ), - }} - /> - - ), - [rule] - ); - - const enableRule = useMemo( - () => ( - - - - - - {i18n.ENABLE_RULE_TEXT} - - - ), - [rule, tooltipText, isButtonDisabled, isRuleEnabled, startMlJobsIfNeeded] - ); - +export const RulePreviewTitle: React.FC = ({ rule }) => { return (
@@ -120,9 +32,24 @@ export const RulePreviewTitle: React.FC = ({ - {createdBy} - {updatedBy} - {enableRule} + + + + + + + + + +
); diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts index 1119b285b03df..32a26d3f87db9 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts @@ -10,7 +10,6 @@ import { CONTENT_TEST_ID, HEADER_TEST_ID } from '../../right/components/expandab /* Rule preview */ export const RULE_PREVIEW_TITLE_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRulePreviewTitle'; -export const RULE_PREVIEW_RULE_SWITCH_TEST_ID = 'ruleSwitch'; export const RULE_PREVIEW_RULE_CREATED_BY_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRulePreviewCreatedByText'; export const RULE_PREVIEW_RULE_UPDATED_BY_TEST_ID = diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts index 717ccb04f76f4..8112b796c1d39 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts @@ -36,8 +36,3 @@ export const ENABLE_RULE_TEXT = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.rulePreviewEnableRuleText', { defaultMessage: 'Enable' } ); - -export const UNKNOWN_TEXT = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.rulePreviewUnknownText', - { defaultMessage: 'Unknown' } -); diff --git a/x-pack/plugins/security_solution/public/flyout/preview/hooks/use_rule_switch.test.tsx b/x-pack/plugins/security_solution/public/flyout/preview/hooks/use_rule_switch.test.tsx deleted file mode 100644 index 8c3be18dc2e47..0000000000000 --- a/x-pack/plugins/security_solution/public/flyout/preview/hooks/use_rule_switch.test.tsx +++ /dev/null @@ -1,101 +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 { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; -import type { Rule } from '../../../detection_engine/rule_management/logic'; -import type { UseRuleSwitchParams, UseRuleSwitchResult } from './use_rule_switch'; -import { useRuleSwitch } from './use_rule_switch'; -import { useHasMlPermissions } from '../../../detection_engine/rule_management_ui/components/rules_table/use_has_ml_permissions'; -import { useUserData } from '../../../detections/components/user_info'; -import { canEditRuleWithActions, hasUserCRUDPermission } from '../../../common/utils/privileges'; - -const mockUseUserData = useUserData as jest.Mock; -jest.mock('../../../detections/components/user_info'); - -const mockCanEditRuleWithActions = canEditRuleWithActions as jest.Mock; -const mockHasUserCRUDPermission = hasUserCRUDPermission as jest.Mock; -jest.mock('../../../common/utils/privileges'); - -const mockUseHasMlPermissions = useHasMlPermissions as jest.Mock; -jest.mock( - '../../../detection_engine/rule_management_ui/components/rules_table/use_has_ml_permissions' -); - -jest.mock( - '../../../detection_engine/rule_management_ui/components/rules_table/use_has_actions_privileges' -); - -const defaultProps = { - rule: {} as Rule, - isExistingRule: true, -}; - -const mlProps = { - rule: { type: 'machine_learning' } as unknown as Rule, - isExistingRule: true, -}; - -describe('useRuleSwitch', () => { - let hookResult: RenderHookResult; - - it('should return correct userInfoLoading', () => { - mockUseUserData.mockReturnValue([{ loading: true }]); - hookResult = renderHook(() => useRuleSwitch(defaultProps)); - expect(hookResult.result.current.userInfoLoading).toEqual(true); - - mockUseUserData.mockReturnValue([{ loading: false }]); - hookResult = renderHook(() => useRuleSwitch(defaultProps)); - expect(hookResult.result.current.userInfoLoading).toEqual(false); - }); - - describe('should return correct button state if rule is ml rule', () => { - beforeEach(() => { - mockCanEditRuleWithActions.mockReturnValue(true); - mockUseUserData.mockReturnValue([{ loading: false, canUserCRUD: true }]); - mockHasUserCRUDPermission.mockReturnValue(true); - }); - - it('button should not be disabled with ml permission', () => { - mockUseHasMlPermissions.mockReturnValue(true); - hookResult = renderHook(() => useRuleSwitch(mlProps)); - expect(hookResult.result.current.isButtonDisabled).toEqual(false); - }); - - it('button should be disabled without ml permission', () => { - mockUseHasMlPermissions.mockReturnValue(false); - hookResult = renderHook(() => useRuleSwitch(mlProps)); - expect(hookResult.result.current.isButtonDisabled).toEqual(true); - }); - }); - - describe('should return correct button state if rule is not ml rule', () => { - beforeEach(() => { - mockUseUserData.mockReturnValue([{ loading: false, canUserCRUD: true }]); - mockCanEditRuleWithActions.mockReturnValue(true); - mockHasUserCRUDPermission.mockReturnValue(true); - mockUseHasMlPermissions.mockReturnValue(false); - }); - - it('should disable button if rule is not an existing rule', () => { - hookResult = renderHook(() => useRuleSwitch({ ...defaultProps, isExistingRule: false })); - expect(hookResult.result.current.isButtonDisabled).toEqual(true); - }); - - it('should disable button if user cannot edut rule with actions', () => { - mockCanEditRuleWithActions.mockReturnValue(false); - hookResult = renderHook(() => useRuleSwitch(defaultProps)); - expect(hookResult.result.current.isButtonDisabled).toEqual(true); - }); - - it('should disable button if user does not have CRUD permission', () => { - mockHasUserCRUDPermission.mockReturnValue(false); - hookResult = renderHook(() => useRuleSwitch(defaultProps)); - expect(hookResult.result.current.isButtonDisabled).toEqual(true); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/flyout/preview/hooks/use_rule_switch.ts b/x-pack/plugins/security_solution/public/flyout/preview/hooks/use_rule_switch.ts deleted file mode 100644 index 84fc3edb788b3..0000000000000 --- a/x-pack/plugins/security_solution/public/flyout/preview/hooks/use_rule_switch.ts +++ /dev/null @@ -1,77 +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 { useHasActionsPrivileges } from '../../../detection_engine/rule_management_ui/components/rules_table/use_has_actions_privileges'; -import { useHasMlPermissions } from '../../../detection_engine/rule_management_ui/components/rules_table/use_has_ml_permissions'; -import { isMlRule } from '../../../../common/machine_learning/helpers'; -import { useUserData } from '../../../detections/components/user_info'; -import { - canEditRuleWithActions, - explainLackOfPermission, - hasUserCRUDPermission, -} from '../../../common/utils/privileges'; -import type { Rule } from '../../../detection_engine/rule_management/logic'; -export interface UseRuleSwitchParams { - /** - * Rule object that represents relevant information about a rule - */ - rule: Rule | null; - /** - * Boolean that indicates whether the rule currently exists - */ - isExistingRule: boolean; -} - -export interface UseRuleSwitchResult { - /** - * Boolean of wheather useUserData is loading - */ - userInfoLoading: boolean; - /** - * Tooltip text that explains why a user does not have permission to rule - */ - tooltipText?: string; - /** - * Boolean that indivates whether the rule switch button is isabled - */ - isButtonDisabled: boolean; - /** - * Boolean that indivates whether the rule switch shoud be enabled - */ - isRuleEnabled: boolean; -} - -/** - * This hook is used to retrieved data for rule switch - * @param rule data - * @param isExistingrule is this rule an existing rule - */ -export const useRuleSwitch = ({ - rule, - isExistingRule, -}: UseRuleSwitchParams): UseRuleSwitchResult => { - const hasMlPermissions = useHasMlPermissions(); - const hasActionsPrivileges = useHasActionsPrivileges(); - const [{ loading: userInfoLoading, canUserCRUD }] = useUserData(); - - const tooltipText = explainLackOfPermission( - rule, - hasMlPermissions, - hasActionsPrivileges, - canUserCRUD - ); - - return { - userInfoLoading, - tooltipText, - isButtonDisabled: - !isExistingRule || - !canEditRuleWithActions(rule, hasActionsPrivileges) || - !hasUserCRUDPermission(canUserCRUD) || - (isMlRule(rule?.type) && !hasMlPermissions), - isRuleEnabled: isExistingRule && (rule?.enabled ?? false), - }; -}; From ae63e2b6f2f68f94054614389db91a141d2ccf8f Mon Sep 17 00:00:00 2001 From: christineweng Date: Wed, 9 Aug 2023 15:46:48 -0500 Subject: [PATCH 3/4] fix type check --- .../rule_details_ui/pages/rule_details/index.tsx | 12 ++---------- .../components/rules/rule_info/index.test.tsx | 6 ++---- .../detections/components/rules/rule_info/index.tsx | 8 ++------ .../flyout/preview/components/rule_preview_title.tsx | 12 ++---------- 4 files changed, 8 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx index eb0a3c24db15c..791db19a92dcd 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx @@ -467,17 +467,9 @@ const RuleDetailsPageComponent: React.FC = ({ () => rule ? ( [ - , + , rule?.updated_by != null ? ( - + ) : ( '' ), diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.test.tsx index 6b070f2d4285a..2d78ecbb05f1b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.test.tsx @@ -16,7 +16,6 @@ describe('Rule related info', () => { const { getByTestId } = render( { it('should render created unknown when created by is not available', () => { const { getByTestId } = render( - + ); expect(getByTestId('createdBy')).toHaveTextContent( @@ -44,7 +43,6 @@ describe('Rule related info', () => { const { getByTestId } = render( { it('should render updated by correctly when updated by is not available', () => { const { getByTestId } = render( - + ); expect(getByTestId('updatedBy')).toHaveTextContent( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.tsx index 02d8b631a7c94..0c99e603f3358 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.tsx @@ -11,7 +11,6 @@ import { UNKNOWN_TEXT } from './translations'; import { FormattedDate } from '../../../../common/components/formatted_date'; interface CreatedByProps { - id: string; createdBy?: string; createdAt?: string; ['data-test-subj']?: string; @@ -21,7 +20,6 @@ interface CreatedByProps { * Created by and created at text that are shown on rule details */ export const CreatedBy: React.FC = ({ - id, createdBy, createdAt, 'data-test-subj': dataTestSubj, @@ -29,7 +27,7 @@ export const CreatedBy: React.FC = ({ return (
= ({ CreatedBy.displayName = 'CreatedBy'; interface UpdatedByProps { - id: string; updatedBy?: string; updatedAt?: string; ['data-test-subj']?: string; @@ -55,7 +52,6 @@ interface UpdatedByProps { * Updated by and updated at text that are shown on rule details */ export const UpdatedBy: React.FC = ({ - id, updatedBy, updatedAt, 'data-test-subj': dataTestSubj, @@ -63,7 +59,7 @@ export const UpdatedBy: React.FC = ({ return (
= ({ rule }) => { - + - + From 166e2f6d10de64dc51144e68b2b96902b1b48f83 Mon Sep 17 00:00:00 2001 From: christineweng Date: Thu, 10 Aug 2023 14:10:34 -0500 Subject: [PATCH 4/4] update icon --- .../public/detections/components/rules/rule_info/index.tsx | 4 ++-- .../public/flyout/right/components/description.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.tsx index 0c99e603f3358..edb9aa23275f4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_info/index.tsx @@ -17,7 +17,7 @@ interface CreatedByProps { } /** - * Created by and created at text that are shown on rule details + * Created by and created at text that are shown on rule details and rule preview in expandable flyout */ export const CreatedBy: React.FC = ({ createdBy, @@ -49,7 +49,7 @@ interface UpdatedByProps { } /** - * Updated by and updated at text that are shown on rule details + * Updated by and updated at text that are shown on rule details and rule preview in expandable flyout */ export const UpdatedBy: React.FC = ({ updatedBy, diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/description.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/description.tsx index c68ed0c39a26e..4f416c97fbb70 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/description.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/description.tsx @@ -61,7 +61,7 @@ export const Description: FC = () => {