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 92d9fc473 [#5168][#5169][#5170] Feature(web): Add ui support for
creating, editing, viewing, and deleting fileset (#5223)
92d9fc473 is described below
commit 92d9fc473fc9c1bd26085014e395f771f164b4f0
Author: Qian Xia <[email protected]>
AuthorDate: Thu Oct 24 17:09:07 2024 +0800
[#5168][#5169][#5170] Feature(web): Add ui support for creating, editing,
viewing, and deleting fileset (#5223)
### What changes were proposed in this pull request?
Add ui support for creating, editing, viewing, and deleting fileset
<img width="1296" alt="image"
src="https://github.com/user-attachments/assets/57b76a6f-2655-40da-b779-b2e6f32ef7ba">
### Why are the changes needed?
N/A
Fix: #5168
Fix: #5169
Fix: #5170
### Does this PR introduce _any_ user-facing change?
N/A
### How was this patch tested?
manually
---
.../metalake/rightContent/CreateFilesetDialog.js | 510 +++++++++++++++++++++
.../metalake/rightContent/RightContent.js | 39 +-
.../tabsContent/tableView/TableView.js | 50 +-
web/web/src/lib/api/filesets/index.js | 20 +-
web/web/src/lib/store/metalakes/index.js | 69 ++-
5 files changed, 682 insertions(+), 6 deletions(-)
diff --git
a/web/web/src/app/metalakes/metalake/rightContent/CreateFilesetDialog.js
b/web/web/src/app/metalakes/metalake/rightContent/CreateFilesetDialog.js
new file mode 100644
index 000000000..cb1901545
--- /dev/null
+++ b/web/web/src/app/metalakes/metalake/rightContent/CreateFilesetDialog.js
@@ -0,0 +1,510 @@
+/*
+ * 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.
+ */
+
+'use client'
+
+import { useState, forwardRef, useEffect, Fragment } from 'react'
+
+import {
+ Box,
+ Grid,
+ Button,
+ Dialog,
+ TextField,
+ Typography,
+ DialogContent,
+ DialogActions,
+ IconButton,
+ Fade,
+ Select,
+ MenuItem,
+ InputLabel,
+ FormControl,
+ FormHelperText
+} from '@mui/material'
+
+import Icon from '@/components/Icon'
+
+import { useAppDispatch } from '@/lib/hooks/useStore'
+import { createFileset, updateFileset } from '@/lib/store/metalakes'
+
+import * as yup from 'yup'
+import { useForm, Controller } from 'react-hook-form'
+import { yupResolver } from '@hookform/resolvers/yup'
+
+import { groupBy } from 'lodash-es'
+import { genUpdates } from '@/lib/utils'
+import { nameRegex, nameRegexDesc, keyRegex } from '@/lib/utils/regex'
+import { useSearchParams } from 'next/navigation'
+
+const defaultValues = {
+ name: '',
+ type: 'managed',
+ storageLocation: '',
+ comment: '',
+ propItems: []
+}
+
+const schema = yup.object().shape({
+ name: yup.string().required().matches(nameRegex, nameRegexDesc),
+ type: yup.mixed().oneOf(['managed', 'external']).required(),
+ storageLocation: yup.string().when('type', {
+ is: 'external',
+ then: schema => schema.required(),
+ otherwise: schema => schema
+ }),
+ 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()
+ })
+ })
+ )
+})
+
+const Transition = forwardRef(function Transition(props, ref) {
+ return <Fade ref={ref} {...props} />
+})
+
+const CreateFilesetDialog = props => {
+ const { open, setOpen, type = 'create', data = {} } = props
+ const searchParams = useSearchParams()
+ const metalake = searchParams.get('metalake')
+ const catalog = searchParams.get('catalog')
+ const catalogType = searchParams.get('type')
+ const schemaName = searchParams.get('schema')
+ const [innerProps, setInnerProps] = useState([])
+ const dispatch = useAppDispatch()
+ const [cacheData, setCacheData] = useState()
+
+ const {
+ control,
+ reset,
+ watch,
+ setValue,
+ getValues,
+ handleSubmit,
+ trigger,
+ formState: { errors }
+ } = useForm({
+ defaultValues,
+ mode: 'all',
+ resolver: yupResolver(schema)
+ })
+
+ const handleFormChange = ({ index, event }) => {
+ let data = [...innerProps]
+ data[index][event.target.name] = event.target.value
+
+ if (event.target.name === 'key') {
+ const invalidKey = !keyRegex.test(event.target.value)
+ data[index].invalid = invalidKey
+ }
+
+ const nonEmptyKeys = data.filter(item => item.key.trim() !== '')
+ const grouped = groupBy(nonEmptyKeys, 'key')
+ const duplicateKeys = Object.keys(grouped).some(key => grouped[key].length
> 1)
+
+ if (duplicateKeys) {
+ data[index].hasDuplicateKey = duplicateKeys
+ } else {
+ data.forEach(it => (it.hasDuplicateKey = false))
+ }
+
+ setInnerProps(data)
+ setValue('propItems', data)
+ }
+
+ const addFields = () => {
+ const duplicateKeys = innerProps
+ .filter(item => item.key.trim() !== '')
+ .some(
+ (item, index, filteredItems) =>
+ filteredItems.findIndex(otherItem => otherItem !== item &&
otherItem.key.trim() === item.key.trim()) !== -1
+ )
+
+ if (duplicateKeys) {
+ return
+ }
+
+ let newField = { key: '', value: '', required: false }
+
+ setInnerProps([...innerProps, newField])
+ setValue('propItems', [...innerProps, newField])
+ }
+
+ const removeFields = index => {
+ let data = [...innerProps]
+ data.splice(index, 1)
+ setInnerProps(data)
+ setValue('propItems', data)
+ }
+
+ const handleClose = () => {
+ reset()
+ setInnerProps([])
+ setValue('propItems', [])
+ setOpen(false)
+ }
+
+ const handleClickSubmit = e => {
+ e.preventDefault()
+
+ return handleSubmit(onSubmit(getValues()), onError)
+ }
+
+ const onSubmit = data => {
+ const duplicateKeys = innerProps
+ .filter(item => item.key.trim() !== '')
+ .some(
+ (item, index, filteredItems) =>
+ filteredItems.findIndex(otherItem => otherItem !== item &&
otherItem.key.trim() === item.key.trim()) !== -1
+ )
+
+ const invalidKeys = innerProps.some(i => i.invalid)
+
+ if (duplicateKeys || invalidKeys) {
+ return
+ }
+
+ trigger()
+
+ schema
+ .validate(data)
+ .then(() => {
+ const properties = innerProps.reduce((acc, item) => {
+ acc[item.key] = item.value
+
+ return acc
+ }, {})
+
+ const filesetData = {
+ name: data.name,
+ type: data.type,
+ storageLocation: data.storageLocation,
+ comment: data.comment,
+ properties
+ }
+
+ if (type === 'create') {
+ dispatch(createFileset({ data: filesetData, metalake, catalog, type:
catalogType, schema: schemaName })).then(
+ res => {
+ if (!res.payload?.err) {
+ handleClose()
+ }
+ }
+ )
+ } else {
+ const reqData = { updates: genUpdates(cacheData, filesetData) }
+
+ if (reqData.updates.length !== 0) {
+ dispatch(
+ updateFileset({
+ metalake,
+ catalog,
+ type: catalogType,
+ schema: schemaName,
+ fileset: cacheData.name,
+ data: reqData
+ })
+ ).then(res => {
+ if (!res.payload?.err) {
+ handleClose()
+ }
+ })
+ }
+ }
+ })
+ .catch(err => {
+ console.error('valid error', err)
+ })
+ }
+
+ const onError = errors => {
+ console.error('fields error', errors)
+ }
+
+ useEffect(() => {
+ if (open && JSON.stringify(data) !== '{}') {
+ const { properties = {} } = data
+
+ setCacheData(data)
+ setValue('name', data.name)
+ setValue('type', data.type)
+ setValue('storageLocation', data.storageLocation)
+ setValue('comment', data.comment)
+
+ const propsItems = Object.entries(properties).map(([key, value]) => {
+ return {
+ key,
+ value
+ }
+ })
+
+ setInnerProps(propsItems)
+ setValue('propItems', propsItems)
+ }
+ }, [open, data, setValue, type])
+
+ return (
+ <Dialog fullWidth maxWidth='sm' scroll='body'
TransitionComponent={Transition} open={open} onClose={handleClose}>
+ <form onSubmit={e => handleClickSubmit(e)}>
+ <DialogContent
+ sx={{
+ position: 'relative',
+ pb: theme => `${theme.spacing(8)} !important`,
+ px: theme => [`${theme.spacing(5)} !important`,
`${theme.spacing(15)} !important`],
+ pt: theme => [`${theme.spacing(8)} !important`,
`${theme.spacing(12.5)} !important`]
+ }}
+ >
+ <IconButton
+ size='small'
+ onClick={() => handleClose()}
+ sx={{ position: 'absolute', right: '1rem', top: '1rem' }}
+ >
+ <Icon icon='bx:x' />
+ </IconButton>
+ <Box sx={{ mb: 8, textAlign: 'center' }}>
+ <Typography variant='h5' sx={{ mb: 3 }}>
+ {type === 'create' ? 'Create' : 'Edit'} Fileset
+ </Typography>
+ </Box>
+
+ <Grid container spacing={6}>
+ <Grid item xs={12}>
+ <FormControl fullWidth>
+ <Controller
+ name='name'
+ control={control}
+ rules={{ required: true }}
+ render={({ field: { value, onChange } }) => (
+ <TextField
+ value={value}
+ label='Name'
+ onChange={onChange}
+ placeholder=''
+ error={Boolean(errors.name)}
+ data-refer='fileset-name-field'
+ />
+ )}
+ />
+ {errors.name && <FormHelperText sx={{ color: 'error.main'
}}>{errors.name.message}</FormHelperText>}
+ </FormControl>
+ </Grid>
+
+ <Grid item xs={12}>
+ <FormControl fullWidth>
+ <InputLabel id='select-fileset-type'
error={Boolean(errors.type)}>
+ Type
+ </InputLabel>
+ <Controller
+ name='type'
+ control={control}
+ rules={{ required: true }}
+ render={({ field: { value, onChange } }) => (
+ <Select
+ value={value}
+ label='Type'
+ defaultValue='managed'
+ onChange={onChange}
+ disabled={type === 'update'}
+ error={Boolean(errors.type)}
+ labelId='select-fileset-type'
+ data-refer='fileset-type-selector'
+ >
+ <MenuItem value={'managed'}>Managed</MenuItem>
+ <MenuItem value={'external'}>External</MenuItem>
+ </Select>
+ )}
+ />
+ {errors.type && <FormHelperText sx={{ color: 'error.main'
}}>{errors.type.message}</FormHelperText>}
+ </FormControl>
+ </Grid>
+
+ <Grid item xs={12}>
+ <FormControl fullWidth>
+ <Controller
+ name='storageLocation'
+ control={control}
+ rules={{ required: true }}
+ render={({ field: { value, onChange } }) => (
+ <TextField
+ value={value}
+ label='Storage Location'
+ onChange={onChange}
+ disabled={type === 'update'}
+ placeholder=''
+ error={Boolean(errors.storageLocation)}
+ data-refer='fileset-storageLocation-field'
+ />
+ )}
+ />
+ {errors.storageLocation ? (
+ <FormHelperText sx={{ color: 'error.main'
}}>{errors.storageLocation.message}</FormHelperText>
+ ) : (
+ <>
+ <FormHelperText sx={{ color: 'text.main' }}>
+ It is optional if the fileset is 'Managed' type and a
storage location is already specified at the
+ parent catalog or schema level.
+ </FormHelperText>
+ <FormHelperText sx={{ color: 'text.main' }}>
+ It becomes mandatory if the fileset type is 'External'
or no storage location is defined at the
+ parent level.
+ </FormHelperText>
+ </>
+ )}
+ </FormControl>
+ </Grid>
+
+ <Grid item xs={12}>
+ <FormControl fullWidth>
+ <Controller
+ name='comment'
+ control={control}
+ rules={{ required: false }}
+ render={({ field: { value, onChange } }) => (
+ <TextField
+ value={value}
+ label='Comment'
+ multiline
+ rows={2}
+ onChange={onChange}
+ placeholder=''
+ error={Boolean(errors.comment)}
+ data-refer='fileset-comment-field'
+ />
+ )}
+ />
+ </FormControl>
+ </Grid>
+
+ <Grid item xs={12} data-refer='fileset-props-layout'>
+ <Typography sx={{ mb: 2 }} variant='body2'>
+ Properties
+ </Typography>
+ {innerProps.map((item, index) => {
+ return (
+ <Fragment key={index}>
+ <Grid item xs={12} sx={{ '& + &': { mt: 2 } }}>
+ <FormControl fullWidth>
+ <Box>
+ <Box
+ sx={{ display: 'flex', alignItems: 'center',
justifyContent: 'space-between' }}
+ data-refer={`fileset-props-${index}`}
+ >
+ <Box>
+ <TextField
+ size='small'
+ name='key'
+ label='Key'
+ value={item.key}
+ disabled={item.disabled || (item.key ===
'location' && type === 'update')}
+ onChange={event => handleFormChange({ index,
event })}
+ error={item.hasDuplicateKey || item.invalid ||
!item.key.trim()}
+ data-refer={`props-key-${index}`}
+ />
+ </Box>
+ <Box>
+ <TextField
+ size='small'
+ name='value'
+ label='Value'
+ error={item.required && item.value === ''}
+ value={item.value}
+ disabled={item.disabled || (item.key ===
'location' && type === 'update')}
+ onChange={event => handleFormChange({ index,
event })}
+ data-refer={`props-value-${index}`}
+ data-prev-refer={`props-${item.key}`}
+ />
+ </Box>
+
+ {!(item.disabled || (item.key === 'location' &&
type === 'update')) ? (
+ <Box sx={{ minWidth: 40 }}>
+ <IconButton onClick={() =>
removeFields(index)}>
+ <Icon icon='mdi:minus-circle-outline' />
+ </IconButton>
+ </Box>
+ ) : (
+ <Box sx={{ minWidth: 40 }}></Box>
+ )}
+ </Box>
+ </Box>
+ <FormHelperText
+ sx={{
+ color: item.required && item.value === '' ?
'error.main' : 'text.main',
+ maxWidth: 'calc(100% - 40px)'
+ }}
+ >
+ {item.description}
+ </FormHelperText>
+ {item.hasDuplicateKey && (
+ <FormHelperText
className={'twc-text-error-main'}>Key already exists</FormHelperText>
+ )}
+ {item.key && item.invalid && (
+ <FormHelperText className={'twc-text-error-main'}>
+ Invalid key, matches strings starting with a
letter/underscore, followed by alphanumeric
+ characters, underscores, hyphens, or dots.
+ </FormHelperText>
+ )}
+ {!item.key.trim() && (
+ <FormHelperText
className={'twc-text-error-main'}>Key is required field</FormHelperText>
+ )}
+ </FormControl>
+ </Grid>
+ </Fragment>
+ )
+ })}
+ </Grid>
+
+ <Grid item xs={12}>
+ <Button
+ size='small'
+ onClick={addFields}
+ variant='outlined'
+ startIcon={<Icon icon='mdi:plus-circle-outline' />}
+ data-refer='add-fileset-props'
+ >
+ Add Property
+ </Button>
+ </Grid>
+ </Grid>
+ </DialogContent>
+ <DialogActions
+ sx={{
+ justifyContent: 'center',
+ px: theme => [`${theme.spacing(5)} !important`,
`${theme.spacing(15)} !important`],
+ pb: theme => [`${theme.spacing(5)} !important`,
`${theme.spacing(12.5)} !important`]
+ }}
+ >
+ <Button variant='contained' sx={{ mr: 1 }} type='submit'
data-refer='handle-submit-fileset'>
+ {type === 'create' ? 'Create' : 'Update'}
+ </Button>
+ <Button variant='outlined' onClick={handleClose}>
+ Cancel
+ </Button>
+ </DialogActions>
+ </form>
+ </Dialog>
+ )
+}
+
+export default CreateFilesetDialog
diff --git a/web/web/src/app/metalakes/metalake/rightContent/RightContent.js
b/web/web/src/app/metalakes/metalake/rightContent/RightContent.js
index 1706399dd..4dfd091a4 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/RightContent.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/RightContent.js
@@ -26,6 +26,7 @@ import Icon from '@/components/Icon'
import MetalakePath from './MetalakePath'
import CreateCatalogDialog from './CreateCatalogDialog'
import CreateSchemaDialog from './CreateSchemaDialog'
+import CreateFilesetDialog from './CreateFilesetDialog'
import TabsContent from './tabsContent/TabsContent'
import { useSearchParams } from 'next/navigation'
import { useAppSelector } from '@/lib/hooks/useStore'
@@ -33,9 +34,11 @@ import { useAppSelector } from '@/lib/hooks/useStore'
const RightContent = () => {
const [open, setOpen] = useState(false)
const [openSchema, setOpenSchema] = useState(false)
+ const [openFileset, setOpenFileset] = useState(false)
const searchParams = useSearchParams()
const [isShowBtn, setBtnVisible] = useState(true)
const [isShowSchemaBtn, setSchemaBtnVisible] = useState(false)
+ const [isShowFilesetBtn, setFilesetBtnVisible] = useState(false)
const store = useAppSelector(state => state.metalakes)
const handleCreateCatalog = () => {
@@ -46,15 +49,33 @@ const RightContent = () => {
setOpenSchema(true)
}
+ const handleCreateFileset = () => {
+ setOpenFileset(true)
+ }
+
useEffect(() => {
const paramsSize = [...searchParams.keys()].length
const isCatalogList = paramsSize == 1 && searchParams.get('metalake')
setBtnVisible(isCatalogList)
+ const isFilesetList =
+ paramsSize == 4 &&
+ searchParams.has('metalake') &&
+ searchParams.has('catalog') &&
+ searchParams.get('type') === 'fileset'
+ searchParams.has('schema')
+ setFilesetBtnVisible(isFilesetList)
+
if (store.catalogs.length) {
const currentCatalog = store.catalogs.filter(ca => ca.name ===
searchParams.get('catalog'))[0]
- const isHideSchemaAction = ['lakehouse-hudi',
'kafka'].includes(currentCatalog?.provider) && paramsSize == 3
- setSchemaBtnVisible(!isHideSchemaAction && !isCatalogList)
+
+ const isSchemaList =
+ paramsSize == 3 &&
+ searchParams.has('metalake') &&
+ searchParams.has('catalog') &&
+ searchParams.has('type') &&
+ !['lakehouse-hudi', 'kafka'].includes(currentCatalog?.provider)
+ setSchemaBtnVisible(isSchemaList)
}
}, [searchParams, store.catalogs, store.catalogs.length])
@@ -105,6 +126,20 @@ const RightContent = () => {
<CreateSchemaDialog open={openSchema} setOpen={setOpenSchema} />
</Box>
)}
+ {isShowFilesetBtn && (
+ <Box className={`twc-flex twc-items-center`}>
+ <Button
+ variant='contained'
+ startIcon={<Icon icon='mdi:plus-box' />}
+ onClick={handleCreateFileset}
+ sx={{ width: 200 }}
+ data-refer='create-schema-btn'
+ >
+ Create Fileset
+ </Button>
+ <CreateFilesetDialog open={openFileset} setOpen={setOpenFileset} />
+ </Box>
+ )}
</Box>
<Box sx={{ height: 'calc(100% - 4.1rem)' }}>
diff --git
a/web/web/src/app/metalakes/metalake/rightContent/tabsContent/tableView/TableView.js
b/web/web/src/app/metalakes/metalake/rightContent/tabsContent/tableView/TableView.js
index cdc94c776..cf8cc3baf 100644
---
a/web/web/src/app/metalakes/metalake/rightContent/tabsContent/tableView/TableView.js
+++
b/web/web/src/app/metalakes/metalake/rightContent/tabsContent/tableView/TableView.js
@@ -41,14 +41,16 @@ import DetailsDrawer from '@/components/DetailsDrawer'
import ConfirmDeleteDialog from '@/components/ConfirmDeleteDialog'
import CreateCatalogDialog from '../../CreateCatalogDialog'
import CreateSchemaDialog from '../../CreateSchemaDialog'
+import CreateFilesetDialog from '../../CreateFilesetDialog'
import { useAppSelector, useAppDispatch } from '@/lib/hooks/useStore'
-import { deleteCatalog, deleteSchema } from '@/lib/store/metalakes'
+import { deleteCatalog, deleteFileset, deleteSchema } from
'@/lib/store/metalakes'
import { to } from '@/lib/utils'
import { getCatalogDetailsApi } from '@/lib/api/catalogs'
import { getSchemaDetailsApi } from '@/lib/api/schemas'
import { useSearchParams } from 'next/navigation'
+import { getFilesetDetailsApi } from '@/lib/api/filesets'
const fonts = Inconsolata({ subsets: ['latin'] })
@@ -76,6 +78,13 @@ const TableView = () => {
const metalake = searchParams.get('metalake') || ''
const catalog = searchParams.get('catalog') || ''
const type = searchParams.get('type') || ''
+ const schema = searchParams.get('schema') || ''
+
+ const isKafkaSchema =
+ paramsSize == 3 &&
+ searchParams.has('metalake') &&
+ searchParams.has('catalog') &&
+ searchParams.get('type') === 'messaging'
const defaultPaginationConfig = { pageSize: 10, page: 0 }
const pageSizeOptions = [10, 25, 50]
@@ -91,6 +100,7 @@ const TableView = () => {
const [openConfirmDelete, setOpenConfirmDelete] = useState(false)
const [openDialog, setOpenDialog] = useState(false)
const [openSchemaDialog, setOpenSchemaDialog] = useState(false)
+ const [openFilesetDialog, setOpenFilesetDialog] = useState(false)
const [dialogData, setDialogData] = useState({})
const [dialogType, setDialogType] = useState('create')
const [isHideSchemaEdit, setIsHideSchemaEdit] = useState(true)
@@ -463,6 +473,15 @@ const TableView = () => {
setOpenDrawer(true)
break
}
+ case 'fileset': {
+ const [err, res] = await to(getFilesetDetailsApi({ metalake, catalog,
schema, fileset: row.name }))
+ if (err || !res) {
+ throw new Error(err)
+ }
+
+ setDrawerData(res.fileset)
+ setOpenDrawer(true)
+ }
default:
return
}
@@ -498,6 +517,18 @@ const TableView = () => {
}
break
}
+ case 'fileset': {
+ if (metalake && catalog && schema) {
+ const [err, res] = await to(getFilesetDetailsApi({ metalake,
catalog, schema, fileset: data.row?.name }))
+ if (err || !res) {
+ throw new Error(err)
+ }
+
+ setDialogType('update')
+ setDialogData(res.fileset)
+ setOpenFilesetDialog(true)
+ }
+ }
default:
return
}
@@ -522,6 +553,9 @@ const TableView = () => {
case 'schema':
dispatch(deleteSchema({ metalake, catalog, type, schema:
confirmCacheData.name }))
break
+ case 'fileset':
+ dispatch(deleteFileset({ metalake, catalog, type, schema, fileset:
confirmCacheData.name }))
+ break
default:
break
}
@@ -533,7 +567,12 @@ const TableView = () => {
const checkColumns = () => {
if (
(paramsSize == 1 && searchParams.has('metalake')) ||
- (paramsSize == 3 && searchParams.has('metalake') &&
searchParams.has('catalog') && searchParams.has('type'))
+ (paramsSize == 3 && searchParams.has('metalake') &&
searchParams.has('catalog') && searchParams.has('type')) ||
+ (paramsSize == 4 &&
+ searchParams.has('metalake') &&
+ searchParams.has('catalog') &&
+ searchParams.get('type') === 'fileset' &&
+ searchParams.has('schema'))
) {
return actionsColumns
} else if (paramsSize == 5 && searchParams.has('table')) {
@@ -580,6 +619,13 @@ const TableView = () => {
<CreateCatalogDialog open={openDialog} setOpen={setOpenDialog}
data={dialogData} type={dialogType} />
<CreateSchemaDialog open={openSchemaDialog}
setOpen={setOpenSchemaDialog} data={dialogData} type={dialogType} />
+
+ <CreateFilesetDialog
+ open={openFilesetDialog}
+ setOpen={setOpenFilesetDialog}
+ data={dialogData}
+ type={dialogType}
+ />
</Box>
)
}
diff --git a/web/web/src/lib/api/filesets/index.js
b/web/web/src/lib/api/filesets/index.js
index 81f05488f..bae492a11 100644
--- a/web/web/src/lib/api/filesets/index.js
+++ b/web/web/src/lib/api/filesets/index.js
@@ -27,7 +27,13 @@ const Apis = {
GET_DETAIL: ({ metalake, catalog, schema, fileset }) =>
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(
catalog
-
)}/schemas/${encodeURIComponent(schema)}/filesets/${encodeURIComponent(fileset)}`
+
)}/schemas/${encodeURIComponent(schema)}/filesets/${encodeURIComponent(fileset)}`,
+ CREATE: ({ metalake, catalog, schema }) =>
+
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(catalog)}/schemas/${encodeURIComponent(schema)}/filesets`,
+ UPDATE: ({ metalake, catalog, schema, fileset }) =>
+
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(catalog)}/schemas/${encodeURIComponent(schema)}/filesets/${encodeURIComponent(fileset)}`,
+ DELETE: ({ metalake, catalog, schema, fileset }) =>
+
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(catalog)}/schemas/${encodeURIComponent(schema)}/filesets/${encodeURIComponent(fileset)}`
}
export const getFilesetsApi = params => {
@@ -41,3 +47,15 @@ export const getFilesetDetailsApi = ({ metalake, catalog,
schema, fileset }) =>
url: `${Apis.GET_DETAIL({ metalake, catalog, schema, fileset })}`
})
}
+
+export const createFilesetApi = ({ metalake, catalog, schema, data }) => {
+ return defHttp.post({ url: `${Apis.CREATE({ metalake, catalog, schema })}`,
data })
+}
+
+export const updateFilesetApi = ({ metalake, catalog, schema, fileset, data })
=> {
+ return defHttp.put({ url: `${Apis.UPDATE({ metalake, catalog, schema,
fileset })}`, data })
+}
+
+export const deleteFilesetApi = ({ metalake, catalog, schema, fileset }) => {
+ return defHttp.delete({ url: `${Apis.DELETE({ metalake, catalog, schema,
fileset })}` })
+}
diff --git a/web/web/src/lib/store/metalakes/index.js
b/web/web/src/lib/store/metalakes/index.js
index 7c58e80e4..445d2838d 100644
--- a/web/web/src/lib/store/metalakes/index.js
+++ b/web/web/src/lib/store/metalakes/index.js
@@ -47,7 +47,13 @@ import {
deleteSchemaApi
} from '@/lib/api/schemas'
import { getTablesApi, getTableDetailsApi } from '@/lib/api/tables'
-import { getFilesetsApi, getFilesetDetailsApi } from '@/lib/api/filesets'
+import {
+ getFilesetsApi,
+ getFilesetDetailsApi,
+ createFilesetApi,
+ updateFilesetApi,
+ deleteFilesetApi
+} from '@/lib/api/filesets'
import { getTopicsApi, getTopicDetailsApi } from '@/lib/api/topics'
export const fetchMetalakes = createAsyncThunk('appMetalakes/fetchMetalakes',
async (params, { getState }) => {
@@ -885,6 +891,67 @@ export const getFilesetDetails = createAsyncThunk(
}
)
+export const createFileset = createAsyncThunk(
+ 'appMetalakes/createFileset',
+ async ({ data, metalake, catalog, type, schema }, { dispatch }) => {
+ dispatch(setTableLoading(true))
+ const [err, res] = await to(createFilesetApi({ data, metalake, catalog,
schema }))
+ dispatch(setTableLoading(false))
+
+ if (err || !res) {
+ return { err: true }
+ }
+
+ const { fileset: filesetItem } = res
+
+ const filesetData = {
+ ...filesetItem,
+ node: 'fileset',
+ id:
`{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema}}}{{${filesetItem.name}}}`,
+ key:
`{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema}}}{{${filesetItem.name}}}`,
+ path: `?${new URLSearchParams({ metalake, catalog, type, schema,
fileset: filesetItem.name }).toString()}`,
+ name: filesetItem.name,
+ title: filesetItem.name,
+ tables: [],
+ children: []
+ }
+
+ dispatch(fetchFilesets({ metalake, catalog, schema, type, init: true }))
+
+ return filesetData
+ }
+)
+
+export const updateFileset = createAsyncThunk(
+ 'appMetalakes/updateFileset',
+ async ({ metalake, catalog, type, schema, fileset, data }, { dispatch }) => {
+ const [err, res] = await to(updateFilesetApi({ metalake, catalog, schema,
fileset, data }))
+ if (err || !res) {
+ return { err: true }
+ }
+ dispatch(fetchFilesets({ metalake, catalog, type, schema, init: true }))
+
+ return res.catalog
+ }
+)
+
+export const deleteFileset = createAsyncThunk(
+ 'appMetalakes/deleteFileset',
+ async ({ metalake, catalog, type, schema, fileset }, { dispatch }) => {
+ dispatch(setTableLoading(true))
+ const [err, res] = await to(deleteFilesetApi({ metalake, catalog, schema,
fileset }))
+ dispatch(setTableLoading(false))
+
+ if (err || !res) {
+ throw new Error(err)
+ }
+
+ dispatch(fetchFilesets({ metalake, catalog, type, schema, page: 'schemas',
init: true }))
+
+ return res
+ }
+)
+
export const fetchTopics = createAsyncThunk(
'appMetalakes/fetchTopics',
async ({ init, page, metalake, catalog, schema }, { getState, dispatch }) =>
{