Fix: Improve some functional issues with the data source. #10703 (#11081)

### What problem does this PR solve?

Fix: Improve some functional issues with the data source. #10703

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
chanx
2025-11-06 20:07:38 +08:00
committed by GitHub
parent 42edecc98f
commit 0b7b88592f
19 changed files with 937 additions and 79 deletions

View File

@ -0,0 +1,28 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="confluence">
<mask id="mask0_847_577111" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
<rect id="&#231;&#159;&#169;&#229;&#189;&#162;" width="24" height="24" fill="white"/>
</mask>
<g mask="url(#mask0_847_577111)">
</g>
<g id="confluence-svgrepo-com 1" clip-path="url(#clip0_847_577111)">
<g id="Group">
<path id="Vector" d="M3.65113 16.5245C3.46543 16.8274 3.25686 17.1788 3.07973 17.4588C2.92118 17.7267 3.0063 18.0722 3.27115 18.2359L6.98526 20.5215C7.11564 20.602 7.27287 20.6267 7.42168 20.5902C7.57048 20.5537 7.69838 20.4589 7.77665 20.3272C7.92522 20.0787 8.11664 19.7558 8.3252 19.4101C9.79656 16.9817 11.2765 17.2788 13.9449 18.553L17.6276 20.3044C17.7671 20.3707 17.9275 20.3778 18.0723 20.324C18.2171 20.2702 18.3339 20.16 18.3962 20.0187L20.1646 16.0188C20.2896 15.7332 20.1625 15.4001 19.8789 15.2703C19.1018 14.9046 17.5562 14.1761 16.1648 13.5047C11.1594 11.0734 6.90527 11.2305 3.65113 16.5245Z" fill="url(#paint0_linear_847_577111)"/>
<path id="Vector_2" d="M20.3049 7.446C20.4906 7.14315 20.6992 6.79174 20.8763 6.51175C21.0348 6.24383 20.9497 5.89828 20.6849 5.73465L16.9708 3.44904C16.8393 3.36051 16.677 3.33054 16.5226 3.3663C16.3683 3.40205 16.2357 3.50031 16.1565 3.6376C16.008 3.88616 15.8165 4.209 15.608 4.5547C14.1366 6.98316 12.6567 6.68603 9.98823 5.4118L6.31698 3.66903C6.1775 3.60264 6.01709 3.59555 5.8723 3.64938C5.72751 3.7032 5.61068 3.81335 5.54844 3.95473L3.77995 7.95454C3.65501 8.24021 3.78214 8.57331 4.06565 8.70308C4.84276 9.06878 6.3884 9.79732 7.77977 10.4687C12.7967 12.8972 17.0508 12.7343 20.3049 7.446Z" fill="url(#paint1_linear_847_577111)"/>
</g>
</g>
</g>
<defs>
<linearGradient id="paint0_linear_847_577111" x1="1709.47" y1="982.917" x2="1293.54" y2="28.7335" gradientUnits="userSpaceOnUse">
<stop offset="0.18" stop-color="#0052CC"/>
<stop offset="1" stop-color="#2684FF"/>
</linearGradient>
<linearGradient id="paint1_linear_847_577111" x1="19.6761" y1="-105.181" x2="436.26" y2="849.465" gradientUnits="userSpaceOnUse">
<stop offset="0.18" stop-color="#0052CC"/>
<stop offset="1" stop-color="#2684FF"/>
</linearGradient>
<clipPath id="clip0_847_577111">
<rect width="18" height="18" fill="white" transform="translate(3 3)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,625 @@
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;
tooltip?: React.ReactNode;
}
// 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,
};
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}
tooltip={field.tooltip}
>
{(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}
tooltip={field.tooltip}
>
{(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}
tooltip={field.tooltip}
>
{(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 w-full', {
'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"
tooltip={field.tooltip}
>
{field.label}{' '}
{field.required && (
<span className="text-destructive">*</span>
)}
</FormLabel>
</div>
)}
{field.label && field.horizontal && (
<div className="space-y-1 leading-none w-1/4">
<FormLabel
className="font-normal"
tooltip={field.tooltip}
>
{field.label}{' '}
{field.required && (
<span className="text-destructive">*</span>
)}
</FormLabel>
</div>
)}
<FormControl>
<div className={cn({ 'w-full': field.horizontal })}>
<Checkbox
checked={formField.value}
onCheckedChange={(checked) => {
formField.onChange(checked);
field.onChange?.(checked);
}}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
case FormFieldType.Tag:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
>
{(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}
tooltip={field.tooltip}
>
{(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 };

View File

@ -1,7 +1,8 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
import { Search } from 'lucide-react';
import { Eye, EyeOff, Search } from 'lucide-react';
import { useState } from 'react';
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
@ -13,6 +14,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
const isControlled = value !== undefined;
const { defaultValue, ...restProps } = props;
const inputValue = isControlled ? value : defaultValue;
const [showPassword, setShowPassword] = useState(false);
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
if (type === 'number') {
const numValue = e.target.value === '' ? '' : Number(e.target.value);
@ -28,17 +30,32 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
}
};
return (
<input
type={type}
className={cn(
'flex h-8 w-full rounded-md border border-input bg-bg-input px-2 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-text-disabled focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 text-text-primary',
className,
<div className="relative w-full">
<input
type={type === 'password' && showPassword ? 'text' : type}
className={cn(
'flex h-8 w-full rounded-md border border-input bg-bg-input px-2 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-text-disabled focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 text-text-primary',
className,
)}
ref={ref}
value={inputValue ?? ''}
onChange={handleChange}
{...restProps}
/>
{type === 'password' && (
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-4 w-4 text-text-secondary" />
) : (
<Eye className="h-4 w-4 text-text-secondary" />
)}
</button>
)}
ref={ref}
value={inputValue ?? ''}
onChange={handleChange}
{...restProps}
/>
</div>
);
},
);

View File

@ -683,6 +683,11 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
tocEnhanceTip: ` During the parsing of the document, table of contents information was generated (see the 'Enable Table of Contents Extraction' option in the General method). This allows the large model to return table of contents items relevant to the user's query, thereby using these items to retrieve related chunks and apply weighting to these chunks during the sorting process. This approach is derived from mimicking the behavioral logic of how humans search for knowledge in books.`,
},
setting: {
addDataSourceModalTital: 'Create your {{name}} connector',
deleteSourceModalTitle: 'Delete data source',
deleteSourceModalContent: `
<p>Are you sure you want to delete this data source link?</p>`,
deleteSourceModalConfirmText: 'Comfirm',
errorMsg: 'Error message',
newDocs: 'New Docs',
timeStarted: 'Time started',

View File

@ -674,6 +674,11 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
tocEnhanceTip: `解析文档时生成了目录信息见General方法的启用目录抽取让大模型返回和用户问题相关的目录项从而利用目录项拿到相关chunk对这些chunk在排序中进行加权。这种方法来源于模仿人类查询书本中知识的行为逻辑`,
},
setting: {
addDataSourceModalTital: '创建你的 {{name}} 链接',
deleteSourceModalTitle: '删除数据源链接',
deleteSourceModalContent: `
<p>您确定要删除此数据源链接吗?</p>`,
deleteSourceModalConfirmText: '确认',
errorMsg: '错误信息',
newDocs: '新文档',
timeStarted: '开始时间',

View File

@ -49,7 +49,7 @@ export interface IFileLogItem {
process_duration: number;
progress: number;
progress_msg: string;
source_from: string;
source_type: string;
status: string;
task_type: string;
tenant_id: string;

View File

@ -11,11 +11,18 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { RunningStatusMap } from '@/constants/knowledge';
import { useTranslate } from '@/hooks/common-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { cn } from '@/lib/utils';
import { PipelineResultSearchParams } from '@/pages/dataflow-result/constant';
import { NavigateToDataflowResultProps } from '@/pages/dataflow-result/interface';
import { DataSourceInfo } from '@/pages/user-setting/data-source/contant';
import { formatDate, formatSecondsToHumanReadable } from '@/utils/date';
import {
ColumnDef,
@ -77,25 +84,34 @@ export const getFileLogsTableColumns = (
{
accessorKey: 'fileName',
header: t('fileName'),
meta: { cellClassName: 'max-w-[20vw]' },
cell: ({ row }) => (
<div
className="flex items-center gap-2 text-text-primary"
// onClick={navigateToDataflowResult(
// row.original.id,
// row.original.kb_id,
// )}
>
<FileIcon name={row.original.document_name}></FileIcon>
{row.original.document_name}
</div>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex gap-2 cursor-pointer">
<FileIcon name={row.original.document_name}></FileIcon>
<span className={cn('truncate')}>
{row.original.document_name}
</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p>{row.original.document_name}</p>
</TooltipContent>
</Tooltip>
),
},
{
accessorKey: 'source_from',
header: t('source'),
meta: { cellClassName: 'max-w-[10vw]' },
cell: ({ row }) => (
<div className="text-text-primary">
{row.original.source_from || t('localUpload')}
{row.original.source_type
? DataSourceInfo[
row.original.source_type as keyof typeof DataSourceInfo
].icon
: t('localUpload')}
</div>
),
},

View File

@ -66,7 +66,7 @@ export const AddedSourceCard = (props: IAddedSourceCardProps) => {
<div
key={item.id}
className={cn(
'flex flex-row items-center justify-between rounded-md bg-bg-input px-2 py-1 cursor-pointer',
'flex flex-row items-center justify-between rounded-md bg-bg-card px-2 py-1 cursor-pointer',
// { hidden: item.name.indexOf(filterString) <= -1 },
)}
onClick={() => {

View File

@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button';
import { Modal } from '@/components/ui/modal/modal';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { IConnector } from '@/interfaces/database/knowledge';
import { delSourceModal } from '@/pages/user-setting/data-source/component/delete-source-modal';
import { DataSourceInfo } from '@/pages/user-setting/data-source/contant';
import { IDataSourceBase } from '@/pages/user-setting/data-source/interface';
import { Link, Settings, Unlink } from 'lucide-react';
@ -26,7 +27,7 @@ interface DataSourceItemProps extends IDataSourceNodeProps {
const DataSourceItem = (props: DataSourceItemProps) => {
const { t } = useTranslation();
const { id, name, icon, openLinkModalFunc, unbindFunc } = props;
const { id, name, icon, unbindFunc } = props;
const { navigateToDataSourceDetail } = useNavigatePage();
const toDetail = (id: string) => {
@ -71,7 +72,7 @@ const DataSourceItem = (props: DataSourceItemProps) => {
return (
<div className="flex items-center justify-between gap-1 px-2 rounded-md border ">
<div className="flex items-center gap-1">
{icon}
<div className="w-6 h-6 flex-shrink-0">{icon}</div>
<div>{name}</div>
</div>
<div className="flex gap-1 items-center">
@ -94,7 +95,12 @@ const DataSourceItem = (props: DataSourceItemProps) => {
variant={'transparent'}
className="border-none"
onClick={() => {
openUnlinkModal();
// openUnlinkModal();
delSourceModal({
data: props,
type: 'unlink',
onOk: (data) => unbindFunc?.(data as DataSourceItemProps),
});
}}
>
<Unlink />

View File

@ -61,6 +61,7 @@ export const useFetchKnowledgeConfigurationOnMount = (
'parser_id',
'language',
'parser_config',
'connectors',
'pagerank',
'avatar',
]),

View File

@ -248,7 +248,7 @@ export default function DatasetSettings() {
/>
)}
<Divider />
{/* <Divider /> */}
{parseType === 1 && <ChunkMethodForm />}
{/* <LinkDataPipeline

View File

@ -11,6 +11,7 @@ import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useSetDocumentStatus } from '@/hooks/use-document-request';
import { IDocumentInfo } from '@/interfaces/database/document';
import { cn } from '@/lib/utils';
import { DataSourceInfo } from '@/pages/user-setting/data-source/contant';
import { formatDate } from '@/utils/date';
import { ColumnDef } from '@tanstack/table-core';
import { ArrowUpDown } from 'lucide-react';
@ -120,6 +121,19 @@ export function useDatasetTableColumns({
</div>
),
},
{
accessorKey: 'source_from',
header: t('source'),
cell: ({ row }) => (
<div className="text-text-primary">
{row.original.source_type
? DataSourceInfo[
row.original.source_type as keyof typeof DataSourceInfo
]?.icon || t('localUpload')
: t('localUpload')}
</div>
),
},
{
accessorKey: 'status',
header: t('enabled'),

View File

@ -1,9 +1,9 @@
import { DynamicForm, FormFieldConfig } from '@/components/dynamic-form';
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,
@ -39,7 +39,12 @@ const AddDataSourceModal = ({
return (
<Modal
title={t('setting.add')}
title={
<div className="flex flex-col">
{sourceData?.icon}
{t('setting.addDataSourceModalTital', { name: sourceData?.name })}
</div>
}
open={visible || false}
onOpenChange={(open) => !open && hideModal?.()}
// onOk={() => handleOk()}

View File

@ -1,9 +1,9 @@
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';
import { delSourceModal } from './delete-source-modal';
export type IAddedSourceCardProps = IDataSorceInfo & {
list: IDataSourceBase[];
@ -19,7 +19,7 @@ export const AddedSourceCard = (props: IAddedSourceCardProps) => {
<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">
<CardTitle className="text-base items-center flex gap-1 font-normal">
{icon}
{name}
</CardTitle>
@ -28,7 +28,7 @@ export const AddedSourceCard = (props: IAddedSourceCardProps) => {
{list.map((item) => (
<div
key={item.id}
className="flex flex-row items-center justify-between rounded-md bg-bg-input px-[10px] py-4"
className="flex flex-row items-center justify-between rounded-md bg-bg-card px-[10px] py-4"
>
<div className="text-sm text-text-secondary ">{item.name}</div>
<div className="text-sm text-text-secondary flex gap-2">
@ -39,9 +39,20 @@ export const AddedSourceCard = (props: IAddedSourceCardProps) => {
toDetail(item.id);
}}
/>
<ConfirmDeleteDialog onOk={() => handleDelete(item)}>
<Trash2 className="cursor-pointer" size={14} />
</ConfirmDeleteDialog>
{/* <ConfirmDeleteDialog onOk={() => handleDelete(item)}> */}
<Trash2
className="cursor-pointer"
size={14}
onClick={() =>
delSourceModal({
data: item,
onOk: () => {
handleDelete(item);
},
})
}
/>
{/* </ConfirmDeleteDialog> */}
</div>
</div>
))}

View File

@ -0,0 +1,80 @@
import { Button } from '@/components/ui/button';
import { Modal, ModalType } from '@/components/ui/modal/modal';
import { t } from 'i18next';
import { DataSourceInfo } from '../contant';
import { IDataSourceBase } from '../interface';
export type IDelSourceModalProps<T> = Partial<ModalType> & {
data?: T;
type?: 'delete' | 'unlink';
onOk?: (data?: T) => void;
};
export const delSourceModal = <T extends IDataSourceBase>(
props: IDelSourceModalProps<T>,
) => {
const { data, onOk, type = 'delete', ...otherProps } = props;
console.log('data', data);
const config = {
title:
type === 'delete'
? t('setting.deleteSourceModalTitle')
: t('dataflowParser.unlinkSourceModalTitle'),
content: (
<div className="px-2 py-6">
<div className="flex items-center gap-1 p-2 border border-border-button rounded-md mb-3">
<div className="w-6 h-6 flex-shrink-0">
{data?.source ? DataSourceInfo[data?.source].icon : ''}
</div>
<div>{data?.name}</div>
</div>
{type === 'delete' ? (
<div
className="text-sm text-text-secondary"
dangerouslySetInnerHTML={{
__html: t('setting.deleteSourceModalContent'),
}}
></div>
) : (
<div
className="text-sm text-text-secondary"
dangerouslySetInnerHTML={{
__html: t('dataflowParser.unlinkSourceModalContent'),
}}
/>
)}
</div>
),
confirmText:
type === 'delete'
? t('setting.deleteSourceModalConfirmText')
: t('dataflowParser.unlinkSourceModalConfirmText'),
};
Modal.show({
visible: true,
className: '!w-[560px]',
...otherProps,
title: config.title,
children: config.content,
onVisibleChange: () => {
Modal.hide();
},
footer: (
<div className="flex justify-end gap-2">
<Button variant={'outline'} onClick={() => Modal.hide()}>
{t('dataflowParser.changeStepModalCancelText')}
</Button>
<Button
variant={'secondary'}
className="!bg-state-error text-text-base"
onClick={() => {
onOk?.(data);
Modal.hide();
}}
>
{config.confirmText}
</Button>
</div>
),
});
};

View File

@ -1,12 +1,12 @@
import { FormFieldType } from '@/components/dynamic-form';
import SvgIcon from '@/components/svg-icon';
import { t } from 'i18next';
import { FormFieldType } from './component/dynamic-form';
export enum DataSourceKey {
CONFLUENCE = 'confluence',
S3 = 's3',
NOTION = 'notion',
DISCORD = 'discord',
// CONFLUENNCE = 'confluence',
// GMAIL = 'gmail',
// GOOGLE_DRIVER = 'google_driver',
// JIRA = 'jira',
@ -19,17 +19,22 @@ export const DataSourceInfo = {
[DataSourceKey.S3]: {
name: 'S3',
description: t(`setting.${DataSourceKey.S3}Description`),
icon: <SvgIcon name={'data-source/s3'} width={28} />,
icon: <SvgIcon name={'data-source/s3'} width={38} />,
},
[DataSourceKey.NOTION]: {
name: 'Notion',
description: t(`setting.${DataSourceKey.NOTION}Description`),
icon: <SvgIcon name={'data-source/notion'} width={28} />,
icon: <SvgIcon name={'data-source/notion'} width={38} />,
},
[DataSourceKey.DISCORD]: {
name: 'Discord',
description: t(`setting.${DataSourceKey.DISCORD}Description`),
icon: <SvgIcon name={'data-source/discord'} width={28} />,
icon: <SvgIcon name={'data-source/discord'} width={38} />,
},
[DataSourceKey.CONFLUENCE]: {
name: 'Confluence',
description: t(`setting.${DataSourceKey.CONFLUENCE}Description`),
icon: <SvgIcon name={'data-source/confluence'} width={38} />,
},
};
@ -71,7 +76,7 @@ export const DataSourceFormFields = {
{
label: 'AWS Secret Access Key',
name: 'config.credentials.aws_secret_access_key',
type: FormFieldType.Text,
type: FormFieldType.Password,
required: true,
},
{
@ -103,7 +108,7 @@ export const DataSourceFormFields = {
{
label: 'Notion Integration Token',
name: 'config.credentials.notion_integration_token',
type: FormFieldType.Text,
type: FormFieldType.Password,
required: true,
},
{
@ -117,7 +122,7 @@ export const DataSourceFormFields = {
{
label: 'Discord Bot Token',
name: 'config.credentials.discord_bot_token',
type: FormFieldType.Text,
type: FormFieldType.Password,
required: true,
},
{
@ -133,6 +138,38 @@ export const DataSourceFormFields = {
required: false,
},
],
[DataSourceKey.CONFLUENCE]: [
{
label: 'Confluence Username',
name: 'config.credentials.confluence_username',
type: FormFieldType.Text,
required: true,
tooltip: 'A descriptive name for the connector.',
},
{
label: 'Confluence Access Token',
name: 'config.credentials.confluence_access_token',
type: FormFieldType.Password,
required: true,
},
{
label: 'Wiki Base URL',
name: 'config.wiki_base',
type: FormFieldType.Text,
required: false,
tooltip:
'The base URL of your Confluence instance (e.g., https://your-domain.atlassian.net/wiki)',
},
{
label: 'Is Cloud',
name: 'config.is_cloud',
type: FormFieldType.Checkbox,
required: false,
tooltip:
'Check if this is a Confluence Cloud instance, uncheck for Confluence Server/Data Center',
},
],
};
export const DataSourceFormDefaultValues = {
@ -170,4 +207,16 @@ export const DataSourceFormDefaultValues = {
},
},
},
[DataSourceKey.CONFLUENCE]: {
name: '',
source: DataSourceKey.CONFLUENCE,
config: {
wiki_base: '',
is_cloud: true,
credentials: {
confluence_username: '',
confluence_access_token: '',
},
},
},
};

View File

@ -1,4 +1,10 @@
import BackButton from '@/components/back-button';
import {
DynamicForm,
DynamicFormRef,
FormFieldConfig,
FormFieldType,
} from '@/components/dynamic-form';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
@ -8,12 +14,6 @@ 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,
@ -112,7 +112,7 @@ const SourceDetailPage = () => {
<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
seconds
</span>
</div>
),

View File

@ -145,7 +145,7 @@ export const DataSourceLogsTable = () => {
const handleToDataSetDetail = useCallback(
(id: string) => {
console.log('handleToDataSetDetail', id);
navigate(`${Routes.DatasetBase}${Routes.DataSetSetting}/${id}`);
navigate(`${Routes.DatasetBase}${Routes.DatasetBase}/${id}`);
},
[navigate],
);

View File

@ -1,4 +1,4 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { CardTitle } from '@/components/ui/card';
import { useTranslation } from 'react-i18next';
import Spotlight from '@/components/spotlight';
@ -11,21 +11,17 @@ import { DataSourceInfo, DataSourceKey } from './contant';
import { useAddDataSource, useListDataSource } from './hooks';
import { IDataSorceInfo } from './interface';
const dataSourceTemplates = [
{
id: DataSourceKey.CONFLUENCE,
name: DataSourceInfo[DataSourceKey.CONFLUENCE].name,
description: DataSourceInfo[DataSourceKey.CONFLUENCE].description,
icon: DataSourceInfo[DataSourceKey.CONFLUENCE].icon,
},
{
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,
@ -94,16 +90,16 @@ const DataSource = () => {
<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>
{/* <Card className="bg-transparent border-none px-0"> */}
<section className="flex flex-row items-center justify-between space-y-0 px-4 pt-4 pb-0">
<div className="text-2xl font-medium">
{t('setting.dataSources')}
<div className="text-sm text-text-secondary">
{t('setting.datasourceDescription')}
</div>
</div>
</section>
{/* </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">
@ -111,8 +107,8 @@ const DataSource = () => {
<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">
<section className="bg-transparent border-none mt-8">
<header 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')}
@ -120,16 +116,16 @@ const DataSource = () => {
{t('setting.availableSourcesDescription')}
</div>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
</header>
<main 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>
</main>
</section>
</div>
{addingModalVisible && (