3
3
* SPDX-License-Identifier: Apache-2.0
4
4
*/
5
5
6
- import React , { useState } from 'react' ;
6
+ import React , { useState , useEffect } from 'react' ;
7
7
import { useFormikContext , getIn } from 'formik' ;
8
8
import { isEmpty } from 'lodash' ;
9
+ import Ajv from 'ajv' ;
9
10
import {
10
11
EuiCodeEditor ,
11
12
EuiFlexGroup ,
@@ -20,6 +21,11 @@ import {
20
21
EuiSmallButton ,
21
22
EuiSpacer ,
22
23
EuiText ,
24
+ EuiPopover ,
25
+ EuiSmallButtonEmpty ,
26
+ EuiCodeBlock ,
27
+ EuiPopoverTitle ,
28
+ EuiIconTip ,
23
29
} from '@elastic/eui' ;
24
30
import {
25
31
IConfigField ,
@@ -48,7 +54,11 @@ import {
48
54
useAppDispatch ,
49
55
} from '../../../../store' ;
50
56
import { getCore } from '../../../../services' ;
51
- import { getDataSourceId , parseModelInputs } from '../../../../utils/utils' ;
57
+ import {
58
+ getDataSourceId ,
59
+ parseModelInputs ,
60
+ parseModelInputsObj ,
61
+ } from '../../../../utils/utils' ;
52
62
import { MapArrayField } from '../input_fields' ;
53
63
54
64
interface InputTransformModalProps {
@@ -89,9 +99,54 @@ export function InputTransformModal(props: InputTransformModalProps) {
89
99
number | undefined
90
100
> ( ( outputOptions [ 0 ] ?. value as number ) ?? undefined ) ;
91
101
92
- // TODO: integrated with Ajv to fetch any model interface and perform validation
93
- // on the produced output on-the-fly. For examples, see
102
+ // popover state containing the model interface details, if applicable
103
+ const [ popoverOpen , setPopoverOpen ] = useState < boolean > ( false ) ;
104
+
105
+ // validation state utilizing the model interface, if applicable. undefined if
106
+ // there is no model interface and/or no source input
107
+ const [ isValid , setIsValid ] = useState < boolean | undefined > ( undefined ) ;
108
+
109
+ // hook to re-generate the transform when any inputs to the transform are updated
110
+ useEffect ( ( ) => {
111
+ if (
112
+ ! isEmpty ( map ) &&
113
+ ! isEmpty ( JSON . parse ( sourceInput ) ) &&
114
+ selectedOutputOption !== undefined
115
+ ) {
116
+ let sampleSourceInput = { } ;
117
+ try {
118
+ // In the context of ingest or search resp, this input will be an array (list of docs)
119
+ // In the context of request, it will be a single JSON
120
+ sampleSourceInput =
121
+ props . context === PROCESSOR_CONTEXT . INGEST ||
122
+ props . context === PROCESSOR_CONTEXT . SEARCH_RESPONSE
123
+ ? JSON . parse ( sourceInput ) [ 0 ]
124
+ : JSON . parse ( sourceInput ) ;
125
+ const output = generateTransform (
126
+ sampleSourceInput ,
127
+ map [ selectedOutputOption ]
128
+ ) ;
129
+ setTransformedOutput ( customStringify ( output ) ) ;
130
+ } catch { }
131
+ }
132
+ } , [ map , sourceInput , selectedOutputOption ] ) ;
133
+
134
+ // hook to re-determine validity when the generated output changes
135
+ // utilize Ajv JSON schema validator library. For more info/examples, see
94
136
// https://www.npmjs.com/package/ajv
137
+ useEffect ( ( ) => {
138
+ if (
139
+ ! isEmpty ( JSON . parse ( sourceInput ) ) &&
140
+ ! isEmpty ( props . modelInterface ?. input ?. properties ?. parameters )
141
+ ) {
142
+ const validateFn = new Ajv ( ) . compile (
143
+ props . modelInterface ?. input ?. properties ?. parameters || { }
144
+ ) ;
145
+ setIsValid ( validateFn ( JSON . parse ( transformedOutput ) ) ) ;
146
+ } else {
147
+ setIsValid ( undefined ) ;
148
+ }
149
+ } , [ transformedOutput ] ) ;
95
150
96
151
return (
97
152
< EuiModal onClose = { props . onClose } style = { { width : '70vw' } } >
@@ -105,7 +160,7 @@ export function InputTransformModal(props: InputTransformModalProps) {
105
160
< EuiFlexItem >
106
161
< >
107
162
< EuiText color = "subdued" >
108
- Fetch some sample input data and how it is transformed.
163
+ Fetch some sample input data and see how it is transformed.
109
164
</ EuiText >
110
165
< EuiSpacer size = "s" />
111
166
< EuiText > Expected input</ EuiText >
@@ -279,49 +334,71 @@ export function InputTransformModal(props: InputTransformModalProps) {
279
334
</ EuiFlexItem >
280
335
< EuiFlexItem >
281
336
< >
282
- { outputOptions . length === 1 ? (
283
- < EuiText > Expected output</ EuiText >
284
- ) : (
285
- < EuiCompressedSelect
286
- prepend = { < EuiText > Expected output for</ EuiText > }
287
- options = { outputOptions }
288
- value = { selectedOutputOption }
289
- onChange = { ( e ) => {
290
- setSelectedOutputOption ( Number ( e . target . value ) ) ;
291
- setTransformedOutput ( '{}' ) ;
292
- } }
293
- />
294
- ) }
295
- < EuiSpacer size = "s" />
296
- < EuiSmallButton
297
- style = { { width : '100px' } }
298
- disabled = { isEmpty ( map ) || isEmpty ( JSON . parse ( sourceInput ) ) }
299
- onClick = { async ( ) => {
300
- if (
301
- ! isEmpty ( map ) &&
302
- ! isEmpty ( JSON . parse ( sourceInput ) ) &&
303
- selectedOutputOption !== undefined
304
- ) {
305
- let sampleSourceInput = { } ;
306
- try {
307
- // In the context of ingest or search resp, this input will be an array (list of docs)
308
- // In the context of request, it will be a single JSON
309
- sampleSourceInput =
310
- props . context === PROCESSOR_CONTEXT . INGEST ||
311
- props . context === PROCESSOR_CONTEXT . SEARCH_RESPONSE
312
- ? JSON . parse ( sourceInput ) [ 0 ]
313
- : JSON . parse ( sourceInput ) ;
314
- const output = generateTransform (
315
- sampleSourceInput ,
316
- map [ selectedOutputOption ]
317
- ) ;
318
- setTransformedOutput ( customStringify ( output ) ) ;
319
- } catch { }
320
- }
321
- } }
322
- >
323
- Generate
324
- </ EuiSmallButton >
337
+ < EuiFlexGroup direction = "row" justifyContent = "spaceBetween" >
338
+ { isValid !== undefined && (
339
+ < EuiFlexItem
340
+ grow = { false }
341
+ style = { {
342
+ marginTop : '16px' ,
343
+ marginLeft : '8px' ,
344
+ marginRight : '-8px' ,
345
+ } }
346
+ >
347
+ < EuiIconTip
348
+ type = { isValid ? 'check' : 'cross' }
349
+ color = { isValid ? 'success' : 'danger' }
350
+ size = "m"
351
+ content = {
352
+ isValid
353
+ ? 'Meets model interface requirements'
354
+ : 'Does not meet model interface requirements'
355
+ }
356
+ />
357
+ </ EuiFlexItem >
358
+ ) }
359
+ < EuiFlexItem grow = { true } >
360
+ { outputOptions . length === 1 ? (
361
+ < EuiText > Expected output</ EuiText >
362
+ ) : (
363
+ < EuiCompressedSelect
364
+ prepend = { < EuiText > Expected output for</ EuiText > }
365
+ options = { outputOptions }
366
+ value = { selectedOutputOption }
367
+ onChange = { ( e ) => {
368
+ setSelectedOutputOption ( Number ( e . target . value ) ) ;
369
+ } }
370
+ />
371
+ ) }
372
+ </ EuiFlexItem >
373
+ { ! isEmpty ( parseModelInputsObj ( props . modelInterface ) ) && (
374
+ < EuiFlexItem grow = { false } >
375
+ < EuiPopover
376
+ isOpen = { popoverOpen }
377
+ closePopover = { ( ) => setPopoverOpen ( false ) }
378
+ button = {
379
+ < EuiSmallButtonEmpty
380
+ onClick = { ( ) => setPopoverOpen ( ! popoverOpen ) }
381
+ >
382
+ View model inputs
383
+ </ EuiSmallButtonEmpty >
384
+ }
385
+ >
386
+ < EuiPopoverTitle >
387
+ The JSON Schema defining the model's expected input
388
+ </ EuiPopoverTitle >
389
+ < EuiCodeBlock
390
+ language = "json"
391
+ fontSize = "m"
392
+ isCopyable = { false }
393
+ >
394
+ { customStringify (
395
+ parseModelInputsObj ( props . modelInterface )
396
+ ) }
397
+ </ EuiCodeBlock >
398
+ </ EuiPopover >
399
+ </ EuiFlexItem >
400
+ ) }
401
+ </ EuiFlexGroup >
325
402
< EuiSpacer size = "s" />
326
403
< EuiCodeEditor
327
404
mode = "json"
0 commit comments