Skip to content

Commit 9b8053f

Browse files
committed
[DOP-29475] Implement Iceberg connection type
1 parent 1f88055 commit 9b8053f

File tree

22 files changed

+542
-26
lines changed

22 files changed

+542
-26
lines changed

src/entities/connection/api/types.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export type ConnectionData =
2020
| ConnectionPostgres
2121
| ConnectionClickhouse
2222
| ConnectionMySql
23-
| ConnectionMsSql;
23+
| ConnectionMsSql
24+
| ConnectionIceberg;
2425

2526
export type ConnectionBucketStyle = 'domain' | 'path';
2627

@@ -30,10 +31,21 @@ export type ConnectionSambaProtocol = 'SMB' | 'NetBIOS';
3031

3132
export type ConnectionSambaAuthType = 'NTLMv1' | 'NTLMv2';
3233

34+
export type ConnectionIcebergS3AccessDelegation = 'vended-credentials' | 'remote-signing';
35+
36+
export enum ConnectionIcebergConnectionType {
37+
ICEBERG_REST_S3_DIRECT = 'iceberg_rest_s3_direct',
38+
ICEBERG_REST_S3_DELEGATED = 'iceberg_rest_s3_delegated',
39+
}
40+
3341
export enum ConnectionAuthType {
3442
BASIC = 'basic',
3543
S3 = 's3',
3644
SAMBA = 'samba',
45+
ICEBERG_REST_BEARER = 'iceberg_rest_bearer',
46+
ICEBERG_REST_OAUTH2_CLIENT_CREDENTIALS = 'iceberg_rest_oauth2_client_credentials',
47+
ICEBERG_REST_BEARER_S3_BASIC = 'iceberg_rest_bearer_s3_basic',
48+
ICEBERG_REST_OAUTH2_CLIENT_CREDENTIALS_S3_BASIC = 'iceberg_rest_oauth2_client_credentials_s3_basic',
3749
}
3850

3951
interface ConnectionAuthBasic {
@@ -63,6 +75,68 @@ export interface ConnectionHive {
6375
};
6476
}
6577

