diff --git a/frontend/src/app/upload/[provider]/page.tsx b/frontend/src/app/upload/[provider]/page.tsx index 8d26de02..27e28cab 100644 --- a/frontend/src/app/upload/[provider]/page.tsx +++ b/frontend/src/app/upload/[provider]/page.tsx @@ -35,6 +35,7 @@ interface CloudConnector { status: "not_connected" | "connecting" | "connected" | "error" type: string connectionId?: string + clientId: string hasAccessToken: boolean accessTokenError?: string } @@ -114,6 +115,7 @@ export default function UploadProviderPage() { status: isConnected ? "connected" : "not_connected", type: provider, connectionId: activeConnection?.connection_id, + clientId: activeConnection?.client_id, hasAccessToken, accessTokenError }) @@ -345,6 +347,7 @@ export default function UploadProviderPage() { isAuthenticated={true} accessToken={accessToken || undefined} connectorType={connector.type as "onedrive" | "sharepoint"} + clientId={connector.clientId} /> )} diff --git a/frontend/src/components/cloud-connectors-dialog.tsx b/frontend/src/components/cloud-connectors-dialog.tsx index e3c64649..5ccbb58f 100644 --- a/frontend/src/components/cloud-connectors-dialog.tsx +++ b/frontend/src/components/cloud-connectors-dialog.tsx @@ -40,6 +40,7 @@ interface CloudConnector { status: "not_connected" | "connecting" | "connected" | "error" type: string connectionId?: string + clientId: string hasAccessToken: boolean accessTokenError?: string } @@ -115,7 +116,8 @@ export function CloudConnectorsDialog({ status: "not_connected" as const, type: type, hasAccessToken: false, - accessTokenError: undefined + accessTokenError: undefined, + clientId: "" })) setConnectors(initialConnectors) @@ -161,6 +163,7 @@ export function CloudConnectorsDialog({ ...c, status: isConnected ? "connected" : "not_connected", connectionId: activeConnection?.connection_id, + clientId: activeConnection?.client_id, hasAccessToken, accessTokenError } @@ -280,6 +283,7 @@ export function CloudConnectorsDialog({ isAuthenticated={connector.status === "connected"} accessToken={connectorAccessTokens[connector.type]} connectorType={connector.type as "onedrive" | "sharepoint"} + clientId={connector.clientId} /> ) diff --git a/frontend/src/components/onedrive-picker.tsx b/frontend/src/components/onedrive-picker.tsx index 20eeeac7..7b0743f1 100644 --- a/frontend/src/components/onedrive-picker.tsx +++ b/frontend/src/components/onedrive-picker.tsx @@ -1,33 +1,26 @@ "use client" -import { useState, useEffect } from "react" +import { useState, useEffect, useRef } from "react" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" -import { Card, CardContent } from "@/components/ui/card" -import { FileText, Folder, Plus, Trash2, ArrowLeft } from "lucide-react" +import { Plus, Trash2, FileText } from "lucide-react" interface OneDrivePickerProps { - onFileSelected: (files: OneDriveFile[]) => void - selectedFiles?: OneDriveFile[] + onFileSelected: (files: SelectedFile[]) => void + selectedFiles?: SelectedFile[] isAuthenticated: boolean accessToken?: string connectorType?: "onedrive" | "sharepoint" - onPickerStateChange?: (isOpen: boolean) => void + baseUrl?: string // e.g., "https://tenant.sharepoint.com/sites/sitename" or "https://tenant-my.sharepoint.com" + clientId: string } -interface OneDriveFile { +interface SelectedFile { id: string name: string mimeType?: string webUrl?: string - driveItem?: { - file?: { mimeType: string } - folder?: unknown - } -} - -interface GraphResponse { - value: OneDriveFile[] + downloadUrl?: string } export function OneDrivePicker({ @@ -36,94 +29,209 @@ export function OneDrivePicker({ isAuthenticated, accessToken, connectorType = "onedrive", - onPickerStateChange + baseUrl: providedBaseUrl, + clientId }: OneDrivePickerProps) { - const [isLoading, setIsLoading] = useState(false) - const [files, setFiles] = useState([]) + // Debug all props + console.log('All OneDrivePicker props:', { + onFileSelected: !!onFileSelected, + selectedFiles: selectedFiles?.length, + isAuthenticated, + accessToken: !!accessToken, + connectorType, + providedBaseUrl, + clientId + }) const [isPickerOpen, setIsPickerOpen] = useState(false) - const [currentPath, setCurrentPath] = useState( - connectorType === "sharepoint" ? 'sites?search=' : 'me/drive/root/children' - ) - const [breadcrumbs, setBreadcrumbs] = useState<{id: string, name: string}[]>([ - {id: 'root', name: connectorType === "sharepoint" ? 'SharePoint' : 'OneDrive'} - ]) + const iframeRef = useRef(null) + const [channelId] = useState(() => crypto.randomUUID()) - const fetchFiles = async (path: string = currentPath) => { - if (!accessToken) return - - setIsLoading(true) - try { - const response = await fetch(`https://graph.microsoft.com/v1.0/${path}`, { - headers: { - 'Authorization': `Bearer ${accessToken}`, - 'Content-Type': 'application/json' - } - }) - - if (response.ok) { - const data: GraphResponse = await response.json() - setFiles(data.value || []) - } else { - console.error('Failed to fetch OneDrive files:', response.statusText) + const [autoBaseUrl, setAutoBaseUrl] = useState(null) + const [isLoadingBaseUrl, setIsLoadingBaseUrl] = useState(false) + const baseUrl = providedBaseUrl || autoBaseUrl + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + // Only process messages from Microsoft domains + if (!event.origin.includes('.sharepoint.com') && + !event.origin.includes('onedrive.live.com')) { + return + } + + const message = event.data + + if (message.type === 'initialize') { + // Picker is ready + console.log('Picker initialized') + } else if (message.type === 'pick') { + // Files were selected + const files: SelectedFile[] = message.items?.map((item: any) => ({ + id: item.id, + name: item.name, + mimeType: item.mimeType, + webUrl: item.webUrl, + downloadUrl: item.downloadUrl + })) || [] + + onFileSelected([...selectedFiles, ...files]) + closePicker() + } else if (message.type === 'cancel') { + // User cancelled + closePicker() + } else if (message.type === 'authenticate') { + // Picker needs authentication token + if (accessToken && iframeRef.current?.contentWindow) { + iframeRef.current.contentWindow.postMessage({ + type: 'token', + token: accessToken + }, '*') + } } - } catch (error) { - console.error('Error fetching OneDrive files:', error) - } finally { - setIsLoading(false) } + + if (isPickerOpen) { + window.addEventListener('message', handleMessage) + return () => window.removeEventListener('message', handleMessage) + } + }, [isPickerOpen, accessToken, selectedFiles, onFileSelected]) + + useEffect(() => { + if (providedBaseUrl || !accessToken || autoBaseUrl) return + + const getBaseUrl = async () => { + setIsLoadingBaseUrl(true) + try { + // For personal accounts, use the picker URL directly + setAutoBaseUrl("https://onedrive.live.com/picker") + } catch (error) { + console.error('Auto-detect baseUrl failed:', error) + } finally { + setIsLoadingBaseUrl(false) + } + } + + getBaseUrl() + }, [accessToken, providedBaseUrl, autoBaseUrl]) + + useEffect(() => { + const handlePopupMessage = (event: MessageEvent) => { + // Only process messages from Microsoft domains + if (!event.origin.includes('onedrive.live.com') && + !event.origin.includes('.live.com')) { + return + } + + const message = event.data + console.log('Received message from popup:', message) // Debug log + + if (message.type === 'pick' && message.items) { + // Files were selected + const files: SelectedFile[] = message.items.map((item: any) => ({ + id: item.id, + name: item.name, + mimeType: item.mimeType, + webUrl: item.webUrl, + downloadUrl: item.downloadUrl || item['@microsoft.graph.downloadUrl'] + })) + + console.log('Selected files:', files) // Debug log + onFileSelected([...selectedFiles, ...files]) + setIsPickerOpen(false) + + // Close popup if it's still open + const popups = window.open('', 'OneDrivePicker') + if (popups && !popups.closed) { + popups.close() + } + } else if (message.type === 'cancel') { + // User cancelled + setIsPickerOpen(false) + } + } + + if (isPickerOpen) { + window.addEventListener('message', handlePopupMessage) + return () => window.removeEventListener('message', handlePopupMessage) + } + }, [isPickerOpen, selectedFiles, onFileSelected]) + + // Add this loading check before your existing checks: + if (isLoadingBaseUrl) { + return ( +
+
+

Loading...

+
+
+ ) } + const [popupRef, setPopupRef] = useState(null) // Add this state + const openPicker = () => { - if (!accessToken) return - + if (!accessToken) { + console.error('Access token required') + return + } + setIsPickerOpen(true) - onPickerStateChange?.(true) - fetchFiles() + + // Use OneDrive.js SDK approach instead of form POST + const script = document.createElement('script') + script.src = 'https://js.live.net/v7.2/OneDrive.js' + script.onload = () => { + // @ts-ignore + const OneDrive = window.OneDrive + + if (OneDrive) { + OneDrive.open({ + clientId: clientId, + action: 'query', + multiSelect: true, + advanced: { + endpointHint: 'api.onedrive.com', + accessToken: accessToken, + }, + success: (files: any) => { + console.log('Files selected:', files) + const selectedFiles: SelectedFile[] = files.value.map((file: any) => ({ + id: file.id, + name: file.name, + mimeType: file.file?.mimeType || 'application/octet-stream', + webUrl: file.webUrl, + downloadUrl: file['@microsoft.graph.downloadUrl'] + })) + + onFileSelected([...selectedFiles, ...selectedFiles]) + setIsPickerOpen(false) + }, + cancel: () => { + console.log('Picker cancelled') + setIsPickerOpen(false) + }, + error: (error: any) => { + console.error('Picker error:', error) + setIsPickerOpen(false) + } + }) + } + } + + script.onerror = () => { + console.error('Failed to load OneDrive SDK') + setIsPickerOpen(false) + } + + document.head.appendChild(script) } + // Update closePicker to close the popup const closePicker = () => { setIsPickerOpen(false) - onPickerStateChange?.(false) - setFiles([]) - setCurrentPath( - connectorType === "sharepoint" ? 'sites?search=' : 'me/drive/root/children' - ) - setBreadcrumbs([ - {id: 'root', name: connectorType === "sharepoint" ? 'SharePoint' : 'OneDrive'} - ]) - } - - const handleFileClick = (file: OneDriveFile) => { - if (file.driveItem?.folder) { - // Navigate to folder - const newPath = `me/drive/items/${file.id}/children` - setCurrentPath(newPath) - setBreadcrumbs([...breadcrumbs, {id: file.id, name: file.name}]) - fetchFiles(newPath) - } else { - // Select file - const isAlreadySelected = selectedFiles.some(f => f.id === file.id) - if (!isAlreadySelected) { - onFileSelected([...selectedFiles, file]) - } - } - } - - const navigateBack = () => { - if (breadcrumbs.length > 1) { - const newBreadcrumbs = breadcrumbs.slice(0, -1) - setBreadcrumbs(newBreadcrumbs) - - if (newBreadcrumbs.length === 1) { - setCurrentPath('me/drive/root/children') - fetchFiles('me/drive/root/children') - } else { - const parentCrumb = newBreadcrumbs[newBreadcrumbs.length - 1] - const newPath = `me/drive/items/${parentCrumb.id}/children` - setCurrentPath(newPath) - fetchFiles(newPath) - } + if (popupRef && !popupRef.closed) { + popupRef.close() } + setPopupRef(null) } const removeFile = (fileId: string) => { @@ -131,145 +239,75 @@ export function OneDrivePicker({ onFileSelected(updatedFiles) } - const getFileIcon = (file: OneDriveFile) => { - if (file.driveItem?.folder) { - return - } - return - } - - const getMimeTypeLabel = (file: OneDriveFile) => { - const mimeType = file.driveItem?.file?.mimeType || file.mimeType || '' - const typeMap: { [key: string]: string } = { - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Word Doc', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'Excel', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PowerPoint', - 'application/pdf': 'PDF', - 'text/plain': 'Text', - 'image/jpeg': 'Image', - 'image/png': 'Image', - } - - if (file.driveItem?.folder) return 'Folder' - return typeMap[mimeType] || 'Document' - } - const serviceName = connectorType === "sharepoint" ? "SharePoint" : "OneDrive" if (!isAuthenticated) { return ( - - +
+

