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

[Backport 2.x] Clean up obj serialization; set up save workflow stub fns #56

Merged
merged 1 commit into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 11 additions & 6 deletions common/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type ReactFlowViewport = {
zoom: number;
};

export type ReactFlowState = {
export type WorkspaceFlowState = {
nodes: ReactFlowComponent[];
edges: ReactFlowEdge[];
viewport?: ReactFlowViewport;
Expand All @@ -38,23 +38,23 @@ export type ReactFlowState = {
********** USE CASE TEMPLATE TYPES/INTERFACES **********
*/

type TemplateNode = {
export type TemplateNode = {
id: string;
inputs: {};
};

type TemplateEdge = {
export type TemplateEdge = {
source: string;
target: string;
};

type TemplateFlow = {
export type TemplateFlow = {
userParams: {};
nodes: TemplateNode[];
edges: TemplateEdge[];
};

type TemplateFlows = {
export type TemplateFlows = {
provision: TemplateFlow;
ingest: TemplateFlow;
query: TemplateFlow;
Expand All @@ -73,7 +73,12 @@ export type Workflow = {
name: string;
description?: string;
// ReactFlow state may not exist if a workflow is created via API/backend-only.
reactFlowState?: ReactFlowState;
workspaceFlowState?: WorkspaceFlowState;
template: UseCaseTemplate;
lastUpdated: number;
};

export enum USE_CASE {
SEMANTIC_SEARCH = 'semantic_search',
CUSTOM = 'custom',
}
15 changes: 15 additions & 0 deletions public/component_types/base_component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

/**
* A base component class.
*/
export abstract class BaseComponent {
// Persist a standard toObj() fn that all component classes can use. This is necessary
// so we have standard JS Object when serializing comoponent state in redux.
toObj() {
return Object.assign({}, this);
}
}
4 changes: 3 additions & 1 deletion public/component_types/indices/knn_index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { COMPONENT_CATEGORY, COMPONENT_CLASS } from '../../utils';
import { BaseComponent } from '../base_component';
import {
IComponent,
IComponentField,
Expand All @@ -15,7 +16,7 @@ import {
/**
* A k-NN index UI component
*/
export class KnnIndex implements IComponent {
export class KnnIndex extends BaseComponent implements IComponent {
type: COMPONENT_CLASS;
label: string;
description: string;
Expand All @@ -30,6 +31,7 @@ export class KnnIndex implements IComponent {
outputs: IComponentOutput[];

constructor() {
super();
this.type = COMPONENT_CLASS.KNN_INDEX;
this.label = 'k-NN Index';
this.description = 'A k-NN Index to be used as a vector store';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { COMPONENT_CATEGORY, COMPONENT_CLASS } from '../../utils';
import { BaseComponent } from '../base_component';
import {
IComponent,
IComponentField,
Expand All @@ -15,7 +16,9 @@ import {
/**
* A text embedding processor UI component
*/
export class TextEmbeddingProcessor implements IComponent {
export class TextEmbeddingProcessor
extends BaseComponent
implements IComponent {
type: COMPONENT_CLASS;
label: string;
description: string;
Expand All @@ -29,6 +32,7 @@ export class TextEmbeddingProcessor implements IComponent {
outputs: IComponentOutput[];

constructor() {
super();
this.type = COMPONENT_CLASS.TEXT_EMBEDDING_PROCESSOR;
this.label = 'Text Embedding Processor';
this.description =
Expand Down
19 changes: 17 additions & 2 deletions public/pages/workflow_detail/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import React, { useContext } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { EuiPageHeader, EuiButton } from '@elastic/eui';
import { Workflow } from '../../../../common';
import { saveWorkflow } from '../utils';
import { rfContext, AppState, removeDirty } from '../../../store';

interface WorkflowDetailHeaderProps {
workflow?: Workflow;
}

export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) {
const dispatch = useDispatch();
const { reactFlowInstance } = useContext(rfContext);
const isDirty = useSelector((state: AppState) => state.workspace.isDirty);

return (
<EuiPageHeader
pageTitle={props.workflow ? props.workflow.name : ''}
Expand All @@ -20,7 +27,15 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) {
<EuiButton fill={false} onClick={() => {}}>
Prototype
</EuiButton>,
<EuiButton fill={false} onClick={() => {}}>
<EuiButton
fill={false}
disabled={!props.workflow || !isDirty}
onClick={() => {
// @ts-ignore
saveWorkflow(props.workflow, reactFlowInstance);
dispatch(removeDirty());
}}
>
Save
</EuiButton>,
]}
Expand Down
6 changes: 6 additions & 0 deletions public/pages/workflow_detail/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export * from './utils';
92 changes: 92 additions & 0 deletions public/pages/workflow_detail/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import {
WorkspaceFlowState,
UseCaseTemplate,
Workflow,
USE_CASE,
ReactFlowComponent,
} from '../../../../common';

export function saveWorkflow(workflow: Workflow, rfInstance: any): void {
let curFlowState = rfInstance.toObject();

curFlowState = {
...curFlowState,
nodes: processNodes(curFlowState.nodes),
};

const isValid = validateFlowState(curFlowState);
if (isValid) {
const updatedWorkflow = {
...workflow,
workspaceFlowState: curFlowState,
template: generateUseCaseTemplate(curFlowState),
} as Workflow;
if (workflow.id) {
// TODO: implement connection to update workflow API
} else {
// TODO: implement connection to create workflow API
}
} else {
return;
}
}

// TODO: implement this. Need more info on UX side to finalize what we need
// to persist, what validation to do, etc.
// Note we don't have to validate connections since that is done via input/output handlers.
function validateFlowState(flowState: WorkspaceFlowState): boolean {
return true;
}

// TODO: implement this
function generateUseCaseTemplate(
flowState: WorkspaceFlowState
): UseCaseTemplate {
return {
name: 'example-name',
description: 'example description',
type: USE_CASE.SEMANTIC_SEARCH,
userInputs: {},
workflows: {
provision: {
userParams: {},
nodes: [],
edges: [],
},
ingest: {
userParams: {},
nodes: [],
edges: [],
},
query: {
userParams: {},
nodes: [],
edges: [],
},
},
} as UseCaseTemplate;
}

// Process the raw ReactFlow nodes to only persist the fields we need
function processNodes(nodes: ReactFlowComponent[]): ReactFlowComponent[] {
return nodes
.map((node: ReactFlowComponent) => {
return Object.fromEntries(
['id', 'data', 'type', 'width', 'height'].map((key: string) => [
key,
node[key],
])
) as ReactFlowComponent;
})
.map((node: ReactFlowComponent) => {
return {
...node,
selected: false,
};
});
}
12 changes: 8 additions & 4 deletions public/pages/workflow_detail/workspace/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import React, { useRef, useContext, useCallback, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import ReactFlow, {
Controls,
Background,
Expand All @@ -12,7 +13,7 @@ import ReactFlow, {
addEdge,
} from 'reactflow';
import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import { rfContext } from '../../../store';
import { rfContext, setDirty } from '../../../store';
import { IComponent, Workflow } from '../../../../common';
import { generateId } from '../../../utils';
import { getCore } from '../../../services';
Expand All @@ -32,6 +33,7 @@ const nodeTypes = { customComponent: WorkspaceComponent };
const edgeTypes = { customEdge: DeletableEdge };

export function Workspace(props: WorkspaceProps) {
const dispatch = useDispatch();
const reactFlowWrapper = useRef(null);
const { reactFlowInstance, setReactFlowInstance } = useContext(rfContext);

Expand All @@ -45,6 +47,7 @@ export function Workspace(props: WorkspaceProps) {
type: 'customEdge',
};
setEdges((eds) => addEdge(edge, eds));
dispatch(setDirty());
},
[setEdges]
);
Expand Down Expand Up @@ -90,6 +93,7 @@ export function Workspace(props: WorkspaceProps) {
};

setNodes((nds) => nds.concat(newNode));
dispatch(setDirty());
},
[reactFlowInstance]
);
Expand All @@ -99,9 +103,9 @@ export function Workspace(props: WorkspaceProps) {
useEffect(() => {
const workflow = props.workflow;
if (workflow) {
if (workflow.reactFlowState) {
setNodes(workflow.reactFlowState.nodes);
setEdges(workflow.reactFlowState.edges);
if (workflow.workspaceFlowState) {
setNodes(workflow.workspaceFlowState.nodes);
setEdges(workflow.workspaceFlowState.edges);
} else {
getCore().notifications.toasts.addWarning(
`There is no configured UI flow for workflow: ${workflow.name}`
Expand Down
10 changes: 5 additions & 5 deletions public/store/reducers/workflows_reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@ const dummyNodes = [
{
id: generateId('text_embedding_processor'),
position: { x: 0, y: 500 },
data: new TextEmbeddingProcessor(),
data: new TextEmbeddingProcessor().toObj(),
type: 'customComponent',
},
{
id: generateId('text_embedding_processor'),
position: { x: 0, y: 200 },
data: new TextEmbeddingProcessor(),
data: new TextEmbeddingProcessor().toObj(),
type: 'customComponent',
},
{
id: generateId('knn_index'),
position: { x: 500, y: 500 },
data: new KnnIndex(),
data: new KnnIndex().toObj(),
type: 'customComponent',
},
] as ReactFlowComponent[];
Expand All @@ -42,7 +42,7 @@ const initialState = {
name: 'Workflow-1',
id: 'workflow-1-id',
description: 'description for workflow 1',
reactFlowState: {
workspaceFlowState: {
nodes: dummyNodes,
edges: [] as ReactFlowEdge[],
},
Expand All @@ -52,7 +52,7 @@ const initialState = {
name: 'Workflow-2',
id: 'workflow-2-id',
description: 'description for workflow 2',
reactFlowState: {
workspaceFlowState: {
nodes: dummyNodes,
edges: [] as ReactFlowEdge[],
},
Expand Down
17 changes: 1 addition & 16 deletions public/store/reducers/workspace_reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,9 @@
*/

import { createSlice } from '@reduxjs/toolkit';
import { IComponent } from '../../../common';
import { KnnIndex, TextEmbeddingProcessor } from '../../component_types';

// TODO: should be fetched from server-side. This will be the list of all
// available components that the framework offers. This will be used in the component
// library to populate the available components to drag-and-drop into the workspace.
const dummyComponents = [
new TextEmbeddingProcessor(),
new KnnIndex(),
] as IComponent[];

const initialState = {
isDirty: false,
components: dummyComponents,
};

const workspaceSlice = createSlice({
Expand All @@ -30,12 +19,8 @@ const workspaceSlice = createSlice({
removeDirty(state) {
state.isDirty = false;
},
setComponents(state, action) {
state.components = action.payload;
state.isDirty = true;
},
},
});

export const workspaceReducer = workspaceSlice.reducer;
export const { setDirty, removeDirty, setComponents } = workspaceSlice.actions;
export const { setDirty, removeDirty } = workspaceSlice.actions;