orenccl commented on code in PR #5524:
URL: https://github.com/apache/gravitino/pull/5524#discussion_r1837616085


##########
web/web/src/app/metalakes/metalake/rightContent/CreateTableDialog.js:
##########
@@ -0,0 +1,643 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * CreateTableDialog component
+ *
+ * A dialog component for creating and editing tables in a metalake catalog.
+ *
+ * Features:
+ * - Create new tables or edit existing ones
+ * - Configure table name, comment and properties
+ * - Add/edit/remove table columns with name, type, nullable, and comment 
fields
+ * - Add/edit/remove custom table properties
+ * - Form validation using yup schema
+ * - Responsive dialog layout
+ *
+ * Props:
+ * @param {boolean} open - Controls dialog visibility
+ * @param {function} setOpen - Function to update dialog visibility
+ * @param {string} type - Dialog mode: 'create' or 'edit'
+ * @param {object} data - Table data for edit mode
+ */
+
+'use client'
+
+// Import required React hooks
+import { useState, forwardRef, useEffect, Fragment } from 'react'
+
+// Import Material UI components
+import {
+  Box,
+  Grid,
+  Button,
+  Dialog,
+  TextField,
+  Typography,
+  DialogContent,
+  DialogActions,
+  IconButton,
+  Fade,
+  FormControl,
+  FormHelperText,
+  Switch,
+  Table,
+  TableBody,
+  TableCell,
+  TableContainer,
+  TableHead,
+  TableRow,
+  Paper,
+  Select,
+  MenuItem
+} from '@mui/material'
+
+// Import custom components
+import Icon from '@/components/Icon'
+
+// Import Redux hooks and actions
+import { useAppDispatch } from '@/lib/hooks/useStore'
+import { createTable, updateTable } from '@/lib/store/metalakes'
+
+// Import form validation libraries
+import * as yup from 'yup'
+import { useForm, Controller } from 'react-hook-form'
+import { yupResolver } from '@hookform/resolvers/yup'
+
+// Import utility functions and constants
+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'
+
+// Default form values
+const defaultFormValues = {
+  name: '',
+  comment: '',
+  columns: [],
+  propItems: []
+}
+
+// Form validation schema
+const schema = yup.object().shape({
+  name: yup.string().required().matches(nameRegex, nameRegexDesc),
+  columns: yup.array().of(
+    yup.object().shape({
+      name: yup.string().required(),
+      type: yup.string().required(),
+      nullable: yup.boolean(),
+      comment: yup.string()
+    })
+  ),
+  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()
+      })
+    })
+  )
+})
+
+// Dialog transition component
+const Transition = forwardRef(function Transition(props, ref) {
+  return <Fade ref={ref} {...props} />
+})
+
+/**
+ * Main CreateTableDialog component
+ * Handles creation and editing of tables with columns and properties
+ */
+const CreateTableDialog = props => {
+  // Destructure props
+  const { open, setOpen, type = 'create', data = {} } = props
+
+  // Get URL parameters
+  const searchParams = useSearchParams()
+  const metalake = searchParams.get('metalake')
+  const catalog = searchParams.get('catalog')
+  const catalogType = searchParams.get('type')
+  const schemaName = searchParams.get('schema')
+
+  // Component state
+  const [innerProps, setInnerProps] = useState([])
+  const [tableColumns, setTableColumns] = useState([])
+  const [initialTableData, setInitialTableData] = useState()
+  const dispatch = useAppDispatch()
+
+  // Initialize form with react-hook-form
+  const {
+    control,
+    reset,
+    setValue,
+    getValues,
+    handleSubmit,
+    trigger,
+    formState: { errors }
+  } = useForm({
+    defaultValues: defaultFormValues,
+    mode: 'all',
+    resolver: yupResolver(schema)
+  })
+
+  /**
+   * Handle changes to property form fields
+   * Validates keys and checks for duplicates
+   */
+  const handlePropertyChange = ({ index, event }) => {
+    let updatedProps = [...innerProps]
+    updatedProps[index][event.target.name] = event.target.value
+
+    if (event.target.name === 'key') {
+      const isInvalidKey = !keyRegex.test(event.target.value)
+      updatedProps[index].invalid = isInvalidKey
+    }
+
+    const nonEmptyKeys = updatedProps.filter(item => item.key.trim() !== '')
+    const groupedKeys = groupBy(nonEmptyKeys, 'key')
+    const hasDuplicateKeys = Object.keys(groupedKeys).some(key => 
groupedKeys[key].length > 1)
+
+    if (hasDuplicateKeys) {
+      updatedProps[index].hasDuplicateKey = hasDuplicateKeys
+    } else {
+      updatedProps.forEach(item => (item.hasDuplicateKey = false))
+    }
+
+    setInnerProps(updatedProps)
+    setValue('propItems', updatedProps)
+  }
+
+  /**
+   * Handle changes to column fields
+   */
+  const handleColumnChange = ({ index, field, value }) => {
+    let updatedColumns = [...tableColumns]
+    updatedColumns[index][field] = value
+    setTableColumns(updatedColumns)
+    setValue('columns', updatedColumns)
+  }
+
+  /**
+   * Add a new empty column
+   */
+  const addColumn = () => {
+    const newColumn = { name: '', type: '', nullable: false, comment: '' }
+    setTableColumns([...tableColumns, newColumn])
+    setValue('columns', [...tableColumns, newColumn])
+  }
+
+  /**
+   * Remove a column at specified index
+   */
+  const removeColumn = index => {
+    let updatedColumns = [...tableColumns]
+    updatedColumns.splice(index, 1)
+    setTableColumns(updatedColumns)
+    setValue('columns', updatedColumns)
+  }
+
+  /**
+   * Add a new property field
+   * Checks for duplicate keys before adding
+   */
+  const addProperty = () => {
+    const hasDuplicateKeys = innerProps
+      .filter(item => item.key.trim() !== '')
+      .some(
+        (item, index, filteredItems) =>
+          filteredItems.findIndex(otherItem => otherItem !== item && 
otherItem.key.trim() === item.key.trim()) !== -1
+      )
+
+    if (hasDuplicateKeys) {
+      return
+    }
+
+    const newProperty = { key: '', value: '', required: false }
+
+    setInnerProps([...innerProps, newProperty])
+    setValue('propItems', [...innerProps, newProperty])
+  }
+
+  /**
+   * Remove a property field at specified index
+   */
+  const removeProperty = index => {
+    let updatedProps = [...innerProps]
+    updatedProps.splice(index, 1)
+    setInnerProps(updatedProps)
+    setValue('propItems', updatedProps)
+  }
+
+  /**
+   * Handle dialog close
+   * Resets form and clears state
+   */
+  const handleDialogClose = () => {
+    reset()
+    setInnerProps([])
+    setTableColumns([])
+    setValue('propItems', [])
+    setValue('columns', [])
+    setOpen(false)
+  }
+
+  /**
+   * Handle form submission
+   */
+  const handleFormSubmit = e => {
+    e.preventDefault()
+
+    return handleSubmit(submitForm(getValues()), handleValidationError)
+  }
+
+  /**
+   * Process form submission
+   * Validates data and dispatches create/update actions
+   */
+  const submitForm = formData => {
+    const hasDuplicateKeys = innerProps
+      .filter(item => item.key.trim() !== '')
+      .some(
+        (item, index, filteredItems) =>
+          filteredItems.findIndex(otherItem => otherItem !== item && 
otherItem.key.trim() === item.key.trim()) !== -1
+      )
+
+    const hasInvalidKeys = innerProps.some(prop => prop.invalid)
+
+    if (hasDuplicateKeys || hasInvalidKeys) {
+      return
+    }
+
+    trigger()
+
+    schema
+      .validate(formData)
+      .then(() => {
+        const properties = innerProps.reduce((acc, item) => {
+          acc[item.key] = item.value
+
+          return acc
+        }, {})
+
+        const tableData = {
+          name: formData.name,
+          comment: formData.comment,
+          columns: formData.columns,
+          properties
+        }
+
+        if (type === 'create') {
+          dispatch(createTable({ data: tableData, metalake, catalog, type: 
catalogType, schema: schemaName })).then(
+            res => {
+              if (!res.payload?.err) {
+                handleDialogClose()
+              }
+            }
+          )
+        } else {
+          const updates = genUpdates(initialTableData, tableData)

Review Comment:
   Thanks for your help. I was overthinking this problem and couldn't resolve 
it.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to