Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config-ui/src/plugins/register/q-dev/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const QDevConfig: IPluginConfig = {
docLink: 'https://devlake.apache.org/docs/UserManual/plugins/qdev',
initialValues: {
name: '',
authType: 'access_key',
accessKeyId: '',
secretAccessKey: '',
region: 'us-east-1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*/

import { ChangeEvent, useEffect, useMemo, useRef } from 'react';
import { Input } from 'antd';
import { Input, Radio } from 'antd';

import { Block } from '@/components';

Expand All @@ -32,7 +32,12 @@ interface Props {
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>) => {
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 });
Expand All @@ -42,9 +47,18 @@ const syncError = (key: string, error: string, setErrors: (errors: any) => void,
export const AwsCredentials = ({ type, initialValues, values, setValues, setErrors }: Props) => {
const isUpdate = type === 'update';

const authType = values.authType ?? 'access_key';
const accessKeyId = values.accessKeyId ?? '';
const secretAccessKey = values.secretAccessKey ?? '';
const region = values.region ?? '';

const isAccessKeyAuth = authType === 'access_key';

useEffect(() => {
if (values.authType === undefined) {
setValues({ authType: initialValues.authType ?? 'access_key' });
}
}, [initialValues.authType, values.authType, setValues]);

useEffect(() => {
if (values.accessKeyId === undefined) {
Expand All @@ -65,31 +79,33 @@ export const AwsCredentials = ({ type, initialValues, values, setValues, setErro
}, [initialValues.region, values.region, setValues]);

const accessKeyError = useMemo(() => {
if (!isAccessKeyAuth) return ''; // Not required for IAM role auth
if (!accessKeyId) {
return isUpdate ? '' : 'AWS Access Key ID is required.';
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 'AWS Access Key ID must contain 16-32 uppercase letters or digits';
}
return '';
}, [accessKeyId, isUpdate]);
}, [accessKeyId, isUpdate, isAccessKeyAuth]);

const secretKeyError = useMemo(() => {
if (!isAccessKeyAuth) return ''; // Not required for IAM role auth
if (!secretAccessKey) {
return isUpdate ? '' : 'AWS Secret Access Key is required.';
return isUpdate ? '' : 'AWS Secret Access Key is required';
}
if (secretAccessKey && secretAccessKey.length < 40) {
return 'AWS Secret Access Key looks too short.';
return 'AWS Secret Access Key looks too short';
}
return '';
}, [secretAccessKey, isUpdate]);
}, [secretAccessKey, isUpdate, isAccessKeyAuth]);

const regionError = useMemo(() => {
if (!region) {
return 'AWS Region is required.';
return 'AWS Region is required';
}
if (!REGION_PATTERN.test(region)) {
return 'AWS Region should look like us-east-1.';
return 'AWS Region should look like us-east-1';
}
return '';
}, [region]);
Expand Down Expand Up @@ -122,31 +138,67 @@ export const AwsCredentials = ({ type, initialValues, values, setValues, setErro
setValues({ region: e.target.value.trim() });
};

const handleAuthTypeChange = (e: any) => {
const newAuthType = e.target.value;
setValues({ authType: newAuthType });

// Clear access key fields when switching to IAM role
if (newAuthType === 'iam_role') {
setValues({
authType: newAuthType,
accessKeyId: '',
secretAccessKey: ''
});
}
};

return (
<>
<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' : ''}
/>
{accessKeyError && <div style={{ marginTop: 4, color: '#f5222d' }}>{accessKeyError}</div>}
</Block>

<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' : ''}
/>
{secretKeyError && <div style={{ marginTop: 4, color: '#f5222d' }}>{secretKeyError}</div>}
<Block title="Authentication Type" description="Choose how to authenticate with AWS" required>
<Radio.Group value={authType} onChange={handleAuthTypeChange}>
<Radio value="access_key">Access Key & Secret</Radio>
<Radio value="iam_role">IAM Role (for EC2/ECS/Lambda)</Radio>
</Radio.Group>
</Block>

<Block title="AWS Region" description="Region of the S3 bucket, e.g. us-east-1." required>
{isAccessKeyAuth && (
<>
<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' : ''}
/>
{accessKeyError && <div style={{ marginTop: 4, color: '#f5222d' }}>{accessKeyError}</div>}
</Block>

<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' : ''}
/>
{secretKeyError && <div style={{ marginTop: 4, color: '#f5222d' }}>{secretKeyError}</div>}
</Block>
</>
)}

{!isAccessKeyAuth && (
<Block title="IAM Role Authentication" description="DevLake will use the IAM role attached to the EC2 instance, ECS task, or Lambda function">
<div style={{ padding: '12px', backgroundColor: '#f6f8fa', borderRadius: '6px', color: '#586069' }}>
<p style={{ margin: 0 }}>
Make sure the IAM role has the necessary S3 permissions to access your bucket.
No additional credentials are required when using IAM role authentication.
</p>
</div>
</Block>
)}

<Block title="AWS Region" description="Region of the S3 bucket, e.g. us-east-1" required>
<Input
style={{ width: 386 }}
placeholder="us-east-1"
Expand Down
24 changes: 15 additions & 9 deletions config-ui/src/plugins/register/q-dev/data-scope.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,12 @@ type FormValues = {
months?: number[];
};

export const QDevDataScope = ({ connectionId: _connectionId, disabledItems, selectedItems, onChangeSelectedItems }: Props) => {
export const QDevDataScope = ({
connectionId: _connectionId,
disabledItems,
selectedItems,
onChangeSelectedItems,
}: Props) => {
const [form] = Form.useForm<FormValues>();

const disabledIds = useMemo(() => new Set(disabledItems?.map((it) => String(it.id)) ?? []), [disabledItems]);
Expand Down Expand Up @@ -234,7 +239,9 @@ export const QDevDataScope = ({ connectionId: _connectionId, disabledItems, sele
return;
}

const uniqueMonths = Array.from(new Set(months)).map((m) => Number(m)).filter((m) => !Number.isNaN(m));
const uniqueMonths = Array.from(new Set(months))
.map((m) => Number(m))
.filter((m) => !Number.isNaN(m));
uniqueMonths.sort((a, b) => a - b);

uniqueMonths.forEach((month) => {
Expand Down Expand Up @@ -289,7 +296,11 @@ export const QDevDataScope = ({ connectionId: _connectionId, disabledItems, sele
key: 'basePath',
render: (_: unknown, item) => {
const meta = extractScopeMeta(item);
return meta.basePath ? <Typography.Text>{meta.basePath}</Typography.Text> : <Typography.Text type="secondary">(bucket root)</Typography.Text>;
return meta.basePath ? (
<Typography.Text>{meta.basePath}</Typography.Text>
) : (
<Typography.Text type="secondary">(bucket root)</Typography.Text>
);
},
},
{
Expand Down Expand Up @@ -340,12 +351,7 @@ export const QDevDataScope = ({ connectionId: _connectionId, disabledItems, sele
<Input placeholder="user-report/AWSLogs/.../us-east-1" />
</Form.Item>

<Form.Item
label="Year"
name="year"
rules={[{ required: true, message: 'Enter year' }]}
style={{ width: 160 }}
>
<Form.Item label="Year" name="year" rules={[{ required: true, message: 'Enter year' }]} style={{ width: 160 }}>
<InputNumber min={2000} max={2100} style={{ width: '100%' }} />
</Form.Item>

Expand Down
2 changes: 1 addition & 1 deletion config-ui/src/plugins/register/q-dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@
*
*/

export * from './config';
export * from './config';
Loading