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