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 d64c514f0 [#5965] subtask(web): ML model support for web UI (#6025)
d64c514f0 is described below
commit d64c514f02e3fdd427d3f42c368875b621e52dfa
Author: Qian Xia <[email protected]>
AuthorDate: Mon Dec 30 15:54:49 2024 +0800
[#5965] subtask(web): ML model support for web UI (#6025)
### What changes were proposed in this pull request?
create catalog
<img width="1410" alt="image"
src="https://github.com/user-attachments/assets/45311026-9458-436b-869c-5d6190641ba7"
/>
create schema
<img width="1424" alt="image"
src="https://github.com/user-attachments/assets/7c1c82b8-2aae-4614-8f02-cceca1a49ae9"
/>
register/view/drop/list model
<img width="1420" alt="image"
src="https://github.com/user-attachments/assets/bbb2ae41-0153-4d8d-af24-e182f16a30e3"
/>
<img width="1469" alt="image"
src="https://github.com/user-attachments/assets/e3bf371d-5e48-4f70-a9a1-4faa1f821c3c"
/>
link/view/drop/list versions
<img width="1445" alt="image"
src="https://github.com/user-attachments/assets/050a2f15-222c-484a-9abe-edcc5335c606"
/>
<img width="1462" alt="image"
src="https://github.com/user-attachments/assets/0f7de28f-1614-40ca-8842-fd79ea68871b"
/>
<img width="1429" alt="image"
src="https://github.com/user-attachments/assets/9a01f634-7113-4553-ad30-9d6e2ed9adc9"
/>
<img width="1409" alt="image"
src="https://github.com/user-attachments/assets/f5292e23-6b90-4b21-b1d7-19c9fe72ba61"
/>
### Why are the changes needed?
(Please clarify why the changes are needed. For instance,
1. If you propose a new API, clarify the use case for a new API.
2. If you fix a bug, describe the bug.)
Fix: #5817
### Does this PR introduce _any_ user-facing change?
N/A
### How was this patch tested?
manually
---
docs/assets/webui/create-table.png | Bin 0 -> 238896 bytes
docs/assets/webui/create-topic.png | Bin 0 -> 223540 bytes
docs/assets/webui/delete-model.png | Bin 0 -> 200946 bytes
docs/assets/webui/delete-table.png | Bin 0 -> 266515 bytes
docs/assets/webui/delete-topic.png | Bin 0 -> 215120 bytes
docs/assets/webui/delete-version.png | Bin 0 -> 199603 bytes
docs/assets/webui/link-version.png | Bin 0 -> 201414 bytes
docs/assets/webui/list-columns.png | Bin 229252 -> 229252 bytes
docs/assets/webui/list-model-versions.png | Bin 0 -> 194337 bytes
docs/assets/webui/list-models.png | Bin 0 -> 203000 bytes
docs/assets/webui/list-tabels.png | Bin 0 -> 293004 bytes
docs/assets/webui/list-topics.png | Bin 0 -> 224115 bytes
docs/assets/webui/model-details.png | Bin 0 -> 184251 bytes
docs/assets/webui/register-model.png | Bin 0 -> 210717 bytes
docs/assets/webui/table-details.png | Bin 0 -> 280416 bytes
docs/assets/webui/table-selected-details.png | Bin 0 -> 320840 bytes
docs/assets/webui/topic-drawer-details.png | Bin 0 -> 256974 bytes
docs/assets/webui/update-table-dialog.png | Bin 0 -> 306737 bytes
docs/assets/webui/update-topic-dialog.png | Bin 0 -> 320990 bytes
docs/assets/webui/version-details.png | Bin 0 -> 180724 bytes
docs/webui.md | 182 ++++++++++-
web/web/src/app/metalakes/metalake/MetalakeTree.js | 38 ++-
web/web/src/app/metalakes/metalake/MetalakeView.js | 40 ++-
.../metalake/rightContent/CreateCatalogDialog.js | 111 ++++---
.../metalake/rightContent/CreateFilesetDialog.js | 18 +-
.../metalake/rightContent/CreateSchemaDialog.js | 18 +-
.../metalake/rightContent/CreateTopicDialog.js | 18 +-
.../{CreateTopicDialog.js => LinkVersionDialog.js} | 184 +++++++----
.../metalake/rightContent/MetalakePath.js | 29 +-
...CreateTopicDialog.js => RegisterModelDialog.js} | 62 +---
.../metalake/rightContent/RightContent.js | 59 ++++
.../rightContent/tabsContent/TabsContent.js | 22 +-
.../tabsContent/detailsView/DetailsView.js | 19 ++
.../tabsContent/tableView/TableView.js | 53 +++-
web/web/src/components/DetailsDrawer.js | 22 +-
web/web/src/lib/api/models/index.js | 100 ++++++
web/web/src/lib/store/metalakes/index.js | 346 ++++++++++++++++++++-
37 files changed, 1070 insertions(+), 251 deletions(-)
diff --git a/docs/assets/webui/create-table.png
b/docs/assets/webui/create-table.png
new file mode 100644
index 000000000..4616828cf
Binary files /dev/null and b/docs/assets/webui/create-table.png differ
diff --git a/docs/assets/webui/create-topic.png
b/docs/assets/webui/create-topic.png
new file mode 100644
index 000000000..2a5be613e
Binary files /dev/null and b/docs/assets/webui/create-topic.png differ
diff --git a/docs/assets/webui/delete-model.png
b/docs/assets/webui/delete-model.png
new file mode 100644
index 000000000..d7614b7e7
Binary files /dev/null and b/docs/assets/webui/delete-model.png differ
diff --git a/docs/assets/webui/delete-table.png
b/docs/assets/webui/delete-table.png
new file mode 100644
index 000000000..530c0fec5
Binary files /dev/null and b/docs/assets/webui/delete-table.png differ
diff --git a/docs/assets/webui/delete-topic.png
b/docs/assets/webui/delete-topic.png
new file mode 100644
index 000000000..34fb5b13b
Binary files /dev/null and b/docs/assets/webui/delete-topic.png differ
diff --git a/docs/assets/webui/delete-version.png
b/docs/assets/webui/delete-version.png
new file mode 100644
index 000000000..7eae573c3
Binary files /dev/null and b/docs/assets/webui/delete-version.png differ
diff --git a/docs/assets/webui/link-version.png
b/docs/assets/webui/link-version.png
new file mode 100644
index 000000000..292a98c08
Binary files /dev/null and b/docs/assets/webui/link-version.png differ
diff --git a/docs/assets/webui/list-columns.png
b/docs/assets/webui/list-columns.png
index f5235a401..f7218705c 100644
Binary files a/docs/assets/webui/list-columns.png and
b/docs/assets/webui/list-columns.png differ
diff --git a/docs/assets/webui/list-model-versions.png
b/docs/assets/webui/list-model-versions.png
new file mode 100644
index 000000000..2b612fd39
Binary files /dev/null and b/docs/assets/webui/list-model-versions.png differ
diff --git a/docs/assets/webui/list-models.png
b/docs/assets/webui/list-models.png
new file mode 100644
index 000000000..753971221
Binary files /dev/null and b/docs/assets/webui/list-models.png differ
diff --git a/docs/assets/webui/list-tabels.png
b/docs/assets/webui/list-tabels.png
new file mode 100644
index 000000000..baa0f986b
Binary files /dev/null and b/docs/assets/webui/list-tabels.png differ
diff --git a/docs/assets/webui/list-topics.png
b/docs/assets/webui/list-topics.png
new file mode 100644
index 000000000..bb26239b2
Binary files /dev/null and b/docs/assets/webui/list-topics.png differ
diff --git a/docs/assets/webui/model-details.png
b/docs/assets/webui/model-details.png
new file mode 100644
index 000000000..d7b7e92cd
Binary files /dev/null and b/docs/assets/webui/model-details.png differ
diff --git a/docs/assets/webui/register-model.png
b/docs/assets/webui/register-model.png
new file mode 100644
index 000000000..949c496c2
Binary files /dev/null and b/docs/assets/webui/register-model.png differ
diff --git a/docs/assets/webui/table-details.png
b/docs/assets/webui/table-details.png
new file mode 100644
index 000000000..0988f510d
Binary files /dev/null and b/docs/assets/webui/table-details.png differ
diff --git a/docs/assets/webui/table-selected-details.png
b/docs/assets/webui/table-selected-details.png
new file mode 100644
index 000000000..8f35c011a
Binary files /dev/null and b/docs/assets/webui/table-selected-details.png differ
diff --git a/docs/assets/webui/topic-drawer-details.png
b/docs/assets/webui/topic-drawer-details.png
new file mode 100644
index 000000000..b878080ea
Binary files /dev/null and b/docs/assets/webui/topic-drawer-details.png differ
diff --git a/docs/assets/webui/update-table-dialog.png
b/docs/assets/webui/update-table-dialog.png
new file mode 100644
index 000000000..20f2b89d4
Binary files /dev/null and b/docs/assets/webui/update-table-dialog.png differ
diff --git a/docs/assets/webui/update-topic-dialog.png
b/docs/assets/webui/update-topic-dialog.png
new file mode 100644
index 000000000..6c46ec1bf
Binary files /dev/null and b/docs/assets/webui/update-topic-dialog.png differ
diff --git a/docs/assets/webui/version-details.png
b/docs/assets/webui/version-details.png
new file mode 100644
index 000000000..027f19acb
Binary files /dev/null and b/docs/assets/webui/version-details.png differ
diff --git a/docs/webui.md b/docs/webui.md
index 960eb821f..cf079d3f1 100644
--- a/docs/webui.md
+++ b/docs/webui.md
@@ -160,11 +160,12 @@ Click on the `CREATE CATALOG` button displays the dialog
to create a catalog.
Creating a catalog requires these fields:
1. **Catalog name**(**_required_**): the name of the catalog
-2. **Type**(**_required_**): `relational`/`fileset`/`messaging`, the default
value is `relational`
+2. **Type**(**_required_**): `relational`/`fileset`/`messaging`/`model`, the
default value is `relational`
3. **Provider**(**_required_**):
1. Type `relational` -
`hive`/`iceberg`/`mysql`/`postgresql`/`doris`/`paimon`/`hudi`/`oceanbase`
2. Type `fileset` - `hadoop`
3. Type `messaging` - `kafka`
+ 4. Type `model` has no provider
4. **Comment**(_optional_): the comment of this catalog
5. **Properties**(**each `provider` must fill in the required property fields
specifically**)
@@ -425,6 +426,7 @@ Displays a confirmation dialog, clicking on the SUBMIT
button deletes this catal

### Schema
+
Click the catalog tree node on the left sidebar or the catalog name link in
the table cell.
Displays the list schemas of the catalog.
@@ -469,14 +471,63 @@ Displays a confirmation dialog, clicking on the `DROP`
button drops this schema.
### Table
-
+Click the hive schema tree node on the left sidebar or the schema name link in
the table cell.
+
+Displays the list tables of the schema.
+
+
+
+#### Create table
+
+Click on the `CREATE TABLE` button displays the dialog to create a table.
+
+
+
+Creating a table needs these fields:
+
+1. **Name**(**_required_**): the name of the table.
+2. **columns**(**_required_**):
+ 1. The name and type of each column are required.
+ 2. Only suppport simple types, cannot support complex types by ui, you can
create complex types by api.
+3. **Comment**(_optional_): the comment of the table.
+4. **Properties**(_optional_): Click on the `ADD PROPERTY` button to add
custom properties.
+
+#### Show table details
+
+Click on the action icon <Icon icon='bx:show-alt' fontSize='24' /> in the
table cell.
+
+You can see the detailed information of this table in the drawer component on
the right.
+
+
+
+Click the table tree node on the left sidebar or the table name link in the
table cell.
+
+You can see the columns and detailed information on the right page.

+
+
+#### Edit table
+
+Click on the action icon <Icon icon='mdi:square-edit-outline' fontSize='24' />
in the table cell.
+
+Displays the dialog for modifying fields of the selected table.
+
+
+
+#### Drop table
+
+Click on the action icon <Icon icon='mdi:delete-outline' fontSize='24'
color='red' /> in the table cell.
+
+Displays a confirmation dialog, clicking on the `DROP` button drops this table.
+
+
### Fileset
+
Click the fileset schema tree node on the left sidebar or the schema name link
in the table cell.
-Displays the list fileset of the schema.
+Displays the list filesets of the schema.

@@ -528,8 +579,127 @@ Displays a confirmation dialog, clicking on the `DROP`
button drops this fileset
### Topic
+Click the kafka schema tree node on the left sidebar or the schema name link
in the table cell.
+
+Displays the list topics of the schema.
+
+
+
+#### Create topic
+
+Click on the `CREATE TOPIC` button displays the dialog to create a topic.
+
+
+
+Creating a topic needs these fields:
+
+1. **Name**(**_required_**): the name of the topic.
+2. **Comment**(_optional_): the comment of the topic.
+3. **Properties**(_optional_): Click on the `ADD PROPERTY` button to add
custom properties.
+
+#### Show topic details
+
+Click on the action icon <Icon icon='bx:show-alt' fontSize='24' /> in the
table cell.
+
+You can see the detailed information of this topic in the drawer component on
the right.
+
+
+
+Click the topic tree node on the left sidebar or the topic name link in the
table cell.
+
+You can see the detailed information on the right page.
+

+#### Edit topic
+
+Click on the action icon <Icon icon='mdi:square-edit-outline' fontSize='24' />
in the table cell.
+
+Displays the dialog for modifying fields of the selected topic.
+
+
+
+#### Drop topic
+
+Click on the action icon <Icon icon='mdi:delete-outline' fontSize='24'
color='red' /> in the table cell.
+
+Displays a confirmation dialog, clicking on the `DROP` button drops this topic.
+
+
+
+### Model
+
+Click the model schema tree node on the left sidebar or the schema name link
in the table cell.
+
+Displays the list model of the schema.
+
+
+
+#### Register model
+
+Click on the `REGISTER MODEL` button displays the dialog to register a model.
+
+
+
+Register a model needs these fields:
+
+1. **Name**(**_required_**): the name of the model.
+2. **Comment**(_optional_): the comment of the model.
+3. **Properties**(_optional_): Click on the `ADD PROPERTY` button to add
custom properties.
+
+#### Show model details
+
+Click on the action icon <Icon icon='bx:show-alt' fontSize='24' /> in the
table cell.
+
+You can see the detailed information of this model in the drawer component on
the right.
+
+
+
+#### Drop model
+
+Click on the action icon <Icon icon='mdi:delete-outline' fontSize='24'
color='red' /> in the table cell.
+
+Displays a confirmation dialog, clicking on the `DROP` button drops this model.
+
+
+
+### Version
+
+Click the model tree node on the left sidebar or the model name link in the
table cell.
+
+Displays the list versions of the model.
+
+
+
+#### Link version
+
+Click on the `LINK VERSION` button displays the dialog to link a version.
+
+
+
+Link a version needs these fields:
+
+1. **URI**(**_required_**): the uri of the version.
+2. **Aliases**(**_required_**): the aliases of the version, aliase cannot be
number or number string.
+3. **Comment**(_optional_): the comment of the model.
+4. **Properties**(_optional_): Click on the `ADD PROPERTY` button to add
custom properties.
+
+#### Show version details
+
+Click on the action icon <Icon icon='bx:show-alt' fontSize='24' /> in the
table cell.
+
+You can see the detailed information of this version in the drawer component
on the right.
+
+
+
+#### Drop version
+
+Click on the action icon <Icon icon='mdi:delete-outline' fontSize='24'
color='red' /> in the table cell.
+
+Displays a confirmation dialog, clicking on the `DROP` button drops this
version.
+
+
+
## Feature capabilities
| Page | Capabilities
|
@@ -537,9 +707,11 @@ Displays a confirmation dialog, clicking on the `DROP`
button drops this fileset
| Metalake | _`View`_ ✔ / _`Create`_ ✔ / _`Edit`_ ✔ /
_`Delete`_ ✔ |
| Catalog | _`View`_ ✔ / _`Create`_ ✔ / _`Edit`_ ✔ /
_`Delete`_ ✔ |
| Schema | _`View`_ ✔ / _`Create`_ ✔ / _`Edit`_ ✔ /
_`Delete`_ ✔ |
-| Table | _`View`_ ✔ / _`Create`_ ✘ / _`Edit`_ ✘ /
_`Delete`_ ✘ |
+| Table | _`View`_ ✔ / _`Create`_ ✔ / _`Edit`_ ✔ /
_`Delete`_ ✔ |
| Fileset | _`View`_ ✔ / _`Create`_ ✔ / _`Edit`_ ✔ /
_`Delete`_ ✔ |
-| Topic | _`View`_ ✔ / _`Create`_ ✘ / _`Edit`_ ✘ /
_`Delete`_ ✘ |
+| Topic | _`View`_ ✔ / _`Create`_ ✔ / _`Edit`_ ✔ /
_`Delete`_ ✔ |
+| Model | _`View`_ ✔ / _`Create`_ ✔ / _`Edit`_ ✘ /
_`Delete`_ ✔ |
+| Version | _`View`_ ✔ / _`Create`_ ✔ / _`Edit`_ ✘ /
_`Delete`_ ✔ |
## E2E test
diff --git a/web/web/src/app/metalakes/metalake/MetalakeTree.js
b/web/web/src/app/metalakes/metalake/MetalakeTree.js
index e6b6ea0c3..dead6c338 100644
--- a/web/web/src/app/metalakes/metalake/MetalakeTree.js
+++ b/web/web/src/app/metalakes/metalake/MetalakeTree.js
@@ -38,7 +38,10 @@ import {
setLoadedNodes,
getTableDetails,
getFilesetDetails,
- getTopicDetails
+ getTopicDetails,
+ getModelDetails,
+ fetchModelVersions,
+ getVersionDetails
} from '@/lib/store/metalakes'
import { extractPlaceholder } from '@/lib/utils'
@@ -81,6 +84,8 @@ const MetalakeTree = props => {
return 'skill-icons:kafka'
case 'fileset':
return 'twemoji:file-folder'
+ case 'model':
+ return 'carbon:machine-learning-model'
default:
return 'bx:book'
}
@@ -115,6 +120,23 @@ const MetalakeTree = props => {
}
break
}
+ case 'model': {
+ if (store.selectedNodes.includes(nodeProps.data.key)) {
+ const pathArr = extractPlaceholder(nodeProps.data.key)
+ const [metalake, catalog, type, schema, model] = pathArr
+ dispatch(fetchModelVersions({ init: true, metalake, catalog, schema,
model }))
+ dispatch(getModelDetails({ init: true, metalake, catalog, schema,
model }))
+ }
+ break
+ }
+ case 'version': {
+ if (store.selectedNodes.includes(nodeProps.data.key)) {
+ const pathArr = extractPlaceholder(nodeProps.data.key)
+ const [metalake, catalog, type, schema, model, version] = pathArr
+ dispatch(getVersionDetails({ init: true, metalake, catalog, schema,
model, version }))
+ }
+ break
+ }
default:
dispatch(setIntoTreeNodeWithFetch({ key: nodeProps.data.key, reload:
true }))
}
@@ -257,6 +279,20 @@ const MetalakeTree = props => {
</IconButton>
)
+ case 'model':
+ return (
+ <IconButton
+ disableRipple={!store.selectedNodes.includes(nodeProps.data.key)}
+ size='small'
+ sx={{ color: '#666' }}
+ onClick={e => handleClickIcon(e, nodeProps)}
+ onMouseEnter={e => onMouseEnter(e, nodeProps)}
+ onMouseLeave={e => onMouseLeave(e, nodeProps)}
+ >
+ <Icon icon={isHover !== nodeProps.data.key ? 'mdi:globe-model' :
'mdi:reload'} fontSize='inherit' />
+ </IconButton>
+ )
+
default:
return <></>
}
diff --git a/web/web/src/app/metalakes/metalake/MetalakeView.js
b/web/web/src/app/metalakes/metalake/MetalakeView.js
index a7726bea5..58c16f9e6 100644
--- a/web/web/src/app/metalakes/metalake/MetalakeView.js
+++ b/web/web/src/app/metalakes/metalake/MetalakeView.js
@@ -34,12 +34,16 @@ import {
fetchTables,
fetchFilesets,
fetchTopics,
+ fetchModels,
+ fetchModelVersions,
getMetalakeDetails,
getCatalogDetails,
getSchemaDetails,
getTableDetails,
getFilesetDetails,
getTopicDetails,
+ getModelDetails,
+ getVersionDetails,
setSelectedNodes
} from '@/lib/store/metalakes'
@@ -49,6 +53,12 @@ const MetalakeView = () => {
const paramsSize = [...searchParams.keys()].length
const store = useAppSelector(state => state.metalakes)
+ const buildNodePath = routeParams => {
+ const keys = ['metalake', 'catalog', 'type', 'schema', 'table', 'fileset',
'topic', 'model']
+
+ return keys.map(key => (routeParams[key] ? `{{${routeParams[key]}}}` :
'')).join('')
+ }
+
useEffect(() => {
const routeParams = {
metalake: searchParams.get('metalake'),
@@ -57,11 +67,13 @@ const MetalakeView = () => {
schema: searchParams.get('schema'),
table: searchParams.get('table'),
fileset: searchParams.get('fileset'),
- topic: searchParams.get('topic')
+ topic: searchParams.get('topic'),
+ model: searchParams.get('model'),
+ version: searchParams.get('version')
}
async function fetchDependsData() {
if ([...searchParams.keys()].length) {
- const { metalake, catalog, type, schema, table, fileset, topic } =
routeParams
+ const { metalake, catalog, type, schema, table, fileset, topic, model,
version } = routeParams
if (paramsSize === 1 && metalake) {
dispatch(fetchCatalogs({ init: true, page: 'metalakes', metalake }))
@@ -91,6 +103,9 @@ const MetalakeView = () => {
case 'messaging':
dispatch(fetchTopics({ init: true, page: 'schemas', metalake,
catalog, schema }))
break
+ case 'model':
+ dispatch(fetchModels({ init: true, page: 'schemas', metalake,
catalog, schema }))
+ break
default:
break
}
@@ -111,24 +126,19 @@ const MetalakeView = () => {
if (topic) {
dispatch(getTopicDetails({ init: true, metalake, catalog, schema,
topic }))
}
+ if (model) {
+ dispatch(fetchModelVersions({ init: true, metalake, catalog,
schema, model }))
+ dispatch(getModelDetails({ init: true, metalake, catalog, schema,
model }))
+ }
+ }
+ if (paramsSize === 6 && version) {
+ dispatch(getVersionDetails({ init: true, metalake, catalog, schema,
model, version }))
}
}
}
fetchDependsData()
- dispatch(
- setSelectedNodes(
- routeParams.catalog
- ? [
-
`{{${routeParams.metalake}}}{{${routeParams.catalog}}}{{${routeParams.type}}}${
- routeParams.schema ? `{{${routeParams.schema}}}` : ''
- }${routeParams.table ? `{{${routeParams.table}}}` : ''}${
- routeParams.fileset ? `{{${routeParams.fileset}}}` : ''
- }${routeParams.topic ? `{{${routeParams.topic}}}` : ''}`
- ]
- : []
- )
- )
+ dispatch(setSelectedNodes(routeParams.catalog ?
[buildNodePath(routeParams)] : []))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams])
diff --git
a/web/web/src/app/metalakes/metalake/rightContent/CreateCatalogDialog.js
b/web/web/src/app/metalakes/metalake/rightContent/CreateCatalogDialog.js
index cd9c101f7..6f0cf70ed 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/CreateCatalogDialog.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/CreateCatalogDialog.js
@@ -64,7 +64,7 @@ const defaultValues = {
const schema = yup.object().shape({
name: yup.string().required().matches(nameRegex, nameRegexDesc),
- type: yup.mixed().oneOf(['relational', 'fileset', 'messaging']).required(),
+ type: yup.mixed().oneOf(['relational', 'fileset', 'messaging',
'model']).required(),
provider: yup.string().when('type', (type, schema) => {
switch (type) {
case 'relational':
@@ -148,12 +148,7 @@ const CreateCatalogDialog = props => {
}
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
- )
+ const duplicateKeys = innerProps.some(item => item.hasDuplicateKey)
if (duplicateKeys) {
return
@@ -212,16 +207,9 @@ const CreateCatalogDialog = props => {
}
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 hasError = innerProps.some(prop => prop.hasDuplicateKey ||
prop.invalid)
- const invalidKeys = innerProps.some(i => i.invalid)
-
- if (duplicateKeys || invalidKeys) {
+ if (hasError) {
return
}
@@ -371,6 +359,11 @@ const CreateCatalogDialog = props => {
setValue('provider', 'kafka')
break
}
+ case 'model': {
+ setProviderTypes([])
+ setValue('provider', '')
+ break
+ }
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -385,11 +378,10 @@ const CreateCatalogDialog = props => {
defaultProps = providerTypes[providerItemIndex].defaultProps
resetPropsFields(providerTypes, providerItemIndex)
-
- if (type === 'create') {
- setInnerProps(defaultProps)
- setValue('propItems', providerTypes[providerItemIndex].defaultProps)
- }
+ }
+ if (type === 'create') {
+ setInnerProps(defaultProps)
+ setValue('propItems', defaultProps)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -420,12 +412,16 @@ const CreateCatalogDialog = props => {
providersItems = messagingProviders
break
}
+ case 'model': {
+ providersItems = []
+ break
+ }
}
setProviderTypes(providersItems)
const providerItem = providersItems.find(i => i.value === data.provider)
- let propsItems = [...providerItem.defaultProps].filter(i => i.required)
+ let propsItems = providerItem ? [...providerItem.defaultProps].filter(i
=> i.required) : []
propsItems = propsItems.map((it, idx) => {
let propItem = {
@@ -528,6 +524,7 @@ const CreateCatalogDialog = props => {
<MenuItem value={'relational'}>Relational</MenuItem>
<MenuItem value={'fileset'}>Fileset</MenuItem>
<MenuItem value={'messaging'}>Messaging</MenuItem>
+ <MenuItem value={'model'}>Model</MenuItem>
</Select>
)}
/>
@@ -535,41 +532,43 @@ const CreateCatalogDialog = props => {
</FormControl>
</Grid>
- <Grid item xs={12}>
- <FormControl fullWidth>
- <InputLabel id='select-catalog-provider'
error={Boolean(errors.provider)}>
- Provider
- </InputLabel>
- <Controller
- name='provider'
- control={control}
- rules={{ required: true }}
- render={({ field: { value, onChange } }) => (
- <Select
- value={value}
- label='Provider'
- defaultValue='hive'
- onChange={e => handleChangeProvider(onChange, e)}
- error={Boolean(errors.provider)}
- labelId='select-catalog-provider'
- disabled={type === 'update'}
- data-refer='catalog-provider-selector'
- >
- {providerTypes.map(item => {
- return (
- <MenuItem key={item.label} value={item.value}>
- {item.label}
- </MenuItem>
- )
- })}
- </Select>
+ {typeSelect !== 'model' && (
+ <Grid item xs={12}>
+ <FormControl fullWidth>
+ <InputLabel id='select-catalog-provider'
error={Boolean(errors.provider)}>
+ Provider
+ </InputLabel>
+ <Controller
+ name='provider'
+ control={control}
+ rules={{ required: true }}
+ render={({ field: { value, onChange } }) => (
+ <Select
+ value={value}
+ label='Provider'
+ defaultValue='hive'
+ onChange={e => handleChangeProvider(onChange, e)}
+ error={Boolean(errors.provider)}
+ labelId='select-catalog-provider'
+ disabled={type === 'update'}
+ data-refer='catalog-provider-selector'
+ >
+ {providerTypes.map(item => {
+ return (
+ <MenuItem key={item.label} value={item.value}>
+ {item.label}
+ </MenuItem>
+ )
+ })}
+ </Select>
+ )}
+ />
+ {errors.provider && (
+ <FormHelperText sx={{ color: 'error.main'
}}>{errors.provider.message}</FormHelperText>
)}
- />
- {errors.provider && (
- <FormHelperText sx={{ color: 'error.main'
}}>{errors.provider.message}</FormHelperText>
- )}
- </FormControl>
- </Grid>
+ </FormControl>
+ </Grid>
+ )}
<Grid item xs={12}>
<FormControl fullWidth>
diff --git
a/web/web/src/app/metalakes/metalake/rightContent/CreateFilesetDialog.js
b/web/web/src/app/metalakes/metalake/rightContent/CreateFilesetDialog.js
index 6a69d82f8..873876e6e 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/CreateFilesetDialog.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/CreateFilesetDialog.js
@@ -135,12 +135,7 @@ const CreateFilesetDialog = props => {
}
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
- )
+ const duplicateKeys = innerProps.some(item => item.hasDuplicateKey)
if (duplicateKeys) {
return
@@ -173,16 +168,9 @@ const CreateFilesetDialog = props => {
}
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 hasError = innerProps.some(prop => prop.hasDuplicateKey ||
prop.invalid)
- const invalidKeys = innerProps.some(i => i.invalid)
-
- if (duplicateKeys || invalidKeys) {
+ if (hasError) {
return
}
diff --git
a/web/web/src/app/metalakes/metalake/rightContent/CreateSchemaDialog.js
b/web/web/src/app/metalakes/metalake/rightContent/CreateSchemaDialog.js
index ce893802a..d09b9052b 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/CreateSchemaDialog.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/CreateSchemaDialog.js
@@ -131,12 +131,7 @@ const CreateSchemaDialog = props => {
}
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
- )
+ const duplicateKeys = innerProps.some(item => item.hasDuplicateKey)
if (duplicateKeys) {
return
@@ -169,16 +164,9 @@ const CreateSchemaDialog = props => {
}
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 hasError = innerProps.some(prop => prop.hasDuplicateKey ||
prop.invalid)
- const invalidKeys = innerProps.some(i => i.invalid)
-
- if (duplicateKeys || invalidKeys) {
+ if (hasError) {
return
}
diff --git
a/web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js
b/web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js
index 4f671cc87..87d33cef7 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js
@@ -127,12 +127,7 @@ const CreateTopicDialog = props => {
}
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
- )
+ const duplicateKeys = innerProps.some(item => item.hasDuplicateKey)
if (duplicateKeys) {
return
@@ -165,16 +160,9 @@ const CreateTopicDialog = props => {
}
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 hasError = innerProps.some(prop => prop.hasDuplicateKey ||
prop.invalid)
- const invalidKeys = innerProps.some(i => i.invalid)
-
- if (duplicateKeys || invalidKeys) {
+ if (hasError) {
return
}
diff --git
a/web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js
b/web/web/src/app/metalakes/metalake/rightContent/LinkVersionDialog.js
similarity index 70%
copy from web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js
copy to web/web/src/app/metalakes/metalake/rightContent/LinkVersionDialog.js
index 4f671cc87..01ea35b0c 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/LinkVersionDialog.js
@@ -40,26 +40,53 @@ import {
import Icon from '@/components/Icon'
import { useAppDispatch } from '@/lib/hooks/useStore'
-import { createTopic, updateTopic } from '@/lib/store/metalakes'
+import { linkVersion } from '@/lib/store/metalakes'
import * as yup from 'yup'
-import { useForm, Controller } from 'react-hook-form'
+import { useForm, Controller, useFieldArray } 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 { keyRegex } from '@/lib/utils/regex'
import { useSearchParams } from 'next/navigation'
import { useAppSelector } from '@/lib/hooks/useStore'
const defaultValues = {
- name: '',
+ uri: '',
+ aliases: [{ name: '' }],
comment: '',
propItems: []
}
const schema = yup.object().shape({
- name: yup.string().required().matches(nameRegex, nameRegexDesc),
+ uri: yup.string().required(),
+ aliases: yup
+ .array()
+ .of(
+ yup.object().shape({
+ name: yup
+ .string()
+ .required('This aliase is required')
+ .test('not-number', 'Aliase cannot be a number or numeric string',
value => {
+ return value === undefined || isNaN(Number(value))
+ })
+ })
+ )
+ .test('unique', 'Aliase must be unique', (aliases, ctx) => {
+ const values = aliases?.filter(a => !!a.name).map(a => a.name)
+ const duplicates = values.filter((value, index, self) =>
self.indexOf(value) !== index)
+
+ if (duplicates.length > 0) {
+ const duplicateIndex = values.lastIndexOf(duplicates[0])
+
+ return ctx.createError({
+ path: `aliases.${duplicateIndex}.name`,
+ message: 'This aliase is duplicated'
+ })
+ }
+
+ return true
+ }),
propItems: yup.array().of(
yup.object().shape({
required: yup.boolean(),
@@ -76,13 +103,14 @@ const Transition = forwardRef(function Transition(props,
ref) {
return <Fade ref={ref} {...props} />
})
-const CreateTopicDialog = props => {
+const LinkVersionDialog = props => {
const { open, setOpen, type = 'create', data = {} } = props
const searchParams = useSearchParams()
const metalake = searchParams.get('metalake')
const catalog = searchParams.get('catalog')
const schemaName = searchParams.get('schema')
const catalogType = searchParams.get('type')
+ const model = searchParams.get('model')
const [innerProps, setInnerProps] = useState([])
const dispatch = useAppDispatch()
const store = useAppSelector(state => state.metalakes)
@@ -127,12 +155,7 @@ const CreateTopicDialog = props => {
}
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
- )
+ const duplicateKeys = innerProps.some(item => item.hasDuplicateKey)
if (duplicateKeys) {
return
@@ -151,6 +174,13 @@ const CreateTopicDialog = props => {
setValue('propItems', data)
}
+ const { fields, append, remove } = useFieldArray({
+ control,
+ name: 'aliases'
+ })
+
+ const watchAliases = watch('aliases')
+
const handleClose = () => {
reset()
setInnerProps([])
@@ -165,16 +195,9 @@ const CreateTopicDialog = props => {
}
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 hasError = innerProps.some(prop => prop.hasDuplicateKey ||
prop.invalid)
- const invalidKeys = innerProps.some(i => i.invalid)
-
- if (duplicateKeys || invalidKeys) {
+ if (hasError) {
return
}
@@ -190,38 +213,20 @@ const CreateTopicDialog = props => {
}, {})
const schemaData = {
- name: data.name,
+ uri: data.uri,
+ aliases: data.aliases.map(alias => alias.name),
comment: data.comment,
properties
}
if (type === 'create') {
- dispatch(createTopic({ data: schemaData, metalake, catalog, schema:
schemaName, type: catalogType })).then(
- res => {
- if (!res.payload?.err) {
- handleClose()
- }
+ dispatch(
+ linkVersion({ data: schemaData, metalake, catalog, schema:
schemaName, type: catalogType, model })
+ ).then(res => {
+ if (!res.payload?.err) {
+ handleClose()
}
- )
- } else {
- const reqData = { updates: genUpdates(cacheData, schemaData) }
-
- if (reqData.updates.length !== 0) {
- dispatch(
- updateTopic({
- metalake,
- catalog,
- type: catalogType,
- schema: schemaName,
- topic: cacheData.name,
- data: reqData
- })
- ).then(res => {
- if (!res.payload?.err) {
- handleClose()
- }
- })
- }
+ })
}
})
.catch(err => {
@@ -238,7 +243,7 @@ const CreateTopicDialog = props => {
const { properties = {} } = data
setCacheData(data)
- setValue('name', data.name)
+ setValue('uri', data.uri)
setValue('comment', data.comment)
const propsItems = Object.entries(properties).map(([key, value]) => {
@@ -273,7 +278,7 @@ const CreateTopicDialog = props => {
</IconButton>
<Box sx={{ mb: 8, textAlign: 'center' }}>
<Typography variant='h5' sx={{ mb: 3 }}>
- {type === 'create' ? 'Create' : 'Edit'} Topic
+ {type === 'create' ? 'Link' : 'Edit'} Version
</Typography>
</Box>
@@ -281,25 +286,76 @@ const CreateTopicDialog = props => {
<Grid item xs={12}>
<FormControl fullWidth>
<Controller
- name='name'
+ name='uri'
control={control}
rules={{ required: true }}
render={({ field: { value, onChange } }) => (
<TextField
value={value}
- label='Name'
+ label='URI'
onChange={onChange}
placeholder=''
disabled={type === 'update'}
- error={Boolean(errors.name)}
- data-refer='topic-name-field'
+ error={Boolean(errors.uri)}
+ data-refer='link-uri-field'
/>
)}
/>
- {errors.name && <FormHelperText sx={{ color: 'error.main'
}}>{errors.name.message}</FormHelperText>}
+ {errors.uri && <FormHelperText sx={{ color: 'error.main'
}}>{errors.uri.message}</FormHelperText>}
</FormControl>
</Grid>
+ <Grid item xs={12}>
+ {fields.map((field, index) => {
+ return (
+ <Grid key={index} item xs={12} sx={{ '& + &': { mt: 2 } }}>
+ <FormControl fullWidth>
+ <Box
+ key={field.id}
+ sx={{ display: 'flex', alignItems: 'center',
justifyContent: 'space-between' }}
+ data-refer={`version-aliases-${index}`}
+ >
+ <Box sx={{ flexGrow: 1 }}>
+ <Controller
+ name={`aliases.${index}.name`}
+ control={control}
+ render={({ field }) => (
+ <TextField
+ {...field}
+ onChange={event => {
+ field.onChange(event)
+ trigger('aliases')
+ }}
+ label={`Aliase ${index + 1}`}
+ error={!!errors.aliases?.[index]?.name ||
!!errors.aliases?.message}
+
helperText={errors.aliases?.[index]?.name?.message || errors.aliases?.message}
+ fullWidth
+ />
+ )}
+ />
+ </Box>
+ <Box>
+ {index === 0 ? (
+ <Box sx={{ minWidth: 40 }}>
+ <IconButton onClick={() => append({ name: '' })}>
+ <Icon icon='mdi:plus-circle-outline' />
+ </IconButton>
+ </Box>
+ ) : (
+ <Box sx={{ minWidth: 40 }}>
+ <IconButton onClick={() => remove(index)}>
+ <Icon icon='mdi:minus-circle-outline' />
+ </IconButton>
+ </Box>
+ )}
+ </Box>
+ </Box>
+ </FormControl>
+ </Grid>
+ )
+ })}
+ </Grid>
+
<Grid item xs={12}>
<FormControl fullWidth>
<Controller
@@ -315,14 +371,14 @@ const CreateTopicDialog = props => {
onChange={onChange}
placeholder=''
error={Boolean(errors.comment)}
- data-refer='topic-comment-field'
+ data-refer='version-comment-field'
/>
)}
/>
</FormControl>
</Grid>
- <Grid item xs={12} data-refer='topic-props-layout'>
+ <Grid item xs={12} data-refer='version-props-layout'>
<Typography sx={{ mb: 2 }} variant='body2'>
Properties
</Typography>
@@ -334,7 +390,7 @@ const CreateTopicDialog = props => {
<Box>
<Box
sx={{ display: 'flex', alignItems: 'center',
justifyContent: 'space-between' }}
- data-refer={`topic-props-${index}`}
+ data-refer={`version-props-${index}`}
>
<Box>
<TextField
@@ -344,7 +400,7 @@ const CreateTopicDialog = props => {
value={item.key}
disabled={item.disabled || (item.key ===
'location' && type === 'update')}
onChange={event => handleFormChange({ index,
event })}
- error={item.hasDuplicateKey || item.invalid ||
!item.key.trim()}
+ error={item.hasDuplicateKey || item.invalid ||
!item.key?.trim()}
data-refer={`props-key-${index}`}
/>
</Box>
@@ -390,7 +446,7 @@ const CreateTopicDialog = props => {
underscores, hyphens, or dots.
</FormHelperText>
)}
- {!item.key.trim() && (
+ {!item.key?.trim() && (
<FormHelperText
className={'twc-text-error-main'}>Key is required</FormHelperText>
)}
</FormControl>
@@ -406,7 +462,7 @@ const CreateTopicDialog = props => {
onClick={addFields}
variant='outlined'
startIcon={<Icon icon='mdi:plus-circle-outline' />}
- data-refer='add-topic-props'
+ data-refer='add-version-props'
>
Add Property
</Button>
@@ -420,8 +476,8 @@ const CreateTopicDialog = props => {
pb: theme => [`${theme.spacing(5)} !important`,
`${theme.spacing(12.5)} !important`]
}}
>
- <Button variant='contained' sx={{ mr: 1 }} type='submit'
data-refer='handle-submit-topic'>
- {type === 'create' ? 'Create' : 'Update'}
+ <Button variant='contained' sx={{ mr: 1 }} type='submit'
data-refer='handle-submit-model'>
+ {type === 'create' ? 'Submit' : 'Update'}
</Button>
<Button variant='outlined' onClick={handleClose}>
Cancel
@@ -432,4 +488,4 @@ const CreateTopicDialog = props => {
)
}
-export default CreateTopicDialog
+export default LinkVersionDialog
diff --git a/web/web/src/app/metalakes/metalake/rightContent/MetalakePath.js
b/web/web/src/app/metalakes/metalake/rightContent/MetalakePath.js
index b991ed2f1..9f41ccc0e 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/MetalakePath.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/MetalakePath.js
@@ -47,10 +47,12 @@ const MetalakePath = props => {
schema: searchParams.get('schema'),
table: searchParams.get('table'),
fileset: searchParams.get('fileset'),
- topic: searchParams.get('topic')
+ topic: searchParams.get('topic'),
+ model: searchParams.get('model'),
+ version: searchParams.get('version')
}
- const { metalake, catalog, type, schema, table, fileset, topic } =
routeParams
+ const { metalake, catalog, type, schema, table, fileset, topic, model,
version } = routeParams
const metalakeUrl = `?metalake=${metalake}`
const catalogUrl = `?metalake=${metalake}&catalog=${catalog}&type=${type}`
@@ -58,6 +60,8 @@ const MetalakePath = props => {
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 topicUrl =
`?metalake=${metalake}&catalog=${catalog}&type=${type}&schema=${schema}&topic=${topic}`
+ const modelUrl =
`?metalake=${metalake}&catalog=${catalog}&type=${type}&schema=${schema}&model=${model}`
+ const versionUrl =
`?metalake=${metalake}&catalog=${catalog}&type=${type}&schema=${schema}&model=${model}&version=${version}`
const handleClick = (event, path) => {
path === `?${searchParams.toString()}` && event.preventDefault()
@@ -152,6 +156,27 @@ const MetalakePath = props => {
</MUILink>
</Tooltip>
)}
+ {model && (
+ <Tooltip title={model} placement='top'>
+ <MUILink component={Link} href={modelUrl} onClick={event =>
handleClick(event, modelUrl)} underline='hover'>
+ <Icon icon='bx:file' fontSize={20} />
+ <Text>{model}</Text>
+ </MUILink>
+ </Tooltip>
+ )}
+ {version && (
+ <Tooltip title={version} placement='top'>
+ <MUILink
+ component={Link}
+ href={versionUrl}
+ onClick={event => handleClick(event, versionUrl)}
+ underline='hover'
+ >
+ <Icon icon='bx:file' fontSize={20} />
+ <Text>{version}</Text>
+ </MUILink>
+ </Tooltip>
+ )}
</Breadcrumbs>
)
}
diff --git
a/web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js
b/web/web/src/app/metalakes/metalake/rightContent/RegisterModelDialog.js
similarity index 86%
copy from web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js
copy to web/web/src/app/metalakes/metalake/rightContent/RegisterModelDialog.js
index 4f671cc87..68661fa9b 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/RegisterModelDialog.js
@@ -40,14 +40,13 @@ import {
import Icon from '@/components/Icon'
import { useAppDispatch } from '@/lib/hooks/useStore'
-import { createTopic, updateTopic } from '@/lib/store/metalakes'
+import { registerModel } 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'
@@ -76,7 +75,7 @@ const Transition = forwardRef(function Transition(props, ref)
{
return <Fade ref={ref} {...props} />
})
-const CreateTopicDialog = props => {
+const RegisterModelDialog = props => {
const { open, setOpen, type = 'create', data = {} } = props
const searchParams = useSearchParams()
const metalake = searchParams.get('metalake')
@@ -127,12 +126,7 @@ const CreateTopicDialog = props => {
}
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
- )
+ const duplicateKeys = innerProps.some(item => item.hasDuplicateKey)
if (duplicateKeys) {
return
@@ -165,16 +159,9 @@ const CreateTopicDialog = props => {
}
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 hasError = innerProps.some(prop => prop.hasDuplicateKey ||
prop.invalid)
- const invalidKeys = innerProps.some(i => i.invalid)
-
- if (duplicateKeys || invalidKeys) {
+ if (hasError) {
return
}
@@ -196,32 +183,13 @@ const CreateTopicDialog = props => {
}
if (type === 'create') {
- dispatch(createTopic({ data: schemaData, metalake, catalog, schema:
schemaName, type: catalogType })).then(
+ dispatch(registerModel({ data: schemaData, metalake, catalog,
schema: schemaName, type: catalogType })).then(
res => {
if (!res.payload?.err) {
handleClose()
}
}
)
- } else {
- const reqData = { updates: genUpdates(cacheData, schemaData) }
-
- if (reqData.updates.length !== 0) {
- dispatch(
- updateTopic({
- metalake,
- catalog,
- type: catalogType,
- schema: schemaName,
- topic: cacheData.name,
- data: reqData
- })
- ).then(res => {
- if (!res.payload?.err) {
- handleClose()
- }
- })
- }
}
})
.catch(err => {
@@ -273,7 +241,7 @@ const CreateTopicDialog = props => {
</IconButton>
<Box sx={{ mb: 8, textAlign: 'center' }}>
<Typography variant='h5' sx={{ mb: 3 }}>
- {type === 'create' ? 'Create' : 'Edit'} Topic
+ {type === 'create' ? 'Register' : 'Edit'} Model
</Typography>
</Box>
@@ -292,7 +260,7 @@ const CreateTopicDialog = props => {
placeholder=''
disabled={type === 'update'}
error={Boolean(errors.name)}
- data-refer='topic-name-field'
+ data-refer='model-name-field'
/>
)}
/>
@@ -315,14 +283,14 @@ const CreateTopicDialog = props => {
onChange={onChange}
placeholder=''
error={Boolean(errors.comment)}
- data-refer='topic-comment-field'
+ data-refer='model-comment-field'
/>
)}
/>
</FormControl>
</Grid>
- <Grid item xs={12} data-refer='topic-props-layout'>
+ <Grid item xs={12} data-refer='model-props-layout'>
<Typography sx={{ mb: 2 }} variant='body2'>
Properties
</Typography>
@@ -334,7 +302,7 @@ const CreateTopicDialog = props => {
<Box>
<Box
sx={{ display: 'flex', alignItems: 'center',
justifyContent: 'space-between' }}
- data-refer={`topic-props-${index}`}
+ data-refer={`model-props-${index}`}
>
<Box>
<TextField
@@ -406,7 +374,7 @@ const CreateTopicDialog = props => {
onClick={addFields}
variant='outlined'
startIcon={<Icon icon='mdi:plus-circle-outline' />}
- data-refer='add-topic-props'
+ data-refer='add-model-props'
>
Add Property
</Button>
@@ -420,8 +388,8 @@ const CreateTopicDialog = props => {
pb: theme => [`${theme.spacing(5)} !important`,
`${theme.spacing(12.5)} !important`]
}}
>
- <Button variant='contained' sx={{ mr: 1 }} type='submit'
data-refer='handle-submit-topic'>
- {type === 'create' ? 'Create' : 'Update'}
+ <Button variant='contained' sx={{ mr: 1 }} type='submit'
data-refer='handle-submit-model'>
+ {type === 'create' ? 'Register' : 'Update'}
</Button>
<Button variant='outlined' onClick={handleClose}>
Cancel
@@ -432,4 +400,4 @@ const CreateTopicDialog = props => {
)
}
-export default CreateTopicDialog
+export default RegisterModelDialog
diff --git a/web/web/src/app/metalakes/metalake/rightContent/RightContent.js
b/web/web/src/app/metalakes/metalake/rightContent/RightContent.js
index 54b248297..1495ae3c5 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/RightContent.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/RightContent.js
@@ -29,6 +29,8 @@ import CreateSchemaDialog from './CreateSchemaDialog'
import CreateFilesetDialog from './CreateFilesetDialog'
import CreateTopicDialog from './CreateTopicDialog'
import CreateTableDialog from './CreateTableDialog'
+import RegisterModelDialog from './RegisterModelDialog'
+import LinkVersionDialog from './LinkVersionDialog'
import TabsContent from './tabsContent/TabsContent'
import { useSearchParams } from 'next/navigation'
import { useAppSelector } from '@/lib/hooks/useStore'
@@ -39,12 +41,16 @@ const RightContent = () => {
const [openFileset, setOpenFileset] = useState(false)
const [openTopic, setOpenTopic] = useState(false)
const [openTable, setOpenTable] = useState(false)
+ const [openModel, setOpenModel] = useState(false)
+ const [openVersion, setOpenVersion] = useState(false)
const searchParams = useSearchParams()
const [isShowBtn, setBtnVisible] = useState(true)
const [isShowSchemaBtn, setSchemaBtnVisible] = useState(false)
const [isShowFilesetBtn, setFilesetBtnVisible] = useState(false)
const [isShowTopicBtn, setTopicBtnVisible] = useState(false)
const [isShowTableBtn, setTableBtnVisible] = useState(false)
+ const [isShowModelBtn, setModelBtnVisible] = useState(false)
+ const [isShowVersionBtn, setVersionBtnVisible] = useState(false)
const store = useAppSelector(state => state.metalakes)
const handleCreateCatalog = () => {
@@ -67,6 +73,14 @@ const RightContent = () => {
setOpenTable(true)
}
+ const handleRegisterModel = () => {
+ setOpenModel(true)
+ }
+
+ const handleLinkVersion = () => {
+ setOpenVersion(true)
+ }
+
useEffect(() => {
const paramsSize = [...searchParams.keys()].length
const isCatalogList = paramsSize == 1 && searchParams.get('metalake')
@@ -88,6 +102,23 @@ const RightContent = () => {
searchParams.has('schema')
setTopicBtnVisible(isTopicList)
+ const isModelList =
+ paramsSize == 4 &&
+ searchParams.has('metalake') &&
+ searchParams.has('catalog') &&
+ searchParams.get('type') === 'model' &&
+ searchParams.has('schema')
+ setModelBtnVisible(isModelList)
+
+ const isVersionList =
+ paramsSize == 5 &&
+ searchParams.has('metalake') &&
+ searchParams.has('catalog') &&
+ searchParams.get('type') === 'model' &&
+ searchParams.has('schema') &&
+ searchParams.has('model')
+ setVersionBtnVisible(isVersionList)
+
if (store.catalogs.length) {
const currentCatalog = store.catalogs.filter(ca => ca.name ===
searchParams.get('catalog'))[0]
@@ -199,6 +230,34 @@ const RightContent = () => {
<CreateTableDialog open={openTable} setOpen={setOpenTable} />
</Box>
)}
+ {isShowModelBtn && (
+ <Box className={`twc-flex twc-items-center`}>
+ <Button
+ variant='contained'
+ startIcon={<Icon icon='mdi:plus-box' />}
+ onClick={handleRegisterModel}
+ sx={{ width: 200 }}
+ data-refer='register-model-btn'
+ >
+ Register Model
+ </Button>
+ <RegisterModelDialog open={openModel} setOpen={setOpenModel} />
+ </Box>
+ )}
+ {isShowVersionBtn && (
+ <Box className={`twc-flex twc-items-center`}>
+ <Button
+ variant='contained'
+ startIcon={<Icon icon='mdi:plus-box' />}
+ onClick={handleLinkVersion}
+ sx={{ width: 200 }}
+ data-refer='link-version-btn'
+ >
+ Link Version
+ </Button>
+ <LinkVersionDialog open={openVersion} setOpen={setOpenVersion} />
+ </Box>
+ )}
</Box>
<Box sx={{ height: 'calc(100% - 4.1rem)' }}>
diff --git
a/web/web/src/app/metalakes/metalake/rightContent/tabsContent/TabsContent.js
b/web/web/src/app/metalakes/metalake/rightContent/tabsContent/TabsContent.js
index 9e45da054..55f2db690 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/tabsContent/TabsContent.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/tabsContent/TabsContent.js
@@ -85,7 +85,10 @@ const TabsContent = () => {
const paramsSize = [...searchParams.keys()].length
const type = searchParams.get('type')
const [tab, setTab] = useState('table')
- const isNotNeedTableTab = type && ['fileset', 'messaging'].includes(type) &&
paramsSize === 5
+
+ const isNotNeedTableTab =
+ (type && ['fileset', 'messaging'].includes(type) && paramsSize === 5) ||
+ (paramsSize === 6 && searchParams.get('version'))
const isShowTableProps = paramsSize === 5 && !['fileset',
'messaging'].includes(type)
const handleChangeTab = (event, newValue) => {
@@ -101,18 +104,29 @@ const TabsContent = () => {
break
case 4:
switch (type) {
+ case 'relational':
+ tableTitle = 'Tables'
+ break
case 'fileset':
tableTitle = 'Filesets'
break
case 'messaging':
tableTitle = 'Topics'
break
- default:
- tableTitle = 'Tables'
+ case 'model':
+ tableTitle = 'Models'
+ break
}
break
case 5:
- tableTitle = 'Columns'
+ switch (type) {
+ case 'relational':
+ tableTitle = 'Columns'
+ break
+ case 'model':
+ tableTitle = 'Versions'
+ break
+ }
break
default:
break
diff --git
a/web/web/src/app/metalakes/metalake/rightContent/tabsContent/detailsView/DetailsView.js
b/web/web/src/app/metalakes/metalake/rightContent/tabsContent/detailsView/DetailsView.js
index 6e21eabdb..580c49717 100644
---
a/web/web/src/app/metalakes/metalake/rightContent/tabsContent/detailsView/DetailsView.js
+++
b/web/web/src/app/metalakes/metalake/rightContent/tabsContent/detailsView/DetailsView.js
@@ -104,6 +104,25 @@ const DetailsView = () => {
</Grid>
</>
) : null}
+
+ {activatedItem?.uri && (
+ <Grid item xs={12} md={6} sx={{ mb: [0, 5] }}>
+ <Typography variant='body2' sx={{ mb: 2 }}>
+ URI
+ </Typography>
+ {renderFieldText({ value: activatedItem?.uri })}
+ </Grid>
+ )}
+
+ {activatedItem?.aliases && (
+ <Grid item xs={12} md={6} sx={{ mb: [0, 5] }}>
+ <Typography variant='body2' sx={{ mb: 2 }}>
+ Aliases
+ </Typography>
+ {renderFieldText({ value: activatedItem?.aliases?.join(', ') })}
+ </Grid>
+ )}
+
<Grid item xs={12} sx={{ mb: [0, 5] }}>
<Typography variant='body2' sx={{ mb: 2 }}>
Comment
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 a2a73c1ec..12716677e 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
@@ -52,6 +52,8 @@ import {
deleteTopic,
deleteSchema,
deleteTable,
+ deleteModel,
+ deleteVersion,
setCatalogInUse
} from '@/lib/store/metalakes'
@@ -62,6 +64,7 @@ import { useSearchParams } from 'next/navigation'
import { getFilesetDetailsApi } from '@/lib/api/filesets'
import { getTopicDetailsApi } from '@/lib/api/topics'
import { getTableDetailsApi } from '@/lib/api/tables'
+import { getModelDetailsApi, getVersionDetailsApi } from '@/lib/api/models'
const fonts = Inconsolata({ subsets: ['latin'] })
@@ -90,6 +93,7 @@ const TableView = () => {
const catalog = searchParams.get('catalog') || ''
const type = searchParams.get('type') || ''
const schema = searchParams.get('schema') || ''
+ const model = searchParams.get('model') || ''
const isCatalogList = paramsSize == 1 && searchParams.has('metalake')
@@ -119,6 +123,7 @@ const TableView = () => {
const [dialogData, setDialogData] = useState({})
const [dialogType, setDialogType] = useState('create')
const [isHideEdit, setIsHideEdit] = useState(true)
+ const [isHideDrop, setIsHideDrop] = useState(true)
useEffect(() => {
if (store.catalogs.length) {
@@ -127,9 +132,10 @@ const TableView = () => {
const isHideAction =
(['lakehouse-hudi', 'kafka'].includes(currentCatalog?.provider) &&
paramsSize == 3) ||
(currentCatalog?.provider === 'lakehouse-hudi' && paramsSize == 4)
- setIsHideEdit(isHideAction)
+ setIsHideEdit(isHideAction || type === 'model')
+ setIsHideDrop(isHideAction)
}
- }, [store.catalogs, store.catalogs.length, paramsSize, catalog])
+ }, [store.catalogs, store.catalogs.length, paramsSize, catalog, type])
const handleClickUrl = path => {
if (!path) {
@@ -249,7 +255,7 @@ const TableView = () => {
disableColumnMenu: true,
type: 'string',
field: 'name',
- headerName: 'Name',
+ headerName: model ? 'Version' : 'Name',
renderCell: ({ row }) => {
const { name, path } = row
@@ -325,7 +331,7 @@ const TableView = () => {
</IconButton>
)}
- {!isHideEdit && (
+ {!isHideDrop && (
<IconButton
title='Delete'
size='small'
@@ -535,6 +541,26 @@ const TableView = () => {
setOpenDrawer(true)
break
}
+ case 'model': {
+ const [err, res] = await to(getModelDetailsApi({ metalake, catalog,
schema, model: row.name }))
+ if (err || !res) {
+ throw new Error(err)
+ }
+
+ setDrawerData(res.model)
+ setOpenDrawer(true)
+ break
+ }
+ case 'version': {
+ const [err, res] = await to(getVersionDetailsApi({ metalake, catalog,
schema, model, version: row.name }))
+ if (err || !res) {
+ throw new Error(err)
+ }
+
+ setDrawerData(res.modelVersion)
+ setOpenDrawer(true)
+ break
+ }
default:
return
}
@@ -642,6 +668,12 @@ const TableView = () => {
case 'table':
dispatch(deleteTable({ metalake, catalog, type, schema, table:
confirmCacheData.name }))
break
+ case 'model':
+ dispatch(deleteModel({ metalake, catalog, type, schema, model:
confirmCacheData.name }))
+ break
+ case 'version':
+ dispatch(deleteVersion({ metalake, catalog, type, schema, model,
version: confirmCacheData.name }))
+ break
default:
break
}
@@ -676,7 +708,18 @@ const TableView = () => {
searchParams.has('metalake') &&
searchParams.has('catalog') &&
searchParams.get('type') === 'relational' &&
- searchParams.has('schema'))
+ searchParams.has('schema')) ||
+ (paramsSize == 4 &&
+ searchParams.has('metalake') &&
+ searchParams.has('catalog') &&
+ searchParams.get('type') === 'model' &&
+ searchParams.has('schema')) ||
+ (paramsSize == 5 &&
+ searchParams.has('metalake') &&
+ searchParams.has('catalog') &&
+ searchParams.get('type') === 'model' &&
+ searchParams.has('schema') &&
+ searchParams.has('model'))
) {
return actionsColumns
} else if (paramsSize == 5 && searchParams.has('table')) {
diff --git a/web/web/src/components/DetailsDrawer.js
b/web/web/src/components/DetailsDrawer.js
index a3cc707fa..740c7ae15 100644
--- a/web/web/src/components/DetailsDrawer.js
+++ b/web/web/src/components/DetailsDrawer.js
@@ -121,10 +121,28 @@ const DetailsDrawer = props => {
}}
data-refer='details-title'
>
- {drawerData.name}
+ {drawerData.name || drawerData.version}
</Typography>
</Grid>
+ {drawerData.uri && (
+ <Grid item xs={12} md={6} sx={{ mb: [0, 5] }}>
+ <Typography variant='body2' sx={{ mb: 2 }}>
+ Type
+ </Typography>
+ {renderFieldText({ value: drawerData.uri })}
+ </Grid>
+ )}
+
+ {drawerData.aliases && (
+ <Grid item xs={12} md={6} sx={{ mb: [0, 5] }}>
+ <Typography variant='body2' sx={{ mb: 2 }}>
+ Aliases
+ </Typography>
+ {renderFieldText({ value: drawerData.aliases.join(', ') })}
+ </Grid>
+ )}
+
{drawerData.type && (
<Grid item xs={12} md={6} sx={{ mb: [0, 5] }}>
<Typography variant='body2' sx={{ mb: 2 }}>
@@ -134,7 +152,7 @@ const DetailsDrawer = props => {
</Grid>
)}
- {drawerData.provider && (
+ {drawerData.provider && drawerData?.type !== 'model' && (
<Grid item xs={12} md={6} sx={{ mb: [0, 5] }}>
<Typography variant='body2' sx={{ mb: 2 }}>
Provider
diff --git a/web/web/src/lib/api/models/index.js
b/web/web/src/lib/api/models/index.js
new file mode 100644
index 000000000..fa968326d
--- /dev/null
+++ b/web/web/src/lib/api/models/index.js
@@ -0,0 +1,100 @@
+/*
+ * 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.
+ */
+
+import { defHttp } from '@/lib/utils/axios'
+
+const Apis = {
+ GET: ({ metalake, catalog, schema }) =>
+
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(
+ catalog
+ )}/schemas/${encodeURIComponent(schema)}/models`,
+ GET_DETAIL: ({ metalake, catalog, schema, model }) =>
+
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(
+ catalog
+
)}/schemas/${encodeURIComponent(schema)}/models/${encodeURIComponent(model)}`,
+ REGISTER: ({ metalake, catalog, schema }) =>
+
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(catalog)}/schemas/${encodeURIComponent(schema)}/models`,
+ UPDATE: ({ metalake, catalog, schema, model }) =>
+
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(catalog)}/schemas/${encodeURIComponent(schema)}/models/${encodeURIComponent(model)}`,
+ DELETE: ({ metalake, catalog, schema, model }) =>
+
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(catalog)}/schemas/${encodeURIComponent(schema)}/models/${encodeURIComponent(model)}`,
+ GET_VERSIONS: ({ metalake, catalog, schema, model }) =>
+
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(
+ catalog
+
)}/schemas/${encodeURIComponent(schema)}/models/${encodeURIComponent(model)}/versions`,
+ GET_VERSION_DETAIL: ({ metalake, catalog, schema, model, version }) =>
+
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(
+ catalog
+
)}/schemas/${encodeURIComponent(schema)}/models/${encodeURIComponent(model)}/versions/${version}`,
+ LINK_VERSION: ({ metalake, catalog, schema, model }) =>
+
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(
+ catalog
+
)}/schemas/${encodeURIComponent(schema)}/models/${encodeURIComponent(model)}`,
+ DELETE_VERSION: ({ metalake, catalog, schema, model, version }) => {
+ return
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(
+ catalog
+
)}/schemas/${encodeURIComponent(schema)}/models/${encodeURIComponent(model)}/versions/${version}`
+ }
+}
+
+export const getModelsApi = params => {
+ return defHttp.get({
+ url: `${Apis.GET(params)}`
+ })
+}
+
+export const getModelDetailsApi = ({ metalake, catalog, schema, model }) => {
+ return defHttp.get({
+ url: `${Apis.GET_DETAIL({ metalake, catalog, schema, model })}`
+ })
+}
+
+export const registerModelApi = ({ metalake, catalog, schema, data }) => {
+ return defHttp.post({ url: `${Apis.REGISTER({ metalake, catalog, schema
})}`, data })
+}
+
+export const updateModelApi = ({ metalake, catalog, schema, model, data }) => {
+ return defHttp.put({ url: `${Apis.UPDATE({ metalake, catalog, schema, model
})}`, data })
+}
+
+export const deleteModelApi = ({ metalake, catalog, schema, model }) => {
+ return defHttp.delete({ url: `${Apis.DELETE({ metalake, catalog, schema,
model })}` })
+}
+
+export const getModelVersionsApi = params => {
+ return defHttp.get({
+ url: `${Apis.GET_VERSIONS(params)}`
+ })
+}
+
+export const linkVersionApi = ({ metalake, catalog, schema, model, data }) => {
+ return defHttp.post({ url: `${Apis.LINK_VERSION({ metalake, catalog, schema,
model })}`, data })
+}
+
+export const getVersionDetailsApi = ({ metalake, catalog, schema, model,
version }) => {
+ return defHttp.get({
+ url: `${Apis.GET_VERSION_DETAIL({ metalake, catalog, schema, model,
version })}`
+ })
+}
+
+export const deleteVersionApi = ({ metalake, catalog, schema, model, version
}) => {
+ return defHttp.delete({
+ url: `${Apis.DELETE_VERSION({ metalake, catalog, schema, model, version
})}`
+ })
+}
diff --git a/web/web/src/lib/store/metalakes/index.js
b/web/web/src/lib/store/metalakes/index.js
index 3d4ad454d..2ff3322b8 100644
--- a/web/web/src/lib/store/metalakes/index.js
+++ b/web/web/src/lib/store/metalakes/index.js
@@ -55,6 +55,17 @@ import {
deleteFilesetApi
} from '@/lib/api/filesets'
import { getTopicsApi, getTopicDetailsApi, createTopicApi, updateTopicApi,
deleteTopicApi } from '@/lib/api/topics'
+import {
+ getModelsApi,
+ getModelDetailsApi,
+ registerModelApi,
+ updateModelApi,
+ deleteModelApi,
+ getModelVersionsApi,
+ getVersionDetailsApi,
+ linkVersionApi,
+ deleteVersionApi
+} from '@/lib/api/models'
export const fetchMetalakes = createAsyncThunk('appMetalakes/fetchMetalakes',
async (params, { getState }) => {
const [err, res] = await to(getMetalakesApi())
@@ -116,7 +127,7 @@ export const setIntoTreeNodeWithFetch = createAsyncThunk(
}
const pathArr = extractPlaceholder(key)
- const [metalake, catalog, type, schema] = pathArr
+ const [metalake, catalog, type, schema, entity] = pathArr
if (pathArr.length === 1) {
const [err, res] = await to(getCatalogsApi({ metalake }))
@@ -250,6 +261,27 @@ export const setIntoTreeNodeWithFetch = createAsyncThunk(
isLeaf: true
}
})
+ } else if (pathArr.length === 4 && type === 'model') {
+ const [err, res] = await to(getModelsApi({ metalake, catalog, schema }))
+
+ if (err || !res) {
+ throw new Error(err)
+ }
+
+ const { identifiers = [] } = res
+
+ result.data = identifiers.map(modelItem => {
+ return {
+ ...modelItem,
+ node: 'model',
+ id:
`{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema}}}{{${modelItem.name}}}`,
+ key:
`{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema}}}{{${modelItem.name}}}`,
+ path: `?${new URLSearchParams({ metalake, catalog, type, schema,
model: modelItem.name }).toString()}`,
+ name: modelItem.name,
+ title: modelItem.name,
+ isLeaf: true
+ }
+ })
}
return result
@@ -602,7 +634,7 @@ export const updateSchema = createAsyncThunk(
}
dispatch(fetchSchemas({ metalake, catalog, type, init: true }))
- return res.catalog
+ return res.schema
}
)
@@ -842,7 +874,7 @@ export const updateTable = createAsyncThunk(
}
dispatch(fetchTables({ metalake, catalog, type, schema, init: true }))
- return res.catalog
+ return res.table
}
)
@@ -993,7 +1025,7 @@ export const updateFileset = createAsyncThunk(
}
dispatch(fetchFilesets({ metalake, catalog, type, schema, init: true }))
- return res.catalog
+ return res.fileset
}
)
@@ -1144,7 +1176,7 @@ export const updateTopic = createAsyncThunk(
}
dispatch(fetchTopics({ metalake, catalog, type, schema, init: true }))
- return res.catalog
+ return res.topic
}
)
@@ -1165,6 +1197,248 @@ export const deleteTopic = createAsyncThunk(
}
)
+export const fetchModels = createAsyncThunk(
+ 'appMetalakes/fetchModels',
+ async ({ init, page, metalake, catalog, schema }, { getState, dispatch }) =>
{
+ if (init) {
+ dispatch(setTableLoading(true))
+ }
+
+ const [err, res] = await to(getModelsApi({ metalake, catalog, schema }))
+ dispatch(setTableLoading(false))
+
+ if (init && (err || !res)) {
+ dispatch(resetTableData())
+ throw new Error(err)
+ }
+
+ const { identifiers = [] } = res
+
+ const models = identifiers.map(model => {
+ return {
+ ...model,
+ node: 'model',
+ id:
`{{${metalake}}}{{${catalog}}}{{${'model'}}}{{${schema}}}{{${model.name}}}`,
+ key:
`{{${metalake}}}{{${catalog}}}{{${'model'}}}{{${schema}}}{{${model.name}}}`,
+ path: `?${new URLSearchParams({
+ metalake,
+ catalog,
+ type: 'model',
+ schema,
+ model: model.name
+ }).toString()}`,
+ name: model.name,
+ title: model.name,
+ isLeaf: true
+ }
+ })
+
+ if (init &&
getState().metalakes.loadedNodes.includes(`{{${metalake}}}{{${catalog}}}{{${'model'}}}{{${schema}}}`))
{
+ dispatch(
+ setIntoTreeNodes({
+ key: `{{${metalake}}}{{${catalog}}}{{${'model'}}}{{${schema}}}`,
+ data: models,
+ tree: getState().metalakes.metalakeTree
+ })
+ )
+ }
+
+ dispatch(
+ setExpandedNodes([
+ `{{${metalake}}}`,
+ `{{${metalake}}}{{${catalog}}}{{${'model'}}}`,
+ `{{${metalake}}}{{${catalog}}}{{${'model'}}}{{${schema}}}`
+ ])
+ )
+
+ return { models, page, init }
+ }
+)
+
+export const getModelDetails = createAsyncThunk(
+ 'appMetalakes/getModelDetails',
+ async ({ init, metalake, catalog, schema, model }, { getState, dispatch })
=> {
+ const [err, res] = await to(getModelDetailsApi({ metalake, catalog,
schema, model }))
+
+ if (err || !res) {
+ throw new Error(err)
+ }
+
+ const { model: resModel } = res
+
+ return resModel
+ }
+)
+
+export const registerModel = createAsyncThunk(
+ 'appMetalakes/registerModel',
+ async ({ data, metalake, catalog, type, schema }, { dispatch }) => {
+ dispatch(setTableLoading(true))
+ const [err, res] = await to(registerModelApi({ data, metalake, catalog,
schema }))
+ dispatch(setTableLoading(false))
+
+ if (err || !res) {
+ return { err: true }
+ }
+
+ const { model: modelItem } = res
+
+ const modelData = {
+ ...modelItem,
+ node: 'model',
+ id:
`{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema}}}{{${modelItem.name}}}`,
+ key:
`{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema}}}{{${modelItem.name}}}`,
+ path: `?${new URLSearchParams({ metalake, catalog, type, schema, model:
modelItem.name }).toString()}`,
+ name: modelItem.name,
+ title: modelItem.name,
+ tables: [],
+ children: []
+ }
+
+ dispatch(fetchModels({ metalake, catalog, schema, type, init: true }))
+
+ return modelData
+ }
+)
+
+export const updateModel = createAsyncThunk(
+ 'appMetalakes/updateModel',
+ async ({ metalake, catalog, type, schema, model, data }, { dispatch }) => {
+ const [err, res] = await to(updateTopicApi({ metalake, catalog, schema,
model, data }))
+ if (err || !res) {
+ return { err: true }
+ }
+ dispatch(fetchModels({ metalake, catalog, type, schema, init: true }))
+
+ return res.model
+ }
+)
+
+export const deleteModel = createAsyncThunk(
+ 'appMetalakes/deleteModel',
+ async ({ metalake, catalog, type, schema, model }, { dispatch }) => {
+ dispatch(setTableLoading(true))
+ const [err, res] = await to(deleteModleApi({ metalake, catalog, schema,
model }))
+ dispatch(setTableLoading(false))
+
+ if (err || !res) {
+ throw new Error(err)
+ }
+
+ dispatch(fetchModels({ metalake, catalog, type, schema, page: 'models',
init: true }))
+
+ return res
+ }
+)
+
+export const fetchModelVersions = createAsyncThunk(
+ 'appMetalakes/fetchModelVersions',
+ async ({ init, page, metalake, catalog, schema, model }, { getState,
dispatch }) => {
+ if (init) {
+ dispatch(setTableLoading(true))
+ }
+
+ const [err, res] = await to(getModelVersionsApi({ metalake, catalog,
schema, model }))
+ dispatch(setTableLoading(false))
+
+ if (init && (err || !res)) {
+ dispatch(resetTableData())
+ throw new Error(err)
+ }
+
+ const { versions = [] } = res
+
+ const versionsData = versions.map(version => {
+ return {
+ node: 'version',
+ id:
`{{${metalake}}}{{${catalog}}}{{${'model'}}}{{${schema}}}{{${model}}}{{${version}}}`,
+ key:
`{{${metalake}}}{{${catalog}}}{{${'model'}}}{{${schema}}}{{${model}}}{{${version}}}`,
+ path: `?${new URLSearchParams({
+ metalake,
+ catalog,
+ type: 'model',
+ schema,
+ model,
+ version
+ }).toString()}`,
+ name: version,
+ title: version,
+ isLeaf: true
+ }
+ })
+
+ return { versions: versionsData, page, init }
+ }
+)
+
+export const getVersionDetails = createAsyncThunk(
+ 'appMetalakes/getVersionDetails',
+ async ({ init, metalake, catalog, schema, model, version }, { getState,
dispatch }) => {
+ dispatch(resetTableData())
+ if (init) {
+ dispatch(setTableLoading(true))
+ }
+ const [err, res] = await to(getVersionDetailsApi({ metalake, catalog,
schema, model, version }))
+ dispatch(setTableLoading(false))
+
+ if (err || !res) {
+ dispatch(resetTableData())
+ throw new Error(err)
+ }
+
+ const { modelVersion } = res
+
+ return modelVersion
+ }
+)
+
+export const linkVersion = createAsyncThunk(
+ 'appMetalakes/linkVersion',
+ async ({ data, metalake, catalog, type, schema, model }, { dispatch }) => {
+ dispatch(setTableLoading(true))
+ const [err, res] = await to(linkVersionApi({ data, metalake, catalog,
schema, model }))
+ dispatch(setTableLoading(false))
+
+ if (err || !res) {
+ return { err: true }
+ }
+
+ const { version: versionItem } = res
+
+ const versionData = {
+ node: 'version',
+ id:
`{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema}}}{{${model}}}{{${versionItem}}}`,
+ key:
`{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema}}}{{${model}}}{{${versionItem}}}`,
+ path: `?${new URLSearchParams({ metalake, catalog, type, schema,
version: versionItem }).toString()}`,
+ name: versionItem,
+ title: versionItem,
+ tables: [],
+ children: []
+ }
+
+ dispatch(fetchModelVersions({ metalake, catalog, schema, type, model,
init: true }))
+
+ return versionData
+ }
+)
+
+export const deleteVersion = createAsyncThunk(
+ 'appMetalakes/deleteVersion',
+ async ({ metalake, catalog, type, schema, model, version }, { dispatch }) =>
{
+ dispatch(setTableLoading(true))
+ const [err, res] = await to(deleteVersionApi({ metalake, catalog, schema,
model, version }))
+ dispatch(setTableLoading(false))
+
+ if (err || !res) {
+ throw new Error(err)
+ }
+
+ dispatch(fetchModelVersions({ metalake, catalog, type, schema, model,
page: 'versions', init: true }))
+
+ return res
+ }
+)
+
export const appMetalakesSlice = createSlice({
name: 'appMetalakes',
initialState: {
@@ -1178,6 +1452,8 @@ export const appMetalakesSlice = createSlice({
columns: [],
filesets: [],
topics: [],
+ models: [],
+ versions: [],
metalakeTree: [],
loadedNodes: [],
selectedNodes: [],
@@ -1232,6 +1508,8 @@ export const appMetalakesSlice = createSlice({
state.columns = []
state.filesets = []
state.topics = []
+ state.models = []
+ state.versions = []
},
setTableLoading(state, action) {
state.tableLoading = action.payload
@@ -1492,6 +1770,64 @@ export const appMetalakesSlice = createSlice({
toast.error(action.error.message)
}
})
+ builder.addCase(fetchModels.fulfilled, (state, action) => {
+ state.models = action.payload.models
+ if (action.payload.init) {
+ state.tableData = action.payload.models
+ }
+ })
+ builder.addCase(fetchModels.rejected, (state, action) => {
+ if (!action.error.message.includes('CanceledError')) {
+ toast.error(action.error.message)
+ }
+ })
+ builder.addCase(getModelDetails.fulfilled, (state, action) => {
+ state.activatedDetails = action.payload
+ })
+ builder.addCase(getModelDetails.rejected, (state, action) => {
+ if (!action.error.message.includes('CanceledError')) {
+ toast.error(action.error.message)
+ }
+ })
+ builder.addCase(registerModel.rejected, (state, action) => {
+ if (!action.error.message.includes('CanceledError')) {
+ toast.error(action.error.message)
+ }
+ })
+ builder.addCase(updateModel.rejected, (state, action) => {
+ if (!action.error.message.includes('CanceledError')) {
+ toast.error(action.error.message)
+ }
+ })
+ builder.addCase(deleteModel.rejected, (state, action) => {
+ if (!action.error.message.includes('CanceledError')) {
+ toast.error(action.error.message)
+ }
+ })
+ builder.addCase(fetchModelVersions.fulfilled, (state, action) => {
+ state.versions = action.payload.versions
+ if (action.payload.init) {
+ state.tableData = action.payload.versions
+ }
+ })
+ builder.addCase(fetchModelVersions.rejected, (state, action) => {
+ if (!action.error.message.includes('CanceledError')) {
+ toast.error(action.error.message)
+ }
+ })
+ builder.addCase(getVersionDetails.fulfilled, (state, action) => {
+ state.activatedDetails = action.payload
+ })
+ builder.addCase(getVersionDetails.rejected, (state, action) => {
+ if (!action.error.message.includes('CanceledError')) {
+ toast.error(action.error.message)
+ }
+ })
+ builder.addCase(linkVersion.rejected, (state, action) => {
+ if (!action.error.message.includes('CanceledError')) {
+ toast.error(action.error.message)
+ }
+ })
}
})