From 0b150844128b8f1f07cae4697daee42c3130050c Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:15:50 -0300 Subject: [PATCH] fix: update all outdated components at once (#4763) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update components to update * Added update all components * Update the logic for updating all components * Added dismiss functionality * Removed node from components to update when updated * ✨ (list/index.tsx): add data-testid attribute to list card component for testing purposes ✨ (reactflow): create edges to connect different nodes for data flow in the chatbot application. 📝 (Prompt): Update prompt template with dynamic variables for better customization and flexibility. 📝 (code): update code in ChatInput component to import necessary modules and classes for chat inputs handling ♻️ (code): refactor code in ChatInput component to improve readability and maintainability by organizing imports and defining class attributes clearly 📝 (input.py): Update input fields display names and information for better clarity and understanding 📝 (input.py): Update file input field to support multiple file types and be a list of files 📝 (input.py): Update sender options to be more descriptive as "Machine" and "User" instead of constants 📝 (input.py): Update sender_name input field information to clarify it is the name of the sender 📝 (input.py): Update session_id input field information to explain its purpose and usage 📝 (input.py): Update files input field information to clarify it is for files to be sent with the message 📝 (input.py): Update input_value input field information to clarify it is the text message to be passed as input 📝 (input.py): Update should_store_message input field information to explain its purpose of storing messages in history 📝 (input.py): Update message_response method to handle storing messages based on conditions and updating status 📝 (metadata): Update metadata fields in ChatInput component for better clarity and consistency 📝 (OpenAIModel): Add OpenAI API Key field to the template for configuring the OpenAI model usage 📝 (LCModelComponent): Update OpenAIModelComponent inputs and add support for new features and configurations to enhance text generation capabilities. 📝 (file.py): Update comments and documentation for better clarity and understanding of the code ♻️ (file.py): Refactor code to improve readability and maintainability by restructuring the logic and removing unnecessary code blocks 📝 (schema.json): Update schema for the Output of the model to enable JSON mode and improve functionality 📝 (ChatOutput): Display a chat message in the Playground for better user interaction and experience 📝 (ChatOutput): Update ChatOutput class inputs and outputs structure for better organization and readability. ✨ (frontend): Add a new file 'outdated_flow.json' to store outdated flow data for frontend tests. ✨ (outdated-actions.spec.ts): add test to ensure user can update outdated components in the application * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * fix tests --------- Co-authored-by: cristhianzl Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Gabriel Luiz Freitas Almeida --- .../hooks/use-update-all-nodes.tsx | 57 +++++++ .../hooks/use-update-node-code.tsx | 4 + .../components/PageComponent/index.tsx | 4 + .../components/UpdateAllComponents/index.tsx | 145 ++++++++++++++++++ src/frontend/src/stores/flowStore.ts | 19 ++- src/frontend/src/types/zustand/flow/index.ts | 5 +- .../features/outdated-actions.spec.ts | 62 ++++++++ 7 files changed, 289 insertions(+), 7 deletions(-) create mode 100644 src/frontend/src/CustomNodes/hooks/use-update-all-nodes.tsx create mode 100644 src/frontend/src/pages/FlowPage/components/UpdateAllComponents/index.tsx create mode 100644 src/frontend/tests/extended/features/outdated-actions.spec.ts diff --git a/src/frontend/src/CustomNodes/hooks/use-update-all-nodes.tsx b/src/frontend/src/CustomNodes/hooks/use-update-all-nodes.tsx new file mode 100644 index 000000000..b507cb4d0 --- /dev/null +++ b/src/frontend/src/CustomNodes/hooks/use-update-all-nodes.tsx @@ -0,0 +1,57 @@ +import { cloneDeep } from "lodash"; +import { useCallback } from "react"; +import { APIClassType } from "../../types/api"; +import { NodeType } from "../../types/flow"; + +export type UpdateNodesType = { + nodeId: string; + newNode: APIClassType; + code: string; + name: string; + type?: string; +}; + +const useUpdateAllNodes = ( + setNodes: (callback: (oldNodes: NodeType[]) => NodeType[]) => void, + updateNodeInternals: (nodeId: string) => void, +) => { + const updateAllNodes = useCallback( + (updates: UpdateNodesType[]) => { + setNodes((oldNodes) => { + const newNodes = cloneDeep(oldNodes); + + updates.forEach(({ nodeId, newNode, code, name, type }) => { + const nodeIndex = newNodes.findIndex((n) => n.id === nodeId); + if (nodeIndex === -1) return; + + const updatedNode = newNodes[nodeIndex]; + updatedNode.data = { + ...updatedNode.data, + node: { + ...newNode, + description: + newNode.description ?? updatedNode.data.node?.description, + display_name: + newNode.display_name ?? updatedNode.data.node?.display_name, + edited: false, + }, + }; + + if (type) { + updatedNode.data.type = type; + } + + updatedNode.data.node!.template[name].value = code; + updateNodeInternals(nodeId); + }); + + return newNodes; + }); + }, + [setNodes, updateNodeInternals], + ); + + return updateAllNodes; +}; + +export default useUpdateAllNodes; diff --git a/src/frontend/src/CustomNodes/hooks/use-update-node-code.tsx b/src/frontend/src/CustomNodes/hooks/use-update-node-code.tsx index beb0430c8..961a8056b 100644 --- a/src/frontend/src/CustomNodes/hooks/use-update-node-code.tsx +++ b/src/frontend/src/CustomNodes/hooks/use-update-node-code.tsx @@ -1,3 +1,4 @@ +import useFlowStore from "@/stores/flowStore"; import { cloneDeep } from "lodash"; // or any other deep cloning library you prefer import { useCallback } from "react"; import { APIClassType } from "../../types/api"; @@ -10,6 +11,8 @@ const useUpdateNodeCode = ( setIsUserEdited: (value: boolean) => void, updateNodeInternals: (id: string) => void, ) => { + const { setComponentsToUpdate } = useFlowStore(); + const updateNodeCode = useCallback( (newNodeClass: APIClassType, code: string, name: string, type: string) => { setNode(dataId, (oldNode) => { @@ -32,6 +35,7 @@ const useUpdateNodeCode = ( return newNode; }); + setComponentsToUpdate((old) => old.filter((id) => id !== dataId)); updateNodeInternals(dataId); }, [dataId, dataNode, setNode, setIsOutdated, updateNodeInternals], diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index 2c871aba9..566a96382 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -64,6 +64,7 @@ import { } from "../../../../utils/reactflowUtils"; import ConnectionLineComponent from "../ConnectionLineComponent"; import SelectionMenu from "../SelectionMenuComponent"; +import UpdateAllComponents from "../UpdateAllComponents"; import getRandomName from "./utils/get-random-name"; import isWrappedWithClass from "./utils/is-wrapped-with-class"; @@ -513,6 +514,8 @@ export default function Page({ view }: { view?: boolean }): JSX.Element { }; }, [isAddingNote, shadowBoxWidth, shadowBoxHeight]); + const componentsToUpdate = useFlowStore((state) => state.componentsToUpdate); + return (
{showCanvas ? ( @@ -591,6 +594,7 @@ export default function Page({ view }: { view?: boolean }): JSX.Element { Components + {componentsToUpdate.length > 0 && } state.templates); + const setErrorData = useAlertStore((state) => state.setErrorData); + const [loadingUpdate, setLoadingUpdate] = useState(false); + + const { mutateAsync: validateComponentCode } = usePostValidateComponentCode(); + const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); + + const updateAllNodes = useUpdateAllNodes(setNodes, updateNodeInternals); + + const [dismissed, setDismissed] = useState(false); + + const handleUpdateAllComponents = () => { + setLoadingUpdate(true); + takeSnapshot(); + + let updatedCount = 0; + const updates: UpdateNodesType[] = []; + + const updatePromises = componentsToUpdate.map((nodeId) => { + const node = nodes.find((n) => n.id === nodeId); + if (!node) return Promise.resolve(); + + const thisNodeTemplate = templates[node.data.type]?.template; + if (!thisNodeTemplate?.code) return Promise.resolve(); + + const currentCode = thisNodeTemplate.code.value; + + return new Promise((resolve) => { + validateComponentCode({ + code: currentCode, + frontend_node: node.data.node, + }) + .then(({ data: resData, type }) => { + if (resData && type) { + const newNode = processNodeAdvancedFields(resData, edges, nodeId); + + updates.push({ + nodeId, + newNode, + code: currentCode, + name: "code", + type, + }); + + updatedCount++; + } + resolve(null); + }) + .catch((error) => { + console.error(error); + resolve(null); + }); + }); + }); + + Promise.all(updatePromises) + .then(() => { + if (updatedCount > 0) { + // Batch update all nodes at once + updateAllNodes(updates); + + useAlertStore.getState().setSuccessData({ + title: `Successfully updated ${updatedCount} component${ + updatedCount > 1 ? "s" : "" + }`, + }); + } + }) + .catch((error) => { + setErrorData({ + title: "Error updating components", + list: [ + "There was an error updating the components.", + "If the error persists, please report it on our Discord or GitHub.", + ], + }); + console.error(error); + }) + .finally(() => { + setLoadingUpdate(false); + }); + }; + + if (componentsToUpdate.length === 0) return null; + + return ( +
+
+ + + {componentsToUpdate.length} component + {componentsToUpdate.length > 1 ? "s" : ""} are ready to update + +
+
+ + +
+
+ ); +} diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index 9669753fc..b1489527c 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -57,20 +57,27 @@ const useFlowStore = create((set, get) => ({ } }, autoSaveFlow: undefined, - componentsToUpdate: false, + componentsToUpdate: [], + setComponentsToUpdate: (change) => { + let newChange = + typeof change === "function" ? change(get().componentsToUpdate) : change; + set({ componentsToUpdate: newChange }); + }, updateComponentsToUpdate: (nodes) => { - let outdatedNodes = false; + let outdatedNodes: string[] = []; const templates = useTypesStore.getState().templates; for (let i = 0; i < nodes.length; i++) { const currentCode = templates[nodes[i].data?.type]?.template?.code?.value; const thisNodesCode = nodes[i].data?.node!.template?.code?.value; - outdatedNodes = + if ( currentCode && thisNodesCode && currentCode !== thisNodesCode && !nodes[i].data?.node?.edited && - !componentsToIgnoreUpdate.includes(nodes[i].data?.type); - if (outdatedNodes) break; + !componentsToIgnoreUpdate.includes(nodes[i].data?.type) + ) { + outdatedNodes.push(nodes[i].id); + } } set({ componentsToUpdate: outdatedNodes }); }, @@ -702,7 +709,7 @@ const useFlowStore = create((set, get) => ({ ?.map((element) => element.id) .filter(Boolean) as string[]) ?? get().nodes.map((n) => n.id); useFlowStore.getState().updateBuildStatus(idList, BuildStatus.ERROR); - if (get().componentsToUpdate) + if (get().componentsToUpdate.length > 0) setErrorData({ title: "There are outdated components in the flow. The error could be related to them.", diff --git a/src/frontend/src/types/zustand/flow/index.ts b/src/frontend/src/types/zustand/flow/index.ts index b8aa9415e..b53653cc5 100644 --- a/src/frontend/src/types/zustand/flow/index.ts +++ b/src/frontend/src/types/zustand/flow/index.ts @@ -56,7 +56,10 @@ export type FlowPoolType = { export type FlowStoreType = { fitViewNode: (nodeId: string) => void; autoSaveFlow: (() => void) | undefined; - componentsToUpdate: boolean; + componentsToUpdate: string[]; + setComponentsToUpdate: ( + update: string[] | ((oldState: string[]) => string[]), + ) => void; updateComponentsToUpdate: (nodes: Node[]) => void; onFlowPage: boolean; setOnFlowPage: (onFlowPage: boolean) => void; diff --git a/src/frontend/tests/extended/features/outdated-actions.spec.ts b/src/frontend/tests/extended/features/outdated-actions.spec.ts new file mode 100644 index 000000000..1f98fd157 --- /dev/null +++ b/src/frontend/tests/extended/features/outdated-actions.spec.ts @@ -0,0 +1,62 @@ +import { expect, test } from "@playwright/test"; +import { readFileSync } from "fs"; + +test("user must be able to update outdated components", async ({ page }) => { + await page.goto("/"); + + 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 Flow", { exact: true }).click(); + await page.waitForTimeout(3000); + modalCount = await page.getByTestId("modal-title")?.count(); + } + await page.locator("span").filter({ hasText: "Close" }).first().click(); + + await page.locator("span").filter({ hasText: "My Collection" }).isVisible(); + // Read your file into a buffer. + const jsonContent = readFileSync("tests/assets/outdated_flow.json", "utf-8"); + + // Create the DataTransfer and File + const dataTransfer = await page.evaluateHandle((data) => { + const dt = new DataTransfer(); + // Convert the buffer to a hex array + const file = new File([data], "outdated_flow.json", { + type: "application/json", + }); + dt.items.add(file); + return dt; + }, jsonContent); + + // Now dispatch + await page.getByTestId("cards-wrapper").dispatchEvent("drop", { + dataTransfer, + }); + + await page.waitForTimeout(3000); + + await page.getByTestId("list-card").first().click(); + + await page.waitForSelector("text=components are ready to update", { + timeout: 30000, + state: "visible", + }); + + let outdatedComponents = await page.getByTestId("icon-AlertTriangle").count(); + expect(outdatedComponents).toBeGreaterThan(0); + + await page.getByText("Update All", { exact: true }).click(); + + await page.waitForTimeout(3000); + + outdatedComponents = await page.getByTestId("icon-AlertTriangle").count(); + expect(outdatedComponents).toBe(0); +});