diff --git a/x-pack/plugins/security_solution/public/common/components/wrapper_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/wrapper_page/__snapshots__/index.test.tsx.snap index 5ade447994a1e..89ed2f45a6bf1 100644 --- a/x-pack/plugins/security_solution/public/common/components/wrapper_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/wrapper_page/__snapshots__/index.test.tsx.snap @@ -7,33 +7,3 @@ exports[`WrapperPage it renders 1`] = `

`; - -exports[`WrapperPage restrict width custom max width when restrictWidth is number 1`] = ` - -

- Test page -

-
-`; - -exports[`WrapperPage restrict width custom max width when restrictWidth is string 1`] = ` - -

- Test page -

-
-`; - -exports[`WrapperPage restrict width default max width when restrictWidth is true 1`] = ` - -

- Test page -

-
-`; diff --git a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.test.tsx index 23cae15004e39..e87faf1b5c82c 100644 --- a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.test.tsx @@ -22,42 +22,4 @@ describe('WrapperPage', () => { expect(wrapper.find('Memo(WrapperPageComponent)')).toMatchSnapshot(); }); - - describe('restrict width', () => { - test('default max width when restrictWidth is true', () => { - const wrapper = shallow( - - -

{'Test page'}

-
-
- ); - - expect(wrapper.find('Memo(WrapperPageComponent)')).toMatchSnapshot(); - }); - - test('custom max width when restrictWidth is number', () => { - const wrapper = shallow( - - -

{'Test page'}

-
-
- ); - - expect(wrapper.find('Memo(WrapperPageComponent)')).toMatchSnapshot(); - }); - - test('custom max width when restrictWidth is string', () => { - const wrapper = shallow( - - -

{'Test page'}

-
-
- ); - - expect(wrapper.find('Memo(WrapperPageComponent)')).toMatchSnapshot(); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx index 0908c887d25f6..8eff52dae89f3 100644 --- a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx @@ -16,16 +16,6 @@ import { AppGlobalStyle } from '../page/index'; const Wrapper = styled.div` padding: ${(props) => `${props.theme.eui.paddingSizes.l}`}; - &.siemWrapperPage--restrictWidthDefault, - &.siemWrapperPage--restrictWidthCustom { - box-sizing: content-box; - margin: 0 auto; - } - - &.siemWrapperPage--restrictWidthDefault { - max-width: 1000px; - } - &.siemWrapperPage--fullHeight { height: 100%; display: flex; @@ -58,7 +48,6 @@ interface WrapperPageProps { const WrapperPageComponent: React.FC = ({ children, className, - restrictWidth, style, noPadding, noTimeline, @@ -74,20 +63,10 @@ const WrapperPageComponent: React.FC = ({ 'siemWrapperPage--noPadding': noPadding, 'siemWrapperPage--withTimeline': !noTimeline, 'siemWrapperPage--fullHeight': globalFullScreen, - 'siemWrapperPage--restrictWidthDefault': - restrictWidth && typeof restrictWidth === 'boolean' && restrictWidth === true, - 'siemWrapperPage--restrictWidthCustom': restrictWidth && typeof restrictWidth !== 'boolean', }); - let customStyle: WrapperPageProps['style']; - - if (restrictWidth && typeof restrictWidth !== 'boolean') { - const value = typeof restrictWidth === 'number' ? `${restrictWidth}px` : restrictWidth; - customStyle = { ...style, maxWidth: value }; - } - return ( - + {children} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.test.tsx index 87401408c3cc1..b2c22af3a75f9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.test.tsx @@ -5,21 +5,235 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import { SelectRuleType } from './index'; -import { useFormFieldMock } from '../../../../common/mock'; +import { TestProviders, useFormFieldMock } from '../../../../common/mock'; jest.mock('../../../../common/lib/kibana'); describe('SelectRuleType', () => { + // I do this to avoid the messy warning from happening + // Warning: React does not recognize the `isVisible` prop on a DOM element. + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(jest.fn()); + }); + + afterEach(() => { + jest.spyOn(console, 'error').mockRestore(); + }); + it('renders correctly', () => { const Component = () => { const field = useFormFieldMock(); - return ; + return ( + + ); }; const wrapper = shallow(); expect(wrapper.dive().find('[data-test-subj="selectRuleType"]')).toHaveLength(1); }); + + describe('update mode vs. non-update mode', () => { + it('renders all the cards when not in update mode', () => { + const field = useFormFieldMock({ value: 'query' }); + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="customRuleType"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="machineLearningRuleType"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="thresholdRuleType"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="eqlRuleType"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="threatMatchRuleType"]').exists()).toBeTruthy(); + }); + + it('renders only the card selected when in update mode of "eql"', () => { + const field = useFormFieldMock({ value: 'eql' }); + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="customRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="machineLearningRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="thresholdRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="eqlRuleType"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="threatMatchRuleType"]').exists()).toBeFalsy(); + }); + + it('renders only the card selected when in update mode of "machine_learning', () => { + const field = useFormFieldMock({ value: 'machine_learning' }); + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="customRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="machineLearningRuleType"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="thresholdRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="eqlRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="threatMatchRuleType"]').exists()).toBeFalsy(); + }); + + it('renders only the card selected when in update mode of "query', () => { + const field = useFormFieldMock({ value: 'query' }); + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="customRuleType"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="machineLearningRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="thresholdRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="eqlRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="threatMatchRuleType"]').exists()).toBeFalsy(); + }); + + it('renders only the card selected when in update mode of "threshold"', () => { + const field = useFormFieldMock({ value: 'threshold' }); + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="customRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="machineLearningRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="thresholdRuleType"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="eqlRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="threatMatchRuleType"]').exists()).toBeFalsy(); + }); + + it('renders only the card selected when in update mode of "threat_match', () => { + const field = useFormFieldMock({ value: 'threat_match' }); + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="customRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="machineLearningRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="thresholdRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="eqlRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="threatMatchRuleType"]').exists()).toBeTruthy(); + }); + }); + + describe('permissions', () => { + it('renders machine learning as disabled if "hasValidLicense" is false and it is not selected', () => { + const field = useFormFieldMock({ value: 'query' }); + const wrapper = mount( + + + + ); + expect( + wrapper.find('[data-test-subj="machineLearningRuleType"]').first().prop('isDisabled') + ).toEqual(true); + }); + + it('renders machine learning as not disabled if "hasValidLicense" is false and it is selected', () => { + const field = useFormFieldMock({ value: 'machine_learning' }); + const wrapper = mount( + + + + ); + expect( + wrapper.find('[data-test-subj="machineLearningRuleType"]').first().prop('isDisabled') + ).toEqual(false); + }); + + it('renders machine learning as disabled if "isMlAdmin" is false and it is not selected', () => { + const field = useFormFieldMock({ value: 'query' }); + const wrapper = mount( + + + + ); + expect( + wrapper.find('[data-test-subj="machineLearningRuleType"]').first().prop('isDisabled') + ).toEqual(true); + }); + + it('renders machine learning as not disabled if "isMlAdmin" is false and it is selected', () => { + const field = useFormFieldMock({ value: 'machine_learning' }); + const wrapper = mount( + + + + ); + expect( + wrapper.find('[data-test-subj="machineLearningRuleType"]').first().prop('isDisabled') + ).toEqual(false); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx index 4b96b8a0ad7bd..c365982db9cd8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx @@ -22,19 +22,19 @@ import { MlCardDescription } from './ml_card_description'; import EqlSearchIcon from './eql_search_icon.svg'; interface SelectRuleTypeProps { - describedByIds?: string[]; + describedByIds: string[]; field: FieldHook; - hasValidLicense?: boolean; - isMlAdmin?: boolean; - isReadOnly?: boolean; + hasValidLicense: boolean; + isMlAdmin: boolean; + isUpdateView: boolean; } export const SelectRuleType: React.FC = ({ describedByIds = [], field, - isReadOnly = false, - hasValidLicense = false, - isMlAdmin = false, + isUpdateView, + hasValidLicense, + isMlAdmin, }) => { const ruleType = field.value as Type; const setType = useCallback( @@ -48,54 +48,54 @@ export const SelectRuleType: React.FC = ({ const setQuery = useCallback(() => setType('query'), [setType]); const setThreshold = useCallback(() => setType('threshold'), [setType]); const setThreatMatch = useCallback(() => setType('threat_match'), [setType]); - const mlCardDisabled = isReadOnly || !hasValidLicense || !isMlAdmin; const licensingUrl = useKibana().services.application.getUrlForApp('kibana', { path: '#/management/stack/license_management', }); const eqlSelectableConfig = useMemo( () => ({ - isDisabled: isReadOnly, onClick: setEql, isSelected: isEqlRule(ruleType), + isVisible: !isUpdateView || isEqlRule(ruleType), }), - [isReadOnly, ruleType, setEql] + [ruleType, setEql, isUpdateView] ); const querySelectableConfig = useMemo( () => ({ - isDisabled: isReadOnly, onClick: setQuery, isSelected: isQueryRule(ruleType), + isVisible: !isUpdateView || isQueryRule(ruleType), }), - [isReadOnly, ruleType, setQuery] + [ruleType, setQuery, isUpdateView] ); const mlSelectableConfig = useMemo( () => ({ - isDisabled: mlCardDisabled, + isDisabled: !hasValidLicense || !isMlAdmin, onClick: setMl, isSelected: isMlRule(ruleType), + isVisible: !isUpdateView || isMlRule(ruleType), }), - [mlCardDisabled, ruleType, setMl] + [ruleType, setMl, isUpdateView, hasValidLicense, isMlAdmin] ); const thresholdSelectableConfig = useMemo( () => ({ - isDisabled: isReadOnly, onClick: setThreshold, isSelected: isThresholdRule(ruleType), + isVisible: !isUpdateView || isThresholdRule(ruleType), }), - [isReadOnly, ruleType, setThreshold] + [ruleType, setThreshold, isUpdateView] ); const threatMatchSelectableConfig = useMemo( () => ({ - isDisabled: isReadOnly, onClick: setThreatMatch, isSelected: isThreatMatchRule(ruleType), + isVisible: !isUpdateView || isThreatMatchRule(ruleType), }), - [isReadOnly, ruleType, setThreatMatch] + [ruleType, setThreatMatch, isUpdateView] ); return ( @@ -105,63 +105,83 @@ export const SelectRuleType: React.FC = ({ describedByIds={describedByIds} label={field.label} > - - - } - isDisabled={querySelectableConfig.isDisabled && !querySelectableConfig.isSelected} - selectable={querySelectableConfig} - /> - - - - } - icon={} - isDisabled={mlSelectableConfig.isDisabled && !mlSelectableConfig.isSelected} - selectable={mlSelectableConfig} - /> - - - } - isDisabled={ - thresholdSelectableConfig.isDisabled && !thresholdSelectableConfig.isSelected - } - selectable={thresholdSelectableConfig} - /> - - - } - isDisabled={eqlSelectableConfig.isDisabled && !eqlSelectableConfig.isSelected} - selectable={eqlSelectableConfig} - /> - - - } - isDisabled={ - threatMatchSelectableConfig.isDisabled && !threatMatchSelectableConfig.isSelected - } - selectable={threatMatchSelectableConfig} - /> - + + {querySelectableConfig.isVisible && ( + + } + selectable={querySelectableConfig} + layout="horizontal" + textAlign="left" + /> + + )} + {mlSelectableConfig.isVisible && ( + + + } + icon={} + isDisabled={mlSelectableConfig.isDisabled && !mlSelectableConfig.isSelected} + selectable={mlSelectableConfig} + layout="horizontal" + textAlign="left" + /> + + )} + {thresholdSelectableConfig.isVisible && ( + + } + selectable={thresholdSelectableConfig} + layout="horizontal" + textAlign="left" + /> + + )} + {eqlSelectableConfig.isVisible && ( + + } + selectable={eqlSelectableConfig} + layout="horizontal" + textAlign="left" + /> + + )} + {threatMatchSelectableConfig.isVisible && ( + + } + selectable={threatMatchSelectableConfig} + layout="horizontal" + textAlign="left" + /> + + )} ); 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 9bd0e4fb4da5d..d13635bfd1b50 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 @@ -236,7 +236,7 @@ const StepDefineRuleComponent: FC = ({ component={SelectRuleType} componentProps={{ describedByIds: ['detectionEngineStepDefineRuleType'], - isReadOnly: isUpdateView, + isUpdateView, hasValidLicense: hasMlLicense(mlCapabilities), isMlAdmin: hasMlAdminPermissions(mlCapabilities), }} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index 542b7b1b84c3c..9d54879ee7495 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonEmpty, EuiAccordion, EuiHorizontalRule, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiAccordion, + EuiHorizontalRule, + EuiPanel, + EuiSpacer, + EuiFlexGroup, +} from '@elastic/eui'; import React, { useCallback, useRef, useState, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import styled, { StyledComponent } from 'styled-components'; @@ -28,7 +35,12 @@ import { StepScheduleRule } from '../../../../components/rules/step_schedule_rul import { StepRuleActions } from '../../../../components/rules/step_rule_actions'; import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page'; import * as RuleI18n from '../translations'; -import { redirectToDetections, getActionMessageParams, userHasNoPermissions } from '../helpers'; +import { + redirectToDetections, + getActionMessageParams, + userHasNoPermissions, + MaxWidthEuiFlexItem, +} from '../helpers'; import { RuleStep, RuleStepsFormData, RuleStepsFormHooks } from '../types'; import { formatRule, stepIsValid } from './helpers'; import * as i18n from './translations'; @@ -273,151 +285,155 @@ const CreateRulePageComponent: React.FC = () => { return ( <> - - - - editStep(RuleStep.defineRule)} - > - {i18n.EDIT_RULE} - - ) - } - > - - submitStep(RuleStep.defineRule)} - descriptionColumns="singleSplit" - /> - - - - - editStep(RuleStep.aboutRule)} - > - {i18n.EDIT_RULE} - - ) - } - > - - submitStep(RuleStep.aboutRule)} - /> - - - - - editStep(RuleStep.scheduleRule)} - > - {i18n.EDIT_RULE} - - ) - } - > - - submitStep(RuleStep.scheduleRule)} - /> - - - - - editStep(RuleStep.ruleActions)} - > - {i18n.EDIT_RULE} - - ) - } - > - - + + + submitStep(RuleStep.ruleActions)} - actionMessageParams={actionMessageParams} + title={i18n.PAGE_TITLE} /> - - + + editStep(RuleStep.defineRule)} + > + {i18n.EDIT_RULE} + + ) + } + > + + submitStep(RuleStep.defineRule)} + descriptionColumns="singleSplit" + /> + + + + + editStep(RuleStep.aboutRule)} + > + {i18n.EDIT_RULE} + + ) + } + > + + submitStep(RuleStep.aboutRule)} + /> + + + + + editStep(RuleStep.scheduleRule)} + > + {i18n.EDIT_RULE} + + ) + } + > + + submitStep(RuleStep.scheduleRule)} + /> + + + + + editStep(RuleStep.ruleActions)} + > + {i18n.EDIT_RULE} + + ) + } + > + + submitStep(RuleStep.ruleActions)} + actionMessageParams={actionMessageParams} + /> + + + + diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx index e2772af72da06..fa74565be660f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx @@ -47,6 +47,7 @@ import { redirectToDetections, getActionMessageParams, userHasNoPermissions, + MaxWidthEuiFlexItem, } from '../helpers'; import * as ruleI18n from '../translations'; import { RuleStep, RuleStepsFormHooks, RuleStepsFormData, RuleStepsData } from '../types'; @@ -332,75 +333,79 @@ const EditRulePageComponent: FC = () => { return ( <> - - - {invalidSteps.length > 0 && ( - - { - if (t === RuleStep.aboutRule) { - return ruleI18n.ABOUT; - } else if (t === RuleStep.defineRule) { - return ruleI18n.DEFINITION; - } else if (t === RuleStep.scheduleRule) { - return ruleI18n.SCHEDULE; - } else if (t === RuleStep.ruleActions) { - return ruleI18n.RULE_ACTIONS; - } - return t; - }) - .join(', '), + + + + - - )} - - t.id === activeStep)} - onTabClick={onTabClick} - tabs={tabs} - /> + {invalidSteps.length > 0 && ( + + { + if (t === RuleStep.aboutRule) { + return ruleI18n.ABOUT; + } else if (t === RuleStep.defineRule) { + return ruleI18n.DEFINITION; + } else if (t === RuleStep.scheduleRule) { + return ruleI18n.SCHEDULE; + } else if (t === RuleStep.ruleActions) { + return ruleI18n.RULE_ACTIONS; + } + return t; + }) + .join(', '), + }} + /> + + )} - + t.id === activeStep)} + onTabClick={onTabClick} + tabs={tabs} + /> - - - - {i18n.CANCEL} - - + - - - {i18n.SAVE_CHANGES} - - + + + {i18n.CANCEL} + + + + + + {i18n.SAVE_CHANGES} + + + + diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 456bf8419a1f7..4dbcffbc807ec 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -9,6 +9,8 @@ import moment from 'moment'; import memoizeOne from 'memoize-one'; import { useLocation } from 'react-router-dom'; +import styled from 'styled-components'; +import { EuiFlexItem } from '@elastic/eui'; import { ActionVariable } from '../../../../../../triggers_actions_ui/public'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { assertUnreachable } from '../../../../../common/utility_types'; @@ -377,3 +379,8 @@ export const getActionMessageParams = memoizeOne((ruleType: Type | undefined): A // typed as null not undefined as the initial state for this value is null. export const userHasNoPermissions = (canUserCRUD: boolean | null): boolean => canUserCRUD != null ? !canUserCRUD : false; + +export const MaxWidthEuiFlexItem = styled(EuiFlexItem)` + max-width: 1000px; + overflow: hidden; +`;