From c8bce782bb9eba2d8d7223166beb61b618e27c6e Mon Sep 17 00:00:00 2001 From: pankaj-peerdb <149565017+pankaj-peerdb@users.noreply.github.com> Date: Fri, 19 Jan 2024 19:15:47 +0530 Subject: [PATCH] Clickhouse UI (#1022) --- ui/app/api/peers/route.ts | 7 + ui/app/dto/PeersDTO.ts | 2 + ui/app/peers/create/[peerType]/handlers.ts | 4 + ui/app/peers/create/[peerType]/helpers/ch.ts | 62 +++++++ .../peers/create/[peerType]/helpers/common.ts | 3 + ui/app/peers/create/[peerType]/page.tsx | 5 + ui/app/peers/create/[peerType]/schema.ts | 40 +++++ ui/components/PeerComponent.tsx | 2 + ui/components/PeerForms/ClickhouseConfig.tsx | 156 ++++++++++++++++++ ui/components/SelectSource.tsx | 4 +- ui/public/svgs/ch.svg | 1 + 11 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 ui/app/peers/create/[peerType]/helpers/ch.ts create mode 100644 ui/components/PeerForms/ClickhouseConfig.tsx create mode 100644 ui/public/svgs/ch.svg diff --git a/ui/app/api/peers/route.ts b/ui/app/api/peers/route.ts index 03aa98ae4a..964157c71b 100644 --- a/ui/app/api/peers/route.ts +++ b/ui/app/api/peers/route.ts @@ -8,6 +8,7 @@ import { import prisma from '@/app/utils/prisma'; import { BigqueryConfig, + ClickhouseConfig, DBType, Peer, PostgresConfig, @@ -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, diff --git a/ui/app/dto/PeersDTO.ts b/ui/app/dto/PeersDTO.ts index 80de38124b..339ba7a9b1 100644 --- a/ui/app/dto/PeersDTO.ts +++ b/ui/app/dto/PeersDTO.ts @@ -1,5 +1,6 @@ import { BigqueryConfig, + ClickhouseConfig, PostgresConfig, S3Config, SnowflakeConfig, @@ -41,6 +42,7 @@ export type PeerConfig = | PostgresConfig | SnowflakeConfig | BigqueryConfig + | ClickhouseConfig | S3Config; export type CatalogPeer = { id: number; diff --git a/ui/app/peers/create/[peerType]/handlers.ts b/ui/app/peers/create/[peerType]/handlers.ts index 2eeb657bbf..ab8718dc14 100644 --- a/ui/app/peers/create/[peerType]/handlers.ts +++ b/ui/app/peers/create/[peerType]/handlers.ts @@ -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; diff --git a/ui/app/peers/create/[peerType]/helpers/ch.ts b/ui/app/peers/create/[peerType]/helpers/ch.ts new file mode 100644 index 0000000000..a85552c2b8 --- /dev/null +++ b/ui/app/peers/create/[peerType]/helpers/ch.ts @@ -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:'' +}; \ No newline at end of file diff --git a/ui/app/peers/create/[peerType]/helpers/common.ts b/ui/app/peers/create/[peerType]/helpers/common.ts index b1c27e0edc..3b7a017283 100644 --- a/ui/app/peers/create/[peerType]/helpers/common.ts +++ b/ui/app/peers/create/[peerType]/helpers/common.ts @@ -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; @@ -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: diff --git a/ui/app/peers/create/[peerType]/page.tsx b/ui/app/peers/create/[peerType]/page.tsx index a611df87ab..4ea5f30262 100644 --- a/ui/app/peers/create/[peerType]/page.tsx +++ b/ui/app/peers/create/[peerType]/page.tsx @@ -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'; @@ -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 }; @@ -44,6 +47,8 @@ export default function CreateConfig({ return ; case 'BIGQUERY': return ; + case 'CLICKHOUSE': + return ; case 'S3': return ; default: diff --git a/ui/app/peers/create/[peerType]/schema.ts b/ui/app/peers/create/[peerType]/schema.ts index 5bedeaa26f..5b4e7f81af 100644 --- a/ui/app/peers/create/[peerType]/schema.ts +++ b/ui/app/peers/create/[peerType]/schema.ts @@ -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({ diff --git a/ui/components/PeerComponent.tsx b/ui/components/PeerComponent.tsx index 7378e91635..6cf36bde11 100644 --- a/ui/components/PeerComponent.tsx +++ b/ui/components/PeerComponent.tsx @@ -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'; diff --git a/ui/components/PeerForms/ClickhouseConfig.tsx b/ui/components/PeerForms/ClickhouseConfig.tsx new file mode 100644 index 0000000000..51ab891cd8 --- /dev/null +++ b/ui/components/PeerForms/ClickhouseConfig.tsx @@ -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(false); + const [sshConfig, setSSHConfig] = useState(blankSSHConfig); + + const handleChange = ( + e: React.ChangeEvent, + 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 ( + + {setting.label}{' '} + {!setting.optional && ( + + + + )} + + } + action={ +
+ ) => + handleChange(e, setting) + } + /> + {setting.tips && ( + + )} +
+ } + /> + ); + })} + + + +
+ + setShowSSH(state)} /> +
+ {showSSH && + sshSetting.map((sshParam, index) => ( + + {sshParam.label}{' '} + {!sshParam.optional && ( + + + + )} + + } + action={ +
+ ) => + 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 && } +
+ } + /> + ))} + + ); +} \ No newline at end of file diff --git a/ui/components/SelectSource.tsx b/ui/components/SelectSource.tsx index a83b7d64ab..a3f848bb3a 100644 --- a/ui/components/SelectSource.tsx +++ b/ui/components/SelectSource.tsx @@ -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 })); diff --git a/ui/public/svgs/ch.svg b/ui/public/svgs/ch.svg new file mode 100644 index 0000000000..f2144b5d7e --- /dev/null +++ b/ui/public/svgs/ch.svg @@ -0,0 +1 @@ + \ No newline at end of file