Skip to content

Commit

Permalink
Clean up obj serialization; use setDirty(); (#51) (#56)
Browse files Browse the repository at this point in the history
Signed-off-by: Tyler Ohlsen <[email protected]>
(cherry picked from commit f30ac98)

Co-authored-by: Tyler Ohlsen <[email protected]>
  • Loading branch information
opensearch-trigger-bot[bot] and ohltyler authored Oct 6, 2023
1 parent cc5fd06 commit 0b3fe2d
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 35 deletions.
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;

0 comments on commit 0b3fe2d

Please sign in to comment.