This is an automated email from the ASF dual-hosted git repository.

yuqi4733 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new d5d7f1904a [#9865] web-v2(UI): support clickhouse catalog UI (#9959)
d5d7f1904a is described below

commit d5d7f1904a3715466ae9e4e414c37da51e65c640
Author: Qian Xia <[email protected]>
AuthorDate: Wed Feb 25 14:00:24 2026 +0800

    [#9865] web-v2(UI): support clickhouse catalog UI (#9959)
    
    ### What changes were proposed in this pull request?
    <img width="2952" height="1548" alt="image"
    
src="https://github.com/user-attachments/assets/d1a5dae4-1bcc-4877-82ca-4b80f83128be";
    />
    <img width="2952" height="1542" alt="image"
    
src="https://github.com/user-attachments/assets/d66b2528-3900-4bdd-8041-c647f280489b";
    />
    
    
    ### Why are the changes needed?
    N/A
    
    Fix: #9865
    
    ### Does this PR introduce _any_ user-facing change?
    N/A
    
    ### How was this patch tested?
    manually
    
    ---------
    
    Co-authored-by: Copilot <[email protected]>
---
 web-v2/web/src/app/catalogs/TreeComponent.js       |  16 +
 .../catalogs/rightContent/CreateCatalogDialog.js   |   4 +-
 .../app/catalogs/rightContent/CreateTableDialog.js | 340 +++++++++++----------
 .../entitiesContent/CatalogDetailsPage.js          |   2 +
 web-v2/web/src/components/Icons.js                 |  28 ++
 web-v2/web/src/config/catalog.js                   |  47 +++
 web-v2/web/src/config/index.js                     |  19 +-
 7 files changed, 291 insertions(+), 165 deletions(-)

diff --git a/web-v2/web/src/app/catalogs/TreeComponent.js 
b/web-v2/web/src/app/catalogs/TreeComponent.js
index f89a3e8167..4d1b80cdf4 100644
--- a/web-v2/web/src/app/catalogs/TreeComponent.js
+++ b/web-v2/web/src/app/catalogs/TreeComponent.js
@@ -227,6 +227,22 @@ export const TreeComponent = forwardRef(function 
TreeComponent(props, ref) {
               )}
             </span>
           )
+        case 'custom-icons-clickhouse':
+          return (
+            <span
+              role='img'
+              className='anticon'
+              onMouseEnter={e => onMouseEnter(e, catalog)}
+              onMouseLeave={e => onMouseLeave(e, catalog)}
+              onClick={e => handleClickIcon(e, catalog)}
+            >
+              {isHover !== key ? (
+                <Icons.clickhouse className='size-4'></Icons.clickhouse>
+              ) : (
+                <Icons.RotateCw className='h-4 w-3'></Icons.RotateCw>
+              )}
+            </span>
+          )
       }
     } else {
       return (
diff --git a/web-v2/web/src/app/catalogs/rightContent/CreateCatalogDialog.js 
b/web-v2/web/src/app/catalogs/rightContent/CreateCatalogDialog.js
index 9ec3715051..d0cff6543e 100644
--- a/web-v2/web/src/app/catalogs/rightContent/CreateCatalogDialog.js
+++ b/web-v2/web/src/app/catalogs/rightContent/CreateCatalogDialog.js
@@ -349,6 +349,8 @@ export default function CreateCatalogDialog({ ...props }) {
           return <Icons.starrocks className={small ? 'size-6' : 
'size-12'}></Icons.starrocks>
         case 'custom-icons-lakehouse':
           return <Icons.lakehouse className={small ? 'size-6' : 
'size-12'}></Icons.lakehouse>
+        case 'custom-icons-clickhouse':
+          return <Icons.clickhouse className={small ? 'size-6' : 
'size-12'}></Icons.clickhouse>
       }
     } else {
       return <Icons.iconify icon={calalogIcon} className={small ? 'size-6' : 
'size-12'} />
@@ -361,7 +363,7 @@ export default function CreateCatalogDialog({ ...props }) {
         key={idx}
         data-refer={`catalog-provider-${provider.value}`}
         className={cn('provider-card flex items-center justify-between', {
-          'actived-default': provider.value === currentProvider,
+          'actived-default bg-defaultPrimary': provider.value === 
currentProvider,
           disabled: editCatalog
         })}
         onClick={() => handleSelectProvider(provider.value)}
diff --git a/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js 
b/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js
index e29ff98af6..4a68258b77 100644
--- a/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js
+++ b/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js
@@ -52,7 +52,7 @@ import { validateMessages, mismatchName } from '@/config'
 import {
   ColumnSpesicalType,
   ColumnType,
-  ColumnTypeForMysql,
+  ColumnTypeForUnsigned,
   ColumnTypeSupportAutoIncrement,
   ColumnWithParamType,
   UnsupportColumnType,
@@ -462,8 +462,8 @@ export default function CreateTableDialog({ ...props }) {
           item => !UnsupportColumnType[provider]?.includes(item)
         )
 
-        if (provider === 'jdbc-mysql') {
-          columnTypes = [...columnTypes, ...ColumnTypeForMysql]
+        if (['jdbc-mysql', 'jdbc-clickhouse'].includes(provider)) {
+          columnTypes = [...columnTypes, ...ColumnTypeForUnsigned]
         }
         setColumnTypes(columnTypes.sort((a, b) => a.localeCompare(b)))
       }
@@ -539,180 +539,198 @@ export default function CreateTableDialog({ ...props }) 
{
       .then(async () => {
         setConfirmLoading(true)
 
-        const submitData = {
-          name: values.name.trim(),
-          comment: values.comment,
-          tagsToAdd: values.tags,
-          columns: values.columns.map(col => {
-            const column = {
-              uniqueId: col.uniqueId || col.name,
-              name: col.name,
-              type: getColumnType(col.typeObj),
-              nullable: !col.required,
-              comment: col.comment || ''
-            }
-            if (autoIncrementInfo) {
-              column['autoIncrement'] = col.autoIncrement
-            }
-            if (col.defaultValue) {
-              switch (col.defaultValue.type) {
-                case 'field':
-                  column['defaultValue'] = {
-                    type: 'field',
-                    fieldName: [col.defaultValue?.fieldName]
-                  }
-                  break
-                case 'function':
-                  column['defaultValue'] = {
-                    type: 'function',
-                    funcName: col.defaultValue?.funcName,
-                    funcArgs: col.defaultValue?.funcArgs.map(f => {
-                      const func = {}
-                      if (f.type === 'literal') {
-                        func['type'] = 'literal'
-                        func['dataType'] = 'string'
-                        func['value'] = f.value
-                      } else {
-                        func['type'] = 'field'
-                        func['fieldName'] = [f.fieldName]
-                      }
+        let submitted = false
 
-                      return func
-                    })
-                  }
-                  break
-                default:
-                  column['defaultValue'] = {
-                    type: 'literal',
-                    dataType: col.defaultValue?.dataType || 'string',
-                    value: col.defaultValue?.value
-                  }
+        try {
+          const submitData = {
+            name: values.name.trim(),
+            comment: values.comment,
+            tagsToAdd: values.tags,
+            columns: values.columns.map(col => {
+              const column = {
+                uniqueId: col.uniqueId || col.name,
+                name: col.name,
+                type: getColumnType(col.typeObj),
+                nullable: !col.required,
+                comment: col.comment || ''
               }
-            }
+              if (autoIncrementInfo) {
+                column['autoIncrement'] = col.autoIncrement
+              }
+              if (col.defaultValue) {
+                switch (col.defaultValue.type) {
+                  case 'field':
+                    column['defaultValue'] = {
+                      type: 'field',
+                      fieldName: [col.defaultValue?.fieldName]
+                    }
+                    break
+                  case 'function':
+                    column['defaultValue'] = {
+                      type: 'function',
+                      funcName: col.defaultValue?.funcName,
+                      funcArgs: col.defaultValue?.funcArgs.map(f => {
+                        const func = {}
+                        if (f.type === 'literal') {
+                          func['type'] = 'literal'
+                          func['dataType'] = 'string'
+                          func['value'] = f.value
+                        } else {
+                          func['type'] = 'field'
+                          func['fieldName'] = [f.fieldName]
+                        }
 
-            return column
-          }),
-          properties:
-            values.properties &&
-            values.properties.reduce((acc, item) => {
-              acc[item.key] = values[item.key] || item.value
+                        return func
+                      })
+                    }
+                    break
+                  default:
+                    column['defaultValue'] = {
+                      type: 'literal',
+                      dataType: col.defaultValue?.dataType || 'string',
+                      value: col.defaultValue?.value
+                    }
+                }
+              }
 
-              return acc
-            }, {})
-        }
-        if (partitioningInfo) {
-          submitData['partitioning'] = values.partitions?.map(p => {
-            const field = {}
-            if (p.strategy === 'list') {
-              field['fieldNames'] = [[p.fieldName]]
-            } else if (p.strategy === 'bucket') {
-              field['numBuckets'] = p.number
-              field['fieldNames'] = [[p.fieldName]]
-            } else if (p.strategy === 'truncate') {
-              field['width'] = p.number
-              field['fieldName'] = [p.fieldName]
-            } else {
-              field['fieldName'] = [p.fieldName]
-            }
+              return column
+            }),
+            properties:
+              values.properties &&
+              values.properties.reduce((acc, item) => {
+                acc[item.key] = values[item.key] || item.value
 
-            return {
-              strategy: p.strategy,
-              ...field
-            }
-          })
-        }
-        if (sortOredsInfo) {
-          submitData['sortOrders'] = values.sortOrders?.map(s => {
-            const field = {
-              sortTerm: {}
-            }
-            if (s.strategy !== 'field') {
-              field.sortTerm['type'] = 'function'
-              field.sortTerm['funcName'] = s.strategy
-              field.sortTerm['funcArgs'] = []
-              if (['truncate', 'bucket'].includes(s.strategy)) {
-                field.sortTerm['funcArgs'] = [
-                  {
-                    type: 'literal',
-                    dataType: 'integer',
-                    value: s.number + ''
-                  },
-                  {
-                    type: 'field',
-                    fieldName: [s.fieldName]
-                  }
-                ]
+                return acc
+              }, {})
+          }
+          if (partitioningInfo) {
+            submitData['partitioning'] = values.partitions?.map(p => {
+              const field = {}
+              if (p.strategy === 'list') {
+                field['fieldNames'] = [[p.fieldName]]
+              } else if (p.strategy === 'bucket') {
+                field['numBuckets'] = p.number
+                field['fieldNames'] = [[p.fieldName]]
+              } else if (p.strategy === 'truncate') {
+                field['width'] = p.number
+                field['fieldName'] = [p.fieldName]
               } else {
-                field.sortTerm['funcArgs'] = [
-                  {
-                    type: 'field',
-                    fieldName: [s.fieldName]
-                  }
-                ]
-              }
-            } else {
-              field.sortTerm = {
-                type: s.strategy,
-                fieldName: [s.fieldName]
+                field['fieldName'] = [p.fieldName]
               }
-            }
-            field['direction'] = s.direction
-            field['nullOrdering'] = s.nullOrdering
 
-            return field
-          })
-        }
-        if (indexesInfo) {
-          submitData['indexes'] = values.indexes?.map(i => {
-            return {
-              indexType: i.indexType,
-              name: i.name,
-              fieldNames: i.fieldName.map(f => [f])
-            }
-          })
-        }
-        if (
-          distributionInfo &&
-          (values?.distribution?.strategy || values?.distribution?.number || 
values?.distribution?.field)
-        ) {
-          submitData['distribution'] = {
-            strategy: values.distribution?.strategy,
-            number: values.distribution?.number || 0,
-            funcArgs:
-              values.distribution?.field?.map(f => {
-                return {
-                  type: 'field',
-                  fieldName: [f]
+              return {
+                strategy: p.strategy,
+                ...field
+              }
+            })
+          }
+          if (sortOredsInfo) {
+            submitData['sortOrders'] = values.sortOrders?.map(s => {
+              const field = {
+                sortTerm: {}
+              }
+              if (s.strategy !== 'field') {
+                field.sortTerm['type'] = 'function'
+                field.sortTerm['funcName'] = s.strategy
+                field.sortTerm['funcArgs'] = []
+                if (['truncate', 'bucket'].includes(s.strategy)) {
+                  field.sortTerm['funcArgs'] = [
+                    {
+                      type: 'literal',
+                      dataType: 'integer',
+                      value: s.number + ''
+                    },
+                    {
+                      type: 'field',
+                      fieldName: [s.fieldName]
+                    }
+                  ]
+                } else {
+                  field.sortTerm['funcArgs'] = [
+                    {
+                      type: 'field',
+                      fieldName: [s.fieldName]
+                    }
+                  ]
                 }
-              }) || []
+              } else {
+                field.sortTerm = {
+                  type: s.strategy,
+                  fieldName: [s.fieldName]
+                }
+              }
+              field['direction'] = s.direction
+              field['nullOrdering'] = s.nullOrdering
+
+              return field
+            })
           }
-        }
-        if (editTable) {
-          // update table
-          const reqData = {
-            updates: genUpdates(cacheData, submitData, ['lakehouse-iceberg', 
'lakehouse-paimon'].includes(provider))
+          if (indexesInfo) {
+            submitData['indexes'] = values.indexes?.map(i => {
+              return {
+                indexType: i.indexType,
+                name: i.name,
+                fieldNames: i.fieldName.map(f => [f])
+              }
+            })
           }
-          if (reqData.updates.length) {
-            await dispatch(
-              updateTable({ init, metalake, catalog, catalogType, schema, 
table: cacheData.name, data: reqData })
-            )
+          if (
+            distributionInfo &&
+            (values?.distribution?.strategy || values?.distribution?.number || 
values?.distribution?.field)
+          ) {
+            submitData['distribution'] = {
+              strategy: values.distribution?.strategy,
+              number: values.distribution?.number || 0,
+              funcArgs:
+                values.distribution?.field?.map(f => {
+                  return {
+                    type: 'field',
+                    fieldName: [f]
+                  }
+                }) || []
+            }
           }
-        } else {
-          submitData.columns.forEach(col => {
-            delete col.uniqueId
-          })
-          if (tableDefaultProps[provider]) {
-            tableDefaultProps[provider].forEach(item => {
-              if (values[item.key]) {
-                submitData.properties[item.key] = values[item.key]
+          if (editTable) {
+            // update table
+            const reqData = {
+              updates: genUpdates(cacheData, submitData, ['lakehouse-iceberg', 
'lakehouse-paimon'].includes(provider))
+            }
+            if (reqData.updates.length) {
+              const action = await dispatch(
+                updateTable({ init, metalake, catalog, catalogType, schema, 
table: cacheData.name, data: reqData })
+              )
+              if (action?.payload?.err) {
+                throw new Error('Failed to update table')
               }
+            }
+            submitted = true
+          } else {
+            submitData.columns.forEach(col => {
+              delete col.uniqueId
             })
+            if (tableDefaultProps[provider]) {
+              tableDefaultProps[provider].forEach(item => {
+                if (values[item.key]) {
+                  submitData.properties[item.key] = values[item.key]
+                }
+              })
+            }
+            const action = await dispatch(createTable({ data: submitData, 
metalake, catalog, schema, catalogType }))
+            if (action?.payload?.err) {
+              throw new Error('Failed to create table')
+            }
+            submitted = true
+          }
+
+          if (submitted) {
+            treeRef.current.onLoadData({ key: `${catalog}/${schema}`, 
nodeType: 'schema' })
+            setOpen(false)
           }
-          await dispatch(createTable({ data: submitData, metalake, catalog, 
schema, catalogType }))
+        } catch (error) {
+          console.error(error)
+        } finally {
+          setConfirmLoading(false)
         }
-        treeRef.current.onLoadData({ key: `${catalog}/${schema}`, nodeType: 
'schema' })
-        setConfirmLoading(false)
-        setOpen(false)
       })
       .catch(info => {
         console.error(info)
diff --git 
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/CatalogDetailsPage.js
 
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/CatalogDetailsPage.js
index 3289fc636b..62add30ac8 100644
--- 
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/CatalogDetailsPage.js
+++ 
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/CatalogDetailsPage.js
@@ -87,6 +87,8 @@ const renderIcon = catalog => {
         return <Icons.starrocks className='size-8'></Icons.starrocks>
       case 'custom-icons-lakehouse':
         return <Icons.lakehouse className='size-8'></Icons.lakehouse>
+      case 'custom-icons-clickhouse':
+        return <Icons.clickhouse className='size-8'></Icons.clickhouse>
     }
   } else {
     return <Icons.iconify icon={calalogIcon} className='size-8' />
diff --git a/web-v2/web/src/components/Icons.js 
b/web-v2/web/src/components/Icons.js
index 8d006e6476..e4c2c423a8 100644
--- a/web-v2/web/src/components/Icons.js
+++ b/web-v2/web/src/components/Icons.js
@@ -45,6 +45,34 @@ const Icons = {
       </g>
     </svg>
   ),
+  clickhouse: ({ className, ...props }) => (
+    <svg
+      {...props}
+      className={['icon', className].filter(Boolean).join(' ')}
+      viewBox='0 0 1024 1024'
+      version='1.1'
+      xmlns='http://www.w3.org/2000/svg'
+      p-id='2751'
+      width='200'
+      height='200'
+    >
+      <path
+        d='M58.7 85.3H112c12.9-1.9 24.8 7.1 26.7 19.9 0.3 2.2 0.3 4.5 0 
6.8v800c1.9 12.9-7.1 24.8-19.9 26.7-2.2 0.3-4.5 0.3-6.8 0H58.7c-12.9 
1.9-24.8-7-26.7-19.9-0.3-2.3-0.3-4.6 0-6.8V112c-1.9-12.9 7.1-24.8 19.9-26.7 
2.3-0.4 4.5-0.4 6.8 0z'
+        fill='#FFCC01'
+        p-id='2752'
+      ></path>
+      <path
+        d='M32 832h106.7v80c0 14.7-11.9 26.7-26.7 26.7H58.7C44 938.7 32 926.8 
32 912v-80z'
+        fill='#FF191A'
+        p-id='2753'
+      ></path>
+      <path
+        d='M272 85.3h53.3c12.9-1.9 24.8 7.1 26.7 19.9 0.3 2.2 0.3 4.5 0 
6.8v800c1.9 12.9-7.1 24.8-19.9 26.7-2.2 0.3-4.5 0.3-6.8 0H272c-12.9 
1.9-24.8-7.1-26.7-19.9-0.3-2.2-0.3-4.5 0-6.8V112c-1.9-12.9 7.1-24.8 19.9-26.7 
2.3-0.3 4.6-0.3 6.8 0zM485.4 85.3h53.3c12.9-1.9 24.8 7.1 26.7 19.9 0.3 2.2 0.3 
4.5 0 6.8v800c1.9 12.9-7.1 24.8-19.9 26.7-2.2 0.3-4.5 0.3-6.8 0h-53.3c-12.9 
1.9-24.8-7.1-26.7-19.9-0.3-2.2-0.3-4.5 0-6.8V112c-1.9-12.9 7.1-24.8 19.9-26.7 
2.2-0.3 4.5-0.3 6.8 0zM698.7 85.3H752c12. [...]
+        fill='#FFCC01'
+        p-id='2754'
+      ></path>
+    </svg>
+  ),
   hive: props => (
     <svg xmlns='http://www.w3.org/2000/svg' version='1.1' viewBox='0 0 1000 
900' {...props}>
       <g stroke='#fdee21' transform='translate(-.53268 66.8)'>
diff --git a/web-v2/web/src/config/catalog.js b/web-v2/web/src/config/catalog.js
index 2f29860a9b..1e997c10f9 100644
--- a/web-v2/web/src/config/catalog.js
+++ b/web-v2/web/src/config/catalog.js
@@ -41,6 +41,8 @@ export const checkCatalogIcon = ({ type, provider }) => {
           return 'custom-icons-starrocks'
         case 'lakehouse-generic':
           return 'custom-icons-lakehouse'
+        case 'jdbc-clickhouse':
+          return 'custom-icons-clickhouse'
         default:
           return 'bx:book'
       }
@@ -290,6 +292,46 @@ export const providerBase = {
       }
     ]
   },
+  'jdbc-clickhouse': {
+    label: 'ClickHouse',
+    defaultProps: [
+      {
+        label: 'JDBC URL',
+        key: 'jdbc-url',
+        value: '',
+        required: true,
+        description: 'e.g. jdbc:clickhouse://localhost:8123'
+      },
+      {
+        label: 'JDBC Driver',
+        key: 'jdbc-driver',
+        value: '',
+        required: true,
+        description: 'e.g. com.clickhouse.ClickHouseDriver'
+      },
+      {
+        label: 'JDBC User',
+        key: 'jdbc-user',
+        value: '',
+        required: true,
+        description: 'The username to connect to ClickHouse'
+      },
+      {
+        label: 'JDBC Password',
+        key: 'jdbc-password',
+        value: '',
+        required: true,
+        description: 'The password to connect to ClickHouse'
+      },
+      {
+        label: 'Pool Min Size',
+        key: 'jdbc.pool.min-size',
+        value: '2',
+        required: false,
+        description: 'The minimum number of connections in the connection pool'
+      }
+    ]
+  },
   'lakehouse-iceberg': {
     label: 'Apache Iceberg',
     defaultProps: [
@@ -700,6 +742,11 @@ export const relationalProviders = [
     value: 'lakehouse-paimon',
     description: 'A realtime lakehouse architecture with Flink and Spark for 
both streaming and batch operations'
   },
+  {
+    label: 'ClickHouse',
+    value: 'jdbc-clickhouse',
+    description: 'A fast open-source column-oriented database management 
system'
+  },
   {
     label: 'Lakehouse Generic',
     value: 'lakehouse-generic',
diff --git a/web-v2/web/src/config/index.js b/web-v2/web/src/config/index.js
index eed62cf22e..a49389b7e9 100644
--- a/web-v2/web/src/config/index.js
+++ b/web-v2/web/src/config/index.js
@@ -108,7 +108,7 @@ export const ColumnType = [
   'decimal'
 ]
 
-export const ColumnTypeForMysql = ['byte unsigned', 'short unsigned', 'integer 
unsigned', 'long unsigned']
+export const ColumnTypeForUnsigned = ['byte unsigned', 'short unsigned', 
'integer unsigned', 'long unsigned']
 
 export const ColumnTypeSupportAutoIncrement = [
   'byte',
@@ -155,7 +155,19 @@ export const UnsupportColumnType = {
     'time'
   ],
   'lakehouse-generic': ['char', 'varchar', 'time', 'timestamp_tz'],
-  'lakehouse-paimon': ['interval_day', 'interval_year', 'union', 'uuid']
+  'lakehouse-paimon': ['interval_day', 'interval_year', 'union', 'uuid'],
+  'jdbc-clickhouse': [
+    'binary',
+    'fixed',
+    'struct',
+    'list',
+    'map',
+    'interval_day',
+    'interval_year',
+    'union',
+    'time',
+    'timestamp_tz'
+  ]
 }
 
 const tableLevelPropInfoMap = {
@@ -288,7 +300,8 @@ export const transformsLimitMap = {
 
 export const sortOrdersInfoMap = {
   hive: ['field'],
-  'lakehouse-iceberg': ['field', 'bucket', 'truncate', 'year', 'month', 'day', 
'hour']
+  'lakehouse-iceberg': ['field', 'bucket', 'truncate', 'year', 'month', 'day', 
'hour'],
+  'jdbc-clickhouse': ['field', 'bucket', 'truncate', 'year', 'month', 'day', 
'hour']
 }
 
 export const indexesInfoMap = {

Reply via email to