mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-18 03:26:42 +08:00
### What problem does this PR solve? Feature: Added data source functionality ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@ -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<FieldValues> & { sourceData?: IDataSorceInfo }) => {
|
||||
const { t } = useTranslation();
|
||||
const [fields, setFields] = useState<FormFieldConfig[]>([]);
|
||||
|
||||
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 (
|
||||
<Modal
|
||||
title={t('setting.add')}
|
||||
open={visible || false}
|
||||
onOpenChange={(open) => !open && hideModal?.()}
|
||||
// onOk={() => handleOk()}
|
||||
okText={t('common.ok')}
|
||||
cancelText={t('common.cancel')}
|
||||
showfooter={false}
|
||||
>
|
||||
<DynamicForm.Root
|
||||
fields={fields}
|
||||
onSubmit={(data) => {
|
||||
console.log(data);
|
||||
}}
|
||||
defaultValues={
|
||||
DataSourceFormDefaultValues[
|
||||
sourceData?.id as keyof typeof DataSourceFormDefaultValues
|
||||
] as FieldValues
|
||||
}
|
||||
>
|
||||
<div className="flex items-center justify-end w-full gap-2">
|
||||
<DynamicForm.CancelButton
|
||||
handleCancel={() => {
|
||||
hideModal?.();
|
||||
}}
|
||||
/>
|
||||
<DynamicForm.SavingButton
|
||||
submitLoading={loading || false}
|
||||
buttonText={t('common.ok')}
|
||||
submitFunc={(values: FieldValues) => {
|
||||
handleOk(values);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DynamicForm.Root>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddDataSourceModal;
|
||||
@ -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 (
|
||||
<Card className="bg-transparent border border-border-button px-5 pt-[10px] pb-5 rounded-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-0 pb-3">
|
||||
{/* <Users className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
|
||||
<CardTitle className="text-base flex gap-1 font-normal">
|
||||
{icon}
|
||||
{name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-2 flex flex-col gap-2">
|
||||
{list.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex flex-row items-center justify-between rounded-md bg-bg-input px-[10px] py-4"
|
||||
>
|
||||
<div className="text-sm text-text-secondary ">{item.name}</div>
|
||||
<div className="text-sm text-text-secondary flex gap-2">
|
||||
<Settings
|
||||
className="cursor-pointer"
|
||||
size={14}
|
||||
onClick={() => {
|
||||
toDetail(item.id);
|
||||
}}
|
||||
/>
|
||||
<ConfirmDeleteDialog onOk={() => handleDelete(item)}>
|
||||
<Trash2 className="cursor-pointer" size={14} />
|
||||
</ConfirmDeleteDialog>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@ -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<T extends FieldValues> {
|
||||
fields: FormFieldConfig[];
|
||||
onSubmit: SubmitHandler<T>;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
defaultValues?: DefaultValues<T>;
|
||||
}
|
||||
|
||||
// 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<any> => {
|
||||
const schema: Record<string, ZodSchema> = {};
|
||||
const nestedSchemas: Record<string, Record<string, ZodSchema>> = {};
|
||||
|
||||
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<z.ZodString>).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<string, any>): ZodSchema => {
|
||||
const nestedSchema: Record<string, ZodSchema> = {};
|
||||
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 = <T extends FieldValues>(
|
||||
fields: FormFieldConfig[],
|
||||
): DefaultValues<T> => {
|
||||
const defaultValues: Record<string, any> = {};
|
||||
|
||||
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<T>;
|
||||
};
|
||||
|
||||
// Dynamic form component
|
||||
const DynamicForm = {
|
||||
Root: forwardRef(
|
||||
<T extends FieldValues>(
|
||||
{
|
||||
fields,
|
||||
onSubmit,
|
||||
className = '',
|
||||
children,
|
||||
defaultValues: formDefaultValues = {} as DefaultValues<T>,
|
||||
}: DynamicFormProps<T>,
|
||||
ref: React.Ref<any>,
|
||||
) => {
|
||||
// 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<T>({
|
||||
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 (
|
||||
<RAGFlowFormItem
|
||||
name={field.name}
|
||||
label={field.label}
|
||||
required={field.required}
|
||||
horizontal={field.horizontal}
|
||||
>
|
||||
{(fieldProps) => {
|
||||
const finalFieldProps = field.onChange
|
||||
? {
|
||||
...fieldProps,
|
||||
onChange: (e: any) => {
|
||||
fieldProps.onChange(e);
|
||||
field.onChange?.(e.target?.value ?? e);
|
||||
},
|
||||
}
|
||||
: fieldProps;
|
||||
return field.render?.(finalFieldProps);
|
||||
}}
|
||||
</RAGFlowFormItem>
|
||||
);
|
||||
}
|
||||
switch (field.type) {
|
||||
case FormFieldType.Textarea:
|
||||
return (
|
||||
<RAGFlowFormItem
|
||||
name={field.name}
|
||||
label={field.label}
|
||||
required={field.required}
|
||||
horizontal={field.horizontal}
|
||||
>
|
||||
{(fieldProps) => {
|
||||
const finalFieldProps = field.onChange
|
||||
? {
|
||||
...fieldProps,
|
||||
onChange: (e: any) => {
|
||||
fieldProps.onChange(e);
|
||||
field.onChange?.(e.target.value);
|
||||
},
|
||||
}
|
||||
: fieldProps;
|
||||
return (
|
||||
<Textarea
|
||||
{...finalFieldProps}
|
||||
placeholder={field.placeholder}
|
||||
className="resize-none"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</RAGFlowFormItem>
|
||||
);
|
||||
|
||||
case FormFieldType.Select:
|
||||
return (
|
||||
<RAGFlowFormItem
|
||||
name={field.name}
|
||||
label={field.label}
|
||||
required={field.required}
|
||||
horizontal={field.horizontal}
|
||||
>
|
||||
{(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 (
|
||||
<SelectWithSearch
|
||||
triggerClassName="!shrink"
|
||||
{...finalFieldProps}
|
||||
options={field.options}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</RAGFlowFormItem>
|
||||
);
|
||||
|
||||
case FormFieldType.Checkbox:
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={field.name as any}
|
||||
render={({ field: formField }) => (
|
||||
<FormItem
|
||||
className={cn('flex items-center', {
|
||||
'flex-row items-start space-x-3 space-y-0':
|
||||
!field.horizontal,
|
||||
})}
|
||||
>
|
||||
{field.label && !field.horizontal && (
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel className="font-normal">
|
||||
{field.label}{' '}
|
||||
{field.required && (
|
||||
<span className="text-destructive">*</span>
|
||||
)}
|
||||
</FormLabel>
|
||||
</div>
|
||||
)}
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={formField.value}
|
||||
onCheckedChange={(checked) => {
|
||||
formField.onChange(checked);
|
||||
field.onChange?.(checked);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
{field.label && field.horizontal && (
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel className="font-normal">
|
||||
{field.label}{' '}
|
||||
{field.required && (
|
||||
<span className="text-destructive">*</span>
|
||||
)}
|
||||
</FormLabel>
|
||||
</div>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
case FormFieldType.Tag:
|
||||
return (
|
||||
<RAGFlowFormItem
|
||||
name={field.name}
|
||||
label={field.label}
|
||||
required={field.required}
|
||||
horizontal={field.horizontal}
|
||||
>
|
||||
{(fieldProps) => {
|
||||
const finalFieldProps = field.onChange
|
||||
? {
|
||||
...fieldProps,
|
||||
onChange: (value: string[]) => {
|
||||
fieldProps.onChange(value);
|
||||
field.onChange?.(value);
|
||||
},
|
||||
}
|
||||
: fieldProps;
|
||||
return (
|
||||
// <TagInput {...fieldProps} placeholder={field.placeholder} />
|
||||
<div className="w-full">
|
||||
<EditTag {...finalFieldProps}></EditTag>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</RAGFlowFormItem>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<RAGFlowFormItem
|
||||
name={field.name}
|
||||
label={field.label}
|
||||
required={field.required}
|
||||
horizontal={field.horizontal}
|
||||
>
|
||||
{(fieldProps) => {
|
||||
const finalFieldProps = field.onChange
|
||||
? {
|
||||
...fieldProps,
|
||||
onChange: (e: any) => {
|
||||
fieldProps.onChange(e);
|
||||
field.onChange?.(e.target.value);
|
||||
},
|
||||
}
|
||||
: fieldProps;
|
||||
return (
|
||||
<Input
|
||||
{...finalFieldProps}
|
||||
type={field.type}
|
||||
placeholder={field.placeholder}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</RAGFlowFormItem>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={`space-y-6 ${className}`}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{fields.map((field) => (
|
||||
<div key={field.name} className={cn({ hidden: field.hidden })}>
|
||||
{renderField(field)}
|
||||
</div>
|
||||
))}
|
||||
{children}
|
||||
</>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
},
|
||||
) as <T extends FieldValues>(
|
||||
props: DynamicFormProps<T> & { ref?: React.Ref<DynamicFormRef> },
|
||||
) => React.ReactElement,
|
||||
|
||||
SavingButton: ({
|
||||
submitLoading,
|
||||
buttonText,
|
||||
submitFunc,
|
||||
}: {
|
||||
submitLoading: boolean;
|
||||
buttonText?: string;
|
||||
submitFunc?: (values: FieldValues) => void;
|
||||
}) => {
|
||||
const form = useFormContext();
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled={submitLoading}
|
||||
onClick={() => {
|
||||
console.log('form submit');
|
||||
(async () => {
|
||||
console.log('form submit2');
|
||||
try {
|
||||
let beValid = await form.formControl.trigger();
|
||||
console.log('form valid', beValid, form, form.formControl);
|
||||
if (beValid) {
|
||||
form.handleSubmit(async (values) => {
|
||||
console.log('form values', values);
|
||||
submitFunc?.(values);
|
||||
})();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
console.log('form submit3');
|
||||
}
|
||||
})();
|
||||
}}
|
||||
className={cn(
|
||||
'px-2 py-1 bg-primary text-primary-foreground rounded-md hover:bg-primary/90',
|
||||
)}
|
||||
>
|
||||
{submitLoading && (
|
||||
<Loader className="inline-block mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
{buttonText ?? t('modal.okText')}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
|
||||
CancelButton: ({
|
||||
handleCancel,
|
||||
cancelText,
|
||||
}: {
|
||||
handleCancel: () => void;
|
||||
cancelText?: string;
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCancel()}
|
||||
className="px-2 py-1 border border-input rounded-md hover:bg-muted"
|
||||
>
|
||||
{cancelText ?? t('modal.cancelText')}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export { DynamicForm };
|
||||
|
||||
/**
|
||||
* Usage Example 1: Basic Form
|
||||
*
|
||||
* <DynamicForm
|
||||
* fields={[
|
||||
* {
|
||||
* name: "username",
|
||||
* label: "Username",
|
||||
* type: FormFieldType.Text,
|
||||
* required: true,
|
||||
* placeholder: "Please enter username"
|
||||
* },
|
||||
* {
|
||||
* name: "email",
|
||||
* label: "Email",
|
||||
* type: FormFieldType.Email,
|
||||
* required: true,
|
||||
* placeholder: "Please enter email address"
|
||||
* }
|
||||
* ]}
|
||||
* onSubmit={(data) => {
|
||||
* console.log(data); // { username: "...", email: "..." }
|
||||
* }}
|
||||
* />
|
||||
*
|
||||
* Usage Example 2: Nested Object Form
|
||||
*
|
||||
* <DynamicForm
|
||||
* fields={[
|
||||
* {
|
||||
* name: "user.name",
|
||||
* label: "Name",
|
||||
* type: FormFieldType.Text,
|
||||
* required: true,
|
||||
* placeholder: "Please enter name"
|
||||
* },
|
||||
* {
|
||||
* name: "user.email",
|
||||
* label: "Email",
|
||||
* type: FormFieldType.Email,
|
||||
* required: true,
|
||||
* placeholder: "Please enter email address"
|
||||
* },
|
||||
* {
|
||||
* name: "user.profile.age",
|
||||
* label: "Age",
|
||||
* type: FormFieldType.Number,
|
||||
* required: true,
|
||||
* validation: {
|
||||
* min: 18,
|
||||
* max: 100,
|
||||
* message: "Age must be between 18 and 100"
|
||||
* }
|
||||
* },
|
||||
* {
|
||||
* name: "user.profile.bio",
|
||||
* label: "Bio",
|
||||
* type: FormFieldType.Textarea,
|
||||
* placeholder: "Please briefly introduce yourself"
|
||||
* },
|
||||
* {
|
||||
* name: "settings.notifications",
|
||||
* label: "Enable Notifications",
|
||||
* type: FormFieldType.Checkbox
|
||||
* }
|
||||
* ]}
|
||||
* onSubmit={(data) => {
|
||||
* console.log(data);
|
||||
* // {
|
||||
* // user: {
|
||||
* // name: "...",
|
||||
* // email: "...",
|
||||
* // profile: {
|
||||
* // age: ...,
|
||||
* // bio: "..."
|
||||
* // }
|
||||
* // },
|
||||
* // settings: {
|
||||
* // notifications: true/false
|
||||
* // }
|
||||
* // }
|
||||
* }}
|
||||
* />
|
||||
*
|
||||
* Usage Example 3: Tag Type Form
|
||||
*
|
||||
* <DynamicForm
|
||||
* fields={[
|
||||
* {
|
||||
* name: "skills",
|
||||
* label: "Skill Tags",
|
||||
* type: FormFieldType.Tag,
|
||||
* required: true,
|
||||
* placeholder: "Enter skill and press Enter to add tag"
|
||||
* },
|
||||
* {
|
||||
* name: "user.hobbies",
|
||||
* label: "Hobbies",
|
||||
* type: FormFieldType.Tag,
|
||||
* placeholder: "Enter hobby and press Enter to add tag"
|
||||
* }
|
||||
* ]}
|
||||
* onSubmit={(data) => {
|
||||
* console.log(data);
|
||||
* // {
|
||||
* // skills: ["JavaScript", "React", "TypeScript"],
|
||||
* // user: {
|
||||
* // hobbies: ["Reading", "Swimming", "Travel"]
|
||||
* // }
|
||||
* // }
|
||||
* }}
|
||||
* />
|
||||
*/
|
||||
173
web/src/pages/user-setting/data-source/contant.tsx
Normal file
173
web/src/pages/user-setting/data-source/contant.tsx
Normal file
@ -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: <SvgIcon name={'data-source/s3'} width={28} />,
|
||||
},
|
||||
[DataSourceKey.NOTION]: {
|
||||
name: 'Notion',
|
||||
description: t(`setting.${DataSourceKey.NOTION}Description`),
|
||||
icon: <SvgIcon name={'data-source/notion'} width={28} />,
|
||||
},
|
||||
[DataSourceKey.DISCORD]: {
|
||||
name: 'Discord',
|
||||
description: t(`setting.${DataSourceKey.DISCORD}Description`),
|
||||
icon: <SvgIcon name={'data-source/discord'} width={28} />,
|
||||
},
|
||||
};
|
||||
|
||||
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: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -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<DynamicFormRef>(null);
|
||||
|
||||
const { data: detail } = useFetchDataSourceDetail();
|
||||
const { handleResume } = useDataSourceResume();
|
||||
|
||||
const detailInfo = useMemo(() => {
|
||||
if (detail) {
|
||||
return DataSourceInfo[detail.source];
|
||||
}
|
||||
}, [detail]);
|
||||
|
||||
const [fields, setFields] = useState<FormFieldConfig[]>([]);
|
||||
const [defaultValues, setDefaultValues] = useState<FieldValues>(
|
||||
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) => (
|
||||
<div className="flex items-center gap-1 w-full relative">
|
||||
<Input {...fieldProps} type={FormFieldType.Number} />
|
||||
<span className="absolute right-0 -translate-x-12 text-text-secondary italic ">
|
||||
minutes
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="text-text-secondary bg-bg-input rounded-sm text-xs h-full p-2 border border-border-button "
|
||||
onClick={() => {
|
||||
runSchedule();
|
||||
}}
|
||||
>
|
||||
{detail?.status === RunningStatus.RUNNING ||
|
||||
detail?.status === RunningStatus.SCHEDULE ? (
|
||||
<CirclePause size={12} />
|
||||
) : (
|
||||
<Repeat size={12} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Prune Freq',
|
||||
name: 'prune_freq',
|
||||
type: FormFieldType.Number,
|
||||
required: false,
|
||||
hidden: true,
|
||||
render: (fieldProps: FormFieldConfig) => {
|
||||
return (
|
||||
<div className="flex items-center gap-1 w-full relative">
|
||||
<Input {...fieldProps} type={FormFieldType.Number} />
|
||||
<span className="absolute right-0 -translate-x-3 text-text-secondary italic ">
|
||||
hours
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Timeout Secs',
|
||||
name: 'timeout_secs',
|
||||
type: FormFieldType.Number,
|
||||
required: false,
|
||||
render: (fieldProps: FormFieldConfig) => (
|
||||
<div className="flex items-center gap-1 w-full relative">
|
||||
<Input {...fieldProps} type={FormFieldType.Number} />
|
||||
<span className="absolute right-0 -translate-x-3 text-text-secondary italic ">
|
||||
minutes
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [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 (
|
||||
<div className="px-10 py-5">
|
||||
<BackButton />
|
||||
<Card className="bg-transparent border border-border-button px-5 pt-[10px] pb-5 rounded-md mt-5">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-0 pb-3">
|
||||
{/* <Users className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
|
||||
<CardTitle className="text-2xl text-text-primary flex gap-1 items-center font-normal pb-3">
|
||||
{detailInfo?.icon}
|
||||
{detail?.name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<Separator className="border-border-button bg-border-button w-[calc(100%+2rem)] -translate-x-4 -translate-y-4" />
|
||||
<CardContent className="p-2 flex flex-col gap-2 max-h-[calc(100vh-190px)] overflow-y-auto scrollbar-auto">
|
||||
<div className="max-w-[1200px]">
|
||||
<DynamicForm.Root
|
||||
ref={formRef}
|
||||
fields={fields}
|
||||
onSubmit={debounce((data) => {
|
||||
handleAddOk(data);
|
||||
}, 500)}
|
||||
defaultValues={defaultValues}
|
||||
/>
|
||||
</div>
|
||||
<section className="flex flex-col gap-2 mt-6">
|
||||
<div className="text-2xl text-text-primary">{t('setting.log')}</div>
|
||||
<DataSourceLogsTable />
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default SourceDetailPage;
|
||||
@ -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 }) => (
|
||||
<div className="flex items-center gap-2 text-text-primary">
|
||||
{row.original.update_date
|
||||
? formatDate(row.original.update_date)
|
||||
: '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: t('knowledgeDetails.status'),
|
||||
cell: ({ row }) => (
|
||||
<FileStatusBadge
|
||||
status={row.original.status as RunningStatus}
|
||||
name={RunningStatusMap[row.original.status as RunningStatus]}
|
||||
className="!w-20"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'kb_name',
|
||||
header: t('knowledgeDetails.dataset'),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-2 text-text-primary cursor-pointer"
|
||||
onClick={() => {
|
||||
console.log('handleToDataSetDetail', row.original.kb_id);
|
||||
handleToDataSetDetail(row.original.kb_id);
|
||||
}}
|
||||
>
|
||||
<RAGFlowAvatar
|
||||
avatar={row.original.avatar}
|
||||
name={row.original.kb_name}
|
||||
className="size-4"
|
||||
/>
|
||||
{row.original.kb_name}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'new_docs_indexed',
|
||||
header: t('setting.newDocs'),
|
||||
},
|
||||
|
||||
{
|
||||
id: 'operations',
|
||||
header: t('setting.errorMsg'),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-1 items-center">
|
||||
{row.original.error_msg}
|
||||
{row.original.error_msg && (
|
||||
<div className="flex justify-start space-x-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<HoverCard>
|
||||
<HoverCardTrigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="p-1"
|
||||
// onClick={() => {
|
||||
// showLog(row, LogTabs.FILE_LOGS);
|
||||
// }}
|
||||
>
|
||||
<Eye />
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-[40vw] max-h-[40vh] overflow-auto bg-bg-base z-[999] px-3 py-2 rounded-md border border-border-default">
|
||||
<div className="space-y-2">
|
||||
{row.original.full_exception_trace}
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
] as ColumnDef<any>[];
|
||||
};
|
||||
|
||||
// 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<any>({
|
||||
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 (
|
||||
// <div className="w-full h-[calc(100vh-360px)]">
|
||||
// <Table rootClassName="max-h-[calc(100vh-380px)]">
|
||||
<div className="w-full">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody className="relative min-w-[1280px] overflow-auto">
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
className="group"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={cell.column.columnDef.meta?.cellClassName}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="flex items-center justify-end mt-4">
|
||||
<div className="space-x-2">
|
||||
{/* <RAGFlowPagination
|
||||
{...{ current: pagination.current, pageSize: pagination.pageSize }}
|
||||
total={pagination.total}
|
||||
onChange={(page, pageSize) => setPagination({ page, pageSize })}
|
||||
/> */}
|
||||
<RAGFlowPagination
|
||||
{...pick(pagination, 'current', 'pageSize')}
|
||||
total={pagination.total}
|
||||
onChange={(page, pageSize) => {
|
||||
setPagination({ page, pageSize });
|
||||
}}
|
||||
></RAGFlowPagination>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
188
web/src/pages/user-setting/data-source/hooks.ts
Normal file
188
web/src/pages/user-setting/data-source/hooks.ts
Normal file
@ -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<IDataSource[]>({
|
||||
queryKey: ['data-source'],
|
||||
queryFn: async () => {
|
||||
const { data } = await dataSourceService.dataSourceList();
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const categorizeDataBySource = (data: IDataSourceBase[]) => {
|
||||
const categorizedData: Record<DataSourceKey, any[]> = {} 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<IDataSorceInfo & { list: Array<IDataSourceBase> }> =
|
||||
[];
|
||||
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<IDataSorceInfo | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [addLoading, setAddLoading] = useState<boolean>(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<boolean>(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<IDataSource>({
|
||||
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 };
|
||||
};
|
||||
151
web/src/pages/user-setting/data-source/index.tsx
Normal file
151
web/src/pages/user-setting/data-source/index.tsx
Normal file
@ -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 (
|
||||
<div className="p-[10px] border border-border-button rounded-lg relative group hover:bg-bg-card">
|
||||
<div className="flex gap-2">
|
||||
<div className="w-6 h-6">{icon}</div>
|
||||
<div className="flex flex-1 flex-col items-start gap-2">
|
||||
<div className="text-base text-text-primary">{name}</div>
|
||||
<div className="text-xs text-text-secondary">{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className=" absolute top-2 right-2">
|
||||
<Button
|
||||
onClick={() =>
|
||||
showAddingModal({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
})
|
||||
}
|
||||
className=" rounded-md px-1 text-bg-base gap-1 bg-text-primary text-xs py-0 h-6 items-center hidden group-hover:flex"
|
||||
>
|
||||
<Plus size={12} />
|
||||
{t('setting.add')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-4 relative ">
|
||||
<Spotlight />
|
||||
|
||||
<Card className="bg-transparent border-none px-0">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 px-4 pt-4 pb-0">
|
||||
<CardTitle className="text-2xl font-medium">
|
||||
{t('setting.dataSources')}
|
||||
<div className="text-sm text-text-secondary">
|
||||
{t('setting.datasourceDescription')}
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Separator className="border-border-button bg-border-button " />
|
||||
<div className=" flex flex-col gap-4 p-4 max-h-[calc(100vh-120px)] overflow-y-auto overflow-x-hidden scrollbar-auto">
|
||||
<div className="flex flex-col gap-3">
|
||||
{categorizedList.map((item, index) => (
|
||||
<AddedSourceCard key={index} {...item} />
|
||||
))}
|
||||
</div>
|
||||
<Card className="bg-transparent border-none mt-8">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-0 pb-4">
|
||||
{/* <Users className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
|
||||
<CardTitle className="text-2xl font-semibold">
|
||||
{t('setting.availableSources')}
|
||||
<div className="text-sm text-text-secondary font-normal">
|
||||
{t('setting.availableSourcesDescription')}
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{/* <TenantTable searchTerm={searchTerm}></TenantTable> */}
|
||||
<div className="grid sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-4 3xl:grid-cols-4 gap-4">
|
||||
{dataSourceTemplates.map((item, index) => (
|
||||
<AbailableSourceCard {...item} key={index} />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{addingModalVisible && (
|
||||
<AddDataSourceModal
|
||||
visible
|
||||
loading={addLoading}
|
||||
hideModal={hideAddingModal}
|
||||
onOk={(data) => {
|
||||
console.log(data);
|
||||
handleAddOk(data);
|
||||
}}
|
||||
sourceData={addSource}
|
||||
></AddDataSourceModal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataSource;
|
||||
45
web/src/pages/user-setting/data-source/interface.ts
Normal file
45
web/src/pages/user-setting/data-source/interface.ts
Normal file
@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user