Merge pull request #2029 from abfeb8/main

feat: add Microsoft Entra ID integration
This commit is contained in:
Manish Madan
2025-10-15 13:38:48 +05:30
committed by GitHub
23 changed files with 586 additions and 47 deletions

View File

@@ -0,0 +1,16 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 48 48" id="b" xmlns="http://www.w3.org/2000/svg" fill="#000000" stroke="#000000" stroke-width="3.312">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier">
<defs>
<style>.c{fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;}</style>
</defs>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,13 @@
const ConnectedStateSkeleton = () => (
<div className="mb-4">
<div className="flex w-full animate-pulse items-center justify-between rounded-[10px] bg-gray-200 px-4 py-2 dark:bg-gray-700">
<div className="flex items-center gap-2">
<div className="h-4 w-4 rounded bg-gray-300 dark:bg-gray-600"></div>
<div className="h-4 w-32 rounded bg-gray-300 dark:bg-gray-600"></div>
</div>
<div className="h-4 w-16 rounded bg-gray-300 dark:bg-gray-600"></div>
</div>
</div>
);
export default ConnectedStateSkeleton;

View File

@@ -150,7 +150,7 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({
{isConnected ? (
<div className="mb-4">
<div className="flex w-full items-center justify-between rounded-[10px] bg-[#8FDD51] px-4 py-2 text-sm font-medium text-[#212121]">
<div className="flex items-center gap-2">
<div className="flex max-w-[500px] items-center gap-2">
<svg className="h-4 w-4" viewBox="0 0 24 24">
<path
fill="currentColor"

View File

@@ -0,0 +1,13 @@
const FilesSectionSkeleton = () => (
<div className="rounded-lg border border-[#EEE6FF78] dark:border-[#6A6A6A]">
<div className="p-4">
<div className="mb-4 flex items-center justify-between">
<div className="h-5 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
<div className="h-8 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
</div>
<div className="h-4 w-40 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
</div>
</div>
);
export default FilesSectionSkeleton;

View File

@@ -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<GoogleDrivePickerProps> = ({
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<GoogleDrivePickerProps> = ({
onSelectionChange([], []);
};
const ConnectedStateSkeleton = () => (
<div className="mb-4">
<div className="flex w-full animate-pulse items-center justify-between rounded-[10px] bg-gray-200 px-4 py-2 dark:bg-gray-700">
<div className="flex items-center gap-2">
<div className="h-4 w-4 rounded bg-gray-300 dark:bg-gray-600"></div>
<div className="h-4 w-32 rounded bg-gray-300 dark:bg-gray-600"></div>
</div>
<div className="h-4 w-16 rounded bg-gray-300 dark:bg-gray-600"></div>
</div>
</div>
);
const FilesSectionSkeleton = () => (
<div className="rounded-lg border border-[#EEE6FF78] dark:border-[#6A6A6A]">
<div className="p-4">
<div className="mb-4 flex items-center justify-between">
<div className="h-5 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
<div className="h-8 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
</div>
<div className="h-4 w-40 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
</div>
</div>
);
return (
<div>
{isValidating ? (

View File

@@ -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<SharePointPickerProps> = ({ token }) => {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [userEmail, setUserEmail] = useState<string>('');
const [isConnected, setIsConnected] = useState(false);
const [authError, setAuthError] = useState<string>('');
const [accessToken, setAccessToken] = useState<string | null>(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 (
<div>
{isValidating ? (
<>
<ConnectedStateSkeleton />
<FilesSectionSkeleton />
</>
) : (
<>
<ConnectorAuth
provider="share_point"
label={t('modals.uploadDoc.connectors.sharePoint.connect')}
onSuccess={(data) => {
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 && (
<div className="rounded-lg border border-[#EEE6FF78] dark:border-[#6A6A6A]">
<div className="p-4">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-sm font-medium">
{t('modals.uploadDoc.connectors.sharePoint.selectedFiles')}
</h3>
<button
onClick={() => handleOpenPicker()}
className="rounded-md bg-[#A076F6] px-3 py-1 text-sm text-white hover:bg-[#8A5FD4]"
disabled={isLoading}
>
{isLoading
? t('modals.uploadDoc.connectors.sharePoint.loading')
: t('modals.uploadDoc.connectors.sharePoint.selectFiles')}
</button>
</div>
</div>
</div>
)}
</>
)}
</div>
);
};
export default SharePointPicker;

View File

@@ -298,6 +298,10 @@
"google_drive": {
"label": "Google Drive",
"heading": "Upload from Google Drive"
},
"share_point": {
"label": "SharePoint",
"heading": "Upload from SharePoint"
}
},
"connectors": {
@@ -327,6 +331,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"
}
}
},

View File

@@ -261,6 +261,10 @@
"google_drive": {
"label": "Google Drive",
"heading": "Subir desde Google Drive"
},
"share_point": {
"label": "SharePoint",
"heading": "Subir desde SharePoint"
}
},
"connectors": {
@@ -290,6 +294,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"
}
}
},

View File

@@ -261,6 +261,10 @@
"google_drive": {
"label": "Google Drive",
"heading": "Google Driveからアップロード"
},
"share_point": {
"label": "SharePoint",
"heading": "SharePointからアップロード"
}
},
"connectors": {
@@ -290,6 +294,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": "ファイル"
}
}
},

View File

@@ -261,6 +261,10 @@
"google_drive": {
"label": "Google Drive",
"heading": "Загрузить из Google Drive"
},
"share_point": {
"label": "SharePoint",
"heading": "Загрузить из SharePoint"
}
},
"connectors": {
@@ -290,6 +294,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": "Файл"
}
}
},

View File

@@ -261,6 +261,10 @@
"google_drive": {
"label": "Google Drive",
"heading": "從Google Drive上傳"
},
"share_point": {
"label": "SharePoint",
"heading": "從SharePoint上傳"
}
},
"connectors": {
@@ -290,6 +294,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": "檔案"
}
}
},

View File

@@ -261,6 +261,10 @@
"google_drive": {
"label": "Google Drive",
"heading": "从Google Drive上传"
},
"share_point": {
"label": "SharePoint",
"heading": "从SharePoint上传"
}
},
"connectors": {
@@ -290,6 +294,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": "文件"
}
}
},

View File

@@ -32,6 +32,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';
@@ -252,6 +253,8 @@ function Upload({
token={token}
/>
);
case 'share_point_picker':
return <SharePointPicker key={field.name} token={token} />;
default:
return null;
}

View File

@@ -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 {

View File

@@ -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),
}),
});
};