diff --git a/common/interfaces.ts b/common/interfaces.ts
index 5487c718..783c8ae7 100644
--- a/common/interfaces.ts
+++ b/common/interfaces.ts
@@ -28,7 +28,7 @@ type ReactFlowViewport = {
zoom: number;
};
-export type ReactFlowState = {
+export type WorkspaceFlowState = {
nodes: ReactFlowComponent[];
edges: ReactFlowEdge[];
viewport?: ReactFlowViewport;
@@ -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;
@@ -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',
+}
diff --git a/public/component_types/base_component.tsx b/public/component_types/base_component.tsx
new file mode 100644
index 00000000..1e6dcd4f
--- /dev/null
+++ b/public/component_types/base_component.tsx
@@ -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);
+ }
+}
diff --git a/public/component_types/indices/knn_index.ts b/public/component_types/indices/knn_index.ts
index 9255b47d..b66e17c0 100644
--- a/public/component_types/indices/knn_index.ts
+++ b/public/component_types/indices/knn_index.ts
@@ -4,6 +4,7 @@
*/
import { COMPONENT_CATEGORY, COMPONENT_CLASS } from '../../utils';
+import { BaseComponent } from '../base_component';
import {
IComponent,
IComponentField,
@@ -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;
@@ -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';
diff --git a/public/component_types/processors/text_embedding_processor.ts b/public/component_types/processors/text_embedding_processor.ts
index 2c3fa078..c6f3961a 100644
--- a/public/component_types/processors/text_embedding_processor.ts
+++ b/public/component_types/processors/text_embedding_processor.ts
@@ -4,6 +4,7 @@
*/
import { COMPONENT_CATEGORY, COMPONENT_CLASS } from '../../utils';
+import { BaseComponent } from '../base_component';
import {
IComponent,
IComponentField,
@@ -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;
@@ -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 =
diff --git a/public/pages/workflow_detail/components/header.tsx b/public/pages/workflow_detail/components/header.tsx
index 4c70ef20..94768eab 100644
--- a/public/pages/workflow_detail/components/header.tsx
+++ b/public/pages/workflow_detail/components/header.tsx
@@ -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 (
{}}>
Prototype
,
- {}}>
+ {
+ // @ts-ignore
+ saveWorkflow(props.workflow, reactFlowInstance);
+ dispatch(removeDirty());
+ }}
+ >
Save
,
]}
diff --git a/public/pages/workflow_detail/utils/index.ts b/public/pages/workflow_detail/utils/index.ts
new file mode 100644
index 00000000..079132ce
--- /dev/null
+++ b/public/pages/workflow_detail/utils/index.ts
@@ -0,0 +1,6 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './utils';
diff --git a/public/pages/workflow_detail/utils/utils.ts b/public/pages/workflow_detail/utils/utils.ts
new file mode 100644
index 00000000..d1aa76cc
--- /dev/null
+++ b/public/pages/workflow_detail/utils/utils.ts
@@ -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,
+ };
+ });
+}
diff --git a/public/pages/workflow_detail/workspace/workspace.tsx b/public/pages/workflow_detail/workspace/workspace.tsx
index 561cb2f6..2d0cc819 100644
--- a/public/pages/workflow_detail/workspace/workspace.tsx
+++ b/public/pages/workflow_detail/workspace/workspace.tsx
@@ -4,6 +4,7 @@
*/
import React, { useRef, useContext, useCallback, useEffect } from 'react';
+import { useDispatch } from 'react-redux';
import ReactFlow, {
Controls,
Background,
@@ -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';
@@ -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);
@@ -45,6 +47,7 @@ export function Workspace(props: WorkspaceProps) {
type: 'customEdge',
};
setEdges((eds) => addEdge(edge, eds));
+ dispatch(setDirty());
},
[setEdges]
);
@@ -90,6 +93,7 @@ export function Workspace(props: WorkspaceProps) {
};
setNodes((nds) => nds.concat(newNode));
+ dispatch(setDirty());
},
[reactFlowInstance]
);
@@ -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}`
diff --git a/public/store/reducers/workflows_reducer.ts b/public/store/reducers/workflows_reducer.ts
index 77d43b94..efc07a07 100644
--- a/public/store/reducers/workflows_reducer.ts
+++ b/public/store/reducers/workflows_reducer.ts
@@ -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[];
@@ -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[],
},
@@ -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[],
},
diff --git a/public/store/reducers/workspace_reducer.ts b/public/store/reducers/workspace_reducer.ts
index 2eb6748e..7bfbd6dc 100644
--- a/public/store/reducers/workspace_reducer.ts
+++ b/public/store/reducers/workspace_reducer.ts
@@ -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({
@@ -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;