bugfix: add steps to delete flows caused by new constraints (#3045)

* 📝 (flows.py): refactor delete_multiple_flows function to improve readability and efficiency
✨ (use-delete-flows.ts): add useDeleteFlows hook to handle deletion of multiple flows in frontend
♻️ (index.tsx): refactor handleDeleteMultiple function to use useDeleteFlows hook for deleting multiple flows
🔧 (actionsMainPage-shard-1.spec.ts): add test for selecting and deleting a flow in end-to-end tests

* 📝 (flows.py): Remove unused imports and variables to clean up the code and improve readability
♻️ (flows.py): Refactor code to remove unnecessary import and variable declarations, making the code more concise and maintainable

* add unit tests to delete multiple flows with transactions and build

* format

* add assert on tests
This commit is contained in:
Cristhian Zanforlin Lousa 2024-07-30 10:41:54 -03:00 • committed by GitHub
commit c2b5b98b64
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 175 additions and 17 deletions

View file

@ -7,6 +7,8 @@ from uuid import UUID
import zipfile import zipfile
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from langflow.services.database.models.transactions.crud import get_transactions_by_flow_id
from langflow.services.database.models.vertex_builds.crud import get_vertex_builds_by_flow_id
import orjson import orjson
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
@ -334,11 +336,20 @@ async def delete_multiple_flows(
""" """
try: try:
deleted_flows = db.exec(select(Flow).where(col(Flow.id).in_(flow_ids)).where(Flow.user_id == user.id)).all() flows_to_delete = db.exec(select(Flow).where(col(Flow.id).in_(flow_ids)).where(Flow.user_id == user.id)).all()
for flow in deleted_flows: for flow in flows_to_delete:
transactions_to_delete = get_transactions_by_flow_id(db, flow.id)
for transaction in transactions_to_delete:
db.delete(transaction)
builds_to_delete = get_vertex_builds_by_flow_id(db, flow.id)
for build in builds_to_delete:
db.delete(build)
db.delete(flow) db.delete(flow)
db.commit() db.commit()
return {"deleted": len(deleted_flows)} return {"deleted": len(flows_to_delete)}
except Exception as exc: except Exception as exc:
logger.exception(exc) logger.exception(exc)
raise HTTPException(status_code=500, detail=str(exc)) from exc raise HTTPException(status_code=500, detail=str(exc)) from exc

View file

@ -1,12 +1,13 @@
import json import json
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from langflow.graph.utils import log_transaction, log_vertex_build
import orjson import orjson
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlmodel import Session from sqlmodel import Session
from langflow.api.v1.schemas import FlowListCreate from langflow.api.v1.schemas import FlowListCreate, ResultDataResponse
from langflow.initial_setup.setup import load_starter_projects, load_flows_from_directory from langflow.initial_setup.setup import load_starter_projects, load_flows_from_directory
from langflow.services.database.models.base import orjson_dumps from langflow.services.database.models.base import orjson_dumps
from langflow.services.database.models.flow import Flow, FlowCreate, FlowUpdate from langflow.services.database.models.flow import Flow, FlowCreate, FlowUpdate
@ -19,6 +20,7 @@ from langflow.services.monitor.utils import (
new_duckdb_locked_connection, new_duckdb_locked_connection,
add_row_to_table, add_row_to_table,
) )
from collections import namedtuple
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@ -133,6 +135,66 @@ def test_delete_flows(client: TestClient, json_flow: str, active_user, logged_in
assert response.json().get("deleted") == number_of_flows assert response.json().get("deleted") == number_of_flows
@pytest.mark.asyncio
async def test_delete_flows_with_transaction_and_build(
client: TestClient, json_flow: str, active_user, logged_in_headers
):
# Create ten flows
number_of_flows = 10
flows = [FlowCreate(name=f"Flow {i}", description="description", data={}) for i in range(number_of_flows)]
flow_ids = []
for flow in flows:
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
flow_ids.append(response.json()["id"])
# Create a transaction for each flow
for flow_id in flow_ids:
VertexTuple = namedtuple("VertexTuple", ["id"])
await log_transaction(
str(flow_id), source=VertexTuple(id="vid"), target=VertexTuple(id="tid"), status="success"
)
# Create a build for each flow
for flow_id in flow_ids:
build = {
"valid": True,
"params": {},
"data": ResultDataResponse(),
"artifacts": {},
"vertex_id": "vid",
"flow_id": flow_id,
}
log_vertex_build(
flow_id=build["flow_id"],
vertex_id=build["vertex_id"],
valid=build["valid"],
params=build["params"],
data=build["data"],
artifacts=build.get("artifacts"),
)
response = client.request("DELETE", "api/v1/flows/", headers=logged_in_headers, json=flow_ids)
assert response.status_code == 200, response.content
assert response.json().get("deleted") == number_of_flows
for flow_id in flow_ids:
response = client.request(
"GET", "api/v1/monitor/transactions", params={"flow_id": flow_id}, headers=logged_in_headers
)
assert response.status_code == 200
assert response.json() == []
for flow_id in flow_ids:
response = client.request(
"GET", "api/v1/monitor/builds", params={"flow_id": flow_id}, headers=logged_in_headers
)
assert response.status_code == 200
assert response.json() == {"vertex_builds": {}}
def test_create_flows(client: TestClient, session: Session, json_flow: str, logged_in_headers): def test_create_flows(client: TestClient, session: Session, json_flow: str, logged_in_headers):
flow = orjson.loads(json_flow) flow = orjson.loads(json_flow)
data = flow["data"] data = flow["data"]

View file

@ -0,0 +1,32 @@
import { useMutationFunctionType } from "@/types/api";
import { UseMutationResult } from "@tanstack/react-query";
import { api } from "../../api";
import { getURL } from "../../helpers/constants";
import { UseRequestProcessor } from "../../services/request-processor";
interface IDeleteFlows {
flow_ids: string[];
}
export const useDeleteFlows: useMutationFunctionType<
undefined,
IDeleteFlows
> = (options?) => {
const { mutate } = UseRequestProcessor();
const deleteFlowsFn = async (payload: IDeleteFlows): Promise<any> => {
const response = await api.delete<any>(`${getURL("FLOWS")}/`, {
data: payload.flow_ids,
});
return response.data;
};
const mutation: UseMutationResult<IDeleteFlows, any, IDeleteFlows> = mutate(
["useLoginUser"],
deleteFlowsFn,
options,
);
return mutation;
};

View file

@ -1,4 +1,5 @@
import { usePostDownloadMultipleFlows } from "@/controllers/API/queries/flows"; import { usePostDownloadMultipleFlows } from "@/controllers/API/queries/flows";
import { useDeleteFlows } from "@/controllers/API/queries/flows/use-delete-flows";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { FormProvider, useForm, useWatch } from "react-hook-form"; import { FormProvider, useForm, useWatch } from "react-hook-form";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
@ -188,19 +189,36 @@ export default function ComponentsComponent({
handleExport, handleExport,
); );
const { handleDeleteMultiple } = useDeleteMultipleFlows( const { mutate: mutateDeleteMultipleFlows } = useDeleteFlows();
selectedFlowsComponentsCards,
removeFlow, const handleDeleteMultiple = () => {
resetFilter, mutateDeleteMultipleFlows(
getFoldersApi, {
folderId, flow_ids: selectedFlowsComponentsCards,
myCollectionId!, },
getFolderById, {
setSuccessData, onSuccess: () => {
setErrorData, setAllFlows([]);
setAllFlows, setSelectedFolder(null);
setSelectedFolder,
); resetFilter();
getFoldersApi(true);
if (!folderId || folderId === myCollectionId) {
getFolderById(folderId ? folderId : myCollectionId);
}
setSuccessData({
title: "Selected items deleted successfully",
});
},
onError: () => {
setErrorData({
title: "Error deleting items",
list: ["Please try again"],
});
},
},
);
};
useSelectedFlows(entireFormValues, setSelectedFlowsComponentsCards); useSelectedFlows(entireFormValues, setSelectedFlowsComponentsCards);

View file

@ -36,6 +36,41 @@ test("select and delete all", async ({ page }) => {
await page.getByText("Selected items deleted successfully").isVisible(); await page.getByText("Selected items deleted successfully").isVisible();
}); });
test("select and delete a flow", async ({ page }) => {
await page.goto("/");
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.getByRole("heading", { name: "Basic Prompting" }).click();
await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {
timeout: 100000,
});
await page.getByTestId("icon-ChevronLeft").first().click();
await page.getByTestId("checkbox-component").first().click();
await page.getByTestId("icon-Trash2").click();
await page.getByText("Delete").last().click();
await page.waitForTimeout(1000);
await page.getByText("Selected items deleted successfully").isVisible();
});
test("search flows", async ({ page }) => { test("search flows", async ({ page }) => {
await page.goto("/"); await page.goto("/");
await page.waitForTimeout(2000); await page.waitForTimeout(2000);