Skip to content

Commit 7f9a7ec

Browse files
committed
feat: add SchemaSearch component for filtering datasets by field types and integrate it into DatasetsRoute
1 parent 7a44309 commit 7f9a7ec

File tree

5 files changed

+262
-13
lines changed

5 files changed

+262
-13
lines changed
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { LOCAL_STORAGE_PREFIX } from '@/config';
2+
import { cn } from '@/lib/utils';
3+
import { SelectLabel } from '@radix-ui/react-select';
4+
import { useSearch, useNavigate } from '@tanstack/react-router';
5+
import { ChevronDown, SlidersHorizontal, X } from 'lucide-react';
6+
import { useEffect, useState } from 'react';
7+
import useLocalStorageState from 'use-local-storage-state';
8+
import { Button } from '@/components/ui/button';
9+
import { Input } from '@/components/ui/input';
10+
import {
11+
Select,
12+
SelectContent,
13+
SelectGroup,
14+
SelectItem,
15+
SelectTrigger,
16+
SelectValue,
17+
} from '@/components/ui/select';
18+
import { usePageParam } from '@/hooks/usePageParam';
19+
import { borderTypeColor } from './borderTypeColor';
20+
import {
21+
decodeSchemaFilters,
22+
encodeSchemaFilters,
23+
SchemaFilter,
24+
} from './schemaFilters';
25+
26+
export function SchemaSearch() {
27+
const typeGroups = [
28+
{
29+
label: 'Common types',
30+
items: [
31+
{ value: 'string' },
32+
{ value: 'f64' },
33+
{ value: 'i128' },
34+
{ value: 'bool' },
35+
],
36+
},
37+
{
38+
label: 'Legacy Common Types (deprecated)',
39+
items: [{ value: 'number' }, { value: 'boolean' }],
40+
},
41+
{
42+
label: 'Popular File Types',
43+
items: [
44+
{ value: 'application/pdf' },
45+
{ value: 'image/jpeg' },
46+
{ value: 'image/png' },
47+
{ value: 'image/gif' },
48+
{ value: 'video/mp4' },
49+
],
50+
},
51+
{
52+
label: 'Other File Types',
53+
items: [
54+
{ value: 'application/octet-stream' },
55+
{ value: 'application/xml' },
56+
{ value: 'application/zip' },
57+
{ value: 'image/bmp' },
58+
{ value: 'image/webp' },
59+
{ value: 'video/mpeg' },
60+
{ value: 'video/x-msvideo' },
61+
{ value: 'audio/midi' },
62+
{ value: 'audio/mpeg' },
63+
{ value: 'audio/x-wav' },
64+
],
65+
},
66+
];
67+
68+
const [isOpen, setIsOpen] = useLocalStorageState<boolean>(
69+
`${LOCAL_STORAGE_PREFIX}_is_datasets_schema_search_open`,
70+
{ defaultValue: true }
71+
);
72+
const [inputPathValue, setInputPathValue] = useState('');
73+
const [selectedType, setSelectedType] = useState<string>('');
74+
const [, setCurrentPage] = usePageParam('datasetsPage');
75+
76+
const search = useSearch({ strict: false });
77+
const navigate = useNavigate();
78+
const filters: SchemaFilter[] = decodeSchemaFilters(search?.schema);
79+
80+
useEffect(() => {
81+
if (!isOpen && filters.length > 0) {
82+
setIsOpen(true);
83+
}
84+
}, []);
85+
86+
const handleAddFilter = () => {
87+
if (!inputPathValue.trim() || !selectedType) return;
88+
const newFilters: SchemaFilter[] = [
89+
...filters.filter(
90+
(f) => !(f.path === inputPathValue && f.type === selectedType)
91+
),
92+
{ path: inputPathValue.trim(), type: selectedType },
93+
];
94+
navigate({
95+
search: { ...search, schema: encodeSchemaFilters(newFilters) },
96+
replace: true,
97+
resetScroll: false,
98+
});
99+
setInputPathValue('');
100+
setSelectedType('');
101+
setCurrentPage(0);
102+
};
103+
104+
const handleRemoveFilter = (index: number) => {
105+
const newFilters = filters.filter((_, i) => i !== index);
106+
const newSearch = { ...search };
107+
if (newFilters.length === 0) {
108+
delete newSearch.schema;
109+
} else {
110+
newSearch.schema = encodeSchemaFilters(newFilters);
111+
}
112+
navigate({
113+
search: newSearch,
114+
replace: true,
115+
resetScroll: false,
116+
});
117+
};
118+
119+
return (
120+
<div className="rounded-2xl border border-[#303038]">
121+
<button
122+
className={cn('flex w-full items-center gap-2 px-10 py-6 duration-300')}
123+
onClick={() => setIsOpen(!isOpen)}
124+
>
125+
<SlidersHorizontal size={16} />
126+
<div className="flex-1 text-left">Schema Search</div>
127+
<ChevronDown
128+
className={cn(
129+
'ml-auto transition-transform',
130+
isOpen && '-rotate-180'
131+
)}
132+
/>
133+
</button>
134+
<div
135+
className={cn(
136+
'grid transition-all duration-300',
137+
isOpen
138+
? 'translate-y-0 grid-rows-[1fr]'
139+
: 'translate-y-2 grid-rows-[0fr]'
140+
)}
141+
>
142+
<div className={cn('text-grey-200 grid overflow-hidden px-6 md:px-10')}>
143+
<hr className="border-secondary" />
144+
<div
145+
className={cn(
146+
'grid transition-all duration-300',
147+
filters.length > 0
148+
? 'mt-6 translate-y-0 grid-rows-[1fr]'
149+
: 'translate-y-2 grid-rows-[0fr]'
150+
)}
151+
>
152+
<div className="flex flex-wrap items-center gap-2.5 overflow-hidden">
153+
Filter by field types:{' '}
154+
{filters.map((schema, index) => {
155+
const borderColor = borderTypeColor.find((color) =>
156+
color.keywords.some((keyword) =>
157+
schema.type?.toLowerCase().includes(keyword)
158+
)
159+
)?.color;
160+
return (
161+
<span
162+
key={index}
163+
className={cn(
164+
'inline-flex w-fit items-center rounded-full border px-4 py-2 text-xs',
165+
borderColor
166+
)}
167+
>
168+
<span className={cn('inline-block')}>{schema.path}</span>
169+
<span className={cn('inline-block')}>: {schema.type}</span>
170+
<button onClick={() => handleRemoveFilter(index)}>
171+
<X className="ml-1 text-white" size={12} />
172+
</button>
173+
</span>
174+
);
175+
})}
176+
</div>
177+
</div>
178+
<div className="mt-6 mb-6 flex translate-y-1 gap-4">
179+
<Input
180+
value={inputPathValue}
181+
onChange={(e) => setInputPathValue(e.target.value)}
182+
className={cn(
183+
'bg-muted border-secondary col-span-2 w-full rounded-2xl px-4 py-6 text-sm text-white'
184+
)}
185+
placeholder="Field path (e.g email, telegram_chatId, nested)"
186+
/>
187+
188+
<Select value={selectedType} onValueChange={setSelectedType}>
189+
<SelectTrigger className="bg-muted border-secondary w-full max-w-1/3 rounded-2xl px-4 py-6 text-sm text-white">
190+
<SelectValue placeholder="Select type" />
191+
</SelectTrigger>
192+
<SelectContent className="bg-muted border-secondary overflow-visible p-6">
193+
{typeGroups.map((group) => (
194+
<SelectGroup
195+
key={group.label}
196+
className="overflow-visible not-first:mt-2"
197+
>
198+
<SelectLabel className="text-grey-300 text-sm">
199+
{group.label}
200+
</SelectLabel>
201+
{group.items.map((type) => (
202+
<SelectItem
203+
key={type.value}
204+
className="text-base font-bold data-[state=checked]:text-yellow-400"
205+
value={type.value}
206+
>
207+
{type.value}
208+
</SelectItem>
209+
))}
210+
</SelectGroup>
211+
))}
212+
</SelectContent>
213+
</Select>
214+
<Button
215+
onClick={handleAddFilter}
216+
disabled={!inputPathValue.trim() || !selectedType}
217+
>
218+
Add filter
219+
</Button>
220+
</div>
221+
{/* {(localError || error) && (
222+
<p className="bg-danger text-danger-foreground border-danger-border absolute -bottom-8 rounded-full border px-4">
223+
{localError ? localError.message : error?.message}
224+
</p>
225+
)} */}
226+
</div>
227+
</div>
228+
</div>
229+
);
230+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const borderTypeColor = [
2+
{ keywords: ['string'], color: 'border-yellow-500 text-yellow-200' },
3+
{ keywords: ['video'], color: 'border-orange-300 text-orange-300' },
4+
{ keywords: ['bool'], color: 'border-blue-200 text-blue-200' },
5+
{ keywords: ['application'], color: 'border-blue-400 text-blue-400' },
6+
{ keywords: ['audio'], color: 'border-[#A0B1FE] text-[#A0B1FE]' },
7+
{ keywords: ['f64'], color: 'border-green-200 text-green-200' },
8+
{ keywords: ['i128'], color: 'border-purple-200 text-purple-200' },
9+
{ keywords: ['image'], color: 'border-[#F05FC5] text-[#F05FC5]' },
10+
{ keywords: ['number'], color: 'border-[#F693B8] text-[#F693B8]' },
11+
{ keywords: ['boolean'], color: 'border-purple-100 text-purple-100' },
12+
];

src/modules/datasets/dataset/schema/TypeBadge.tsx

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { DatasetSchemaQuery } from '@/graphql/dataprotector/graphql';
22
import { cn } from '@/lib/utils';
33
import React from 'react';
44
import { pluralize } from '@/utils/pluralize';
5+
import { borderTypeColor } from '../../borderTypeColor';
56

67
interface TypeBadgeProps {
78
schemaPaths?: NonNullable<
@@ -14,19 +15,6 @@ interface TypeBadgeProps {
1415
overflowHidden?: boolean;
1516
}
1617

17-
const borderTypeColor = [
18-
{ keywords: ['string'], color: 'border-yellow-500 text-yellow-200' },
19-
{ keywords: ['video'], color: 'border-orange-300 text-orange-300' },
20-
{ keywords: ['bool'], color: 'border-blue-200 text-blue-200' },
21-
{ keywords: ['application'], color: 'border-blue-400 text-blue-400' },
22-
{ keywords: ['audio'], color: 'border-[#A0B1FE] text-[#A0B1FE]' },
23-
{ keywords: ['f64'], color: 'border-green-200 text-green-200' },
24-
{ keywords: ['i128'], color: 'border-purple-200 text-purple-200' },
25-
{ keywords: ['image'], color: 'border-[#F05FC5] text-[#F05FC5]' },
26-
{ keywords: ['number'], color: 'border-[#F693B8] text-[#F693B8]' },
27-
{ keywords: ['boolean'], color: 'border-purple-100 text-purple-100' },
28-
];
29-
3018
const TypeBadge: React.FC<TypeBadgeProps> = ({
3119
schemaPaths,
3220
isLoading,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export type SchemaFilter = { path: string; type: string };
2+
3+
export function decodeSchemaFilters(str?: string): SchemaFilter[] {
4+
if (!str) return [];
5+
return str
6+
.split(',')
7+
.map((pair) => {
8+
const [path, type] = pair.split(':');
9+
return path && type ? { path, type } : null;
10+
})
11+
.filter(Boolean) as SchemaFilter[];
12+
}
13+
14+
export function encodeSchemaFilters(filters: SchemaFilter[]): string {
15+
return filters.map((f) => `${f.path}:${f.type}`).join(',');
16+
}

src/routes/$chainSlug/_layout/datasets.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { BackButton } from '@/components/ui/BackButton';
1010
import { usePageParam } from '@/hooks/usePageParam';
1111
import { ErrorAlert } from '@/modules/ErrorAlert';
1212
import { DatasetBreadcrumbsList } from '@/modules/datasets/DatasetBreadcrumbs';
13+
import { SchemaSearch } from '@/modules/datasets/SchemaSearch';
1314
import { datasetsQuery } from '@/modules/datasets/datasetsQuery';
1415
import { columns } from '@/modules/datasets/datasetsTable/columns';
1516
import { useDatasetsSchemas } from '@/modules/datasets/hooks/useDatasetsSchemas';
@@ -110,6 +111,8 @@ function DatasetsRoute() {
110111
</div>
111112
</div>
112113

114+
<SchemaSearch />
115+
113116
{hasPastError && !data.length ? (
114117
<ErrorAlert message="An error occurred during datasets loading." />
115118
) : (

0 commit comments

Comments
 (0)