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 { cloneDeep } from "lodash"; // or any other deep cloning library you prefer
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { APIClassType } from "../../types/api";
|
import { APIClassType } from "../../types/api";
|
||||||
|
|
@ -10,6 +11,8 @@ const useUpdateNodeCode = (
|
||||||
setIsUserEdited: (value: boolean) => void,
|
setIsUserEdited: (value: boolean) => void,
|
||||||
updateNodeInternals: (id: string) => void,
|
updateNodeInternals: (id: string) => void,
|
||||||
) => {
|
) => {
|
||||||
|
const { setComponentsToUpdate } = useFlowStore();
|
||||||
|
|
||||||
const updateNodeCode = useCallback(
|
const updateNodeCode = useCallback(
|
||||||
(newNodeClass: APIClassType, code: string, name: string, type: string) => {
|
(newNodeClass: APIClassType, code: string, name: string, type: string) => {
|
||||||
setNode(dataId, (oldNode) => {
|
setNode(dataId, (oldNode) => {
|
||||||
|
|
@ -32,6 +35,7 @@ const useUpdateNodeCode = (
|
||||||
return newNode;
|
return newNode;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setComponentsToUpdate((old) => old.filter((id) => id !== dataId));
|
||||||
updateNodeInternals(dataId);
|
updateNodeInternals(dataId);
|
||||||
},
|
},
|
||||||
[dataId, dataNode, setNode, setIsOutdated, updateNodeInternals],
|
[dataId, dataNode, setNode, setIsOutdated, updateNodeInternals],
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ import {
|
||||||
} from "../../../../utils/reactflowUtils";
|
} from "../../../../utils/reactflowUtils";
|
||||||
import ConnectionLineComponent from "../ConnectionLineComponent";
|
import ConnectionLineComponent from "../ConnectionLineComponent";
|
||||||
import SelectionMenu from "../SelectionMenuComponent";
|
import SelectionMenu from "../SelectionMenuComponent";
|
||||||
|
import UpdateAllComponents from "../UpdateAllComponents";
|
||||||
import getRandomName from "./utils/get-random-name";
|
import getRandomName from "./utils/get-random-name";
|
||||||
import isWrappedWithClass from "./utils/is-wrapped-with-class";
|
import isWrappedWithClass from "./utils/is-wrapped-with-class";
|
||||||
|
|
||||||
|
|
@ -513,6 +514,8 @@ export default function Page({ view }: { view?: boolean }): JSX.Element {
|
||||||
};
|
};
|
||||||
}, [isAddingNote, shadowBoxWidth, shadowBoxHeight]);
|
}, [isAddingNote, shadowBoxWidth, shadowBoxHeight]);
|
||||||
|
|
||||||
|
const componentsToUpdate = useFlowStore((state) => state.componentsToUpdate);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full bg-canvas" ref={reactFlowWrapper}>
|
<div className="h-full w-full bg-canvas" ref={reactFlowWrapper}>
|
||||||
{showCanvas ? (
|
{showCanvas ? (
|
||||||
|
|
@ -591,6 +594,7 @@ export default function Page({ view }: { view?: boolean }): JSX.Element {
|
||||||
<span className="text-foreground">Components</span>
|
<span className="text-foreground">Components</span>
|
||||||
</SidebarTrigger>
|
</SidebarTrigger>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
{componentsToUpdate.length > 0 && <UpdateAllComponents />}
|
||||||
<SelectionMenu
|
<SelectionMenu
|
||||||
lastSelection={lastSelection}
|
lastSelection={lastSelection}
|
||||||
isVisible={selectionMenuVisible}
|
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,
|
autoSaveFlow: undefined,
|
||||||
componentsToUpdate: false,
|
componentsToUpdate: [],
|
||||||
|
setComponentsToUpdate: (change) => {
|
||||||
|
let newChange =
|
||||||
|
typeof change === "function" ? change(get().componentsToUpdate) : change;
|
||||||
|
set({ componentsToUpdate: newChange });
|
||||||
|
},
|
||||||
updateComponentsToUpdate: (nodes) => {
|
updateComponentsToUpdate: (nodes) => {
|
||||||
let outdatedNodes = false;
|
let outdatedNodes: string[] = [];
|
||||||
const templates = useTypesStore.getState().templates;
|
const templates = useTypesStore.getState().templates;
|
||||||
for (let i = 0; i < nodes.length; i++) {
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
const currentCode = templates[nodes[i].data?.type]?.template?.code?.value;
|
const currentCode = templates[nodes[i].data?.type]?.template?.code?.value;
|
||||||
const thisNodesCode = nodes[i].data?.node!.template?.code?.value;
|
const thisNodesCode = nodes[i].data?.node!.template?.code?.value;
|
||||||
outdatedNodes =
|
if (
|
||||||
currentCode &&
|
currentCode &&
|
||||||
thisNodesCode &&
|
thisNodesCode &&
|
||||||
currentCode !== thisNodesCode &&
|
currentCode !== thisNodesCode &&
|
||||||
!nodes[i].data?.node?.edited &&
|
!nodes[i].data?.node?.edited &&
|
||||||
!componentsToIgnoreUpdate.includes(nodes[i].data?.type);
|
!componentsToIgnoreUpdate.includes(nodes[i].data?.type)
|
||||||
if (outdatedNodes) break;
|
) {
|
||||||
|
outdatedNodes.push(nodes[i].id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
set({ componentsToUpdate: outdatedNodes });
|
set({ componentsToUpdate: outdatedNodes });
|
||||||
},
|
},
|
||||||
|
|
@ -702,7 +709,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
||||||
?.map((element) => element.id)
|
?.map((element) => element.id)
|
||||||
.filter(Boolean) as string[]) ?? get().nodes.map((n) => n.id);
|
.filter(Boolean) as string[]) ?? get().nodes.map((n) => n.id);
|
||||||
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.ERROR);
|
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.ERROR);
|
||||||
if (get().componentsToUpdate)
|
if (get().componentsToUpdate.length > 0)
|
||||||
setErrorData({
|
setErrorData({
|
||||||
title:
|
title:
|
||||||
"There are outdated components in the flow. The error could be related to them.",
|
"There are outdated components in the flow. The error could be related to them.",
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,10 @@ export type FlowPoolType = {
|
||||||
export type FlowStoreType = {
|
export type FlowStoreType = {
|
||||||
fitViewNode: (nodeId: string) => void;
|
fitViewNode: (nodeId: string) => void;
|
||||||
autoSaveFlow: (() => void) | undefined;
|
autoSaveFlow: (() => void) | undefined;
|
||||||
componentsToUpdate: boolean;
|
componentsToUpdate: string[];
|
||||||
|
setComponentsToUpdate: (
|
||||||
|
update: string[] | ((oldState: string[]) => string[]),
|
||||||
|
) => void;
|
||||||
updateComponentsToUpdate: (nodes: Node[]) => void;
|
updateComponentsToUpdate: (nodes: Node[]) => void;
|
||||||
onFlowPage: boolean;
|
onFlowPage: boolean;
|
||||||
setOnFlowPage: (onFlowPage: boolean) => void;
|
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