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 = {