Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add input and output handles to components #49

Merged
merged 4 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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[];
ohltyler marked this conversation as resolved.
Show resolved Hide resolved
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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, aware a lot of the questions I am asking might be not relevant as lots hasn't been decided. Here do we have the processor as an input for the index? I thought we wont be doing that as the processor is an input to the ingest pipeline creation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's undecided, refer to above comments.

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
13 changes: 4 additions & 9 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 Down Expand Up @@ -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
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
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>
);
}
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>
);
}
56 changes: 56 additions & 0 deletions public/pages/workflow_detail/workspace_component/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quick question if we do here the validation if the source class aligns with target class, what is left for the onConnect method where we do validation as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onConnect will handle any other updates we need to do after a connection is made, such as updating properties of the edge itself, or update properties of the node being connected potentially. isValidConnection is just for gatekeeping that a connection is allowed to be made or not.

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('|') || [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you test here if we have a list of 3 output classes, and a list of 3 target classes, just one of them matching is enough?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Also, I was splitting the target handle instead of the source handle. Updated in latest commit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got rid of input/output language since that added some confusion.


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