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 a89e394b5c [#10027][#10028][#10044][#10045][#10048][#10056] fix and
improve function related issue (#10114)
a89e394b5c is described below
commit a89e394b5c249ad722c4f1ae292cefe8f08bfe48
Author: Qian Xia <[email protected]>
AuthorDate: Tue Mar 3 20:35:48 2026 +0800
[#10027][#10028][#10044][#10045][#10048][#10056] fix and improve function
related issue (#10114)
### What changes were proposed in this pull request?
<img width="2762" height="1866" alt="image"
src="https://github.com/user-attachments/assets/19bb4725-8c09-4e1a-a23b-1c48cd6421fa"
/>
### Why are the changes needed?
N/A
Fix: #10027
Fix: #10028
Fix: #10044
Fix: #10045
Fix: #10048
Fix: #10056
### Does this PR introduce _any_ user-facing change?
N/A
### How was this patch tested?
manually
---
web-v2/web/src/app/catalogs/TreeComponent.js | 113 +--
web-v2/web/src/app/catalogs/page.js | 245 ++++---
.../entitiesContent/FunctionDetailsPage.js | 84 ++-
.../rightContent/entitiesContent/ListFiles.js | 1 -
web-v2/web/src/lib/store/metalakes/index.js | 70 +-
web-v2/web/src/lib/utils/initial.js | 775 ---------------------
6 files changed, 308 insertions(+), 980 deletions(-)
diff --git a/web-v2/web/src/app/catalogs/TreeComponent.js
b/web-v2/web/src/app/catalogs/TreeComponent.js
index 4d1b80cdf4..c023f5d3ef 100644
--- a/web-v2/web/src/app/catalogs/TreeComponent.js
+++ b/web-v2/web/src/app/catalogs/TreeComponent.js
@@ -21,10 +21,9 @@
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef,
useState } from 'react'
-import { usePathname, useRouter, useSearchParams } from 'next/navigation'
+import { useRouter, useSearchParams } from 'next/navigation'
import { Empty, Input, Spin, Tabs, Tooltip, Tree } from 'antd'
-import { useTheme } from 'next-themes'
import Icons from '@/components/Icons'
import { checkCatalogIcon } from '@/config/catalog'
@@ -34,7 +33,6 @@ import {
setExpanded,
setExpandedNodes,
setIntoTreeNodeWithFetch,
- removeExpandedNode,
setSelectedNodes,
setLoadedNodes,
getTableDetails,
@@ -52,18 +50,12 @@ const { Search } = Input
export const TreeComponent = forwardRef(function TreeComponent(props, ref) {
const [treeData, setTreeData] = useState([])
const [searchValue, setSearchValue] = useState('')
- const [expandedKeys, setExpandedKeys] = useState([])
- const [loadedKeys, setLoadedKeys] = useState([])
const [autoExpandParent, setAutoExpandParent] = useState(false)
- const [selectedKeys, setSelectedKeys] = useState([])
const router = useRouter()
- const pathname = usePathname()
const searchParams = useSearchParams()
const currentMetalake = searchParams.get('metalake')
const [currentCatalogType, setCatalogType] =
useState(searchParams.get('catalogType') || 'relational')
- const [, locale, , catalog, schema, entity] = pathname.split('/')
const treeRef = useRef(null)
- const { theme } = useTheme()
const [isHover, setIsHover] = useState('')
const dispatch = useAppDispatch()
const store = useAppSelector(state => state.metalakes)
@@ -75,26 +67,6 @@ export const TreeComponent = forwardRef(function
TreeComponent(props, ref) {
{ label: 'Model', key: 'model' }
]
- const updateTreeData = (list, key, children) =>
- list.map(node => {
- if (node.key === key) {
- return {
- ...node,
- isLeaf: children?.length === 0,
- children
- }
- }
- if (node.children) {
- return {
- ...node,
- isLeaf: node.children.length === 0,
- children: updateTreeData(node.children, key, children)
- }
- }
-
- return node
- })
-
const handleDoubleClick = event => {
event.preventDefault()
event.stopPropagation()
@@ -323,29 +295,62 @@ export const TreeComponent = forwardRef(function
TreeComponent(props, ref) {
if (searchParams.get('catalogType')) {
setCatalogType(searchParams.get('catalogType'))
} else {
- setExpandedKeys([])
+ dispatch(setExpanded([]))
setCatalogType('relational')
}
- }, [searchParams.get('catalogType')])
+ }, [dispatch, searchParams])
useEffect(() => {
- setTimeout(() => {
- if (entity) {
- setExpandedKeys(
- Array.from(new Set([...expandedKeys, catalog,
`${catalog}/${schema}`, `${catalog}/${schema}/${entity}`]))
- )
- setSelectedKeys([`${catalog}/${schema}/${entity}`])
- } else if (schema && !entity) {
- setExpandedKeys(Array.from(new Set([...expandedKeys, catalog,
`${catalog}/${schema}`])))
- setSelectedKeys([`${catalog}/${schema}`])
- } else if (catalog && !schema && !entity) {
- setExpandedKeys(Array.from(new Set([...expandedKeys, catalog])))
- setSelectedKeys([catalog])
- } else if (!catalog && !schema && !entity) {
- setExpandedKeys(Array.from(new Set([...expandedKeys])))
+ const timerId = setTimeout(() => {
+ const metalake = searchParams.get('metalake')
+ const catalog = searchParams.get('catalog')
+ const catalogType = searchParams.get('catalogType')
+ const schema = searchParams.get('schema')
+ const table = searchParams.get('table')
+ const fileset = searchParams.get('fileset')
+ const topic = searchParams.get('topic')
+ const model = searchParams.get('model')
+ const func = searchParams.get('function')
+ const entity = table || fileset || topic || model || func
+
+ if (!metalake) {
+ dispatch(setExpanded([]))
+ dispatch(setSelectedNodes([]))
+
+ return
+ }
+
+ const metalakeKey = `{{${metalake}}}`
+ const nextExpandedKeys = [metalakeKey]
+ let nextSelectedKey = ''
+
+ if (catalog && catalogType) {
+ const catalogKey = `${metalakeKey}{{${catalog}}}{{${catalogType}}}`
+ nextExpandedKeys.push(catalogKey)
+ nextSelectedKey = catalogKey
+
+ if (schema) {
+ const schemaKey = `${catalogKey}{{${schema}}}`
+ nextExpandedKeys.push(schemaKey)
+ nextSelectedKey = schemaKey
+
+ if (entity) {
+ nextSelectedKey = `${schemaKey}{{${entity}}}`
+ }
+ }
+ }
+
+ dispatch(setExpandedNodes(Array.from(new Set(nextExpandedKeys))))
+
+ if (nextSelectedKey) {
+ dispatch(setSelectedNodes([nextSelectedKey]))
}
}, 1000)
- }, [pathname])
+
+ return () => {
+ clearTimeout(timerId)
+ }
+ }, [dispatch, searchParams])
const renderTitle = title => {
return (
@@ -493,12 +498,16 @@ export const TreeComponent = forwardRef(function
TreeComponent(props, ref) {
}
}
if (inUseStr === 'false') {
+ const catalogPrefix =
`{{${currentMetalake}}}{{${name}}}{{${currentCatalogType}}}`
+
// Remove from expanded and loaded nodes
- dispatch(setExpandedNodes([...store.expandedNodes.filter(key =>
!key.includes(name))]))
- dispatch(setLoadedNodes([...store.loadedNodes.filter(key =>
!key.includes(name))]))
+ dispatch(setExpandedNodes([...store.expandedNodes.filter(key =>
!key.startsWith(catalogPrefix))]))
+ dispatch(setLoadedNodes([...store.loadedNodes.filter(key =>
!key.startsWith(catalogPrefix))]))
} else {
+ const catalogKey =
`{{${currentMetalake}}}{{${name}}}{{${currentCatalogType}}}`
+
// Remove from loaded nodes to allow re-fetching children when expanded
- dispatch(setLoadedNodes([...store.loadedNodes.filter(key => key !==
name)]))
+ dispatch(setLoadedNodes([...store.loadedNodes.filter(key => key !==
catalogKey)]))
}
setTreeData(tree)
}
@@ -614,9 +623,9 @@ export const TreeComponent = forwardRef(function
TreeComponent(props, ref) {
const onChangeType = key => {
setCatalogType(key)
- setExpandedKeys([])
- setLoadedKeys([])
- setSelectedKeys([])
+ dispatch(setExpanded([]))
+ dispatch(setLoadedNodes([]))
+ dispatch(setSelectedNodes([]))
setTreeData([])
router.push(`/catalogs?metalake=${currentMetalake}&catalogType=${key}`)
}
diff --git a/web-v2/web/src/app/catalogs/page.js
b/web-v2/web/src/app/catalogs/page.js
index 2231c9d6eb..e4af25ad55 100644
--- a/web-v2/web/src/app/catalogs/page.js
+++ b/web-v2/web/src/app/catalogs/page.js
@@ -40,6 +40,7 @@ import {
fetchModels,
fetchModelVersions,
fetchFunctions,
+ getFunctionDetails,
getMetalakeDetails,
getCatalogDetails,
getSchemaDetails,
@@ -52,7 +53,8 @@ import {
getCurrentEntityTags,
getCurrentEntityPolicies,
resetActivatedDetails,
- setActivatedDetailsLoading
+ setActivatedDetailsLoading,
+ setLoadedNodes
} from '@/lib/store/metalakes'
import { fetchTags } from '@/lib/store/tags'
@@ -67,7 +69,7 @@ const CatalogsListPage = () => {
const treeRef = useRef()
const buildNodePath = routeParams => {
- const keys = ['metalake', 'catalog', 'catalogType', 'schema', 'table',
'fileset', 'topic', 'model']
+ const keys = ['metalake', 'catalog', 'catalogType', 'schema', 'table',
'fileset', 'topic', 'model', 'function']
return keys.map(key => (routeParams[key] ? `{{${routeParams[key]}}}` :
'')).join('')
}
@@ -82,11 +84,23 @@ const CatalogsListPage = () => {
fileset: searchParams.get('fileset'),
topic: searchParams.get('topic'),
model: searchParams.get('model'),
+ function: searchParams.get('function'),
version: searchParams.get('version')
}
async function fetchDependsData() {
if ([...searchParams.keys()].length) {
- const { metalake, catalog, catalogType, schema, table, fileset, topic,
model, version } = routeParams
+ const {
+ metalake,
+ catalog,
+ catalogType,
+ schema,
+ table,
+ fileset,
+ topic,
+ model,
+ function: func,
+ version
+ } = routeParams
if (metalake) {
dispatch(fetchTags({ metalake, details: true }))
@@ -99,6 +113,13 @@ const CatalogsListPage = () => {
}
if (paramsSize === 3 && catalog) {
+ if (catalogType) {
+ const catalogKey =
`{{${metalake}}}{{${catalog}}}{{${catalogType}}}`
+ if (!store.loadedNodes.includes(catalogKey)) {
+ await dispatch(setLoadedNodes([...new Set([...store.loadedNodes,
catalogKey])]))
+ }
+ }
+
if (!store.catalogs.length) {
await dispatch(fetchCatalogs({ metalake }))
}
@@ -171,115 +192,123 @@ const CatalogsListPage = () => {
)
}
- if (paramsSize === 5 && catalog && catalogType && schema && (table ||
fileset || topic || model)) {
+ if (paramsSize === 5 && catalog && catalogType && schema) {
if (!store.catalogs.length) {
await dispatch(fetchCatalogs({ metalake }))
await dispatch(fetchSchemas({ metalake, catalog, catalogType }))
}
- switch (catalogType) {
- case 'relational':
- store.tables.length === 0 &&
- (await dispatch(fetchTables({ init: true, page: 'schemas',
metalake, catalog, schema })))
- await dispatch(resetActivatedDetails())
- await dispatch(setActivatedDetailsLoading(true))
- await dispatch(getTableDetails({ init: true, metalake, catalog,
schema, table }))
- await dispatch(setActivatedDetailsLoading(false))
- dispatch(
- getCurrentEntityTags({
- init: true,
- metalake,
- metadataObjectType: 'table',
- metadataObjectFullName: `${catalog}.${schema}.${table}`
- })
- )
- dispatch(
- getCurrentEntityPolicies({
- init: true,
- metalake,
- metadataObjectType: 'table',
- metadataObjectFullName: `${catalog}.${schema}.${table}`,
- details: true
- })
- )
- break
- case 'fileset':
- store.filesets.length === 0 &&
- (await dispatch(fetchFilesets({ init: true, page: 'schemas',
metalake, catalog, schema })))
- await dispatch(setActivatedDetailsLoading(true))
- await dispatch(getFilesetDetails({ init: true, metalake,
catalog, schema, fileset }))
- await dispatch(setActivatedDetailsLoading(false))
- dispatch(
- getCurrentEntityTags({
- init: true,
- metalake,
- metadataObjectType: 'fileset',
- metadataObjectFullName: `${catalog}.${schema}.${fileset}`,
- details: true
- })
- )
- dispatch(
- getCurrentEntityPolicies({
- init: true,
- metalake,
- metadataObjectType: 'fileset',
- metadataObjectFullName: `${catalog}.${schema}.${fileset}`,
- details: true
- })
- )
- break
- case 'messaging':
- store.topics.length === 0 &&
- (await dispatch(fetchTopics({ init: true, page: 'schemas',
metalake, catalog, schema })))
- await dispatch(setActivatedDetailsLoading(true))
- await dispatch(getTopicDetails({ init: true, metalake, catalog,
schema, topic }))
- await dispatch(setActivatedDetailsLoading(false))
- dispatch(
- getCurrentEntityTags({
- init: true,
- metalake,
- metadataObjectType: 'topic',
- metadataObjectFullName: `${catalog}.${schema}.${topic}`,
- details: true
- })
- )
- dispatch(
- getCurrentEntityPolicies({
- init: true,
- metalake,
- metadataObjectType: 'topic',
- metadataObjectFullName: `${catalog}.${schema}.${topic}`,
- details: true
- })
- )
- break
- case 'model':
- store.models.length === 0 &&
- (await dispatch(fetchModels({ init: true, page: 'schemas',
metalake, catalog, schema })))
- await dispatch(fetchModelVersions({ init: true, metalake,
catalog, schema, model }))
- await dispatch(setActivatedDetailsLoading(true))
- await dispatch(getModelDetails({ init: true, metalake, catalog,
schema, model }))
- await dispatch(setActivatedDetailsLoading(false))
- dispatch(
- getCurrentEntityTags({
- init: true,
- metalake,
- metadataObjectType: 'model',
- metadataObjectFullName: `${catalog}.${schema}.${model}`,
- details: true
- })
- )
- dispatch(
- getCurrentEntityPolicies({
- init: true,
- metalake,
- metadataObjectType: 'model',
- metadataObjectFullName: `${catalog}.${schema}.${model}`,
- details: true
- })
- )
- break
- default:
- break
+ if (table || fileset || topic || model) {
+ switch (catalogType) {
+ case 'relational':
+ store.tables.length === 0 &&
+ (await dispatch(fetchTables({ init: true, page: 'schemas',
metalake, catalog, schema })))
+ await dispatch(resetActivatedDetails())
+ await dispatch(setActivatedDetailsLoading(true))
+ await dispatch(getTableDetails({ init: true, metalake,
catalog, schema, table }))
+ await dispatch(setActivatedDetailsLoading(false))
+ dispatch(
+ getCurrentEntityTags({
+ init: true,
+ metalake,
+ metadataObjectType: 'table',
+ metadataObjectFullName: `${catalog}.${schema}.${table}`
+ })
+ )
+ dispatch(
+ getCurrentEntityPolicies({
+ init: true,
+ metalake,
+ metadataObjectType: 'table',
+ metadataObjectFullName: `${catalog}.${schema}.${table}`,
+ details: true
+ })
+ )
+ break
+ case 'fileset':
+ store.filesets.length === 0 &&
+ (await dispatch(fetchFilesets({ init: true, page: 'schemas',
metalake, catalog, schema })))
+ await dispatch(setActivatedDetailsLoading(true))
+ await dispatch(getFilesetDetails({ init: true, metalake,
catalog, schema, fileset }))
+ await dispatch(setActivatedDetailsLoading(false))
+ dispatch(
+ getCurrentEntityTags({
+ init: true,
+ metalake,
+ metadataObjectType: 'fileset',
+ metadataObjectFullName: `${catalog}.${schema}.${fileset}`,
+ details: true
+ })
+ )
+ dispatch(
+ getCurrentEntityPolicies({
+ init: true,
+ metalake,
+ metadataObjectType: 'fileset',
+ metadataObjectFullName: `${catalog}.${schema}.${fileset}`,
+ details: true
+ })
+ )
+ break
+ case 'messaging':
+ store.topics.length === 0 &&
+ (await dispatch(fetchTopics({ init: true, page: 'schemas',
metalake, catalog, schema })))
+ await dispatch(setActivatedDetailsLoading(true))
+ await dispatch(getTopicDetails({ init: true, metalake,
catalog, schema, topic }))
+ await dispatch(setActivatedDetailsLoading(false))
+ dispatch(
+ getCurrentEntityTags({
+ init: true,
+ metalake,
+ metadataObjectType: 'topic',
+ metadataObjectFullName: `${catalog}.${schema}.${topic}`,
+ details: true
+ })
+ )
+ dispatch(
+ getCurrentEntityPolicies({
+ init: true,
+ metalake,
+ metadataObjectType: 'topic',
+ metadataObjectFullName: `${catalog}.${schema}.${topic}`,
+ details: true
+ })
+ )
+ break
+ case 'model':
+ store.models.length === 0 &&
+ (await dispatch(fetchModels({ init: true, page: 'schemas',
metalake, catalog, schema })))
+ await dispatch(fetchModelVersions({ init: true, metalake,
catalog, schema, model }))
+ await dispatch(setActivatedDetailsLoading(true))
+ await dispatch(getModelDetails({ init: true, metalake,
catalog, schema, model }))
+ await dispatch(setActivatedDetailsLoading(false))
+ dispatch(
+ getCurrentEntityTags({
+ init: true,
+ metalake,
+ metadataObjectType: 'model',
+ metadataObjectFullName: `${catalog}.${schema}.${model}`,
+ details: true
+ })
+ )
+ dispatch(
+ getCurrentEntityPolicies({
+ init: true,
+ metalake,
+ metadataObjectType: 'model',
+ metadataObjectFullName: `${catalog}.${schema}.${model}`,
+ details: true
+ })
+ )
+ break
+ default:
+ break
+ }
+ }
+ if (func) {
+ store.functions.length === 0 && (await dispatch(fetchFunctions({
init: true, metalake, catalog, schema })))
+ await dispatch(setActivatedDetailsLoading(true))
+ await dispatch(getFunctionDetails({ init: true, metalake, catalog,
schema, functionName: func }))
+ await dispatch(setActivatedDetailsLoading(false))
}
}
if (paramsSize === 6 && version) {
diff --git
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/FunctionDetailsPage.js
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/FunctionDetailsPage.js
index 6101684da6..221d0971ae 100644
---
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/FunctionDetailsPage.js
+++
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/FunctionDetailsPage.js
@@ -23,9 +23,8 @@ import { useEffect, useState } from 'react'
import { Collapse, Descriptions, Space, Spin, Tag, Typography, Flex, Tooltip,
Divider, Tabs } from 'antd'
import { useSearchParams } from 'next/navigation'
import { formatToDateTime, isValidDate } from '@/lib/utils/date'
-import { getFunctionDetailsApi } from '@/lib/api/functions'
-import { to } from '@/lib/utils'
import Icons from '@/components/Icons'
+import { useAppSelector } from '@/lib/hooks/useStore'
const { Title, Paragraph } = Typography
@@ -37,11 +36,30 @@ const formatType = type => {
return JSON.stringify(type)
}
+const formatDefaultValue = defaultValue => {
+ if (defaultValue == null) return ''
+
+ if (typeof defaultValue === 'string' || typeof defaultValue === 'number' ||
typeof defaultValue === 'boolean') {
+ return `${defaultValue}`
+ }
+
+ if (typeof defaultValue === 'object') {
+ if (Object.prototype.hasOwnProperty.call(defaultValue, 'value')) {
+ return `${defaultValue.value}`
+ }
+
+ return JSON.stringify(defaultValue)
+ }
+
+ return ''
+}
+
const renderParameters = parameters => {
if (!parameters?.length) return null
return parameters.map((param, index) => {
const comment = typeof param?.comment === 'string' ? param.comment.trim()
: ''
+ const defaultText = formatDefaultValue(param?.defaultValue)
return (
<span key={`${param.name || 'param'}-${index}`}>
@@ -53,7 +71,7 @@ const renderParameters = parameters => {
</span>
</Tooltip>
)}
- <span>{`: ${formatType(param.dataType)}`}</span>
+ <span>{`: ${formatType(param.dataType)}${defaultText ? ` =
${defaultText}` : ''}`}</span>
{index < parameters.length - 1 && <span>, </span>}
</span>
)
@@ -118,6 +136,10 @@ const buildImplDetails = impl => {
const FunctionImplTabs = ({ impls }) => {
const [activeKey, setActiveKey] = useState(impls[0]?.runtime || 'runtime-0')
+ useEffect(() => {
+ setActiveKey(impls[0]?.runtime || 'runtime-0')
+ }, [impls])
+
const tabItems = impls.map((impl, implIndex) => {
const runtimeKey = impl?.runtime || `runtime-${implIndex}`
const tabLabel = impl?.runtime || `Runtime ${implIndex + 1}`
@@ -138,7 +160,14 @@ const FunctionImplTabs = ({ impls }) => {
return (
<div className='flex flex-col gap-2 w-full'>
<Tabs size='small' className='w-full' items={tabItems}
activeKey={activeKey} onChange={setActiveKey} />
- <Descriptions layout='horizontal' column={1} size='small' bordered
style={{ width: '100%' }}>
+ <Descriptions
+ layout='horizontal'
+ column={1}
+ size='small'
+ bordered
+ style={{ width: '100%' }}
+ labelStyle={{ width: '40%' }}
+ >
{activeDetails.map(detail => (
<Descriptions.Item key={`${activeKey}-${detail.label}`}
label={detail.label}>
{detail.value}
@@ -151,37 +180,14 @@ const FunctionImplTabs = ({ impls }) => {
export default function FunctionDetailsPage() {
const searchParams = useSearchParams()
- const metalake = searchParams.get('metalake')
- const catalog = searchParams.get('catalog')
- const schema = searchParams.get('schema')
const functionName = searchParams.get('function')
- const [loading, setLoading] = useState(false)
- const [functionData, setFunctionData] = useState(null)
+ const store = useAppSelector(state => state.metalakes)
+ const functionData = store.activatedDetails
+ const [activeDefinitionKeys, setActiveDefinitionKeys] = useState([])
useEffect(() => {
- const loadDetails = async () => {
- if (!metalake || !catalog || !schema || !functionName) return
- setLoading(true)
-
- const [err, res] = await to(
- getFunctionDetailsApi({
- metalake,
- catalog,
- schema,
- functionName
- })
- )
- setLoading(false)
- if (err || !res) {
- setFunctionData(null)
-
- return
- }
- setFunctionData(res.function)
- }
-
- loadDetails()
- }, [metalake, catalog, schema, functionName])
+ setActiveDefinitionKeys([])
+ }, [functionName])
const definitions = functionData?.definitions || []
const createdAt = functionData?.audit?.createTime
@@ -211,7 +217,7 @@ export default function FunctionDetailsPage() {
})
return (
- <Spin spinning={loading}>
+ <Spin spinning={store.activatedDetailsLoading}>
<div className='bg-white min-h-[calc(100vh-24rem)]'>
<Space direction='vertical' size='small' style={{ width: '100%' }}>
<Flex className='mb-2' gap='small' align='flex-start'>
@@ -260,11 +266,19 @@ export default function FunctionDetailsPage() {
</Space>
</Space>
<div className='max-h-[60vh] overflow-auto'>
- <div className='mb-2 font-medium'>Definitions</div>
+ <Tabs
+ data-refer='details-tabs'
+ defaultActiveKey={'Definitions'}
+ items={[{ label: 'Definitions', key: 'Definitions' }]}
+ />
{definitions.length === 0 ? (
<span className='text-slate-400'>No definitions</span>
) : (
- <Collapse items={definitionItems} defaultActiveKey={[]} />
+ <Collapse
+ items={definitionItems}
+ activeKey={activeDefinitionKeys}
+ onChange={keys => setActiveDefinitionKeys(Array.isArray(keys)
? keys : [keys])}
+ />
)}
</div>
</Space>
diff --git
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/ListFiles.js
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/ListFiles.js
index 849e1409c3..3fd84ba3f9 100644
--- a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/ListFiles.js
+++ b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/ListFiles.js
@@ -56,7 +56,6 @@ const ListFiles = ({ metalake, catalog, schema, fileset,
storageLocations, defau
}, [sub_path])
useEffect(() => {
- console.log('sub_path changed:', sub_path)
if (metalake && catalog && schema && fileset) {
dispatch(
getFilesetFiles({
diff --git a/web-v2/web/src/lib/store/metalakes/index.js
b/web-v2/web/src/lib/store/metalakes/index.js
index d0f2c318f1..78cc3719d0 100644
--- a/web-v2/web/src/lib/store/metalakes/index.js
+++ b/web-v2/web/src/lib/store/metalakes/index.js
@@ -77,7 +77,7 @@ import {
updateVersionApi,
deleteVersionApi
} from '@/lib/api/models'
-import { getFunctionsApi } from '@/lib/api/functions'
+import { getFunctionsApi, getFunctionDetailsApi } from '@/lib/api/functions'
const remapExpandedAndLoadedNodes = ({ getState, mapNode }) => {
const expandedNodes = getState().metalakes.expandedNodes.map(mapNode)
@@ -89,6 +89,13 @@ const remapExpandedAndLoadedNodes = ({ getState, mapNode })
=> {
}
}
+const mergeWithFunctionNodes = ({ tree, key, entities }) => {
+ const existingNode = findInTree(tree, 'key', key)
+ const functions = existingNode?.children?.filter(child => child?.node ===
'function') || []
+
+ return _.uniqBy([...entities, ...functions], 'key')
+}
+
export const fetchMetalakes = createAsyncThunk('appMetalakes/fetchMetalakes',
async (params, { getState }) => {
const [err, res] = await to(getMetalakesApi())
@@ -799,6 +806,24 @@ export const fetchSchemas = createAsyncThunk(
await dispatch(setTableLoading(false))
if (err || !res) {
+ const isCanceledRequest =
+ err?.name === 'CanceledError' ||
+ err?.code === 'ERR_CANCELED' ||
+ err?.message?.includes?.('canceled') ||
+ err?.message?.includes?.('aborted')
+
+ if (isCanceledRequest) {
+ const catalogKey = `{{${metalake}}}{{${catalog}}}{{${catalogType}}}`
+ const catalogNode = findInTree(getState().metalakes.metalakeTree,
'key', catalogKey)
+ const cachedSchemas = (catalogNode?.children || []).filter(item =>
item?.node === 'schema')
+
+ if (cachedSchemas.length > 0) {
+ dispatch(setExpandedNodes([`{{${metalake}}}`, catalogKey]))
+
+ return { schemas: cachedSchemas, page, init }
+ }
+ }
+
throw new Error(err)
}
@@ -1035,10 +1060,11 @@ export const fetchTables = createAsyncThunk(
init &&
getState().metalakes.loadedNodes.includes(`{{${metalake}}}{{${catalog}}}{{${'relational'}}}{{${schema}}}`)
) {
+ const tableKey =
`{{${metalake}}}{{${catalog}}}{{${'relational'}}}{{${schema}}}`
dispatch(
setIntoTreeNodes({
- key: `{{${metalake}}}{{${catalog}}}{{${'relational'}}}{{${schema}}}`,
- data: tables
+ key: tableKey,
+ data: mergeWithFunctionNodes({ tree:
getState().metalakes.metalakeTree, key: tableKey, entities: tables })
})
)
}
@@ -1348,10 +1374,11 @@ export const fetchFilesets = createAsyncThunk(
init &&
getState().metalakes.loadedNodes.includes(`{{${metalake}}}{{${catalog}}}{{${'fileset'}}}{{${schema}}}`)
) {
+ const filesetKey =
`{{${metalake}}}{{${catalog}}}{{${'fileset'}}}{{${schema}}}`
dispatch(
setIntoTreeNodes({
- key: `{{${metalake}}}{{${catalog}}}{{${'fileset'}}}{{${schema}}}`,
- data: filesets
+ key: filesetKey,
+ data: mergeWithFunctionNodes({ tree:
getState().metalakes.metalakeTree, key: filesetKey, entities: filesets })
})
)
}
@@ -1588,10 +1615,11 @@ export const fetchTopics = createAsyncThunk(
init &&
getState().metalakes.loadedNodes.includes(`{{${metalake}}}{{${catalog}}}{{${'messaging'}}}{{${schema}}}`)
) {
+ const topicKey =
`{{${metalake}}}{{${catalog}}}{{${'messaging'}}}{{${schema}}}`
await dispatch(
setIntoTreeNodes({
- key: `{{${metalake}}}{{${catalog}}}{{${'messaging'}}}{{${schema}}}`,
- data: topics
+ key: topicKey,
+ data: mergeWithFunctionNodes({ tree:
getState().metalakes.metalakeTree, key: topicKey, entities: topics })
})
)
}
@@ -1803,10 +1831,11 @@ export const fetchModels = createAsyncThunk(
})
if (init &&
getState().metalakes.loadedNodes.includes(`{{${metalake}}}{{${catalog}}}{{${'model'}}}{{${schema}}}`))
{
+ const modelKey =
`{{${metalake}}}{{${catalog}}}{{${'model'}}}{{${schema}}}`
await dispatch(
setIntoTreeNodes({
- key: `{{${metalake}}}{{${catalog}}}{{${'model'}}}{{${schema}}}`,
- data: models
+ key: modelKey,
+ data: mergeWithFunctionNodes({ tree:
getState().metalakes.metalakeTree, key: modelKey, entities: models })
})
)
}
@@ -2105,6 +2134,19 @@ export const fetchFunctions = createAsyncThunk(
}
)
+export const getFunctionDetails = createAsyncThunk(
+ 'appMetalakes/getFunctionDetails',
+ async ({ init, metalake, catalog, schema, functionName }) => {
+ const [err, res] = await to(getFunctionDetailsApi({ metalake, catalog,
schema, functionName }))
+
+ if (err || !res) {
+ throw new Error(err)
+ }
+
+ return { function: res.function || res, init }
+ }
+)
+
export const appMetalakesSlice = createSlice({
name: 'appMetalakes',
initialState: {
@@ -2804,11 +2846,21 @@ export const appMetalakesSlice = createSlice({
builder.addCase(fetchFunctions.fulfilled, (state, action) => {
state.functions = action.payload.functions
})
+ builder.addCase(getFunctionDetails.fulfilled, (state, action) => {
+ if (action.payload.init) {
+ state.activatedDetails = action.payload.function
+ }
+ })
builder.addCase(fetchFunctions.rejected, (state, action) => {
if (!action.error.message.includes('CanceledError')) {
toast.error(action.error.message)
}
})
+ builder.addCase(getFunctionDetails.rejected, (state, action) => {
+ if (!action.error.message.includes('CanceledError')) {
+ toast.error(action.error.message)
+ }
+ })
builder.addCase(getVersionDetails.rejected, (state, action) => {
if (!action.error.message.includes('CanceledError')) {
toast.error(action.error.message)
diff --git a/web-v2/web/src/lib/utils/initial.js
b/web-v2/web/src/lib/utils/initial.js
deleted file mode 100644
index 1690c3573b..0000000000
--- a/web-v2/web/src/lib/utils/initial.js
+++ /dev/null
@@ -1,775 +0,0 @@
-/*
- * 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.
- */
-
-export const filesetProviders = [
- {
- label: 'Apache Hadoop',
- value: 'hadoop',
- defaultProps: [
- {
- key: 'location',
- value: '',
- required: false,
- description: 'The storage location of the fileset'
- }
- ]
- }
-]
-
-export const messagingProviders = [
- {
- label: 'Apache Kafka',
- value: 'kafka',
- defaultProps: [
- {
- key: 'bootstrap.servers',
- value: '',
- required: true,
- description: 'The Apache Kafka brokers to connect to, allowing for
multiple brokers separated by commas'
- }
- ]
- }
-]
-
-export const providers = [
- {
- label: 'Apache Doris',
- value: 'jdbc-doris',
- defaultProps: [
- {
- key: 'jdbc-driver',
- value: '',
- required: true,
- description: 'e.g. com.mysql.jdbc.Driver'
- },
- {
- key: 'jdbc-url',
- value: '',
- required: true,
- description: 'e.g. jdbc:mysql://localhost:9030'
- },
- {
- key: 'jdbc-user',
- value: '',
- required: true
- },
- {
- key: 'jdbc-password',
- value: '',
- required: true
- }
- ]
- },
- {
- label: 'Apache Hive',
- value: 'hive',
- defaultProps: [
- {
- key: 'metastore.uris',
- value: '',
- required: true,
- description: 'The Apache Hive metastore URIs'
- }
- ]
- },
- {
- label: 'Apache Hudi',
- value: 'lakehouse-hudi',
- defaultProps: [
- {
- key: 'catalog-backend',
- value: 'hms',
- defaultValue: 'hms',
- required: true,
- select: ['hms'],
- description: 'Apache Hudi catalog type choose properties'
- },
- {
- key: 'uri',
- value: '',
- required: true,
- description: 'Apache Hudi catalog uri config'
- }
- ]
- },
- {
- label: 'Apache Iceberg',
- value: 'lakehouse-iceberg',
- defaultProps: [
- {
- key: 'catalog-backend',
- value: 'hive',
- defaultValue: 'hive',
- required: true,
- description: 'Apache Iceberg catalog type choose properties',
- select: ['hive', 'jdbc', 'rest']
- },
- {
- key: 'uri',
- value: '',
- required: true,
- description: 'Apache Iceberg catalog uri config'
- },
- {
- key: 'warehouse',
- value: '',
- description: 'Apache Iceberg catalog warehouse config'
- },
- {
- key: 'jdbc-driver',
- value: '',
- required: true,
- parentField: 'catalog-backend',
- hide: ['hive', 'rest'],
- description: `"com.mysql.jdbc.Driver" or "com.mysql.cj.jdbc.Driver"
for MySQL, "org.postgresql.Driver" for PostgreSQL`
- },
- {
- key: 'jdbc-user',
- value: '',
- required: true,
- parentField: 'catalog-backend',
- hide: ['hive', 'rest']
- },
- {
- key: 'jdbc-password',
- value: '',
- required: true,
- parentField: 'catalog-backend',
- hide: ['hive', 'rest']
- },
- {
- key: 'authentication.type',
- value: 'simple',
- defaultValue: 'simple',
- required: false,
- description:
- 'The type of authentication for Paimon catalog backend, currently
Gravitino only supports Kerberos and simple',
- select: ['simple', 'Kerberos']
- },
- {
- key: 'authentication.kerberos.principal',
- value: '',
- required: true,
- description: 'The principal of the Kerberos authentication.',
- parentField: 'authentication.type',
- hide: ['simple']
- },
- {
- key: 'authentication.kerberos.keytab-uri',
- value: '',
- required: true,
- description: 'The URI of The keytab for the Kerberos authentication.',
- parentField: 'authentication.type',
- hide: ['simple']
- }
- ]
- },
- {
- label: 'Apache Paimon',
- value: 'lakehouse-paimon',
- defaultProps: [
- {
- key: 'catalog-backend',
- value: 'filesystem',
- defaultValue: 'filesystem',
- required: true,
- select: ['filesystem', 'hive', 'jdbc']
- },
- {
- key: 'uri',
- value: '',
- required: true,
- description:
- 'e.g. thrift://127.0.0.1:9083 or
jdbc:postgresql://127.0.0.1:5432/db_name or
jdbc:mysql://127.0.0.1:3306/metastore_db',
- parentField: 'catalog-backend',
- hide: ['filesystem']
- },
- {
- key: 'warehouse',
- value: '',
- required: true,
- description: 'e.g. file:///user/hive/warehouse-paimon/ or
hdfs://namespace/hdfs/path'
- },
- {
- key: 'jdbc-driver',
- value: '',
- required: true,
- parentField: 'catalog-backend',
- hide: ['hive', 'filesystem'],
- description: `"com.mysql.jdbc.Driver" or "com.mysql.cj.jdbc.Driver"
for MySQL, "org.postgresql.Driver" for PostgreSQL`
- },
- {
- key: 'jdbc-user',
- value: '',
- required: true,
- parentField: 'catalog-backend',
- hide: ['hive', 'filesystem']
- },
- {
- key: 'jdbc-password',
- value: '',
- required: true,
- parentField: 'catalog-backend',
- hide: ['hive', 'filesystem']
- },
- {
- key: 'authentication.type',
- value: 'simple',
- defaultValue: 'simple',
- required: false,
- description:
- 'The type of authentication for Paimon catalog backend, currently
Gravitino only supports Kerberos and simple',
- select: ['simple', 'Kerberos']
- },
- {
- key: 'authentication.kerberos.principal',
- value: '',
- required: true,
- description: 'The principal of the Kerberos authentication.',
- parentField: 'authentication.type',
- hide: ['simple']
- },
- {
- key: 'authentication.kerberos.keytab-uri',
- value: '',
- required: true,
- description: 'The URI of The keytab for the Kerberos authentication.',
- parentField: 'authentication.type',
- hide: ['simple']
- }
- ]
- },
- {
- label: 'Lakehouse Generic',
- value: 'lakehouse-generic',
- defaultProps: []
- },
- {
- label: 'MySQL',
- value: 'jdbc-mysql',
- defaultProps: [
- {
- key: 'jdbc-driver',
- value: '',
- required: true,
- description: 'e.g. com.mysql.jdbc.Driver or com.mysql.cj.jdbc.Driver'
- },
- {
- key: 'jdbc-url',
- value: '',
- required: true,
- description: 'e.g. jdbc:mysql://localhost:3306'
- },
- {
- key: 'jdbc-user',
- value: '',
- required: true
- },
- {
- key: 'jdbc-password',
- value: '',
- required: true
- }
- ]
- },
- {
- label: 'OceanBase',
- value: 'jdbc-oceanbase',
- defaultProps: [
- {
- key: 'jdbc-driver',
- value: '',
- required: true,
- description: 'e.g. com.mysql.jdbc.Driver or com.mysql.cj.jdbc.Driver
or com.oceanbase.jdbc.Driver'
- },
- {
- key: 'jdbc-url',
- value: '',
- required: true,
- description: 'e.g. jdbc:mysql://localhost:2881 or
jdbc:oceanbase://localhost:2881'
- },
- {
- key: 'jdbc-user',
- value: '',
- required: true
- },
- {
- key: 'jdbc-password',
- value: '',
- required: true
- }
- ]
- },
- {
- label: 'PostgreSQL',
- value: 'jdbc-postgresql',
- defaultProps: [
- {
- key: 'jdbc-driver',
- value: '',
- required: true,
- description: 'e.g. org.postgresql.Driver'
- },
- {
- key: 'jdbc-url',
- value: '',
- required: true,
- description: 'e.g. jdbc:postgresql://localhost:5432/your_database'
- },
- {
- key: 'jdbc-user',
- value: '',
- required: true
- },
- {
- key: 'jdbc-password',
- value: '',
- required: true
- },
- {
- key: 'jdbc-database',
- value: '',
- required: true
- }
- ]
- },
- {
- label: 'StarRocks',
- value: 'jdbc-starrocks',
- defaultProps: [
- {
- key: 'jdbc-driver',
- value: '',
- required: true,
- description: 'e.g. com.mysql.jdbc.Driver'
- },
- {
- key: 'jdbc-url',
- value: '',
- required: true,
- description: 'e.g. jdbc:mysql://localhost:9030'
- },
- {
- key: 'jdbc-user',
- value: '',
- required: true
- },
- {
- key: 'jdbc-password',
- value: '',
- required: true
- }
- ]
- }
-]
-
-const parameterizedColumnTypes = {
- char: {
- params: ['length'],
- validateParams: params => {
- if (params.length !== 1) {
- return {
- valid: false,
- message: 'Please set length'
- }
- }
-
- const length = params[0]
-
- if (length <= 0) {
- return {
- valid: false,
- message: 'The length must be greater than 0'
- }
- }
-
- return {
- valid: true
- }
- }
- },
- decimal: {
- params: ['precision', 'scale'],
- validateParams: params => {
- if (params.length !== 2) {
- return {
- valid: false,
- message: 'Please set precision and scale'
- }
- }
-
- const [param1, param2] = params
- if (param1 <= 0 || param1 > 38) {
- return {
- valid: false,
- message: 'The precision must be between 1 and 38'
- }
- }
-
- if (param2 < 0 || param2 > param1) {
- return {
- valid: false,
- message: 'The scale must be between 0 and the precision'
- }
- }
-
- return {
- valid: true
- }
- }
- },
- fixed: {
- params: ['length'],
- validateParams: params => {
- if (params.length !== 1) {
- return {
- valid: false,
- message: 'Please set length'
- }
- }
-
- const length = params[0]
-
- if (length <= 0) {
- return {
- valid: false,
- message: 'The length must be greater than 0'
- }
- }
-
- return {
- valid: true
- }
- }
- },
- varchar: {
- params: ['length'],
- validateParams: params => {
- if (params.length !== 1) {
- return {
- valid: false,
- message: 'Please set length'
- }
- }
-
- const length = params[0]
-
- if (length <= 0) {
- return {
- valid: false,
- message: 'The length must be greater than 0'
- }
- }
-
- return {
- valid: true
- }
- }
- }
-}
-
-export const getParameterizedColumnType = type => {
- if (Object.hasOwn(parameterizedColumnTypes, type)) {
- return parameterizedColumnTypes[type]
- }
-}
-
-const relationalColumnTypeMap = {
- 'lakehouse-iceberg': [
- 'binary',
- 'boolean',
- 'date',
- 'decimal',
- 'double',
- 'fixed',
- 'float',
- 'integer',
- 'long',
- 'string',
- 'time',
- 'timestamp',
- 'timestamp_tz',
- 'uuid'
- ],
- hive: [
- 'binary',
- 'boolean',
- 'byte',
- 'char',
- 'date',
- 'decimal',
- 'double',
- 'float',
- 'integer',
- 'interval_day',
- 'interval_year',
- 'long',
- 'short',
- 'string',
- 'timestamp',
- 'varchar'
- ],
- 'jdbc-mysql': [
- 'binary',
- 'byte',
- 'byte unsigned',
- 'char',
- 'date',
- 'decimal',
- 'double',
- 'float',
- 'integer',
- 'integer unsigned',
- 'long',
- 'long unsigned',
- 'short',
- 'short unsigned',
- 'string',
- 'time',
- 'timestamp',
- 'varchar'
- ],
- 'jdbc-postgresql': [
- 'binary',
- 'boolean',
- 'char',
- 'date',
- 'decimal',
- 'double',
- 'float',
- 'integer',
- 'long',
- 'short',
- 'string',
- 'time',
- 'timestamp',
- 'timestamp_tz',
- 'varchar'
- ],
- 'jdbc-starrocks': [
- 'binary',
- 'boolean',
- 'byte',
- 'char',
- 'date',
- 'decimal',
- 'double',
- 'float',
- 'integer',
- 'long',
- 'short',
- 'string',
- 'timestamp',
- 'varchar'
- ],
- 'jdbc-doris': [
- 'boolean',
- 'byte',
- 'char',
- 'date',
- 'decimal',
- 'double',
- 'float',
- 'integer',
- 'long',
- 'short',
- 'string',
- 'timestamp',
- 'varchar'
- ],
- 'lakehouse-paimon': [
- 'binary',
- 'boolean',
- 'byte',
- 'char',
- 'date',
- 'decimal',
- 'double',
- 'fixed',
- 'float',
- 'integer',
- 'long',
- 'short',
- 'string',
- 'time',
- 'timestamp',
- 'timestamp_tz',
- 'varchar'
- ],
- 'lakehouse-generic': [
- 'binary',
- 'boolean',
- 'byte',
- 'date',
- 'decimal',
- 'double',
- 'fixed',
- 'float',
- 'integer',
- 'interval_day',
- 'interval_year',
- 'list',
- 'long',
- 'map',
- 'short',
- 'struct',
- 'string',
- 'time',
- 'timestamp',
- 'union'
- ],
- 'jdbc-oceanbase': [
- 'binary',
- 'byte',
- 'byte unsigned',
- 'char',
- 'date',
- 'decimal',
- 'double',
- 'float',
- 'integer',
- 'integer unsigned',
- 'long',
- 'long unsigned',
- 'short',
- 'short unsigned',
- 'string',
- 'time',
- 'timestamp',
- 'varchar'
- ]
-}
-
-export const getRelationalColumnType = catalog => {
- if (Object.hasOwn(relationalColumnTypeMap, catalog)) {
- return relationalColumnTypeMap[catalog]
- }
-
- return []
-}
-
-const relationalTablePropInfoMap = {
- hive: {
- reserved: ['comment', 'EXTERNAL', 'numFiles', 'totalSize',
'transient_lastDdlTime'],
- immutable: [
- 'format',
- 'input-format',
- 'location',
- 'output-format',
- 'serde-name',
- 'serde-lib',
- 'serde.parameter',
- 'table-type'
- ],
- allowDelete: true,
- allowAdd: true
- },
- 'jdbc-starrocks': {
- reserved: [],
- allowDelete: true,
- allowAdd: true
- },
- 'jdbc-doris': {
- reserved: [],
- allowDelete: true,
- allowAdd: true
- },
- 'jdbc-mysql': {
- reserved: [],
- immutable: ['auto-increment-offset', 'engine'],
- allowDelete: false,
- allowAdd: true
- },
- 'jdbc-oceanbase': {
- reserved: [],
- immutable: [],
- allowDelete: false,
- allowAdd: false
- },
- 'jdbc-postgresql': {
- reserved: [],
- immutable: [],
- allowDelete: false,
- allowAdd: false
- },
- 'lakehouse-hudi': {
- reserved: [],
- immutable: [],
- allowDelete: true,
- allowAdd: true
- },
- 'lakehouse-iceberg': {
- reserved: [
- 'cherry-pick-snapshot-id',
- 'comment',
- 'creator',
- 'current-snapshot-id',
- 'identifier-fields',
- 'sort-order',
- 'write.distribution-mode'
- ], // Can't be set or modified
- immutable: ['location', 'provider', 'format', 'format-version'], // Can't
be modified after creation
- allowDelete: true,
- allowAdd: true
- },
- 'lakehouse-paimon': {
- reserved: [
- 'bucket-key',
- 'comment',
- 'merge-engine',
- 'owner',
- 'partition',
- 'primary-key',
- 'rowkind.field',
- 'sequence.field'
- ],
- immutable: ['merge-engine', 'rowkind.field', 'sequence.field'],
- allowDelete: true,
- allowAdd: true
- },
- 'lakehouse-generic': {
- reserved: [],
- immutable: ['format', 'location'],
- allowDelete: true,
- allowAdd: true
- },
- kafka: {
- reserved: [],
- immutable: ['replication-factor'],
- allowAdd: true
- },
- fileset: {
- reserved: [],
- immutable: ['default-location-name'],
- allowAdd: true
- }
-}
-
-export const getRelationalTablePropInfo = catalog => {
- if (Object.hasOwn(relationalTablePropInfoMap, catalog)) {
- return relationalTablePropInfoMap[catalog]
- }
-
- return {
- reserved: [],
- immutable: [],
- allowDelete: true,
- allowAdd: true
- }
-}