🔧 chore(tabsContext.tsx): refactor code to improve readability and maintainability

✨ feat(tabsContext.tsx): add support for reading and writing flows to the database
The code has been refactored to improve readability and maintainability. The `addFlow` function has been refactored to extract data from the flow and create a new flow with a default name if no flow is provided. The `processFlowEdges`, `processFlowNodes`, `updateNodeBaseClasses`, `updateNodeEdges`, `updateNodeDescription`, `updateNodeTemplate`, `updateStateWithTabsData`, and `updateStateWithDbData` functions have been added to improve code organization. Support for reading and writing flows to the database has been added to improve data persistence and allow for multiple users to access the same flows.

🔥 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.
This commit is contained in:
Gabriel Luiz Freitas Almeida 2023-06-09 12:39:39 -03:00
commit 07d9d1163a
3 changed files with 350 additions and 166 deletions

View file

@ -20,6 +20,10 @@ import { APITemplateType, TemplateVariableType } from "../types/api";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { addEdge } from "reactflow"; import { addEdge } from "reactflow";
import _ from "lodash"; import _ from "lodash";
import {
readFlowsFromDatabase,
deleteFlowFromDatabase,
} from "../controllers/API";
const TabsContextInitialValue: TabsContextType = { const TabsContextInitialValue: TabsContextType = {
save: () => {}, save: () => {},
@ -84,55 +88,158 @@ export function TabsProvider({ children }: { children: ReactNode }) {
} }
} }
useEffect(() => { // function loadCookie(cookie: string) {
//get tabs locally saved // if (cookie && Object.keys(templates).length > 0) {
let cookie = window.localStorage.getItem("tabsData"); // let cookieObject: LangFlowState = JSON.parse(cookie);
if (cookie && Object.keys(templates).length > 0) { // try {
let cookieObject: LangFlowState = JSON.parse(cookie); // cookieObject.flows.forEach((flow) => {
try { // if (!flow.data) {
cookieObject.flows.forEach((flow) => { // return;
if (!flow.data) { // }
return; // flow.data.edges.forEach((edge) => {
} // edge.className = "";
flow.data.edges.forEach((edge) => { // edge.style = { stroke: "#555555" };
edge.className = ""; // });
edge.style = { stroke: "#555555" };
});
flow.data.nodes.forEach((node) => { // flow.data.nodes.forEach((node) => {
const template = templates[node.data.type]; // const template = templates[node.data.type];
if (!template) { // if (!template) {
setErrorData({ title: `Unknown node type: ${node.data.type}` }); // setErrorData({ title: `Unknown node type: ${node.data.type}` });
return; // return;
} // }
if (Object.keys(template["template"]).length > 0) { // if (Object.keys(template["template"]).length > 0) {
node.data.node.base_classes = template["base_classes"]; // node.data.node.base_classes = template["base_classes"];
flow.data.edges.forEach((edge) => { // flow.data.edges.forEach((edge) => {
if (edge.source === node.id) { // if (edge.source === node.id) {
edge.sourceHandle = edge.sourceHandle // edge.sourceHandle = edge.sourceHandle
.split("|") // .split("|")
.slice(0, 2) // .slice(0, 2)
.concat(template["base_classes"]) // .concat(template["base_classes"])
.join("|"); // .join("|");
} // }
}); // });
node.data.node.description = template["description"]; // node.data.node.description = template["description"];
node.data.node.template = updateTemplate( // node.data.node.template = updateTemplate(
template["template"] as unknown as APITemplateType, // template["template"] as unknown as APITemplateType,
node.data.node.template as APITemplateType // node.data.node.template as APITemplateType
); // );
} // }
}); // });
}); // });
setTabIndex(cookieObject.tabIndex); // setTabIndex(cookieObject.tabIndex);
setFlows(cookieObject.flows); // setFlows(cookieObject.flows);
setId(cookieObject.id); // setId(cookieObject.id);
} catch (e) { // } catch (e) {
console.log(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]); }, [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(() => { useEffect(() => {
//save tabs locally //save tabs locally
console.log(id); console.log(id);
@ -208,14 +315,16 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const newFlows = [...prevState]; const newFlows = [...prevState];
const index = newFlows.findIndex((flow) => flow.id === id); const index = newFlows.findIndex((flow) => flow.id === id);
if (index >= 0) { if (index >= 0) {
if (index === tabIndex) { deleteFlowFromDatabase(id).then(() => {
setTabIndex(flows.length - 2); if (index === tabIndex) {
newFlows.splice(index, 1); setTabIndex(flows.length - 2);
} else { newFlows.splice(index, 1);
let flowId = flows[tabIndex].id; } else {
newFlows.splice(index, 1); let flowId = flows[tabIndex].id;
setTabIndex(newFlows.findIndex((flow) => flow.id === flowId)); newFlows.splice(index, 1);
} setTabIndex(newFlows.findIndex((flow) => flow.id === flowId));
}
});
} }
return newFlows; return newFlows;
}); });
@ -309,67 +418,90 @@ export function TabsProvider({ children }: { children: ReactNode }) {
reactFlowInstance.setEdges(edges); reactFlowInstance.setEdges(edges);
} }
function addFlow(flow?: FlowType) { const addFlow = (flow?: FlowType) => {
// Get data from the flow or set it to null if there's no flow provided. 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. // Create a new flow with a default name if no flow is provided.
let newFlow: FlowType = { const newFlow = createNewFlow(flowData, flow);
description,
name: flow?.name ?? "New Flow",
id: uuidv4(),
data,
};
// Increment the ID counter. // Increment the ID counter.
setId(uuidv4()); setId(uuidv4());
// Add the new flow to the list of flows. // Add the new flow to the list of flows.
addFlowToLocalState(newFlow);
setFlows((prevState) => {
const newFlows = [...prevState, newFlow];
return newFlows;
});
// Set the tab index to the new flow. // 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); setTabIndex(flows.length);
} };
/** /**
* Updates an existing flow with new data * Updates an existing flow with new data
* @param newFlow - The new flow object containing the updated data * @param newFlow - The new flow object containing the updated data
@ -386,6 +518,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
return newFlows; return newFlows;
}); });
} }
const [disableCopyPaste, setDisableCopyPaste] = useState(false); const [disableCopyPaste, setDisableCopyPaste] = useState(false);
return ( return (

View file

@ -39,3 +39,96 @@ export async function getExamples(): Promise<FlowType[]> {
return await Promise.all(contentsPromises); 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");
}

View file

@ -23,6 +23,7 @@ import {
CodeBracketSquareIcon, CodeBracketSquareIcon,
GlobeAltIcon, GlobeAltIcon,
PencilSquareIcon, PencilSquareIcon,
CloudArrowUpIcon,
PlusCircleIcon, PlusCircleIcon,
PlusIcon, PlusIcon,
PlusSmallIcon, PlusSmallIcon,
@ -66,6 +67,8 @@ import {
MenubarRadioItem, MenubarRadioItem,
MenubarTrigger, MenubarTrigger,
} from "../../components/ui/menubar"; } from "../../components/ui/menubar";
import { updateFlowInDatabase } from "../../controllers/API";
import { CardComponent } from "../../components/cardComponent";
export default function HomePage() { export default function HomePage() {
const { const {
@ -92,6 +95,12 @@ export default function HomePage() {
addFlow(); addFlow();
} }
}, [addFlow, flows.length, templates]); }, [addFlow, flows.length, templates]);
function handleSave(flow) {
// Put your save logic here.
updateFlowInDatabase(flow);
}
return ( return (
flows.length !== 0 && ( flows.length !== 0 && (
<Tabs <Tabs
@ -155,6 +164,10 @@ export default function HomePage() {
<MenubarMenu> <MenubarMenu>
<MenubarTrigger>Edit</MenubarTrigger> <MenubarTrigger>Edit</MenubarTrigger>
<MenubarContent> <MenubarContent>
<MenubarItem onClick={handleSave(flows[tabIndex])}>
<CloudArrowUpIcon className="w-4 h-4 mr-2" />
Save
</MenubarItem>
<MenubarItem <MenubarItem
onClick={() => { onClick={() => {
setRename(true); setRename(true);
@ -285,68 +298,13 @@ export default function HomePage() {
<TabsContent value="myflows" className="w-full h-full bg-muted"> <TabsContent value="myflows" className="w-full h-full bg-muted">
<div className="w-full p-4 grid gap-4 md:grid-cols-2 lg:grid-cols-4"> <div className="w-full p-4 grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{flows.map((flow, idx) => ( {flows.map((flow, idx) => (
<Card className="group"> <CardComponent
<CardHeader> flow={flow}
<CardTitle className="flex justify-between items-start"> idx={idx}
<div className="flex gap-4 items-center"> removeFlow={removeFlow}
<span setTabIndex={setTabIndex}
className={ setActiveTab={setActiveTab}
"rounded-md w-10 h-10 flex items-center justify-center text-2xl " + />
(idx === 0 ? "bg-blue-100" : " bg-orange-100")
}
>
{idx === 0 ? "🤖" : "🛠️"}
</span>
{flow.name}
</div>
<button
onClick={() => {
removeFlow(flow.id);
}}
>
<TrashIcon className="w-5 text-primary opacity-0 group-hover:opacity-100 transition-all" />
</button>
</CardTitle>
<CardDescription className="pt-2 pb-2">
<div className="truncate-doubleline">
{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} */}
</div>
</CardDescription>
</CardHeader>
<CardFooter>
<div className="flex gap-2 w-full justify-between items-end">
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">
{idx === 0 ? "Agent" : "Tool"}
</Badge>
{idx === 0 && (
<Badge variant="secondary">
<div className="w-3">
<OpenAiIcon />
</div>
<span className="text-base">&nbsp;</span>OpenAI+
</Badge>
)}
</div>
<Button
variant="outline"
size="sm"
className="whitespace-nowrap opacity-0 group-hover:opacity-100 transition-all"
onClick={() => {
setTabIndex(idx);
setActiveTab("myflow");
}}
>
<ArrowTopRightOnSquareIcon className="w-4 mr-2" />
Edit
</Button>
</div>
</CardFooter>
</Card>
))} ))}
</div> </div>
</TabsContent> </TabsContent>