Skip to content

Commit

Permalink
fix: update all outdated components at once (langflow-ai#4763)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <[email protected]>
  • Loading branch information
4 people authored and diogocabral committed Nov 26, 2024
1 parent 9681faa commit 329fd60
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 7 deletions.
57 changes: 57 additions & 0 deletions src/frontend/src/CustomNodes/hooks/use-update-all-nodes.tsx
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 4 additions & 0 deletions src/frontend/src/CustomNodes/hooks/use-update-node-code.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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) => {
Expand All @@ -32,6 +35,7 @@ const useUpdateNodeCode = (
return newNode;
});

setComponentsToUpdate((old) => old.filter((id) => id !== dataId));
updateNodeInternals(dataId);
},
[dataId, dataNode, setNode, setIsOutdated, updateNodeInternals],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 ? (
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
);
}
19 changes: 13 additions & 6 deletions src/frontend/src/stores/flowStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
},
Expand Down Expand Up @@ -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.",
Expand Down
5 changes: 4 additions & 1 deletion src/frontend/src/types/zustand/flow/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
62 changes: 62 additions & 0 deletions src/frontend/tests/extended/features/outdated-actions.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});

0 comments on commit 329fd60

Please sign in to comment.