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 <[email protected]>
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

Create part 2

Update

---
.../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' }
]