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:
parent
fc73e2c448
commit
c2b5b98b64
5 changed files with 175 additions and 17 deletions
|
|
@ -7,6 +7,8 @@ from uuid import UUID
|
|||
import zipfile
|
||||
|
||||
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
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
|
@ -334,11 +336,20 @@ async def delete_multiple_flows(
|
|||
|
||||
"""
|
||||
try:
|
||||
deleted_flows = db.exec(select(Flow).where(col(Flow.id).in_(flow_ids)).where(Flow.user_id == user.id)).all()
|
||||
for flow in deleted_flows:
|
||||
flows_to_delete = db.exec(select(Flow).where(col(Flow.id).in_(flow_ids)).where(Flow.user_id == user.id)).all()
|
||||
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.commit()
|
||||
return {"deleted": len(deleted_flows)}
|
||||
return {"deleted": len(flows_to_delete)}
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import json
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from langflow.graph.utils import log_transaction, log_vertex_build
|
||||
import orjson
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
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.services.database.models.base import orjson_dumps
|
||||
from langflow.services.database.models.flow import Flow, FlowCreate, FlowUpdate
|
||||
|
|
@ -19,6 +20,7 @@ from langflow.services.monitor.utils import (
|
|||
new_duckdb_locked_connection,
|
||||
add_row_to_table,
|
||||
)
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@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):
|
||||
flow = orjson.loads(json_flow)
|
||||
data = flow["data"]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { usePostDownloadMultipleFlows } from "@/controllers/API/queries/flows";
|
||||
import { useDeleteFlows } from "@/controllers/API/queries/flows/use-delete-flows";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { FormProvider, useForm, useWatch } from "react-hook-form";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
|
@ -188,19 +189,36 @@ export default function ComponentsComponent({
|
|||
handleExport,
|
||||
);
|
||||
|
||||
const { handleDeleteMultiple } = useDeleteMultipleFlows(
|
||||
selectedFlowsComponentsCards,
|
||||
removeFlow,
|
||||
resetFilter,
|
||||
getFoldersApi,
|
||||
folderId,
|
||||
myCollectionId!,
|
||||
getFolderById,
|
||||
setSuccessData,
|
||||
setErrorData,
|
||||
setAllFlows,
|
||||
setSelectedFolder,
|
||||
);
|
||||
const { mutate: mutateDeleteMultipleFlows } = useDeleteFlows();
|
||||
|
||||
const handleDeleteMultiple = () => {
|
||||
mutateDeleteMultipleFlows(
|
||||
{
|
||||
flow_ids: selectedFlowsComponentsCards,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setAllFlows([]);
|
||||
setSelectedFolder(null);
|
||||
|
||||
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);
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,41 @@ test("select and delete all", async ({ page }) => {
|
|||
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 }) => {
|
||||
await page.goto("/");
|
||||
await page.waitForTimeout(2000);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue