diff --git a/public/pages/temp/index.ts b/public/pages/temp/index.ts
new file mode 100644
index 00000000..1d744f92
--- /dev/null
+++ b/public/pages/temp/index.ts
@@ -0,0 +1,6 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './workspace';
diff --git a/public/pages/temp/reactflow-styles.scss b/public/pages/temp/reactflow-styles.scss
new file mode 100644
index 00000000..840c718e
--- /dev/null
+++ b/public/pages/temp/reactflow-styles.scss
@@ -0,0 +1,10 @@
+.reactflow-parent-wrapper {
+ display: flex;
+ flex-grow: 1;
+ height: 100%;
+}
+
+.reactflow-parent-wrapper .reactflow-wrapper {
+ flex-grow: 1;
+ height: 100%;
+}
diff --git a/public/pages/temp/workspace.tsx b/public/pages/temp/workspace.tsx
new file mode 100644
index 00000000..f6d5ece8
--- /dev/null
+++ b/public/pages/temp/workspace.tsx
@@ -0,0 +1,141 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useRef, useContext, useCallback, useEffect } from 'react';
+import ReactFlow, {
+ Controls,
+ Background,
+ useNodesState,
+ useEdgesState,
+ addEdge,
+} from 'reactflow';
+import { useSelector } from 'react-redux';
+import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
+import { AppState, rfContext } from '../../store';
+
+// styling
+import 'reactflow/dist/style.css';
+import './reactflow-styles.scss';
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+interface WorkspaceProps {}
+
+export function Workspace(props: WorkspaceProps) {
+ const reactFlowWrapper = useRef(null);
+ const { reactFlowInstance, setReactFlowInstance } = useContext(rfContext);
+
+ // Fetching workspace state to populate the initial nodes/edges
+ const storedComponents = useSelector(
+ (state: AppState) => state.workspace.components
+ );
+ const storedEdges = useSelector((state: AppState) => state.workspace.edges);
+ const [nodes, setNodes, onNodesChange] = useNodesState(storedComponents);
+ const [edges, setEdges, onEdgesChange] = useEdgesState(storedEdges);
+
+ const onConnect = useCallback(
+ (params) => {
+ setEdges((eds) => addEdge(params, eds));
+ },
+ // TODO: add customized logic to prevent connections based on the node's
+ // allowed inputs. If allowed, update that node state as well with the added
+ // connection details.
+ [setEdges]
+ );
+
+ const onDragOver = useCallback((event) => {
+ event.preventDefault();
+ event.dataTransfer.dropEffect = 'move';
+ }, []);
+
+ const onDrop = useCallback(
+ (event) => {
+ event.preventDefault();
+ // Get the node info from the event metadata
+ const nodeData = event.dataTransfer.getData('application/reactflow');
+
+ // check if the dropped element is valid
+ if (typeof nodeData === 'undefined' || !nodeData) {
+ return;
+ }
+
+ // Fetch bounds based on the ref'd div component, adjust as needed.
+ // TODO: remove hardcoded bounds and fetch from a constant somewhere
+ // @ts-ignore
+ const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
+ // @ts-ignore
+ const position = reactFlowInstance.project({
+ x: event.clientX - reactFlowBounds.left - 80,
+ y: event.clientY - reactFlowBounds.top - 90,
+ });
+
+ // TODO: remove hardcoded values when more component info is passed in the event.
+ // Only keep the calculated 'positioning' field.
+ const newNode = {
+ // TODO: generate ID based on the node data maybe
+ id: Date.now().toFixed(),
+ type: nodeData.type,
+ position,
+ data: { label: nodeData.label },
+ style: {
+ background: 'white',
+ },
+ };
+
+ // TODO: add logic to add node into the redux datastore
+
+ setNodes((nds) => nds.concat(newNode));
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [reactFlowInstance]
+ );
+
+ // Initialization hook
+ useEffect(() => {
+ // TODO: fetch the nodes/edges dynamically (loading existing flow,
+ // creating fresh from template, creating blank template, etc.)
+ // Will involve populating and/or fetching from redux store
+ }, []);
+
+ return (
+
+
+
+ {/**
+ * We have these wrapper divs & reactFlowWrapper ref to control and calculate the
+ * ReactFlow bounds when calculating node positioning.
+ */}
+
+
+
+
+ );
+}
diff --git a/public/render_app.tsx b/public/render_app.tsx
index 4393262a..33ddca1f 100644
--- a/public/render_app.tsx
+++ b/public/render_app.tsx
@@ -9,7 +9,7 @@ import { BrowserRouter as Router, Route } from 'react-router-dom';
import { Provider } from 'react-redux';
import { AppMountParameters, CoreStart } from '../../../src/core/public';
import { AiFlowDashboardsApp } from './app';
-import { store } from './store';
+import { store, ReactFlowContextProvider } from './store';
export const renderApp = (
coreStart: CoreStart,
@@ -17,9 +17,11 @@ export const renderApp = (
) => {
ReactDOM.render(
-
- } />
-
+
+
+ } />
+
+
,
element
);
diff --git a/public/store/context/index.ts b/public/store/context/index.ts
new file mode 100644
index 00000000..1584425e
--- /dev/null
+++ b/public/store/context/index.ts
@@ -0,0 +1,6 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './react_flow_context_provider';
diff --git a/public/store/context/react_flow_context_provider.tsx b/public/store/context/react_flow_context_provider.tsx
new file mode 100644
index 00000000..da9eb694
--- /dev/null
+++ b/public/store/context/react_flow_context_provider.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { createContext, useState } from 'react';
+// import { useDispatch } from 'react-redux';
+
+const initialValues = {
+ reactFlowInstance: null,
+ setReactFlowInstance: () => {},
+ deleteNode: (nodeId: string) => {},
+ deleteEdge: (edgeId: string) => {},
+};
+
+export const rfContext = createContext(initialValues);
+
+/**
+ * This returns a provider from the rfContext context created above. The initial
+ * values are set so any nested components can use useContext to access these
+ * values.
+ *
+ * This is how we can manage ReactFlow context consistently across the various
+ * nested child components.
+ */
+export function ReactFlowContextProvider({ children }: any) {
+ // const dispatch = useDispatch();
+ const [reactFlowInstance, setReactFlowInstance] = useState(null);
+
+ const deleteNode = (nodeId: string) => {
+ // reactFlowInstance.setNodes(...)
+ };
+
+ const deleteEdge = (edgeId: string) => {
+ // reactFlowInstance.setEdges(...)
+ };
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/public/store/index.ts b/public/store/index.ts
index ccc2465d..45aab826 100644
--- a/public/store/index.ts
+++ b/public/store/index.ts
@@ -5,3 +5,4 @@
export * from './store';
export * from './reducers';
+export * from './context';
diff --git a/public/store/reducers/workspace_reducer.ts b/public/store/reducers/workspace_reducer.ts
index 4ef99dc7..340d1c5b 100644
--- a/public/store/reducers/workspace_reducer.ts
+++ b/public/store/reducers/workspace_reducer.ts
@@ -4,14 +4,77 @@
*/
import { createSlice } from '@reduxjs/toolkit';
+import { Node, Edge } from 'reactflow';
import { IComponent } from '../../../common';
import { KnnIndex, TextEmbeddingProcessor } from '../../component_types';
+// TODO: fetch from server-size if it is a created workflow, else have some default
+// mapping somewhere (e.g., 'semantic search': text_embedding_processor, knn_index, etc.)
+
+// TODO: we should store as IComponents. Have some helper fn for converting these to a
+// actual ReactFlow Node. examples of reactflow nodes below.
+const iComponents = [
+ new TextEmbeddingProcessor(),
+ new KnnIndex(),
+] as IComponent[];
+
+const dummyComponents = [
+ {
+ id: 'semantic-search',
+ position: { x: 40, y: 10 },
+ data: { label: 'Semantic Search' },
+ type: 'group',
+ style: {
+ height: 110,
+ width: 700,
+ },
+ },
+ {
+ id: 'model',
+ position: { x: 25, y: 25 },
+ data: { label: 'Deployed Model ID' },
+ type: 'default',
+ parentNode: 'semantic-search',
+ extent: 'parent',
+ },
+ {
+ id: 'ingest-pipeline',
+ position: { x: 262, y: 25 },
+ data: { label: 'Ingest Pipeline Name' },
+ type: 'default',
+ parentNode: 'semantic-search',
+ extent: 'parent',
+ },
+] as Array<
+ Node<
+ {
+ label: string;
+ },
+ string | undefined
+ >
+>;
+
+const dummyEdges = [
+ {
+ id: 'e1-2',
+ source: 'model',
+ target: 'ingest-pipeline',
+ style: {
+ strokeWidth: 2,
+ stroke: 'black',
+ },
+ markerEnd: {
+ type: 'arrow',
+ strokeWidth: 1,
+ color: 'black',
+ },
+ },
+] as Array>;
+
const initialState = {
isDirty: false,
- // TODO: fetch from server-size if it is a created workflow, else have some default
- // mapping somewhere (e.g., 'semantic search': text_embedding_processor, knn_index, etc.)
- components: [new TextEmbeddingProcessor(), new KnnIndex()] as IComponent[],
+ components: dummyComponents,
+ edges: dummyEdges,
};
const workspaceSlice = createSlice({
@@ -28,8 +91,17 @@ const workspaceSlice = createSlice({
state.components = action.payload;
state.isDirty = true;
},
+ setEdges(state, action) {
+ state.edges = action.payload;
+ state.isDirty = true;
+ },
},
});
export const workspaceReducer = workspaceSlice.reducer;
-export const { setDirty, removeDirty, setComponents } = workspaceSlice.actions;
+export const {
+ setDirty,
+ removeDirty,
+ setComponents,
+ setEdges,
+} = workspaceSlice.actions;