diff --git a/web/src/pages/user-setting/data-source/add-datasource-modal.tsx b/web/src/pages/user-setting/data-source/add-datasource-modal.tsx
new file mode 100644
index 000000000..79ee933a4
--- /dev/null
+++ b/web/src/pages/user-setting/data-source/add-datasource-modal.tsx
@@ -0,0 +1,80 @@
+import { Modal } from '@/components/ui/modal/modal';
+import { IModalProps } from '@/interfaces/common';
+import { useEffect, useState } from 'react';
+import { FieldValues } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+import { DynamicForm, FormFieldConfig } from './component/dynamic-form';
+import {
+ DataSourceFormBaseFields,
+ DataSourceFormDefaultValues,
+ DataSourceFormFields,
+} from './contant';
+import { IDataSorceInfo } from './interface';
+
+const AddDataSourceModal = ({
+ visible,
+ hideModal,
+ loading,
+ sourceData,
+ onOk,
+}: IModalProps
& { sourceData?: IDataSorceInfo }) => {
+ const { t } = useTranslation();
+ const [fields, setFields] = useState([]);
+
+ useEffect(() => {
+ if (sourceData) {
+ setFields([
+ ...DataSourceFormBaseFields,
+ ...DataSourceFormFields[
+ sourceData.id as keyof typeof DataSourceFormFields
+ ],
+ ] as FormFieldConfig[]);
+ }
+ }, [sourceData]);
+
+ const handleOk = async (values?: FieldValues) => {
+ await onOk?.(values);
+ hideModal?.();
+ };
+
+ return (
+ !open && hideModal?.()}
+ // onOk={() => handleOk()}
+ okText={t('common.ok')}
+ cancelText={t('common.cancel')}
+ showfooter={false}
+ >
+ {
+ console.log(data);
+ }}
+ defaultValues={
+ DataSourceFormDefaultValues[
+ sourceData?.id as keyof typeof DataSourceFormDefaultValues
+ ] as FieldValues
+ }
+ >
+
+ {
+ hideModal?.();
+ }}
+ />
+ {
+ handleOk(values);
+ }}
+ />
+
+
+
+ );
+};
+
+export default AddDataSourceModal;
diff --git a/web/src/pages/user-setting/data-source/component/added-source-card.tsx b/web/src/pages/user-setting/data-source/component/added-source-card.tsx
new file mode 100644
index 000000000..fa3e39401
--- /dev/null
+++ b/web/src/pages/user-setting/data-source/component/added-source-card.tsx
@@ -0,0 +1,51 @@
+import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
+import { Settings, Trash2 } from 'lucide-react';
+import { useDeleteDataSource } from '../hooks';
+import { IDataSorceInfo, IDataSourceBase } from '../interface';
+
+export type IAddedSourceCardProps = IDataSorceInfo & {
+ list: IDataSourceBase[];
+};
+export const AddedSourceCard = (props: IAddedSourceCardProps) => {
+ const { list, name, icon } = props;
+ const { handleDelete } = useDeleteDataSource();
+ const { navigateToDataSourceDetail } = useNavigatePage();
+ const toDetail = (id: string) => {
+ navigateToDataSourceDetail(id);
+ };
+ return (
+
+
+ {/* */}
+
+ {icon}
+ {name}
+
+
+
+ {list.map((item) => (
+
+
{item.name}
+
+ {
+ toDetail(item.id);
+ }}
+ />
+ handleDelete(item)}>
+
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/web/src/pages/user-setting/data-source/component/dynamic-form.tsx b/web/src/pages/user-setting/data-source/component/dynamic-form.tsx
new file mode 100644
index 000000000..ae0eb1be4
--- /dev/null
+++ b/web/src/pages/user-setting/data-source/component/dynamic-form.tsx
@@ -0,0 +1,725 @@
+import { zodResolver } from '@hookform/resolvers/zod';
+import { forwardRef, useEffect, useImperativeHandle, useMemo } from 'react';
+import {
+ DefaultValues,
+ FieldValues,
+ SubmitHandler,
+ useForm,
+ useFormContext,
+} from 'react-hook-form';
+import { ZodSchema, z } from 'zod';
+
+import EditTag from '@/components/edit-tag';
+import { SelectWithSearch } from '@/components/originui/select-with-search';
+import { RAGFlowFormItem } from '@/components/ragflow-form';
+import { Checkbox } from '@/components/ui/checkbox';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@/components/ui/form';
+import { Input } from '@/components/ui/input';
+import { Textarea } from '@/components/ui/textarea';
+import { cn } from '@/lib/utils';
+import { t } from 'i18next';
+import { Loader } from 'lucide-react';
+
+// Field type enumeration
+export enum FormFieldType {
+ Text = 'text',
+ Email = 'email',
+ Password = 'password',
+ Number = 'number',
+ Textarea = 'textarea',
+ Select = 'select',
+ Checkbox = 'checkbox',
+ Tag = 'tag',
+}
+
+// Field configuration interface
+export interface FormFieldConfig {
+ name: string;
+ label: string;
+ type: FormFieldType;
+ hidden?: boolean;
+ required?: boolean;
+ placeholder?: string;
+ options?: { label: string; value: string }[];
+ defaultValue?: any;
+ validation?: {
+ pattern?: RegExp;
+ minLength?: number;
+ maxLength?: number;
+ min?: number;
+ max?: number;
+ message?: string;
+ };
+ render?: (fieldProps: any) => React.ReactNode;
+ horizontal?: boolean;
+ onChange?: (value: any) => void;
+}
+
+// Component props interface
+interface DynamicFormProps {
+ fields: FormFieldConfig[];
+ onSubmit: SubmitHandler;
+ className?: string;
+ children?: React.ReactNode;
+ defaultValues?: DefaultValues;
+}
+
+// Form ref interface
+export interface DynamicFormRef {
+ submit: () => void;
+ getValues: () => any;
+ reset: (values?: any) => void;
+}
+
+// Generate Zod validation schema based on field configurations
+const generateSchema = (fields: FormFieldConfig[]): ZodSchema => {
+ const schema: Record = {};
+ const nestedSchemas: Record> = {};
+
+ fields.forEach((field) => {
+ let fieldSchema: ZodSchema;
+
+ // Create base validation schema based on field type
+ switch (field.type) {
+ case FormFieldType.Email:
+ fieldSchema = z.string().email('Please enter a valid email address');
+ break;
+ case FormFieldType.Number:
+ fieldSchema = z.coerce.number();
+ if (field.validation?.min !== undefined) {
+ fieldSchema = (fieldSchema as z.ZodNumber).min(
+ field.validation.min,
+ field.validation.message ||
+ `Value cannot be less than ${field.validation.min}`,
+ );
+ }
+ if (field.validation?.max !== undefined) {
+ fieldSchema = (fieldSchema as z.ZodNumber).max(
+ field.validation.max,
+ field.validation.message ||
+ `Value cannot be greater than ${field.validation.max}`,
+ );
+ }
+ break;
+ case FormFieldType.Checkbox:
+ fieldSchema = z.boolean();
+ break;
+ case FormFieldType.Tag:
+ fieldSchema = z.array(z.string());
+ break;
+ default:
+ fieldSchema = z.string();
+ break;
+ }
+
+ // Handle required fields
+ if (field.required) {
+ if (field.type === FormFieldType.Checkbox) {
+ fieldSchema = (fieldSchema as z.ZodBoolean).refine(
+ (val) => val === true,
+ {
+ message: `${field.label} is required`,
+ },
+ );
+ } else if (field.type === FormFieldType.Tag) {
+ fieldSchema = (fieldSchema as z.ZodArray).min(1, {
+ message: `${field.label} is required`,
+ });
+ } else {
+ fieldSchema = (fieldSchema as z.ZodString).min(1, {
+ message: `${field.label} is required`,
+ });
+ }
+ }
+
+ if (!field.required) {
+ fieldSchema = fieldSchema.optional();
+ }
+
+ // Handle other validation rules
+ if (
+ field.type !== FormFieldType.Number &&
+ field.type !== FormFieldType.Checkbox &&
+ field.type !== FormFieldType.Tag &&
+ field.required
+ ) {
+ fieldSchema = fieldSchema as z.ZodString;
+
+ if (field.validation?.minLength !== undefined) {
+ fieldSchema = (fieldSchema as z.ZodString).min(
+ field.validation.minLength,
+ field.validation.message ||
+ `Enter at least ${field.validation.minLength} characters`,
+ );
+ }
+
+ if (field.validation?.maxLength !== undefined) {
+ fieldSchema = (fieldSchema as z.ZodString).max(
+ field.validation.maxLength,
+ field.validation.message ||
+ `Enter up to ${field.validation.maxLength} characters`,
+ );
+ }
+
+ if (field.validation?.pattern) {
+ fieldSchema = (fieldSchema as z.ZodString).regex(
+ field.validation.pattern,
+ field.validation.message || 'Invalid input format',
+ );
+ }
+ }
+
+ if (field.name.includes('.')) {
+ const keys = field.name.split('.');
+ const firstKey = keys[0];
+
+ if (!nestedSchemas[firstKey]) {
+ nestedSchemas[firstKey] = {};
+ }
+
+ let currentSchema = nestedSchemas[firstKey];
+ for (let i = 1; i < keys.length - 1; i++) {
+ const key = keys[i];
+ if (!currentSchema[key]) {
+ currentSchema[key] = {};
+ }
+ currentSchema = currentSchema[key];
+ }
+
+ const lastKey = keys[keys.length - 1];
+ currentSchema[lastKey] = fieldSchema;
+ } else {
+ schema[field.name] = fieldSchema;
+ }
+ });
+
+ Object.keys(nestedSchemas).forEach((key) => {
+ const buildNestedSchema = (obj: Record): ZodSchema => {
+ const nestedSchema: Record = {};
+ Object.keys(obj).forEach((subKey) => {
+ if (
+ typeof obj[subKey] === 'object' &&
+ !(obj[subKey] instanceof z.ZodType)
+ ) {
+ nestedSchema[subKey] = buildNestedSchema(obj[subKey]);
+ } else {
+ nestedSchema[subKey] = obj[subKey];
+ }
+ });
+ return z.object(nestedSchema);
+ };
+
+ schema[key] = buildNestedSchema(nestedSchemas[key]);
+ });
+ return z.object(schema);
+};
+
+// Generate default values based on field configurations
+const generateDefaultValues = (
+ fields: FormFieldConfig[],
+): DefaultValues => {
+ const defaultValues: Record = {};
+
+ fields.forEach((field) => {
+ if (field.name.includes('.')) {
+ const keys = field.name.split('.');
+ let current = defaultValues;
+
+ for (let i = 0; i < keys.length - 1; i++) {
+ const key = keys[i];
+ if (!current[key]) {
+ current[key] = {};
+ }
+ current = current[key];
+ }
+
+ const lastKey = keys[keys.length - 1];
+ if (field.defaultValue !== undefined) {
+ current[lastKey] = field.defaultValue;
+ } else if (field.type === FormFieldType.Checkbox) {
+ current[lastKey] = false;
+ } else if (field.type === FormFieldType.Tag) {
+ current[lastKey] = [];
+ } else {
+ current[lastKey] = '';
+ }
+ } else {
+ if (field.defaultValue !== undefined) {
+ defaultValues[field.name] = field.defaultValue;
+ } else if (field.type === FormFieldType.Checkbox) {
+ defaultValues[field.name] = false;
+ } else if (field.type === FormFieldType.Tag) {
+ defaultValues[field.name] = [];
+ } else {
+ defaultValues[field.name] = '';
+ }
+ }
+ });
+
+ return defaultValues as DefaultValues;
+};
+
+// Dynamic form component
+const DynamicForm = {
+ Root: forwardRef(
+ (
+ {
+ fields,
+ onSubmit,
+ className = '',
+ children,
+ defaultValues: formDefaultValues = {} as DefaultValues,
+ }: DynamicFormProps,
+ ref: React.Ref,
+ ) => {
+ // Generate validation schema and default values
+ const schema = useMemo(() => generateSchema(fields), [fields]);
+
+ const defaultValues = useMemo(() => {
+ const value = {
+ ...generateDefaultValues(fields),
+ ...formDefaultValues,
+ };
+ console.log('generateDefaultValues', fields, value);
+ return value;
+ }, [fields, formDefaultValues]);
+
+ // Initialize form
+ const form = useForm({
+ resolver: zodResolver(schema),
+ defaultValues,
+ });
+
+ // Expose form methods via ref
+ useImperativeHandle(ref, () => ({
+ submit: () => form.handleSubmit(onSubmit)(),
+ getValues: () => form.getValues(),
+ reset: (values?: T) => {
+ if (values) {
+ form.reset(values);
+ } else {
+ form.reset();
+ }
+ },
+ setError: form.setError,
+ clearErrors: form.clearErrors,
+ trigger: form.trigger,
+ }));
+
+ useEffect(() => {
+ if (formDefaultValues && Object.keys(formDefaultValues).length > 0) {
+ form.reset({
+ ...generateDefaultValues(fields),
+ ...formDefaultValues,
+ });
+ }
+ }, [form, formDefaultValues, fields]);
+
+ // Submit handler
+ // const handleSubmit = form.handleSubmit(onSubmit);
+
+ // Render form fields
+ const renderField = (field: FormFieldConfig) => {
+ if (field.render) {
+ return (
+
+ {(fieldProps) => {
+ const finalFieldProps = field.onChange
+ ? {
+ ...fieldProps,
+ onChange: (e: any) => {
+ fieldProps.onChange(e);
+ field.onChange?.(e.target?.value ?? e);
+ },
+ }
+ : fieldProps;
+ return field.render?.(finalFieldProps);
+ }}
+
+ );
+ }
+ switch (field.type) {
+ case FormFieldType.Textarea:
+ return (
+
+ {(fieldProps) => {
+ const finalFieldProps = field.onChange
+ ? {
+ ...fieldProps,
+ onChange: (e: any) => {
+ fieldProps.onChange(e);
+ field.onChange?.(e.target.value);
+ },
+ }
+ : fieldProps;
+ return (
+
+ );
+ }}
+
+ );
+
+ case FormFieldType.Select:
+ return (
+
+ {(fieldProps) => {
+ const finalFieldProps = field.onChange
+ ? {
+ ...fieldProps,
+ onChange: (value: string) => {
+ console.log('select value', value);
+ if (fieldProps.onChange) {
+ fieldProps.onChange(value);
+ }
+ field.onChange?.(value);
+ },
+ }
+ : fieldProps;
+ return (
+
+ );
+ }}
+
+ );
+
+ case FormFieldType.Checkbox:
+ return (
+ (
+
+ {field.label && !field.horizontal && (
+
+
+ {field.label}{' '}
+ {field.required && (
+ *
+ )}
+
+
+ )}
+
+ {
+ formField.onChange(checked);
+ field.onChange?.(checked);
+ }}
+ />
+
+ {field.label && field.horizontal && (
+
+
+ {field.label}{' '}
+ {field.required && (
+ *
+ )}
+
+
+ )}
+
+
+ )}
+ />
+ );
+
+ case FormFieldType.Tag:
+ return (
+
+ {(fieldProps) => {
+ const finalFieldProps = field.onChange
+ ? {
+ ...fieldProps,
+ onChange: (value: string[]) => {
+ fieldProps.onChange(value);
+ field.onChange?.(value);
+ },
+ }
+ : fieldProps;
+ return (
+ //
+
+
+
+ );
+ }}
+
+ );
+
+ default:
+ return (
+
+ {(fieldProps) => {
+ const finalFieldProps = field.onChange
+ ? {
+ ...fieldProps,
+ onChange: (e: any) => {
+ fieldProps.onChange(e);
+ field.onChange?.(e.target.value);
+ },
+ }
+ : fieldProps;
+ return (
+
+ );
+ }}
+
+ );
+ }
+ };
+
+ return (
+
+
+ );
+ },
+ ) as (
+ props: DynamicFormProps & { ref?: React.Ref },
+ ) => React.ReactElement,
+
+ SavingButton: ({
+ submitLoading,
+ buttonText,
+ submitFunc,
+ }: {
+ submitLoading: boolean;
+ buttonText?: string;
+ submitFunc?: (values: FieldValues) => void;
+ }) => {
+ const form = useFormContext();
+ return (
+
+ );
+ },
+
+ CancelButton: ({
+ handleCancel,
+ cancelText,
+ }: {
+ handleCancel: () => void;
+ cancelText?: string;
+ }) => {
+ return (
+
+ );
+ },
+};
+
+export { DynamicForm };
+
+/**
+ * Usage Example 1: Basic Form
+ *
+ * {
+ * console.log(data); // { username: "...", email: "..." }
+ * }}
+ * />
+ *
+ * Usage Example 2: Nested Object Form
+ *
+ * {
+ * console.log(data);
+ * // {
+ * // user: {
+ * // name: "...",
+ * // email: "...",
+ * // profile: {
+ * // age: ...,
+ * // bio: "..."
+ * // }
+ * // },
+ * // settings: {
+ * // notifications: true/false
+ * // }
+ * // }
+ * }}
+ * />
+ *
+ * Usage Example 3: Tag Type Form
+ *
+ * {
+ * console.log(data);
+ * // {
+ * // skills: ["JavaScript", "React", "TypeScript"],
+ * // user: {
+ * // hobbies: ["Reading", "Swimming", "Travel"]
+ * // }
+ * // }
+ * }}
+ * />
+ */
diff --git a/web/src/pages/user-setting/data-source/contant.tsx b/web/src/pages/user-setting/data-source/contant.tsx
new file mode 100644
index 000000000..ccf91bb1a
--- /dev/null
+++ b/web/src/pages/user-setting/data-source/contant.tsx
@@ -0,0 +1,173 @@
+import SvgIcon from '@/components/svg-icon';
+import { t } from 'i18next';
+import { FormFieldType } from './component/dynamic-form';
+
+export enum DataSourceKey {
+ S3 = 's3',
+ NOTION = 'notion',
+ DISCORD = 'discord',
+ // CONFLUENNCE = 'confluence',
+ // GMAIL = 'gmail',
+ // GOOGLE_DRIVER = 'google_driver',
+ // JIRA = 'jira',
+ // SHAREPOINT = 'sharepoint',
+ // SLACK = 'slack',
+ // TEAMS = 'teams',
+}
+
+export const DataSourceInfo = {
+ [DataSourceKey.S3]: {
+ name: 'S3',
+ description: t(`setting.${DataSourceKey.S3}Description`),
+ icon: ,
+ },
+ [DataSourceKey.NOTION]: {
+ name: 'Notion',
+ description: t(`setting.${DataSourceKey.NOTION}Description`),
+ icon: ,
+ },
+ [DataSourceKey.DISCORD]: {
+ name: 'Discord',
+ description: t(`setting.${DataSourceKey.DISCORD}Description`),
+ icon: ,
+ },
+};
+
+export const DataSourceFormBaseFields = [
+ {
+ id: 'Id',
+ name: 'id',
+ type: FormFieldType.Text,
+ required: false,
+ hidden: true,
+ },
+ {
+ label: 'Name',
+ name: 'name',
+ type: FormFieldType.Text,
+ required: true,
+ },
+ {
+ label: 'Source',
+ name: 'source',
+ type: FormFieldType.Select,
+ required: true,
+ hidden: true,
+ options: Object.keys(DataSourceKey).map((item) => ({
+ label: item,
+ value: DataSourceKey[item as keyof typeof DataSourceKey],
+ })),
+ },
+];
+
+export const DataSourceFormFields = {
+ [DataSourceKey.S3]: [
+ {
+ label: 'AWS Access Key ID',
+ name: 'config.credentials.aws_access_key_id',
+ type: FormFieldType.Text,
+ required: true,
+ },
+ {
+ label: 'AWS Secret Access Key',
+ name: 'config.credentials.aws_secret_access_key',
+ type: FormFieldType.Text,
+ required: true,
+ },
+ {
+ label: 'Bucket Name',
+ name: 'config.bucket_name',
+ type: FormFieldType.Text,
+ required: true,
+ },
+ {
+ label: 'Bucket Type',
+ name: 'config.bucket_type',
+ type: FormFieldType.Select,
+ options: [
+ { label: 'S3', value: 's3' },
+ { label: 'R2', value: 'r2' },
+ { label: 'Google Cloud Storage', value: 'google_cloud_storage' },
+ { label: 'OCI Storage', value: 'oci_storage' },
+ ],
+ required: true,
+ },
+ {
+ label: 'Prefix',
+ name: 'config.prefix',
+ type: FormFieldType.Text,
+ required: false,
+ },
+ ],
+ [DataSourceKey.NOTION]: [
+ {
+ label: 'Notion Integration Token',
+ name: 'config.credentials.notion_integration_token',
+ type: FormFieldType.Text,
+ required: true,
+ },
+ {
+ label: 'Root Page Id',
+ name: 'config.root_page_id',
+ type: FormFieldType.Text,
+ required: false,
+ },
+ ],
+ [DataSourceKey.DISCORD]: [
+ {
+ label: 'Discord Bot Token',
+ name: 'config.credentials.discord_bot_token',
+ type: FormFieldType.Text,
+ required: true,
+ },
+ {
+ label: 'Server IDs',
+ name: 'config.server_ids',
+ type: FormFieldType.Tag,
+ required: false,
+ },
+ {
+ label: 'Channels',
+ name: 'config.channels',
+ type: FormFieldType.Tag,
+ required: false,
+ },
+ ],
+};
+
+export const DataSourceFormDefaultValues = {
+ [DataSourceKey.S3]: {
+ name: '',
+ source: DataSourceKey.S3,
+ config: {
+ bucket_name: '',
+ bucket_type: 's3',
+ prefix: '',
+ credentials: {
+ aws_access_key_id: '',
+ aws_secret_access_key: '',
+ },
+ },
+ },
+ [DataSourceKey.NOTION]: {
+ name: '',
+ source: DataSourceKey.NOTION,
+ config: {
+ root_page_id: '',
+ credentials: {
+ notion_integration_token: '',
+ },
+ },
+ },
+ [DataSourceKey.DISCORD]: {
+ name: '',
+ source: DataSourceKey.DISCORD,
+ config: {
+ server_ids: [],
+ channels: [],
+ credentials: {
+ discord_bot_token: '',
+ },
+ },
+ },
+};
diff --git a/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx b/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx
new file mode 100644
index 000000000..d99ec9b1c
--- /dev/null
+++ b/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx
@@ -0,0 +1,193 @@
+import BackButton from '@/components/back-button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Separator } from '@/components/ui/separator';
+import { RunningStatus } from '@/constants/knowledge';
+import { t } from 'i18next';
+import { debounce } from 'lodash';
+import { CirclePause, Repeat } from 'lucide-react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { FieldValues } from 'react-hook-form';
+import {
+ DynamicForm,
+ DynamicFormRef,
+ FormFieldConfig,
+ FormFieldType,
+} from '../component/dynamic-form';
+import {
+ DataSourceFormBaseFields,
+ DataSourceFormDefaultValues,
+ DataSourceFormFields,
+ DataSourceInfo,
+} from '../contant';
+import {
+ useAddDataSource,
+ useDataSourceResume,
+ useFetchDataSourceDetail,
+} from '../hooks';
+import { DataSourceLogsTable } from './log-table';
+
+const SourceDetailPage = () => {
+ const formRef = useRef(null);
+
+ const { data: detail } = useFetchDataSourceDetail();
+ const { handleResume } = useDataSourceResume();
+
+ const detailInfo = useMemo(() => {
+ if (detail) {
+ return DataSourceInfo[detail.source];
+ }
+ }, [detail]);
+
+ const [fields, setFields] = useState([]);
+ const [defaultValues, setDefaultValues] = useState(
+ DataSourceFormDefaultValues[
+ detail?.source as keyof typeof DataSourceFormDefaultValues
+ ] as FieldValues,
+ );
+
+ const runSchedule = useCallback(() => {
+ handleResume({
+ resume:
+ detail?.status === RunningStatus.RUNNING ||
+ detail?.status === RunningStatus.SCHEDULE
+ ? false
+ : true,
+ });
+ }, [detail, handleResume]);
+
+ const customFields = useMemo(() => {
+ return [
+ {
+ label: 'Refresh Freq',
+ name: 'refresh_freq',
+ type: FormFieldType.Number,
+ required: false,
+ render: (fieldProps: FormFieldConfig) => (
+
+
+
+ minutes
+
+
+
+ ),
+ },
+ {
+ label: 'Prune Freq',
+ name: 'prune_freq',
+ type: FormFieldType.Number,
+ required: false,
+ hidden: true,
+ render: (fieldProps: FormFieldConfig) => {
+ return (
+
+
+
+ hours
+
+
+ );
+ },
+ },
+ {
+ label: 'Timeout Secs',
+ name: 'timeout_secs',
+ type: FormFieldType.Number,
+ required: false,
+ render: (fieldProps: FormFieldConfig) => (
+
+
+
+ minutes
+
+
+ ),
+ },
+ ];
+ }, [detail, runSchedule]);
+
+ const { handleAddOk } = useAddDataSource();
+
+ const onSubmit = useCallback(() => {
+ formRef?.current?.submit();
+ }, [formRef]);
+
+ useEffect(() => {
+ if (detail) {
+ const fields = [
+ ...DataSourceFormBaseFields,
+ ...DataSourceFormFields[
+ detail.source as keyof typeof DataSourceFormFields
+ ],
+ ...customFields,
+ ] as FormFieldConfig[];
+
+ const neweFields = fields.map((field) => {
+ return {
+ ...field,
+ horizontal: true,
+ onChange: () => {
+ onSubmit();
+ },
+ };
+ });
+ setFields(neweFields);
+
+ const defultValueTemp = {
+ ...(DataSourceFormDefaultValues[
+ detail?.source as keyof typeof DataSourceFormDefaultValues
+ ] as FieldValues),
+ ...detail,
+ };
+ console.log('defaultValue', defultValueTemp);
+ setDefaultValues(defultValueTemp);
+ }
+ }, [detail, customFields, onSubmit]);
+
+ return (
+
+
+
+
+ {/* */}
+
+ {detailInfo?.icon}
+ {detail?.name}
+
+
+
+
+
+ {
+ handleAddOk(data);
+ }, 500)}
+ defaultValues={defaultValues}
+ />
+
+
+
+
+
+ );
+};
+export default SourceDetailPage;
diff --git a/web/src/pages/user-setting/data-source/data-source-detail-page/log-table.tsx b/web/src/pages/user-setting/data-source/data-source-detail-page/log-table.tsx
new file mode 100644
index 000000000..d64b000ac
--- /dev/null
+++ b/web/src/pages/user-setting/data-source/data-source-detail-page/log-table.tsx
@@ -0,0 +1,240 @@
+import FileStatusBadge from '@/components/file-status-badge';
+import { RAGFlowAvatar } from '@/components/ragflow-avatar';
+import { Button } from '@/components/ui/button';
+import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import { RunningStatusMap } from '@/constants/knowledge';
+import { RunningStatus } from '@/pages/dataset/dataset/constant';
+import { Routes } from '@/routes';
+import { formatDate } from '@/utils/date';
+import {
+ HoverCard,
+ HoverCardContent,
+ HoverCardTrigger,
+} from '@radix-ui/react-hover-card';
+import {
+ ColumnDef,
+ flexRender,
+ getCoreRowModel,
+ getFilteredRowModel,
+ getPaginationRowModel,
+ getSortedRowModel,
+ useReactTable,
+} from '@tanstack/react-table';
+import { t } from 'i18next';
+import { pick } from 'lodash';
+import { Eye } from 'lucide-react';
+import { useCallback, useMemo } from 'react';
+import { useNavigate } from 'umi';
+import { useLogListDataSource } from '../hooks';
+
+const columns = ({
+ handleToDataSetDetail,
+}: {
+ handleToDataSetDetail: (id: string) => void;
+}) => {
+ return [
+ {
+ accessorKey: 'update_date',
+ header: t('setting.timeStarted'),
+ cell: ({ row }) => (
+
+ {row.original.update_date
+ ? formatDate(row.original.update_date)
+ : '-'}
+
+ ),
+ },
+ {
+ accessorKey: 'status',
+ header: t('knowledgeDetails.status'),
+ cell: ({ row }) => (
+
+ ),
+ },
+ {
+ accessorKey: 'kb_name',
+ header: t('knowledgeDetails.dataset'),
+ cell: ({ row }) => {
+ return (
+ {
+ console.log('handleToDataSetDetail', row.original.kb_id);
+ handleToDataSetDetail(row.original.kb_id);
+ }}
+ >
+
+ {row.original.kb_name}
+
+ );
+ },
+ },
+ {
+ accessorKey: 'new_docs_indexed',
+ header: t('setting.newDocs'),
+ },
+
+ {
+ id: 'operations',
+ header: t('setting.errorMsg'),
+ cell: ({ row }) => (
+
+ {row.original.error_msg}
+ {row.original.error_msg && (
+
+
+
+
+
+
+
+ {row.original.full_exception_trace}
+
+
+
+
+ )}
+
+ ),
+ },
+ ] as ColumnDef[];
+};
+
+// const paginationInit = {
+// current: 1,
+// pageSize: 10,
+// total: 0,
+// };
+export const DataSourceLogsTable = () => {
+ // const [pagination, setPagination] = useState(paginationInit);
+ const { data, pagination, setPagination } = useLogListDataSource();
+ const navigate = useNavigate();
+ const currentPagination = useMemo(
+ () => ({
+ pageIndex: (pagination.current || 1) - 1,
+ pageSize: pagination.pageSize || 10,
+ }),
+ [pagination],
+ );
+
+ const handleToDataSetDetail = useCallback(
+ (id: string) => {
+ console.log('handleToDataSetDetail', id);
+ navigate(`${Routes.DatasetBase}${Routes.DataSetSetting}/${id}`);
+ },
+ [navigate],
+ );
+
+ const table = useReactTable({
+ data: data || [],
+ columns: columns({ handleToDataSetDetail }),
+ manualPagination: true,
+ getCoreRowModel: getCoreRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ // onSortingChange: setSorting,
+ // onColumnFiltersChange: setColumnFilters,
+ // onRowSelectionChange: setRowSelection,
+ state: {
+ // sorting,
+ // columnFilters,
+ // rowSelection,
+ pagination: currentPagination,
+ },
+ // pageCount: pagination.total
+ // ? Math.ceil(pagination.total / pagination.pageSize)
+ // : 0,
+ rowCount: pagination.total ?? 0,
+ });
+
+ return (
+ //
+ //
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )}
+
+ ))}
+
+ ))}
+
+
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ ))}
+
+ ))
+ ) : (
+
+
+ No results.
+
+
+ )}
+
+
+
+
+ {/* setPagination({ page, pageSize })}
+ /> */}
+ {
+ setPagination({ page, pageSize });
+ }}
+ >
+
+
+
+ );
+};
diff --git a/web/src/pages/user-setting/data-source/hooks.ts b/web/src/pages/user-setting/data-source/hooks.ts
new file mode 100644
index 000000000..846d61724
--- /dev/null
+++ b/web/src/pages/user-setting/data-source/hooks.ts
@@ -0,0 +1,188 @@
+import message from '@/components/ui/message';
+import { useSetModalState } from '@/hooks/common-hooks';
+import { useGetPaginationWithRouter } from '@/hooks/logic-hooks';
+import dataSourceService, {
+ dataSourceResume,
+ deleteDataSource,
+ featchDataSourceDetail,
+ getDataSourceLogs,
+} from '@/services/data-source-service';
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { t } from 'i18next';
+import { useCallback, useMemo, useState } from 'react';
+import { useSearchParams } from 'umi';
+import { DataSourceInfo, DataSourceKey } from './contant';
+import { IDataSorceInfo, IDataSource, IDataSourceBase } from './interface';
+
+export const useListDataSource = () => {
+ const { data: list, isFetching } = useQuery({
+ queryKey: ['data-source'],
+ queryFn: async () => {
+ const { data } = await dataSourceService.dataSourceList();
+ return data.data;
+ },
+ });
+
+ const categorizeDataBySource = (data: IDataSourceBase[]) => {
+ const categorizedData: Record = {} as Record<
+ DataSourceKey,
+ any[]
+ >;
+
+ data.forEach((item) => {
+ const source = item.source;
+ if (!categorizedData[source]) {
+ categorizedData[source] = [];
+ }
+ categorizedData[source].push({
+ ...item,
+ });
+ });
+
+ return categorizedData;
+ };
+
+ const updatedDataSourceTemplates = useMemo(() => {
+ const categorizedData = categorizeDataBySource(list || []);
+ let sourcelist: Array }> =
+ [];
+ Object.keys(categorizedData).forEach((key: string) => {
+ const k = key as DataSourceKey;
+ sourcelist.push({
+ id: k,
+ name: DataSourceInfo[k].name,
+ description: DataSourceInfo[k].description,
+ icon: DataSourceInfo[k].icon,
+ list: categorizedData[k] || [],
+ });
+ });
+
+ console.log('🚀 ~ useListDataSource ~ sourcelist:', sourcelist);
+ return sourcelist;
+ }, [list]);
+
+ return { list, categorizedList: updatedDataSourceTemplates, isFetching };
+};
+
+export const useAddDataSource = () => {
+ const [addSource, setAddSource] = useState(
+ undefined,
+ );
+ const [addLoading, setAddLoading] = useState(false);
+ const {
+ visible: addingModalVisible,
+ hideModal: hideAddingModal,
+ showModal,
+ } = useSetModalState();
+ const showAddingModal = useCallback(
+ (data: IDataSorceInfo) => {
+ setAddSource(data);
+ showModal();
+ },
+ [showModal],
+ );
+ const queryClient = useQueryClient();
+
+ const handleAddOk = useCallback(
+ async (data: any) => {
+ setAddLoading(true);
+ const { data: res } = await dataSourceService.dataSourceSet(data);
+ console.log('🚀 ~ handleAddOk ~ code:', res.code);
+ if (res.code === 0) {
+ queryClient.invalidateQueries({ queryKey: ['data-source'] });
+ message.success(t(`message.operated`));
+ hideAddingModal();
+ }
+ setAddLoading(false);
+ },
+ [hideAddingModal, queryClient],
+ );
+
+ return {
+ addSource,
+ addLoading,
+ setAddSource,
+ addingModalVisible,
+ hideAddingModal,
+ showAddingModal,
+ handleAddOk,
+ };
+};
+
+export const useLogListDataSource = () => {
+ const { pagination, setPagination } = useGetPaginationWithRouter();
+ const [currentQueryParameters] = useSearchParams();
+ const id = currentQueryParameters.get('id');
+
+ const { data, isFetching } = useQuery<{ logs: IDataSource[]; total: number }>(
+ {
+ queryKey: ['data-source-logs', id, pagination],
+ queryFn: async () => {
+ const { data } = await getDataSourceLogs(id as string, {
+ page_size: pagination.pageSize,
+ page: pagination.current,
+ });
+ return data.data;
+ },
+ },
+ );
+ return {
+ data: data?.logs,
+ isFetching,
+ pagination: { ...pagination, total: data?.total },
+ setPagination,
+ };
+};
+
+export const useDeleteDataSource = () => {
+ const [deleteLoading, setDeleteLoading] = useState(false);
+ const { hideModal, showModal } = useSetModalState();
+ const queryClient = useQueryClient();
+ const handleDelete = useCallback(
+ async ({ id }: { id: string }) => {
+ setDeleteLoading(true);
+ const { data } = await deleteDataSource(id);
+ if (data.code === 0) {
+ message.success(t(`message.deleted`));
+ queryClient.invalidateQueries({ queryKey: ['data-source'] });
+ }
+ setDeleteLoading(false);
+ },
+ [setDeleteLoading, queryClient],
+ );
+ return { deleteLoading, hideModal, showModal, handleDelete };
+};
+
+export const useFetchDataSourceDetail = () => {
+ const [currentQueryParameters] = useSearchParams();
+ const id = currentQueryParameters.get('id');
+ const { data } = useQuery({
+ queryKey: ['data-source-detail', id],
+ enabled: !!id,
+ queryFn: async () => {
+ const { data } = await featchDataSourceDetail(id as string);
+ // if (data.code === 0) {
+
+ // }
+ return data.data;
+ },
+ });
+ return { data };
+};
+
+export const useDataSourceResume = () => {
+ const [currentQueryParameters] = useSearchParams();
+ const id = currentQueryParameters.get('id');
+ const queryClient = useQueryClient();
+ const handleResume = useCallback(
+ async (param: { resume: boolean }) => {
+ const { data } = await dataSourceResume(id as string, param);
+ if (data.code === 0) {
+ queryClient.invalidateQueries({ queryKey: ['data-source-detail', id] });
+ message.success(t(`message.operated`));
+ }
+ },
+ [id, queryClient],
+ );
+ return { handleResume };
+};
diff --git a/web/src/pages/user-setting/data-source/index.tsx b/web/src/pages/user-setting/data-source/index.tsx
new file mode 100644
index 000000000..4efb0adf5
--- /dev/null
+++ b/web/src/pages/user-setting/data-source/index.tsx
@@ -0,0 +1,151 @@
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { useTranslation } from 'react-i18next';
+
+import Spotlight from '@/components/spotlight';
+import { Button } from '@/components/ui/button';
+import { Separator } from '@/components/ui/separator';
+import { Plus } from 'lucide-react';
+import AddDataSourceModal from './add-datasource-modal';
+import { AddedSourceCard } from './component/added-source-card';
+import { DataSourceInfo, DataSourceKey } from './contant';
+import { useAddDataSource, useListDataSource } from './hooks';
+import { IDataSorceInfo } from './interface';
+const dataSourceTemplates = [
+ {
+ id: DataSourceKey.S3,
+ name: DataSourceInfo[DataSourceKey.S3].name,
+ description: DataSourceInfo[DataSourceKey.S3].description,
+ icon: DataSourceInfo[DataSourceKey.S3].icon,
+ list: [
+ {
+ id: '1',
+ name: 'S3 Bucket 1',
+ },
+ {
+ id: '2',
+ name: 'S3 Bucket 1',
+ },
+ ],
+ },
+ {
+ id: DataSourceKey.DISCORD,
+ name: DataSourceInfo[DataSourceKey.DISCORD].name,
+ description: DataSourceInfo[DataSourceKey.DISCORD].description,
+ icon: DataSourceInfo[DataSourceKey.DISCORD].icon,
+ },
+ {
+ id: DataSourceKey.NOTION,
+ name: DataSourceInfo[DataSourceKey.NOTION].name,
+ description: DataSourceInfo[DataSourceKey.NOTION].description,
+ icon: DataSourceInfo[DataSourceKey.NOTION].icon,
+ },
+];
+const DataSource = () => {
+ const { t } = useTranslation();
+
+ // useListTenantUser();
+ const { categorizedList } = useListDataSource();
+
+ const {
+ addSource,
+ addLoading,
+ addingModalVisible,
+ handleAddOk,
+ hideAddingModal,
+ showAddingModal,
+ } = useAddDataSource();
+
+ const AbailableSourceCard = ({
+ id,
+ name,
+ description,
+ icon,
+ }: IDataSorceInfo) => {
+ return (
+
+
+
{icon}
+
+
{name}
+
{description}
+
+
+
+
+
+
+ );
+ };
+
+ return (
+
+
+
+
+
+
+ {t('setting.dataSources')}
+
+ {t('setting.datasourceDescription')}
+
+
+
+
+
+
+
+ {categorizedList.map((item, index) => (
+
+ ))}
+
+
+
+ {/* */}
+
+ {t('setting.availableSources')}
+
+ {t('setting.availableSourcesDescription')}
+
+
+
+
+ {/* */}
+
+ {dataSourceTemplates.map((item, index) => (
+
+ ))}
+
+
+
+
+
+ {addingModalVisible && (
+
{
+ console.log(data);
+ handleAddOk(data);
+ }}
+ sourceData={addSource}
+ >
+ )}
+
+ );
+};
+
+export default DataSource;
diff --git a/web/src/pages/user-setting/data-source/interface.ts b/web/src/pages/user-setting/data-source/interface.ts
new file mode 100644
index 000000000..5e237857e
--- /dev/null
+++ b/web/src/pages/user-setting/data-source/interface.ts
@@ -0,0 +1,45 @@
+import { RunningStatus } from '@/constants/knowledge';
+import { DataSourceKey } from './contant';
+
+export interface IDataSorceInfo {
+ id: DataSourceKey;
+ name: string;
+ description: string;
+ icon: React.ReactNode;
+}
+
+export type IDataSource = IDataSourceBase & {
+ config: any;
+ indexing_start: null | string;
+ input_type: string;
+ prune_freq: number;
+ refresh_freq: number;
+ status: string;
+ tenant_id: string;
+ update_date: string;
+ update_time: number;
+};
+
+export interface IDataSourceBase {
+ id: string;
+ name: string;
+ source: DataSourceKey;
+}
+
+export interface IDataSourceLog {
+ connector_id: string;
+ error_count: number;
+ error_msg: string;
+ id: string;
+ kb_id: string;
+ kb_name: string;
+ name: string;
+ new_docs_indexed: number;
+ poll_range_end: null | string;
+ poll_range_start: null | string;
+ reindex: string;
+ source: DataSourceKey;
+ status: RunningStatus;
+ tenant_id: string;
+ timeout_secs: number;
+}
diff --git a/web/src/pages/user-setting/setting-model/components/system-setting.tsx b/web/src/pages/user-setting/setting-model/components/system-setting.tsx
index 07941c0ff..aef87b703 100644
--- a/web/src/pages/user-setting/setting-model/components/system-setting.tsx
+++ b/web/src/pages/user-setting/setting-model/components/system-setting.tsx
@@ -27,6 +27,7 @@ interface IProps {
const SystemSetting = ({ onOk, loading }: IProps) => {
const { systemSetting: initialValues, allOptions } =
useFetchSystemModelSettingOnMount();
+ const { t: tcommon } = useTranslate('common');
const { t } = useTranslate('setting');
const [formData, setFormData] = useState({
@@ -159,7 +160,7 @@ const SystemSetting = ({ onOk, loading }: IProps) => {
value={value}
options={options}
onChange={(value) => handleFieldChange(id, value)}
- placeholder={t('common:selectPlaceholder')}
+ placeholder={tcommon('selectPlaceholder')}
/>
);
diff --git a/web/src/pages/user-setting/setting-model/components/used-model.tsx b/web/src/pages/user-setting/setting-model/components/used-model.tsx
index 4eb19a6bc..52590f580 100644
--- a/web/src/pages/user-setting/setting-model/components/used-model.tsx
+++ b/web/src/pages/user-setting/setting-model/components/used-model.tsx
@@ -1,4 +1,5 @@
import { LlmItem, useSelectLlmList } from '@/hooks/llm-hooks';
+import { t } from 'i18next';
import { ModelProviderCard } from './modal-card';
export const UsedModel = ({
@@ -11,7 +12,9 @@ export const UsedModel = ({
const { factoryList, myLlmList: llmList, loading } = useSelectLlmList();
return (
-
Added models
+
+ {t('setting.addedModels')}
+
{llmList.map((llm) => {
return (
- Log Out
+ {t('setting.logout')}
diff --git a/web/src/routes.ts b/web/src/routes.ts
index eca98dcb6..5ab34b920 100644
--- a/web/src/routes.ts
+++ b/web/src/routes.ts
@@ -27,6 +27,8 @@ export enum Routes {
System = '/system',
Model = '/model',
Prompt = '/prompt',
+ DataSource = '/data-source',
+ DataSourceDetailPage = '/data-source-detail-page',
ProfileMcp = `${ProfileSetting}${Mcp}`,
ProfileTeam = `${ProfileSetting}${Team}`,
ProfilePlan = `${ProfileSetting}${Plan}`,
@@ -400,9 +402,19 @@ const routes = [
path: `/user-setting${Routes.Mcp}`,
component: `@/pages${Routes.ProfileMcp}`,
},
+ {
+ path: `/user-setting${Routes.DataSource}`,
+ component: `@/pages/user-setting${Routes.DataSource}`,
+ },
],
},
+ {
+ path: `/user-setting${Routes.DataSource}${Routes.DataSourceDetailPage}`,
+ component: `@/pages/user-setting${Routes.DataSource}${Routes.DataSourceDetailPage}`,
+ layout: false,
+ },
+
// Admin routes
{
path: Routes.Admin,
diff --git a/web/src/services/data-source-service.ts b/web/src/services/data-source-service.ts
new file mode 100644
index 000000000..23fc68b9f
--- /dev/null
+++ b/web/src/services/data-source-service.ts
@@ -0,0 +1,33 @@
+import api from '@/utils/api';
+import registerServer from '@/utils/register-server';
+import request from '@/utils/request';
+
+const { dataSourceSet, dataSourceList } = api;
+const methods = {
+ dataSourceSet: {
+ url: dataSourceSet,
+ method: 'post',
+ },
+ dataSourceList: {
+ url: dataSourceList,
+ method: 'get',
+ },
+} as const;
+const dataSourceService = registerServer(
+ methods,
+ request,
+);
+
+export const deleteDataSource = (id: string) =>
+ request.post(api.dataSourceDel(id));
+export const dataSourceResume = (id: string, data: { resume: boolean }) => {
+ console.log('api.dataSourceResume(id)', data);
+ return request.put(api.dataSourceResume(id), { data });
+};
+
+export const getDataSourceLogs = (id: string, params?: any) =>
+ request.get(api.dataSourceLogs(id), { params });
+export const featchDataSourceDetail = (id: string) =>
+ request.get(api.dataSourceDetail(id));
+
+export default dataSourceService;
diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts
index 25af5bbc3..8e8f8f5d3 100644
--- a/web/src/utils/api.ts
+++ b/web/src/utils/api.ts
@@ -34,6 +34,14 @@ export default {
enable_llm: `${api_host}/llm/enable_llm`,
deleteFactory: `${api_host}/llm/delete_factory`,
+ // data source
+ dataSourceSet: `${api_host}/connector/set`,
+ dataSourceList: `${api_host}/connector/list`,
+ dataSourceDel: (id: string) => `${api_host}/connector/${id}/rm`,
+ dataSourceResume: (id: string) => `${api_host}/connector/${id}/resume`,
+ dataSourceLogs: (id: string) => `${api_host}/connector/${id}/logs`,
+ dataSourceDetail: (id: string) => `${api_host}/connector/${id}`,
+
// plugin
llm_tools: `${api_host}/plugin/llm_tools`,