Skip to content

Commit

Permalink
Add input & output handlers and some validation
Browse files Browse the repository at this point in the history
Signed-off-by: Tyler Ohlsen <[email protected]>
  • Loading branch information
ohltyler committed Oct 3, 2023
1 parent bc60dc0 commit 5660f24
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 52 deletions.
10 changes: 9 additions & 1 deletion public/component_types/indices/knn_index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 5 additions & 1 deletion public/pages/workflow_detail/workspace/reactflow-styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
}

.workspace {
width: 50vh;
width: 80vh;
height: 50vh;
padding: 0;
}

.workspace-component {
width: 300px;
}
48 changes: 48 additions & 0 deletions public/pages/workflow_detail/workspace_component/input_handle.tsx
Original file line number Diff line number Diff line change
@@ -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<number>(0);

useEffect(() => {
setPosition(calculateHandlePosition(ref));
}, [ref]);

return (
<div ref={ref}>
<>
<EuiText textAlign="left">{props.input.label}</EuiText>
<Handle
type="target"
id={props.input.baseClass}
position={Position.Left}
isValidConnection={(connection: Connection) =>
isValidConnection(connection, reactFlowInstance)
}
style={{
height: 10,
width: 10,
backgroundColor: 'black',
top: position,
}}
/>
</>
</div>
);
}
49 changes: 49 additions & 0 deletions public/pages/workflow_detail/workspace_component/output_handle.tsx
Original file line number Diff line number Diff line change
@@ -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<number>(0);
const outputClasses = props.output.baseClasses.join('|');

useEffect(() => {
setPosition(calculateHandlePosition(ref));
}, [ref]);

return (
<div ref={ref}>
<>
<EuiText textAlign="right">{props.output.label}</EuiText>
<Handle
type="source"
id={outputClasses}
position={Position.Right}
isValidConnection={(connection: Connection) =>
isValidConnection(connection, reactFlowInstance)
}
style={{
height: 10,
width: 10,
backgroundColor: 'black',
top: position,
}}
/>
</>
</div>
);
}
58 changes: 58 additions & 0 deletions public/pages/workflow_detail/workspace_component/utils.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,38 +31,31 @@ export function WorkspaceComponent(props: WorkspaceComponentProps) {
: component.fields;

return (
<EuiCard title={component.label} style={{ maxWidth: '40vh' }}>
<EuiCard title={component.label} className="workspace-component">
<EuiFlexGroup direction="column">
<EuiFlexItem>
{/* <EuiFlexItem>
{component.allowsCreation ? (
<NewOrExistingTabs
setSelectedTabId={setSelectedTabId}
selectedTabId={selectedTabId}
/>
) : undefined}
</EuiFlexItem>
</EuiFlexItem> */}
{component.inputs?.map((input, index) => {
return (
<EuiFlexItem key={index}>
<InputHandle input={input} data={component} />
</EuiFlexItem>
);
})}
<InputFieldList inputFields={fieldsToDisplay} />
{/**
* Hardcoding the interfaced inputs/outputs for readability
* TODO: remove when moving this into the context of a ReactFlow node with Handles.
*/}
<EuiFlexItem>
<>
<EuiText>
<b>Inputs:</b>
</EuiText>
{component.inputs?.map((input, idx) => {
return <EuiText key={idx}>{input.label}</EuiText>;
})}
<EuiSpacer size="s" />
<EuiText>
<b>Outputs:</b>
</EuiText>
{component.outputs?.map((output, idx) => {
return <EuiText key={idx}>{output.label}</EuiText>;
})}
</>
</EuiFlexItem>
{component.outputs?.map((output, index) => {
return (
<EuiFlexItem key={index}>
<OutputHandle output={output} data={component} />
</EuiFlexItem>
);
})}
</EuiFlexGroup>
</EuiCard>
);
Expand Down
27 changes: 8 additions & 19 deletions public/store/reducers/workflows_reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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: [
Expand All @@ -54,7 +43,7 @@ const initialState = {
description: 'description for workflow 1',
reactFlowState: {
nodes: dummyNodes,
edges: dummyEdges,
edges: [] as ReactFlowEdge[],
},
template: {},
},
Expand All @@ -64,7 +53,7 @@ const initialState = {
description: 'description for workflow 2',
reactFlowState: {
nodes: dummyNodes,
edges: dummyEdges,
edges: [] as ReactFlowEdge[],
},
template: {},
},
Expand Down

0 comments on commit 5660f24

Please sign in to comment.