From 5660f24a4f36bbf12b837cf5624c3246d8a1479a Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Mon, 2 Oct 2023 17:09:12 -0700 Subject: [PATCH 1/4] Add input & output handlers and some validation Signed-off-by: Tyler Ohlsen --- public/component_types/indices/knn_index.ts | 10 +++- .../workspace/reactflow-styles.scss | 6 +- .../workspace_component/input_handle.tsx | 48 +++++++++++++++ .../workspace_component/output_handle.tsx | 49 ++++++++++++++++ .../workspace_component/utils.ts | 58 +++++++++++++++++++ .../workspace_component.tsx | 51 +++++++--------- public/store/reducers/workflows_reducer.ts | 27 +++------ 7 files changed, 197 insertions(+), 52 deletions(-) create mode 100644 public/pages/workflow_detail/workspace_component/input_handle.tsx create mode 100644 public/pages/workflow_detail/workspace_component/output_handle.tsx create mode 100644 public/pages/workflow_detail/workspace_component/utils.ts diff --git a/public/component_types/indices/knn_index.ts b/public/component_types/indices/knn_index.ts index acf81a66..3b390e5a 100644 --- a/public/component_types/indices/knn_index.ts +++ b/public/component_types/indices/knn_index.ts @@ -44,7 +44,15 @@ export class KnnIndex implements IComponent { // that will be referenced/used as input across multiple flows this.allowedFlows = ['Ingest', 'Query', 'Other']; this.baseClasses = [this.type]; - this.inputs = []; + this.inputs = [ + { + id: 'text-embedding-processor', + label: 'Text embedding processor', + baseClass: 'text_embedding_processor', + optional: false, + acceptMultiple: false, + }, + ]; this.fields = [ { label: 'Index Name', diff --git a/public/pages/workflow_detail/workspace/reactflow-styles.scss b/public/pages/workflow_detail/workspace/reactflow-styles.scss index 1f5479b4..9c027fd5 100644 --- a/public/pages/workflow_detail/workspace/reactflow-styles.scss +++ b/public/pages/workflow_detail/workspace/reactflow-styles.scss @@ -10,7 +10,11 @@ } .workspace { - width: 50vh; + width: 80vh; height: 50vh; padding: 0; } + +.workspace-component { + width: 300px; +} diff --git a/public/pages/workflow_detail/workspace_component/input_handle.tsx b/public/pages/workflow_detail/workspace_component/input_handle.tsx new file mode 100644 index 00000000..6a8c4634 --- /dev/null +++ b/public/pages/workflow_detail/workspace_component/input_handle.tsx @@ -0,0 +1,48 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useRef, useEffect, useContext } from 'react'; +import { Connection, Handle, Position } from 'reactflow'; +import { EuiText } from '@elastic/eui'; +import { IComponent, IComponentInput } from '../../../component_types'; +import { calculateHandlePosition, isValidConnection } from './utils'; +import { rfContext } from '../../../store'; + +interface InputHandleProps { + data: IComponent; + input: IComponentInput; +} + +export function InputHandle(props: InputHandleProps) { + const ref = useRef(null); + const { reactFlowInstance } = useContext(rfContext); + const [position, setPosition] = useState(0); + + useEffect(() => { + setPosition(calculateHandlePosition(ref)); + }, [ref]); + + return ( +
+ <> + {props.input.label} + + isValidConnection(connection, reactFlowInstance) + } + style={{ + height: 10, + width: 10, + backgroundColor: 'black', + top: position, + }} + /> + +
+ ); +} diff --git a/public/pages/workflow_detail/workspace_component/output_handle.tsx b/public/pages/workflow_detail/workspace_component/output_handle.tsx new file mode 100644 index 00000000..38f89346 --- /dev/null +++ b/public/pages/workflow_detail/workspace_component/output_handle.tsx @@ -0,0 +1,49 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useRef, useEffect, useContext } from 'react'; +import { Connection, Handle, Position } from 'reactflow'; +import { EuiText } from '@elastic/eui'; +import { IComponent, IComponentOutput } from '../../../component_types'; +import { calculateHandlePosition, isValidConnection } from './utils'; +import { rfContext } from '../../../store'; + +interface OutputHandleProps { + data: IComponent; + output: IComponentOutput; +} + +export function OutputHandle(props: OutputHandleProps) { + const ref = useRef(null); + const { reactFlowInstance } = useContext(rfContext); + const [position, setPosition] = useState(0); + const outputClasses = props.output.baseClasses.join('|'); + + useEffect(() => { + setPosition(calculateHandlePosition(ref)); + }, [ref]); + + return ( +
+ <> + {props.output.label} + + isValidConnection(connection, reactFlowInstance) + } + style={{ + height: 10, + width: 10, + backgroundColor: 'black', + top: position, + }} + /> + +
+ ); +} diff --git a/public/pages/workflow_detail/workspace_component/utils.ts b/public/pages/workflow_detail/workspace_component/utils.ts new file mode 100644 index 00000000..4d9f471c --- /dev/null +++ b/public/pages/workflow_detail/workspace_component/utils.ts @@ -0,0 +1,58 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Connection, ReactFlowInstance } from 'reactflow'; +import { IComponentInput } from '../../../../common'; + +/** + * Collection of utility functions for the workspace component + */ + +// Uses DOM elements to calculate where the handle should be placed +// vertically on the ReactFlow component. offsetTop is the offset relative to the +// parent element, and clientHeight is the element height including padding. +// We can combine them to get the exact amount, in pixels. +export function calculateHandlePosition(ref: any): number { + if (ref.current && ref.current.offsetTop && ref.current.clientHeight) { + return ref.current.offsetTop + ref.current.clientHeight / 2; + } else { + return 0; + } +} + +export function isValidConnection( + connection: Connection, + rfInstance: ReactFlowInstance +): boolean { + 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('|') || []; + + 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 + ); + const existingEdge = rfInstance + .getEdges() + .find((edge) => edge.targetHandle === targetHandle); + if (existingEdge && inputConfig.acceptMultiple === false) { + return false; + } + } + return true; + } else { + return false; + } +} diff --git a/public/pages/workflow_detail/workspace_component/workspace_component.tsx b/public/pages/workflow_detail/workspace_component/workspace_component.tsx index 7c8b8dcf..7e634b30 100644 --- a/public/pages/workflow_detail/workspace_component/workspace_component.tsx +++ b/public/pages/workflow_detail/workspace_component/workspace_component.tsx @@ -4,16 +4,12 @@ */ import React, { useState } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiSpacer, - EuiCard, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiCard } from '@elastic/eui'; import { IComponent } from '../../../component_types'; import { InputFieldList } from './input_field_list'; import { NewOrExistingTabs } from './new_or_existing_tabs'; +import { InputHandle } from './input_handle'; +import { OutputHandle } from './output_handle'; interface WorkspaceComponentProps { data: IComponent; @@ -35,38 +31,31 @@ export function WorkspaceComponent(props: WorkspaceComponentProps) { : component.fields; return ( - + - + {/* {component.allowsCreation ? ( ) : undefined} - + */} + {component.inputs?.map((input, index) => { + 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}; - })} - - + {component.outputs?.map((output, index) => { + return ( + + + + ); + })} ); diff --git a/public/store/reducers/workflows_reducer.ts b/public/store/reducers/workflows_reducer.ts index 4da54a13..314928b9 100644 --- a/public/store/reducers/workflows_reducer.ts +++ b/public/store/reducers/workflows_reducer.ts @@ -20,6 +20,12 @@ const dummyNodes = [ data: new TextEmbeddingProcessor(), type: 'customComponent', }, + { + id: 'text-embedding-processor-2', + position: { x: 0, y: 200 }, + data: new TextEmbeddingProcessor(), + type: 'customComponent', + }, { id: 'knn-index', position: { x: 500, y: 500 }, @@ -28,23 +34,6 @@ const dummyNodes = [ }, ] as ReactFlowComponent[]; -const dummyEdges = [ - { - id: 'e1-2', - source: 'model', - target: 'ingest-pipeline', - style: { - strokeWidth: 2, - stroke: 'black', - }, - markerEnd: { - type: 'arrow', - strokeWidth: 1, - color: 'black', - }, - }, -] as ReactFlowEdge[]; - const initialState = { // TODO: fetch from server-side later workflows: [ @@ -54,7 +43,7 @@ const initialState = { description: 'description for workflow 1', reactFlowState: { nodes: dummyNodes, - edges: dummyEdges, + edges: [] as ReactFlowEdge[], }, template: {}, }, @@ -64,7 +53,7 @@ const initialState = { description: 'description for workflow 2', reactFlowState: { nodes: dummyNodes, - edges: dummyEdges, + edges: [] as ReactFlowEdge[], }, template: {}, }, From 303c70edcddc839b3d4f1e92d17de9735beae7f7 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 3 Oct 2023 09:08:09 -0700 Subject: [PATCH 2/4] Clean up IDs & base classes Signed-off-by: Tyler Ohlsen --- common/index.ts | 1 + public/component_types/indices/knn_index.ts | 14 +++++--------- public/component_types/interfaces.ts | 13 ++++--------- .../processors/text_embedding_processor.ts | 12 ++++-------- .../pages/workflow_detail/workspace/workspace.tsx | 14 ++++++++------ .../workspace_component/input_handle.tsx | 1 + .../workspace_component/output_handle.tsx | 1 + .../workflow_detail/workspace_component/utils.ts | 10 ++++------ public/store/reducers/workflows_reducer.ts | 7 ++++--- public/utils/constants.ts | 5 +++++ public/utils/index.ts | 1 + public/utils/utils.ts | 13 +++++++++++++ 12 files changed, 51 insertions(+), 41 deletions(-) create mode 100644 public/utils/utils.ts 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()}`; +} From 828854215c684b1d9a015bd68db35d8303d0267c Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Wed, 4 Oct 2023 15:23:23 -0700 Subject: [PATCH 3/4] Clean up isValidConnection Signed-off-by: Tyler Ohlsen --- .../workflow_detail/workspace_component/utils.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/public/pages/workflow_detail/workspace_component/utils.ts b/public/pages/workflow_detail/workspace_component/utils.ts index fe968539..fc424e8b 100644 --- a/public/pages/workflow_detail/workspace_component/utils.ts +++ b/public/pages/workflow_detail/workspace_component/utils.ts @@ -32,19 +32,23 @@ 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('|') || []; + const sourceClasses = sourceHandle?.split('|') || []; + const targetClass = targetHandle || ''; - if (outputClasses?.includes(inputClass)) { + if (sourceClasses?.includes(targetClass)) { const targetNode = rfInstance.getNode(targetNodeId || ''); if (targetNode) { const inputConfig = targetNode.data.inputs.find( - (input: IComponentInput) => input.baseClass === inputClass + (input: IComponentInput) => sourceClasses.includes(input.baseClass) ) as IComponentInput; const existingEdge = rfInstance .getEdges() - .find((edge) => edge.targetHandle === targetHandle); + .find( + (edge) => + edge.target === targetNodeId && edge.targetHandle === targetHandle + ); if (existingEdge && inputConfig.acceptMultiple === false) { return false; } From c3e82b3d664bc5a8ea84a1415d21bd08150ac7a2 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Wed, 4 Oct 2023 15:57:27 -0700 Subject: [PATCH 4/4] Enforce baseclass in input Signed-off-by: Tyler Ohlsen --- public/component_types/interfaces.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/component_types/interfaces.ts b/public/component_types/interfaces.ts index 6d2843b0..2abf45d5 100644 --- a/public/component_types/interfaces.ts +++ b/public/component_types/interfaces.ts @@ -22,7 +22,7 @@ export type FieldType = 'string' | 'json' | 'select'; export interface IComponentInput { id: string; label: string; - baseClass: string; + baseClass: COMPONENT_CLASS; optional: boolean; acceptMultiple: boolean; }