Skip to content

Commit

Permalink
Merge pull request #159 from kreneskyp/fastapi_editor
Browse files Browse the repository at this point in the history
ChainEditor converted to FastAPI
  • Loading branch information
kreneskyp authored Aug 12, 2023
2 parents 4001ea2 + 662fefc commit b7bb4ef
Show file tree
Hide file tree
Showing 21 changed files with 418 additions and 363 deletions.
9 changes: 5 additions & 4 deletions frontend/agents/AgentEditorView.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ export const useChains = () => {
export const AgentEditorView = () => {
const { id } = useParams();
const {
data: agent,
load,
response,
call,
error: agentError,
} = useDetailAPI(`/api/agents/${id}`, { load: false });
} = useDetailAPI(`/api/agents/${id}`);
const { page: chainsPage, error: chainsError } = useChains();
const chains = chainsPage?.objects;
const { isNew, idRef } = useObjectEditorView(id, load);
const { isNew, idRef } = useObjectEditorView(id, call);
const agent = response?.data;

let content;
if (!chains) {
Expand Down
63 changes: 8 additions & 55 deletions frontend/chains/ChainEditorView.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,26 @@
import React, { useEffect, useState } from "react";
import { v4 as uuid4 } from "uuid";
import { usePreloadedQuery } from "react-relay/hooks";
import { useQueryLoader } from "react-relay";
import React from "react";
import { useParams } from "react-router-dom";
import { Spinner, VStack } from "@chakra-ui/react";

import { Layout, LayoutContent, LayoutLeftPane } from "site/Layout";
import { ChainGraphByIdQuery } from "chains/graphql/ChainGraphByIdQuery";
import ChainGraphEditor from "chains/ChainGraphEditor";
import { ChainGraphEditorSideBar } from "chains/editor/ChainGraphEditorSideBar";

const ChainEditorShim = ({ chainQueryRef }) => {
const { graph } = usePreloadedQuery(ChainGraphByIdQuery, chainQueryRef);
return <ChainGraphEditor graph={graph} />;
};
import { useDetailAPI } from "utils/hooks/useDetailAPI";
import { useObjectEditorView } from "utils/hooks/useObjectEditorView";

export const ChainEditorView = () => {
const [chainQueryRef, loadChainQuery] = useQueryLoader(ChainGraphByIdQuery);
const { id } = useParams();

// state for handling whether how to load the data (new vs existing)
// and when to reset the editor when opened. The cached state does not
// reset when the url changes as protection against reloading when
// creating new chains. This state tracks when to reset the cache.
const [idRef, setIdRef] = useState(null);
const [isNew, setIsNew] = useState(null);
const [wasCreated, setWasCreated] = useState(null);
useEffect(() => {
const firstRender = isNew === null;
if (firstRender) {
// first render caches whether this started as a new chain
setIsNew(id === undefined);
} else {
// switch from existing to new
if (id === undefined && !isNew) {
setIsNew(true);
setWasCreated(false);
}
// a new chain was created
if (id !== undefined && isNew) {
setWasCreated(true);
}
// switch from created to new
if (id === undefined && wasCreated) {
setIsNew(true);
setWasCreated(false);
setIdRef(uuid4());
}
}
}, [id]);

useEffect(() => {
// load chain if id is provided on view load
// otherwise state will be handled internally by the editor
if (isNew === false) {
loadChainQuery({ id }, { fetchPolicy: "network-only" });
setIdRef(id);
} else {
setIdRef(uuid4());
}
}, [isNew]);
const { response, call, isLoading } = useDetailAPI(`/api/chains/${id}/graph`);
const { isNew, idRef } = useObjectEditorView(id, call);
const graph = response?.data;

let content;
if (isNew) {
content = <ChainGraphEditor key={idRef} />;
} else if (!chainQueryRef) {
} else if (isLoading || !graph) {
content = <Spinner />;
} else {
content = <ChainEditorShim chainQueryRef={chainQueryRef} />;
content = <ChainGraphEditor graph={graph} />;
}

return (
Expand Down
80 changes: 40 additions & 40 deletions frontend/chains/ChainGraphEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ import { useColorMode } from "@chakra-ui/color-mode";
import { RootNode } from "chains/flow/RootNode";
import { getDefaults } from "chains/flow/TypeAutoFields";
import { useDebounce } from "utils/hooks/useDebounce";
import { useAxios } from "utils/hooks/useAxios";

// Nodes are either a single node or a group of nodes
// ConfigNode renders class_path specific content
const nodeTypes = {
NODE: ConfigNode,
LIST: ConfigNode,
node: ConfigNode,
list: ConfigNode,
root: RootNode,
};

Expand All @@ -39,6 +40,7 @@ const ChainGraphEditor = ({ graph }) => {
const edgeUpdate = useRef(true);
const [chainRef, setChainRef] = useState(graph?.chain);
const [chainLoaded, setChainLoaded] = useState(graph?.chain !== undefined);
const { call: loadChain } = useAxios();

const reactFlowGraph = useGraphForReactFlow(graph);
const [nodes, setNodes, onNodesChange] = useNodesState(reactFlowGraph.nodes);
Expand All @@ -59,7 +61,7 @@ const ChainGraphEditor = ({ graph }) => {
}, []);

const api = useChainEditorAPI({
chainRef,
chain: chainRef,
onError: onAPIError,
reactFlowInstance,
});
Expand All @@ -71,16 +73,17 @@ const ChainGraphEditor = ({ graph }) => {
}, []);

const onNodeSaved = useCallback(
({ addChainNode }) => {
(response) => {
// first node creates the new chain
// redirect to the correct URL
const { node } = addChainNode;
const chain_id = node?.chain?.id;

if (!chainLoaded) {
navigate(`/chains/${chain_id}`, { replace: true });
setChainLoaded(true);
setChainRef(node?.chain);
navigate(`/chains/${response.data.chain_id}`, { replace: true });
loadChain(`/api/chains/${response.data.chain_id}`, {
onSuccess: (response) => {
setChainRef(response.data);
setChainLoaded(true);
},
});
}
},
[chainRef?.id, chainLoaded]
Expand Down Expand Up @@ -108,8 +111,8 @@ const ChainGraphEditor = ({ graph }) => {
// create data object instead of waiting for graphql
const data = {
id: uuid4(),
chainId: chainRef?.id || null,
classPath: nodeType.classPath,
chain_id: chainRef?.id || null,
class_path: nodeType.class_path,
position: position,
config: getDefaults(nodeType),
};
Expand All @@ -118,7 +121,7 @@ const ChainGraphEditor = ({ graph }) => {
const flowNode = toReactFlowNode(data, nodeType);

// add to API and ReactFlow
api.addNode({ data }, { onCompleted: onNodeSaved });
api.addNode(data, { onSuccess: onNodeSaved });
setNodes((nds) => nds.concat(flowNode));
},
[reactFlowInstance, chainRef?.id]
Expand All @@ -137,12 +140,7 @@ const ChainGraphEditor = ({ graph }) => {

const onNodeDragStop = useCallback((event, node) => {
// update node with new position
api.updateNodePosition({
data: {
id: node.id,
position: node.position,
},
});
api.updateNodePosition(node.id, node.position);
}, []);

// new edges
Expand All @@ -159,18 +157,18 @@ const ChainGraphEditor = ({ graph }) => {
// save via API
if (source.id === "root") {
// link from root node uses setRoot since it's not stored as an edge
api.setRoot({ chainId: chainRef.id, nodeId: params.target });
api.setRoot(chainRef.id, { node_id: params.target });
} else {
// normal link and prop edges
const data = {
id,
sourceId: params.source,
targetId: params.target,
source_id: params.source,
target_id: params.target,
key: params.targetHandle,
chainId: chainRef?.id,
chain_id: chainRef?.id,
relation: params.sourceHandle === "out" ? "LINK" : "PROP",
};
api.addEdge({ data });
api.addEdge(data);
}
},
[chainRef, reactFlowInstance, colorMode]
Expand All @@ -190,7 +188,7 @@ const ChainGraphEditor = ({ graph }) => {
expectedType = "chain-link";
} else {
connector = connectors.find((c) => c.key === connection.targetHandle);
expectedType = connector?.sourceType;
expectedType = connector?.source_type;
}
const supportsMultiple = connector?.multiple || false;

Expand Down Expand Up @@ -263,19 +261,16 @@ const ChainGraphEditor = ({ graph }) => {
setEdges((els) => updateEdge(oldEdge, newConnection, els));
if (newConnection.source === "root") {
if (oldEdge.target !== newConnection.target) {
api.setRoot({ chainId: chainRef.id, nodeId: newConnection.target });
api.setRoot({ chain_id: chainRef.id, node_id: newConnection.target });
}
} else {
const isSame =
oldEdge.source === newConnection.source &&
oldEdge.target === newConnection.target;
if (!isSame) {
api.updateEdge({
data: {
id: oldEdge.data.id,
sourceId: newConnection.source,
targetId: newConnection.target,
},
api.updateEdge(oldEdge.data.id, {
source_id: newConnection.source,
target_id: newConnection.target,
});
}
}
Expand All @@ -289,9 +284,9 @@ const ChainGraphEditor = ({ graph }) => {
if (!edgeUpdate.toHandle) {
setEdges((eds) => eds.filter((e) => e.id !== edge.id));
if (edge.source === "root") {
api.setRoot({ chainId: chainRef.id, nodeId: null });
api.setRoot(chainRef.id, { node_id: null });
} else {
api.deleteEdge({ id: edge.data.id });
api.deleteEdge(edge.data.id);
}
}
edgeUpdate.edge = null;
Expand All @@ -303,25 +298,30 @@ const ChainGraphEditor = ({ graph }) => {
api.updateChain(...args);
}, 1000);

const { callback: debouncedChainCreate } = useDebounce((...args) => {
api.createChain(...args);
}, 1000);

const onTitleChange = useCallback(
(event) => {
setChainRef({ ...chainRef, name: event.target.value });
if (!chainLoaded) {
debouncedChainUpdate(
{ data: { name: event.target.value } },
debouncedChainCreate(
{ name: event.target.value, description: "" },
{
onCompleted: (data) => {
navigate(`/chains/${data.updateChain.chain.id}`, {
onSuccess: (response) => {
navigate(`/chains/${response.data.id}`, {
replace: true,
});
setChainRef(data.updateChain.chain);
setChainRef(response.data);
setChainLoaded(true);
},
}
);
} else {
debouncedChainUpdate({
data: { ...chainRef, name: event.target.value },
...chainRef,
name: event.target.value,
});
}
},
Expand Down
46 changes: 7 additions & 39 deletions frontend/chains/editor/NodeTypeSearch.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { usePreloadedQuery, useQueryLoader } from "react-relay/hooks";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import React, { useCallback, useMemo } from "react";
import { NodeSelector } from "chains/editor/NodeSelector";
import { useDebounce } from "utils/hooks/useDebounce";
import { Box, Heading, HStack, Input, Text, VStack } from "@chakra-ui/react";
import { SearchNodeTypesQuery } from "chains/graphql/SearchNodeTypesQuery";
import { useSideBarColorMode } from "chains/editor/useColorMode";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DEFAULT_NODE_STYLE, NODE_STYLES } from "chains/editor/styles";
import { usePaginatedAPI } from "utils/hooks/usePaginatedAPI";

const NodeSelectorHeader = ({ label, icon }) => {
const { color } = useSideBarColorMode();
Expand All @@ -27,15 +26,6 @@ const NodeSelectorHeader = ({ label, icon }) => {
);
};

const SearchNodeTypesQueryRunner = ({ queryRef, setResults }) => {
// load query and then update state
const data = usePreloadedQuery(SearchNodeTypesQuery, queryRef);
const nodeTypes = data?.searchNodeTypes;
useEffect(() => {
setResults(nodeTypes);
}, [queryRef, nodeTypes]);
};

const SCROLLBAR_CSS = {
"&::-webkit-scrollbar": {
width: "5px",
Expand Down Expand Up @@ -117,51 +107,30 @@ const NodeTypeGroup = ({ typeKey, group }) => {
* Searching queries SearchNodeTypeQuery
*/
export const NodeTypeSearch = () => {
const [results, setResults] = useState([]);
const { border } = useSideBarColorMode();

// queries
const [nodeTypesQueryRef, loadNodeTypesQuery, disposeNodeTypesQuery] =
useQueryLoader(SearchNodeTypesQuery);
const { load, page } = usePaginatedAPI(`/api/node_types/`, { load: false });
const { callback: searchNodeTypes, clear } = useDebounce(
useCallback((search) => {
loadNodeTypesQuery({ search }, { fetchPolicy: "store-and-network" });
load({ search });
}, []),
500
);

// callback for search bar changing
const onSearchChange = useCallback((event) => {
const search = event.target.value;
if (search) {
searchNodeTypes(search);
} else {
clear();
if (nodeTypesQueryRef) {
disposeNodeTypesQuery();
}
if (results) {
setResults([]);
}
}
}, []);

// Couldn't determine a good pattern for using relay to fetch
// search updates for the autocomplete. The results are needed
// here to decide whether to render the popover or not.
//
// QueryRunner components render empty but will fetch the query
// data then update the results state. This is a huge hack but
// it works for now.
const nodeTypeSearchRunner = nodeTypesQueryRef !== null && (
<SearchNodeTypesQueryRunner
queryRef={nodeTypesQueryRef}
setResults={setResults}
/>
);

const groupedTypes = useMemo(() => {
return groupByNodeTypeGroup(results || []);
}, [results]);
return groupByNodeTypeGroup(page?.objects || []);
}, [page]);

return (
<Box
Expand All @@ -172,7 +141,6 @@ export const NodeTypeSearch = () => {
minWidth={170}
overflowY={"hidden"}
>
{nodeTypeSearchRunner}
<Heading as="h3" size="xs" mb={2}>
Components
</Heading>
Expand Down
Loading

0 comments on commit b7bb4ef

Please sign in to comment.