fix: update all outdated components at once (#4763)
* 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 <cristhian.lousa@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
This commit is contained in:
parent
ca2aa082df
commit
0b15084412
7 changed files with 289 additions and 7 deletions
57
src/frontend/src/CustomNodes/hooks/use-update-all-nodes.tsx
Normal file
57
src/frontend/src/CustomNodes/hooks/use-update-all-nodes.tsx
Normal file
|
|
@ -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;
|
||||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="h-full w-full bg-canvas" ref={reactFlowWrapper}>
|
||||
{showCanvas ? (
|
||||
|
|
@ -591,6 +594,7 @@ export default function Page({ view }: { view?: boolean }): JSX.Element {
|
|||
<span className="text-foreground">Components</span>
|
||||
</SidebarTrigger>
|
||||
</Panel>
|
||||
{componentsToUpdate.length > 0 && <UpdateAllComponents />}
|
||||
<SelectionMenu
|
||||
lastSelection={lastSelection}
|
||||
isVisible={selectionMenuVisible}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
import { ForwardedIconComponent } from "@/components/genericIconComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { usePostValidateComponentCode } from "@/controllers/API/queries/nodes/use-post-validate-component-code";
|
||||
import { processNodeAdvancedFields } from "@/CustomNodes/helpers/process-node-advanced-fields";
|
||||
import useUpdateAllNodes, {
|
||||
UpdateNodesType,
|
||||
} from "@/CustomNodes/hooks/use-update-all-nodes";
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||
import useFlowStore from "@/stores/flowStore";
|
||||
import { useTypesStore } from "@/stores/typesStore";
|
||||
import { cn } from "@/utils/utils";
|
||||
import { useState } from "react";
|
||||
import { useUpdateNodeInternals } from "reactflow";
|
||||
|
||||
export default function UpdateAllComponents() {
|
||||
const { componentsToUpdate, nodes, edges, setNodes } = useFlowStore();
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const templates = useTypesStore((state) => 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 (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute bottom-2 left-1/2 z-50 flex w-[500px] -translate-x-1/2 items-center gap-8 rounded-lg bg-warning px-4 py-2 text-sm font-medium text-warning-foreground shadow-md transition-all ease-in",
|
||||
dismissed && "translate-y-[120%]",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<ForwardedIconComponent
|
||||
name="AlertTriangle"
|
||||
className="!h-[18px] !w-[18px] shrink-0"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<span>
|
||||
{componentsToUpdate.length} component
|
||||
{componentsToUpdate.length > 1 ? "s" : ""} are ready to update
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
className="shrink-0 text-sm text-warning-foreground"
|
||||
onClick={() => {
|
||||
setDismissed(true);
|
||||
}}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
<Button
|
||||
variant="warning"
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={handleUpdateAllComponents}
|
||||
loading={loadingUpdate}
|
||||
>
|
||||
Update All
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -57,20 +57,27 @@ const useFlowStore = create<FlowStoreType>((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<FlowStoreType>((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.",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue