add pagination in all components in default state

This commit is contained in:
Abhimanyu Yadav
2025-05-26 21:13:51 +05:30
parent 9012eff1ac
commit e034c16f31
13 changed files with 501 additions and 250 deletions

View File

@@ -1,29 +1,8 @@
import React, { useEffect, useState } from "react";
import BlocksList from "./BlocksList";
import { Block } from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import React from "react";
import PaginatedBlocksContent from "./PaginatedBlocksContent";
const ActionBlocksContent: React.FC = () => {
const [blocks, setBlocks] = useState<Block[]>([]);
const [loading, setLoading] = useState(true);
const api = useBackendAPI();
useEffect(() => {
const fetchBlocks = async () => {
setLoading(true);
try {
const response = await api.getBuilderBlocks({ type: "action" });
setBlocks(response.blocks);
} catch (error) {
console.error("Error fetching blocks:", error);
} finally {
setLoading(false);
}
};
fetchBlocks();
}, [api]);
return <BlocksList blocks={blocks} loading={loading} />;
return <PaginatedBlocksContent blockRequest={{ type: "action" }} />;
};
export default ActionBlocksContent;
export default ActionBlocksContent;

View File

@@ -43,8 +43,6 @@ const AllBlocksContent: React.FC = () => {
return cat;
});
await new Promise((resolve) => setTimeout(resolve, 3000));
setCategories(updatedCategories);
} catch (error) {
console.error(`Failed to fetch blocks for category ${category}:`, error);
@@ -75,7 +73,6 @@ const AllBlocksContent: React.FC = () => {
}
return (
// BLOCK TODO : NEED to add the laoding skeleton when clicking see all
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200">
<div className="w-full space-y-3 px-4 pb-4">
{categories.map((category, index) => (
@@ -113,6 +110,16 @@ const AllBlocksContent: React.FC = () => {
/>
))}
{loadingCategories.has(category.name) && (
<>
{[0, 1, 2, 3, 4].map((skeletonIndex) => (
<Block.Skeleton
key={`skeleton-${category.name}-${skeletonIndex}`}
/>
))}
</>
)}
{category.total_blocks > category.blocks.length && (
<Button
variant={"link"}

View File

@@ -11,28 +11,26 @@ interface BlocksListProps {
const BlocksList: React.FC<BlocksListProps> = ({ blocks, loading = false }) => {
const { addNode } = useBlockMenuContext();
return (
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200">
<div className="w-full space-y-3 px-4 pb-4">
{loading
? Array.from({ length: 7 }).map((_, index) => (
<Block.Skeleton key={index} />
))
: blocks.map((block) => (
<Block
key={block.id}
title={block.name}
description={block.description}
onClick={() => {
addNode(
block.id,
block.name,
block.hardcodedValues || {},
block,
);
}}
/>
))}
</div>
<div className="w-full space-y-3 px-4 pb-4">
{loading
? Array.from({ length: 7 }).map((_, index) => (
<Block.Skeleton key={index} />
))
: blocks.map((block) => (
<Block
key={block.id}
title={block.name}
description={block.description}
onClick={() => {
addNode(
block.id,
block.name,
block.hardcodedValues || {},
block,
);
}}
/>
))}
</div>
);
};

View File

@@ -1,29 +1,8 @@
import React, { useEffect, useState } from "react";
import BlocksList from "./BlocksList";
import { Block } from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import React from "react";
import PaginatedBlocksContent from "./PaginatedBlocksContent";
const InputBlocksContent: React.FC = () => {
const [blocks, setBlocks] = useState<Block[]>([]);
const [loading, setLoading] = useState(true);
const api = useBackendAPI();
useEffect(() => {
const fetchBlocks = async () => {
setLoading(true);
try {
const response = await api.getBuilderBlocks({ type: "input" });
setBlocks(response.blocks);
} catch (error) {
console.error("Error fetching blocks:", error);
} finally {
setLoading(false);
}
};
fetchBlocks();
}, [api]);
return <BlocksList blocks={blocks} loading={loading} />;
return <PaginatedBlocksContent blockRequest={{ type: "input" }} />;
};
export default InputBlocksContent;
export default InputBlocksContent;

View File

@@ -1,59 +0,0 @@
import React, { useState, useEffect } from "react";
import Integration from "../Integration";
import { useBlockMenuContext } from "../block-menu-provider";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { Provider } from "@/lib/autogpt-server-api";
const IntegrationList: React.FC = ({}) => {
const { setIntegration } = useBlockMenuContext();
const [integrations, setIntegrations] = useState<Provider[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const api = useBackendAPI();
useEffect(() => {
const fetchIntegrations = async () => {
setIsLoading(true);
try {
// Some integrations are missing, like twitter or todoist or more
const providers = await api.getProviders();
setIntegrations(providers.providers);
} catch (error) {
console.error("Failed to fetch integrations:", error);
} finally {
setIsLoading(false);
}
};
fetchIntegrations();
}, [api]);
if (isLoading) {
return (
<div className="space-y-3">
{Array(5)
.fill(null)
.map((_, index) => (
<Integration.Skeleton key={index} />
))}
</div>
);
}
return (
<div className="space-y-3">
{integrations.map((integration, index) => (
<Integration
key={index}
title={integration.name}
icon_url={`/integrations/${integration.name}.png`}
description={integration.description}
number_of_blocks={integration.integration_count}
onClick={() => setIntegration(integration.name)}
/>
))}
</div>
);
};
export default IntegrationList;

View File

@@ -1,14 +1,19 @@
import React from "react";
import IntegrationList from "./IntegrationList";
import PaginatedIntegrationList from "./PaginatedIntegrationList";
import IntegrationBlocks from "./IntegrationBlocks";
import { useBlockMenuContext } from "../block-menu-provider";
const IntegrationsContent: React.FC = () => {
const { integration } = useBlockMenuContext();
if (!integration) {
return <PaginatedIntegrationList />;
}
return (
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200">
<div className="w-full px-4 pb-4">
{!integration ? <IntegrationList /> : <IntegrationBlocks />}
<IntegrationBlocks />
</div>
</div>
);

View File

@@ -1,54 +1,57 @@
import React, { useState, useEffect } from "react";
import React from "react";
import MarketplaceAgentBlock from "../MarketplaceAgentBlock";
import { marketplaceAgentData } from "../../testing_data";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { StoreAgent } from "@/lib/autogpt-server-api";
import { Button } from "@/components/ui/button";
import { usePagination } from "@/hooks/usePagination";
const MarketplaceAgentsContent: React.FC = () => {
const [agents, setAgents] = useState<StoreAgent[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const api = useBackendAPI();
useEffect(() => {
const fetchAgents = async () => {
try {
const response = await api.getStoreAgents();
// BLOCK MENU TODO : figure out how to add agent in flow and add pagination as well
setAgents(response.agents);
setLoading(false);
} catch (err) {
setLoading(false);
}
};
fetchAgents();
}, [api]);
if (loading) {
return (
<div className="w-full space-y-3 p-4">
{Array(5)
.fill(null)
.map((_, index) => (
<MarketplaceAgentBlock.Skeleton key={index} />
))}
</div>
);
}
const { data: agents, loading, loadingMore, hasMore, error, scrollRef, refresh } = usePagination({
request: { apiType: 'store-agents' },
pageSize: 10,
});
return (
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200">
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200"
>
<div className="w-full space-y-3 px-4 pb-4">
{agents.map((agent) => (
<MarketplaceAgentBlock
key={agent.slug}
title={agent.agent_name}
image_url={agent.agent_image}
creator_name={agent.creator}
number_of_runs={agent.runs}
/>
))}
{loading
? Array(5)
.fill(null)
.map((_, index) => (
<MarketplaceAgentBlock.Skeleton key={index} />
))
: agents.map((agent) => (
<MarketplaceAgentBlock
key={agent.slug}
title={agent.agent_name}
image_url={agent.agent_image}
creator_name={agent.creator}
number_of_runs={agent.runs}
/>
))}
{error && (
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
<p className="text-sm text-red-600 mb-2">
Error loading marketplace agents: {error}
</p>
<Button
variant="outline"
size="sm"
onClick={refresh}
className="h-7 text-xs"
>
Retry
</Button>
</div>
)}
{loadingMore && hasMore && (
<>
{Array.from({ length: 3 }).map((_, index) => (
<MarketplaceAgentBlock.Skeleton key={`loading-${index}`} />
))}
</>
)}
</div>
</div>
);

View File

@@ -1,53 +1,57 @@
import React, { useState, useEffect } from "react";
import React from "react";
import UGCAgentBlock from "../UGCAgentBlock";
import { myAgentData } from "../../testing_data";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { LibraryAgent } from "@/lib/autogpt-server-api";
import { Button } from "@/components/ui/button";
import { usePagination } from "@/hooks/usePagination";
const MyAgentsContent: React.FC = () => {
const [agents, setAgents] = useState<LibraryAgent[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const api = useBackendAPI();
// TEMPORARY FETCHING
useEffect(() => {
const fetchAgents = async () => {
try {
// BLOCK MENU TODO : figure out how to add agent in flow and add pagination as well
const response = await api.listLibraryAgents();
setAgents(response.agents);
setLoading(false);
} catch (err) {
setLoading(false);
}
};
fetchAgents();
}, [api]);
if (loading) {
return (
<div className="w-full space-y-3 p-4">
{Array(5)
.fill(null)
.map((_, index) => (
<UGCAgentBlock.Skeleton key={index} />
))}
</div>
);
}
const { data: agents, loading, loadingMore, hasMore, error, scrollRef, refresh } = usePagination({
request: { apiType: 'library-agents' },
pageSize: 10,
});
return (
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200">
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200"
>
<div className="w-full space-y-3 px-4 pb-4">
{agents.map((agent) => (
<UGCAgentBlock
key={agent.id}
title={agent.name}
edited_time={agent.updated_at}
version={agent.graph_version}
image_url={agent.image_url}
/>
))}
{loading
? Array(5)
.fill(null)
.map((_, index) => (
<UGCAgentBlock.Skeleton key={index} />
))
: agents.map((agent) => (
<UGCAgentBlock
key={agent.id}
title={agent.name}
edited_time={agent.updated_at}
version={agent.graph_version}
image_url={agent.image_url}
/>
))}
{error && (
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
<p className="text-sm text-red-600 mb-2">
Error loading library agents: {error}
</p>
<Button
variant="outline"
size="sm"
onClick={refresh}
className="h-7 text-xs"
>
Retry
</Button>
</div>
)}
{loadingMore && hasMore && (
<>
{Array.from({ length: 3 }).map((_, index) => (
<UGCAgentBlock.Skeleton key={`loading-${index}`} />
))}
</>
)}
</div>
</div>
);

View File

@@ -1,29 +1,8 @@
import React, { useEffect, useState } from "react";
import BlocksList from "./BlocksList";
import { Block } from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import React from "react";
import PaginatedBlocksContent from "./PaginatedBlocksContent";
const OutputBlocksContent: React.FC = () => {
const [blocks, setBlocks] = useState<Block[]>([]);
const [loading, setLoading] = useState(true);
const api = useBackendAPI();
useEffect(() => {
const fetchBlocks = async () => {
setLoading(true);
try {
const response = await api.getBuilderBlocks({ type: "output" });
setBlocks(response.blocks);
} catch (error) {
console.error("Error fetching blocks:", error);
} finally {
setLoading(false);
}
};
fetchBlocks();
}, [api]);
return <BlocksList blocks={blocks} loading={loading} />;
return <PaginatedBlocksContent blockRequest={{ type: "output" }} />;
};
export default OutputBlocksContent;
export default OutputBlocksContent;

View File

@@ -0,0 +1,56 @@
import React from "react";
import BlocksList from "./BlocksList";
import Block from "../Block";
import { Button } from "@/components/ui/button";
import { BlockRequest } from "@/lib/autogpt-server-api";
import { usePagination } from "@/hooks/usePagination";
interface PaginatedBlocksContentProps {
blockRequest: BlockRequest;
pageSize?: number;
}
const PaginatedBlocksContent: React.FC<PaginatedBlocksContentProps> = ({
blockRequest,
pageSize = 10,
}) => {
const { data: blocks, loading, loadingMore, hasMore, error, scrollRef, refresh } = usePagination({
request: { apiType: 'blocks', ...blockRequest },
pageSize,
});
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200"
>
<BlocksList blocks={blocks} loading={loading} />
{error && (
<div className="w-full px-4 pb-4">
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
<p className="text-sm text-red-600 mb-2">
Error loading blocks: {error}
</p>
<Button
variant="outline"
size="sm"
onClick={refresh}
className="h-7 text-xs"
>
Retry
</Button>
</div>
</div>
)}
{loadingMore && hasMore && (
<div className="w-full space-y-3 px-4 pb-4">
{Array.from({ length: 3 }).map((_, index) => (
<Block.Skeleton key={`loading-${index}`} />
))}
</div>
)}
</div>
);
};
export default PaginatedBlocksContent;

View File

@@ -0,0 +1,65 @@
import React from "react";
import Integration from "../Integration";
import { Button } from "@/components/ui/button";
import { useBlockMenuContext } from "../block-menu-provider";
import { usePagination } from "@/hooks/usePagination";
const PaginatedIntegrationList: React.FC = () => {
const { setIntegration } = useBlockMenuContext();
const { data: providers, loading, loadingMore, hasMore, error, scrollRef, refresh } = usePagination({
request: { apiType: 'providers' },
pageSize: 10,
});
return (
<div
ref={scrollRef}
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200"
>
<div className="w-full px-4 pb-4">
<div className="space-y-3">
{loading
? Array(5)
.fill(null)
.map((_, index) => (
<Integration.Skeleton key={index} />
))
: providers.map((integration, index) => (
<Integration
key={index}
title={integration.name}
icon_url={`/integrations/${integration.name}.png`}
description={integration.description}
number_of_blocks={integration.integration_count}
onClick={() => setIntegration(integration.name)}
/>
))}
{error && (
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
<p className="text-sm text-red-600 mb-2">
Error loading integrations: {error}
</p>
<Button
variant="outline"
size="sm"
onClick={refresh}
className="h-7 text-xs"
>
Retry
</Button>
</div>
)}
{loadingMore && hasMore && (
<>
{Array.from({ length: 3 }).map((_, index) => (
<Integration.Skeleton key={`loading-${index}`} />
))}
</>
)}
</div>
</div>
</div>
);
};
export default PaginatedIntegrationList;

View File

@@ -0,0 +1 @@
export { usePagination } from './usePagination';

View File

@@ -0,0 +1,234 @@
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import {
Block,
BlockRequest,
Provider,
StoreAgent,
LibraryAgent,
LibraryAgentSortEnum,
} from "@/lib/autogpt-server-api";
type BlocksPaginationRequest = { apiType: "blocks" } & BlockRequest;
type ProvidersPaginationRequest = { apiType: "providers" } & {
page?: number;
page_size?: number;
};
type StoreAgentsPaginationRequest = { apiType: "store-agents" } & {
featured?: boolean;
creator?: string;
sorted_by?: string;
search_query?: string;
category?: string;
page?: number;
page_size?: number;
};
type LibraryAgentsPaginationRequest = { apiType: "library-agents" } & {
search_term?: string;
sort_by?: LibraryAgentSortEnum;
page?: number;
page_size?: number;
};
type PaginationRequest =
| BlocksPaginationRequest
| ProvidersPaginationRequest
| StoreAgentsPaginationRequest
| LibraryAgentsPaginationRequest;
interface UsePaginationOptions<T extends PaginationRequest> {
request: T;
pageSize?: number;
enabled?: boolean;
}
interface UsePaginationReturn<T> {
data: T[];
loading: boolean;
loadingMore: boolean;
hasMore: boolean;
error: string | null;
scrollRef: React.RefObject<HTMLDivElement>;
refresh: () => void;
loadMore: () => void;
}
type GetReturnType<T> = T extends BlocksPaginationRequest
? Block
: T extends ProvidersPaginationRequest
? Provider
: T extends StoreAgentsPaginationRequest
? StoreAgent
: T extends LibraryAgentsPaginationRequest
? LibraryAgent
: never;
export const usePagination = <T extends PaginationRequest>({
request,
pageSize = 10,
enabled = true, // to allow pagination or nor
}: UsePaginationOptions<T>): UsePaginationReturn<GetReturnType<T>> => {
const [data, setData] = useState<GetReturnType<T>[]>([]);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [error, setError] = useState<string | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const isLoadingRef = useRef(false);
const requestRef = useRef(request);
const api = useBackendAPI();
// because we are using this pagination for multiple components
requestRef.current = request;
const fetchData = useCallback(
async (page: number, isLoadMore = false) => {
if (isLoadingRef.current || !enabled) return;
isLoadingRef.current = true;
if (isLoadMore) {
setLoadingMore(true);
} else {
setLoading(true);
}
setError(null);
try {
let response;
let newData: GetReturnType<T>[];
let pagination;
const currentRequest = requestRef.current;
const requestWithPagination = {
...currentRequest,
page,
page_size: pageSize,
};
switch (currentRequest.apiType) {
case "blocks":
const { apiType: _, ...blockRequest } = requestWithPagination;
response = await api.getBuilderBlocks(blockRequest);
newData = response.blocks as GetReturnType<T>[];
pagination = response.pagination;
break;
case "providers":
const { apiType: __, ...providerRequest } = requestWithPagination;
response = await api.getProviders(providerRequest);
newData = response.providers as GetReturnType<T>[];
pagination = response.pagination;
break;
case "store-agents":
const { apiType: ___, ...storeAgentRequest } =
requestWithPagination;
response = await api.getStoreAgents(storeAgentRequest);
newData = response.agents as GetReturnType<T>[];
pagination = response.pagination;
break;
case "library-agents":
const { apiType: ____, ...libraryAgentRequest } =
requestWithPagination;
response = await api.listLibraryAgents(libraryAgentRequest);
newData = response.agents as GetReturnType<T>[];
pagination = response.pagination;
break;
default:
throw new Error(
`Unknown request type: ${(currentRequest as any).apiType}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 2000));
if (isLoadMore) {
setData((prev) => [...prev, ...newData]);
} else {
setData(newData);
}
setHasMore(page < pagination.total_pages);
setCurrentPage(page);
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : "Failed to fetch data";
setError(errorMessage);
console.error("Error fetching data:", err);
} finally {
setLoading(false);
setLoadingMore(false);
isLoadingRef.current = false;
}
},
[api, pageSize, enabled],
);
const handleScroll = useCallback(() => {
const scrollElement = scrollRef.current;
if (
!scrollElement ||
loadingMore ||
!hasMore ||
isLoadingRef.current ||
!enabled
)
return;
const { scrollTop, scrollHeight, clientHeight } = scrollElement;
const threshold = 100;
if (scrollTop + clientHeight >= scrollHeight - threshold) {
fetchData(currentPage + 1, true);
}
}, [fetchData, currentPage, loadingMore, hasMore, enabled]);
const refresh = useCallback(() => {
setCurrentPage(1);
setHasMore(true);
setError(null);
fetchData(1);
}, [fetchData]);
const loadMore = useCallback(() => {
if (!loadingMore && hasMore && !isLoadingRef.current && enabled) {
fetchData(currentPage + 1, true);
}
}, [fetchData, currentPage, loadingMore, hasMore, enabled]);
const requestString = JSON.stringify(request);
useEffect(() => {
if (enabled) {
setCurrentPage(1);
setHasMore(true);
setError(null);
setData([]);
fetchData(1);
}
}, [requestString, enabled, fetchData]);
useEffect(() => {
const scrollElement = scrollRef.current;
if (scrollElement && enabled) {
scrollElement.addEventListener("scroll", handleScroll);
return () => scrollElement.removeEventListener("scroll", handleScroll);
}
}, [handleScroll, enabled]);
return {
data,
loading,
loadingMore,
hasMore,
error,
scrollRef,
refresh,
loadMore,
};
};