Skip to content

Commit

Permalink
Add input and output handles to components (#49)
Browse files Browse the repository at this point in the history
* Add input & output handlers and some validation

Signed-off-by: Tyler Ohlsen <[email protected]>

* Clean up IDs & base classes

Signed-off-by: Tyler Ohlsen <[email protected]>

* Clean up isValidConnection

Signed-off-by: Tyler Ohlsen <[email protected]>

* Enforce baseclass in input

Signed-off-by: Tyler Ohlsen <[email protected]>

---------

Signed-off-by: Tyler Ohlsen <[email protected]>
  • Loading branch information
ohltyler authored Oct 4, 2023
1 parent bc60dc0 commit a9113e8
Show file tree
Hide file tree
Showing 14 changed files with 245 additions and 86 deletions.
1 change: 1 addition & 0 deletions common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
export * from './constants';
export * from './interfaces';
export * from '../public/component_types';
export * from '../public/utils';
22 changes: 13 additions & 9 deletions public/component_types/indices/knn_index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -44,7 +41,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: COMPONENT_CLASS.TEXT_EMBEDDING_PROCESSOR,
optional: false,
acceptMultiple: false,
},
];
this.fields = [
{
label: 'Index Name',
Expand Down Expand Up @@ -74,7 +79,6 @@ export class KnnIndex implements IComponent {
];
this.outputs = [
{
id: this.id,
label: this.label,
baseClasses: this.baseClasses,
},
Expand Down
15 changes: 5 additions & 10 deletions public/component_types/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -25,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;
}
Expand All @@ -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
Expand All @@ -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
Expand Down
12 changes: 4 additions & 8 deletions public/component_types/processors/text_embedding_processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -64,7 +61,6 @@ export class TextEmbeddingProcessor implements IComponent {
];
this.outputs = [
{
id: this.id,
label: this.label,
baseClasses: this.baseClasses,
},
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;
}
14 changes: 8 additions & 6 deletions public/pages/workflow_detail/workspace/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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) {
Expand All @@ -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',
},
Expand Down
49 changes: 49 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,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, 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) =>
// @ts-ignore
isValidConnection(connection, reactFlowInstance)
}
style={{
height: 10,
width: 10,
backgroundColor: 'black',
top: position,
}}
/>
</>
</div>
);
}
50 changes: 50 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,50 @@
/*
* 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) =>
// @ts-ignore
isValidConnection(connection, reactFlowInstance)
}
style={{
height: 10,
width: 10,
backgroundColor: 'black',
top: position,
}}
/>
</>
</div>
);
}
60 changes: 60 additions & 0 deletions public/pages/workflow_detail/workspace_component/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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;
}
}

// 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
): boolean {
const sourceHandle = connection.sourceHandle;
const targetHandle = connection.targetHandle;
const targetNodeId = connection.target;

// We store the output classes in a pipe-delimited string. Converting back to a list.
const sourceClasses = sourceHandle?.split('|') || [];
const targetClass = targetHandle || '';

if (sourceClasses?.includes(targetClass)) {
const targetNode = rfInstance.getNode(targetNodeId || '');
if (targetNode) {
const inputConfig = targetNode.data.inputs.find(
(input: IComponentInput) => sourceClasses.includes(input.baseClass)
) as IComponentInput;
const existingEdge = rfInstance
.getEdges()
.find(
(edge) =>
edge.target === targetNodeId && edge.targetHandle === targetHandle
);
if (existingEdge && inputConfig.acceptMultiple === false) {
return false;
}
}
return true;
} else {
return false;
}
}
Loading

0 comments on commit a9113e8

Please sign in to comment.