From 2868e47cf8cdf92d4a3e783fc3bbcdee144a0d9a Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 29 Aug 2025 18:05:58 +0530 Subject: [PATCH] (feat:connector) provider metadata, separate fe nested display --- application/api/user/routes.py | 6 +- application/worker.py | 7 +- frontend/src/components/ConnectorTree.tsx | 526 ++++++++++++++++++++++ 3 files changed, 535 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/ConnectorTree.tsx diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 0bf6aa2f..15024545 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -1022,7 +1022,8 @@ class PaginatedSources(Resource): "tokens": doc.get("tokens", ""), "retriever": doc.get("retriever", "classic"), "syncFrequency": doc.get("sync_frequency", ""), - "isNested": bool(doc.get("directory_structure")) + "isNested": bool(doc.get("directory_structure")), + "type": doc.get("type", "file") } paginated_docs.append(doc_data) response = { @@ -1070,7 +1071,8 @@ class CombinedJson(Resource): "tokens": index.get("tokens", ""), "retriever": index.get("retriever", "classic"), "syncFrequency": index.get("sync_frequency", ""), - "is_nested": bool(index.get("directory_structure")) + "is_nested": bool(index.get("directory_structure")), + "type": index.get("type", "file") # Add type field with default "file" } ) except Exception as err: diff --git a/application/worker.py b/application/worker.py index 719ebccc..e231474c 100755 --- a/application/worker.py +++ b/application/worker.py @@ -981,8 +981,11 @@ def ingest_connector( "tokens": tokens, "retriever": retriever, "id": str(id), - "type": source_type, - "remote_data": json.dumps(api_source_config), + "type": "connector", + "remote_data": json.dumps({ + "provider": source_type, + **api_source_config + }), "directory_structure": json.dumps(directory_structure) } diff --git a/frontend/src/components/ConnectorTree.tsx b/frontend/src/components/ConnectorTree.tsx new file mode 100644 index 00000000..cee07aa4 --- /dev/null +++ b/frontend/src/components/ConnectorTree.tsx @@ -0,0 +1,526 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import { selectToken } from '../preferences/preferenceSlice'; +import Chunks from './Chunks'; +import ContextMenu, { MenuOption } from './ContextMenu'; +import userService from '../api/services/userService'; +import FileIcon from '../assets/file.svg'; +import FolderIcon from '../assets/folder.svg'; +import ArrowLeft from '../assets/arrow-left.svg'; +import ThreeDots from '../assets/three-dots.svg'; +import EyeView from '../assets/eye-view.svg'; +import { useOutsideAlerter } from '../hooks'; + +interface ConnectorFileNode { + id: string; + name: string; + type: string; + size: string; + modifiedTime: string; + token_count?: number; + mimeType?: string; + isFolder?: boolean; +} + +interface ConnectorDirectoryStructure { + [key: string]: ConnectorFileNode; +} + +interface ConnectorTreeProps { + docId: string; + sourceName: string; + onBackToDocuments: () => void; +} + +interface SearchResult { + name: string; + path: string; + isFile: boolean; + id: string; +} + +const ConnectorTree: React.FC = ({ + docId, + sourceName, + onBackToDocuments, +}) => { + const { t } = useTranslation(); + const [directoryStructure, setDirectoryStructure] = useState(null); + const [currentPath, setCurrentPath] = useState([]); + const token = useSelector(selectToken); + const [selectedFile, setSelectedFile] = useState<{ id: string; name: string } | null>(null); + const [activeMenuId, setActiveMenuId] = useState(null); + const menuRefs = useRef<{ [key: string]: React.RefObject }>({}); + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const searchDropdownRef = useRef(null); + + useOutsideAlerter( + searchDropdownRef, + () => { + setSearchQuery(''); + setSearchResults([]); + }, + [], + false, + ); + + + + useEffect(() => { + const fetchDirectoryStructure = async () => { + try { + const response = await userService.getDirectoryStructure(docId, token); + const data = await response.json(); + + if (data && data.directory_structure) { + const structure: ConnectorDirectoryStructure = {}; + // Convert the directory structure to our format + Object.entries(data.directory_structure).forEach(([key, value]: [string, any]) => { + structure[key] = { + id: key, + name: key, + type: value.type || 'file', + size: value.size_bytes ? `${value.size_bytes} bytes` : '-', + modifiedTime: '-', + token_count: value.token_count, + isFolder: !value.type, + }; + }); + setDirectoryStructure(structure); + + // Update search results when directory structure changes + if (searchQuery && structure) { + setSearchResults(searchFiles(searchQuery, structure)); + } + } else { + // Handle invalid response format + console.log('Invalid response format'); + } + } catch (err) { + console.error('Failed to load directory structure', err); + } + }; + + if (docId) { + fetchDirectoryStructure(); + } + }, [docId, token, searchQuery]); + + const handleFileClick = (fileId: string, fileName: string) => { + setSelectedFile({ id: fileId, name: fileName }); + }; + + const navigateToDirectory = (_folderId: string, folderName: string) => { + setCurrentPath(prev => [...prev, folderName]); + }; + + const navigateUp = () => { + if (currentPath.length > 0) { + setCurrentPath(prev => prev.slice(0, -1)); + } + }; + + const getCurrentDirectory = (): ConnectorDirectoryStructure => { + return directoryStructure || {}; + }; + + const searchFiles = ( + query: string, + structure: ConnectorDirectoryStructure, + currentPath: string[] = [], + ): SearchResult[] => { + let results: SearchResult[] = []; + + Object.entries(structure).forEach(([name, node]) => { + const fullPath = [...currentPath, name].join('/'); + + if (name.toLowerCase().includes(query.toLowerCase())) { + results.push({ + name, + path: fullPath, + isFile: !!node.type, + id: node.id, + }); + } + + if (!node.type) { + // If it's a directory, search recursively + results = [ + ...results, + ...searchFiles(query, node as unknown as ConnectorDirectoryStructure, [ + ...currentPath, + name, + ]), + ]; + } + }); + + return results; + }; + + const handleSearchSelect = (result: SearchResult) => { + if (result.isFile) { + const pathParts = result.path.split('/'); + const fileName = pathParts.pop() || ''; + setCurrentPath(pathParts); + + setSelectedFile({ + id: result.id, + name: fileName, + }); + } else { + setCurrentPath(result.path.split('/')); + setSelectedFile(null); + } + setSearchQuery(''); + setSearchResults([]); + }; + + const handleBackNavigation = () => { + if (selectedFile) { + setSelectedFile(null); + } else if (currentPath.length === 0) { + if (onBackToDocuments) { + onBackToDocuments(); + } + } else { + navigateUp(); + } + }; + + const getMenuRef = (itemId: string) => { + if (!menuRefs.current[itemId]) { + menuRefs.current[itemId] = React.createRef(); + } + return menuRefs.current[itemId]; + }; + + const handleMenuClick = (e: React.MouseEvent, itemId: string) => { + e.preventDefault(); + e.stopPropagation(); + + if (activeMenuId === itemId) { + setActiveMenuId(null); + return; + } + setActiveMenuId(itemId); + }; + + const getActionOptions = ( + name: string, + id: string, + isFile: boolean, + _itemId: string, + ): MenuOption[] => { + const options: MenuOption[] = []; + + options.push({ + icon: EyeView, + label: t('settings.sources.view'), + onClick: (event: React.SyntheticEvent) => { + event.stopPropagation(); + if (isFile) { + handleFileClick(id, name); + } else { + navigateToDirectory(id, name); + } + }, + iconWidth: 18, + iconHeight: 18, + variant: 'primary', + }); + + // Remove delete option for connector files since they're not on our servers + // Connector files will be managed through the main Google Drive integration + + return options; + }; + + + + const currentDirectory = getCurrentDirectory(); + + const renderFileSearch = () => { + return ( +
+ { + setSearchQuery(e.target.value); + if (directoryStructure) { + setSearchResults(searchFiles(e.target.value, directoryStructure)); + } + }} + placeholder={t('settings.sources.searchFiles')} + className={`w-full h-[38px] border border-[#D1D9E0] px-4 py-2 dark:border-[#6A6A6A] + ${searchQuery ? 'rounded-t-[24px]' : 'rounded-[24px]'} + bg-transparent focus:outline-none dark:text-[#E0E0E0]`} + /> + + {searchQuery && ( +
+
+ {searchResults.length === 0 ? ( +
+ {t('settings.sources.noResults')} +
+ ) : ( + searchResults.map((result, index) => ( +
handleSearchSelect(result)} + title={result.path} + className={`flex min-w-0 cursor-pointer items-center px-3 py-2 hover:bg-[#ECEEEF] dark:hover:bg-[#27282D] ${index !== searchResults.length - 1 + ? 'border-b border-[#D1D9E0] dark:border-[#6A6A6A]' + : '' + }`} + > + { + + {result.path.split('/').pop() || result.path} + +
+ )) + )} +
+
+ )} +
+ ); + }; + + const renderConnectorFileTree = (structure: ConnectorDirectoryStructure): React.ReactNode[] => { + const entries = Object.entries(structure); + const directories = entries.filter(([_, node]) => node.isFolder); + const files = entries.filter(([_, node]) => !node.isFolder); + + return [ + ...directories.map(([name, node]) => { + const itemId = `dir-${node.id}`; + const menuRef = getMenuRef(itemId); + + return ( + navigateToDirectory(node.id, name)} + > + +
+ {t('settings.sources.folderAlt')} + + {name} + +
+ + + - + + + {node.modifiedTime || '-'} + + +
+ + + setActiveMenuId(isOpen ? itemId : null) + } + options={getActionOptions(name, node.id, false, itemId)} + anchorRef={menuRef} + position="bottom-left" + offset={{ x: -4, y: 4 }} + /> +
+ + + ); + }), + ...files.map(([name, node]) => { + const itemId = `file-${node.id}`; + const menuRef = getMenuRef(itemId); + + return ( + handleFileClick(node.id, name)} + > + +
+ {t('settings.sources.fileAlt')} + + {name} + +
+ + + {node.token_count?.toLocaleString() || '-'} + + + {node.size || '-'} + + +
+ + + setActiveMenuId(isOpen ? itemId : null) + } + options={getActionOptions(name, node.id, true, itemId)} + anchorRef={menuRef} + position="bottom-left" + offset={{ x: -4, y: 4 }} + /> +
+ + + ); + }), + ]; + }; + + const renderPathNavigation = () => { + return ( +
+ {/* Left side with path navigation */} +
+ + +
+ + {sourceName} + + {currentPath.length > 0 && ( + <> + / + {currentPath.map((dir, index) => ( + + + {dir} + + {index < currentPath.length - 1 && ( + + / + + )} + + ))} + + )} + {selectedFile && ( + <> + / + + {selectedFile.name} + + + )} +
+
+ + {/* Right side with search */} +
+ {renderFileSearch()} +
+
+ ); + }; + + return ( +
+ {selectedFile ? ( +
+
+ setSelectedFile(null)} + path={selectedFile.id} + /> +
+
+ ) : ( +
+
{renderPathNavigation()}
+ +
+
+ + + + + + + + + + + {renderConnectorFileTree(currentDirectory)} + +
+ {t('settings.sources.fileName')} + + {t('settings.sources.tokens')} + + {t('settings.sources.size')} + + + {t('settings.sources.actions')} + +
+
+
+
+ )} +
+ ); +}; + +export default ConnectorTree;