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
+      }
+    ]
   }
 }
 

Reply via email to