Skip to content

Commit

Permalink
Clickhouse UI (#1022)
Browse files Browse the repository at this point in the history
  • Loading branch information
pankaj-peerdb authored Jan 19, 2024
1 parent 390b98f commit c8bce78
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 1 deletion.
7 changes: 7 additions & 0 deletions ui/app/api/peers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import prisma from '@/app/utils/prisma';
import {
BigqueryConfig,
ClickhouseConfig,
DBType,
Peer,
PostgresConfig,
Expand Down Expand Up @@ -50,6 +51,12 @@ const constructPeer = (
type: DBType.BIGQUERY,
bigqueryConfig: config as BigqueryConfig,
};
case 'CLICKHOUSE':
return {
name,
type: DBType.CLICKHOUSE,
clickhouseConfig: config as ClickhouseConfig,
};
case 'S3':
return {
name,
Expand Down
2 changes: 2 additions & 0 deletions ui/app/dto/PeersDTO.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
BigqueryConfig,
ClickhouseConfig,
PostgresConfig,
S3Config,
SnowflakeConfig,
Expand Down Expand Up @@ -41,6 +42,7 @@ export type PeerConfig =
| PostgresConfig
| SnowflakeConfig
| BigqueryConfig
| ClickhouseConfig
| S3Config;
export type CatalogPeer = {
id: number;
Expand Down
4 changes: 4 additions & 0 deletions ui/app/peers/create/[peerType]/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ const validateFields = (
const bqConfig = bqSchema.safeParse(config);
if (!bqConfig.success) validationErr = bqConfig.error.issues[0].message;
break;
case 'CLICKHOUSE':
const chConfig = chSchema.safeParse(config);
if (!chConfig.success) validationErr = chConfig.error.issues[0].message;
break;
case 'S3':
const s3Config = s3Schema.safeParse(config);
if (!s3Config.success) validationErr = s3Config.error.issues[0].message;
Expand Down
62 changes: 62 additions & 0 deletions ui/app/peers/create/[peerType]/helpers/ch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ClickhouseConfig, SSHConfig } from '@/grpc_generated/peers';
import { Dispatch, SetStateAction } from 'react';
import { PeerSetting } from './common';

export const clickhouseSetting: PeerSetting[] = [
{
label: 'Host',
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, host: value })),
tips: 'Specifies the IP host name or address on which postgres is to listen for TCP/IP connections from client applications. Ensure that this host has us whitelisted so we can connect to it.',
},
{
label: 'Port',
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, port: parseInt(value, 10) })),
type: 'number', // type for textfield
default: 5432,
tips: 'Specifies the TCP/IP port or local Unix domain socket file extension on which postgres is listening for connections from client applications.',
},
{
label: 'User',
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, user: value })),
tips: 'Specify the user that we should use to connect to this host.',
helpfulLink: 'https://www.postgresql.org/docs/8.0/user-manag.html',
},
{
label: 'Password',
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, password: value })),
type: 'password',
tips: 'Password associated with the user you provided.',
helpfulLink: 'https://www.postgresql.org/docs/current/auth-password.html',
},
{
label: 'Database',
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, database: value })),
tips: 'Specify which database to associate with this peer.',
helpfulLink:
'https://www.postgresql.org/docs/current/sql-createdatabase.html',
},
{
label: 'S3 Integration',
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, s3Integration: value })),
optional: true,
tips: `This is needed only if you plan to run a mirror and you wish to stage AVRO files on S3.`,
helpfulLink:
'https://docs.snowflake.com/en/user-guide/data-load-s3-config-storage-integration',
},

];

export const blankClickhouseSetting: ClickhouseConfig = {
host: '',
port: 5432,
user: '',
password: '',
database: '',
s3Integration:''
};
3 changes: 3 additions & 0 deletions ui/app/peers/create/[peerType]/helpers/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { blankBigquerySetting } from './bq';
import { blankPostgresSetting } from './pg';
import { blankS3Setting } from './s3';
import { blankSnowflakeSetting } from './sf';
import {blankClickhouseSetting} from './ch';

