diff --git a/common/index.ts b/common/index.ts index 483a6933..a78ce423 100644 --- a/common/index.ts +++ b/common/index.ts @@ -6,3 +6,4 @@ export * from './constants'; export * from './interfaces'; export * from '../public/component_types'; +export * from '../public/utils'; diff --git a/public/component_types/indices/knn_index.ts b/public/component_types/indices/knn_index.ts index 3b390e5a..9255b47d 100644 --- a/public/component_types/indices/knn_index.ts +++ b/public/component_types/indices/knn_index.ts @@ -3,37 +3,34 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { COMPONENT_CATEGORY } from '../../utils'; +import { COMPONENT_CATEGORY, COMPONENT_CLASS } from '../../utils'; import { IComponent, IComponentField, IComponentInput, IComponentOutput, UIFlow, - BaseClass, } from '../interfaces'; /** * A k-NN index UI component */ export class KnnIndex implements IComponent { - id: string; - type: BaseClass; + type: COMPONENT_CLASS; label: string; description: string; category: COMPONENT_CATEGORY; allowsCreation: boolean; isApplicationStep: boolean; allowedFlows: UIFlow[]; - baseClasses: BaseClass[]; + baseClasses: COMPONENT_CLASS[]; inputs: IComponentInput[]; fields: IComponentField[]; createFields: IComponentField[]; outputs: IComponentOutput[]; constructor() { - this.id = 'knn_index'; - this.type = 'knn_index'; + this.type = COMPONENT_CLASS.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; @@ -48,7 +45,7 @@ export class KnnIndex implements IComponent { { id: 'text-embedding-processor', label: 'Text embedding processor', - baseClass: 'text_embedding_processor', + baseClass: COMPONENT_CLASS.TEXT_EMBEDDING_PROCESSOR, optional: false, acceptMultiple: false, }, @@ -82,7 +79,6 @@ export class KnnIndex implements IComponent { ]; this.outputs = [ { - id: this.id, label: this.label, baseClasses: this.baseClasses, }, diff --git a/public/component_types/interfaces.ts b/public/component_types/interfaces.ts index 676f9824..6d2843b0 100644 --- a/public/component_types/interfaces.ts +++ b/public/component_types/interfaces.ts @@ -3,14 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { COMPONENT_CATEGORY } from '../utils'; +import { COMPONENT_CATEGORY, COMPONENT_CLASS } 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'; @@ -48,17 +45,15 @@ export interface IComponentField { * a component. */ export interface IComponentOutput { - id: string; label: string; - baseClasses: BaseClass[]; + baseClasses: COMPONENT_CLASS[]; } /** * The base interface the components will implement. */ export interface IComponent { - id: string; - type: BaseClass; + type: COMPONENT_CLASS; label: string; description: string; // will be used for grouping together in the drag-and-drop component library @@ -74,7 +69,7 @@ export interface IComponent { // 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[]; + baseClasses?: COMPONENT_CLASS[]; inputs?: IComponentInput[]; fields?: IComponentField[]; // if the component supports creation, we will have a different set of input fields diff --git a/public/component_types/processors/text_embedding_processor.ts b/public/component_types/processors/text_embedding_processor.ts index 539eb3aa..2c3fa078 100644 --- a/public/component_types/processors/text_embedding_processor.ts +++ b/public/component_types/processors/text_embedding_processor.ts @@ -3,36 +3,33 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { COMPONENT_CATEGORY } from '../../utils'; +import { COMPONENT_CATEGORY, COMPONENT_CLASS } from '../../utils'; import { IComponent, IComponentField, IComponentInput, IComponentOutput, UIFlow, - BaseClass, } from '../interfaces'; /** * A text embedding processor UI component */ export class TextEmbeddingProcessor implements IComponent { - id: string; - type: BaseClass; + type: COMPONENT_CLASS; label: string; description: string; category: COMPONENT_CATEGORY; allowsCreation: boolean; isApplicationStep: boolean; allowedFlows: UIFlow[]; - baseClasses: BaseClass[]; + baseClasses: COMPONENT_CLASS[]; inputs: IComponentInput[]; fields: IComponentField[]; outputs: IComponentOutput[]; constructor() { - this.id = 'text_embedding_processor'; - this.type = 'text_embedding_processor'; + this.type = COMPONENT_CLASS.TEXT_EMBEDDING_PROCESSOR; this.label = 'Text Embedding Processor'; this.description = 'A text embedding ingest processor to be used in an ingest pipeline'; @@ -64,7 +61,6 @@ export class TextEmbeddingProcessor implements IComponent { ]; this.outputs = [ { - id: this.id, label: this.label, baseClasses: this.baseClasses, }, diff --git a/public/pages/workflow_detail/workspace/workspace.tsx b/public/pages/workflow_detail/workspace/workspace.tsx index 56acab1e..1acd61a2 100644 --- a/public/pages/workflow_detail/workspace/workspace.tsx +++ b/public/pages/workflow_detail/workspace/workspace.tsx @@ -13,7 +13,8 @@ import ReactFlow, { } from 'reactflow'; import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { rfContext } from '../../../store'; -import { Workflow } from '../../../../common'; +import { IComponent, Workflow } from '../../../../common'; +import { generateId } from '../../../utils'; import { getCore } from '../../../services'; import { WorkspaceComponent } from '../workspace_component'; @@ -54,7 +55,9 @@ export function Workspace(props: WorkspaceProps) { (event) => { event.preventDefault(); // Get the node info from the event metadata - const nodeData = event.dataTransfer.getData('application/reactflow'); + const nodeData = event.dataTransfer.getData( + 'application/reactflow' + ) as IComponent; // check if the dropped element is valid if (typeof nodeData === 'undefined' || !nodeData) { @@ -72,13 +75,12 @@ export function Workspace(props: WorkspaceProps) { }); // TODO: remove hardcoded values when more component info is passed in the event. - // Only keep the calculated 'positioning' field. + // Only keep the calculated 'position' field. const newNode = { - // TODO: generate ID based on the node data maybe - id: Date.now().toFixed(), + id: generateId(nodeData.type), type: nodeData.type, position, - data: { label: nodeData.label }, + data: nodeData, style: { background: 'white', }, diff --git a/public/pages/workflow_detail/workspace_component/input_handle.tsx b/public/pages/workflow_detail/workspace_component/input_handle.tsx index 6a8c4634..d4f87444 100644 --- a/public/pages/workflow_detail/workspace_component/input_handle.tsx +++ b/public/pages/workflow_detail/workspace_component/input_handle.tsx @@ -33,6 +33,7 @@ export function InputHandle(props: InputHandleProps) { id={props.input.baseClass} position={Position.Left} isValidConnection={(connection: Connection) => + // @ts-ignore isValidConnection(connection, reactFlowInstance) } style={{ diff --git a/public/pages/workflow_detail/workspace_component/output_handle.tsx b/public/pages/workflow_detail/workspace_component/output_handle.tsx index 38f89346..92d8ef68 100644 --- a/public/pages/workflow_detail/workspace_component/output_handle.tsx +++ b/public/pages/workflow_detail/workspace_component/output_handle.tsx @@ -34,6 +34,7 @@ export function OutputHandle(props: OutputHandleProps) { id={outputClasses} position={Position.Right} isValidConnection={(connection: Connection) => + // @ts-ignore isValidConnection(connection, reactFlowInstance) } style={{ diff --git a/public/pages/workflow_detail/workspace_component/utils.ts b/public/pages/workflow_detail/workspace_component/utils.ts index 4d9f471c..fe968539 100644 --- a/public/pages/workflow_detail/workspace_component/utils.ts +++ b/public/pages/workflow_detail/workspace_component/utils.ts @@ -22,6 +22,9 @@ export function calculateHandlePosition(ref: any): number { } } +// Validates that connections can only be made when the source and target classes align, and +// that multiple connections to the same target handle are not allowed unless the input configuration +// for that particular component allows for it. export function isValidConnection( connection: Connection, rfInstance: ReactFlowInstance @@ -29,7 +32,6 @@ export function isValidConnection( const sourceHandle = connection.sourceHandle; const targetHandle = connection.targetHandle; const targetNodeId = connection.target; - const inputClass = sourceHandle || ''; // We store the output classes in a pipe-delimited string. Converting back to a list. const outputClasses = targetHandle?.split('|') || []; @@ -37,13 +39,9 @@ export function isValidConnection( if (outputClasses?.includes(inputClass)) { const targetNode = rfInstance.getNode(targetNodeId || ''); if (targetNode) { - // We pull out the relevant IComponentInput config, and check if it allows multiple connections. - // We also check the existing edges in the ReactFlow state. - // If there is an existing edge, and we don't allow multiple, we don't allow this connection. - // For all other scenarios, we allow the connection. const inputConfig = targetNode.data.inputs.find( (input: IComponentInput) => input.baseClass === inputClass - ); + ) as IComponentInput; const existingEdge = rfInstance .getEdges() .find((edge) => edge.targetHandle === targetHandle); diff --git a/public/store/reducers/workflows_reducer.ts b/public/store/reducers/workflows_reducer.ts index 314928b9..77d43b94 100644 --- a/public/store/reducers/workflows_reducer.ts +++ b/public/store/reducers/workflows_reducer.ts @@ -10,24 +10,25 @@ import { ReactFlowEdge, KnnIndex, TextEmbeddingProcessor, + generateId, } from '../../../common'; // TODO: remove after fetching from server-side const dummyNodes = [ { - id: 'text-embedding-processor', + id: generateId('text_embedding_processor'), position: { x: 0, y: 500 }, data: new TextEmbeddingProcessor(), type: 'customComponent', }, { - id: 'text-embedding-processor-2', + id: generateId('text_embedding_processor'), position: { x: 0, y: 200 }, data: new TextEmbeddingProcessor(), type: 'customComponent', }, { - id: 'knn-index', + id: generateId('knn_index'), position: { x: 500, y: 500 }, data: new KnnIndex(), type: 'customComponent', diff --git a/public/utils/constants.ts b/public/utils/constants.ts index 10b5eb82..35ccc224 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -27,3 +27,8 @@ export enum COMPONENT_CATEGORY { INGEST_PROCESSORS = 'Ingest Processors', INDICES = 'Indices', } + +export enum COMPONENT_CLASS { + KNN_INDEX = 'knn_index', + TEXT_EMBEDDING_PROCESSOR = 'text_embedding_processor', +} diff --git a/public/utils/index.ts b/public/utils/index.ts index 2e209c79..bcc8722e 100644 --- a/public/utils/index.ts +++ b/public/utils/index.ts @@ -4,3 +4,4 @@ */ export * from './constants'; +export * from './utils'; diff --git a/public/utils/utils.ts b/public/utils/utils.ts new file mode 100644 index 00000000..1304c5cf --- /dev/null +++ b/public/utils/utils.ts @@ -0,0 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Append 16 random characters +export function generateId(prefix: string) { + const uniqueChar = () => { + // eslint-disable-next-line no-bitwise + return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); + }; + return `${prefix}_${uniqueChar()}${uniqueChar()}${uniqueChar()}${uniqueChar()}`; +}