From cc54cea783c2eaaf238399fc474f889fece63c6e Mon Sep 17 00:00:00 2001 From: Abhishek Malviya Date: Fri, 10 Oct 2025 15:15:38 +0530 Subject: [PATCH] feat: add SharePoint integration with session validation and UI components --- frontend/src/assets/sharepoint.svg | 16 ++ .../src/components/ConnectedStateSkeleton.tsx | 13 ++ frontend/src/components/ConnectorAuth.tsx | 2 +- .../src/components/FileSelectionSkeleton.tsx | 13 ++ frontend/src/components/GoogleDrivePicker.tsx | 44 +---- frontend/src/components/SharePointPicker.tsx | 175 ++++++++++++++++++ frontend/src/locale/en.json | 22 +++ frontend/src/locale/es.json | 22 +++ frontend/src/locale/jp.json | 22 +++ frontend/src/locale/ru.json | 22 +++ frontend/src/locale/zh-TW.json | 22 +++ frontend/src/locale/zh.json | 22 +++ frontend/src/upload/Upload.tsx | 3 + frontend/src/upload/types/ingestor.ts | 33 +++- frontend/src/utils/providerUtils.ts | 18 ++ 15 files changed, 408 insertions(+), 41 deletions(-) create mode 100644 frontend/src/assets/sharepoint.svg create mode 100644 frontend/src/components/ConnectedStateSkeleton.tsx create mode 100644 frontend/src/components/FileSelectionSkeleton.tsx create mode 100644 frontend/src/components/SharePointPicker.tsx diff --git a/frontend/src/assets/sharepoint.svg b/frontend/src/assets/sharepoint.svg new file mode 100644 index 000000000..9a332f8ed --- /dev/null +++ b/frontend/src/assets/sharepoint.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/ConnectedStateSkeleton.tsx b/frontend/src/components/ConnectedStateSkeleton.tsx new file mode 100644 index 000000000..f871fc9b6 --- /dev/null +++ b/frontend/src/components/ConnectedStateSkeleton.tsx @@ -0,0 +1,13 @@ +const ConnectedStateSkeleton = () => ( +
+
+
+
+
+
+
+
+
+); + +export default ConnectedStateSkeleton; diff --git a/frontend/src/components/ConnectorAuth.tsx b/frontend/src/components/ConnectorAuth.tsx index a60d293c5..6411df90f 100644 --- a/frontend/src/components/ConnectorAuth.tsx +++ b/frontend/src/components/ConnectorAuth.tsx @@ -150,7 +150,7 @@ const ConnectorAuth: React.FC = ({ {isConnected ? (
-
+
( +
+
+
+
+
+
+
+
+
+); + +export default FilesSectionSkeleton; diff --git a/frontend/src/components/GoogleDrivePicker.tsx b/frontend/src/components/GoogleDrivePicker.tsx index be7f178e9..bb3d240b2 100644 --- a/frontend/src/components/GoogleDrivePicker.tsx +++ b/frontend/src/components/GoogleDrivePicker.tsx @@ -7,7 +7,10 @@ import { getSessionToken, setSessionToken, removeSessionToken, + validateProviderSession, } from '../utils/providerUtils'; +import ConnectedStateSkeleton from './ConnectedStateSkeleton'; +import FilesSectionSkeleton from './FileSelectionSkeleton'; interface PickerFile { id: string; @@ -50,20 +53,9 @@ const GoogleDrivePicker: React.FC = ({ const validateSession = async (sessionToken: string) => { try { - const apiHost = import.meta.env.VITE_API_HOST; - const validateResponse = await fetch( - `${apiHost}/api/connectors/validate-session`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - provider: 'google_drive', - session_token: sessionToken, - }), - }, + const validateResponse = await validateProviderSession( + token, + 'google_drive', ); if (!validateResponse.ok) { @@ -234,30 +226,6 @@ const GoogleDrivePicker: React.FC = ({ onSelectionChange([], []); }; - const ConnectedStateSkeleton = () => ( -
-
-
-
-
-
-
-
-
- ); - - const FilesSectionSkeleton = () => ( -
-
-
-
-
-
-
-
-
- ); - return (
{isValidating ? ( diff --git a/frontend/src/components/SharePointPicker.tsx b/frontend/src/components/SharePointPicker.tsx new file mode 100644 index 000000000..682fd1cd6 --- /dev/null +++ b/frontend/src/components/SharePointPicker.tsx @@ -0,0 +1,175 @@ +import { useTranslation } from 'react-i18next'; +import ConnectorAuth from './ConnectorAuth'; +import { useEffect, useState } from 'react'; + +import { + getSessionToken, + setSessionToken, + removeSessionToken, + validateProviderSession, +} from '../utils/providerUtils'; +import ConnectedStateSkeleton from './ConnectedStateSkeleton'; +import FilesSectionSkeleton from './FileSelectionSkeleton'; + +interface SharePointPickerProps { + token: string | null; +} + +const SharePointPicker: React.FC = ({ token }) => { + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const [userEmail, setUserEmail] = useState(''); + const [isConnected, setIsConnected] = useState(false); + const [authError, setAuthError] = useState(''); + const [accessToken, setAccessToken] = useState(null); + const [isValidating, setIsValidating] = useState(false); + + useEffect(() => { + const sessionToken = getSessionToken('share_point'); + if (sessionToken) { + setIsValidating(true); + setIsConnected(true); // Optimistically set as connected for skeleton + validateSession(sessionToken); + } + }, [token]); + + const validateSession = async (sessionToken: string) => { + try { + const validateResponse = await validateProviderSession( + token, + 'share_point', + ); + + if (!validateResponse.ok) { + setIsConnected(false); + setAuthError( + t('modals.uploadDoc.connectors.sharePoint.sessionExpired'), + ); + setIsValidating(false); + return false; + } + + const validateData = await validateResponse.json(); + if (validateData.success) { + setUserEmail( + validateData.user_email || + t('modals.uploadDoc.connectors.auth.connectedUser'), + ); + setIsConnected(true); + setAuthError(''); + setAccessToken(validateData.access_token || null); + setIsValidating(false); + + return true; + } else { + setIsConnected(false); + setAuthError( + validateData.error || + t('modals.uploadDoc.connectors.sharePoint.sessionExpiredGeneric'), + ); + setIsValidating(false); + return false; + } + } catch (error) { + console.error('Error validating session:', error); + setAuthError(t('modals.uploadDoc.connectors.sharePoint.validateFailed')); + setIsConnected(false); + setIsValidating(false); + return false; + } + }; + + const handleDisconnect = async () => { + const sessionToken = getSessionToken('share_point'); + if (sessionToken) { + try { + const apiHost = import.meta.env.VITE_API_HOST; + await fetch(`${apiHost}/api/connectors/disconnect`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + provider: 'share_point', + session_token: sessionToken, + }), + }); + } catch (err) { + console.error('Error disconnecting from SharePoint:', err); + } + } + + removeSessionToken('share_point'); + setIsConnected(false); + setAccessToken(null); + setUserEmail(''); + setAuthError(''); + }; + + const handleOpenPicker = async () => { + alert('Feature not supported yet.'); + }; + + return ( +
+ {isValidating ? ( + <> + + + + ) : ( + <> + { + setUserEmail( + data.user_email || + t('modals.uploadDoc.connectors.auth.connectedUser'), + ); + setIsConnected(true); + setAuthError(''); + + if (data.session_token) { + setSessionToken('share_point', data.session_token); + validateSession(data.session_token); + } + }} + onError={(error) => { + setAuthError(error); + setIsConnected(false); + }} + isConnected={isConnected} + userEmail={userEmail} + onDisconnect={handleDisconnect} + errorMessage={authError} + /> + + {isConnected && ( +
+
+
+

+ {t('modals.uploadDoc.connectors.sharePoint.selectedFiles')} +

+ +
+
+
+ )} + + )} +
+ ); +}; + +export default SharePointPicker; diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 25a8fb9e6..9620738bb 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -297,6 +297,10 @@ "google_drive": { "label": "Google Drive", "heading": "Upload from Google Drive" + }, + "share_point": { + "label": "SharePoint", + "heading": "Upload from SharePoint" } }, "connectors": { @@ -326,6 +330,24 @@ "remove": "Remove", "folderAlt": "Folder", "fileAlt": "File" + }, + "sharePoint": { + "connect": "Connect to SharePoint", + "sessionExpired": "Session expired. Please reconnect to SharePoint.", + "sessionExpiredGeneric": "Session expired. Please reconnect your account.", + "validateFailed": "Failed to validate session. Please reconnect.", + "noSession": "No valid session found. Please reconnect to SharePoint.", + "noAccessToken": "No access token available. Please reconnect to SharePoint.", + "pickerFailed": "Failed to open file picker. Please try again.", + "selectedFiles": "Selected Files", + "selectFiles": "Select Files", + "loading": "Loading...", + "noFilesSelected": "No files or folders selected", + "folders": "Folders", + "files": "Files", + "remove": "Remove", + "folderAlt": "Folder", + "fileAlt": "File" } } }, diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index cedc32326..7f6d06904 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -260,6 +260,10 @@ "google_drive": { "label": "Google Drive", "heading": "Subir desde Google Drive" + }, + "share_point": { + "label": "SharePoint", + "heading": "Subir desde SharePoint" } }, "connectors": { @@ -289,6 +293,24 @@ "remove": "Eliminar", "folderAlt": "Carpeta", "fileAlt": "Archivo" + }, + "sharePoint": { + "connect": "Conectar a SharePoint", + "sessionExpired": "Sesión expirada. Por favor, reconecte a SharePoint.", + "sessionExpiredGeneric": "Sesión expirada. Por favor, reconecte su cuenta.", + "validateFailed": "Error al validar la sesión. Por favor, reconecte.", + "noSession": "No se encontró una sesión válida. Por favor, reconecte a SharePoint.", + "noAccessToken": "No hay token de acceso disponible. Por favor, reconecte a SharePoint.", + "pickerFailed": "Error al abrir el selector de archivos. Por favor, inténtelo de nuevo.", + "selectedFiles": "Archivos Seleccionados", + "selectFiles": "Seleccionar Archivos", + "loading": "Cargando...", + "noFilesSelected": "No hay archivos o carpetas seleccionados", + "folders": "Carpetas", + "files": "Archivos", + "remove": "Eliminar", + "folderAlt": "Carpeta", + "fileAlt": "Archivo" } } }, diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index 1072b17c9..0ab898982 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -260,6 +260,10 @@ "google_drive": { "label": "Google Drive", "heading": "Google Driveからアップロード" + }, + "share_point": { + "label": "SharePoint", + "heading": "SharePointからアップロード" } }, "connectors": { @@ -289,6 +293,24 @@ "remove": "削除", "folderAlt": "フォルダ", "fileAlt": "ファイル" + }, + "sharePoint": { + "connect": "SharePointに接続", + "sessionExpired": "セッションが期限切れです。SharePointに再接続してください。", + "sessionExpiredGeneric": "セッションが期限切れです。アカウントに再接続してください。", + "validateFailed": "セッションの検証に失敗しました。再接続してください。", + "noSession": "有効なセッションが見つかりません。SharePointに再接続してください。", + "noAccessToken": "アクセストークンが利用できません。SharePointに再接続してください。", + "pickerFailed": "ファイルピッカーを開けませんでした。もう一度お試しください。", + "selectedFiles": "選択されたファイル", + "selectFiles": "ファイルを選択", + "loading": "読み込み中...", + "noFilesSelected": "ファイルまたはフォルダが選択されていません", + "folders": "フォルダ", + "files": "ファイル", + "remove": "削除", + "folderAlt": "フォルダ", + "fileAlt": "ファイル" } } }, diff --git a/frontend/src/locale/ru.json b/frontend/src/locale/ru.json index 0ca63ec5e..97dc18c82 100644 --- a/frontend/src/locale/ru.json +++ b/frontend/src/locale/ru.json @@ -260,6 +260,10 @@ "google_drive": { "label": "Google Drive", "heading": "Загрузить из Google Drive" + }, + "share_point": { + "label": "SharePoint", + "heading": "Загрузить из SharePoint" } }, "connectors": { @@ -289,6 +293,24 @@ "remove": "Удалить", "folderAlt": "Папка", "fileAlt": "Файл" + }, + "sharePoint": { + "connect": "Подключиться к SharePoint", + "sessionExpired": "Сеанс истек. Пожалуйста, переподключитесь к SharePoint.", + "sessionExpiredGeneric": "Сеанс истек. Пожалуйста, переподключите свою учетную запись.", + "validateFailed": "Не удалось проверить сеанс. Пожалуйста, переподключитесь.", + "noSession": "Действительный сеанс не найден. Пожалуйста, переподключитесь к SharePoint.", + "noAccessToken": "Токен доступа недоступен. Пожалуйста, переподключитесь к SharePoint.", + "pickerFailed": "Не удалось открыть средство выбора файлов. Пожалуйста, попробуйте еще раз.", + "selectedFiles": "Выбранные файлы", + "selectFiles": "Выбрать файлы", + "loading": "Загрузка...", + "noFilesSelected": "Файлы или папки не выбраны", + "folders": "Папки", + "files": "Файлы", + "remove": "Удалить", + "folderAlt": "Папка", + "fileAlt": "Файл" } } }, diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json index 60a0c91d0..8fbf13bb6 100644 --- a/frontend/src/locale/zh-TW.json +++ b/frontend/src/locale/zh-TW.json @@ -260,6 +260,10 @@ "google_drive": { "label": "Google Drive", "heading": "從Google Drive上傳" + }, + "share_point": { + "label": "SharePoint", + "heading": "從SharePoint上傳" } }, "connectors": { @@ -289,6 +293,24 @@ "remove": "移除", "folderAlt": "資料夾", "fileAlt": "檔案" + }, + "sharePoint": { + "connect": "連接到 SharePoint", + "sessionExpired": "工作階段已過期。請重新連接到 SharePoint。", + "sessionExpiredGeneric": "工作階段已過期。請重新連接您的帳戶。", + "validateFailed": "驗證工作階段失敗。請重新連接。", + "noSession": "未找到有效工作階段。請重新連接到 SharePoint。", + "noAccessToken": "存取權杖不可用。請重新連接到 SharePoint。", + "pickerFailed": "無法開啟檔案選擇器。請重試。", + "selectedFiles": "已選擇的檔案", + "selectFiles": "選擇檔案", + "loading": "載入中...", + "noFilesSelected": "未選擇檔案或資料夾", + "folders": "資料夾", + "files": "檔案", + "remove": "移除", + "folderAlt": "資料夾", + "fileAlt": "檔案" } } }, diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index c3f4ec594..732d235e5 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -260,6 +260,10 @@ "google_drive": { "label": "Google Drive", "heading": "从Google Drive上传" + }, + "share_point": { + "label": "SharePoint", + "heading": "从SharePoint上传" } }, "connectors": { @@ -289,6 +293,24 @@ "remove": "删除", "folderAlt": "文件夹", "fileAlt": "文件" + }, + "sharePoint": { + "connect": "连接到 SharePoint", + "sessionExpired": "会话已过期。请重新连接到 SharePoint。", + "sessionExpiredGeneric": "会话已过期。请重新连接您的账户。", + "validateFailed": "验证会话失败。请重新连接。", + "noSession": "未找到有效会话。请重新连接到 SharePoint。", + "noAccessToken": "访问令牌不可用。请重新连接到 SharePoint。", + "pickerFailed": "无法打开文件选择器。请重试。", + "selectedFiles": "已选择的文件", + "selectFiles": "选择文件", + "loading": "加载中...", + "noFilesSelected": "未选择文件或文件夹", + "folders": "文件夹", + "files": "文件", + "remove": "删除", + "folderAlt": "文件夹", + "fileAlt": "文件" } } }, diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index 6d2223d10..86b3dc816 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -31,6 +31,7 @@ import { FormField, IngestorConfig, IngestorType } from './types/ingestor'; import { FilePicker } from '../components/FilePicker'; import GoogleDrivePicker from '../components/GoogleDrivePicker'; +import SharePointPicker from '../components/SharePointPicker'; import ChevronRight from '../assets/chevron-right.svg'; @@ -250,6 +251,8 @@ function Upload({ token={token} /> ); + case 'share_point_picker': + return ; default: return null; } diff --git a/frontend/src/upload/types/ingestor.ts b/frontend/src/upload/types/ingestor.ts index c06c90431..c56fa5b37 100644 --- a/frontend/src/upload/types/ingestor.ts +++ b/frontend/src/upload/types/ingestor.ts @@ -4,6 +4,7 @@ import UrlIcon from '../../assets/url.svg'; import GithubIcon from '../../assets/github.svg'; import RedditIcon from '../../assets/reddit.svg'; import DriveIcon from '../../assets/drive.svg'; +import SharePoint from '../../assets/sharepoint.svg'; export type IngestorType = | 'crawler' @@ -11,7 +12,8 @@ export type IngestorType = | 'reddit' | 'url' | 'google_drive' - | 'local_file'; + | 'local_file' + | 'share_point'; export interface IngestorConfig { type: IngestorType | null; @@ -33,7 +35,8 @@ export type FieldType = | 'boolean' | 'local_file_picker' | 'remote_file_picker' - | 'google_drive_picker'; + | 'google_drive_picker' + | 'share_point_picker'; export interface FormField { name: string; @@ -147,6 +150,24 @@ export const IngestorFormSchemas: IngestorSchema[] = [ }, ], }, + { + key: 'share_point', + label: 'Share Point', + icon: SharePoint, + heading: 'Upload from Share Point', + validate: () => { + const sharePointClientId = import.meta.env.VITE_SHARE_POINT_CLIENT_ID; + return !!sharePointClientId; + }, + fields: [ + { + name: 'files', + label: 'Select Files from Share Point', + type: 'share_point_picker', + required: true, + }, + ], + }, ]; export const IngestorDefaultConfigs: Record< @@ -175,6 +196,14 @@ export const IngestorDefaultConfigs: Record< }, }, local_file: { name: '', config: { files: [] } }, + share_point: { + name: '', + config: { + file_ids: '', + folder_ids: '', + recursive: true, + }, + }, }; export interface IngestorOption { diff --git a/frontend/src/utils/providerUtils.ts b/frontend/src/utils/providerUtils.ts index 25236ad2e..01f25c5c9 100644 --- a/frontend/src/utils/providerUtils.ts +++ b/frontend/src/utils/providerUtils.ts @@ -14,3 +14,21 @@ export const setSessionToken = (provider: string, token: string): void => { export const removeSessionToken = (provider: string): void => { localStorage.removeItem(`${provider}_session_token`); }; + +export const validateProviderSession = async ( + token: string | null, + provider: string, +) => { + const apiHost = import.meta.env.VITE_API_HOST; + return await fetch(`${apiHost}/api/connectors/validate-session`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + provider: provider, + session_token: getSessionToken(provider), + }), + }); +};