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

jshao 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 c347a08d6c [#7438] subtask(UI): Web UI supports model version with 
multiple URIs (#8331)
c347a08d6c is described below

commit c347a08d6c80ee271ffed4e6446bd63a527e5c9a
Author: Qian Xia <[email protected]>
AuthorDate: Mon Sep 1 14:09:10 2025 +0800

    [#7438] subtask(UI): Web UI supports model version with multiple URIs 
(#8331)
    
    ### What changes were proposed in this pull request?
    1. Web UI supports model version with multiple URIs
    2. support edit aliases
    <img width="700" height="844" alt="image"
    
src="https://github.com/user-attachments/assets/f9296878-d6bc-415b-88af-9ad8f790213d";
    />
    <img width="1051" height="885" alt="image"
    
src="https://github.com/user-attachments/assets/9d3fc85e-9209-4db2-badd-6b23f2964d95";
    />
    <img width="1148" height="781" alt="image"
    
src="https://github.com/user-attachments/assets/000c9020-3268-4562-b868-7ad32f296076";
    />
    <img width="779" height="709" alt="image"
    
src="https://github.com/user-attachments/assets/90d43765-b986-48db-9aca-89976862e714";
    />
    
    
    
    ### Why are the changes needed?
    N/A
    
    Fix: #7438
    
    ### Does this PR introduce _any_ user-facing change?
    N/A
    
    ### How was this patch tested?
    munually
---
 .../metalake/rightContent/LinkVersionDialog.js     | 193 ++++++++++++++++++---
 .../tabsContent/detailsView/DetailsView.js         |  51 ++++++
 web/web/src/components/DetailsDrawer.js            |  51 ++++++
 web/web/src/lib/utils/index.js                     |  33 ++++
 4 files changed, 302 insertions(+), 26 deletions(-)

diff --git 
a/web/web/src/app/metalakes/metalake/rightContent/LinkVersionDialog.js 
b/web/web/src/app/metalakes/metalake/rightContent/LinkVersionDialog.js
index f0b619698b..3769d33b55 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/LinkVersionDialog.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/LinkVersionDialog.js
@@ -34,7 +34,9 @@ import {
   IconButton,
   InputLabel,
   TextField,
-  Typography
+  Typography,
+  Tooltip,
+  Switch
 } from '@mui/material'
 
 import Icon from '@/components/Icon'
@@ -50,16 +52,42 @@ import { groupBy } from 'lodash-es'
 import { keyRegex } from '@/lib/utils/regex'
 import { useSearchParams } from 'next/navigation'
 import { useAppSelector } from '@/lib/hooks/useStore'
+import clsx from 'clsx'
 
 const defaultValues = {
-  uri: '',
+  uris: [{ name: '', uri: '', defaultUri: true }],
   aliases: [{ name: '' }],
   comment: '',
   propItems: []
 }
 
 const schema = yup.object().shape({
-  uri: yup.string().required(),
+  uris: yup
+    .array()
+    .of(
+      yup.object().shape({
+        uri: yup.string().when('name', {
+          is: name => !!name,
+          then: schema => schema.required(),
+          otherwise: schema => schema
+        })
+      })
+    )
+    .test('unique', 'Uri name must be unique', (uris, ctx) => {
+      const values = uris?.filter(l => !!l.name).map(l => l.name)
+      const duplicates = values.filter((value, index, self) => 
self.indexOf(value) !== index)
+
+      if (duplicates.length > 0) {
+        const duplicateIndex = values.lastIndexOf(duplicates[0])
+
+        return ctx.createError({
+          path: `uris.${duplicateIndex}.name`,
+          message: 'This URI name is duplicated'
+        })
+      }
+
+      return true
+    }),
   aliases: yup
     .array()
     .of(
@@ -128,6 +156,9 @@ const LinkVersionDialog = props => {
     resolver: yupResolver(schema)
   })
 
+  const defaultUriProps = watch('propItems').filter(item => item.defaultUri)[0]
+  const urisItems = watch('uris')
+
   const handleFormChange = ({ index, event }) => {
     let data = [...innerProps]
     data[index][event.target.name] = event.target.value
@@ -171,11 +202,29 @@ const LinkVersionDialog = props => {
     setValue('propItems', data)
   }
 
+  const onChangeDefaultUri = ({ index, event }) => {
+    fields.forEach((item, i) => {
+      if (i !== index) {
+        setValue(`uris.${i}.defaultUri`, false)
+      }
+    })
+    setValue(`uris.${index}.defaultUri`, event.target.checked)
+  }
+
   const { fields, append, remove } = useFieldArray({
     control,
     name: 'aliases'
   })
 
+  const {
+    fields: uris,
+    append: appendUri,
+    remove: removeUri
+  } = useFieldArray({
+    control,
+    name: 'uris'
+  })
+
   const watchAliases = watch('aliases')
 
   const handleClose = () => {
@@ -210,7 +259,16 @@ const LinkVersionDialog = props => {
         }, {})
 
         const schemaData = {
-          uri: data.uri,
+          uris: data.uris.reduce((acc, item) => {
+            if (item.name && item.uri) {
+              acc[item.name] = item.uri
+              if (item.defaultUri && !properties['default-uri-name']) {
+                properties['default-uri-name'] = item.name
+              }
+            }
+
+            return acc
+          }, {}),
           aliases: data.aliases.map(alias => alias.name).filter(aliasName => 
aliasName),
           comment: data.comment,
           properties
@@ -260,9 +318,16 @@ const LinkVersionDialog = props => {
       const { properties = {} } = data
 
       setCacheData(data)
-      setValue('uri', data.uri)
       setValue('comment', data.comment)
 
+      const uris =
+        data.uris &&
+        Object.entries(data.uris).map(([name, uri]) => ({
+          name,
+          uri,
+          defaultUri: properties['default-uri-name'] === name
+        }))
+      setValue('uris', uris)
       const aliases = data.aliases.map(alias => ({ name: alias }))
       setValue(`aliases`, aliases)
 
@@ -304,24 +369,103 @@ const LinkVersionDialog = props => {
 
           <Grid container spacing={6}>
             <Grid item xs={12}>
-              <FormControl fullWidth>
-                <Controller
-                  name='uri'
-                  control={control}
-                  rules={{ required: true }}
-                  render={({ field: { value, onChange } }) => (
-                    <TextField
-                      value={value}
-                      label='URI'
-                      onChange={onChange}
-                      placeholder=''
-                      error={Boolean(errors.uri)}
-                      data-refer='link-uri-field'
-                    />
-                  )}
-                />
-                {errors.uri && <FormHelperText sx={{ color: 'error.main' 
}}>{errors.uri.message}</FormHelperText>}
-              </FormControl>
+              <Typography sx={{ mb: 2 }} variant='body2'>
+                Uris
+              </Typography>
+              {uris.map((field, index) => {
+                return (
+                  <Grid key={index} item xs={12} sx={{ '& + &': { mt: 2 } }}>
+                    <FormControl fullWidth>
+                      <Box
+                        key={field.id}
+                        sx={{ display: 'flex', alignItems: 'center', 
justifyContent: 'space-between', gap: 1 }}
+                        data-refer={`uris-${index}`}
+                      >
+                        <Box className={clsx(uris.length === 1 && 
uris[index]?.name === 'unknown' ? 'twc-hidden' : '')}>
+                          <Controller
+                            name={`uris.${index}.name`}
+                            control={control}
+                            render={({ field: { value, onChange } }) => (
+                              <TextField
+                                {...field}
+                                value={value}
+                                onChange={onChange}
+                                label={`Name ${index + 1}`}
+                                data-refer={`uris-name-${index}`}
+                                error={!!errors.uris?.[index]?.name || 
!!errors.uris?.message}
+                                
helperText={errors.uris?.[index]?.name?.message || errors.uris?.message}
+                                fullWidth
+                              />
+                            )}
+                          />
+                        </Box>
+                        <Box sx={{ flexGrow: 1 }}>
+                          <Controller
+                            name={`uris.${index}.uri`}
+                            control={control}
+                            render={({ field: { value, onChange } }) => (
+                              <TextField
+                                {...field}
+                                value={value}
+                                onChange={onChange}
+                                label={`URI ${index + 1}`}
+                                data-refer={`uris-uri-${index}`}
+                                error={!!errors.uris?.[index]?.uri || 
!!errors.uris?.message}
+                                helperText={errors.uris?.[index]?.uri?.message 
|| errors.uris?.message}
+                                fullWidth
+                              />
+                            )}
+                          />
+                        </Box>
+                        {!defaultUriProps && urisItems.length > 1 && 
urisItems[0].name && urisItems[0].uri && (
+                          <Box>
+                            <Controller
+                              name={`uris.${index}.defaultUri`}
+                              control={control}
+                              render={({ field: { value, onChange } }) => (
+                                <Tooltip title='Default URI' placement='top'>
+                                  <Switch
+                                    checked={value}
+                                    onChange={event => onChangeDefaultUri({ 
index, event })}
+                                    disabled={type === 'update'}
+                                    size='small'
+                                  />
+                                </Tooltip>
+                              )}
+                            />
+                          </Box>
+                        )}
+                        <Box>
+                          {index === 0 ? (
+                            <Box sx={{ minWidth: 40 }}>
+                              <IconButton
+                                sx={{ cursor: 'pointer' }}
+                                onClick={() => {
+                                  appendUri({ name: '', uri: '' })
+                                }}
+                              >
+                                <Icon icon='mdi:plus-circle-outline' />
+                              </IconButton>
+                            </Box>
+                          ) : (
+                            <Box sx={{ minWidth: 40 }}>
+                              <IconButton
+                                sx={{ cursor: 'pointer' }}
+                                onClick={() => {
+                                  removeUri(index)
+                                }}
+                              >
+                                <Icon icon='mdi:minus-circle-outline' />
+                              </IconButton>
+                            </Box>
+                          )}
+                        </Box>
+                      </Box>
+                    </FormControl>
+                  </Grid>
+                )
+              })}
+              {errors.uris && <FormHelperText sx={{ color: 'error.main' 
}}>{errors.uris.message}</FormHelperText>}
             </Grid>
 
             <Grid item xs={12}>
@@ -345,7 +489,6 @@ const LinkVersionDialog = props => {
                                   field.onChange(event)
                                   trigger('aliases')
                                 }}
-                                disabled={type === 'update'}
                                 label={`Alias ${index + 1}`}
                                 error={!!errors.aliases?.[index]?.name || 
!!errors.aliases?.message}
                                 
helperText={errors.aliases?.[index]?.name?.message || errors.aliases?.message}
@@ -360,7 +503,6 @@ const LinkVersionDialog = props => {
                               <IconButton
                                 sx={{ cursor: type === 'update' ? 
'not-allowed' : 'pointer' }}
                                 onClick={() => {
-                                  if (type === 'update') return
                                   append({ name: '' })
                                 }}
                               >
@@ -372,7 +514,6 @@ const LinkVersionDialog = props => {
                               <IconButton
                                 sx={{ cursor: type === 'update' ? 
'not-allowed' : 'pointer' }}
                                 onClick={() => {
-                                  if (type === 'update') return
                                   remove(index)
                                 }}
                               >
diff --git 
a/web/web/src/app/metalakes/metalake/rightContent/tabsContent/detailsView/DetailsView.js
 
b/web/web/src/app/metalakes/metalake/rightContent/tabsContent/detailsView/DetailsView.js
index 30a0456dc3..40133fef65 100644
--- 
a/web/web/src/app/metalakes/metalake/rightContent/tabsContent/detailsView/DetailsView.js
+++ 
b/web/web/src/app/metalakes/metalake/rightContent/tabsContent/detailsView/DetailsView.js
@@ -184,6 +184,57 @@ const DetailsView = () => {
           </Grid>
         )}
 
+        {activatedItem?.uris && (
+          <Grid item xs={12} sx={{ mb: [0, 5] }}>
+            <Typography variant='body2' sx={{ mb: 2 }}>
+              URI(s)
+            </Typography>
+
+            <TableContainer>
+              <Table>
+                <TableHead
+                  sx={{
+                    backgroundColor: theme => theme.palette.action.hover
+                  }}
+                >
+                  <TableRow>
+                    <TableCell sx={{ py: 2 }}>Name</TableCell>
+                    <TableCell sx={{ py: 2 }}>URI</TableCell>
+                  </TableRow>
+                </TableHead>
+                <TableBody data-refer='details-uris-table'>
+                  {Object.keys(activatedItem?.uris).map((name, index) => {
+                    return (
+                      <TableRow key={index} 
data-refer={`details-uris-index-${index}`}>
+                        <TableCell
+                          className={'twc-py-[0.7rem] twc-truncate 
twc-max-w-[134px]'}
+                          data-refer={`uris-name-${name}`}
+                        >
+                          <Tooltip title={<span 
data-refer={`tip-uris-name-${name}`}>{name}</span>} placement='bottom'>
+                            {name}
+                          </Tooltip>
+                        </TableCell>
+                        <TableCell
+                          className={'twc-py-[0.7rem] twc-truncate 
twc-max-w-[134px]'}
+                          
data-refer={`uris-location-${activatedItem?.uris[name]}`}
+                          data-prev-refer={`uris-name-${name}`}
+                        >
+                          <Tooltip
+                            title={<span 
data-prev-refer={`uris-name-${name}`}>{activatedItem?.uris[name]}</span>}
+                            placement='bottom'
+                          >
+                            {activatedItem?.uris[name]}
+                          </Tooltip>
+                        </TableCell>
+                      </TableRow>
+                    )
+                  })}
+                </TableBody>
+              </Table>
+            </TableContainer>
+          </Grid>
+        )}
+
         {activatedItem?.aliases && (
           <Grid item xs={12} md={6} sx={{ mb: [0, 5] }}>
             <Typography variant='body2' sx={{ mb: 2 }}>
diff --git a/web/web/src/components/DetailsDrawer.js 
b/web/web/src/components/DetailsDrawer.js
index 3228261794..b6fc3b0107 100644
--- a/web/web/src/components/DetailsDrawer.js
+++ b/web/web/src/components/DetailsDrawer.js
@@ -134,6 +134,57 @@ const DetailsDrawer = props => {
           </Grid>
         )}
 
+        {drawerData.uris && (
+          <Grid item xs={12} sx={{ mb: [0, 5] }}>
+            <Typography variant='body2' sx={{ mb: 2 }}>
+              URI(s)
+            </Typography>
+
+            <TableContainer>
+              <Table>
+                <TableHead
+                  sx={{
+                    backgroundColor: theme => theme.palette.action.hover
+                  }}
+                >
+                  <TableRow>
+                    <TableCell sx={{ py: 2 }}>Name</TableCell>
+                    <TableCell sx={{ py: 2 }}>URI</TableCell>
+                  </TableRow>
+                </TableHead>
+                <TableBody data-refer='details-props-table'>
+                  {Object.keys(drawerData.uris).map((name, index) => {
+                    return (
+                      <TableRow key={index} 
data-refer={`details-props-index-${index}`}>
+                        <TableCell
+                          className={'twc-py-[0.7rem] twc-truncate 
twc-max-w-[134px]'}
+                          data-refer={`uris-name-${name}`}
+                        >
+                          <Tooltip title={<span 
data-refer={`tip-uris-name-${name}`}>{name}</span>} placement='bottom'>
+                            {name}
+                          </Tooltip>
+                        </TableCell>
+                        <TableCell
+                          className={'twc-py-[0.7rem] twc-truncate 
twc-max-w-[134px]'}
+                          data-refer={`uris-uri-${drawerData.uris[name]}`}
+                          data-prev-refer={`uris-name-${name}`}
+                        >
+                          <Tooltip
+                            title={<span 
data-prev-refer={`uris-uri-${name}`}>{drawerData.uris[name]}</span>}
+                            placement='bottom'
+                          >
+                            {drawerData.uris[name]}
+                          </Tooltip>
+                        </TableCell>
+                      </TableRow>
+                    )
+                  })}
+                </TableBody>
+              </Table>
+            </TableContainer>
+          </Grid>
+        )}
+
         {drawerData.aliases && (
           <Grid item xs={12} md={6} sx={{ mb: [0, 5] }}>
             <Typography variant='body2' sx={{ mb: 2 }}>
diff --git a/web/web/src/lib/utils/index.js b/web/web/src/lib/utils/index.js
index 2755a8346c..d08579fa5e 100644
--- a/web/web/src/lib/utils/index.js
+++ b/web/web/src/lib/utils/index.js
@@ -63,6 +63,39 @@ export const genUpdates = (originalData, newData) => {
     updates.push({ '@type': 'updateUri', newUri: newData.uri })
   }
 
+  const originalUris = originalData.uris || {}
+  const newUris = newData.uris || {}
+
+  for (const key in originalUris) {
+    if (!(key in newUris)) {
+      updates.push({ '@type': 'removeUri', uriName: key })
+    }
+  }
+
+  for (const key in newUris) {
+    if (originalUris[key] !== newUris[key]) {
+      if (originalUris[key] === undefined) {
+        updates.push({ '@type': 'addUri', uriName: key, uri: newUris[key] })
+      } else {
+        updates.push({ '@type': 'updateUri', uriName: key, newUri: 
newUris[key] })
+      }
+    }
+  }
+
+  const originalAliases = originalData.aliases || []
+  const newAliases = newData.aliases || []
+
+  const aliasesToAdd = newAliases.filter(alias => 
!originalAliases.includes(alias))
+  const aliasesToRemove = originalAliases.filter(alias => 
!newAliases.includes(alias))
+
+  if (aliasesToAdd.length > 0 || aliasesToRemove.length > 0) {
+    updates.push({
+      '@type': 'updateAliases',
+      aliasesToAdd: aliasesToAdd,
+      aliasesToRemove: aliasesToRemove
+    })
+  }
+
   if (originalData.comment !== newData.comment) {
     updates.push({ '@type': 'updateComment', newComment: newData.comment })
   }

Reply via email to