Skip to content

Commit f8a5583

Browse files
committed
feat: implement advanced schema search functionality with pagination and filtering, enhance dataset type handling
1 parent 751d109 commit f8a5583

File tree

7 files changed

+529
-16
lines changed

7 files changed

+529
-16
lines changed

src/config.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,36 @@ export const SUPPORTED_CHAINS = [
4646
isExperimental: true,
4747
},
4848
];
49+
50+
// Schema filter type constants
51+
export const LEGACY_TYPES = ['boolean', 'number'] as const;
52+
53+
export const SUPPORTED_MIME_TYPES = [
54+
'application/octet-stream',
55+
'application/pdf',
56+
'application/xml',
57+
'application/zip',
58+
'audio/midi',
59+
'audio/mpeg',
60+
'audio/x-wav',
61+
'image/bmp',
62+
'image/gif',
63+
'image/jpeg',
64+
'image/png',
65+
'image/webp',
66+
'video/mp4',
67+
'video/mpeg',
68+
'video/x-msvideo',
69+
] as const;
70+
71+
// Most commonly used types
72+
export const COMMON_DATA_TYPES = ['string', 'bool', 'f64', 'i128'] as const;
73+
74+
// Popular file types
75+
export const POPULAR_MIME_TYPES = [
76+
'application/pdf',
77+
'image/jpeg',
78+
'image/png',
79+
'image/gif',
80+
'video/mp4',
81+
] as const;
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import {
2+
LEGACY_TYPES,
3+
SUPPORTED_MIME_TYPES,
4+
COMMON_DATA_TYPES,
5+
POPULAR_MIME_TYPES,
6+
} from '@/config';
7+
import { cn } from '@/lib/utils';
8+
import { Plus, X, Filter, ChevronDown } from 'lucide-react';
9+
import { useState, useCallback } from 'react';
10+
import { Button } from '@/components/ui/button';
11+
import { Input } from '@/components/ui/input';
12+
import {
13+
Select,
14+
SelectContent,
15+
SelectItem,
16+
SelectTrigger,
17+
SelectValue,
18+
} from '@/components/ui/select';
19+
20+
export interface SchemaFilter {
21+
id: string;
22+
path: string;
23+
type: string;
24+
}
25+
26+
type PopularMimeType = (typeof POPULAR_MIME_TYPES)[number];
27+
28+
interface SchemaSearchProps {
29+
className?: string;
30+
onFiltersChanged?: (filters: SchemaFilter[]) => void;
31+
filters: SchemaFilter[];
32+
}
33+
34+
export function SchemaSearch({
35+
className,
36+
onFiltersChanged,
37+
filters,
38+
}: SchemaSearchProps) {
39+
const [isOpen, setIsOpen] = useState(true); // Ouvert par défaut pour une meilleure UX
40+
const [newPath, setNewPath] = useState('');
41+
const [newType, setNewType] = useState<string>('');
42+
43+
const addFilter = useCallback(() => {
44+
if (!newPath.trim() || !newType) {
45+
return;
46+
}
47+
48+
const newFilter: SchemaFilter = {
49+
id: Math.random().toString(36).substr(2, 9),
50+
path: newPath.trim(),
51+
type: newType,
52+
};
53+
54+
const updatedFilters = [...filters, newFilter];
55+
onFiltersChanged?.(updatedFilters);
56+
setNewPath('');
57+
setNewType('');
58+
}, [newPath, newType, filters, onFiltersChanged]);
59+
60+
const removeFilter = useCallback(
61+
(filterId: string) => {
62+
const updatedFilters = filters.filter((f) => f.id !== filterId);
63+
onFiltersChanged?.(updatedFilters);
64+
},
65+
[filters, onFiltersChanged]
66+
);
67+
68+
const clearFilters = useCallback(() => {
69+
onFiltersChanged?.([]);
70+
}, [onFiltersChanged]);
71+
72+
return (
73+
<div className={cn('w-full space-y-4', className)}>
74+
{/* Collapsible trigger */}
75+
<Button
76+
variant="outline"
77+
className="border-primary/20 hover:border-primary/40 w-full justify-start gap-2"
78+
size="lg"
79+
onClick={() => setIsOpen(!isOpen)}
80+
>
81+
<Filter size={16} />
82+
<span className="font-semibold">Advanced Schema Search</span>
83+
<span className="text-muted-foreground text-xs">
84+
{isOpen
85+
? 'Filter datasets by field types'
86+
: 'Click to add schema filters'}
87+
</span>
88+
{filters.length > 0 && (
89+
<span className="bg-primary text-primary-foreground ml-auto inline-flex items-center rounded-full px-2 py-0.5 text-xs font-semibold">
90+
{filters.length} filter{filters.length > 1 ? 's' : ''}
91+
</span>
92+
)}
93+
<ChevronDown
94+
size={16}
95+
className={`ml-2 transition-transform ${isOpen ? 'rotate-180' : ''}`}
96+
/>
97+
</Button>
98+
99+
{/* Collapsible content */}
100+
{isOpen && (
101+
<div className="border-border/50 bg-muted/30 space-y-4 rounded-lg border p-4 pt-4">
102+
{/* Add New Filter - En premier pour une meilleure UX */}
103+
<div className="space-y-3">
104+
<h4 className="flex items-center gap-2 text-sm font-medium">
105+
<Plus size={16} className="text-primary" />
106+
Filter by Field Type
107+
</h4>
108+
<div className="flex gap-2">
109+
<Input
110+
placeholder="Field path (e.g., email, user.email, nested.field)"
111+
value={newPath}
112+
onChange={(e) => setNewPath(e.target.value)}
113+
className="flex-1"
114+
onKeyDown={(e) => {
115+
if (e.key === 'Enter' && newPath && newType) {
116+
addFilter();
117+
}
118+
}}
119+
/>
120+
<Select value={newType} onValueChange={setNewType}>
121+
<SelectTrigger className="w-40">
122+
<SelectValue placeholder="Select type" />
123+
</SelectTrigger>
124+
<SelectContent className="max-h-80">
125+
{/* Common Data Types */}
126+
<div className="text-muted-foreground border-b p-2 text-xs font-medium">
127+
📝 Common Types
128+
</div>
129+
{COMMON_DATA_TYPES.map((type) => (
130+
<SelectItem key={type} value={type}>
131+
<span className="font-mono text-green-600">{type}</span>
132+
</SelectItem>
133+
))}
134+
135+
{/* Legacy Types */}
136+
<div className="text-muted-foreground mt-2 border-b p-2 text-xs font-medium">
137+
⚠️ Legacy Common Types (deprecated)
138+
</div>
139+
{LEGACY_TYPES.map((type) => (
140+
<SelectItem key={type} value={type}>
141+
<span className="font-mono text-orange-600">{type}</span>
142+
<span className="text-muted-foreground ml-2 text-xs">
143+
(legacy)
144+
</span>
145+
</SelectItem>
146+
))}
147+
148+
{/* Popular MIME Types */}
149+
<div className="text-muted-foreground mt-2 border-b p-2 text-xs font-medium">
150+
🗂️ Popular File Types
151+
</div>
152+
{POPULAR_MIME_TYPES.map((type) => (
153+
<SelectItem key={type} value={type}>
154+
<span className="font-mono text-blue-600">{type}</span>
155+
</SelectItem>
156+
))}
157+
158+
{/* All Other MIME Types */}
159+
<div className="text-muted-foreground mt-2 border-b p-2 text-xs font-medium">
160+
📎 Other File Types
161+
</div>
162+
{SUPPORTED_MIME_TYPES.filter(
163+
(t) => !POPULAR_MIME_TYPES.includes(t as PopularMimeType)
164+
).map((type) => (
165+
<SelectItem key={type} value={type}>
166+
<span className="font-mono text-blue-400">{type}</span>
167+
</SelectItem>
168+
))}
169+
</SelectContent>
170+
</Select>
171+
<Button
172+
onClick={addFilter}
173+
disabled={!newPath.trim() || !newType}
174+
size="icon"
175+
className="shrink-0"
176+
>
177+
<Plus size={16} />
178+
</Button>
179+
</div>
180+
{/* Helper text */}
181+
<p className="text-muted-foreground text-xs">
182+
Add field types to filter datasets by their schema structure.
183+
Filters are applied automatically.
184+
</p>
185+
</div>
186+
187+
{/* Current Filters - Affiché en dessous, seulement s'il y en a */}
188+
{filters.length > 0 && (
189+
<div className="border-border/50 space-y-3 border-t pt-3">
190+
<div className="flex items-center justify-between">
191+
<h4 className="flex items-center gap-2 text-sm font-medium">
192+
<Filter size={16} className="text-primary" />
193+
Active Filters ({filters.length})
194+
</h4>
195+
<Button
196+
variant="ghost"
197+
size="sm"
198+
onClick={clearFilters}
199+
className="hover:bg-destructive/10 hover:text-destructive h-7 px-3 text-xs"
200+
>
201+
Clear all
202+
</Button>
203+
</div>
204+
<div className="flex flex-wrap gap-2">
205+
{filters.map((filter) => (
206+
<span
207+
key={filter.id}
208+
className="border-primary/20 bg-primary/5 hover:bg-primary/10 inline-flex items-center gap-1 rounded-full border px-3 py-1 text-xs font-medium transition-colors"
209+
>
210+
<span className="text-primary font-mono">
211+
{filter.path}:{filter.type}
212+
</span>
213+
<button
214+
onClick={() => removeFilter(filter.id)}
215+
className="hover:bg-destructive/20 hover:text-destructive ml-1 rounded-full p-0.5 transition-colors"
216+
title="Remove filter"
217+
>
218+
<X size={12} />
219+
</button>
220+
</span>
221+
))}
222+
</div>
223+
</div>
224+
)}
225+
</div>
226+
)}
227+
</div>
228+
);
229+
}

