From a811834b937c9019a5d0ce2f026e4c876dd416ca Mon Sep 17 00:00:00 2001 From: Cristhian Zanforlin Lousa <72977554+Cristhianzl@users.noreply.github.com> Date: Mon, 29 Jul 2024 19:56:54 -0300 Subject: [PATCH] refactor: change endpoint monitor/builds to use useQuery (#2622) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ (constants.ts): add BUILDS endpoint to URLs constants ✨ (index.ts): create index file for builds-related queries ✨ (use-delete-builds.ts): implement useDeleteFLowPool hook for deleting builds * ✨ (chatView): integrate useDeleteFlowPool hook for deleting flow pool ♻️ (chatView): refactor clearChat function to use mutateFlowPool for deletion * ♻️ (use-delete-builds.ts): rename useDeleteFLowPool to useDeleteBuilds for clarity ♻️ (index.tsx): update import and usage of useDeleteFLowPool to useDeleteBuilds * ✨ (API): add use-get-builds query to fetch build data ✨ (PageComponent): integrate use-get-builds query for fetching builds ✨ (flowStore): add setters for inputs, outputs, and hasIO in flowStore * ♻️ (flowStore.ts): refactor hasIO to derive its value from inputs and outputs 🔥 (flowStore.ts): remove unused resetFlow function to clean up the codebase * ♻️ (use-get-builds.ts): refactor useGetBuildsQuery to remove unused params ✨ (PageComponent): add logic to handle flow state, inputs, outputs, and viewport ♻️ (flowStore): refactor and add resetFlow method to handle flow state reset * ✅ (chatComponent): add data-testid attributes for better testability ✅ (generalBugs-shard-3.spec.ts): add end-to-end test for playground button state --------- Co-authored-by: anovazzi1 --- .../src/components/chatComponent/index.tsx | 6 +- .../src/controllers/API/helpers/constants.ts | 1 + .../controllers/API/queries/_builds/index.ts | 2 + .../API/queries/_builds/use-delete-builds.ts | 26 +++++++++ .../API/queries/_builds/use-get-builds.ts | 58 +++++++++++++++++++ .../IOModal/components/chatView/index.tsx | 14 ++++- .../components/PageComponent/index.tsx | 48 ++++++++------- src/frontend/src/stores/flowStore.ts | 11 +++- src/frontend/src/types/zustand/flow/index.ts | 7 +++ .../end-to-end/generalBugs-shard-3.spec.ts | 55 ++++++++++++++++++ 10 files changed, 203 insertions(+), 25 deletions(-) create mode 100644 src/frontend/src/controllers/API/queries/_builds/index.ts create mode 100644 src/frontend/src/controllers/API/queries/_builds/use-delete-builds.ts create mode 100644 src/frontend/src/controllers/API/queries/_builds/use-get-builds.ts diff --git a/src/frontend/src/components/chatComponent/index.tsx b/src/frontend/src/components/chatComponent/index.tsx index a88bd9573..fd2c7a080 100644 --- a/src/frontend/src/components/chatComponent/index.tsx +++ b/src/frontend/src/components/chatComponent/index.tsx @@ -113,7 +113,10 @@ export default function FlowToolbar(): JSX.Element {
{hasIO ? ( -
+
= ( + options, +) => { + const { mutate } = UseRequestProcessor(); + + const deleteBuildsFn = async (payload: IDeleteBuilds): Promise => { + const config = {}; + config["params"] = { flow_id: payload.flowId }; + const res = await api.delete(`${getURL("BUILDS")}`, config); + return res.data; + }; + + const mutation = mutate(["useDeleteBuilds"], deleteBuildsFn, options); + + return mutation; +}; diff --git a/src/frontend/src/controllers/API/queries/_builds/use-get-builds.ts b/src/frontend/src/controllers/API/queries/_builds/use-get-builds.ts new file mode 100644 index 000000000..eeaa34ef4 --- /dev/null +++ b/src/frontend/src/controllers/API/queries/_builds/use-get-builds.ts @@ -0,0 +1,58 @@ +import useFlowStore from "@/stores/flowStore"; +import useFlowsManagerStore from "@/stores/flowsManagerStore"; +import { FlowPoolType } from "@/types/zustand/flow"; +import { cleanEdges } from "@/utils/reactflowUtils"; +import { getInputsAndOutputs } from "@/utils/storeUtils"; +import { keepPreviousData } from "@tanstack/react-query"; +import { AxiosResponse } from "axios"; +import { useQueryFunctionType } from "../../../../types/api"; +import { api } from "../../api"; +import { getURL } from "../../helpers/constants"; +import { UseRequestProcessor } from "../../services/request-processor"; + +interface BuildsQueryParams { + flowId?: string; + nodeId?: string; +} + +export const useGetBuildsQuery: useQueryFunctionType< + BuildsQueryParams, + AxiosResponse<{ vertex_builds: FlowPoolType }> +> = ({}) => { + const { query } = UseRequestProcessor(); + + const setFlowPool = useFlowStore((state) => state.setFlowPool); + const currentFlow = useFlowsManagerStore((state) => state.currentFlow); + + const getBuildsFn = async ( + params: BuildsQueryParams, + ): Promise> => { + const config = {}; + config["params"] = { flow_id: params.flowId }; + + if (params.nodeId) { + config["params"] = { nodeId: params.nodeId }; + } + + return await api.get(`${getURL("BUILDS")}`, config); + }; + + const responseFn = async () => { + const response = await getBuildsFn({ + flowId: currentFlow!.id, + }); + + if (currentFlow) { + const flowPool = response.data.vertex_builds; + setFlowPool(flowPool); + } + + return response; + }; + + const queryResult = query(["useGetBuildsQuery"], responseFn, { + placeholderData: keepPreviousData, + }); + + return queryResult; +}; diff --git a/src/frontend/src/modals/IOModal/components/chatView/index.tsx b/src/frontend/src/modals/IOModal/components/chatView/index.tsx index f9d771fea..511cb1085 100644 --- a/src/frontend/src/modals/IOModal/components/chatView/index.tsx +++ b/src/frontend/src/modals/IOModal/components/chatView/index.tsx @@ -1,3 +1,4 @@ +import { useDeleteBuilds } from "@/controllers/API/queries/_builds"; import { usePostUploadFile } from "@/controllers/API/queries/files/use-post-upload-file"; import { useEffect, useRef, useState } from "react"; import ShortUniqueId from "short-unique-id"; @@ -40,6 +41,7 @@ export default function ChatView({ const outputIds = outputs.map((obj) => obj.id); const updateFlowPool = useFlowStore((state) => state.updateFlowPool); const [id, setId] = useState(""); + const { mutate: mutateDeleteFlowPool } = useDeleteBuilds(); //build chat history useEffect(() => { @@ -116,9 +118,15 @@ export default function ChatView({ function clearChat(): void { setChatHistory([]); - deleteFlowPool(currentFlowId).then((_) => { - CleanFlowPool(); - }); + + mutateDeleteFlowPool( + { flowId: currentFlowId }, + { + onSuccess: () => { + CleanFlowPool(); + }, + }, + ); //TODO tell backend to clear chat session if (lockChat) setLockChat(false); } diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index f0c97572f..70f546546 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -1,3 +1,5 @@ +import { useGetBuildsQuery } from "@/controllers/API/queries/_builds"; +import { getInputsAndOutputs } from "@/utils/storeUtils"; import _, { cloneDeep } from "lodash"; import { KeyboardEvent, @@ -35,6 +37,7 @@ import { APIClassType } from "../../../../types/api"; import { FlowType, NodeType } from "../../../../types/flow"; import { checkOldComponents, + cleanEdges, generateFlow, generateNodeFromFlow, getNodeId, @@ -88,7 +91,6 @@ export default function Page({ const redo = useFlowsManagerStore((state) => state.redo); const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); const paste = useFlowStore((state) => state.paste); - const resetFlow = useFlowStore((state) => state.resetFlow); const lastCopiedSelection = useFlowStore( (state) => state.lastCopiedSelection, ); @@ -106,6 +108,13 @@ export default function Page({ const [lastSelection, setLastSelection] = useState(null); + const setFlowState = useFlowStore((state) => state.setFlowState); + const setInputs = useFlowStore((state) => state.setInputs); + const setOutputs = useFlowStore((state) => state.setOutputs); + const setHasIO = useFlowStore((state) => state.setHasIO); + const { inputs, outputs } = getInputsAndOutputs(flow.data!.nodes); + const viewport = flow?.data?.viewport ?? { zoom: 1, x: 0, y: 0 }; + function handleGroupNode() { takeSnapshot(); if (validateSelection(lastSelection!, edges).length === 0) { @@ -113,14 +122,15 @@ export default function Page({ const clonedEdges = cloneDeep(edges); const clonedSelection = cloneDeep(lastSelection); updateIds({ nodes: clonedNodes, edges: clonedEdges }, clonedSelection!); - const { newFlow, removedEdges } = generateFlow( + const { newFlow } = generateFlow( clonedSelection!, clonedNodes, clonedEdges, getRandomName(), ); + const newGroupNode = generateNodeFromFlow(newFlow, getNodeId); - // const newEdges = reconnectEdges(newGroupNode, removedEdges); + setNodes([ ...clonedNodes.filter( (oldNodes) => @@ -130,17 +140,6 @@ export default function Page({ ), newGroupNode, ]); - // setEdges([ - // ...clonedEdges.filter( - // (oldEdge) => - // !clonedSelection!.nodes.some( - // (selectionNode) => - // selectionNode.id === oldEdge.target || - // selectionNode.id === oldEdge.source, - // ), - // ), - // ...newEdges, - // ]); } else { setErrorData({ title: INVALID_SELECTION_ERROR_ALERT, @@ -149,7 +148,6 @@ export default function Page({ } } - const setNode = useFlowStore((state) => state.setNode); useEffect(() => { const handleMouseMove = (event) => { position.current = { x: event.clientX, y: event.clientY }; @@ -164,14 +162,24 @@ export default function Page({ useEffect(() => { if (reactFlowInstance && currentFlowId) { - resetFlow({ - nodes: flow?.data?.nodes ?? [], - edges: flow?.data?.edges ?? [], - viewport: flow?.data?.viewport ?? { zoom: 1, x: 0, y: 0 }, - }); + reactFlowInstance!.setViewport(viewport); } }, [currentFlowId, reactFlowInstance]); + const { isFetching } = useGetBuildsQuery({}); + + useEffect(() => { + if (!isFetching) { + let newEdges = cleanEdges(flow.data!.nodes, flow.data!.edges); + setNodes(flow.data!.nodes); + setEdges(newEdges); + setFlowState(undefined); + setInputs(inputs); + setOutputs(outputs); + setHasIO(inputs.length > 0 || outputs.length > 0); + } + }, [isFetching]); + useEffect(() => { if (checkOldComponents({ nodes: flow?.data?.nodes ?? [] })) { setNoticeData({ diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index 94216d244..408ead525 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -59,12 +59,21 @@ const useFlowStore = create((set, get) => ({ edges: [], isBuilding: false, isPending: true, - hasIO: false, + setHasIO: (hasIO) => { + set({ hasIO }); + }, reactFlowInstance: null, lastCopiedSelection: null, flowPool: {}, + setInputs: (inputs) => { + set({ inputs }); + }, + setOutputs: (outputs) => { + set({ outputs }); + }, inputs: [], outputs: [], + hasIO: get()?.inputs?.length > 0 || get()?.outputs?.length > 0, setFlowPool: (flowPool) => { set({ flowPool }); }, diff --git a/src/frontend/src/types/zustand/flow/index.ts b/src/frontend/src/types/zustand/flow/index.ts index c7a0f1f09..8d17aa34a 100644 --- a/src/frontend/src/types/zustand/flow/index.ts +++ b/src/frontend/src/types/zustand/flow/index.ts @@ -56,6 +56,13 @@ export type FlowStoreType = { onFlowPage: boolean; setOnFlowPage: (onFlowPage: boolean) => void; flowPool: FlowPoolType; + setHasIO: (hasIO: boolean) => void; + setInputs: ( + inputs: Array<{ type: string; id: string; displayName: string }>, + ) => void; + setOutputs: ( + outputs: Array<{ type: string; id: string; displayName: string }>, + ) => void; inputs: Array<{ type: string; id: string; diff --git a/src/frontend/tests/end-to-end/generalBugs-shard-3.spec.ts b/src/frontend/tests/end-to-end/generalBugs-shard-3.spec.ts index cef4b1951..b8452569b 100644 --- a/src/frontend/tests/end-to-end/generalBugs-shard-3.spec.ts +++ b/src/frontend/tests/end-to-end/generalBugs-shard-3.spec.ts @@ -196,3 +196,58 @@ test("should copy code from playground modal", async ({ page }) => { expect(clipboardContent.length).toBeGreaterThan(0); expect(clipboardContent).toContain("Hello"); }); + +test("playground button should be enabled or disabled", async ({ page }) => { + await page.goto("/"); + await page.locator("span").filter({ hasText: "My Collection" }).isVisible(); + await page.waitForTimeout(2000); + + let modalCount = 0; + try { + const modalTitleElement = await page?.getByTestId("modal-title"); + if (modalTitleElement) { + modalCount = await modalTitleElement.count(); + } + } catch (error) { + modalCount = 0; + } + + while (modalCount === 0) { + await page.getByText("New Project", { exact: true }).click(); + await page.waitForTimeout(5000); + modalCount = await page.getByTestId("modal-title")?.count(); + } + + await page.waitForSelector('[data-testid="blank-flow"]', { + timeout: 30000, + }); + + await page.getByTestId("blank-flow").click(); + await page.waitForSelector('[data-testid="extended-disclosure"]', { + timeout: 30000, + }); + + await page.getByTestId("playground-btn-flow").click({ force: true }); + + expect(await page.getByText("Langflow Chat").isHidden()); + + await page.getByPlaceholder("Search").click(); + await page.getByPlaceholder("Search").fill("chat output"); + + await page.waitForTimeout(2000); + + await page + .locator('//*[@id="outputsChat Output"]') + .dragTo(page.locator('//*[@id="react-flow-id"]')); + await page.mouse.up(); + await page.mouse.down(); + await page.waitForSelector('[title="fit view"]', { + timeout: 100000, + }); + + await page.waitForTimeout(2000); + + await page.getByTestId("playground-btn-flow-io").click({ force: true }); + + expect(await page.getByText("Langflow Chat").isVisible()); +});