From 07d9d1163abd18fc9be1ecc96dd99d703e67849e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 9 Jun 2023 12:39:39 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20chore(tabsContext.tsx):=20refact?= =?UTF-8?q?or=20code=20to=20improve=20readability=20and=20maintainability?= =?UTF-8?q?=20=E2=9C=A8=20feat(tabsContext.tsx):=20add=20support=20for=20r?= =?UTF-8?q?eading=20and=20writing=20flows=20to=20the=20database=20The=20co?= =?UTF-8?q?de=20has=20been=20refactored=20to=20improve=20readability=20and?= =?UTF-8?q?=20maintainability.=20The=20`addFlow`=20function=20has=20been?= =?UTF-8?q?=20refactored=20to=20extract=20data=20from=20the=20flow=20and?= =?UTF-8?q?=20create=20a=20new=20flow=20with=20a=20default=20name=20if=20n?= =?UTF-8?q?o=20flow=20is=20provided.=20The=20`processFlowEdges`,=20`proces?= =?UTF-8?q?sFlowNodes`,=20`updateNodeBaseClasses`,=20`updateNodeEdges`,=20?= =?UTF-8?q?`updateNodeDescription`,=20`updateNodeTemplate`,=20`updateState?= =?UTF-8?q?WithTabsData`,=20and=20`updateStateWithDbData`=20functions=20ha?= =?UTF-8?q?ve=20been=20added=20to=20improve=20code=20organization.=20Suppo?= =?UTF-8?q?rt=20for=20reading=20and=20writing=20flows=20to=20the=20databas?= =?UTF-8?q?e=20has=20been=20added=20to=20improve=20data=20persistence=20an?= =?UTF-8?q?d=20allow=20for=20multiple=20users=20to=20access=20the=20same?= =?UTF-8?q?=20flows.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔥 refactor(TabsProvider): remove unnecessary whitespace The commit removes an unnecessary whitespace in the TabsProvider function. 🐛 fix(API): add missing import statement for axios ✨ feat(API): add functions to interact with the backend API for CRUD operations on flows The missing import statement for axios has been added to the file. The new functions added to the API file allow the frontend to interact with the backend API for CRUD operations on flows. The functions added are `saveFlowToDatabase`, `updateFlowInDatabase`, `readFlowsFromDatabase`, `deleteFlowFromDatabase`, and `getFlowFromDatabase`. These functions allow the frontend to create, read, update, and delete flows from the backend API. 🎨 style(MainPage): refactor card component into a separate file The card component has been refactored into a separate file to improve code organization and readability. The `CardComponent` is now imported into the `MainPage` file and used to render the flow cards. The `handleSave` function has been updated to call the `updateFlowInDatabase` function with the current flow as an argument. --- src/frontend/src/contexts/tabsContext.tsx | 341 +++++++++++++++------- src/frontend/src/controllers/API/index.ts | 93 ++++++ src/frontend/src/pages/MainPage/index.tsx | 82 ++---- 3 files changed, 350 insertions(+), 166 deletions(-) diff --git a/src/frontend/src/contexts/tabsContext.tsx b/src/frontend/src/contexts/tabsContext.tsx index 1907a5590..5d85a8225 100644 --- a/src/frontend/src/contexts/tabsContext.tsx +++ b/src/frontend/src/contexts/tabsContext.tsx @@ -20,6 +20,10 @@ import { APITemplateType, TemplateVariableType } from "../types/api"; import { v4 as uuidv4 } from "uuid"; import { addEdge } from "reactflow"; import _ from "lodash"; +import { + readFlowsFromDatabase, + deleteFlowFromDatabase, +} from "../controllers/API"; const TabsContextInitialValue: TabsContextType = { save: () => {}, @@ -84,55 +88,158 @@ export function TabsProvider({ children }: { children: ReactNode }) { } } - useEffect(() => { - //get tabs locally saved - let cookie = window.localStorage.getItem("tabsData"); - if (cookie && Object.keys(templates).length > 0) { - let cookieObject: LangFlowState = JSON.parse(cookie); - try { - cookieObject.flows.forEach((flow) => { - if (!flow.data) { - return; - } - flow.data.edges.forEach((edge) => { - edge.className = ""; - edge.style = { stroke: "#555555" }; - }); + // function loadCookie(cookie: string) { + // if (cookie && Object.keys(templates).length > 0) { + // let cookieObject: LangFlowState = JSON.parse(cookie); + // try { + // cookieObject.flows.forEach((flow) => { + // if (!flow.data) { + // return; + // } + // flow.data.edges.forEach((edge) => { + // edge.className = ""; + // edge.style = { stroke: "#555555" }; + // }); - flow.data.nodes.forEach((node) => { - const template = templates[node.data.type]; - if (!template) { - setErrorData({ title: `Unknown node type: ${node.data.type}` }); - return; - } - if (Object.keys(template["template"]).length > 0) { - node.data.node.base_classes = template["base_classes"]; - flow.data.edges.forEach((edge) => { - if (edge.source === node.id) { - edge.sourceHandle = edge.sourceHandle - .split("|") - .slice(0, 2) - .concat(template["base_classes"]) - .join("|"); - } - }); - node.data.node.description = template["description"]; - node.data.node.template = updateTemplate( - template["template"] as unknown as APITemplateType, - node.data.node.template as APITemplateType - ); - } - }); - }); - setTabIndex(cookieObject.tabIndex); - setFlows(cookieObject.flows); - setId(cookieObject.id); - } catch (e) { - console.log(e); + // flow.data.nodes.forEach((node) => { + // const template = templates[node.data.type]; + // if (!template) { + // setErrorData({ title: `Unknown node type: ${node.data.type}` }); + // return; + // } + // if (Object.keys(template["template"]).length > 0) { + // node.data.node.base_classes = template["base_classes"]; + // flow.data.edges.forEach((edge) => { + // if (edge.source === node.id) { + // edge.sourceHandle = edge.sourceHandle + // .split("|") + // .slice(0, 2) + // .concat(template["base_classes"]) + // .join("|"); + // } + // }); + // node.data.node.description = template["description"]; + // node.data.node.template = updateTemplate( + // template["template"] as unknown as APITemplateType, + // node.data.node.template as APITemplateType + // ); + // } + // }); + // }); + // setTabIndex(cookieObject.tabIndex); + // setFlows(cookieObject.flows); + // setId(cookieObject.id); + // } catch (e) { + // console.log(e); + // } + // } + // } + + useEffect(() => { + // get data from db + //get tabs locally saved + // let tabsData = getLocalStorageTabsData(); + getTabsDataFromDB().then((DbData) => { + if (DbData && Object.keys(templates).length > 0) { + try { + processDBData(DbData); + updateStateWithDbData(DbData); + } catch (e) { + console.error(e); + } } - } + }); }, [templates]); + function getLocalStorageTabsData() { + let cookie = window.localStorage.getItem("tabsData"); + let cookieObject: LangFlowState = JSON.parse(cookie); + return cookieObject; + } + + function getTabsDataFromDB() { + //get tabs from db + return readFlowsFromDatabase(); + } + function processDBData(DbData) { + DbData.forEach((flow) => { + if (!flow.data) { + return; + } + processFlowEdges(flow.data.edges); + processFlowNodes(flow.data.nodes); + }); + } + function processTabsData(tabsData) { + tabsData.flows.forEach((flow) => { + if (!flow.data) { + return; + } + processFlowEdges(flow.data.edges); + processFlowNodes(flow.data.nodes); + }); + } + + function processFlowEdges(edges) { + edges.forEach((edge) => { + edge.className = ""; + edge.style = { stroke: "#555555" }; + }); + } + + function processFlowNodes(nodes) { + nodes.forEach((node) => { + const template = templates[node.data.type]; + if (!template) { + setErrorData({ title: `Unknown node type: ${node.data.type}` }); + return; + } + if (Object.keys(template["template"]).length > 0) { + updateNodeBaseClasses(node, template); + updateNodeEdges(node, template); + updateNodeDescription(node, template); + updateNodeTemplate(node, template); + } + }); + } + + function updateNodeBaseClasses(node, template) { + node.data.node.base_classes = template["base_classes"]; + } + + function updateNodeEdges(node, template) { + node.data.edges.forEach((edge) => { + if (edge.source === node.id) { + edge.sourceHandle = edge.sourceHandle + .split("|") + .slice(0, 2) + .concat(template["base_classes"]) + .join("|"); + } + }); + } + + function updateNodeDescription(node, template) { + node.data.node.description = template["description"]; + } + + function updateNodeTemplate(node, template) { + node.data.node.template = updateTemplate( + template["template"] as unknown as APITemplateType, + node.data.node.template as APITemplateType + ); + } + + function updateStateWithTabsData(tabsData) { + setTabIndex(tabsData.tabIndex); + setFlows(tabsData.flows); + setId(tabsData.id); + } + + function updateStateWithDbData(tabsData) { + setFlows(tabsData); + } + useEffect(() => { //save tabs locally console.log(id); @@ -208,14 +315,16 @@ export function TabsProvider({ children }: { children: ReactNode }) { const newFlows = [...prevState]; const index = newFlows.findIndex((flow) => flow.id === id); if (index >= 0) { - if (index === tabIndex) { - setTabIndex(flows.length - 2); - newFlows.splice(index, 1); - } else { - let flowId = flows[tabIndex].id; - newFlows.splice(index, 1); - setTabIndex(newFlows.findIndex((flow) => flow.id === flowId)); - } + deleteFlowFromDatabase(id).then(() => { + if (index === tabIndex) { + setTabIndex(flows.length - 2); + newFlows.splice(index, 1); + } else { + let flowId = flows[tabIndex].id; + newFlows.splice(index, 1); + setTabIndex(newFlows.findIndex((flow) => flow.id === flowId)); + } + }); } return newFlows; }); @@ -309,67 +418,90 @@ export function TabsProvider({ children }: { children: ReactNode }) { reactFlowInstance.setEdges(edges); } - function addFlow(flow?: FlowType) { - // Get data from the flow or set it to null if there's no flow provided. + const addFlow = (flow?: FlowType) => { + let flowData = extractDataFromFlow(flow); - let data = flow?.data ? flow.data : null; - const description = flow?.description ? flow.description : ""; - if (data) { - data.edges.forEach((edge) => { - edge.style = { stroke: "inherit" }; - edge.className = - edge.targetHandle.split("|")[0] === "Text" - ? "stroke-gray-800 dark:stroke-gray-300" - : "stroke-gray-900 dark:stroke-gray-200"; - edge.animated = edge.targetHandle.split("|")[0] === "Text"; - }); - data.nodes.forEach((node) => { - const template = templates[node.data.type]; - if (!template) { - setErrorData({ title: `Unknown node type: ${node.data.type}` }); - return; - } - if (Object.keys(template["template"]).length > 0) { - node.data.node.base_classes = template["base_classes"]; - flow.data.edges.forEach((edge) => { - if (edge.source === node.id) { - edge.sourceHandle = edge.sourceHandle - .split("|") - .slice(0, 2) - .concat(template["base_classes"]) - .join("|"); - } - }); - node.data.node.description = template["description"]; - node.data.node.template = updateTemplate( - template["template"] as unknown as APITemplateType, - node.data.node.template as APITemplateType - ); - } - }); - updateIds(data, getNodeId); - } // Create a new flow with a default name if no flow is provided. - let newFlow: FlowType = { - description, - name: flow?.name ?? "New Flow", - id: uuidv4(), - data, - }; + const newFlow = createNewFlow(flowData, flow); // Increment the ID counter. setId(uuidv4()); // Add the new flow to the list of flows. - - setFlows((prevState) => { - const newFlows = [...prevState, newFlow]; - return newFlows; - }); + addFlowToLocalState(newFlow); // Set the tab index to the new flow. + setTabIndexToLocalState(); + }; + + const extractDataFromFlow = (flow) => { + let data = flow?.data ? flow.data : null; + const description = flow?.description ? flow.description : ""; + + if (data) { + updateEdges(data.edges); + updateNodes(data.nodes, data.edges); + updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere + } + + return { data, description }; + }; + + const updateEdges = (edges) => { + edges.forEach((edge) => { + edge.style = { stroke: "inherit" }; + edge.className = + edge.targetHandle.split("|")[0] === "Text" + ? "stroke-gray-800 dark:stroke-gray-300" + : "stroke-gray-900 dark:stroke-gray-200"; + edge.animated = edge.targetHandle.split("|")[0] === "Text"; + }); + }; + + const updateNodes = (nodes, edges) => { + nodes.forEach((node) => { + const template = templates[node.data.type]; + if (!template) { + setErrorData({ title: `Unknown node type: ${node.data.type}` }); + return; + } + if (Object.keys(template["template"]).length > 0) { + node.data.node.base_classes = template["base_classes"]; + edges.forEach((edge) => { + if (edge.source === node.id) { + edge.sourceHandle = edge.sourceHandle + .split("|") + .slice(0, 2) + .concat(template["base_classes"]) + .join("|"); + } + }); + node.data.node.description = template["description"]; + node.data.node.template = updateTemplate( + template["template"] as unknown as APITemplateType, + node.data.node.template as APITemplateType + ); + } + }); + }; + + const createNewFlow = (flowData, flow) => ({ + description: flowData.description, + name: flow?.name ?? "New Flow", + id: uuidv4(), + data: flowData.data, + }); + + const addFlowToLocalState = (newFlow) => { + setFlows((prevState) => { + return [...prevState, newFlow]; + }); + }; + + const setTabIndexToLocalState = () => { setTabIndex(flows.length); - } + }; + /** * Updates an existing flow with new data * @param newFlow - The new flow object containing the updated data @@ -386,6 +518,7 @@ export function TabsProvider({ children }: { children: ReactNode }) { return newFlows; }); } + const [disableCopyPaste, setDisableCopyPaste] = useState(false); return ( diff --git a/src/frontend/src/controllers/API/index.ts b/src/frontend/src/controllers/API/index.ts index ffa9eef72..42038a89a 100644 --- a/src/frontend/src/controllers/API/index.ts +++ b/src/frontend/src/controllers/API/index.ts @@ -39,3 +39,96 @@ export async function getExamples(): Promise { return await Promise.all(contentsPromises); } + +export async function saveFlowToDatabase(newFlow: FlowType) { + try { + const response = await fetch("/flows/", { + method: "POST", + headers: { + accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: newFlow.name, + data: newFlow.data, + description: newFlow.description, + }), + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + } catch (error) { + console.error(error); + throw error; + } +} + +export async function updateFlowInDatabase(updatedFlow: FlowType) { + try { + const response = await fetch(`/flows/${updatedFlow.id}`, { + method: "PATCH", // Or "PATCH" depending on your backend API + headers: { + accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: updatedFlow.name, + data: updatedFlow.data, + description: updatedFlow.description, + }), + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + } catch (error) { + console.error(error); + throw error; + } +} + +export async function readFlowsFromDatabase() { + try { + const response = await fetch("/flows/"); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + } catch (error) { + console.error(error); + throw error; + } +} + +export async function deleteFlowFromDatabase(flowId: string) { + try { + const response = await fetch(`/flows/${flowId}`, { + method: "DELETE", + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + } catch (error) { + console.error(error); + throw error; + } +} + +export async function getFlowFromDatabase(flowId: number) { + try { + const response = await fetch(`/flows/${flowId}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + } catch (error) { + console.error(error); + throw error; + } +} + +export async function getHealth() { + return await axios.get("/health"); +} diff --git a/src/frontend/src/pages/MainPage/index.tsx b/src/frontend/src/pages/MainPage/index.tsx index 8f391b5fc..b4757132c 100644 --- a/src/frontend/src/pages/MainPage/index.tsx +++ b/src/frontend/src/pages/MainPage/index.tsx @@ -23,6 +23,7 @@ import { CodeBracketSquareIcon, GlobeAltIcon, PencilSquareIcon, + CloudArrowUpIcon, PlusCircleIcon, PlusIcon, PlusSmallIcon, @@ -66,6 +67,8 @@ import { MenubarRadioItem, MenubarTrigger, } from "../../components/ui/menubar"; +import { updateFlowInDatabase } from "../../controllers/API"; +import { CardComponent } from "../../components/cardComponent"; export default function HomePage() { const { @@ -92,6 +95,12 @@ export default function HomePage() { addFlow(); } }, [addFlow, flows.length, templates]); + + function handleSave(flow) { + // Put your save logic here. + updateFlowInDatabase(flow); + } + return ( flows.length !== 0 && ( Edit + + + Save + { setRename(true); @@ -285,68 +298,13 @@ export default function HomePage() {
{flows.map((flow, idx) => ( - - - -
- - {idx === 0 ? "🤖" : "🛠️"} - - {flow.name} -
- -
- -
- {idx === 0 - ? "This flow creates an agent that accesses a department store database and APIs to monitor customer activity and overall storage." - : "This is a new Flow"} - {/* {flow.description} */} -
-
-
- - -
-
- - {idx === 0 ? "Agent" : "Tool"} - - {idx === 0 && ( - -
- -
-  OpenAI+ -
- )} -
- -
-
-
+ ))}