From e9319360e21ed6d85e00b2f99c2764e9e228a0f2 Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Mon, 13 Jan 2020 21:59:45 +0100 Subject: [PATCH] [SIEM] Detection Engine Create Rule Design Review #1 (#54442) --- .../detection_engine/rules/all/columns.tsx | 19 +- .../rules/components/add_item_form/index.tsx | 23 +- .../assets/list_tree_icon.svg | 1 + .../components/description_step/helpers.tsx | 147 +++++++----- .../components/description_step/index.tsx | 48 ++-- .../rules/components/mitre/index.tsx | 16 +- .../components/optional_field_label/index.tsx | 16 ++ .../rules/components/query_bar/index.tsx | 2 +- .../components/schedule_item_form/index.tsx | 42 +++- .../rules/components/severity_badge/index.tsx | 32 +++ .../rules/components/step_about_rule/data.tsx | 17 +- .../components/step_about_rule/index.tsx | 150 ++++++------ .../components/step_about_rule/schema.tsx | 14 +- .../components/step_content_wrapper/index.tsx | 18 ++ .../components/step_define_rule/index.tsx | 227 ++++++++++-------- .../components/step_schedule_rule/index.tsx | 186 +++++++------- .../components/step_schedule_rule/schema.tsx | 6 +- .../detection_engine/rules/create/index.tsx | 47 ++-- .../detection_engine/rules/details/index.tsx | 2 +- .../pages/detection_engine/rules/types.ts | 1 + 20 files changed, 588 insertions(+), 426 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/assets/list_tree_icon.svg create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index 95b9c9324894fb..636cbb8ecb0647 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -8,7 +8,6 @@ import { EuiBadge, - EuiHealth, EuiIconTip, EuiLink, EuiTextColor, @@ -17,7 +16,6 @@ import { } from '@elastic/eui'; import * as H from 'history'; import React from 'react'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { getEmptyTagValue } from '../../../../components/empty_value'; import { deleteRulesAction, @@ -32,6 +30,7 @@ import { TableData } from '../types'; import * as i18n from '../translations'; import { PreferenceFormattedDate } from '../../../../components/formatted_date'; import { RuleSwitch } from '../components/rule_switch'; +import { SeverityBadge } from '../components/severity_badge'; const getActions = (dispatch: React.Dispatch, history: H.History) => [ { @@ -92,21 +91,7 @@ export const getColumns = ( { field: 'severity', name: i18n.COLUMN_SEVERITY, - render: (value: TableData['severity']) => ( - - {value} - - ), + render: (value: TableData['severity']) => , truncateText: true, }, { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx index b3cc81b5cdfcfc..0c75da7d8a6320 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx @@ -37,6 +37,25 @@ const MyEuiFormRow = styled(EuiFormRow)` } `; +export const MyAddItemButton = styled(EuiButtonEmpty)` + margin-top: 4px; + + &.euiButtonEmpty--xSmall { + font-size: 12px; + } + + .euiIcon { + width: 12px; + height: 12px; + } +`; + +MyAddItemButton.defaultProps = { + flush: 'left', + iconType: 'plusInCircle', + size: 'xs', +}; + export const AddItem = ({ addText, dataTestSubj, @@ -160,9 +179,9 @@ export const AddItem = ({ ); })} - + {addText} - + ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/assets/list_tree_icon.svg b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/assets/list_tree_icon.svg new file mode 100644 index 00000000000000..527d8d445bc03e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/assets/list_tree_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx index 09d0c1131ea10d..e8b6919165c8bf 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -9,12 +9,10 @@ import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, - EuiHealth, EuiLink, - EuiText, - EuiListGroup, + EuiButtonEmpty, + EuiSpacer, } from '@elastic/eui'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { isEmpty } from 'lodash/fp'; import React from 'react'; @@ -27,6 +25,11 @@ import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_ import { FilterLabel } from './filter_label'; import * as i18n from './translations'; import { BuildQueryBarDescription, BuildThreatsDescription, ListItems } from './types'; +import { SeverityBadge } from '../severity_badge'; +import ListTreeIcon from './assets/list_tree_icon.svg'; + +const isNotEmptyArray = (values: string[]) => + !isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0; const EuiBadgeWrap = styled(EuiBadge)` .euiBadge__text { @@ -97,10 +100,17 @@ const ThreatsEuiFlexGroup = styled(EuiFlexGroup)` } `; -const MyEuiListGroup = styled(EuiListGroup)` - padding: 0px; - .euiListGroupItem__button { - padding: 0px; +const TechniqueLinkItem = styled(EuiButtonEmpty)` + .euiIcon { + width: 8px; + height: 8px; + } +`; + +const ReferenceLinkItem = styled(EuiButtonEmpty)` + .euiIcon { + width: 12px; + height: 12px; } `; @@ -118,28 +128,31 @@ export const buildThreatsDescription = ({ const tactic = tacticsOptions.find(t => t.name === threat.tactic.name); return ( - -
- - {tactic != null ? tactic.text : ''} - -
- { - const myTechnique = techniquesOptions.find(t => t.name === technique.name); - return { - label: myTechnique != null ? myTechnique.label : '', - href: technique.reference, - target: '_blank', - }; - })} - /> -
+ + {tactic != null ? tactic.text : ''} + + + {threat.techniques.map(technique => { + const myTechnique = techniquesOptions.find(t => t.name === technique.name); + return ( + + + {myTechnique != null ? myTechnique.label : ''} + + + ); + })} +
); })} + ), }, @@ -148,12 +161,34 @@ export const buildThreatsDescription = ({ return []; }; +export const buildUnorderedListArrayDescription = ( + label: string, + field: string, + values: string[] +): ListItems[] => { + if (isNotEmptyArray(values)) { + return [ + { + title: label, + description: ( +
    + {values.map((val: string) => + isEmpty(val) ? null :
  • {val}
  • + )} +
+ ), + }, + ]; + } + return []; +}; + export const buildStringArrayDescription = ( label: string, field: string, values: string[] ): ListItems[] => { - if (!isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0) { + if (isNotEmptyArray(values)) { return [ { title: label, @@ -174,46 +209,34 @@ export const buildStringArrayDescription = ( return []; }; -export const buildSeverityDescription = (label: string, value: string): ListItems[] => { - return [ - { - title: label, - description: ( - - {value} - - ), - }, - ]; -}; +export const buildSeverityDescription = (label: string, value: string): ListItems[] => [ + { + title: label, + description: , + }, +]; export const buildUrlsDescription = (label: string, values: string[]): ListItems[] => { - if (!isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0) { + if (isNotEmptyArray(values)) { return [ { title: label, description: ( - ({ - label: val, - href: val, - iconType: 'link', - size: 'xs', - target: '_blank', - }))} - /> + + {values.map((val: string) => ( + + + {val} + + + ))} + ), }, ]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx index af4f93c0fdbcd2..8cf1601e2c4b64 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiDescriptionList, EuiFlexGroup, EuiFlexItem, EuiTextArea } from '@elastic/eui'; +import { EuiDescriptionList, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEmpty, chunk, get, pick } from 'lodash/fp'; import React, { memo, useState } from 'react'; -import styled from 'styled-components'; import { IIndexPattern, @@ -26,6 +25,7 @@ import { buildSeverityDescription, buildStringArrayDescription, buildThreatsDescription, + buildUnorderedListArrayDescription, buildUrlsDescription, } from './helpers'; @@ -36,15 +36,6 @@ interface StepRuleDescriptionProps { schema: FormSchema; } -const EuiFlexItemWidth = styled(EuiFlexItem)<{ direction: string }>` - ${props => (props.direction === 'row' ? 'width : 50%;' : 'width: 100%;')}; -`; - -const MyEuiTextArea = styled(EuiTextArea)` - max-width: 100%; - height: 80px; -`; - const StepRuleDescriptionComponent: React.FC = ({ data, direction = 'row', @@ -62,13 +53,24 @@ const StepRuleDescriptionComponent: React.FC = ({ ], [] ); + + if (direction === 'row') { + return ( + + {chunk(Math.ceil(listItems.length / 2), listItems).map((chunkListItems, index) => ( + + + + ))} + + ); + } + return ( - - {chunk(Math.ceil(listItems.length / 2), listItems).map((chunkListItems, index) => ( - - - - ))} + + + + ); }; @@ -123,18 +125,28 @@ const getDescriptionItem = ( return [ { title: label, - description: , + description: get(field, value), }, ]; } else if (field === 'references') { const urls: string[] = get(field, value); return buildUrlsDescription(label, urls); + } else if (field === 'falsePositives') { + const values: string[] = get(field, value); + return buildUnorderedListArrayDescription(label, field, values); } else if (Array.isArray(get(field, value))) { const values: string[] = get(field, value); return buildStringArrayDescription(label, field, values); } else if (field === 'severity') { const val: string = get(field, value); return buildSeverityDescription(label, val); + } else if (field === 'riskScore') { + return [ + { + title: label, + description: get(field, value), + }, + ]; } else if (field === 'timeline') { const timeline = get(field, value) as FieldValueTimeline; return [ diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx index 2c19e99e901140..f9a22c37cfdf00 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx @@ -5,7 +5,6 @@ */ import { - EuiButtonEmpty, EuiButtonIcon, EuiFormRow, EuiSuperSelect, @@ -24,6 +23,7 @@ import * as Rulei18n from '../../translations'; import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; import { threatsDefault } from '../step_about_rule/default_value'; import { IMitreEnterpriseAttack } from '../../types'; +import { MyAddItemButton } from '../add_item_form'; import { isMitreAttackInvalid } from './helpers'; import * as i18n from './translations'; @@ -134,13 +134,19 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI const getSelectTechniques = (item: IMitreEnterpriseAttack, index: number, disabled: boolean) => { const invalid = isMitreAttackInvalid(item.tactic.name, item.techniques); + const options = techniquesOptions.filter(t => t.tactics.includes(kebabCase(item.tactic.name))); + const selectedOptions = item.techniques.map(technic => ({ + ...technic, + label: `${technic.name} (${technic.id})`, // API doesn't allow for label field + })); + return ( t.tactics.includes(kebabCase(item.tactic.name)))} - selectedOptions={item.techniques} + options={options} + selectedOptions={selectedOptions} onChange={updateTechniques.bind(null, index)} isDisabled={disabled || item.tactic.name === 'none'} fullWidth={true} @@ -202,9 +208,9 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI {values.length - 1 !== index && } ))} - + {i18n.ADD_MITRE_ATTACK} - + ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.tsx new file mode 100644 index 00000000000000..0dab87b0a3b744 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiText } from '@elastic/eui'; +import React from 'react'; + +import * as RuleI18n from '../../translations'; + +export const OptionalFieldLabel = ( + + {RuleI18n.OPTIONAL_FIELD} + +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx index 3e39beb6e61b78..46a7a13ec03f16 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx @@ -51,7 +51,7 @@ interface QueryBarDefineRuleProps { const StyledEuiFormRow = styled(EuiFormRow)` .kbnTypeahead__items { - max-height: 14vh !important; + max-height: 45vh !important; } .globalQueryBar { padding: 4px 0px 0px 0px; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx index 8097c27cddfe8b..fa4bea319f8590 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiFieldNumber, EuiFormRow, EuiSelect } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFieldNumber, + EuiFormRow, + EuiSelect, + EuiFormControlLayout, +} from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; @@ -26,10 +33,28 @@ const timeTypeOptions = [ { value: 'h', text: I18n.HOURS }, ]; +// move optional label to the end of input +const StyledLabelAppend = styled(EuiFlexItem)` + &.euiFlexItem.euiFlexItem--flexGrowZero { + margin-left: 31px; + } +`; + const StyledEuiFormRow = styled(EuiFormRow)` + max-width: none; + .euiFormControlLayout { max-width: 200px !important; } + + .euiFormControlLayout__childrenWrapper > *:first-child { + box-shadow: none; + height: 38px; + } + + .euiFormControlLayout:not(:first-child) { + border-left: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; + } `; const MyEuiSelect = styled(EuiSelect)` @@ -89,9 +114,9 @@ export const ScheduleItem = ({ dataTestSubj, field, idAria, isDisabled }: Schedu {field.label} - + {field.labelAppend} - + ), [field.label, field.labelAppend] @@ -107,7 +132,7 @@ export const ScheduleItem = ({ dataTestSubj, field, idAria, isDisabled }: Schedu data-test-subj={dataTestSubj} describedByIds={idAria ? [idAria] : undefined} > - } - fullWidth - min={0} - onChange={onChangeTimeVal} - value={timeVal} - {...rest} - /> + > + + ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.tsx new file mode 100644 index 00000000000000..09c02dfca56f96 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { upperFirst } from 'lodash/fp'; +import React from 'react'; +import { EuiHealth } from '@elastic/eui'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +interface Props { + value: string; +} + +const SeverityBadgeComponent: React.FC = ({ value }) => ( + + {upperFirst(value)} + +); + +export const SeverityBadge = React.memo(SeverityBadgeComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx index 9fb64189ebd1af..269d2d4509508d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import styled from 'styled-components'; import { EuiHealth } from '@elastic/eui'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import React from 'react'; @@ -16,22 +17,30 @@ interface SeverityOptionItem { inputDisplay: React.ReactElement; } +const StyledEuiHealth = styled(EuiHealth)` + line-height: inherit; +`; + export const severityOptions: SeverityOptionItem[] = [ { value: 'low', - inputDisplay: {I18n.LOW}, + inputDisplay: {I18n.LOW}, }, { value: 'medium', - inputDisplay: {I18n.MEDIUM} , + inputDisplay: ( + {I18n.MEDIUM} + ), }, { value: 'high', - inputDisplay: {I18n.HIGH} , + inputDisplay: {I18n.HIGH}, }, { value: 'critical', - inputDisplay: {I18n.CRITICAL} , + inputDisplay: ( + {I18n.CRITICAL} + ), }, ]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx index 8956776dcd3b21..0e03a11776fb7e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx @@ -6,7 +6,7 @@ import { EuiButton, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEqual, get } from 'lodash/fp'; -import React, { memo, useCallback, useEffect, useState } from 'react'; +import React, { FC, memo, useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; import { RuleStepProps, RuleStep, AboutStepRule } from '../../types'; @@ -22,6 +22,7 @@ import { isUrlInvalid } from './helpers'; import { schema } from './schema'; import * as I18n from './translations'; import { PickTimeline } from '../pick_timeline'; +import { StepContentWrapper } from '../step_content_wrapper'; const CommonUseField = getUseField({ component: Field }); @@ -33,64 +34,67 @@ const TagContainer = styled.div` margin-top: 16px; `; -export const StepAboutRule = memo( - ({ - defaultValues, - descriptionDirection = 'row', - isReadOnlyView, - isUpdateView = false, - isLoading, - setForm, - setStepData, - }) => { - const [myStepData, setMyStepData] = useState(stepAboutDefaultValue); +const StepAboutRuleComponent: FC = ({ + addPadding = false, + defaultValues, + descriptionDirection = 'row', + isReadOnlyView, + isUpdateView = false, + isLoading, + setForm, + setStepData, +}) => { + const [myStepData, setMyStepData] = useState(stepAboutDefaultValue); - const { form } = useForm({ - defaultValue: myStepData, - options: { stripEmptyFields: false }, - schema, - }); + const { form } = useForm({ + defaultValue: myStepData, + options: { stripEmptyFields: false }, + schema, + }); - const onSubmit = useCallback(async () => { - if (setStepData) { - setStepData(RuleStep.aboutRule, null, false); - const { isValid, data } = await form.submit(); - if (isValid) { - setStepData(RuleStep.aboutRule, data, isValid); - setMyStepData({ ...data, isNew: false } as AboutStepRule); - } + const onSubmit = useCallback(async () => { + if (setStepData) { + setStepData(RuleStep.aboutRule, null, false); + const { isValid, data } = await form.submit(); + if (isValid) { + setStepData(RuleStep.aboutRule, data, isValid); + setMyStepData({ ...data, isNew: false } as AboutStepRule); } - }, [form]); + } + }, [form]); - useEffect(() => { - const { isNew, ...initDefaultValue } = myStepData; - if (defaultValues != null && !isEqual(initDefaultValue, defaultValues)) { - const myDefaultValues = { - ...defaultValues, - isNew: false, - }; - setMyStepData(myDefaultValues); - if (!isReadOnlyView) { - Object.keys(schema).forEach(key => { - const val = get(key, myDefaultValues); - if (val != null) { - form.setFieldValue(key, val); - } - }); - } + useEffect(() => { + const { isNew, ...initDefaultValue } = myStepData; + if (defaultValues != null && !isEqual(initDefaultValue, defaultValues)) { + const myDefaultValues = { + ...defaultValues, + isNew: false, + }; + setMyStepData(myDefaultValues); + if (!isReadOnlyView) { + Object.keys(schema).forEach(key => { + const val = get(key, myDefaultValues); + if (val != null) { + form.setFieldValue(key, val); + } + }); } - }, [defaultValues]); + } + }, [defaultValues]); - useEffect(() => { - if (setForm != null) { - setForm(RuleStep.aboutRule, form); - } - }, [form]); + useEffect(() => { + if (setForm != null) { + setForm(RuleStep.aboutRule, form); + } + }, [form]); - return isReadOnlyView && myStepData != null ? ( + return isReadOnlyView && myStepData != null ? ( + - ) : ( - <> + + ) : ( + <> +
( }} - {!isUpdateView && ( - <> - - - - - {RuleI18n.CONTINUE} - - - - - )} - - ); - } -); +
+ {!isUpdateView && ( + <> + + + + + {RuleI18n.CONTINUE} + + + + + )} + + ); +}; + +export const StepAboutRule = memo(StepAboutRuleComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx index 008a1b48610d69..3de0e7605f3d97 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx @@ -4,11 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; -import * as RuleI18n from '../../translations'; import { IMitreEnterpriseAttack } from '../../types'; import { FIELD_TYPES, @@ -18,6 +15,7 @@ import { ERROR_CODE, } from '../shared_imports'; import { isMitreAttackInvalid } from '../mitre/helpers'; +import { OptionalFieldLabel } from '../optional_field_label'; import { isUrlInvalid } from './helpers'; import * as I18n from './translations'; @@ -108,7 +106,7 @@ export const schema: FormSchema = { defaultMessage: 'Reference URLs', } ), - labelAppend: {RuleI18n.OPTIONAL_FIELD}, + labelAppend: OptionalFieldLabel, validations: [ { validator: ( @@ -136,10 +134,10 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldFalsePositiveLabel', { - defaultMessage: 'False positives examples', + defaultMessage: 'False positive examples', } ), - labelAppend: {RuleI18n.OPTIONAL_FIELD}, + labelAppend: OptionalFieldLabel, }, threats: { label: i18n.translate( @@ -148,7 +146,7 @@ export const schema: FormSchema = { defaultMessage: 'MITRE ATT&CK\\u2122', } ), - labelAppend: {RuleI18n.OPTIONAL_FIELD}, + labelAppend: OptionalFieldLabel, validations: [ { validator: ( @@ -184,6 +182,6 @@ export const schema: FormSchema = { 'Type one or more custom identifying tags for this rule. Press enter after each tag to begin a new one.', } ), - labelAppend: {RuleI18n.OPTIONAL_FIELD}, + labelAppend: OptionalFieldLabel, }, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx new file mode 100644 index 00000000000000..b04a321dab05bc --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled from 'styled-components'; + +const StyledDiv = styled.div<{ addPadding: boolean }>` + padding-left: ${({ addPadding }) => addPadding && '53px'}; /* to align with the step title */ +`; + +StyledDiv.defaultProps = { + addPadding: false, +}; + +export const StepContentWrapper = React.memo(StyledDiv); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index ecd2ce442238fa..6bdef4a69af1e6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -12,7 +12,8 @@ import { EuiButton, } from '@elastic/eui'; import { isEmpty, isEqual, get } from 'lodash/fp'; -import React, { memo, useCallback, useState, useEffect } from 'react'; +import React, { FC, memo, useCallback, useState, useEffect } from 'react'; +import styled from 'styled-components'; import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules'; @@ -22,6 +23,7 @@ import * as RuleI18n from '../../translations'; import { DefineStepRule, RuleStep, RuleStepProps } from '../../types'; import { StepRuleDescription } from '../description_step'; import { QueryBarDefineRule } from '../query_bar'; +import { StepContentWrapper } from '../step_content_wrapper'; import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; import { schema } from './schema'; import * as i18n from './translations'; @@ -42,6 +44,20 @@ const stepDefineDefaultValue = { }, }; +const MyLabelButton = styled(EuiButtonEmpty)` + height: 18px; + font-size: 12px; + + .euiIcon { + width: 14px; + height: 14px; + } +`; + +MyLabelButton.defaultProps = { + flush: 'right', +}; + const getStepDefaultValue = ( indicesConfig: string[], defaultValues: DefineStepRule | null @@ -59,106 +75,104 @@ const getStepDefaultValue = ( } }; -export const StepDefineRule = memo( - ({ - defaultValues, - descriptionDirection = 'row', - isReadOnlyView, - isLoading, - isUpdateView = false, - resizeParentContainer, - setForm, - setStepData, - }) => { - const [openTimelineSearch, setOpenTimelineSearch] = useState(false); - const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(false); - const [indicesConfig] = useUiSetting$(DEFAULT_INDEX_KEY); - const [mylocalIndicesConfig, setMyLocalIndicesConfig] = useState( - defaultValues != null ? defaultValues.index : indicesConfig ?? [] - ); - const [ - { - browserFields, - indexPatterns: indexPatternQueryBar, - isLoading: indexPatternLoadingQueryBar, - }, - ] = useFetchIndexPatterns(mylocalIndicesConfig); - const [myStepData, setMyStepData] = useState( - getStepDefaultValue(indicesConfig, null) - ); - - const { form } = useForm({ - defaultValue: myStepData, - options: { stripEmptyFields: false }, - schema, - }); - - const onSubmit = useCallback(async () => { - if (setStepData) { - setStepData(RuleStep.defineRule, null, false); - const { isValid, data } = await form.submit(); - if (isValid && setStepData) { - setStepData(RuleStep.defineRule, data, isValid); - setMyStepData({ ...data, isNew: false } as DefineStepRule); - } +const StepDefineRuleComponent: FC = ({ + addPadding = false, + defaultValues, + descriptionDirection = 'row', + isReadOnlyView, + isLoading, + isUpdateView = false, + setForm, + setStepData, +}) => { + const [openTimelineSearch, setOpenTimelineSearch] = useState(false); + const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(false); + const [indicesConfig] = useUiSetting$(DEFAULT_INDEX_KEY); + const [mylocalIndicesConfig, setMyLocalIndicesConfig] = useState( + defaultValues != null ? defaultValues.index : indicesConfig ?? [] + ); + const [ + { browserFields, indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar }, + ] = useFetchIndexPatterns(mylocalIndicesConfig); + const [myStepData, setMyStepData] = useState( + getStepDefaultValue(indicesConfig, null) + ); + + const { form } = useForm({ + defaultValue: myStepData, + options: { stripEmptyFields: false }, + schema, + }); + + const onSubmit = useCallback(async () => { + if (setStepData) { + setStepData(RuleStep.defineRule, null, false); + const { isValid, data } = await form.submit(); + if (isValid && setStepData) { + setStepData(RuleStep.defineRule, data, isValid); + setMyStepData({ ...data, isNew: false } as DefineStepRule); } - }, [form]); - - useEffect(() => { - if (indicesConfig != null && defaultValues != null) { - const myDefaultValues = getStepDefaultValue(indicesConfig, defaultValues); - if (!isEqual(myDefaultValues, myStepData)) { - setMyStepData(myDefaultValues); - setLocalUseIndicesConfig(isEqual(myDefaultValues.index, indicesConfig)); - if (!isReadOnlyView) { - Object.keys(schema).forEach(key => { - const val = get(key, myDefaultValues); - if (val != null) { - form.setFieldValue(key, val); - } - }); - } + } + }, [form]); + + useEffect(() => { + if (indicesConfig != null && defaultValues != null) { + const myDefaultValues = getStepDefaultValue(indicesConfig, defaultValues); + if (!isEqual(myDefaultValues, myStepData)) { + setMyStepData(myDefaultValues); + setLocalUseIndicesConfig(isEqual(myDefaultValues.index, indicesConfig)); + if (!isReadOnlyView) { + Object.keys(schema).forEach(key => { + const val = get(key, myDefaultValues); + if (val != null) { + form.setFieldValue(key, val); + } + }); } } - }, [defaultValues, indicesConfig]); + } + }, [defaultValues, indicesConfig]); - useEffect(() => { - if (setForm != null) { - setForm(RuleStep.defineRule, form); - } - }, [form]); + useEffect(() => { + if (setForm != null) { + setForm(RuleStep.defineRule, form); + } + }, [form]); - const handleResetIndices = useCallback(() => { - const indexField = form.getFields().index; - indexField.setValue(indicesConfig); - }, [form, indicesConfig]); + const handleResetIndices = useCallback(() => { + const indexField = form.getFields().index; + indexField.setValue(indicesConfig); + }, [form, indicesConfig]); - const handleOpenTimelineSearch = useCallback(() => { - setOpenTimelineSearch(true); - }, []); + const handleOpenTimelineSearch = useCallback(() => { + setOpenTimelineSearch(true); + }, []); - const handleCloseTimelineSearch = useCallback(() => { - setOpenTimelineSearch(false); - }, []); + const handleCloseTimelineSearch = useCallback(() => { + setOpenTimelineSearch(false); + }, []); - return isReadOnlyView && myStepData != null ? ( + return isReadOnlyView && myStepData != null ? ( + - ) : ( - <> + + ) : ( + <> +
- {i18n.RESET_DEFAULT_INDEX} - + + {i18n.RESET_DEFAULT_INDEX} + ) : null, }} componentProps={{ @@ -176,9 +190,9 @@ export const StepDefineRule = memo( config={{ ...schema.queryBar, labelAppend: ( - - {i18n.IMPORT_TIMELINE_QUERY} - + + {i18n.IMPORT_TIMELINE_QUERY} + ), }} component={QueryBarDefineRule} @@ -192,7 +206,6 @@ export const StepDefineRule = memo( dataTestSubj: 'detectionEngineStepDefineRuleQueryBar', openTimelineSearch, onCloseTimelineSearch: handleCloseTimelineSearch, - resizeParentContainer, }} /> @@ -212,24 +225,26 @@ export const StepDefineRule = memo( }} - {!isUpdateView && ( - <> - - - - - {RuleI18n.CONTINUE} - - - - - )} - - ); - } -); +
+ {!isUpdateView && ( + <> + + + + + {RuleI18n.CONTINUE} + + + + + )} + + ); +}; + +export const StepDefineRule = memo(StepDefineRuleComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx index 35b8ca6650bf6d..b99201abe87777 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx @@ -6,12 +6,13 @@ import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; import { isEqual, get } from 'lodash/fp'; -import React, { memo, useCallback, useEffect, useState } from 'react'; +import React, { FC, memo, useCallback, useEffect, useState } from 'react'; import { RuleStep, RuleStepProps, ScheduleStepRule } from '../../types'; import { StepRuleDescription } from '../description_step'; import { ScheduleItem } from '../schedule_item_form'; import { Form, UseField, useForm } from '../shared_imports'; +import { StepContentWrapper } from '../step_content_wrapper'; import { schema } from './schema'; import * as I18n from './translations'; @@ -26,67 +27,70 @@ const stepScheduleDefaultValue = { from: '0m', }; -export const StepScheduleRule = memo( - ({ - defaultValues, - descriptionDirection = 'row', - isReadOnlyView, - isLoading, - isUpdateView = false, - setStepData, - setForm, - }) => { - const [myStepData, setMyStepData] = useState(stepScheduleDefaultValue); +const StepScheduleRuleComponent: FC = ({ + addPadding = false, + defaultValues, + descriptionDirection = 'row', + isReadOnlyView, + isLoading, + isUpdateView = false, + setStepData, + setForm, +}) => { + const [myStepData, setMyStepData] = useState(stepScheduleDefaultValue); - const { form } = useForm({ - defaultValue: myStepData, - options: { stripEmptyFields: false }, - schema, - }); + const { form } = useForm({ + defaultValue: myStepData, + options: { stripEmptyFields: false }, + schema, + }); - const onSubmit = useCallback( - async (enabled: boolean) => { - if (setStepData) { - setStepData(RuleStep.scheduleRule, null, false); - const { isValid: newIsValid, data } = await form.submit(); - if (newIsValid) { - setStepData(RuleStep.scheduleRule, { ...data, enabled }, newIsValid); - setMyStepData({ ...data, isNew: false } as ScheduleStepRule); - } - } - }, - [form] - ); - - useEffect(() => { - const { isNew, ...initDefaultValue } = myStepData; - if (defaultValues != null && !isEqual(initDefaultValue, defaultValues)) { - const myDefaultValues = { - ...defaultValues, - isNew: false, - }; - setMyStepData(myDefaultValues); - if (!isReadOnlyView) { - Object.keys(schema).forEach(key => { - const val = get(key, myDefaultValues); - if (val != null) { - form.setFieldValue(key, val); - } - }); + const onSubmit = useCallback( + async (enabled: boolean) => { + if (setStepData) { + setStepData(RuleStep.scheduleRule, null, false); + const { isValid: newIsValid, data } = await form.submit(); + if (newIsValid) { + setStepData(RuleStep.scheduleRule, { ...data, enabled }, newIsValid); + setMyStepData({ ...data, isNew: false } as ScheduleStepRule); } } - }, [defaultValues]); + }, + [form] + ); - useEffect(() => { - if (setForm != null) { - setForm(RuleStep.scheduleRule, form); + useEffect(() => { + const { isNew, ...initDefaultValue } = myStepData; + if (defaultValues != null && !isEqual(initDefaultValue, defaultValues)) { + const myDefaultValues = { + ...defaultValues, + isNew: false, + }; + setMyStepData(myDefaultValues); + if (!isReadOnlyView) { + Object.keys(schema).forEach(key => { + const val = get(key, myDefaultValues); + if (val != null) { + form.setFieldValue(key, val); + } + }); } - }, [form]); + } + }, [defaultValues]); - return isReadOnlyView && myStepData != null ? ( + useEffect(() => { + if (setForm != null) { + setForm(RuleStep.scheduleRule, form); + } + }, [form]); + + return isReadOnlyView && myStepData != null ? ( + - ) : ( - <> + + ) : ( + <> +
( }} /> +
+ + {!isUpdateView && ( + <> + + + + + {I18n.COMPLETE_WITHOUT_ACTIVATING} + + + + + {I18n.COMPLETE_WITH_ACTIVATING} + + + + + )} + + ); +}; - {!isUpdateView && ( - <> - - - - - {I18n.COMPLETE_WITHOUT_ACTIVATING} - - - - - {I18n.COMPLETE_WITH_ACTIVATING} - - - - - )} - - ); - } -); +export const StepScheduleRule = memo(StepScheduleRuleComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx index 31e56265dec424..4da17b88b9ad0f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiText } from '@elastic/eui'; -import React from 'react'; import { i18n } from '@kbn/i18n'; -import * as RuleI18n from '../../translations'; +import { OptionalFieldLabel } from '../optional_field_label'; import { FormSchema } from '../shared_imports'; export const schema: FormSchema = { @@ -33,7 +31,7 @@ export const schema: FormSchema = { defaultMessage: 'Additional look-back', } ), - labelAppend: {RuleI18n.OPTIONAL_FIELD}, + labelAppend: OptionalFieldLabel, helpText: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackHelpText', { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index 9a0f41bbd8c517..e5656f5b081fb0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -27,26 +27,17 @@ import * as i18n from './translations'; const stepsRuleOrder = [RuleStep.defineRule, RuleStep.aboutRule, RuleStep.scheduleRule]; -const ResizeEuiPanel = styled(EuiPanel)<{ - height?: number; +const MyEuiPanel = styled(EuiPanel)<{ + zIndex?: number; }>` + position: relative; + z-index: ${props => props.zIndex}; /* ugly fix to allow searchBar to overflow the EuiPanel */ + .euiAccordion__iconWrapper { display: none; } .euiAccordion__childWrapper { - height: ${props => (props.height !== -1 ? `${props.height}px !important` : 'auto')}; - } - .euiAccordion__button { - cursor: default !important; - &:hover { - text-decoration: none !important; - } - } -`; - -const MyEuiPanel = styled(EuiPanel)` - .euiAccordion__iconWrapper { - display: none; + overflow: visible; } .euiAccordion__button { cursor: default !important; @@ -64,7 +55,6 @@ export const CreateRuleComponent = React.memo(() => { canUserCRUD, hasManageApiKey, } = useUserInfo(); - const [heightAccordion, setHeightAccordion] = useState(-1); const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule); const defineRuleRef = useRef(null); const aboutRuleRef = useRef(null); @@ -239,7 +229,7 @@ export const CreateRuleComponent = React.memo(() => { isLoading={isLoading || loading} title={i18n.PAGE_TITLE} /> - + { ) } > - + setHeightAccordion(height)} + descriptionDirection="row" /> - - - + + + { ) } > - + { /> - - + + { ) } > - + ( {aboutRuleData != null && ( void; isReadOnlyView: boolean;