Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding feature direction and moving suppression rules to each feature #960

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 14 additions & 15 deletions public/components/FormattedFormRow/FormattedFormRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/

import React, { ReactElement, ReactNode } from 'react';
import { EuiCompressedFormRow, EuiText, EuiLink, EuiIcon } from '@elastic/eui';
import { EuiCompressedFormRow, EuiText, EuiLink, EuiIcon, EuiToolTip } from '@elastic/eui';

type FormattedFormRowProps = {
title?: string;
Expand All @@ -22,26 +22,25 @@ type FormattedFormRowProps = {
fullWidth?: boolean;
helpText?: string;
hintLink?: string;
linkToolTip?: boolean;
};

export const FormattedFormRow = (props: FormattedFormRowProps) => {
let hints;
if (props.hint) {
const hintTexts = Array.isArray(props.hint) ? props.hint : [props.hint];
hints = hintTexts.map((hint, i) => {
return (
const hints = props.hint
? (Array.isArray(props.hint) ? props.hint : [props.hint]).map((hint, i) => (
<EuiText key={i} className="sublabel" style={{ maxWidth: '400px' }}>
{hint}
{props.hintLink ? ' ' : null}
{props.hintLink ? (
<EuiLink href={props.hintLink} target="_blank">
Learn more
</EuiLink>
) : null}
{props.hintLink && (
<>
{' '}
<EuiLink href={props.hintLink} target="_blank">
Learn more
</EuiLink>
</>
)}
</EuiText>
);
});
}
))
: null;

const { formattedTitle, ...euiFormRowProps } = props;

Expand Down
16 changes: 14 additions & 2 deletions public/models/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ export enum ThresholdType {
* (b-a)/|a| is less than or equal to ignoreNearExpectedFromBelowByRatio.
*/
EXPECTED_OVER_ACTUAL_RATIO = "EXPECTED_OVER_ACTUAL_RATIO",

/**
* Specifies a threshold for ignoring anomalies based on whether the actual value
* is over the expected value returned from the model.
*/
ACTUAL_IS_OVER_EXPECTED = "ACTUAL_IS_OVER_EXPECTED",

/**
* Specifies a threshold for ignoring anomalies based on whether the actual value
* is below the expected value returned from the model.
* */
ACTUAL_IS_BELOW_EXPECTED = "ACTUAL_IS_BELOW_EXPECTED",
}

// Method to get the description of ThresholdType
Expand All @@ -113,7 +125,7 @@ export interface Rule {
export interface Condition {
featureName: string;
thresholdType: ThresholdType;
operator: Operator;
value: number;
operator?: Operator;
value?: number;
}

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
EuiButtonIcon,
EuiCompressedFieldText,
EuiToolTip,
EuiButtonEmpty,
} from '@elastic/eui';
import { Field, FieldProps, FieldArray } from 'formik';
import React, { useEffect, useState } from 'react';
Expand Down Expand Up @@ -48,47 +49,7 @@ export function AdvancedSettings(props: AdvancedSettingsProps) {
{ value: SparseDataOptionValue.SET_TO_ZERO, text: 'Set to zero' },
{ 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={
Expand Down Expand Up @@ -257,160 +218,6 @@ export function AdvancedSettings(props: AdvancedSettingsProps) {
);
}}
</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>
Expand Down

This file was deleted.

Loading
Loading