Fix: Data-source S3 page style (#12255)

### What problem does this PR solve?

Fix: Data-source S3 page style

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
chanx
2025-12-29 09:46:35 +08:00
committed by GitHub
parent 2114b9e3ad
commit 647fb115a0
13 changed files with 256 additions and 69 deletions

View File

@ -35,8 +35,15 @@ import { cn } from '@/lib/utils';
import { t } from 'i18next'; import { t } from 'i18next';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { MultiSelect, MultiSelectOptionType } from './ui/multi-select'; import { MultiSelect, MultiSelectOptionType } from './ui/multi-select';
import { Segmented } from './ui/segmented';
import { Switch } from './ui/switch'; import { Switch } from './ui/switch';
const getNestedValue = (obj: any, path: string) => {
return path.split('.').reduce((current, key) => {
return current && current[key] !== undefined ? current[key] : undefined;
}, obj);
};
// Field type enumeration // Field type enumeration
export enum FormFieldType { export enum FormFieldType {
Text = 'text', Text = 'text',
@ -49,6 +56,7 @@ export enum FormFieldType {
Checkbox = 'checkbox', Checkbox = 'checkbox',
Switch = 'switch', Switch = 'switch',
Tag = 'tag', Tag = 'tag',
Segmented = 'segmented',
Custom = 'custom', Custom = 'custom',
} }
@ -138,6 +146,9 @@ export const generateSchema = (fields: FormFieldConfig[]): ZodSchema<any> => {
}); });
} }
break; break;
case FormFieldType.Segmented:
fieldSchema = z.string();
break;
case FormFieldType.Number: case FormFieldType.Number:
fieldSchema = z.coerce.number(); fieldSchema = z.coerce.number();
if (field.validation?.min !== undefined) { if (field.validation?.min !== undefined) {
@ -359,6 +370,34 @@ export const RenderField = ({
); );
} }
switch (field.type) { switch (field.type) {
case FormFieldType.Segmented:
return (
<RAGFlowFormItem
{...field}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (value: any) => {
fieldProps.onChange(value);
field.onChange?.(value);
},
}
: fieldProps;
return (
<Segmented
{...finalFieldProps}
options={field.options || []}
className="w-full"
itemClassName="flex-1 justify-center"
disabled={field.disabled}
/>
);
}}
</RAGFlowFormItem>
);
case FormFieldType.Textarea: case FormFieldType.Textarea:
return ( return (
<RAGFlowFormItem <RAGFlowFormItem
@ -634,17 +673,31 @@ const DynamicForm = {
// Initialize form // Initialize form
const form = useForm<T>({ const form = useForm<T>({
resolver: async (data, context, options) => { resolver: async (data, context, options) => {
const zodResult = await zodResolver(schema)(data, context, options); // Filter out fields that should not render
const activeFields = fields.filter(
(field) => !field.shouldRender || field.shouldRender(data),
);
const activeSchema = generateSchema(activeFields);
const zodResult = await zodResolver(activeSchema)(
data,
context,
options,
);
let combinedErrors = { ...zodResult.errors }; let combinedErrors = { ...zodResult.errors };
const fieldErrors: Record<string, { type: string; message: string }> = const fieldErrors: Record<string, { type: string; message: string }> =
{}; {};
for (const field of fields) { for (const field of fields) {
if (field.customValidate && data[field.name] !== undefined) { if (
field.customValidate &&
getNestedValue(data, field.name) !== undefined &&
(!field.shouldRender || field.shouldRender(data))
) {
try { try {
const result = await field.customValidate( const result = await field.customValidate(
data[field.name], getNestedValue(data, field.name),
data, data,
); );
if (typeof result === 'string') { if (typeof result === 'string') {

View File

@ -71,6 +71,9 @@ export function Segmented({
const [selectedValue, setSelectedValue] = React.useState< const [selectedValue, setSelectedValue] = React.useState<
SegmentedValue | undefined SegmentedValue | undefined
>(value); >(value);
React.useEffect(() => {
setSelectedValue(value);
}, [value]);
const handleOnChange = (e: SegmentedValue) => { const handleOnChange = (e: SegmentedValue) => {
if (onChange) { if (onChange) {
onChange(e); onChange(e);

View File

@ -24,7 +24,7 @@ import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { PipelineResultSearchParams } from '@/pages/dataflow-result/constant'; import { PipelineResultSearchParams } from '@/pages/dataflow-result/constant';
import { NavigateToDataflowResultProps } from '@/pages/dataflow-result/interface'; import { NavigateToDataflowResultProps } from '@/pages/dataflow-result/interface';
import { useDataSourceInfo } from '@/pages/user-setting/data-source/contant'; import { useDataSourceInfo } from '@/pages/user-setting/data-source/constant';
import { IDataSourceInfoMap } from '@/pages/user-setting/data-source/interface'; import { IDataSourceInfoMap } from '@/pages/user-setting/data-source/interface';
import { formatDate, formatSecondsToHumanReadable } from '@/utils/date'; import { formatDate, formatSecondsToHumanReadable } from '@/utils/date';
import { import {

View File

@ -9,7 +9,7 @@ import {
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { IConnector } from '@/interfaces/database/knowledge'; import { IConnector } from '@/interfaces/database/knowledge';
import { delSourceModal } from '@/pages/user-setting/data-source/component/delete-source-modal'; import { delSourceModal } from '@/pages/user-setting/data-source/component/delete-source-modal';
import { useDataSourceInfo } from '@/pages/user-setting/data-source/contant'; import { useDataSourceInfo } from '@/pages/user-setting/data-source/constant';
import { useDataSourceRebuild } from '@/pages/user-setting/data-source/hooks'; import { useDataSourceRebuild } from '@/pages/user-setting/data-source/hooks';
import { IDataSourceBase } from '@/pages/user-setting/data-source/interface'; import { IDataSourceBase } from '@/pages/user-setting/data-source/interface';
import { Link, Settings, Unlink } from 'lucide-react'; import { Link, Settings, Unlink } from 'lucide-react';

View File

@ -8,7 +8,7 @@ import { FormLayout } from '@/constants/form';
import { DocumentParserType } from '@/constants/knowledge'; import { DocumentParserType } from '@/constants/knowledge';
import { PermissionRole } from '@/constants/permission'; import { PermissionRole } from '@/constants/permission';
import { IConnector } from '@/interfaces/database/knowledge'; import { IConnector } from '@/interfaces/database/knowledge';
import { useDataSourceInfo } from '@/pages/user-setting/data-source/contant'; import { useDataSourceInfo } from '@/pages/user-setting/data-source/constant';
import { IDataSourceBase } from '@/pages/user-setting/data-source/interface'; import { IDataSourceBase } from '@/pages/user-setting/data-source/interface';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';

View File

@ -11,7 +11,7 @@ import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useSetDocumentStatus } from '@/hooks/use-document-request'; import { useSetDocumentStatus } from '@/hooks/use-document-request';
import { IDocumentInfo } from '@/interfaces/database/document'; import { IDocumentInfo } from '@/interfaces/database/document';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useDataSourceInfo } from '@/pages/user-setting/data-source/contant'; import { useDataSourceInfo } from '@/pages/user-setting/data-source/constant';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { ColumnDef } from '@tanstack/table-core'; import { ColumnDef } from '@tanstack/table-core';
import { ArrowUpDown, MonitorUp } from 'lucide-react'; import { ArrowUpDown, MonitorUp } from 'lucide-react';

View File

@ -8,7 +8,7 @@ import {
DataSourceFormBaseFields, DataSourceFormBaseFields,
DataSourceFormDefaultValues, DataSourceFormDefaultValues,
DataSourceFormFields, DataSourceFormFields,
} from './contant'; } from './constant';
import { IDataSorceInfo } from './interface'; import { IDataSorceInfo } from './interface';
const AddDataSourceModal = ({ const AddDataSourceModal = ({

View File

@ -2,7 +2,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { Settings, Trash2 } from 'lucide-react'; import { Settings, Trash2 } from 'lucide-react';
import { useDataSourceInfo } from '../contant'; import { useDataSourceInfo } from '../constant';
import { useDeleteDataSource } from '../hooks'; import { useDeleteDataSource } from '../hooks';
import { IDataSorceInfo, IDataSourceBase } from '../interface'; import { IDataSorceInfo, IDataSourceBase } from '../interface';
import { delSourceModal } from './delete-source-modal'; import { delSourceModal } from './delete-source-modal';

View File

@ -3,13 +3,12 @@ import SvgIcon from '@/components/svg-icon';
import { t, TFunction } from 'i18next'; import { t, TFunction } from 'i18next';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { BedrockRegionList } from '../setting-model/constant'; import BoxTokenField from '../component/box-token-field';
import BlobTokenField from './component/blob-token-field'; import { ConfluenceIndexingModeField } from '../component/confluence-token-field';
import BoxTokenField from './component/box-token-field'; import GmailTokenField from '../component/gmail-token-field';
import { ConfluenceIndexingModeField } from './component/confluence-token-field'; import GoogleDriveTokenField from '../component/google-drive-token-field';
import GmailTokenField from './component/gmail-token-field'; import { IDataSourceInfoMap } from '../interface';
import GoogleDriveTokenField from './component/google-drive-token-field'; import { S3Constant } from './s3-constant';
import { IDataSourceInfoMap } from './interface';
export enum DataSourceKey { export enum DataSourceKey {
CONFLUENCE = 'confluence', CONFLUENCE = 'confluence',
S3 = 's3', S3 = 's3',
@ -113,11 +112,6 @@ export const generateDataSourceInfo = (t: TFunction) => {
}; };
}; };
const awsRegionOptions = BedrockRegionList.map((r) => ({
label: r,
value: r,
}));
export const useDataSourceInfo = () => { export const useDataSourceInfo = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [dataSourceInfo, setDataSourceInfo] = useState<IDataSourceInfoMap>( const [dataSourceInfo, setDataSourceInfo] = useState<IDataSourceInfoMap>(
@ -234,47 +228,7 @@ export const DataSourceFormFields = {
required: true, required: true,
}, },
], ],
[DataSourceKey.S3]: [ [DataSourceKey.S3]: S3Constant(t),
{
label: 'Bucket Name',
name: 'config.bucket_name',
type: FormFieldType.Text,
required: true,
},
{
label: 'Region',
name: 'config.credentials.region',
type: FormFieldType.Select,
required: false,
options: awsRegionOptions,
customValidate: (val: string, formValues: any) => {
const credentials = formValues?.config?.credentials || {};
const bucketType = formValues?.config?.bucket_type || 's3';
const hasAccessKey = Boolean(
credentials.aws_access_key_id || credentials.aws_secret_access_key,
);
if (bucketType === 's3' && hasAccessKey) {
return Boolean(val) || 'Region is required when using access key';
}
return true;
},
},
{
label: 'Prefix',
name: 'config.prefix',
type: FormFieldType.Text,
required: false,
tooltip: t('setting.s3PrefixTip'),
},
{
label: 'Credentials',
name: 'config.credentials.__blob_token',
type: FormFieldType.Custom,
hideLabel: true,
required: false,
render: () => <BlobTokenField />,
},
],
[DataSourceKey.NOTION]: [ [DataSourceKey.NOTION]: [
{ {
label: 'Notion Integration Token', label: 'Notion Integration Token',
@ -707,6 +661,7 @@ export const DataSourceFormDefaultValues = {
config: { config: {
bucket_name: '', bucket_name: '',
bucket_type: 's3', bucket_type: 's3',
authMode: 'access_key',
prefix: '', prefix: '',
credentials: { credentials: {
aws_access_key_id: '', aws_access_key_id: '',

View File

@ -0,0 +1,176 @@
import { FormFieldType } from '@/components/dynamic-form';
import { TFunction } from 'i18next';
import { BedrockRegionList } from '../../setting-model/constant';
const awsRegionOptions = BedrockRegionList.map((r) => ({
label: r,
value: r,
}));
export const S3Constant = (t: TFunction) => [
{
label: 'Bucket Name',
name: 'config.bucket_name',
type: FormFieldType.Text,
required: true,
},
{
label: 'Region',
name: 'config.credentials.region',
type: FormFieldType.Select,
required: false,
options: awsRegionOptions,
customValidate: (val: string, formValues: any) => {
const credentials = formValues?.config?.credentials || {};
const bucketType = formValues?.config?.bucket_type || 's3';
const hasAccessKey = Boolean(
credentials.aws_access_key_id || credentials.aws_secret_access_key,
);
if (bucketType === 's3' && hasAccessKey) {
return Boolean(val) || 'Region is required when using access key';
}
return true;
},
},
{
label: 'Prefix',
name: 'config.prefix',
type: FormFieldType.Text,
required: false,
tooltip: t('setting.s3PrefixTip'),
},
{
label: 'Mode',
name: 'config.bucket_type',
type: FormFieldType.Segmented,
options: [
{ label: 'S3', value: 's3' },
{ label: 'S3 Compatible', value: 's3_compatible' },
],
},
{
label: 'Authentication',
name: 'config.authMode',
type: FormFieldType.Segmented,
options: [
{ label: 'Access Key', value: 'access_key' },
{ label: 'IAM Role', value: 'iam_role' },
{ label: 'Assume Role', value: 'assume_role' },
],
shouldRender: (formValues: any) => {
const bucketType = formValues?.config?.bucket_type;
return bucketType === 's3';
},
},
{
name: 'config.credentials.aws_access_key_id',
label: 'AWS Access Key ID',
type: FormFieldType.Text,
customValidate: (val: string, formValues: any) => {
const authMode = formValues?.config?.authMode;
const bucketType = formValues?.config?.bucket_type;
console.log('authMode', authMode, val);
if (
!val &&
(authMode === 'access_key' || bucketType === 's3_compatible')
) {
return 'AWS Access Key ID is required';
}
return true;
},
shouldRender: (formValues: any) => {
const authMode = formValues?.config?.authMode;
const bucketType = formValues?.config?.bucket_type;
return authMode === 'access_key' || bucketType === 's3_compatible';
},
},
{
name: 'config.credentials.aws_secret_access_key',
label: 'AWS Secret Access Key',
type: FormFieldType.Password,
customValidate: (val: string, formValues: any) => {
const authMode = formValues?.config?.authMode;
const bucketType = formValues?.config?.bucket_type;
if (authMode === 'access_key' || bucketType === 's3_compatible') {
return Boolean(val) || '"AWS Secret Access Key" is required';
}
return true;
},
shouldRender: (formValues: any) => {
const authMode = formValues?.config?.authMode;
const bucketType = formValues?.config?.bucket_type;
return authMode === 'access_key' || bucketType === 's3_compatible';
},
},
{
name: 'config.credentials.aws_role_arn',
label: 'Role ARN',
tooltip: 'The role will be assumed by the runtime environment.',
type: FormFieldType.Text,
placeholder: 'arn:aws:iam::123456789012:role/YourRole',
customValidate: (val: string, formValues: any) => {
const authMode = formValues?.config?.authMode;
const bucketType = formValues?.config?.bucket_type;
if (authMode === 'iam_role' || bucketType === 's3') {
return Boolean(val) || '"AWS Secret Access Key" is required';
}
return true;
},
shouldRender: (formValues: any) => {
const authMode = formValues?.config?.authMode;
const bucketType = formValues?.config?.bucket_type;
return authMode === 'iam_role' && bucketType === 's3';
},
},
{
name: 'static.tip',
label: ' ',
type: FormFieldType.Custom,
shouldRender: (formValues: any) => {
const authMode = formValues?.config?.authMode;
const bucketType = formValues?.config?.bucket_type;
return authMode === 'assume_role' && bucketType === 's3';
},
render: () => (
<div className="text-sm text-text-secondary bg-bg-card border border-border-button rounded-md px-3 py-2">
{'No credentials required. Uses the default environment role.'}
</div>
),
},
{
name: 'config.credentials.addressing_style',
label: 'Addressing Style',
tooltip: t('setting.S3CompatibleAddressingStyleTip'),
required: false,
type: FormFieldType.Select,
options: [
{ label: 'Virtual Hosted Style', value: 'virtual' },
{ label: 'Path Style', value: 'path' },
],
shouldRender: (formValues: any) => {
// const authMode = formValues?.config?.authMode;
const bucketType = formValues?.config?.bucket_type;
return bucketType === 's3_compatible';
},
},
{
name: 'config.credentials.endpoint_url',
label: 'Endpoint URL',
tooltip: t('setting.S3CompatibleEndpointUrlTip'),
placeholder: 'https://fsn1.your-objectstorage.com',
required: false,
type: FormFieldType.Text,
shouldRender: (formValues: any) => {
const bucketType = formValues?.config?.bucket_type;
return bucketType === 's3_compatible';
},
},
// {
// label: 'Credentials',
// name: 'config.credentials.__blob_token',
// type: FormFieldType.Custom,
// hideLabel: true,
// required: false,
// render: () => <BlobTokenField />,
// },
];

View File

@ -19,7 +19,7 @@ import {
DataSourceFormDefaultValues, DataSourceFormDefaultValues,
DataSourceFormFields, DataSourceFormFields,
useDataSourceInfo, useDataSourceInfo,
} from '../contant'; } from '../constant';
import { import {
useAddDataSource, useAddDataSource,
useDataSourceResume, useDataSourceResume,
@ -37,7 +37,7 @@ const SourceDetailPage = () => {
if (detail) { if (detail) {
return dataSourceInfo[detail.source]; return dataSourceInfo[detail.source];
} }
}, [detail]); }, [detail, dataSourceInfo]);
const [fields, setFields] = useState<FormFieldConfig[]>([]); const [fields, setFields] = useState<FormFieldConfig[]>([]);
const [defaultValues, setDefaultValues] = useState<FieldValues>( const [defaultValues, setDefaultValues] = useState<FieldValues>(
@ -145,14 +145,14 @@ const SourceDetailPage = () => {
}); });
setFields(newFields); setFields(newFields);
const defultValueTemp = { const defaultValueTemp = {
...(DataSourceFormDefaultValues[ ...(DataSourceFormDefaultValues[
detail?.source as keyof typeof DataSourceFormDefaultValues detail?.source as keyof typeof DataSourceFormDefaultValues
] as FieldValues), ] as FieldValues),
...detail, ...detail,
}; };
console.log('defaultValue', defultValueTemp); console.log('defaultValue', defaultValueTemp);
setDefaultValues(defultValueTemp); setDefaultValues(defaultValueTemp);
} }
}, [detail, customFields, onSubmit]); }, [detail, customFields, onSubmit]);

View File

@ -12,7 +12,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
import { t } from 'i18next'; import { t } from 'i18next';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useParams, useSearchParams } from 'umi'; import { useParams, useSearchParams } from 'umi';
import { DataSourceKey, useDataSourceInfo } from './contant'; import { DataSourceKey, useDataSourceInfo } from './constant';
import { IDataSorceInfo, IDataSource, IDataSourceBase } from './interface'; import { IDataSorceInfo, IDataSource, IDataSourceBase } from './interface';
export const useListDataSource = () => { export const useListDataSource = () => {

View File

@ -10,7 +10,7 @@ import {
} from '../components/user-setting-header'; } from '../components/user-setting-header';
import AddDataSourceModal from './add-datasource-modal'; import AddDataSourceModal from './add-datasource-modal';
import { AddedSourceCard } from './component/added-source-card'; import { AddedSourceCard } from './component/added-source-card';
import { DataSourceKey, useDataSourceInfo } from './contant'; import { DataSourceKey, useDataSourceInfo } from './constant';
import { useAddDataSource, useListDataSource } from './hooks'; import { useAddDataSource, useListDataSource } from './hooks';
import { IDataSorceInfo } from './interface'; import { IDataSorceInfo } from './interface';