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 })
}