diff --git a/.github/workflows/remote-integ-tests-workflow.yml b/.github/workflows/remote-integ-tests-workflow.yml index 05b96d1f..31f56a6c 100644 --- a/.github/workflows/remote-integ-tests-workflow.yml +++ b/.github/workflows/remote-integ-tests-workflow.yml @@ -151,10 +151,7 @@ jobs: - name: Run spec files from output run: | - for i in $FILELIST; do - yarn cypress:run-without-security --browser electron --spec "${i}" - sleep 60 - done + env CYPRESS_NO_COMMAND_LOG=1 yarn cypress:run-without-security --browser chromium --spec 'cypress/integration/plugins/anomaly-detection-dashboards-plugin/*' working-directory: opensearch-dashboards-functional-test - name: Capture failure screenshots diff --git a/public/models/interfaces.ts b/public/models/interfaces.ts index f6dfc651..74a0a1e6 100644 --- a/public/models/interfaces.ts +++ b/public/models/interfaces.ts @@ -15,7 +15,10 @@ import { DETECTOR_STATE } from '../../server/utils/constants'; import { Duration } from 'moment'; import moment from 'moment'; import { MDSQueryParams } from '../../server/models/types'; -import { ImputationOption } from './types'; +import { + ImputationOption, + Rule +} from './types'; export type FieldInfo = { label: string; @@ -212,6 +215,7 @@ export type Detector = { taskProgress?: number; taskError?: string; imputationOption?: ImputationOption; + rules?: Rule[]; }; export type DetectorListItem = { diff --git a/public/models/types.ts b/public/models/types.ts index 6d559276..866396cf 100644 --- a/public/models/types.ts +++ b/public/models/types.ts @@ -33,3 +33,87 @@ export enum ImputationMethod { PREVIOUS = 'PREVIOUS', } +// Constants for field names +export const RULES_FIELD = "rules"; +export const ACTION_FIELD = "action"; +export const CONDITIONS_FIELD = "conditions"; +export const FEATURE_NAME_FIELD = "feature_name"; +export const THRESHOLD_TYPE_FIELD = "threshold_type"; +export const OPERATOR_FIELD = "operator"; +export const VALUE_FIELD = "value"; + +// Enums +export enum Action { + IGNORE_ANOMALY = "IGNORE_ANOMALY", // ignore anomaly if found +} + +export enum ThresholdType { + /** + * Specifies a threshold for ignoring anomalies where the actual value + * exceeds the expected value by a certain margin. + * + * Assume a represents the actual value and b signifies the expected value. + * IGNORE_SIMILAR_FROM_ABOVE implies the anomaly should be disregarded if a-b + * is less than or equal to ignoreSimilarFromAbove. + */ + ACTUAL_OVER_EXPECTED_MARGIN = "ACTUAL_OVER_EXPECTED_MARGIN", + + /** + * Specifies a threshold for ignoring anomalies where the actual value + * is below the expected value by a certain margin. + * + * Assume a represents the actual value and b signifies the expected value. + * Likewise, IGNORE_SIMILAR_FROM_BELOW + * implies the anomaly should be disregarded if b-a is less than or equal to + * ignoreSimilarFromBelow. + */ + EXPECTED_OVER_ACTUAL_MARGIN = "EXPECTED_OVER_ACTUAL_MARGIN", + + /** + * Specifies a threshold for ignoring anomalies based on the ratio of + * the difference to the actual value when the actual value exceeds + * the expected value. + * + * Assume a represents the actual value and b signifies the expected value. + * The variable IGNORE_NEAR_EXPECTED_FROM_ABOVE_BY_RATIO presumably implies the + * anomaly should be disregarded if the ratio of the deviation from the actual + * to the expected (a-b)/|a| is less than or equal to IGNORE_NEAR_EXPECTED_FROM_ABOVE_BY_RATIO. + */ + ACTUAL_OVER_EXPECTED_RATIO = "ACTUAL_OVER_EXPECTED_RATIO", + + /** + * Specifies a threshold for ignoring anomalies based on the ratio of + * the difference to the actual value when the actual value is below + * the expected value. + * + * Assume a represents the actual value and b signifies the expected value. + * Likewise, IGNORE_NEAR_EXPECTED_FROM_BELOW_BY_RATIO appears to indicate that the anomaly + * should be ignored if the ratio of the deviation from the expected to the actual + * (b-a)/|a| is less than or equal to ignoreNearExpectedFromBelowByRatio. + */ + EXPECTED_OVER_ACTUAL_RATIO = "EXPECTED_OVER_ACTUAL_RATIO", +} + +// Method to get the description of ThresholdType +export function getThresholdTypeDescription(thresholdType: ThresholdType): string { + return thresholdType; // In TypeScript, the enum itself holds the description. +} + +// Enums for Operators +export enum Operator { + LTE = "LTE", +} + +// Interfaces for Rule and Condition +export interface Rule { + action: Action; + conditions: Condition[]; +} + +export interface Condition { + featureName: string; + thresholdType: ThresholdType; + operator: Operator; + value: number; +} + diff --git a/public/pages/ConfigureModel/components/AdvancedSettings/AdvancedSettings.tsx b/public/pages/ConfigureModel/components/AdvancedSettings/AdvancedSettings.tsx index 0ccac512..3cae536e 100644 --- a/public/pages/ConfigureModel/components/AdvancedSettings/AdvancedSettings.tsx +++ b/public/pages/ConfigureModel/components/AdvancedSettings/AdvancedSettings.tsx @@ -20,8 +20,9 @@ import { EuiCompressedSelect, EuiButtonIcon, EuiCompressedFieldText, + EuiToolTip, } from '@elastic/eui'; -import { Field, FieldProps, FieldArray, } from 'formik'; +import { Field, FieldProps, FieldArray } from 'formik'; import React, { useEffect, useState } from 'react'; import ContentPanel from '../../../../components/ContentPanel/ContentPanel'; import { BASE_DOCS_LINK } from '../../../../utils/constants'; @@ -29,6 +30,7 @@ import { isInvalid, getError, validatePositiveInteger, + validatePositiveDecimal, } from '../../../../utils/utils'; import { FormattedFormRow } from '../../../../components/FormattedFormRow/FormattedFormRow'; import { SparseDataOptionValue } from '../../utils/constants'; @@ -47,6 +49,46 @@ export function AdvancedSettings(props: AdvancedSettingsProps) { { value: SparseDataOptionValue.CUSTOM_VALUE, text: 'Custom value' }, ]; + const aboveBelowOptions = [ + { value: 'above', text: 'above' }, + { value: 'below', text: 'below' }, + ]; + + function extractArrayError(fieldName: string, form: any): string { + const error = form.errors[fieldName]; + console.log('Error for field:', fieldName, error); // Log the error for debugging + + // Check if the error is an array with objects inside + if (Array.isArray(error) && error.length > 0) { + // Iterate through the array to find the first non-empty error message + for (const err of error) { + if (typeof err === 'object' && err !== null) { + const entry = Object.entries(err).find( + ([_, fieldError]) => fieldError + ); // Find the first entry with a non-empty error message + if (entry) { + const [fieldKey, fieldError] = entry; + + // Replace fieldKey with a more user-friendly name if it matches specific fields + const friendlyFieldName = + fieldKey === 'absoluteThreshold' + ? 'absolute threshold' + : fieldKey === 'relativeThreshold' + ? 'relative threshold' + : fieldKey; // Use the original fieldKey if no match + + return typeof fieldError === 'string' + ? `${friendlyFieldName} ${fieldError.toLowerCase()}` // Format the error message with the friendly field name + : String(fieldError || ''); + } + } + } + } + + // Default case to handle other types of errors + return typeof error === 'string' ? error : String(error || ''); + } + return ( <ContentPanel title={ @@ -126,91 +168,249 @@ export function AdvancedSettings(props: AdvancedSettingsProps) { }, [field.value, form]); return ( - <> - <FormattedFormRow - title="Sparse data handling" - hint={[`Choose how to handle missing data points.`]} - hintLink={`${BASE_DOCS_LINK}/ad`} - isInvalid={isInvalid(field.name, form)} - error={getError(field.name, form)} - > - <EuiCompressedSelect {...field} options={sparseDataOptions}/> - </FormattedFormRow> + <> + <FormattedFormRow + title="Sparse data handling" + hint={[`Choose how to handle missing data points.`]} + hintLink={`${BASE_DOCS_LINK}/ad`} + isInvalid={isInvalid(field.name, form)} + error={getError(field.name, form)} + > + <EuiCompressedSelect + {...field} + options={sparseDataOptions} + /> + </FormattedFormRow> - {/* Conditionally render the "Custom value" title and the input fields when 'Custom value' is selected */} - {field.value === SparseDataOptionValue.CUSTOM_VALUE && ( - <> - <EuiSpacer size="m" /> - <EuiText size="xs"> - <h5>Custom value</h5> - </EuiText> - <EuiSpacer size="s" /> - <FieldArray name="imputationOption.custom_value"> - {(arrayHelpers) => ( - <> - {form.values.imputationOption.custom_value?.map((_, index) => ( - <EuiFlexGroup - key={index} - gutterSize="s" - alignItems="center" - > - <EuiFlexItem grow={false}> - <Field - name={`imputationOption.custom_value.${index}.featureName`} - id={`imputationOption.custom_value.${index}.featureName`} + {/* Conditionally render the "Custom value" title and the input fields when 'Custom value' is selected */} + {field.value === SparseDataOptionValue.CUSTOM_VALUE && ( + <> + <EuiSpacer size="m" /> + <EuiText size="xs"> + <h5>Custom value</h5> + </EuiText> + <EuiSpacer size="s" /> + <FieldArray name="imputationOption.custom_value"> + {(arrayHelpers) => ( + <> + {form.values.imputationOption.custom_value?.map( + (_, index) => ( + <EuiFlexGroup + key={index} + gutterSize="s" + alignItems="center" > - {({ field }: FieldProps) => ( - <EuiCompressedFieldText - placeholder="Feature name" - {...field} + <EuiFlexItem grow={false}> + <Field + name={`imputationOption.custom_value.${index}.featureName`} + id={`imputationOption.custom_value.${index}.featureName`} + > + {({ field }: FieldProps) => ( + <EuiCompressedFieldText + placeholder="Feature name" + {...field} + /> + )} + </Field> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <Field + name={`imputationOption.custom_value.${index}.data`} + id={`imputationOption.custom_value.${index}.data`} + > + {/* the value is set to field.value || '' to avoid displaying 0 as a default value. */} + {({ field, form }: FieldProps) => ( + <EuiCompressedFieldNumber + placeholder="Custom value" + {...field} + value={field.value || ''} + /> + )} + </Field> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonIcon + iconType="trash" + color="danger" + aria-label="Delete row" + onClick={() => arrayHelpers.remove(index)} /> - )} - </Field> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <Field - name={`imputationOption.custom_value.${index}.data`} - id={`imputationOption.custom_value.${index}.data`} - > - {/* the value is set to field.value || '' to avoid displaying 0 as a default value. */ } - {({ field, form }: FieldProps) => ( - <EuiCompressedFieldNumber - fullWidth - placeholder="Custom value" - {...field} - value={field.value || ''} - /> - )} - </Field> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButtonIcon - iconType="trash" - color="danger" - aria-label="Delete row" - onClick={() => arrayHelpers.remove(index)} - /> - </EuiFlexItem> - </EuiFlexGroup> - ))} - <EuiSpacer size="s" /> - { /* add new rows with empty values when the add button is clicked. */} - <EuiButtonIcon - iconType="plusInCircle" - onClick={() => - arrayHelpers.push({ featureName: '', value: 0 }) - } - aria-label="Add row" - /> - </> - )} - </FieldArray> + </EuiFlexItem> + </EuiFlexGroup> + ) + )} + <EuiSpacer size="s" /> + {/* add new rows with empty values when the add button is clicked. */} + <EuiButtonIcon + iconType="plusInCircle" + onClick={() => + arrayHelpers.push({ featureName: '', value: 0 }) + } + aria-label="Add row" + /> + </> + )} + </FieldArray> </> )} </> ); }} </Field> + + <EuiSpacer size="m" /> + <FieldArray name="suppressionRules"> + {(arrayHelpers) => ( + <> + <Field name="suppressionRules"> + {({ field, form }: FieldProps) => ( + <> + <EuiFlexGroup> + {/* Controls the width of the whole row as FormattedFormRow does not allow that. Otherwise, our row is too packed. */} + <EuiFlexItem + grow={false} + style={{ maxWidth: '1200px' }} + > + <FormattedFormRow + title="Suppression Rules" + hint={[ + `Set rules to ignore anomalies by comparing actual values against expected values. + Anomalies can be ignored if the difference is within a specified absolute value or a relative percentage of the expected value.`, + ]} + hintLink={`${BASE_DOCS_LINK}/ad`} + isInvalid={isInvalid(field.name, form)} + error={extractArrayError(field.name, form)} + fullWidth + > + <> + {form.values.suppressionRules?.map( + (rule, index) => ( + <EuiFlexGroup + key={index} + gutterSize="s" + alignItems="center" + > + <EuiFlexItem grow={false}> + <EuiText size="s"> + Ignore anomalies for the feature + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={2}> + <Field + name={`suppressionRules.${index}.featureName`} + > + {({ field }: FieldProps) => ( + <EuiCompressedFieldText + placeholder="Feature name" + {...field} + fullWidth + /> + )} + </Field> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiText size="s"> + when the actual value is no more than + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={1}> + <EuiToolTip content="Absolute threshold value"> + <Field + name={`suppressionRules.${index}.absoluteThreshold`} + validate={validatePositiveDecimal} + > + {({ field }: FieldProps) => ( + <EuiCompressedFieldNumber + placeholder="Absolute" + {...field} + value={field.value || ''} + /> + )} + </Field> + </EuiToolTip> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiText size="s">or</EuiText> + </EuiFlexItem> + <EuiFlexItem grow={1}> + <EuiToolTip content="Relative threshold value as a percentage"> + <Field + name={`suppressionRules.${index}.relativeThreshold`} + validate={validatePositiveDecimal} + > + {({ field }: FieldProps) => ( + <div + style={{ + display: 'flex', + alignItems: 'center', + }} + > + <EuiCompressedFieldNumber + placeholder="Relative" + {...field} + value={field.value || ''} + /> + <EuiText size="s">%</EuiText> + </div> + )} + </Field> + </EuiToolTip> + </EuiFlexItem> + <EuiFlexItem grow={1}> + <EuiToolTip content="Select above or below expected value"> + <Field + name={`suppressionRules.${index}.aboveBelow`} + > + {({ field }: FieldProps) => ( + <EuiCompressedSelect + options={aboveBelowOptions} + {...field} + /> + )} + </Field> + </EuiToolTip> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiText size="s"> + the expected value. + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonIcon + iconType="trash" + color="danger" + aria-label="Delete rule" + onClick={() => + arrayHelpers.remove(index) + } + /> + </EuiFlexItem> + </EuiFlexGroup> + ) + )} + </> + </FormattedFormRow> + </EuiFlexItem> + </EuiFlexGroup> + </> + )} + </Field> + <EuiSpacer size="s" /> + <EuiButtonIcon + iconType="plusInCircle" + onClick={() => + arrayHelpers.push({ + fieldName: '', + absoluteThreshold: null, // Set to null to allow empty inputs + relativeThreshold: null, // Set to null to allow empty inputs + aboveBelow: 'above', + }) + } + aria-label="Add rule" + /> + </> + )} + </FieldArray> </> ) : null} </ContentPanel> diff --git a/public/pages/ConfigureModel/components/AdvancedSettings/__tests__/AdvancedSettings.test.tsx b/public/pages/ConfigureModel/components/AdvancedSettings/__tests__/AdvancedSettings.test.tsx new file mode 100644 index 00000000..0d4853ff --- /dev/null +++ b/public/pages/ConfigureModel/components/AdvancedSettings/__tests__/AdvancedSettings.test.tsx @@ -0,0 +1,74 @@ +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 { AdvancedSettings } from '../AdvancedSettings'; // Adjust the path as necessary + +describe('AdvancedSettings Component', () => { + test('displays error when -1 is entered in suppression rules absolute threshold', async () => { + render( + <Formik + initialValues={{ + suppressionRules: [{ featureName: '', absoluteThreshold: '', relativeThreshold: '', aboveBelow: 'above' }], + }} + onSubmit={jest.fn()} + > + {() => <AdvancedSettings />} + </Formik> + ); + + // Open the advanced settings + userEvent.click(screen.getByText('Show')); + + screen.logTestingPlaygroundURL(); + + // Click to add a new suppression rule + const addButton = screen.getByRole('button', { name: /add rule/i }); + fireEvent.click(addButton); + + // Find the absolute threshold input and type -1 + const absoluteThresholdInput = screen.getAllByPlaceholderText('Absolute')[0]; // Select the first absolute threshold input + userEvent.type(absoluteThresholdInput, '-1'); + + // Trigger validation + fireEvent.blur(absoluteThresholdInput); + + // 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( + <Formik + initialValues={{ + suppressionRules: [{ featureName: '', absoluteThreshold: '', relativeThreshold: '', aboveBelow: 'above' }], + }} + onSubmit={jest.fn()} + > + {() => <AdvancedSettings />} + </Formik> + ); + + // Open the advanced settings + userEvent.click(screen.getByText('Show')); + + screen.logTestingPlaygroundURL(); + + // Click to add a new suppression rule + const addButton = screen.getByRole('button', { name: /add rule/i }); + fireEvent.click(addButton); + + // Find the relative threshold input and type -1 + const relativeThresholdInput = screen.getAllByPlaceholderText('Relative')[0]; // Select the first absolute threshold input + userEvent.type(relativeThresholdInput, '-1'); + + // Trigger validation + fireEvent.blur(relativeThresholdInput); + + // 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/containers/ConfigureModel.tsx b/public/pages/ConfigureModel/containers/ConfigureModel.tsx index 8b31ae40..cfd18338 100644 --- a/public/pages/ConfigureModel/containers/ConfigureModel.tsx +++ b/public/pages/ConfigureModel/containers/ConfigureModel.tsx @@ -21,7 +21,6 @@ import { EuiSpacer, EuiText, EuiLink, - EuiIcon, } from '@elastic/eui'; import { FormikProps, Formik } from 'formik'; import { get, isEmpty } from 'lodash'; @@ -41,6 +40,7 @@ import { focusOnCategoryField, modelConfigurationToFormik, focusOnImputationOption, + focusOnSuppressionRules, } from '../utils/helpers'; import { formikToDetector } from '../../ReviewAndCreate/utils/helpers'; import { formikToModelConfiguration } from '../utils/helpers'; @@ -53,7 +53,11 @@ import { CoreServicesContext } from '../../../components/CoreServices/CoreServic import { Detector } from '../../../models/interfaces'; import { prettifyErrorMessage } from '../../../../server/utils/helpers'; import { DetectorDefinitionFormikValues } from '../../DefineDetector/models/interfaces'; -import { ModelConfigurationFormikValues, FeaturesFormikValues } from '../models/interfaces'; +import { + ModelConfigurationFormikValues, + FeaturesFormikValues, + RuleFormikValues +} from '../models/interfaces'; import { CreateDetectorFormikValues } from '../../CreateDetectorSteps/models/interfaces'; import { DETECTOR_STATE } from '../../../../server/utils/constants'; import { getErrorMessage } from '../../../utils/utils'; @@ -217,6 +221,35 @@ export function ConfigureModel(props: ConfigureModelProps) { } }; + const validateRules = ( + formikValues: ModelConfigurationFormikValues, + errors: any + ) => { + const rules = formikValues.suppressionRules || []; + + // Initialize an array to hold individual error messages + const featureNameErrors: string[] = []; + + // List of enabled features + const enabledFeatures = formikValues.featureList + .filter((feature: FeaturesFormikValues) => feature.featureEnabled) + .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.` + ); + } + }); + + // If there are any custom value errors, join them into a single string with proper formatting + if (featureNameErrors.length > 0) { + errors.suppressionRules = featureNameErrors.join(' '); + } + }; + const handleFormValidation = async ( formikProps: FormikProps<ModelConfigurationFormikValues> ) => { @@ -230,10 +263,12 @@ export function ConfigureModel(props: ConfigureModelProps) { formikProps.setFieldTouched('categoryField', isHCDetector); formikProps.setFieldTouched('shingleSize'); formikProps.setFieldTouched('imputationOption'); + formikProps.setFieldTouched('suppressionRules'); formikProps.validateForm().then((errors) => { // Call the extracted validation method validateImputationOption(formikProps.values, errors); + validateRules(formikProps.values, errors); if (isEmpty(errors)) { if (props.isEdit) { @@ -262,6 +297,15 @@ export function ConfigureModel(props: ConfigureModelProps) { return; } + const ruleValueError = get(errors, 'suppressionRules') + if (ruleValueError) { + core.notifications.toasts.addDanger( + ruleValueError + ); + focusOnSuppressionRules(); + return; + } + // TODO: can add focus to all components or possibly customize error message too if (get(errors, 'featureList')) { focusOnFirstWrongFeature(errors, formikProps.setFieldTouched); diff --git a/public/pages/ConfigureModel/models/interfaces.ts b/public/pages/ConfigureModel/models/interfaces.ts index f7575bc4..9bf2a496 100644 --- a/public/pages/ConfigureModel/models/interfaces.ts +++ b/public/pages/ConfigureModel/models/interfaces.ts @@ -19,6 +19,7 @@ export interface ModelConfigurationFormikValues { categoryField: string[]; shingleSize: number; imputationOption?: ImputationFormikValues; + suppressionRules?: RuleFormikValues[]; } export interface FeaturesFormikValues { @@ -41,3 +42,10 @@ export interface CustomValueFormikValues { featureName: string; data: number; } + +export interface RuleFormikValues { + featureName: string; + absoluteThreshold?: number; + relativeThreshold?: number; + aboveBelow: string; +} diff --git a/public/pages/ConfigureModel/utils/__tests__/helpers.test.tsx b/public/pages/ConfigureModel/utils/__tests__/helpers.test.tsx index 49b19750..0f8798c0 100644 --- a/public/pages/ConfigureModel/utils/__tests__/helpers.test.tsx +++ b/public/pages/ConfigureModel/utils/__tests__/helpers.test.tsx @@ -13,7 +13,7 @@ import { getRandomDetector } from '../../../../redux/reducers/__tests__/utils'; import { prepareDetector } from '../../utils/helpers'; import { FEATURE_TYPE } from '../../../../models/interfaces'; import { FeaturesFormikValues } from '../../models/interfaces'; -import { modelConfigurationToFormik } from '../helpers'; +import { modelConfigurationToFormik, rulesToFormik } from '../helpers'; import { SparseDataOptionValue } from '../constants'; import { ImputationMethod } from '../../../../models/types'; @@ -127,4 +127,21 @@ describe('featuresToFormik', () => { ); } }); + test('should return correct rules', () => { + const randomDetector = getRandomDetector(); // Generate a random detector object for testing + const adFormikValues = modelConfigurationToFormik(randomDetector); // Convert detector to Formik values + + const rules = randomDetector.rules; // Get the rules from the detector + + if (rules) { + // If rules exist, convert them to formik format using rulesToFormik + const expectedFormikRules = rulesToFormik(rules); // Convert rules to Formik-compatible format + + // Compare the converted rules with the suppressionRules in Formik values + expect(adFormikValues.suppressionRules).toEqual(expectedFormikRules); + } else { + // If no rules exist, suppressionRules should be undefined + expect(adFormikValues.suppressionRules).toEqual([]); + } + }); }); diff --git a/public/pages/ConfigureModel/utils/helpers.ts b/public/pages/ConfigureModel/utils/helpers.ts index 73e4ce78..c88c135e 100644 --- a/public/pages/ConfigureModel/utils/helpers.ts +++ b/public/pages/ConfigureModel/utils/helpers.ts @@ -23,6 +23,7 @@ import { FeaturesFormikValues, CustomValueFormikValues, ImputationFormikValues, + RuleFormikValues, } from '../../ConfigureModel/models/interfaces'; import { INITIAL_MODEL_CONFIGURATION_VALUES } from '../../ConfigureModel/utils/constants'; import { @@ -32,6 +33,11 @@ import { import { ImputationMethod, ImputationOption, + Condition, + Rule, + ThresholdType, + Operator, + Action, } from '../../../models/types'; import { SparseDataOptionValue @@ -218,6 +224,11 @@ export const focusOnImputationOption = () => { component?.focus(); }; +export const focusOnSuppressionRules = () => { + const component = document.getElementById('suppressionRules'); + component?.focus(); +}; + export const getShingleSizeFromObject = (obj: object) => { return get(obj, 'shingleSize', DEFAULT_SHINGLE_SIZE); }; @@ -269,6 +280,7 @@ export function modelConfigurationToFormik( categoryField: get(detector, 'categoryField', []), shingleSize: get(detector, 'shingleSize', DEFAULT_SHINGLE_SIZE), imputationOption: imputationFormikValues, + suppressionRules: rulesToFormik(detector.rules), }; } @@ -317,6 +329,7 @@ export function formikToModelConfiguration( ? values.categoryField : undefined, imputationOption: formikToImputationOption(values.imputationOption), + rules: formikToRules(values.suppressionRules), } as Detector; return detectorBody; @@ -425,3 +438,138 @@ export const getCustomValueStrArray = (imputationMethodStr : string, detector: D } return [] } + +export const getSuppressionRulesArray = (detector: Detector): string[] => { + if (!detector.rules || detector.rules.length === 0) { + return []; // Return an empty array if there are no rules + } + + return detector.rules.flatMap((rule) => { + // Convert each condition to a readable string + return rule.conditions.map((condition) => { + const featureName = condition.featureName; + const thresholdType = condition.thresholdType; + 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 => { + if (!formikValues || formikValues.length === 0) { + return undefined; // Return undefined for undefined or empty input + } + + return formikValues.map((formikValue) => { + const conditions: Condition[] = []; + + // 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; + } + }; + + // 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 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, + }); + } + + return { + action: Action.IGNORE_ANOMALY, + conditions, + }; + }); +}; + +// Convert Rule[] to RuleFormikValues[] +export const rulesToFormik = (rules?: Rule[]): RuleFormikValues[] => { + if (!rules || rules.length === 0) { + return []; // Return empty array for undefined or empty input + } + + return rules.map((rule) => { + // Start with default values + 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; + + // Determine the value and type of threshold + switch (condition.thresholdType) { + case ThresholdType.ACTUAL_OVER_EXPECTED_MARGIN: + formikValue.absoluteThreshold = condition.value; + formikValue.aboveBelow = 'above'; + break; + case ThresholdType.EXPECTED_OVER_ACTUAL_MARGIN: + formikValue.absoluteThreshold = condition.value; + formikValue.aboveBelow = 'below'; + break; + case ThresholdType.ACTUAL_OVER_EXPECTED_RATIO: + // *100 to convert to percentage + formikValue.relativeThreshold = condition.value * 100; + formikValue.aboveBelow = 'above'; + break; + case ThresholdType.EXPECTED_OVER_ACTUAL_RATIO: + // *100 to convert to percentage + formikValue.relativeThreshold = condition.value * 100; + formikValue.aboveBelow = 'below'; + break; + default: + break; + } + }); + + return formikValue; + }); +}; + + diff --git a/public/pages/DefineDetector/utils/constants.tsx b/public/pages/DefineDetector/utils/constants.tsx index cb64ce1d..d6ef4dff 100644 --- a/public/pages/DefineDetector/utils/constants.tsx +++ b/public/pages/DefineDetector/utils/constants.tsx @@ -48,6 +48,4 @@ export const INITIAL_DETECTOR_DEFINITION_VALUES: DetectorDefinitionFormikValues resultIndexMinSize: 51200, resultIndexTtl: 60, flattenCustomResultIndex: false, - imputationMethod: undefined, - customImputationValue: undefined }; diff --git a/public/pages/DetectorConfig/components/AdditionalSettings/AdditionalSettings.tsx b/public/pages/DetectorConfig/components/AdditionalSettings/AdditionalSettings.tsx index 18a322d5..ece8466c 100644 --- a/public/pages/DetectorConfig/components/AdditionalSettings/AdditionalSettings.tsx +++ b/public/pages/DetectorConfig/components/AdditionalSettings/AdditionalSettings.tsx @@ -9,27 +9,58 @@ * GitHub history for details. */ -import React from 'react'; +import React, { useState } from 'react'; import { get, isEmpty } from 'lodash'; -import { EuiBasicTable } from '@elastic/eui'; +import { + EuiBasicTable, + EuiButtonEmpty, + EuiOverlayMask +} from '@elastic/eui'; import ContentPanel from '../../../../components/ContentPanel/ContentPanel'; import { convertToCategoryFieldString } from '../../../utils/anomalyResultUtils'; +import { SuppressionRulesModal } from '../../../ReviewAndCreate/components/AdditionalSettings/SuppressionRulesModal'; interface AdditionalSettingsProps { shingleSize: number; categoryField: string[]; imputationMethod: string; customValues: string[]; + suppressionRules: string[]; } export function AdditionalSettings(props: AdditionalSettingsProps) { const renderCustomValues = (customValues: string[]) => ( <div> - {customValues.map((value, index) => ( - <p key={index}>{value}</p> - ))} + {customValues.length > 0 ? ( + customValues.map((value, index) => <p key={index}>{value}</p>) + ) : ( + <p>-</p> + )} </div> ); + + const [isModalVisible, setIsModalVisible] = useState(false); + const [modalContent, setModalContent] = useState<string[]>([]); + + const closeModal = () => setIsModalVisible(false); + + const showRulesInModal = (rules: string[]) => { + setModalContent(rules); + setIsModalVisible(true); + }; + + const renderSuppressionRules = (suppressionRules: string[]) => ( + <div> + {suppressionRules.length > 0 ? ( + <EuiButtonEmpty size="s" onClick={() => showRulesInModal(suppressionRules)}> + {suppressionRules.length} rules + </EuiButtonEmpty> + ) : ( + <p>-</p> + )} + </div> + ); + const tableItems = [ { categoryField: isEmpty(get(props, 'categoryField', [])) @@ -38,6 +69,7 @@ export function AdditionalSettings(props: AdditionalSettingsProps) { shingleSize: props.shingleSize, imputationMethod: props.imputationMethod, customValues: props.customValues, + suppresionRules: props.suppressionRules, }, ]; const tableColumns = [ @@ -48,6 +80,10 @@ export function AdditionalSettings(props: AdditionalSettingsProps) { field: 'customValues', render: (customValues: string[]) => renderCustomValues(customValues), // Use a custom render function }, + { name: 'Suppression rules', + field: 'suppresionRules', + render: (suppresionRules: string[]) => renderSuppressionRules(suppresionRules), // Use a custom render function + }, ]; return ( <ContentPanel title="Additional settings" titleSize="s"> @@ -56,6 +92,11 @@ export function AdditionalSettings(props: AdditionalSettingsProps) { items={tableItems} columns={tableColumns} /> + {isModalVisible && ( + <EuiOverlayMask> + <SuppressionRulesModal onClose={closeModal} rules={modalContent} /> + </EuiOverlayMask> + )} </ContentPanel> ); } diff --git a/public/pages/DetectorConfig/containers/Features.tsx b/public/pages/DetectorConfig/containers/Features.tsx index 2c5c6f8b..10897431 100644 --- a/public/pages/DetectorConfig/containers/Features.tsx +++ b/public/pages/DetectorConfig/containers/Features.tsx @@ -14,7 +14,6 @@ import { EuiBasicTable, EuiText, EuiLink, - EuiIcon, EuiSmallButton, EuiEmptyPrompt, EuiSpacer, @@ -34,6 +33,7 @@ import { getShingleSizeFromObject, imputationMethodToFormik, getCustomValueStrArray, + getSuppressionRulesArray, } from '../../ConfigureModel/utils/helpers'; interface FeaturesProps { @@ -256,6 +256,7 @@ export const Features = (props: FeaturesProps) => { imputationMethodStr, props.detector )} + suppressionRules={getSuppressionRulesArray(props.detector)} /> </div> )} diff --git a/public/pages/DetectorConfig/containers/__tests__/DetectorConfig.test.tsx b/public/pages/DetectorConfig/containers/__tests__/DetectorConfig.test.tsx index b57721ff..e1a2cada 100644 --- a/public/pages/DetectorConfig/containers/__tests__/DetectorConfig.test.tsx +++ b/public/pages/DetectorConfig/containers/__tests__/DetectorConfig.test.tsx @@ -28,17 +28,24 @@ import { UiFeature, FeatureAttributes, OPERATORS_MAP, + UNITS, } from '../../../../models/interfaces'; import { getRandomDetector, - randomFixedValue, + getUIMetadata, } from '../../../../redux/reducers/__tests__/utils'; import { coreServicesMock } from '../../../../../test/mocks'; import { toStringConfigCell } from '../../../ReviewAndCreate/utils/helpers'; import { DATA_TYPES } from '../../../../utils/constants'; import { mockedStore, initialState } from '../../../../redux/utils/testUtils'; import { CoreServicesContext } from '../../../../components/CoreServices/CoreServices'; -import { ImputationMethod } from '../../../../models/types'; +import { + ImputationMethod, + Action, + ThresholdType, + Operator, +} from '../../../../models/types'; +import { DETECTOR_STATE } from '../../../../../server/utils/constants'; const renderWithRouter = (detector: Detector) => ({ ...render( @@ -143,6 +150,25 @@ describe('<DetectorConfig /> spec', () => { const randomDetector = { ...getRandomDetector(false), imputationOption: { method: ImputationMethod.PREVIOUS }, + rules: [ + { + action: Action.IGNORE_ANOMALY, + conditions: [ + { + featureName: 'value', // Matches a feature in featureAttributes + thresholdType: ThresholdType.ACTUAL_OVER_EXPECTED_MARGIN, + operator: Operator.LTE, + value: 5, + }, + { + featureName: 'value2', // Matches another feature in featureAttributes + thresholdType: ThresholdType.EXPECTED_OVER_ACTUAL_RATIO, + operator: Operator.LTE, + value: 10, + }, + ], + }, + ], }; const { container } = renderWithRouter(randomDetector); expect(container.firstChild).toMatchSnapshot(); @@ -362,8 +388,88 @@ describe('<DetectorConfig /> spec', () => { }, } as UiMetaData, imputationOption: imputationOption, + rules: [ + { + action: Action.IGNORE_ANOMALY, + conditions: [ + { + featureName: 'value', // Matches a feature in featureAttributes + thresholdType: ThresholdType.ACTUAL_OVER_EXPECTED_MARGIN, + operator: Operator.LTE, + value: 5, + }, + { + featureName: 'value2', // Matches another feature in featureAttributes + thresholdType: ThresholdType.EXPECTED_OVER_ACTUAL_RATIO, + operator: Operator.LTE, + value: 10, + }, + ], + }, + ], }; const { container } = renderWithRouter(randomDetector); expect(container.firstChild).toMatchSnapshot(); }); + test('renders rules', () => { + // Define example features + const features = [ + { + featureName: 'value', + featureEnabled: true, + aggregationQuery: featureQuery1, + }, + { + featureName: 'value2', + featureEnabled: true, + aggregationQuery: featureQuery2, + }, + { + featureName: 'value', + featureEnabled: false, + }, + ] as FeatureAttributes[]; + + // Updated example detector + const testDetector: Detector = { + primaryTerm: 1, + seqNo: 1, + id: 'detector-1', + name: 'Sample Detector', + description: 'A sample detector for testing', + timeField: 'timestamp', + indices: ['index1'], + filterQuery: {}, + featureAttributes: features, // Using the provided features + windowDelay: { period: { interval: 1, unit: UNITS.MINUTES } }, + detectionInterval: { period: { interval: 1, unit: UNITS.MINUTES } }, + shingleSize: 8, + lastUpdateTime: 1586823218000, + curState: DETECTOR_STATE.RUNNING, + stateError: '', + uiMetadata: getUIMetadata(features), + rules: [ + { + action: Action.IGNORE_ANOMALY, + conditions: [ + { + featureName: 'value', // Matches a feature in featureAttributes + thresholdType: ThresholdType.ACTUAL_OVER_EXPECTED_MARGIN, + operator: Operator.LTE, + value: 5, + }, + { + featureName: 'value2', // Matches another feature in featureAttributes + thresholdType: ThresholdType.EXPECTED_OVER_ACTUAL_RATIO, + operator: Operator.LTE, + value: 10, + }, + ], + }, + ], + }; + + const { container } = renderWithRouter(testDetector); + expect(container.firstChild).toMatchSnapshot(); + }); }); diff --git a/public/pages/DetectorConfig/containers/__tests__/__snapshots__/DetectorConfig.test.tsx.snap b/public/pages/DetectorConfig/containers/__tests__/__snapshots__/DetectorConfig.test.tsx.snap index 46363942..23c72f8d 100644 --- a/public/pages/DetectorConfig/containers/__tests__/__snapshots__/DetectorConfig.test.tsx.snap +++ b/public/pages/DetectorConfig/containers/__tests__/__snapshots__/DetectorConfig.test.tsx.snap @@ -1,5 +1,1465 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`<DetectorConfig /> spec renders rules 1`] = ` +<div + class="euiPage euiPage--paddingMedium euiPage--grow" + style="margin-top: 16px; padding-top: 0px;" +> + <div + class="euiPageBody euiPageBody--borderRadiusNone" + > + <div + class="euiSpacer euiSpacer--l" + /> + <div + class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow" + style="padding: 20px;" + > + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + style="padding: 0px;" + > + <div + class="euiFlexItem" + > + <h3 + class="euiTitle euiTitle--small" + data-test-subj="detectorSettingsHeader" + > + Detector settings + </h3> + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem content-panel-subTitle" + style="line-height: normal; max-width: 75%;" + /> + </div> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + class="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem" + > + <button + class="euiButton euiButton--primary euiButton--small" + data-test-subj="editDetectorSettingsButton" + type="button" + > + <span + class="euiButtonContent euiButton__content" + > + <span + class="euiButton__text" + > + Edit + </span> + </span> + </button> + </div> + </div> + </div> + </div> + <div + class="euiSpacer euiSpacer--s" + /> + <div> + <hr + class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" + /> + <div + style="padding: 10px 0px;" + > + <div + class="euiFlexGrid euiFlexGrid--gutterLarge euiFlexGrid--wrap euiFlexGrid--responsive" + > + <div + class="euiFlexItem" + data-test-subj="detectorNameCell" + > + <div + class="euiFormRow euiFormRow--compressed" + id="random_html_id-row" + style="width: 250px;" + > + <div + class="euiFormRow__labelWrapper" + > + <label + class="euiFormLabel euiFormRow__label" + for="random_html_id" + > + Name + </label> + </div> + <div + class="euiFormRow__fieldWrapper" + > + <div + class="euiText euiText--medium" + id="random_html_id" + > + <p + class="enabled" + > + Sample Detector + </p> + </div> + </div> + </div> + </div> + <div + class="euiFlexItem" + data-test-subj="indexNameCell" + > + <div + class="euiFormRow euiFormRow--compressed" + id="random_html_id-row" + style="width: 250px;" + > + <div + class="euiFormRow__labelWrapper" + > + <label + class="euiFormLabel euiFormRow__label" + for="random_html_id" + > + Data source index + </label> + </div> + <div + class="euiFormRow__fieldWrapper" + > + <div + class="euiText euiText--medium" + id="random_html_id" + > + <p + class="enabled" + > + index1 + </p> + </div> + </div> + </div> + </div> + <div + class="euiFlexItem" + > + <div + class="euiFormRow euiFormRow--compressed" + id="random_html_id-row" + style="width: 250px;" + > + <div + class="euiFormRow__labelWrapper" + > + <label + class="euiFormLabel euiFormRow__label" + for="random_html_id" + > + Data filter + </label> + </div> + <div + class="euiFormRow__fieldWrapper" + > + <div> + <div + class="euiText euiText--medium" + > + <p + class="enabled" + > + Custom expression: + + <button + class="euiLink euiLink--primary" + data-test-subj="viewFilter" + type="button" + > + View code + </button> + </p> + </div> + </div> + </div> + </div> + </div> + <div + class="euiFlexItem" + > + <div + class="euiFormRow euiFormRow--compressed" + id="random_html_id-row" + style="width: 250px;" + > + <div + class="euiFormRow__labelWrapper" + > + <label + class="euiFormLabel euiFormRow__label" + for="random_html_id" + > + Detector interval + </label> + </div> + <div + class="euiFormRow__fieldWrapper" + > + <div + class="euiText euiText--medium" + id="random_html_id" + > + <p + class="enabled" + > + 1 Minutes + </p> + </div> + </div> + </div> + </div> + <div + class="euiFlexItem" + data-test-subj="detectorIdCell" + > + <div + class="euiFormRow euiFormRow--compressed" + id="random_html_id-row" + style="width: 250px;" + > + <div + class="euiFormRow__labelWrapper" + > + <label + class="euiFormLabel euiFormRow__label" + for="random_html_id" + > + ID + </label> + </div> + <div + class="euiFormRow__fieldWrapper" + > + <div + class="euiText euiText--medium" + id="random_html_id" + > + <p + class="enabled" + > + detector-1 + </p> + </div> + </div> + </div> + </div> + <div + class="euiFlexItem" + data-test-subj="detectorDescriptionCell" + > + <div + class="euiFormRow euiFormRow--compressed" + id="random_html_id-row" + style="width: 250px;" + > + <div + class="euiFormRow__labelWrapper" + > + <label + class="euiFormLabel euiFormRow__label" + for="random_html_id" + > + Description + </label> + </div> + <div + class="euiFormRow__fieldWrapper" + > + <div + class="euiText euiText--medium" + id="random_html_id" + > + <p + class="enabled" + > + A sample detector for testing + </p> + </div> + </div> + </div> + </div> + <div + class="euiFlexItem" + data-test-subj="timestampNameCell" + > + <div + class="euiFormRow euiFormRow--compressed" + id="random_html_id-row" + style="width: 250px;" + > + <div + class="euiFormRow__labelWrapper" + > + <label + class="euiFormLabel euiFormRow__label" + for="random_html_id" + > + Timestamp + </label> + </div> + <div + class="euiFormRow__fieldWrapper" + > + <div + class="euiText euiText--medium" + id="random_html_id" + > + <p + class="enabled" + > + timestamp + </p> + </div> + </div> + </div> + </div> + <div + class="euiFlexItem" + > + <div + class="euiFormRow euiFormRow--compressed" + id="random_html_id-row" + style="width: 250px;" + > + <div + class="euiFormRow__labelWrapper" + > + <label + class="euiFormLabel euiFormRow__label" + for="random_html_id" + > + Last Updated + </label> + </div> + <div + class="euiFormRow__fieldWrapper" + > + <div + class="euiText euiText--medium" + id="random_html_id" + > + <p + class="enabled" + > + 04/14/20 12:13 AM + </p> + </div> + </div> + </div> + </div> + <div + class="euiFlexItem" + > + <div + class="euiFormRow euiFormRow--compressed" + id="random_html_id-row" + style="width: 250px;" + > + <div + class="euiFormRow__labelWrapper" + > + <label + class="euiFormLabel euiFormRow__label" + for="random_html_id" + > + Window delay + </label> + </div> + <div + class="euiFormRow__fieldWrapper" + > + <div + class="euiText euiText--medium" + id="random_html_id" + > + <p + class="enabled" + > + 1 Minutes + </p> + </div> + </div> + </div> + </div> + <div + class="euiFlexItem" + > + <div + class="euiFormRow euiFormRow--compressed" + id="random_html_id-row" + style="width: 250px;" + > + <div + class="euiFormRow__labelWrapper" + > + <label + class="euiFormLabel euiFormRow__label" + for="random_html_id" + > + Custom result index + </label> + </div> + <div + class="euiFormRow__fieldWrapper" + > + <div + class="euiText euiText--medium" + id="random_html_id" + > + <p + class="enabled" + > + - + </p> + </div> + </div> + </div> + </div> + <div + class="euiFlexItem" + > + <div + class="euiFormRow euiFormRow--compressed" + id="random_html_id-row" + style="width: 250px;" + > + <div + class="euiFormRow__labelWrapper" + > + <label + class="euiFormLabel euiFormRow__label" + for="random_html_id" + > + Flatten custom result index + </label> + </div> + <div + class="euiFormRow__fieldWrapper" + > + <div + class="euiText euiText--medium" + id="random_html_id" + > + <p + class="enabled" + > + - + </p> + </div> + </div> + </div> + </div> + <div + class="euiFlexItem" + > + <div + class="euiFormRow euiFormRow--compressed" + id="random_html_id-row" + style="width: 250px;" + > + <div + class="euiFormRow__labelWrapper" + > + <label + class="euiFormLabel euiFormRow__label" + for="random_html_id" + > + Custom result index min age + </label> + </div> + <div + class="euiFormRow__fieldWrapper" + > + <div + class="euiText euiText--medium" + id="random_html_id" + > + <p + class="enabled" + > + - + </p> + </div> + </div> + </div> + </div> + <div + class="euiFlexItem" + > + <div + class="euiFormRow euiFormRow--compressed" + id="random_html_id-row" + style="width: 250px;" + > + <div + class="euiFormRow__labelWrapper" + > + <label + class="euiFormLabel euiFormRow__label" + for="random_html_id" + > + Custom result index min size + </label> + </div> + <div + class="euiFormRow__fieldWrapper" + > + <div + class="euiText euiText--medium" + id="random_html_id" + > + <p + class="enabled" + > + - + </p> + </div> + </div> + </div> + </div> + <div + class="euiFlexItem" + > + <div + class="euiFormRow euiFormRow--compressed" + id="random_html_id-row" + style="width: 250px;" + > + <div + class="euiFormRow__labelWrapper" + > + <label + class="euiFormLabel euiFormRow__label" + for="random_html_id" + > + Custom result index TTL + </label> + </div> + <div + class="euiFormRow__fieldWrapper" + > + <div + class="euiText euiText--medium" + id="random_html_id" + > + <p + class="enabled" + > + - + </p> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + <div + class="euiSpacer euiSpacer--l" + /> + <div + class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow" + style="padding: 20px;" + > + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + style="padding: 0px;" + > + <div + class="euiFlexItem" + > + <h3 + class="euiTitle euiTitle--small" + data-test-subj="modelConfigurationHeader" + > + Model configuration + </h3> + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem content-panel-subTitle" + style="line-height: normal; max-width: 75%;" + /> + </div> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + class="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem" + > + <button + class="euiButton euiButton--primary euiButton--small" + data-test-subj="editModelConfigurationButton" + type="button" + > + <span + class="euiButtonContent euiButton__content" + > + <span + class="euiButton__text" + > + Edit + </span> + </span> + </button> + </div> + </div> + </div> + </div> + <div + class="euiSpacer euiSpacer--s" + /> + <div> + <hr + class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" + /> + <div + style="padding: 10px 0px;" + > + <div> + <div + class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow" + style="padding: 20px;" + > + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + style="padding: 0px;" + > + <div + class="euiFlexItem" + > + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive" + data-test-subj="contentPanelTitle" + > + <div + class="euiFlexItem" + > + <h3 + class="euiTitle euiTitle--small" + style="display: flex; flex-direction: row;" + > + <p> + Features + + </p> + <p + style="color: rgb(83, 89, 102);" + > + (3) + </p> + </h3> + </div> + </div> + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem content-panel-subTitle" + style="line-height: normal; max-width: 75%;" + /> + </div> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + class="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem" + /> + </div> + </div> + </div> + <div> + <hr + class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" + /> + <div + style="padding: 10px 0px;" + > + <div + class="euiBasicTable" + data-test-subj="featureTable" + > + <div> + <div + class="euiTableHeaderMobile" + > + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + class="euiTableSortMobile" + > + <div + class="euiPopover euiPopover--anchorDownRight" + > + <div + class="euiPopover__anchor" + > + <button + class="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall euiButtonEmpty--flushRight" + type="button" + > + <span + class="euiButtonContent euiButtonContent--iconRight euiButtonEmpty__content" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--small euiIcon--inherit euiButtonContent__icon" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M13.069 5.157 8.384 9.768a.546.546 0 0 1-.768 0L2.93 5.158a.552.552 0 0 0-.771 0 .53.53 0 0 0 0 .759l4.684 4.61c.641.631 1.672.63 2.312 0l4.684-4.61a.53.53 0 0 0 0-.76.552.552 0 0 0-.771 0Z" + fill-rule="non-zero" + /> + </svg> + <span + class="euiButtonEmpty__text" + > + Sorting + </span> + </span> + </button> + </div> + </div> + </div> + </div> + </div> + </div> + <table + class="euiTable euiTable--responsive" + id="random_html_id" + tabindex="-1" + > + <caption + class="euiScreenReaderOnly euiTableCaption" + /> + <thead> + <tr> + <th + aria-live="polite" + aria-sort="ascending" + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_name_0" + role="columnheader" + scope="col" + > + <button + class="euiTableHeaderButton euiTableHeaderButton-isSorted" + data-test-subj="tableHeaderSortButton" + type="button" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Feature name + </span> + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiTableSortIcon" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M8 4.207v8.237c0 .307-.224.556-.5.556s-.5-.249-.5-.556V4.207L2.904 8.303a.5.5 0 0 1-.707-.707l4.242-4.242a1.5 1.5 0 0 1 2.122 0l4.242 4.242a.5.5 0 1 1-.707.707L8 4.207Z" + /> + </svg> + </span> + </button> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_definition_1" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Feature definition + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_state_2" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Feature state + </span> + </span> + </th> + </tr> + </thead> + <tbody> + <tr + class="euiTableRow" + > + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Feature name + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + value + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Feature definition + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + <div> + <p> + Field: + value + </p> + <p> + Aggregation method: + min + </p> + </div> + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Feature state + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Enabled + </span> + </div> + </td> + </tr> + <tr + class="euiTableRow" + > + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Feature name + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + value + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Feature definition + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + <div> + <p> + Field: + value + </p> + <p> + Aggregation method: + min + </p> + </div> + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Feature state + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Disabled + </span> + </div> + </td> + </tr> + <tr + class="euiTableRow" + > + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Feature name + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + value2 + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Feature definition + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + <div> + <p> + Field: + value2 + </p> + <p> + Aggregation method: + avg + </p> + </div> + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Feature state + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Enabled + </span> + </div> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + </div> + </div> + <div + class="euiSpacer euiSpacer--m" + /> + <div + class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow" + style="padding: 20px;" + > + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + style="padding: 0px;" + > + <div + class="euiFlexItem" + > + <h3 + class="euiTitle euiTitle--small" + data-test-subj="contentPanelTitle" + > + Additional settings + </h3> + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem content-panel-subTitle" + style="line-height: normal; max-width: 75%;" + /> + </div> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + class="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem" + /> + </div> + </div> + </div> + <div> + <hr + class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" + /> + <div + style="padding: 10px 0px;" + > + <div + class="euiBasicTable header-single-value-euiBasicTable" + > + <div> + <div + class="euiTableHeaderMobile" + > + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + </div> + </div> + <table + class="euiTable euiTable--responsive" + id="random_html_id" + tabindex="-1" + > + <caption + class="euiScreenReaderOnly euiTableCaption" + /> + <thead> + <tr> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_categoryField_0" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Categorical fields + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_shingleSize_1" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Shingle size + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_imputationMethod_2" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Imputation method + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_customValues_3" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Custom values + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_suppresionRules_4" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Suppression rules + </span> + </span> + </th> + </tr> + </thead> + <tbody> + <tr + class="euiTableRow" + > + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Categorical fields + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + - + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Shingle size + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + 8 + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Imputation method + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + ignore + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Custom values + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="" + > + <p> + - + </p> + </div> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Suppression rules + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="" + > + <button + class="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--small" + type="button" + > + <span + class="euiButtonContent euiButtonEmpty__content" + > + <span + class="euiButtonEmpty__text" + > + 2 + rules + </span> + </span> + </button> + </div> + </div> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + <div + class="euiSpacer euiSpacer--l" + /> + <div + class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow" + style="padding: 20px;" + > + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + style="padding: 0px;" + > + <div + class="euiFlexItem" + > + <h3 + class="euiTitle euiTitle--small" + data-test-subj="detectorJobsHeader" + > + Detector jobs + </h3> + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem content-panel-subTitle" + style="line-height: normal; max-width: 75%;" + /> + </div> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + class="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + /> + </div> + </div> + <div> + <hr + class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" + /> + <div + style="padding: 10px 0px;" + > + <div + class="euiFlexGrid euiFlexGrid--gutterSmall euiFlexGrid--halves euiFlexGrid--responsive" + > + <div + class="euiFlexItem" + > + <div + class="euiFormRow euiFormRow--compressed" + id="random_html_id-row" + style="width: 250px;" + > + <div + class="euiFormRow__labelWrapper" + > + <label + class="euiFormLabel euiFormRow__label" + for="random_html_id" + > + Real-time detector + </label> + </div> + <div + class="euiFormRow__fieldWrapper" + > + <div + class="euiText euiText--medium" + id="random_html_id" + > + <p + class="enabled" + > + <p + class="enabled" + > + + </p> + </p> + </div> + </div> + </div> + </div> + <div + class="euiFlexItem" + > + <div + class="euiFormRow euiFormRow--compressed" + id="random_html_id-row" + > + <div + class="euiFormRow__labelWrapper" + > + <label + class="euiFormLabel euiFormRow__label" + for="random_html_id" + > + Historical analysis detector + </label> + </div> + <div + class="euiFormRow__fieldWrapper" + > + <div + class="euiText euiText--medium" + id="random_html_id" + > + <p + class="enabledLongerWidth" + > + Disabled + </p> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> +</div> +`; + exports[`<DetectorConfig /> spec renders the component 1`] = ` <div class="euiPage euiPage--paddingMedium euiPage--grow" @@ -1131,6 +2591,23 @@ exports[`<DetectorConfig /> spec renders the component 1`] = ` </span> </span> </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_suppresionRules_4" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Suppression rules" + > + Suppression rules + </span> + </span> + </th> </tr> </thead> <tbody> @@ -1204,7 +2681,43 @@ exports[`<DetectorConfig /> spec renders the component 1`] = ` > <div class="" - /> + > + <p> + - + </p> + </div> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Suppression rules + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="" + > + <button + class="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--small" + type="button" + > + <span + class="euiButtonContent euiButtonEmpty__content" + > + <span + class="euiButtonEmpty__text" + > + 2 + rules + </span> + </span> + </button> + </div> </div> </td> </tr> @@ -2579,6 +4092,22 @@ exports[`<DetectorConfig /> spec renders the component with 2 custom and 1 simpl </span> </span> </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_suppresionRules_4" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Suppression rules + </span> + </span> + </th> </tr> </thead> <tbody> @@ -2662,6 +4191,38 @@ exports[`<DetectorConfig /> spec renders the component with 2 custom and 1 simpl </div> </div> </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Suppression rules + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="" + > + <button + class="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--small" + type="button" + > + <span + class="euiButtonContent euiButtonEmpty__content" + > + <span + class="euiButtonEmpty__text" + > + 2 + rules + </span> + </span> + </button> + </div> + </div> + </td> </tr> </tbody> </table> diff --git a/public/pages/ReviewAndCreate/components/AdditionalSettings/AdditionalSettings.tsx b/public/pages/ReviewAndCreate/components/AdditionalSettings/AdditionalSettings.tsx index eb7dcc5a..ac0c1905 100644 --- a/public/pages/ReviewAndCreate/components/AdditionalSettings/AdditionalSettings.tsx +++ b/public/pages/ReviewAndCreate/components/AdditionalSettings/AdditionalSettings.tsx @@ -9,31 +9,64 @@ * GitHub history for details. */ -import React from 'react'; +import React, { useState } from 'react'; import { get } from 'lodash'; -import { EuiBasicTable } from '@elastic/eui'; +import { + EuiBasicTable, + EuiButtonEmpty, + EuiOverlayMask +} from '@elastic/eui'; +import ContentPanel from '../../../../components/ContentPanel/ContentPanel'; +import { SuppressionRulesModal } from './SuppressionRulesModal'; interface AdditionalSettingsProps { shingleSize: number; categoryField: string[]; imputationMethod: string; customValues: string[]; + suppressionRules: string[]; } export function AdditionalSettings(props: AdditionalSettingsProps) { const renderCustomValues = (customValues: string[]) => ( <div> - {customValues.map((value, index) => ( - <p key={index}>{value}</p> - ))} + {customValues.length > 0 ? ( + customValues.map((value, index) => <p key={index}>{value}</p>) + ) : ( + <p>-</p> + )} </div> ); + + const [isModalVisible, setIsModalVisible] = useState(false); + const [modalContent, setModalContent] = useState<string[]>([]); + + const closeModal = () => setIsModalVisible(false); + + const showRulesInModal = (rules: string[]) => { + setModalContent(rules); + setIsModalVisible(true); + }; + + const renderSuppressionRules = (suppressionRules: string[]) => ( + <div> + {suppressionRules.length > 0 ? ( + <EuiButtonEmpty size="s" onClick={() => showRulesInModal(suppressionRules)}> + {suppressionRules.length} rules + </EuiButtonEmpty> + ) : ( + <p>-</p> + )} + </div> + ); + const tableItems = [ { categoryField: get(props, 'categoryField.0', '-'), shingleSize: props.shingleSize, imputationMethod: props.imputationMethod, customValues: props.customValues, + suppresionRules: props.suppressionRules, }, ]; const tableColumns = [ @@ -43,13 +76,24 @@ export function AdditionalSettings(props: AdditionalSettingsProps) { { name: 'Custom values', field: 'customValues', render: (customValues: string[]) => renderCustomValues(customValues), // Use a custom render function - }, + }, + { name: 'Suppression rules', + field: 'suppresionRules', + render: (suppressionRules: string[]) => renderSuppressionRules(suppressionRules), // Use a custom render function + }, ]; return ( + <ContentPanel title="Additional settings" titleSize="s"> <EuiBasicTable className="header-single-value-euiBasicTable" items={tableItems} columns={tableColumns} /> + {isModalVisible && ( + <EuiOverlayMask> + <SuppressionRulesModal onClose={closeModal} rules={modalContent} /> + </EuiOverlayMask> + )} + </ContentPanel> ); } diff --git a/public/pages/ReviewAndCreate/components/AdditionalSettings/SuppressionRulesModal.tsx b/public/pages/ReviewAndCreate/components/AdditionalSettings/SuppressionRulesModal.tsx new file mode 100644 index 00000000..22e34407 --- /dev/null +++ b/public/pages/ReviewAndCreate/components/AdditionalSettings/SuppressionRulesModal.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle, EuiText, EuiSpacer } from '@elastic/eui'; + +interface SuppressionRulesModalProps { + rules: string[]; + onClose: () => void; +} + +export const SuppressionRulesModal: React.FC<SuppressionRulesModalProps> = ({ rules, onClose }) => { + return ( + <EuiModal onClose={onClose} style={{ maxWidth: 600 }} role="dialog"> + <EuiModalHeader> + <EuiModalHeaderTitle> + <h2>Suppression Rules</h2> + </EuiModalHeaderTitle> + </EuiModalHeader> + + <EuiModalBody> + <EuiText size="s"> + {rules.map((rule, index) => ( + <EuiText key={index} size="s"> + {rule} + </EuiText> + ))} + </EuiText> + <EuiSpacer size="m" /> + </EuiModalBody> + </EuiModal> + ); +}; diff --git a/public/pages/ReviewAndCreate/components/ModelConfigurationFields/ModelConfigurationFields.tsx b/public/pages/ReviewAndCreate/components/ModelConfigurationFields/ModelConfigurationFields.tsx index 37c14555..fc92a6ce 100644 --- a/public/pages/ReviewAndCreate/components/ModelConfigurationFields/ModelConfigurationFields.tsx +++ b/public/pages/ReviewAndCreate/components/ModelConfigurationFields/ModelConfigurationFields.tsx @@ -35,6 +35,7 @@ import { getShingleSizeFromObject, imputationMethodToFormik, getCustomValueStrArray, + getSuppressionRulesArray, } from '../../../ConfigureModel/utils/helpers'; import { SORT_DIRECTION } from '../../../../../server/utils/constants'; @@ -324,6 +325,7 @@ export const ModelConfigurationFields = ( categoryField={get(props, 'detector.categoryField', [])} imputationMethod={imputationMethodStr} customValues={getCustomValueStrArray(imputationMethodStr, props.detector)} + suppressionRules={getSuppressionRulesArray(props.detector)} /> <EuiSpacer /> <ContentPanel diff --git a/public/pages/ReviewAndCreate/components/__tests__/AdditionalSettings.test.tsx b/public/pages/ReviewAndCreate/components/__tests__/AdditionalSettings.test.tsx index c8866829..d2de2682 100644 --- a/public/pages/ReviewAndCreate/components/__tests__/AdditionalSettings.test.tsx +++ b/public/pages/ReviewAndCreate/components/__tests__/AdditionalSettings.test.tsx @@ -10,18 +10,24 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; import { AdditionalSettings } from '../AdditionalSettings/AdditionalSettings'; import { Formik } from 'formik'; describe('<AdditionalSettings /> spec', () => { test('renders the component with high cardinality disabled', () => { - const { container, getByText, getAllByText } = render( + const { container, getByText, getAllByText, queryByRole } = render( <Formik initialValues={{ detectorName: '' }} onSubmit={jest.fn()}> {() => ( <div> - <AdditionalSettings categoryField={[]} shingleSize={8} imputationMethod="Ignore" customValues={[]}/> + <AdditionalSettings + categoryField={[]} + shingleSize={8} + imputationMethod="Ignore" + customValues={[]} + suppressionRules={[]} + /> </div> )} </Formik> @@ -29,33 +35,82 @@ describe('<AdditionalSettings /> spec', () => { expect(container.firstChild).toMatchSnapshot(); getAllByText('Category field'); getAllByText('Shingle size'); - getByText('-'); getByText('8'); - getByText("Ignore"); + getByText('Ignore'); + + // Assert that multiple elements with the text '-' are present + const dashElements = getAllByText('-'); + expect(dashElements.length).toBeGreaterThan(1); // Checks that more than one '-' is found + + // Check that the 'Suppression rules' title is present + // Assert that multiple elements with the text '-' are present + const ruleElements = getAllByText('Suppression rules'); + expect(ruleElements.length).toBeGreaterThan(1); // one is table cell title, another is the button + + // Use queryByRole to check that the button link is not present + const button = screen.queryByRole('button', { name: '0 rules' }); + expect(button).toBeNull(); }); - test('renders the component with high cardinality enabled', () => { - const { container, getByText, getAllByText } = render( - <Formik initialValues={{ detectorName: '' }} onSubmit={jest.fn()}> - {() => ( - <div> - <AdditionalSettings - categoryField={['test_field']} - shingleSize={8} - imputationMethod="Custom" - customValues={["denyMax:5", "denySum:10"]} - /> - </div> - )} - </Formik> - ); + test('renders the component with high cardinality enabled', async () => { + const { container, getByText, getAllByText, getByRole, queryByRole } = + render( + <Formik initialValues={{ detectorName: '' }} onSubmit={jest.fn()}> + {() => ( + <div> + <AdditionalSettings + categoryField={['test_field']} + shingleSize={8} + imputationMethod="Custom" + customValues={['denyMax:5', 'denySum:10']} + suppressionRules={[ + "Ignore anomalies for feature 'CPU Usage' with no more than 5 above expected value.", + "Ignore anomalies for feature 'Memory Usage' with no more than 10% below expected value.", + ]} + /> + </div> + )} + </Formik> + ); expect(container.firstChild).toMatchSnapshot(); getAllByText('Category field'); getAllByText('Shingle size'); getByText('test_field'); getByText('8'); - getByText("Custom"); + getByText('Custom'); // Check for the custom values getByText('denyMax:5'); getByText('denySum:10'); + + // Check for the suppression rules button link + const button = getByRole('button', { name: '2 rules' }); + expect(button).toBeInTheDocument(); + + // Click the button to open the modal + fireEvent.click(button); + + // Wait for the modal to appear and check for its content + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); // Ensure modal is opened + }); + + getByText('Suppression Rules'); // Modal header + getByText( + "Ignore anomalies for feature 'CPU Usage' with no more than 5 above expected value." + ); + getByText( + "Ignore anomalies for feature 'Memory Usage' with no more than 10% below expected value." + ); + + // Close the modal by clicking the close button (X) + // Close the modal by clicking the close button (X) + const closeButton = getByRole('button', { + name: 'Closes this modal window', + }); + fireEvent.click(closeButton); + + // Ensure the modal is closed + await waitFor(() => { + expect(queryByRole('dialog')).toBeNull(); + }); }); }); diff --git a/public/pages/ReviewAndCreate/components/__tests__/ModelConfigurationFields.test.tsx b/public/pages/ReviewAndCreate/components/__tests__/ModelConfigurationFields.test.tsx index c0f59a4d..98c7ea09 100644 --- a/public/pages/ReviewAndCreate/components/__tests__/ModelConfigurationFields.test.tsx +++ b/public/pages/ReviewAndCreate/components/__tests__/ModelConfigurationFields.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import chance from 'chance'; import userEvent from '@testing-library/user-event'; -import { render, waitFor } from '@testing-library/react'; +import { render, waitFor, fireEvent, screen, within } from '@testing-library/react'; import { ModelConfigurationFields } from '../ModelConfigurationFields/ModelConfigurationFields'; import { Detector, @@ -19,13 +19,32 @@ import { DATA_TYPES } from '../../../../utils/constants'; import { getRandomFeature } from '../../../../redux/reducers/__tests__/utils'; import { CoreServicesContext } from '../../../../components/CoreServices/CoreServices'; import { coreServicesMock } from '../../../../../test/mocks'; -import { ImputationMethod } from '../../../../models/types'; +import { + ImputationMethod, + ThresholdType, + Action, + Operator, + Rule +} from '../../../../models/types'; const detectorFaker = new chance('seed'); const features = new Array(detectorFaker.natural({ min: 1, max: 5 })) .fill(null) .map(() => getRandomFeature(false)); +// Generate rules based on the existing features +const rules = features.map((feature, index) => ({ + action: Action.IGNORE_ANOMALY, + conditions: [ + { + featureName: feature.featureName, + thresholdType: index % 2 === 0 ? ThresholdType.ACTUAL_OVER_EXPECTED_MARGIN : ThresholdType.EXPECTED_OVER_ACTUAL_RATIO, // Alternate threshold types for variety + operator: Operator.LTE, + value: index % 2 === 0 ? 5 : 0.1, // Use different values for variety + }, + ], +})) as Rule[]; + const testDetector = { id: 'test-id', name: 'test-detector', @@ -60,13 +79,14 @@ const testDetector = { ], }, featureAttributes: features, - imputationOption: { method: ImputationMethod.ZERO} + imputationOption: { method: ImputationMethod.ZERO}, + rules: rules } as Detector; describe('ModelConfigurationFields', () => { test('renders the component in create mode (no ID)', async () => { const onEditModelConfiguration = jest.fn(); - const { container, getByText, getByTestId, queryByText } = render( + const { container, getByText, getByTestId, queryByText, getByRole, queryByRole } = render( <CoreServicesContext.Provider value={coreServicesMock}> <ModelConfigurationFields detector={testDetector} @@ -80,10 +100,15 @@ describe('ModelConfigurationFields', () => { </CoreServicesContext.Provider> ); expect(container.firstChild).toMatchSnapshot(); + getByText('set_to_zero'); + + // Check for the suppression rules button link + const button = getByRole('button', { name: '2 rules' }); + expect(button).toBeInTheDocument(); + userEvent.click(getByTestId('viewFeature-0')); await waitFor(() => { queryByText('max'); - queryByText('Zero'); }); }); }); diff --git a/public/pages/ReviewAndCreate/components/__tests__/__snapshots__/AdditionalSettings.test.tsx.snap b/public/pages/ReviewAndCreate/components/__tests__/__snapshots__/AdditionalSettings.test.tsx.snap index 022afa0e..4a9e79af 100644 --- a/public/pages/ReviewAndCreate/components/__tests__/__snapshots__/AdditionalSettings.test.tsx.snap +++ b/public/pages/ReviewAndCreate/components/__tests__/__snapshots__/AdditionalSettings.test.tsx.snap @@ -3,180 +3,269 @@ exports[`<AdditionalSettings /> spec renders the component with high cardinality disabled 1`] = ` <div> <div - class="euiBasicTable header-single-value-euiBasicTable" + class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow" + style="padding: 20px;" > - <div> + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + style="padding: 0px;" + > <div - class="euiTableHeaderMobile" + class="euiFlexItem" > + <h3 + class="euiTitle euiTitle--small" + data-test-subj="contentPanelTitle" + > + Additional settings + </h3> <div - class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" > <div - class="euiFlexItem euiFlexItem--flexGrowZero" + class="euiFlexItem content-panel-subTitle" + style="line-height: normal; max-width: 75%;" /> + </div> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + class="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + > <div - class="euiFlexItem euiFlexItem--flexGrowZero" + class="euiFlexItem" /> </div> </div> - <table - class="euiTable euiTable--responsive" - id="random_html_id" - tabindex="-1" + </div> + <div> + <hr + class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" + /> + <div + style="padding: 10px 0px;" > - <caption - class="euiScreenReaderOnly euiTableCaption" - /> - <thead> - <tr> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_categoryField_0" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Category field" - > - Category field - </span> - </span> - </th> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_shingleSize_1" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Shingle size" - > - Shingle size - </span> - </span> - </th> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_imputationMethod_2" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Imputation method" - > - Imputation method - </span> - </span> - </th> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_customValues_3" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Custom values" - > - Custom values - </span> - </span> - </th> - </tr> - </thead> - <tbody> - <tr - class="euiTableRow" - > - <td - class="euiTableRowCell" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Category field - </div> - <div - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - > - - - </span> - </div> - </td> - <td - class="euiTableRowCell" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Shingle size - </div> - <div - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - > - 8 - </span> - </div> - </td> - <td - class="euiTableRowCell" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Imputation method - </div> - <div - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - > - Ignore - </span> - </div> - </td> - <td - class="euiTableRowCell" + <div + class="euiBasicTable header-single-value-euiBasicTable" + > + <div> + <div + class="euiTableHeaderMobile" > <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Custom values - </div> - <div - class="euiTableCellContent euiTableCellContent--overflowingContent" + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" > <div - class="" + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" /> </div> - </td> - </tr> - </tbody> - </table> + </div> + <table + class="euiTable euiTable--responsive" + id="random_html_id" + tabindex="-1" + > + <caption + class="euiScreenReaderOnly euiTableCaption" + /> + <thead> + <tr> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_categoryField_0" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Category field" + > + Category field + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_shingleSize_1" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Shingle size" + > + Shingle size + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_imputationMethod_2" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Imputation method" + > + Imputation method + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_customValues_3" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Custom values" + > + Custom values + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_suppresionRules_4" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Suppression rules" + > + Suppression rules + </span> + </span> + </th> + </tr> + </thead> + <tbody> + <tr + class="euiTableRow" + > + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Category field + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + - + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Shingle size + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + 8 + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Imputation method + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Ignore + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Custom values + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="" + > + <p> + - + </p> + </div> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Suppression rules + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="" + > + <p> + - + </p> + </div> + </div> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> </div> </div> </div> @@ -185,187 +274,284 @@ exports[`<AdditionalSettings /> spec renders the component with high cardinality exports[`<AdditionalSettings /> spec renders the component with high cardinality enabled 1`] = ` <div> <div - class="euiBasicTable header-single-value-euiBasicTable" + class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow" + style="padding: 20px;" > - <div> + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + style="padding: 0px;" + > <div - class="euiTableHeaderMobile" + class="euiFlexItem" > + <h3 + class="euiTitle euiTitle--small" + data-test-subj="contentPanelTitle" + > + Additional settings + </h3> <div - class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" > <div - class="euiFlexItem euiFlexItem--flexGrowZero" + class="euiFlexItem content-panel-subTitle" + style="line-height: normal; max-width: 75%;" /> + </div> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + class="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + > <div - class="euiFlexItem euiFlexItem--flexGrowZero" + class="euiFlexItem" /> </div> </div> - <table - class="euiTable euiTable--responsive" - id="random_html_id" - tabindex="-1" + </div> + <div> + <hr + class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" + /> + <div + style="padding: 10px 0px;" > - <caption - class="euiScreenReaderOnly euiTableCaption" - /> - <thead> - <tr> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_categoryField_0" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Category field" - > - Category field - </span> - </span> - </th> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_shingleSize_1" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Shingle size" - > - Shingle size - </span> - </span> - </th> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_imputationMethod_2" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Imputation method" - > - Imputation method - </span> - </span> - </th> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_customValues_3" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Custom values" - > - Custom values - </span> - </span> - </th> - </tr> - </thead> - <tbody> - <tr - class="euiTableRow" - > - <td - class="euiTableRowCell" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Category field - </div> - <div - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - > - test_field - </span> - </div> - </td> - <td - class="euiTableRowCell" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Shingle size - </div> - <div - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - > - 8 - </span> - </div> - </td> - <td - class="euiTableRowCell" + <div + class="euiBasicTable header-single-value-euiBasicTable" + > + <div> + <div + class="euiTableHeaderMobile" > <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Imputation method - </div> - <div - class="euiTableCellContent" + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" > - <span - class="euiTableCellContent__text" - > - Custom - </span> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> </div> - </td> - <td - class="euiTableRowCell" + </div> + <table + class="euiTable euiTable--responsive" + id="random_html_id" + tabindex="-1" > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Custom values - </div> - <div - class="euiTableCellContent euiTableCellContent--overflowingContent" - > - <div - class="" + <caption + class="euiScreenReaderOnly euiTableCaption" + /> + <thead> + <tr> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_categoryField_0" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Category field" + > + Category field + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_shingleSize_1" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Shingle size" + > + Shingle size + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_imputationMethod_2" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Imputation method" + > + Imputation method + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_customValues_3" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Custom values" + > + Custom values + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_suppresionRules_4" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Suppression rules" + > + Suppression rules + </span> + </span> + </th> + </tr> + </thead> + <tbody> + <tr + class="euiTableRow" > - <p> - denyMax:5 - </p> - <p> - denySum:10 - </p> - </div> - </div> - </td> - </tr> - </tbody> - </table> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Category field + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + test_field + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Shingle size + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + 8 + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Imputation method + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Custom + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Custom values + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="" + > + <p> + denyMax:5 + </p> + <p> + denySum:10 + </p> + </div> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Suppression rules + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="" + > + <button + class="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--small" + type="button" + > + <span + class="euiButtonContent euiButtonEmpty__content" + > + <span + class="euiButtonEmpty__text" + > + 2 + rules + </span> + </span> + </button> + </div> + </div> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> </div> </div> </div> diff --git a/public/pages/ReviewAndCreate/components/__tests__/__snapshots__/ModelConfigurationFields.test.tsx.snap b/public/pages/ReviewAndCreate/components/__tests__/__snapshots__/ModelConfigurationFields.test.tsx.snap index a0cb87f0..d1098678 100644 --- a/public/pages/ReviewAndCreate/components/__tests__/__snapshots__/ModelConfigurationFields.test.tsx.snap +++ b/public/pages/ReviewAndCreate/components/__tests__/__snapshots__/ModelConfigurationFields.test.tsx.snap @@ -94,180 +94,281 @@ exports[`ModelConfigurationFields renders the component in create mode (no ID) 1 </div> <div> <div - class="euiBasicTable header-single-value-euiBasicTable" + class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow" + style="padding: 20px;" > - <div> + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + style="padding: 0px;" + > <div - class="euiTableHeaderMobile" + class="euiFlexItem" > + <h3 + class="euiTitle euiTitle--small" + data-test-subj="contentPanelTitle" + > + Additional settings + </h3> <div - class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" > <div - class="euiFlexItem euiFlexItem--flexGrowZero" + class="euiFlexItem content-panel-subTitle" + style="line-height: normal; max-width: 75%;" /> + </div> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + class="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + > <div - class="euiFlexItem euiFlexItem--flexGrowZero" + class="euiFlexItem" /> </div> </div> - <table - class="euiTable euiTable--responsive" - id="random_html_id" - tabindex="-1" + </div> + <div> + <hr + class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" + /> + <div + style="padding: 10px 0px;" > - <caption - class="euiScreenReaderOnly euiTableCaption" - /> - <thead> - <tr> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_categoryField_0" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Category field" - > - Category field - </span> - </span> - </th> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_shingleSize_1" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Shingle size" - > - Shingle size - </span> - </span> - </th> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_imputationMethod_2" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Imputation method" - > - Imputation method - </span> - </span> - </th> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_customValues_3" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Custom values" - > - Custom values - </span> - </span> - </th> - </tr> - </thead> - <tbody> - <tr - class="euiTableRow" - > - <td - class="euiTableRowCell" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Category field - </div> - <div - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - > - - - </span> - </div> - </td> - <td - class="euiTableRowCell" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Shingle size - </div> - <div - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - > - 8 - </span> - </div> - </td> - <td - class="euiTableRowCell" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Imputation method - </div> - <div - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - > - set_to_zero - </span> - </div> - </td> - <td - class="euiTableRowCell" + <div + class="euiBasicTable header-single-value-euiBasicTable" + > + <div> + <div + class="euiTableHeaderMobile" > <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Custom values - </div> - <div - class="euiTableCellContent euiTableCellContent--overflowingContent" + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" > <div - class="" + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" /> </div> - </td> - </tr> - </tbody> - </table> + </div> + <table + class="euiTable euiTable--responsive" + id="random_html_id" + tabindex="-1" + > + <caption + class="euiScreenReaderOnly euiTableCaption" + /> + <thead> + <tr> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_categoryField_0" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Category field" + > + Category field + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_shingleSize_1" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Shingle size" + > + Shingle size + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_imputationMethod_2" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Imputation method" + > + Imputation method + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_customValues_3" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Custom values" + > + Custom values + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_suppresionRules_4" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Suppression rules" + > + Suppression rules + </span> + </span> + </th> + </tr> + </thead> + <tbody> + <tr + class="euiTableRow" + > + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Category field + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + - + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Shingle size + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + 8 + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Imputation method + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + set_to_zero + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Custom values + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="" + > + <p> + - + </p> + </div> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Suppression rules + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="" + > + <button + class="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--small" + type="button" + > + <span + class="euiButtonContent euiButtonEmpty__content" + > + <span + class="euiButtonEmpty__text" + > + 2 + rules + </span> + </span> + </button> + </div> + </div> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> </div> </div> <div diff --git a/public/pages/ReviewAndCreate/containers/__tests__/__snapshots__/ReviewAndCreate.test.tsx.snap b/public/pages/ReviewAndCreate/containers/__tests__/__snapshots__/ReviewAndCreate.test.tsx.snap index 3d8d46a6..13019616 100644 --- a/public/pages/ReviewAndCreate/containers/__tests__/__snapshots__/ReviewAndCreate.test.tsx.snap +++ b/public/pages/ReviewAndCreate/containers/__tests__/__snapshots__/ReviewAndCreate.test.tsx.snap @@ -636,180 +636,269 @@ exports[`<ReviewAndCreate /> spec renders the component, validation loading 1`] </div> <div> <div - class="euiBasicTable header-single-value-euiBasicTable" + class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow" + style="padding: 20px;" > - <div> + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + style="padding: 0px;" + > <div - class="euiTableHeaderMobile" + class="euiFlexItem" > + <h3 + class="euiTitle euiTitle--small" + data-test-subj="contentPanelTitle" + > + Additional settings + </h3> <div - class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" > <div - class="euiFlexItem euiFlexItem--flexGrowZero" + class="euiFlexItem content-panel-subTitle" + style="line-height: normal; max-width: 75%;" /> + </div> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + class="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + > <div - class="euiFlexItem euiFlexItem--flexGrowZero" + class="euiFlexItem" /> </div> </div> - <table - class="euiTable euiTable--responsive" - id="random_html_id" - tabindex="-1" + </div> + <div> + <hr + class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" + /> + <div + style="padding: 10px 0px;" > - <caption - class="euiScreenReaderOnly euiTableCaption" - /> - <thead> - <tr> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_categoryField_0" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Category field" - > - Category field - </span> - </span> - </th> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_shingleSize_1" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Shingle size" - > - Shingle size - </span> - </span> - </th> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_imputationMethod_2" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Imputation method" - > - Imputation method - </span> - </span> - </th> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_customValues_3" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Custom values" - > - Custom values - </span> - </span> - </th> - </tr> - </thead> - <tbody> - <tr - class="euiTableRow" - > - <td - class="euiTableRowCell" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Category field - </div> - <div - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - > - - - </span> - </div> - </td> - <td - class="euiTableRowCell" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Shingle size - </div> - <div - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - > - 8 - </span> - </div> - </td> - <td - class="euiTableRowCell" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Imputation method - </div> - <div - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - > - ignore - </span> - </div> - </td> - <td - class="euiTableRowCell" + <div + class="euiBasicTable header-single-value-euiBasicTable" + > + <div> + <div + class="euiTableHeaderMobile" > <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Custom values - </div> - <div - class="euiTableCellContent euiTableCellContent--overflowingContent" + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" > <div - class="" + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" /> </div> - </td> - </tr> - </tbody> - </table> + </div> + <table + class="euiTable euiTable--responsive" + id="random_html_id" + tabindex="-1" + > + <caption + class="euiScreenReaderOnly euiTableCaption" + /> + <thead> + <tr> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_categoryField_0" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Category field" + > + Category field + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_shingleSize_1" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Shingle size" + > + Shingle size + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_imputationMethod_2" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Imputation method" + > + Imputation method + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_customValues_3" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Custom values" + > + Custom values + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_suppresionRules_4" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Suppression rules" + > + Suppression rules + </span> + </span> + </th> + </tr> + </thead> + <tbody> + <tr + class="euiTableRow" + > + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Category field + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + - + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Shingle size + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + 8 + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Imputation method + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + ignore + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Custom values + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="" + > + <p> + - + </p> + </div> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Suppression rules + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="" + > + <p> + - + </p> + </div> + </div> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> </div> </div> <div @@ -1846,180 +1935,269 @@ exports[`issue in detector validation issues in feature query 1`] = ` </div> <div> <div - class="euiBasicTable header-single-value-euiBasicTable" + class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow" + style="padding: 20px;" > - <div> + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + style="padding: 0px;" + > <div - class="euiTableHeaderMobile" + class="euiFlexItem" > + <h3 + class="euiTitle euiTitle--small" + data-test-subj="contentPanelTitle" + > + Additional settings + </h3> <div - class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" > <div - class="euiFlexItem euiFlexItem--flexGrowZero" + class="euiFlexItem content-panel-subTitle" + style="line-height: normal; max-width: 75%;" /> + </div> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + class="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + > <div - class="euiFlexItem euiFlexItem--flexGrowZero" + class="euiFlexItem" /> </div> </div> - <table - class="euiTable euiTable--responsive" - id="random_html_id" - tabindex="-1" + </div> + <div> + <hr + class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" + /> + <div + style="padding: 10px 0px;" > - <caption - class="euiScreenReaderOnly euiTableCaption" - /> - <thead> - <tr> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_categoryField_0" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Category field" - > - Category field - </span> - </span> - </th> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_shingleSize_1" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Shingle size" - > - Shingle size - </span> - </span> - </th> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_imputationMethod_2" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Imputation method" - > - Imputation method - </span> - </span> - </th> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_customValues_3" - role="columnheader" - scope="col" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Custom values" - > - Custom values - </span> - </span> - </th> - </tr> - </thead> - <tbody> - <tr - class="euiTableRow" - > - <td - class="euiTableRowCell" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Category field - </div> - <div - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - > - - - </span> - </div> - </td> - <td - class="euiTableRowCell" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Shingle size - </div> - <div - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - > - 8 - </span> - </div> - </td> - <td - class="euiTableRowCell" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Imputation method - </div> - <div - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - > - ignore - </span> - </div> - </td> - <td - class="euiTableRowCell" + <div + class="euiBasicTable header-single-value-euiBasicTable" + > + <div> + <div + class="euiTableHeaderMobile" > <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Custom values - </div> - <div - class="euiTableCellContent euiTableCellContent--overflowingContent" + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" > <div - class="" + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" /> </div> - </td> - </tr> - </tbody> - </table> + </div> + <table + class="euiTable euiTable--responsive" + id="random_html_id" + tabindex="-1" + > + <caption + class="euiScreenReaderOnly euiTableCaption" + /> + <thead> + <tr> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_categoryField_0" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Category field" + > + Category field + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_shingleSize_1" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Shingle size" + > + Shingle size + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_imputationMethod_2" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Imputation method" + > + Imputation method + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_customValues_3" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Custom values" + > + Custom values + </span> + </span> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_suppresionRules_4" + role="columnheader" + scope="col" + > + <span + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + title="Suppression rules" + > + Suppression rules + </span> + </span> + </th> + </tr> + </thead> + <tbody> + <tr + class="euiTableRow" + > + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Category field + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + - + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Shingle size + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + 8 + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Imputation method + </div> + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + ignore + </span> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Custom values + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="" + > + <p> + - + </p> + </div> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Suppression rules + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="" + > + <p> + - + </p> + </div> + </div> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> </div> </div> <div diff --git a/public/pages/ReviewAndCreate/utils/helpers.ts b/public/pages/ReviewAndCreate/utils/helpers.ts index 24703c7d..f123f6fd 100644 --- a/public/pages/ReviewAndCreate/utils/helpers.ts +++ b/public/pages/ReviewAndCreate/utils/helpers.ts @@ -25,7 +25,7 @@ import { CreateDetectorFormikValues } from '../../CreateDetectorSteps/models/int import { OPERATORS_QUERY_MAP } from '../../DefineDetector/utils/whereFilters'; import { convertTimestampToNumber } from '../../../utils/utils'; import { CUSTOM_AD_RESULT_INDEX_PREFIX } from '../../../../server/utils/constants'; -import { formikToImputationOption } from '../../ConfigureModel/utils/helpers'; +import { formikToImputationOption, formikToRules } from '../../ConfigureModel/utils/helpers'; export function formikToDetector(values: CreateDetectorFormikValues): Detector { const detectionDateRange = values.historical @@ -77,6 +77,7 @@ export function formikToDetector(values: CreateDetectorFormikValues): Detector { ? values.flattenCustomResultIndex : undefined, imputationOption: formikToImputationOption(values.imputationOption), + rules: formikToRules(values.suppressionRules), } as Detector; // Optionally add detection date range diff --git a/public/redux/reducers/__tests__/utils.ts b/public/redux/reducers/__tests__/utils.ts index 01df3d9c..76a734e4 100644 --- a/public/redux/reducers/__tests__/utils.ts +++ b/public/redux/reducers/__tests__/utils.ts @@ -15,7 +15,6 @@ import { Detector, FeatureAttributes, FEATURE_TYPE, - FILTER_TYPES, UiMetaData, UNITS, Monitor, @@ -24,7 +23,13 @@ import moment from 'moment'; import { DETECTOR_STATE } from '../../../../server/utils/constants'; import { DEFAULT_SHINGLE_SIZE } from '../../../utils/constants'; import { - ImputationMethod, ImputationOption, + ImputationMethod, + ImputationOption, + Rule, + Action, + Condition, + ThresholdType, + Operator, } from '../../../models/types'; const detectorFaker = new chance('seed'); @@ -67,7 +72,7 @@ const randomQuery = () => { }; }; -const getUIMetadata = (features: FeatureAttributes[]) => { +export const getUIMetadata = (features: FeatureAttributes[]) => { const metaFeatures = features.reduce( (acc, feature) => ({ ...acc, @@ -127,7 +132,8 @@ export const getRandomDetector = ( resultIndexMinSize: 51200, resultIndexTtl: 60, flattenCustomResultIndex: true, - imputationOption: randomImputationOption(features) + imputationOption: randomImputationOption(features), + rules: randomRules(features) }; }; @@ -247,3 +253,58 @@ export const randomImputationOption = (features: FeatureAttributes[]): Imputatio } return options[randomIndex]; }; + +// Helper function to get a random item from an array +function getRandomItem<T>(items: T[]): T { + return items[random(0, items.length - 1)]; +} + +// Helper function to generate a random value (for simplicity, let's use a range of 0 to 100) +function getRandomValue(): number { + return random(0, 100, true); // Generates a random float between 0 and 100 +} + +export const randomRules = (features: FeatureAttributes[]): Rule[] | undefined => { + // If there are no features, return undefined + if (features.length === 0) { + return undefined; + } + + const rules: Rule[] = []; + + // Generate a random number of rules (between 1 and 3 for testing) + const numberOfRules = random(1, 3); + + for (let i = 0; i < numberOfRules; i++) { + // Random action + const action = Action.IGNORE_ANOMALY; + + // Generate a random number of conditions (between 1 and 2 for testing) + const numberOfConditions = random(1, 2); + const conditions: Condition[] = []; + + for (let j = 0; j < numberOfConditions; j++) { + const featureName = getRandomItem(features.map((f) => f.featureName)); + const thresholdType = getRandomItem(Object.values(ThresholdType)); + const operator = getRandomItem(Object.values(Operator)); + const value = getRandomValue(); + + conditions.push({ + featureName, + thresholdType, + operator, + value, + }); + } + + // Create the rule with the generated action and conditions + rules.push({ + action, + conditions, + }); + } + + // Randomly decide whether to return undefined or the generated rules + const shouldReturnUndefined = random(0, 1) === 0; + return shouldReturnUndefined ? undefined : rules; +}; diff --git a/public/utils/utils.tsx b/public/utils/utils.tsx index a2af39ff..ea979cb3 100644 --- a/public/utils/utils.tsx +++ b/public/utils/utils.tsx @@ -110,6 +110,21 @@ export const validatePositiveInteger = (value: any) => { 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') { + return undefined; // No error for empty, NaN, or non-number values + } + + // Validate that the value is a positive number greater than zero + if (value <= 0) { + return 'Must be a positive number greater than zero'; + } + + return undefined; // No error if the value is valid +} + export const validateEmptyOrPositiveInteger = (value: any) => { if (Number.isInteger(value) && value < 1) return 'Must be a positive integer';