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 09607368f feat:add q dev ui (#8587)
09607368f is described below

commit 09607368f50182f91e85f8b7f11c8bbe8c9fd0e2
Author: Warren Chen <[email protected]>
AuthorDate: Tue Sep 23 16:42:06 2025 +0800

    feat:add q dev ui (#8587)
---
 .../migrationscripts/20250320_modify_file_meta.go  |   2 +-
 config-ui/src/plugins/register/index.ts            |   2 +
 .../src/plugins/register/q-dev/assets/icon.svg     |  62 ++++++++++
 config-ui/src/plugins/register/q-dev/config.tsx    |  82 +++++++++++++
 .../q-dev/connection-fields/aws-credentials.tsx    | 130 +++++++++++++++++++++
 .../connection-fields/identity-center-config.tsx   | 100 ++++++++++++++++
 .../register/q-dev/connection-fields/index.ts      |  21 ++++
 .../register/q-dev/connection-fields/s3-config.tsx |  64 ++++++++++
 config-ui/src/plugins/register/q-dev/index.ts      |  19 +++
 9 files changed, 481 insertions(+), 1 deletion(-)

diff --git 
a/backend/plugins/q_dev/models/migrationscripts/20250320_modify_file_meta.go 
b/backend/plugins/q_dev/models/migrationscripts/20250320_modify_file_meta.go
index 9744f6d09..83311cf07 100644
--- a/backend/plugins/q_dev/models/migrationscripts/20250320_modify_file_meta.go
+++ b/backend/plugins/q_dev/models/migrationscripts/20250320_modify_file_meta.go
@@ -51,7 +51,7 @@ func (*modifyFileMetaTable) Up(basicRes context.BasicRes) 
errors.Error {
                        return errors.Default.Wrap(err, "failed to load column 
metadata for _tool_q_dev_s3_file_meta.processed_time")
                }
                if len(cols) == 0 {
-                       // If column is not visible in metadata, treat as no 
processing needed 
+                       // If column is not visible in metadata, treat as no 
processing needed
                        return nil
                }
                if nullable, ok := cols[0].Nullable(); ok {
diff --git a/config-ui/src/plugins/register/index.ts 
b/config-ui/src/plugins/register/index.ts
index 36f87f701..8fb38ee29 100644
--- a/config-ui/src/plugins/register/index.ts
+++ b/config-ui/src/plugins/register/index.ts
@@ -33,6 +33,7 @@ import { TAPDConfig } from './tapd';
 import { WebhookConfig } from './webhook';
 import { ZenTaoConfig } from './zentao';
 import { OpsgenieConfig } from './opsgenie';
+import { QDevConfig } from './q-dev';
 import { TeambitionConfig } from './teambition';
 import { TestmoConfig } from './testmo';
 import { SlackConfig } from './slack/config';
@@ -50,6 +51,7 @@ export const pluginConfigs: IPluginConfig[] = [
   JiraConfig,
   PagerDutyConfig,
   SlackConfig,
+  QDevConfig,
   SonarQubeConfig,
   TAPDConfig,
   TestmoConfig,
diff --git a/config-ui/src/plugins/register/q-dev/assets/icon.svg 
b/config-ui/src/plugins/register/q-dev/assets/icon.svg
new file mode 100644
index 000000000..503114f14
--- /dev/null
+++ b/config-ui/src/plugins/register/q-dev/assets/icon.svg
@@ -0,0 +1,62 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<svg width="100" height="100" viewBox="0 0 36 36" 
xmlns="http://www.w3.org/2000/svg"; role="img" aria-label="Hex logo with pointer 
to edge">
+    <defs>
+        <linearGradient id="g" x1="7" y1="6" x2="29" y2="30" 
gradientUnits="userSpaceOnUse">
+            <stop offset="0" stop-color="#5AA4FF"/>
+            <stop offset="0.55" stop-color="#5C72FF"/>
+            <stop offset="1" stop-color="#7B3BFF"/>
+        </linearGradient>
+        <linearGradient id="glow" x1="8" y1="7" x2="28" y2="29" 
gradientUnits="userSpaceOnUse">
+            <stop offset="0" stop-color="#FFF" stop-opacity="0.95"/>
+            <stop offset="1" stop-color="#FFF" stop-opacity="0.85"/>
+        </linearGradient>
+    </defs>
+
+    <path fill="url(#g)" d="
+    M 18 6
+    L 26.6 10.9
+    a 2.9 2.9 0 0 1 1.45 2.52
+    v 9.16
+    a 2.9 2.9 0 0 1 -1.45 2.52
+    L 18 30
+    L 9.4 25.09
+    A 2.9 2.9 0 0 1 7.95 22.57
+    v -9.16
+    A 2.9 2.9 0 0 1 9.4 10.9
+    L 18 6
+    Z"/>
+
+    <path id="inner" fill="none" stroke="url(#glow)" stroke-width="1.7" d="
+    M 18 8.2
+    L 25.6 12.3
+    a 1.9 1.9 0 0 1 0.95 1.64
+    v 7.92
+    a 1.9 1.9 0 0 1 -0.95 1.64
+    L 18 27.8
+    L 10.4 23.7
+    a 1.9 1.9 0 0 1 -0.95 -1.64
+    v -7.92
+    a 1.9 1.9 0 0 1 0.95 -1.64
+    L 18 8.2
+    Z"/>
+
+    <g stroke="#FFF" stroke-width="2.0" stroke-linecap="round" 
stroke-linejoin="round" fill="none">
+        <path d="M 18.1 17.0 L 24.9 20.3"/>
+        <circle cx="18.1" cy="17.0" r="1.7" fill="#FFF" stroke="none"/>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/config-ui/src/plugins/register/q-dev/config.tsx 
b/config-ui/src/plugins/register/q-dev/config.tsx
new file mode 100644
index 000000000..ad3045203
--- /dev/null
+++ b/config-ui/src/plugins/register/q-dev/config.tsx
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { IPluginConfig } from '@/types';
+
+import Icon from './assets/icon.svg?react';
+
+export const QDevConfig: IPluginConfig = {
+  plugin: 'q_dev',
+  name: 'Q Developer',
+  icon: ({ color }) => <Icon fill={color} />,
+  sort: 20,
+  connection: {
+    docLink: '', // TODO: 添加文档链接
+    initialValues: {
+      accessKeyId: '',
+      secretAccessKey: '',
+      region: 'us-east-1',
+      bucket: '',
+      identityStoreId: '',
+      identityStoreRegion: 'us-east-1',
+      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区域',
+      },
+      'proxy',
+      {
+        key: 'rateLimitPerHour',
+        subLabel: '设置每小时的API请求限制,用于控制数据收集速度',
+        defaultValue: 20000,
+      },
+    ],
+  },
+  dataScope: {
+    title: 'Data Sources',
+  },
+};
\ 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
new file mode 100644
index 000000000..58e627b2b
--- /dev/null
+++ b/config-ui/src/plugins/register/q-dev/connection-fields/aws-credentials.tsx
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { ChangeEvent, useEffect } 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;
+  };
+  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是必填项';
+    }
+    if (!/^[A-Z0-9]{20}$/.test(value)) {
+      return 'AWS Access Key ID格式不正确';
+    }
+    return '';
+  };
+
+  const validateSecretAccessKey = (value: string) => {
+    if (!value) {
+      return 'AWS Secret Access Key是必填项';
+    }
+    if (value.length < 40) {
+      return 'AWS Secret Access Key长度不足';
+    }
+    return '';
+  };
+
+  const validateRegion = (value: string) => {
+    if (!value) {
+      return 'AWS区域是必填项';
+    }
+    if (!/^[a-z0-9-]+$/.test(value)) {
+      return 'AWS区域格式不正确';
+    }
+    return '';
+  };
+
+  const handleAccessKeyIdChange = (e: ChangeEvent<HTMLInputElement>) => {
+    const value = e.target.value;
+    setValues({ accessKeyId: value });
+    setErrors({ accessKeyId: validateAccessKeyId(value) });
+  };
+
+  const handleSecretAccessKeyChange = (e: ChangeEvent<HTMLInputElement>) => {
+    const value = e.target.value;
+    setValues({ secretAccessKey: value });
+    setErrors({ secretAccessKey: validateSecretAccessKey(value) });
+  };
+
+  const handleRegionChange = (e: ChangeEvent<HTMLInputElement>) => {
+    const value = e.target.value;
+    setValues({ region: value });
+    setErrors({ region: validateRegion(value) });
+  };
+
+  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' : ''}
+        />
+        {errors.accessKeyId && <div style={{ color: 'red', fontSize: '12px', 
marginTop: '4px' }}>{errors.accessKeyId}</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' : ''}
+        />
+        {errors.secretAccessKey && <div style={{ color: 'red', fontSize: 
'12px', marginTop: '4px' }}>{errors.secretAccessKey}</div>}
+      </Block>
+
+      <Block title="AWS区域" description="请输入AWS区域,例如:us-east-1" required>
+        <Input 
+          style={{ width: 386 }} 
+          placeholder="us-east-1" 
+          value={region} 
+          onChange={handleRegionChange}
+          status={errors.region ? 'error' : ''}
+        />
+        {errors.region && <div style={{ color: 'red', fontSize: '12px', 
marginTop: '4px' }}>{errors.region}</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
new file mode 100644
index 000000000..4f0de4666
--- /dev/null
+++ 
b/config-ui/src/plugins/register/q-dev/connection-fields/identity-center-config.tsx
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { ChangeEvent } from 'react';
+import { Input } from 'antd';
+
+import { Block } from '@/components';
+
+interface Props {
+  identityStoreId: string;
+  identityStoreRegion: string;
+  errors: {
+    identityStoreId?: string;
+    identityStoreRegion?: string;
+  };
+  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是必填项';
+    }
+    if (!/^d-[a-z0-9]{10}$/.test(value)) {
+      return 'Identity Store ID格式不正确,应为:d-xxxxxxxxxx';
+    }
+    return '';
+  };
+
+  const validateIdentityStoreRegion = (value: string) => {
+    if (!value) {
+      return 'Identity Center区域是必填项';
+    }
+    if (!/^[a-z0-9-]+$/.test(value)) {
+      return 'Identity Center区域格式不正确';
+    }
+    return '';
+  };
+
+  const handleIdentityStoreIdChange = (e: ChangeEvent<HTMLInputElement>) => {
+    const value = e.target.value;
+    setValues({ identityStoreId: value });
+    setErrors({ identityStoreId: validateIdentityStoreId(value) });
+  };
+
+  const handleIdentityStoreRegionChange = (e: ChangeEvent<HTMLInputElement>) 
=> {
+    const value = e.target.value;
+    setValues({ identityStoreRegion: value });
+    setErrors({ identityStoreRegion: validateIdentityStoreRegion(value) });
+  };
+
+  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' : ''}
+        />
+        {errors.identityStoreId && <div style={{ color: 'red', fontSize: 
'12px', marginTop: '4px' }}>{errors.identityStoreId}</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' : ''}
+        />
+        {errors.identityStoreRegion && <div style={{ color: 'red', fontSize: 
'12px', marginTop: '4px' }}>{errors.identityStoreRegion}</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
new file mode 100644
index 000000000..c01b72f0b
--- /dev/null
+++ b/config-ui/src/plugins/register/q-dev/connection-fields/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './aws-credentials';
+export * from './s3-config';
+export * from './identity-center-config';
\ No newline at end of file
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
new file mode 100644
index 000000000..9cdfa8f3c
--- /dev/null
+++ b/config-ui/src/plugins/register/q-dev/connection-fields/s3-config.tsx
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { ChangeEvent } from 'react';
+import { Input } from 'antd';
+
+import { Block } from '@/components';
+
+interface Props {
+  bucket: string;
+  error?: string;
+  setValue: (value: string) => void;
+  setError: (error: string) => void;
+}
+
+export const S3Config = ({ bucket, error, setValue, setError }: Props) => {
+  
+  const validateBucket = (value: string) => {
+    if (!value) {
+      return 'S3存储桶名称是必填项';
+    }
+    if (!/^[a-z0-9.-]+$/.test(value)) {
+      return 'S3存储桶名称格式不正确,只能包含小写字母、数字、点和连字符';
+    }
+    if (value.length < 3 || value.length > 63) {
+      return 'S3存储桶名称长度必须在3-63个字符之间';
+    }
+    return '';
+  };
+
+  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
+    const value = e.target.value;
+    setValue(value);
+    setError(validateBucket(value));
+  };
+
+  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' : ''}
+      />
+      {error && <div style={{ color: 'red', fontSize: '12px', marginTop: '4px' 
}}>{error}</div>}
+    </Block>
+  );
+};
\ No newline at end of file
diff --git a/config-ui/src/plugins/register/q-dev/index.ts 
b/config-ui/src/plugins/register/q-dev/index.ts
new file mode 100644
index 000000000..257c4bc7d
--- /dev/null
+++ b/config-ui/src/plugins/register/q-dev/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './config';
\ No newline at end of file

Reply via email to