Skip to content

Commit

Permalink
Persist form state and validation in Workspace (#61)
Browse files Browse the repository at this point in the history
Signed-off-by: Tyler Ohlsen <[email protected]>
  • Loading branch information
ohltyler authored Oct 23, 2023
1 parent 52e5cec commit 10810e3
Show file tree
Hide file tree
Showing 26 changed files with 650 additions and 244 deletions.
2 changes: 1 addition & 1 deletion common/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { Node, Edge } from 'reactflow';
import { IComponent as IComponentData } from '../public/component_types';
import { IComponentData } from '../public/component_types';

export type Index = {
name: string;
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
]
},
"dependencies": {
"reactflow": "^11.8.3"
"formik": "2.4.2",
"reactflow": "^11.8.3",
"yup": "^1.3.2"
},
"devDependencies": {
"pre-commit": "^1.2.2"
Expand Down
3 changes: 3 additions & 0 deletions public/component_types/indices/knn_index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export class KnnIndex extends BaseComponent implements IComponent {
this.fields = [
{
label: 'Index Name',
name: 'indexName',
type: 'select',
optional: false,
advanced: false,
Expand All @@ -63,6 +64,7 @@ export class KnnIndex extends BaseComponent implements IComponent {
this.createFields = [
{
label: 'Index Name',
name: 'indexName',
type: 'string',
optional: false,
advanced: false,
Expand All @@ -73,6 +75,7 @@ export class KnnIndex extends BaseComponent implements IComponent {
// simple form inputs vs. complex JSON editor
{
label: 'Mappings',
name: 'indexMappings',
type: 'json',
placeholder: 'Enter an index mappings JSON blob...',
optional: false,
Expand Down
22 changes: 17 additions & 5 deletions public/component_types/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,26 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { FormikValues } from 'formik';
import { ObjectSchema } from 'yup';
import { COMPONENT_CATEGORY, COMPONENT_CLASS } from '../utils';

/**
* ************ Types **************************
* ************ Types *************************
*/
export type UIFlow = string;
export type FieldType = 'string' | 'json' | 'select';

/**
* ************ Base interfaces ****************
*/
// TODO: this may expand to more types in the future. Formik supports 'any' so we can too.
// For now, limiting scope to expected types.
export type FieldValue = string | {};
export type ComponentFormValues = FormikValues;
export type WorkspaceFormValues = {
[componentId: string]: ComponentFormValues;
};
export type WorkspaceSchemaObj = {
[componentId: string]: ObjectSchema<any, any, any>;
};
export type WorkspaceSchema = ObjectSchema<WorkspaceSchemaObj>;

/**
* Represents a single base class as an input handle for a component.
Expand All @@ -35,6 +44,8 @@ export interface IComponentInput {
export interface IComponentField {
label: string;
type: FieldType;
name: string;
value?: FieldValue;
placeholder?: string;
optional?: boolean;
advanced?: boolean;
Expand Down Expand Up @@ -84,4 +95,5 @@ export interface IComponent {
*/
export interface IComponentData extends IComponent {
id: string;
selected?: boolean;
}
3 changes: 3 additions & 0 deletions public/component_types/processors/text_embedding_processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,21 @@ export class TextEmbeddingProcessor
this.fields = [
{
label: 'Model ID',
name: 'modelId',
type: 'string',
optional: false,
advanced: false,
},
{
label: 'Input Field',
name: 'inputField',
type: 'string',
optional: false,
advanced: false,
},
{
label: 'Output Field',
name: 'outputField',
type: 'string',
optional: false,
advanced: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { ReactFlowComponent } from '../../../../common';
import { ComponentInputs } from './component_inputs';
import { EmptyComponentInputs } from './empty_component_inputs';

// styling
import '../workspace/workspace-styles.scss';

interface ComponentDetailsProps {
selectedComponent?: ReactFlowComponent;
}

/**
* A panel that will be nested in a resizable container to dynamically show
* the details and user-required inputs based on the selected component
* in the flow workspace.
*/
export function ComponentDetails(props: ComponentDetailsProps) {
return (
<EuiFlexGroup
direction="column"
gutterSize="none"
className="workspace-panel"
>
<EuiFlexItem>
<EuiPanel paddingSize="m">
{props.selectedComponent ? (
<ComponentInputs selectedComponent={props.selectedComponent} />
) : (
<EmptyComponentInputs />
)}
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { EuiSpacer, EuiTitle } from '@elastic/eui';
import { InputFieldList } from './input_field_list';
import { ReactFlowComponent } from '../../../../common';

interface ComponentInputsProps {
selectedComponent: ReactFlowComponent;
}

export function ComponentInputs(props: ComponentInputsProps) {
return (
<>
<EuiTitle size="m">
<h2>{props.selectedComponent.data.label || ''}</h2>
</EuiTitle>
<EuiSpacer size="s" />
<InputFieldList selectedComponent={props.selectedComponent} />
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { EuiEmptyPrompt, EuiText } from '@elastic/eui';

export function EmptyComponentInputs() {
return (
<EuiEmptyPrompt
iconType={'cross'}
title={<h2>No component selected</h2>}
titleSize="s"
body={
<>
<EuiText>
Add a component, or select a component to view or edit its
configuration.
</EuiText>
</>
}
/>
);
}
6 changes: 6 additions & 0 deletions public/pages/workflow_detail/component_details/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export * from './component_details';
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,55 @@

import React from 'react';
import { EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { IComponentField } from '../../../component_types';
import { TextField, JsonField, SelectField } from './input_fields';
import { ReactFlowComponent } from '../../../../common';

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

interface InputFieldListProps {
inputFields?: IComponentField[];
selectedComponent: ReactFlowComponent;
}

export function InputFieldList(props: InputFieldListProps) {
const inputFields = props.selectedComponent.data.fields || [];
return (
<EuiFlexItem grow={false}>
{props.inputFields?.map((field, idx) => {
{inputFields.map((field, idx) => {
let el;
switch (field.type) {
case 'string': {
el = (
<EuiFlexItem key={idx}>
<TextField
label={field.label}
placeholder={field.placeholder || ''}
field={field}
componentId={props.selectedComponent.id}
/>
<EuiSpacer size="s" />
</EuiFlexItem>
);
break;
}
case 'json': {
case 'select': {
el = (
<EuiFlexItem key={idx}>
<JsonField
label={field.label}
placeholder={field.placeholder || ''}
<SelectField
field={field}
componentId={props.selectedComponent.id}
/>
</EuiFlexItem>
);
break;
}
case 'select': {
case 'json': {
el = (
<EuiFlexItem key={idx}>
<SelectField />
<JsonField
label={field.label}
placeholder={field.placeholder || ''}
/>
</EuiFlexItem>
);
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface JsonFieldProps {
* An input field for a component where users manually enter
* in some custom JSON
*/
// TODO: integrate with formik
export function JsonField(props: JsonFieldProps) {
return (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import {
EuiFormRow,
EuiSuperSelect,
EuiSuperSelectOption,
EuiText,
} from '@elastic/eui';
import { Field, FieldProps, useFormikContext } from 'formik';
import {
IComponentField,
WorkspaceFormValues,
getInitialValue,
isFieldInvalid,
} from '../../../../../common';

// TODO: Should be fetched from global state.
// Need to have a way to determine where to fetch this dynamic data.
const existingIndices = [
{
value: 'index-1',
inputDisplay: <EuiText>my-index-1</EuiText>,
disabled: false,
},
{
value: 'index-2',
inputDisplay: <EuiText>my-index-2</EuiText>,
disabled: false,
},
] as Array<EuiSuperSelectOption<string>>;

interface SelectFieldProps {
field: IComponentField;
componentId: string;
}

/**
* An input field for a component where users select from a list of available
* options.
*/
export function SelectField(props: SelectFieldProps) {
const options = existingIndices;
const formField = `${props.componentId}.${props.field.name}`;
const { errors, touched } = useFormikContext<WorkspaceFormValues>();

return (
<Field name={formField}>
{({ field, form }: FieldProps) => {
return (
<EuiFormRow label={props.field.label}>
<EuiSuperSelect
options={options}
valueOfSelected={field.value || getInitialValue(props.field.type)}
onChange={(option) => {
field.onChange(option);
form.setFieldValue(formField, option);
}}
isInvalid={isFieldInvalid(
props.componentId,
props.field.name,
errors,
touched
)}
/>
</EuiFormRow>
);
}}
</Field>
);
}
Loading

0 comments on commit 10810e3

Please sign in to comment.