mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-01-02 02:25:31 +08:00
Feat: Bitbucket connector (#12332)
### What problem does this PR solve? Feat: Bitbucket connector NOT READY TO MERGE ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
7
web/src/assets/svg/data-source/bitbucket.svg
Normal file
7
web/src/assets/svg/data-source/bitbucket.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="Bitbucket" role="img"
|
||||
viewBox="0 0 512 512"><rect
|
||||
width="512" height="512"
|
||||
rx="15%"
|
||||
fill="#ffffff"/><path fill="#2684ff" d="M422 130a10 10 0 00-9.9-11.7H100.5a10 10 0 00-10 11.7L136 409a10 10 0 009.9 8.4h221c5 0 9.2-3.5 10 -8.4L422 130zM291 316.8h-69.3l-18.7-98h104.8z"/><path fill="url(#a)" d="M59.632 25.2H40.94l-3.1 18.3h-13v18.9H52c1 0 1.7-.7 1.8-1.6l5.8-35.6z" transform="translate(89.8 85) scale(5.3285)"/><linearGradient id="a" x2="1" gradientTransform="rotate(141 22.239 22.239) scale(31.4)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient></svg>
|
||||
|
After Width: | Height: | Size: 803 B |
@ -947,6 +947,19 @@ Beispiel: Virtual Hosted Style`,
|
||||
'Laden Sie das OAuth-JSON hoch, das von der Google Console generiert wurde. Wenn es nur Client-Anmeldeinformationen enthält, führen Sie die browserbasierte Überprüfung einmal durch, um langlebige Refresh-Token zu erstellen.',
|
||||
dropboxDescription:
|
||||
'Verbinden Sie Ihre Dropbox, um Dateien und Ordner von einem ausgewählten Konto zu synchronisieren.',
|
||||
bitbucketDescription:
|
||||
'Bitbucket verbinden, um PR-Inhalte zu synchronisieren.',
|
||||
zendeskDescription:
|
||||
'Verbinden Sie Ihr Zendesk, um Tickets, Artikel und andere Inhalte zu synchronisieren.',
|
||||
bitbucketTopWorkspaceTip:
|
||||
'Der zu indizierende Bitbucket-Workspace (z. B. "atlassian" aus https://bitbucket.org/atlassian/workspace )',
|
||||
bitbucketWorkspaceTip:
|
||||
'Dieser Connector indiziert alle Repositories im Workspace.',
|
||||
bitbucketProjectsTip: 'Kommagetrennte Projekt-Keys, z. B.: PROJ1,PROJ2',
|
||||
bitbucketRepositorySlugsTip:
|
||||
'Kommagetrennte Repository-Slugs, z. B.: repo-one,repo-two',
|
||||
connectorNameTip:
|
||||
'Geben Sie einen aussagekräftigen Namen für den Connector an',
|
||||
boxDescription:
|
||||
'Verbinden Sie Ihr Box-Laufwerk, um Dateien und Ordner zu synchronisieren.',
|
||||
githubDescription:
|
||||
|
||||
@ -879,6 +879,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
|
||||
cropImage: 'Crop image',
|
||||
selectModelPlaceholder: 'Select model',
|
||||
configureModelTitle: 'Configure model',
|
||||
connectorNameTip: 'A descriptive name for the connector',
|
||||
confluenceIsCloudTip:
|
||||
'Check if this is a Confluence Cloud instance, uncheck for Confluence Server/Data Center',
|
||||
confluenceWikiBaseUrlTip:
|
||||
@ -923,7 +924,9 @@ Example: Virtual Hosted Style`,
|
||||
google_driveTokenTip:
|
||||
'Upload the OAuth token JSON generated from the OAuth helper or Google Cloud Console. You may also upload a client_secret JSON from an "installed" or "web" application. If this is your first sync, a browser window will open to complete the OAuth consent. If the JSON already contains a refresh token, it will be reused automatically.',
|
||||
google_drivePrimaryAdminTip:
|
||||
'Email address that has access to the Drive content being synced.',
|
||||
'Email address that has access to the Drive content being synced',
|
||||
zendeskDescription:
|
||||
'Connect your Zendesk to sync tickets, articles, and other content.',
|
||||
google_driveMyDriveEmailsTip:
|
||||
'Comma-separated emails whose "My Drive" contents should be indexed (include the primary admin).',
|
||||
google_driveSharedFoldersTip:
|
||||
@ -934,7 +937,16 @@ Example: Virtual Hosted Style`,
|
||||
'Upload the OAuth JSON generated from Google Console. If it only contains client credentials, run the browser-based verification once to mint long-lived refresh tokens.',
|
||||
dropboxDescription:
|
||||
'Connect your Dropbox to sync files and folders from a chosen account.',
|
||||
bitbucketDescription: 'Connect Bitbucket to sync PR content.',
|
||||
bitbucketTopWorkspaceTip:
|
||||
'The Bitbucket workspace to index (e.g., "atlassian" from https://bitbucket.org/atlassian/workspace ).',
|
||||
bitbucketRepositorySlugsTip:
|
||||
'Comma separated repository slugs. E.g., repo-one,repo-two',
|
||||
bitbucketProjectsTip: 'Comma separated project keys. E.g., PROJ1,PROJ2',
|
||||
bitbucketWorkspaceTip:
|
||||
'This connector will index all repositories in the workspace.',
|
||||
boxDescription: 'Connect your Box drive to sync files and folders.',
|
||||
|
||||
githubDescription:
|
||||
'Connect GitHub to sync pull requests and issues for retrieval.',
|
||||
airtableDescription:
|
||||
|
||||
@ -731,6 +731,7 @@ export default {
|
||||
newDocs: 'Новые документы',
|
||||
timeStarted: 'Время начала',
|
||||
log: 'Лог',
|
||||
connectorNameTip: 'Укажите понятное имя для коннектора',
|
||||
confluenceDescription:
|
||||
'Интегрируйте ваше рабочее пространство Confluence для поиска документации.',
|
||||
s3Description:
|
||||
@ -747,6 +748,18 @@ export default {
|
||||
'Синхронизируйте страницы и базы данных из Notion для извлечения знаний.',
|
||||
boxDescription:
|
||||
'Подключите ваш диск Box для синхронизации файлов и папок.',
|
||||
bitbucketDescription:
|
||||
'Подключите Bitbucket для синхронизации содержимого PR.',
|
||||
zendeskDescription:
|
||||
'Подключите Zendesk для синхронизации тикетов, статей и другого контента.',
|
||||
bitbucketTopWorkspaceTip:
|
||||
'Рабочее пространство Bitbucket для индексации (например, "atlassian" из https://bitbucket.org/atlassian/workspace )',
|
||||
bitbucketWorkspaceTip:
|
||||
'Этот коннектор проиндексирует все репозитории в рабочем пространстве.',
|
||||
bitbucketProjectsTip:
|
||||
'Ключи проектов через запятую, например: PROJ1,PROJ2',
|
||||
bitbucketRepositorySlugsTip:
|
||||
'Слоги репозиториев через запятую, например: repo-one,repo-two',
|
||||
githubDescription:
|
||||
'Подключите GitHub для синхронизации содержимого Pull Request и Issue для поиска.',
|
||||
airtableDescription:
|
||||
|
||||
@ -726,6 +726,16 @@ export default {
|
||||
view: '查看',
|
||||
modelsToBeAddedTooltip:
|
||||
'若您的模型供應商未列於此處,但宣稱與 OpenAI 相容,可透過選擇「OpenAI-API-compatible」卡片來設定相關模型。',
|
||||
dropboxDescription: '連接 Dropbox,同步指定帳號下的文件與文件夾。',
|
||||
bitbucketDescription: '連接 Bitbucket,同步 PR 內容。',
|
||||
zendeskDescription: '連接 Zendesk,同步工單、文章及其他內容。',
|
||||
bitbucketTopWorkspaceTip:
|
||||
'要索引的 Bitbucket 工作區(例如:https://bitbucket.org/atlassian/workspace 中的 "atlassian")',
|
||||
bitbucketWorkspaceTip: '此連接器將索引工作區下的所有倉庫。',
|
||||
bitbucketRepositorySlugsTip:
|
||||
'以英文逗號分隔的倉庫 slug,例如:repo-one,repo-two',
|
||||
bitbucketProjectsTip: '以英文逗號分隔的項目鍵,例如:PROJ1,PROJ2',
|
||||
connectorNameTip: '為連接器填寫一個有意義的名稱',
|
||||
},
|
||||
message: {
|
||||
registered: '註冊成功',
|
||||
|
||||
@ -53,6 +53,7 @@ export default {
|
||||
noData: '暂无数据',
|
||||
bedrockCredentialsHint:
|
||||
'提示:Access Key / Secret Key 可留空,以启用 AWS IAM 自动验证。',
|
||||
zendeskDescription: '连接 Zendesk,同步工单、文章及其他内容。',
|
||||
promptPlaceholder: '请输入或使用 / 快速插入变量。',
|
||||
selected: '已选择',
|
||||
},
|
||||
@ -864,6 +865,14 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
'请上传由 Google Console 生成的 OAuth JSON。如果仅包含 client credentials,请通过浏览器授权一次以获取长期有效的刷新 Token。',
|
||||
dropboxDescription: '连接 Dropbox,同步指定账号下的文件与文件夹。',
|
||||
boxDescription: '连接你的 Box 云盘以同步文件和文件夹。',
|
||||
bitbucketDescription: '连接 Bitbucket,同步 PR 内容。',
|
||||
bitbucketTopWorkspaceTip:
|
||||
'要索引的 Bitbucket 工作区(例如:https://bitbucket.org/atlassian/workspace 中的 "atlassian")',
|
||||
bitbucketWorkspaceTip: '该连接器将索引工作区下的所有仓库。',
|
||||
bitbucketProjectsTip: '用英文逗号分隔的项目 key,例如:PROJ1,PROJ2',
|
||||
bitbucketRepositorySlugsTip:
|
||||
'用英文逗号分隔的仓库 slug,例如:repo-one,repo-two',
|
||||
connectorNameTip: '为连接器命名',
|
||||
githubDescription:
|
||||
'连接 GitHub,可同步 Pull Request 与 Issue 内容用于检索。',
|
||||
airtableDescription: '连接 Airtable,同步指定工作区下指定表格中的文件。',
|
||||
|
||||
@ -1,247 +0,0 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Segmented } from '@/components/ui/segmented';
|
||||
import { t } from 'i18next';
|
||||
|
||||
// UI-only auth modes for S3
|
||||
// access_key: Access Key ID + Secret
|
||||
// iam_role: only Role ARN
|
||||
// assume_role: no input fields (uses environment role)
|
||||
type AuthMode = 'access_key' | 'iam_role' | 'assume_role';
|
||||
type BlobMode = 's3' | 's3_compatible';
|
||||
|
||||
const modeOptions = [
|
||||
{ label: 'S3', value: 's3' },
|
||||
{ label: 'S3 Compatible', value: 's3_compatible' },
|
||||
];
|
||||
|
||||
const authOptions = [
|
||||
{ label: 'Access Key', value: 'access_key' },
|
||||
{ label: 'IAM Role', value: 'iam_role' },
|
||||
{ label: 'Assume Role', value: 'assume_role' },
|
||||
];
|
||||
|
||||
const addressingOptions = [
|
||||
{ label: 'Virtual Hosted Style', value: 'virtual' },
|
||||
{ label: 'Path Style', value: 'path' },
|
||||
];
|
||||
|
||||
const deriveInitialAuthMode = (credentials: any): AuthMode => {
|
||||
const authMethod = credentials?.authentication_method;
|
||||
if (authMethod === 'iam_role') return 'iam_role';
|
||||
if (authMethod === 'assume_role') return 'assume_role';
|
||||
if (credentials?.aws_role_arn) return 'iam_role';
|
||||
if (credentials?.aws_access_key_id || credentials?.aws_secret_access_key)
|
||||
return 'access_key';
|
||||
return 'access_key';
|
||||
};
|
||||
|
||||
const deriveInitialMode = (bucketType?: string): BlobMode =>
|
||||
bucketType === 's3_compatible' ? 's3_compatible' : 's3';
|
||||
|
||||
const BlobTokenField = () => {
|
||||
const form = useFormContext();
|
||||
const credentials = form.watch('config.credentials');
|
||||
const watchedBucketType = form.watch('config.bucket_type');
|
||||
|
||||
const [mode, setMode] = useState<BlobMode>(
|
||||
deriveInitialMode(watchedBucketType),
|
||||
);
|
||||
const [authMode, setAuthMode] = useState<AuthMode>(() =>
|
||||
deriveInitialAuthMode(credentials),
|
||||
);
|
||||
|
||||
// Keep bucket_type in sync with UI mode
|
||||
useEffect(() => {
|
||||
const nextMode = deriveInitialMode(watchedBucketType);
|
||||
setMode((prev) => (prev === nextMode ? prev : nextMode));
|
||||
}, [watchedBucketType]);
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue('config.bucket_type', mode, { shouldDirty: true });
|
||||
// Default addressing style for compatible mode
|
||||
if (
|
||||
mode === 's3_compatible' &&
|
||||
!form.getValues('config.credentials.addressing_style')
|
||||
) {
|
||||
form.setValue('config.credentials.addressing_style', 'virtual', {
|
||||
shouldDirty: false,
|
||||
});
|
||||
}
|
||||
if (mode === 's3_compatible' && authMode !== 'access_key') {
|
||||
setAuthMode('access_key');
|
||||
}
|
||||
// Persist authentication_method for backend
|
||||
const nextAuthMethod: AuthMode =
|
||||
mode === 's3_compatible' ? 'access_key' : authMode;
|
||||
form.setValue('config.credentials.authentication_method', nextAuthMethod, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
// Clear errors for fields that are not relevant in the current mode/auth selection
|
||||
const inactiveFields: string[] = [];
|
||||
if (mode === 's3_compatible') {
|
||||
inactiveFields.push('config.credentials.aws_role_arn');
|
||||
} else {
|
||||
if (authMode === 'iam_role') {
|
||||
inactiveFields.push('config.credentials.aws_access_key_id');
|
||||
inactiveFields.push('config.credentials.aws_secret_access_key');
|
||||
}
|
||||
if (authMode === 'assume_role') {
|
||||
inactiveFields.push('config.credentials.aws_access_key_id');
|
||||
inactiveFields.push('config.credentials.aws_secret_access_key');
|
||||
inactiveFields.push('config.credentials.aws_role_arn');
|
||||
}
|
||||
}
|
||||
if (inactiveFields.length) {
|
||||
form.clearErrors(inactiveFields as any);
|
||||
}
|
||||
}, [form, mode, authMode]);
|
||||
|
||||
const isS3 = mode === 's3';
|
||||
const requiresAccessKey =
|
||||
authMode === 'access_key' || mode === 's3_compatible';
|
||||
const requiresRoleArn = isS3 && authMode === 'iam_role';
|
||||
|
||||
// Help text for assume role (no inputs)
|
||||
const assumeRoleNote = useMemo(
|
||||
() => t('No credentials required. Uses the default environment role.'),
|
||||
[t],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-sm text-text-secondary">Mode</div>
|
||||
<Segmented
|
||||
options={modeOptions}
|
||||
value={mode}
|
||||
onChange={(val) => setMode(val as BlobMode)}
|
||||
className="w-full"
|
||||
itemClassName="flex-1 justify-center"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isS3 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-sm text-text-secondary">Authentication</div>
|
||||
<Segmented
|
||||
options={authOptions}
|
||||
value={authMode}
|
||||
onChange={(val) => setAuthMode(val as AuthMode)}
|
||||
className="w-full"
|
||||
itemClassName="flex-1 justify-center"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{requiresAccessKey && (
|
||||
<RAGFlowFormItem
|
||||
name="config.credentials.aws_access_key_id"
|
||||
label="AWS Access Key ID"
|
||||
required={requiresAccessKey}
|
||||
rules={{
|
||||
validate: (val) =>
|
||||
requiresAccessKey
|
||||
? Boolean(val) || 'Access Key ID is required'
|
||||
: true,
|
||||
}}
|
||||
>
|
||||
{(field) => (
|
||||
<Input {...field} placeholder="AKIA..." autoComplete="off" />
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
|
||||
{requiresAccessKey && (
|
||||
<RAGFlowFormItem
|
||||
name="config.credentials.aws_secret_access_key"
|
||||
label="AWS Secret Access Key"
|
||||
required={requiresAccessKey}
|
||||
rules={{
|
||||
validate: (val) =>
|
||||
requiresAccessKey
|
||||
? Boolean(val) || 'Secret Access Key is required'
|
||||
: true,
|
||||
}}
|
||||
>
|
||||
{(field) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="password"
|
||||
placeholder="****************"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
|
||||
{requiresRoleArn && (
|
||||
<RAGFlowFormItem
|
||||
name="config.credentials.aws_role_arn"
|
||||
label="Role ARN"
|
||||
required={requiresRoleArn}
|
||||
tooltip="The role will be assumed by the runtime environment."
|
||||
rules={{
|
||||
validate: (val) =>
|
||||
requiresRoleArn ? Boolean(val) || 'Role ARN is required' : true,
|
||||
}}
|
||||
>
|
||||
{(field) => (
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="arn:aws:iam::123456789012:role/YourRole"
|
||||
autoComplete="off"
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
|
||||
{isS3 && authMode === 'assume_role' && (
|
||||
<div className="text-sm text-text-secondary bg-bg-card border border-border-button rounded-md px-3 py-2">
|
||||
{assumeRoleNote}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 's3_compatible' && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<RAGFlowFormItem
|
||||
name="config.credentials.addressing_style"
|
||||
label="Addressing Style"
|
||||
tooltip={t('setting.S3CompatibleAddressingStyleTip')}
|
||||
required={false}
|
||||
>
|
||||
{(field) => (
|
||||
<SelectWithSearch
|
||||
triggerClassName="!shrink"
|
||||
options={addressingOptions}
|
||||
value={field.value || 'virtual'}
|
||||
onChange={(val) => field.onChange(val)}
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
|
||||
<RAGFlowFormItem
|
||||
name="config.credentials.endpoint_url"
|
||||
label="Endpoint URL"
|
||||
required={false}
|
||||
tooltip={t('setting.S3CompatibleEndpointUrlTip')}
|
||||
>
|
||||
{(field) => (
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="https://fsn1.your-objectstorage.com"
|
||||
autoComplete="off"
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlobTokenField;
|
||||
@ -131,7 +131,6 @@ const BoxTokenField = ({ value, onChange }: BoxTokenFieldProps) => {
|
||||
|
||||
const finalValue: Record<string, any> = {
|
||||
...rest,
|
||||
// 确保客户端配置字段有值(优先后端返回,其次当前输入)
|
||||
client_id: rest.client_id ?? clientId.trim(),
|
||||
client_secret: rest.client_secret ?? clientSecret.trim(),
|
||||
};
|
||||
@ -146,8 +145,6 @@ const BoxTokenField = ({ value, onChange }: BoxTokenFieldProps) => {
|
||||
finalValue.authorization_code = code;
|
||||
}
|
||||
|
||||
// access_token / refresh_token 由后端返回,已在 ...rest 中带上,无需额外 state
|
||||
|
||||
onChange(JSON.stringify(finalValue));
|
||||
message.success('Box authorization completed.');
|
||||
clearWebState();
|
||||
|
||||
@ -1,200 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ControllerRenderProps, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
/* ---------------- Token Field ---------------- */
|
||||
|
||||
export type ConfluenceTokenFieldProps = ControllerRenderProps & {
|
||||
fieldType: 'username' | 'token';
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const ConfluenceTokenField = ({
|
||||
fieldType,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
disabled,
|
||||
...rest
|
||||
}: ConfluenceTokenFieldProps) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<Input
|
||||
className="w-full"
|
||||
type={fieldType === 'token' ? 'password' : 'text'}
|
||||
value={value ?? ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={
|
||||
placeholder ||
|
||||
(fieldType === 'token'
|
||||
? 'Enter your Confluence access token'
|
||||
: 'Confluence username or email')
|
||||
}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------------- Indexing Mode Field ---------------- */
|
||||
|
||||
type ConfluenceIndexingMode = 'everything' | 'space' | 'page';
|
||||
|
||||
export type ConfluenceIndexingModeFieldProps = ControllerRenderProps;
|
||||
|
||||
export const ConfluenceIndexingModeField = (
|
||||
fieldProps: ControllerRenderProps,
|
||||
) => {
|
||||
const { value, onChange, disabled } = fieldProps;
|
||||
const [mode, setMode] = useState<ConfluenceIndexingMode>(
|
||||
value || 'everything',
|
||||
);
|
||||
const { watch, setValue } = useFormContext();
|
||||
|
||||
useEffect(() => setMode(value), [value]);
|
||||
|
||||
const spaceValue = watch('config.space');
|
||||
const pageIdValue = watch('config.page_id');
|
||||
const indexRecursively = watch('config.index_recursively');
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) onChange('everything');
|
||||
}, [value, onChange]);
|
||||
|
||||
const handleModeChange = useCallback(
|
||||
(nextMode?: string) => {
|
||||
let normalized: ConfluenceIndexingMode = 'everything';
|
||||
if (nextMode) {
|
||||
normalized = nextMode as ConfluenceIndexingMode;
|
||||
setMode(normalized);
|
||||
onChange(normalized);
|
||||
} else {
|
||||
setMode(mode);
|
||||
normalized = mode;
|
||||
onChange(mode);
|
||||
// onChange(mode);
|
||||
}
|
||||
if (normalized === 'everything') {
|
||||
setValue('config.space', '');
|
||||
setValue('config.page_id', '');
|
||||
setValue('config.index_recursively', false);
|
||||
} else if (normalized === 'space') {
|
||||
setValue('config.page_id', '');
|
||||
setValue('config.index_recursively', false);
|
||||
} else if (normalized === 'page') {
|
||||
setValue('config.space', '');
|
||||
}
|
||||
},
|
||||
[mode, onChange, setValue],
|
||||
);
|
||||
|
||||
const debouncedHandleChange = useMemo(
|
||||
() =>
|
||||
debounce(() => {
|
||||
handleModeChange();
|
||||
}, 300),
|
||||
[handleModeChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-lg border border-border-button bg-bg-card p-4 space-y-4">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-text-secondary">
|
||||
{INDEX_MODE_OPTIONS.map((option) => {
|
||||
const isActive = option.value === mode;
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => handleModeChange(option.value)}
|
||||
className={cn(
|
||||
'flex-1 rounded-lg border px-3 py-2 transition-all',
|
||||
'border-transparent bg-transparent text-text-secondary hover:border-border-button hover:bg-bg-card-secondary',
|
||||
isActive &&
|
||||
'border-border-button bg-background text-primary shadow-sm',
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{mode === 'everything' && (
|
||||
<p className="text-sm text-text-secondary">
|
||||
This connector will index all pages the provided credentials have
|
||||
access to.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{mode === 'space' && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-semibold text-text-primary">
|
||||
Space Key
|
||||
</div>
|
||||
<Input
|
||||
className="w-full"
|
||||
value={spaceValue ?? ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setValue('config.space', value);
|
||||
debouncedHandleChange();
|
||||
}}
|
||||
placeholder="e.g. KB"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<p className="text-xs text-text-secondary">
|
||||
The Confluence space key to index.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'page' && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-semibold text-text-primary">Page ID</div>
|
||||
<Input
|
||||
className="w-full"
|
||||
value={pageIdValue ?? ''}
|
||||
onChange={(e) => {
|
||||
setValue('config.page_id', e.target.value);
|
||||
debouncedHandleChange();
|
||||
}}
|
||||
placeholder="e.g. 123456"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<p className="text-xs text-text-secondary">
|
||||
The Confluence page ID to index.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<Checkbox
|
||||
checked={Boolean(indexRecursively)}
|
||||
onCheckedChange={(checked) => {
|
||||
setValue('config.index_recursively', Boolean(checked));
|
||||
debouncedHandleChange();
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<span className="text-sm text-text-secondary">
|
||||
Index child pages recursively
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const INDEX_MODE_OPTIONS = [
|
||||
{ label: 'Everything', value: 'everything' },
|
||||
{ label: 'Space', value: 'space' },
|
||||
{ label: 'Page', value: 'page' },
|
||||
];
|
||||
|
||||
export default ConfluenceTokenField;
|
||||
@ -0,0 +1,83 @@
|
||||
import { FilterFormField, FormFieldType } from '@/components/dynamic-form';
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
export const bitbucketConstant = (t: TFunction) => [
|
||||
{
|
||||
label: 'Bitbucket Account Email',
|
||||
name: 'config.credentials.bitbucket_account_email',
|
||||
type: FormFieldType.Email,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: 'Bitbucket API Token',
|
||||
name: 'config.credentials.bitbucket_api_token',
|
||||
type: FormFieldType.Password,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: 'Workspace',
|
||||
name: 'config.workspace',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
tooltip: t('setting.bitbucketTopWorkspaceTip'),
|
||||
},
|
||||
{
|
||||
label: 'Index Mode',
|
||||
name: 'config.index_mode',
|
||||
type: FormFieldType.Segmented,
|
||||
options: [
|
||||
{ label: 'Repositories', value: 'repositories' },
|
||||
{ label: 'Project(s)', value: 'projects' },
|
||||
{ label: 'Workspace', value: 'workspace' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Repository Slugs',
|
||||
name: 'config.repository_slugs',
|
||||
type: FormFieldType.Text,
|
||||
customValidate: (val: string, formValues: any) => {
|
||||
const index_mode = formValues?.config?.index_mode;
|
||||
if (!val && index_mode === 'repositories') {
|
||||
return 'Repository Slugs is required';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
shouldRender: (formValues: any) => {
|
||||
const index_mode = formValues?.config?.index_mode;
|
||||
return index_mode === 'repositories';
|
||||
},
|
||||
tooltip: t('setting.bitbucketRepositorySlugsTip'),
|
||||
},
|
||||
{
|
||||
label: 'Projects',
|
||||
name: 'config.projects',
|
||||
type: FormFieldType.Text,
|
||||
customValidate: (val: string, formValues: any) => {
|
||||
const index_mode = formValues?.config?.index_mode;
|
||||
if (!val && index_mode === 'projects') {
|
||||
return 'Projects is required';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
shouldRender: (formValues: any) => {
|
||||
const index_mode = formValues?.config?.index_mode;
|
||||
console.log('formValues.config', formValues?.config);
|
||||
return index_mode === 'projects';
|
||||
},
|
||||
tooltip: t('setting.bitbucketProjectsTip'),
|
||||
},
|
||||
{
|
||||
name: FilterFormField + '.tip',
|
||||
label: ' ',
|
||||
type: FormFieldType.Custom,
|
||||
shouldRender: (formValues: any) => {
|
||||
const index_mode = formValues?.config?.index_mode;
|
||||
return index_mode === 'workspace';
|
||||
},
|
||||
render: () => (
|
||||
<div className="text-sm text-text-secondary bg-bg-card border border-border-button rounded-md px-3 py-2">
|
||||
{t('setting.bitbucketWorkspaceTip')}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
@ -0,0 +1,121 @@
|
||||
import { FilterFormField, FormFieldType } from '@/components/dynamic-form';
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
export const confluenceConstant = (t: TFunction) => [
|
||||
{
|
||||
label: 'Confluence Username',
|
||||
name: 'config.credentials.confluence_username',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
tooltip: t('setting.connectorNameTip'),
|
||||
},
|
||||
{
|
||||
label: 'Confluence Access Token',
|
||||
name: 'config.credentials.confluence_access_token',
|
||||
type: FormFieldType.Password,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: 'Wiki Base URL',
|
||||
name: 'config.wiki_base',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
tooltip: t('setting.confluenceWikiBaseUrlTip'),
|
||||
},
|
||||
{
|
||||
label: 'Is Cloud',
|
||||
name: 'config.is_cloud',
|
||||
type: FormFieldType.Checkbox,
|
||||
required: false,
|
||||
tooltip: t('setting.confluenceIsCloudTip'),
|
||||
},
|
||||
{
|
||||
label: 'Index Mode',
|
||||
name: 'config.index_mode',
|
||||
type: FormFieldType.Segmented,
|
||||
options: [
|
||||
{ label: 'Everything', value: 'everything' },
|
||||
{ label: 'Space', value: 'space' },
|
||||
{ label: 'Page', value: 'page' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'config.page_id',
|
||||
label: 'Page ID',
|
||||
type: FormFieldType.Text,
|
||||
customValidate: (val: string, formValues: any) => {
|
||||
const index_mode = formValues?.config?.index_mode;
|
||||
console.log('index_mode', index_mode, val);
|
||||
if (!val && index_mode === 'page') {
|
||||
return 'Page ID is required';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
shouldRender: (formValues: any) => {
|
||||
const index_mode = formValues?.config?.index_mode;
|
||||
return index_mode === 'page';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'config.space',
|
||||
label: 'Space Key',
|
||||
type: FormFieldType.Text,
|
||||
customValidate: (val: string, formValues: any) => {
|
||||
const index_mode = formValues?.config?.index_mode;
|
||||
if (!val && index_mode === 'space') {
|
||||
return 'Space Key is required';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
shouldRender: (formValues: any) => {
|
||||
const index_mode = formValues?.config?.index_mode;
|
||||
return index_mode === 'space';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'config.index_recursively',
|
||||
label: 'Index Recursively',
|
||||
type: FormFieldType.Checkbox,
|
||||
shouldRender: (formValues: any) => {
|
||||
const index_mode = formValues?.config?.index_mode;
|
||||
return index_mode === 'page';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: FilterFormField + '.tip',
|
||||
label: ' ',
|
||||
type: FormFieldType.Custom,
|
||||
shouldRender: (formValues: any) => {
|
||||
const index_mode = formValues?.config?.index_mode;
|
||||
return index_mode === 'everything';
|
||||
},
|
||||
render: () => (
|
||||
<div className="text-sm text-text-secondary bg-bg-card border border-border-button rounded-md px-3 py-2">
|
||||
{
|
||||
'This choice will index all pages the provided credentials have access to.'
|
||||
}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Space Key',
|
||||
name: 'config.space',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
label: 'Page ID',
|
||||
name: 'config.page_id',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
label: 'Index Recursively',
|
||||
name: 'config.index_recursively',
|
||||
type: FormFieldType.Checkbox,
|
||||
required: false,
|
||||
hidden: true,
|
||||
},
|
||||
];
|
||||
@ -4,11 +4,13 @@ import { t, TFunction } from 'i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import BoxTokenField from '../component/box-token-field';
|
||||
import { ConfluenceIndexingModeField } from '../component/confluence-token-field';
|
||||
import GmailTokenField from '../component/gmail-token-field';
|
||||
import GoogleDriveTokenField from '../component/google-drive-token-field';
|
||||
import { IDataSourceInfoMap } from '../interface';
|
||||
import { bitbucketConstant } from './bitbucket-constant';
|
||||
import { confluenceConstant } from './confluence-constant';
|
||||
import { S3Constant } from './s3-constant';
|
||||
|
||||
export enum DataSourceKey {
|
||||
CONFLUENCE = 'confluence',
|
||||
S3 = 's3',
|
||||
@ -29,6 +31,7 @@ export enum DataSourceKey {
|
||||
ASANA = 'asana',
|
||||
IMAP = 'imap',
|
||||
GITHUB = 'github',
|
||||
BITBUCKET = 'bitbucket',
|
||||
ZENDESK = 'zendesk',
|
||||
// SHAREPOINT = 'sharepoint',
|
||||
// SLACK = 'slack',
|
||||
@ -134,6 +137,11 @@ export const generateDataSourceInfo = (t: TFunction) => {
|
||||
description: t(`setting.${DataSourceKey.IMAP}Description`),
|
||||
icon: <SvgIcon name={'data-source/imap'} width={38} />,
|
||||
},
|
||||
[DataSourceKey.BITBUCKET]: {
|
||||
name: 'Bitbucket',
|
||||
description: t(`setting.${DataSourceKey.BITBUCKET}Description`),
|
||||
icon: <SvgIcon name={'data-source/bitbucket'} width={38} />,
|
||||
},
|
||||
[DataSourceKey.ZENDESK]: {
|
||||
name: 'Zendesk',
|
||||
description: t(`setting.${DataSourceKey.ZENDESK}Description`),
|
||||
@ -294,67 +302,7 @@ export const DataSourceFormFields = {
|
||||
},
|
||||
],
|
||||
|
||||
[DataSourceKey.CONFLUENCE]: [
|
||||
{
|
||||
label: 'Confluence Username',
|
||||
name: 'config.credentials.confluence_username',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
tooltip: 'A descriptive name for the connector.',
|
||||
},
|
||||
{
|
||||
label: 'Confluence Access Token',
|
||||
name: 'config.credentials.confluence_access_token',
|
||||
type: FormFieldType.Password,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: 'Wiki Base URL',
|
||||
name: 'config.wiki_base',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
tooltip: t('setting.confluenceWikiBaseUrlTip'),
|
||||
},
|
||||
{
|
||||
label: 'Is Cloud',
|
||||
name: 'config.is_cloud',
|
||||
type: FormFieldType.Checkbox,
|
||||
required: false,
|
||||
tooltip: t('setting.confluenceIsCloudTip'),
|
||||
},
|
||||
{
|
||||
label: 'Index Method',
|
||||
name: 'config.index_mode',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
horizontal: true,
|
||||
labelClassName: 'self-start pt-4',
|
||||
render: (fieldProps: any) => (
|
||||
<ConfluenceIndexingModeField {...fieldProps} />
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Space Key',
|
||||
name: 'config.space',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
label: 'Page ID',
|
||||
name: 'config.page_id',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
label: 'Index Recursively',
|
||||
name: 'config.index_recursively',
|
||||
type: FormFieldType.Checkbox,
|
||||
required: false,
|
||||
hidden: true,
|
||||
},
|
||||
],
|
||||
[DataSourceKey.CONFLUENCE]: confluenceConstant(t),
|
||||
[DataSourceKey.GOOGLE_DRIVE]: [
|
||||
{
|
||||
label: 'Primary Admin Email',
|
||||
@ -828,6 +776,7 @@ export const DataSourceFormFields = {
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
[DataSourceKey.BITBUCKET]: bitbucketConstant(t),
|
||||
[DataSourceKey.ZENDESK]: [
|
||||
{
|
||||
label: 'Zendesk Domain',
|
||||
@ -919,6 +868,7 @@ export const DataSourceFormDefaultValues = {
|
||||
wiki_base: '',
|
||||
is_cloud: true,
|
||||
space: '',
|
||||
page_id: '',
|
||||
credentials: {
|
||||
confluence_username: '',
|
||||
confluence_access_token: '',
|
||||
@ -1112,6 +1062,19 @@ export const DataSourceFormDefaultValues = {
|
||||
},
|
||||
},
|
||||
},
|
||||
[DataSourceKey.BITBUCKET]: {
|
||||
name: '',
|
||||
source: DataSourceKey.BITBUCKET,
|
||||
config: {
|
||||
workspace: '',
|
||||
index_mode: 'workspace',
|
||||
repository_slugs: '',
|
||||
projects: '',
|
||||
},
|
||||
credentials: {
|
||||
bitbucket_api_token: '',
|
||||
},
|
||||
},
|
||||
[DataSourceKey.ZENDESK]: {
|
||||
name: '',
|
||||
source: DataSourceKey.ZENDESK,
|
||||
|
||||
Reference in New Issue
Block a user