diff --git a/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx b/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx index b77147d3..d62ccb04 100644 --- a/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx +++ b/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx @@ -24,7 +24,7 @@ import { EuiSpacer, } from '@elastic/eui'; import './styles.scss'; -import { Field, FieldArray, FieldProps } from 'formik'; +import { Field, FieldProps } from 'formik'; import { required, isInvalid, diff --git a/public/pages/ConfigureModel/components/SuppressionRules/SuppressionRules.tsx b/public/pages/ConfigureModel/components/SuppressionRules/SuppressionRules.tsx index c9a4649e..848a9270 100644 --- a/public/pages/ConfigureModel/components/SuppressionRules/SuppressionRules.tsx +++ b/public/pages/ConfigureModel/components/SuppressionRules/SuppressionRules.tsx @@ -13,19 +13,12 @@ import { EuiFlexItem, EuiFlexGroup, EuiText, - EuiLink, - EuiTitle, EuiCompressedFieldNumber, EuiSpacer, EuiCompressedSelect, EuiButtonIcon, - EuiCompressedFieldText, EuiToolTip, - OuiTextColor, - EuiTextColor, EuiSelect, - EuiCheckbox, - EuiFieldNumber, EuiButtonEmpty, } from '@elastic/eui'; import { Field, FieldProps, FieldArray } from 'formik'; @@ -34,8 +27,6 @@ import ContentPanel from '../../../../components/ContentPanel/ContentPanel'; import { BASE_DOCS_LINK } from '../../../../utils/constants'; import { isInvalid, - getError, - validatePositiveInteger, validatePositiveDecimal, } from '../../../../utils/utils'; import { FormattedFormRow } from '../../../../components/FormattedFormRow/FormattedFormRow'; @@ -72,8 +63,8 @@ export function SuppressionRules(props: SuppressionRulesProps) { ? 'relative threshold' : fieldKey; return typeof fieldError === 'string' - ? `${friendlyFieldName} ${fieldError.toLowerCase()}` - : String(fieldError || ''); + ? `${friendlyFieldName} ${fieldError.toLowerCase()}` + : String(fieldError || ''); } } } @@ -315,23 +306,69 @@ export function SuppressionRules(props: SuppressionRulesProps) { - {({ field }: FieldProps) => ( - - - - )} + {({ field }: FieldProps) => { + const currentRules = + form.values.suppressionRules?.[ + props.featureIndex + ] || []; + + // Check if there's a directionRule = true and get its "aboveBelow" value + const directionRule = currentRules.find( + (rule) => rule.directionRule === true + ); + + let options = [ + { + value: 'above', + text: 'above the expected value', + disabled: false, + }, + { + value: 'below', + text: 'below the expected value', + disabled: false, + }, + ]; + + let tooltipContent = + 'Select above or below expected value'; + + // Modify options based on the directionRule logic + if (directionRule) { + options = options.map((option) => ({ + ...option, + disabled: + directionRule.aboveBelow !== + option.value, + })); + + if ( + field.value !== + directionRule.aboveBelow + ) { + form.setFieldValue( + `suppressionRules.${props.featureIndex}[${index}].aboveBelow`, + directionRule.aboveBelow + ); + } + if (directionRule?.aboveBelow) { + const directionText = + directionRule.aboveBelow === 'above' + ? 'exceeds' + : 'drops below'; + tooltipContent = `Base criteria includes anomalies where the actual value ${directionText} the expected value. Rules can only be made in this direction.`; + } + } + + return ( + + + + ); + }} diff --git a/public/pages/ConfigureModel/utils/helpers.ts b/public/pages/ConfigureModel/utils/helpers.ts index 7551d5a7..965b7fa1 100644 --- a/public/pages/ConfigureModel/utils/helpers.ts +++ b/public/pages/ConfigureModel/utils/helpers.ts @@ -479,7 +479,6 @@ export const getSuppressionRulesArray = (detector: Detector): string[] => { 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 || @@ -558,15 +557,14 @@ export const getSuppressionRulesArrayForFeature = ( export const formikToRules = ( formikValues?: RuleFormikValues[] ): Rule[] | undefined => { - if (!formikValues || formikValues.length === 0) { return undefined; // Return undefined for undefined or empty input } - // 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 - ); + // 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[] = []; @@ -592,12 +590,14 @@ export const formikToRules = ( } }; - - if (formikValue.directionRule) { conditions.push({ featureName: formikValue.featureName, - thresholdType: getThresholdType(formikValue.aboveBelow, true, formikValue.directionRule), + thresholdType: getThresholdType( + formikValue.aboveBelow, + true, + formikValue.directionRule + ), operator: undefined, value: undefined, }); @@ -647,28 +647,26 @@ export const formikToRules = ( }); }; -// Convert Rule[] to RuleFormikValues[][] -export const rulesToFormik = (rules?: Rule[]): (RuleFormikValues[] | null)[] => { +export const rulesToFormik = ( + rules?: Rule[] +): (RuleFormikValues[] | null)[] => { if (!rules || rules.length === 0) { - return []; // Return empty array for undefined or empty input + return []; } // Group rules by featureName const groupedRules: { [featureName: string]: RuleFormikValues[] } = {}; rules.forEach((rule) => { - // Start with default values for each rule - const formikValue: RuleFormikValues = { - featureName: '', - absoluteThreshold: undefined, - relativeThreshold: undefined, - aboveBelow: 'above', // Default to 'above', adjust as needed - }; - - // Loop through conditions to populate formikValue rule.conditions.forEach((condition) => { - formikValue.featureName = condition.featureName; + // Create a new formikValue for each condition + const formikValue: RuleFormikValues = { + featureName: condition.featureName, + absoluteThreshold: undefined, + relativeThreshold: undefined, + aboveBelow: 'above', // Default to 'above', adjust as needed + }; - // Determine the value and type of threshold + // Populate formikValue based on threshold type switch (condition.thresholdType) { case ThresholdType.ACTUAL_OVER_EXPECTED_MARGIN: formikValue.absoluteThreshold = condition.value; @@ -679,13 +677,11 @@ export const rulesToFormik = (rules?: Rule[]): (RuleFormikValues[] | null)[] => formikValue.aboveBelow = 'below'; break; case ThresholdType.ACTUAL_OVER_EXPECTED_RATIO: - // *100 to convert to percentage - formikValue.relativeThreshold = (condition.value ?? 1) * 100; + formikValue.relativeThreshold = (condition.value ?? 1) * 100; // Convert to percentage formikValue.aboveBelow = 'above'; break; case ThresholdType.EXPECTED_OVER_ACTUAL_RATIO: - // *100 to convert to percentage - formikValue.relativeThreshold = (condition.value ?? 1) * 100; + formikValue.relativeThreshold = (condition.value ?? 1) * 100; // Convert to percentage formikValue.aboveBelow = 'below'; break; case ThresholdType.ACTUAL_IS_BELOW_EXPECTED: @@ -701,21 +697,17 @@ export const rulesToFormik = (rules?: Rule[]): (RuleFormikValues[] | null)[] => default: break; } - }); - // Add the rule to the grouped object based on featureName - if (!groupedRules[formikValue.featureName]) { - groupedRules[formikValue.featureName] = []; - } - groupedRules[formikValue.featureName].push(formikValue); + 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 featureList = Object.keys(groupedRules); const finalRules: (RuleFormikValues[] | null)[] = featureList.map( (featureName) => groupedRules[featureName] || null ); - return finalRules; }; diff --git a/public/pages/DetectorConfig/containers/Features.tsx b/public/pages/DetectorConfig/containers/Features.tsx index 87847bd6..f4ad0089 100644 --- a/public/pages/DetectorConfig/containers/Features.tsx +++ b/public/pages/DetectorConfig/containers/Features.tsx @@ -25,8 +25,8 @@ import { FEATURE_TYPE, FeatureAttributes, } from '../../../models/interfaces'; -import { get, isEmpty, sortBy } from 'lodash'; -import { PLUGIN_NAME, BASE_DOCS_LINK } from '../../../utils/constants'; +import { get, sortBy } from 'lodash'; +import { PLUGIN_NAME } from '../../../utils/constants'; import ContentPanel from '../../../components/ContentPanel/ContentPanel'; import { CodeModal } from '../components/CodeModal/CodeModal'; import { getTitleWithCount } from '../../../utils/utils'; @@ -39,7 +39,6 @@ import { getSuppressionRulesArrayForFeature, } from '../../ConfigureModel/utils/helpers'; import { SuppressionRulesModal } from '../../ReviewAndCreate/components/SuppressionRulesModal/SuppressionRulesModal'; -import { Rule } from '../../../models/types'; interface FeaturesProps { detectorId: string; diff --git a/public/pages/DetectorConfig/containers/__tests__/Features.test.tsx b/public/pages/DetectorConfig/containers/__tests__/Features.test.tsx index 4b7a7f28..39b8c163 100644 --- a/public/pages/DetectorConfig/containers/__tests__/Features.test.tsx +++ b/public/pages/DetectorConfig/containers/__tests__/Features.test.tsx @@ -28,15 +28,11 @@ import { } from '../../../../models/types'; import { getRandomDetector } from '../../../../redux/reducers/__tests__/utils'; import { - Detector, UiMetaData, FILTER_TYPES, - UIFilter, FEATURE_TYPE, UiFeature, FeatureAttributes, - OPERATORS_MAP, - UNITS, } from '../../../../models/interfaces'; import { featureQuery1, featureQuery2 } from './DetectorConfig.test';