diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx index 6673262a15906..04bca0cdbd61b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButtonEmpty, EuiButtonIcon, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui'; -import { isEmpty, isEqual } from 'lodash/fp'; +import { isEmpty } from 'lodash/fp'; import React, { ChangeEvent, useCallback, useEffect, useState, useRef } from 'react'; import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; @@ -21,15 +21,22 @@ interface AddItemProps { export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: AddItemProps) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - const [items, setItems] = useState(['']); - const [haveBeenKeyboardDeleted, setHaveBeenKeyboardDeleted] = useState(false); + // const [items, setItems] = useState(['']); + const [haveBeenKeyboardDeleted, setHaveBeenKeyboardDeleted] = useState(-1); - const lastInputRef = useRef(null); + const inputsRef = useRef([]); const removeItem = useCallback( (index: number) => { const values = field.value as string[]; field.setValue([...values.slice(0, index), ...values.slice(index + 1)]); + inputsRef.current = [ + ...inputsRef.current.slice(0, index), + ...inputsRef.current.slice(index + 1), + ]; + if (inputsRef.current[index] != null) { + inputsRef.current[index].value = 're-render'; + } }, [field] ); @@ -38,16 +45,26 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad const values = field.value as string[]; if (!isEmpty(values[values.length - 1])) { field.setValue([...values, '']); + } else { + field.setValue(['']); } }, [field]); const updateItem = useCallback( (event: ChangeEvent, index: number) => { + event.persist(); const values = field.value as string[]; const value = event.target.value; if (isEmpty(value)) { - setHaveBeenKeyboardDeleted(true); field.setValue([...values.slice(0, index), ...values.slice(index + 1)]); + inputsRef.current = [ + ...inputsRef.current.slice(0, index), + ...inputsRef.current.slice(index + 1), + ]; + setHaveBeenKeyboardDeleted(inputsRef.current.length - 1); + if (inputsRef.current[index] != null) { + inputsRef.current[index].value = 're-render'; + } } else { field.setValue([...values.slice(0, index), value, ...values.slice(index + 1)]); } @@ -56,31 +73,30 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad ); const handleLastInputRef = useCallback( - (element: HTMLInputElement | null) => { - lastInputRef.current = element; + (index: number, element: HTMLInputElement | null) => { + if (element != null) { + inputsRef.current = [ + ...inputsRef.current.slice(0, index), + element, + ...inputsRef.current.slice(index + 1), + ]; + } }, - [lastInputRef] + [inputsRef] ); useEffect(() => { - if (!isEqual(field.value, items)) { - setItems( - isEmpty(field.value) - ? [''] - : haveBeenKeyboardDeleted - ? [...(field.value as string[]), ''] - : (field.value as string[]) - ); - setHaveBeenKeyboardDeleted(false); - } - }, [field.value]); - - useEffect(() => { - if (!haveBeenKeyboardDeleted && lastInputRef != null && lastInputRef.current != null) { - lastInputRef.current.focus(); + if ( + haveBeenKeyboardDeleted !== -1 && + !isEmpty(inputsRef.current) && + inputsRef.current[haveBeenKeyboardDeleted] != null + ) { + inputsRef.current[haveBeenKeyboardDeleted].focus(); + setHaveBeenKeyboardDeleted(-1); } - }, [haveBeenKeyboardDeleted, lastInputRef]); + }, [haveBeenKeyboardDeleted, inputsRef.current]); + const values = field.value as string[]; return ( <> - {items.map((item, index) => { + {values.map((item, index) => { const euiFieldProps = { disabled: isDisabled, - ...(index === items.length - 1 ? { inputRef: handleLastInputRef } : {}), + ...(index === values.length - 1 + ? { inputRef: handleLastInputRef.bind(null, index) } + : {}), + ...(inputsRef.current[index] != null && inputsRef.current[index].value !== item + ? { value: item } + : {}), }; return (
@@ -109,13 +130,12 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad aria-label={I18n.DELETE} /> } - value={item} onChange={e => updateItem(e, index)} compressed fullWidth {...euiFieldProps} /> - {items.length - 1 !== index && } + {values.length - 1 !== index && }
); })} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_label.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_label.tsx new file mode 100644 index 0000000000000..15844f5012291 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_label.tsx @@ -0,0 +1,93 @@ +/* + * 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, { memo } from 'react'; +import { EuiTextColor } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { esFilters } from '../../../../../../../../../../src/plugins/data/public'; +import { existsOperator, isOneOfOperator } from './filter_operator'; + +interface Props { + filter: esFilters.Filter; + valueLabel?: string; +} + +export const FilterLabel = memo(({ filter, valueLabel }) => { + const prefixText = filter.meta.negate + ? ` ${i18n.translate('xpack.siem.detectionEngine.createRule.filterLabel.negatedFilterPrefix', { + defaultMessage: 'NOT ', + })}` + : ''; + const prefix = + filter.meta.negate && !filter.meta.disabled ? ( + {prefixText} + ) : ( + prefixText + ); + + if (filter.meta.alias !== null) { + return ( + <> + {prefix} + {filter.meta.alias} + + ); + } + + switch (filter.meta.type) { + case esFilters.FILTERS.EXISTS: + return ( + <> + {prefix} + {`${filter.meta.key}: ${existsOperator.message}`} + + ); + case esFilters.FILTERS.GEO_BOUNDING_BOX: + return ( + <> + {prefix} + {`${filter.meta.key}: ${valueLabel}`} + + ); + case esFilters.FILTERS.GEO_POLYGON: + return ( + <> + {prefix} + {`${filter.meta.key}: ${valueLabel}`} + + ); + case esFilters.FILTERS.PHRASES: + return ( + <> + {prefix} + {filter.meta.key} {isOneOfOperator.message} {valueLabel} + + ); + case esFilters.FILTERS.QUERY_STRING: + return ( + <> + {prefix} + {valueLabel} + + ); + case esFilters.FILTERS.PHRASE: + case esFilters.FILTERS.RANGE: + return ( + <> + {prefix} + {`${filter.meta.key}: ${valueLabel}`} + + ); + default: + return ( + <> + {prefix} + {JSON.stringify(filter.query)} + + ); + } +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_operator.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_operator.tsx new file mode 100644 index 0000000000000..7aa5b0beed2d6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_operator.tsx @@ -0,0 +1,119 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { esFilters } from '../../../../../../../../../../src/plugins/data/public'; + +export interface Operator { + message: string; + type: esFilters.FILTERS; + negate: boolean; + fieldTypes?: string[]; +} + +export const isOperator = { + message: i18n.translate( + 'xpack.siem.detectionEngine.createRule.filterLabel.isOperatorOptionLabel', + { + defaultMessage: 'is', + } + ), + type: esFilters.FILTERS.PHRASE, + negate: false, +}; + +export const isNotOperator = { + message: i18n.translate( + 'xpack.siem.detectionEngine.createRule.filterLabel.isNotOperatorOptionLabel', + { + defaultMessage: 'is not', + } + ), + type: esFilters.FILTERS.PHRASE, + negate: true, +}; + +export const isOneOfOperator = { + message: i18n.translate( + 'xpack.siem.detectionEngine.createRule.filterLabel.isOneOfOperatorOptionLabel', + { + defaultMessage: 'is one of', + } + ), + type: esFilters.FILTERS.PHRASES, + negate: false, + fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'], +}; + +export const isNotOneOfOperator = { + message: i18n.translate( + 'xpack.siem.detectionEngine.createRule.filterLabel.isNotOneOfOperatorOptionLabel', + { + defaultMessage: 'is not one of', + } + ), + type: esFilters.FILTERS.PHRASES, + negate: true, + fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'], +}; + +export const isBetweenOperator = { + message: i18n.translate( + 'xpack.siem.detectionEngine.createRule.filterLabel.isBetweenOperatorOptionLabel', + { + defaultMessage: 'is between', + } + ), + type: esFilters.FILTERS.RANGE, + negate: false, + fieldTypes: ['number', 'date', 'ip'], +}; + +export const isNotBetweenOperator = { + message: i18n.translate( + 'xpack.siem.detectionEngine.createRule.filterLabel.isNotBetweenOperatorOptionLabel', + { + defaultMessage: 'is not between', + } + ), + type: esFilters.FILTERS.RANGE, + negate: true, + fieldTypes: ['number', 'date', 'ip'], +}; + +export const existsOperator = { + message: i18n.translate( + 'xpack.siem.detectionEngine.createRule.filterLabel.existsOperatorOptionLabel', + { + defaultMessage: 'exists', + } + ), + type: esFilters.FILTERS.EXISTS, + negate: false, +}; + +export const doesNotExistOperator = { + message: i18n.translate( + 'xpack.siem.detectionEngine.createRule.filterLabel.doesNotExistOperatorOptionLabel', + { + defaultMessage: 'does not exist', + } + ), + type: esFilters.FILTERS.EXISTS, + negate: true, +}; + +export const FILTER_OPERATORS: Operator[] = [ + isOperator, + isNotOperator, + isOneOfOperator, + isNotOneOfOperator, + isBetweenOperator, + isNotBetweenOperator, + existsOperator, + doesNotExistOperator, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx new file mode 100644 index 0000000000000..3e8147e5ca3c1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx @@ -0,0 +1,155 @@ +/* + * 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 { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiFlexItem, EuiTextArea } from '@elastic/eui'; +import { isEmpty, chunk, get, pick } from 'lodash/fp'; +import React, { memo, ReactNode } from 'react'; +import styled from 'styled-components'; + +import { + IIndexPattern, + esFilters, + Query, + utils, +} from '../../../../../../../../../../src/plugins/data/public'; + +import { FilterLabel } from './filter_label'; +import { FormSchema } from '../shared_imports'; +import * as I18n from './translations'; + +interface StepRuleDescriptionProps { + data: unknown; + indexPatterns?: IIndexPattern; + schema: FormSchema; +} + +const EuiBadgeWrap = styled(EuiBadge)` + .euiBadge__text { + white-space: pre-wrap !important; + } +`; + +const EuiFlexItemWidth = styled(EuiFlexItem)` + width: 50%; +`; + +export const StepRuleDescription = memo( + ({ data, indexPatterns, schema }) => { + const keys = Object.keys(schema); + return ( + + {chunk(keys.includes('queryBar') ? 3 : Math.ceil(keys.length / 2), keys).map(key => ( + + + + ))} + + ); + } +); + +interface ListItems { + title: NonNullable; + description: NonNullable; +} + +const buildListItems = ( + data: unknown, + schema: FormSchema, + indexPatterns?: IIndexPattern +): ListItems[] => + Object.keys(schema).reduce( + (acc, field) => [ + ...acc, + ...getDescriptionItem(field, get([field, 'label'], schema), data, indexPatterns), + ], + [] + ); + +const getDescriptionItem = ( + field: string, + label: string, + value: unknown, + indexPatterns?: IIndexPattern +): ListItems[] => { + if (field === 'queryBar' && indexPatterns != null) { + const filters = get('queryBar.filters', value) as esFilters.Filter[]; + const query = get('queryBar.query', value) as Query; + const savedId = get('queryBar.saved_id', value); + let items: ListItems[] = []; + if (!isEmpty(filters)) { + items = [ + ...items, + { + title: <>{I18n.FILTERS_LABEL}, + description: ( + + {filters.map((filter, index) => ( + + + + + + ))} + + ), + }, + ]; + } + if (!isEmpty(query.query)) { + items = [ + ...items, + { + title: <>{I18n.QUERY_LABEL}, + description: <>{query.query}, + }, + ]; + } + if (!isEmpty(savedId)) { + items = [ + ...items, + { + title: <>{I18n.SAVED_ID_LABEL}, + description: <>{savedId}, + }, + ]; + } + return items; + } else if (field === 'description') { + return [ + { + title: label, + description: , + }, + ]; + } else if (Array.isArray(get(field, value))) { + return [ + { + title: label, + description: ( + + {get(field, value).map((val: string) => ( + + {val} + + ))} + + ), + }, + ]; + } + return [ + { + title: label, + description: get(field, value), + }, + ]; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/translations.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/translations.tsx new file mode 100644 index 0000000000000..0995e0e916652 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/translations.tsx @@ -0,0 +1,19 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const FILTERS_LABEL = i18n.translate('xpack.siem.detectionEngine.createRule.filtersLabel', { + defaultMessage: 'Filters', +}); + +export const QUERY_LABEL = i18n.translate('xpack.siem.detectionEngine.createRule.QueryLabel', { + defaultMessage: 'Query', +}); + +export const SAVED_ID_LABEL = i18n.translate('xpack.siem.detectionEngine.createRule.savedIdLabel', { + defaultMessage: 'Saved query name', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx index 4e7832c890255..8db9d3b44e3f5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx @@ -28,7 +28,7 @@ import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; export interface FieldValueQueryBar { filters: esFilters.Filter[]; query: Query; - saved_id: string; + saved_id: string | null; } interface QueryBarDefineRuleProps { dataTestSubj: string; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts index 6c91c4a02edf9..8eb85c9fe3fae 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts @@ -8,9 +8,14 @@ export { getUseField, getFieldValidityAndErrorMessage, FieldHook, + FIELD_TYPES, Form, FormDataProvider, + FormSchema, UseField, useForm, + ValidationFunc, } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { Field } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; +export { fieldValidators } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; +export { ERROR_CODE } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/status_icon/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/status_icon/index.tsx index ad0011ff8ed18..22b116557ae6e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/status_icon/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/status_icon/index.tsx @@ -9,8 +9,7 @@ import React, { memo } from 'react'; import styled from 'styled-components'; import { useEuiTheme } from '../../../../../lib/theme/use_eui_theme'; - -export type RuleStatusType = 'passive' | 'active' | 'valid'; +import { RuleStatusType } from '../../types'; export interface RuleStatusIconProps { name: string; @@ -32,7 +31,7 @@ export const RuleStatusIcon = memo(({ name, type }) => { return ( - {type === 'valid' ? : null} + {type === 'valid' ? : null} ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts index b94fa8c933937..7c4d78f364479 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts @@ -4,12 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -export const defaultValue = { +import { AboutStepRule } from '../../types'; + +export const defaultValue: AboutStepRule = { name: '', description: '', + isNew: true, severity: 'low', riskScore: 50, - references: [], - falsePositives: [], + references: [''], + falsePositives: [''], tags: [], }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx index 4393f39ad2f85..56830f252748f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx @@ -5,9 +5,9 @@ */ import { EuiButton, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { memo, useCallback } from 'react'; +import React, { memo, useCallback, useState } from 'react'; -import { RuleStepProps, RuleStep } from '../../types'; +import { RuleStepProps, RuleStep, AboutStepRule } from '../../types'; import * as CreateRuleI18n from '../../translations'; import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; import { AddItem } from '../add_item_form'; @@ -15,24 +15,29 @@ import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './da import { defaultValue } from './default_value'; import { schema } from './schema'; import * as I18n from './translations'; +import { StepRuleDescription } from '../description_step'; const CommonUseField = getUseField({ component: Field }); -export const StepAboutRule = memo(({ isLoading, setStepData }) => { +export const StepAboutRule = memo(({ isEditView, isLoading, setStepData }) => { + const [myStepData, setMyStepData] = useState(defaultValue); const { form } = useForm({ - schema, - defaultValue, + defaultValue: myStepData, options: { stripEmptyFields: false }, + schema, }); const onSubmit = useCallback(async () => { - const { isValid: newIsValid, data } = await form.submit(); - if (newIsValid) { - setStepData(RuleStep.aboutRule, data, newIsValid); + const { isValid, data } = await form.submit(); + if (isValid) { + setStepData(RuleStep.aboutRule, data, isValid); + setMyStepData({ ...data, isNew: false } as AboutStepRule); } }, [form]); - return ( + return isEditView && myStepData != null ? ( + + ) : ( <>
(({ isLoading, setStepData }) => - {CreateRuleI18n.CONTINUE} + {myStepData.isNew ? CreateRuleI18n.CONTINUE : CreateRuleI18n.UPDATE} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx index 97ad3d595a938..da908bdf02e43 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx @@ -8,13 +8,8 @@ import { EuiText } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; -import { - FormSchema, - FIELD_TYPES, -} from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -import { fieldValidators } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; - import * as CreateRuleI18n from '../../translations'; +import { FIELD_TYPES, fieldValidators, FormSchema } from '../shared_imports'; const { emptyField } = fieldValidators; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx index b09d0df962793..26306d3573926 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx @@ -8,11 +8,13 @@ import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elasti import { isEqual } from 'lodash/fp'; import React, { memo, useCallback, useEffect, useState } from 'react'; +import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules/fetch_index_patterns'; import { DEFAULT_INDEX_KEY, DEFAULT_SIGNALS_INDEX_KEY } from '../../../../../../common/constants'; import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting'; import * as CreateRuleI18n from '../../translations'; -import { RuleStep, RuleStepProps } from '../../types'; +import { DefineStepRule, RuleStep, RuleStepProps } from '../../types'; +import { StepRuleDescription } from '../description_step'; import { QueryBarDefineRule } from '../query_bar'; import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; import { schema } from './schema'; @@ -20,7 +22,7 @@ import * as I18n from './translations'; const CommonUseField = getUseField({ component: Field }); -export const StepDefineRule = memo(({ isLoading, setStepData }) => { +export const StepDefineRule = memo(({ isEditView, isLoading, setStepData }) => { const [initializeOutputIndex, setInitializeOutputIndex] = useState(true); const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(''); const [ @@ -29,26 +31,28 @@ export const StepDefineRule = memo(({ isLoading, setStepData }) = ] = useFetchIndexPatterns(); const [indicesConfig] = useKibanaUiSetting(DEFAULT_INDEX_KEY); const [signalIndexConfig] = useKibanaUiSetting(DEFAULT_SIGNALS_INDEX_KEY); - + const [myStepData, setMyStepData] = useState({ + index: indicesConfig || [], + isNew: true, + outputIndex: signalIndexConfig, + queryBar: { + query: { query: '', language: 'kuery' }, + filters: [], + saved_id: null, + }, + useIndicesConfig: 'true', + }); const { form } = useForm({ schema, - defaultValue: { - index: indicesConfig || [], - outputIndex: signalIndexConfig, - queryBar: { - query: { query: '', language: 'kuery' }, - filters: [], - saved_id: null, - }, - useIndicesConfig: 'true', - }, + defaultValue: myStepData, options: { stripEmptyFields: false }, }); const onSubmit = useCallback(async () => { - const { isValid: newIsValid, data } = await form.submit(); - if (newIsValid) { - setStepData(RuleStep.defineRule, data, newIsValid); + const { isValid, data } = await form.submit(); + if (isValid) { + setStepData(RuleStep.defineRule, data, isValid); + setMyStepData({ ...data, isNew: false } as DefineStepRule); } }, [form]); @@ -60,7 +64,13 @@ export const StepDefineRule = memo(({ isLoading, setStepData }) = } }, [initializeOutputIndex, signalIndexConfig, form]); - return ( + return isEditView && myStepData != null ? ( + + ) : ( <> (({ isLoading, setStepData }) = } else if ( indexField != null && useIndicesConfig === 'false' && - !isEqual(indexField.value, []) + isEqual(indexField.value, indicesConfig) ) { indexField.setValue([]); setIndices([]); @@ -150,7 +160,7 @@ export const StepDefineRule = memo(({ isLoading, setStepData }) = - {CreateRuleI18n.CONTINUE} + {myStepData.isNew ? CreateRuleI18n.CONTINUE : CreateRuleI18n.UPDATE} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx index 58a9e57b32ce6..9f1644e73bf0b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx @@ -9,18 +9,18 @@ import { EuiText } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React from 'react'; -import { - FormSchema, - FIELD_TYPES, - ValidationFunc, -} from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -import { fieldValidators } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; -import { ERROR_CODE } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; import { esKuery } from '../../../../../../../../../../src/plugins/data/public'; import * as CreateRuleI18n from '../../translations'; import { FieldValueQueryBar } from '../query_bar'; +import { + ERROR_CODE, + FIELD_TYPES, + fieldValidators, + FormSchema, + ValidationFunc, +} from '../shared_imports'; import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY } from './translations'; const { emptyField } = fieldValidators; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx index 10b95ac6c8742..bd4d5aa4f8ca1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx @@ -5,21 +5,25 @@ */ import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; -import React, { memo, useCallback } from 'react'; +import React, { memo, useCallback, useState } from 'react'; -import { RuleStep, RuleStepProps } from '../../types'; +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 { schema } from './schema'; import * as I18n from './translations'; -export const StepScheduleRule = memo(({ isLoading, setStepData }) => { +export const StepScheduleRule = memo(({ isEditView, isLoading, setStepData }) => { + const [myStepData, setMyStepData] = useState({ + enabled: true, + interval: '5m', + isNew: true, + from: '0m', + }); const { form } = useForm({ schema, - defaultValue: { - interval: '5m', - from: '0m', - }, + defaultValue: myStepData, options: { stripEmptyFields: false }, }); @@ -28,12 +32,15 @@ export const StepScheduleRule = memo(({ isLoading, setStepData }) const { isValid: newIsValid, data } = await form.submit(); if (newIsValid) { setStepData(RuleStep.scheduleRule, { ...data, enabled }, newIsValid); + setMyStepData({ ...data, isNew: false } as ScheduleStepRule); } }, [form] ); - return ( + return isEditView && myStepData != null ? ( + + ) : ( <> { [RuleStep.aboutRule]: { isValid: false, data: {} }, [RuleStep.scheduleRule]: { isValid: false, data: {} }, }); + const [isStepRuleInEditView, setIsStepRuleInEditView] = useState>({ + [RuleStep.defineRule]: false, + [RuleStep.aboutRule]: false, + [RuleStep.scheduleRule]: false, + }); const [{ isLoading, isSaved }, setRule] = usePersistRule(); const setStepData = (step: RuleStep, data: unknown, isValid: boolean) => { - stepsData.current[step] = { data, isValid }; + stepsData.current[step] = { ...stepsData.current[step], data, isValid }; if (isValid) { const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item); if ([0, 1].includes(stepRuleIdx)) { - openCloseAccordion(step); - openCloseAccordion(stepsRuleOrder[stepRuleIdx + 1]); - setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); + setIsStepRuleInEditView({ + ...isStepRuleInEditView, + [step]: true, + }); + if (openAccordionId !== stepsRuleOrder[stepRuleIdx + 1]) { + openCloseAccordion(stepsRuleOrder[stepRuleIdx + 1]); + setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); + } } else if ( stepRuleIdx === 2 && stepsData.current[RuleStep.defineRule].isValid && @@ -106,6 +116,7 @@ export const CreateRuleComponent = React.memo(() => { const manageAccordions = useCallback( (id: RuleStep, isOpen: boolean) => { + const activeRuleIdx = stepsRuleOrder.findIndex(step => step === openAccordionId); const stepRuleIdx = stepsRuleOrder.findIndex(step => step === id); const isLatestStepsRuleValid = stepRuleIdx === 0 @@ -114,23 +125,35 @@ export const CreateRuleComponent = React.memo(() => { .filter((stepRule, index) => index < stepRuleIdx) .every(stepRule => stepsData.current[stepRule].isValid); - if ( - openAccordionId != null && - openAccordionId !== id && - !stepsData.current[openAccordionId].isValid && - isOpen - ) { - openCloseAccordion(id); - } else if (!isLatestStepsRuleValid && isOpen) { + if (stepRuleIdx < activeRuleIdx && !isOpen) { openCloseAccordion(id); - } else if (openAccordionId != null && id !== openAccordionId && isOpen) { - openCloseAccordion(openAccordionId); - setOpenAccordionId(id); - } else if (openAccordionId == null && isOpen) { - setOpenAccordionId(id); + } else if (stepRuleIdx >= activeRuleIdx) { + if ( + openAccordionId != null && + openAccordionId !== id && + !stepsData.current[openAccordionId].isValid && + !isStepRuleInEditView[id] && + isOpen + ) { + openCloseAccordion(id); + } else if (!isLatestStepsRuleValid && isOpen) { + openCloseAccordion(id); + } else if (id !== openAccordionId && isOpen) { + setOpenAccordionId(id); + } } }, - [openAccordionId] + [isStepRuleInEditView, openAccordionId] + ); + + const manageIsEditable = useCallback( + (id: RuleStep) => { + setIsStepRuleInEditView({ + ...isStepRuleInEditView, + [id]: false, + }); + }, + [isStepRuleInEditView] ); if (isSaved && stepsData.current[RuleStep.scheduleRule].isValid) { @@ -154,9 +177,24 @@ export const CreateRuleComponent = React.memo(() => { paddingSize="xs" ref={defineRuleRef} onToggle={manageAccordions.bind(null, RuleStep.defineRule)} + extraAction={ + stepsData.current[RuleStep.defineRule].isValid && ( + + {`Edit`} + + ) + } > - + @@ -168,9 +206,24 @@ export const CreateRuleComponent = React.memo(() => { paddingSize="xs" ref={aboutRuleRef} onToggle={manageAccordions.bind(null, RuleStep.aboutRule)} + extraAction={ + stepsData.current[RuleStep.aboutRule].isValid && ( + + {`Edit`} + + ) + } > - + @@ -182,9 +235,24 @@ export const CreateRuleComponent = React.memo(() => { paddingSize="xs" ref={scheduleRuleRef} onToggle={manageAccordions.bind(null, RuleStep.scheduleRule)} + extraAction={ + stepsData.current[RuleStep.scheduleRule].isValid && ( + + {`Edit`} + + ) + } > - + diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts index ca96566305a6b..1ef3a435bbc30 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts @@ -38,3 +38,7 @@ export const CONTINUE = i18n.translate( defaultMessage: 'Continue', } ); + +export const UPDATE = i18n.translate('xpack.siem.detectionEngine.createRule.updateButtonTitle', { + defaultMessage: 'Update', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts index a03f6a0b11bee..8c395c458e59a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts @@ -12,24 +12,46 @@ export enum RuleStep { aboutRule = 'about-rule', scheduleRule = 'schedule-rule', } +export type RuleStatusType = 'passive' | 'active' | 'valid'; export interface RuleStepData { - isValid: boolean; data: unknown; + isValid: boolean; } export interface RuleStepProps { setStepData: (step: RuleStep, data: unknown, isValid: boolean) => void; + isEditView: boolean; isLoading: boolean; } -export interface DefineStepRule { +interface StepRuleData { + isNew: boolean; +} +export interface AboutStepRule extends StepRuleData { + name: string; + description: string; + severity: string; + riskScore: number; + references: string[]; + falsePositives: string[]; + tags: string[]; +} + +export interface DefineStepRule extends StepRuleData { outputIndex: string; useIndicesConfig: string; index: string[]; queryBar: FieldValueQueryBar; } +export interface ScheduleStepRule extends StepRuleData { + enabled: boolean; + interval: string; + from: string; + to?: string; +} + export interface DefineStepRuleJson { output_index: string; index: string[]; @@ -39,16 +61,6 @@ export interface DefineStepRuleJson { language: string; } -export interface AboutStepRule { - name: string; - description: string; - severity: string; - riskScore: number; - references: string[]; - falsePositives: string[]; - tags: string[]; -} - export interface AboutStepRuleJson { name: string; description: string; @@ -59,12 +71,6 @@ export interface AboutStepRuleJson { tags: string[]; } -export interface ScheduleStepRule { - enabled: boolean; - interval: string; - from: string; - to?: string; -} export type ScheduleStepRuleJson = ScheduleStepRule; export type FormatRuleType = 'query' | 'saved_query';