- Please connect to {serviceName} first to select specific files. + Please connect to {serviceName} first to select files.

- - +
+
) } - if (!accessToken) { + if (!accessToken || !baseUrl) { return ( - - +
+

- Access token unavailable + Configuration required

- Try disconnecting and reconnecting your {serviceName} account. + {!accessToken && "Access token required. "} + {!baseUrl && "Base URL required."}

- - +
+
) } return (
- {!isPickerOpen ? ( - - + {isPickerOpen ? ( +
+
+
+

OneDrive Picker is open in popup

+ +
+

+ Please select your files in the popup window. This window will update when you're done. +

+
+
+ ) : ( +
+

- Select files from {serviceName} to ingest. + Select files from {serviceName} to ingest into OpenRAG.

- - - ) : ( - - -
-

Select Files from {serviceName}

- -
- - {/* Navigation */} -
- {breadcrumbs.length > 1 && ( - - )} -
- {breadcrumbs.map((crumb, index) => ( - - {index > 0 && /} - {crumb.name} - - ))} -
-
- - {/* File List */} -
- {isLoading ? ( -
Loading...
- ) : files.length === 0 ? ( -
No files found
- ) : ( -
- {files.map((file) => ( -
handleFileClick(file)} - > -
- {getFileIcon(file)} - {file.name} - - {getMimeTypeLabel(file)} - -
- {selectedFiles.some(f => f.id === file.id) && ( - Selected - )} -
- ))} -
- )} -
-
-
+
+
)} {selectedFiles.length > 0 && (

- Added files + Selected files ({selectedFiles.length})

) -} +} \ No newline at end of file diff --git a/frontend/src/components/sharepoint-picker.tsx b/frontend/src/components/sharepoint-picker.tsx new file mode 100644 index 00000000..0ca82646 --- /dev/null +++ b/frontend/src/components/sharepoint-picker.tsx @@ -0,0 +1,455 @@ +"use client" + +import { useState, useEffect } from "react" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Card, CardContent } from "@/components/ui/card" +import { FileText, Folder, Plus, Trash2, ArrowLeft, Building2 } from "lucide-react" + +interface SharePointPickerProps { + onFileSelected: (files: SharePointFile[]) => void + selectedFiles?: SharePointFile[] + isAuthenticated: boolean + accessToken?: string + onPickerStateChange?: (isOpen: boolean) => void +} + +interface SharePointFile { + id: string + name: string + mimeType?: string + webUrl?: string + driveItem?: { + file?: { mimeType: string } + folder?: unknown + } + parentReference?: { + siteId?: string + driveId?: string + } +} + +interface SharePointSite { + id: string + displayName: string + name: string + webUrl: string +} + +interface GraphResponse { + value: SharePointFile[] | SharePointSite[] +} + +export function SharePointPicker({ + onFileSelected, + selectedFiles = [], + isAuthenticated, + accessToken, + onPickerStateChange +}: SharePointPickerProps) { + const [isLoading, setIsLoading] = useState(false) + const [files, setFiles] = useState([]) + const [sites, setSites] = useState([]) + const [isPickerOpen, setIsPickerOpen] = useState(false) + const [currentView, setCurrentView] = useState<'sites' | 'drives' | 'files'>('sites') + const [currentSite, setCurrentSite] = useState(null) + const [currentDrive, setCurrentDrive] = useState(null) + const [currentPath, setCurrentPath] = useState('') + const [breadcrumbs, setBreadcrumbs] = useState<{id: string, name: string, type: 'site' | 'drive' | 'folder'}[]>([ + {id: 'root', name: 'SharePoint Sites', type: 'site'} + ]) + + const fetchSites = async () => { + if (!accessToken) return + + setIsLoading(true) + try { + const response = await fetch('https://graph.microsoft.com/v1.0/sites?search=*', { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + }) + + if (response.ok) { + const data: { value: SharePointSite[] } = await response.json() + setSites(data.value || []) + } else { + console.error('Failed to fetch SharePoint sites:', response.statusText) + } + } catch (error) { + console.error('Error fetching SharePoint sites:', error) + } finally { + setIsLoading(false) + } + } + + const fetchDrives = async (siteId: string) => { + if (!accessToken) return + + setIsLoading(true) + try { + const response = await fetch(`https://graph.microsoft.com/v1.0/sites/${siteId}/drives`, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + }) + + if (response.ok) { + const data: GraphResponse = await response.json() + // Convert drives to file-like objects for display + const driveFiles: SharePointFile[] = (data.value as any[]).map(drive => ({ + id: drive.id, + name: drive.name || 'Document Library', + driveItem: { folder: {} }, // Mark as folder + webUrl: drive.webUrl + })) + setFiles(driveFiles) + } else { + console.error('Failed to fetch drives:', response.statusText) + } + } catch (error) { + console.error('Error fetching drives:', error) + } finally { + setIsLoading(false) + } + } + + const fetchFiles = async (path: string) => { + if (!accessToken || !currentSite || !currentDrive) return + + setIsLoading(true) + try { + const url = path || `https://graph.microsoft.com/v1.0/sites/${currentSite.id}/drives/${currentDrive}/root/children` + const response = await fetch(url, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + }) + + if (response.ok) { + const data: GraphResponse = await response.json() + setFiles(data.value as SharePointFile[] || []) + } else { + console.error('Failed to fetch SharePoint files:', response.statusText) + } + } catch (error) { + console.error('Error fetching SharePoint files:', error) + } finally { + setIsLoading(false) + } + } + + const openPicker = () => { + if (!accessToken) return + + setIsPickerOpen(true) + onPickerStateChange?.(true) + setCurrentView('sites') + fetchSites() + } + + const closePicker = () => { + setIsPickerOpen(false) + onPickerStateChange?.(false) + setFiles([]) + setSites([]) + setCurrentView('sites') + setCurrentSite(null) + setCurrentDrive(null) + setCurrentPath('') + setBreadcrumbs([{id: 'root', name: 'SharePoint Sites', type: 'site'}]) + } + + const handleSiteClick = (site: SharePointSite) => { + setCurrentSite(site) + setCurrentView('drives') + setBreadcrumbs([ + {id: 'root', name: 'SharePoint Sites', type: 'site'}, + {id: site.id, name: site.displayName, type: 'site'} + ]) + fetchDrives(site.id) + } + + const handleDriveClick = (drive: SharePointFile) => { + setCurrentDrive(drive.id) + setCurrentView('files') + setBreadcrumbs([ + {id: 'root', name: 'SharePoint Sites', type: 'site'}, + {id: currentSite!.id, name: currentSite!.displayName, type: 'site'}, + {id: drive.id, name: drive.name, type: 'drive'} + ]) + fetchFiles('') + } + + const handleFileClick = (file: SharePointFile) => { + if (file.driveItem?.folder) { + // Navigate to folder + const newPath = `https://graph.microsoft.com/v1.0/sites/${currentSite!.id}/drives/${currentDrive}/items/${file.id}/children` + setCurrentPath(newPath) + setBreadcrumbs([...breadcrumbs, {id: file.id, name: file.name, type: 'folder'}]) + fetchFiles(newPath) + } else { + // Select file - allow multiple selections + const isAlreadySelected = selectedFiles.some(f => f.id === file.id) + if (isAlreadySelected) { + // Deselect if already selected + const updatedFiles = selectedFiles.filter(f => f.id !== file.id) + onFileSelected(updatedFiles) + } else { + // Add to selection + onFileSelected([...selectedFiles, file]) + } + } + } + + const navigateBack = () => { + if (breadcrumbs.length > 1) { + const newBreadcrumbs = breadcrumbs.slice(0, -1) + setBreadcrumbs(newBreadcrumbs) + const lastCrumb = newBreadcrumbs[newBreadcrumbs.length - 1] + + if (lastCrumb.type === 'site' && lastCrumb.id === 'root') { + // Back to sites + setCurrentView('sites') + setCurrentSite(null) + setCurrentDrive(null) + fetchSites() + } else if (lastCrumb.type === 'site') { + // Back to drives + setCurrentView('drives') + setCurrentDrive(null) + fetchDrives(lastCrumb.id) + } else if (lastCrumb.type === 'drive') { + // Back to root of drive + setCurrentView('files') + setCurrentPath('') + fetchFiles('') + } else { + // Back to parent folder + const parentCrumb = newBreadcrumbs[newBreadcrumbs.length - 2] + if (parentCrumb.type === 'drive') { + setCurrentPath('') + fetchFiles('') + } else { + const newPath = `https://graph.microsoft.com/v1.0/sites/${currentSite!.id}/drives/${currentDrive}/items/${lastCrumb.id}/children` + setCurrentPath(newPath) + fetchFiles(newPath) + } + } + } + } + + const removeFile = (fileId: string) => { + const updatedFiles = selectedFiles.filter(file => file.id !== fileId) + onFileSelected(updatedFiles) + } + + const getFileIcon = (item: SharePointFile | SharePointSite) => { + if ('displayName' in item) { + return + } + if (item.driveItem?.folder) { + return + } + return + } + + const getMimeTypeLabel = (file: SharePointFile) => { + const mimeType = file.driveItem?.file?.mimeType || file.mimeType || '' + const typeMap: { [key: string]: string } = { + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Word Doc', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'Excel', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PowerPoint', + 'application/pdf': 'PDF', + 'text/plain': 'Text', + 'image/jpeg': 'Image', + 'image/png': 'Image', + } + + if (file.driveItem?.folder) return 'Folder' + return typeMap[mimeType] || 'Document' + } + + if (!isAuthenticated) { + return ( + + +

+ Please connect to SharePoint first to select specific files. +

+
+
+ ) + } + + if (!accessToken) { + return ( + + +

+ Access token unavailable +

+

+ Try disconnecting and reconnecting your SharePoint account. +

+
+
+ ) + } + + return ( +
+ {!isPickerOpen ? ( + + +

+ Select files from SharePoint to ingest. +

+ +
+
+ ) : ( + + +
+

Select Files from SharePoint

+ +
+ + {/* Navigation */} +
+ {breadcrumbs.length > 1 && ( + + )} +
+ {breadcrumbs.map((crumb, index) => ( + + {index > 0 && /} + {crumb.name} + + ))} +
+
+ + {/* Content List */} +
+ {isLoading ? ( +
Loading...
+ ) : currentView === 'sites' ? ( + sites.length === 0 ? ( +
No sites found
+ ) : ( +
+ {sites.map((site) => ( +
handleSiteClick(site)} + > +
+ {getFileIcon(site)} + {site.displayName} + + Site + +
+ Click to open +
+ ))} +
+ ) + ) : files.length === 0 ? ( +
No files found
+ ) : ( +
+ {files.map((file) => ( +
currentView === 'drives' ? handleDriveClick(file) : handleFileClick(file)} + > +
+ {getFileIcon(file)} + {file.name} + + {currentView === 'drives' ? 'Library' : getMimeTypeLabel(file)} + +
+ {currentView === 'files' && selectedFiles.some(f => f.id === file.id) ? ( + Selected + ) : file.driveItem?.folder || currentView === 'drives' ? ( + Click to open + ) : ( + Click to select + )} +
+ ))} +
+ )} +
+
+
+ )} + + {selectedFiles.length > 0 && ( +
+
+

+ Added files +

+ +
+
+ {selectedFiles.map((file) => ( +
+
+ {getFileIcon(file)} + {file.name} + + {getMimeTypeLabel(file)} + +
+ +
+ ))} +
+
+ )} +
+ ) +} diff --git a/src/api/connectors.py b/src/api/connectors.py index 2696ca08..25ddd6bd 100644 --- a/src/api/connectors.py +++ b/src/api/connectors.py @@ -127,6 +127,20 @@ async def connector_status(request: Request, connector_service, session_manager) user_id=user.user_id, connector_type=connector_type ) + # Get the connector for each connection + connection_client_ids = {} + for connection in connections: + try: + connector = await connector_service._get_connector(connection.connection_id) + connection_client_ids[connection.connection_id] = connector.get_client_id() + except Exception as e: + logger.warning( + "Could not get connector for connection", + connection_id=connection.connection_id, + error=str(e), + ) + connection.connector = None + # Check if there are any active connections active_connections = [conn for conn in connections if conn.is_active] has_authenticated_connection = len(active_connections) > 0 @@ -140,6 +154,7 @@ async def connector_status(request: Request, connector_service, session_manager) { "connection_id": conn.connection_id, "name": conn.name, + "client_id": connection_client_ids.get(conn.connection_id), "is_active": conn.is_active, "created_at": conn.created_at.isoformat(), "last_sync": conn.last_sync.isoformat() if conn.last_sync else None,