@@ -20,15 +20,17 @@ import {
20
20
EuiSelect ,
21
21
EuiButtonIcon ,
22
22
EuiFieldText ,
23
+ EuiToolTip ,
23
24
} from '@elastic/eui' ;
24
- import { Field , FieldProps , FieldArray , } from 'formik' ;
25
+ import { Field , FieldProps , FieldArray } from 'formik' ;
25
26
import React , { useEffect , useState } from 'react' ;
26
27
import ContentPanel from '../../../../components/ContentPanel/ContentPanel' ;
27
28
import { BASE_DOCS_LINK } from '../../../../utils/constants' ;
28
29
import {
29
30
isInvalid ,
30
31
getError ,
31
32
validatePositiveInteger ,
33
+ validatePositiveDecimal ,
32
34
} from '../../../../utils/utils' ;
33
35
import { FormattedFormRow } from '../../../../components/FormattedFormRow/FormattedFormRow' ;
34
36
import { SparseDataOptionValue } from '../../utils/constants' ;
@@ -47,6 +49,46 @@ export function AdvancedSettings(props: AdvancedSettingsProps) {
47
49
{ value : SparseDataOptionValue . CUSTOM_VALUE , text : 'Custom value' } ,
48
50
] ;
49
51
52
+ const aboveBelowOptions = [
53
+ { value : 'above' , text : 'above' } ,
54
+ { value : 'below' , text : 'below' } ,
55
+ ] ;
56
+
57
+ function extractArrayError ( fieldName : string , form : any ) : string {
58
+ const error = form . errors [ fieldName ] ;
59
+ console . log ( 'Error for field:' , fieldName , error ) ; // Log the error for debugging
60
+
61
+ // Check if the error is an array with objects inside
62
+ if ( Array . isArray ( error ) && error . length > 0 ) {
63
+ // Iterate through the array to find the first non-empty error message
64
+ for ( const err of error ) {
65
+ if ( typeof err === 'object' && err !== null ) {
66
+ const entry = Object . entries ( err ) . find (
67
+ ( [ _ , fieldError ] ) => fieldError
68
+ ) ; // Find the first entry with a non-empty error message
69
+ if ( entry ) {
70
+ const [ fieldKey , fieldError ] = entry ;
71
+
72
+ // Replace fieldKey with a more user-friendly name if it matches specific fields
73
+ const friendlyFieldName =
74
+ fieldKey === 'absoluteThreshold'
75
+ ? 'absolute threshold'
76
+ : fieldKey === 'relativeThreshold'
77
+ ? 'relative threshold'
78
+ : fieldKey ; // Use the original fieldKey if no match
79
+
80
+ return typeof fieldError === 'string'
81
+ ? `${ friendlyFieldName } ${ fieldError . toLowerCase ( ) } ` // Format the error message with the friendly field name
82
+ : String ( fieldError || '' ) ;
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ // Default case to handle other types of errors
89
+ return typeof error === 'string' ? error : String ( error || '' ) ;
90
+ }
91
+
50
92
return (
51
93
< ContentPanel
52
94
title = {
@@ -137,28 +179,29 @@ export function AdvancedSettings(props: AdvancedSettingsProps) {
137
179
< EuiSelect { ...field } options = { sparseDataOptions } />
138
180
</ FormattedFormRow >
139
181
140
- { /* Conditionally render the "Custom value" title and the input fields when 'Custom value' is selected */ }
141
- { field . value === SparseDataOptionValue . CUSTOM_VALUE && (
142
- < >
143
- < EuiSpacer size = "m" />
144
- < EuiText size = "xs" >
145
- < h5 > Custom value</ h5 >
146
- </ EuiText >
147
- < EuiSpacer size = "s" />
148
- < FieldArray name = "imputationOption.custom_value" >
149
- { ( arrayHelpers ) => (
150
- < >
151
- { form . values . imputationOption . custom_value ?. map ( ( _ , index ) => (
152
- < EuiFlexGroup
153
- key = { index }
154
- gutterSize = "s"
155
- alignItems = "center"
156
- >
157
- < EuiFlexItem grow = { false } >
158
- < Field
159
- name = { `imputationOption.custom_value.${ index } .featureName` }
160
- id = { `imputationOption.custom_value.${ index } .featureName` }
182
+ { /* Conditionally render the "Custom value" title and the input fields when 'Custom value' is selected */ }
183
+ { field . value === SparseDataOptionValue . CUSTOM_VALUE && (
184
+ < >
185
+ < EuiSpacer size = "m" />
186
+ < EuiText size = "xs" >
187
+ < h5 > Custom value</ h5 >
188
+ </ EuiText >
189
+ < EuiSpacer size = "s" />
190
+ < FieldArray name = "imputationOption.custom_value" >
191
+ { ( arrayHelpers ) => (
192
+ < >
193
+ { form . values . imputationOption . custom_value ?. map (
194
+ ( _ , index ) => (
195
+ < EuiFlexGroup
196
+ key = { index }
197
+ gutterSize = "s"
198
+ alignItems = "center"
161
199
>
200
+ < EuiFlexItem grow = { false } >
201
+ < Field
202
+ name = { `imputationOption.custom_value.${ index } .featureName` }
203
+ id = { `imputationOption.custom_value.${ index } .featureName` }
204
+ >
162
205
{ ( { field } : FieldProps ) => (
163
206
< EuiFieldText
164
207
placeholder = "Feature name"
@@ -211,6 +254,160 @@ export function AdvancedSettings(props: AdvancedSettingsProps) {
211
254
) ;
212
255
} }
213
256
</ Field >
257
+
258
+ < EuiSpacer size = "m" />
259
+ < FieldArray name = "suppressionRules" >
260
+ { ( arrayHelpers ) => (
261
+ < >
262
+ < Field name = "suppressionRules" >
263
+ { ( { field, form } : FieldProps ) => (
264
+ < >
265
+ < EuiFlexGroup >
266
+ { /* Controls the width of the whole row as FormattedFormRow does not allow that. Otherwise, our row is too packed. */ }
267
+ < EuiFlexItem
268
+ grow = { false }
269
+ style = { { maxWidth : '1200px' } }
270
+ >
271
+ < FormattedFormRow
272
+ title = "Suppression Rules"
273
+ hint = { [
274
+ `Set rules to ignore anomalies by comparing actual values against expected values.
275
+ Anomalies can be ignored if the difference is within a specified absolute value or a relative percentage of the expected value.` ,
276
+ ] }
277
+ hintLink = { `${ BASE_DOCS_LINK } /ad` }
278
+ isInvalid = { isInvalid ( field . name , form ) }
279
+ error = { extractArrayError ( field . name , form ) }
280
+ fullWidth
281
+ >
282
+ < >
283
+ { form . values . suppressionRules ?. map (
284
+ ( rule , index ) => (
285
+ < EuiFlexGroup
286
+ key = { index }
287
+ gutterSize = "s"
288
+ alignItems = "center"
289
+ >
290
+ < EuiFlexItem grow = { false } >
291
+ < EuiText size = "s" >
292
+ Ignore anomalies for the feature
293
+ </ EuiText >
294
+ </ EuiFlexItem >
295
+ < EuiFlexItem grow = { 2 } >
296
+ < Field
297
+ name = { `suppressionRules.${ index } .featureName` }
298
+ >
299
+ { ( { field } : FieldProps ) => (
300
+ < EuiFieldText
301
+ placeholder = "Feature name"
302
+ { ...field }
303
+ fullWidth
304
+ />
305
+ ) }
306
+ </ Field >
307
+ </ EuiFlexItem >
308
+ < EuiFlexItem grow = { false } >
309
+ < EuiText size = "s" >
310
+ when the actual value is no more than
311
+ </ EuiText >
312
+ </ EuiFlexItem >
313
+ < EuiFlexItem grow = { 1 } >
314
+ < EuiToolTip content = "Absolute threshold value" >
315
+ < Field
316
+ name = { `suppressionRules.${ index } .absoluteThreshold` }
317
+ validate = { validatePositiveDecimal }
318
+ >
319
+ { ( { field } : FieldProps ) => (
320
+ < EuiFieldNumber
321
+ placeholder = "Absolute"
322
+ { ...field }
323
+ value = { field . value || '' }
324
+ />
325
+ ) }
326
+ </ Field >
327
+ </ EuiToolTip >
328
+ </ EuiFlexItem >
329
+ < EuiFlexItem grow = { false } >
330
+ < EuiText size = "s" > or</ EuiText >
331
+ </ EuiFlexItem >
332
+ < EuiFlexItem grow = { 1 } >
333
+ < EuiToolTip content = "Relative threshold value as a percentage" >
334
+ < Field
335
+ name = { `suppressionRules.${ index } .relativeThreshold` }
336
+ validate = { validatePositiveDecimal }
337
+ >
338
+ { ( { field } : FieldProps ) => (
339
+ < div
340
+ style = { {
341
+ display : 'flex' ,
342
+ alignItems : 'center' ,
343
+ } }
344
+ >
345
+ < EuiFieldNumber
346
+ placeholder = "Relative"
347
+ { ...field }
348
+ value = { field . value || '' }
349
+ />
350
+ < EuiText size = "s" > %</ EuiText >
351
+ </ div >
352
+ ) }
353
+ </ Field >
354
+ </ EuiToolTip >
355
+ </ EuiFlexItem >
356
+ < EuiFlexItem grow = { 1 } >
357
+ < EuiToolTip content = "Select above or below expected value" >
358
+ < Field
359
+ name = { `suppressionRules.${ index } .aboveBelow` }
360
+ >
361
+ { ( { field } : FieldProps ) => (
362
+ < EuiSelect
363
+ options = { aboveBelowOptions }
364
+ { ...field }
365
+ />
366
+ ) }
367
+ </ Field >
368
+ </ EuiToolTip >
369
+ </ EuiFlexItem >
370
+ < EuiFlexItem grow = { false } >
371
+ < EuiText size = "s" >
372
+ the expected value.
373
+ </ EuiText >
374
+ </ EuiFlexItem >
375
+ < EuiFlexItem grow = { false } >
376
+ < EuiButtonIcon
377
+ iconType = "trash"
378
+ color = "danger"
379
+ aria-label = "Delete rule"
380
+ onClick = { ( ) =>
381
+ arrayHelpers . remove ( index )
382
+ }
383
+ />
384
+ </ EuiFlexItem >
385
+ </ EuiFlexGroup >
386
+ )
387
+ ) }
388
+ </ >
389
+ </ FormattedFormRow >
390
+ </ EuiFlexItem >
391
+ </ EuiFlexGroup >
392
+ </ >
393
+ ) }
394
+ </ Field >
395
+ < EuiSpacer size = "s" />
396
+ < EuiButtonIcon
397
+ iconType = "plusInCircle"
398
+ onClick = { ( ) =>
399
+ arrayHelpers . push ( {
400
+ fieldName : '' ,
401
+ absoluteThreshold : null , // Set to null to allow empty inputs
402
+ relativeThreshold : null , // Set to null to allow empty inputs
403
+ aboveBelow : 'above' ,
404
+ } )
405
+ }
406
+ aria-label = "Add rule"
407
+ />
408
+ </ >
409
+ ) }
410
+ </ FieldArray >
214
411
</ >
215
412
) : null }
216
413
</ ContentPanel >
0 commit comments