diff --git a/src/backend/base/langflow/api/v1/flow_versions.py b/src/backend/base/langflow/api/v1/flow_versions.py index 072b862498a4..fce56a794647 100644 --- a/src/backend/base/langflow/api/v1/flow_versions.py +++ b/src/backend/base/langflow/api/v1/flow_versions.py @@ -20,6 +20,7 @@ FlowVersionCreate, FlowVersionRead, FlowVersionRejectRequest, + FlowVersionPaginatedResponse, ) from langflow.services.database.models.version_flow_input_sample.model import ( VersionFlowInputSample, @@ -251,22 +252,37 @@ async def submit_for_approval( ) -@router.get("/pending-reviews", response_model=list[FlowVersionRead]) +@router.get("/pending-reviews", response_model=FlowVersionPaginatedResponse) async def get_pending_reviews( session: DbSession, current_user: CurrentActiveUser, - skip: int = Query(0, ge=0), - limit: int = Query(50, ge=1, le=100), + page: int = Query(1, ge=1), + limit: int = Query(12, ge=1, le=100), ): """ Get all flow versions pending review (status = Submitted). This endpoint is intended for admin users to see all submissions awaiting approval. + Returns paginated results. """ try: # Get "Submitted" status ID submitted_status_id = await _get_status_id_by_name(session, FlowStatusEnum.SUBMITTED.value) + # Count total items + count_stmt = ( + select(func.count()) + .select_from(FlowVersion) + .where(FlowVersion.status_id == submitted_status_id) + ) + count_result = await session.exec(count_stmt) + total = count_result.one() + + # Calculate pagination + pages = (total + limit - 1) // limit # Ceiling division + offset = (page - 1) * limit + + # Get paginated results stmt = ( select(FlowVersion) .where(FlowVersion.status_id == submitted_status_id) @@ -275,13 +291,13 @@ async def get_pending_reviews( joinedload(FlowVersion.status), ) .order_by(FlowVersion.submitted_at.desc()) - .offset(skip) + .offset(offset) .limit(limit) ) result = await session.exec(stmt) versions = result.unique().all() - return [ + items = [ FlowVersionRead( id=v.id, original_flow_id=v.original_flow_id, @@ -312,6 +328,13 @@ async def get_pending_reviews( for v in versions ] + return FlowVersionPaginatedResponse( + items=items, + total=total, + page=page, + pages=pages, + ) + except HTTPException: raise except Exception as e: @@ -322,6 +345,125 @@ async def get_pending_reviews( ) +@router.get("/all", response_model=FlowVersionPaginatedResponse) +async def get_all_flow_versions( + session: DbSession, + current_user: CurrentActiveUser, + page: int = Query(1, ge=1), + limit: int = Query(12, ge=1, le=100), + status: str | None = Query(None), +): + """ + Get all flow versions with optional status filtering. + + This endpoint returns all flow versions (Submitted, Approved, Rejected) in a single list. + Optionally filter by status using the status parameter. + Returns paginated results. + + Valid status values: "Submitted", "Approved", "Rejected" + If status is not provided, returns all flow versions. + """ + try: + # Build base query + query_conditions = [] + + if status: + # Validate status name + valid_statuses = ["Submitted", "Approved", "Rejected"] + if status not in valid_statuses: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid status. Must be one of: {', '.join(valid_statuses)}", + ) + status_id = await _get_status_id_by_name(session, status) + query_conditions.append(FlowVersion.status_id == status_id) + else: + # Get all relevant statuses (Submitted, Approved, Rejected) + submitted_id = await _get_status_id_by_name(session, FlowStatusEnum.SUBMITTED.value) + approved_id = await _get_status_id_by_name(session, FlowStatusEnum.APPROVED.value) + rejected_id = await _get_status_id_by_name(session, FlowStatusEnum.REJECTED.value) + query_conditions.append( + FlowVersion.status_id.in_([submitted_id, approved_id, rejected_id]) + ) + + # Count total items + count_stmt = ( + select(func.count()) + .select_from(FlowVersion) + .where(*query_conditions) + ) + count_result = await session.exec(count_stmt) + total = count_result.one() + + # Calculate pagination + pages = (total + limit - 1) // limit # Ceiling division + offset = (page - 1) * limit + + # Get paginated results + stmt = ( + select(FlowVersion) + .where(*query_conditions) + .options( + joinedload(FlowVersion.submitter), + joinedload(FlowVersion.reviewer), + joinedload(FlowVersion.status), + ) + .order_by(FlowVersion.submitted_at.desc()) + .offset(offset) + .limit(limit) + ) + result = await session.exec(stmt) + versions = result.unique().all() + + items = [ + FlowVersionRead( + id=v.id, + original_flow_id=v.original_flow_id, + version_flow_id=v.version_flow_id, + status_id=v.status_id, + version=v.version, + title=v.title, + description=v.description, + tags=v.tags, + agent_logo=v.agent_logo, + sample_id=v.sample_id, + submitted_by=v.submitted_by, + submitted_by_name=v.submitted_by_name, + submitted_by_email=v.submitted_by_email, + submitted_at=v.submitted_at, + reviewed_by=v.reviewed_by, + reviewed_by_name=v.reviewed_by_name, + reviewed_by_email=v.reviewed_by_email, + reviewed_at=v.reviewed_at, + rejection_reason=v.rejection_reason, + created_at=v.created_at, + updated_at=v.updated_at, + status_name=v.status.status_name if v.status else None, + # Use stored name/email, fallback to User relationship for backward compatibility + submitter_name=v.submitted_by_name or (v.submitter.username if v.submitter else None), + submitter_email=v.submitted_by_email, + reviewer_name=v.reviewed_by_name or (v.reviewer.username if v.reviewer else None), + ) + for v in versions + ] + + return FlowVersionPaginatedResponse( + items=items, + total=total, + page=page, + pages=pages, + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error fetching all flow versions: {e}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to fetch flow versions: {str(e)}", + ) + + @router.get("/my-submissions", response_model=list[FlowVersionRead]) async def get_my_submissions( session: DbSession, @@ -385,18 +527,19 @@ async def get_my_submissions( ) -@router.get("/by-status/{status_name}", response_model=list[FlowVersionRead]) +@router.get("/by-status/{status_name}", response_model=FlowVersionPaginatedResponse) async def get_versions_by_status( status_name: str, session: DbSession, current_user: CurrentActiveUser, - skip: int = Query(0, ge=0), - limit: int = Query(50, ge=1, le=100), + page: int = Query(1, ge=1), + limit: int = Query(12, ge=1, le=100), ): """ Get flow versions filtered by status name. Valid status names: Draft, Submitted, Approved, Rejected, Published, Unpublished, Deleted + Returns paginated results. """ try: # Validate status name @@ -409,6 +552,20 @@ async def get_versions_by_status( status_id = await _get_status_id_by_name(session, status_name) + # Count total items + count_stmt = ( + select(func.count()) + .select_from(FlowVersion) + .where(FlowVersion.status_id == status_id) + ) + count_result = await session.exec(count_stmt) + total = count_result.one() + + # Calculate pagination + pages = (total + limit - 1) // limit # Ceiling division + offset = (page - 1) * limit + + # Get paginated results stmt = ( select(FlowVersion) .where(FlowVersion.status_id == status_id) @@ -418,13 +575,13 @@ async def get_versions_by_status( joinedload(FlowVersion.status), ) .order_by(FlowVersion.submitted_at.desc()) - .offset(skip) + .offset(offset) .limit(limit) ) result = await session.exec(stmt) versions = result.unique().all() - return [ + items = [ FlowVersionRead( id=v.id, original_flow_id=v.original_flow_id, @@ -456,6 +613,13 @@ async def get_versions_by_status( for v in versions ] + return FlowVersionPaginatedResponse( + items=items, + total=total, + page=page, + pages=pages, + ) + except HTTPException: raise except Exception as e: diff --git a/src/backend/base/langflow/services/database/models/flow_version/model.py b/src/backend/base/langflow/services/database/models/flow_version/model.py index 7bf2ed0da1ce..bc239545c5f3 100644 --- a/src/backend/base/langflow/services/database/models/flow_version/model.py +++ b/src/backend/base/langflow/services/database/models/flow_version/model.py @@ -236,5 +236,14 @@ class FlowVersionRejectRequest(SQLModel): rejection_reason: str | None = None +class FlowVersionPaginatedResponse(SQLModel): + """Schema for paginated flow version responses.""" + + items: list[FlowVersionRead] + total: int + page: int + pages: int + + # Rebuild Pydantic models to resolve forward references FlowVersionRead.model_rebuild() diff --git a/src/frontend/src/components/core/flowToolbarComponent/components/flow-toolbar-options.tsx b/src/frontend/src/components/core/flowToolbarComponent/components/flow-toolbar-options.tsx index e0fad5948565..a241f696cb84 100644 --- a/src/frontend/src/components/core/flowToolbarComponent/components/flow-toolbar-options.tsx +++ b/src/frontend/src/components/core/flowToolbarComponent/components/flow-toolbar-options.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -31,6 +32,7 @@ import SubmitForApprovalModal from "@/modals/submitForApprovalModal"; import PublishFlowModal from "@/modals/publishFlowModal"; export default function FlowToolbarOptions() { + const navigate = useNavigate(); const [open, setOpen] = useState(false); const [openSubmitModal, setOpenSubmitModal] = useState(false); const [openPublishModal, setOpenPublishModal] = useState(false); @@ -104,6 +106,7 @@ export default function FlowToolbarOptions() { approveVersion(latestVersionId, { onSuccess: () => { setSuccessData({ title: `"${currentFlowName}" has been approved` }); + navigate("/all-requests"); }, onError: (error: any) => { setErrorData({ @@ -136,6 +139,7 @@ export default function FlowToolbarOptions() { setSuccessData({ title: `"${currentFlowName}" has been rejected` }); setOpenRejectModal(false); setRejectionReason(""); + navigate("/all-requests"); }, onError: (error: any) => { setErrorData({ diff --git a/src/frontend/src/controllers/API/queries/flow-versions/index.ts b/src/frontend/src/controllers/API/queries/flow-versions/index.ts index 30db110cad99..7a8702bfdf4d 100644 --- a/src/frontend/src/controllers/API/queries/flow-versions/index.ts +++ b/src/frontend/src/controllers/API/queries/flow-versions/index.ts @@ -2,6 +2,7 @@ export * from "./use-get-flow-latest-status"; export * from "./use-submit-flow-for-approval"; export * from "./use-get-pending-reviews"; export * from "./use-get-versions-by-status"; +export * from "./use-get-all-flow-versions"; export * from "./use-approve-version"; export * from "./use-reject-version"; export * from "./use-cancel-submission"; diff --git a/src/frontend/src/controllers/API/queries/flow-versions/use-get-all-flow-versions.ts b/src/frontend/src/controllers/API/queries/flow-versions/use-get-all-flow-versions.ts new file mode 100644 index 000000000000..622e71e9b6cc --- /dev/null +++ b/src/frontend/src/controllers/API/queries/flow-versions/use-get-all-flow-versions.ts @@ -0,0 +1,30 @@ +import { useQuery } from "@tanstack/react-query"; +import { api } from "../../api"; +import { getURL } from "../../helpers/constants"; +import type { FlowVersionPaginatedResponse } from "./use-get-pending-reviews"; + +export const useGetAllFlowVersions = ( + page: number = 1, + limit: number = 12, + status?: string +) => { + return useQuery({ + queryKey: ["all-flow-versions", page, limit, status], + queryFn: async () => { + const response = await api.get( + `${getURL("FLOW_VERSIONS")}/all`, + { + params: { + page, + limit, + ...(status && status !== "all" ? { status } : {}), + }, + } + ); + return response.data; + }, + staleTime: 0, // Always fetch fresh data + refetchOnMount: "always", // Refetch when component mounts + refetchOnWindowFocus: true, + }); +}; diff --git a/src/frontend/src/controllers/API/queries/flow-versions/use-get-pending-reviews.ts b/src/frontend/src/controllers/API/queries/flow-versions/use-get-pending-reviews.ts index 718e74e9bb14..6505a2707fff 100644 --- a/src/frontend/src/controllers/API/queries/flow-versions/use-get-pending-reviews.ts +++ b/src/frontend/src/controllers/API/queries/flow-versions/use-get-pending-reviews.ts @@ -38,12 +38,22 @@ export interface FlowVersionRead { updated_at: string; } -export const useGetPendingReviews = () => { - return useQuery({ - queryKey: ["pending-reviews"], +export interface FlowVersionPaginatedResponse { + items: FlowVersionRead[]; + total: number; + page: number; + pages: number; +} + +export const useGetPendingReviews = (page: number = 1, limit: number = 12) => { + return useQuery({ + queryKey: ["pending-reviews", page, limit], queryFn: async () => { - const response = await api.get( - `${getURL("FLOW_VERSIONS")}/pending-reviews` + const response = await api.get( + `${getURL("FLOW_VERSIONS")}/pending-reviews`, + { + params: { page, limit }, + } ); return response.data; }, diff --git a/src/frontend/src/controllers/API/queries/flow-versions/use-get-versions-by-status.ts b/src/frontend/src/controllers/API/queries/flow-versions/use-get-versions-by-status.ts index ddfd8fe8c21e..e0139c042b96 100644 --- a/src/frontend/src/controllers/API/queries/flow-versions/use-get-versions-by-status.ts +++ b/src/frontend/src/controllers/API/queries/flow-versions/use-get-versions-by-status.ts @@ -1,16 +1,24 @@ import { useQuery } from "@tanstack/react-query"; import { api } from "../../api"; import { getURL } from "../../helpers/constants"; -import type { FlowVersionRead } from "./use-get-pending-reviews"; +import type { FlowVersionPaginatedResponse } from "./use-get-pending-reviews"; export type FlowStatusName = "Draft" | "Submitted" | "Approved" | "Rejected" | "Published" | "Unpublished" | "Deleted"; -export const useGetVersionsByStatus = (statusName: FlowStatusName, enabled = true) => { - return useQuery({ - queryKey: ["flow-versions-by-status", statusName], +export const useGetVersionsByStatus = ( + statusName: FlowStatusName, + page: number = 1, + limit: number = 12, + enabled = true +) => { + return useQuery({ + queryKey: ["flow-versions-by-status", statusName, page, limit], queryFn: async () => { - const response = await api.get( - `${getURL("FLOW_VERSIONS")}/by-status/${statusName}` + const response = await api.get( + `${getURL("FLOW_VERSIONS")}/by-status/${statusName}`, + { + params: { page, limit }, + } ); return response.data; }, diff --git a/src/frontend/src/pages/AllRequestsPage/index.tsx b/src/frontend/src/pages/AllRequestsPage/index.tsx index 7659c3fa1fa1..cab454dc7140 100644 --- a/src/frontend/src/pages/AllRequestsPage/index.tsx +++ b/src/frontend/src/pages/AllRequestsPage/index.tsx @@ -1,6 +1,5 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Table, @@ -11,112 +10,55 @@ import { TableRow, } from "@/components/ui/table"; import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Textarea } from "@/components/ui/textarea"; -import { Label } from "@/components/ui/label"; + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import IconComponent from "@/components/common/genericIconComponent"; -import { - useGetPendingReviews, - useGetVersionsByStatus, - useApproveVersion, - useRejectVersion, - type FlowVersionRead, -} from "@/controllers/API/queries/flow-versions"; -import useAlertStore from "@/stores/alertStore"; +import { useGetAllFlowVersions } from "@/controllers/API/queries/flow-versions"; import CustomLoader from "@/customization/components/custom-loader"; +import FlowPagination from "@/pages/MarketplacePage/components/FlowPagination"; export default function AllRequestsPage() { const navigate = useNavigate(); - const [activeTab, setActiveTab] = useState("submitted"); - const [rejectModalOpen, setRejectModalOpen] = useState(false); - const [selectedVersion, setSelectedVersion] = - useState(null); - const [rejectionReason, setRejectionReason] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); - const setSuccessData = useAlertStore((state) => state.setSuccessData); - const setErrorData = useAlertStore((state) => state.setErrorData); + // Pagination state + const [pageIndex, setPageIndex] = useState(1); + const [pageSize, setPageSize] = useState(12); const handleReview = (flowId: string) => { navigate(`/flow/${flowId}`); }; - // Fetch data for each tab - const { data: pendingReviews, isLoading: loadingPending } = - useGetPendingReviews(); - const { data: approvedVersions, isLoading: loadingApproved } = - useGetVersionsByStatus("Approved"); - const { data: rejectedVersions, isLoading: loadingRejected } = - useGetVersionsByStatus("Rejected"); - - // Calculate total count - const totalCount = - (pendingReviews?.length || 0) + - (approvedVersions?.length || 0) + - (rejectedVersions?.length || 0); - - // Mutations - const { mutate: approveVersion, isPending: isApproving } = - useApproveVersion(); - const { mutate: rejectVersion, isPending: isRejecting } = useRejectVersion(); + // Handle status filter change + const handleStatusFilterChange = (value: string) => { + setStatusFilter(value); + setPageIndex(1); // Reset to first page + }; - const handleApprove = (version: FlowVersionRead) => { - approveVersion(version.id, { - onSuccess: () => { - setSuccessData({ title: `"${version.title}" has been approved` }); - }, - onError: (error: any) => { - setErrorData({ - title: "Failed to approve", - list: [ - error?.response?.data?.detail || error.message || "Unknown error", - ], - }); - }, - }); + // Pagination handlers + const handlePageChange = (page: number) => { + setPageIndex(page); }; - const handleRejectClick = (version: FlowVersionRead) => { - setSelectedVersion(version); - setRejectionReason(""); - setRejectModalOpen(true); + const handlePageSizeChange = (size: number) => { + setPageSize(size); + setPageIndex(1); // Reset to first page when changing page size }; - const handleRejectConfirm = () => { - if (!selectedVersion) return; + // Fetch all flow versions with optional status filter + const { data, isLoading } = useGetAllFlowVersions( + pageIndex, + pageSize, + statusFilter === "all" ? undefined : statusFilter + ); - rejectVersion( - { - versionId: selectedVersion.id, - payload: rejectionReason - ? { rejection_reason: rejectionReason } - : undefined, - }, - { - onSuccess: () => { - setSuccessData({ - title: `"${selectedVersion.title}" has been rejected`, - }); - setRejectModalOpen(false); - setSelectedVersion(null); - setRejectionReason(""); - }, - onError: (error: any) => { - setErrorData({ - title: "Failed to reject", - list: [ - error?.response?.data?.detail || error.message || "Unknown error", - ], - }); - }, - } - ); - }; + // Extract items and metadata + const allVersions = data?.items || []; + const totalCount = data?.total || 0; const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString("en-US", { @@ -128,138 +70,34 @@ export default function AllRequestsPage() { }); }; - const renderSubmittedTable = () => { - if (loadingPending) { - return ( -
- -
- ); - } - - if (!pendingReviews || pendingReviews.length === 0) { - return ( -
- -

No pending submissions

-
- ); + // Get status badge styling + const getStatusBadge = (statusName: string) => { + switch (statusName) { + case "Submitted": + return ( + + Under Review + + ); + case "Approved": + return ( + + Approved + + ); + case "Rejected": + return ( + + Rejected + + ); + default: + return statusName; } - - return ( - - - - Agent Name - Description - Submitted by - Submission Date - Versions - Status - Action - {/* Tags column - commented out for now - Tags - - Action*/} - - - - {pendingReviews.map((version) => ( - - {version.title} - - {version.description || ( - - - )} - - - <> -
- {version.submitted_by_name || - version.submitter_name || - "Unknown"} -
- {(version.submitted_by_email || version.submitter_email) && ( -
- {version.submitted_by_email || version.submitter_email} -
- )} - -
- {formatDate(version.submitted_at)} - {version.version} - - - Under Review - - - - - - {/* Tags cell - commented out for now - - {version.tags?.length ? ( -
- {version.tags.slice(0, 2).map((tag) => ( - - {tag} - - ))} - {version.tags.length > 2 && ( - - +{version.tags.length - 2} - - )} -
- ) : ( - - - )} -
- - -
- - -
-
- */} -
- ))} -
-
- ); }; - const renderApprovedTable = () => { - if (loadingApproved) { + const renderTable = () => { + if (isLoading) { return (
@@ -267,11 +105,11 @@ export default function AllRequestsPage() { ); } - if (!approvedVersions || approvedVersions.length === 0) { + if (!allVersions || allVersions.length === 0) { return ( -
- -

No approved submissions

+
+ +

No requests found

); } @@ -286,12 +124,13 @@ export default function AllRequestsPage() { Submission Date Versions Status + Remark Reviewed By Action - {approvedVersions.map((version) => ( + {allVersions.map((version) => ( {version.title} @@ -316,97 +155,24 @@ export default function AllRequestsPage() { {formatDate(version.submitted_at)} {version.version} - - Approved - - - - {version.reviewed_by_name || version.reviewer_name || "Unknown"} + {getStatusBadge(version.status_name || "")} - - - - - ))} - - - ); - }; - - const renderRejectedTable = () => { - if (loadingRejected) { - return ( -
- -
- ); - } - - if (!rejectedVersions || rejectedVersions.length === 0) { - return ( -
- -

No rejected submissions

-
- ); - } - - return ( - - - - Agent Name - Description - Submitted by - Submission Date - Versions - Status - Rejection Reason - Action - - - - {rejectedVersions.map((version) => ( - - {version.title} - - {version.description || ( - - + + {version.status_name === "Rejected" && version.rejection_reason ? ( + + {version.rejection_reason} + + ) : ( + - )} -
+ {version.status_name === "Approved" || version.status_name === "Rejected" ? (
- {version.submitted_by_name || - version.submitter_name || - "Unknown"} + {version.reviewed_by_name || version.reviewer_name || "-"}
- {(version.submitted_by_email || version.submitter_email) && ( -
- {version.submitted_by_email || version.submitter_email} -
- )} -
-
- {formatDate(version.submitted_at)} - {version.version} - - - Rejected - - - - {version.rejection_reason || ( - - No reason provided - + ) : ( + - )} @@ -428,83 +194,51 @@ export default function AllRequestsPage() { return (
-

- All Requests{" "} - {totalCount > 0 && ({totalCount})} -

- - - - - Submitted - {pendingReviews && pendingReviews.length > 0 && ( - ({pendingReviews.length}) - )} - - - Approved - {approvedVersions && approvedVersions.length > 0 && ( - ({approvedVersions.length}) - )} - - - Rejected - {rejectedVersions && rejectedVersions.length > 0 && ( - ({rejectedVersions.length}) - )} - - - -
- - {renderSubmittedTable()} - - - {renderApprovedTable()} - - - {renderRejectedTable()} - +
+

+ All Requests{" "} + {totalCount > 0 && ({totalCount})} +

+ + {/* Status Filter */} + +
+ +
+ {/* Table */} +
+ {renderTable()}
- - {/* Reject Modal */} - - - - Reject Submission - - Are you sure you want to reject "{selectedVersion?.title}"? You - can optionally provide a reason. - - -
- -