@@ -148,7 +148,6 @@ const DeleteDocumentDialog: React.FC<{
148148const DataExplorerPage : React . FC < DataExplorerPageProps > = ( {
149149 resource,
150150 dbInfo,
151- accountName,
152151 availableDbs,
153152 availableAccounts,
154153 initialDocumentId,
@@ -182,13 +181,31 @@ const DataExplorerPage: React.FC<DataExplorerPageProps> = ({
182181 const [ pageInput , setPageInput ] = useState ( String ( currentPage ) ) ;
183182
184183 // --- Filtering State ---
185- const [ filterKey , setFilterKey ] = useState ( 'all' ) ;
186- const [ filterValue , setFilterValue ] = useState ( '' ) ;
187- const [ debouncedFilterValue , setDebouncedFilterValue ] = useState ( filterValue ) ;
184+ interface FilterState {
185+ id : string ;
186+ key : string ;
187+ value : string ;
188+ isCustom : boolean ;
189+ operator ?: string ;
190+ }
191+ const [ filters , setFilters ] = useState < FilterState [ ] > ( [ { id : 'default' , key : 'all' , value : '' , isCustom : false , operator : 'equals' } ] ) ;
192+ const [ debouncedFilters , setDebouncedFilters ] = useState < FilterState [ ] > ( filters ) ;
188193 const [ schemaTree , setSchemaTree ] = useState < SchemaKeyNode [ ] > ( [ ] ) ;
189194 const [ isFetchingSchema , setIsFetchingSchema ] = useState ( false ) ;
190195 const [ currentCollectionInfo , setCurrentCollectionInfo ] = useState < CollectionInfo | null > ( null ) ;
191196
197+ const addFilter = ( ) => {
198+ setFilters ( prev => [ ...prev , { id : Math . random ( ) . toString ( 36 ) . substring ( 7 ) , key : 'all' , value : '' , isCustom : false , operator : 'equals' } ] ) ;
199+ } ;
200+
201+ const removeFilter = ( id : string ) => {
202+ setFilters ( prev => prev . filter ( f => f . id !== id ) ) ;
203+ } ;
204+
205+ const updateFilter = ( id : string , updates : Partial < FilterState > ) => {
206+ setFilters ( prev => prev . map ( f => f . id === id ? { ...f , ...updates } : f ) ) ;
207+ } ;
208+
192209 // --- Editor State ---
193210 const [ selectedDocument , setSelectedDocument ] = useState < Record < string , any > | null > ( null ) ;
194211 const [ editMode , setEditMode ] = useState ( false ) ;
@@ -354,25 +371,24 @@ const DataExplorerPage: React.FC<DataExplorerPageProps> = ({
354371 setTotalPages ( 1 ) ;
355372 setTotalDocuments ( 0 ) ;
356373 setPageInput ( '1' ) ;
357- setFilterKey ( 'all' ) ;
358- setFilterValue ( '' ) ;
359- setDebouncedFilterValue ( '' ) ;
374+ setFilters ( [ { id : 'default' , key : 'all' , value : '' , isCustom : false , operator : 'equals' } ] ) ;
375+ setDebouncedFilters ( [ { id : 'default' , key : 'all' , value : '' , isCustom : false , operator : 'equals' } ] ) ;
360376 setSchemaTree ( [ ] ) ;
361377 setIsFetchingSchema ( false ) ;
362378 setSelectedDocument ( null ) ;
363379 setBreadcrumbs ( [ ] ) ;
364380 // Do not reset pinned documents here, as they should persist across DB/collection changes.
365381 } , [ ] ) ;
366382
367- // Debounce search input
383+ // Debounce filters
368384 useEffect ( ( ) => {
369385 const handler = setTimeout ( ( ) => {
370- setDebouncedFilterValue ( filterValue ) ;
386+ setDebouncedFilters ( filters ) ;
371387 setCurrentPage ( 1 ) ; // Reset to page 1 on new search
372388 setSelectedDocument ( null ) ; // Clear selection on new search
373389 } , 300 ) ;
374390 return ( ) => clearTimeout ( handler ) ;
375- } , [ filterValue ] ) ;
391+ } , [ filters ] ) ;
376392
377393 // Sync page input with current page
378394 useEffect ( ( ) => setPageInput ( String ( currentPage ) ) , [ currentPage ] ) ;
@@ -432,8 +448,12 @@ const DataExplorerPage: React.FC<DataExplorerPageProps> = ({
432448 setIsLoading ( true ) ;
433449 setError ( null ) ;
434450 try {
435- const processedValue = getCoercedFilterValue ( debouncedFilterValue ) ;
436- const response = await getDocuments ( selectedCollection , currentResource , currentPage , 20 , { key : filterKey , value : processedValue } ) ;
451+ const activeFilters = debouncedFilters . map ( f => ( {
452+ key : f . key ,
453+ value : getCoercedFilterValue ( f . value ) ,
454+ operator : f . operator || 'equals'
455+ } ) ) ;
456+ const response = await getDocuments ( selectedCollection , currentResource , currentPage , 20 , undefined , activeFilters ) ;
437457 setDocuments ( response . documents ) ;
438458 setTotalPages ( response . totalPages ) ;
439459 setTotalDocuments ( response . totalDocuments ) ;
@@ -446,7 +466,7 @@ const DataExplorerPage: React.FC<DataExplorerPageProps> = ({
446466 } finally {
447467 setIsLoading ( false ) ;
448468 }
449- } , [ selectedCollection , currentResource , currentPage , filterKey , debouncedFilterValue ] ) ;
469+ } , [ selectedCollection , currentResource , currentPage , debouncedFilters ] ) ;
450470
451471 useEffect ( ( ) => {
452472 if ( breadcrumbs . length > 0 && selectedDocument ) return ;
@@ -569,8 +589,8 @@ const DataExplorerPage: React.FC<DataExplorerPageProps> = ({
569589 if ( selectedCollection === collectionName ) return ;
570590 setSelectedCollection ( collectionName ) ;
571591 setCurrentPage ( 1 ) ;
572- setFilterValue ( '' ) ;
573- setFilterKey ( ' all') ;
592+ setFilters ( [ { id : 'default' , key : 'all' , value : '' , isCustom : false , operator : 'equals' } ] ) ;
593+ setDebouncedFilters ( [ { id : 'default' , key : ' all', value : '' , isCustom : false , operator : 'equals' } ] ) ;
574594 setSelectedDocument ( null ) ;
575595 setBreadcrumbs ( [ ] ) ;
576596 await fetchSchemaForCollection ( collectionName ) ;
@@ -715,7 +735,7 @@ const DataExplorerPage: React.FC<DataExplorerPageProps> = ({
715735 return (
716736 < div className = "text-center text-slate-500 dark:text-slate-400 py-10" >
717737 < p > No documents found.</ p >
718- { debouncedFilterValue && < p className = "text-xs" > Try a different filter or value.</ p > }
738+ { debouncedFilters . some ( f => f . value || f . operator === 'exists' || f . operator === 'not_exists' ) && < p className = "text-xs" > Try a different filter or value.</ p > }
719739 </ div >
720740 ) ;
721741 }
@@ -1237,30 +1257,100 @@ const DataExplorerPage: React.FC<DataExplorerPageProps> = ({
12371257 </ div >
12381258 </ div >
12391259 < div className = "space-y-2" >
1240- < div className = "relative" >
1241- < select
1242- value = { filterKey }
1243- onChange = { ( e ) => setFilterKey ( e . target . value ) }
1244- disabled = { ! selectedCollection || isFetchingSchema }
1245- className = "w-full text-sm appearance-none cursor-pointer p-2 pr-8 bg-slate-100 dark:bg-slate-700/50 border border-slate-300 dark:border-slate-600 rounded-lg text-slate-900 dark:text-slate-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors disabled:opacity-50"
1246- title = "Select a field to filter by"
1247- >
1248- < option value = "all" > All Fields</ option >
1249- < RenderOptions nodes = { schemaTree } level = { 0 } />
1250- </ select >
1251- < ChevronDownIcon className = "absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 dark:text-slate-500 pointer-events-none" />
1252- </ div >
1253- < div className = "relative" >
1254- < input
1255- type = "text"
1256- placeholder = { isFetchingSchema ? 'Loading schema...' : 'Filter value...' }
1257- value = { filterValue }
1258- onChange = { ( e ) => setFilterValue ( e . target . value ) }
1259- disabled = { ! selectedCollection || isFetchingSchema }
1260- className = "w-full pl-9 pr-4 py-2 bg-slate-100 dark:bg-slate-700/50 border border-slate-300 dark:border-slate-600 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors disabled:opacity-50"
1261- />
1262- < SearchIcon className = "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 dark:text-slate-500" />
1263- </ div >
1260+ { filters . map ( ( f , i ) => (
1261+ < div key = { f . id } className = "flex flex-col gap-2 p-3 bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 rounded-lg relative" >
1262+ { filters . length > 1 && (
1263+ < button onClick = { ( ) => removeFilter ( f . id ) } className = "absolute -top-2 -right-2 p-1 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-full text-slate-400 hover:text-red-500 hover:border-red-500 shadow-sm z-10 transition-colors" >
1264+ < XIcon className = "w-3 h-3" />
1265+ </ button >
1266+ ) }
1267+ < div className = "flex gap-2" >
1268+ < div className = "flex-[2_2_0%] relative" >
1269+ { ! f . isCustom ? (
1270+ < >
1271+ < select
1272+ value = { f . key }
1273+ onChange = { ( e ) => {
1274+ if ( e . target . value === '__custom__' ) {
1275+ updateFilter ( f . id , { isCustom : true , key : '' } ) ;
1276+ } else {
1277+ updateFilter ( f . id , { key : e . target . value } ) ;
1278+ }
1279+ } }
1280+ disabled = { ! selectedCollection || isFetchingSchema }
1281+ className = "w-full text-sm appearance-none cursor-pointer p-2 pr-8 bg-white dark:bg-slate-700/50 border border-slate-300 dark:border-slate-600 rounded-lg text-slate-900 dark:text-slate-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors disabled:opacity-50"
1282+ title = "Select a field to filter by"
1283+ >
1284+ < option value = "all" > All Fields</ option >
1285+ < RenderOptions nodes = { schemaTree } level = { 0 } />
1286+ < option disabled > ──────────</ option >
1287+ < option value = "__custom__" > Type custom field...</ option >
1288+ </ select >
1289+ < ChevronDownIcon className = "absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 dark:text-slate-500 pointer-events-none" />
1290+ </ >
1291+ ) : (
1292+ < div className = "flex items-center relative" >
1293+ < input
1294+ type = "text"
1295+ value = { f . key }
1296+ onChange = { ( e ) => updateFilter ( f . id , { key : e . target . value } ) }
1297+ disabled = { ! selectedCollection || isFetchingSchema }
1298+ placeholder = "e.g. internal_info.internal_id"
1299+ className = "w-full text-sm p-2 pr-8 bg-white dark:bg-slate-700/50 border border-slate-300 dark:border-slate-600 rounded-lg text-slate-900 dark:text-slate-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors disabled:opacity-50"
1300+ autoFocus
1301+ />
1302+ < button
1303+ onClick = { ( ) => updateFilter ( f . id , { isCustom : false , key : 'all' } ) }
1304+ className = "absolute right-2 top-1/2 -translate-y-1/2 p-0.5 rounded-full text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors bg-slate-200 dark:bg-slate-600"
1305+ title = "Back to dropdown"
1306+ >
1307+ < XIcon className = "w-3 h-3" />
1308+ </ button >
1309+ </ div >
1310+ ) }
1311+ </ div >
1312+ { f . key !== 'all' && (
1313+ < div className = "flex-1 relative" >
1314+ < select
1315+ value = { f . operator || 'equals' }
1316+ onChange = { ( e ) => updateFilter ( f . id , { operator : e . target . value } ) }
1317+ disabled = { ! selectedCollection || isFetchingSchema }
1318+ className = "w-full text-sm appearance-none cursor-pointer p-2 pr-8 bg-white dark:bg-slate-700/50 border border-slate-300 dark:border-slate-600 rounded-lg text-slate-900 dark:text-slate-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors disabled:opacity-50"
1319+ >
1320+ < option value = "equals" > Equals</ option >
1321+ < option value = "not_equals" > Does Not Equal</ option >
1322+ < option value = "contains" > Contains</ option >
1323+ < option value = "greater_than" > Greater Than (>)</ option >
1324+ < option value = "less_than" > Less Than (<)</ option >
1325+ < option value = "exists" > Exists</ option >
1326+ < option value = "not_exists" > Does Not Exist</ option >
1327+ </ select >
1328+ < ChevronDownIcon className = "absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 dark:text-slate-500 pointer-events-none" />
1329+ </ div >
1330+ ) }
1331+ </ div >
1332+ { ( ! f . operator || ( f . operator !== 'exists' && f . operator !== 'not_exists' ) ) && (
1333+ < div className = "relative" >
1334+ < input
1335+ type = "text"
1336+ placeholder = { isFetchingSchema ? 'Loading schema...' : 'Filter value...' }
1337+ value = { f . value }
1338+ onChange = { ( e ) => updateFilter ( f . id , { value : e . target . value } ) }
1339+ disabled = { ! selectedCollection || isFetchingSchema }
1340+ className = "w-full pl-9 pr-4 py-2 bg-white dark:bg-slate-700/50 border border-slate-300 dark:border-slate-600 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors disabled:opacity-50"
1341+ />
1342+ < SearchIcon className = "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 dark:text-slate-500" />
1343+ </ div >
1344+ ) }
1345+ </ div >
1346+ ) ) }
1347+ < button
1348+ onClick = { addFilter }
1349+ disabled = { ! selectedCollection || isFetchingSchema }
1350+ className = "w-full flex justify-center items-center gap-1 p-2 text-sm font-medium text-slate-600 dark:text-slate-300 bg-slate-100 dark:bg-slate-700/50 hover:bg-slate-200 dark:hover:bg-slate-600 border border-slate-300 dark:border-slate-600 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
1351+ >
1352+ < span > + Add Filter</ span >
1353+ </ button >
12641354 </ div >
12651355 </ div >
12661356 < div className = "flex-grow overflow-hidden" >
0 commit comments