move to stand alone page
This commit is contained in:
parent
4573b056f6
commit
0ed98cb6e1
9 changed files with 1201 additions and 100 deletions
|
|
@ -1,7 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect, useRef } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ChevronDown, Upload, FolderOpen, Cloud, PlugZap, Plus } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
|
|
@ -9,6 +8,7 @@ import { Input } from "@/components/ui/input"
|
|||
import { Label } from "@/components/ui/label"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useTask } from "@/contexts/task-context"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
interface KnowledgeDropdownProps {
|
||||
active?: boolean
|
||||
|
|
@ -16,8 +16,8 @@ interface KnowledgeDropdownProps {
|
|||
}
|
||||
|
||||
export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeDropdownProps) {
|
||||
const router = useRouter()
|
||||
const { addTask } = useTask()
|
||||
const router = useRouter()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [showFolderDialog, setShowFolderDialog] = useState(false)
|
||||
const [showS3Dialog, setShowS3Dialog] = useState(false)
|
||||
|
|
@ -27,23 +27,76 @@ export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeD
|
|||
const [folderLoading, setFolderLoading] = useState(false)
|
||||
const [s3Loading, setS3Loading] = useState(false)
|
||||
const [fileUploading, setFileUploading] = useState(false)
|
||||
const [cloudConnectors, setCloudConnectors] = useState<{[key: string]: {name: string, available: boolean, connected: boolean, hasToken: boolean}}>({})
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Check AWS availability on mount
|
||||
// Check AWS availability and cloud connectors on mount
|
||||
useEffect(() => {
|
||||
const checkAws = async () => {
|
||||
const checkAvailability = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/upload_options")
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setAwsEnabled(Boolean(data.aws))
|
||||
// Check AWS
|
||||
const awsRes = await fetch("/api/upload_options")
|
||||
if (awsRes.ok) {
|
||||
const awsData = await awsRes.json()
|
||||
setAwsEnabled(Boolean(awsData.aws))
|
||||
}
|
||||
|
||||
// Check cloud connectors
|
||||
const connectorsRes = await fetch('/api/connectors')
|
||||
if (connectorsRes.ok) {
|
||||
const connectorsResult = await connectorsRes.json()
|
||||
const cloudConnectorTypes = ['google_drive', 'onedrive', 'sharepoint']
|
||||
const connectorInfo: {[key: string]: {name: string, available: boolean, connected: boolean, hasToken: boolean}} = {}
|
||||
|
||||
for (const type of cloudConnectorTypes) {
|
||||
if (connectorsResult.connectors[type]) {
|
||||
connectorInfo[type] = {
|
||||
name: connectorsResult.connectors[type].name,
|
||||
available: connectorsResult.connectors[type].available,
|
||||
connected: false,
|
||||
hasToken: false
|
||||
}
|
||||
|
||||
// Check connection status
|
||||
try {
|
||||
const statusRes = await fetch(`/api/connectors/${type}/status`)
|
||||
if (statusRes.ok) {
|
||||
const statusData = await statusRes.json()
|
||||
const connections = statusData.connections || []
|
||||
const activeConnection = connections.find((conn: {is_active: boolean, connection_id: string}) => conn.is_active)
|
||||
const isConnected = activeConnection !== undefined
|
||||
|
||||
if (isConnected && activeConnection) {
|
||||
connectorInfo[type].connected = true
|
||||
|
||||
// Check token availability
|
||||
try {
|
||||
const tokenRes = await fetch(`/api/connectors/${type}/token?connection_id=${activeConnection.connection_id}`)
|
||||
if (tokenRes.ok) {
|
||||
const tokenData = await tokenRes.json()
|
||||
if (tokenData.access_token) {
|
||||
connectorInfo[type].hasToken = true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Token check failed
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Status check failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setCloudConnectors(connectorInfo)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to check AWS availability", err)
|
||||
console.error("Failed to check availability", err)
|
||||
}
|
||||
}
|
||||
checkAws()
|
||||
checkAvailability()
|
||||
}, [])
|
||||
|
||||
// Handle click outside to close dropdown
|
||||
|
|
@ -194,6 +247,25 @@ export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeD
|
|||
}
|
||||
}
|
||||
|
||||
const cloudConnectorItems = Object.entries(cloudConnectors)
|
||||
.filter(([, info]) => info.available)
|
||||
.map(([type, info]) => ({
|
||||
label: info.name,
|
||||
icon: PlugZap,
|
||||
onClick: () => {
|
||||
setIsOpen(false)
|
||||
if (info.connected && info.hasToken) {
|
||||
router.push(`/upload/${type}`)
|
||||
} else {
|
||||
router.push('/settings')
|
||||
}
|
||||
},
|
||||
disabled: !info.connected || !info.hasToken,
|
||||
tooltip: !info.connected ? `Connect ${info.name} in Settings first` :
|
||||
!info.hasToken ? `Reconnect ${info.name} - access token required` :
|
||||
undefined
|
||||
}))
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: "Add File",
|
||||
|
|
@ -216,14 +288,7 @@ export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeD
|
|||
setShowS3Dialog(true)
|
||||
}
|
||||
}] : []),
|
||||
{
|
||||
label: "Cloud Connectors",
|
||||
icon: PlugZap,
|
||||
onClick: () => {
|
||||
setIsOpen(false)
|
||||
router.push("/settings")
|
||||
}
|
||||
}
|
||||
...cloudConnectorItems
|
||||
]
|
||||
|
||||
return (
|
||||
|
|
@ -265,7 +330,12 @@ export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeD
|
|||
<button
|
||||
key={index}
|
||||
onClick={item.onClick}
|
||||
className="w-full px-3 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground"
|
||||
disabled={'disabled' in item ? item.disabled : false}
|
||||
title={'tooltip' in item ? item.tooltip : undefined}
|
||||
className={cn(
|
||||
"w-full px-3 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground",
|
||||
'disabled' in item && item.disabled && "opacity-50 cursor-not-allowed hover:bg-transparent hover:text-current"
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
|
|
@ -364,6 +434,7 @@ export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeD
|
|||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,14 +1,60 @@
|
|||
"use client"
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { GoogleDrivePicker, type DriveSelection } from "./GoogleDrivePicker"
|
||||
import { GoogleDrivePicker } from "@/components/google-drive-picker"
|
||||
|
||||
const [driveSelection, setDriveSelection] = useState<DriveSelection>({ files: [], folders: [] });
|
||||
interface GoogleDriveFile {
|
||||
id: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
webViewLink?: string;
|
||||
iconLink?: string;
|
||||
}
|
||||
|
||||
// in JSX
|
||||
<GoogleDrivePicker value={driveSelection} onChange={setDriveSelection} />
|
||||
export default function ConnectorsPage() {
|
||||
const [selectedFiles, setSelectedFiles] = useState<GoogleDriveFile[]>([]);
|
||||
|
||||
// when calling sync:
|
||||
const body: { file_ids: string[]; folder_ids: string[]; recursive: boolean } = {
|
||||
file_ids: driveSelection.files,
|
||||
folder_ids: driveSelection.folders,
|
||||
recursive: true,
|
||||
};
|
||||
const handleFileSelection = (files: GoogleDriveFile[]) => {
|
||||
setSelectedFiles(files);
|
||||
};
|
||||
|
||||
const handleSync = () => {
|
||||
const fileIds = selectedFiles.map(file => file.id);
|
||||
const body = {
|
||||
file_ids: fileIds,
|
||||
folder_ids: [], // Add folder handling if needed
|
||||
recursive: true,
|
||||
};
|
||||
|
||||
console.log('Syncing with:', body);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">Connectors</h1>
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
This is a demo page for the Google Drive picker component.
|
||||
For full connector functionality, visit the Settings page.
|
||||
</p>
|
||||
|
||||
<GoogleDrivePicker
|
||||
onFileSelected={handleFileSelection}
|
||||
selectedFiles={selectedFiles}
|
||||
isAuthenticated={false} // This would come from auth context in real usage
|
||||
accessToken={undefined} // This would come from connected account
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedFiles.length > 0 && (
|
||||
<button
|
||||
onClick={handleSync}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
Sync {selectedFiles.length} Selected Items
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import { Loader2, PlugZap, RefreshCw } from "lucide-react"
|
|||
import { ProtectedRoute } from "@/components/protected-route"
|
||||
import { useTask } from "@/contexts/task-context"
|
||||
import { useAuth } from "@/contexts/auth-context"
|
||||
import { GoogleDrivePicker } from "@/components/google-drive-picker"
|
||||
|
||||
|
||||
interface GoogleDriveFile {
|
||||
|
|
@ -23,6 +22,17 @@ interface GoogleDriveFile {
|
|||
iconLink?: string
|
||||
}
|
||||
|
||||
interface OneDriveFile {
|
||||
id: string
|
||||
name: string
|
||||
mimeType?: string
|
||||
webUrl?: string
|
||||
driveItem?: {
|
||||
file?: { mimeType: string }
|
||||
folder?: any
|
||||
}
|
||||
}
|
||||
|
||||
interface Connector {
|
||||
id: string
|
||||
name: string
|
||||
|
|
@ -32,7 +42,7 @@ interface Connector {
|
|||
type: string
|
||||
connectionId?: string
|
||||
access_token?: string
|
||||
selectedFiles?: GoogleDriveFile[]
|
||||
selectedFiles?: GoogleDriveFile[] | OneDriveFile[]
|
||||
}
|
||||
|
||||
interface SyncResult {
|
||||
|
|
@ -63,8 +73,6 @@ function KnowledgeSourcesPage() {
|
|||
const [syncResults, setSyncResults] = useState<{[key: string]: SyncResult | null}>({})
|
||||
const [maxFiles, setMaxFiles] = useState<number>(10)
|
||||
const [syncAllFiles, setSyncAllFiles] = useState<boolean>(false)
|
||||
const [selectedFiles, setSelectedFiles] = useState<{[connectorId: string]: GoogleDriveFile[]}>({})
|
||||
const [connectorAccessTokens, setConnectorAccessTokens] = useState<{[connectorId: string]: string}>({})
|
||||
|
||||
// Settings state
|
||||
// Note: backend internal Langflow URL is not needed on the frontend
|
||||
|
|
@ -155,23 +163,6 @@ function KnowledgeSourcesPage() {
|
|||
const activeConnection = connections.find((conn: Connection) => conn.is_active)
|
||||
const isConnected = activeConnection !== undefined
|
||||
|
||||
// For Google Drive, try to get access token for the picker
|
||||
if (connectorType === 'google_drive' && activeConnection) {
|
||||
try {
|
||||
const tokenResponse = await fetch(`/api/connectors/${connectorType}/token?connection_id=${activeConnection.connection_id}`)
|
||||
if (tokenResponse.ok) {
|
||||
const tokenData = await tokenResponse.json()
|
||||
if (tokenData.access_token) {
|
||||
setConnectorAccessTokens(prev => ({
|
||||
...prev,
|
||||
[connectorType]: tokenData.access_token
|
||||
}))
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Could not fetch access token for Google Drive picker:', e)
|
||||
}
|
||||
}
|
||||
|
||||
setConnectors(prev => prev.map(c =>
|
||||
c.type === connectorType
|
||||
|
|
@ -238,19 +229,6 @@ function KnowledgeSourcesPage() {
|
|||
}
|
||||
}
|
||||
|
||||
const handleFileSelection = (connectorId: string, files: GoogleDriveFile[]) => {
|
||||
setSelectedFiles(prev => ({
|
||||
...prev,
|
||||
[connectorId]: files
|
||||
}))
|
||||
|
||||
// Update the connector with selected files
|
||||
setConnectors(prev => prev.map(c =>
|
||||
c.id === connectorId
|
||||
? { ...c, selectedFiles: files }
|
||||
: c
|
||||
))
|
||||
}
|
||||
|
||||
const handleSync = async (connector: Connector) => {
|
||||
if (!connector.connectionId) return
|
||||
|
|
@ -268,10 +246,7 @@ function KnowledgeSourcesPage() {
|
|||
max_files: syncAllFiles ? 0 : (maxFiles || undefined)
|
||||
}
|
||||
|
||||
// Add selected files for Google Drive
|
||||
if (connector.type === "google_drive" && selectedFiles[connector.id]?.length > 0) {
|
||||
syncBody.selected_files = selectedFiles[connector.id].map(file => file.id)
|
||||
}
|
||||
// Note: File selection is now handled via the cloud connectors dialog
|
||||
|
||||
const response = await fetch(`/api/connectors/${connector.type}/sync`, {
|
||||
method: 'POST',
|
||||
|
|
@ -488,16 +463,6 @@ function KnowledgeSourcesPage() {
|
|||
<CardContent className="flex-1 flex flex-col justify-end space-y-4">
|
||||
{connector.status === "connected" ? (
|
||||
<div className="space-y-3">
|
||||
{/* Google Drive file picker */}
|
||||
{connector.type === "google_drive" && (
|
||||
<GoogleDrivePicker
|
||||
onFileSelected={(files) => handleFileSelection(connector.id, files)}
|
||||
selectedFiles={selectedFiles[connector.id] || []}
|
||||
isAuthenticated={connector.status === "connected"}
|
||||
accessToken={connectorAccessTokens[connector.type]}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => handleSync(connector)}
|
||||
disabled={isSyncing === connector.id}
|
||||
|
|
|
|||
307
frontend/src/app/upload/[provider]/page.tsx
Normal file
307
frontend/src/app/upload/[provider]/page.tsx
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useParams, useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { ArrowLeft, AlertCircle } from "lucide-react"
|
||||
import { GoogleDrivePicker } from "@/components/google-drive-picker"
|
||||
import { OneDrivePicker } from "@/components/onedrive-picker"
|
||||
|
||||
interface GoogleDriveFile {
|
||||
id: string
|
||||
name: string
|
||||
mimeType: string
|
||||
webViewLink?: string
|
||||
iconLink?: string
|
||||
}
|
||||
|
||||
interface OneDriveFile {
|
||||
id: string
|
||||
name: string
|
||||
mimeType?: string
|
||||
webUrl?: string
|
||||
driveItem?: {
|
||||
file?: { mimeType: string }
|
||||
folder?: object
|
||||
}
|
||||
}
|
||||
|
||||
interface CloudConnector {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
status: "not_connected" | "connecting" | "connected" | "error"
|
||||
type: string
|
||||
connectionId?: string
|
||||
hasAccessToken: boolean
|
||||
accessTokenError?: string
|
||||
}
|
||||
|
||||
export default function UploadProviderPage() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const provider = params.provider as string
|
||||
|
||||
const [connector, setConnector] = useState<CloudConnector | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null)
|
||||
const [selectedFiles, setSelectedFiles] = useState<GoogleDriveFile[] | OneDriveFile[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConnectorInfo = async () => {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// Fetch available connectors to validate the provider
|
||||
const connectorsResponse = await fetch('/api/connectors')
|
||||
if (!connectorsResponse.ok) {
|
||||
throw new Error('Failed to load connectors')
|
||||
}
|
||||
|
||||
const connectorsResult = await connectorsResponse.json()
|
||||
const providerInfo = connectorsResult.connectors[provider]
|
||||
|
||||
if (!providerInfo || !providerInfo.available) {
|
||||
setError(`Cloud provider "${provider}" is not available or configured.`)
|
||||
return
|
||||
}
|
||||
|
||||
// Check connector status
|
||||
const statusResponse = await fetch(`/api/connectors/${provider}/status`)
|
||||
if (!statusResponse.ok) {
|
||||
throw new Error(`Failed to check ${provider} status`)
|
||||
}
|
||||
|
||||
const statusData = await statusResponse.json()
|
||||
const connections = statusData.connections || []
|
||||
const activeConnection = connections.find((conn: {is_active: boolean, connection_id: string}) => conn.is_active)
|
||||
const isConnected = activeConnection !== undefined
|
||||
|
||||
let hasAccessToken = false
|
||||
let accessTokenError: string | undefined = undefined
|
||||
|
||||
// Try to get access token for connected connectors
|
||||
if (isConnected && activeConnection) {
|
||||
try {
|
||||
const tokenResponse = await fetch(`/api/connectors/${provider}/token?connection_id=${activeConnection.connection_id}`)
|
||||
if (tokenResponse.ok) {
|
||||
const tokenData = await tokenResponse.json()
|
||||
if (tokenData.access_token) {
|
||||
hasAccessToken = true
|
||||
setAccessToken(tokenData.access_token)
|
||||
}
|
||||
} else {
|
||||
const errorData = await tokenResponse.json().catch(() => ({ error: 'Token unavailable' }))
|
||||
accessTokenError = errorData.error || 'Access token unavailable'
|
||||
}
|
||||
} catch {
|
||||
accessTokenError = 'Failed to fetch access token'
|
||||
}
|
||||
}
|
||||
|
||||
setConnector({
|
||||
id: provider,
|
||||
name: providerInfo.name,
|
||||
description: providerInfo.description,
|
||||
status: isConnected ? "connected" : "not_connected",
|
||||
type: provider,
|
||||
connectionId: activeConnection?.connection_id,
|
||||
hasAccessToken,
|
||||
accessTokenError
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load connector info:', error)
|
||||
setError(error instanceof Error ? error.message : 'Failed to load connector information')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (provider) {
|
||||
fetchConnectorInfo()
|
||||
}
|
||||
}, [provider])
|
||||
|
||||
const handleFileSelected = (files: GoogleDriveFile[] | OneDriveFile[]) => {
|
||||
setSelectedFiles(files)
|
||||
console.log(`Selected ${files.length} files from ${provider}:`, files)
|
||||
// You can add additional handling here like triggering sync, etc.
|
||||
}
|
||||
|
||||
const getProviderDisplayName = () => {
|
||||
const nameMap: { [key: string]: string } = {
|
||||
'google_drive': 'Google Drive',
|
||||
'onedrive': 'OneDrive',
|
||||
'sharepoint': 'SharePoint'
|
||||
}
|
||||
return nameMap[provider] || provider
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
|
||||
<p>Loading {getProviderDisplayName()} connector...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !connector) {
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => router.back()}
|
||||
className="mb-4"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-center max-w-md">
|
||||
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">Provider Not Available</h2>
|
||||
<p className="text-muted-foreground mb-4">{error}</p>
|
||||
<Button onClick={() => router.push('/settings')}>
|
||||
Configure Connectors
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (connector.status !== "connected") {
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => router.back()}
|
||||
className="mb-4"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-center max-w-md">
|
||||
<AlertCircle className="h-12 w-12 text-yellow-500 mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">{connector.name} Not Connected</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
You need to connect your {connector.name} account before you can select files.
|
||||
</p>
|
||||
<Button onClick={() => router.push('/settings')}>
|
||||
Connect {connector.name}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!connector.hasAccessToken) {
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => router.back()}
|
||||
className="mb-4"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-center max-w-md">
|
||||
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">Access Token Required</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
{connector.accessTokenError || `Unable to get access token for ${connector.name}. Try reconnecting your account.`}
|
||||
</p>
|
||||
<Button onClick={() => router.push('/settings')}>
|
||||
Reconnect {connector.name}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => router.back()}
|
||||
className="mb-4"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<div className="mb-6 max-w-4xl mx-auto">
|
||||
<h1 className="text-2xl font-bold mb-2">Select Files from {connector.name}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Choose specific files from your {connector.name} account to add to your knowledge base.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{connector.type === "google_drive" && (
|
||||
<GoogleDrivePicker
|
||||
onFileSelected={handleFileSelected}
|
||||
selectedFiles={selectedFiles as GoogleDriveFile[]}
|
||||
isAuthenticated={true}
|
||||
accessToken={accessToken || undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(connector.type === "onedrive" || connector.type === "sharepoint") && (
|
||||
<OneDrivePicker
|
||||
onFileSelected={handleFileSelected}
|
||||
selectedFiles={selectedFiles as OneDriveFile[]}
|
||||
isAuthenticated={true}
|
||||
accessToken={accessToken || undefined}
|
||||
connectorType={connector.type as "onedrive" | "sharepoint"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="max-w-4xl mx-auto mt-8 flex justify-center gap-3">
|
||||
<Button
|
||||
onClick={() => {
|
||||
// Handle sync/upload action here
|
||||
console.log('Starting sync for selected files:', selectedFiles)
|
||||
// You can add API call to trigger sync
|
||||
}}
|
||||
disabled={selectedFiles.length === 0}
|
||||
>
|
||||
Sync Selected Files ({selectedFiles.length})
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setSelectedFiles([])}>
|
||||
Clear Selection
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
299
frontend/src/components/cloud-connectors-dialog.tsx
Normal file
299
frontend/src/components/cloud-connectors-dialog.tsx
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { GoogleDrivePicker } from "@/components/google-drive-picker"
|
||||
import { OneDrivePicker } from "@/components/onedrive-picker"
|
||||
import { Loader2 } from "lucide-react"
|
||||
|
||||
interface GoogleDriveFile {
|
||||
id: string
|
||||
name: string
|
||||
mimeType: string
|
||||
webViewLink?: string
|
||||
iconLink?: string
|
||||
}
|
||||
|
||||
interface OneDriveFile {
|
||||
id: string
|
||||
name: string
|
||||
mimeType?: string
|
||||
webUrl?: string
|
||||
driveItem?: {
|
||||
file?: { mimeType: string }
|
||||
folder?: any
|
||||
}
|
||||
}
|
||||
|
||||
interface CloudConnector {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
icon: React.ReactNode
|
||||
status: "not_connected" | "connecting" | "connected" | "error"
|
||||
type: string
|
||||
connectionId?: string
|
||||
hasAccessToken: boolean
|
||||
accessTokenError?: string
|
||||
}
|
||||
|
||||
interface CloudConnectorsDialogProps {
|
||||
isOpen: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onFileSelected?: (files: GoogleDriveFile[] | OneDriveFile[], connectorType: string) => void
|
||||
}
|
||||
|
||||
export function CloudConnectorsDialog({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
onFileSelected
|
||||
}: CloudConnectorsDialogProps) {
|
||||
const [connectors, setConnectors] = useState<CloudConnector[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [selectedFiles, setSelectedFiles] = useState<{[connectorId: string]: GoogleDriveFile[] | OneDriveFile[]}>({})
|
||||
const [connectorAccessTokens, setConnectorAccessTokens] = useState<{[connectorType: string]: string}>({})
|
||||
const [activePickerType, setActivePickerType] = useState<string | null>(null)
|
||||
const [isGooglePickerOpen, setIsGooglePickerOpen] = useState(false)
|
||||
|
||||
const getConnectorIcon = (iconName: string) => {
|
||||
const iconMap: { [key: string]: React.ReactElement } = {
|
||||
'google-drive': (
|
||||
<div className="w-8 h-8 bg-blue-600 rounded flex items-center justify-center text-white font-bold leading-none shrink-0">
|
||||
G
|
||||
</div>
|
||||
),
|
||||
'sharepoint': (
|
||||
<div className="w-8 h-8 bg-blue-700 rounded flex items-center justify-center text-white font-bold leading-none shrink-0">
|
||||
SP
|
||||
</div>
|
||||
),
|
||||
'onedrive': (
|
||||
<div className="w-8 h-8 bg-blue-400 rounded flex items-center justify-center text-white font-bold leading-none shrink-0">
|
||||
OD
|
||||
</div>
|
||||
),
|
||||
}
|
||||
return iconMap[iconName] || (
|
||||
<div className="w-8 h-8 bg-gray-500 rounded flex items-center justify-center text-white font-bold leading-none shrink-0">
|
||||
?
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const fetchConnectorStatuses = useCallback(async () => {
|
||||
if (!isOpen) return
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
// Fetch available connectors from backend
|
||||
const connectorsResponse = await fetch('/api/connectors')
|
||||
if (!connectorsResponse.ok) {
|
||||
throw new Error('Failed to load connectors')
|
||||
}
|
||||
|
||||
const connectorsResult = await connectorsResponse.json()
|
||||
const connectorTypes = Object.keys(connectorsResult.connectors)
|
||||
|
||||
// Filter to only cloud connectors
|
||||
const cloudConnectorTypes = connectorTypes.filter(type =>
|
||||
['google_drive', 'onedrive', 'sharepoint'].includes(type) &&
|
||||
connectorsResult.connectors[type].available
|
||||
)
|
||||
|
||||
// Initialize connectors list
|
||||
const initialConnectors = cloudConnectorTypes.map(type => ({
|
||||
id: type,
|
||||
name: connectorsResult.connectors[type].name,
|
||||
description: connectorsResult.connectors[type].description,
|
||||
icon: getConnectorIcon(connectorsResult.connectors[type].icon),
|
||||
status: "not_connected" as const,
|
||||
type: type,
|
||||
hasAccessToken: false,
|
||||
accessTokenError: undefined
|
||||
}))
|
||||
|
||||
setConnectors(initialConnectors)
|
||||
|
||||
// Check status for each cloud connector type
|
||||
for (const connectorType of cloudConnectorTypes) {
|
||||
try {
|
||||
const response = await fetch(`/api/connectors/${connectorType}/status`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
const connections = data.connections || []
|
||||
const activeConnection = connections.find((conn: any) => conn.is_active)
|
||||
const isConnected = activeConnection !== undefined
|
||||
|
||||
let hasAccessToken = false
|
||||
let accessTokenError: string | undefined = undefined
|
||||
|
||||
// Try to get access token for connected connectors
|
||||
if (isConnected && activeConnection) {
|
||||
try {
|
||||
const tokenResponse = await fetch(`/api/connectors/${connectorType}/token?connection_id=${activeConnection.connection_id}`)
|
||||
if (tokenResponse.ok) {
|
||||
const tokenData = await tokenResponse.json()
|
||||
if (tokenData.access_token) {
|
||||
hasAccessToken = true
|
||||
setConnectorAccessTokens(prev => ({
|
||||
...prev,
|
||||
[connectorType]: tokenData.access_token
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
const errorData = await tokenResponse.json().catch(() => ({ error: 'Token unavailable' }))
|
||||
accessTokenError = errorData.error || 'Access token unavailable'
|
||||
}
|
||||
} catch (e) {
|
||||
accessTokenError = 'Failed to fetch access token'
|
||||
}
|
||||
}
|
||||
|
||||
setConnectors(prev => prev.map(c =>
|
||||
c.type === connectorType
|
||||
? {
|
||||
...c,
|
||||
status: isConnected ? "connected" : "not_connected",
|
||||
connectionId: activeConnection?.connection_id,
|
||||
hasAccessToken,
|
||||
accessTokenError
|
||||
}
|
||||
: c
|
||||
))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to check status for ${connectorType}:`, error)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load cloud connectors:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const handleFileSelection = (connectorId: string, files: GoogleDriveFile[] | OneDriveFile[]) => {
|
||||
setSelectedFiles(prev => ({
|
||||
...prev,
|
||||
[connectorId]: files
|
||||
}))
|
||||
|
||||
onFileSelected?.(files, connectorId)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchConnectorStatuses()
|
||||
}, [fetchConnectorStatuses])
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-2xl max-h-[80vh] overflow-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Cloud File Connectors</DialogTitle>
|
||||
<DialogDescription>
|
||||
Select files from your connected cloud storage providers
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||
Loading connectors...
|
||||
</div>
|
||||
) : connectors.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
No cloud connectors available. Configure them in Settings first.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* Service Buttons Row */}
|
||||
<div className="flex flex-wrap gap-3 justify-center">
|
||||
{connectors
|
||||
.filter(connector => connector.status === "connected")
|
||||
.map((connector) => (
|
||||
<Button
|
||||
key={connector.id}
|
||||
variant={connector.hasAccessToken ? "default" : "secondary"}
|
||||
disabled={!connector.hasAccessToken}
|
||||
title={!connector.hasAccessToken ?
|
||||
(connector.accessTokenError || "Access token required - try reconnecting your account")
|
||||
: `Select files from ${connector.name}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (connector.hasAccessToken) {
|
||||
setActivePickerType(connector.id)
|
||||
}
|
||||
}}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
{connector.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{connectors.every(c => c.status !== "connected") && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<p>No connected cloud providers found.</p>
|
||||
<p className="text-sm mt-1">Go to Settings to connect your cloud storage accounts.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Render pickers inside dialog */}
|
||||
{activePickerType && connectors.find(c => c.id === activePickerType) && (() => {
|
||||
const connector = connectors.find(c => c.id === activePickerType)!
|
||||
|
||||
if (connector.type === "google_drive") {
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<GoogleDrivePicker
|
||||
onFileSelected={(files) => {
|
||||
handleFileSelection(connector.id, files)
|
||||
setActivePickerType(null)
|
||||
setIsGooglePickerOpen(false)
|
||||
}}
|
||||
selectedFiles={selectedFiles[connector.id] as GoogleDriveFile[] || []}
|
||||
isAuthenticated={connector.status === "connected"}
|
||||
accessToken={connectorAccessTokens[connector.type]}
|
||||
onPickerStateChange={setIsGooglePickerOpen}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (connector.type === "onedrive" || connector.type === "sharepoint") {
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<OneDrivePicker
|
||||
onFileSelected={(files) => {
|
||||
handleFileSelection(connector.id, files)
|
||||
setActivePickerType(null)
|
||||
}}
|
||||
selectedFiles={selectedFiles[connector.id] as OneDriveFile[] || []}
|
||||
isAuthenticated={connector.status === "connected"}
|
||||
accessToken={connectorAccessTokens[connector.type]}
|
||||
connectorType={connector.type as "onedrive" | "sharepoint"}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
77
frontend/src/components/cloud-connectors-dropdown.tsx
Normal file
77
frontend/src/components/cloud-connectors-dropdown.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { CloudConnectorsDialog } from "@/components/cloud-connectors-dialog"
|
||||
import { Cloud, ChevronDown } from "lucide-react"
|
||||
|
||||
interface GoogleDriveFile {
|
||||
id: string
|
||||
name: string
|
||||
mimeType: string
|
||||
webViewLink?: string
|
||||
iconLink?: string
|
||||
}
|
||||
|
||||
interface OneDriveFile {
|
||||
id: string
|
||||
name: string
|
||||
mimeType?: string
|
||||
webUrl?: string
|
||||
driveItem?: {
|
||||
file?: { mimeType: string }
|
||||
folder?: any
|
||||
}
|
||||
}
|
||||
|
||||
interface CloudConnectorsDropdownProps {
|
||||
onFileSelected?: (files: GoogleDriveFile[] | OneDriveFile[], connectorType: string) => void
|
||||
buttonText?: string
|
||||
variant?: "default" | "outline" | "secondary" | "ghost" | "link" | "destructive"
|
||||
size?: "default" | "sm" | "lg" | "icon"
|
||||
}
|
||||
|
||||
export function CloudConnectorsDropdown({
|
||||
onFileSelected,
|
||||
buttonText = "Cloud Files",
|
||||
variant = "outline",
|
||||
size = "default"
|
||||
}: CloudConnectorsDropdownProps) {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
|
||||
const handleOpenDialog = () => {
|
||||
setIsDialogOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant={variant} size={size}>
|
||||
<Cloud className="mr-2 h-4 w-4" />
|
||||
{buttonText}
|
||||
<ChevronDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem onClick={handleOpenDialog} className="cursor-pointer">
|
||||
<Cloud className="mr-2 h-4 w-4" />
|
||||
Select Cloud Files
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<CloudConnectorsDialog
|
||||
isOpen={isDialogOpen}
|
||||
onOpenChange={setIsDialogOpen}
|
||||
onFileSelected={onFileSelected}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ interface GoogleDrivePickerProps {
|
|||
selectedFiles?: GoogleDriveFile[]
|
||||
isAuthenticated: boolean
|
||||
accessToken?: string
|
||||
onPickerStateChange?: (isOpen: boolean) => void
|
||||
}
|
||||
|
||||
interface GoogleDriveFile {
|
||||
|
|
@ -88,7 +89,8 @@ export function GoogleDrivePicker({
|
|||
onFileSelected,
|
||||
selectedFiles = [],
|
||||
isAuthenticated,
|
||||
accessToken
|
||||
accessToken,
|
||||
onPickerStateChange
|
||||
}: GoogleDrivePickerProps) {
|
||||
const [isPickerLoaded, setIsPickerLoaded] = useState(false)
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false)
|
||||
|
|
@ -131,6 +133,7 @@ export function GoogleDrivePicker({
|
|||
}
|
||||
}, [])
|
||||
|
||||
|
||||
const openPicker = () => {
|
||||
if (!isPickerLoaded || !accessToken || !window.google?.picker) {
|
||||
return
|
||||
|
|
@ -138,7 +141,9 @@ export function GoogleDrivePicker({
|
|||
|
||||
try {
|
||||
setIsPickerOpen(true)
|
||||
onPickerStateChange?.(true)
|
||||
|
||||
// Create picker with higher z-index and focus handling
|
||||
const picker = new window.google.picker.PickerBuilder()
|
||||
.addView(window.google.picker.ViewId.DOCS)
|
||||
.addView(window.google.picker.ViewId.FOLDERS)
|
||||
|
|
@ -149,9 +154,23 @@ export function GoogleDrivePicker({
|
|||
.build()
|
||||
|
||||
picker.setVisible(true)
|
||||
|
||||
// Apply z-index fix after a short delay to ensure picker is rendered
|
||||
setTimeout(() => {
|
||||
const pickerElements = document.querySelectorAll('.picker-dialog, .goog-modalpopup')
|
||||
pickerElements.forEach(el => {
|
||||
(el as HTMLElement).style.zIndex = '10000'
|
||||
})
|
||||
const bgElements = document.querySelectorAll('.picker-dialog-bg, .goog-modalpopup-bg')
|
||||
bgElements.forEach(el => {
|
||||
(el as HTMLElement).style.zIndex = '9999'
|
||||
})
|
||||
}, 100)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating picker:', error)
|
||||
setIsPickerOpen(false)
|
||||
onPickerStateChange?.(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -169,6 +188,7 @@ export function GoogleDrivePicker({
|
|||
}
|
||||
|
||||
setIsPickerOpen(false)
|
||||
onPickerStateChange?.(false)
|
||||
}
|
||||
|
||||
const removeFile = (fileId: string) => {
|
||||
|
|
|
|||
322
frontend/src/components/onedrive-picker.tsx
Normal file
322
frontend/src/components/onedrive-picker.tsx
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { FileText, Folder, X } from "lucide-react"
|
||||
|
||||
interface OneDrivePickerProps {
|
||||
onFileSelected: (files: OneDriveFile[]) => void
|
||||
selectedFiles?: OneDriveFile[]
|
||||
isAuthenticated: boolean
|
||||
accessToken?: string
|
||||
connectorType?: "onedrive" | "sharepoint"
|
||||
onPickerStateChange?: (isOpen: boolean) => void
|
||||
}
|
||||
|
||||
interface OneDriveFile {
|
||||
id: string
|
||||
name: string
|
||||
mimeType?: string
|
||||
webUrl?: string
|
||||
driveItem?: {
|
||||
file?: { mimeType: string }
|
||||
folder?: any
|
||||
}
|
||||
}
|
||||
|
||||
interface GraphResponse {
|
||||
value: OneDriveFile[]
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
mgt?: {
|
||||
Providers: {
|
||||
globalProvider: any
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function OneDrivePicker({
|
||||
onFileSelected,
|
||||
selectedFiles = [],
|
||||
isAuthenticated,
|
||||
accessToken,
|
||||
connectorType = "onedrive",
|
||||
onPickerStateChange
|
||||
}: OneDrivePickerProps) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [files, setFiles] = useState<OneDriveFile[]>([])
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false)
|
||||
const [currentPath, setCurrentPath] = useState<string>(
|
||||
connectorType === "sharepoint" ? 'sites?search=' : 'me/drive/root/children'
|
||||
)
|
||||
const [breadcrumbs, setBreadcrumbs] = useState<{id: string, name: string}[]>([
|
||||
{id: 'root', name: connectorType === "sharepoint" ? 'SharePoint' : 'OneDrive'}
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const loadMGT = async () => {
|
||||
if (typeof window !== 'undefined' && !window.mgt) {
|
||||
try {
|
||||
const mgtModule = await import('@microsoft/mgt-components')
|
||||
const mgtProvider = await import('@microsoft/mgt-msal2-provider')
|
||||
|
||||
// Initialize provider if needed
|
||||
if (!window.mgt?.Providers?.globalProvider && accessToken) {
|
||||
// For simplicity, we'll use direct Graph API calls instead of MGT components
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('MGT not available, falling back to direct API calls')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadMGT()
|
||||
}, [accessToken])
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching OneDrive files:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const openPicker = () => {
|
||||
if (!accessToken) return
|
||||
|
||||
setIsPickerOpen(true)
|
||||
onPickerStateChange?.(true)
|
||||
fetchFiles()
|
||||
}
|
||||
|
||||
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 navigateToBreadcrumb = (index: number) => {
|
||||
if (index === 0) {
|
||||
setCurrentPath('me/drive/root/children')
|
||||
setBreadcrumbs([{id: 'root', name: 'OneDrive'}])
|
||||
fetchFiles('me/drive/root/children')
|
||||
} else {
|
||||
const targetCrumb = breadcrumbs[index]
|
||||
const newPath = `me/drive/items/${targetCrumb.id}/children`
|
||||
setCurrentPath(newPath)
|
||||
setBreadcrumbs(breadcrumbs.slice(0, index + 1))
|
||||
fetchFiles(newPath)
|
||||
}
|
||||
}
|
||||
|
||||
const removeFile = (fileId: string) => {
|
||||
const updatedFiles = selectedFiles.filter(file => file.id !== fileId)
|
||||
onFileSelected(updatedFiles)
|
||||
}
|
||||
|
||||
const getFileIcon = (file: OneDriveFile) => {
|
||||
if (file.driveItem?.folder) {
|
||||
return <Folder className="h-4 w-4" />
|
||||
}
|
||||
return <FileText className="h-4 w-4" />
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="text-sm text-muted-foreground p-4 bg-muted/20 rounded-md">
|
||||
Please connect to {serviceName} first to select specific files.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium">{serviceName} File Selection</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choose specific files to sync instead of syncing everything
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={openPicker}
|
||||
disabled={!accessToken}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
title={!accessToken ? `Access token required - try disconnecting and reconnecting ${serviceName}` : ""}
|
||||
>
|
||||
{!accessToken ? "No Access Token" : "Select Files"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Status message when access token is missing */}
|
||||
{isAuthenticated && !accessToken && (
|
||||
<div className="text-xs text-amber-600 bg-amber-50 p-3 rounded-md border border-amber-200">
|
||||
<div className="font-medium mb-1">Access token unavailable</div>
|
||||
<div>The file picker requires an access token. Try disconnecting and reconnecting your {serviceName} account.</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File Picker Modal */}
|
||||
{isPickerOpen && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[100]">
|
||||
<div className="bg-white rounded-lg p-6 max-w-2xl w-full max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<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="ghost">
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Breadcrumbs */}
|
||||
<div className="flex items-center space-x-2 mb-4 text-sm">
|
||||
{breadcrumbs.map((crumb, index) => (
|
||||
<div key={crumb.id} className="flex items-center">
|
||||
{index > 0 && <span className="mx-2 text-gray-400">/</span>}
|
||||
<button
|
||||
onClick={() => navigateToBreadcrumb(index)}
|
||||
className="text-blue-600 hover:underline"
|
||||
>
|
||||
{crumb.name}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* File List */}
|
||||
<div className="flex-1 overflow-y-auto border rounded-md">
|
||||
{isLoading ? (
|
||||
<div className="p-4 text-center text-muted-foreground">Loading...</div>
|
||||
) : files.length === 0 ? (
|
||||
<div className="p-4 text-center text-muted-foreground">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 variant="default" className="text-xs">Selected</Badge>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Selected files ({selectedFiles.length}):
|
||||
</p>
|
||||
<div className="max-h-32 overflow-y-auto space-y-1">
|
||||
{selectedFiles.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex items-center justify-between p-2 bg-muted/30 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"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => onFileSelected([])}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-xs h-6"
|
||||
>
|
||||
Clear all
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -24,11 +24,8 @@ async def connector_sync(request: Request, connector_service, session_manager):
|
|||
max_files = data.get("max_files")
|
||||
|
||||
try:
|
||||
logger.debug("Starting connector sync", connector_type=connector_type, max_files=max_files)
|
||||
|
||||
user = request.state.user
|
||||
jwt_token = request.state.jwt_token
|
||||
logger.debug("User authenticated", user_id=user.user_id)
|
||||
|
||||
# Get all active connections for this connector type and user
|
||||
connections = await connector_service.connection_manager.list_connections(
|
||||
|
|
@ -45,18 +42,15 @@ async def connector_sync(request: Request, connector_service, session_manager):
|
|||
# Start sync tasks for all active connections
|
||||
task_ids = []
|
||||
for connection in active_connections:
|
||||
logger.debug("About to call sync_connector_files for connection", connection_id=connection.connection_id)
|
||||
task_id = await connector_service.sync_connector_files(
|
||||
connection.connection_id,
|
||||
user.user_id,
|
||||
max_files,
|
||||
jwt_token=jwt_token,
|
||||
# NEW: thread picker selections through
|
||||
selected_files=data.get("selected_files"),
|
||||
selected_folders=data.get("selected_folders"),
|
||||
)
|
||||
task_ids.append(task_id)
|
||||
logger.debug("Got task ID", task_id=task_id)
|
||||
|
||||
return JSONResponse(
|
||||
{
|
||||
|
|
@ -69,14 +63,7 @@ async def connector_sync(request: Request, connector_service, session_manager):
|
|||
)
|
||||
|
||||
except Exception as e:
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
error_msg = f"[ERROR] Connector sync failed: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
sys.stderr.flush()
|
||||
|
||||
logger.error("Connector sync failed", error=str(e))
|
||||
return JSONResponse({"error": f"Sync failed: {str(e)}"}, status_code=500)
|
||||
|
||||
|
||||
|
|
@ -247,9 +234,6 @@ async def connector_webhook(request: Request, connector_service, session_manager
|
|||
|
||||
except Exception as e:
|
||||
logger.error("Failed to process webhook for connection", connection_id=connection.connection_id, error=str(e))
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return JSONResponse(
|
||||
{
|
||||
"status": "error",
|
||||
|
|
@ -261,10 +245,7 @@ async def connector_webhook(request: Request, connector_service, session_manager
|
|||
)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
logger.error("Webhook processing failed", error=str(e))
|
||||
traceback.print_exc()
|
||||
return JSONResponse(
|
||||
{"error": f"Webhook processing failed: {str(e)}"}, status_code=500
|
||||
)
|
||||
|
|
@ -288,7 +269,7 @@ async def connector_token(request: Request, connector_service, session_manager):
|
|||
# Get the connector instance
|
||||
connector = await connector_service._get_connector(connection_id)
|
||||
if not connector:
|
||||
return JSONResponse({"error": "Connector not available"}, status_code=404)
|
||||
return JSONResponse({"error": f"Connector not available - authentication may have failed for {connector_type}"}, status_code=404)
|
||||
|
||||
# For Google Drive, get the access token
|
||||
if connector_type == "google_drive" and hasattr(connector, 'oauth'):
|
||||
|
|
@ -301,9 +282,22 @@ async def connector_token(request: Request, connector_service, session_manager):
|
|||
})
|
||||
else:
|
||||
return JSONResponse({"error": "Invalid or expired credentials"}, status_code=401)
|
||||
|
||||
# For OneDrive and SharePoint, get the access token
|
||||
elif connector_type in ["onedrive", "sharepoint"] and hasattr(connector, 'oauth'):
|
||||
try:
|
||||
access_token = connector.oauth.get_access_token()
|
||||
return JSONResponse({
|
||||
"access_token": access_token,
|
||||
"expires_in": None # MSAL handles token expiry internally
|
||||
})
|
||||
except ValueError as e:
|
||||
return JSONResponse({"error": f"Failed to get access token: {str(e)}"}, status_code=401)
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": f"Authentication error: {str(e)}"}, status_code=500)
|
||||
|
||||
return JSONResponse({"error": "Token not available for this connector type"}, status_code=400)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting connector token: {e}")
|
||||
logger.error("Error getting connector token", error=str(e))
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue