diff --git a/src/backend/base/langflow/api/v1/flows.py b/src/backend/base/langflow/api/v1/flows.py
index 014250107..b6c7896a3 100644
--- a/src/backend/base/langflow/api/v1/flows.py
+++ b/src/backend/base/langflow/api/v1/flows.py
@@ -12,7 +12,8 @@ import orjson
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
-from loguru import logger
+from fastapi_pagination import Page, Params, add_pagination
+from fastapi_pagination.ext.sqlalchemy import paginate
from sqlmodel import Session, and_, col, select
from langflow.api.utils import cascade_delete_flow, remove_api_keys, validate_is_component
@@ -20,6 +21,7 @@ from langflow.api.v1.schemas import FlowListCreate
from langflow.initial_setup.setup import STARTER_FOLDER_NAME
from langflow.services.auth.utils import get_current_active_user
from langflow.services.database.models.flow import Flow, FlowCreate, FlowRead, FlowUpdate
+from langflow.services.database.models.flow.model import FlowHeader
from langflow.services.database.models.flow.utils import get_webhook_component_in_flow
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
from langflow.services.database.models.folder.model import Folder
@@ -121,7 +123,7 @@ def create_flow(
raise HTTPException(status_code=500, detail=str(e)) from e
-@router.get("/", response_model=list[FlowRead], status_code=200)
+@router.get("/", response_model=list[FlowRead] | Page[FlowRead] | list[FlowHeader], status_code=200)
def read_flows(
*,
current_user: User = Depends(get_current_active_user),
@@ -129,56 +131,72 @@ def read_flows(
settings_service: SettingsService = Depends(get_settings_service),
remove_example_flows: bool = False,
components_only: bool = False,
+ get_all: bool = False,
+ folder_id: UUID | None = None,
+ params: Params = Depends(),
+ header_flows: bool = False,
):
"""
- Retrieve a list of flows.
+ Retrieve a list of flows with pagination support.
Args:
current_user (User): The current authenticated user.
session (Session): The database session.
settings_service (SettingsService): The settings service.
- remove_example_flows (bool, optional): Whether to remove example flows. Defaults to False.
components_only (bool, optional): Whether to return only components. Defaults to False.
-
+ get_all (bool, optional): Whether to return all flows without pagination. Defaults to False.
+ folder_id (UUID, optional): The folder ID. Defaults to None.
+ params (Params): Pagination parameters.
+ remove_example_flows (bool, optional): Whether to remove example flows. Defaults to False.
Returns:
- List[Dict]: A list of flows in JSON format.
+ Union[list[FlowRead], Page[FlowRead]]: A list of flows or a paginated response containing the list of flows.
"""
try:
auth_settings = settings_service.auth_settings
+
+ default_folder = session.exec(select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME)).first()
+ default_folder_id = default_folder.id if default_folder else None
+
+ starter_folder = session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME)).first()
+ starter_folder_id = starter_folder.id if starter_folder else None
+
+ if not folder_id:
+ folder_id = default_folder_id
+
if auth_settings.AUTO_LOGIN:
stmt = select(Flow).where(
(Flow.user_id == None) | (Flow.user_id == current_user.id) # noqa: E711
)
- if components_only:
- stmt = stmt.where(Flow.is_component == True) # noqa: E712
- flows = session.exec(stmt).all()
-
else:
- flows = current_user.flows
-
- flows = validate_is_component(flows)
- if components_only:
- flows = [flow for flow in flows if flow.is_component]
- flow_ids = [flow.id for flow in flows]
- # with the session get the flows that DO NOT have a user_id
- folder = session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME)).first()
-
- if not remove_example_flows and not components_only:
- try:
- example_flows = folder.flows if folder else []
- for example_flow in example_flows:
- if example_flow.id not in flow_ids:
- flows.append(example_flow)
- except Exception: # noqa: BLE001
- logger.exception("Error getting example flows")
+ stmt = select(Flow).where(Flow.user_id == current_user.id)
if remove_example_flows:
- flows = [flow for flow in flows if flow.folder_id != folder.id]
+ stmt = stmt.where(Flow.folder_id != starter_folder_id)
+
+ if components_only:
+ stmt = stmt.where(Flow.is_component == True) # noqa: E712
+
+ if not get_all:
+ stmt = stmt.where(Flow.folder_id == folder_id)
+
+ if get_all:
+ flows = session.exec(stmt).all()
+ flows = validate_is_component(flows)
+ if components_only:
+ flows = [flow for flow in flows if flow.is_component]
+ if remove_example_flows and starter_folder_id:
+ flows = [flow for flow in flows if flow.folder_id != starter_folder_id]
+ if header_flows:
+ return [
+ {"id": flow.id, "name": flow.name, "folder_id": flow.folder_id, "is_component": flow.is_component}
+ for flow in flows
+ ]
+ return flows
+ return paginate(session, stmt, params=params)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e
- return [jsonable_encoder(flow) for flow in flows]
@router.get("/{flow_id}", response_model=FlowRead, status_code=200)
@@ -401,3 +419,35 @@ async def download_multiple_file(
headers={"Content-Disposition": f"attachment; filename={filename}"},
)
return flows_without_api_keys[0]
+
+
+@router.get("/basic_examples/", response_model=list[FlowRead], status_code=200)
+def read_basic_examples(
+ *,
+ session: Session = Depends(get_session),
+):
+ """
+ Retrieve a list of basic example flows.
+
+ Args:
+ session (Session): The database session.
+
+ Returns:
+ list[FlowRead]: A list of basic example flows.
+ """
+
+ try:
+ # Get the starter folder
+ starter_folder = session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME)).first()
+
+ if not starter_folder:
+ return []
+
+ # Get all flows in the starter folder
+ return session.exec(select(Flow).where(Flow.folder_id == starter_folder.id)).all()
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e)) from e
+
+
+add_pagination(router)
diff --git a/src/backend/base/langflow/api/v1/folders.py b/src/backend/base/langflow/api/v1/folders.py
index 4fb9ae2d4..42fc581a4 100644
--- a/src/backend/base/langflow/api/v1/folders.py
+++ b/src/backend/base/langflow/api/v1/folders.py
@@ -1,5 +1,7 @@
import orjson
from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile, status
+from fastapi_pagination import Params
+from fastapi_pagination.ext.sqlmodel import paginate
from sqlalchemy import or_, update
from sqlmodel import Session, select
@@ -8,6 +10,7 @@ from langflow.api.v1.flows import create_flows
from langflow.api.v1.schemas import FlowListCreate, FlowListReadWithFolderName
from langflow.helpers.flow import generate_unique_flow_name
from langflow.helpers.folders import generate_unique_folder_name
+from langflow.initial_setup.setup import STARTER_FOLDER_NAME
from langflow.services.auth.utils import get_current_active_user
from langflow.services.database.models.flow.model import Flow, FlowCreate, FlowRead
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
@@ -15,9 +18,9 @@ from langflow.services.database.models.folder.model import (
Folder,
FolderCreate,
FolderRead,
- FolderReadWithFlows,
FolderUpdate,
)
+from langflow.services.database.models.folder.pagination_model import FolderWithPaginatedFlows
from langflow.services.database.models.user.model import User
from langflow.services.deps import get_session
@@ -89,25 +92,41 @@ def read_folders(
or_(Folder.user_id == current_user.id, Folder.user_id == None) # noqa: E711
)
).all()
+ folders = [folder for folder in folders if folder.name != STARTER_FOLDER_NAME]
return sorted(folders, key=lambda x: x.name != DEFAULT_FOLDER_NAME)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e
-@router.get("/{folder_id}", response_model=FolderReadWithFlows, status_code=200)
+@router.get("/{folder_id}", response_model=FolderWithPaginatedFlows, status_code=200)
def read_folder(
*,
session: Session = Depends(get_session),
folder_id: str,
current_user: User = Depends(get_current_active_user),
+ params: Params = Depends(),
+ is_component: bool = False,
+ is_flow: bool = False,
+ search: str = "",
):
try:
folder = session.exec(select(Folder).where(Folder.id == folder_id, Folder.user_id == current_user.id)).first()
if not folder:
raise HTTPException(status_code=404, detail="Folder not found")
- flows_from_current_user_in_folder = [flow for flow in folder.flows if flow.user_id == current_user.id]
- folder.flows = flows_from_current_user_in_folder
- return folder
+
+ stmt = select(Flow).where(Flow.folder_id == folder_id, Flow.user_id == current_user.id)
+
+ if Flow.updated_at is not None:
+ stmt = stmt.order_by(Flow.updated_at.desc()) # type: ignore[attr-defined]
+ if is_component:
+ stmt = stmt.where(Flow.is_component == True) # noqa: E712
+ if is_flow:
+ stmt = stmt.where(Flow.is_component == False) # noqa: E712
+ if search:
+ stmt = stmt.where(Flow.name.like(f"%{search}%")) # type: ignore[attr-defined]
+ paginated_flows = paginate(session, stmt, params=params)
+
+ return FolderWithPaginatedFlows(folder=FolderRead.model_validate(folder), flows=paginated_flows)
except Exception as e:
if "No result found" in str(e):
raise HTTPException(status_code=404, detail="Folder not found") from e
diff --git a/src/backend/base/langflow/services/database/models/flow/model.py b/src/backend/base/langflow/services/database/models/flow/model.py
index 100da7681..88dd2b5e8 100644
--- a/src/backend/base/langflow/services/database/models/flow/model.py
+++ b/src/backend/base/langflow/services/database/models/flow/model.py
@@ -9,7 +9,7 @@ import emoji
from emoji import purely_emoji
from fastapi import HTTPException, status
from loguru import logger
-from pydantic import field_serializer, field_validator
+from pydantic import BaseModel, field_serializer, field_validator
from sqlalchemy import Text, UniqueConstraint
from sqlmodel import JSON, Column, Field, Relationship, SQLModel
@@ -190,6 +190,22 @@ class FlowRead(FlowBase):
folder_id: UUID | None = Field()
+class FlowHeader(BaseModel):
+ id: UUID
+ name: str
+ folder_id: UUID | None = None
+ is_component: bool | None = None
+ endpoint_name: str | None = None
+ description: str | None = None
+
+
+class PaginatedFlowResponse(BaseModel):
+ flows: list[FlowRead]
+ total: int
+ page_size: int
+ page_index: int
+
+
class FlowUpdate(SQLModel):
name: str | None = None
description: str | None = None
diff --git a/src/backend/base/langflow/services/database/models/folder/pagination_model.py b/src/backend/base/langflow/services/database/models/folder/pagination_model.py
new file mode 100644
index 000000000..46dcdc687
--- /dev/null
+++ b/src/backend/base/langflow/services/database/models/folder/pagination_model.py
@@ -0,0 +1,10 @@
+from fastapi_pagination import Page
+
+from langflow.helpers.base_model import BaseModel
+from langflow.services.database.models.flow.model import Flow
+from langflow.services.database.models.folder.model import FolderRead
+
+
+class FolderWithPaginatedFlows(BaseModel):
+ folder: FolderRead
+ flows: Page[Flow]
diff --git a/src/backend/base/pyproject.toml b/src/backend/base/pyproject.toml
index 913d51a1d..e4cd7dbb6 100644
--- a/src/backend/base/pyproject.toml
+++ b/src/backend/base/pyproject.toml
@@ -154,7 +154,8 @@ dependencies = [
"spider-client>=0.0.27",
"diskcache>=5.6.3",
"clickhouse-connect==0.7.19",
- "assemblyai>=0.33.0"
+ "assemblyai>=0.33.0",
+ "fastapi-pagination>=0.12.29",
]
[project.urls]
diff --git a/src/backend/tests/unit/test_database.py b/src/backend/tests/unit/test_database.py
index 1b9668e50..0f99d850b 100644
--- a/src/backend/tests/unit/test_database.py
+++ b/src/backend/tests/unit/test_database.py
@@ -67,8 +67,30 @@ async def test_read_flows(client: TestClient, json_flow: str, active_user, logge
assert len(response.json()) > 0
+async def test_read_flows_pagination(client: TestClient, json_flow: str, active_user, logged_in_headers):
+ response = await client.get("api/v1/flows/", headers=logged_in_headers)
+ assert response.status_code == 200
+ assert response.json()["page"] == 1
+ assert response.json()["size"] == 50
+ assert response.json()["pages"] == 0
+ assert response.json()["total"] == 0
+ assert len(response.json()["items"]) == 0
+
+
+async def test_read_flows_pagination_with_params(client: TestClient, json_flow: str, active_user, logged_in_headers):
+ response = await client.get("api/v1/flows/", headers=logged_in_headers, params={"page": 3, "size": 10})
+ assert response.status_code == 200
+ assert response.json()["page"] == 3
+ assert response.json()["size"] == 10
+ assert response.json()["pages"] == 0
+ assert response.json()["total"] == 0
+ assert len(response.json()["items"]) == 0
+
+
async def test_read_flows_components_only(client: TestClient, flow_component: dict, logged_in_headers):
- response = await client.get("api/v1/flows/", headers=logged_in_headers, params={"components_only": True})
+ response = await client.get(
+ "api/v1/flows/", headers=logged_in_headers, params={"components_only": True, "get_all": True}
+ )
assert response.status_code == 200
names = [flow["name"] for flow in response.json()]
assert any("Chat Input Component" in name for name in names)
@@ -267,6 +289,52 @@ async def test_delete_folder_with_flows_with_transaction_and_build(
assert response.json() == {"vertex_builds": {}}
+async def test_get_flows_from_folder_pagination(client: TestClient, logged_in_headers):
+ # Create a new folder
+ folder_name = f"Test Folder {uuid4()}"
+ folder = FolderCreate(name=folder_name, description="Test folder description", components_list=[], flows_list=[])
+
+ response = await client.post("api/v1/folders/", json=folder.model_dump(), headers=logged_in_headers)
+ assert response.status_code == 201, f"Expected status code 201, but got {response.status_code}"
+
+ created_folder = response.json()
+ folder_id = created_folder["id"]
+
+ response = await client.get(f"api/v1/folders/{folder_id}", headers=logged_in_headers)
+ assert response.status_code == 200
+ assert response.json()["folder"]["name"] == folder_name
+ assert response.json()["folder"]["description"] == "Test folder description"
+ assert response.json()["flows"]["page"] == 1
+ assert response.json()["flows"]["size"] == 50
+ assert response.json()["flows"]["pages"] == 0
+ assert response.json()["flows"]["total"] == 0
+ assert len(response.json()["flows"]["items"]) == 0
+
+
+async def test_get_flows_from_folder_pagination_with_params(client: TestClient, logged_in_headers):
+ # Create a new folder
+ folder_name = f"Test Folder {uuid4()}"
+ folder = FolderCreate(name=folder_name, description="Test folder description", components_list=[], flows_list=[])
+
+ response = await client.post("api/v1/folders/", json=folder.model_dump(), headers=logged_in_headers)
+ assert response.status_code == 201, f"Expected status code 201, but got {response.status_code}"
+
+ created_folder = response.json()
+ folder_id = created_folder["id"]
+
+ response = await client.get(
+ f"api/v1/folders/{folder_id}", headers=logged_in_headers, params={"page": 3, "size": 10}
+ )
+ assert response.status_code == 200
+ assert response.json()["folder"]["name"] == folder_name
+ assert response.json()["folder"]["description"] == "Test folder description"
+ assert response.json()["flows"]["page"] == 3
+ assert response.json()["flows"]["size"] == 10
+ assert response.json()["flows"]["pages"] == 0
+ assert response.json()["flows"]["total"] == 0
+ assert len(response.json()["flows"]["items"]) == 0
+
+
async def test_create_flows(client: TestClient, session: Session, json_flow: str, logged_in_headers):
flow = orjson.loads(json_flow)
data = flow["data"]
@@ -412,7 +480,7 @@ async def test_delete_nonexistent_flow(client: TestClient, active_user, logged_i
async def test_read_only_starter_projects(client: TestClient, active_user, logged_in_headers):
- response = await client.get("api/v1/flows/", headers=logged_in_headers)
+ response = await client.get("api/v1/flows/basic_examples/", headers=logged_in_headers)
starter_projects = load_starter_projects()
assert response.status_code == 200
assert len(response.json()) == len(starter_projects)
diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json
index b07ae39d7..e54783c66 100644
--- a/src/frontend/package-lock.json
+++ b/src/frontend/package-lock.json
@@ -858,7 +858,6 @@
},
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
"version": "1.3.0",
- "extraneous": true,
"inBundle": true,
"license": "MIT",
"engines": {
diff --git a/src/frontend/src/App.css b/src/frontend/src/App.css
index 6a91177c6..e41830102 100644
--- a/src/frontend/src/App.css
+++ b/src/frontend/src/App.css
@@ -153,6 +153,7 @@ body {
width: 100%;
height: 100%;
}
+
.react-flow__resize-control.handle {
width: 0.75rem !important;
height: 0.75rem !important;
diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeDescription/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeDescription/index.tsx
index 376ce83f6..ab111ce23 100644
--- a/src/frontend/src/CustomNodes/GenericNode/components/NodeDescription/index.tsx
+++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeDescription/index.tsx
@@ -36,10 +36,6 @@ export default function NodeDescription({
//timeout to wait for the dom to update
setTimeout(() => {
if (overflowRef.current) {
- console.log(
- overflowRef.current.clientHeight,
- overflowRef.current.scrollHeight,
- );
if (
overflowRef.current.clientHeight < overflowRef.current.scrollHeight
) {
diff --git a/src/frontend/src/components/cardComponent/index.tsx b/src/frontend/src/components/cardComponent/index.tsx
index e207b8853..763e37754 100644
--- a/src/frontend/src/components/cardComponent/index.tsx
+++ b/src/frontend/src/components/cardComponent/index.tsx
@@ -42,6 +42,7 @@ export default function CollectionCardComponent({
const selectedFlowsComponentsCards = useFlowsManagerStore(
(state) => state.selectedFlowsComponentsCards,
);
+
function hasPlayground(flow?: FlowType) {
if (!flow) {
return false;
@@ -60,9 +61,9 @@ export default function CollectionCardComponent({
e.stopPropagation();
track("Playground Button Clicked", { flowId: data.id });
setLoadingPlayground(true);
- const flow = getFlowById(data.id);
- if (flow) {
- if (!hasPlayground(flow)) {
+
+ if (data) {
+ if (!hasPlayground(data)) {
setErrorData({
title: "Error",
list: ["This flow doesn't have a playground."],
@@ -70,7 +71,7 @@ export default function CollectionCardComponent({
setLoadingPlayground(false);
return;
}
- setCurrentFlow(flow);
+ setCurrentFlow(data);
setOpenPlayground(true);
setLoadingPlayground(false);
} else {
diff --git a/src/frontend/src/components/folderSidebarComponent/components/sideBarFolderButtons/index.tsx b/src/frontend/src/components/folderSidebarComponent/components/sideBarFolderButtons/index.tsx
index 38ac3c1d4..cad16e6cf 100644
--- a/src/frontend/src/components/folderSidebarComponent/components/sideBarFolderButtons/index.tsx
+++ b/src/frontend/src/components/folderSidebarComponent/components/sideBarFolderButtons/index.tsx
@@ -10,7 +10,7 @@ import { track } from "@/customization/utils/analytics";
import { createFileUpload } from "@/helpers/create-file-upload";
import { getObjectsFromFilelist } from "@/helpers/get-objects-from-filelist";
import useUploadFlow from "@/hooks/flows/use-upload-flow";
-import { useIsFetching } from "@tanstack/react-query";
+import { useIsFetching, useIsMutating } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom";
import { FolderType } from "../../../../pages/MainPage/entities";
@@ -230,7 +230,21 @@ const SideBarFoldersButtonsComponent = ({
exact: false,
});
- const isUpdatingFolder = isFetchingFolders || isPending || loading;
+ const isFetchingFolder = !!useIsFetching({
+ queryKey: ["useGetFolder"],
+ exact: false,
+ });
+
+ const isDeletingFolder = !!useIsMutating({
+ mutationKey: ["useDeleteFolders"],
+ });
+
+ const isUpdatingFolder =
+ isFetchingFolders ||
+ isFetchingFolder ||
+ isPending ||
+ loading ||
+ isDeletingFolder;
const HeaderButtons = () => (
diff --git a/src/frontend/src/components/folderSidebarComponent/hooks/use-on-file-drop.tsx b/src/frontend/src/components/folderSidebarComponent/hooks/use-on-file-drop.tsx
index 7e2393444..070ec084a 100644
--- a/src/frontend/src/components/folderSidebarComponent/hooks/use-on-file-drop.tsx
+++ b/src/frontend/src/components/folderSidebarComponent/hooks/use-on-file-drop.tsx
@@ -15,6 +15,7 @@ const useFileDrop = (folderId: string) => {
(state) => state.setFolderIdDragging,
);
+ const myCollectionId = useFolderStore((state) => state.myCollectionId);
const setErrorData = useAlertStore((state) => state.setErrorData);
const flows = useFlowsManagerStore((state) => state.flows);
const saveFlow = useSaveFlow();
@@ -105,7 +106,14 @@ const useFileDrop = (folderId: string) => {
}
const updatedFlow = { ...selectedFlow, folder_id: folderId };
- const newName = addVersionToDuplicates(updatedFlow, flows ?? []);
+ const flowsToCheckNames = flows?.filter(
+ (f) => f.folder_id === myCollectionId,
+ );
+
+ const newName = addVersionToDuplicates(
+ updatedFlow,
+ flowsToCheckNames ?? [],
+ );
updatedFlow.name = newName;
diff --git a/src/frontend/src/components/folderSidebarComponent/index.tsx b/src/frontend/src/components/folderSidebarComponent/index.tsx
index 25df8aa26..499d625aa 100644
--- a/src/frontend/src/components/folderSidebarComponent/index.tsx
+++ b/src/frontend/src/components/folderSidebarComponent/index.tsx
@@ -1,4 +1,6 @@
import { useGetFoldersQuery } from "@/controllers/API/queries/folders/use-get-folders";
+import { useFolderStore } from "@/stores/foldersStore";
+import { useIsFetching } from "@tanstack/react-query";
import { useLocation } from "react-router-dom";
import { FolderType } from "../../pages/MainPage/entities";
import { cn } from "../../utils/utils";
@@ -19,8 +21,12 @@ export default function FolderSidebarNav({
}: SidebarNavProps) {
const location = useLocation();
const pathname = location.pathname;
+ const folders = useFolderStore((state) => state.folders);
- const { data: folders, isPending } = useGetFoldersQuery();
+ const isPending = !!useIsFetching({
+ queryKey: ["useGetFolders"],
+ exact: false,
+ });
return (