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

liuxun 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 bcc4c42f5 [#5453] subtask(web): support for table column types with 
parameters (#5592)
bcc4c42f5 is described below

commit bcc4c42f5a5ba9964f9b49278f8ec621e7ec937b
Author: JUN <oren....@gmail.com>
AuthorDate: Tue Nov 19 11:49:34 2024 +0800

    [#5453] subtask(web): support for table column types with parameters (#5592)
    
    ### What changes were proposed in this pull request?
    
    Create and update column types `['char', 'varchar', 'fixed', `decimal`]`
    with parameters
    
    ### Why are the changes needed?
    
    Close: #5453
    
    ### Does this PR introduce _any_ user-facing change?
    
    four more table type column type on selection
    
    ### How was this patch tested?
    
    Create part 1
    
    
![chrome-capture-2024-11-16](https://github.com/user-attachments/assets/befec5c6-0afb-469a-928f-f6fa49a792f2)
    
    Create part 2
    ![chrome-capture-2024-11-16
    
(1)](https://github.com/user-attachments/assets/efcacfdc-18e0-44b4-b80d-8caf65d71df2)
    
    Update
    ![chrome-capture-2024-11-16
    
(2)](https://github.com/user-attachments/assets/cec70ac7-cbd5-411f-8970-27d854fbc4d5)
---
 .../metalake/rightContent/CreateTableDialog.js     | 163 ++++++++++++++++++---
 web/web/src/lib/utils/index.js                     |   2 +-
 web/web/src/lib/utils/initial.js                   | 140 +++++++++++++++---
 3 files changed, 263 insertions(+), 42 deletions(-)

diff --git 
a/web/web/src/app/metalakes/metalake/rightContent/CreateTableDialog.js 
b/web/web/src/app/metalakes/metalake/rightContent/CreateTableDialog.js
index 1132964b4..df0091a26 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/CreateTableDialog.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/CreateTableDialog.js
@@ -85,7 +85,7 @@ import { groupBy } from 'lodash-es'
 import { genUpdates } from '@/lib/utils'
 import { nameRegex, nameRegexDesc, keyRegex } from '@/lib/utils/regex'
 import { useSearchParams } from 'next/navigation'
-import { relationalTypes } from '@/lib/utils/initial'
+import { tableColumnTypes } from '@/lib/utils/initial'
 
 // Default form values
 const defaultFormValues = {
@@ -142,6 +142,7 @@ const CreateTableDialog = props => {
   const [innerProps, setInnerProps] = useState([])
   const [tableColumns, setTableColumns] = useState([{ name: '', type: '', 
nullable: true, comment: '' }])
   const [initialTableData, setInitialTableData] = useState()
+  const [selectedColumnIndex, setSelectedColumnIndex] = useState(null)
   const dispatch = useAppDispatch()
 
   // Initialize form with react-hook-form
@@ -205,6 +206,34 @@ const CreateTableDialog = props => {
       }
     }
 
+    // reset type suffix and param errors
+    if (field === 'type') {
+      updatedColumns[index].typeSuffix = ''
+      updatedColumns[index].paramErrors = ''
+      if (tableColumnTypes.find(type => type.key === value)?.params) {
+        updatedColumns[index].paramValues = []
+      }
+    }
+
+    setTableColumns(updatedColumns)
+    setValue('columns', updatedColumns)
+  }
+
+  const transformParamValues = index => {
+    let updatedColumns = [...tableColumns]
+
+    const validateParams = tableColumnTypes.find(type => type.key === 
updatedColumns[index].type)?.validateParams
+    const paramValues = updatedColumns[index].paramValues.filter(param => 
param !== undefined).map(Number)
+    const validateResult = validateParams(paramValues)
+
+    if (validateResult.valid) {
+      updatedColumns[index].typeSuffix = `(${paramValues.join(',')})`
+      updatedColumns[index].paramErrors = ''
+    } else {
+      updatedColumns[index].paramErrors = validateResult.message
+    }
+
+    updatedColumns[index].paramValues = undefined
     setTableColumns(updatedColumns)
     setValue('columns', updatedColumns)
   }
@@ -303,7 +332,9 @@ const CreateTableDialog = props => {
           filteredCols.findIndex(otherCol => otherCol !== col && 
otherCol.name.trim() === col.name.trim()) !== -1
       )
 
-    if (hasDuplicateKeys || hasInvalidKeys || hasDuplicateColumnNames) {
+    const hasInvalidColumnTypes = tableColumns.some(col => col.paramErrors)
+
+    if (hasDuplicateKeys || hasInvalidKeys || hasDuplicateColumnNames || 
hasInvalidColumnTypes) {
       return
     }
 
@@ -321,7 +352,14 @@ const CreateTableDialog = props => {
         const tableData = {
           name: formData.name,
           comment: formData.comment,
-          columns: formData.columns.map(({ hasDuplicateName, ...rest }) => 
rest),
+
+          // remove redundant fields
+          columns: formData.columns.map(({ hasDuplicateName, paramErrors, 
typeSuffix, ...rest }) => {
+            return {
+              ...rest,
+              type: rest.type + typeSuffix || '' // combine type and type 
suffix, like decimal(10,2)
+            }
+          }),
           properties
         }
 
@@ -381,6 +419,13 @@ const CreateTableDialog = props => {
         // Set uniqueId to the column name to detect changes
         column.uniqueId = column.name
 
+        // Extract type suffix for types with parameters
+        const match = column.type.match(/(\w+)(\([\d,]+\))/)
+        if (match && match.length === 3) {
+          column.typeSuffix = match[2]
+          column.type = match[1]
+        }
+
         return {
           ...column
         }
@@ -401,10 +446,32 @@ const CreateTableDialog = props => {
     }
   }, [open, data, setValue, type])
 
+  // Handle click outside of table rows
+  useEffect(() => {
+    const handleClickOutside = e => {
+      const selectElements = document.querySelectorAll('[role="listbox"]')
+      const isClickInsideSelect = Array.from(selectElements).some(el => 
el.contains(e.target))
+      if (isClickInsideSelect) {
+        return
+      }
+
+      const isClickInsideTableCell = e.target.closest('td')
+      if (isClickInsideTableCell) {
+        return
+      }
+
+      setSelectedColumnIndex(null)
+    }
+
+    document.addEventListener('click', handleClickOutside)
+
+    return () => document.removeEventListener('click', handleClickOutside)
+  }, [])
+
   return (
     <Dialog
       fullWidth
-      maxWidth='md'
+      maxWidth='lg'
       scroll='body'
       TransitionComponent={Transition}
       open={open}
@@ -484,10 +551,10 @@ const CreateTableDialog = props => {
                 <Table stickyHeader>
                   <TableHead>
                     <TableRow>
-                      <TableCell sx={{ minWidth: 100 }}>Name</TableCell>
+                      <TableCell sx={{ minWidth: 100, width: 200 
}}>Name</TableCell>
                       <TableCell sx={{ minWidth: 100 }}>Type</TableCell>
                       <TableCell sx={{ minWidth: 100 }}>Nullable</TableCell>
-                      <TableCell sx={{ minWidth: 200 }}>Comment</TableCell>
+                      <TableCell sx={{ minWidth: 200, width: 550 
}}>Comment</TableCell>
                       <TableCell sx={{ minWidth: 50 }}>Action</TableCell>
                     </TableRow>
                   </TableHead>
@@ -512,25 +579,73 @@ const CreateTableDialog = props => {
                             )}
                           </FormControl>
                         </TableCell>
-                        <TableCell sx={{ verticalAlign: 'top' }}>
+                        <TableCell sx={{ verticalAlign: 'top' }} onClick={() 
=> setSelectedColumnIndex(index)}>
                           <FormControl fullWidth>
-                            <Select
-                              size='small'
-                              fullWidth
-                              value={column.type}
-                              onChange={e => handleColumnChange({ index, 
field: 'type', value: e.target.value })}
-                              error={!column.type.trim()}
-                              data-refer={`column-type-${index}`}
-                            >
-                              {relationalTypes.map(type => (
-                                <MenuItem key={type.value} value={type.value}>
-                                  {type.label}
-                                </MenuItem>
-                              ))}
-                            </Select>
-                            {!column.type.trim() && (
-                              <FormHelperText 
className={'twc-text-error-main'}>Type is required</FormHelperText>
-                            )}
+                            <Box sx={{ display: 'flex', gap: 1 }}>
+                              <Box sx={{ minWidth: 120 }}>
+                                <Select
+                                  size='small'
+                                  fullWidth
+                                  value={column.type}
+                                  onChange={e => handleColumnChange({ index, 
field: 'type', value: e.target.value })}
+                                  error={!column.type.trim() || 
column.paramErrors}
+                                  data-refer={`column-type-${index}`}
+                                  renderValue={selected => 
<Box>{`${selected}${column.typeSuffix || ''}`}</Box>}
+                                >
+                                  {tableColumnTypes.map(type => (
+                                    <MenuItem key={type.key} value={type.key}>
+                                      {type.key}
+                                    </MenuItem>
+                                  ))}
+                                </Select>
+                                {!column.type.trim() && (
+                                  <FormHelperText 
className={'twc-text-error-main'}>Type is required</FormHelperText>
+                                )}
+                                {column.paramErrors && (
+                                  <FormHelperText 
className={'twc-text-error-main'}>
+                                    {column.paramErrors}
+                                  </FormHelperText>
+                                )}
+                              </Box>
+                              {selectedColumnIndex === index &&
+                                column.type &&
+                                (() => {
+                                  // Process typeSuffix before mapping
+                                  if (column.typeSuffix && 
!column.paramValues) {
+                                    const paramStr = 
column.typeSuffix.slice(1, -1) // Remove parentheses
+                                    const values = paramStr.split(',').map(v 
=> v.trim())
+                                    handleColumnChange({
+                                      index,
+                                      field: 'paramValues',
+                                      value: values
+                                    })
+                                  }
+
+                                  return tableColumnTypes
+                                    .find(t => t.key === column.type)
+                                    ?.params?.map((param, paramIndex) => (
+                                      <TextField
+                                        key={paramIndex}
+                                        size='small'
+                                        type='number'
+                                        sx={{ minWidth: 60 }}
+                                        
value={column.paramValues?.[paramIndex] || ''}
+                                        onChange={e => {
+                                          const newParamValues = 
[...(column.paramValues || [])]
+                                          newParamValues[paramIndex] = 
e.target.value
+                                          handleColumnChange({ index, field: 
'paramValues', value: newParamValues })
+                                        }}
+                                        placeholder={`${param}`}
+                                        
data-refer={`column-param-${index}-${paramIndex}`}
+                                        inputProps={{ min: 0 }}
+                                      />
+                                    ))
+                                })()}
+                              {selectedColumnIndex !== index &&
+                                tableColumnTypes.find(type => type.key === 
column.type)?.params &&
+                                column.paramValues &&
+                                transformParamValues(index)}
+                            </Box>
                           </FormControl>
                         </TableCell>
                         <TableCell sx={{ verticalAlign: 'top' }}>
diff --git a/web/web/src/lib/utils/index.js b/web/web/src/lib/utils/index.js
index d2dde2a4b..17444dfa8 100644
--- a/web/web/src/lib/utils/index.js
+++ b/web/web/src/lib/utils/index.js
@@ -102,7 +102,7 @@ export const genUpdates = (originalData, newData) => {
           newFieldName: newColumnsMap[key].name
         })
       }
-      if (originalColumnsMap[key].type !== newColumnsMap[key].type) {
+      if ((originalColumnsMap[key].type + originalColumnsMap[key].typeSuffix 
|| '') !== newColumnsMap[key].type) {
         updates.push({
           '@type': 'updateColumnType',
           fieldName: [newColumnsMap[key].name],
diff --git a/web/web/src/lib/utils/initial.js b/web/web/src/lib/utils/initial.js
index 9efd67a7a..1ca2854da 100644
--- a/web/web/src/lib/utils/initial.js
+++ b/web/web/src/lib/utils/initial.js
@@ -347,21 +347,127 @@ export const providers = [
   }
 ]
 
-export const relationalTypes = [
-  { label: 'Boolean', value: 'boolean' },
-  { label: 'Byte', value: 'byte' },
-  { label: 'Short', value: 'short' },
-  { label: 'Integer', value: 'integer' },
-  { label: 'Long', value: 'long' },
-  { label: 'Float', value: 'float' },
-  { label: 'Double', value: 'double' },
-  { label: 'Date', value: 'date' },
-  { label: 'Time', value: 'time' },
-  { label: 'Timestamp', value: 'timestamp' },
-  { label: 'Timestamp_tz', value: 'timestamp_tz' },
-  { label: 'String', value: 'string' },
-  { label: 'Interval_day', value: 'interval_day' },
-  { label: 'Interval_year', value: 'interval_year' },
-  { label: 'Uuid', value: 'uuid' },
-  { label: 'Binary', value: 'binary' }
+export const tableColumnTypes = [
+  { key: 'boolean' },
+  { key: 'byte' },
+  { key: 'short' },
+  { key: 'integer' },
+  { key: 'long' },
+  { key: 'float' },
+  { key: 'double' },
+  {
+    key: 'decimal',
+    params: ['precision', 'scale'],
+    validateParams: params => {
+      if (params.length !== 2) {
+        return {
+          valid: false,
+          message: 'Please set precision and scale'
+        }
+      }
+
+      const [param1, param2] = params
+      if (param1 <= 0 || param1 > 38) {
+        return {
+          valid: false,
+          message: 'The precision must be between 1 and 38'
+        }
+      }
+
+      if (param2 < 0 || param2 > param1) {
+        return {
+          valid: false,
+          message: 'The scale must be between 0 and the precision'
+        }
+      }
+
+      return {
+        valid: true
+      }
+    }
+  },
+  { key: 'date' },
+  { key: 'time' },
+  { key: 'timestamp' },
+  { key: 'timestamp_tz' },
+  { key: 'string' },
+  {
+    key: 'char',
+    params: ['length'],
+    validateParams: params => {
+      if (params.length !== 1) {
+        return {
+          valid: false,
+          message: 'Please set length'
+        }
+      }
+
+      const length = params[0]
+
+      if (length <= 0) {
+        return {
+          valid: false,
+          message: 'The length must be greater than 0'
+        }
+      }
+
+      return {
+        valid: true
+      }
+    }
+  },
+  {
+    key: 'varchar',
+    params: ['length'],
+    validateParams: params => {
+      if (params.length !== 1) {
+        return {
+          valid: false,
+          message: 'Please set length'
+        }
+      }
+
+      const length = params[0]
+
+      if (length <= 0) {
+        return {
+          valid: false,
+          message: 'The length must be greater than 0'
+        }
+      }
+
+      return {
+        valid: true
+      }
+    }
+  },
+  { key: 'interval_day' },
+  { key: 'interval_year' },
+  {
+    key: 'fixed',
+    params: ['length'],
+    validateParams: params => {
+      if (params.length !== 1) {
+        return {
+          valid: false,
+          message: 'Please set length'
+        }
+      }
+
+      const length = params[0]
+
+      if (length <= 0) {
+        return {
+          valid: false,
+          message: 'The length must be greater than 0'
+        }
+      }
+
+      return {
+        valid: true
+      }
+    }
+  },
+  { key: 'uuid' },
+  { key: 'binary' }
 ]

Reply via email to