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
+};