diff --git a/web/src/components/dynamic-form.tsx b/web/src/components/dynamic-form.tsx index 5aea77273..2bf30605d 100644 --- a/web/src/components/dynamic-form.tsx +++ b/web/src/components/dynamic-form.tsx @@ -35,8 +35,15 @@ import { cn } from '@/lib/utils'; import { t } from 'i18next'; import { Loader } from 'lucide-react'; import { MultiSelect, MultiSelectOptionType } from './ui/multi-select'; +import { Segmented } from './ui/segmented'; 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 export enum FormFieldType { Text = 'text', @@ -49,6 +56,7 @@ export enum FormFieldType { Checkbox = 'checkbox', Switch = 'switch', Tag = 'tag', + Segmented = 'segmented', Custom = 'custom', } @@ -138,6 +146,9 @@ export const generateSchema = (fields: FormFieldConfig[]): ZodSchema => { }); } break; + case FormFieldType.Segmented: + fieldSchema = z.string(); + break; case FormFieldType.Number: fieldSchema = z.coerce.number(); if (field.validation?.min !== undefined) { @@ -359,6 +370,34 @@ export const RenderField = ({ ); } switch (field.type) { + case FormFieldType.Segmented: + return ( + + {(fieldProps) => { + const finalFieldProps = field.onChange + ? { + ...fieldProps, + onChange: (value: any) => { + fieldProps.onChange(value); + field.onChange?.(value); + }, + } + : fieldProps; + return ( + + ); + }} + + ); case FormFieldType.Textarea: return ( ({ 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 }; const fieldErrors: Record = {}; 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 { const result = await field.customValidate( - data[field.name], + getNestedValue(data, field.name), data, ); if (typeof result === 'string') { diff --git a/web/src/components/ui/segmented.tsx b/web/src/components/ui/segmented.tsx index 3f9b0cc53..c63cd6916 100644 --- a/web/src/components/ui/segmented.tsx +++ b/web/src/components/ui/segmented.tsx @@ -71,6 +71,9 @@ export function Segmented({ const [selectedValue, setSelectedValue] = React.useState< SegmentedValue | undefined >(value); + React.useEffect(() => { + setSelectedValue(value); + }, [value]); const handleOnChange = (e: SegmentedValue) => { if (onChange) { onChange(e); diff --git a/web/src/pages/dataset/dataset-overview/overview-table.tsx b/web/src/pages/dataset/dataset-overview/overview-table.tsx index a09f56c9a..978f697b9 100644 --- a/web/src/pages/dataset/dataset-overview/overview-table.tsx +++ b/web/src/pages/dataset/dataset-overview/overview-table.tsx @@ -24,7 +24,7 @@ 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 { 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 { formatDate, formatSecondsToHumanReadable } from '@/utils/date'; import { diff --git a/web/src/pages/dataset/dataset-setting/components/link-data-source.tsx b/web/src/pages/dataset/dataset-setting/components/link-data-source.tsx index f101f14da..088fa5193 100644 --- a/web/src/pages/dataset/dataset-setting/components/link-data-source.tsx +++ b/web/src/pages/dataset/dataset-setting/components/link-data-source.tsx @@ -9,7 +9,7 @@ import { 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 { 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 { IDataSourceBase } from '@/pages/user-setting/data-source/interface'; import { Link, Settings, Unlink } from 'lucide-react'; diff --git a/web/src/pages/dataset/dataset-setting/index.tsx b/web/src/pages/dataset/dataset-setting/index.tsx index 57ebb4fb8..81fbc4c53 100644 --- a/web/src/pages/dataset/dataset-setting/index.tsx +++ b/web/src/pages/dataset/dataset-setting/index.tsx @@ -8,7 +8,7 @@ import { FormLayout } from '@/constants/form'; import { DocumentParserType } from '@/constants/knowledge'; import { PermissionRole } from '@/constants/permission'; 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 { zodResolver } from '@hookform/resolvers/zod'; import { useEffect, useState } from 'react'; diff --git a/web/src/pages/dataset/dataset/use-dataset-table-columns.tsx b/web/src/pages/dataset/dataset/use-dataset-table-columns.tsx index e303136d6..ce877ed1f 100644 --- a/web/src/pages/dataset/dataset/use-dataset-table-columns.tsx +++ b/web/src/pages/dataset/dataset/use-dataset-table-columns.tsx @@ -11,7 +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 { useDataSourceInfo } from '@/pages/user-setting/data-source/contant'; +import { useDataSourceInfo } from '@/pages/user-setting/data-source/constant'; import { formatDate } from '@/utils/date'; import { ColumnDef } from '@tanstack/table-core'; import { ArrowUpDown, MonitorUp } from 'lucide-react'; diff --git a/web/src/pages/user-setting/data-source/add-datasource-modal.tsx b/web/src/pages/user-setting/data-source/add-datasource-modal.tsx index 24243c326..5196ef17b 100644 --- a/web/src/pages/user-setting/data-source/add-datasource-modal.tsx +++ b/web/src/pages/user-setting/data-source/add-datasource-modal.tsx @@ -8,7 +8,7 @@ import { DataSourceFormBaseFields, DataSourceFormDefaultValues, DataSourceFormFields, -} from './contant'; +} from './constant'; import { IDataSorceInfo } from './interface'; const AddDataSourceModal = ({ diff --git a/web/src/pages/user-setting/data-source/component/added-source-card.tsx b/web/src/pages/user-setting/data-source/component/added-source-card.tsx index aeffe80ba..7c340c156 100644 --- a/web/src/pages/user-setting/data-source/component/added-source-card.tsx +++ b/web/src/pages/user-setting/data-source/component/added-source-card.tsx @@ -2,7 +2,7 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { Settings, Trash2 } from 'lucide-react'; -import { useDataSourceInfo } from '../contant'; +import { useDataSourceInfo } from '../constant'; import { useDeleteDataSource } from '../hooks'; import { IDataSorceInfo, IDataSourceBase } from '../interface'; import { delSourceModal } from './delete-source-modal'; diff --git a/web/src/pages/user-setting/data-source/contant.tsx b/web/src/pages/user-setting/data-source/constant/index.tsx similarity index 92% rename from web/src/pages/user-setting/data-source/contant.tsx rename to web/src/pages/user-setting/data-source/constant/index.tsx index 14c6e1642..ab8bb730f 100644 --- a/web/src/pages/user-setting/data-source/contant.tsx +++ b/web/src/pages/user-setting/data-source/constant/index.tsx @@ -3,13 +3,12 @@ import SvgIcon from '@/components/svg-icon'; import { t, TFunction } from 'i18next'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { BedrockRegionList } from '../setting-model/constant'; -import BlobTokenField from './component/blob-token-field'; -import BoxTokenField from './component/box-token-field'; -import { ConfluenceIndexingModeField } from './component/confluence-token-field'; -import GmailTokenField from './component/gmail-token-field'; -import GoogleDriveTokenField from './component/google-drive-token-field'; -import { IDataSourceInfoMap } from './interface'; +import BoxTokenField from '../component/box-token-field'; +import { ConfluenceIndexingModeField } from '../component/confluence-token-field'; +import GmailTokenField from '../component/gmail-token-field'; +import GoogleDriveTokenField from '../component/google-drive-token-field'; +import { IDataSourceInfoMap } from '../interface'; +import { S3Constant } from './s3-constant'; export enum DataSourceKey { CONFLUENCE = 'confluence', S3 = 's3', @@ -113,11 +112,6 @@ export const generateDataSourceInfo = (t: TFunction) => { }; }; -const awsRegionOptions = BedrockRegionList.map((r) => ({ - label: r, - value: r, -})); - export const useDataSourceInfo = () => { const { t } = useTranslation(); const [dataSourceInfo, setDataSourceInfo] = useState( @@ -234,47 +228,7 @@ export const DataSourceFormFields = { required: true, }, ], - [DataSourceKey.S3]: [ - { - 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: () => , - }, - ], + [DataSourceKey.S3]: S3Constant(t), [DataSourceKey.NOTION]: [ { label: 'Notion Integration Token', @@ -707,6 +661,7 @@ export const DataSourceFormDefaultValues = { config: { bucket_name: '', bucket_type: 's3', + authMode: 'access_key', prefix: '', credentials: { aws_access_key_id: '', diff --git a/web/src/pages/user-setting/data-source/constant/s3-constant.tsx b/web/src/pages/user-setting/data-source/constant/s3-constant.tsx new file mode 100644 index 000000000..93ebda7a1 --- /dev/null +++ b/web/src/pages/user-setting/data-source/constant/s3-constant.tsx @@ -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: () => ( +
+ {'No credentials required. Uses the default environment role.'} +
+ ), + }, + { + 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: () => , + // }, +]; diff --git a/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx b/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx index 19551196b..f4ce8ff27 100644 --- a/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx +++ b/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx @@ -19,7 +19,7 @@ import { DataSourceFormDefaultValues, DataSourceFormFields, useDataSourceInfo, -} from '../contant'; +} from '../constant'; import { useAddDataSource, useDataSourceResume, @@ -37,7 +37,7 @@ const SourceDetailPage = () => { if (detail) { return dataSourceInfo[detail.source]; } - }, [detail]); + }, [detail, dataSourceInfo]); const [fields, setFields] = useState([]); const [defaultValues, setDefaultValues] = useState( @@ -145,14 +145,14 @@ const SourceDetailPage = () => { }); setFields(newFields); - const defultValueTemp = { + const defaultValueTemp = { ...(DataSourceFormDefaultValues[ detail?.source as keyof typeof DataSourceFormDefaultValues ] as FieldValues), ...detail, }; - console.log('defaultValue', defultValueTemp); - setDefaultValues(defultValueTemp); + console.log('defaultValue', defaultValueTemp); + setDefaultValues(defaultValueTemp); } }, [detail, customFields, onSubmit]); diff --git a/web/src/pages/user-setting/data-source/hooks.ts b/web/src/pages/user-setting/data-source/hooks.ts index a07e3c72d..4df6a078a 100644 --- a/web/src/pages/user-setting/data-source/hooks.ts +++ b/web/src/pages/user-setting/data-source/hooks.ts @@ -12,7 +12,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { t } from 'i18next'; import { useCallback, useMemo, useState } from 'react'; import { useParams, useSearchParams } from 'umi'; -import { DataSourceKey, useDataSourceInfo } from './contant'; +import { DataSourceKey, useDataSourceInfo } from './constant'; import { IDataSorceInfo, IDataSource, IDataSourceBase } from './interface'; export const useListDataSource = () => { diff --git a/web/src/pages/user-setting/data-source/index.tsx b/web/src/pages/user-setting/data-source/index.tsx index 9830f4549..801b919db 100644 --- a/web/src/pages/user-setting/data-source/index.tsx +++ b/web/src/pages/user-setting/data-source/index.tsx @@ -10,7 +10,7 @@ import { } from '../components/user-setting-header'; import AddDataSourceModal from './add-datasource-modal'; import { AddedSourceCard } from './component/added-source-card'; -import { DataSourceKey, useDataSourceInfo } from './contant'; +import { DataSourceKey, useDataSourceInfo } from './constant'; import { useAddDataSource, useListDataSource } from './hooks'; import { IDataSorceInfo } from './interface';