+ The aggregation method determines what constitutes an anomaly.
+
+
spec Empty results renders component with aggre
class="euiFormHelpText euiFormRow__text"
id="random_html_id-help-0"
>
- The aggregation method determines what constitutes an anomaly. For example, if you choose min(), the detector focuses on finding anomalies based on the minimum values of your feature.
+ E.g, if you choose min(), the detector focuses on finding anomalies based on the minimum values of your feature.
+
+
+
+
+ {({ field }: FieldProps) => (
+
+
+
+ )}
+
+
+
+ {
+ if (
+ form.values.suppressionRules[
+ props.featureIndex
+ ].length === 1
+ ) {
+ arrayHelpers.remove(index);
+ const cleanedSuppressionRules =
+ form.values.suppressionRules.filter(
+ (_, i) => i === props.featureIndex
+ );
+ form.setFieldValue(
+ `suppressionRules.`,
+ cleanedSuppressionRules
+ );
+ } else {
+ arrayHelpers.remove(index);
+ }
+ }}
+ />
+
+
+ );
+ })}
+ >
+
+
+
+ >
+ );
+ }}
+
+
+ {
+ arrayHelpers.push({
+ featureName: props.feature.featureName,
+ absoluteThreshold: null, // Set to null to allow empty inputs
+ relativeThreshold: null, // Set to null to allow empty inputs
+ aboveBelow: 'above',
+ directionRule: false,
+ });
+ }}
+ aria-label="Add rule"
+ style={{ marginTop: '5px' }}
+ >
+ Add suppression rule
+
+ >
+ )}
+
+ );
+}
diff --git a/public/pages/ConfigureModel/components/SuppressionRules/__tests__/SuppressionRules.test.tsx b/public/pages/ConfigureModel/components/SuppressionRules/__tests__/SuppressionRules.test.tsx
new file mode 100644
index 00000000..fbc61aec
--- /dev/null
+++ b/public/pages/ConfigureModel/components/SuppressionRules/__tests__/SuppressionRules.test.tsx
@@ -0,0 +1,113 @@
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { Formik } from 'formik';
+import { SuppressionRules } from '../SuppressionRules';
+
+export const testFeature = {
+ featureId: '42a50483-59cd-4470-a65e-e85547e1a173',
+ featureName: 'f1',
+ featureType: 'simple_aggs',
+ featureEnabled: true,
+ importance: 1,
+ aggregationBy: 'sum',
+ newFeature: true,
+ aggregationOf: [
+ {
+ label: 'items_purchased_failure',
+ type: 'number',
+ },
+ ],
+};
+
+describe('SuppressionRules Component', () => {
+ test('displays error when -1 is entered in suppression rules absolute threshold', async () => {
+ render(
+
+ {() => }
+
+ );
+
+ screen.logTestingPlaygroundURL();
+
+ // Click to add a new suppression rule
+ const addButton = screen.getByRole('button', { name: /add rule/i });
+ fireEvent.click(addButton);
+
+ // Find the threshold input and type -1
+ const thresholdInput = screen.getAllByPlaceholderText('Threshold')[0];
+ userEvent.type(thresholdInput, '-1');
+
+ // Find the dropdown using the data-test-subj attribute
+ const unitDropdown = screen.getByTestId('thresholdType-dropdown-0-0');
+ expect(unitDropdown).toBeInTheDocument();
+
+ fireEvent.change(unitDropdown, { target: { value: 'units' } });
+
+ // Trigger validation
+ fireEvent.blur(thresholdInput);
+
+ // Wait for the error message to appear
+ await waitFor(() => {
+ expect(
+ screen.getByText(
+ 'absolute threshold must be a positive number greater than zero'
+ )
+ ).toBeInTheDocument();
+ });
+ });
+ test('displays error when -1 is entered in suppression rules relative threshold', async () => {
+ render(
+
+ {() => }
+
+ );
+
+ screen.logTestingPlaygroundURL();
+
+ // Click to add a new suppression rule
+ const addButton = screen.getByRole('button', { name: /add rule/i });
+ fireEvent.click(addButton);
+
+ // Find the threshold input and type -1
+ const thresholdInput = screen.getAllByPlaceholderText('Threshold')[0];
+ userEvent.type(thresholdInput, '-1');
+
+// // Find the dropdown using the data-test-subj attribute
+// const percentageDropdown = screen.getByTestId('thresholdType-dropdown-0-0');
+// expect(percentageDropdown).toBeInTheDocument();
+
+// fireEvent.change(percentageDropdown, { target: { value: 'percentage' } });
+
+ // Trigger validation
+ fireEvent.blur(thresholdInput);
+
+ // Wait for the error message to appear
+ await waitFor(() => {
+ expect(
+ screen.getByText(
+ 'relative threshold must be a positive number greater than zero'
+ )
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/public/pages/ConfigureModel/components/SuppressionRules/index.ts b/public/pages/ConfigureModel/components/SuppressionRules/index.ts
new file mode 100644
index 00000000..75792be2
--- /dev/null
+++ b/public/pages/ConfigureModel/components/SuppressionRules/index.ts
@@ -0,0 +1,12 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ *
+ * Modifications Copyright OpenSearch Contributors. See
+ * GitHub history for details.
+ */
+
+export { SuppressionRules } from './SuppressionRules';
diff --git a/public/pages/ConfigureModel/containers/ConfigureModel.tsx b/public/pages/ConfigureModel/containers/ConfigureModel.tsx
index 1c0c0cd3..6427ba50 100644
--- a/public/pages/ConfigureModel/containers/ConfigureModel.tsx
+++ b/public/pages/ConfigureModel/containers/ConfigureModel.tsx
@@ -228,11 +228,29 @@ export function ConfigureModel(props: ConfigureModelProps) {
}
};
+ const flattenErrorMessages = (errors): string => {
+ if (Array.isArray(errors)) {
+ return errors
+ .flatMap((innerArray) =>
+ Array.isArray(innerArray)
+ ? innerArray.map((errorObj) =>
+ Object.entries(errorObj || {})
+ .map(([key, value]) => `${key}: ${value}`)
+ .join(", ")
+ )
+ : []
+ )
+ .join(", ");
+ }
+ return typeof errors === "string" ? errors : "";
+ };
+
+
const validateRules = (
formikValues: ModelConfigurationFormikValues,
errors: any
) => {
- const rules = formikValues.suppressionRules || [];
+ const suppressionRules = formikValues.suppressionRules || [];
// Initialize an array to hold individual error messages
const featureNameErrors: string[] = [];
@@ -243,11 +261,30 @@ export function ConfigureModel(props: ConfigureModelProps) {
.map((feature: FeaturesFormikValues) => feature.featureName);
// Validate that each featureName in suppressionRules exists in enabledFeatures
- rules.forEach((rule: RuleFormikValues) => {
- if (!enabledFeatures.includes(rule.featureName)) {
- featureNameErrors.push(
- `Feature "${rule.featureName}" in suppression rules does not exist or is not enabled in the feature list.`
- );
+ suppressionRules.forEach((featureRules: RuleFormikValues[], featureIndex: number) => {
+ if (featureRules != null && featureRules.length > 0) {
+ const featureName = featureRules[0]?.featureName;
+ if (featureName === "" || featureName === undefined) {
+ featureNameErrors.push(
+ "Please make sure all features have unique names"
+ );
+ return;
+ }
+
+ if (!enabledFeatures.includes(featureName)) {
+ featureNameErrors.push(
+ `Feature "${featureName}" in suppression rules does not exist or is not enabled in the feature list.`
+ );
+ }
+
+ // Additional validation for each rule if needed
+ featureRules.forEach((rule: RuleFormikValues, ruleIndex: number) => {
+ if (rule.absoluteThreshold === null && rule.relativeThreshold === null) {
+ featureNameErrors.push(
+ `Rule ${ruleIndex + 1} for feature "${featureName}" must have either an absolute or relative threshold.`
+ );
+ }
+ });
}
});
@@ -271,7 +308,6 @@ export function ConfigureModel(props: ConfigureModelProps) {
formikProps.setFieldTouched('shingleSize');
formikProps.setFieldTouched('imputationOption');
formikProps.setFieldTouched('suppressionRules');
-
formikProps.validateForm().then((errors) => {
// Call the extracted validation method
validateImputationOption(formikProps.values, errors);
@@ -306,8 +342,9 @@ export function ConfigureModel(props: ConfigureModelProps) {
const ruleValueError = get(errors, 'suppressionRules')
if (ruleValueError) {
+ const errorString = flattenErrorMessages(ruleValueError);
core.notifications.toasts.addDanger(
- ruleValueError
+ errorString
);
focusOnSuppressionRules();
return;
diff --git a/public/pages/ConfigureModel/containers/__tests__/__snapshots__/ConfigureModel.test.tsx.snap b/public/pages/ConfigureModel/containers/__tests__/__snapshots__/ConfigureModel.test.tsx.snap
index edd26cde..4ad25010 100644
--- a/public/pages/ConfigureModel/containers/__tests__/__snapshots__/ConfigureModel.test.tsx.snap
+++ b/public/pages/ConfigureModel/containers/__tests__/__snapshots__/ConfigureModel.test.tsx.snap
@@ -273,7 +273,7 @@ exports[` spec creating model configuration renders the compon
class="euiFormHelpText euiFormRow__text"
id="random_html_id-help-0"
>
- Enter a descriptive name. The name must be unique within this detector. Feature name must contain 1-64 characters. Valid characters are a-z, A-Z, 0-9, -(hyphen) and _(underscore).
+ Enter a descriptive, unique name. The name must contain 1-64 characters. Valid characters are a-z, A-Z, 0-9, -(hyphen) and _(underscore).
@@ -386,7 +386,9 @@ exports[` spec creating model configuration renders the compon
spec creating model configuration renders the compon
class="euiFormLabel euiFormRow__label"
for="random_html_id"
>
- Aggregation method
+
+
+ Aggregation method
+
+
+
+ The aggregation method determines what constitutes an anomaly.
+
+
spec creating model configuration renders the compon
class="euiFormHelpText euiFormRow__text"
id="random_html_id-help-0"
>
- The aggregation method determines what constitutes an anomaly. For example, if you choose min(), the detector focuses on finding anomalies based on the minimum values of your feature.
+ E.g, if you choose min(), the detector focuses on finding anomalies based on the minimum values of your feature.
@@ -562,6 +577,276 @@ exports[` spec creating model configuration renders the compon
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ignore anomalies when the actual value is no more than:
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1255,7 +1540,7 @@ exports[` spec editing model configuration renders the compone
class="euiFormHelpText euiFormRow__text"
id="random_html_id-help-0"
>
- Enter a descriptive name. The name must be unique within this detector. Feature name must contain 1-64 characters. Valid characters are a-z, A-Z, 0-9, -(hyphen) and _(underscore).
+ Enter a descriptive, unique name. The name must contain 1-64 characters. Valid characters are a-z, A-Z, 0-9, -(hyphen) and _(underscore).
@@ -1369,7 +1654,9 @@ exports[` spec editing model configuration renders the compone
spec editing model configuration renders the compone
class="euiFormLabel euiFormRow__label"
for="random_html_id"
>
- Aggregation method
+
+
+ Aggregation method
+
+
+
+ The aggregation method determines what constitutes an anomaly.
+
+
spec editing model configuration renders the compone
class="euiFormHelpText euiFormRow__text"
id="random_html_id-help-0"
>
- The aggregation method determines what constitutes an anomaly. For example, if you choose min(), the detector focuses on finding anomalies based on the minimum values of your feature.
+ E.g, if you choose min(), the detector focuses on finding anomalies based on the minimum values of your feature.
@@ -1547,6 +1847,278 @@ exports[` spec editing model configuration renders the compone
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ignore anomalies when the actual value is no more than:
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/pages/ConfigureModel/models/interfaces.ts b/public/pages/ConfigureModel/models/interfaces.ts
index 9bf2a496..824635da 100644
--- a/public/pages/ConfigureModel/models/interfaces.ts
+++ b/public/pages/ConfigureModel/models/interfaces.ts
@@ -19,7 +19,7 @@ export interface ModelConfigurationFormikValues {
categoryField: string[];
shingleSize: number;
imputationOption?: ImputationFormikValues;
- suppressionRules?: RuleFormikValues[];
+ suppressionRules?: Array;
}
export interface FeaturesFormikValues {
@@ -48,4 +48,5 @@ export interface RuleFormikValues {
absoluteThreshold?: number;
relativeThreshold?: number;
aboveBelow: string;
+ directionRule?: boolean;
}
diff --git a/public/pages/ConfigureModel/utils/constants.tsx b/public/pages/ConfigureModel/utils/constants.tsx
index b0a84b2e..1f8f6248 100644
--- a/public/pages/ConfigureModel/utils/constants.tsx
+++ b/public/pages/ConfigureModel/utils/constants.tsx
@@ -76,3 +76,9 @@ export enum SparseDataOptionValue {
SET_TO_ZERO = 'set_to_zero',
CUSTOM_VALUE = 'custom_value',
}
+
+export const FEATURE_DIRECTION_OPTIONS = [
+ { text: 'Deviation in any direction (default)', value: "both" },
+ { text: 'Rise above expected value', value: "above" },
+ { text: 'Drop below expected value', value: "below" }
+];
diff --git a/public/pages/ConfigureModel/utils/helpers.ts b/public/pages/ConfigureModel/utils/helpers.ts
index 7df261a1..7551d5a7 100644
--- a/public/pages/ConfigureModel/utils/helpers.ts
+++ b/public/pages/ConfigureModel/utils/helpers.ts
@@ -39,9 +39,7 @@ import {
Operator,
Action,
} from '../../../models/types';
-import {
- SparseDataOptionValue
-} from './constants'
+import { SparseDataOptionValue } from './constants';
export const getFieldOptions = (
allFields: { [key: string]: string[] },
@@ -259,18 +257,25 @@ export function modelConfigurationToFormik(
var defaultFillArray: CustomValueFormikValues[] = [];
if (SparseDataOptionValue.CUSTOM_VALUE === imputationMethod) {
- const defaultFill = get(detector, 'imputationOption.defaultFill', null) as Array<{ featureName: string; data: number }> | null;
+ const defaultFill = get(
+ detector,
+ 'imputationOption.defaultFill',
+ null
+ ) as Array<{ featureName: string; data: number }> | null;
defaultFillArray = defaultFill
- ? defaultFill.map(({ featureName, data }) => ({
- featureName,
- data,
- }))
- : [];
+ ? defaultFill.map(({ featureName, data }) => ({
+ featureName,
+ data,
+ }))
+ : [];
}
const imputationFormikValues: ImputationFormikValues = {
imputationMethod: imputationMethod,
- custom_value: SparseDataOptionValue.CUSTOM_VALUE === imputationMethod ? defaultFillArray : undefined,
+ custom_value:
+ SparseDataOptionValue.CUSTOM_VALUE === imputationMethod
+ ? defaultFillArray
+ : undefined,
};
return {
@@ -382,27 +387,34 @@ export function formikToSimpleAggregation(value: FeaturesFormikValues) {
}
}
-export function formikToImputationOption(imputationFormikValues?: ImputationFormikValues): ImputationOption | undefined {
+export function formikToImputationOption(
+ imputationFormikValues?: ImputationFormikValues
+): ImputationOption | undefined {
// Map the formik method to the imputation method; return undefined if method is not recognized.
- const method = formikToImputationMethod(imputationFormikValues?.imputationMethod);
+ const method = formikToImputationMethod(
+ imputationFormikValues?.imputationMethod
+ );
if (!method) return undefined;
// Convert custom_value array to defaultFill if the method is FIXED_VALUES.
- const defaultFill = method === ImputationMethod.FIXED_VALUES
- ? imputationFormikValues?.custom_value?.map(({ featureName, data }) => ({
- featureName,
- data,
- }))
- : undefined;
+ const defaultFill =
+ method === ImputationMethod.FIXED_VALUES
+ ? imputationFormikValues?.custom_value?.map(({ featureName, data }) => ({
+ featureName,
+ data,
+ }))
+ : undefined;
// Construct and return the ImputationOption object.
return { method, defaultFill };
}
-export function imputationMethodToFormik(
- detector: Detector
-): string {
- var imputationMethod = get(detector, 'imputationOption.method', undefined) as ImputationMethod;
+export function imputationMethodToFormik(detector: Detector): string {
+ var imputationMethod = get(
+ detector,
+ 'imputationOption.method',
+ undefined
+ ) as ImputationMethod;
switch (imputationMethod) {
case ImputationMethod.FIXED_VALUES:
@@ -433,15 +445,23 @@ export function formikToImputationMethod(
}
}
-export const getCustomValueStrArray = (imputationMethodStr : string, detector: Detector): string[] => {
+export const getCustomValueStrArray = (
+ imputationMethodStr: string,
+ detector: Detector
+): string[] => {
if (SparseDataOptionValue.CUSTOM_VALUE === imputationMethodStr) {
- const defaultFill : Array<{ featureName: string; data: number }> = get(detector, 'imputationOption.defaultFill', []);
-
- return defaultFill
- .map(({ featureName, data }) => `${featureName}: ${data}`);
+ const defaultFill: Array<{ featureName: string; data: number }> = get(
+ detector,
+ 'imputationOption.defaultFill',
+ []
+ );
+
+ return defaultFill.map(
+ ({ featureName, data }) => `${featureName}: ${data}`
+ );
}
- return []
-}
+ return [];
+};
export const getSuppressionRulesArray = (detector: Detector): string[] => {
if (!detector.rules || detector.rules.length === 0) {
@@ -453,8 +473,17 @@ export const getSuppressionRulesArray = (detector: Detector): string[] => {
return rule.conditions.map((condition) => {
const featureName = condition.featureName;
const thresholdType = condition.thresholdType;
+ if (thresholdType === ThresholdType.ACTUAL_IS_OVER_EXPECTED) {
+ return `Ignore anomalies for feature "${featureName}" when actual value is above the expected value`;
+ } else if (thresholdType === ThresholdType.ACTUAL_IS_BELOW_EXPECTED) {
+ return `Ignore anomalies for feature "${featureName}" when actual value is below the expected value`;
+ }
+
+
let value = condition.value;
- const isPercentage = thresholdType === ThresholdType.ACTUAL_OVER_EXPECTED_RATIO || thresholdType === ThresholdType.EXPECTED_OVER_ACTUAL_RATIO;
+ const isPercentage =
+ thresholdType === ThresholdType.ACTUAL_OVER_EXPECTED_RATIO ||
+ thresholdType === ThresholdType.EXPECTED_OVER_ACTUAL_RATIO;
// If it is a percentage, multiply by 100
if (isPercentage) {
@@ -462,63 +491,153 @@ export const getSuppressionRulesArray = (detector: Detector): string[] => {
}
// Determine whether it is "above" or "below" based on ThresholdType
- const aboveOrBelow = thresholdType === ThresholdType.ACTUAL_OVER_EXPECTED_MARGIN || thresholdType === ThresholdType.ACTUAL_OVER_EXPECTED_RATIO ? 'above' : 'below';
+ const aboveOrBelow =
+ thresholdType === ThresholdType.ACTUAL_OVER_EXPECTED_MARGIN ||
+ thresholdType === ThresholdType.ACTUAL_OVER_EXPECTED_RATIO
+ ? 'above'
+ : 'below';
// Construct the formatted string
- return `Ignore anomalies for feature "${featureName}" with no more than ${value}${isPercentage ? '%' : ''} ${aboveOrBelow} expected value.`;
+ return `Ignore anomalies for feature "${featureName}" with no more than ${value}${
+ isPercentage ? '%' : ''
+ } ${aboveOrBelow} expected value.`;
});
});
};
+export const getSuppressionRulesArrayForFeature = (
+ detector: Detector,
+ featureName: string
+): string[] => {
+ if (!detector.rules || detector.rules.length === 0) {
+ return []; // Return an empty array if there are no rules
+ }
+
+ return detector.rules.flatMap((rule) => {
+ // Filter conditions based on the specified feature name
+ const featureConditions = rule.conditions.filter(
+ (condition) => condition.featureName === featureName
+ );
+
+ // Convert each filtered condition to a readable string
+ return featureConditions.map((condition) => {
+ const thresholdType = condition.thresholdType;
+
+ if (thresholdType === ThresholdType.ACTUAL_IS_OVER_EXPECTED) {
+ return `Ignore anomalies for feature "${featureName}" when actual value is above the expected value.`;
+ } else if (thresholdType === ThresholdType.ACTUAL_IS_BELOW_EXPECTED) {
+ return `Ignore anomalies for feature "${featureName}" when actual value is below the expected value.`;
+ }
+
+ let value = condition.value;
+ const isPercentage =
+ thresholdType === ThresholdType.ACTUAL_OVER_EXPECTED_RATIO ||
+ thresholdType === ThresholdType.EXPECTED_OVER_ACTUAL_RATIO;
+
+ // If it is a percentage, multiply by 100
+ if (isPercentage) {
+ value *= 100;
+ }
+
+ // Determine whether it is "above" or "below" based on ThresholdType
+ const aboveOrBelow =
+ thresholdType === ThresholdType.ACTUAL_OVER_EXPECTED_MARGIN ||
+ thresholdType === ThresholdType.ACTUAL_OVER_EXPECTED_RATIO
+ ? 'above'
+ : 'below';
+
+ // Construct the formatted string
+ return `Ignore anomalies for feature "${featureName}" with no more than ${value}${
+ isPercentage ? '%' : ''
+ } ${aboveOrBelow} expected value.`;
+ });
+ });
+};
// Convert RuleFormikValues[] to Rule[]
-export const formikToRules = (formikValues?: RuleFormikValues[]): Rule[] | undefined => {
+export const formikToRules = (
+ formikValues?: RuleFormikValues[]
+): Rule[] | undefined => {
+
if (!formikValues || formikValues.length === 0) {
return undefined; // Return undefined for undefined or empty input
}
- return formikValues.map((formikValue) => {
+ // Flatten the nested array of suppressionRule by feature and filter out null entries
+ const flattenedSuppressionFormikValues = formikValues.flatMap((nestedArray) =>
+ nestedArray || [] // If null, replace with an empty array
+ );
+
+ return flattenedSuppressionFormikValues.map((formikValue) => {
const conditions: Condition[] = [];
+ if (formikValue != null) {
+ // Determine the threshold type based on aboveBelow and the threshold type (absolute or relative)
+ const getThresholdType = (
+ aboveBelow: string,
+ isAbsolute: boolean,
+ directionRule?: boolean
+ ): ThresholdType => {
+ if (directionRule) {
+ return aboveBelow === 'above'
+ ? ThresholdType.ACTUAL_IS_BELOW_EXPECTED
+ : ThresholdType.ACTUAL_IS_OVER_EXPECTED;
+ } else if (isAbsolute) {
+ return aboveBelow === 'above'
+ ? ThresholdType.ACTUAL_OVER_EXPECTED_MARGIN
+ : ThresholdType.EXPECTED_OVER_ACTUAL_MARGIN;
+ } else {
+ return aboveBelow === 'above'
+ ? ThresholdType.ACTUAL_OVER_EXPECTED_RATIO
+ : ThresholdType.EXPECTED_OVER_ACTUAL_RATIO;
+ }
+ };
- // Determine the threshold type based on aboveBelow and the threshold type (absolute or relative)
- const getThresholdType = (aboveBelow: string, isAbsolute: boolean): ThresholdType => {
- if (isAbsolute) {
- return aboveBelow === 'above'
- ? ThresholdType.ACTUAL_OVER_EXPECTED_MARGIN
- : ThresholdType.EXPECTED_OVER_ACTUAL_MARGIN;
- } else {
- return aboveBelow === 'above'
- ? ThresholdType.ACTUAL_OVER_EXPECTED_RATIO
- : ThresholdType.EXPECTED_OVER_ACTUAL_RATIO;
+
+
+ if (formikValue.directionRule) {
+ conditions.push({
+ featureName: formikValue.featureName,
+ thresholdType: getThresholdType(formikValue.aboveBelow, true, formikValue.directionRule),
+ operator: undefined,
+ value: undefined,
+ });
}
- };
- // Check if absoluteThreshold is provided, create a condition
- if (formikValue.absoluteThreshold !== undefined && formikValue.absoluteThreshold !== 0 && formikValue.absoluteThreshold !== null
- && typeof formikValue.absoluteThreshold === 'number' && // Check if it's a number
- !isNaN(formikValue.absoluteThreshold) && // Ensure it's not NaN
- formikValue.absoluteThreshold > 0 // Check if it's positive
- ) {
- conditions.push({
- featureName: formikValue.featureName,
- thresholdType: getThresholdType(formikValue.aboveBelow, true),
- operator: Operator.LTE,
- value: formikValue.absoluteThreshold,
- });
- }
+ // Check if absoluteThreshold is provided, create a condition
+ if (
+ formikValue.absoluteThreshold !== undefined &&
+ formikValue.absoluteThreshold !== 0 &&
+ formikValue.absoluteThreshold !== null &&
+ typeof formikValue.absoluteThreshold === 'number' && // Check if it's a number
+ !isNaN(formikValue.absoluteThreshold) && // Ensure it's not NaN
+ formikValue.absoluteThreshold > 0 && // Check if it's positive
+ formikValue?.directionRule != true
+ ) {
+ conditions.push({
+ featureName: formikValue.featureName,
+ thresholdType: getThresholdType(formikValue.aboveBelow, true),
+ operator: Operator.LTE,
+ value: formikValue.absoluteThreshold,
+ });
+ }
- // Check if relativeThreshold is provided, create a condition
- if (formikValue.relativeThreshold !== undefined && formikValue.relativeThreshold !== 0 && formikValue.relativeThreshold !== null
- && typeof formikValue.relativeThreshold === 'number' && // Check if it's a number
- !isNaN(formikValue.relativeThreshold) && // Ensure it's not NaN
- formikValue.relativeThreshold > 0 // Check if it's positive
- ) {
- conditions.push({
- featureName: formikValue.featureName,
- thresholdType: getThresholdType(formikValue.aboveBelow, false),
- operator: Operator.LTE,
- value: formikValue.relativeThreshold / 100, // Convert percentage to decimal,
- });
+ // Check if relativeThreshold is provided, create a condition
+ if (
+ formikValue.relativeThreshold !== undefined &&
+ formikValue.relativeThreshold !== 0 &&
+ formikValue.relativeThreshold !== null &&
+ typeof formikValue.relativeThreshold === 'number' && // Check if it's a number
+ !isNaN(formikValue.relativeThreshold) && // Ensure it's not NaN
+ formikValue.relativeThreshold > 0 && // Check if it's positive
+ formikValue?.directionRule != true
+ ) {
+ conditions.push({
+ featureName: formikValue.featureName,
+ thresholdType: getThresholdType(formikValue.aboveBelow, false),
+ operator: Operator.LTE,
+ value: formikValue.relativeThreshold / 100, // Convert percentage to decimal,
+ });
+ }
}
return {
@@ -528,14 +647,16 @@ export const formikToRules = (formikValues?: RuleFormikValues[]): Rule[] | undef
});
};
-// Convert Rule[] to RuleFormikValues[]
-export const rulesToFormik = (rules?: Rule[]): RuleFormikValues[] => {
+// Convert Rule[] to RuleFormikValues[][]
+export const rulesToFormik = (rules?: Rule[]): (RuleFormikValues[] | null)[] => {
if (!rules || rules.length === 0) {
return []; // Return empty array for undefined or empty input
}
+ // Group rules by featureName
+ const groupedRules: { [featureName: string]: RuleFormikValues[] } = {};
- return rules.map((rule) => {
- // Start with default values
+ rules.forEach((rule) => {
+ // Start with default values for each rule
const formikValue: RuleFormikValues = {
featureName: '',
absoluteThreshold: undefined,
@@ -559,21 +680,42 @@ export const rulesToFormik = (rules?: Rule[]): RuleFormikValues[] => {
break;
case ThresholdType.ACTUAL_OVER_EXPECTED_RATIO:
// *100 to convert to percentage
- formikValue.relativeThreshold = condition.value * 100;
+ formikValue.relativeThreshold = (condition.value ?? 1) * 100;
formikValue.aboveBelow = 'above';
break;
case ThresholdType.EXPECTED_OVER_ACTUAL_RATIO:
// *100 to convert to percentage
- formikValue.relativeThreshold = condition.value * 100;
+ formikValue.relativeThreshold = (condition.value ?? 1) * 100;
+ formikValue.aboveBelow = 'below';
+ break;
+ case ThresholdType.ACTUAL_IS_BELOW_EXPECTED:
+ formikValue.relativeThreshold = 0;
+ formikValue.aboveBelow = 'above';
+ formikValue.directionRule = true;
+ break;
+ case ThresholdType.ACTUAL_IS_OVER_EXPECTED:
+ formikValue.relativeThreshold = 0;
formikValue.aboveBelow = 'below';
+ formikValue.directionRule = true;
break;
default:
break;
}
});
- return formikValue;
+ // Add the rule to the grouped object based on featureName
+ if (!groupedRules[formikValue.featureName]) {
+ groupedRules[formikValue.featureName] = [];
+ }
+ groupedRules[formikValue.featureName].push(formikValue);
});
-};
+ // Convert grouped object into an array of arrays based on featureList index
+ const featureList = Object.keys(groupedRules); // Ensure you have a reference to your feature list somewhere
+ const finalRules: (RuleFormikValues[] | null)[] = featureList.map(
+ (featureName) => groupedRules[featureName] || null
+ );
+
+ return finalRules;
+};
diff --git a/public/pages/DetectorConfig/components/AdditionalSettings/AdditionalSettings.tsx b/public/pages/DetectorConfig/components/AdditionalSettings/AdditionalSettings.tsx
index ece8466c..f48c9c24 100644
--- a/public/pages/DetectorConfig/components/AdditionalSettings/AdditionalSettings.tsx
+++ b/public/pages/DetectorConfig/components/AdditionalSettings/AdditionalSettings.tsx
@@ -18,14 +18,13 @@ import {
} from '@elastic/eui';
import ContentPanel from '../../../../components/ContentPanel/ContentPanel';
import { convertToCategoryFieldString } from '../../../utils/anomalyResultUtils';
-import { SuppressionRulesModal } from '../../../ReviewAndCreate/components/AdditionalSettings/SuppressionRulesModal';
+import { SuppressionRulesModal } from '../../../ReviewAndCreate/components/SuppressionRulesModal/SuppressionRulesModal';
interface AdditionalSettingsProps {
shingleSize: number;
categoryField: string[];
imputationMethod: string;
customValues: string[];
- suppressionRules: string[];
}
export function AdditionalSettings(props: AdditionalSettingsProps) {
@@ -39,28 +38,6 @@ export function AdditionalSettings(props: AdditionalSettingsProps) {
);
- const [isModalVisible, setIsModalVisible] = useState(false);
- const [modalContent, setModalContent] = useState([]);
-
- const closeModal = () => setIsModalVisible(false);
-
- const showRulesInModal = (rules: string[]) => {
- setModalContent(rules);
- setIsModalVisible(true);
- };
-
- const renderSuppressionRules = (suppressionRules: string[]) => (
-
@@ -2176,26 +2139,6 @@ exports[`issue in detector validation issues in feature query 1`] = `
-
-
- Suppression rules
-
-
-
-
- -
-
-
-
-
@@ -2413,6 +2356,23 @@ exports[`issue in detector validation issues in feature query 1`] = `
+
+
+
+ Anomaly Criteria
+
+
+
@@ -2421,7 +2381,7 @@ exports[`issue in detector validation issues in feature query 1`] = `
>
{
return 'Must be a positive integer';
};
-// Validation function for positive decimal numbers
export function validatePositiveDecimal(value: any) {
// Allow empty, NaN, or non-number values without showing an error
if (value === '' || value === null || isNaN(value) || typeof value !== 'number') {