src/modules/datasets/dataset/protectedDataQuery.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,60 @@ export const optimizedProtectedDatasQueryString = graphql(`
2424
}
2525
}
2626
`);
27+
28+
// Query pour la recherche par schéma avec pagination optimisée et filtrage côté serveur
29+
export const schemaSearchPaginatedQuery = graphql(`
30+
query SchemaSearchPaginated(
31+
$length: Int = 20
32+
$skip: Int = 0
33+
$nextSkip: Int = 20
34+
$nextNextSkip: Int = 40
35+
$requiredSchema: [String!]
36+
) {
37+
protectedDatas(
38+
where: {
39+
transactionHash_not: "0x"
40+
schema_contains: $requiredSchema
41+
}
42+
skip: $skip
43+
first: $length
44+
orderBy: creationTimestamp
45+
orderDirection: desc
46+
) {
47+
id
48+
name
49+
creationTimestamp
50+
owner {
51+
id
52+
}
53+
schema {
54+
path
55+
type
56+
}
57+
}
58+
protectedDatasHasNext: protectedDatas(
59+
where: {
60+
transactionHash_not: "0x"
61+
schema_contains: $requiredSchema
62+
}
63+
first: 1
64+
skip: $nextSkip
65+
orderBy: creationTimestamp
66+
orderDirection: desc
67+
) {
68+
id
69+
}
70+
protectedDatasHasNextNext: protectedDatas(
71+
where: {
72+
transactionHash_not: "0x"
73+
schema_contains: $requiredSchema
74+
}
75+
first: 1
76+
skip: $nextNextSkip
77+
orderBy: creationTimestamp
78+
orderDirection: desc
79+
) {
80+
id
81+
}
82+
}
83+
`);

src/modules/datasets/datasetsTable/columns.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { formatElapsedTime } from '@/utils/formatElapsedTime';
66
import { truncateAddress } from '@/utils/truncateAddress';
77

88
type Dataset = DatasetsQuery['datasets'][number] & {
9-
schemaPaths?: Array<{ path?: string | null }>;
9+
schemaPaths?: Array<{ path?: string | null; type?: string | null }>;
1010
isSchemaLoading?: boolean;
1111
};
1212

0 commit comments

Comments
 (0)