Skip to content

Commit 6948bd0

Browse files
feat: add orderBy for workerpools and apps (#98)
* feat: implement filter parameter synchronization and enhance workerpools query with ordering options * feat: enhance AppsPreviewTable and appsQuery with ordering options and recent usage filter * feat: improve SearcherBar layout and error handling visibility * feat: enhance pagination controls with filter key handling and stability improvements * refactor: simplify navigation logic in useFilterParam hook * fix: remove TypeScript error comments for updated query signatures in appsQuery and workerpoolsQuery * feat: add SlidersHorizontal icon to order selection in WorkerpoolsRoute * feat: replace inline recent timestamp calculation with getRecentFromTimestamp utility function * refactor: improve raw value extraction and validation logic in useFilterParam hook --------- Co-authored-by: ErwanDecoster <[email protected]>
1 parent e198fe7 commit 6948bd0

File tree

12 files changed

+297
-52
lines changed

12 files changed

+297
-52
lines changed

src/components/PaginatedNavigation.tsx

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,27 @@ type PaginationControlsProps = {
1414
currentPage: number;
1515
totalPages: number;
1616
onPageChange: (page: number) => void;
17+
// Optional key whose change can shrink/grow pages immediately (e.g. active filter)
18+
filterKey?: string;
1719
};
1820

1921
export const PaginatedNavigation = ({
2022
currentPage,
2123
totalPages,
2224
onPageChange,
25+
filterKey,
2326
}: PaginationControlsProps) => {
2427
const { chainId } = useUserStore();
2528

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

33+
const lastFilterKeyRef = useRef<string | undefined>(undefined);
34+
const filterChangeFrameRef = useRef(0);
35+
3036
const chainHasChanged = chainId !== lastChainIdRef.current;
37+
const filterHasChanged = filterKey !== lastFilterKeyRef.current;
3138

3239
if (chainHasChanged) {
3340
lastChainIdRef.current = chainId ?? null;
@@ -36,66 +43,72 @@ export const PaginatedNavigation = ({
3643
chainChangeFrameRef.current++;
3744
}
3845

46+
if (filterHasChanged) {
47+
lastFilterKeyRef.current = filterKey;
48+
filterChangeFrameRef.current = 0;
49+
} else {
50+
filterChangeFrameRef.current++;
51+
}
52+
3953
let stableTotalPages = lastValidTotalPagesRef.current;
4054

4155
const isRecentChainChange = chainChangeFrameRef.current <= 5;
42-
43-
if (chainHasChanged || isRecentChainChange) {
56+
const isRecentFilterChange = filterChangeFrameRef.current <= 5;
57+
58+
if (
59+
chainHasChanged ||
60+
filterHasChanged ||
61+
isRecentChainChange ||
62+
isRecentFilterChange
63+
) {
4464
stableTotalPages = Math.max(totalPages, 1);
4565
} else if (totalPages > 0 && totalPages >= lastValidTotalPagesRef.current) {
4666
stableTotalPages = totalPages;
4767
}
68+
// Reset page if it no longer exists after filter change
69+
if (filterHasChanged && currentPage > stableTotalPages) {
70+
onPageChange(1);
71+
}
4872

4973
lastValidTotalPagesRef.current = stableTotalPages;
5074

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

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

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

6385
if (stableTotalPages <= maxVisiblePages) {
64-
// Show all pages if within limit
6586
for (let i = 1; i <= stableTotalPages; i++) {
6687
pages.push(i);
6788
}
6889
} else if (isMobile) {
69-
// Mobile: simplified pagination - only show current and neighbors
7090
if (currentPage === 1) {
71-
// At start: 1 2 ... last
7291
pages.push(1, 2, 'ellipsis', stableTotalPages);
7392
} else if (currentPage === stableTotalPages) {
74-
// At end: 1 ... (last-1) last
7593
pages.push(1, 'ellipsis', stableTotalPages - 1, stableTotalPages);
7694
} else {
77-
// Middle: 1 ... current ... last
7895
pages.push(1, 'ellipsis', currentPage, 'ellipsis', stableTotalPages);
7996
}
8097
} else {
81-
// Desktop: full pagination logic
8298
pages.push(1);
8399

84100
if (currentPage <= 3) {
85-
// Near beginning: 1 2 3 4 ... last
86101
for (let i = 2; i <= 4; i++) {
87102
pages.push(i);
88103
}
89104
pages.push('ellipsis');
90105
pages.push(stableTotalPages);
91106
} else if (currentPage >= stableTotalPages - 2) {
92-
// Near end: 1 ... (last-3) (last-2) (last-1) last
93107
pages.push('ellipsis');
94108
for (let i = stableTotalPages - 3; i <= stableTotalPages; i++) {
95109
pages.push(i);
96110
}
97111
} else {
98-
// In middle: 1 ... (current-1) current (current+1) ... last
99112
pages.push('ellipsis');
100113
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
101114
pages.push(i);

src/components/ui/select.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function SelectTrigger({
4141
>
4242
{children}
4343
<SelectPrimitive.Icon asChild>
44-
<ChevronDownIcon className="size-4" />
44+
<ChevronDownIcon className="text-foreground size-4" />
4545
</SelectPrimitive.Icon>
4646
</SelectPrimitive.Trigger>
4747
);

src/hooks/useFilterParam.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { useSearch, useNavigate } from '@tanstack/react-router';
2+
3+
/**
4+
* Synchronize a string filter value with the URL search params.
5+
* Ensures the value is part of the allowedValues list; otherwise falls back to defaultValue.
6+
*
7+
* @param paramName Query string key to store the filter value under.
8+
* @param allowedValues List of allowed string values for the filter.
9+
* @param defaultValue Default value if none present or invalid.
10+
* @returns [currentValue, setValue]
11+
*/
12+
export function useFilterParam(
13+
paramName: string,
14+
allowedValues: string[],
15+
defaultValue: string
16+
) {
17+
const search = useSearch({ strict: false });
18+
const navigate = useNavigate();
19+
20+
const rawCandidate =
21+
search && Object.prototype.hasOwnProperty.call(search, paramName)
22+
? (search as Record<string, unknown>)[paramName]
23+
: undefined;
24+
25+
const value =
26+
typeof rawCandidate === 'string' && allowedValues.includes(rawCandidate)
27+
? rawCandidate
28+
: defaultValue;
29+
30+
const setValue = (newValue: string) => {
31+
if (!allowedValues.includes(newValue)) return; // ignore invalid values
32+
if (newValue !== value) {
33+
navigate({
34+
search: (prev) => ({
35+
...prev,
36+
[paramName]: newValue,
37+
}),
38+
replace: true,
39+
resetScroll: false,
40+
});
41+
}
42+
};
43+
44+
return [value, setValue] as const;
45+
}

src/modules/apps/AppsPreviewTable.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { PREVIEW_TABLE_LENGTH, PREVIEW_TABLE_REFETCH_INTERVAL } from '@/config';
22
import { execute } from '@/graphql/poco/execute';
3+
import { App_OrderBy, OrderDirection } from '@/graphql/poco/graphql';
34
import { cn } from '@/lib/utils';
45
import { useQuery } from '@tanstack/react-query';
56
import { LoaderCircle } from 'lucide-react';
@@ -8,25 +9,39 @@ import { DataTable } from '@/components/DataTable';
89
import AppIcon from '@/components/icons/AppIcon';
910
import { Button } from '@/components/ui/button';
1011
import useUserStore from '@/stores/useUser.store';
11-
import { createPlaceholderDataFnForQueryKey } from '@/utils/createPlaceholderDataFnForQueryKey';
12+
import { createPlaceholderDataFn } from '@/utils/createPlaceholderDataFnForQueryKey';
13+
import { getRecentFromTimestamp } from '@/utils/format';
1214
import { ErrorAlert } from '../ErrorAlert';
1315
import { appsQuery } from './appsQuery';
1416
import { columns } from './appsTable/columns';
1517

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

19-
const queryKey = [chainId, 'apps_preview'];
21+
// Pertinent ordering: usageCount desc + recent usage constraint (last 14 days)
22+
const recentFrom = getRecentFromTimestamp();
23+
const orderBy: App_OrderBy = App_OrderBy.UsageCount;
24+
const orderDirection: OrderDirection = OrderDirection.Desc;
25+
const queryKey = [
26+
chainId,
27+
'apps_preview',
28+
orderBy,
29+
orderDirection,
30+
recentFrom,
31+
];
2032
const apps = useQuery({
2133
queryKey,
2234
queryFn: () =>
2335
execute(appsQuery, chainId, {
2436
length: PREVIEW_TABLE_LENGTH,
2537
skip: 0,
38+
orderBy,
39+
orderDirection,
40+
recentFrom,
2641
}),
2742
refetchInterval: PREVIEW_TABLE_REFETCH_INTERVAL,
2843
enabled: !!chainId,
29-
placeholderData: createPlaceholderDataFnForQueryKey(queryKey),
44+
placeholderData: createPlaceholderDataFn(),
3045
});
3146

3247
const formattedData =
@@ -40,7 +55,7 @@ export function AppsPreviewTable({ className }: { className?: string }) {
4055
<div className="flex items-center justify-between">
4156
<h2 className="flex items-center gap-2 font-sans">
4257
<AppIcon size={20} className="text-foreground" />
43-
Latest apps deployed
58+
Most pertinent apps
4459
{apps.data && apps.isError && (
4560
<span className="text-muted-foreground text-sm font-light">
4661
(outdated)

src/modules/apps/appsQuery.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ export const appsQuery = graphql(`
66
$skip: Int = 0
77
$nextSkip: Int = 20
88
$nextNextSkip: Int = 40
9+
$orderBy: App_orderBy = timestamp
10+
$orderDirection: OrderDirection = desc
11+
$recentFrom: BigInt = 0
912
) {
1013
apps(
1114
first: $length
1215
skip: $skip
13-
orderBy: timestamp
14-
orderDirection: desc
16+
where: { lastUsageTimestamp_gte: $recentFrom }
17+
orderBy: $orderBy
18+
orderDirection: $orderDirection
1519
) {
1620
address: id
1721
owner {
@@ -23,6 +27,7 @@ export const appsQuery = graphql(`
2327
multiaddr
2428
checksum
2529
mrenclave
30+
lastUsageTimestamp
2631
transfers(orderBy: timestamp, orderDirection: desc) {
2732
transaction {
2833
txHash: id
@@ -34,16 +39,18 @@ export const appsQuery = graphql(`
3439
appsHasNext: apps(
3540
first: 1
3641
skip: $nextSkip
37-
orderBy: timestamp
38-
orderDirection: desc
42+
orderBy: $orderBy
43+
orderDirection: $orderDirection
44+
where: { lastUsageTimestamp_gte: $recentFrom }
3945
) {
4046
address: id
4147
}
4248
appsHasNextNext: apps(
4349
first: 1
4450
skip: $nextNextSkip
45-
orderBy: timestamp
46-
orderDirection: desc
51+
orderBy: $orderBy
52+
orderDirection: $orderDirection
53+
where: { lastUsageTimestamp_gte: $recentFrom }
4754
) {
4855
address: id
4956
}

src/modules/search/SearcherBar.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,13 @@ export function SearcherBar({
174174
/>
175175
</div>
176176

177-
<div className={cn('mt-4 flex justify-center gap-4', isError && 'mt-10')}>
178-
<div className="flex justify-center sm:hidden">
177+
<div
178+
className={cn(
179+
'mt-4 flex justify-center gap-4 sm:hidden',
180+
isError && 'mt-10'
181+
)}
182+
>
183+
<div className="flex justify-center">
179184
<Button variant="outline" onClick={handleSearch} disabled={isPending}>
180185
{isPending ? 'Searching...' : 'Search'}
181186
</Button>

src/modules/workerpools/WorkerpoolsPreviewTable.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { PREVIEW_TABLE_LENGTH, PREVIEW_TABLE_REFETCH_INTERVAL } from '@/config';
22
import { execute } from '@/graphql/poco/execute';
3+
import { Workerpool_OrderBy, OrderDirection } from '@/graphql/poco/graphql';
34
import { cn } from '@/lib/utils';
45
import { useQuery } from '@tanstack/react-query';
56
import { LoaderCircle } from 'lucide-react';
@@ -8,25 +9,39 @@ import { DataTable } from '@/components/DataTable';
89
import WorkerpoolIcon from '@/components/icons/WorkerpoolIcon';
910
import { Button } from '@/components/ui/button';
1011
import useUserStore from '@/stores/useUser.store';
11-
import { createPlaceholderDataFnForQueryKey } from '@/utils/createPlaceholderDataFnForQueryKey';
12+
import { createPlaceholderDataFn } from '@/utils/createPlaceholderDataFnForQueryKey';
13+
import { getRecentFromTimestamp } from '@/utils/format';
1214
import { ErrorAlert } from '../ErrorAlert';
1315
import { workerpoolsQuery } from './workerpoolsQuery';
1416
import { columns } from './workerpoolsTable/columns';
1517

1618
export function WorkerpoolsPreviewTable({ className }: { className?: string }) {
1719
const { chainId } = useUserStore();
1820

19-
const queryKey = [chainId, 'workerpools_preview'];
21+
// Pertinent ordering: usageCount desc + recent usage constraint (last 14 days)
22+
const recentFrom = getRecentFromTimestamp();
23+
const orderBy: Workerpool_OrderBy = Workerpool_OrderBy.UsageCount;
24+
const orderDirection: OrderDirection = OrderDirection.Desc;
25+
const queryKey = [
26+
chainId,
27+
'workerpools_preview',
28+
orderBy,
29+
orderDirection,
30+
recentFrom,
31+
];
2032
const workerpools = useQuery({
2133
queryKey,
2234
queryFn: () =>
2335
execute(workerpoolsQuery, chainId, {
2436
length: PREVIEW_TABLE_LENGTH,
2537
skip: 0,
38+
orderBy,
39+
orderDirection,
40+
recentFrom,
2641
}),
2742
refetchInterval: PREVIEW_TABLE_REFETCH_INTERVAL,
2843
enabled: !!chainId,
29-
placeholderData: createPlaceholderDataFnForQueryKey(queryKey),
44+
placeholderData: createPlaceholderDataFn(),
3045
});
3146

3247
const formattedData =
@@ -40,7 +55,7 @@ export function WorkerpoolsPreviewTable({ className }: { className?: string }) {
4055
<div className="flex items-center justify-between">
4156
<h2 className="flex items-center gap-2 font-sans">
4257
<WorkerpoolIcon size={20} className="text-foreground" />
43-
Latest workerpools deployed
58+
Most pertinent workerpools
4459
{workerpools.data && workerpools.isError && (
4560
<span className="text-muted-foreground text-sm font-light">
4661
(outdated)

0 commit comments

Comments
 (0)