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 a682a2948e [#9388] web(ui): Support provider lakehouse-generic for
relational catalog in the UI (#9392)
a682a2948e is described below
commit a682a2948eadb77d141ba9cd64886189023c5ad4
Author: Qian Xia <[email protected]>
AuthorDate: Mon Dec 8 17:24:00 2025 +0800
[#9388] web(ui): Support provider lakehouse-generic for relational catalog
in the UI (#9392)
### What changes were proposed in this pull request?
<img width="2598" height="1832" alt="image"
src="https://github.com/user-attachments/assets/1e6ca191-1669-47b0-84ca-542463a73e05"
/>
<img width="2658" height="1856" alt="image"
src="https://github.com/user-attachments/assets/7b323e80-3312-4adc-93d8-65dbbfd8fe42"
/>
### Why are the changes needed?
N/A
Fix: #9388
### Does this PR introduce _any_ user-facing change?
N/A
### How was this patch tested?
manually
---
web/web/src/app/metalakes/metalake/MetalakeTree.js | 2 +
.../metalake/rightContent/CreateTableDialog.js | 135 +++++++++++++++++++--
web/web/src/lib/utils/initial.js | 44 +++++++
3 files changed, 169 insertions(+), 12 deletions(-)
diff --git a/web/web/src/app/metalakes/metalake/MetalakeTree.js
b/web/web/src/app/metalakes/metalake/MetalakeTree.js
index 7f9aa34883..661c04bf18 100644
--- a/web/web/src/app/metalakes/metalake/MetalakeTree.js
+++ b/web/web/src/app/metalakes/metalake/MetalakeTree.js
@@ -77,6 +77,8 @@ const MetalakeTree = props => {
return 'custom-icons-paimon'
case 'lakehouse-hudi':
return 'custom-icons-hudi'
+ case 'lakehouse-generic':
+ return 'material-symbols:houseboat-outline'
case 'jdbc-oceanbase':
return 'custom-icons-oceanbase'
default:
diff --git
a/web/web/src/app/metalakes/metalake/rightContent/CreateTableDialog.js
b/web/web/src/app/metalakes/metalake/rightContent/CreateTableDialog.js
index ccb0531171..371de52f4a 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/CreateTableDialog.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/CreateTableDialog.js
@@ -40,7 +40,7 @@
'use client'
// Import required React hooks
-import { useState, forwardRef, useEffect, Fragment } from 'react'
+import { useState, forwardRef, useEffect, Fragment, useRef, useMemo,
useCallback } from 'react'
// Import Material UI components
import {
@@ -86,6 +86,9 @@ import { genUpdates } from '@/lib/utils'
import { nameRegex, nameRegexDesc, keyRegex } from '@/lib/utils/regex'
import { useSearchParams } from 'next/navigation'
import { getRelationalColumnType, getParameterizedColumnType,
getRelationalTablePropInfo } from '@/lib/utils/initial'
+import { getCatalogDetailsApi } from '@/lib/api/catalogs'
+import { getSchemaDetailsApi } from '@/lib/api/schemas'
+import { to } from '@/lib/utils'
// Default form values
const defaultFormValues = {
@@ -116,11 +119,14 @@ const schema = yup.object().shape({
propItems: yup.array().of(
yup.object().shape({
required: yup.boolean(),
- key: yup.string().required(),
- value: yup.string().when('required', {
- is: true,
- then: schema => schema.required()
- })
+ key: yup.string().trim().required(),
+ value: yup
+ .string()
+ .trim()
+ .when('required', {
+ is: true,
+ then: schema => schema.required('Value is required for this
property')
+ })
})
)
})
@@ -147,8 +153,15 @@ const CreateTableDialog = props => {
const store = useAppSelector(state => state.metalakes)
const currentCatalog = store.catalogs.find(ca => ca.name === catalog)
- const columnTypes = getRelationalColumnType(currentCatalog?.provider)
- const propInfo = getRelationalTablePropInfo(currentCatalog?.provider)
+ const provider = currentCatalog?.provider
+ const columnTypes = useMemo(() => getRelationalColumnType(provider),
[provider])
+ const propInfo = useMemo(() => getRelationalTablePropInfo(provider),
[provider])
+
+ // Cache for location requirement check to avoid duplicate API calls
+ const locationCheckCache = useRef({
+ key: null,
+ isLocationRequired: false
+ })
const [innerProps, setInnerProps] = useState([])
const [tableColumns, setTableColumns] = useState([{ name: '', type: '',
nullable: true, comment: '' }])
@@ -327,6 +340,8 @@ const CreateTableDialog = props => {
const submitForm = formData => {
const hasErrorProperties = innerProps.some(prop => prop.hasDuplicateKey ||
prop.isReserved || prop.invalid)
+ const hasRequiredEmptyProperties = innerProps.some(prop => prop.required
&& !prop.value?.trim())
+
const hasDuplicateColumnNames = tableColumns
.filter(col => col.name.trim() !== '')
.some(
@@ -336,7 +351,7 @@ const CreateTableDialog = props => {
const hasInvalidColumnTypes = tableColumns.some(col => col.paramErrors)
- if (hasErrorProperties || hasDuplicateColumnNames ||
hasInvalidColumnTypes) {
+ if (hasErrorProperties || hasDuplicateColumnNames || hasInvalidColumnTypes
|| hasRequiredEmptyProperties) {
return
}
@@ -406,6 +421,100 @@ const CreateTableDialog = props => {
console.error('Form validation errors:', errors)
}
+ /**
+ * Check if location is required for lakehouse-generic tables
+ * Uses caching to avoid duplicate API calls
+ */
+ const checkLocationRequired = useCallback(async () => {
+ const cacheKey = `${metalake}-${catalog}-${schemaName}`
+
+ // Return cached result if available
+ if (locationCheckCache.current.key === cacheKey) {
+ return locationCheckCache.current.isLocationRequired
+ }
+
+ let isLocationRequired = false
+
+ // Check catalog properties for location
+ const [catalogErr, catalogRes] = await to(getCatalogDetailsApi({ metalake,
catalog }))
+ if (catalogErr) {
+ console.error('Error fetching catalog details:', catalogErr)
+
+ // If catalog fetch fails, we can't determine location requirement,
default to not required
+ return false
+ }
+ const catalogLocation = catalogRes?.catalog?.properties?.location
+
+ // If catalog has location, no need to check schema
+ if (catalogLocation) {
+ locationCheckCache.current = {
+ key: cacheKey,
+ isLocationRequired: false
+ }
+
+ return false
+ }
+
+ // Check schema properties for location
+ const [schemaErr, schemaRes] = await to(getSchemaDetailsApi({ metalake,
catalog, schema: schemaName }))
+ if (schemaErr) {
+ console.error('Error fetching schema details:', schemaErr)
+
+ // If schema fetch fails but catalog has no location, be safe and
require location
+ isLocationRequired = true
+ } else {
+ const schemaLocation = schemaRes?.schema?.properties?.location
+
+ // Location is required if not set in both catalog and schema
+ isLocationRequired = !schemaLocation
+ }
+
+ // Cache the result
+ locationCheckCache.current = {
+ key: cacheKey,
+ isLocationRequired
+ }
+
+ return isLocationRequired
+ }, [metalake, catalog, schemaName])
+
+ /**
+ * Effect to initialize default properties when creating a new table
+ * For lakehouse-generic, checks if location is set in catalog or schema
+ */
+ useEffect(() => {
+ // Only run when dialog opens for create mode
+ if (!open || type !== 'create') {
+ return
+ }
+
+ // Skip if no default props configured
+ if (!propInfo.defaultProps || propInfo.defaultProps.length === 0) {
+ return
+ }
+
+ const initDefaultProps = async () => {
+ let isLocationRequired = false
+
+ // For lakehouse-generic, check if location is set in catalog or schema
+ if (provider === 'lakehouse-generic') {
+ isLocationRequired = await checkLocationRequired()
+ }
+
+ const defaultPropertyItems = propInfo.defaultProps.map(prop => ({
+ key: prop.key,
+ value: prop.value || '',
+ required: prop.key === 'location' ? isLocationRequired : prop.required
|| false,
+ description: prop.description || '',
+ disabled: prop.disabled || false
+ }))
+ setInnerProps(defaultPropertyItems)
+ setValue('propItems', defaultPropertyItems)
+ }
+
+ initDefaultProps()
+ }, [open, type, propInfo, setValue, provider, checkLocationRequired])
+
/**
* Effect to populate form when editing existing table
*/
@@ -749,7 +858,7 @@ const CreateTableDialog = props => {
size='small'
name='value'
label='Value'
- error={item.required && item.value === ''}
+ error={item.required && !item.value?.trim()}
value={item.value}
disabled={item.disabled}
onChange={event => handlePropertyChange({
index, event })}
@@ -771,11 +880,13 @@ const CreateTableDialog = props => {
</Box>
<FormHelperText
sx={{
- color: item.required && item.value === '' ?
'error.main' : 'text.main',
+ color: item.required && !item.value?.trim() ?
'error.main' : 'text.main',
maxWidth: 'calc(100% - 40px)'
}}
>
- {item.description}
+ {item.required && !item.value?.trim()
+ ? `${item.description ? item.description + ' ' :
''}(Required)`
+ : item.description}
</FormHelperText>
{item.hasDuplicateKey && (
<FormHelperText
className={'twc-text-error-main'}>Key already exists</FormHelperText>
diff --git a/web/web/src/lib/utils/initial.js b/web/web/src/lib/utils/initial.js
index 9c09caccf2..db6334b4be 100644
--- a/web/web/src/lib/utils/initial.js
+++ b/web/web/src/lib/utils/initial.js
@@ -255,6 +255,11 @@ export const providers = [
}
]
},
+ {
+ label: 'Lakehouse Generic',
+ value: 'lakehouse-generic',
+ defaultProps: []
+ },
{
label: 'MySQL',
value: 'jdbc-mysql',
@@ -607,6 +612,24 @@ const relationalColumnTypeMap = {
'timestamp_tz',
'varchar'
],
+ 'lakehouse-generic': [
+ 'binary',
+ 'boolean',
+ 'byte',
+ 'date',
+ 'decimal',
+ 'double',
+ 'fixed',
+ 'float',
+ 'integer',
+ 'interval_day',
+ 'interval_year',
+ 'long',
+ 'short',
+ 'string',
+ 'time',
+ 'timestamp'
+ ],
'jdbc-oceanbase': [
'binary',
'byte',
@@ -715,6 +738,27 @@ const relationalTablePropInfoMap = {
immutable: ['merge-engine', 'rowkind.field', 'sequence.field'],
allowDelete: true,
allowAdd: true
+ },
+ 'lakehouse-generic': {
+ reserved: [],
+ immutable: ['format', 'location'],
+ allowDelete: true,
+ allowAdd: true,
+ defaultProps: [
+ {
+ key: 'location',
+ value: '',
+ required: false,
+ description: 'The storage location of the table. Required if not set
in catalog or schema.'
+ },
+ {
+ key: 'format',
+ value: 'lance',
+ required: true,
+ description: "The table format. Currently only 'lance' is supported.",
+ disabled: true
+ }
+ ]
}
}