diff --git a/public/component_types/base_interfaces.ts b/public/component_types/base_interfaces.ts new file mode 100644 index 00000000..76127dd1 --- /dev/null +++ b/public/component_types/base_interfaces.ts @@ -0,0 +1,85 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { COMPONENT_CATEGORY } from '../utils'; + +/** + * ************ Types ************************** + */ + +// TODO: may change some/all of these to enums later +export type BaseClass = string; +export type UIFlow = string; +export type FieldType = 'string' | 'json' | 'select'; + +/** + * ************ Base interfaces **************** + */ + +/** + * Represents a single base class as an input handle for a component. + * It may be optional. It may also accept multiples of that class. + */ +export interface IComponentInput { + id: string; + label: string; + baseClass: string; + optional: boolean; + acceptMultiple: boolean; +} + +/** + * An input field for a component. Specifies enough configuration for the + * UI node to render it properly within the component (show it as optional, + * put it in advanced settings, placeholder values, etc.) + */ +export interface IComponentField { + label: string; + type: FieldType; + placeholder?: string; + optional?: boolean; + advanced?: boolean; +} + +/** + * Represents the list of base classes as a single output handle for + * a component. + */ +export interface IComponentOutput { + id: string; + label: string; + baseClasses: BaseClass[]; +} + +/** + * The base interface the components will implement. + */ +export interface IComponent { + id: string; + type: BaseClass; + label: string; + description: string; + category: COMPONENT_CATEGORY; + // determines if this component allows for new creation. this means to + // allow a "create" option on the UI component, as well as potentially + // include in the use case template construction ('provisioning' flow) + allowsCreation: boolean; + // determines if this is something that will be included in the use + // case template construction (query or ingest flows). provisioning flow + // is handled by the allowsCreation flag above. + isApplicationStep: boolean; + // the set of allowed flows this component can be drug into the workspace + allowedFlows: UIFlow[]; + // the list of base classes that will be used in the component output + baseClasses?: BaseClass[]; + inputs?: IComponentInput[]; + fields?: IComponentField[]; + // if the component supports creation, we will have a different set of input fields + // the user needs to fill out + createFields?: IComponentField[]; + outputs?: IComponentOutput[]; + // we will need some init function when the component is drug into the workspace + init?(): Promise; +} diff --git a/public/component_types/index.ts b/public/component_types/index.ts new file mode 100644 index 00000000..b732283b --- /dev/null +++ b/public/component_types/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './base_interfaces'; +export * from './processors'; +export * from './indices'; diff --git a/public/component_types/indices/index.ts b/public/component_types/indices/index.ts new file mode 100644 index 00000000..cc5778c9 --- /dev/null +++ b/public/component_types/indices/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './knn_index'; diff --git a/public/component_types/indices/knn_index.ts b/public/component_types/indices/knn_index.ts new file mode 100644 index 00000000..70978071 --- /dev/null +++ b/public/component_types/indices/knn_index.ts @@ -0,0 +1,87 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { COMPONENT_CATEGORY } from '../../utils'; +import { + IComponent, + IComponentField, + IComponentInput, + IComponentOutput, + UIFlow, + BaseClass, +} from '../base_interfaces'; + +/** + * A k-NN index UI component + */ +export class KnnIndex implements IComponent { + id: string; + type: BaseClass; + label: string; + description: string; + category: COMPONENT_CATEGORY; + allowsCreation: boolean; + isApplicationStep: boolean; + allowedFlows: UIFlow[]; + baseClasses: BaseClass[]; + inputs: IComponentInput[]; + fields: IComponentField[]; + createFields: IComponentField[]; + outputs: IComponentOutput[]; + + constructor() { + this.id = 'knn_index'; + this.type = 'knn_index'; + this.label = 'k-NN Index'; + this.description = 'A k-NN Index to be used as a vector store'; + this.category = COMPONENT_CATEGORY.INDICES; + this.allowsCreation = true; + this.isApplicationStep = false; + // TODO: 'other' may not be how this is stored. the idea is 'other' allows + // for placement outside of the ingest or query flows- typically something + // that will be referenced/used as input across multiple flows + this.allowedFlows = ['Ingest', 'Query', 'Other']; + this.baseClasses = [this.type]; + this.inputs = []; + this.fields = [ + { + label: 'Index Name', + type: 'select', + optional: false, + advanced: false, + }, + ]; + this.createFields = [ + { + label: 'Index Name', + type: 'string', + optional: false, + advanced: false, + }, + // we don't need to expose "settings" here since it will be index.knn by default + // just let users customize the mappings + // TODO: figure out how to handle defaults for all of these values. maybe toggle between + // simple form inputs vs. complex JSON editor + { + label: 'Mappings', + type: 'json', + placeholder: 'Enter an index mappings JSON blob...', + optional: false, + advanced: false, + }, + ]; + this.outputs = [ + { + id: this.id, + label: this.label, + baseClasses: this.baseClasses, + }, + ]; + } + + async init(): Promise { + return new KnnIndex(); + } +} diff --git a/public/component_types/processors/index.ts b/public/component_types/processors/index.ts new file mode 100644 index 00000000..364e51bc --- /dev/null +++ b/public/component_types/processors/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './text_embedding_processor'; diff --git a/public/component_types/processors/text_embedding_processor.ts b/public/component_types/processors/text_embedding_processor.ts new file mode 100644 index 00000000..1000bdbe --- /dev/null +++ b/public/component_types/processors/text_embedding_processor.ts @@ -0,0 +1,77 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { COMPONENT_CATEGORY } from '../../utils'; +import { + IComponent, + IComponentField, + IComponentInput, + IComponentOutput, + UIFlow, + BaseClass, +} from '../base_interfaces'; + +/** + * A text embedding processor UI component + */ +export class TextEmbeddingProcessor implements IComponent { + id: string; + type: BaseClass; + label: string; + description: string; + category: COMPONENT_CATEGORY; + allowsCreation: boolean; + isApplicationStep: boolean; + allowedFlows: UIFlow[]; + baseClasses: BaseClass[]; + inputs: IComponentInput[]; + fields: IComponentField[]; + outputs: IComponentOutput[]; + + constructor() { + this.id = 'text_embedding_processor'; + this.type = 'text_embedding_processor'; + this.label = 'Text Embedding Processor'; + this.description = + 'A text embedding ingest processor to be used in an ingest pipeline'; + this.category = COMPONENT_CATEGORY.INGEST_PROCESSORS; + this.allowsCreation = false; + this.isApplicationStep = false; + this.allowedFlows = ['Ingest']; + this.baseClasses = [this.type]; + this.inputs = []; + this.fields = [ + { + label: 'Model ID', + type: 'string', + optional: false, + advanced: false, + }, + { + label: 'Input Field', + type: 'string', + optional: false, + advanced: false, + }, + { + label: 'Output Field', + type: 'string', + optional: false, + advanced: false, + }, + ]; + this.outputs = [ + { + id: this.id, + label: this.label, + baseClasses: this.baseClasses, + }, + ]; + } + + async init(): Promise { + return new TextEmbeddingProcessor(); + } +} diff --git a/public/pages/workflow_builder/components/index.ts b/public/pages/workflow_builder/components/index.ts new file mode 100644 index 00000000..e2edf8bd --- /dev/null +++ b/public/pages/workflow_builder/components/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { TextField } from './text_field'; +export { JsonField } from './json_field'; +export { SelectField } from './select_field'; diff --git a/public/pages/workflow_builder/components/json_field.tsx b/public/pages/workflow_builder/components/json_field.tsx new file mode 100644 index 00000000..1ff51305 --- /dev/null +++ b/public/pages/workflow_builder/components/json_field.tsx @@ -0,0 +1,27 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiText, EuiTextArea } from '@elastic/eui'; + +interface JsonFieldProps { + label: string; + placeholder: string; +} + +/** + * An input field for a component where users select manually enter + * in some custom JSON + */ +export function JsonField(props: JsonFieldProps) { + return ( + <> + + {props.label} + + + + ); +} diff --git a/public/pages/workflow_builder/components/select_field.tsx b/public/pages/workflow_builder/components/select_field.tsx new file mode 100644 index 00000000..eaec1c0f --- /dev/null +++ b/public/pages/workflow_builder/components/select_field.tsx @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { EuiSuperSelect, EuiSuperSelectOption, EuiText } from '@elastic/eui'; + +// 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: my-index-1, + disabled: false, + }, + { + value: 'index-2', + inputDisplay: my-index-2, + disabled: false, + }, +] as Array>; + +/** + * An input field for a component where users select from a list of available + * options. + */ +export function SelectField() { + const options = existingIndices; + + const [selectedOption, setSelectedOption] = useState( + options[0].value + ); + + const onChange = (option: string) => { + setSelectedOption(option); + }; + + return ( + onChange(option)} + /> + ); +} diff --git a/public/pages/workflow_builder/components/text_field.tsx b/public/pages/workflow_builder/components/text_field.tsx new file mode 100644 index 00000000..b22635db --- /dev/null +++ b/public/pages/workflow_builder/components/text_field.tsx @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFieldText } from '@elastic/eui'; + +interface TextFieldProps { + label: string; + placeholder: string; +} + +/** + * An input field for a component where users input plaintext + */ +export function TextField(props: TextFieldProps) { + return ( + + ); +} diff --git a/public/pages/workflow_builder/index.ts b/public/pages/workflow_builder/index.ts index a84bee82..0e54fa8c 100644 --- a/public/pages/workflow_builder/index.ts +++ b/public/pages/workflow_builder/index.ts @@ -4,3 +4,4 @@ */ export { WorkflowBuilder } from './workflow_builder'; +export { WorkflowComponent } from './workflow_component'; diff --git a/public/pages/workflow_builder/workflow_builder.tsx b/public/pages/workflow_builder/workflow_builder.tsx index d50759ac..645f9129 100644 --- a/public/pages/workflow_builder/workflow_builder.tsx +++ b/public/pages/workflow_builder/workflow_builder.tsx @@ -9,11 +9,16 @@ import { EuiFlexGroup, EuiFlexItem, EuiTitle, - EuiText, EuiSpacer, } from '@elastic/eui'; import { BREADCRUMBS } from '../../utils'; import { getCore } from '../../services'; +import { + TextEmbeddingProcessor, + IComponent, + KnnIndex, +} from '../../component_types'; +import { WorkflowComponent } from './workflow_component'; export function WorkflowBuilder() { useEffect(() => { @@ -23,6 +28,12 @@ export function WorkflowBuilder() { ]); }); + // TODO: Should be fetched from global state. Using some defaults for testing purposes + const curComponents = [ + new TextEmbeddingProcessor(), + new KnnIndex(), + ] as IComponent[]; + return (
@@ -35,9 +46,15 @@ export function WorkflowBuilder() { - - Placeholder for workflow builder page... - + + {curComponents.map((component, idx) => { + return ( + + + + ); + })} +
); } diff --git a/public/pages/workflow_builder/workflow_component.tsx b/public/pages/workflow_builder/workflow_component.tsx new file mode 100644 index 00000000..11cb5e20 --- /dev/null +++ b/public/pages/workflow_builder/workflow_component.tsx @@ -0,0 +1,131 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiSpacer, + EuiCard, + EuiTab, + EuiTabs, +} from '@elastic/eui'; +import { IComponent } from '../../component_types'; +import { JsonField, SelectField, TextField } from './components'; + +interface WorkflowComponentProps { + component: IComponent; +} + +const inputTabs = [ + { + id: 'existing', + name: 'Existing', + disabled: false, + }, + { + id: 'new', + name: 'New', + disabled: false, + }, +]; + +/** + * TODO: This will be the ReactFlow node in the drag-and-drop workspace. It will take in a component + * from the global workflow state and render it appropriately (inputs / params / outputs / etc.) + * Similar to Flowise's CanvasNode - see + * https://github.com/FlowiseAI/Flowise/blob/main/packages/ui/src/views/canvas/CanvasNode.js + */ +export function WorkflowComponent(props: WorkflowComponentProps) { + const { component } = props; + + const [selectedTabId, setSelectedTabId] = useState('existing'); + + const onSelectedTabChanged = (id: string) => { + setSelectedTabId(id); + }; + + const isCreatingNew = component.allowsCreation && selectedTabId === 'new'; + const fieldsToDisplay = isCreatingNew + ? component.createFields + : component.fields; + + return ( + + + + {component.allowsCreation ? ( + + {inputTabs.map((tab, idx) => { + return ( + onSelectedTabChanged(tab.id)} + isSelected={tab.id === selectedTabId} + disabled={tab.disabled} + key={idx} + > + {tab.name} + + ); + })} + + ) : undefined} + + + {fieldsToDisplay?.map((field, idx) => { + if (field.type === 'string') { + return ( + + + + + ); + } else if (field.type === 'json') { + return ( + + + + ); + } else if (field.type === 'select') { + return ( + + + + ); + } + })} + + {/** + * Hardcoding the interfaced inputs/outputs for readability + * TODO: remove when moving this into the context of a ReactFlow node with Handles. + */} + + <> + + Inputs: + + {component.inputs?.map((input, idx) => { + return {input.label}; + })} + + + Outputs: + + {component.outputs?.map((output, idx) => { + return {output.label}; + })} + + + + + ); +} diff --git a/public/utils/constants.ts b/public/utils/constants.ts index 8db2f6dd..b20b36d9 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -28,3 +28,8 @@ export const BREADCRUMBS = Object.freeze({ href: `#${APP_PATH.WORKFLOW_BUILDER}`, }, }); + +export enum COMPONENT_CATEGORY { + INGEST_PROCESSORS = 'Ingest Processors', + INDICES = 'Indices', +}