From ab3e7cd328d30c8be5ff7fdb765e3ffaaa0a60e1 Mon Sep 17 00:00:00 2001 From: Qian Xia Date: Tue, 22 Oct 2024 18:34:18 +0800 Subject: [PATCH] [#5141][#5142][#5143] web(ui): Add support for creating, editing, and deleting schema (#5164) ### What changes were proposed in this pull request? Add support for creating, editing, and deleting schema image image image image image image ### Why are the changes needed? N/A Fix: #5141, #5142, #5143 ### Does this PR introduce _any_ user-facing change? N/A ### How was this patch tested? manually --- .../test/web/ui/pages/CatalogsPage.java | 6 +- .../rightContent/CreateSchemaDialog.js | 439 ++++++++++++++++++ .../metalake/rightContent/RightContent.js | 37 +- .../tabsContent/tableView/TableView.js | 163 ++++--- web/web/src/components/DetailsDrawer.js | 81 ++-- web/web/src/lib/api/schemas/index.js | 20 +- web/web/src/lib/icons/iconify-icons.css | 2 +- web/web/src/lib/icons/svg/hudi.svg | 2 +- web/web/src/lib/store/metalakes/index.js | 84 +++- 9 files changed, 730 insertions(+), 104 deletions(-) create mode 100644 web/web/src/app/metalakes/metalake/rightContent/CreateSchemaDialog.js diff --git a/web/integration-test/src/test/java/org/apache/gravitino/integration/test/web/ui/pages/CatalogsPage.java b/web/integration-test/src/test/java/org/apache/gravitino/integration/test/web/ui/pages/CatalogsPage.java index 222cbd22714..b397c26a7e4 100644 --- a/web/integration-test/src/test/java/org/apache/gravitino/integration/test/web/ui/pages/CatalogsPage.java +++ b/web/integration-test/src/test/java/org/apache/gravitino/integration/test/web/ui/pages/CatalogsPage.java @@ -186,7 +186,7 @@ public void setCatalogPropsAt(int index, String key, String value) { public void clickViewCatalogBtn(String name) { try { - String xpath = "//button[@data-refer='view-catalog-" + name + "']"; + String xpath = "//button[@data-refer='view-entity-" + name + "']"; WebElement btn = driver.findElement(By.xpath(xpath)); WebDriverWait wait = new WebDriverWait(driver, MAX_TIMEOUT); wait.until(ExpectedConditions.elementToBeClickable(By.xpath(xpath))); @@ -198,7 +198,7 @@ public void clickViewCatalogBtn(String name) { public void clickEditCatalogBtn(String name) { try { - String xpath = "//button[@data-refer='edit-catalog-" + name + "']"; + String xpath = "//button[@data-refer='edit-entity-" + name + "']"; WebElement btn = driver.findElement(By.xpath(xpath)); WebDriverWait wait = new WebDriverWait(driver, MAX_TIMEOUT); wait.until(ExpectedConditions.elementToBeClickable(By.xpath(xpath))); @@ -210,7 +210,7 @@ public void clickEditCatalogBtn(String name) { public void clickDeleteCatalogBtn(String name) { try { - String xpath = "//button[@data-refer='delete-catalog-" + name + "']"; + String xpath = "//button[@data-refer='delete-entity-" + name + "']"; WebElement btn = driver.findElement(By.xpath(xpath)); WebDriverWait wait = new WebDriverWait(driver, MAX_TIMEOUT); wait.until(ExpectedConditions.elementToBeClickable(By.xpath(xpath))); diff --git a/web/web/src/app/metalakes/metalake/rightContent/CreateSchemaDialog.js b/web/web/src/app/metalakes/metalake/rightContent/CreateSchemaDialog.js new file mode 100644 index 00000000000..9b90c0bb6d5 --- /dev/null +++ b/web/web/src/app/metalakes/metalake/rightContent/CreateSchemaDialog.js @@ -0,0 +1,439 @@ +/* + * 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 { createSchema, updateSchema } 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' +import { useAppSelector } from '@/lib/hooks/useStore' + +const defaultValues = { + name: '', + comment: '', + propItems: [] +} + +const schema = yup.object().shape({ + name: yup.string().required().matches(nameRegex, nameRegexDesc), + 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 +}) + +const CreateSchemaDialog = 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 [innerProps, setInnerProps] = useState([]) + const dispatch = useAppDispatch() + const store = useAppSelector(state => state.metalakes) + const activatedCatalogDetail = store.activatedDetails + 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 schemaData = { + name: data.name, + comment: data.comment, + properties + } + + if (type === 'create') { + dispatch(createSchema({ data: schemaData, metalake, catalog, type: catalogType })).then(res => { + if (!res.payload?.err) { + handleClose() + } + }) + } else { + const reqData = { updates: genUpdates(cacheData, schemaData) } + + if (reqData.updates.length !== 0) { + dispatch( + updateSchema({ metalake, catalog, type: catalogType, schema: 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('comment', data.comment) + + const propsItems = Object.entries(properties).map(([key, value]) => { + return { + key, + value + } + }) + + setInnerProps(propsItems) + setValue('propItems', propsItems) + } + }, [open, data, setValue, type]) + + return ( + +
handleClickSubmit(e)}> + `${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`] + }} + > + handleClose()} + sx={{ position: 'absolute', right: '1rem', top: '1rem' }} + > + + + + + {type === 'create' ? 'Create' : 'Edit'} Schema + + + + + + + ( + + )} + /> + {errors.name && {errors.name.message}} + + + + {!['jdbc-mysql', 'lakehouse-paimon'].includes(activatedCatalogDetail?.provider) && ( + + + ( + + )} + /> + + + )} + + {!['jdbc-postgresql', 'lakehouse-paimon', 'kafka', 'jdbc-mysql'].includes( + activatedCatalogDetail?.provider + ) && ( + + + Properties + + {innerProps.map((item, index) => { + return ( + + + + + + + handleFormChange({ index, event })} + error={item.hasDuplicateKey || item.invalid || !item.key.trim()} + data-refer={`props-key-${index}`} + /> + + + handleFormChange({ index, event })} + data-refer={`props-value-${index}`} + data-prev-refer={`props-${item.key}`} + /> + + + {!(item.disabled || (item.key === 'location' && type === 'update')) ? ( + + removeFields(index)}> + + + + ) : ( + + )} + + + + {item.description} + + {item.hasDuplicateKey && ( + Key already exists + )} + {item.key && item.invalid && ( + + Invalid key, matches strings starting with a letter/underscore, followed by alphanumeric + characters, underscores, hyphens, or dots. + + )} + {!item.key.trim() && ( + Key is required field + )} + + + + ) + })} + + )} + + {!['jdbc-postgresql', 'lakehouse-paimon', 'kafka', 'jdbc-mysql'].includes( + activatedCatalogDetail?.provider + ) && ( + + + + )} + + + [`${theme.spacing(5)} !important`, `${theme.spacing(15)} !important`], + pb: theme => [`${theme.spacing(5)} !important`, `${theme.spacing(12.5)} !important`] + }} + > + + + +
+
+ ) +} + +export default CreateSchemaDialog diff --git a/web/web/src/app/metalakes/metalake/rightContent/RightContent.js b/web/web/src/app/metalakes/metalake/rightContent/RightContent.js index 050f84ad558..1706399ddc2 100644 --- a/web/web/src/app/metalakes/metalake/rightContent/RightContent.js +++ b/web/web/src/app/metalakes/metalake/rightContent/RightContent.js @@ -25,23 +25,38 @@ import { Box, Button, IconButton } from '@mui/material' import Icon from '@/components/Icon' import MetalakePath from './MetalakePath' import CreateCatalogDialog from './CreateCatalogDialog' +import CreateSchemaDialog from './CreateSchemaDialog' import TabsContent from './tabsContent/TabsContent' import { useSearchParams } from 'next/navigation' +import { useAppSelector } from '@/lib/hooks/useStore' const RightContent = () => { const [open, setOpen] = useState(false) + const [openSchema, setOpenSchema] = useState(false) const searchParams = useSearchParams() - const [isShowBtn, setBtnVisiable] = useState(true) + const [isShowBtn, setBtnVisible] = useState(true) + const [isShowSchemaBtn, setSchemaBtnVisible] = useState(false) + const store = useAppSelector(state => state.metalakes) const handleCreateCatalog = () => { setOpen(true) } + const handleCreateSchema = () => { + setOpenSchema(true) + } + useEffect(() => { const paramsSize = [...searchParams.keys()].length - const isMetalakePage = paramsSize == 1 && searchParams.get('metalake') - setBtnVisiable(isMetalakePage) - }, [searchParams]) + const isCatalogList = paramsSize == 1 && searchParams.get('metalake') + setBtnVisible(isCatalogList) + + 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) + } + }, [searchParams, store.catalogs, store.catalogs.length]) return ( @@ -76,6 +91,20 @@ const RightContent = () => { )} + {isShowSchemaBtn && ( + + + + + )} 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 08a4e634902..cdc94c776df 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 @@ -40,12 +40,14 @@ import ColumnTypeChip from '@/components/ColumnTypeChip' import DetailsDrawer from '@/components/DetailsDrawer' import ConfirmDeleteDialog from '@/components/ConfirmDeleteDialog' import CreateCatalogDialog from '../../CreateCatalogDialog' +import CreateSchemaDialog from '../../CreateSchemaDialog' import { useAppSelector, useAppDispatch } from '@/lib/hooks/useStore' -import { updateCatalog, deleteCatalog } from '@/lib/store/metalakes' +import { deleteCatalog, 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' const fonts = Inconsolata({ subsets: ['latin'] }) @@ -72,6 +74,8 @@ const TableView = () => { const searchParams = useSearchParams() const paramsSize = [...searchParams.keys()].length const metalake = searchParams.get('metalake') || '' + const catalog = searchParams.get('catalog') || '' + const type = searchParams.get('type') || '' const defaultPaginationConfig = { pageSize: 10, page: 0 } const pageSizeOptions = [10, 25, 50] @@ -86,8 +90,18 @@ const TableView = () => { const [confirmCacheData, setConfirmCacheData] = useState(null) const [openConfirmDelete, setOpenConfirmDelete] = useState(false) const [openDialog, setOpenDialog] = useState(false) + const [openSchemaDialog, setOpenSchemaDialog] = useState(false) const [dialogData, setDialogData] = useState({}) const [dialogType, setDialogType] = useState('create') + const [isHideSchemaEdit, setIsHideSchemaEdit] = useState(true) + + useEffect(() => { + if (store.catalogs.length) { + const currentCatalog = store.catalogs.filter(ca => ca.name === catalog)[0] + const isHideSchemaAction = ['lakehouse-hudi', 'kafka'].includes(currentCatalog?.provider) && paramsSize == 3 + setIsHideSchemaEdit(isHideSchemaAction) + } + }, [store.catalogs, store.catalogs.length, paramsSize, catalog]) const handleClickUrl = path => { if (!path) { @@ -200,7 +214,7 @@ const TableView = () => { } ] - const catalogsColumns = [ + const actionsColumns = [ { flex: 0.1, minWidth: 60, @@ -251,31 +265,35 @@ const TableView = () => { title='Details' size='small' sx={{ color: theme => theme.palette.text.secondary }} - onClick={() => handleShowDetails({ row, type: 'catalog' })} - data-refer={`view-catalog-${row.name}`} + onClick={() => handleShowDetails({ row, type: row.node })} + data-refer={`view-entity-${row.name}`} > - theme.palette.text.secondary }} - onClick={() => handleShowEditDialog({ row, type: 'catalog' })} - data-refer={`edit-catalog-${row.name}`} - > - - - - theme.palette.error.light }} - onClick={() => handleDelete({ name: row.name, type: 'catalog', catalogType: row.type })} - data-refer={`delete-catalog-${row.name}`} - > - - + {!isHideSchemaEdit && ( + theme.palette.text.secondary }} + onClick={() => handleShowEditDialog({ row, type: row.node })} + data-refer={`edit-entity-${row.name}`} + > + + + )} + + {!isHideSchemaEdit && ( + theme.palette.error.light }} + onClick={() => handleDelete({ name: row.name, type: row.node, catalogType: row.type })} + data-refer={`delete-entity-${row.name}`} + > + + + )} ) } @@ -422,34 +440,66 @@ const TableView = () => { ] const handleShowDetails = async ({ row, type }) => { - if (type === 'catalog') { - const [err, res] = await to(getCatalogDetailsApi({ metalake, catalog: row.name })) + switch (type) { + case 'catalog': { + const [err, res] = await to(getCatalogDetailsApi({ metalake, catalog: row.name })) - if (err || !res) { - throw new Error(err) + if (err || !res) { + throw new Error(err) + } + + setDrawerData(res.catalog) + setOpenDrawer(true) + break } + case 'schema': { + const [err, res] = await to(getSchemaDetailsApi({ metalake, catalog, schema: row.name })) + + if (err || !res) { + throw new Error(err) + } - setDrawerData(res.catalog) - setOpenDrawer(true) + setDrawerData(res.schema) + setOpenDrawer(true) + break + } + default: + return } } const handleShowEditDialog = async data => { - const metalake = data.row.namespace[0] || null - const catalog = data.row.name || null + switch (data.type) { + case 'catalog': { + const [err, res] = await to(getCatalogDetailsApi({ metalake, catalog: data.row?.name })) - if (metalake && catalog) { - const [err, res] = await to(getCatalogDetailsApi({ metalake, catalog })) + if (err || !res) { + throw new Error(err) + } - if (err || !res) { - throw new Error(err) + const { catalog: resCatalog } = res + + setDialogType('update') + setDialogData(resCatalog) + setOpenDialog(true) + break } + case 'schema': { + if (metalake && catalog) { + const [err, res] = await to(getSchemaDetailsApi({ metalake, catalog, schema: data.row?.name })) - const { catalog: resCatalog } = res + if (err || !res) { + throw new Error(err) + } - setDialogType('update') - setDialogData(resCatalog) - setOpenDialog(true) + setDialogType('update') + setDialogData(res.schema) + setOpenSchemaDialog(true) + } + break + } + default: + return } } @@ -465,8 +515,15 @@ const TableView = () => { const handleConfirmDeleteSubmit = () => { if (confirmCacheData) { - if (confirmCacheData.type === 'catalog') { - dispatch(deleteCatalog({ metalake, catalog: confirmCacheData.name, type: confirmCacheData.catalogType })) + switch (confirmCacheData.type) { + case 'catalog': + dispatch(deleteCatalog({ metalake, catalog: confirmCacheData.name, type: confirmCacheData.catalogType })) + break + case 'schema': + dispatch(deleteSchema({ metalake, catalog, type, schema: confirmCacheData.name })) + break + default: + break } setOpenConfirmDelete(false) @@ -474,8 +531,11 @@ const TableView = () => { } const checkColumns = () => { - if (paramsSize == 1 && searchParams.has('metalake')) { - return catalogsColumns + if ( + (paramsSize == 1 && searchParams.has('metalake')) || + (paramsSize == 3 && searchParams.has('metalake') && searchParams.has('catalog') && searchParams.has('type')) + ) { + return actionsColumns } else if (paramsSize == 5 && searchParams.has('table')) { return tableColumns } else { @@ -508,12 +568,7 @@ const TableView = () => { paginationModel={paginationModel} onPaginationModelChange={setPaginationModel} /> - + { handleConfirmDeleteSubmit={handleConfirmDeleteSubmit} /> - + + + ) } diff --git a/web/web/src/components/DetailsDrawer.js b/web/web/src/components/DetailsDrawer.js index a310e5fb162..c470af43cc5 100644 --- a/web/web/src/components/DetailsDrawer.js +++ b/web/web/src/components/DetailsDrawer.js @@ -41,7 +41,7 @@ import EmptyText from '@/components/EmptyText' import { formatToDateTime, isValidDate } from '@/lib/utils/date' const DetailsDrawer = props => { - const { openDrawer, setOpenDrawer, drawerData = {}, isMetalakePage } = props + const { openDrawer, setOpenDrawer, drawerData = {} } = props const { audit = {} } = drawerData @@ -125,22 +125,23 @@ const DetailsDrawer = props => { - {isMetalakePage ? ( - <> - - - Type - - {renderFieldText({ value: drawerData.type })} - - - - Provider - - {renderFieldText({ value: drawerData.provider })} - - - ) : null} + {drawerData.type && ( + + + Type + + {renderFieldText({ value: drawerData.type })} + + )} + + {drawerData.provider && ( + + + Provider + + {renderFieldText({ value: drawerData.provider })} + + )} @@ -156,26 +157,32 @@ const DetailsDrawer = props => { {renderFieldText({ value: audit.creator })} - - - Created at - - {renderFieldText({ value: audit.createTime, isDate: true })} - - - - - Last modified by - - {renderFieldText({ value: audit.lastModifier })} - - - - - Last modified at - - {renderFieldText({ value: audit.lastModifiedTime, isDate: true })} - + {audit.createTime && ( + + + Created at + + {renderFieldText({ value: audit.createTime, isDate: true })} + + )} + + {audit.lastModifier && ( + + + Last modified by + + {renderFieldText({ value: audit.lastModifier })} + + )} + + {audit.lastModifiedTime && ( + + + Last modified at + + {renderFieldText({ value: audit.lastModifiedTime, isDate: true })} + + )} diff --git a/web/web/src/lib/api/schemas/index.js b/web/web/src/lib/api/schemas/index.js index 654ed40179f..0848c1e545a 100644 --- a/web/web/src/lib/api/schemas/index.js +++ b/web/web/src/lib/api/schemas/index.js @@ -25,7 +25,13 @@ const Apis = { GET_DETAIL: ({ metalake, catalog, schema }) => `/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent( catalog - )}/schemas/${encodeURIComponent(schema)}` + )}/schemas/${encodeURIComponent(schema)}`, + CREATE: ({ metalake, catalog }) => + `/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(catalog)}/schemas`, + UPDATE: ({ metalake, catalog, schema }) => + `/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(catalog)}/schemas/${encodeURIComponent(schema)}`, + DELETE: ({ metalake, catalog, schema }) => + `/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(catalog)}/schemas/${encodeURIComponent(schema)}` } export const getSchemasApi = params => { @@ -39,3 +45,15 @@ export const getSchemaDetailsApi = ({ metalake, catalog, schema }) => { url: `${Apis.GET_DETAIL({ metalake, catalog, schema })}` }) } + +export const createSchemaApi = ({ metalake, catalog, data }) => { + return defHttp.post({ url: `${Apis.CREATE({ metalake, catalog })}`, data }) +} + +export const updateSchemaApi = ({ metalake, catalog, schema, data }) => { + return defHttp.put({ url: `${Apis.UPDATE({ metalake, catalog, schema })}`, data }) +} + +export const deleteSchemaApi = ({ metalake, catalog, schema }) => { + return defHttp.delete({ url: `${Apis.DELETE({ metalake, catalog, schema })}` }) +} diff --git a/web/web/src/lib/icons/iconify-icons.css b/web/web/src/lib/icons/iconify-icons.css index f5803fdf93d..465c9880930 100644 --- a/web/web/src/lib/icons/iconify-icons.css +++ b/web/web/src/lib/icons/iconify-icons.css @@ -38,7 +38,7 @@ } .custom-icons-hudi { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2000 2000' width='2000' height='2000'%3E%3Cpath fill='%23FFF' d='M0 1000V0h2000v2000H0zm1529 553c0-5-102-212-227-461l-228-453-22 46c-20 42-25 45-60 45h-38l-54-110c-30-61-57-110-60-110-4 0-470 924-470 933 0 1 59 1 130-1l130-4v41c0 23-5 51-10 62-11 19-2 19 450 19 253 0 460-3 459-7m290-154 103-84-47-14-47-15 6-111c8-127-5-191-56-288-82-154-302-287-433-263-32 6-33 7-23 45l10 38 82 5c102 7 177 41 242 110 68 73 96 142 102 254 2 50 2 108-2 128-6 31-11 36-35 36-16 0-47-7-70-15-39-13-42-13-37 3 3 9 19 67 36 127 17 61 34 118 36 128 4 12 10 15 17 9 7-5 59-46 116-93M232 1248c13-19 11-27-19-87-66-129-67-286-3-418 46-94 146-173 218-173 27 0 31 4 42 48 7 26 16 50 19 54 7 8 171-204 171-220 0-6-243-92-263-92-3 0-2 12 3 26 6 14 10 42 10 62 0 34-5 39-48 63-225 125-326 365-249 588 24 68 83 171 98 171 4 0 13-10 21-22'/%3E%3Cpath fill='%2300B3EF' d='M600 1479c0-9-36-10-132-5-73 3-138 3-144-1-8-6 71-173 248-526 226-453 261-517 274-506 9 7 43 67 76 133 33 67 62 123 65 126s11-4 19-14c13-18 14-17 14 17 0 28-46 129-187 412-164 327-191 375-210 375-13 0-23-5-23-11m-434-195c-148-189-149-478-2-665 38-48 165-147 206-161 9-3 6-24-10-76-12-40-20-75-18-77 5-5 369 114 381 124 7 7-230 331-242 331-4 0-19-36-32-80l-24-79-33 16c-93 46-163 143-188 264-22 105 5 218 72 307l26 33-26 37c-14 20-35 48-45 61l-18 24z'/%3E%3Cpath fill='%23004E7F' d='M596 1493c6-21 3-22-78-26l-83-3 90-2 90-2 198-395c108-217 201-395 205-394 4 0 92 172 195 382 185 377 210 440 175 452-7 2-189 6-405 7l-392 3zm724-778c0-22 4-25 35-25s35 3 35 25-4 25-35 25-35-3-35-25'/%3E%3Cpath fill='%232F5B7F' d='m577 1538 28-53 375-3c206-1 378-6 383-11 4-4-76-177-180-384L996 711l39-81 39-81 258 513c141 282 257 516 257 521 1 4-233 7-520 7H549z'/%3E%3Cpath fill='%23013868' d='M1616 1369c-32-110-55-203-52-206 2-3 39 6 80 20l75 25 7-35c11-57-4-161-30-219-51-111-162-195-277-210-30-4-57-12-61-18-5-8-8-7-8 2 0 6-11 12-24 12-20 0-25-8-41-65-9-36-15-68-12-71 13-13 131-16 189-4 142 27 286 138 352 270 47 95 60 163 53 286l-6 100 70 23c38 12 69 24 69 25 0 3-272 224-316 258-8 6-29-54-68-193'/%3E%3C/svg%3E"); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2000 2000' width='2000' height='2000'%3E%3Cpath fill='transparent' d='M0 1000V0h2000v2000H0zm1529 553c0-5-102-212-227-461l-228-453-22 46c-20 42-25 45-60 45h-38l-54-110c-30-61-57-110-60-110-4 0-470 924-470 933 0 1 59 1 130-1l130-4v41c0 23-5 51-10 62-11 19-2 19 450 19 253 0 460-3 459-7m290-154 103-84-47-14-47-15 6-111c8-127-5-191-56-288-82-154-302-287-433-263-32 6-33 7-23 45l10 38 82 5c102 7 177 41 242 110 68 73 96 142 102 254 2 50 2 108-2 128-6 31-11 36-35 36-16 0-47-7-70-15-39-13-42-13-37 3 3 9 19 67 36 127 17 61 34 118 36 128 4 12 10 15 17 9 7-5 59-46 116-93M232 1248c13-19 11-27-19-87-66-129-67-286-3-418 46-94 146-173 218-173 27 0 31 4 42 48 7 26 16 50 19 54 7 8 171-204 171-220 0-6-243-92-263-92-3 0-2 12 3 26 6 14 10 42 10 62 0 34-5 39-48 63-225 125-326 365-249 588 24 68 83 171 98 171 4 0 13-10 21-22'/%3E%3Cpath fill='%2300B3EF' d='M600 1479c0-9-36-10-132-5-73 3-138 3-144-1-8-6 71-173 248-526 226-453 261-517 274-506 9 7 43 67 76 133 33 67 62 123 65 126s11-4 19-14c13-18 14-17 14 17 0 28-46 129-187 412-164 327-191 375-210 375-13 0-23-5-23-11m-434-195c-148-189-149-478-2-665 38-48 165-147 206-161 9-3 6-24-10-76-12-40-20-75-18-77 5-5 369 114 381 124 7 7-230 331-242 331-4 0-19-36-32-80l-24-79-33 16c-93 46-163 143-188 264-22 105 5 218 72 307l26 33-26 37c-14 20-35 48-45 61l-18 24z'/%3E%3Cpath fill='%23004E7F' d='M596 1493c6-21 3-22-78-26l-83-3 90-2 90-2 198-395c108-217 201-395 205-394 4 0 92 172 195 382 185 377 210 440 175 452-7 2-189 6-405 7l-392 3zm724-778c0-22 4-25 35-25s35 3 35 25-4 25-35 25-35-3-35-25'/%3E%3Cpath fill='%232F5B7F' d='m577 1538 28-53 375-3c206-1 378-6 383-11 4-4-76-177-180-384L996 711l39-81 39-81 258 513c141 282 257 516 257 521 1 4-233 7-520 7H549z'/%3E%3Cpath fill='%23013868' d='M1616 1369c-32-110-55-203-52-206 2-3 39 6 80 20l75 25 7-35c11-57-4-161-30-219-51-111-162-195-277-210-30-4-57-12-61-18-5-8-8-7-8 2 0 6-11 12-24 12-20 0-25-8-41-65-9-36-15-68-12-71 13-13 131-16 189-4 142 27 286 138 352 270 47 95 60 163 53 286l-6 100 70 23c38 12 69 24 69 25 0 3-272 224-316 258-8 6-29-54-68-193'/%3E%3C/svg%3E"); } .custom-icons-paimon { diff --git a/web/web/src/lib/icons/svg/hudi.svg b/web/web/src/lib/icons/svg/hudi.svg index 8dfc3cb27b6..19a86a1c9e8 100644 --- a/web/web/src/lib/icons/svg/hudi.svg +++ b/web/web/src/lib/icons/svg/hudi.svg @@ -1,6 +1,6 @@ - + diff --git a/web/web/src/lib/store/metalakes/index.js b/web/web/src/lib/store/metalakes/index.js index 5dd55501001..7c58e80e4cc 100644 --- a/web/web/src/lib/store/metalakes/index.js +++ b/web/web/src/lib/store/metalakes/index.js @@ -39,7 +39,13 @@ import { updateCatalogApi, deleteCatalogApi } from '@/lib/api/catalogs' -import { getSchemasApi, getSchemaDetailsApi } from '@/lib/api/schemas' +import { + getSchemasApi, + getSchemaDetailsApi, + createSchemaApi, + updateSchemaApi, + deleteSchemaApi +} from '@/lib/api/schemas' import { getTablesApi, getTableDetailsApi } from '@/lib/api/tables' import { getFilesetsApi, getFilesetDetailsApi } from '@/lib/api/filesets' import { getTopicsApi, getTopicDetailsApi } from '@/lib/api/topics' @@ -549,6 +555,67 @@ export const getSchemaDetails = createAsyncThunk( } ) +export const createSchema = createAsyncThunk( + 'appMetalakes/createSchema', + async ({ data, metalake, catalog, type }, { dispatch }) => { + dispatch(setTableLoading(true)) + const [err, res] = await to(createSchemaApi({ data, metalake, catalog })) + dispatch(setTableLoading(false)) + + if (err || !res) { + return { err: true } + } + + const { schema: schemaItem } = res + + const schemaData = { + ...schemaItem, + node: 'schema', + id: `{{${metalake}}}{{${catalog}}}{{${type}}}{{${schemaItem.name}}}`, + key: `{{${metalake}}}{{${catalog}}}{{${type}}}{{${schemaItem.name}}}`, + path: `?${new URLSearchParams({ metalake, catalog, type, schema: schemaItem.name }).toString()}`, + name: schemaItem.name, + title: schemaItem.name, + tables: [], + children: [] + } + + dispatch(fetchSchemas({ metalake, catalog, type, init: true })) + + return schemaData + } +) + +export const updateSchema = createAsyncThunk( + 'appMetalakes/updateSchema', + async ({ metalake, catalog, type, schema, data }, { dispatch }) => { + const [err, res] = await to(updateSchemaApi({ metalake, catalog, schema, data })) + if (err || !res) { + return { err: true } + } + dispatch(fetchSchemas({ metalake, catalog, type, init: true })) + + return res.catalog + } +) + +export const deleteSchema = createAsyncThunk( + 'appMetalakes/deleteSchema', + async ({ metalake, catalog, type, schema }, { dispatch }) => { + dispatch(setTableLoading(true)) + const [err, res] = await to(deleteSchemaApi({ metalake, catalog, schema })) + dispatch(setTableLoading(false)) + + if (err || !res) { + throw new Error(err) + } + + dispatch(fetchSchemas({ metalake, catalog, type, page: 'catalogs', init: true })) + + return res + } +) + export const fetchTables = createAsyncThunk( 'appMetalakes/fetchTables', async ({ init, page, metalake, catalog, schema }, { getState, dispatch }) => { @@ -1087,6 +1154,21 @@ export const appMetalakesSlice = createSlice({ toast.error(action.error.message) } }) + builder.addCase(createSchema.rejected, (state, action) => { + if (!action.error.message.includes('CanceledError')) { + toast.error(action.error.message) + } + }) + builder.addCase(updateSchema.rejected, (state, action) => { + if (!action.error.message.includes('CanceledError')) { + toast.error(action.error.message) + } + }) + builder.addCase(deleteSchema.rejected, (state, action) => { + if (!action.error.message.includes('CanceledError')) { + toast.error(action.error.message) + } + }) builder.addCase(fetchTables.fulfilled, (state, action) => { state.tables = action.payload.tables if (action.payload.init) {