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

Persist form state and validation in Workspace #61

Merged
merged 8 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
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',
ohltyler marked this conversation as resolved.
Show resolved Hide resolved
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
ohltyler marked this conversation as resolved.
Show resolved Hide resolved
{
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;
amitgalitz marked this conversation as resolved.
Show resolved Hide resolved
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;
}
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 = [
amitgalitz marked this conversation as resolved.
Show resolved Hide resolved
{
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
Loading