export interface PeerSetting {
label: string;
Expand All @@ -22,6 +23,8 @@ export const getBlankSetting = (dbType: string): PeerConfig => {
return blankSnowflakeSetting;
case 'BIGQUERY':
return blankBigquerySetting;
case 'CLICKHOUSE':
return blankClickhouseSetting;
case 'S3':
return blankS3Setting;
default:
Expand Down
5 changes: 5 additions & 0 deletions ui/app/peers/create/[peerType]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import BigqueryForm from '@/components/PeerForms/BigqueryConfig';
import PostgresForm from '@/components/PeerForms/PostgresForm';
import S3Form from '@/components/PeerForms/S3Form';
import SnowflakeForm from '@/components/PeerForms/SnowflakeForm';
import ClickhouseForm from '@/components/PeerForms/ClickhouseConfig';

import { Button } from '@/lib/Button';
import { ButtonGroup } from '@/lib/ButtonGroup';
import { Label } from '@/lib/Label';
Expand All @@ -18,6 +20,7 @@ import { handleCreate, handleValidate } from './handlers';
import { getBlankSetting } from './helpers/common';
import { postgresSetting } from './helpers/pg';
import { snowflakeSetting } from './helpers/sf';
import {clickhouseSetting} from './helpers/ch';

type CreateConfigProps = {
params: { peerType: string };
Expand All @@ -44,6 +47,8 @@ export default function CreateConfig({
return <SnowflakeForm settings={snowflakeSetting} setter={setConfig} />;
case 'BIGQUERY':
return <BigqueryForm setter={setConfig} />;
case 'CLICKHOUSE':
return <ClickhouseForm settings={clickhouseSetting} setter={setConfig} />;
case 'S3':
return <S3Form setter={setConfig} />;
default:
Expand Down
40 changes: 40 additions & 0 deletions ui/app/peers/create/[peerType]/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,46 @@ export const bqSchema = z.object({
),
});

export const chSchema = z.object({
host: z
.string({
required_error: 'Host is required',
invalid_type_error: 'Host must be a string',
})
.min(1, { message: 'Host cannot be empty' })
.max(255, 'Host must be less than 255 characters'),
port: z
.number({
required_error: 'Port is required',
invalid_type_error: 'Port must be a number',
})
.int()
.min(1, 'Port must be a positive integer')
.max(65535, 'Port must be below 65535'),
database: z
.string({
required_error: 'Database is required',
invalid_type_error: 'Database must be a string',
})
.min(1, { message: 'Database name should be non-empty' })
.max(100, 'Database must be less than 100 characters'),
user: z
.string({
required_error: 'User is required',
invalid_type_error: 'User must be a string',
})
.min(1, 'User must be non-empty')
.max(64, 'User must be less than 64 characters'),
password: z
.string({
required_error: 'Password is required',
invalid_type_error: 'Password must be a string',
})
.min(1, 'Password must be non-empty')
.max(100, 'Password must be less than 100 characters'),
});


export const s3Schema = z.object({
url: z
.string({
Expand Down
2 changes: 2 additions & 0 deletions ui/components/PeerComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const DBTypeToImageMapping = (peerType: DBType | string) => {
case DBType.S3:
case 'S3':
return '/svgs/aws.svg';
case 'CLICKHOUSE':
return '/svgs/ch.svg';
case DBType.EVENTHUB_GROUP:
case DBType.EVENTHUB:
return '/svgs/ms.svg';
Expand Down
156 changes: 156 additions & 0 deletions ui/components/PeerForms/ClickhouseConfig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
'use client';
import { PeerSetter } from '@/app/dto/PeersDTO';
import { PeerSetting } from '@/app/peers/create/[peerType]/helpers/common';
import {
blankSSHConfig,
sshSetting,
} from '@/app/peers/create/[peerType]/helpers/pg';
import { SSHConfig } from '@/grpc_generated/peers';
import { Label } from '@/lib/Label';
import { RowWithTextField } from '@/lib/Layout';
import { Switch } from '@/lib/Switch';
import { TextField } from '@/lib/TextField';
import { Tooltip } from '@/lib/Tooltip';
import { useEffect, useState } from 'react';
import { InfoPopover } from '../InfoPopover';
interface ConfigProps {
settings: PeerSetting[];
setter: PeerSetter;
}

export default function PostgresForm({ settings, setter }: ConfigProps) {
const [showSSH, setShowSSH] = useState<boolean>(false);
const [sshConfig, setSSHConfig] = useState<SSHConfig>(blankSSHConfig);

const handleChange = (
e: React.ChangeEvent<HTMLInputElement>,
setting: PeerSetting
) => {
setting.stateHandler(e.target.value, setter);
};

useEffect(() => {
setter((prev) => {
return {
...prev,
sshConfig: showSSH ? sshConfig : undefined,
};
});
}, [sshConfig, setter, showSSH]);

return (
<>
{settings.map((setting, id) => {
return (
<RowWithTextField
key={id}
label={
<Label>
{setting.label}{' '}
{!setting.optional && (
<Tooltip
style={{ width: '100%' }}
content={'This is a required field.'}
>
<Label colorName='lowContrast' colorSet='destructive'>
*
</Label>
</Tooltip>
)}
</Label>
}
action={
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
}}
>
<TextField
variant='simple'
type={setting.type}
defaultValue={setting.default}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
handleChange(e, setting)
}
/>
{setting.tips && (
<InfoPopover tips={setting.tips} link={setting.helpfulLink} />
)}
</div>
}
/>
);
})}

<Label
as='label'
style={{ marginTop: '1rem' }}
variant='subheadline'
colorName='lowContrast'
>
SSH Configuration
</Label>
<Label>
You may provide SSH configuration to connect to your PostgreSQL database
through SSH tunnel.
</Label>
<div style={{ width: '50%', display: 'flex', alignItems: 'center' }}>
<Label variant='subheadline'>Configure SSH Tunnel</Label>
<Switch onCheckedChange={(state) => setShowSSH(state)} />
</div>
{showSSH &&
sshSetting.map((sshParam, index) => (
<RowWithTextField
key={index}
label={
<Label>
{sshParam.label}{' '}
{!sshParam.optional && (
<Tooltip
style={{ width: '100%' }}
content={'This is a required field.'}
>
<Label colorName='lowContrast' colorSet='destructive'>
*
</Label>
</Tooltip>
)}
</Label>
}
action={
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
}}
>
<TextField
variant='simple'
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
sshParam.stateHandler(e.target.value, setSSHConfig)
}
type={sshParam.type}
defaultValue={
(sshConfig as SSHConfig)[
sshParam.label === 'BASE64 Private Key'
? 'privateKey'
: (sshParam.label.toLowerCase() as
| 'host'
| 'port'
| 'user'
| 'password'
| 'privateKey')
] || ''
}
/>
{sshParam.tips && <InfoPopover tips={sshParam.tips} />}
</div>
}
/>
))}
</>
);
}
4 changes: 3 additions & 1 deletion ui/components/SelectSource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ export default function SelectSource({
(value === 'POSTGRES' ||
value === 'SNOWFLAKE' ||
value === 'BIGQUERY' ||
value === 'S3')
value === 'S3' ||
value === 'CLICKHOUSE'
)
)
.map((value) => ({ label: value, value }));

Expand Down
1 change: 1 addition & 0 deletions ui/public/svgs/ch.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit c8bce78

Please sign in to comment.