This is an automated email from the ASF dual-hosted git repository.

klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 04d560778 Feat: modify Q dev connection  (#8592)
04d560778 is described below

commit 04d560778a262b2e11e6fd3046692ea19f77c5b9
Author: Warren Chen <[email protected]>
AuthorDate: Thu Sep 25 18:00:35 2025 +0800

    Feat: modify Q dev connection  (#8592)
    
    * feat:modify q dev connection ui
    
    * feat:modify q dev connection ui
    
    * fix:test
    
    * fix: scopedata
---
 backend/plugins/q_dev/api/connection.go            |  10 +-
 backend/plugins/q_dev/api/connection_test.go       |  30 +++-
 .../plugins/components/connection-form/index.tsx   |  10 +-
 config-ui/src/plugins/register/q-dev/config.tsx    |  74 ++++-----
 .../q-dev/connection-fields/aws-credentials.tsx    | 173 ++++++++++++---------
 .../connection-fields/identity-center-config.tsx   | 130 +++++++++-------
 .../register/q-dev/connection-fields/index.ts      |   2 +-
 .../register/q-dev/connection-fields/s3-config.tsx |  68 ++++----
 8 files changed, 297 insertions(+), 200 deletions(-)

diff --git a/backend/plugins/q_dev/api/connection.go 
b/backend/plugins/q_dev/api/connection.go
index 5a01e8787..1094d14ef 100644
--- a/backend/plugins/q_dev/api/connection.go
+++ b/backend/plugins/q_dev/api/connection.go
@@ -122,12 +122,12 @@ func validateConnection(connection 
*models.QDevConnection) error {
                return errors.Default.New("Bucket is required")
        }
 
-       // Validate Identity Store fields (now required)
-       if connection.IdentityStoreId == "" {
-               return errors.Default.New("IdentityStoreId is required")
+       // Identity Store fields are optional, but must be provided together if 
used
+       if connection.IdentityStoreId == "" && connection.IdentityStoreRegion 
!= "" {
+               return errors.Default.New("IdentityStoreRegion provided but 
IdentityStoreId is empty")
        }
-       if connection.IdentityStoreRegion == "" {
-               return errors.Default.New("IdentityStoreRegion is required")
+       if connection.IdentityStoreId != "" && connection.IdentityStoreRegion 
== "" {
+               return errors.Default.New("IdentityStoreId provided but 
IdentityStoreRegion is empty")
        }
 
        // Validate rate limit
diff --git a/backend/plugins/q_dev/api/connection_test.go 
b/backend/plugins/q_dev/api/connection_test.go
index 6c39dee3f..03a7e51ca 100644
--- a/backend/plugins/q_dev/api/connection_test.go
+++ b/backend/plugins/q_dev/api/connection_test.go
@@ -111,24 +111,40 @@ func TestValidateConnection_MissingBucket(t *testing.T) {
        assert.Contains(t, err.Error(), "Bucket is required")
 }
 
-func TestValidateConnection_MissingIdentityStoreId(t *testing.T) {
+func TestValidateConnection_EmptyIdentityStoreOk(t *testing.T) {
        connection := &models.QDevConnection{
                QDevConn: models.QDevConn{
                        AccessKeyId:         "AKIAIOSFODNN7EXAMPLE",
                        SecretAccessKey:     
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
                        Region:              "us-east-1",
                        Bucket:              "my-q-dev-bucket",
-                       IdentityStoreId:     "", // Missing
-                       IdentityStoreRegion: "us-west-2",
+                       IdentityStoreId:     "",
+                       IdentityStoreRegion: "",
+               },
+       }
+
+       err := validateConnection(connection)
+       assert.NoError(t, err)
+}
+
+func TestValidateConnection_IdentityStoreRegionWithoutId(t *testing.T) {
+       connection := &models.QDevConnection{
+               QDevConn: models.QDevConn{
+                       AccessKeyId:         "AKIAIOSFODNN7EXAMPLE",
+                       SecretAccessKey:     
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
+                       Region:              "us-east-1",
+                       Bucket:              "my-q-dev-bucket",
+                       IdentityStoreId:     "",
+                       IdentityStoreRegion: "us-east-1",
                },
        }
 
        err := validateConnection(connection)
        assert.Error(t, err)
-       assert.Contains(t, err.Error(), "IdentityStoreId is required")
+       assert.Contains(t, err.Error(), "IdentityStoreRegion")
 }
 
-func TestValidateConnection_MissingIdentityStoreRegion(t *testing.T) {
+func TestValidateConnection_IdentityStoreIdWithoutRegion(t *testing.T) {
        connection := &models.QDevConnection{
                QDevConn: models.QDevConn{
                        AccessKeyId:         "AKIAIOSFODNN7EXAMPLE",
@@ -136,13 +152,13 @@ func TestValidateConnection_MissingIdentityStoreRegion(t 
*testing.T) {
                        Region:              "us-east-1",
                        Bucket:              "my-q-dev-bucket",
                        IdentityStoreId:     "d-1234567890",
-                       IdentityStoreRegion: "", // Missing
+                       IdentityStoreRegion: "",
                },
        }
 
        err := validateConnection(connection)
        assert.Error(t, err)
-       assert.Contains(t, err.Error(), "IdentityStoreRegion is required")
+       assert.Contains(t, err.Error(), "IdentityStoreId provided but 
IdentityStoreRegion is empty")
 }
 
 func TestValidateConnection_InvalidRateLimit(t *testing.T) {
diff --git a/config-ui/src/plugins/components/connection-form/index.tsx 
b/config-ui/src/plugins/components/connection-form/index.tsx
index a0b60b0f7..818f70d09 100644
--- a/config-ui/src/plugins/components/connection-form/index.tsx
+++ b/config-ui/src/plugins/components/connection-form/index.tsx
@@ -77,7 +77,8 @@ export const ConnectionForm = ({ plugin, connectionId, 
onSuccess }: Props) => {
             })
           : API.connection.testOld(
               plugin,
-              pick(values, [
+              pick({ ...initialValues, ...values }, [
+                'name',
                 'endpoint',
                 'token',
                 'username',
@@ -86,6 +87,13 @@ export const ConnectionForm = ({ plugin, connectionId, 
onSuccess }: Props) => {
                 'authMethod',
                 'appId',
                 'secretKey',
+                'accessKeyId',
+                'secretAccessKey',
+                'region',
+                'bucket',
+                'identityStoreId',
+                'identityStoreRegion',
+                'rateLimitPerHour',
                 'tenantId',
                 'tenantType',
                 'dbUrl',
diff --git a/config-ui/src/plugins/register/q-dev/config.tsx 
b/config-ui/src/plugins/register/q-dev/config.tsx
index ad3045203..3c0f71781 100644
--- a/config-ui/src/plugins/register/q-dev/config.tsx
+++ b/config-ui/src/plugins/register/q-dev/config.tsx
@@ -19,6 +19,7 @@
 import { IPluginConfig } from '@/types';
 
 import Icon from './assets/icon.svg?react';
+import { AwsCredentials, IdentityCenterConfig, S3Config } from 
'./connection-fields';
 
 export const QDevConfig: IPluginConfig = {
   plugin: 'q_dev',
@@ -26,57 +27,60 @@ export const QDevConfig: IPluginConfig = {
   icon: ({ color }) => <Icon fill={color} />,
   sort: 20,
   connection: {
-    docLink: '', // TODO: 添加文档链接
+    docLink: 'https://devlake.apache.org/docs/UserManual/plugins/qdev',
     initialValues: {
+      name: '',
       accessKeyId: '',
       secretAccessKey: '',
       region: 'us-east-1',
       bucket: '',
       identityStoreId: '',
-      identityStoreRegion: 'us-east-1',
+      identityStoreRegion: '',
       rateLimitPerHour: 20000,
     },
     fields: [
       'name',
-      {
-        key: 'accessKeyId',
-        label: 'AWS Access Key ID',
-        subLabel: '请输入您的AWS Access Key ID',
-      },
-      {
-        key: 'secretAccessKey',
-        label: 'AWS Secret Access Key',
-        subLabel: '请输入您的AWS Secret Access Key',
-      },
-      {
-        key: 'region',
-        label: 'AWS区域',
-        subLabel: '请输入AWS区域,例如:us-east-1',
-      },
-      {
-        key: 'bucket',
-        label: 'S3存储桶名称',
-        subLabel: '请输入存储Q Developer数据的S3存储桶名称',
-      },
-      {
-        key: 'identityStoreId',
-        label: 'IAM Identity Store ID',
-        subLabel: '请输入Identity Store ID,格式:d-xxxxxxxxxx',
-      },
-      {
-        key: 'identityStoreRegion',
-        label: 'IAM Identity Center区域',
-        subLabel: '请输入IAM Identity Center所在的AWS区域',
-      },
+      ({ type, initialValues, values, setValues, setErrors }: any) => (
+        <AwsCredentials
+          key="qdev-aws"
+          type={type}
+          initialValues={initialValues}
+          values={values}
+          setValues={setValues}
+          setErrors={setErrors}
+        />
+      ),
+      ({ initialValues, values, setValues, setErrors }: any) => (
+        <S3Config
+          key="qdev-s3"
+          initialValues={initialValues}
+          values={values}
+          setValues={setValues}
+          setErrors={setErrors}
+        />
+      ),
+      ({ initialValues, values, setValues, setErrors }: any) => (
+        <IdentityCenterConfig
+          key="qdev-identity"
+          initialValues={initialValues}
+          values={values}
+          setValues={setValues}
+          setErrors={setErrors}
+        />
+      ),
       'proxy',
       {
         key: 'rateLimitPerHour',
-        subLabel: '设置每小时的API请求限制,用于控制数据收集速度',
+        subLabel: 'Set a fixed hourly rate limit if you need to throttle 
collection speed (default 20,000).',
         defaultValue: 20000,
       },
     ],
   },
   dataScope: {
-    title: 'Data Sources',
+    title: 'S3 Prefixes',
+  },
+  scopeConfig: {
+    entities: ['CROSS'],
+    transformation: {},
   },
-};
\ No newline at end of file
+};
diff --git 
a/config-ui/src/plugins/register/q-dev/connection-fields/aws-credentials.tsx 
b/config-ui/src/plugins/register/q-dev/connection-fields/aws-credentials.tsx
index 58e627b2b..c5fd1de10 100644
--- a/config-ui/src/plugins/register/q-dev/connection-fields/aws-credentials.tsx
+++ b/config-ui/src/plugins/register/q-dev/connection-fields/aws-credentials.tsx
@@ -16,115 +16,146 @@
  *
  */
 
-import { ChangeEvent, useEffect } from 'react';
+import { ChangeEvent, useEffect, useMemo, useRef } from 'react';
 import { Input } from 'antd';
 
 import { Block } from '@/components';
 
 interface Props {
-  accessKeyId: string;
-  secretAccessKey: string;
-  region: string;
-  errors: {
-    accessKeyId?: string;
-    secretAccessKey?: string;
-    region?: string;
-  };
+  type: 'create' | 'update';
+  initialValues: any;
+  values: any;
   setValues: (values: any) => void;
   setErrors: (errors: any) => void;
 }
 
-export const AwsCredentials = ({ 
-  accessKeyId, 
-  secretAccessKey, 
-  region, 
-  errors, 
-  setValues, 
-  setErrors 
-}: Props) => {
-  
-  const validateAccessKeyId = (value: string) => {
-    if (!value) {
-      return 'AWS Access Key ID是必填项';
+const ACCESS_KEY_PATTERN = /^[A-Z0-9]{16,32}$/;
+const REGION_PATTERN = /^[a-z]{2}-[a-z]+-\d$/;
+
+const syncError = (key: string, error: string, setErrors: (errors: any) => 
void, ref: React.MutableRefObject<string | undefined>) => {
+  if (ref.current !== error) {
+    ref.current = error;
+    setErrors({ [key]: error });
+  }
+};
+
+export const AwsCredentials = ({ type, initialValues, values, setValues, 
setErrors }: Props) => {
+  const isUpdate = type === 'update';
+
+  const accessKeyId = values.accessKeyId ?? '';
+  const secretAccessKey = values.secretAccessKey ?? '';
+  const region = values.region ?? '';
+
+  useEffect(() => {
+    if (values.accessKeyId === undefined) {
+      setValues({ accessKeyId: initialValues.accessKeyId ?? '' });
+    }
+  }, [initialValues.accessKeyId, values.accessKeyId, setValues]);
+
+  useEffect(() => {
+    if (values.secretAccessKey === undefined) {
+      setValues({ secretAccessKey: type === 'create' ? 
initialValues.secretAccessKey ?? '' : '' });
+    }
+  }, [type, initialValues.secretAccessKey, values.secretAccessKey, setValues]);
+
+  useEffect(() => {
+    if (values.region === undefined) {
+      setValues({ region: initialValues.region ?? 'us-east-1' });
     }
-    if (!/^[A-Z0-9]{20}$/.test(value)) {
-      return 'AWS Access Key ID格式不正确';
+  }, [initialValues.region, values.region, setValues]);
+
+  const accessKeyError = useMemo(() => {
+    if (!accessKeyId) {
+      return isUpdate ? '' : 'AWS Access Key ID is required.';
+    }
+    if (!ACCESS_KEY_PATTERN.test(accessKeyId)) {
+      return 'AWS Access Key ID must contain 16-32 upper case letters or 
digits.';
     }
     return '';
-  };
+  }, [accessKeyId, isUpdate]);
 
-  const validateSecretAccessKey = (value: string) => {
-    if (!value) {
-      return 'AWS Secret Access Key是必填项';
+  const secretKeyError = useMemo(() => {
+    if (!secretAccessKey) {
+      return isUpdate ? '' : 'AWS Secret Access Key is required.';
     }
-    if (value.length < 40) {
-      return 'AWS Secret Access Key长度不足';
+    if (secretAccessKey && secretAccessKey.length < 40) {
+      return 'AWS Secret Access Key looks too short.';
     }
     return '';
-  };
+  }, [secretAccessKey, isUpdate]);
 
-  const validateRegion = (value: string) => {
-    if (!value) {
-      return 'AWS区域是必填项';
+  const regionError = useMemo(() => {
+    if (!region) {
+      return 'AWS Region is required.';
     }
-    if (!/^[a-z0-9-]+$/.test(value)) {
-      return 'AWS区域格式不正确';
+    if (!REGION_PATTERN.test(region)) {
+      return 'AWS Region should look like us-east-1.';
     }
     return '';
-  };
+  }, [region]);
+
+  const accessKeyErrorRef = useRef<string>();
+  const secretKeyErrorRef = useRef<string>();
+  const regionErrorRef = useRef<string>();
+
+  useEffect(() => {
+    syncError('accessKeyId', accessKeyError, setErrors, accessKeyErrorRef);
+  }, [accessKeyError, setErrors]);
+
+  useEffect(() => {
+    syncError('secretAccessKey', secretKeyError, setErrors, secretKeyErrorRef);
+  }, [secretKeyError, setErrors]);
+
+  useEffect(() => {
+    syncError('region', regionError, setErrors, regionErrorRef);
+  }, [regionError, setErrors]);
 
-  const handleAccessKeyIdChange = (e: ChangeEvent<HTMLInputElement>) => {
-    const value = e.target.value;
-    setValues({ accessKeyId: value });
-    setErrors({ accessKeyId: validateAccessKeyId(value) });
+  const handleAccessKeyChange = (e: ChangeEvent<HTMLInputElement>) => {
+    setValues({ accessKeyId: e.target.value.trim() });
   };
 
-  const handleSecretAccessKeyChange = (e: ChangeEvent<HTMLInputElement>) => {
-    const value = e.target.value;
-    setValues({ secretAccessKey: value });
-    setErrors({ secretAccessKey: validateSecretAccessKey(value) });
+  const handleSecretKeyChange = (e: ChangeEvent<HTMLInputElement>) => {
+    setValues({ secretAccessKey: e.target.value.trim() });
   };
 
   const handleRegionChange = (e: ChangeEvent<HTMLInputElement>) => {
-    const value = e.target.value;
-    setValues({ region: value });
-    setErrors({ region: validateRegion(value) });
+    setValues({ region: e.target.value.trim() });
   };
 
   return (
     <>
-      <Block title="AWS Access Key ID" description="请输入您的AWS Access Key ID" 
required>
-        <Input 
-          style={{ width: 386 }} 
-          placeholder="AKIAIOSFODNN7EXAMPLE" 
-          value={accessKeyId} 
-          onChange={handleAccessKeyIdChange}
-          status={errors.accessKeyId ? 'error' : ''}
+      <Block title="AWS Access Key ID" description="Use the Access Key ID of 
the IAM user that can access your S3 bucket." required>
+        <Input
+          style={{ width: 386 }}
+          placeholder="AKIAIOSFODNN7EXAMPLE"
+          value={accessKeyId}
+          onChange={handleAccessKeyChange}
+          status={accessKeyError ? 'error' : ''}
         />
-        {errors.accessKeyId && <div style={{ color: 'red', fontSize: '12px', 
marginTop: '4px' }}>{errors.accessKeyId}</div>}
+        {accessKeyError && <div style={{ marginTop: 4, color: '#f5222d' 
}}>{accessKeyError}</div>}
       </Block>
 
-      <Block title="AWS Secret Access Key" description="请输入您的AWS Secret Access 
Key" required>
-        <Input.Password 
-          style={{ width: 386 }} 
-          placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" 
-          value={secretAccessKey} 
-          onChange={handleSecretAccessKeyChange}
-          status={errors.secretAccessKey ? 'error' : ''}
+      <Block title="AWS Secret Access Key" description="Use the Secret Access 
Key paired with the Access Key ID." required>
+        <Input.Password
+          style={{ width: 386 }}
+          placeholder={isUpdate ? '********' : 
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'}
+          value={secretAccessKey}
+          onChange={handleSecretKeyChange}
+          status={secretKeyError ? 'error' : ''}
         />
-        {errors.secretAccessKey && <div style={{ color: 'red', fontSize: 
'12px', marginTop: '4px' }}>{errors.secretAccessKey}</div>}
+        {secretKeyError && <div style={{ marginTop: 4, color: '#f5222d' 
}}>{secretKeyError}</div>}
       </Block>
 
-      <Block title="AWS区域" description="请输入AWS区域,例如:us-east-1" required>
-        <Input 
-          style={{ width: 386 }} 
-          placeholder="us-east-1" 
-          value={region} 
+      <Block title="AWS Region" description="Region of the S3 bucket, e.g. 
us-east-1." required>
+        <Input
+          style={{ width: 386 }}
+          placeholder="us-east-1"
+          value={region}
           onChange={handleRegionChange}
-          status={errors.region ? 'error' : ''}
+          status={regionError ? 'error' : ''}
         />
-        {errors.region && <div style={{ color: 'red', fontSize: '12px', 
marginTop: '4px' }}>{errors.region}</div>}
+        {regionError && <div style={{ marginTop: 4, color: '#f5222d' 
}}>{regionError}</div>}
       </Block>
     </>
   );
-};
\ No newline at end of file
+};
diff --git 
a/config-ui/src/plugins/register/q-dev/connection-fields/identity-center-config.tsx
 
b/config-ui/src/plugins/register/q-dev/connection-fields/identity-center-config.tsx
index 4f0de4666..bd2333807 100644
--- 
a/config-ui/src/plugins/register/q-dev/connection-fields/identity-center-config.tsx
+++ 
b/config-ui/src/plugins/register/q-dev/connection-fields/identity-center-config.tsx
@@ -16,85 +16,111 @@
  *
  */
 
-import { ChangeEvent } from 'react';
+import { ChangeEvent, useEffect, useMemo, useRef } from 'react';
 import { Input } from 'antd';
 
 import { Block } from '@/components';
 
 interface Props {
-  identityStoreId: string;
-  identityStoreRegion: string;
-  errors: {
-    identityStoreId?: string;
-    identityStoreRegion?: string;
-  };
+  initialValues: any;
+  values: any;
   setValues: (values: any) => void;
   setErrors: (errors: any) => void;
 }
 
-export const IdentityCenterConfig = ({ 
-  identityStoreId, 
-  identityStoreRegion, 
-  errors, 
-  setValues, 
-  setErrors 
-}: Props) => {
-  
-  const validateIdentityStoreId = (value: string) => {
-    if (!value) {
-      return 'Identity Store ID是必填项';
+const STORE_ID_PATTERN = /^d-[a-z0-9]{10}$/;
+const REGION_PATTERN = /^[a-z]{2}-[a-z]+-\d$/;
+
+export const IdentityCenterConfig = ({ initialValues, values, setValues, 
setErrors }: Props) => {
+  const identityStoreId = values.identityStoreId ?? '';
+  const identityStoreRegion = values.identityStoreRegion ?? '';
+
+  useEffect(() => {
+    if (values.identityStoreId === undefined) {
+      setValues({ identityStoreId: initialValues.identityStoreId ?? '' });
+    }
+  }, [initialValues.identityStoreId, values.identityStoreId, setValues]);
+
+  useEffect(() => {
+    if (values.identityStoreRegion === undefined) {
+      setValues({ identityStoreRegion: initialValues.identityStoreRegion ?? '' 
});
+    }
+  }, [initialValues.identityStoreRegion, values.identityStoreRegion, 
setValues]);
+
+  const storeIdError = useMemo(() => {
+    if (!identityStoreId) {
+      return '';
     }
-    if (!/^d-[a-z0-9]{10}$/.test(value)) {
-      return 'Identity Store ID格式不正确,应为:d-xxxxxxxxxx';
+    if (!STORE_ID_PATTERN.test(identityStoreId)) {
+      return 'Expected format d-xxxxxxxxxx (lowercase letters and digits).';
     }
     return '';
-  };
+  }, [identityStoreId]);
 
-  const validateIdentityStoreRegion = (value: string) => {
-    if (!value) {
-      return 'Identity Center区域是必填项';
+  const regionError = useMemo(() => {
+    if (!identityStoreRegion) {
+      return identityStoreId ? 'Identity Center region is required when 
providing an Identity Store ID.' : '';
     }
-    if (!/^[a-z0-9-]+$/.test(value)) {
-      return 'Identity Center区域格式不正确';
+    if (!REGION_PATTERN.test(identityStoreRegion)) {
+      return 'Region should look like us-east-1.';
     }
     return '';
-  };
+  }, [identityStoreRegion, identityStoreId]);
+
+  const storeIdErrorRef = useRef<string>();
+  const regionErrorRef = useRef<string>();
+
+  useEffect(() => {
+    if (storeIdErrorRef.current !== storeIdError) {
+      storeIdErrorRef.current = storeIdError;
+      setErrors({ identityStoreId: storeIdError });
+    }
+  }, [storeIdError, setErrors]);
+
+  useEffect(() => {
+    if (regionErrorRef.current !== regionError) {
+      regionErrorRef.current = regionError;
+      setErrors({ identityStoreRegion: regionError });
+    }
+  }, [regionError, setErrors]);
 
-  const handleIdentityStoreIdChange = (e: ChangeEvent<HTMLInputElement>) => {
-    const value = e.target.value;
-    setValues({ identityStoreId: value });
-    setErrors({ identityStoreId: validateIdentityStoreId(value) });
+  const handleStoreIdChange = (e: ChangeEvent<HTMLInputElement>) => {
+    setValues({ identityStoreId: e.target.value.trim() });
   };
 
-  const handleIdentityStoreRegionChange = (e: ChangeEvent<HTMLInputElement>) 
=> {
-    const value = e.target.value;
-    setValues({ identityStoreRegion: value });
-    setErrors({ identityStoreRegion: validateIdentityStoreRegion(value) });
+  const handleRegionChange = (e: ChangeEvent<HTMLInputElement>) => {
+    setValues({ identityStoreRegion: e.target.value.trim() });
   };
 
   return (
     <>
-      <Block title="IAM Identity Store ID" description="请输入Identity Store 
ID,格式:d-xxxxxxxxxx" required>
-        <Input 
-          style={{ width: 386 }} 
-          placeholder="d-1234567890" 
-          value={identityStoreId} 
-          onChange={handleIdentityStoreIdChange}
-          status={errors.identityStoreId ? 'error' : ''}
+      <Block
+        title="IAM Identity Store ID"
+        description="Optional. Provide if you want DevLake to resolve user 
display names (format d-xxxxxxxxxx)."
+      >
+        <Input
+          style={{ width: 386 }}
+          placeholder="d-1234567890"
+          value={identityStoreId}
+          onChange={handleStoreIdChange}
+          status={storeIdError ? 'error' : ''}
         />
-        {errors.identityStoreId && <div style={{ color: 'red', fontSize: 
'12px', marginTop: '4px' }}>{errors.identityStoreId}</div>}
+        {storeIdError && <div style={{ marginTop: 4, color: '#f5222d' 
}}>{storeIdError}</div>}
       </Block>
 
-      <Block title="IAM Identity Center区域" description="请输入IAM Identity 
Center所在的AWS区域" required>
-        <Input 
-          style={{ width: 386 }} 
-          placeholder="us-east-1" 
-          value={identityStoreRegion} 
-          onChange={handleIdentityStoreRegionChange}
-          status={errors.identityStoreRegion ? 'error' : ''}
+      <Block
+        title="IAM Identity Center Region"
+        description="Optional. Required only when Identity Store ID is 
provided (e.g. us-east-1)."
+      >
+        <Input
+          style={{ width: 386 }}
+          placeholder="us-east-1"
+          value={identityStoreRegion}
+          onChange={handleRegionChange}
+          status={regionError ? 'error' : ''}
         />
-        {errors.identityStoreRegion && <div style={{ color: 'red', fontSize: 
'12px', marginTop: '4px' }}>{errors.identityStoreRegion}</div>}
+        {regionError && <div style={{ marginTop: 4, color: '#f5222d' 
}}>{regionError}</div>}
       </Block>
     </>
   );
-};
\ No newline at end of file
+};
diff --git a/config-ui/src/plugins/register/q-dev/connection-fields/index.ts 
b/config-ui/src/plugins/register/q-dev/connection-fields/index.ts
index c01b72f0b..2ad90f775 100644
--- a/config-ui/src/plugins/register/q-dev/connection-fields/index.ts
+++ b/config-ui/src/plugins/register/q-dev/connection-fields/index.ts
@@ -18,4 +18,4 @@
 
 export * from './aws-credentials';
 export * from './s3-config';
-export * from './identity-center-config';
\ No newline at end of file
+export * from './identity-center-config';
diff --git 
a/config-ui/src/plugins/register/q-dev/connection-fields/s3-config.tsx 
b/config-ui/src/plugins/register/q-dev/connection-fields/s3-config.tsx
index 9cdfa8f3c..d36843080 100644
--- a/config-ui/src/plugins/register/q-dev/connection-fields/s3-config.tsx
+++ b/config-ui/src/plugins/register/q-dev/connection-fields/s3-config.tsx
@@ -16,49 +16,61 @@
  *
  */
 
-import { ChangeEvent } from 'react';
+import { ChangeEvent, useEffect, useMemo, useRef } from 'react';
 import { Input } from 'antd';
 
 import { Block } from '@/components';
 
 interface Props {
-  bucket: string;
-  error?: string;
-  setValue: (value: string) => void;
-  setError: (error: string) => void;
+  initialValues: any;
+  values: any;
+  setValues: (values: any) => void;
+  setErrors: (errors: any) => void;
 }
 
-export const S3Config = ({ bucket, error, setValue, setError }: Props) => {
-  
-  const validateBucket = (value: string) => {
-    if (!value) {
-      return 'S3存储桶名称是必填项';
+const BUCKET_PATTERN = /^[a-z0-9](?:[a-z0-9.-]{1,61}[a-z0-9])?$/;
+
+export const S3Config = ({ initialValues, values, setValues, setErrors }: 
Props) => {
+  const bucket = values.bucket ?? '';
+
+  useEffect(() => {
+    if (values.bucket === undefined) {
+      setValues({ bucket: initialValues.bucket ?? '' });
     }
-    if (!/^[a-z0-9.-]+$/.test(value)) {
-      return 'S3存储桶名称格式不正确,只能包含小写字母、数字、点和连字符';
+  }, [initialValues.bucket, values.bucket, setValues]);
+
+  const bucketError = useMemo(() => {
+    if (!bucket) {
+      return 'S3 bucket name is required.';
     }
-    if (value.length < 3 || value.length > 63) {
-      return 'S3存储桶名称长度必须在3-63个字符之间';
+    if (!BUCKET_PATTERN.test(bucket) || bucket.length < 3 || bucket.length > 
63 || bucket.includes('..')) {
+      return 'Bucket names must be 3-63 characters, lowercase, numbers, dots 
or hyphens.';
     }
     return '';
-  };
+  }, [bucket]);
+
+  const bucketErrorRef = useRef<string>();
+  useEffect(() => {
+    if (bucketErrorRef.current !== bucketError) {
+      bucketErrorRef.current = bucketError;
+      setErrors({ bucket: bucketError });
+    }
+  }, [bucketError, setErrors]);
 
-  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
-    const value = e.target.value;
-    setValue(value);
-    setError(validateBucket(value));
+  const handleBucketChange = (e: ChangeEvent<HTMLInputElement>) => {
+    setValues({ bucket: e.target.value.trim() });
   };
 
   return (
-    <Block title="S3存储桶名称" description="请输入存储Q Developer数据的S3存储桶名称" required>
-      <Input 
-        style={{ width: 386 }} 
-        placeholder="my-qdev-data-bucket" 
-        value={bucket} 
-        onChange={handleChange}
-        status={error ? 'error' : ''}
+    <Block title="S3 Bucket" description="Name of the bucket that stores the Q 
Developer CSV files." required>
+      <Input
+        style={{ width: 386 }}
+        placeholder="my-q-dev-data"
+        value={bucket}
+        onChange={handleBucketChange}
+        status={bucketError ? 'error' : ''}
       />
-      {error && <div style={{ color: 'red', fontSize: '12px', marginTop: '4px' 
}}>{error}</div>}
+      {bucketError && <div style={{ marginTop: 4, color: '#f5222d' 
}}>{bucketError}</div>}
     </Block>
   );
-};
\ No newline at end of file
+};

Reply via email to