From d13fb76d643a4fe38fff3eea4888b1a174739574 Mon Sep 17 00:00:00 2001 From: Nikita Indik Date: Tue, 26 Nov 2024 08:18:40 +0100 Subject: [PATCH] Add `threshold` --- .../threshold_edit/field_configs.ts | 102 +++++++++++ .../components/threshold_edit/index.tsx | 8 + .../threshold_edit/threshold_edit.tsx | 76 +++++++++ .../components/threshold_edit/translations.ts | 78 +++++++++ .../components/description_step/index.tsx | 3 +- .../components/step_define_rule/index.tsx | 57 +------ .../components/step_define_rule/schema.tsx | 159 +----------------- .../use_persistent_threshold_state.ts | 54 ++++++ .../components/threshold_input/index.tsx | 28 ++- .../components/threshold_input/layout.tsx | 35 ++++ .../pages/rule_creation/index.tsx | 2 +- .../fields/threshold/threshold_adapter.tsx | 38 +++++ .../fields/threshold/threshold_edit_form.tsx | 68 ++++++++ .../final_edit/threshold_rule_field_edit.tsx | 11 +- 14 files changed, 491 insertions(+), 228 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/field_configs.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/index.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/threshold_edit.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/translations.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/use_persistent_threshold_state.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threshold_input/layout.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threshold/threshold_adapter.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threshold/threshold_edit_form.tsx diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/field_configs.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/field_configs.ts new file mode 100644 index 0000000000000..163cf2b3ab7be --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/field_configs.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ValidationFunc } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { FIELD_TYPES } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import type { ERROR_CODE } from '@kbn/es-ui-shared-plugin/static/forms/helpers/field_validators/types'; +import { isEmpty } from 'lodash'; +import { fieldValidators } from '../../../../shared_imports'; +import * as i18n from './translations'; + +export const THRESHOLD_FIELD_CONFIG = { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.THRESHOLD_FIELD_LABEL, + helpText: i18n.THRESHOLD_FIELD_HELP_TEXT, + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + return fieldValidators.maxLengthField({ + length: 3, + message: i18n.THRESHOLD_FIELD_COUNT_VALIDATION_ERROR, + })(...args); + }, + }, + ], +}; + +export const THRESHOLD_VALUE_CONFIG = { + type: FIELD_TYPES.NUMBER, + label: i18n.THRESHOLD_VALUE_LABEL, + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + return fieldValidators.numberGreaterThanField({ + than: 1, + message: i18n.THRESHOLD_VALUE_VALIDATION_ERROR, + allowEquality: true, + })(...args); + }, + }, + ], +}; + +export function getCardinalityFieldConfig(path: string) { + return { + defaultValue: [] as unknown, + fieldsToValidateOnChange: [`${path}.cardinality.field`, `${path}.cardinality.value`], + type: FIELD_TYPES.COMBO_BOX, + label: i18n.CARDINALITY_FIELD_LABEL, + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ formData }] = args; + + if ( + isEmpty(formData[`${path}.cardinality.field`]) && + !isEmpty(formData[`${path}.cardinality.value`]) + ) { + return fieldValidators.emptyField(i18n.CARDINALITY_FIELD_MISSING_VALIDATION_ERROR)( + ...args + ); + } + }, + }, + ], + helpText: i18n.CARDINALITY_FIELD_HELP_TEXT, + }; +} + +export function getCardinalityValueConfig(path: string) { + return { + fieldsToValidateOnChange: [`${path}.cardinality.field`, `${path}.cardinality.value`], + type: FIELD_TYPES.NUMBER, + label: i18n.CARDINALITY_VALUE_LABEL, + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ formData }] = args; + + if (!isEmpty(formData[`${path}.cardinality.field`])) { + return fieldValidators.numberGreaterThanField({ + than: 1, + message: i18n.CARDINALITY_VALUE_VALIDATION_ERROR, + allowEquality: true, + })(...args); + } + }, + }, + ], + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/index.tsx new file mode 100644 index 0000000000000..aad44c4a8b842 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ThresholdEdit } from './threshold_edit'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/threshold_edit.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/threshold_edit.tsx new file mode 100644 index 0000000000000..d69e111d9a4c3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/threshold_edit.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; +import { type FieldSpec } from '@kbn/data-views-plugin/common'; +import { type FieldHook, UseMultiFields } from '../../../../shared_imports'; +import { ThresholdInput } from '../../../rule_creation_ui/components/threshold_input'; +import { + THRESHOLD_FIELD_CONFIG, + THRESHOLD_VALUE_CONFIG, + getCardinalityFieldConfig, + getCardinalityValueConfig, +} from './field_configs'; + +interface ThresholdEditProps { + path: string; + esFields: FieldSpec[]; + direction?: 'row' | 'column'; +} + +export function ThresholdEdit({ + path, + esFields, + direction = 'row', +}: ThresholdEditProps): JSX.Element { + const ThresholdInputChildren = useCallback( + ({ + thresholdField, + thresholdValue, + thresholdCardinalityField, + thresholdCardinalityValue, + }: Record) => ( + + ), + [esFields, direction] + ); + + const cardinalityFieldConfig = useMemo(() => getCardinalityFieldConfig(path), [path]); + const cardinalityValueConfig = useMemo(() => getCardinalityValueConfig(path), [path]); + + return ( + + {ThresholdInputChildren} + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/translations.ts new file mode 100644 index 0000000000000..26981afd15e3a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/translations.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const THRESHOLD_FIELD_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldLabel', + { + defaultMessage: 'Group by', + } +); + +export const THRESHOLD_FIELD_HELP_TEXT = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldHelpText', + { + defaultMessage: "Select fields to group by. Fields are joined together with 'AND'", + } +); + +export const THRESHOLD_FIELD_COUNT_VALIDATION_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.validations.thresholdFieldFieldData.arrayLengthGreaterThanMaxErrorMessage', + { + defaultMessage: 'Number of fields must be 3 or less.', + } +); + +export const THRESHOLD_VALUE_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdValueLabel', + { + defaultMessage: 'Threshold', + } +); + +export const THRESHOLD_VALUE_VALIDATION_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.validations.thresholdValueFieldData.numberGreaterThanOrEqualOneErrorMessage', + { + defaultMessage: 'Value must be greater than or equal to one.', + } +); + +export const CARDINALITY_FIELD_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityFieldLabel', + { + defaultMessage: 'Count', + } +); + +export const CARDINALITY_FIELD_MISSING_VALIDATION_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.validations.thresholdCardinalityFieldFieldData.thresholdCardinalityFieldNotSuppliedMessage', + { + defaultMessage: 'A Cardinality Field is required.', + } +); + +export const CARDINALITY_FIELD_HELP_TEXT = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText', + { + defaultMessage: 'Select a field to check cardinality', + } +); + +export const CARDINALITY_VALUE_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityValueFieldLabel', + { + defaultMessage: 'Unique values', + } +); + +export const CARDINALITY_VALUE_VALIDATION_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.validations.thresholdCardinalityValueFieldData.numberGreaterThanOrEqualOneErrorMessage', + { + defaultMessage: 'Value must be greater than or equal to one.', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx index 24ad5f4135a14..88cb9698b0fe7 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx @@ -72,6 +72,7 @@ import { ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, } from '../../../rule_creation/components/alert_suppression_edit'; import { THRESHOLD_ALERT_SUPPRESSION_ENABLED } from '../../../rule_creation/components/threshold_alert_suppression_edit'; +import { THRESHOLD_VALUE_LABEL } from '../../../rule_creation/components/threshold_edit/translations'; const DescriptionListContainer = styled(EuiDescriptionList)` max-width: 600px; @@ -282,7 +283,7 @@ export const getDescriptionItem = ( return buildThreatDescription({ label, threat: filterEmptyThreats(threats) }); } else if (field === 'threshold') { const threshold = get(field, data); - return buildThresholdDescription(label, threshold); + return buildThresholdDescription(THRESHOLD_VALUE_LABEL, threshold); } else if (field === 'references') { const urls: string[] = get(field, data); return buildUrlsDescription(label, urls); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx index ec598ce74fe59..0f4f8a6276e2c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx @@ -45,7 +45,6 @@ import { QueryBarDefineRule } from '../query_bar'; import { SelectRuleType } from '../select_rule_type'; import { PickTimeline } from '../../../rule_creation/components/pick_timeline'; import { StepContentWrapper } from '../../../rule_creation/components/step_content_wrapper'; -import { ThresholdInput } from '../threshold_input'; import { EsqlInfoIcon } from '../../../rule_creation/components/esql_info_icon'; import { Field, @@ -96,6 +95,8 @@ import { ThresholdAlertSuppressionEdit } from '../../../rule_creation/components import { usePersistentAlertSuppressionState } from './use_persistent_alert_suppression_state'; import { MachineLearningJobIdEdit } from '../../../rule_creation/components/machine_learning_job_id_edit'; import { AnomalyThresholdEdit } from '../../../rule_creation/components/anomaly_threshold'; +import { ThresholdEdit } from '../../../rule_creation/components/threshold_edit'; +import { usePersistentThresholdState } from './use_persistent_threshold_state'; const CommonUseField = getUseField({ component: Field }); @@ -247,10 +248,6 @@ const StepDefineRuleComponent: FC = ({ [form] ); - const aggFields = useMemo( - () => (indexPattern.fields as FieldSpec[]).filter((field) => field.aggregatable === true), - [indexPattern.fields] - ); const termsAggregationFields = useMemo( /** * Typecasting to FieldSpec because fields is @@ -370,6 +367,7 @@ const StepDefineRuleComponent: FC = ({ }, [ruleType, previousRuleType, getFields]); usePersistentAlertSuppressionState({ form }); + usePersistentThresholdState({ form, ruleTypePath: 'ruleType', thresholdPath: 'threshold' }); // if saved query failed to load: // - reset shouldLoadFormDynamically to false, as non existent query cannot be used for loading and execution @@ -397,24 +395,6 @@ const StepDefineRuleComponent: FC = ({ setOpenTimelineSearch(false); }, []); - const ThresholdInputChildren = useCallback( - ({ - thresholdField, - thresholdValue, - thresholdCardinalityField, - thresholdCardinalityValue, - }: Record) => ( - - ), - [aggFields] - ); - const ThreatMatchInputChildren = useCallback( ({ threatMapping }: Record) => ( = ({ - - <> - - {ThresholdInputChildren} - - - + {isThresholdRule && ( + + + + )} = { } ), }, - threshold: { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdLabel', - { - defaultMessage: 'Threshold', - } - ), - field: { - type: FIELD_TYPES.COMBO_BOX, - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldLabel', - { - defaultMessage: 'Group by', - } - ), - helpText: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldHelpText', - { - defaultMessage: "Select fields to group by. Fields are joined together with 'AND'", - } - ), - validations: [ - { - validator: ( - ...args: Parameters - ): ReturnType> | undefined => { - const [{ formData }] = args; - const needsValidation = isThresholdRule(formData.ruleType); - if (!needsValidation) { - return; - } - return fieldValidators.maxLengthField({ - length: 3, - message: i18n.translate( - 'xpack.securitySolution.detectionEngine.validations.thresholdFieldFieldData.arrayLengthGreaterThanMaxErrorMessage', - { - defaultMessage: 'Number of fields must be 3 or less.', - } - ), - })(...args); - }, - }, - ], - }, - value: { - type: FIELD_TYPES.NUMBER, - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdValueLabel', - { - defaultMessage: 'Threshold', - } - ), - validations: [ - { - validator: ( - ...args: Parameters - ): ReturnType> | undefined => { - const [{ formData }] = args; - const needsValidation = isThresholdRule(formData.ruleType); - if (!needsValidation) { - return; - } - return fieldValidators.numberGreaterThanField({ - than: 1, - message: i18n.translate( - 'xpack.securitySolution.detectionEngine.validations.thresholdValueFieldData.numberGreaterThanOrEqualOneErrorMessage', - { - defaultMessage: 'Value must be greater than or equal to one.', - } - ), - allowEquality: true, - })(...args); - }, - }, - ], - }, - cardinality: { - field: { - defaultValue: [], - fieldsToValidateOnChange: ['threshold.cardinality.field', 'threshold.cardinality.value'], - type: FIELD_TYPES.COMBO_BOX, - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityFieldLabel', - { - defaultMessage: 'Count', - } - ), - validations: [ - { - validator: ( - ...args: Parameters - ): ReturnType> | undefined => { - const [{ formData }] = args; - const needsValidation = isThresholdRule(formData.ruleType); - if (!needsValidation) { - return; - } - if ( - isEmpty(formData['threshold.cardinality.field']) && - !isEmpty(formData['threshold.cardinality.value']) - ) { - return fieldValidators.emptyField( - i18n.translate( - 'xpack.securitySolution.detectionEngine.validations.thresholdCardinalityFieldFieldData.thresholdCardinalityFieldNotSuppliedMessage', - { - defaultMessage: 'A Cardinality Field is required.', - } - ) - )(...args); - } - }, - }, - ], - helpText: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText', - { - defaultMessage: 'Select a field to check cardinality', - } - ), - }, - value: { - fieldsToValidateOnChange: ['threshold.cardinality.field', 'threshold.cardinality.value'], - type: FIELD_TYPES.NUMBER, - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityValueFieldLabel', - { - defaultMessage: 'Unique values', - } - ), - validations: [ - { - validator: ( - ...args: Parameters - ): ReturnType> | undefined => { - const [{ formData }] = args; - const needsValidation = isThresholdRule(formData.ruleType); - if (!needsValidation) { - return; - } - if (!isEmpty(formData['threshold.cardinality.field'])) { - return fieldValidators.numberGreaterThanField({ - than: 1, - message: i18n.translate( - 'xpack.securitySolution.detectionEngine.validations.thresholdCardinalityValueFieldData.numberGreaterThanOrEqualOneErrorMessage', - { - defaultMessage: 'Value must be greater than or equal to one.', - } - ), - allowEquality: true, - })(...args); - } - }, - }, - ], - }, - }, - }, + threshold: {}, threatIndex: { type: FIELD_TYPES.COMBO_BOX, label: i18n.translate( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/use_persistent_threshold_state.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/use_persistent_threshold_state.ts new file mode 100644 index 0000000000000..0895ef466e72b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/use_persistent_threshold_state.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useRef } from 'react'; +import usePrevious from 'react-use/lib/usePrevious'; +import { isThresholdRule } from '../../../../../common/detection_engine/utils'; +import type { FormHook } from '../../../../shared_imports'; +import { useFormData } from '../../../../shared_imports'; +import { type DefineStepRule } from '../../../../detections/pages/detection_engine/rules/types'; +import type { FieldValueThreshold } from '../threshold_input'; + +interface LastThresholdState { + threshold: FieldValueThreshold; +} + +interface UsePersistentThresholdStateParams { + form: FormHook; + ruleTypePath: string; + thresholdPath: string; +} + +export function usePersistentThresholdState({ + form, + ruleTypePath, + thresholdPath, +}: UsePersistentThresholdStateParams): void { + const lastThresholdState = useRef(); + const [formData] = useFormData({ form }); + + const { [ruleTypePath]: ruleType, [thresholdPath]: threshold } = formData; + const previousRuleType = usePrevious(ruleType); + + useEffect(() => { + if ( + isThresholdRule(ruleType) && + !isThresholdRule(previousRuleType) && + lastThresholdState.current + ) { + form.updateFieldValues({ + [thresholdPath]: lastThresholdState.current.threshold, + }); + + return; + } + + if (isThresholdRule(ruleType)) { + lastThresholdState.current = { threshold }; + } + }, [form, ruleType, previousRuleType, threshold, thresholdPath]); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threshold_input/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threshold_input/index.tsx index 3875aa853256c..640fdea4ee560 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threshold_input/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threshold_input/index.tsx @@ -7,11 +7,11 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import styled from 'styled-components'; import type { DataViewFieldBase } from '@kbn/es-query'; import type { FieldHook } from '../../../../shared_imports'; import { Field } from '../../../../shared_imports'; +import { ResponsiveGroup, Operator } from './layout'; import { THRESHOLD_FIELD_PLACEHOLDER } from './translations'; const FIELD_COMBO_BOX_WIDTH = 410; @@ -31,12 +31,9 @@ interface ThresholdInputProps { thresholdCardinalityField: FieldHook; thresholdCardinalityValue: FieldHook; browserFields: DataViewFieldBase[]; + direction?: 'row' | 'column'; } -const OperatorWrapper = styled(EuiFlexItem)` - align-self: center; -`; - const fieldDescribedByIds = ['detectionEngineStepDefineRuleThresholdField']; const valueDescribedByIds = ['detectionEngineStepDefineRuleThresholdValue']; const cardinalityFieldDescribedByIds = ['detectionEngineStepDefineRuleThresholdCardinalityField']; @@ -48,6 +45,7 @@ const ThresholdInputComponent: React.FC = ({ browserFields, thresholdCardinalityField, thresholdCardinalityValue, + direction = 'row', }: ThresholdInputProps) => { const fieldEuiFieldProps = useMemo( () => ({ @@ -56,9 +54,9 @@ const ThresholdInputComponent: React.FC = ({ options: browserFields.map((field) => ({ label: field.name })), placeholder: THRESHOLD_FIELD_PLACEHOLDER, onCreateOption: undefined, - style: { width: `${FIELD_COMBO_BOX_WIDTH}px` }, + style: direction === 'row' ? { width: `${FIELD_COMBO_BOX_WIDTH}px` } : {}, }), - [browserFields] + [browserFields, direction] ); const cardinalityFieldEuiProps = useMemo( () => ({ @@ -67,15 +65,15 @@ const ThresholdInputComponent: React.FC = ({ options: browserFields.map((field) => ({ label: field.name })), placeholder: THRESHOLD_FIELD_PLACEHOLDER, onCreateOption: undefined, - style: { width: `${FIELD_COMBO_BOX_WIDTH}px` }, + style: direction === 'row' ? { width: `${FIELD_COMBO_BOX_WIDTH}px` } : {}, singleSelection: { asPlainText: true }, }), - [browserFields] + [browserFields, direction] ); return ( - + = ({ euiFieldProps={fieldEuiFieldProps} /> - {'>='} + = ({ type={thresholdValue.type} /> - - + + = ({ euiFieldProps={cardinalityFieldEuiProps} /> - {'>='} + = ({ type={thresholdCardinalityValue.type} /> - + ); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threshold_input/layout.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threshold_input/layout.tsx new file mode 100644 index 0000000000000..ef50e6ee9a546 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threshold_input/layout.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { css } from '@emotion/css'; + +const operatorStyle = css` + align-self: center; +`; + +export function Operator(): JSX.Element { + return ( + + {'>='} + + ); +} + +interface ResponsiveGroupProps { + direction: 'row' | 'column'; + children: React.ReactNode; +} + +export function ResponsiveGroup({ direction, children }: ResponsiveGroupProps): JSX.Element { + return ( + + {children} + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx index 01435a2f7c654..0761837aa05cf 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx @@ -565,7 +565,7 @@ const CreateRulePageComponent: React.FC = () => { shouldLoadQueryDynamically={defineStepData.shouldLoadQueryDynamically} queryBarTitle={defineStepData.queryBar.title} queryBarSavedId={defineStepData.queryBar.saved_id} - thresholdFields={defineStepData.threshold.field} + thresholdFields={defineStepData?.threshold?.field || []} /> + {(size) => ( +
+ +
+ )} + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threshold/threshold_edit_form.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threshold/threshold_edit_form.tsx new file mode 100644 index 0000000000000..393abf5baa2b0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threshold/threshold_edit_form.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { FormData } from '../../../../../../../../shared_imports'; +import { RuleFieldEditFormWrapper } from '../rule_field_edit_form_wrapper'; +import { ThresholdAdapter } from './threshold_adapter'; +import type { FieldValueThreshold } from '../../../../../../../rule_creation_ui/components/threshold_input'; +import type { Threshold } from '../../../../../../../../../common/api/detection_engine'; +import { normalizeThresholdField } from '../../../../../../../../../common/detection_engine/utils'; + +export function ThresholdEditForm(): JSX.Element { + return ( + + ); +} + +interface ThresholdFormData { + threshold: FieldValueThreshold; +} + +function deserializer(defaultValue: FormData): ThresholdFormData { + const threshold = defaultValue.threshold as Threshold; + + return { + threshold: { + field: normalizeThresholdField(threshold.field), + value: `${threshold?.value || 100}`, + ...(threshold.cardinality?.length + ? { + cardinality: { + field: [`${threshold.cardinality[0].field}`], + value: `${threshold.cardinality[0].value}`, + }, + } + : {}), + }, + }; +} + +function serializer(formData: FormData): { threshold: Threshold } { + const threshold: Threshold = { + field: formData.threshold.field, + value: Number.parseInt(formData.threshold.value, 10), + }; + + if (formData.threshold.cardinality && formData.threshold.cardinality.field.length > 0) { + threshold.cardinality = [ + { + field: formData.threshold.cardinality.field[0], + value: parseInt(formData.threshold.cardinality.value, 10), + }, + ]; + } + + return { threshold }; +} + +const schema = {}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/threshold_rule_field_edit.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/threshold_rule_field_edit.tsx index ad9efe076b5bc..a0efcdedc286e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/threshold_rule_field_edit.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/threshold_rule_field_edit.tsx @@ -10,6 +10,7 @@ import type { UpgradeableThresholdFields } from '../../../../model/prebuilt_rule import { KqlQueryEditForm } from './fields/kql_query'; import { DataSourceEditForm } from './fields/data_source'; import { ThresholdAlertSuppressionEditForm } from './fields/threshold_alert_suppression'; +import { ThresholdEditForm } from './fields/threshold/threshold_edit_form'; interface ThresholdRuleFieldEditProps { fieldName: UpgradeableThresholdFields; @@ -17,12 +18,14 @@ interface ThresholdRuleFieldEditProps { export function ThresholdRuleFieldEdit({ fieldName }: ThresholdRuleFieldEditProps) { switch (fieldName) { - case 'kql_query': - return ; - case 'data_source': - return ; case 'alert_suppression': return ; + case 'data_source': + return ; + case 'kql_query': + return ; + case 'threshold': + return ; default: return null; // Will be replaced with `assertUnreachable(fieldName)` once all fields are implemented }