refactor: change endpoint monitor/builds to use useQuery (#2622)

*  (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 <otavio2204@gmail.com>
This commit is contained in:
Cristhian Zanforlin Lousa 2024-07-29 19:56:54 -03:00 committed by GitHub
commit a811834b93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 203 additions and 25 deletions

View file

@ -113,7 +113,10 @@ export default function FlowToolbar(): JSX.Element {
<div className="flex h-full w-full gap-1 rounded-sm transition-all">
{hasIO ? (
<IOModal open={open} setOpen={setOpen} disable={!hasIO}>
<div className="relative inline-flex w-full items-center justify-center gap-1 px-5 py-3 text-sm font-semibold transition-all duration-500 ease-in-out hover:bg-hover">
<div
data-testid="playground-btn-flow-io"
className="relative inline-flex w-full items-center justify-center gap-1 px-5 py-3 text-sm font-semibold transition-all duration-500 ease-in-out hover:bg-hover"
>
<ForwardedIconComponent
name="BotMessageSquareIcon"
className={"h-5 w-5 transition-all"}
@ -124,6 +127,7 @@ export default function FlowToolbar(): JSX.Element {
) : (
<div
className={`relative inline-flex w-full cursor-not-allowed items-center justify-center gap-1 px-5 py-3 text-sm font-semibold text-muted-foreground transition-all duration-150 ease-in-out`}
data-testid="playground-btn-flow"
>
<ForwardedIconComponent
name="BotMessageSquareIcon"

View file

@ -6,6 +6,7 @@ export const URLs = {
FILES: `files`,
VERSION: `version`,
MESSAGES: `monitor/messages`,
BUILDS: `monitor/builds`,
STORE: `store`,
USERS: "users",
LOGOUT: `logout`,

View file

@ -0,0 +1,2 @@
export * from "./use-delete-builds";
export * from "./use-get-builds";

View file

@ -0,0 +1,26 @@
import { useMutationFunctionType } from "@/types/api";
import { api } from "../../api";
import { getURL } from "../../helpers/constants";
import { UseRequestProcessor } from "../../services/request-processor";
interface IDeleteBuilds {
flowId: string;
}
// add types for error handling and success
export const useDeleteBuilds: useMutationFunctionType<IDeleteBuilds> = (
options,
) => {
const { mutate } = UseRequestProcessor();
const deleteBuildsFn = async (payload: IDeleteBuilds): Promise<any> => {
const config = {};
config["params"] = { flow_id: payload.flowId };
const res = await api.delete<any>(`${getURL("BUILDS")}`, config);
return res.data;
};
const mutation = mutate(["useDeleteBuilds"], deleteBuildsFn, options);
return mutation;
};

View file

@ -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<AxiosResponse<{ vertex_builds: FlowPoolType }>> => {
const config = {};
config["params"] = { flow_id: params.flowId };
if (params.nodeId) {
config["params"] = { nodeId: params.nodeId };
}
return await api.get<any>(`${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;
};

View file

@ -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<string>("");
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);
}

View file

@ -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<OnSelectionChangeParams | null>(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({

View file

@ -59,12 +59,21 @@ const useFlowStore = create<FlowStoreType>((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 });
},

View file

@ -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;

View file

@ -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());
});