Skip to content

Commit f224eb5

Browse files
Persist form state and validation in Workspace (#61) (#62)
Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com> (cherry picked from commit 10810e3) Co-authored-by: Tyler Ohlsen <ohltyler@amazon.com>
1 parent 6226131 commit f224eb5

26 files changed

+650
-244
lines changed

common/interfaces.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { Node, Edge } from 'reactflow';
7-
import { IComponent as IComponentData } from '../public/component_types';
7+
import { IComponentData } from '../public/component_types';
88

99
export type Index = {
1010
name: string;

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
]
3131
},
3232
"dependencies": {
33-
"reactflow": "^11.8.3"
33+
"formik": "2.4.2",
34+
"reactflow": "^11.8.3",
35+
"yup": "^1.3.2"
3436
},
3537
"devDependencies": {
3638
"pre-commit": "^1.2.2"

public/component_types/indices/knn_index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export class KnnIndex extends BaseComponent implements IComponent {
5555
this.fields = [
5656
{
5757
label: 'Index Name',
58+
name: 'indexName',
5859
type: 'select',
5960
optional: false,
6061
advanced: false,
@@ -63,6 +64,7 @@ export class KnnIndex extends BaseComponent implements IComponent {
6364
this.createFields = [
6465
{
6566
label: 'Index Name',
67+
name: 'indexName',
6668
type: 'string',
6769
optional: false,
6870
advanced: false,
@@ -73,6 +75,7 @@ export class KnnIndex extends BaseComponent implements IComponent {
7375
// simple form inputs vs. complex JSON editor
7476
{
7577
label: 'Mappings',
78+
name: 'indexMappings',
7679
type: 'json',
7780
placeholder: 'Enter an index mappings JSON blob...',
7881
optional: false,

public/component_types/interfaces.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,26 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6+
import { FormikValues } from 'formik';
7+
import { ObjectSchema } from 'yup';
68
import { COMPONENT_CATEGORY, COMPONENT_CLASS } from '../utils';
79

810
/**
9-
* ************ Types **************************
11+
* ************ Types *************************
1012
*/
1113
export type UIFlow = string;
1214
export type FieldType = 'string' | 'json' | 'select';
13-
14-
/**
15-
* ************ Base interfaces ****************
16-
*/
15+
// TODO: this may expand to more types in the future. Formik supports 'any' so we can too.
16+
// For now, limiting scope to expected types.
17+
export type FieldValue = string | {};
18+
export type ComponentFormValues = FormikValues;
19+
export type WorkspaceFormValues = {
20+
[componentId: string]: ComponentFormValues;
21+
};
22+
export type WorkspaceSchemaObj = {
23+
[componentId: string]: ObjectSchema<any, any, any>;
24+
};
25+
export type WorkspaceSchema = ObjectSchema<WorkspaceSchemaObj>;
1726

1827
/**
1928
* Represents a single base class as an input handle for a component.
@@ -35,6 +44,8 @@ export interface IComponentInput {
3544
export interface IComponentField {
3645
label: string;
3746
type: FieldType;
47+
name: string;
48+
value?: FieldValue;
3849
placeholder?: string;
3950
optional?: boolean;
4051
advanced?: boolean;
@@ -84,4 +95,5 @@ export interface IComponent {
8495
*/
8596
export interface IComponentData extends IComponent {
8697
id: string;
98+
selected?: boolean;
8799
}

public/component_types/processors/text_embedding_processor.ts

+3
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,21 @@ export class TextEmbeddingProcessor
4646
this.fields = [
4747
{
4848
label: 'Model ID',
49+
name: 'modelId',
4950
type: 'string',
5051
optional: false,
5152
advanced: false,
5253
},
5354
{
5455
label: 'Input Field',
56+
name: 'inputField',
5557
type: 'string',
5658
optional: false,
5759
advanced: false,
5860
},
5961
{
6062
label: 'Output Field',
63+
name: 'outputField',
6164
type: 'string',
6265
optional: false,
6366
advanced: false,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React from 'react';
7+
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
8+
import { ReactFlowComponent } from '../../../../common';
9+
import { ComponentInputs } from './component_inputs';
10+
import { EmptyComponentInputs } from './empty_component_inputs';
11+
12+
// styling
13+
import '../workspace/workspace-styles.scss';
14+
15+
interface ComponentDetailsProps {
16+
selectedComponent?: ReactFlowComponent;
17+
}
18+
19+
/**
20+
* A panel that will be nested in a resizable container to dynamically show
21+
* the details and user-required inputs based on the selected component
22+
* in the flow workspace.
23+
*/
24+
export function ComponentDetails(props: ComponentDetailsProps) {
25+
return (
26+
<EuiFlexGroup
27+
direction="column"
28+
gutterSize="none"
29+
className="workspace-panel"
30+
>
31+
<EuiFlexItem>
32+
<EuiPanel paddingSize="m">
33+
{props.selectedComponent ? (
34+
<ComponentInputs selectedComponent={props.selectedComponent} />
35+
) : (
36+
<EmptyComponentInputs />
37+
)}
38+
</EuiPanel>
39+
</EuiFlexItem>
40+
</EuiFlexGroup>
41+
);
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React from 'react';
7+
import { EuiSpacer, EuiTitle } from '@elastic/eui';
8+
import { InputFieldList } from './input_field_list';
9+
import { ReactFlowComponent } from '../../../../common';
10+
11+
interface ComponentInputsProps {
12+
selectedComponent: ReactFlowComponent;
13+
}
14+
15+
export function ComponentInputs(props: ComponentInputsProps) {
16+
return (
17+
<>
18+
<EuiTitle size="m">
19+
<h2>{props.selectedComponent.data.label || ''}</h2>
20+
</EuiTitle>
21+
<EuiSpacer size="s" />
22+
<InputFieldList selectedComponent={props.selectedComponent} />
23+
</>
24+
);
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React from 'react';
7+
import { EuiEmptyPrompt, EuiText } from '@elastic/eui';
8+
9+
export function EmptyComponentInputs() {
10+
return (
11+
<EuiEmptyPrompt
12+
iconType={'cross'}
13+
title={<h2>No component selected</h2>}
14+
titleSize="s"
15+
body={
16+
<>
17+
<EuiText>
18+
Add a component, or select a component to view or edit its
19+
configuration.
20+
</EuiText>
21+
</>
22+
}
23+
/>
24+
);
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
export * from './component_details';

public/pages/workflow_detail/workspace_component/input_field_list.tsx public/pages/workflow_detail/component_details/input_field_list.tsx

+15-11
Original file line numberDiff line numberDiff line change
@@ -5,51 +5,55 @@
55

66
import React from 'react';
77
import { EuiFlexItem, EuiSpacer } from '@elastic/eui';
8-
import { IComponentField } from '../../../component_types';
98
import { TextField, JsonField, SelectField } from './input_fields';
9+
import { ReactFlowComponent } from '../../../../common';
1010

1111
/**
1212
* A helper component to format all of the input fields for a component. Dynamically
1313
* render based on the input type.
1414
*/
1515

1616
interface InputFieldListProps {
17-
inputFields?: IComponentField[];
17+
selectedComponent: ReactFlowComponent;
1818
}
1919

2020
export function InputFieldList(props: InputFieldListProps) {
21+
const inputFields = props.selectedComponent.data.fields || [];
2122
return (
2223
<EuiFlexItem grow={false}>
23-
{props.inputFields?.map((field, idx) => {
24+
{inputFields.map((field, idx) => {
2425
let el;
2526
switch (field.type) {
2627
case 'string': {
2728
el = (
2829
<EuiFlexItem key={idx}>
2930
<TextField
30-
label={field.label}
31-
placeholder={field.placeholder || ''}
31+
field={field}
32+
componentId={props.selectedComponent.id}
3233
/>
3334
<EuiSpacer size="s" />
3435
</EuiFlexItem>
3536
);
3637
break;
3738
}
38-
case 'json': {
39+
case 'select': {
3940
el = (
4041
<EuiFlexItem key={idx}>
41-
<JsonField
42-
label={field.label}
43-
placeholder={field.placeholder || ''}
42+
<SelectField
43+
field={field}
44+
componentId={props.selectedComponent.id}
4445
/>
4546
</EuiFlexItem>
4647
);
4748
break;
4849
}
49-
case 'select': {
50+
case 'json': {
5051
el = (
5152
<EuiFlexItem key={idx}>
52-
<SelectField />
53+
<JsonField
54+
label={field.label}
55+
placeholder={field.placeholder || ''}
56+
/>
5357
</EuiFlexItem>
5458
);
5559
break;

public/pages/workflow_detail/workspace_component/input_fields/json_field.tsx public/pages/workflow_detail/component_details/input_fields/json_field.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface JsonFieldProps {
1515
* An input field for a component where users manually enter
1616
* in some custom JSON
1717
*/
18+
// TODO: integrate with formik
1819
export function JsonField(props: JsonFieldProps) {
1920
return (
2021
<>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React from 'react';
7+
import {
8+
EuiFormRow,
9+
EuiSuperSelect,
10+
EuiSuperSelectOption,
11+
EuiText,
12+
} from '@elastic/eui';
13+
import { Field, FieldProps, useFormikContext } from 'formik';
14+
import {
15+
IComponentField,
16+
WorkspaceFormValues,
17+
getInitialValue,
18+
isFieldInvalid,
19+
} from '../../../../../common';
20+
21+
// TODO: Should be fetched from global state.
22+
// Need to have a way to determine where to fetch this dynamic data.
23+
const existingIndices = [
24+
{
25+
value: 'index-1',
26+
inputDisplay: <EuiText>my-index-1</EuiText>,
27+
disabled: false,
28+
},
29+
{
30+
value: 'index-2',
31+
inputDisplay: <EuiText>my-index-2</EuiText>,
32+
disabled: false,
33+
},
34+
] as Array<EuiSuperSelectOption<string>>;
35+
36+
interface SelectFieldProps {
37+
field: IComponentField;
38+
componentId: string;
39+
}
40+
41+
/**
42+
* An input field for a component where users select from a list of available
43+
* options.
44+
*/
45+
export function SelectField(props: SelectFieldProps) {
46+
const options = existingIndices;
47+
const formField = `${props.componentId}.${props.field.name}`;
48+
const { errors, touched } = useFormikContext<WorkspaceFormValues>();
49+
50+
return (
51+
<Field name={formField}>
52+
{({ field, form }: FieldProps) => {
53+
return (
54+
<EuiFormRow label={props.field.label}>
55+
<EuiSuperSelect
56+
options={options}
57+
valueOfSelected={field.value || getInitialValue(props.field.type)}
58+
onChange={(option) => {
59+
field.onChange(option);
60+
form.setFieldValue(formField, option);
61+
}}
62+
isInvalid={isFieldInvalid(
63+
props.componentId,
64+
props.field.name,
65+
errors,
66+
touched
67+
)}
68+
/>
69+
</EuiFormRow>
70+
);
71+
}}
72+
</Field>
73+
);
74+
}

0 commit comments

Comments
 (0)