78+
export interface ConnectionIcebergRestS3Direct {
79+
type: ConnectionIcebergConnectionType.ICEBERG_REST_S3_DIRECT;
80+
rest_catalog_url: string;
81+
s3_warehouse_path: string;
82+
s3_host: string;
83+
s3_bucket: string;
84+
s3_bucket_style: ConnectionBucketStyle;
85+
s3_port: number | null;
86+
s3_region: string;
87+
s3_protocol: ConnectionProtocol;
88+
}
89+
90+
export interface ConnectionIcebergRestS3Delegated {
91+
type: ConnectionIcebergConnectionType.ICEBERG_REST_S3_DELEGATED;
92+
rest_catalog_url: string;
93+
s3_warehouse_name: string | null;
94+
s3_access_delegation: ConnectionIcebergS3AccessDelegation;
95+
}
96+
97+
export interface ConnectionIcebergRestBearer {
98+
type: ConnectionAuthType.ICEBERG_REST_BEARER;
99+
rest_catalog_token?: string;
100+
}
101+
102+
export interface ConnectionIcebergRestBearerS3Basic {
103+
type: ConnectionAuthType.ICEBERG_REST_BEARER_S3_BASIC;
104+
rest_catalog_token?: string;
105+
s3_access_key: string;
106+
s3_secret_key?: string;
107+
}
108+
109+
export interface ConnectionIcebergRestClientCredentials {
110+
type: ConnectionAuthType.ICEBERG_REST_OAUTH2_CLIENT_CREDENTIALS;
111+
rest_catalog_oauth2_client_id: string;
112+
rest_catalog_oauth2_scopes: string[];
113+
rest_catalog_oauth2_resource: string | null;
114+
rest_catalog_oauth2_audience: string | null;
115+
rest_catalog_oauth2_token_endpoint: string | null;
116+
}
117+
118+
export interface ConnectionIcebergRestClientCredentialsS3Basic {
119+
type: ConnectionAuthType.ICEBERG_REST_OAUTH2_CLIENT_CREDENTIALS_S3_BASIC;
120+
rest_catalog_oauth2_client_id: string;
121+
rest_catalog_oauth2_client_secret?: string;
122+
rest_catalog_oauth2_scopes: string[];
123+
rest_catalog_oauth2_resource: string | null;
124+
rest_catalog_oauth2_audience: string | null;
125+
rest_catalog_oauth2_token_endpoint: string | null;
126+
s3_access_key: string;
127+
s3_secret_key?: string;
128+
}
129+
130+
export interface ConnectionIceberg {
131+
type: ConnectionType.ICEBERG;
132+
auth_data:
133+
| ConnectionIcebergRestBearer
134+
| ConnectionIcebergRestClientCredentials
135+
| ConnectionIcebergRestBearerS3Basic
136+
| ConnectionIcebergRestClientCredentialsS3Basic;
137+
connection_data: ConnectionIcebergRestS3Direct | ConnectionIcebergRestS3Delegated;
138+
}
139+
66140
export interface ConnectionHdfs {
67141
type: ConnectionType.HDFS;
68142
auth_data: ConnectionAuthBasic;
Lines changed: 24 additions & 0 deletions
Loading

src/entities/connection/assets/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import S3Icon from './s3.svg';
22
import ClickhouseIcon from './clickhouse.svg';
33
import HdfsIcon from './hdfs.svg';
44
import HiveIcon from './hive.svg';
5+
import IcebergIcon from './iceberg.svg';
56
import MssqlIcon from './mssql.svg';
67
import MysqlIcon from './mysql.svg';
78
import OracleIcon from './oracle.svg';
@@ -15,6 +16,7 @@ export {
1516
ClickhouseIcon,
1617
HdfsIcon,
1718
HiveIcon,
19+
IcebergIcon,
1820
MssqlIcon,
1921
MysqlIcon,
2022
OracleIcon,

src/entities/connection/constants.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
FtpIcon,
88
HdfsIcon,
99
HiveIcon,
10+
IcebergIcon,
1011
MssqlIcon,
1112
MysqlIcon,
1213
OracleIcon,
@@ -23,6 +24,7 @@ export const CONNECTION_TYPE_NAMES: Record<ConnectionType, string> = {
2324
[ConnectionType.FTPS]: 'FTPS',
2425
[ConnectionType.HDFS]: 'HDFS',
2526
[ConnectionType.HIVE]: 'Hive',
27+
[ConnectionType.ICEBERG]: 'Iceberg',
2628
[ConnectionType.MSSQL]: 'MSSQL',
2729
[ConnectionType.MYSQL]: 'MySQL',
2830
[ConnectionType.ORACLE]: 'Oracle',
@@ -39,6 +41,7 @@ export const CONNECTION_ICONS: Record<ConnectionType, ReactNode> = {
3941
[ConnectionType.FTPS]: <FtpIcon />,
4042
[ConnectionType.HDFS]: <HdfsIcon />,
4143
[ConnectionType.HIVE]: <HiveIcon />,
44+
[ConnectionType.ICEBERG]: <IcebergIcon />,
4245
[ConnectionType.MSSQL]: <MssqlIcon />,
4346
[ConnectionType.MYSQL]: <MysqlIcon />,
4447
[ConnectionType.ORACLE]: <OracleIcon />,
@@ -56,6 +59,7 @@ export const CONNECTION_TYPE_SELECT_OPTIONS = prepareOptionsForSelect({
5659
{ value: ConnectionType.FTPS, label: CONNECTION_TYPE_NAMES[ConnectionType.FTPS] },
5760
{ value: ConnectionType.HDFS, label: CONNECTION_TYPE_NAMES[ConnectionType.HDFS] },
5861
{ value: ConnectionType.HIVE, label: CONNECTION_TYPE_NAMES[ConnectionType.HIVE] },
62+
{ value: ConnectionType.ICEBERG, label: CONNECTION_TYPE_NAMES[ConnectionType.ICEBERG] },
5963
{ value: ConnectionType.MSSQL, label: CONNECTION_TYPE_NAMES[ConnectionType.MSSQL] },
6064
{ value: ConnectionType.MYSQL, label: CONNECTION_TYPE_NAMES[ConnectionType.MYSQL] },
6165
{ value: ConnectionType.ORACLE, label: CONNECTION_TYPE_NAMES[ConnectionType.ORACLE] },

src/entities/connection/ui/ConnectionTypeForm/components/ConnectionHttpProtocol/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
44

55
import { MAX_ALLOWED_PORT, MIN_ALLOWED_PORT } from '../../constants';
66

7-
export const ConnectionHttpProtocol = () => {
7+
export const ConnectionHttpProtocol = ({ fieldsPrefix = '' }: { fieldsPrefix?: string }) => {
88
const { t } = useTranslation('connection');
99

1010
const formInstance = Form.useFormInstance();
@@ -24,7 +24,7 @@ export const ConnectionHttpProtocol = () => {
2424
<>
2525
<Form.Item
2626
label={t('protocol')}
27-
name={['connection_data', 'protocol']}
27+
name={['connection_data', `${fieldsPrefix}protocol`]}
2828
rules={[{ required: true }]}
2929
initialValue="https"
3030
>
@@ -33,7 +33,7 @@ export const ConnectionHttpProtocol = () => {
3333
<Radio.Button value="https">HTTPS</Radio.Button>
3434
</Radio.Group>
3535
</Form.Item>
36-
<Form.Item label={t('port')} name={['connection_data', 'port']}>
36+
<Form.Item label={t('port')} name={['connection_data', `${fieldsPrefix}port`]}>
3737
<InputNumber size="large" min={MIN_ALLOWED_PORT} max={MAX_ALLOWED_PORT} placeholder={defaultPort} />
3838
</Form.Item>
3939
</>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from 'react';
2+
import { Form, Input } from 'antd';
3+
import { useTranslation } from 'react-i18next';
4+
5+
import { useSensitiveFields } from '../../hooks';
6+
7+
export const ConnectionAuthIcebergBearer = () => {
8+
const { t } = useTranslation('connection');
9+
const { isRequired } = useSensitiveFields();
10+
11+
return (
12+
<>
13+
<Form.Item
14+
label={t('iceberg.token')}
15+
name={['auth_data', 'rest_catalog_token']}
16+
rules={[{ required: isRequired }]}
17+
>
18+
<Input.Password size="large" />
19+
</Form.Item>
20+
</>
21+
);
22+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React from 'react';
2+
import { Button, Form, Input, Space } from 'antd';
3+
import { useTranslation } from 'react-i18next';
4+
5+
import { useSensitiveFields } from '../../hooks';
6+
7+
import { ConnectionAuthIcebergOAuth2Scope } from './ConnectionAuthIcebergOAuth2Scope';
8+
9+
export const ConnectionAuthIcebergOAuth2ClientCredentials = () => {
10+
const { t } = useTranslation('connection');
11+
const { isRequired } = useSensitiveFields();
12+
13+
return (
14+
<>
15+
<Form.Item
16+
label={t('iceberg.oauth2ClientId')}
17+
name={['auth_data', 'rest_catalog_oauth2_client_id']}
18+
rules={[{ required: true }]}
19+
>
20+
<Input size="large" />
21+
</Form.Item>
22+
<Form.Item
23+
label={t('iceberg.oauth2ClientSecret')}
24+
name={['auth_data', 'rest_catalog_oauth2_client_secret']}
25+
rules={[{ required: isRequired }]}
26+
>
27+
<Input.Password size="large" />
28+
</Form.Item>
29+
<Form.Item
30+
label={t('iceberg.oauth2TokenEndpoint')}
31+
name={['auth_data', 'rest_catalog_oauth2_token_endpoint']}
32+
rules={[{ type: 'url' }]}
33+
>
34+
<Input
35+
size="large"
36+
placeholder="http://keycloak.mycompany.com/auth/realms/myrealm/protocol/openid-connect/token"
37+
/>
38+
</Form.Item>
39+
40+
<Form.List name={['auth_data', 'rest_catalog_oauth2_scopes']}>
41+
{(fields, { add, remove }) => (
42+
<Form.Item label={t('iceberg.oauth2Scopes')}>
43+
<Button size="large" onClick={() => add()}>
44+
{t('add', { ns: 'shared' })}
45+
</Button>
46+
<Space direction="vertical">
47+
{fields.map((field) => (
48+
<ConnectionAuthIcebergOAuth2Scope key={field.key} field={field} remove={remove} />
49+
))}
50+
</Space>
51+
</Form.Item>
52+
)}
53+
</Form.List>
54+
55+
<Form.Item
56+
label={t('iceberg.oauth2Resource')}
57+
name={['auth_data', 'rest_catalog_oauth2_resource']}
58+
rules={[{ type: 'string', whitespace: true }]}
59+
>
60+
<Input size="large" />
61+
</Form.Item>
62+
<Form.Item label={t('iceberg.oauth2Audience')} name={['auth_data', 'rest_catalog_oauth2_audience']}>
63+
<Input size="large" />
64+
</Form.Item>
65+
</>
66+
);
67+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React, { ChangeEvent, useMemo, useState } from 'react';
2+
import { Form, FormListFieldData, FormListOperation, Input, Space } from 'antd';
3+
import { CloseOutlined } from '@ant-design/icons';
4+
5+
export const ConnectionAuthIcebergOAuth2Scope = ({
6+
field,
7+
remove,
8+
}: {
9+
field: FormListFieldData;
10+
remove: FormListOperation['remove'];
11+
}) => {
12+
const formInstance = Form.useFormInstance();
13+
14+
/** Use custom type state, because Form.useWatch doesn't support dynamic fieldname like in Form.List */
15+
const initialValue = useMemo(() => {
16+
return formInstance.getFieldValue(['auth_data', 'rest_catalog_oauth2_scopes', field.name]);
17+
}, [formInstance, field]);
18+
19+
const [value, setValue] = useState(() => initialValue);
20+
21+
const handleValueChange = (e: ChangeEvent<HTMLInputElement>) => {
22+
setValue(e.target.value);
23+
};
24+
25+
return (
26+
<Form.Item {...field} noStyle>
27+
<Space>
28+
<Input size="large" value={value} onChange={handleValueChange} />
29+
<CloseOutlined onClick={() => remove(field.name)} />
30+
</Space>
31+
</Form.Item>
32+
);
33+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react';
2+
import { Form, Input, Radio } from 'antd';
3+
import { useTranslation } from 'react-i18next';
4+
5+
export const ConnectionIcebergS3Delegated = () => {
6+
const { t } = useTranslation('connection');
7+
8+
return (
9+
<>
10+
<Form.Item label={t('iceberg.warehouseName')} name={['connection_data', 's3_warehouse_name']}>
11+
<Input size="large" placeholder="my-warehouse" />
12+
</Form.Item>
13+
<Form.Item
14+
label={t('s3.accessDelegation')}
15+
name={['connection_data', 's3_access_delegation']}
16+
rules={[{ required: true }]}
17+
initialValue="vended-credentials"
18+
>
19+
<Radio.Group>
20+
<Radio.Button value="vended-credentials">{t('s3.vendedCredentials')}</Radio.Button>
21+
<Radio.Button value="remote-signing">{t('s3.remoteSigning')}</Radio.Button>
22+
</Radio.Group>
23+
</Form.Item>
24+
</>
25+
);
26+
};

0 commit comments

Comments
 (0)