Remove the Add knowledge forms for synced services

Nothing opens the Google Drive, SharePoint, Confluence, S3 or Reddit
forms in Add knowledge any more: those services sync through the connect
wizard. Their pickers, saved-key account logic, setup notice, advanced
fields and strings go, and the remote upload sends the form's own
fields.
This commit is contained in:
arc53-machine committed 2026-09-29 18:51:10 +01:00
1 parent 6ccc506b8c
commit cdab471c22
10 files changed
+55 -1013

No files matched your search

-37
View File
@@ -1677,14 +1677,6 @@
"link": "Link",
"urlLink": "URL-Link",
"repoUrl": "Repository-URL",
"reddit": {
"id": "Client-ID",
"secret": "Client-Secret",
"agent": "User-Agent",
"searchQueries": "Suchanfragen",
"numberOfPosts": "Anzahl der Beiträge",
"addQuery": "Anfrage hinzufügen"
},
"drag": {
"title": "Anhänge hier ablegen",
"description": "Loslassen, um deine Anhänge hochzuladen"
@@ -1706,8 +1698,6 @@
"clear": "Löschen",
"ingestionFailed": "Aufnahme fehlgeschlagen."
},
"showAdvanced": "Erweiterte Optionen anzeigen",
"hideAdvanced": "Erweiterte Optionen ausblenden",
"ingestors": {
"local_file": {
"label": "Datei hochladen",
@@ -1725,26 +1715,6 @@
"label": "GitHub",
"heading": "Inhalt von GitHub hinzufügen"
},
"reddit": {
"label": "Reddit",
"heading": "Inhalt von Reddit hinzufügen"
},
"google_drive": {
"label": "Google Drive",
"heading": "Von Google Drive hochladen"
},
"s3": {
"label": "Amazon S3",
"heading": "Inhalt von Amazon S3 hinzufügen"
},
"share_point": {
"label": "SharePoint",
"heading": "Von SharePoint hochladen"
},
"confluence": {
"label": "Confluence",
"heading": "Von Confluence hochladen"
},
"wiki": {
"label": "Neues Wiki",
"heading": "Lebendiges Wiki erstellen"
@@ -1798,15 +1768,8 @@
},
"fields": {
"url": "URL",
"awsAccessKeyId": "AWS-Zugriffsschlüssel-ID",
"awsSecretAccessKey": "Geheimer AWS-Zugriffsschlüssel",
"bucket": "Bucket-Name",
"prefix": "Pfadpräfix (optional)",
"region": "AWS-Region",
"endpointUrl": "Benutzerdefinierte Endpunkt-URL (optional)",
"initialContent": "Anfangsinhalt (optional)"
},
"newCredentials": "Neue Zugangsdaten eingeben",
"github": {
"privateHint": "Privates Repository? Verbinde dein GitHub-Konto, um es zu synchronisieren.",
"connectedHint": "Private Repositorys werden über deine GitHub-Verbindung ({{account}}) synchronisiert.",
-37
View File
@@ -1683,14 +1683,6 @@
"link": "Link",
"urlLink": "URL Link",
"repoUrl": "Repository URL",
"reddit": {
"id": "Client ID",
"secret": "Client Secret",
"agent": "User agent",
"searchQueries": "Search queries",
"numberOfPosts": "Number of posts",
"addQuery": "Add Query"
},
"drag": {
"title": "Drop attachments here",
"description": "Release to upload your attachments"
@@ -1712,8 +1704,6 @@
"clear": "Clear",
"ingestionFailed": "Ingestion failed."
},
"showAdvanced": "Show advanced options",
"hideAdvanced": "Hide advanced options",
"ingestors": {
"local_file": {
"label": "Upload File",
@@ -1731,26 +1721,6 @@
"label": "GitHub",
"heading": "Add content from GitHub"
},
"reddit": {
"label": "Reddit",
"heading": "Add content from Reddit"
},
"google_drive": {
"label": "Google Drive",
"heading": "Upload from Google Drive"
},
"s3": {
"label": "Amazon S3",
"heading": "Add content from Amazon S3"
},
"share_point": {
"label": "SharePoint",
"heading": "Upload from SharePoint"
},
"confluence": {
"label": "Confluence",
"heading": "Upload from Confluence"
},
"wiki": {
"label": "New wiki",
"heading": "Create a living wiki"
@@ -1804,15 +1774,8 @@
},
"fields": {
"url": "URL",
"awsAccessKeyId": "AWS Access Key ID",
"awsSecretAccessKey": "AWS Secret Access Key",
"bucket": "Bucket Name",
"prefix": "Path Prefix (optional)",
"region": "AWS Region",
"endpointUrl": "Custom Endpoint URL (optional)",
"initialContent": "Initial content (optional)"
},
"newCredentials": "Enter new credentials",
"github": {
"privateHint": "Private repository? Connect your GitHub account to sync it.",
"connectedHint": "Private repositories sync from your GitHub connection ({{account}}).",
-37
View File
@@ -1677,14 +1677,6 @@
"link": "Enlace",
"urlLink": "Enlace URL",
"repoUrl": "URL del Repositorio",
"reddit": {
"id": "ID del Cliente",
"secret": "Secreto del Cliente",
"agent": "Agente de usuario",
"searchQueries": "Consultas de búsqueda",
"numberOfPosts": "Número de publicaciones",
"addQuery": "Agregar Consulta"
},
"drag": {
"title": "Suelta los archivos adjuntos aquí",
"description": "Suelta para subir tus archivos adjuntos"
@@ -1706,8 +1698,6 @@
"clear": "Limpiar",
"ingestionFailed": "La ingesta falló."
},
"showAdvanced": "Mostrar opciones avanzadas",
"hideAdvanced": "Ocultar opciones avanzadas",
"ingestors": {
"local_file": {
"label": "Subir archivo",
@@ -1725,26 +1715,6 @@
"label": "GitHub",
"heading": "Agregar contenido desde GitHub"
},
"reddit": {
"label": "Reddit",
"heading": "Agregar contenido desde Reddit"
},
"google_drive": {
"label": "Google Drive",
"heading": "Subir desde Google Drive"
},
"s3": {
"label": "Amazon S3",
"heading": "Agregar contenido desde Amazon S3"
},
"share_point": {
"label": "SharePoint",
"heading": "Subir desde SharePoint"
},
"confluence": {
"label": "Confluence",
"heading": "Subir desde Confluence"
},
"wiki": {
"label": "Nueva wiki",
"heading": "Crear una wiki viva"
@@ -1798,15 +1768,8 @@
},
"fields": {
"url": "URL",
"awsAccessKeyId": "ID de clave de acceso de AWS",
"awsSecretAccessKey": "Clave de acceso secreta de AWS",
"bucket": "Nombre del bucket",
"prefix": "Prefijo de ruta (opcional)",
"region": "Región de AWS",
"endpointUrl": "URL de endpoint personalizada (opcional)",
"initialContent": "Contenido inicial (opcional)"
},
"newCredentials": "Introducir credenciales nuevas",
"github": {
"privateHint": "¿Repositorio privado? Conecta tu cuenta de GitHub para sincronizarlo.",
"connectedHint": "Los repositorios privados se sincronizan desde tu conexión de GitHub ({{account}}).",
-37
View File
@@ -1666,14 +1666,6 @@
"link": "リンク",
"urlLink": "URLリンク",
"repoUrl": "リポジトリURL",
"reddit": {
"id": "クライアントID",
"secret": "クライアントシークレット",
"agent": "ユーザーエージェント",
"searchQueries": "検索クエリ",
"numberOfPosts": "投稿数",
"addQuery": "クエリを追加"
},
"drag": {
"title": "添付ファイルをここにドロップ",
"description": "リリースして添付ファイルをアップロード"
@@ -1695,8 +1687,6 @@
"clear": "クリア",
"ingestionFailed": "取り込みに失敗しました。"
},
"showAdvanced": "詳細オプションを表示",
"hideAdvanced": "詳細オプションを非表示",
"ingestors": {
"local_file": {
"label": "ファイルをアップロード",
@@ -1714,26 +1704,6 @@
"label": "GitHub",
"heading": "GitHubからコンテンツを追加"
},
"reddit": {
"label": "Reddit",
"heading": "Redditからコンテンツを追加"
},
"google_drive": {
"label": "Google Drive",
"heading": "Google Driveからアップロード"
},
"s3": {
"label": "Amazon S3",
"heading": "Amazon S3からコンテンツを追加"
},
"share_point": {
"label": "SharePoint",
"heading": "SharePointからアップロード"
},
"confluence": {
"label": "Confluence",
"heading": "Confluenceからアップロード"
},
"wiki": {
"label": "新しいWiki",
"heading": "リビングWikiを作成"
@@ -1787,15 +1757,8 @@
},
"fields": {
"url": "URL",
"awsAccessKeyId": "AWS アクセスキー ID",
"awsSecretAccessKey": "AWS シークレットアクセスキー",
"bucket": "バケット名",
"prefix": "パスプレフィックス(任意)",
"region": "AWS リージョン",
"endpointUrl": "カスタムエンドポイント URL(任意)",
"initialContent": "初期コンテンツ(任意)"
},
"newCredentials": "新しい認証情報を入力",
"github": {
"privateHint": "プライベートリポジトリですか?同期するには GitHub アカウントを接続してください。",
"connectedHint": "プライベートリポジトリは GitHub 接続({{account}})から同期されます。",
-37
View File
@@ -1761,14 +1761,6 @@
"link": "Ссылка",
"urlLink": "URL ссылка",
"repoUrl": "URL репозитория",
"reddit": {
"id": "ID клиента",
"secret": "Секрет клиента",
"agent": "Пользовательский агент",
"searchQueries": "Поисковые запросы",
"numberOfPosts": "Количество сообщений",
"addQuery": "Добавить запрос"
},
"drag": {
"title": "Перетащите вложения сюда",
"description": "Отпустите, чтобы загрузить ваши вложения"
@@ -1790,8 +1782,6 @@
"clear": "Очистить",
"ingestionFailed": "Не удалось загрузить данные."
},
"showAdvanced": "Показать расширенные настройки",
"hideAdvanced": "Скрыть расширенные настройки",
"ingestors": {
"local_file": {
"label": "Загрузить файл",
@@ -1809,26 +1799,6 @@
"label": "GitHub",
"heading": "Добавить контент из GitHub"
},
"reddit": {
"label": "Reddit",
"heading": "Добавить контент из Reddit"
},
"google_drive": {
"label": "Google Drive",
"heading": "Загрузить из Google Drive"
},
"s3": {
"label": "Amazon S3",
"heading": "Добавить контент из Amazon S3"
},
"share_point": {
"label": "SharePoint",
"heading": "Загрузить из SharePoint"
},
"confluence": {
"label": "Confluence",
"heading": "Загрузить из Confluence"
},
"wiki": {
"label": "Новая вики",
"heading": "Создать живую вики"
@@ -1882,15 +1852,8 @@
},
"fields": {
"url": "URL",
"awsAccessKeyId": "Идентификатор ключа доступа AWS",
"awsSecretAccessKey": "Секретный ключ доступа AWS",
"bucket": "Имя бакета",
"prefix": "Префикс пути (необязательно)",
"region": "Регион AWS",
"endpointUrl": "Пользовательский URL эндпоинта (необязательно)",
"initialContent": "Исходное содержимое (необязательно)"
},
"newCredentials": "Ввести новые учётные данные",
"github": {
"privateHint": "Приватный репозиторий? Подключите аккаунт GitHub, чтобы синхронизировать его.",
"connectedHint": "Приватные репозитории синхронизируются через ваше подключение GitHub ({{account}}).",
-37
View File
@@ -1666,14 +1666,6 @@
"link": "連結",
"urlLink": "URL 連結",
"repoUrl": "儲存庫 URL",
"reddit": {
"id": "客戶端ID",
"secret": "客戶端密鑰",
"agent": "使用者代理",
"searchQueries": "搜尋查詢",
"numberOfPosts": "貼文數量",
"addQuery": "新增查詢"
},
"drag": {
"title": "將附件拖放到此處",
"description": "釋放以上傳您的附件"
@@ -1695,8 +1687,6 @@
"clear": "清除",
"ingestionFailed": "匯入失敗。"
},
"showAdvanced": "顯示進階選項",
"hideAdvanced": "隱藏進階選項",
"ingestors": {
"local_file": {
"label": "上傳檔案",
@@ -1714,26 +1704,6 @@
"label": "GitHub",
"heading": "從GitHub新增內容"
},
"reddit": {
"label": "Reddit",
"heading": "從Reddit新增內容"
},
"google_drive": {
"label": "Google Drive",
"heading": "從Google Drive上傳"
},
"s3": {
"label": "Amazon S3",
"heading": "從Amazon S3新增內容"
},
"share_point": {
"label": "SharePoint",
"heading": "從SharePoint上傳"
},
"confluence": {
"label": "Confluence",
"heading": "從Confluence上傳"
},
"wiki": {
"label": "新 Wiki",
"heading": "建立動態 Wiki"
@@ -1787,15 +1757,8 @@
},
"fields": {
"url": "URL",
"awsAccessKeyId": "AWS 存取金鑰 ID",
"awsSecretAccessKey": "AWS 秘密存取金鑰",
"bucket": "儲存貯體名稱",
"prefix": "路徑前綴(選填)",
"region": "AWS 區域",
"endpointUrl": "自訂端點 URL(選填)",
"initialContent": "初始內容(選填)"
},
"newCredentials": "輸入新的憑證",
"github": {
"privateHint": "私人儲存庫?連結你的 GitHub 帳號即可同步。",
"connectedHint": "私人儲存庫透過你的 GitHub 連線({{account}})同步。",
-37
View File
@@ -1666,14 +1666,6 @@
"link": "链接",
"urlLink": "URL 链接",
"repoUrl": "存储库 URL",
"reddit": {
"id": "客户端ID",
"secret": "客户端密钥",
"agent": "用户代理",
"searchQueries": "搜索查询",
"numberOfPosts": "帖子数量",
"addQuery": "添加查询"
},
"drag": {
"title": "将附件拖放到此处",
"description": "释放以上传您的附件"
@@ -1695,8 +1687,6 @@
"clear": "清除",
"ingestionFailed": "导入失败。"
},
"showAdvanced": "显示高级选项",
"hideAdvanced": "隐藏高级选项",
"ingestors": {
"local_file": {
"label": "上传文件",
@@ -1714,26 +1704,6 @@
"label": "GitHub",
"heading": "从GitHub添加内容"
},
"reddit": {
"label": "Reddit",
"heading": "从Reddit添加内容"
},
"google_drive": {
"label": "Google Drive",
"heading": "从Google Drive上传"
},
"s3": {
"label": "Amazon S3",
"heading": "从Amazon S3添加内容"
},
"share_point": {
"label": "SharePoint",
"heading": "从SharePoint上传"
},
"confluence": {
"label": "Confluence",
"heading": "从Confluence上传"
},
"wiki": {
"label": "新建 Wiki",
"heading": "创建动态 Wiki"
@@ -1787,15 +1757,8 @@
},
"fields": {
"url": "URL",
"awsAccessKeyId": "AWS 访问密钥 ID",
"awsSecretAccessKey": "AWS 秘密访问密钥",
"bucket": "存储桶名称",
"prefix": "路径前缀(可选)",
"region": "AWS 区域",
"endpointUrl": "自定义端点 URL(可选)",
"initialContent": "初始内容(可选)"
},
"newCredentials": "输入新的凭据",
"github": {
"privateHint": "私有仓库?连接你的 GitHub 账号即可同步。",
"connectedHint": "私有仓库通过你的 GitHub 连接({{account}})同步。",
+36 -90
View File
@@ -51,25 +51,6 @@ vi.mock('../connectors/useConnectorLauncher', () => ({
default: () => ({ launch, modals: null }),
}));
// The file picker reports its default account and a pick as it mounts.
vi.mock('../components/FilePicker', async () => {
const { useEffect } = await import('react');
return {
FilePicker: function Picker(props: {
onConnectionChange: (id: string | null) => void;
onSelectionChange: (files: string[], folders?: string[]) => void;
onFirstPickName: (name: string) => void;
}) {
useEffect(() => {
props.onConnectionChange('drive-1');
props.onSelectionChange(['file-1'], []);
props.onFirstPickName('Handbook');
}, []);
return null;
},
};
});
import Upload from './Upload';
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
@@ -308,92 +289,57 @@ describe('Upload source-type tiles', () => {
});
});
it('picks the saved account once connections finish loading', async () => {
connectorsState.catalog = [
{
key: 's3',
icon: 's3',
sync_ingestor: 's3',
auth_kind: 'api_key',
available: true,
missing_settings: [],
credential_fields: [
{ key: 'aws_access_key_id', label: 'Access key ID', secret: false },
],
},
];
const props = {
receivedFile: [],
setModalState: vi.fn(),
isOnboarding: false,
renderTab: null,
close: vi.fn(),
initialIngestor: 's3' as const,
};
await act(async () => root.render(<Upload {...props} />));
const triggers = () =>
Array.from(
document.body.querySelectorAll('[data-slot="select-trigger"]'),
).map((el) => el.textContent);
expect(triggers()).not.toContain('modals.uploadDoc.newCredentials');
connectorsState.connections = [
{
id: 'k1',
connector_key: 's3',
status: 'connected',
account_label: '…WXYZ',
},
];
await act(async () => root.render(<Upload {...props} />));
expect(triggers()).toContain('settings.connectors.detail.keyEnding');
connectorsState.catalog = [];
connectorsState.connections = [];
});
it("sends the picker's account, not one cleared by the type change", async () => {
connectorsState.catalog = [
{
key: 'google_drive',
icon: 'drive',
sync_ingestor: 'google_drive',
auth_kind: 'oauth',
available: true,
missing_settings: [],
},
];
const sent: FormData[] = [];
it('sends a crawler source to the remote ingest with its URL', async () => {
const sent: { url: string; body: FormData }[] = [];
class FakeXhr {
upload = { addEventListener() {} };
url = '';
addEventListener() {}
open() {}
open(_method: string, url: string) {
this.url = url;
}
setRequestHeader() {}
send(body: FormData) {
sent.push(body);
sent.push({ url: this.url, body });
}
}
vi.stubGlobal('XMLHttpRequest', FakeXhr);
await act(async () =>
root.render(
<Upload
receivedFile={[]}
setModalState={vi.fn()}
isOnboarding={false}
renderTab={null}
close={vi.fn()}
initialIngestor="google_drive"
/>,
),
await render();
const crawler = tiles().find((tile) =>
tile.textContent?.includes('modals.uploadDoc.ingestors.crawler.label'),
)!;
await act(async () => crawler.click());
const type = async (input: HTMLInputElement, value: string) => {
const setter = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'value',
)!.set!;
await act(async () => {
setter.call(input, value);
input.dispatchEvent(new Event('input', { bubbles: true }));
});
};
const inputs = Array.from(
document.body.querySelectorAll<HTMLInputElement>('input[type="text"]'),
);
// The source's name first, then the crawler's URL field.
await type(inputs[0], 'Docs site');
await type(
document.body.querySelector<HTMLInputElement>('input[name="url"]')!,
'https://docs.example',
);
const train = Array.from(document.body.querySelectorAll('button')).find(
(b) => b.textContent === 'modals.uploadDoc.train',
)!;
expect(train.disabled).toBe(false);
await act(async () => train.click());
expect(sent).toHaveLength(1);
expect(JSON.parse(String(sent[0].get('data'))).connection_id).toBe(
'drive-1',
);
expect(sent[0].url).toContain('/api/remote');
expect(sent[0].body.get('source')).toBe('crawler');
expect(JSON.parse(String(sent[0].body.get('data')))).toEqual({
url: 'https://docs.example',
});
vi.unstubAllGlobals();
connectorsState.catalog = [];
});
it('leaves the disabled Train button on the default variant', async () => {
+15 -446
View File
@@ -1,7 +1,6 @@
import { ChevronLeft, FileText, Lock, Plug } from 'lucide-react';
import { envVar } from '@/env';
import { cn } from '@/lib/utils';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { nanoid } from '@reduxjs/toolkit';
import type { FileRejection } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
@@ -14,20 +13,9 @@ import { Alert, AlertDescription } from '../components/ui/alert';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { FormField as UiFormField } from '../components/ui/form-field';
import { Label } from '../components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '../components/ui/select';
import { Switch } from '../components/ui/switch';
import { Textarea } from '../components/ui/textarea';
import { Modal } from '../components/ui/modal';
import { Separator } from '../components/ui/separator';
import { OptionCard } from '../components/ui/option-card';
import ConnectorSetupNotice from '../connectors/ConnectorSetupNotice';
import useConnectorLauncher from '../connectors/useConnectorLauncher';
import { connectorName } from '../connectors/i18n';
import { intlLocale } from '../utils/dateTimeUtils';
@@ -53,7 +41,6 @@ import {
setSourceDocs,
} from '../preferences/preferenceSlice';
import {
CONNECTION_INGESTORS,
IngestorDefaultConfigs,
IngestorFormSchemas,
getIngestorSchema,
@@ -64,8 +51,6 @@ import { addUploadTask, updateUploadTask } from './uploadSlice';
import { FormField, IngestorConfig, IngestorType } from './types/ingestor';
import { FilePicker } from '../components/FilePicker';
import GoogleDrivePicker from '../components/GoogleDrivePicker';
import { FILE_UPLOAD_ACCEPT } from '../constants/fileUpload';
import RetrievalOptions, {
DEFAULT_RETRIEVAL_OPTIONS,
@@ -86,7 +71,6 @@ function Upload({
close,
onSuccessfulUpload = () => undefined,
selectUploadedDoc = true,
initialIngestor,
}: {
receivedFile: File[];
setModalState: (state: ActiveState) => void;
@@ -101,8 +85,6 @@ function Upload({
* uploading never repoints the conversation the user left open.
*/
selectUploadedDoc?: boolean;
/** Open straight on this source type's form (Connect from a connector card). */
initialIngestor?: IngestorType;
}) {
const token = useSelector(selectToken);
const selectedDocs = useSelector(selectSelectedDocs);
@@ -124,82 +106,18 @@ function Upload({
// an unaccepted type), shown under the dropzone.
const [rejectedFiles, setRejectedFiles] = useState<string[]>([]);
const [activeTab, setActiveTab] = useState<boolean>(true);
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const [retrievalOptions, setRetrievalOptions] =
useState<RetrievalOptionsValue>(DEFAULT_RETRIEVAL_OPTIONS);
const { graphRAGAvailable, hybridAvailable, availableModels } =
useRetrievalAvailability(token);
// File picker state
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
const [selectedFolders, setSelectedFolders] = useState<string[]>([]);
// The connection (account) the source syncs from. Pickers report it; S3
// and Reddit pick it here ('' means "enter new credentials").
const [connectionId, setConnectionId] = useState<string | null>(null);
const renderFormFields = () => {
if (!ingestor.type) return null;
const ingestorSchema = getIngestorSchema(ingestor.type as IngestorType);
if (!ingestorSchema) return null;
const schema: FormField[] = ingestorSchema.fields;
const generalFields = schema.filter(
(field: FormField) =>
!field.advanced && !(usingSavedKeys && credentialKeys.has(field.name)),
);
const advancedFields = schema.filter((field: FormField) => field.advanced);
return (
<div className="flex flex-col gap-5">
{keyAccounts.length > 0 && (
<UiFormField label={t('filePicker.account')}>
<Select
value={connectionId ?? 'new'}
onValueChange={(value) => {
accountPicked.current = true;
setConnectionId(value);
}}
>
<SelectTrigger className="w-full" size="field" shape="pill">
<SelectValue />
</SelectTrigger>
<SelectContent>
{keyAccounts.map((account) => (
<SelectItem key={account.id} value={account.id}>
{t('settings.connectors.detail.keyEnding', {
hint: account.account_label,
interpolation: { escapeValue: false },
})}
</SelectItem>
))}
<SelectItem value="new">
{t('modals.uploadDoc.newCredentials')}
</SelectItem>
</SelectContent>
</Select>
</UiFormField>
)}
<div className="flex flex-col gap-5">
{generalFields.map((field: FormField) => renderField(field))}
</div>
{advancedFields.length > 0 && (
<div
className={cn(
'grid transition-[grid-template-rows,opacity] duration-300 ease-out',
showAdvancedOptions
? 'grid-rows-[1fr] opacity-100'
: 'grid-rows-[0fr] opacity-0',
)}
>
<div className="flex flex-col gap-4 overflow-hidden">
<Separator className="my-4" />
<div className="flex flex-col gap-5">
{advancedFields.map((field: FormField) => renderField(field))}
</div>
</div>
</div>
)}
{ingestorSchema.fields.map((field: FormField) => renderField(field))}
</div>
);
};
@@ -227,81 +145,6 @@ function Upload({
required={isRequired}
/>
);
case 'number':
return (
<Input
key={field.name}
label={fieldLabel}
type="number"
name={field.name}
value={String(
ingestor.config[field.name as keyof typeof ingestor.config],
)}
onChange={(e) =>
handleIngestorChange(
field.name as keyof IngestorConfig['config'],
Number(e.target.value),
)
}
required={isRequired}
/>
);
case 'enum': {
const currentValue = String(
ingestor.config[field.name as keyof typeof ingestor.config] ?? '',
);
return (
<UiFormField
key={field.name}
label={fieldLabel}
required={isRequired}
>
<Select
value={currentValue || undefined}
onValueChange={(value) => {
handleIngestorChange(
field.name as keyof IngestorConfig['config'],
value,
);
}}
>
<SelectTrigger className="w-full" size="field" shape="pill">
<SelectValue placeholder={fieldLabel} />
</SelectTrigger>
<SelectContent>
{(field.options || []).map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</UiFormField>
);
}
case 'boolean':
return (
<div
key={field.name}
className="mt-2 flex flex-row items-center gap-3 text-base"
>
<Label htmlFor={`field-${field.name}`} className="text-foreground">
{fieldLabel}
</Label>
<Switch
id={`field-${field.name}`}
checked={Boolean(
ingestor.config[field.name as keyof typeof ingestor.config],
)}
onCheckedChange={(checked: boolean) => {
handleIngestorChange(
field.name as keyof IngestorConfig['config'],
checked,
);
}}
/>
</div>
);
case 'textarea':
return (
<UiFormField
@@ -371,98 +214,6 @@ function Upload({
)}
</div>
);
case 'remote_file_picker':
return (
<FilePicker
key={field.name}
onSelectionChange={(
selectedFileIds: string[],
selectedFolderIds: string[] = [],
) => {
setSelectedFiles(selectedFileIds);
setSelectedFolders(selectedFolderIds);
}}
onFirstPickName={prefillName}
onConnectionChange={setConnectionId}
provider={ingestor.type as unknown as string}
token={token}
initialSelectedFiles={selectedFiles}
initialSelectedFolders={selectedFolders}
/>
);
case 'google_drive_picker':
// Google's own picker needs the client id in the browser; without it
// the server-side file browser lists the same Drive.
return envVar('VITE_GOOGLE_CLIENT_ID') ? (
<GoogleDrivePicker
key={field.name}
onSelectionChange={(
selectedFileIds: string[],
selectedFolderIds: string[] = [],
) => {
setSelectedFiles(selectedFileIds);
setSelectedFolders(selectedFolderIds);
}}
onFirstPickName={prefillName}
onConnectionChange={setConnectionId}
token={token}
/>
) : (
<FilePicker
key={field.name}
onSelectionChange={(
selectedFileIds: string[],
selectedFolderIds: string[] = [],
) => {
setSelectedFiles(selectedFileIds);
setSelectedFolders(selectedFolderIds);
}}
onFirstPickName={prefillName}
onConnectionChange={setConnectionId}
provider="google_drive"
token={token}
initialSelectedFiles={selectedFiles}
initialSelectedFolders={selectedFolders}
/>
);
case 'share_point_picker':
return (
<FilePicker
key={field.name}
onSelectionChange={(
selectedFileIds: string[],
selectedFolderIds: string[] = [],
) => {
setSelectedFiles(selectedFileIds);
setSelectedFolders(selectedFolderIds);
}}
onFirstPickName={prefillName}
onConnectionChange={setConnectionId}
provider="share_point"
token={token}
initialSelectedFiles={selectedFiles}
initialSelectedFolders={selectedFolders}
/>
);
case 'confluence_picker':
return (
<FilePicker
key={field.name}
onSelectionChange={(
selectedFileIds: string[],
selectedFolderIds: string[] = [],
) => {
setSelectedFiles(selectedFileIds);
setSelectedFolders(selectedFolderIds);
}}
onFirstPickName={prefillName}
onConnectionChange={setConnectionId}
provider="confluence"
token={token}
initialSelectedFiles={selectedFiles}
initialSelectedFolders={selectedFolders}
/>
);
default:
return null;
}
@@ -484,50 +235,6 @@ function Upload({
if (!connectorsLoaded) dispatch(loadConnectors({ token }));
}, [connectorsLoaded, dispatch, token]);
useEffect(() => {
if (initialIngestor) handleIngestorTypeChange(initialIngestor);
// Only the type the modal opened with; later picks are the user's.
}, []);
/** The name field follows the first picked item until the user edits it. */
const prefillName = (name: string) => {
if (nameTouched || !name) return;
setIngestor((prev) => (prev.name ? prev : { ...prev, name }));
};
const connectorFor = (type: IngestorType | null) =>
type && CONNECTION_INGESTORS.includes(type)
? connectorCatalog.find((c) => c.sync_ingestor === type)
: undefined;
const selectedConnector = connectorFor(ingestor.type);
const needsSetup = !!selectedConnector && !selectedConnector.available;
// S3 and Reddit keep their keys on a connection: once an account is
// chosen the key fields go away ("enter secrets once").
const keyAccounts =
selectedConnector?.auth_kind === 'api_key'
? connections.filter(
(c) =>
c.connector_key === selectedConnector.key &&
c.status === 'connected',
)
: [];
const credentialKeys = new Set(
(selectedConnector?.credential_fields ?? []).map((f) => f.key),
);
const usingSavedKeys =
keyAccounts.length > 0 && !!connectionId && connectionId !== 'new';
// Default to the first saved account, also when the connections arrive
// after the modal opened on S3 or Reddit, but never over a choice the
// user made in the account picker.
const accountPicked = useRef(false);
const firstKeyAccount = keyAccounts[0]?.id;
useEffect(() => {
if (selectedConnector?.auth_kind !== 'api_key' || accountPicked.current)
return;
setConnectionId(firstKeyAccount ?? 'new');
}, [ingestor.type, selectedConnector?.auth_kind, firstKeyAccount]);
const ingestorOptions: IngestorOption[] = IngestorFormSchemas.map(
(schema) => ({
label: schema.label,
@@ -541,9 +248,6 @@ function Upload({
setIngestor({ type: null, name: '', config: {} });
setfiles([]);
setRejectedFiles([]);
setSelectedFiles([]);
setSelectedFolders([]);
setShowAdvancedOptions(false);
setRetrievalOptions(DEFAULT_RETRIEVAL_OPTIONS);
setNameTouched(false);
}, []);
@@ -824,62 +528,11 @@ function Upload({
JSON.stringify(optionsToConfig(retrievalOptions)),
);
const ingestorSchema = getIngestorSchema(ingestor.type as IngestorType);
if (!ingestorSchema) {
handleTaskFailure(clientTaskId);
return;
}
const schema: FormField[] = ingestorSchema.fields;
const hasLocalFilePicker = schema.some(
(field: FormField) => field.type === 'local_file_picker',
);
const hasRemoteFilePicker = schema.some(
(field: FormField) => field.type === 'remote_file_picker',
);
const hasGoogleDrivePicker = schema.some(
(field: FormField) => field.type === 'google_drive_picker',
);
const hasSharePointPicker = schema.some(
(field: FormField) => field.type === 'share_point_picker',
);
const hasConfluencePicker = schema.some(
(field: FormField) => field.type === 'confluence_picker',
);
let configData: Record<string, unknown> = { ...ingestor.config };
if (hasLocalFilePicker) {
files.forEach((file) => {
formData.append('file', file);
});
} else if (
hasRemoteFilePicker ||
hasGoogleDrivePicker ||
hasSharePointPicker ||
hasConfluencePicker
) {
configData = {
provider: ingestor.type as string,
connection_id: connectionId,
file_ids: selectedFiles,
folder_ids: selectedFolders,
};
}
if (usingSavedKeys) {
configData = Object.fromEntries(
Object.entries(configData).filter(([key]) => !credentialKeys.has(key)),
);
configData.connection_id = connectionId;
}
formData.append('data', JSON.stringify(configData));
formData.append('data', JSON.stringify(ingestor.config));
const apiHost: string = envVar('VITE_API_HOST');
const endpoint =
ingestor.type === 'local_file'
? `${apiHost}/api/upload`
: `${apiHost}/api/remote`;
// Local files go through uploadFile; everything else is fetched remotely.
const endpoint = `${apiHost}/api/remote`;
const xhr = new XMLHttpRequest();
@@ -1041,72 +694,14 @@ function Upload({
if (!isPrescreenConfigValid(retrievalOptions)) return true;
if (!ingestor.type) return true;
if (needsSetup) return true;
const ingestorSchemaForValidation = getIngestorSchema(
ingestor.type as IngestorType,
);
if (!ingestorSchemaForValidation) return true;
const schema: FormField[] = ingestorSchemaForValidation.fields;
const hasLocalFilePicker = schema.some(
(field: FormField) => field.type === 'local_file_picker',
);
const hasRemoteFilePicker = schema.some(
(field: FormField) => field.type === 'remote_file_picker',
);
const hasGoogleDrivePicker = schema.some(
(field: FormField) => field.type === 'google_drive_picker',
);
const hasSharePointPicker = schema.some(
(field: FormField) => field.type === 'share_point_picker',
);
const hasConfluencePicker = schema.some(
(field: FormField) => field.type === 'confluence_picker',
);
if (hasLocalFilePicker) {
if (files.length === 0) {
return true;
}
} else if (
hasRemoteFilePicker ||
hasGoogleDrivePicker ||
hasSharePointPicker ||
hasConfluencePicker
) {
if (selectedFiles.length === 0 && selectedFolders.length === 0) {
return true;
}
}
const ingestorSchemaForFields = getIngestorSchema(
ingestor.type as IngestorType,
);
if (!ingestorSchemaForFields) return false;
const formFields: FormField[] = ingestorSchemaForFields.fields;
for (const field of formFields) {
if (usingSavedKeys && credentialKeys.has(field.name)) continue;
if (field.required) {
// Validate only required fields
const value =
ingestor.config[field.name as keyof typeof ingestor.config];
if (typeof value === 'string' && !value.trim()) {
return true;
}
if (
typeof value === 'number' &&
(value === null || value === undefined || value <= 0)
) {
return true;
}
if (typeof value === 'boolean' && value === undefined) {
return true;
}
}
}
return false;
const schema = getIngestorSchema(ingestor.type);
if (!schema) return true;
return schema.fields.some((field: FormField) => {
if (field.type === 'local_file_picker') return files.length === 0;
if (!field.required) return false;
const value = ingestor.config[field.name];
return typeof value === 'string' && !value.trim();
});
};
const handleIngestorChange = (
key: keyof IngestorConfig['config'],
@@ -1121,11 +716,6 @@ function Upload({
}));
};
const handleIngestorTypeChange = (type: IngestorType | null) => {
// The account resets with the type, here rather than in an effect: the
// new type's picker reports its default account as it mounts, and an
// effect would run after that and clear it.
accountPicked.current = false;
setConnectionId(null);
if (type === null) {
setIngestor({
type: null,
@@ -1259,10 +849,6 @@ function Upload({
</Alert>
) : null;
const renderSetupNotice = () =>
selectedConnector && needsSetup ? (
<ConnectorSetupNotice connector={selectedConnector} />
) : null;
if (handedOver) return <>{connectModals}</>;
return (
@@ -1331,9 +917,9 @@ function Upload({
required={true}
className="w-full"
/>
{needsSetup ? renderSetupNotice() : renderFormFields()}
{renderFormFields()}
{ingestor.type === 'github' && renderGitHubHandOver()}
{ingestor.type !== 'wiki' && !needsSetup && (
{ingestor.type !== 'wiki' && (
<RetrievalOptions
value={retrievalOptions}
onChange={setRetrievalOptions}
@@ -1344,23 +930,6 @@ function Upload({
)}
</div>
)}
{ingestor.type &&
getIngestorSchema(ingestor.type as IngestorType)?.fields.some(
(field: FormField) => field.advanced,
) && (
<Button
type="button"
variant="link"
size="sm"
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
className="-ml-3 w-fit justify-start"
>
{showAdvancedOptions
? t('modals.uploadDoc.hideAdvanced')
: t('modals.uploadDoc.showAdvanced')}
</Button>
)}
</>
)}
</div>
+4 -218
View File
@@ -2,24 +2,9 @@ import CrawlerIcon from '../../assets/crawler.svg';
import FileUploadIcon from '../../assets/file_upload.svg';
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 S3Icon from '../../assets/s3.svg';
import SharePoint from '../../assets/sharepoint.svg';
import ConfluenceIcon from '../../assets/confluence.svg';
import BookIcon from '../../assets/book-mono.svg';
export type IngestorType =
| 'confluence'
| 'crawler'
| 'github'
| 'reddit'
| 'url'
| 'google_drive'
| 'local_file'
| 's3'
| 'share_point'
| 'wiki';
export type IngestorType = 'crawler' | 'github' | 'url' | 'local_file' | 'wiki';
export interface IngestorConfig {
type: IngestorType | null;
@@ -34,17 +19,7 @@ export type IngestorFormData = {
data: string;
};
export type FieldType =
| 'string'
| 'number'
| 'enum'
| 'boolean'
| 'textarea'
| 'local_file_picker'
| 'remote_file_picker'
| 'google_drive_picker'
| 'share_point_picker'
| 'confluence_picker';
export type FieldType = 'string' | 'textarea' | 'local_file_picker';
export interface FormField {
name: string;
@@ -53,8 +28,6 @@ export interface FormField {
labelKey?: string;
type: FieldType;
required?: boolean;
advanced?: boolean;
options?: { label: string; value: string }[];
}
export interface IngestorSchema {
@@ -66,9 +39,8 @@ export interface IngestorSchema {
}
/**
* Add Source tiles: what needs no account. The connection types sync from
* the Connectors page; their forms open only when a connector card hands
* one over (`initialIngestor`).
* Add knowledge tiles: the source types that need no account. Services
* that sync into Knowledge are connected through the connect wizard.
*/
export const UPLOAD_AND_WEB_INGESTORS: IngestorType[] = [
'local_file',
@@ -77,13 +49,6 @@ export const UPLOAD_AND_WEB_INGESTORS: IngestorType[] = [
'github',
'wiki',
];
export const CONNECTION_INGESTORS: IngestorType[] = [
'google_drive',
'share_point',
'confluence',
's3',
'reddit',
];
export const IngestorFormSchemas: IngestorSchema[] = [
{
@@ -145,141 +110,6 @@ export const IngestorFormSchemas: IngestorSchema[] = [
},
],
},
{
key: 'reddit',
label: 'Reddit',
icon: RedditIcon,
heading: 'Add content from Reddit',
fields: [
{
name: 'client_id',
label: 'Client ID',
labelKey: 'modals.uploadDoc.reddit.id',
type: 'string',
required: true,
},
{
name: 'client_secret',
label: 'Client Secret',
labelKey: 'modals.uploadDoc.reddit.secret',
type: 'string',
required: true,
},
{
name: 'user_agent',
label: 'User Agent',
labelKey: 'modals.uploadDoc.reddit.agent',
type: 'string',
required: true,
},
{
name: 'search_queries',
label: 'Search Queries',
labelKey: 'modals.uploadDoc.reddit.searchQueries',
type: 'string',
required: true,
},
{
name: 'number_posts',
label: 'Number of Posts',
labelKey: 'modals.uploadDoc.reddit.numberOfPosts',
type: 'number',
required: true,
},
],
},
{
key: 'google_drive',
label: 'Google Drive',
icon: DriveIcon,
heading: 'Upload from Google Drive',
fields: [
{
name: 'files',
label: 'Select Files from Google Drive',
type: 'google_drive_picker',
required: true,
},
],
},
{
key: 's3',
label: 'Amazon S3',
icon: S3Icon,
heading: 'Add content from Amazon S3',
fields: [
{
name: 'aws_access_key_id',
label: 'AWS Access Key ID',
labelKey: 'modals.uploadDoc.fields.awsAccessKeyId',
type: 'string',
required: true,
},
{
name: 'aws_secret_access_key',
label: 'AWS Secret Access Key',
labelKey: 'modals.uploadDoc.fields.awsSecretAccessKey',
type: 'string',
required: true,
},
{
name: 'bucket',
label: 'Bucket Name',
labelKey: 'modals.uploadDoc.fields.bucket',
type: 'string',
required: true,
},
{
name: 'prefix',
label: 'Path Prefix (optional)',
labelKey: 'modals.uploadDoc.fields.prefix',
type: 'string',
required: false,
},
{
name: 'region',
label: 'AWS Region',
labelKey: 'modals.uploadDoc.fields.region',
type: 'string',
required: false,
},
{
name: 'endpoint_url',
label: 'Custom Endpoint URL (optional)',
labelKey: 'modals.uploadDoc.fields.endpointUrl',
type: 'string',
required: false,
},
],
},
{
key: 'share_point',
label: 'Share Point',
icon: SharePoint,
heading: 'Upload from Share Point',
fields: [
{
name: 'files',
label: 'Select Files from Share Point',
type: 'share_point_picker',
required: true,
},
],
},
{
key: 'confluence',
label: 'Confluence',
icon: ConfluenceIcon,
heading: 'Upload from Confluence',
fields: [
{
name: 'files',
label: 'Select Pages from Confluence',
type: 'confluence_picker',
required: true,
},
],
},
{
key: 'wiki',
label: 'New wiki',
@@ -303,52 +133,8 @@ export const IngestorDefaultConfigs: Record<
> = {
crawler: { name: '', config: { url: '' } },
url: { name: '', config: { url: '' } },
reddit: {
name: '',
config: {
client_id: '',
client_secret: '',
user_agent: '',
search_queries: '',
number_posts: 10,
},
},
github: { name: '', config: { repo_url: '' } },
google_drive: {
name: '',
config: {
file_ids: '',
folder_ids: '',
recursive: true,
},
},
local_file: { name: '', config: { files: [] } },
s3: {
name: '',
config: {
aws_access_key_id: '',
aws_secret_access_key: '',
bucket: '',
prefix: '',
region: 'us-east-1',
endpoint_url: '',
},
},
share_point: {
name: '',
config: {
file_ids: '',
folder_ids: '',
recursive: true,
},
},
confluence: {
name: '',
config: {
file_ids: '',
folder_ids: '',
},
},
wiki: {
name: '',
config: {