Client id for microsoft call
This commit is contained in:
parent
a1327d7d5d
commit
384afe856c
5 changed files with 718 additions and 201 deletions
|
|
@ -35,6 +35,7 @@ interface CloudConnector {
|
||||||
status: "not_connected" | "connecting" | "connected" | "error"
|
status: "not_connected" | "connecting" | "connected" | "error"
|
||||||
type: string
|
type: string
|
||||||
connectionId?: string
|
connectionId?: string
|
||||||
|
clientId: string
|
||||||
hasAccessToken: boolean
|
hasAccessToken: boolean
|
||||||
accessTokenError?: string
|
accessTokenError?: string
|
||||||
}
|
}
|
||||||
|
|
@ -114,6 +115,7 @@ export default function UploadProviderPage() {
|
||||||
status: isConnected ? "connected" : "not_connected",
|
status: isConnected ? "connected" : "not_connected",
|
||||||
type: provider,
|
type: provider,
|
||||||
connectionId: activeConnection?.connection_id,
|
connectionId: activeConnection?.connection_id,
|
||||||
|
clientId: activeConnection?.client_id,
|
||||||
hasAccessToken,
|
hasAccessToken,
|
||||||
accessTokenError
|
accessTokenError
|
||||||
})
|
})
|
||||||
|
|
@ -345,6 +347,7 @@ export default function UploadProviderPage() {
|
||||||
isAuthenticated={true}
|
isAuthenticated={true}
|
||||||
accessToken={accessToken || undefined}
|
accessToken={accessToken || undefined}
|
||||||
connectorType={connector.type as "onedrive" | "sharepoint"}
|
connectorType={connector.type as "onedrive" | "sharepoint"}
|
||||||
|
clientId={connector.clientId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ interface CloudConnector {
|
||||||
status: "not_connected" | "connecting" | "connected" | "error"
|
status: "not_connected" | "connecting" | "connected" | "error"
|
||||||
type: string
|
type: string
|
||||||
connectionId?: string
|
connectionId?: string
|
||||||
|
clientId: string
|
||||||
hasAccessToken: boolean
|
hasAccessToken: boolean
|
||||||
accessTokenError?: string
|
accessTokenError?: string
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +116,8 @@ export function CloudConnectorsDialog({
|
||||||
status: "not_connected" as const,
|
status: "not_connected" as const,
|
||||||
type: type,
|
type: type,
|
||||||
hasAccessToken: false,
|
hasAccessToken: false,
|
||||||
accessTokenError: undefined
|
accessTokenError: undefined,
|
||||||
|
clientId: ""
|
||||||
}))
|
}))
|
||||||
|
|
||||||
setConnectors(initialConnectors)
|
setConnectors(initialConnectors)
|
||||||
|
|
@ -161,6 +163,7 @@ export function CloudConnectorsDialog({
|
||||||
...c,
|
...c,
|
||||||
status: isConnected ? "connected" : "not_connected",
|
status: isConnected ? "connected" : "not_connected",
|
||||||
connectionId: activeConnection?.connection_id,
|
connectionId: activeConnection?.connection_id,
|
||||||
|
clientId: activeConnection?.client_id,
|
||||||
hasAccessToken,
|
hasAccessToken,
|
||||||
accessTokenError
|
accessTokenError
|
||||||
}
|
}
|
||||||
|
|
@ -280,6 +283,7 @@ export function CloudConnectorsDialog({
|
||||||
isAuthenticated={connector.status === "connected"}
|
isAuthenticated={connector.status === "connected"}
|
||||||
accessToken={connectorAccessTokens[connector.type]}
|
accessToken={connectorAccessTokens[connector.type]}
|
||||||
connectorType={connector.type as "onedrive" | "sharepoint"}
|
connectorType={connector.type as "onedrive" | "sharepoint"}
|
||||||
|
clientId={connector.clientId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,26 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect, useRef } from "react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Plus, Trash2, FileText } from "lucide-react"
|
||||||
import { FileText, Folder, Plus, Trash2, ArrowLeft } from "lucide-react"
|
|
||||||
|
|
||||||
interface OneDrivePickerProps {
|
interface OneDrivePickerProps {
|
||||||
onFileSelected: (files: OneDriveFile[]) => void
|
onFileSelected: (files: SelectedFile[]) => void
|
||||||
selectedFiles?: OneDriveFile[]
|
selectedFiles?: SelectedFile[]
|
||||||
isAuthenticated: boolean
|
isAuthenticated: boolean
|
||||||
accessToken?: string
|
accessToken?: string
|
||||||
connectorType?: "onedrive" | "sharepoint"
|
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
|
id: string
|
||||||
name: string
|
name: string
|
||||||
mimeType?: string
|
mimeType?: string
|
||||||
webUrl?: string
|
webUrl?: string
|
||||||
driveItem?: {
|
downloadUrl?: string
|
||||||
file?: { mimeType: string }
|
|
||||||
folder?: unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GraphResponse {
|
|
||||||
value: OneDriveFile[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OneDrivePicker({
|
export function OneDrivePicker({
|
||||||
|
|
@ -36,94 +29,209 @@ export function OneDrivePicker({
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
accessToken,
|
accessToken,
|
||||||
connectorType = "onedrive",
|
connectorType = "onedrive",
|
||||||
onPickerStateChange
|
baseUrl: providedBaseUrl,
|
||||||
|
clientId
|
||||||
}: OneDrivePickerProps) {
|
}: OneDrivePickerProps) {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
// Debug all props
|
||||||
const [files, setFiles] = useState<OneDriveFile[]>([])
|
console.log('All OneDrivePicker props:', {
|
||||||
|
onFileSelected: !!onFileSelected,
|
||||||
|
selectedFiles: selectedFiles?.length,
|
||||||
|
isAuthenticated,
|
||||||
|
accessToken: !!accessToken,
|
||||||
|
connectorType,
|
||||||
|
providedBaseUrl,
|
||||||
|
clientId
|
||||||
|
})
|
||||||
const [isPickerOpen, setIsPickerOpen] = useState(false)
|
const [isPickerOpen, setIsPickerOpen] = useState(false)
|
||||||
const [currentPath, setCurrentPath] = useState<string>(
|
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||||
connectorType === "sharepoint" ? 'sites?search=' : 'me/drive/root/children'
|
const [channelId] = useState(() => crypto.randomUUID())
|
||||||
)
|
|
||||||
const [breadcrumbs, setBreadcrumbs] = useState<{id: string, name: string}[]>([
|
|
||||||
{id: 'root', name: connectorType === "sharepoint" ? 'SharePoint' : 'OneDrive'}
|
|
||||||
])
|
|
||||||
|
|
||||||
const fetchFiles = async (path: string = currentPath) => {
|
const [autoBaseUrl, setAutoBaseUrl] = useState<string | null>(null)
|
||||||
if (!accessToken) return
|
const [isLoadingBaseUrl, setIsLoadingBaseUrl] = useState(false)
|
||||||
|
const baseUrl = providedBaseUrl || autoBaseUrl
|
||||||
setIsLoading(true)
|
|
||||||
try {
|
useEffect(() => {
|
||||||
const response = await fetch(`https://graph.microsoft.com/v1.0/${path}`, {
|
const handleMessage = (event: MessageEvent) => {
|
||||||
headers: {
|
// Only process messages from Microsoft domains
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
if (!event.origin.includes('.sharepoint.com') &&
|
||||||
'Content-Type': 'application/json'
|
!event.origin.includes('onedrive.live.com')) {
|
||||||
}
|
return
|
||||||
})
|
}
|
||||||
|
|
||||||
if (response.ok) {
|
const message = event.data
|
||||||
const data: GraphResponse = await response.json()
|
|
||||||
setFiles(data.value || [])
|
if (message.type === 'initialize') {
|
||||||
} else {
|
// Picker is ready
|
||||||
console.error('Failed to fetch OneDrive files:', response.statusText)
|
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 (
|
||||||
|
<div className="border rounded-lg shadow-sm bg-white">
|
||||||
|
<div className="flex flex-col items-center text-center p-6">
|
||||||
|
<p className="text-sm text-gray-600">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [popupRef, setPopupRef] = useState<Window | null>(null) // Add this state
|
||||||
|
|
||||||
const openPicker = () => {
|
const openPicker = () => {
|
||||||
if (!accessToken) return
|
if (!accessToken) {
|
||||||
|
console.error('Access token required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setIsPickerOpen(true)
|
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 = () => {
|
const closePicker = () => {
|
||||||
setIsPickerOpen(false)
|
setIsPickerOpen(false)
|
||||||
onPickerStateChange?.(false)
|
if (popupRef && !popupRef.closed) {
|
||||||
setFiles([])
|
popupRef.close()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
setPopupRef(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeFile = (fileId: string) => {
|
const removeFile = (fileId: string) => {
|
||||||
|
|
@ -131,145 +239,75 @@ export function OneDrivePicker({
|
||||||
onFileSelected(updatedFiles)
|
onFileSelected(updatedFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFileIcon = (file: OneDriveFile) => {
|
|
||||||
if (file.driveItem?.folder) {
|
|
||||||
return <Folder className="h-4 w-4 text-blue-600" />
|
|
||||||
}
|
|
||||||
return <FileText className="h-4 w-4 text-gray-600" />
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
const serviceName = connectorType === "sharepoint" ? "SharePoint" : "OneDrive"
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<div className="border rounded-lg shadow-sm bg-white">
|
||||||
<CardContent className="flex flex-col items-center text-center p-6">
|
<div className="flex flex-col items-center text-center p-6">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
Please connect to {serviceName} first to select specific files.
|
Please connect to {serviceName} first to select files.
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken || !baseUrl) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<div className="border rounded-lg shadow-sm bg-white">
|
||||||
<CardContent className="flex flex-col items-center text-center p-6">
|
<div className="flex flex-col items-center text-center p-6">
|
||||||
<p className="text-sm text-gray-600 mb-2">
|
<p className="text-sm text-gray-600 mb-2">
|
||||||
Access token unavailable
|
Configuration required
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-amber-600">
|
<p className="text-xs text-amber-600">
|
||||||
Try disconnecting and reconnecting your {serviceName} account.
|
{!accessToken && "Access token required. "}
|
||||||
|
{!baseUrl && "Base URL required."}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{!isPickerOpen ? (
|
{isPickerOpen ? (
|
||||||
<Card>
|
<div className="border rounded-lg shadow-sm bg-white">
|
||||||
<CardContent className="flex flex-col items-center text-center p-6">
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold">OneDrive Picker is open in popup</h3>
|
||||||
|
<Button onClick={closePicker} size="sm" variant="outline">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Please select your files in the popup window. This window will update when you're done.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="border rounded-lg shadow-sm bg-white">
|
||||||
|
<div className="flex flex-col items-center text-center p-6">
|
||||||
<p className="text-sm text-gray-600 mb-4">
|
<p className="text-sm text-gray-600 mb-4">
|
||||||
Select files from {serviceName} to ingest.
|
Select files from {serviceName} to ingest into OpenRAG.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={openPicker}
|
onClick={openPicker}
|
||||||
className="bg-black text-white hover:bg-gray-800"
|
className="bg-blue-600 text-white hover:bg-blue-700 border-0"
|
||||||
|
style={{ backgroundColor: '#2563eb', color: '#ffffff' }}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
Add Files
|
Add Files
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
) : (
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h3 className="text-lg font-semibold">Select Files from {serviceName}</h3>
|
|
||||||
<Button onClick={closePicker} size="sm" variant="outline">
|
|
||||||
Done
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Navigation */}
|
|
||||||
<div className="flex items-center space-x-2 mb-4">
|
|
||||||
{breadcrumbs.length > 1 && (
|
|
||||||
<Button
|
|
||||||
onClick={navigateBack}
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="p-1"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<div className="text-sm text-gray-600">
|
|
||||||
{breadcrumbs.map((crumb, index) => (
|
|
||||||
<span key={crumb.id}>
|
|
||||||
{index > 0 && <span className="mx-1">/</span>}
|
|
||||||
{crumb.name}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* File List */}
|
|
||||||
<div className="border rounded-md max-h-96 overflow-y-auto">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="p-4 text-center text-gray-600">Loading...</div>
|
|
||||||
) : files.length === 0 ? (
|
|
||||||
<div className="p-4 text-center text-gray-600">No files found</div>
|
|
||||||
) : (
|
|
||||||
<div className="divide-y">
|
|
||||||
{files.map((file) => (
|
|
||||||
<div
|
|
||||||
key={file.id}
|
|
||||||
className="flex items-center p-3 hover:bg-gray-50 cursor-pointer"
|
|
||||||
onClick={() => handleFileClick(file)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3 flex-1">
|
|
||||||
{getFileIcon(file)}
|
|
||||||
<span className="font-medium">{file.name}</span>
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
{getMimeTypeLabel(file)}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
{selectedFiles.some(f => f.id === file.id) && (
|
|
||||||
<Badge className="text-xs bg-green-100 text-green-800">Selected</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedFiles.length > 0 && (
|
{selectedFiles.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-xs text-gray-600">
|
<p className="text-xs text-gray-600">
|
||||||
Added files
|
Selected files ({selectedFiles.length})
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onFileSelected([])}
|
onClick={() => onFileSelected([])}
|
||||||
|
|
@ -287,11 +325,13 @@ export function OneDrivePicker({
|
||||||
className="flex items-center justify-between p-2 bg-gray-100 rounded-md text-xs"
|
className="flex items-center justify-between p-2 bg-gray-100 rounded-md text-xs"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
{getFileIcon(file)}
|
<FileText className="h-4 w-4 text-gray-600" />
|
||||||
<span className="truncate font-medium">{file.name}</span>
|
<span className="truncate font-medium">{file.name}</span>
|
||||||
<Badge variant="secondary" className="text-xs px-1 py-0.5 h-auto">
|
{file.mimeType && (
|
||||||
{getMimeTypeLabel(file)}
|
<Badge variant="secondary" className="text-xs px-1 py-0.5 h-auto">
|
||||||
</Badge>
|
{file.mimeType.split('/').pop() || 'File'}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => removeFile(file.id)}
|
onClick={() => removeFile(file.id)}
|
||||||
|
|
@ -308,4 +348,4 @@ export function OneDrivePicker({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
455
frontend/src/components/sharepoint-picker.tsx
Normal file
455
frontend/src/components/sharepoint-picker.tsx
Normal file
|
|
@ -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<SharePointFile[]>([])
|
||||||
|
const [sites, setSites] = useState<SharePointSite[]>([])
|
||||||
|
const [isPickerOpen, setIsPickerOpen] = useState(false)
|
||||||
|
const [currentView, setCurrentView] = useState<'sites' | 'drives' | 'files'>('sites')
|
||||||
|
const [currentSite, setCurrentSite] = useState<SharePointSite | null>(null)
|
||||||
|
const [currentDrive, setCurrentDrive] = useState<string | null>(null)
|
||||||
|
const [currentPath, setCurrentPath] = useState<string>('')
|
||||||
|
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 <Building2 className="h-4 w-4 text-blue-600" />
|
||||||
|
}
|
||||||
|
if (item.driveItem?.folder) {
|
||||||
|
return <Folder className="h-4 w-4 text-blue-600" />
|
||||||
|
}
|
||||||
|
return <FileText className="h-4 w-4 text-gray-600" />
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex flex-col items-center text-center p-6">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Please connect to SharePoint first to select specific files.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex flex-col items-center text-center p-6">
|
||||||
|
<p className="text-sm text-gray-600 mb-2">
|
||||||
|
Access token unavailable
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-amber-600">
|
||||||
|
Try disconnecting and reconnecting your SharePoint account.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{!isPickerOpen ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex flex-col items-center text-center p-6">
|
||||||
|
<p className="text-sm text-gray-600 mb-4">
|
||||||
|
Select files from SharePoint to ingest.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={openPicker}
|
||||||
|
className="bg-black text-white hover:bg-gray-800"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Add Files
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold">Select Files from SharePoint</h3>
|
||||||
|
<Button onClick={closePicker} size="sm" variant="outline">
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
|
{breadcrumbs.length > 1 && (
|
||||||
|
<Button
|
||||||
|
onClick={navigateBack}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="p-1"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
{breadcrumbs.map((crumb, index) => (
|
||||||
|
<span key={`${crumb.type}-${crumb.id}`}>
|
||||||
|
{index > 0 && <span className="mx-1">/</span>}
|
||||||
|
{crumb.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content List */}
|
||||||
|
<div className="border rounded-md max-h-96 overflow-y-auto">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="p-4 text-center text-gray-600">Loading...</div>
|
||||||
|
) : currentView === 'sites' ? (
|
||||||
|
sites.length === 0 ? (
|
||||||
|
<div className="p-4 text-center text-gray-600">No sites found</div>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y">
|
||||||
|
{sites.map((site) => (
|
||||||
|
<div
|
||||||
|
key={site.id}
|
||||||
|
className="flex items-center p-3 hover:bg-gray-50 cursor-pointer"
|
||||||
|
onClick={() => handleSiteClick(site)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 flex-1">
|
||||||
|
{getFileIcon(site)}
|
||||||
|
<span className="font-medium">{site.displayName}</span>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
Site
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-500">Click to open</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : files.length === 0 ? (
|
||||||
|
<div className="p-4 text-center text-gray-600">No files found</div>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y">
|
||||||
|
{files.map((file) => (
|
||||||
|
<div
|
||||||
|
key={file.id}
|
||||||
|
className="flex items-center p-3 hover:bg-gray-50 cursor-pointer"
|
||||||
|
onClick={() => currentView === 'drives' ? handleDriveClick(file) : handleFileClick(file)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 flex-1">
|
||||||
|
{getFileIcon(file)}
|
||||||
|
<span className="font-medium">{file.name}</span>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{currentView === 'drives' ? 'Library' : getMimeTypeLabel(file)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{currentView === 'files' && selectedFiles.some(f => f.id === file.id) ? (
|
||||||
|
<Badge className="text-xs bg-green-100 text-green-800 border-green-300">Selected</Badge>
|
||||||
|
) : file.driveItem?.folder || currentView === 'drives' ? (
|
||||||
|
<span className="text-xs text-gray-500">Click to open</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-gray-500">Click to select</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedFiles.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-xs text-gray-600">
|
||||||
|
Added files
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => onFileSelected([])}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="text-xs h-6"
|
||||||
|
>
|
||||||
|
Clear all
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-64 overflow-y-auto space-y-1">
|
||||||
|
{selectedFiles.map((file) => (
|
||||||
|
<div
|
||||||
|
key={file.id}
|
||||||
|
className="flex items-center justify-between p-2 bg-gray-100 rounded-md text-xs"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
|
{getFileIcon(file)}
|
||||||
|
<span className="truncate font-medium">{file.name}</span>
|
||||||
|
<Badge variant="secondary" className="text-xs px-1 py-0.5 h-auto">
|
||||||
|
{getMimeTypeLabel(file)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => removeFile(file.id)}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -127,6 +127,20 @@ async def connector_status(request: Request, connector_service, session_manager)
|
||||||
user_id=user.user_id, connector_type=connector_type
|
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
|
# Check if there are any active connections
|
||||||
active_connections = [conn for conn in connections if conn.is_active]
|
active_connections = [conn for conn in connections if conn.is_active]
|
||||||
has_authenticated_connection = len(active_connections) > 0
|
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,
|
"connection_id": conn.connection_id,
|
||||||
"name": conn.name,
|
"name": conn.name,
|
||||||
|
"client_id": connection_client_ids.get(conn.connection_id),
|
||||||
"is_active": conn.is_active,
|
"is_active": conn.is_active,
|
||||||
"created_at": conn.created_at.isoformat(),
|
"created_at": conn.created_at.isoformat(),
|
||||||
"last_sync": conn.last_sync.isoformat() if conn.last_sync else None,
|
"last_sync": conn.last_sync.isoformat() if conn.last_sync else None,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue