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:
parent
0fcba55f51
commit
a811834b93
10 changed files with 203 additions and 25 deletions
|
|
@ -113,7 +113,10 @@ export default function FlowToolbar(): JSX.Element {
|
||||||
<div className="flex h-full w-full gap-1 rounded-sm transition-all">
|
<div className="flex h-full w-full gap-1 rounded-sm transition-all">
|
||||||
{hasIO ? (
|
{hasIO ? (
|
||||||
<IOModal open={open} setOpen={setOpen} disable={!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
|
<ForwardedIconComponent
|
||||||
name="BotMessageSquareIcon"
|
name="BotMessageSquareIcon"
|
||||||
className={"h-5 w-5 transition-all"}
|
className={"h-5 w-5 transition-all"}
|
||||||
|
|
@ -124,6 +127,7 @@ export default function FlowToolbar(): JSX.Element {
|
||||||
) : (
|
) : (
|
||||||
<div
|
<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`}
|
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
|
<ForwardedIconComponent
|
||||||
name="BotMessageSquareIcon"
|
name="BotMessageSquareIcon"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export const URLs = {
|
||||||
FILES: `files`,
|
FILES: `files`,
|
||||||
VERSION: `version`,
|
VERSION: `version`,
|
||||||
MESSAGES: `monitor/messages`,
|
MESSAGES: `monitor/messages`,
|
||||||
|
BUILDS: `monitor/builds`,
|
||||||
STORE: `store`,
|
STORE: `store`,
|
||||||
USERS: "users",
|
USERS: "users",
|
||||||
LOGOUT: `logout`,
|
LOGOUT: `logout`,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./use-delete-builds";
|
||||||
|
export * from "./use-get-builds";
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useDeleteBuilds } from "@/controllers/API/queries/_builds";
|
||||||
import { usePostUploadFile } from "@/controllers/API/queries/files/use-post-upload-file";
|
import { usePostUploadFile } from "@/controllers/API/queries/files/use-post-upload-file";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import ShortUniqueId from "short-unique-id";
|
import ShortUniqueId from "short-unique-id";
|
||||||
|
|
@ -40,6 +41,7 @@ export default function ChatView({
|
||||||
const outputIds = outputs.map((obj) => obj.id);
|
const outputIds = outputs.map((obj) => obj.id);
|
||||||
const updateFlowPool = useFlowStore((state) => state.updateFlowPool);
|
const updateFlowPool = useFlowStore((state) => state.updateFlowPool);
|
||||||
const [id, setId] = useState<string>("");
|
const [id, setId] = useState<string>("");
|
||||||
|
const { mutate: mutateDeleteFlowPool } = useDeleteBuilds();
|
||||||
|
|
||||||
//build chat history
|
//build chat history
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -116,9 +118,15 @@ export default function ChatView({
|
||||||
|
|
||||||
function clearChat(): void {
|
function clearChat(): void {
|
||||||
setChatHistory([]);
|
setChatHistory([]);
|
||||||
deleteFlowPool(currentFlowId).then((_) => {
|
|
||||||
|
mutateDeleteFlowPool(
|
||||||
|
{ flowId: currentFlowId },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
CleanFlowPool();
|
CleanFlowPool();
|
||||||
});
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
//TODO tell backend to clear chat session
|
//TODO tell backend to clear chat session
|
||||||
if (lockChat) setLockChat(false);
|
if (lockChat) setLockChat(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { useGetBuildsQuery } from "@/controllers/API/queries/_builds";
|
||||||
|
import { getInputsAndOutputs } from "@/utils/storeUtils";
|
||||||
import _, { cloneDeep } from "lodash";
|
import _, { cloneDeep } from "lodash";
|
||||||
import {
|
import {
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
|
|
@ -35,6 +37,7 @@ import { APIClassType } from "../../../../types/api";
|
||||||
import { FlowType, NodeType } from "../../../../types/flow";
|
import { FlowType, NodeType } from "../../../../types/flow";
|
||||||
import {
|
import {
|
||||||
checkOldComponents,
|
checkOldComponents,
|
||||||
|
cleanEdges,
|
||||||
generateFlow,
|
generateFlow,
|
||||||
generateNodeFromFlow,
|
generateNodeFromFlow,
|
||||||
getNodeId,
|
getNodeId,
|
||||||
|
|
@ -88,7 +91,6 @@ export default function Page({
|
||||||
const redo = useFlowsManagerStore((state) => state.redo);
|
const redo = useFlowsManagerStore((state) => state.redo);
|
||||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||||
const paste = useFlowStore((state) => state.paste);
|
const paste = useFlowStore((state) => state.paste);
|
||||||
const resetFlow = useFlowStore((state) => state.resetFlow);
|
|
||||||
const lastCopiedSelection = useFlowStore(
|
const lastCopiedSelection = useFlowStore(
|
||||||
(state) => state.lastCopiedSelection,
|
(state) => state.lastCopiedSelection,
|
||||||
);
|
);
|
||||||
|
|
@ -106,6 +108,13 @@ export default function Page({
|
||||||
const [lastSelection, setLastSelection] =
|
const [lastSelection, setLastSelection] =
|
||||||
useState<OnSelectionChangeParams | null>(null);
|
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() {
|
function handleGroupNode() {
|
||||||
takeSnapshot();
|
takeSnapshot();
|
||||||
if (validateSelection(lastSelection!, edges).length === 0) {
|
if (validateSelection(lastSelection!, edges).length === 0) {
|
||||||
|
|
@ -113,14 +122,15 @@ export default function Page({
|
||||||
const clonedEdges = cloneDeep(edges);
|
const clonedEdges = cloneDeep(edges);
|
||||||
const clonedSelection = cloneDeep(lastSelection);
|
const clonedSelection = cloneDeep(lastSelection);
|
||||||
updateIds({ nodes: clonedNodes, edges: clonedEdges }, clonedSelection!);
|
updateIds({ nodes: clonedNodes, edges: clonedEdges }, clonedSelection!);
|
||||||
const { newFlow, removedEdges } = generateFlow(
|
const { newFlow } = generateFlow(
|
||||||
clonedSelection!,
|
clonedSelection!,
|
||||||
clonedNodes,
|
clonedNodes,
|
||||||
clonedEdges,
|
clonedEdges,
|
||||||
getRandomName(),
|
getRandomName(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const newGroupNode = generateNodeFromFlow(newFlow, getNodeId);
|
const newGroupNode = generateNodeFromFlow(newFlow, getNodeId);
|
||||||
// const newEdges = reconnectEdges(newGroupNode, removedEdges);
|
|
||||||
setNodes([
|
setNodes([
|
||||||
...clonedNodes.filter(
|
...clonedNodes.filter(
|
||||||
(oldNodes) =>
|
(oldNodes) =>
|
||||||
|
|
@ -130,17 +140,6 @@ export default function Page({
|
||||||
),
|
),
|
||||||
newGroupNode,
|
newGroupNode,
|
||||||
]);
|
]);
|
||||||
// setEdges([
|
|
||||||
// ...clonedEdges.filter(
|
|
||||||
// (oldEdge) =>
|
|
||||||
// !clonedSelection!.nodes.some(
|
|
||||||
// (selectionNode) =>
|
|
||||||
// selectionNode.id === oldEdge.target ||
|
|
||||||
// selectionNode.id === oldEdge.source,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ...newEdges,
|
|
||||||
// ]);
|
|
||||||
} else {
|
} else {
|
||||||
setErrorData({
|
setErrorData({
|
||||||
title: INVALID_SELECTION_ERROR_ALERT,
|
title: INVALID_SELECTION_ERROR_ALERT,
|
||||||
|
|
@ -149,7 +148,6 @@ export default function Page({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setNode = useFlowStore((state) => state.setNode);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMouseMove = (event) => {
|
const handleMouseMove = (event) => {
|
||||||
position.current = { x: event.clientX, y: event.clientY };
|
position.current = { x: event.clientX, y: event.clientY };
|
||||||
|
|
@ -164,14 +162,24 @@ export default function Page({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (reactFlowInstance && currentFlowId) {
|
if (reactFlowInstance && currentFlowId) {
|
||||||
resetFlow({
|
reactFlowInstance!.setViewport(viewport);
|
||||||
nodes: flow?.data?.nodes ?? [],
|
|
||||||
edges: flow?.data?.edges ?? [],
|
|
||||||
viewport: flow?.data?.viewport ?? { zoom: 1, x: 0, y: 0 },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [currentFlowId, reactFlowInstance]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (checkOldComponents({ nodes: flow?.data?.nodes ?? [] })) {
|
if (checkOldComponents({ nodes: flow?.data?.nodes ?? [] })) {
|
||||||
setNoticeData({
|
setNoticeData({
|
||||||
|
|
|
||||||
|
|
@ -59,12 +59,21 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
||||||
edges: [],
|
edges: [],
|
||||||
isBuilding: false,
|
isBuilding: false,
|
||||||
isPending: true,
|
isPending: true,
|
||||||
hasIO: false,
|
setHasIO: (hasIO) => {
|
||||||
|
set({ hasIO });
|
||||||
|
},
|
||||||
reactFlowInstance: null,
|
reactFlowInstance: null,
|
||||||
lastCopiedSelection: null,
|
lastCopiedSelection: null,
|
||||||
flowPool: {},
|
flowPool: {},
|
||||||
|
setInputs: (inputs) => {
|
||||||
|
set({ inputs });
|
||||||
|
},
|
||||||
|
setOutputs: (outputs) => {
|
||||||
|
set({ outputs });
|
||||||
|
},
|
||||||
inputs: [],
|
inputs: [],
|
||||||
outputs: [],
|
outputs: [],
|
||||||
|
hasIO: get()?.inputs?.length > 0 || get()?.outputs?.length > 0,
|
||||||
setFlowPool: (flowPool) => {
|
setFlowPool: (flowPool) => {
|
||||||
set({ flowPool });
|
set({ flowPool });
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,13 @@ export type FlowStoreType = {
|
||||||
onFlowPage: boolean;
|
onFlowPage: boolean;
|
||||||
setOnFlowPage: (onFlowPage: boolean) => void;
|
setOnFlowPage: (onFlowPage: boolean) => void;
|
||||||
flowPool: FlowPoolType;
|
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<{
|
inputs: Array<{
|
||||||
type: string;
|
type: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -196,3 +196,58 @@ test("should copy code from playground modal", async ({ page }) => {
|
||||||
expect(clipboardContent.length).toBeGreaterThan(0);
|
expect(clipboardContent.length).toBeGreaterThan(0);
|
||||||
expect(clipboardContent).toContain("Hello");
|
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());
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue