Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4af6293
fix: update DealTasksTable to use <span> to fix index alignment
ErwanDecoster Oct 31, 2025
8ae5304
feat: add pocoBulk code generation and execution functionality
ErwanDecoster Nov 3, 2025
f8dd348
feat: add TaskDatasetsTable component and integrate into task route
ErwanDecoster Nov 3, 2025
ac2fd19
feat: add dataset type in task dataset tab
ErwanDecoster Nov 3, 2025
cb220ed
feat: enhance task page with loading and outdated state management li…
ErwanDecoster Nov 3, 2025
0052631
feat: add DatasetTasksTable component and integrate into dataset rout…
ErwanDecoster Nov 5, 2025
30ad184
feat: add button to view tasks for deals with null dataset in buildDe…
ErwanDecoster Nov 5, 2025
9635f11
feat: rename GraphQL query to TaskDatasets for clarity
ErwanDecoster Nov 5, 2025
babb40e
feat: add VITE_POCO_BULK_SUBGRAPH_URL to environment configuration
ErwanDecoster Nov 6, 2025
72175c7
feat: add onSeeTasks callback to buildDealDetails for task navigation
ErwanDecoster Nov 6, 2025
8c0ec9e
feat: add DealAssociatedDealsTable component and integrate into Deals…
ErwanDecoster Nov 6, 2025
1f6b6c6
feat: enhance DealTasksTable and DealAssociatedDealsTable with loadin…
ErwanDecoster Nov 6, 2025
e85f8e1
fix: remove unnecessary semicolon in DatasetTasksTable component
ErwanDecoster Nov 6, 2025
7fb0384
refactor: reorder imports
ErwanDecoster Nov 6, 2025
fbd1fba
Merge branch 'main' into feature/add-bulk
ErwanDecoster Nov 7, 2025
6bbf1ee
refactor: remove pocoBulk graph to use it in poco
ErwanDecoster Nov 12, 2025
487862b
feat: implement pagination for task datasets
ErwanDecoster Nov 12, 2025
537d993
feat: add pagination support to deal tasks query and table
ErwanDecoster Nov 12, 2025
a6f684e
feat: enhance DealTasksTable with type definitions and improved task …
ErwanDecoster Nov 13, 2025
3ee17a8
feat: add MetaMask Flask and simplify networks assignment
ErwanDecoster Nov 14, 2025
082a348
feat: update Arbitrum Sepolia poco subgraph URL to the latest version
ErwanDecoster Nov 14, 2025
f1ed9c2
feat: implement filter parameter synchronization and enhance workerpo…
ErwanDecoster Nov 17, 2025
a549dea
feat: enhance AppsPreviewTable and appsQuery with ordering options an…
ErwanDecoster Nov 17, 2025
3da9123
feat: improve SearcherBar layout and error handling visibility
ErwanDecoster Nov 17, 2025
c880bd7
feat: enhance pagination controls with filter key handling and stabil…
ErwanDecoster Nov 17, 2025
7b9718a
refactor: simplify navigation logic in useFilterParam hook
ErwanDecoster Nov 17, 2025
42807fb
fix: remove TypeScript error comments for updated query signatures in…
ErwanDecoster Nov 17, 2025
a947198
feat: add SlidersHorizontal icon to order selection in WorkerpoolsRoute
ErwanDecoster Nov 17, 2025
034b42e
feat: replace inline recent timestamp calculation with getRecentFromT…
ErwanDecoster Nov 18, 2025
7a4a49e
refactor: improve raw value extraction and validation logic in useFil…
ErwanDecoster Nov 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 26 additions & 13 deletions src/components/PaginatedNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,27 @@ type PaginationControlsProps = {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
// Optional key whose change can shrink/grow pages immediately (e.g. active filter)
filterKey?: string;
};

export const PaginatedNavigation = ({
currentPage,
totalPages,
onPageChange,
filterKey,
}: PaginationControlsProps) => {
const { chainId } = useUserStore();

const lastValidTotalPagesRef = useRef(1);
const lastChainIdRef = useRef<number | null>(null);
const chainChangeFrameRef = useRef(0);

const lastFilterKeyRef = useRef<string | undefined>(undefined);
const filterChangeFrameRef = useRef(0);

const chainHasChanged = chainId !== lastChainIdRef.current;
const filterHasChanged = filterKey !== lastFilterKeyRef.current;

if (chainHasChanged) {
lastChainIdRef.current = chainId ?? null;
Expand All @@ -36,66 +43,72 @@ export const PaginatedNavigation = ({
chainChangeFrameRef.current++;
}

if (filterHasChanged) {
lastFilterKeyRef.current = filterKey;
filterChangeFrameRef.current = 0;
} else {
filterChangeFrameRef.current++;
}

let stableTotalPages = lastValidTotalPagesRef.current;

const isRecentChainChange = chainChangeFrameRef.current <= 5;

if (chainHasChanged || isRecentChainChange) {
const isRecentFilterChange = filterChangeFrameRef.current <= 5;

if (
chainHasChanged ||
filterHasChanged ||
isRecentChainChange ||
isRecentFilterChange
) {
stableTotalPages = Math.max(totalPages, 1);
} else if (totalPages > 0 && totalPages >= lastValidTotalPagesRef.current) {
stableTotalPages = totalPages;
}
// Reset page if it no longer exists after filter change
if (filterHasChanged && currentPage > stableTotalPages) {
onPageChange(1);
}

lastValidTotalPagesRef.current = stableTotalPages;

// Don't render pagination if no pages or invalid state
if (!stableTotalPages || stableTotalPages <= 0 || currentPage <= 0) {
return null;
}

const generatePages = () => {
const pages: (number | 'ellipsis')[] = [];

// Mobile-first approach: show fewer pages on small screens
const isMobile = typeof window !== 'undefined' && window.innerWidth < 640;
const maxVisiblePages = isMobile ? 3 : 7;

if (stableTotalPages <= maxVisiblePages) {
// Show all pages if within limit
for (let i = 1; i <= stableTotalPages; i++) {
pages.push(i);
}
} else if (isMobile) {
// Mobile: simplified pagination - only show current and neighbors
if (currentPage === 1) {
// At start: 1 2 ... last
pages.push(1, 2, 'ellipsis', stableTotalPages);
} else if (currentPage === stableTotalPages) {
// At end: 1 ... (last-1) last
pages.push(1, 'ellipsis', stableTotalPages - 1, stableTotalPages);
} else {
// Middle: 1 ... current ... last
pages.push(1, 'ellipsis', currentPage, 'ellipsis', stableTotalPages);
}
} else {
// Desktop: full pagination logic
pages.push(1);

if (currentPage <= 3) {
// Near beginning: 1 2 3 4 ... last
for (let i = 2; i <= 4; i++) {
pages.push(i);
}
pages.push('ellipsis');
pages.push(stableTotalPages);
} else if (currentPage >= stableTotalPages - 2) {
// Near end: 1 ... (last-3) (last-2) (last-1) last
pages.push('ellipsis');
for (let i = stableTotalPages - 3; i <= stableTotalPages; i++) {
pages.push(i);
}
} else {
// In middle: 1 ... (current-1) current (current+1) ... last
pages.push('ellipsis');
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
pages.push(i);
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function SelectTrigger({
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4" />
<ChevronDownIcon className="text-foreground size-4" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
Expand Down
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const SUPPORTED_CHAINS = [
icon: arbitrumSepoliaIcon,
blockExplorerUrl: 'https://sepolia.arbiscan.io/',
subgraphUrl: {
poco: 'https://thegraph.arbitrum-sepolia-testnet.iex.ec/api/subgraphs/id/2GCj8gzLCihsiEDq8cYvC5nUgK6VfwZ6hm3Wj8A3kcxz',
poco: 'https://thegraph.iex.ec/subgraphs/name/bellecour/poco-v5',
dataprotector:
'https://thegraph.arbitrum-sepolia-testnet.iex.ec/api/subgraphs/id/5YjRPLtjS6GH6bB4yY55Qg4HzwtRGQ8TaHtGf9UBWWd',
},
Expand Down
45 changes: 45 additions & 0 deletions src/hooks/useFilterParam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useSearch, useNavigate } from '@tanstack/react-router';

/**
* Synchronize a string filter value with the URL search params.
* Ensures the value is part of the allowedValues list; otherwise falls back to defaultValue.
*
* @param paramName Query string key to store the filter value under.
* @param allowedValues List of allowed string values for the filter.
* @param defaultValue Default value if none present or invalid.
* @returns [currentValue, setValue]
*/
export function useFilterParam(
paramName: string,
allowedValues: string[],
defaultValue: string
) {
const search = useSearch({ strict: false });
const navigate = useNavigate();

const rawCandidate =
search && Object.prototype.hasOwnProperty.call(search, paramName)
? (search as Record<string, unknown>)[paramName]
: undefined;

const value =
typeof rawCandidate === 'string' && allowedValues.includes(rawCandidate)
? rawCandidate
: defaultValue;

const setValue = (newValue: string) => {
if (!allowedValues.includes(newValue)) return; // ignore invalid values
if (newValue !== value) {
navigate({
search: (prev) => ({
...prev,
[paramName]: newValue,
}),
replace: true,
resetScroll: false,
});
}
};

return [value, setValue] as const;
}
23 changes: 19 additions & 4 deletions src/modules/apps/AppsPreviewTable.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PREVIEW_TABLE_LENGTH, PREVIEW_TABLE_REFETCH_INTERVAL } from '@/config';
import { execute } from '@/graphql/poco/execute';
import { App_OrderBy, OrderDirection } from '@/graphql/poco/graphql';
import { cn } from '@/lib/utils';
import { useQuery } from '@tanstack/react-query';
import { LoaderCircle } from 'lucide-react';
Expand All @@ -8,25 +9,39 @@ import { DataTable } from '@/components/DataTable';
import AppIcon from '@/components/icons/AppIcon';
import { Button } from '@/components/ui/button';
import useUserStore from '@/stores/useUser.store';
import { createPlaceholderDataFnForQueryKey } from '@/utils/createPlaceholderDataFnForQueryKey';
import { createPlaceholderDataFn } from '@/utils/createPlaceholderDataFnForQueryKey';
import { getRecentFromTimestamp } from '@/utils/format';
import { ErrorAlert } from '../ErrorAlert';
import { appsQuery } from './appsQuery';
import { columns } from './appsTable/columns';

export function AppsPreviewTable({ className }: { className?: string }) {
const { chainId } = useUserStore();

const queryKey = [chainId, 'apps_preview'];
// Pertinent ordering: usageCount desc + recent usage constraint (last 14 days)
const recentFrom = getRecentFromTimestamp();
const orderBy: App_OrderBy = App_OrderBy.UsageCount;
const orderDirection: OrderDirection = OrderDirection.Desc;
const queryKey = [
chainId,
'apps_preview',
orderBy,
orderDirection,
recentFrom,
];
const apps = useQuery({
queryKey,
queryFn: () =>
execute(appsQuery, chainId, {
length: PREVIEW_TABLE_LENGTH,
skip: 0,
orderBy,
orderDirection,
recentFrom,
}),
refetchInterval: PREVIEW_TABLE_REFETCH_INTERVAL,
enabled: !!chainId,
placeholderData: createPlaceholderDataFnForQueryKey(queryKey),
placeholderData: createPlaceholderDataFn(),
});

const formattedData =
Expand All @@ -40,7 +55,7 @@ export function AppsPreviewTable({ className }: { className?: string }) {
<div className="flex items-center justify-between">
<h2 className="flex items-center gap-2 font-sans">
<AppIcon size={20} className="text-foreground" />
Latest apps deployed
Most pertinent apps
{apps.data && apps.isError && (
<span className="text-muted-foreground text-sm font-light">
(outdated)
Expand Down
19 changes: 13 additions & 6 deletions src/modules/apps/appsQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ export const appsQuery = graphql(`
$skip: Int = 0
$nextSkip: Int = 20
$nextNextSkip: Int = 40
$orderBy: App_orderBy = timestamp
$orderDirection: OrderDirection = desc
$recentFrom: BigInt = 0
) {
apps(
first: $length
skip: $skip
orderBy: timestamp
orderDirection: desc
where: { lastUsageTimestamp_gte: $recentFrom }
orderBy: $orderBy
orderDirection: $orderDirection
) {
address: id
owner {
Expand All @@ -23,6 +27,7 @@ export const appsQuery = graphql(`
multiaddr
checksum
mrenclave
lastUsageTimestamp
transfers(orderBy: timestamp, orderDirection: desc) {
transaction {
txHash: id
Expand All @@ -34,16 +39,18 @@ export const appsQuery = graphql(`
appsHasNext: apps(
first: 1
skip: $nextSkip
orderBy: timestamp
orderDirection: desc
orderBy: $orderBy
orderDirection: $orderDirection
where: { lastUsageTimestamp_gte: $recentFrom }
) {
address: id
}
appsHasNextNext: apps(
first: 1
skip: $nextNextSkip
orderBy: timestamp
orderDirection: desc
orderBy: $orderBy
orderDirection: $orderDirection
where: { lastUsageTimestamp_gte: $recentFrom }
) {
address: id
}
Expand Down
109 changes: 109 additions & 0 deletions src/modules/datasets/dataset/DatasetTasksTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { DETAIL_TABLE_LENGTH, TABLE_REFETCH_INTERVAL } from '@/config';
import { execute } from '@/graphql/poco/execute';
import { useQuery } from '@tanstack/react-query';
import { useEffect } from 'react';
import { DataTable } from '@/components/DataTable';
import { PaginatedNavigation } from '@/components/PaginatedNavigation';
import { usePageParam } from '@/hooks/usePageParam';
import { ErrorAlert } from '@/modules/ErrorAlert';
import { columns } from '@/modules/tasks/tasksTable/columns';
import useUserStore from '@/stores/useUser.store';
import { createPlaceholderDataFnForQueryKey } from '@/utils/createPlaceholderDataFnForQueryKey';
import { getAdditionalPages } from '@/utils/format';
import { datasetTasksQuery } from './datasetTasksQuery';

function useDatasetTasksData({
datasetId,
currentPage,
}: {
datasetId: string;
currentPage: number;
}) {
const { chainId } = useUserStore();
const skip = currentPage * DETAIL_TABLE_LENGTH;
const nextSkip = skip + DETAIL_TABLE_LENGTH;
const nextNextSkip = skip + 2 * DETAIL_TABLE_LENGTH;

const queryKey = [chainId, 'dataset', 'tasks', datasetId];
const { data, isLoading, isRefetching, isError, errorUpdateCount } = useQuery(
{
queryKey,
queryFn: () =>
execute(datasetTasksQuery, chainId, {
length: DETAIL_TABLE_LENGTH,
skip,
nextSkip,
nextNextSkip,
datasetId,
}),
refetchInterval: TABLE_REFETCH_INTERVAL,
placeholderData: createPlaceholderDataFnForQueryKey(queryKey),
}
);

const tasks = data?.dataset?.bulkUsages.map((usage) => usage.task);
// 0 = only current, 1 = next, 2 = next+1
const additionalPages = getAdditionalPages(
Boolean(data?.dataset?.bulkUsagesHasNext?.length),
Boolean(data?.dataset?.bulkUsagesHasNextNext?.length)
);

const formattedTask =
tasks?.map((task) => ({
...task,
destination: `/task/${task.taskid}`,
})) ?? [];

return {
data: formattedTask,
isLoading,
isRefetching,
isError,
additionalPages,
hasPastError: isError || errorUpdateCount > 0,
};
}

export function DatasetTasksTable({
datasetId,
setLoading,
setOutdated,
}: {
datasetId: string;
setLoading: (loading: boolean) => void;
setOutdated: (outdated: boolean) => void;
}) {
const [currentPage, setCurrentPage] = usePageParam('datasetTasksPage');
const {
data: tasks,
isError,
isLoading,
isRefetching,
additionalPages,
hasPastError,
} = useDatasetTasksData({ datasetId, currentPage: currentPage - 1 });

useEffect(
() => setLoading(isLoading || isRefetching),
[isLoading, isRefetching, setLoading]
);
useEffect(
() => setOutdated(tasks.length > 0 && isError),
[tasks.length, isError, setOutdated]
);

return (
<div className="space-y-6">
{hasPastError && !tasks.length ? (
<ErrorAlert message="An error occurred during dataset tasks loading." />
) : (
<DataTable columns={columns} data={tasks} />
)}
<PaginatedNavigation
currentPage={currentPage}
totalPages={currentPage + additionalPages}
onPageChange={setCurrentPage}
/>
</div>
);
}
Loading
Loading