Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#5453] subtask(web): support for table column types with parameters #5592

Merged
merged 3 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 139 additions & 24 deletions web/web/src/app/metalakes/metalake/rightContent/CreateTableDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}

Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
Expand All @@ -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}
Expand Down Expand Up @@ -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>
Expand All @@ -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' }}>
Expand Down
2 changes: 1 addition & 1 deletion web/web/src/lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
140 changes: 123 additions & 17 deletions web/web/src/lib/utils/initial.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
]
Loading