diff --git a/integration-test/src/test/java/com/datastrato/gravitino/integration/test/web/ui/CatalogsPageTest.java b/integration-test/src/test/java/com/datastrato/gravitino/integration/test/web/ui/CatalogsPageTest.java index e4399771521..0173539e031 100644 --- a/integration-test/src/test/java/com/datastrato/gravitino/integration/test/web/ui/CatalogsPageTest.java +++ b/integration-test/src/test/java/com/datastrato/gravitino/integration/test/web/ui/CatalogsPageTest.java @@ -29,6 +29,7 @@ public class CatalogsPageTest extends AbstractWebIT { private static final String metalakeName = "metalake_name"; String catalogName = "catalog_name"; + String catalogType = "relational"; String modifiedCatalogName = catalogName + "_edited"; String schemaName = "default"; String tableName = "employee"; @@ -124,21 +125,22 @@ public void testEditCatalog() throws InterruptedException { @Test @Order(6) public void testClickCatalogLink() { - catalogsPage.clickCatalogLink(metalakeName, modifiedCatalogName); + catalogsPage.clickCatalogLink(metalakeName, modifiedCatalogName, catalogType); Assertions.assertTrue(catalogsPage.verifyShowTableTitle("Schemas")); } @Test @Order(7) public void testClickSchemaLink() { - catalogsPage.clickSchemaLink(metalakeName, modifiedCatalogName, schemaName); + catalogsPage.clickSchemaLink(metalakeName, modifiedCatalogName, catalogType, schemaName); Assertions.assertTrue(catalogsPage.verifyShowTableTitle("Tables")); } @Test @Order(8) public void testClickTableLink() { - catalogsPage.clickTableLink(metalakeName, modifiedCatalogName, schemaName, tableName); + catalogsPage.clickTableLink( + metalakeName, modifiedCatalogName, catalogType, schemaName, tableName); Assertions.assertTrue(catalogsPage.verifyShowTableTitle("Columns")); Assertions.assertTrue(catalogsPage.verifyTableColumns()); } diff --git a/integration-test/src/test/java/com/datastrato/gravitino/integration/test/web/ui/pages/CatalogsPage.java b/integration-test/src/test/java/com/datastrato/gravitino/integration/test/web/ui/pages/CatalogsPage.java index 5c22a9be224..e4c68cd5fcb 100644 --- a/integration-test/src/test/java/com/datastrato/gravitino/integration/test/web/ui/pages/CatalogsPage.java +++ b/integration-test/src/test/java/com/datastrato/gravitino/integration/test/web/ui/pages/CatalogsPage.java @@ -174,9 +174,16 @@ public void clickDeleteCatalogBtn(String name) { } } - public void clickCatalogLink(String metalakeName, String catalogName) { + public void clickCatalogLink(String metalakeName, String catalogName, String catalogType) { try { - String xpath = "//a[@href='?metalake=" + metalakeName + "&catalog=" + catalogName + "']"; + String xpath = + "//a[@href='?metalake=" + + metalakeName + + "&catalog=" + + catalogName + + "&type=" + + catalogType + + "']"; WebElement link = tableGrid.findElement(By.xpath(xpath)); WebDriverWait wait = new WebDriverWait(driver, MAX_TIMEOUT); wait.until(ExpectedConditions.elementToBeClickable(By.xpath(xpath))); @@ -186,13 +193,16 @@ public void clickCatalogLink(String metalakeName, String catalogName) { } } - public void clickSchemaLink(String metalakeName, String catalogName, String schemaName) { + public void clickSchemaLink( + String metalakeName, String catalogName, String catalogType, String schemaName) { try { String xpath = "//a[@href='?metalake=" + metalakeName + "&catalog=" + catalogName + + "&type=" + + catalogType + "&schema=" + schemaName + "']"; @@ -206,13 +216,19 @@ public void clickSchemaLink(String metalakeName, String catalogName, String sche } public void clickTableLink( - String metalakeName, String catalogName, String schemaName, String tableName) { + String metalakeName, + String catalogName, + String catalogType, + String schemaName, + String tableName) { try { String xpath = "//a[@href='?metalake=" + metalakeName + "&catalog=" + catalogName + + "&type=" + + catalogType + "&schema=" + schemaName + "&table=" diff --git a/web/src/app/metalakes/metalake/MetalakeTree.js b/web/src/app/metalakes/metalake/MetalakeTree.js index f2642e9a8d1..4b807eaf15c 100644 --- a/web/src/app/metalakes/metalake/MetalakeTree.js +++ b/web/src/app/metalakes/metalake/MetalakeTree.js @@ -22,7 +22,8 @@ import { removeExpandedNode, setSelectedNodes, setLoadedNodes, - getTableDetails + getTableDetails, + getFilesetDetails } from '@/lib/store/metalakes' import { extractPlaceholder } from '@/lib/utils' @@ -47,6 +48,12 @@ const MetalakeTree = props => { const [metalake, catalog, schema, table] = pathArr dispatch(getTableDetails({ init: true, metalake, catalog, schema, table })) } + } else if (nodeProps.data.node === 'fileset') { + if (store.selectedNodes.includes(nodeProps.data.key)) { + const pathArr = extractPlaceholder(nodeProps.data.key) + const [metalake, catalog, schema, fileset] = pathArr + dispatch(getFilesetDetails({ init: true, metalake, catalog, schema, fileset })) + } } else { dispatch(setIntoTreeNodeWithFetch({ key: nodeProps.data.key })) } @@ -143,6 +150,19 @@ const MetalakeTree = props => { ) + case 'fileset': + return ( + handleClickIcon(e, nodeProps)} + onMouseEnter={e => onMouseEnter(e, nodeProps)} + onMouseLeave={e => onMouseLeave(e, nodeProps)} + > + + + ) default: return <> diff --git a/web/src/app/metalakes/metalake/MetalakeView.js b/web/src/app/metalakes/metalake/MetalakeView.js index 116c0428367..e700ff145fc 100644 --- a/web/src/app/metalakes/metalake/MetalakeView.js +++ b/web/src/app/metalakes/metalake/MetalakeView.js @@ -18,10 +18,12 @@ import { fetchCatalogs, fetchSchemas, fetchTables, + fetchFilesets, getMetalakeDetails, getCatalogDetails, getSchemaDetails, getTableDetails, + getFilesetDetails, setSelectedNodes } from '@/lib/store/metalakes' @@ -35,39 +37,51 @@ const MetalakeView = () => { const routeParams = { metalake: searchParams.get('metalake'), catalog: searchParams.get('catalog'), + type: searchParams.get('type'), schema: searchParams.get('schema'), - table: searchParams.get('table') + table: searchParams.get('table'), + fileset: searchParams.get('fileset') } if ([...searchParams.keys()].length) { - const { metalake, catalog, schema, table } = routeParams + const { metalake, catalog, type, schema, table, fileset } = routeParams if (paramsSize === 1 && metalake) { dispatch(fetchCatalogs({ init: true, page: 'metalakes', metalake })) dispatch(getMetalakeDetails({ metalake })) } - if (paramsSize === 2 && catalog) { - dispatch(fetchSchemas({ init: true, page: 'catalogs', metalake, catalog })) - dispatch(getCatalogDetails({ metalake, catalog })) + if (paramsSize === 3 && catalog) { + dispatch(fetchSchemas({ init: true, page: 'catalogs', metalake, catalog, type })) + dispatch(getCatalogDetails({ metalake, catalog, type })) } - if (paramsSize === 3 && catalog && schema) { - dispatch(fetchTables({ init: true, page: 'schemas', metalake, catalog, schema })) + if (paramsSize === 4 && catalog && type && schema) { + if (type === 'fileset') { + dispatch(fetchFilesets({ init: true, page: 'schemas', metalake, catalog, schema })) + } else { + dispatch(fetchTables({ init: true, page: 'schemas', metalake, catalog, schema })) + } dispatch(getSchemaDetails({ metalake, catalog, schema })) } - if (paramsSize === 4 && catalog && schema && table) { + if (paramsSize === 5 && catalog && schema && table) { dispatch(getTableDetails({ init: true, metalake, catalog, schema, table })) } + + if (paramsSize === 5 && catalog && schema && fileset) { + dispatch(getFilesetDetails({ init: true, metalake, catalog, schema, fileset })) + } } dispatch( setSelectedNodes( routeParams.catalog ? [ - `{{${routeParams.metalake}}}{{${routeParams.catalog}}}${ + `{{${routeParams.metalake}}}{{${routeParams.catalog}}}{{${routeParams.type}}}${ routeParams.schema ? `{{${routeParams.schema}}}` : '' - }${routeParams.table ? `{{${routeParams.table}}}` : ''}` + }${routeParams.table ? `{{${routeParams.table}}}` : ''}${ + routeParams.fileset ? `{{${routeParams.fileset}}}` : '' + }` ] : [] ) diff --git a/web/src/app/metalakes/metalake/rightContent/CreateCatalogDialog.js b/web/src/app/metalakes/metalake/rightContent/CreateCatalogDialog.js index 6580ce89aa3..63963eb1e0d 100644 --- a/web/src/app/metalakes/metalake/rightContent/CreateCatalogDialog.js +++ b/web/src/app/metalakes/metalake/rightContent/CreateCatalogDialog.js @@ -43,7 +43,7 @@ import { useSearchParams } from 'next/navigation' const defaultValues = { name: '', type: 'relational', - provider: 'hive', + provider: '', comment: '', propItems: providers[0].defaultProps } @@ -58,7 +58,7 @@ const schema = yup.object().shape({ nameRegex, 'This field must start with a letter or underscore, and can only contain letters, numbers, and underscores' ), - type: yup.mixed().oneOf(['relational']).required(), + type: yup.mixed().oneOf(['relational', 'fileset']).required(), provider: yup.mixed().oneOf(providerTypeValues).required(), propItems: yup.array().of( yup.object().shape({ @@ -87,6 +87,8 @@ const CreateCatalogDialog = props => { const [cacheData, setCacheData] = useState() + const [providerTypes, setProviderTypes] = useState(providers) + const { control, reset, @@ -103,6 +105,7 @@ const CreateCatalogDialog = props => { }) const providerSelect = watch('provider') + const typeSelect = watch('type') const handleFormChange = ({ index, event }) => { let data = [...innerProps] @@ -280,6 +283,18 @@ const CreateCatalogDialog = props => { console.error('fields error', errors) } + useEffect(() => { + if (typeSelect === 'fileset') { + setProviderTypes(providers.filter(p => p.value === 'hadoop')) + setValue('provider', 'hadoop') + } else { + setProviderTypes(providers.filter(p => p.value !== 'hadoop')) + setValue('provider', 'hive') + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [typeSelect, open]) + useEffect(() => { let defaultProps = [] @@ -410,6 +425,7 @@ const CreateCatalogDialog = props => { disabled={type === 'update'} > relational + fileset )} /> @@ -436,10 +452,13 @@ const CreateCatalogDialog = props => { labelId='select-catalog-provider' disabled={type === 'update'} > - hive - iceberg - mysql - postgresql + {providerTypes.map(item => { + return ( + + {item.label} + + ) + })} )} /> diff --git a/web/src/app/metalakes/metalake/rightContent/MetalakePath.js b/web/src/app/metalakes/metalake/rightContent/MetalakePath.js index 58d6e3a1f17..4f7a400535a 100644 --- a/web/src/app/metalakes/metalake/rightContent/MetalakePath.js +++ b/web/src/app/metalakes/metalake/rightContent/MetalakePath.js @@ -24,16 +24,19 @@ const MetalakePath = props => { const routeParams = { metalake: searchParams.get('metalake'), catalog: searchParams.get('catalog'), + type: searchParams.get('type'), schema: searchParams.get('schema'), - table: searchParams.get('table') + table: searchParams.get('table'), + fileset: searchParams.get('fileset') } - const { metalake, catalog, schema, table } = routeParams + const { metalake, catalog, type, schema, table, fileset } = routeParams const metalakeUrl = `?metalake=${metalake}` - const catalogUrl = `?metalake=${metalake}&catalog=${catalog}` - const schemaUrl = `?metalake=${metalake}&catalog=${catalog}&schema=${schema}` - const tableUrl = `?metalake=${metalake}&catalog=${catalog}&schema=${schema}&table=${table}` + const catalogUrl = `?metalake=${metalake}&catalog=${catalog}&type=${type}` + const schemaUrl = `?metalake=${metalake}&catalog=${catalog}&type=${type}&schema=${schema}` + const tableUrl = `?metalake=${metalake}&catalog=${catalog}&type=${type}&schema=${schema}&table=${table}` + const filesetUrl = `?metalake=${metalake}&catalog=${catalog}&type=${type}&schema=${schema}&fileset=${fileset}` const handleClick = (event, path) => { path === `?${searchParams.toString()}` && event.preventDefault() @@ -91,6 +94,19 @@ const MetalakePath = props => { )} + {fileset && ( + + handleClick(event, filesetUrl)} + underline='hover' + > + + {fileset} + + + )} ) } diff --git a/web/src/app/metalakes/metalake/rightContent/tabsContent/TabsContent.js b/web/src/app/metalakes/metalake/rightContent/tabsContent/TabsContent.js index 00ae259f367..7ae1a86543b 100644 --- a/web/src/app/metalakes/metalake/rightContent/tabsContent/TabsContent.js +++ b/web/src/app/metalakes/metalake/rightContent/tabsContent/TabsContent.js @@ -5,7 +5,7 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import Box from '@mui/material/Box' import Tab from '@mui/material/Tab' @@ -49,44 +49,60 @@ const CustomTabPanel = props => { } const TabsContent = () => { + let tableTitle = '' + const searchParams = useSearchParams() + const paramsSize = [...searchParams.keys()].length + const type = searchParams.get('type') const [tab, setTab] = useState('table') + const isNotNeedTableTab = type && type === 'fileset' && paramsSize === 5 const handleChangeTab = (event, newValue) => { setTab(newValue) } - let tableTitle = '' - const searchParams = useSearchParams() - const paramsSize = [...searchParams.keys()].length - switch (paramsSize) { case 1: tableTitle = 'Catalogs' break - case 2: - tableTitle = 'Schemas' - break case 3: - tableTitle = 'Tables' + tableTitle = 'Schemas' break case 4: + tableTitle = type === 'fileset' ? 'Filesets' : 'Tables' + break + case 5: tableTitle = 'Columns' break default: break } + useEffect(() => { + if (isNotNeedTableTab) { + setTab('details') + } else { + setTab('table') + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchParams]) + return ( - + {!isNotNeedTableTab ? ( + + ) : null} - - - + {!isNotNeedTableTab ? ( + + + + ) : null} + diff --git a/web/src/app/metalakes/metalake/rightContent/tabsContent/tableView/TableView.js b/web/src/app/metalakes/metalake/rightContent/tabsContent/tableView/TableView.js index 6ec15defc54..4c0ee996f5b 100644 --- a/web/src/app/metalakes/metalake/rightContent/tabsContent/tableView/TableView.js +++ b/web/src/app/metalakes/metalake/rightContent/tabsContent/tableView/TableView.js @@ -170,7 +170,7 @@ const TableView = () => { title='Delete' size='small' sx={{ color: theme => theme.palette.error.light }} - onClick={() => handleDelete({ name: row.name, type: 'catalog' })} + onClick={() => handleDelete({ name: row.name, type: 'catalog', catalogType: row.type })} data-refer={`delete-catalog-${row.name}`} > @@ -345,9 +345,9 @@ const TableView = () => { } } - const handleDelete = ({ name, type }) => { + const handleDelete = ({ name, type, catalogType }) => { setOpenConfirmDelete(true) - setConfirmCacheData({ name, type }) + setConfirmCacheData({ name, type, catalogType }) } const handleCloseConfirm = () => { @@ -358,7 +358,7 @@ const TableView = () => { const handleConfirmDeleteSubmit = () => { if (confirmCacheData) { if (confirmCacheData.type === 'catalog') { - dispatch(deleteCatalog({ metalake, catalog: confirmCacheData.name })) + dispatch(deleteCatalog({ metalake, catalog: confirmCacheData.name, type: confirmCacheData.catalogType })) } setOpenConfirmDelete(false) @@ -368,7 +368,7 @@ const TableView = () => { const checkColumns = () => { if (paramsSize == 1 && searchParams.has('metalake')) { return catalogsColumns - } else if (paramsSize == 4 && searchParams.has('table')) { + } else if (paramsSize == 5 && searchParams.has('table')) { return tableColumns } else { return columns diff --git a/web/src/lib/api/catalogs/index.js b/web/src/lib/api/catalogs/index.js index 45c1c6eb3fe..b8acee1fdd0 100644 --- a/web/src/lib/api/catalogs/index.js +++ b/web/src/lib/api/catalogs/index.js @@ -6,7 +6,7 @@ import { defHttp } from '@/lib/utils/axios' const Apis = { - GET: ({ metalake }) => `/api/metalakes/${metalake}/catalogs`, + GET: ({ metalake }) => `/api/metalakes/${metalake}/catalogs?details=true`, GET_DETAIL: ({ metalake, catalog }) => `/api/metalakes/${metalake}/catalogs/${catalog}`, CREATE: ({ metalake }) => `/api/metalakes/${metalake}/catalogs`, UPDATE: ({ metalake, catalog }) => `/api/metalakes/${metalake}/catalogs/${catalog}`, diff --git a/web/src/lib/api/filesets/index.js b/web/src/lib/api/filesets/index.js new file mode 100644 index 00000000000..a2e03b970d8 --- /dev/null +++ b/web/src/lib/api/filesets/index.js @@ -0,0 +1,24 @@ +/* + * Copyright 2024 Datastrato Pvt Ltd. + * This software is licensed under the Apache License version 2. + */ + +import { defHttp } from '@/lib/utils/axios' + +const Apis = { + GET: ({ metalake, catalog, schema }) => `/api/metalakes/${metalake}/catalogs/${catalog}/schemas/${schema}/filesets`, + GET_DETAIL: ({ metalake, catalog, schema, fileset }) => + `/api/metalakes/${metalake}/catalogs/${catalog}/schemas/${schema}/filesets/${fileset}` +} + +export const getFilesetsApi = params => { + return defHttp.get({ + url: `${Apis.GET(params)}` + }) +} + +export const getFilesetDetailsApi = ({ metalake, catalog, schema, fileset }) => { + return defHttp.get({ + url: `${Apis.GET_DETAIL({ metalake, catalog, schema, fileset })}` + }) +} diff --git a/web/src/lib/store/metalakes/index.js b/web/src/lib/store/metalakes/index.js index 6e263996576..5c16154feb4 100644 --- a/web/src/lib/store/metalakes/index.js +++ b/web/src/lib/store/metalakes/index.js @@ -6,6 +6,7 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' import { to, extractPlaceholder, updateTreeData, findInTree } from '@/lib/utils' +import toast from 'react-hot-toast' import _ from 'lodash-es' @@ -26,6 +27,7 @@ import { } from '@/lib/api/catalogs' import { getSchemasApi, getSchemaDetailsApi } from '@/lib/api/schemas' import { getTablesApi, getTableDetailsApi } from '@/lib/api/tables' +import { getFilesetsApi, getFilesetDetailsApi } from '@/lib/api/filesets' export const fetchMetalakes = createAsyncThunk('appMetalakes/fetchMetalakes', async (params, { getState }) => { const [err, res] = await to(getMetalakesApi()) @@ -85,7 +87,7 @@ export const setIntoTreeNodeWithFetch = createAsyncThunk( } const pathArr = extractPlaceholder(key) - const [metalake, catalog, schema, table] = pathArr + const [metalake, catalog, type, schema] = pathArr if (pathArr.length === 1) { const [err, res] = await to(getCatalogsApi({ metalake })) @@ -94,22 +96,23 @@ export const setIntoTreeNodeWithFetch = createAsyncThunk( throw new Error(err) } - const { identifiers = [] } = res + const { catalogs = [] } = res - result.data = identifiers.map(catalogItem => { + result.data = catalogs.map(catalogItem => { return { ...catalogItem, node: 'catalog', - id: `{{${metalake}}}{{${catalogItem.name}}}`, - key: `{{${metalake}}}{{${catalogItem.name}}}`, - path: `?${new URLSearchParams({ metalake, catalog: catalogItem.name }).toString()}`, + id: `{{${metalake}}}{{${catalogItem.name}}}{{${catalogItem.type}}}`, + key: `{{${metalake}}}{{${catalogItem.name}}}{{${catalogItem.type}}}`, + path: `?${new URLSearchParams({ metalake, catalog: catalogItem.name, type: catalogItem.type }).toString()}`, name: catalogItem.name, title: catalogItem.name, + namespace: [metalake], schemas: [], children: [] } }) - } else if (pathArr.length === 2) { + } else if (pathArr.length === 3) { const [err, res] = await to(getSchemasApi({ metalake, catalog })) if (err || !res) { @@ -122,16 +125,16 @@ export const setIntoTreeNodeWithFetch = createAsyncThunk( return { ...schemaItem, node: 'schema', - id: `{{${metalake}}}{{${catalog}}}{{${schemaItem.name}}}`, - key: `{{${metalake}}}{{${catalog}}}{{${schemaItem.name}}}`, - path: `?${new URLSearchParams({ metalake, catalog, schema: schemaItem.name }).toString()}`, + 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: [] } }) - } else if (pathArr.length === 3) { + } else if (pathArr.length === 4 && type !== 'fileset') { const [err, res] = await to(getTablesApi({ metalake, catalog, schema })) const { identifiers = [] } = res @@ -144,9 +147,9 @@ export const setIntoTreeNodeWithFetch = createAsyncThunk( return { ...tableItem, node: 'table', - id: `{{${metalake}}}{{${catalog}}}{{${schema}}}{{${tableItem.name}}}`, - key: `{{${metalake}}}{{${catalog}}}{{${schema}}}{{${tableItem.name}}}`, - path: `?${new URLSearchParams({ metalake, catalog, schema, table: tableItem.name }).toString()}`, + id: `{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema}}}{{${tableItem.name}}}`, + key: `{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema}}}{{${tableItem.name}}}`, + path: `?${new URLSearchParams({ metalake, catalog, type, schema, table: tableItem.name }).toString()}`, name: tableItem.name, title: tableItem.name, isLeaf: true, @@ -154,6 +157,27 @@ export const setIntoTreeNodeWithFetch = createAsyncThunk( children: [] } }) + } else if (pathArr.length === 4 && type === 'fileset') { + const [err, res] = await to(getFilesetsApi({ metalake, catalog, schema })) + + const { identifiers = [] } = res + + if (err || !res) { + throw new Error(err) + } + + result.data = identifiers.map(filesetItem => { + return { + ...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, + isLeaf: true + } + }) } return result @@ -187,17 +211,18 @@ export const fetchCatalogs = createAsyncThunk( throw new Error(err) } - const { identifiers = [] } = res + const { catalogs = [] } = res - const catalogs = identifiers.map(catalog => { + const catalogsData = catalogs.map(catalog => { return { ...catalog, node: 'catalog', - id: `{{${metalake}}}{{${catalog.name}}}`, - key: `{{${metalake}}}{{${catalog.name}}}`, - path: `?${new URLSearchParams({ metalake, catalog: catalog.name }).toString()}`, + id: `{{${metalake}}}{{${catalog.name}}}{{${catalog.type}}}`, + key: `{{${metalake}}}{{${catalog.name}}}{{${catalog.type}}}`, + path: `?${new URLSearchParams({ metalake, catalog: catalog.name, type: catalog.type }).toString()}`, name: catalog.name, title: catalog.name, + namespace: [metalake], schemas: [], children: [] } @@ -214,11 +239,12 @@ export const fetchCatalogs = createAsyncThunk( ? schema.children.map(table => { return { ...table, - id: `{{${metalake}}}{{${update.newCatalog.name}}}{{${schema.name}}}{{${table.name}}}`, - key: `{{${metalake}}}{{${update.newCatalog.name}}}{{${schema.name}}}{{${table.name}}}`, + id: `{{${metalake}}}{{${update.newCatalog.name}}}{{${update.newCatalog.type}}}{{${schema.name}}}{{${table.name}}}`, + key: `{{${metalake}}}{{${update.newCatalog.name}}}{{${update.newCatalog.type}}}{{${schema.name}}}{{${table.name}}}`, path: `?${new URLSearchParams({ metalake, catalog: update.newCatalog.name, + type: update.newCatalog.type, schema: schema.name, table: table.name }).toString()}` @@ -228,11 +254,12 @@ export const fetchCatalogs = createAsyncThunk( return { ...schema, - id: `{{${metalake}}}{{${update.newCatalog.name}}}{{${schema.name}}}`, - key: `{{${metalake}}}{{${update.newCatalog.name}}}{{${schema.name}}}`, + id: `{{${metalake}}}{{${update.newCatalog.name}}}{{${update.newCatalog.type}}}{{${schema.name}}}`, + key: `{{${metalake}}}{{${update.newCatalog.name}}}{{${update.newCatalog.type}}}{{${schema.name}}}`, path: `?${new URLSearchParams({ metalake, catalog: update.newCatalog.name, + type: update.newCatalog.type, schema: schema.name }).toString()}`, tables: tables, @@ -243,9 +270,13 @@ export const fetchCatalogs = createAsyncThunk( return { ...catalog, - id: `{{${metalake}}}{{${update.newCatalog.name}}}`, - key: `{{${metalake}}}{{${update.newCatalog.name}}}`, - path: `?${new URLSearchParams({ metalake, catalog: update.newCatalog.name }).toString()}`, + id: `{{${metalake}}}{{${update.newCatalog.name}}}{{${update.newCatalog.type}}}`, + key: `{{${metalake}}}{{${update.newCatalog.name}}}{{${update.newCatalog.type}}}`, + path: `?${new URLSearchParams({ + metalake, + catalog: update.newCatalog.name, + type: update.newCatalog.type + }).toString()}`, name: update.newCatalog.name, title: update.newCatalog.name, schemas: schemas, @@ -261,9 +292,9 @@ export const fetchCatalogs = createAsyncThunk( const expandedNodes = getState().metalakes.expandedNodes.map(node => { const [metalake, catalog, schema, table] = extractPlaceholder(node) if (catalog === update.catalog) { - const updatedNode = `{{${metalake}}}{{${update.newCatalog.name}}}${schema ? `{{${schema}}}` : ''}${ - table ? `{{${table}}}` : '' - }` + const updatedNode = `{{${metalake}}}{{${update.newCatalog.name}}}{{${update.newCatalog.type}}}${ + schema ? `{{${schema}}}` : '' + }${table ? `{{${table}}}` : ''}` return updatedNode } @@ -277,9 +308,9 @@ export const fetchCatalogs = createAsyncThunk( const loadedNodes = getState().metalakes.loadedNodes.map(node => { const [metalake, catalog, schema, table] = extractPlaceholder(node) if (catalog === update.catalog) { - const updatedNode = `{{${metalake}}}{{${update.newCatalog.name}}}${schema ? `{{${schema}}}` : ''}${ - table ? `{{${table}}}` : '' - }` + const updatedNode = `{{${metalake}}}{{${update.newCatalog.name}}}{{${update.newCatalog.type}}}${ + schema ? `{{${schema}}}` : '' + }${table ? `{{${table}}}` : ''}` return updatedNode } @@ -292,14 +323,14 @@ export const fetchCatalogs = createAsyncThunk( } } else { const mergedTree = _.values( - _.merge(_.keyBy(getState().metalakes.metalakeTree, 'key'), _.keyBy(catalogs, 'key')) + _.merge(_.keyBy(catalogsData, 'key'), _.keyBy(getState().metalakes.metalakeTree, 'key')) ) dispatch(setMetalakeTree(mergedTree)) } } return { - catalogs, + catalogs: catalogsData, page, init } @@ -334,16 +365,17 @@ export const createCatalog = createAsyncThunk( const catalogData = { ...catalogItem, node: 'catalog', - id: `{{${metalake}}}{{${catalogItem.name}}}`, - key: `{{${metalake}}}{{${catalogItem.name}}}`, - path: `?${new URLSearchParams({ metalake, catalog: catalogItem.name }).toString()}`, + id: `{{${metalake}}}{{${catalogItem.name}}}{{${catalogItem.type}}}`, + key: `{{${metalake}}}{{${catalogItem.name}}}{{${catalogItem.type}}}`, + path: `?${new URLSearchParams({ metalake, catalog: catalogItem.name, type: catalogItem.type }).toString()}`, name: catalogItem.name, title: catalogItem.name, + namespace: [metalake], schemas: [], children: [] } - dispatch(dispatch(fetchCatalogs({ metalake, init: true }))) + dispatch(fetchCatalogs({ metalake, init: true })) dispatch(addCatalogToTree(catalogData)) @@ -366,7 +398,7 @@ export const updateCatalog = createAsyncThunk( export const deleteCatalog = createAsyncThunk( 'appMetalakes/deleteCatalog', - async ({ metalake, catalog }, { dispatch }) => { + async ({ metalake, catalog, type }, { dispatch }) => { dispatch(setTableLoading(true)) const [err, res] = await to(deleteCatalogApi({ metalake, catalog })) dispatch(setTableLoading(false)) @@ -377,7 +409,7 @@ export const deleteCatalog = createAsyncThunk( dispatch(fetchCatalogs({ metalake, catalog, page: 'metalakes', init: true })) - dispatch(removeCatalogFromTree(`{{${metalake}}}{{${catalog}}}`)) + dispatch(removeCatalogFromTree(`{{${metalake}}}{{${catalog}}}{{${type}}}`)) return res } @@ -385,7 +417,7 @@ export const deleteCatalog = createAsyncThunk( export const fetchSchemas = createAsyncThunk( 'appMetalakes/fetchSchemas', - async ({ init, page, metalake, catalog }, { getState, dispatch }) => { + async ({ init, page, metalake, catalog, type }, { getState, dispatch }) => { if (init) { dispatch(setTableLoading(true)) } @@ -403,15 +435,15 @@ export const fetchSchemas = createAsyncThunk( const schemaItem = findInTree( getState().metalakes.metalakeTree, 'key', - `{{${metalake}}}{{${catalog}}}{{${schema.name}}}` + `{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema.name}}}` ) return { ...schema, node: 'schema', - id: `{{${metalake}}}{{${catalog}}}{{${schema.name}}}`, - key: `{{${metalake}}}{{${catalog}}}{{${schema.name}}}`, - path: `?${new URLSearchParams({ metalake, catalog, schema: schema.name }).toString()}`, + id: `{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema.name}}}`, + key: `{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema.name}}}`, + path: `?${new URLSearchParams({ metalake, catalog, type, schema: schema.name }).toString()}`, name: schema.name, title: schema.name, tables: schemaItem ? schemaItem.children : [], @@ -419,10 +451,10 @@ export const fetchSchemas = createAsyncThunk( } }) - if (init && getState().metalakes.loadedNodes.includes(`{{${metalake}}}{{${catalog}}}`)) { + if (init && getState().metalakes.loadedNodes.includes(`{{${metalake}}}{{${catalog}}}{{${type}}}`)) { dispatch( setIntoTreeNodes({ - key: `{{${metalake}}}{{${catalog}}}`, + key: `{{${metalake}}}{{${catalog}}}{{${type}}}`, data: schemas, tree: getState().metalakes.metalakeTree }) @@ -433,7 +465,7 @@ export const fetchSchemas = createAsyncThunk( dispatch(fetchCatalogs({ metalake })) } - dispatch(setExpandedNodes([`{{${metalake}}}`, `{{${metalake}}}{{${catalog}}}`])) + dispatch(setExpandedNodes([`{{${metalake}}}`, `{{${metalake}}}{{${catalog}}}{{${type}}}`])) return { schemas, page, init } } @@ -474,9 +506,15 @@ export const fetchTables = createAsyncThunk( return { ...table, node: 'table', - id: `{{${metalake}}}{{${catalog}}}{{${schema}}}{{${table.name}}}`, - key: `{{${metalake}}}{{${catalog}}}{{${schema}}}{{${table.name}}}`, - path: `?${new URLSearchParams({ metalake, catalog, schema, table: table.name }).toString()}`, + id: `{{${metalake}}}{{${catalog}}}{{${'relational'}}}{{${schema}}}{{${table.name}}}`, + key: `{{${metalake}}}{{${catalog}}}{{${'relational'}}}{{${schema}}}{{${table.name}}}`, + path: `?${new URLSearchParams({ + metalake, + catalog, + type: 'relational', + schema, + table: table.name + }).toString()}`, name: table.name, title: table.name, isLeaf: true, @@ -485,10 +523,13 @@ export const fetchTables = createAsyncThunk( } }) - if (init && getState().metalakes.loadedNodes.includes(`{{${metalake}}}{{${catalog}}}{{${schema}}}`)) { + if ( + init && + getState().metalakes.loadedNodes.includes(`{{${metalake}}}{{${catalog}}}{{${'relational'}}}{{${schema}}}`) + ) { dispatch( setIntoTreeNodes({ - key: `{{${metalake}}}{{${catalog}}}{{${schema}}}`, + key: `{{${metalake}}}{{${catalog}}}{{${'relational'}}}{{${schema}}}`, data: tables, tree: getState().metalakes.metalakeTree }) @@ -502,8 +543,8 @@ export const fetchTables = createAsyncThunk( dispatch( setExpandedNodes([ `{{${metalake}}}`, - `{{${metalake}}}{{${catalog}}}`, - `{{${metalake}}}{{${catalog}}}{{${schema}}}` + `{{${metalake}}}{{${catalog}}}{{${'relational'}}}`, + `{{${metalake}}}{{${catalog}}}{{${'relational'}}}{{${schema}}}` ]) ) @@ -535,8 +576,8 @@ export const getTableDetails = createAsyncThunk( dispatch( setExpandedNodes([ `{{${metalake}}}`, - `{{${metalake}}}{{${catalog}}}`, - `{{${metalake}}}{{${catalog}}}{{${schema}}}` + `{{${metalake}}}{{${catalog}}}{{${'relational'}}}`, + `{{${metalake}}}{{${catalog}}}{{${'relational'}}}{{${schema}}}` ]) ) @@ -544,6 +585,104 @@ export const getTableDetails = createAsyncThunk( } ) +export const fetchFilesets = createAsyncThunk( + 'appMetalakes/fetchFilesets', + async ({ init, page, metalake, catalog, schema }, { getState, dispatch }) => { + if (init) { + dispatch(setTableLoading(true)) + } + + const [err, res] = await to(getFilesetsApi({ metalake, catalog, schema })) + dispatch(setTableLoading(false)) + + if (init && (err || !res)) { + dispatch(resetTableData()) + throw new Error(err) + } + + const { identifiers = [] } = res + + const filesets = identifiers.map(fileset => { + return { + ...fileset, + node: 'fileset', + id: `{{${metalake}}}{{${catalog}}}{{${'fileset'}}}{{${schema}}}{{${fileset.name}}}`, + key: `{{${metalake}}}{{${catalog}}}{{${'fileset'}}}{{${schema}}}{{${fileset.name}}}`, + path: `?${new URLSearchParams({ + metalake, + catalog, + type: 'fileset', + schema, + fileset: fileset.name + }).toString()}`, + name: fileset.name, + title: fileset.name, + isLeaf: true + } + }) + + if ( + init && + getState().metalakes.loadedNodes.includes(`{{${metalake}}}{{${catalog}}}{{${'fileset'}}}{{${schema}}}`) + ) { + dispatch( + setIntoTreeNodes({ + key: `{{${metalake}}}{{${catalog}}}{{${'fileset'}}}{{${schema}}}`, + data: filesets, + tree: getState().metalakes.metalakeTree + }) + ) + } + + if (getState().metalakes.metalakeTree.length === 0) { + dispatch(fetchCatalogs({ metalake })) + } + + dispatch( + setExpandedNodes([ + `{{${metalake}}}`, + `{{${metalake}}}{{${catalog}}}{{${'fileset'}}}`, + `{{${metalake}}}{{${catalog}}}{{${'fileset'}}}{{${schema}}}` + ]) + ) + + return { filesets, page, init } + } +) + +export const getFilesetDetails = createAsyncThunk( + 'appMetalakes/getFilesetDetails', + async ({ init, metalake, catalog, schema, fileset }, { getState, dispatch }) => { + dispatch(resetTableData()) + if (init) { + dispatch(setTableLoading(true)) + } + const [err, res] = await to(getFilesetDetailsApi({ metalake, catalog, schema, fileset })) + dispatch(setTableLoading(false)) + + if (err || !res) { + dispatch(resetTableData()) + throw new Error(err) + } + + const { fileset: resFileset } = res + + if (getState().metalakes.metalakeTree.length === 0) { + dispatch(fetchCatalogs({ metalake })) + } + + dispatch( + setExpandedNodes([ + `{{${metalake}}}`, + `{{${metalake}}}{{${catalog}}}{{${'fileset'}}}`, + `{{${metalake}}}{{${catalog}}}{{${'fileset'}}}{{${schema}}}` + ]) + ) + + return resFileset + } +) + export const appMetalakesSlice = createSlice({ name: 'appMetalakes', initialState: { @@ -554,6 +693,7 @@ export const appMetalakesSlice = createSlice({ schemas: [], tables: [], columns: [], + filesets: [], metalakeTree: [], loadedNodes: [], selectedNodes: [], @@ -604,6 +744,7 @@ export const appMetalakesSlice = createSlice({ state.schemas = [] state.tables = [] state.columns = [] + state.filesets = [] }, setTableLoading(state, action) { state.tableLoading = action.payload @@ -616,7 +757,12 @@ export const appMetalakesSlice = createSlice({ state.metalakeTree = updateTreeData(tree, key, data) }, addCatalogToTree(state, action) { - state.metalakeTree.push(action.payload) + const catalogIndex = state.metalakeTree.findIndex(c => c.key === action.payload.key) + if (catalogIndex === -1) { + state.metalakeTree.push(action.payload) + } else { + state.metalakeTree.splice(catalogIndex, 1, action.payload) + } }, removeCatalogFromTree(state, action) { state.metalakeTree = state.metalakeTree.filter(i => i.key !== action.payload) @@ -627,13 +773,28 @@ export const appMetalakesSlice = createSlice({ builder.addCase(fetchMetalakes.fulfilled, (state, action) => { state.metalakes = action.payload.metalakes }) + builder.addCase(fetchMetalakes.rejected, (state, action) => { + toast.error(action.error.message) + }) builder.addCase(setIntoTreeNodeWithFetch.fulfilled, (state, action) => { const { key, data, tree } = action.payload state.metalakeTree = updateTreeData(tree, key, data) }) + builder.addCase(setIntoTreeNodeWithFetch.rejected, (state, action) => { + toast.error(action.error.message) + }) builder.addCase(getMetalakeDetails.fulfilled, (state, action) => { state.activatedDetails = action.payload }) + builder.addCase(getMetalakeDetails.rejected, (state, action) => { + toast.error(action.error.message) + }) + builder.addCase(createCatalog.rejected, (state, action) => { + toast.error(action.error.message) + }) + builder.addCase(updateCatalog.rejected, (state, action) => { + toast.error(action.error.message) + }) builder.addCase(fetchCatalogs.fulfilled, (state, action) => { state.catalogs = action.payload.catalogs @@ -645,28 +806,65 @@ export const appMetalakesSlice = createSlice({ state.metalakeTree = action.payload.catalogs } }) + builder.addCase(fetchCatalogs.rejected, (state, action) => { + toast.error(action.error.message) + }) builder.addCase(getCatalogDetails.fulfilled, (state, action) => { state.activatedDetails = action.payload }) + builder.addCase(getCatalogDetails.rejected, (state, action) => { + toast.error(action.error.message) + }) + builder.addCase(deleteCatalog.rejected, (state, action) => { + toast.error(action.error.message) + }) builder.addCase(fetchSchemas.fulfilled, (state, action) => { state.schemas = action.payload.schemas if (action.payload.init) { state.tableData = action.payload.schemas } }) + builder.addCase(fetchSchemas.rejected, (state, action) => { + toast.error(action.error.message) + }) builder.addCase(getSchemaDetails.fulfilled, (state, action) => { state.activatedDetails = action.payload }) + builder.addCase(getSchemaDetails.rejected, (state, action) => { + toast.error(action.error.message) + }) builder.addCase(fetchTables.fulfilled, (state, action) => { state.tables = action.payload.tables if (action.payload.init) { state.tableData = action.payload.tables } }) + builder.addCase(fetchTables.rejected, (state, action) => { + toast.error(action.error.message) + }) builder.addCase(getTableDetails.fulfilled, (state, action) => { state.activatedDetails = action.payload state.tableData = action.payload.columns || [] }) + builder.addCase(getTableDetails.rejected, (state, action) => { + toast.error(action.error.message) + }) + builder.addCase(fetchFilesets.fulfilled, (state, action) => { + state.filesets = action.payload.filesets + if (action.payload.init) { + state.tableData = action.payload.filesets + } + }) + builder.addCase(fetchFilesets.rejected, (state, action) => { + toast.error(action.error.message) + }) + builder.addCase(getFilesetDetails.fulfilled, (state, action) => { + state.activatedDetails = action.payload + state.tableData = [] + }) + builder.addCase(getFilesetDetails.rejected, (state, action) => { + toast.error(action.error.message) + }) } }) diff --git a/web/src/lib/utils/initial.js b/web/src/lib/utils/initial.js index 2eaa4d9ac76..476879b1e4b 100644 --- a/web/src/lib/utils/initial.js +++ b/web/src/lib/utils/initial.js @@ -4,6 +4,11 @@ */ export const providers = [ + { + label: 'hadoop', + value: 'hadoop', + defaultProps: [] + }, { label: 'hive', value: 'hive',