Skip to content

Commit

Permalink
Add threshold
Browse files Browse the repository at this point in the history
  • Loading branch information
nikitaindik committed Nov 26, 2024
1 parent 8013dcd commit d13fb76
Show file tree
Hide file tree
Showing 14 changed files with 491 additions and 228 deletions.
Original file line number Diff line number Diff line change
@@ -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<ValidationFunc>
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | 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<ValidationFunc>
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | 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<ValidationFunc>
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | 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<ValidationFunc>
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => {
const [{ formData }] = args;

if (!isEmpty(formData[`${path}.cardinality.field`])) {
return fieldValidators.numberGreaterThanField({
than: 1,
message: i18n.CARDINALITY_VALUE_VALIDATION_ERROR,
allowEquality: true,
})(...args);
}
},
},
],
};
}
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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<string, FieldHook>) => (
<ThresholdInput
browserFields={esFields}
thresholdField={thresholdField}
thresholdValue={thresholdValue}
thresholdCardinalityField={thresholdCardinalityField}
thresholdCardinalityValue={thresholdCardinalityValue}
direction={direction}
/>
),
[esFields, direction]
);

const cardinalityFieldConfig = useMemo(() => getCardinalityFieldConfig(path), [path]);
const cardinalityValueConfig = useMemo(() => getCardinalityValueConfig(path), [path]);

return (
<UseMultiFields
fields={{
thresholdField: {
path: `${path}.field`,
config: THRESHOLD_FIELD_CONFIG,
},
thresholdValue: {
path: `${path}.value`,
config: THRESHOLD_VALUE_CONFIG,
},
thresholdCardinalityField: {
path: `${path}.cardinality.field`,
config: cardinalityFieldConfig,
},
thresholdCardinalityValue: {
path: `${path}.cardinality.value`,
config: cardinalityValueConfig,
},
}}
>
{ThresholdInputChildren}
</UseMultiFields>
);
}
Original file line number Diff line number Diff line change
@@ -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.',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 });

Expand Down Expand Up @@ -247,10 +248,6 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
[form]
);

const aggFields = useMemo(
() => (indexPattern.fields as FieldSpec[]).filter((field) => field.aggregatable === true),
[indexPattern.fields]
);
const termsAggregationFields = useMemo(
/**
* Typecasting to FieldSpec because fields is
Expand Down Expand Up @@ -370,6 +367,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
}, [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
Expand Down Expand Up @@ -397,24 +395,6 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
setOpenTimelineSearch(false);
}, []);

const ThresholdInputChildren = useCallback(
({
thresholdField,
thresholdValue,
thresholdCardinalityField,
thresholdCardinalityValue,
}: Record<string, FieldHook>) => (
<ThresholdInput
browserFields={aggFields}
thresholdField={thresholdField}
thresholdValue={thresholdValue}
thresholdCardinalityField={thresholdCardinalityField}
thresholdCardinalityValue={thresholdCardinalityValue}
/>
),
[aggFields]
);

const ThreatMatchInputChildren = useCallback(
({ threatMapping }: Record<string, FieldHook>) => (
<ThreatMatchInput
Expand Down Expand Up @@ -809,32 +789,11 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
<AnomalyThresholdEdit path="anomalyThreshold" />
</>
</RuleTypeEuiFormRow>
<RuleTypeEuiFormRow
$isVisible={isThresholdRule}
data-test-subj="thresholdInput"
fullWidth
>
<>
<UseMultiFields
fields={{
thresholdField: {
path: 'threshold.field',
},
thresholdValue: {
path: 'threshold.value',
},
thresholdCardinalityField: {
path: 'threshold.cardinality.field',
},
thresholdCardinalityValue: {
path: 'threshold.cardinality.value',
},
}}
>
{ThresholdInputChildren}
</UseMultiFields>
</>
</RuleTypeEuiFormRow>
{isThresholdRule && (
<EuiFormRow data-test-subj="thresholdInput" fullWidth>
<ThresholdEdit esFields={indexPattern.fields as FieldSpec[]} path="threshold" />
</EuiFormRow>
)}
<RuleTypeEuiFormRow
$isVisible={isThreatMatchRule(ruleType)}
data-test-subj="threatMatchInput"
Expand Down
Loading

0 comments on commit d13fb76

Please sign in to comment.