mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-10 00:05:27 +00:00
[ER Diagrams] Add ER diagram APIs and sample data (#18021)
* Add ER diag APIs and sample data * fix pylint * formatting fixes2 * fixed es client return * fixed os client return * supported TableDetailPage tabs as classBase for supporting collate only tabs * Added schema Apis * change the base class to .ts and move the component in the util files * beautify function arguments * Added optimizations * Ingestion changes * svg dimension change * supported class base tab in databaseSchema * supported classBase action button in schema table name column * added further keys data for constraint modal * fix sonar issue * remove old method to override edit action on column and shifted to DisplayNameModal for fields * supported table right panel component to further extends on collate side * minor fix around duplicate constraint * added support to update table constraints and column constraints in the UI * code optimization and minor fixes * review comments and multi col fix * added queryFilter option in NodeSuggestion and tableConstrainst to fetch and use only in service tables --------- Co-authored-by: Ashish Gupta <ashish@getcollate.io>
This commit is contained in:
parent
9d91325af8
commit
4a0c8406e9
8
ingestion/examples/sample_data/mysql/database.json
Normal file
8
ingestion/examples/sample_data/mysql/database.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": null,
|
||||
"name": "default",
|
||||
"service": {
|
||||
"id": "b946d870-03b2-4d33-a075-13665a7a76b9",
|
||||
"type": "MYSQL"
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": null,
|
||||
"name": "posts_db",
|
||||
"service": {
|
||||
"id": "b946d870-03b2-4d33-a075-13665a7a76b9",
|
||||
"type": "MYSQL"
|
||||
}
|
||||
}
|
20
ingestion/examples/sample_data/mysql/database_service.json
Normal file
20
ingestion/examples/sample_data/mysql/database_service.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"type": "mysql",
|
||||
"serviceName": "mysql_sample",
|
||||
"serviceConnection": {
|
||||
"config": {
|
||||
"type": "Mysql",
|
||||
"hostPort": "localhost:3306",
|
||||
"username": "openmetadata_user",
|
||||
"authType": {
|
||||
"password": "openmetadata_password"
|
||||
},
|
||||
"databaseSchema": "posts_db"
|
||||
}
|
||||
},
|
||||
"sourceConfig": {
|
||||
"config": {
|
||||
"type": "DatabaseMetadata"
|
||||
}
|
||||
}
|
||||
}
|
573
ingestion/examples/sample_data/mysql/tables.json
Normal file
573
ingestion/examples/sample_data/mysql/tables.json
Normal file
@ -0,0 +1,573 @@
|
||||
{
|
||||
"tables": [
|
||||
{
|
||||
"name": "Tags",
|
||||
"displayName": null,
|
||||
"fullyQualifiedName": "mysql_sample.default.new_er_database.Tags",
|
||||
"description": null,
|
||||
"tableType": "Regular",
|
||||
"columns": [
|
||||
{
|
||||
"name": "tag_id",
|
||||
"dataType": "INT",
|
||||
"dataLength": 1,
|
||||
"description": null,
|
||||
"constraint": "PRIMARY_KEY",
|
||||
"ordinalPosition": 1
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"dataType": "VARCHAR",
|
||||
"dataLength": 100,
|
||||
"dataTypeDisplay": "varchar(100)",
|
||||
"description": null,
|
||||
"constraint": "NOT_NULL",
|
||||
"ordinalPosition": 2
|
||||
}
|
||||
],
|
||||
"databaseSchema": {
|
||||
"id": "5f40fbdc-7652-4bb5-8dd8-5834c382b8cf",
|
||||
"type": "databaseSchema",
|
||||
"name": "new_er_database",
|
||||
"fullyQualifiedName": "mysql_sample.default.new_er_database",
|
||||
"description": null,
|
||||
"displayName": "new_er_database",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
},
|
||||
"database": {
|
||||
"id": "9ec40d31-2cc3-434b-b79e-93a22ffb695b",
|
||||
"type": "database",
|
||||
"name": "default",
|
||||
"fullyQualifiedName": "mysql_sample.default",
|
||||
"description": null,
|
||||
"displayName": "default",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
},
|
||||
"service": {
|
||||
"id": "93fd8fbb-cecd-46b5-ae9a-0f6cda13a923",
|
||||
"type": "databaseService",
|
||||
"name": "mysql_sample",
|
||||
"fullyQualifiedName": "mysql_sample",
|
||||
"description": null,
|
||||
"displayName": "mysql_sample",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Users",
|
||||
"displayName": null,
|
||||
"fullyQualifiedName": "mysql_sample.default.new_er_database.Users",
|
||||
"description": null,
|
||||
"tableType": "Regular",
|
||||
"columns": [
|
||||
{
|
||||
"name": "user_id",
|
||||
"dataType": "INT",
|
||||
"dataLength": 1,
|
||||
"dataTypeDisplay": "int",
|
||||
"constraint": "PRIMARY_KEY",
|
||||
"ordinalPosition": 1
|
||||
},
|
||||
{
|
||||
"name": "username",
|
||||
"dataType": "VARCHAR",
|
||||
"dataLength": 50,
|
||||
"dataTypeDisplay": "varchar(50)",
|
||||
"constraint": "NOT_NULL",
|
||||
"ordinalPosition": 2
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"dataType": "VARCHAR",
|
||||
"dataLength": 100,
|
||||
"dataTypeDisplay": "varchar(100)",
|
||||
"constraint": "NOT_NULL",
|
||||
"ordinalPosition": 3
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"dataType": "TIMESTAMP",
|
||||
"dataLength": 1,
|
||||
"dataTypeDisplay": "timestamp",
|
||||
"constraint": "NULL",
|
||||
"ordinalPosition": 4
|
||||
}
|
||||
],
|
||||
"databaseSchema": {
|
||||
"id": "5f40fbdc-7652-4bb5-8dd8-5834c382b8cf",
|
||||
"type": "databaseSchema",
|
||||
"name": "new_er_database",
|
||||
"fullyQualifiedName": "mysql_sample.default.new_er_database",
|
||||
"description": null,
|
||||
"displayName": "new_er_database",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
},
|
||||
"database": {
|
||||
"id": "9ec40d31-2cc3-434b-b79e-93a22ffb695b",
|
||||
"type": "database",
|
||||
"name": "default",
|
||||
"fullyQualifiedName": "mysql_sample.default",
|
||||
"description": null,
|
||||
"displayName": "default",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
},
|
||||
"service": {
|
||||
"id": "93fd8fbb-cecd-46b5-ae9a-0f6cda13a923",
|
||||
"type": "databaseService",
|
||||
"name": "mysql_sample",
|
||||
"fullyQualifiedName": "mysql_sample",
|
||||
"description": null,
|
||||
"displayName": "mysql_sample",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Categories",
|
||||
"displayName": null,
|
||||
"fullyQualifiedName": "mysql_sample.default.posts_db.Categories",
|
||||
"description": null,
|
||||
"tableType": "Regular",
|
||||
"columns": [
|
||||
{
|
||||
"name": "category_id",
|
||||
"displayName": null,
|
||||
"dataType": "INT",
|
||||
"dataLength": 100,
|
||||
"dataTypeDisplay": "int",
|
||||
"description": null,
|
||||
"constraint": "PRIMARY_KEY",
|
||||
"ordinalPosition": 1
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"dataType": "VARCHAR",
|
||||
"dataLength": 100,
|
||||
"dataTypeDisplay": "varchar(100)",
|
||||
"description": null,
|
||||
"constraint": "NOT_NULL",
|
||||
"ordinalPosition": 2
|
||||
}
|
||||
],
|
||||
"databaseSchema": {
|
||||
"type": "databaseSchema",
|
||||
"name": "posts_db",
|
||||
"fullyQualifiedName": "mysql_sample.default.posts_db",
|
||||
"description": null,
|
||||
"displayName": "posts_db",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
},
|
||||
"database": {
|
||||
"type": "database",
|
||||
"name": "default",
|
||||
"fullyQualifiedName": "mysql_sample.default",
|
||||
"description": null,
|
||||
"displayName": "default",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
},
|
||||
"service": {
|
||||
"type": "databaseService",
|
||||
"name": "mysql_sample",
|
||||
"fullyQualifiedName": "mysql_sample",
|
||||
"description": null,
|
||||
"displayName": "mysql_sample",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Comments",
|
||||
"displayName": null,
|
||||
"fullyQualifiedName": "mysql_sample.default.posts_db.Comments",
|
||||
"description": null,
|
||||
"tableType": "Regular",
|
||||
"columns": [
|
||||
{
|
||||
"name": "comment_id",
|
||||
"displayName": null,
|
||||
"dataType": "INT",
|
||||
"dataTypeDisplay": "int",
|
||||
"description": null,
|
||||
"constraint": "PRIMARY_KEY",
|
||||
"ordinalPosition": 1
|
||||
},
|
||||
{
|
||||
"name": "post_id",
|
||||
"displayName": null,
|
||||
"dataType": "INT",
|
||||
"dataTypeDisplay": "int",
|
||||
"description": null,
|
||||
"constraint": "NULL",
|
||||
"ordinalPosition": 2
|
||||
},
|
||||
{
|
||||
"name": "user_id",
|
||||
"displayName": null,
|
||||
"dataType": "INT",
|
||||
"dataTypeDisplay": "int",
|
||||
"description": null,
|
||||
"constraint": "NULL",
|
||||
"ordinalPosition": 3
|
||||
},
|
||||
{
|
||||
"name": "comment",
|
||||
"displayName": null,
|
||||
"dataType": "TEXT",
|
||||
"dataTypeDisplay": "text",
|
||||
"description": null,
|
||||
"constraint": "NOT_NULL",
|
||||
"ordinalPosition": 4
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"displayName": null,
|
||||
"dataType": "TIMESTAMP",
|
||||
"dataTypeDisplay": "timestamp",
|
||||
"description": null,
|
||||
"constraint": "NULL",
|
||||
"ordinalPosition": 5
|
||||
}
|
||||
],
|
||||
"tableConstraints": [
|
||||
{
|
||||
"constraintType": "FOREIGN_KEY",
|
||||
"columns": [
|
||||
"post_id"
|
||||
],
|
||||
"referredColumns": [
|
||||
"mysql_sample.default.posts_db.Posts.post_id"
|
||||
],
|
||||
"relationshipType": "MANY_TO_ONE"
|
||||
},
|
||||
{
|
||||
"constraintType": "FOREIGN_KEY",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"referredColumns": [
|
||||
"mysql_sample.default.posts_db.Users.user_id"
|
||||
],
|
||||
"relationshipType": "MANY_TO_ONE"
|
||||
}
|
||||
],
|
||||
"databaseSchema": {
|
||||
"type": "databaseSchema",
|
||||
"name": "posts_db",
|
||||
"fullyQualifiedName": "mysql_sample.default.posts_db",
|
||||
"description": null,
|
||||
"displayName": "posts_db",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
},
|
||||
"database": {
|
||||
"type": "database",
|
||||
"name": "default",
|
||||
"fullyQualifiedName": "mysql_sample.default",
|
||||
"description": null,
|
||||
"displayName": "default",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
},
|
||||
"service": {
|
||||
"type": "databaseService",
|
||||
"name": "mysql_sample",
|
||||
"fullyQualifiedName": "mysql_sample",
|
||||
"description": null,
|
||||
"displayName": "mysql_sample",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Posts",
|
||||
"displayName": null,
|
||||
"fullyQualifiedName": "mysql_sample.default.posts_db.Posts",
|
||||
"description": null,
|
||||
"tableType": "Regular",
|
||||
"columns": [
|
||||
{
|
||||
"name": "post_id",
|
||||
"displayName": null,
|
||||
"dataType": "INT",
|
||||
"dataTypeDisplay": "int",
|
||||
"description": null,
|
||||
"constraint": "PRIMARY_KEY",
|
||||
"ordinalPosition": 1
|
||||
},
|
||||
{
|
||||
"name": "user_id",
|
||||
"displayName": null,
|
||||
"dataType": "INT",
|
||||
"dataTypeDisplay": "int",
|
||||
"description": null,
|
||||
"constraint": "NULL",
|
||||
"ordinalPosition": 2
|
||||
},
|
||||
{
|
||||
"name": "category_id",
|
||||
"displayName": null,
|
||||
"dataType": "INT",
|
||||
"dataTypeDisplay": "int",
|
||||
"description": null,
|
||||
"constraint": "NULL",
|
||||
"ordinalPosition": 3
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"displayName": null,
|
||||
"dataType": "VARCHAR",
|
||||
"arrayDataType": null,
|
||||
"dataLength": 255,
|
||||
"precision": null,
|
||||
"scale": null,
|
||||
"dataTypeDisplay": "varchar(255)",
|
||||
"description": null,
|
||||
"constraint": "NOT_NULL",
|
||||
"ordinalPosition": 4
|
||||
},
|
||||
{
|
||||
"name": "content",
|
||||
"displayName": null,
|
||||
"dataType": "TEXT",
|
||||
"dataTypeDisplay": "text",
|
||||
"description": null,
|
||||
"constraint": "NOT_NULL",
|
||||
"ordinalPosition": 5
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"displayName": null,
|
||||
"dataType": "TIMESTAMP",
|
||||
"dataTypeDisplay": "timestamp",
|
||||
"description": null,
|
||||
"constraint": "NULL",
|
||||
"ordinalPosition": 6
|
||||
}
|
||||
],
|
||||
"tableConstraints": [
|
||||
{
|
||||
"constraintType": "FOREIGN_KEY",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"referredColumns": [
|
||||
"mysql_sample.default.posts_db.Users.user_id"
|
||||
],
|
||||
"relationshipType": "MANY_TO_ONE"
|
||||
},
|
||||
{
|
||||
"constraintType": "FOREIGN_KEY",
|
||||
"columns": [
|
||||
"category_id"
|
||||
],
|
||||
"referredColumns": [
|
||||
"mysql_sample.default.posts_db.Categories.category_id"
|
||||
],
|
||||
"relationshipType": "MANY_TO_ONE"
|
||||
}
|
||||
],
|
||||
"databaseSchema": {
|
||||
"id": "3b2c045a-03ea-4303-abf3-082ac4e73804",
|
||||
"type": "databaseSchema",
|
||||
"name": "posts_db",
|
||||
"fullyQualifiedName": "mysql_sample.default.posts_db",
|
||||
"description": null,
|
||||
"displayName": "posts_db",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
},
|
||||
"database": {
|
||||
"id": "c1a9f3bf-8bb8-43e3-beeb-e4c2293b977a",
|
||||
"type": "database",
|
||||
"name": "default",
|
||||
"fullyQualifiedName": "mysql_sample.default",
|
||||
"description": null,
|
||||
"displayName": "default",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
},
|
||||
"service": {
|
||||
"id": "c0382692-7cf3-40b7-9aa7-b14bf2cbecdd",
|
||||
"type": "databaseService",
|
||||
"name": "mysql_sample",
|
||||
"fullyQualifiedName": "mysql_sample",
|
||||
"description": null,
|
||||
"displayName": "mysql_sample",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "PostTags",
|
||||
"displayName": null,
|
||||
"fullyQualifiedName": "mysql_sample.default.posts_db.PostTags",
|
||||
"description": "testdesc2",
|
||||
"tableType": "Regular",
|
||||
"columns": [
|
||||
{
|
||||
"name": "post_id",
|
||||
"displayName": null,
|
||||
"dataType": "INT",
|
||||
"dataTypeDisplay": "int",
|
||||
"description": null,
|
||||
"constraint": null,
|
||||
"ordinalPosition": 1
|
||||
},
|
||||
{
|
||||
"name": "tag_id",
|
||||
"displayName": null,
|
||||
"dataType": "INT",
|
||||
"dataTypeDisplay": "int",
|
||||
"description": null,
|
||||
"constraint": null,
|
||||
"ordinalPosition": 2
|
||||
}
|
||||
],
|
||||
"tableConstraints": [
|
||||
{
|
||||
"constraintType": "PRIMARY_KEY",
|
||||
"columns": [
|
||||
"post_id",
|
||||
"tag_id"
|
||||
],
|
||||
"referredColumns": null,
|
||||
"relationshipType": null
|
||||
},
|
||||
{
|
||||
"constraintType": "FOREIGN_KEY",
|
||||
"columns": [
|
||||
"post_id"
|
||||
],
|
||||
"referredColumns": [
|
||||
"mysql_sample.default.posts_db.Posts.post_id"
|
||||
],
|
||||
"relationshipType": "MANY_TO_ONE"
|
||||
},
|
||||
{
|
||||
"constraintType": "FOREIGN_KEY",
|
||||
"columns": [
|
||||
"tag_id"
|
||||
],
|
||||
"referredColumns": [
|
||||
"mysql_sample.default.posts_db.Tags.tag_id"
|
||||
],
|
||||
"relationshipType": "MANY_TO_ONE"
|
||||
}
|
||||
],
|
||||
"databaseSchema": {
|
||||
"type": "databaseSchema",
|
||||
"name": "posts_db",
|
||||
"fullyQualifiedName": "mysql_sample.default.posts_db",
|
||||
"description": null,
|
||||
"displayName": "posts_db",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
},
|
||||
"database": {
|
||||
"id": "c1a9f3bf-8bb8-43e3-beeb-e4c2293b977a",
|
||||
"type": "database",
|
||||
"name": "default",
|
||||
"fullyQualifiedName": "mysql_sample.default",
|
||||
"description": null,
|
||||
"displayName": "default",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
},
|
||||
"service": {
|
||||
"id": "c0382692-7cf3-40b7-9aa7-b14bf2cbecdd",
|
||||
"type": "databaseService",
|
||||
"name": "mysql_sample",
|
||||
"fullyQualifiedName": "mysql_sample",
|
||||
"description": null,
|
||||
"displayName": "mysql_sample",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Profiles",
|
||||
"displayName": null,
|
||||
"fullyQualifiedName": "mysql_sample.default.posts_db.Profiles",
|
||||
"description": null,
|
||||
"tableType": "Regular",
|
||||
"columns": [
|
||||
{
|
||||
"name": "profile_id",
|
||||
"displayName": null,
|
||||
"dataType": "INT",
|
||||
"dataTypeDisplay": "int",
|
||||
"description": null,
|
||||
"constraint": "PRIMARY_KEY",
|
||||
"ordinalPosition": 1
|
||||
},
|
||||
{
|
||||
"name": "user_id",
|
||||
"displayName": null,
|
||||
"dataType": "INT",
|
||||
"dataTypeDisplay": "int",
|
||||
"description": null,
|
||||
"constraint": "UNIQUE",
|
||||
"ordinalPosition": 2
|
||||
},
|
||||
{
|
||||
"name": "bio",
|
||||
"displayName": null,
|
||||
"dataType": "TEXT",
|
||||
"dataTypeDisplay": "text",
|
||||
"description": null,
|
||||
"constraint": "NULL",
|
||||
"ordinalPosition": 3
|
||||
}
|
||||
],
|
||||
"tableConstraints": [
|
||||
{
|
||||
"constraintType": "FOREIGN_KEY",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"referredColumns": [
|
||||
"mysql_sample.default.posts_db.Users.user_id"
|
||||
],
|
||||
"relationshipType": "ONE_TO_ONE"
|
||||
}
|
||||
],
|
||||
"databaseSchema": {
|
||||
"id": "3b2c045a-03ea-4303-abf3-082ac4e73804",
|
||||
"type": "databaseSchema",
|
||||
"name": "posts_db",
|
||||
"fullyQualifiedName": "mysql_sample.default.posts_db",
|
||||
"description": null,
|
||||
"displayName": "posts_db",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
},
|
||||
"database": {
|
||||
"id": "c1a9f3bf-8bb8-43e3-beeb-e4c2293b977a",
|
||||
"type": "database",
|
||||
"name": "default",
|
||||
"fullyQualifiedName": "mysql_sample.default",
|
||||
"description": null,
|
||||
"displayName": "default",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
},
|
||||
"service": {
|
||||
"id": "c0382692-7cf3-40b7-9aa7-b14bf2cbecdd",
|
||||
"type": "databaseService",
|
||||
"name": "mysql_sample",
|
||||
"fullyQualifiedName": "mysql_sample",
|
||||
"description": null,
|
||||
"displayName": "mysql_sample",
|
||||
"deleted": false,
|
||||
"inherited": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -11,13 +11,14 @@
|
||||
"""
|
||||
Generic source to build SQL connectors.
|
||||
"""
|
||||
import copy
|
||||
import math
|
||||
import time
|
||||
import traceback
|
||||
from abc import ABC
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from copy import deepcopy
|
||||
from typing import Any, Iterable, List, Optional, Tuple, Union, cast
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union, cast
|
||||
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.engine import Connection
|
||||
@ -38,6 +39,7 @@ from metadata.generated.schema.api.lineage.addLineage import AddLineageRequest
|
||||
from metadata.generated.schema.entity.data.database import Database
|
||||
from metadata.generated.schema.entity.data.databaseSchema import DatabaseSchema
|
||||
from metadata.generated.schema.entity.data.table import (
|
||||
Column,
|
||||
ConstraintType,
|
||||
Table,
|
||||
TableConstraint,
|
||||
@ -63,6 +65,7 @@ from metadata.ingestion.api.models import Either
|
||||
from metadata.ingestion.connections.session import create_and_bind_thread_safe_session
|
||||
from metadata.ingestion.models.ometa_classification import OMetaTagAndClassification
|
||||
from metadata.ingestion.models.ometa_lineage import OMetaLineageRequest
|
||||
from metadata.ingestion.models.patch_request import PatchedEntity, PatchRequest
|
||||
from metadata.ingestion.models.topology import Queue
|
||||
from metadata.ingestion.ometa.ometa_api import OpenMetadata
|
||||
from metadata.ingestion.source.connections import (
|
||||
@ -75,6 +78,7 @@ from metadata.ingestion.source.database.sqlalchemy_source import SqlAlchemySourc
|
||||
from metadata.ingestion.source.database.stored_procedures_mixin import QueryByProcedure
|
||||
from metadata.ingestion.source.models import TableView
|
||||
from metadata.utils import fqn
|
||||
from metadata.utils.constraints import get_relationship_type
|
||||
from metadata.utils.db_utils import get_view_lineage
|
||||
from metadata.utils.execution_time_tracker import (
|
||||
ExecutionTimeTrackerContextMap,
|
||||
@ -88,6 +92,13 @@ from metadata.utils.ssl_manager import SSLManager, check_ssl_and_init
|
||||
logger = ingestion_logger()
|
||||
|
||||
|
||||
class ColumnAndReferredColumn(BaseModel):
|
||||
table_name: str
|
||||
schema_name: str
|
||||
db_name: Optional[str]
|
||||
column: Dict
|
||||
|
||||
|
||||
class TableNameAndType(BaseModel):
|
||||
"""
|
||||
Helper model for passing down
|
||||
@ -141,6 +152,7 @@ class CommonDbSourceService(
|
||||
self.database_source_state = set()
|
||||
self.context.get_global().table_views = []
|
||||
self.context.get_global().table_constrains = []
|
||||
self.context.get_global().foreign_tables = []
|
||||
self.context.set_threads(self.source_config.threads)
|
||||
super().__init__()
|
||||
|
||||
@ -518,7 +530,12 @@ class CommonDbSourceService(
|
||||
)
|
||||
|
||||
table_constraints = self.update_table_constraints(
|
||||
table_constraints, foreign_columns
|
||||
schema_name=schema_name,
|
||||
table_name=table_name,
|
||||
db_name=self.context.get().database,
|
||||
table_constraints=table_constraints,
|
||||
foreign_columns=foreign_columns,
|
||||
columns=columns,
|
||||
)
|
||||
|
||||
description = (
|
||||
@ -697,7 +714,74 @@ class CommonDbSourceService(
|
||||
else:
|
||||
yield from self._process_view_def_serial()
|
||||
|
||||
def _get_foreign_constraints(self, foreign_columns) -> List[TableConstraint]:
|
||||
def _prepare_foreign_constraints( # pylint: disable=too-many-arguments, too-many-locals
|
||||
self,
|
||||
supports_database: bool,
|
||||
column: Dict,
|
||||
table_name: str,
|
||||
schema_name: str,
|
||||
db_name: str,
|
||||
columns: List[Column],
|
||||
add_to_global: bool = True,
|
||||
):
|
||||
"""
|
||||
Method to prepare the foreign constraints
|
||||
"""
|
||||
referred_column_fqns = []
|
||||
if supports_database:
|
||||
database_name = column.get("referred_database")
|
||||
else:
|
||||
database_name = self.context.get().database
|
||||
referred_table_fqn = fqn.build(
|
||||
metadata=self.metadata,
|
||||
entity_type=Table,
|
||||
table_name=column.get("referred_table"),
|
||||
schema_name=column.get("referred_schema"),
|
||||
database_name=database_name,
|
||||
service_name=self.context.get().database_service,
|
||||
)
|
||||
referred_table = self.metadata.get_by_name(entity=Table, fqn=referred_table_fqn)
|
||||
if referred_table:
|
||||
for referred_column in column.get("referred_columns"):
|
||||
col_fqn = fqn._build( # pylint: disable=protected-access
|
||||
referred_table_fqn, referred_column, quote=False
|
||||
)
|
||||
if col_fqn:
|
||||
referred_column_fqns.append(FullyQualifiedEntityName(col_fqn))
|
||||
else:
|
||||
if add_to_global:
|
||||
column_and_referred_columns = ColumnAndReferredColumn(
|
||||
table_name=table_name,
|
||||
schema_name=schema_name,
|
||||
db_name=db_name,
|
||||
column=column,
|
||||
)
|
||||
self.context.get_global().foreign_tables.append(
|
||||
column_and_referred_columns
|
||||
)
|
||||
return None
|
||||
relationship_type = None
|
||||
if referred_table:
|
||||
relationship_type = get_relationship_type(
|
||||
column, # sqlalchemy foreign column
|
||||
referred_table.columns, # referred table columns
|
||||
columns, # current table om columns
|
||||
)
|
||||
return TableConstraint(
|
||||
constraintType=ConstraintType.FOREIGN_KEY,
|
||||
columns=column.get("constrained_columns"),
|
||||
referredColumns=referred_column_fqns,
|
||||
relationshipType=relationship_type,
|
||||
)
|
||||
|
||||
def _get_foreign_constraints(
|
||||
self,
|
||||
table_name,
|
||||
schema_name,
|
||||
db_name,
|
||||
foreign_columns: List[Dict],
|
||||
columns: List[Column],
|
||||
) -> List[TableConstraint]:
|
||||
"""
|
||||
Search the referred table for foreign constraints
|
||||
and get referred column fqn
|
||||
@ -706,48 +790,31 @@ class CommonDbSourceService(
|
||||
|
||||
foreign_constraints = []
|
||||
for column in foreign_columns:
|
||||
referred_column_fqns = []
|
||||
if supports_database:
|
||||
database_name = column.get("referred_database")
|
||||
else:
|
||||
database_name = self.context.get().database
|
||||
referred_table_fqn = fqn.build(
|
||||
metadata=self.metadata,
|
||||
entity_type=Table,
|
||||
table_name=column.get("referred_table"),
|
||||
schema_name=column.get("referred_schema"),
|
||||
database_name=database_name,
|
||||
service_name=self.context.get().database_service,
|
||||
)
|
||||
if referred_table_fqn:
|
||||
for referred_column in column.get("referred_columns"):
|
||||
col_fqn = fqn._build(
|
||||
referred_table_fqn, referred_column, quote=False
|
||||
)
|
||||
if col_fqn:
|
||||
referred_column_fqns.append(FullyQualifiedEntityName(col_fqn))
|
||||
else:
|
||||
# do not build partial foreign constraint. It will updated in next run.
|
||||
continue
|
||||
foreign_constraints.append(
|
||||
TableConstraint(
|
||||
constraintType=ConstraintType.FOREIGN_KEY,
|
||||
columns=column.get("constrained_columns"),
|
||||
referredColumns=referred_column_fqns,
|
||||
)
|
||||
foreign_constraint = self._prepare_foreign_constraints(
|
||||
supports_database, column, table_name, schema_name, db_name, columns
|
||||
)
|
||||
if foreign_constraint:
|
||||
foreign_constraints.append(foreign_constraint)
|
||||
|
||||
return foreign_constraints
|
||||
|
||||
@calculate_execution_time()
|
||||
def update_table_constraints(
|
||||
self, table_constraints, foreign_columns
|
||||
self,
|
||||
table_name,
|
||||
schema_name,
|
||||
db_name,
|
||||
table_constraints,
|
||||
foreign_columns,
|
||||
columns,
|
||||
) -> List[TableConstraint]:
|
||||
"""
|
||||
From topology.
|
||||
process the table constraints of all tables
|
||||
"""
|
||||
foreign_table_constraints = self._get_foreign_constraints(foreign_columns)
|
||||
foreign_table_constraints = self._get_foreign_constraints(
|
||||
table_name, schema_name, db_name, foreign_columns, columns
|
||||
)
|
||||
if foreign_table_constraints:
|
||||
if table_constraints:
|
||||
table_constraints.extend(foreign_table_constraints)
|
||||
@ -816,3 +883,55 @@ class CommonDbSourceService(
|
||||
"""
|
||||
By default the source url is not supported for
|
||||
"""
|
||||
|
||||
def yield_table_constraints(self) -> Iterable[Either[PatchedEntity]]:
|
||||
"""
|
||||
Process remaining table constraints by patching the table
|
||||
"""
|
||||
supports_database = hasattr(self.service_connection, "supportsDatabase")
|
||||
|
||||
for foreign_table in self.context.get_global().foreign_tables or []:
|
||||
try:
|
||||
foreign_constraints = []
|
||||
table_fqn = fqn.build(
|
||||
metadata=self.metadata,
|
||||
entity_type=Table,
|
||||
service_name=self.context.get().database_service,
|
||||
database_name=foreign_table.db_name,
|
||||
schema_name=foreign_table.schema_name,
|
||||
table_name=foreign_table.table_name,
|
||||
)
|
||||
table = self.metadata.get_by_name(entity=Table, fqn=table_fqn)
|
||||
if table:
|
||||
foreign_constraint = self._prepare_foreign_constraints(
|
||||
supports_database,
|
||||
foreign_table.column,
|
||||
foreign_table.table_name,
|
||||
foreign_table.schema_name,
|
||||
foreign_table.db_name,
|
||||
table.columns,
|
||||
False,
|
||||
)
|
||||
if foreign_constraint:
|
||||
foreign_constraints.append(foreign_constraint)
|
||||
|
||||
# send the patch request
|
||||
if foreign_constraints:
|
||||
new_entity = copy.deepcopy(table)
|
||||
new_entity.tableConstraints = (
|
||||
new_entity.tableConstraints or []
|
||||
) + foreign_constraints
|
||||
patch_request = PatchRequest(
|
||||
original_entity=table,
|
||||
new_entity=new_entity,
|
||||
override_metadata=True,
|
||||
)
|
||||
yield Either(right=patch_request)
|
||||
except Exception as exc:
|
||||
yield Either(
|
||||
left=StackTraceError(
|
||||
name=str(foreign_table.table_name),
|
||||
error=f"Error to yield tableConstraints for {str(foreign_table.table_name)}: {exc}",
|
||||
stackTrace=traceback.format_exc(),
|
||||
)
|
||||
)
|
||||
|
@ -118,6 +118,7 @@ class DatabaseServiceTopology(ServiceTopology):
|
||||
post_process=[
|
||||
"yield_view_lineage",
|
||||
"yield_external_table_lineage",
|
||||
"yield_table_constraints",
|
||||
],
|
||||
)
|
||||
database: Annotated[
|
||||
@ -352,7 +353,13 @@ class DatabaseServiceSource(
|
||||
"""
|
||||
|
||||
def update_table_constraints(
|
||||
self, table_constraints: List[TableConstraint], foreign_columns: []
|
||||
self,
|
||||
table_name,
|
||||
schema_name,
|
||||
db_name,
|
||||
table_constraints: List[TableConstraint],
|
||||
foreign_columns: [],
|
||||
columns,
|
||||
) -> List[TableConstraint]:
|
||||
"""
|
||||
process the table constraints of all tables
|
||||
@ -538,7 +545,7 @@ class DatabaseServiceSource(
|
||||
self.inspector, "get_table_owner"
|
||||
):
|
||||
owner_name = self.inspector.get_table_owner(
|
||||
connection=self.connection, # pylint: disable=no-member.fetchall()
|
||||
connection=self.connection, # pylint: disable=no-member
|
||||
table_name=table_name,
|
||||
schema=self.context.get().database_schema,
|
||||
)
|
||||
@ -609,6 +616,11 @@ class DatabaseServiceSource(
|
||||
Process external table lineage
|
||||
"""
|
||||
|
||||
def yield_table_constraints(self) -> Iterable[Either[AddLineageRequest]]:
|
||||
"""
|
||||
Process remaining table constraints by patching the table
|
||||
"""
|
||||
|
||||
def test_connection(self) -> None:
|
||||
test_connection_fn = get_test_connection_fn(self.service_connection)
|
||||
result = test_connection_fn(
|
||||
|
@ -242,6 +242,41 @@ class SampleDataSource(
|
||||
entity=DatabaseService,
|
||||
config=WorkflowSource(**self.glue_database_service_json),
|
||||
)
|
||||
|
||||
# MYSQL service for er diagrams
|
||||
self.mysql_database_service_json = json.load(
|
||||
open( # pylint: disable=consider-using-with
|
||||
sample_data_folder + "/mysql/database_service.json",
|
||||
"r",
|
||||
encoding=UTF_8,
|
||||
)
|
||||
)
|
||||
self.mysql_database = json.load(
|
||||
open( # pylint: disable=consider-using-with
|
||||
sample_data_folder + "/mysql/database.json",
|
||||
"r",
|
||||
encoding=UTF_8,
|
||||
)
|
||||
)
|
||||
self.mysql_database_schema = json.load(
|
||||
open( # pylint: disable=consider-using-with
|
||||
sample_data_folder + "/mysql/database_schema.json",
|
||||
"r",
|
||||
encoding=UTF_8,
|
||||
)
|
||||
)
|
||||
self.mysql_tables = json.load(
|
||||
open( # pylint: disable=consider-using-with
|
||||
sample_data_folder + "/mysql/tables.json",
|
||||
"r",
|
||||
encoding=UTF_8,
|
||||
)
|
||||
)
|
||||
self.mysql_database_service = self.metadata.get_service_or_create(
|
||||
entity=DatabaseService,
|
||||
config=WorkflowSource(**self.mysql_database_service_json),
|
||||
)
|
||||
|
||||
self.database_service_json = json.load(
|
||||
open( # pylint: disable=consider-using-with
|
||||
sample_data_folder + "/datasets/service.json",
|
||||
@ -615,6 +650,7 @@ class SampleDataSource(
|
||||
yield from self.ingest_users()
|
||||
yield from self.ingest_tables()
|
||||
yield from self.ingest_glue()
|
||||
yield from self.ingest_mysql()
|
||||
yield from self.ingest_stored_procedures()
|
||||
yield from self.ingest_topics()
|
||||
yield from self.ingest_charts()
|
||||
@ -666,6 +702,55 @@ class SampleDataSource(
|
||||
|
||||
yield Either(right=team_to_ingest)
|
||||
|
||||
def ingest_mysql(self) -> Iterable[Either[Entity]]:
|
||||
"""Ingest Sample Data for mysql database source including ER diagrams metadata"""
|
||||
|
||||
db = CreateDatabaseRequest(
|
||||
name=self.mysql_database["name"],
|
||||
service=self.mysql_database_service.fullyQualifiedName,
|
||||
)
|
||||
|
||||
yield Either(right=db)
|
||||
|
||||
database_entity = fqn.build(
|
||||
self.metadata,
|
||||
entity_type=Database,
|
||||
service_name=self.mysql_database_service.fullyQualifiedName.root,
|
||||
database_name=db.name.root,
|
||||
)
|
||||
|
||||
database_object = self.metadata.get_by_name(
|
||||
entity=Database, fqn=database_entity
|
||||
)
|
||||
schema = CreateDatabaseSchemaRequest(
|
||||
name=self.mysql_database_schema["name"],
|
||||
database=database_object.fullyQualifiedName,
|
||||
)
|
||||
yield Either(right=schema)
|
||||
|
||||
database_schema_entity = fqn.build(
|
||||
self.metadata,
|
||||
entity_type=DatabaseSchema,
|
||||
service_name=self.mysql_database_service.fullyQualifiedName.root,
|
||||
database_name=db.name.root,
|
||||
schema_name=schema.name.root,
|
||||
)
|
||||
|
||||
database_schema_object = self.metadata.get_by_name(
|
||||
entity=DatabaseSchema, fqn=database_schema_entity
|
||||
)
|
||||
|
||||
for table in self.mysql_tables["tables"]:
|
||||
table_request = CreateTableRequest(
|
||||
name=table["name"],
|
||||
description=table["description"],
|
||||
columns=table["columns"],
|
||||
databaseSchema=database_schema_object.fullyQualifiedName,
|
||||
tableConstraints=table.get("tableConstraints"),
|
||||
tableType=table["tableType"],
|
||||
)
|
||||
yield Either(right=table_request)
|
||||
|
||||
def ingest_glue(self) -> Iterable[Either[Entity]]:
|
||||
"""Ingest Sample Data for glue database source"""
|
||||
|
||||
|
@ -325,7 +325,7 @@ class UnitycatalogSource(
|
||||
) = self.get_table_constraints(table.table_constraints)
|
||||
|
||||
table_constraints = self.update_table_constraints(
|
||||
primary_constraints, foreign_constraints
|
||||
primary_constraints, foreign_constraints, columns
|
||||
)
|
||||
|
||||
table_request = CreateTableRequest(
|
||||
@ -436,7 +436,7 @@ class UnitycatalogSource(
|
||||
return table_constraints
|
||||
|
||||
def update_table_constraints(
|
||||
self, table_constraints, foreign_columns
|
||||
self, table_constraints, foreign_columns, columns
|
||||
) -> List[TableConstraint]:
|
||||
"""
|
||||
From topology.
|
||||
|
65
ingestion/src/metadata/utils/constraints.py
Normal file
65
ingestion/src/metadata/utils/constraints.py
Normal file
@ -0,0 +1,65 @@
|
||||
# Copyright 2024 Collate
|
||||
# Licensed 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.
|
||||
|
||||
"""
|
||||
Define constraints helper methods useful for the metadata ingestion
|
||||
"""
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from metadata.generated.schema.entity.data.table import (
|
||||
Column,
|
||||
ConstraintType,
|
||||
RelationshipType,
|
||||
)
|
||||
from metadata.ingestion.ometa.utils import model_str
|
||||
|
||||
|
||||
def _is_column_unique(column: Dict, columns: List[Column]) -> bool:
|
||||
"""
|
||||
Method to check if the column in unique in the table
|
||||
"""
|
||||
if column and len(column) > 0:
|
||||
constrained_column = column[0]
|
||||
for col in columns or []:
|
||||
if model_str(col.name) == constrained_column:
|
||||
if col.constraint and col.constraint.value in {
|
||||
ConstraintType.UNIQUE.value,
|
||||
ConstraintType.PRIMARY_KEY.value,
|
||||
}:
|
||||
return True
|
||||
break
|
||||
return False
|
||||
|
||||
|
||||
def get_relationship_type(
|
||||
column: Dict, referred_table_columns: List[Column], columns: List[Column]
|
||||
) -> str:
|
||||
"""
|
||||
Determine the type of relationship (one-to-one, one-to-many, etc.)
|
||||
"""
|
||||
# Check if the column is unique in the current table
|
||||
is_unique_in_current_table = _is_column_unique(
|
||||
column.get("constrained_columns"), columns
|
||||
)
|
||||
|
||||
# Check if the referred column is unique in the referred table
|
||||
is_unique_in_referred_table = _is_column_unique(
|
||||
column.get("referred_columns"), referred_table_columns
|
||||
)
|
||||
|
||||
if is_unique_in_current_table and is_unique_in_referred_table:
|
||||
return RelationshipType.ONE_TO_ONE
|
||||
if is_unique_in_current_table:
|
||||
return RelationshipType.ONE_TO_MANY
|
||||
if is_unique_in_referred_table:
|
||||
return RelationshipType.MANY_TO_ONE
|
||||
return RelationshipType.MANY_TO_MANY
|
@ -15,6 +15,7 @@ package org.openmetadata.service.resources.databases;
|
||||
|
||||
import static org.openmetadata.common.utils.CommonUtil.listOf;
|
||||
|
||||
import es.org.elasticsearch.action.search.SearchResponse;
|
||||
import io.swagger.v3.oas.annotations.ExternalDocumentation;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@ -644,6 +645,42 @@ public class DatabaseSchemaResource
|
||||
return addHref(uriInfo, databaseSchema);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/entityRelationship")
|
||||
@Operation(
|
||||
operationId = "searchSchemaEntityRelationship",
|
||||
summary = "Search Schema Entity Relationship",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "search response",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = SearchResponse.class)))
|
||||
})
|
||||
public Response searchSchemaEntityRelationship(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@Parameter(description = "fqn") @QueryParam("fqn") String fqn,
|
||||
@Parameter(description = "upstreamDepth") @QueryParam("upstreamDepth") int upstreamDepth,
|
||||
@Parameter(description = "downstreamDepth") @QueryParam("downstreamDepth")
|
||||
int downstreamDepth,
|
||||
@Parameter(
|
||||
description =
|
||||
"Elasticsearch query that will be combined with the query_string query generator from the `query` argument")
|
||||
@QueryParam("query_filter")
|
||||
String queryFilter,
|
||||
@Parameter(description = "Filter documents by deleted param. By default deleted is false")
|
||||
@QueryParam("includeDeleted")
|
||||
@DefaultValue("false")
|
||||
boolean deleted)
|
||||
throws IOException {
|
||||
|
||||
return Entity.getSearchRepository()
|
||||
.searchSchemaEntityRelationship(fqn, upstreamDepth, downstreamDepth, queryFilter, deleted);
|
||||
}
|
||||
|
||||
private DatabaseSchema getDatabaseSchema(CreateDatabaseSchema create, String user) {
|
||||
return repository
|
||||
.copy(new DatabaseSchema(), create, user)
|
||||
|
@ -15,6 +15,7 @@ package org.openmetadata.service.resources.databases;
|
||||
|
||||
import static org.openmetadata.common.utils.CommonUtil.listOf;
|
||||
|
||||
import es.org.elasticsearch.action.search.SearchResponse;
|
||||
import io.swagger.v3.oas.annotations.ExternalDocumentation;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@ -127,7 +128,8 @@ public class TableResource extends EntityResource<Table, TableRepository> {
|
||||
MetadataOperation.EDIT_QUERIES,
|
||||
MetadataOperation.EDIT_DATA_PROFILE,
|
||||
MetadataOperation.EDIT_SAMPLE_DATA,
|
||||
MetadataOperation.EDIT_LINEAGE);
|
||||
MetadataOperation.EDIT_LINEAGE,
|
||||
MetadataOperation.EDIT_ENTITY_RELATIONSHIP);
|
||||
}
|
||||
|
||||
public static class TableList extends ResultList<Table> {
|
||||
@ -1219,6 +1221,42 @@ public class TableResource extends EntityResource<Table, TableRepository> {
|
||||
.toResponse();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/entityRelationship")
|
||||
@Operation(
|
||||
operationId = "searchEntityRelationship",
|
||||
summary = "Search Entity Relationship",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "search response",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = SearchResponse.class)))
|
||||
})
|
||||
public Response searchEntityRelationship(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@Parameter(description = "fqn") @QueryParam("fqn") String fqn,
|
||||
@Parameter(description = "upstreamDepth") @QueryParam("upstreamDepth") int upstreamDepth,
|
||||
@Parameter(description = "downstreamDepth") @QueryParam("downstreamDepth")
|
||||
int downstreamDepth,
|
||||
@Parameter(
|
||||
description =
|
||||
"Elasticsearch query that will be combined with the query_string query generator from the `query` argument")
|
||||
@QueryParam("query_filter")
|
||||
String queryFilter,
|
||||
@Parameter(description = "Filter documents by deleted param. By default deleted is false")
|
||||
@QueryParam("includeDeleted")
|
||||
@DefaultValue("false")
|
||||
boolean deleted)
|
||||
throws IOException {
|
||||
|
||||
return Entity.getSearchRepository()
|
||||
.searchEntityRelationship(fqn, upstreamDepth, downstreamDepth, queryFilter, deleted);
|
||||
}
|
||||
|
||||
public static Table validateNewTable(Table table) {
|
||||
table.setId(UUID.randomUUID());
|
||||
DatabaseUtil.validateConstraints(table.getColumns(), table.getTableConstraints());
|
||||
|
@ -6,6 +6,7 @@ import java.io.IOException;
|
||||
import java.security.KeyStoreException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import javax.json.JsonArray;
|
||||
@ -77,6 +78,12 @@ public interface SearchClient {
|
||||
|
||||
String ADD_UPDATE_LINEAGE =
|
||||
"boolean docIdExists = false; for (int i = 0; i < ctx._source.lineage.size(); i++) { if (ctx._source.lineage[i].doc_id.equalsIgnoreCase(params.lineageData.doc_id)) { ctx._source.lineage[i] = params.lineageData; docIdExists = true; break;}}if (!docIdExists) {ctx._source.lineage.add(params.lineageData);}";
|
||||
|
||||
// The script is used for updating the entityRelationship attribute of the entity in ES
|
||||
// It checks if any duplicate entry is present based on the doc_id and updates only if it is not
|
||||
// present
|
||||
String ADD_UPDATE_ENTITY_RELATIONSHIP =
|
||||
"boolean docIdExists = false; for (int i = 0; i < ctx._source.entityRelationship.size(); i++) { if (ctx._source.entityRelationship[i].doc_id.equalsIgnoreCase(params.entityRelationshipData.doc_id)) { ctx._source.entityRelationship[i] = params.entityRelationshipData; docIdExists = true; break;}}if (!docIdExists) {ctx._source.entityRelationship.add(params.entityRelationshipData);}";
|
||||
String UPDATE_ADDED_DELETE_GLOSSARY_TAGS =
|
||||
"if (ctx._source.tags != null) { for (int i = ctx._source.tags.size() - 1; i >= 0; i--) { if (params.tagDeleted != null) { for (int j = 0; j < params.tagDeleted.size(); j++) { if (ctx._source.tags[i].tagFQN.equalsIgnoreCase(params.tagDeleted[j].tagFQN)) { ctx._source.tags.remove(i); } } } } } if (ctx._source.tags == null) { ctx._source.tags = []; } if (params.tagAdded != null) { ctx._source.tags.addAll(params.tagAdded); } ctx._source.tags = ctx._source.tags .stream() .distinct() .sorted((o1, o2) -> o1.tagFQN.compareTo(o2.tagFQN)) .collect(Collectors.toList());";
|
||||
String REMOVE_TEST_SUITE_CHILDREN_SCRIPT =
|
||||
@ -98,6 +105,37 @@ public interface SearchClient {
|
||||
|
||||
String NOT_IMPLEMENTED_ERROR_TYPE = "NOT_IMPLEMENTED";
|
||||
|
||||
String ENTITY_RELATIONSHIP_DIRECTION_ENTITY = "entityRelationship.entity.fqnHash.keyword";
|
||||
|
||||
String ENTITY_RELATIONSHIP_DIRECTION_RELATED_ENTITY =
|
||||
"entityRelationship.relatedEntity.fqnHash.keyword";
|
||||
|
||||
Set<String> FIELDS_TO_REMOVE_ENTITY_RELATIONSHIP =
|
||||
Set.of(
|
||||
"suggest",
|
||||
"service_suggest",
|
||||
"column_suggest",
|
||||
"schema_suggest",
|
||||
"database_suggest",
|
||||
"lifeCycle",
|
||||
"fqnParts",
|
||||
"chart_suggest",
|
||||
"field_suggest",
|
||||
"lineage",
|
||||
"entityRelationship",
|
||||
"customMetrics",
|
||||
"descriptionStatus",
|
||||
"columnNames",
|
||||
"totalVotes",
|
||||
"usageSummary",
|
||||
"dataProducts",
|
||||
"tags",
|
||||
"followers",
|
||||
"domain",
|
||||
"votes",
|
||||
"tier",
|
||||
"changeDescription");
|
||||
|
||||
boolean isClientAvailable();
|
||||
|
||||
ElasticSearchConfiguration.SearchType getSearchType();
|
||||
@ -142,9 +180,17 @@ public interface SearchClient {
|
||||
String entityType)
|
||||
throws IOException;
|
||||
|
||||
Response searchEntityRelationship(
|
||||
String fqn, int upstreamDepth, int downstreamDepth, String queryFilter, boolean deleted)
|
||||
throws IOException;
|
||||
|
||||
Response searchDataQualityLineage(
|
||||
String fqn, int upstreamDepth, String queryFilter, boolean deleted) throws IOException;
|
||||
|
||||
Response searchSchemaEntityRelationship(
|
||||
String fqn, int upstreamDepth, int downstreamDepth, String queryFilter, boolean deleted)
|
||||
throws IOException;
|
||||
|
||||
/*
|
||||
Used for listing knowledge page hierarchy for a given parent and page type, used in Elastic/Open SearchClientExtension
|
||||
*/
|
||||
@ -211,6 +257,11 @@ public interface SearchClient {
|
||||
void updateLineage(
|
||||
String indexName, Pair<String, String> fieldAndValue, Map<String, Object> lineagaData);
|
||||
|
||||
void updateEntityRelationship(
|
||||
String indexName,
|
||||
Pair<String, String> fieldAndValue,
|
||||
Map<String, Object> entityRelationshipData);
|
||||
|
||||
Response listDataInsightChartResult(
|
||||
Long startTs,
|
||||
Long endTs,
|
||||
|
@ -837,11 +837,25 @@ public class SearchRepository {
|
||||
fqn, upstreamDepth, downstreamDepth, queryFilter, deleted, entityType);
|
||||
}
|
||||
|
||||
public Response searchEntityRelationship(
|
||||
String fqn, int upstreamDepth, int downstreamDepth, String queryFilter, boolean deleted)
|
||||
throws IOException {
|
||||
return searchClient.searchEntityRelationship(
|
||||
fqn, upstreamDepth, downstreamDepth, queryFilter, deleted);
|
||||
}
|
||||
|
||||
public Response searchDataQualityLineage(
|
||||
String fqn, int upstreamDepth, String queryFilter, boolean deleted) throws IOException {
|
||||
return searchClient.searchDataQualityLineage(fqn, upstreamDepth, queryFilter, deleted);
|
||||
}
|
||||
|
||||
public Response searchSchemaEntityRelationship(
|
||||
String fqn, int upstreamDepth, int downstreamDepth, String queryFilter, boolean deleted)
|
||||
throws IOException {
|
||||
return searchClient.searchSchemaEntityRelationship(
|
||||
fqn, upstreamDepth, downstreamDepth, queryFilter, deleted);
|
||||
}
|
||||
|
||||
public Map<String, Object> searchLineageForExport(
|
||||
String fqn,
|
||||
int upstreamDepth,
|
||||
|
@ -12,6 +12,7 @@ import static org.openmetadata.service.Entity.FIELD_NAME;
|
||||
import static org.openmetadata.service.Entity.GLOSSARY_TERM;
|
||||
import static org.openmetadata.service.Entity.QUERY;
|
||||
import static org.openmetadata.service.Entity.RAW_COST_ANALYSIS_REPORT_DATA;
|
||||
import static org.openmetadata.service.Entity.TABLE;
|
||||
import static org.openmetadata.service.exception.CatalogGenericExceptionMapper.getResponse;
|
||||
import static org.openmetadata.service.search.EntityBuilderConstant.API_RESPONSE_SCHEMA_FIELD;
|
||||
import static org.openmetadata.service.search.EntityBuilderConstant.API_RESPONSE_SCHEMA_FIELD_KEYWORD;
|
||||
@ -146,6 +147,7 @@ import org.openmetadata.schema.dataInsight.custom.DataInsightCustomChart;
|
||||
import org.openmetadata.schema.dataInsight.custom.DataInsightCustomChartResultList;
|
||||
import org.openmetadata.schema.dataInsight.custom.FormulaHolder;
|
||||
import org.openmetadata.schema.entity.data.EntityHierarchy__1;
|
||||
import org.openmetadata.schema.entity.data.Table;
|
||||
import org.openmetadata.schema.service.configuration.elasticsearch.ElasticSearchConfiguration;
|
||||
import org.openmetadata.schema.tests.DataQualityReport;
|
||||
import org.openmetadata.schema.type.EntityReference;
|
||||
@ -156,6 +158,8 @@ import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.dataInsight.DataInsightAggregatorInterface;
|
||||
import org.openmetadata.service.jdbi3.DataInsightChartRepository;
|
||||
import org.openmetadata.service.jdbi3.DataInsightSystemChartRepository;
|
||||
import org.openmetadata.service.jdbi3.ListFilter;
|
||||
import org.openmetadata.service.jdbi3.TableRepository;
|
||||
import org.openmetadata.service.jdbi3.TestCaseResultRepository;
|
||||
import org.openmetadata.service.search.SearchAggregation;
|
||||
import org.openmetadata.service.search.SearchClient;
|
||||
@ -749,6 +753,123 @@ public class ElasticSearchClient implements SearchClient {
|
||||
return Response.status(OK).entity(responseMap).build();
|
||||
}
|
||||
|
||||
private void getEntityRelationship(
|
||||
String fqn,
|
||||
int depth,
|
||||
Set<Map<String, Object>> edges,
|
||||
Set<Map<String, Object>> nodes,
|
||||
String queryFilter,
|
||||
String direction,
|
||||
boolean deleted)
|
||||
throws IOException {
|
||||
if (depth <= 0) {
|
||||
return;
|
||||
}
|
||||
es.org.elasticsearch.action.search.SearchRequest searchRequest =
|
||||
new es.org.elasticsearch.action.search.SearchRequest(
|
||||
Entity.getSearchRepository().getIndexOrAliasName(GLOBAL_SEARCH_ALIAS));
|
||||
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
|
||||
searchSourceBuilder.query(
|
||||
QueryBuilders.boolQuery()
|
||||
.must(QueryBuilders.termQuery(direction, FullyQualifiedName.buildHash(fqn))));
|
||||
if (CommonUtil.nullOrEmpty(deleted)) {
|
||||
searchSourceBuilder.query(
|
||||
QueryBuilders.boolQuery()
|
||||
.must(QueryBuilders.termQuery(direction, FullyQualifiedName.buildHash(fqn)))
|
||||
.must(QueryBuilders.termQuery("deleted", deleted)));
|
||||
}
|
||||
if (!nullOrEmpty(queryFilter) && !queryFilter.equals("{}")) {
|
||||
try {
|
||||
XContentParser filterParser =
|
||||
XContentType.JSON
|
||||
.xContent()
|
||||
.createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, queryFilter);
|
||||
es.org.elasticsearch.index.query.QueryBuilder filter =
|
||||
SearchSourceBuilder.fromXContent(filterParser).query();
|
||||
es.org.elasticsearch.index.query.BoolQueryBuilder newQuery =
|
||||
QueryBuilders.boolQuery().must(searchSourceBuilder.query()).filter(filter);
|
||||
searchSourceBuilder.query(newQuery);
|
||||
} catch (Exception ex) {
|
||||
LOG.warn("Error parsing query_filter from query parameters, ignoring filter", ex);
|
||||
}
|
||||
}
|
||||
searchRequest.source(searchSourceBuilder.size(1000));
|
||||
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
|
||||
for (var hit : searchResponse.getHits().getHits()) {
|
||||
List<Map<String, Object>> entityRelationship =
|
||||
(List<Map<String, Object>>) hit.getSourceAsMap().get("entityRelationship");
|
||||
HashMap<String, Object> tempMap = new HashMap<>(JsonUtils.getMap(hit.getSourceAsMap()));
|
||||
tempMap.keySet().removeAll(FIELDS_TO_REMOVE_ENTITY_RELATIONSHIP);
|
||||
nodes.add(tempMap);
|
||||
for (Map<String, Object> er : entityRelationship) {
|
||||
Map<String, String> entity = (HashMap<String, String>) er.get("entity");
|
||||
Map<String, String> relatedEntity = (HashMap<String, String>) er.get("relatedEntity");
|
||||
if (direction.equalsIgnoreCase(ENTITY_RELATIONSHIP_DIRECTION_ENTITY)) {
|
||||
if (!edges.contains(er) && entity.get("fqn").equals(fqn)) {
|
||||
edges.add(er);
|
||||
getEntityRelationship(
|
||||
relatedEntity.get("fqn"), depth - 1, edges, nodes, queryFilter, direction, deleted);
|
||||
}
|
||||
} else {
|
||||
if (!edges.contains(er) && relatedEntity.get("fqn").equals(fqn)) {
|
||||
edges.add(er);
|
||||
getEntityRelationship(
|
||||
entity.get("fqn"), depth - 1, edges, nodes, queryFilter, direction, deleted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> searchEntityRelationshipInternal(
|
||||
String fqn, int upstreamDepth, int downstreamDepth, String queryFilter, boolean deleted)
|
||||
throws IOException {
|
||||
Map<String, Object> responseMap = new HashMap<>();
|
||||
Set<Map<String, Object>> edges = new HashSet<>();
|
||||
Set<Map<String, Object>> nodes = new HashSet<>();
|
||||
es.org.elasticsearch.action.search.SearchRequest searchRequest =
|
||||
new es.org.elasticsearch.action.search.SearchRequest(
|
||||
Entity.getSearchRepository().getIndexOrAliasName(GLOBAL_SEARCH_ALIAS));
|
||||
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
|
||||
searchSourceBuilder.query(
|
||||
QueryBuilders.boolQuery().must(QueryBuilders.termQuery("fullyQualifiedName", fqn)));
|
||||
searchRequest.source(searchSourceBuilder.size(1000));
|
||||
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
|
||||
for (var hit : searchResponse.getHits().getHits()) {
|
||||
Map<String, Object> tempMap = new HashMap<>(JsonUtils.getMap(hit.getSourceAsMap()));
|
||||
tempMap.keySet().removeAll(FIELDS_TO_REMOVE);
|
||||
responseMap.put("entity", tempMap);
|
||||
}
|
||||
getEntityRelationship(
|
||||
fqn,
|
||||
downstreamDepth,
|
||||
edges,
|
||||
nodes,
|
||||
queryFilter,
|
||||
ENTITY_RELATIONSHIP_DIRECTION_ENTITY,
|
||||
deleted);
|
||||
getEntityRelationship(
|
||||
fqn,
|
||||
upstreamDepth,
|
||||
edges,
|
||||
nodes,
|
||||
queryFilter,
|
||||
ENTITY_RELATIONSHIP_DIRECTION_RELATED_ENTITY,
|
||||
deleted);
|
||||
responseMap.put("edges", edges);
|
||||
responseMap.put("nodes", nodes);
|
||||
return responseMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response searchEntityRelationship(
|
||||
String fqn, int upstreamDepth, int downstreamDepth, String queryFilter, boolean deleted)
|
||||
throws IOException {
|
||||
Map<String, Object> responseMap =
|
||||
searchEntityRelationshipInternal(fqn, upstreamDepth, downstreamDepth, queryFilter, deleted);
|
||||
return Response.status(OK).entity(responseMap).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response searchDataQualityLineage(
|
||||
String fqn, int upstreamDepth, String queryFilter, boolean deleted) throws IOException {
|
||||
@ -761,6 +882,80 @@ public class ElasticSearchClient implements SearchClient {
|
||||
return Response.status(OK).entity(responseMap).build();
|
||||
}
|
||||
|
||||
public Map<String, Object> searchSchemaEntityRelationshipInternal(
|
||||
String fqn, int upstreamDepth, int downstreamDepth, String queryFilter, boolean deleted)
|
||||
throws IOException {
|
||||
Map<String, Object> responseMap = new HashMap<>();
|
||||
Set<Map<String, Object>> edges = new HashSet<>();
|
||||
Set<Map<String, Object>> nodes = new HashSet<>();
|
||||
es.org.elasticsearch.action.search.SearchRequest searchRequest =
|
||||
new es.org.elasticsearch.action.search.SearchRequest(
|
||||
Entity.getSearchRepository().getIndexOrAliasName(GLOBAL_SEARCH_ALIAS));
|
||||
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
|
||||
searchSourceBuilder.query(
|
||||
QueryBuilders.boolQuery().must(QueryBuilders.termQuery("fullyQualifiedName", fqn)));
|
||||
searchRequest.source(searchSourceBuilder.size(1000));
|
||||
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
|
||||
for (var hit : searchResponse.getHits().getHits()) {
|
||||
Map<String, Object> tempMap = new HashMap<>(JsonUtils.getMap(hit.getSourceAsMap()));
|
||||
tempMap.keySet().removeAll(FIELDS_TO_REMOVE);
|
||||
responseMap.put("entity", tempMap);
|
||||
}
|
||||
TableRepository repository = (TableRepository) Entity.getEntityRepository(TABLE);
|
||||
ListFilter filter = new ListFilter(Include.NON_DELETED).addQueryParam("databaseSchema", fqn);
|
||||
List<Table> tables =
|
||||
repository.listAll(repository.getFields("tableConstraints, displayName, owners"), filter);
|
||||
for (Table table : tables) {
|
||||
getEntityRelationship(
|
||||
table.getFullyQualifiedName(),
|
||||
downstreamDepth,
|
||||
edges,
|
||||
nodes,
|
||||
queryFilter,
|
||||
ENTITY_RELATIONSHIP_DIRECTION_ENTITY,
|
||||
deleted);
|
||||
getEntityRelationship(
|
||||
table.getFullyQualifiedName(),
|
||||
upstreamDepth,
|
||||
edges,
|
||||
nodes,
|
||||
queryFilter,
|
||||
ENTITY_RELATIONSHIP_DIRECTION_RELATED_ENTITY,
|
||||
deleted);
|
||||
}
|
||||
// Add the remaining tables from the list into the nodes
|
||||
// These will the one's that do not have any entity relationship
|
||||
for (Table table : tables) {
|
||||
boolean tablePresent = false;
|
||||
for (Map<String, Object> node : nodes) {
|
||||
if (table.getId().toString().equals(node.get("id"))) {
|
||||
tablePresent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!tablePresent) {
|
||||
HashMap<String, Object> tableMap = new HashMap<>(JsonUtils.getMap(table));
|
||||
tableMap.keySet().removeAll(FIELDS_TO_REMOVE_ENTITY_RELATIONSHIP);
|
||||
tableMap.put("entityType", "table");
|
||||
nodes.add(tableMap);
|
||||
}
|
||||
}
|
||||
|
||||
responseMap.put("edges", edges);
|
||||
responseMap.put("nodes", nodes);
|
||||
return responseMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response searchSchemaEntityRelationship(
|
||||
String fqn, int upstreamDepth, int downstreamDepth, String queryFilter, boolean deleted)
|
||||
throws IOException {
|
||||
Map<String, Object> responseMap =
|
||||
searchSchemaEntityRelationshipInternal(
|
||||
fqn, upstreamDepth, downstreamDepth, queryFilter, deleted);
|
||||
return Response.status(OK).entity(responseMap).build();
|
||||
}
|
||||
|
||||
private void getLineage(
|
||||
String fqn,
|
||||
int depth,
|
||||
@ -1850,6 +2045,28 @@ public class ElasticSearchClient implements SearchClient {
|
||||
}
|
||||
}
|
||||
|
||||
public void updateEntityRelationship(
|
||||
String indexName,
|
||||
Pair<String, String> fieldAndValue,
|
||||
Map<String, Object> entityRelationshipData) {
|
||||
if (isClientAvailable) {
|
||||
UpdateByQueryRequest updateByQueryRequest = new UpdateByQueryRequest(indexName);
|
||||
updateByQueryRequest.setQuery(
|
||||
new MatchQueryBuilder(fieldAndValue.getKey(), fieldAndValue.getValue())
|
||||
.operator(Operator.AND));
|
||||
Map<String, Object> params =
|
||||
Collections.singletonMap("entityRelationshipData", entityRelationshipData);
|
||||
Script script =
|
||||
new Script(
|
||||
ScriptType.INLINE,
|
||||
Script.DEFAULT_SCRIPT_LANG,
|
||||
ADD_UPDATE_ENTITY_RELATIONSHIP,
|
||||
params);
|
||||
updateByQueryRequest.setScript(script);
|
||||
updateElasticSearchByQuery(updateByQueryRequest);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateLineage(
|
||||
String indexName, Pair<String, String> fieldAndValue, Map<String, Object> lineageData) {
|
||||
|
@ -1,13 +1,16 @@
|
||||
package org.openmetadata.service.search.indexes;
|
||||
|
||||
import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
|
||||
import static org.openmetadata.schema.type.Include.NON_DELETED;
|
||||
import static org.openmetadata.service.Entity.FIELD_DESCRIPTION;
|
||||
import static org.openmetadata.service.Entity.FIELD_DISPLAY_NAME;
|
||||
import static org.openmetadata.service.Entity.getEntityByName;
|
||||
import static org.openmetadata.service.jdbi3.LineageRepository.buildRelationshipDetailsMap;
|
||||
import static org.openmetadata.service.search.EntityBuilderConstant.DISPLAY_NAME_KEYWORD;
|
||||
import static org.openmetadata.service.search.EntityBuilderConstant.FIELD_DISPLAY_NAME_NGRAM;
|
||||
import static org.openmetadata.service.search.EntityBuilderConstant.FULLY_QUALIFIED_NAME;
|
||||
import static org.openmetadata.service.search.EntityBuilderConstant.FULLY_QUALIFIED_NAME_PARTS;
|
||||
import static org.openmetadata.service.util.FullyQualifiedName.getParentFQN;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@ -16,14 +19,22 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.openmetadata.common.utils.CommonUtil;
|
||||
import org.openmetadata.schema.EntityInterface;
|
||||
import org.openmetadata.schema.entity.data.Table;
|
||||
import org.openmetadata.schema.type.EntityReference;
|
||||
import org.openmetadata.schema.type.Include;
|
||||
import org.openmetadata.schema.type.LineageDetails;
|
||||
import org.openmetadata.schema.type.Relationship;
|
||||
import org.openmetadata.schema.type.TableConstraint;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.exception.EntityNotFoundException;
|
||||
import org.openmetadata.service.jdbi3.CollectionDAO;
|
||||
import org.openmetadata.service.search.SearchClient;
|
||||
import org.openmetadata.service.search.SearchIndexUtils;
|
||||
import org.openmetadata.service.search.models.IndexMapping;
|
||||
import org.openmetadata.service.search.models.SearchSuggest;
|
||||
import org.openmetadata.service.util.FullyQualifiedName;
|
||||
import org.openmetadata.service.util.JsonUtils;
|
||||
@ -31,6 +42,7 @@ import org.openmetadata.service.util.JsonUtils;
|
||||
public interface SearchIndex {
|
||||
Set<String> DEFAULT_EXCLUDED_FIELDS =
|
||||
Set.of("changeDescription", "lineage.pipeline.changeDescription", "connection");
|
||||
public static final SearchClient searchClient = Entity.getSearchRepository().getSearchClient();
|
||||
|
||||
default Map<String, Object> buildSearchIndexDoc() {
|
||||
// Build Index Doc
|
||||
@ -162,6 +174,146 @@ public interface SearchIndex {
|
||||
return data;
|
||||
}
|
||||
|
||||
static List<Map<String, Object>> populateEntityRelationshipData(Table entity) {
|
||||
List<Map<String, Object>> constraints = new ArrayList<>();
|
||||
if (CommonUtil.nullOrEmpty(entity.getTableConstraints())) {
|
||||
return constraints;
|
||||
}
|
||||
for (TableConstraint tableConstraint : entity.getTableConstraints()) {
|
||||
if (!tableConstraint
|
||||
.getConstraintType()
|
||||
.value()
|
||||
.equalsIgnoreCase(TableConstraint.ConstraintType.FOREIGN_KEY.value())) {
|
||||
continue;
|
||||
}
|
||||
for (String referredColumn : tableConstraint.getReferredColumns()) {
|
||||
String relatedEntityFQN = getParentFQN(referredColumn);
|
||||
Table relatedEntity;
|
||||
try {
|
||||
relatedEntity = getEntityByName(Entity.TABLE, relatedEntityFQN, "*", NON_DELETED);
|
||||
IndexMapping destinationIndexMapping =
|
||||
Entity.getSearchRepository()
|
||||
.getIndexMapping(relatedEntity.getEntityReference().getType());
|
||||
String destinationIndexName =
|
||||
destinationIndexMapping.getIndexName(Entity.getSearchRepository().getClusterAlias());
|
||||
Map<String, Object> relationshipsMap = buildRelationshipsMap(entity, relatedEntity);
|
||||
int relatedEntityIndex =
|
||||
checkRelatedEntity(relatedEntity.getFullyQualifiedName(), constraints);
|
||||
if (relatedEntityIndex >= 0) {
|
||||
updateExistingConstraint(
|
||||
entity,
|
||||
tableConstraint,
|
||||
constraints.get(relatedEntityIndex),
|
||||
destinationIndexName,
|
||||
relatedEntity,
|
||||
referredColumn);
|
||||
} else {
|
||||
addNewConstraint(
|
||||
entity,
|
||||
tableConstraint,
|
||||
constraints,
|
||||
relationshipsMap,
|
||||
destinationIndexName,
|
||||
relatedEntity,
|
||||
referredColumn);
|
||||
}
|
||||
} catch (EntityNotFoundException ex) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return constraints;
|
||||
}
|
||||
|
||||
static int checkRelatedEntity(String relatedEntityFQN, List<Map<String, Object>> constraints) {
|
||||
int index = 0;
|
||||
for (Map<String, Object> constraint : constraints) {
|
||||
Map<String, Object> relatedConstraintEntity =
|
||||
(Map<String, Object>) constraint.get("relatedEntity");
|
||||
if (relatedConstraintEntity.get("fqn").equals(relatedEntityFQN)) {
|
||||
return index;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static Map<String, Object> buildRelationshipsMap(
|
||||
EntityInterface entity, Table relatedEntity) {
|
||||
Map<String, Object> relationshipsMap = new HashMap<>();
|
||||
relationshipsMap.put("entity", buildEntityRefMap(entity.getEntityReference()));
|
||||
relationshipsMap.put("relatedEntity", buildEntityRefMap(relatedEntity.getEntityReference()));
|
||||
relationshipsMap.put(
|
||||
"doc_id", entity.getId().toString() + "-" + relatedEntity.getId().toString());
|
||||
return relationshipsMap;
|
||||
}
|
||||
|
||||
private static void updateRelatedEntityIndex(
|
||||
String destinationIndexName, Table relatedEntity, Map<String, Object> constraint) {
|
||||
Pair<String, String> to = new ImmutablePair<>("_id", relatedEntity.getId().toString());
|
||||
searchClient.updateEntityRelationship(destinationIndexName, to, constraint);
|
||||
}
|
||||
|
||||
private static void updateExistingConstraint(
|
||||
EntityInterface entity,
|
||||
TableConstraint tableConstraint,
|
||||
Map<String, Object> presentConstraint,
|
||||
String destinationIndexName,
|
||||
Table relatedEntity,
|
||||
String referredColumn) {
|
||||
for (String currentColumn : tableConstraint.getColumns()) {
|
||||
if (currentColumn.equals(FullyQualifiedName.getColumnName(referredColumn))) {
|
||||
String columnFQN = FullyQualifiedName.add(entity.getFullyQualifiedName(), currentColumn);
|
||||
|
||||
Map<String, Object> columnMap = new HashMap<>();
|
||||
columnMap.put("columnFQN", columnFQN);
|
||||
columnMap.put("relatedColumnFQN", referredColumn);
|
||||
columnMap.put("relationshipType", tableConstraint.getRelationshipType());
|
||||
|
||||
List<Map<String, Object>> presentColumns =
|
||||
(List<Map<String, Object>>) presentConstraint.get("columns");
|
||||
presentColumns.add(columnMap);
|
||||
|
||||
updateRelatedEntityIndex(destinationIndexName, relatedEntity, presentConstraint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addNewConstraint(
|
||||
EntityInterface entity,
|
||||
TableConstraint tableConstraint,
|
||||
List<Map<String, Object>> constraints,
|
||||
Map<String, Object> relationshipsMap,
|
||||
String destinationIndexName,
|
||||
Table relatedEntity,
|
||||
String referredColumn) {
|
||||
for (String currentColumn : tableConstraint.getColumns()) {
|
||||
if (currentColumn.equals(FullyQualifiedName.getColumnName(referredColumn))) {
|
||||
List<Map<String, Object>> columns = new ArrayList<>();
|
||||
String columnFQN = FullyQualifiedName.add(entity.getFullyQualifiedName(), currentColumn);
|
||||
|
||||
Map<String, Object> columnMap = new HashMap<>();
|
||||
columnMap.put("columnFQN", columnFQN);
|
||||
columnMap.put("relatedColumnFQN", referredColumn);
|
||||
columnMap.put("relationshipType", tableConstraint.getRelationshipType());
|
||||
columns.add(columnMap);
|
||||
relationshipsMap.put("columns", columns);
|
||||
constraints.add(JsonUtils.getMap(relationshipsMap));
|
||||
|
||||
updateRelatedEntityIndex(destinationIndexName, relatedEntity, relationshipsMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, Object> buildEntityRefMap(EntityReference entityRef) {
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("id", entityRef.getId().toString());
|
||||
details.put("type", entityRef.getType());
|
||||
details.put("fqn", entityRef.getFullyQualifiedName());
|
||||
details.put("fqnHash", FullyQualifiedName.buildHash(entityRef.getFullyQualifiedName()));
|
||||
return details;
|
||||
}
|
||||
|
||||
static Map<String, Float> getDefaultFields() {
|
||||
Map<String, Float> fields = new HashMap<>();
|
||||
fields.put(DISPLAY_NAME_KEYWORD, 10.0f);
|
||||
|
@ -103,6 +103,7 @@ public record TableIndex(Table table) implements ColumnIndex {
|
||||
doc.put("service", getEntityWithDisplayName(table.getService()));
|
||||
doc.put("database", getEntityWithDisplayName(table.getDatabase()));
|
||||
doc.put("lineage", SearchIndex.getLineageData(table.getEntityReference()));
|
||||
doc.put("entityRelationship", SearchIndex.populateEntityRelationshipData(table));
|
||||
doc.put("databaseSchema", getEntityWithDisplayName(table.getDatabaseSchema()));
|
||||
return doc;
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import static org.openmetadata.service.Entity.FIELD_DISPLAY_NAME;
|
||||
import static org.openmetadata.service.Entity.GLOSSARY_TERM;
|
||||
import static org.openmetadata.service.Entity.QUERY;
|
||||
import static org.openmetadata.service.Entity.RAW_COST_ANALYSIS_REPORT_DATA;
|
||||
import static org.openmetadata.service.Entity.TABLE;
|
||||
import static org.openmetadata.service.exception.CatalogGenericExceptionMapper.getResponse;
|
||||
import static org.openmetadata.service.search.EntityBuilderConstant.API_RESPONSE_SCHEMA_FIELD;
|
||||
import static org.openmetadata.service.search.EntityBuilderConstant.API_RESPONSE_SCHEMA_FIELD_KEYWORD;
|
||||
@ -66,6 +67,7 @@ import org.openmetadata.schema.dataInsight.custom.DataInsightCustomChart;
|
||||
import org.openmetadata.schema.dataInsight.custom.DataInsightCustomChartResultList;
|
||||
import org.openmetadata.schema.dataInsight.custom.FormulaHolder;
|
||||
import org.openmetadata.schema.entity.data.EntityHierarchy__1;
|
||||
import org.openmetadata.schema.entity.data.Table;
|
||||
import org.openmetadata.schema.service.configuration.elasticsearch.ElasticSearchConfiguration;
|
||||
import org.openmetadata.schema.tests.DataQualityReport;
|
||||
import org.openmetadata.schema.type.EntityReference;
|
||||
@ -76,6 +78,8 @@ import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.dataInsight.DataInsightAggregatorInterface;
|
||||
import org.openmetadata.service.jdbi3.DataInsightChartRepository;
|
||||
import org.openmetadata.service.jdbi3.DataInsightSystemChartRepository;
|
||||
import org.openmetadata.service.jdbi3.ListFilter;
|
||||
import org.openmetadata.service.jdbi3.TableRepository;
|
||||
import org.openmetadata.service.jdbi3.TestCaseResultRepository;
|
||||
import org.openmetadata.service.search.SearchAggregation;
|
||||
import org.openmetadata.service.search.SearchClient;
|
||||
@ -227,6 +231,7 @@ public class OpenSearchClient implements SearchClient {
|
||||
"fqnParts",
|
||||
"chart_suggest",
|
||||
"field_suggest");
|
||||
|
||||
private static final List<String> SOURCE_FIELDS_TO_EXCLUDE =
|
||||
Stream.concat(FIELDS_TO_REMOVE.stream(), Stream.of("schemaDefinition", "customMetrics"))
|
||||
.toList();
|
||||
@ -752,6 +757,123 @@ public class OpenSearchClient implements SearchClient {
|
||||
return Response.status(OK).entity(responseMap).build();
|
||||
}
|
||||
|
||||
private void getEntityRelationship(
|
||||
String fqn,
|
||||
int depth,
|
||||
Set<Map<String, Object>> edges,
|
||||
Set<Map<String, Object>> nodes,
|
||||
String queryFilter,
|
||||
String direction,
|
||||
boolean deleted)
|
||||
throws IOException {
|
||||
if (depth <= 0) {
|
||||
return;
|
||||
}
|
||||
os.org.opensearch.action.search.SearchRequest searchRequest =
|
||||
new os.org.opensearch.action.search.SearchRequest(
|
||||
Entity.getSearchRepository().getIndexOrAliasName(GLOBAL_SEARCH_ALIAS));
|
||||
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
|
||||
searchSourceBuilder.query(
|
||||
QueryBuilders.boolQuery()
|
||||
.must(QueryBuilders.termQuery(direction, FullyQualifiedName.buildHash(fqn))));
|
||||
if (CommonUtil.nullOrEmpty(deleted)) {
|
||||
searchSourceBuilder.query(
|
||||
QueryBuilders.boolQuery()
|
||||
.must(QueryBuilders.termQuery(direction, FullyQualifiedName.buildHash(fqn)))
|
||||
.must(QueryBuilders.termQuery("deleted", deleted)));
|
||||
}
|
||||
if (!nullOrEmpty(queryFilter) && !queryFilter.equals("{}")) {
|
||||
try {
|
||||
XContentParser filterParser =
|
||||
XContentType.JSON
|
||||
.xContent()
|
||||
.createParser(X_CONTENT_REGISTRY, LoggingDeprecationHandler.INSTANCE, queryFilter);
|
||||
QueryBuilder filter = SearchSourceBuilder.fromXContent(filterParser).query();
|
||||
BoolQueryBuilder newQuery =
|
||||
QueryBuilders.boolQuery().must(searchSourceBuilder.query()).filter(filter);
|
||||
searchSourceBuilder.query(newQuery);
|
||||
} catch (Exception ex) {
|
||||
LOG.warn("Error parsing query_filter from query parameters, ignoring filter", ex);
|
||||
}
|
||||
}
|
||||
searchRequest.source(searchSourceBuilder.size(1000));
|
||||
os.org.opensearch.action.search.SearchResponse searchResponse =
|
||||
client.search(searchRequest, RequestOptions.DEFAULT);
|
||||
for (var hit : searchResponse.getHits().getHits()) {
|
||||
List<Map<String, Object>> entityRelationship =
|
||||
(List<Map<String, Object>>) hit.getSourceAsMap().get("entityRelationship");
|
||||
HashMap<String, Object> tempMap = new HashMap<>(JsonUtils.getMap(hit.getSourceAsMap()));
|
||||
tempMap.keySet().removeAll(FIELDS_TO_REMOVE_ENTITY_RELATIONSHIP);
|
||||
nodes.add(tempMap);
|
||||
for (Map<String, Object> er : entityRelationship) {
|
||||
Map<String, String> entity = (HashMap<String, String>) er.get("entity");
|
||||
Map<String, String> relatedEntity = (HashMap<String, String>) er.get("relatedEntity");
|
||||
if (direction.equalsIgnoreCase(ENTITY_RELATIONSHIP_DIRECTION_ENTITY)) {
|
||||
if (!edges.contains(er) && entity.get("fqn").equals(fqn)) {
|
||||
edges.add(er);
|
||||
getEntityRelationship(
|
||||
relatedEntity.get("fqn"), depth - 1, edges, nodes, queryFilter, direction, deleted);
|
||||
}
|
||||
} else {
|
||||
if (!edges.contains(er) && relatedEntity.get("fqn").equals(fqn)) {
|
||||
edges.add(er);
|
||||
getEntityRelationship(
|
||||
entity.get("fqn"), depth - 1, edges, nodes, queryFilter, direction, deleted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> searchEntityRelationshipInternal(
|
||||
String fqn, int upstreamDepth, int downstreamDepth, String queryFilter, boolean deleted)
|
||||
throws IOException {
|
||||
Map<String, Object> responseMap = new HashMap<>();
|
||||
Set<Map<String, Object>> edges = new HashSet<>();
|
||||
Set<Map<String, Object>> nodes = new HashSet<>();
|
||||
os.org.opensearch.action.search.SearchRequest searchRequest =
|
||||
new os.org.opensearch.action.search.SearchRequest(
|
||||
Entity.getSearchRepository().getIndexOrAliasName(GLOBAL_SEARCH_ALIAS));
|
||||
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
|
||||
searchSourceBuilder.query(
|
||||
QueryBuilders.boolQuery().must(QueryBuilders.termQuery("fullyQualifiedName", fqn)));
|
||||
searchRequest.source(searchSourceBuilder.size(1000));
|
||||
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
|
||||
for (var hit : searchResponse.getHits().getHits()) {
|
||||
HashMap<String, Object> tempMap = new HashMap<>(JsonUtils.getMap(hit.getSourceAsMap()));
|
||||
tempMap.keySet().removeAll(FIELDS_TO_REMOVE);
|
||||
responseMap.put("entity", tempMap);
|
||||
}
|
||||
getEntityRelationship(
|
||||
fqn,
|
||||
downstreamDepth,
|
||||
edges,
|
||||
nodes,
|
||||
queryFilter,
|
||||
ENTITY_RELATIONSHIP_DIRECTION_ENTITY,
|
||||
deleted);
|
||||
getEntityRelationship(
|
||||
fqn,
|
||||
upstreamDepth,
|
||||
edges,
|
||||
nodes,
|
||||
queryFilter,
|
||||
ENTITY_RELATIONSHIP_DIRECTION_RELATED_ENTITY,
|
||||
deleted);
|
||||
responseMap.put("edges", edges);
|
||||
responseMap.put("nodes", nodes);
|
||||
return responseMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response searchEntityRelationship(
|
||||
String fqn, int upstreamDepth, int downstreamDepth, String queryFilter, boolean deleted)
|
||||
throws IOException {
|
||||
Map<String, Object> responseMap =
|
||||
searchEntityRelationshipInternal(fqn, upstreamDepth, downstreamDepth, queryFilter, deleted);
|
||||
return Response.status(OK).entity(responseMap).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response searchDataQualityLineage(
|
||||
String fqn, int upstreamDepth, String queryFilter, boolean deleted) throws IOException {
|
||||
@ -764,6 +886,78 @@ public class OpenSearchClient implements SearchClient {
|
||||
return Response.status(OK).entity(responseMap).build();
|
||||
}
|
||||
|
||||
public Map<String, Object> searchSchemaEntityRelationshipInternal(
|
||||
String fqn, int upstreamDepth, int downstreamDepth, String queryFilter, boolean deleted)
|
||||
throws IOException {
|
||||
Map<String, Object> responseMap = new HashMap<>();
|
||||
Set<Map<String, Object>> edges = new HashSet<>();
|
||||
Set<Map<String, Object>> nodes = new HashSet<>();
|
||||
os.org.opensearch.action.search.SearchRequest searchRequest =
|
||||
new os.org.opensearch.action.search.SearchRequest(
|
||||
Entity.getSearchRepository().getIndexOrAliasName(GLOBAL_SEARCH_ALIAS));
|
||||
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
|
||||
searchSourceBuilder.query(
|
||||
QueryBuilders.boolQuery().must(QueryBuilders.termQuery("fullyQualifiedName", fqn)));
|
||||
searchRequest.source(searchSourceBuilder.size(1000));
|
||||
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
|
||||
for (var hit : searchResponse.getHits().getHits()) {
|
||||
HashMap<String, Object> tempMap = new HashMap<>(JsonUtils.getMap(hit.getSourceAsMap()));
|
||||
tempMap.keySet().removeAll(FIELDS_TO_REMOVE);
|
||||
responseMap.put("entity", tempMap);
|
||||
}
|
||||
TableRepository repository = (TableRepository) Entity.getEntityRepository(TABLE);
|
||||
ListFilter filter = new ListFilter(Include.NON_DELETED).addQueryParam("databaseSchema", fqn);
|
||||
List<Table> tables =
|
||||
repository.listAll(repository.getFields("tableConstraints, displayName, owners"), filter);
|
||||
for (Table table : tables) {
|
||||
getEntityRelationship(
|
||||
table.getFullyQualifiedName(),
|
||||
downstreamDepth,
|
||||
edges,
|
||||
nodes,
|
||||
queryFilter,
|
||||
ENTITY_RELATIONSHIP_DIRECTION_ENTITY,
|
||||
deleted);
|
||||
getEntityRelationship(
|
||||
table.getFullyQualifiedName(),
|
||||
upstreamDepth,
|
||||
edges,
|
||||
nodes,
|
||||
queryFilter,
|
||||
ENTITY_RELATIONSHIP_DIRECTION_RELATED_ENTITY,
|
||||
deleted);
|
||||
}
|
||||
// Add the remaining tables from the list into the nodes
|
||||
// These will the one's that do not have any entity relationship
|
||||
for (Table table : tables) {
|
||||
boolean tablePresent = false;
|
||||
for (Map<String, Object> node : nodes) {
|
||||
if (table.getId().toString().equals(node.get("id"))) {
|
||||
tablePresent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!tablePresent) {
|
||||
HashMap<String, Object> tableMap = new HashMap<>(JsonUtils.getMap(table));
|
||||
tableMap.keySet().removeAll(FIELDS_TO_REMOVE_ENTITY_RELATIONSHIP);
|
||||
tableMap.put("entityType", "table");
|
||||
nodes.add(tableMap);
|
||||
}
|
||||
}
|
||||
responseMap.put("edges", edges);
|
||||
responseMap.put("nodes", nodes);
|
||||
return responseMap;
|
||||
}
|
||||
|
||||
public Response searchSchemaEntityRelationship(
|
||||
String fqn, int upstreamDepth, int downstreamDepth, String queryFilter, boolean deleted)
|
||||
throws IOException {
|
||||
Map<String, Object> responseMap =
|
||||
searchSchemaEntityRelationshipInternal(
|
||||
fqn, upstreamDepth, downstreamDepth, queryFilter, deleted);
|
||||
return Response.status(OK).entity(responseMap).build();
|
||||
}
|
||||
|
||||
private void getLineage(
|
||||
String fqn,
|
||||
int depth,
|
||||
@ -1839,6 +2033,29 @@ public class OpenSearchClient implements SearchClient {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateEntityRelationship(
|
||||
String indexName,
|
||||
Pair<String, String> fieldAndValue,
|
||||
Map<String, Object> entityRelationshipData) {
|
||||
if (isClientAvailable) {
|
||||
UpdateByQueryRequest updateByQueryRequest = new UpdateByQueryRequest(indexName);
|
||||
updateByQueryRequest.setQuery(
|
||||
new MatchQueryBuilder(fieldAndValue.getKey(), fieldAndValue.getValue())
|
||||
.operator(Operator.AND));
|
||||
Map<String, Object> params =
|
||||
Collections.singletonMap("entityRelationshipData", entityRelationshipData);
|
||||
Script script =
|
||||
new Script(
|
||||
ScriptType.INLINE,
|
||||
Script.DEFAULT_SCRIPT_LANG,
|
||||
ADD_UPDATE_ENTITY_RELATIONSHIP,
|
||||
params);
|
||||
updateByQueryRequest.setScript(script);
|
||||
updateOpenSearchByQuery(updateByQueryRequest);
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private void updateOpenSearchByQuery(UpdateByQueryRequest updateByQueryRequest) {
|
||||
if (updateByQueryRequest != null && isClientAvailable) {
|
||||
|
@ -587,6 +587,9 @@
|
||||
"lineage": {
|
||||
"type" : "object"
|
||||
},
|
||||
"entityRelationship": {
|
||||
"type" : "object"
|
||||
},
|
||||
"serviceType": {
|
||||
"type": "keyword",
|
||||
"normalizer": "lowercase_normalizer"
|
||||
|
@ -210,6 +210,15 @@
|
||||
"$ref": "../../type/basic.json#/definitions/fullyQualifiedEntityName"
|
||||
},
|
||||
"default": null
|
||||
},
|
||||
"relationshipType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ONE_TO_ONE",
|
||||
"ONE_TO_MANY",
|
||||
"MANY_TO_ONE",
|
||||
"MANY_TO_MANY"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
@ -30,6 +30,7 @@
|
||||
"EditDescription",
|
||||
"EditDisplayName",
|
||||
"EditLineage",
|
||||
"EditEntityRelationship",
|
||||
"EditPolicy",
|
||||
"EditOwners",
|
||||
"EditQueries",
|
||||
|
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"><path stroke="currentColor" stroke-width=".3" d="M7.571 14.85H5.943v-2.825h4.114v2.825H7.572Zm7.34-13.266v2.391H1.089V1.584c0-.258.18-.434.363-.434h13.096c.182 0 .363.176.363.434Zm.739 0c0-.662-.477-1.234-1.102-1.234H1.452C.827.35.35.922.35 1.584v12.452c0 .871.626 1.614 1.436 1.614h12.428c.81 0 1.436-.743 1.436-1.614V1.584ZM1.09 8.4h4.113v2.825H1.09V8.4Zm0-.8V4.775h4.113V7.6H1.09Zm4.853-2.825h4.114V7.6H5.943V4.775Zm4.853 0h4.115V7.6h-4.115V4.775ZM14.911 8.4v2.825h-4.115V8.4h4.115Zm-9.708 6.45H1.786c-.366 0-.697-.346-.697-.814v-2.01h4.114v2.824Zm.74-3.625V8.4h4.114v2.825H5.943Zm8.27 3.625h-3.417v-2.825h4.115v2.011c0 .468-.33.814-.697.814Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor"><path stroke="currentColor" stroke-width=".3" d="M7.571 14.85H5.943v-2.825h4.114v2.825H7.572Zm7.34-13.266v2.391H1.089V1.584c0-.258.18-.434.363-.434h13.096c.182 0 .363.176.363.434Zm.739 0c0-.662-.477-1.234-1.102-1.234H1.452C.827.35.35.922.35 1.584v12.452c0 .871.626 1.614 1.436 1.614h12.428c.81 0 1.436-.743 1.436-1.614V1.584ZM1.09 8.4h4.113v2.825H1.09V8.4Zm0-.8V4.775h4.113V7.6H1.09Zm4.853-2.825h4.114V7.6H5.943V4.775Zm4.853 0h4.115V7.6h-4.115V4.775ZM14.911 8.4v2.825h-4.115V8.4h4.115Zm-9.708 6.45H1.786c-.366 0-.697-.346-.697-.814v-2.01h4.114v2.824Zm.74-3.625V8.4h4.114v2.825H5.943Zm8.27 3.625h-3.417v-2.825h4.115v2.011c0 .468-.33.814-.697.814Z"/></svg>
|
Before Width: | Height: | Size: 737 B After Width: | Height: | Size: 734 B |
@ -20,7 +20,7 @@ export type Props = {
|
||||
hasDescriptionEditAccess: boolean;
|
||||
hasTagEditAccess: boolean;
|
||||
isReadOnly?: boolean;
|
||||
testCaseSummary?: TestSummary;
|
||||
onThreadLinkSelect: (value: string, threadType?: ThreadType) => void;
|
||||
onUpdate: (columns: Table['columns']) => Promise<void>;
|
||||
testCaseSummary?: TestSummary;
|
||||
};
|
||||
|
@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import { Button, Form, Select, Tooltip, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { ExpandableConfig } from 'antd/lib/table/interface';
|
||||
import {
|
||||
@ -36,7 +36,10 @@ import {
|
||||
ICON_DIMENSION,
|
||||
NO_DATA_PLACEHOLDER,
|
||||
} from '../../../constants/constants';
|
||||
import { TABLE_SCROLL_VALUE } from '../../../constants/Table.constants';
|
||||
import {
|
||||
COLUMN_CONSTRAINT_TYPE_OPTIONS,
|
||||
TABLE_SCROLL_VALUE,
|
||||
} from '../../../constants/Table.constants';
|
||||
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
|
||||
import {
|
||||
OperationPermission,
|
||||
@ -71,22 +74,29 @@ import FilterTablePlaceHolder from '../../common/ErrorWithPlaceholder/FilterTabl
|
||||
import Table from '../../common/Table/Table';
|
||||
import TestCaseStatusSummaryIndicator from '../../common/TestCaseStatusSummaryIndicator/TestCaseStatusSummaryIndicator.component';
|
||||
import EntityNameModal from '../../Modals/EntityNameModal/EntityNameModal.component';
|
||||
import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface';
|
||||
import {
|
||||
EntityName,
|
||||
EntityNameWithAdditionFields,
|
||||
} from '../../Modals/EntityNameModal/EntityNameModal.interface';
|
||||
import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
|
||||
import { ColumnFilter } from '../ColumnFilter/ColumnFilter.component';
|
||||
import TableDescription from '../TableDescription/TableDescription.component';
|
||||
import TableTags from '../TableTags/TableTags.component';
|
||||
import { SchemaTableProps, TableCellRendered } from './SchemaTable.interface';
|
||||
import {
|
||||
SchemaTableProps,
|
||||
TableCellRendered,
|
||||
UpdatedColumnFieldData,
|
||||
} from './SchemaTable.interface';
|
||||
|
||||
const SchemaTable = ({
|
||||
searchText,
|
||||
onUpdate,
|
||||
hasDescriptionEditAccess,
|
||||
hasTagEditAccess,
|
||||
isReadOnly = false,
|
||||
onThreadLinkSelect,
|
||||
table,
|
||||
testCaseSummary,
|
||||
onUpdate,
|
||||
onThreadLinkSelect,
|
||||
}: SchemaTableProps) => {
|
||||
const { theme } = useApplicationStore();
|
||||
const { t } = useTranslation();
|
||||
@ -171,12 +181,7 @@ const SchemaTable = ({
|
||||
field,
|
||||
value,
|
||||
columns,
|
||||
}: {
|
||||
fqn: string;
|
||||
field: keyof Column;
|
||||
value?: string;
|
||||
columns: Column[];
|
||||
}) => {
|
||||
}: UpdatedColumnFieldData) => {
|
||||
columns?.forEach((col) => {
|
||||
if (col.fullyQualifiedName === fqn) {
|
||||
set(col, field, value);
|
||||
@ -310,18 +315,27 @@ const SchemaTable = ({
|
||||
setEditColumnDisplayName(record);
|
||||
};
|
||||
|
||||
const handleEditDisplayName = async ({ displayName }: EntityName) => {
|
||||
const handleEditColumnData = async (data: EntityName) => {
|
||||
const { displayName, constraint } = data as EntityNameWithAdditionFields;
|
||||
if (
|
||||
!isUndefined(editColumnDisplayName) &&
|
||||
editColumnDisplayName.fullyQualifiedName
|
||||
) {
|
||||
const tableCols = cloneDeep(tableColumns);
|
||||
|
||||
updateColumnFields({
|
||||
fqn: editColumnDisplayName.fullyQualifiedName,
|
||||
value: isEmpty(displayName) ? undefined : displayName,
|
||||
field: 'displayName',
|
||||
columns: tableCols,
|
||||
});
|
||||
|
||||
updateColumnFields({
|
||||
fqn: editColumnDisplayName.fullyQualifiedName,
|
||||
value: isEmpty(constraint) ? undefined : constraint,
|
||||
field: 'constraint',
|
||||
columns: tableCols,
|
||||
});
|
||||
await onUpdate(tableCols);
|
||||
setEditColumnDisplayName(undefined);
|
||||
} else {
|
||||
@ -372,13 +386,14 @@ const SchemaTable = ({
|
||||
{getEntityName(record)}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
|
||||
{(tablePermissions?.EditAll ||
|
||||
tablePermissions?.EditDisplayName) &&
|
||||
!isReadOnly && (
|
||||
<Tooltip
|
||||
placement="right"
|
||||
title={t('label.edit-entity', {
|
||||
entity: t('label.display-name'),
|
||||
entity: t('label.column'),
|
||||
})}>
|
||||
<Button
|
||||
className="cursor-pointer hover-cell-icon w-fit-content"
|
||||
@ -516,6 +531,19 @@ const SchemaTable = ({
|
||||
]
|
||||
);
|
||||
|
||||
const additionalFieldsInEntityNameModal = (
|
||||
<Form.Item
|
||||
label={t('label.entity-type-plural', {
|
||||
entity: t('label.constraint'),
|
||||
})}
|
||||
name="constraint">
|
||||
<Select
|
||||
data-testid="constraint-type-select"
|
||||
options={COLUMN_CONSTRAINT_TYPE_OPTIONS}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setExpandedRowKeys(nestedTableFqnKeys);
|
||||
}, [searchText]);
|
||||
@ -568,13 +596,14 @@ const SchemaTable = ({
|
||||
)}
|
||||
{editColumnDisplayName && (
|
||||
<EntityNameModal
|
||||
additionalFields={additionalFieldsInEntityNameModal}
|
||||
entity={editColumnDisplayName}
|
||||
title={`${t('label.edit-entity', {
|
||||
entity: t('label.column'),
|
||||
})}: "${editColumnDisplayName?.name}"`}
|
||||
visible={Boolean(editColumnDisplayName)}
|
||||
onCancel={() => setEditColumnDisplayName(undefined)}
|
||||
onSave={handleEditDisplayName}
|
||||
onSave={handleEditColumnData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -41,3 +41,10 @@ export interface EditColumnTag {
|
||||
column: Column;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export interface UpdatedColumnFieldData {
|
||||
fqn: string;
|
||||
field: keyof Column;
|
||||
value?: string;
|
||||
columns: Column[];
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ import { PAGE_SIZE } from '../../../constants/constants';
|
||||
import { EntityType, FqnPart } from '../../../enums/entity.enum';
|
||||
import { SearchIndex } from '../../../enums/search.enum';
|
||||
import { EntityReference } from '../../../generated/entity/type';
|
||||
import { searchData } from '../../../rest/miscAPI';
|
||||
import { searchQuery } from '../../../rest/searchAPI';
|
||||
import { getPartialNameFromTableFQN } from '../../../utils/CommonUtils';
|
||||
import { getEntityNodeIcon } from '../../../utils/EntityLineageUtils';
|
||||
import { getEntityName } from '../../../utils/EntityUtils';
|
||||
@ -40,10 +40,12 @@ import './node-suggestion.less';
|
||||
interface EntitySuggestionProps extends HTMLAttributes<HTMLDivElement> {
|
||||
onSelectHandler: (value: EntityReference) => void;
|
||||
entityType: string;
|
||||
queryFilter?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const NodeSuggestions: FC<EntitySuggestionProps> = ({
|
||||
entityType,
|
||||
queryFilter,
|
||||
onSelectHandler,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
@ -67,16 +69,15 @@ const NodeSuggestions: FC<EntitySuggestionProps> = ({
|
||||
|
||||
const getSearchResults = async (value: string) => {
|
||||
try {
|
||||
const data = await searchData<ExploreSearchIndex>(
|
||||
value,
|
||||
1,
|
||||
PAGE_SIZE,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
(entityType as ExploreSearchIndex) ?? SearchIndex.TABLE
|
||||
);
|
||||
const sources = data.data.hits.hits.map((hit) => hit._source);
|
||||
const data = await searchQuery({
|
||||
query: value,
|
||||
searchIndex: (entityType as ExploreSearchIndex) ?? SearchIndex.TABLE,
|
||||
queryFilter: queryFilter,
|
||||
pageNumber: 1,
|
||||
pageSize: PAGE_SIZE,
|
||||
includeDeleted: false,
|
||||
});
|
||||
const sources = data.hits.hits.map((hit) => hit._source);
|
||||
setData(sources);
|
||||
} catch (error) {
|
||||
showErrorToast(
|
||||
|
@ -14,7 +14,7 @@
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { SearchIndex } from '../../../enums/search.enum';
|
||||
import { searchData } from '../../../rest/miscAPI';
|
||||
import { searchQuery } from '../../../rest/searchAPI';
|
||||
import NodeSuggestions from './NodeSuggestions.component';
|
||||
|
||||
const mockProps = {
|
||||
@ -34,8 +34,8 @@ const entityType = [
|
||||
SearchIndex.DASHBOARD_DATA_MODEL,
|
||||
];
|
||||
|
||||
jest.mock('../../../rest/miscAPI', () => ({
|
||||
searchData: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
jest.mock('../../../rest/searchAPI', () => ({
|
||||
searchQuery: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
}));
|
||||
|
||||
describe('Test NodeSuggestions Component', () => {
|
||||
@ -52,15 +52,21 @@ describe('Test NodeSuggestions Component', () => {
|
||||
entityType.forEach((value) => {
|
||||
it(`Suggest & Suggest API for ${value} should work properly`, async () => {
|
||||
jest.useFakeTimers('modern');
|
||||
const mockSearchData = searchData as jest.Mock;
|
||||
const mockSearchQueryData = searchQuery as jest.Mock;
|
||||
const searchValue = 'sale';
|
||||
await act(async () => {
|
||||
render(<NodeSuggestions {...mockProps} entityType={value} />);
|
||||
});
|
||||
|
||||
// 1st call on page load with empty search string and respective searchIndex
|
||||
expect(mockSearchData.mock.calls[0][0]).toBe('');
|
||||
expect(mockSearchData.mock.calls[0][6]).toEqual(value);
|
||||
expect(mockSearchQueryData.mock.calls[0][0]).toStrictEqual({
|
||||
includeDeleted: false,
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
query: '',
|
||||
queryFilter: undefined,
|
||||
searchIndex: value,
|
||||
});
|
||||
|
||||
const suggestionNode = await screen.findByTestId('suggestion-node');
|
||||
const searchInput = await screen.findByRole('combobox');
|
||||
@ -77,7 +83,7 @@ describe('Test NodeSuggestions Component', () => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(mockSearchData.mock.instances).toHaveLength(2);
|
||||
expect(mockSearchQueryData.mock.instances).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -10,7 +10,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Button, Form, Input, Modal, Typography } from 'antd';
|
||||
import { Button, Form, FormProps, Input, Modal, Typography } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ENTITY_NAME_REGEX } from '../../../constants/regex.constants';
|
||||
@ -26,12 +26,13 @@ const EntityNameModal: React.FC<EntityNameModalProps> = ({
|
||||
// By default its disabled, send allowRename true to get the functionality
|
||||
allowRename = false,
|
||||
nameValidationRules = [],
|
||||
additionalFields,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm<{ name: string; displayName: string }>();
|
||||
const [form] = Form.useForm();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSave = async (obj: { name: string; displayName: string }) => {
|
||||
const handleSave: FormProps['onFinish'] = async (obj) => {
|
||||
setIsLoading(true);
|
||||
await form.validateFields();
|
||||
// Error must be handled by the parent component
|
||||
@ -40,7 +41,7 @@ const EntityNameModal: React.FC<EntityNameModalProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({ name: entity.name, displayName: entity.displayName });
|
||||
form.setFieldsValue(entity);
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
@ -96,6 +97,8 @@ const EntityNameModal: React.FC<EntityNameModalProps> = ({
|
||||
<Form.Item label={t('label.display-name')} name="displayName">
|
||||
<Input placeholder={t('message.enter-display-name')} />
|
||||
</Form.Item>
|
||||
|
||||
{additionalFields}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -11,9 +11,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Rule } from 'antd/lib/form';
|
||||
import { Constraint } from '../../../generated/entity/data/table';
|
||||
|
||||
export type EntityName = { name: string; displayName: string };
|
||||
|
||||
export type EntityNameWithAdditionFields = EntityName & {
|
||||
constraint: Constraint;
|
||||
};
|
||||
|
||||
export interface EntityNameModalProps {
|
||||
visible: boolean;
|
||||
allowRename?: boolean;
|
||||
@ -22,4 +27,5 @@ export interface EntityNameModalProps {
|
||||
entity: Partial<EntityName>;
|
||||
title: string;
|
||||
nameValidationRules?: Rule[];
|
||||
additionalFields?: React.ReactNode;
|
||||
}
|
||||
|
@ -11,7 +11,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConstraintType } from '../generated/entity/data/table';
|
||||
import {
|
||||
Constraint,
|
||||
ConstraintType,
|
||||
RelationshipType,
|
||||
} from '../generated/entity/data/table';
|
||||
import i18n from '../utils/i18next/LocalUtil';
|
||||
|
||||
export const TABLE_SCROLL_VALUE = { x: 1200 };
|
||||
|
||||
@ -19,3 +24,41 @@ export const SUPPORTED_TABLE_CONSTRAINTS = [
|
||||
ConstraintType.ForeignKey,
|
||||
ConstraintType.PrimaryKey,
|
||||
];
|
||||
|
||||
export const COLUMN_CONSTRAINT_TYPE_OPTIONS = [
|
||||
{
|
||||
label: i18n.t('label.primary-key'),
|
||||
value: Constraint.PrimaryKey,
|
||||
},
|
||||
{
|
||||
label: i18n.t('label.not-null'),
|
||||
value: Constraint.NotNull,
|
||||
},
|
||||
{
|
||||
label: i18n.t('label.null'),
|
||||
value: Constraint.Null,
|
||||
},
|
||||
{
|
||||
label: i18n.t('label.unique'),
|
||||
value: Constraint.Unique,
|
||||
},
|
||||
];
|
||||
|
||||
export const RELATIONSHIP_TYPE_OPTION = [
|
||||
{
|
||||
label: i18n.t('label.one-to-one'),
|
||||
value: RelationshipType.OneToOne,
|
||||
},
|
||||
{
|
||||
label: i18n.t('label.one-to-many'),
|
||||
value: RelationshipType.OneToMany,
|
||||
},
|
||||
{
|
||||
label: i18n.t('label.many-to-one'),
|
||||
value: RelationshipType.ManyToOne,
|
||||
},
|
||||
{
|
||||
label: i18n.t('label.many-to-many'),
|
||||
value: RelationshipType.ManyToMany,
|
||||
},
|
||||
];
|
||||
|
@ -683,6 +683,8 @@
|
||||
"manage-entity": "{{entity}} verwalten",
|
||||
"manage-rule": "Regeln verwalten",
|
||||
"mandatory": "Obligatorisch",
|
||||
"many-to-many": "Many to Many",
|
||||
"many-to-one": "Many to One",
|
||||
"march": "März",
|
||||
"mark-all-deleted-table-plural": "Alle gelöschten Tabellen markieren",
|
||||
"mark-deleted-entity": "{{entity}} als gelöscht markieren",
|
||||
@ -805,6 +807,8 @@
|
||||
"on-demand": "Auf Abruf",
|
||||
"on-lowercase": "auf",
|
||||
"one-reply": "1 Reply",
|
||||
"one-to-many": "One to Many",
|
||||
"one-to-one": "One to One",
|
||||
"open": "Öffnen",
|
||||
"open-lowercase": "öffnen",
|
||||
"open-metadata": "OpenMetadata",
|
||||
@ -940,8 +944,10 @@
|
||||
"reject": "Ablehnen",
|
||||
"reject-all": "Reject All",
|
||||
"rejected": "Rejected",
|
||||
"related-column": "Related Column",
|
||||
"related-metric-plural": "Related Metrics",
|
||||
"related-term-plural": "Verwandte Begriffe",
|
||||
"relationship": "Relationship",
|
||||
"relevance": "Relevanz",
|
||||
"remove": "Entfernen",
|
||||
"remove-entity": "{{entity}} entfernen",
|
||||
|
@ -683,6 +683,8 @@
|
||||
"manage-entity": "Manage {{entity}}",
|
||||
"manage-rule": "Manage Rule",
|
||||
"mandatory": "Mandatory",
|
||||
"many-to-many": "Many to Many",
|
||||
"many-to-one": "Many to One",
|
||||
"march": "March",
|
||||
"mark-all-deleted-table-plural": "Mark All Deleted Tables",
|
||||
"mark-deleted-entity": "Mark Deleted {{entity}}",
|
||||
@ -805,6 +807,8 @@
|
||||
"on-demand": "On Demand",
|
||||
"on-lowercase": "on",
|
||||
"one-reply": "1 Reply",
|
||||
"one-to-many": "One to Many",
|
||||
"one-to-one": "One to One",
|
||||
"open": "Open",
|
||||
"open-lowercase": "open",
|
||||
"open-metadata": "OpenMetadata",
|
||||
@ -940,8 +944,10 @@
|
||||
"reject": "Reject",
|
||||
"reject-all": "Reject All",
|
||||
"rejected": "Rejected",
|
||||
"related-column": "Related Column",
|
||||
"related-metric-plural": "Related Metrics",
|
||||
"related-term-plural": "Related Terms",
|
||||
"relationship": "Relationship",
|
||||
"relevance": "Relevance",
|
||||
"remove": "Remove",
|
||||
"remove-entity": "Remove {{entity}}",
|
||||
|
@ -683,6 +683,8 @@
|
||||
"manage-entity": "Administrar {{entity}}",
|
||||
"manage-rule": "Administrar regla",
|
||||
"mandatory": "Obligatorio",
|
||||
"many-to-many": "Many to Many",
|
||||
"many-to-one": "Many to One",
|
||||
"march": "Marzo",
|
||||
"mark-all-deleted-table-plural": "Marcar Todas las Tablas como Eliminadas",
|
||||
"mark-deleted-entity": "Marcar {{entity}} como Eliminado",
|
||||
@ -805,6 +807,8 @@
|
||||
"on-demand": "Bajo Demanda",
|
||||
"on-lowercase": "en",
|
||||
"one-reply": "1 Reply",
|
||||
"one-to-many": "One to Many",
|
||||
"one-to-one": "One to One",
|
||||
"open": "Abrir",
|
||||
"open-lowercase": "abrir",
|
||||
"open-metadata": "OpenMetadata",
|
||||
@ -940,8 +944,10 @@
|
||||
"reject": "Rechazar",
|
||||
"reject-all": "Reject All",
|
||||
"rejected": "Rechazado",
|
||||
"related-column": "Related Column",
|
||||
"related-metric-plural": "Related Metrics",
|
||||
"related-term-plural": "Términos relacionados",
|
||||
"relationship": "Relationship",
|
||||
"relevance": "Relevancia",
|
||||
"remove": "Eliminar",
|
||||
"remove-entity": "Eliminar {{entity}}",
|
||||
|
@ -683,6 +683,8 @@
|
||||
"manage-entity": "Gérer {{entity}}",
|
||||
"manage-rule": "Gérer les Règles",
|
||||
"mandatory": "Obligatoire",
|
||||
"many-to-many": "Many to Many",
|
||||
"many-to-one": "Many to One",
|
||||
"march": "Mars",
|
||||
"mark-all-deleted-table-plural": "Marquer Toutes les Tables Supprimées",
|
||||
"mark-deleted-entity": "Marquer {{entity}} Supprimé·e",
|
||||
@ -805,6 +807,8 @@
|
||||
"on-demand": "Sur Demande",
|
||||
"on-lowercase": "sur",
|
||||
"one-reply": "1 Reply",
|
||||
"one-to-many": "One to Many",
|
||||
"one-to-one": "One to One",
|
||||
"open": "Ouvrir",
|
||||
"open-lowercase": "ouvrir",
|
||||
"open-metadata": "OpenMetadata",
|
||||
@ -940,8 +944,10 @@
|
||||
"reject": "Rejeter",
|
||||
"reject-all": "Rejeter Tout",
|
||||
"rejected": "Rejeté",
|
||||
"related-column": "Related Column",
|
||||
"related-metric-plural": "Related Metrics",
|
||||
"related-term-plural": "Termes Liés",
|
||||
"relationship": "Relationship",
|
||||
"relevance": "Pertinence",
|
||||
"remove": "Retirer",
|
||||
"remove-entity": "Retirer un·e {{entity}}",
|
||||
|
@ -683,6 +683,8 @@
|
||||
"manage-entity": "נהל {{entity}}",
|
||||
"manage-rule": "נהל כלל",
|
||||
"mandatory": "חובה",
|
||||
"many-to-many": "Many to Many",
|
||||
"many-to-one": "Many to One",
|
||||
"march": "מרץ",
|
||||
"mark-all-deleted-table-plural": "סמן את כל הטבלאות שנמחקו",
|
||||
"mark-deleted-entity": "סמן {{entity}} שנמחק",
|
||||
@ -805,6 +807,8 @@
|
||||
"on-demand": "על פי דרישה",
|
||||
"on-lowercase": "על",
|
||||
"one-reply": "1 Reply",
|
||||
"one-to-many": "One to Many",
|
||||
"one-to-one": "One to One",
|
||||
"open": "פתוח",
|
||||
"open-lowercase": "פתוח",
|
||||
"open-metadata": "OpenMetadata",
|
||||
@ -940,8 +944,10 @@
|
||||
"reject": "דחה",
|
||||
"reject-all": "Reject All",
|
||||
"rejected": "נדחה",
|
||||
"related-column": "Related Column",
|
||||
"related-metric-plural": "Related Metrics",
|
||||
"related-term-plural": "מונחים קשורים",
|
||||
"relationship": "Relationship",
|
||||
"relevance": "רלוונטיות",
|
||||
"remove": "הסר",
|
||||
"remove-entity": "הסר {{entity}}",
|
||||
|
@ -683,6 +683,8 @@
|
||||
"manage-entity": "{{entity}}の管理",
|
||||
"manage-rule": "ルールの管理",
|
||||
"mandatory": "Mandatory",
|
||||
"many-to-many": "Many to Many",
|
||||
"many-to-one": "Many to One",
|
||||
"march": "3月",
|
||||
"mark-all-deleted-table-plural": "削除済みの全テーブルをマーク",
|
||||
"mark-deleted-entity": "Mark Deleted {{entity}}",
|
||||
@ -805,6 +807,8 @@
|
||||
"on-demand": "On Demand",
|
||||
"on-lowercase": "の上の",
|
||||
"one-reply": "1 Reply",
|
||||
"one-to-many": "One to Many",
|
||||
"one-to-one": "One to One",
|
||||
"open": "開く",
|
||||
"open-lowercase": "開く",
|
||||
"open-metadata": "OpenMetadata",
|
||||
@ -940,8 +944,10 @@
|
||||
"reject": "Reject",
|
||||
"reject-all": "Reject All",
|
||||
"rejected": "Rejected",
|
||||
"related-column": "Related Column",
|
||||
"related-metric-plural": "Related Metrics",
|
||||
"related-term-plural": "関連する用語",
|
||||
"relationship": "Relationship",
|
||||
"relevance": "Relevance",
|
||||
"remove": "除外",
|
||||
"remove-entity": "{{entity}}を除外",
|
||||
|
@ -683,6 +683,8 @@
|
||||
"manage-entity": "Beheer {{entity}}",
|
||||
"manage-rule": "Beheer regel",
|
||||
"mandatory": "Verplicht",
|
||||
"many-to-many": "Many to Many",
|
||||
"many-to-one": "Many to One",
|
||||
"march": "maart",
|
||||
"mark-all-deleted-table-plural": "Markeer alle verwijderde tabellen",
|
||||
"mark-deleted-entity": "Markeer verwijderde {{entity}}",
|
||||
@ -805,6 +807,8 @@
|
||||
"on-demand": "Op verzoek",
|
||||
"on-lowercase": "op",
|
||||
"one-reply": "1 Reply",
|
||||
"one-to-many": "One to Many",
|
||||
"one-to-one": "One to One",
|
||||
"open": "Open",
|
||||
"open-lowercase": "open",
|
||||
"open-metadata": "OpenMetadata",
|
||||
@ -940,8 +944,10 @@
|
||||
"reject": "Weigeren",
|
||||
"reject-all": "Reject All",
|
||||
"rejected": "Geweigerd",
|
||||
"related-column": "Related Column",
|
||||
"related-metric-plural": "Related Metrics",
|
||||
"related-term-plural": "Gerelateerde termen",
|
||||
"relationship": "Relationship",
|
||||
"relevance": "Relevantie",
|
||||
"remove": "Verwijderen",
|
||||
"remove-entity": "Verwijder {{entity}}",
|
||||
|
@ -683,6 +683,8 @@
|
||||
"manage-entity": "مدیریت {{entity}}",
|
||||
"manage-rule": "مدیریت قانون",
|
||||
"mandatory": "اجباری",
|
||||
"many-to-many": "Many to Many",
|
||||
"many-to-one": "Many to One",
|
||||
"march": "مارس",
|
||||
"mark-all-deleted-table-plural": "علامتگذاری همه جداول حذف شده",
|
||||
"mark-deleted-entity": "علامتگذاری حذف {{entity}}",
|
||||
@ -805,6 +807,8 @@
|
||||
"on-demand": "درخواستشده",
|
||||
"on-lowercase": "روی",
|
||||
"one-reply": "1 پاسخ",
|
||||
"one-to-many": "One to Many",
|
||||
"one-to-one": "One to One",
|
||||
"open": "باز",
|
||||
"open-lowercase": "باز",
|
||||
"open-metadata": "متادیتای باز",
|
||||
@ -940,8 +944,10 @@
|
||||
"reject": "رد کردن",
|
||||
"reject-all": "رد کردن همه",
|
||||
"rejected": "رد شد",
|
||||
"related-column": "Related Column",
|
||||
"related-metric-plural": "شاخصهای مرتبط",
|
||||
"related-term-plural": "اصطلاحات مرتبط",
|
||||
"relationship": "Relationship",
|
||||
"relevance": "مربوط بودن",
|
||||
"remove": "حذف",
|
||||
"remove-entity": "حذف {{entity}}",
|
||||
|
@ -683,6 +683,8 @@
|
||||
"manage-entity": "Gerenciar {{entity}}",
|
||||
"manage-rule": "Gerenciar Regra",
|
||||
"mandatory": "Obrigatório",
|
||||
"many-to-many": "Many to Many",
|
||||
"many-to-one": "Many to One",
|
||||
"march": "Março",
|
||||
"mark-all-deleted-table-plural": "Marcar Todas as Tabelas Excluídas",
|
||||
"mark-deleted-entity": "Marcar {{entity}} Como Excluída",
|
||||
@ -805,6 +807,8 @@
|
||||
"on-demand": "Sob Demanda",
|
||||
"on-lowercase": "em",
|
||||
"one-reply": "1 Reply",
|
||||
"one-to-many": "One to Many",
|
||||
"one-to-one": "One to One",
|
||||
"open": "Abrir",
|
||||
"open-lowercase": "abrir",
|
||||
"open-metadata": "OpenMetadata",
|
||||
@ -940,8 +944,10 @@
|
||||
"reject": "Rejeitar",
|
||||
"reject-all": "Reject All",
|
||||
"rejected": "Rejeitado",
|
||||
"related-column": "Related Column",
|
||||
"related-metric-plural": "Related Metrics",
|
||||
"related-term-plural": "Termos Relacionados",
|
||||
"relationship": "Relationship",
|
||||
"relevance": "Relevância",
|
||||
"remove": "Remover",
|
||||
"remove-entity": "Remover {{entity}}",
|
||||
|
@ -683,6 +683,8 @@
|
||||
"manage-entity": "Управление {{entity}}",
|
||||
"manage-rule": "Правила управления",
|
||||
"mandatory": "Обязательный",
|
||||
"many-to-many": "Many to Many",
|
||||
"many-to-one": "Many to One",
|
||||
"march": "Март",
|
||||
"mark-all-deleted-table-plural": "Отметить все удаленные таблицы",
|
||||
"mark-deleted-entity": "Отметить как удаленное {{entity}}",
|
||||
@ -805,6 +807,8 @@
|
||||
"on-demand": "По запросу",
|
||||
"on-lowercase": "на",
|
||||
"one-reply": "1 Reply",
|
||||
"one-to-many": "One to Many",
|
||||
"one-to-one": "One to One",
|
||||
"open": "Открыто",
|
||||
"open-lowercase": "открыть",
|
||||
"open-metadata": "OpenMetadata",
|
||||
@ -940,8 +944,10 @@
|
||||
"reject": "Reject",
|
||||
"reject-all": "Reject All",
|
||||
"rejected": "Rejected",
|
||||
"related-column": "Related Column",
|
||||
"related-metric-plural": "Related Metrics",
|
||||
"related-term-plural": "Связанные термины",
|
||||
"relationship": "Relationship",
|
||||
"relevance": "Актуальность",
|
||||
"remove": "Удалить",
|
||||
"remove-entity": "Удалить {{entity}}",
|
||||
|
@ -683,6 +683,8 @@
|
||||
"manage-entity": "管理{{entity}}",
|
||||
"manage-rule": "管理规则",
|
||||
"mandatory": "必填",
|
||||
"many-to-many": "Many to Many",
|
||||
"many-to-one": "Many to One",
|
||||
"march": "三月",
|
||||
"mark-all-deleted-table-plural": "标记所有已删除的表",
|
||||
"mark-deleted-entity": "标记已删除的{{entity}}",
|
||||
@ -805,6 +807,8 @@
|
||||
"on-demand": "即时",
|
||||
"on-lowercase": "on",
|
||||
"one-reply": "1 Reply",
|
||||
"one-to-many": "One to Many",
|
||||
"one-to-one": "One to One",
|
||||
"open": "打开",
|
||||
"open-lowercase": "打开",
|
||||
"open-metadata": "OpenMetadata",
|
||||
@ -940,8 +944,10 @@
|
||||
"reject": "拒绝",
|
||||
"reject-all": "拒绝全部",
|
||||
"rejected": "已拒绝",
|
||||
"related-column": "Related Column",
|
||||
"related-metric-plural": "Related Metrics",
|
||||
"related-term-plural": "关联术语",
|
||||
"relationship": "Relationship",
|
||||
"relevance": "相关性",
|
||||
"remove": "删除",
|
||||
"remove-entity": "删除{{entity}}",
|
||||
|
@ -25,22 +25,15 @@ import React, {
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import ActivityFeedProvider, {
|
||||
useActivityFeedProvider,
|
||||
} from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider';
|
||||
import { ActivityFeedTab } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component';
|
||||
import { useActivityFeedProvider } from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider';
|
||||
import ActivityThreadPanel from '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel';
|
||||
import { withActivityFeed } from '../../components/AppRouter/withActivityFeed';
|
||||
import { CustomPropertyTable } from '../../components/common/CustomPropertyTable/CustomPropertyTable';
|
||||
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
import Loader from '../../components/common/Loader/Loader';
|
||||
import { PagingHandlerParams } from '../../components/common/NextPrevious/NextPrevious.interface';
|
||||
import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels';
|
||||
import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component';
|
||||
import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component';
|
||||
import ProfilerSettings from '../../components/Database/Profiler/ProfilerSettings/ProfilerSettings';
|
||||
import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface';
|
||||
import EntityRightPanel from '../../components/Entity/EntityRightPanel/EntityRightPanel';
|
||||
import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface';
|
||||
import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1';
|
||||
import {
|
||||
@ -50,7 +43,6 @@ import {
|
||||
ROUTES,
|
||||
} from '../../constants/constants';
|
||||
import { FEED_COUNT_INITIAL_DATA } from '../../constants/entity.constants';
|
||||
import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../constants/ResizablePanel.constants';
|
||||
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
|
||||
import {
|
||||
OperationPermission,
|
||||
@ -72,7 +64,6 @@ import { Include } from '../../generated/type/include';
|
||||
import { TagLabel } from '../../generated/type/tagLabel';
|
||||
import { useFqn } from '../../hooks/useFqn';
|
||||
import { FeedCounts } from '../../interface/feed.interface';
|
||||
import StoredProcedureTab from '../../pages/StoredProcedure/StoredProcedureTab';
|
||||
import {
|
||||
getDatabaseSchemaDetailsByFQN,
|
||||
patchDatabaseSchemaDetails,
|
||||
@ -87,13 +78,13 @@ import {
|
||||
getFeedCounts,
|
||||
sortTagsCaseInsensitive,
|
||||
} from '../../utils/CommonUtils';
|
||||
import databaseSchemaClassBase from '../../utils/DatabaseSchemaClassBase';
|
||||
import entityUtilClassBase from '../../utils/EntityUtilClassBase';
|
||||
import { getEntityName } from '../../utils/EntityUtils';
|
||||
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
|
||||
import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils';
|
||||
import { createTagObject, updateTierTag } from '../../utils/TagsUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
||||
import SchemaTablesTab from './SchemaTablesTab';
|
||||
|
||||
const DatabaseSchemaPage: FunctionComponent = () => {
|
||||
const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider();
|
||||
@ -572,135 +563,67 @@ const DatabaseSchemaPage: FunctionComponent = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const tabs: TabsProps['items'] = [
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
count={tableData.paging.total}
|
||||
id={EntityTabs.TABLE}
|
||||
isActive={activeTab === EntityTabs.TABLE}
|
||||
name={t('label.table-plural')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.TABLE,
|
||||
children: (
|
||||
<Row gutter={[0, 16]} wrap={false}>
|
||||
<Col className="tab-content-height-with-resizable-panel" span={24}>
|
||||
<ResizablePanels
|
||||
firstPanel={{
|
||||
className: 'entity-resizable-panel-container',
|
||||
children: (
|
||||
<div className="p-t-sm m-x-lg">
|
||||
<SchemaTablesTab
|
||||
currentTablesPage={currentTablesPage}
|
||||
databaseSchemaDetails={databaseSchema}
|
||||
description={description}
|
||||
editDescriptionPermission={editDescriptionPermission}
|
||||
isEdit={isEdit}
|
||||
showDeletedTables={showDeletedTables}
|
||||
tableData={tableData}
|
||||
tableDataLoading={tableDataLoading}
|
||||
tablePaginationHandler={tablePaginationHandler}
|
||||
onCancel={onEditCancel}
|
||||
onDescriptionEdit={onDescriptionEdit}
|
||||
onDescriptionUpdate={onDescriptionUpdate}
|
||||
onShowDeletedTablesChange={handleShowDeletedTables}
|
||||
onThreadLinkSelect={onThreadLinkSelect}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL,
|
||||
}}
|
||||
secondPanel={{
|
||||
children: (
|
||||
<div data-testid="entity-right-panel">
|
||||
<EntityRightPanel<EntityType.DATABASE_SCHEMA>
|
||||
customProperties={databaseSchema}
|
||||
dataProducts={databaseSchema?.dataProducts ?? []}
|
||||
domain={databaseSchema?.domain}
|
||||
editCustomAttributePermission={
|
||||
editCustomAttributePermission
|
||||
}
|
||||
editTagPermission={editTagsPermission}
|
||||
entityFQN={decodedDatabaseSchemaFQN}
|
||||
entityId={databaseSchema?.id ?? ''}
|
||||
entityType={EntityType.DATABASE_SCHEMA}
|
||||
selectedTags={tags}
|
||||
viewAllPermission={viewAllPermission}
|
||||
onExtensionUpdate={handleExtensionUpdate}
|
||||
onTagSelectionChange={handleTagSelection}
|
||||
onThreadLinkSelect={onThreadLinkSelect}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL,
|
||||
className:
|
||||
'entity-resizable-right-panel-container entity-resizable-panel-container',
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
count={storedProcedureCount}
|
||||
id={EntityTabs.STORED_PROCEDURE}
|
||||
isActive={activeTab === EntityTabs.STORED_PROCEDURE}
|
||||
name={t('label.stored-procedure-plural')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.STORED_PROCEDURE,
|
||||
children: <StoredProcedureTab />,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
count={feedCount.totalCount}
|
||||
id={EntityTabs.ACTIVITY_FEED}
|
||||
isActive={activeTab === EntityTabs.ACTIVITY_FEED}
|
||||
name={t('label.activity-feed-plural')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.ACTIVITY_FEED,
|
||||
children: (
|
||||
<ActivityFeedProvider>
|
||||
<ActivityFeedTab
|
||||
refetchFeed
|
||||
entityFeedTotalCount={feedCount.totalCount}
|
||||
entityType={EntityType.DATABASE_SCHEMA}
|
||||
fqn={databaseSchema.fullyQualifiedName ?? ''}
|
||||
onFeedUpdate={getEntityFeedCount}
|
||||
onUpdateEntityDetails={fetchDatabaseSchemaDetails}
|
||||
onUpdateFeedCount={handleFeedCount}
|
||||
/>
|
||||
</ActivityFeedProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
id={EntityTabs.CUSTOM_PROPERTIES}
|
||||
name={t('label.custom-property-plural')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.CUSTOM_PROPERTIES,
|
||||
children: databaseSchema && (
|
||||
<div className="m-sm">
|
||||
<CustomPropertyTable<EntityType.DATABASE_SCHEMA>
|
||||
className=""
|
||||
entityDetails={databaseSchema}
|
||||
entityType={EntityType.DATABASE_SCHEMA}
|
||||
handleExtensionUpdate={handleExtensionUpdate}
|
||||
hasEditAccess={editCustomAttributePermission}
|
||||
hasPermission={viewAllPermission}
|
||||
isVersionView={false}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
const tabs: TabsProps['items'] = useMemo(
|
||||
() =>
|
||||
databaseSchemaClassBase.getDatabaseSchemaPageTabs({
|
||||
feedCount,
|
||||
tableData,
|
||||
activeTab,
|
||||
currentTablesPage,
|
||||
databaseSchema,
|
||||
description,
|
||||
editDescriptionPermission,
|
||||
isEdit,
|
||||
showDeletedTables,
|
||||
tableDataLoading,
|
||||
editCustomAttributePermission,
|
||||
editTagsPermission,
|
||||
decodedDatabaseSchemaFQN,
|
||||
tags,
|
||||
viewAllPermission,
|
||||
storedProcedureCount,
|
||||
onEditCancel,
|
||||
handleExtensionUpdate,
|
||||
handleTagSelection,
|
||||
onThreadLinkSelect,
|
||||
tablePaginationHandler,
|
||||
onDescriptionEdit,
|
||||
onDescriptionUpdate,
|
||||
handleShowDeletedTables,
|
||||
getEntityFeedCount,
|
||||
fetchDatabaseSchemaDetails,
|
||||
handleFeedCount,
|
||||
}),
|
||||
[
|
||||
feedCount,
|
||||
tableData,
|
||||
activeTab,
|
||||
currentTablesPage,
|
||||
databaseSchema,
|
||||
description,
|
||||
editDescriptionPermission,
|
||||
isEdit,
|
||||
showDeletedTables,
|
||||
tableDataLoading,
|
||||
editCustomAttributePermission,
|
||||
editTagsPermission,
|
||||
decodedDatabaseSchemaFQN,
|
||||
tags,
|
||||
viewAllPermission,
|
||||
storedProcedureCount,
|
||||
handleExtensionUpdate,
|
||||
handleTagSelection,
|
||||
onThreadLinkSelect,
|
||||
tablePaginationHandler,
|
||||
onEditCancel,
|
||||
onDescriptionEdit,
|
||||
onDescriptionUpdate,
|
||||
handleShowDeletedTables,
|
||||
getEntityFeedCount,
|
||||
fetchDatabaseSchemaDetails,
|
||||
handleFeedCount,
|
||||
]
|
||||
);
|
||||
|
||||
const updateVote = async (data: QueryVote, id: string) => {
|
||||
try {
|
||||
|
@ -10,122 +10,191 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Space, Tooltip, Typography } from 'antd';
|
||||
import { Button, Space, Tooltip, Typography } from 'antd';
|
||||
import { isEmpty, map } from 'lodash';
|
||||
import React, { FC, Fragment, useMemo } from 'react';
|
||||
import React, { FC, Fragment, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ReactComponent as IconEdit } from '../../../assets/svg/edit-new.svg';
|
||||
import { ReactComponent as PlusIcon } from '../../../assets/svg/plus-primary.svg';
|
||||
import TagButton from '../../../components/common/TagButton/TagButton.component';
|
||||
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
|
||||
import { DE_ACTIVE_COLOR, ICON_DIMENSION } from '../../../constants/constants';
|
||||
import { SUPPORTED_TABLE_CONSTRAINTS } from '../../../constants/Table.constants';
|
||||
import { EntityType, FqnPart } from '../../../enums/entity.enum';
|
||||
import { ConstraintType, Table } from '../../../generated/entity/data/table';
|
||||
import { getPartialNameFromTableFQN } from '../../../utils/CommonUtils';
|
||||
|
||||
import entityUtilClassBase from '../../../utils/EntityUtilClassBase';
|
||||
import ForeignKeyConstraint from './ForeignKeyConstraint';
|
||||
import PrimaryKeyConstraint from './PrimaryKeyConstraint';
|
||||
import './table-constraints.less';
|
||||
import TableConstraintsModal from './TableConstraintsModal/TableConstraintsModal.component';
|
||||
|
||||
interface TableConstraintsProps {
|
||||
constraints: Table['tableConstraints'];
|
||||
hasPermission: boolean;
|
||||
tableDetails?: Table;
|
||||
onUpdate: (updateData: Table['tableConstraints']) => Promise<void>;
|
||||
}
|
||||
|
||||
const TableConstraints: FC<TableConstraintsProps> = ({ constraints }) => {
|
||||
const TableConstraints: FC<TableConstraintsProps> = ({
|
||||
tableDetails,
|
||||
hasPermission,
|
||||
onUpdate,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const supportedConstraints = useMemo(
|
||||
() =>
|
||||
constraints?.filter((constraint) =>
|
||||
tableDetails?.tableConstraints?.filter((constraint) =>
|
||||
SUPPORTED_TABLE_CONSTRAINTS.includes(
|
||||
constraint.constraintType as ConstraintType
|
||||
)
|
||||
) ?? [],
|
||||
[constraints]
|
||||
[tableDetails?.tableConstraints]
|
||||
);
|
||||
|
||||
if (isEmpty(supportedConstraints)) {
|
||||
return null;
|
||||
}
|
||||
const handleOpenEditConstraintModal = useCallback(
|
||||
() => setIsModalOpen(true),
|
||||
[]
|
||||
);
|
||||
const handleCloseEditConstraintModal = useCallback(
|
||||
() => setIsModalOpen(false),
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSubmit = async (values: Table['tableConstraints']) => {
|
||||
await onUpdate(values);
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Space className="p-b-sm" direction="vertical">
|
||||
<Typography.Text className="right-panel-label">
|
||||
{t('label.table-constraints')}
|
||||
</Typography.Text>
|
||||
{supportedConstraints.map(
|
||||
({ constraintType, columns, referredColumns }, index) => {
|
||||
if (constraintType === ConstraintType.PrimaryKey) {
|
||||
return (
|
||||
<div className="d-flex constraint-columns" key={index}>
|
||||
<Space
|
||||
className="constraint-icon-container"
|
||||
direction="vertical"
|
||||
size={0}>
|
||||
{columns?.map((column, index) => (
|
||||
<Fragment key={column}>
|
||||
{(columns?.length ?? 0) - 1 !== index ? (
|
||||
<PrimaryKeyConstraint />
|
||||
) : null}
|
||||
</Fragment>
|
||||
))}
|
||||
</Space>
|
||||
<>
|
||||
<Space className="p-b-sm" direction="vertical">
|
||||
<Space size="middle">
|
||||
<Typography.Text className="right-panel-label">
|
||||
{t('label.table-constraints')}
|
||||
</Typography.Text>
|
||||
|
||||
<Space direction="vertical" size={16}>
|
||||
{columns?.map((column) => (
|
||||
<Typography.Text
|
||||
className="w-60"
|
||||
ellipsis={{ tooltip: true }}
|
||||
key={column}>
|
||||
{column}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (constraintType === ConstraintType.ForeignKey) {
|
||||
return (
|
||||
<Space className="constraint-columns" key={index}>
|
||||
<ForeignKeyConstraint />
|
||||
<Space direction="vertical" size={16}>
|
||||
<Typography.Text>{columns?.join(', ')}</Typography.Text>
|
||||
<div data-testid="referred-column-name">
|
||||
{map(referredColumns, (referredColumn) => (
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={referredColumn}
|
||||
trigger="hover">
|
||||
<Link
|
||||
className="no-underline"
|
||||
to={entityUtilClassBase.getEntityLink(
|
||||
EntityType.TABLE,
|
||||
getPartialNameFromTableFQN(
|
||||
referredColumn,
|
||||
[
|
||||
FqnPart.Service,
|
||||
FqnPart.Database,
|
||||
FqnPart.Schema,
|
||||
FqnPart.Table,
|
||||
],
|
||||
FQN_SEPARATOR_CHAR
|
||||
)
|
||||
)}>
|
||||
<Typography.Text className="truncate referred-column-name">
|
||||
{referredColumn}
|
||||
</Typography.Text>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
{hasPermission && !isEmpty(supportedConstraints) && (
|
||||
<Tooltip
|
||||
placement="right"
|
||||
title={t('label.edit-entity', {
|
||||
entity: t('label.table-constraint-plural'),
|
||||
})}>
|
||||
<Button
|
||||
className="cursor-pointer hover-cell-icon w-fit-content"
|
||||
data-testid="edit-displayName-button"
|
||||
style={{
|
||||
color: DE_ACTIVE_COLOR,
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
}}
|
||||
onClick={handleOpenEditConstraintModal}>
|
||||
<IconEdit
|
||||
style={{ color: DE_ACTIVE_COLOR, ...ICON_DIMENSION }}
|
||||
/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
{hasPermission && supportedConstraints.length === 0 && (
|
||||
<TagButton
|
||||
className="text-primary cursor-pointer"
|
||||
dataTestId="synonym-add-button"
|
||||
icon={<PlusIcon height={16} name="plus" width={16} />}
|
||||
label={t('label.add')}
|
||||
tooltip=""
|
||||
onClick={handleOpenEditConstraintModal}
|
||||
/>
|
||||
)}
|
||||
|
||||
{supportedConstraints.map(
|
||||
({ constraintType, columns, referredColumns }, index) => {
|
||||
if (constraintType === ConstraintType.PrimaryKey) {
|
||||
return (
|
||||
<div className="d-flex constraint-columns" key={index}>
|
||||
<Space
|
||||
className="constraint-icon-container"
|
||||
direction="vertical"
|
||||
size={0}>
|
||||
{columns?.map((column, index) => (
|
||||
<Fragment key={column}>
|
||||
{(columns?.length ?? 0) - 1 !== index ? (
|
||||
<PrimaryKeyConstraint />
|
||||
) : null}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</Space>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
</Space>
|
||||
|
||||
return null;
|
||||
}
|
||||
<Space direction="vertical" size={16}>
|
||||
{columns?.map((column) => (
|
||||
<Typography.Text
|
||||
className="w-60"
|
||||
ellipsis={{ tooltip: true }}
|
||||
key={column}>
|
||||
{column}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (constraintType === ConstraintType.ForeignKey) {
|
||||
return (
|
||||
<Space className="constraint-columns" key={index}>
|
||||
<ForeignKeyConstraint />
|
||||
<Space direction="vertical" size={16}>
|
||||
<Typography.Text>{columns?.join(', ')}</Typography.Text>
|
||||
<div data-testid="referred-column-name">
|
||||
{map(referredColumns, (referredColumn) => (
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={referredColumn}
|
||||
trigger="hover">
|
||||
<Link
|
||||
className="no-underline"
|
||||
to={entityUtilClassBase.getEntityLink(
|
||||
EntityType.TABLE,
|
||||
getPartialNameFromTableFQN(
|
||||
referredColumn,
|
||||
[
|
||||
FqnPart.Service,
|
||||
FqnPart.Database,
|
||||
FqnPart.Schema,
|
||||
FqnPart.Table,
|
||||
],
|
||||
FQN_SEPARATOR_CHAR
|
||||
)
|
||||
)}>
|
||||
<Typography.Text className="truncate referred-column-name">
|
||||
{referredColumn}
|
||||
</Typography.Text>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</Space>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
)}
|
||||
</Space>
|
||||
|
||||
{isModalOpen && (
|
||||
<TableConstraintsModal
|
||||
constraint={supportedConstraints}
|
||||
tableDetails={tableDetails}
|
||||
onClose={handleCloseEditConstraintModal}
|
||||
onSave={handleSubmit}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,293 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Licensed 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 Icon from '@ant-design/icons/lib/components/Icon';
|
||||
import { Button, Col, Form, Modal, Row, Select } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { debounce, isEmpty } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as IconDelete } from '../../../../assets/svg/ic-delete.svg';
|
||||
import { ReactComponent as PlusIcon } from '../../../../assets/svg/plus-primary.svg';
|
||||
import { PAGE_SIZE } from '../../../../constants/constants';
|
||||
import { RELATIONSHIP_TYPE_OPTION } from '../../../../constants/Table.constants';
|
||||
import { SearchIndex } from '../../../../enums/search.enum';
|
||||
import { ConstraintType, Table } from '../../../../generated/entity/data/table';
|
||||
import { searchQuery } from '../../../../rest/searchAPI';
|
||||
import { getServiceNameQueryFilter } from '../../../../utils/ServiceUtils';
|
||||
import { showErrorToast } from '../../../../utils/ToastUtils';
|
||||
import {
|
||||
SelectOptions,
|
||||
TableConstraintForm,
|
||||
TableConstraintModalProps,
|
||||
} from './TableConstraintsModal.interface';
|
||||
|
||||
const TableConstraintsModal = ({
|
||||
tableDetails,
|
||||
constraint,
|
||||
onSave,
|
||||
onClose,
|
||||
}: TableConstraintModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm<{ constraint: TableConstraintForm[] }>();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isRelatedColumnLoading, setIsRelatedColumnLoading] =
|
||||
useState<boolean>(false);
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [relatedColumns, setRelatedColumns] = useState<SelectOptions[]>([]);
|
||||
|
||||
const tableColumnNameOptions = useMemo(
|
||||
() =>
|
||||
tableDetails?.columns.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
})) ?? [],
|
||||
[tableDetails?.columns]
|
||||
);
|
||||
|
||||
const getSearchResults = async (value: string) => {
|
||||
setIsRelatedColumnLoading(true);
|
||||
try {
|
||||
const data = await searchQuery({
|
||||
query: value,
|
||||
searchIndex: SearchIndex.TABLE,
|
||||
queryFilter: getServiceNameQueryFilter(
|
||||
tableDetails?.service?.name ?? ''
|
||||
),
|
||||
pageNumber: 1,
|
||||
pageSize: PAGE_SIZE,
|
||||
includeDeleted: false,
|
||||
});
|
||||
const sources = data.hits.hits.map((hit) => hit._source);
|
||||
|
||||
const allColumns = sources.reduce((acc: SelectOptions[], cv: Table) => {
|
||||
const columnOption = cv.columns
|
||||
.map((item) => ({
|
||||
label: item.fullyQualifiedName ?? '',
|
||||
value: item.fullyQualifiedName ?? '',
|
||||
}))
|
||||
.filter(Boolean);
|
||||
|
||||
return [...acc, ...columnOption];
|
||||
}, []);
|
||||
|
||||
setRelatedColumns(allColumns);
|
||||
} catch (error) {
|
||||
showErrorToast(
|
||||
error as AxiosError,
|
||||
t('server.entity-fetch-error', {
|
||||
entity: t('label.suggestion-lowercase-plural'),
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setIsRelatedColumnLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const debounceOnSearch = useCallback(debounce(getSearchResults, 300), []);
|
||||
|
||||
const handleSearch = (value: string): void => {
|
||||
setSearchValue(value);
|
||||
debounceOnSearch(value);
|
||||
};
|
||||
|
||||
const handleSubmit = async (obj: { constraint: TableConstraintForm[] }) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await form.validateFields();
|
||||
const constraintData = obj.constraint.map((item) => ({
|
||||
...item,
|
||||
columns: [item.columns],
|
||||
referredColumns: [item.referredColumns],
|
||||
constraintType: ConstraintType.ForeignKey,
|
||||
}));
|
||||
|
||||
await onSave(constraintData);
|
||||
} catch (_) {
|
||||
// Nothing here
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const filteredConstraints = !isEmpty(constraint)
|
||||
? constraint
|
||||
?.filter((item) => item.constraintType !== ConstraintType.PrimaryKey)
|
||||
.map((item) => ({
|
||||
columns: item.columns?.[0],
|
||||
relationshipType: item.relationshipType,
|
||||
referredColumns: item.referredColumns?.[0],
|
||||
}))
|
||||
: [
|
||||
{
|
||||
columns: '',
|
||||
relationshipType: '',
|
||||
referredColumns: '',
|
||||
},
|
||||
];
|
||||
|
||||
form.setFieldValue('constraint', filteredConstraints);
|
||||
}, [constraint]);
|
||||
|
||||
useEffect(() => {
|
||||
getSearchResults(searchValue);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
centered
|
||||
destroyOnClose
|
||||
open
|
||||
closable={false}
|
||||
data-testid="table-constraint-modal"
|
||||
footer={[
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
key="cancel-btn"
|
||||
type="link"
|
||||
onClick={onClose}>
|
||||
{t('label.cancel')}
|
||||
</Button>,
|
||||
<Button
|
||||
data-testid="save-btn"
|
||||
key="save-btn"
|
||||
loading={isLoading}
|
||||
type="primary"
|
||||
onClick={form.submit}>
|
||||
{t('label.save')}
|
||||
</Button>,
|
||||
]}
|
||||
maskClosable={false}
|
||||
title={t(`label.${isEmpty(constraint) ? 'add' : 'update'}-entity`, {
|
||||
entity: t('label.table-constraint-plural'),
|
||||
})}
|
||||
width={600}
|
||||
onCancel={onClose}>
|
||||
<Form
|
||||
className="table-constraint-form"
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}>
|
||||
<Form.List name="constraint">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Row gutter={8} key={key}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
className="w-full"
|
||||
{...restField}
|
||||
label={t('label.entity-name', {
|
||||
entity: t('label.column'),
|
||||
})}
|
||||
name={[name, 'columns']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('label.field-required', {
|
||||
field: t('label.entity-name', {
|
||||
entity: t('label.column'),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
]}>
|
||||
<Select
|
||||
data-testid={`${key}-column-type-select`}
|
||||
options={tableColumnNameOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
label={t('label.entity-type-plural', {
|
||||
entity: t('label.relationship'),
|
||||
})}
|
||||
name={[name, 'relationshipType']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('label.field-required', {
|
||||
field: t('label.entity-type-plural', {
|
||||
entity: t('label.relationship'),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
]}>
|
||||
<Select
|
||||
data-testid={`${key}-relationship-type-select`}
|
||||
options={RELATIONSHIP_TYPE_OPTION}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={23}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
label={t('label.related-column')}
|
||||
name={[name, 'referredColumns']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('label.field-required', {
|
||||
field: t('label.related-column'),
|
||||
}),
|
||||
},
|
||||
]}>
|
||||
<Select
|
||||
showSearch
|
||||
data-testid={`${key}-related-column-select`}
|
||||
loading={isRelatedColumnLoading}
|
||||
options={relatedColumns}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onSearch={handleSearch}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={1}>
|
||||
<Button
|
||||
data-testid={`${key}-delete-constraint-button`}
|
||||
icon={
|
||||
<Icon
|
||||
className="align-middle"
|
||||
component={IconDelete}
|
||||
style={{ fontSize: '16px' }}
|
||||
/>
|
||||
}
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={() => remove(name)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
className="text-primary d-flex items-center"
|
||||
data-testid="add-constraint-button"
|
||||
icon={<PlusIcon className="anticon" />}
|
||||
size="small"
|
||||
onClick={() => add()}>
|
||||
{t('label.add')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableConstraintsModal;
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 Collate.
|
||||
* Copyright 2024 Collate.
|
||||
* Licensed 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
|
||||
@ -10,8 +10,26 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
RelationshipType,
|
||||
Table,
|
||||
TableConstraint,
|
||||
} from '../../../../generated/entity/data/table';
|
||||
|
||||
import { TabSpecificField } from '../enums/entity.enum';
|
||||
export interface TableConstraintModalProps {
|
||||
constraint: Table['tableConstraints'];
|
||||
tableDetails?: Table;
|
||||
onClose: () => void;
|
||||
onSave: (values: TableConstraint[]) => Promise<void>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
export const defaultFields = `${TabSpecificField.TAGS},${TabSpecificField.OWNERS},${TabSpecificField.USAGE_SUMMARY},${TabSpecificField.DOMAIN},${TabSpecificField.DATA_PRODUCTS}`;
|
||||
export interface TableConstraintForm {
|
||||
columns: string;
|
||||
referredColumns: string;
|
||||
relationshipType: RelationshipType;
|
||||
}
|
||||
|
||||
export interface SelectOptions {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
@ -12,39 +12,29 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Col, Row, Space, Tabs, Typography } from 'antd';
|
||||
import { Col, Row, Space, Tabs } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import { compare } from 'fast-json-patch';
|
||||
import { get, isEmpty, isEqual, isUndefined } from 'lodash';
|
||||
import { isEmpty, isEqual, isUndefined } from 'lodash';
|
||||
import { EntityTags } from 'Models';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { useActivityFeedProvider } from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider';
|
||||
import { ActivityFeedTab } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component';
|
||||
import ActivityThreadPanel from '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel';
|
||||
import { withActivityFeed } from '../../components/AppRouter/withActivityFeed';
|
||||
import { withSuggestions } from '../../components/AppRouter/withSuggestions';
|
||||
import { CustomPropertyTable } from '../../components/common/CustomPropertyTable/CustomPropertyTable';
|
||||
import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1';
|
||||
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
import Loader from '../../components/common/Loader/Loader';
|
||||
import QueryViewer from '../../components/common/QueryViewer/QueryViewer.component';
|
||||
import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels';
|
||||
import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component';
|
||||
import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component';
|
||||
import TableProfiler from '../../components/Database/Profiler/TableProfiler/TableProfiler';
|
||||
import SampleDataTableComponent from '../../components/Database/SampleDataTable/SampleDataTable.component';
|
||||
import SchemaTab from '../../components/Database/SchemaTab/SchemaTab.component';
|
||||
import TableQueries from '../../components/Database/TableQueries/TableQueries';
|
||||
import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface';
|
||||
import EntityRightPanel from '../../components/Entity/EntityRightPanel/EntityRightPanel';
|
||||
import IncidentManager from '../../components/IncidentManager/IncidentManager.component';
|
||||
import Lineage from '../../components/Lineage/Lineage.component';
|
||||
import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface';
|
||||
import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1';
|
||||
import { SourceType } from '../../components/SearchedData/SearchedData.interface';
|
||||
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
|
||||
import {
|
||||
getEntityDetailsPath,
|
||||
@ -54,7 +44,6 @@ import {
|
||||
import { FEED_COUNT_INITIAL_DATA } from '../../constants/entity.constants';
|
||||
import { mockDatasetData } from '../../constants/mockTourData.constants';
|
||||
import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../constants/ResizablePanel.constants';
|
||||
import LineageProvider from '../../context/LineageProvider/LineageProvider';
|
||||
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
|
||||
import {
|
||||
OperationPermission,
|
||||
@ -108,6 +97,7 @@ import EntityLink from '../../utils/EntityLink';
|
||||
import entityUtilClassBase from '../../utils/EntityUtilClassBase';
|
||||
import { getEntityName } from '../../utils/EntityUtils';
|
||||
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
|
||||
import tableClassBase from '../../utils/TableClassBase';
|
||||
import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils';
|
||||
import { createTagObject, updateTierTag } from '../../utils/TagsUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
||||
@ -452,6 +442,19 @@ const TableDetailsPageV1: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onTableConstraintsUpdate = async (
|
||||
updatedTableConstraints: Table['tableConstraints']
|
||||
) => {
|
||||
if (!tableDetails) {
|
||||
return;
|
||||
}
|
||||
const updatedTableDetails = {
|
||||
...tableDetails,
|
||||
tableConstraints: updatedTableConstraints,
|
||||
};
|
||||
await onTableUpdate(updatedTableDetails, 'tableConstraints');
|
||||
};
|
||||
|
||||
const onColumnsUpdate = async (updateColumns: Table['columns']) => {
|
||||
if (tableDetails && !isEqual(tableDetails.columns, updateColumns)) {
|
||||
const updatedTableDetails = {
|
||||
@ -598,7 +601,9 @@ const TableDetailsPageV1: React.FC = () => {
|
||||
direction="vertical"
|
||||
size="large">
|
||||
<TableConstraints
|
||||
constraints={tableDetails?.tableConstraints}
|
||||
hasPermission={editAllPermission && !deleted}
|
||||
tableDetails={tableDetails}
|
||||
onUpdate={onTableConstraintsUpdate}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
@ -655,222 +660,51 @@ const TableDetailsPageV1: React.FC = () => {
|
||||
);
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
const allTabs = [
|
||||
{
|
||||
label: <TabsLabel id={EntityTabs.SCHEMA} name={t('label.schema')} />,
|
||||
key: EntityTabs.SCHEMA,
|
||||
children: schemaTab,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
count={feedCount.totalCount}
|
||||
id={EntityTabs.ACTIVITY_FEED}
|
||||
isActive={activeTab === EntityTabs.ACTIVITY_FEED}
|
||||
name={t('label.activity-feed-and-task-plural')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.ACTIVITY_FEED,
|
||||
children: (
|
||||
<ActivityFeedTab
|
||||
refetchFeed
|
||||
columns={tableDetails?.columns}
|
||||
entityFeedTotalCount={feedCount.totalCount}
|
||||
entityType={EntityType.TABLE}
|
||||
fqn={tableDetails?.fullyQualifiedName ?? ''}
|
||||
owners={tableDetails?.owners}
|
||||
onFeedUpdate={getEntityFeedCount}
|
||||
onUpdateEntityDetails={fetchTableDetails}
|
||||
onUpdateFeedCount={handleFeedCount}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
id={EntityTabs.SAMPLE_DATA}
|
||||
name={t('label.sample-data')}
|
||||
/>
|
||||
),
|
||||
|
||||
key: EntityTabs.SAMPLE_DATA,
|
||||
children:
|
||||
!isTourOpen && !viewSampleDataPermission ? (
|
||||
<ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />
|
||||
) : (
|
||||
<SampleDataTableComponent
|
||||
isTableDeleted={deleted}
|
||||
owners={tableDetails?.owners ?? []}
|
||||
permissions={tablePermissions}
|
||||
tableId={tableDetails?.id ?? ''}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
count={queryCount}
|
||||
id={EntityTabs.TABLE_QUERIES}
|
||||
isActive={activeTab === EntityTabs.TABLE_QUERIES}
|
||||
name={t('label.query-plural')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.TABLE_QUERIES,
|
||||
children: !viewQueriesPermission ? (
|
||||
<ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />
|
||||
) : (
|
||||
<TableQueries
|
||||
isTableDeleted={deleted}
|
||||
tableId={tableDetails?.id ?? ''}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
id={EntityTabs.PROFILER}
|
||||
name={t('label.profiler-amp-data-quality')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.PROFILER,
|
||||
children:
|
||||
!isTourOpen && !viewProfilerPermission ? (
|
||||
<ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />
|
||||
) : (
|
||||
<TableProfiler
|
||||
permissions={tablePermissions}
|
||||
table={tableDetails}
|
||||
testCaseSummary={testCaseSummary}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
id={EntityTabs.INCIDENTS}
|
||||
name={t('label.incident-plural')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.INCIDENTS,
|
||||
children:
|
||||
tablePermissions.ViewAll || tablePermissions.ViewTests ? (
|
||||
<div className="p-x-lg p-b-lg p-t-md">
|
||||
<IncidentManager
|
||||
isIncidentPage={false}
|
||||
tableDetails={tableDetails}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />
|
||||
),
|
||||
},
|
||||
{
|
||||
label: <TabsLabel id={EntityTabs.LINEAGE} name={t('label.lineage')} />,
|
||||
key: EntityTabs.LINEAGE,
|
||||
children: (
|
||||
<LineageProvider>
|
||||
<Lineage
|
||||
deleted={deleted}
|
||||
entity={tableDetails as SourceType}
|
||||
entityType={EntityType.TABLE}
|
||||
hasEditAccess={editLineagePermission}
|
||||
/>
|
||||
</LineageProvider>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
label: (
|
||||
<TabsLabel id={EntityTabs.DBT} name={t('label.dbt-lowercase')} />
|
||||
),
|
||||
isHidden: !(
|
||||
tableDetails?.dataModel?.sql || tableDetails?.dataModel?.rawSql
|
||||
),
|
||||
key: EntityTabs.DBT,
|
||||
children: (
|
||||
<QueryViewer
|
||||
sqlQuery={
|
||||
get(tableDetails, 'dataModel.sql', '') ||
|
||||
get(tableDetails, 'dataModel.rawSql', '')
|
||||
}
|
||||
title={
|
||||
<Space className="p-y-xss">
|
||||
<Typography.Text className="text-grey-muted">
|
||||
{`${t('label.path')}:`}
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
{tableDetails?.dataModel?.path}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
id={
|
||||
isViewTableType
|
||||
? EntityTabs.VIEW_DEFINITION
|
||||
: EntityTabs.SCHEMA_DEFINITION
|
||||
}
|
||||
name={
|
||||
isViewTableType
|
||||
? t('label.view-definition')
|
||||
: t('label.schema-definition')
|
||||
}
|
||||
/>
|
||||
),
|
||||
isHidden: isUndefined(tableDetails?.schemaDefinition),
|
||||
key: isViewTableType
|
||||
? EntityTabs.VIEW_DEFINITION
|
||||
: EntityTabs.SCHEMA_DEFINITION,
|
||||
children: (
|
||||
<QueryViewer sqlQuery={tableDetails?.schemaDefinition ?? ''} />
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
id={EntityTabs.CUSTOM_PROPERTIES}
|
||||
name={t('label.custom-property-plural')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.CUSTOM_PROPERTIES,
|
||||
children: tableDetails && (
|
||||
<div className="m-sm">
|
||||
<CustomPropertyTable<EntityType.TABLE>
|
||||
entityDetails={tableDetails}
|
||||
entityType={EntityType.TABLE}
|
||||
handleExtensionUpdate={onExtensionUpdate}
|
||||
hasEditAccess={editCustomAttributePermission}
|
||||
hasPermission={viewAllPermission}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return allTabs.filter((data) => !data.isHidden);
|
||||
return tableClassBase
|
||||
.getTableDetailPageTabs({
|
||||
schemaTab,
|
||||
queryCount,
|
||||
isTourOpen,
|
||||
tablePermissions,
|
||||
activeTab,
|
||||
deleted,
|
||||
tableDetails,
|
||||
totalFeedCount: feedCount.totalCount,
|
||||
onExtensionUpdate,
|
||||
getEntityFeedCount,
|
||||
handleFeedCount,
|
||||
viewAllPermission,
|
||||
editCustomAttributePermission,
|
||||
viewSampleDataPermission,
|
||||
viewQueriesPermission,
|
||||
viewProfilerPermission,
|
||||
editLineagePermission,
|
||||
fetchTableDetails,
|
||||
testCaseSummary,
|
||||
isViewTableType,
|
||||
})
|
||||
.filter((data) => !data.isHidden);
|
||||
}, [
|
||||
schemaTab,
|
||||
queryCount,
|
||||
isTourOpen,
|
||||
tablePermissions,
|
||||
activeTab,
|
||||
schemaTab,
|
||||
deleted,
|
||||
tableDetails,
|
||||
feedCount.totalCount,
|
||||
entityName,
|
||||
onExtensionUpdate,
|
||||
getEntityFeedCount,
|
||||
handleFeedCount,
|
||||
tableDetails?.dataModel,
|
||||
viewAllPermission,
|
||||
editCustomAttributePermission,
|
||||
viewSampleDataPermission,
|
||||
viewQueriesPermission,
|
||||
viewProfilerPermission,
|
||||
editLineagePermission,
|
||||
fetchTableDetails,
|
||||
testCaseSummary,
|
||||
isViewTableType,
|
||||
]);
|
||||
|
||||
const onTierUpdate = useCallback(
|
||||
|
@ -102,6 +102,11 @@
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
// left
|
||||
.left-3 {
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
//bottom
|
||||
.bottom-full {
|
||||
bottom: 100%;
|
||||
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Licensed 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 { TabsProps } from 'antd';
|
||||
import { EntityTags, PagingResponse } from 'Models';
|
||||
import { PagingHandlerParams } from '../components/common/NextPrevious/NextPrevious.interface';
|
||||
import { EntityTabs } from '../enums/entity.enum';
|
||||
import { DatabaseSchema } from '../generated/entity/data/databaseSchema';
|
||||
import { Table } from '../generated/entity/data/table';
|
||||
import { ThreadType } from '../generated/entity/feed/thread';
|
||||
import { FeedCounts } from '../interface/feed.interface';
|
||||
import { getDataBaseSchemaPageBaseTabs } from './DatabaseSchemaDetailsUtils';
|
||||
|
||||
export interface DatabaseSchemaPageTabProps {
|
||||
feedCount: FeedCounts;
|
||||
tableData: PagingResponse<Table[]>;
|
||||
activeTab: EntityTabs;
|
||||
currentTablesPage: number;
|
||||
databaseSchema: DatabaseSchema;
|
||||
description: string;
|
||||
editDescriptionPermission: boolean;
|
||||
isEdit: boolean;
|
||||
showDeletedTables: boolean;
|
||||
tableDataLoading: boolean;
|
||||
editCustomAttributePermission: boolean;
|
||||
editTagsPermission: boolean;
|
||||
decodedDatabaseSchemaFQN: string;
|
||||
tags: any[];
|
||||
viewAllPermission: boolean;
|
||||
storedProcedureCount: number;
|
||||
handleExtensionUpdate: (schema: DatabaseSchema) => Promise<void>;
|
||||
handleTagSelection: (selectedTags: EntityTags[]) => Promise<void>;
|
||||
onThreadLinkSelect: (link: string, threadType?: ThreadType) => void;
|
||||
tablePaginationHandler: ({
|
||||
cursorType,
|
||||
currentPage,
|
||||
}: PagingHandlerParams) => void;
|
||||
onEditCancel: () => void;
|
||||
onDescriptionEdit: () => void;
|
||||
onDescriptionUpdate: (updatedHTML: string) => Promise<void>;
|
||||
handleShowDeletedTables: (value: boolean) => void;
|
||||
getEntityFeedCount: () => void;
|
||||
fetchDatabaseSchemaDetails: () => Promise<void>;
|
||||
handleFeedCount: (data: FeedCounts) => void;
|
||||
}
|
||||
|
||||
class DatabaseSchemaClassBase {
|
||||
public getDatabaseSchemaPageTabs(
|
||||
databaseSchemaTabData: DatabaseSchemaPageTabProps
|
||||
): TabsProps['items'] {
|
||||
return getDataBaseSchemaPageBaseTabs(databaseSchemaTabData);
|
||||
}
|
||||
}
|
||||
|
||||
const databaseSchemaClassBase = new DatabaseSchemaClassBase();
|
||||
|
||||
export default databaseSchemaClassBase;
|
||||
export { DatabaseSchemaClassBase };
|
@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright 2022 Collate.
|
||||
* Licensed 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 { Col, Row } from 'antd';
|
||||
import { t } from 'i18next';
|
||||
import React from 'react';
|
||||
import ActivityFeedProvider from '../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider';
|
||||
import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component';
|
||||
import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable';
|
||||
import ResizablePanels from '../components/common/ResizablePanels/ResizablePanels';
|
||||
import TabsLabel from '../components/common/TabsLabel/TabsLabel.component';
|
||||
import EntityRightPanel from '../components/Entity/EntityRightPanel/EntityRightPanel';
|
||||
import { COMMON_RESIZABLE_PANEL_CONFIG } from '../constants/ResizablePanel.constants';
|
||||
import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum';
|
||||
import SchemaTablesTab from '../pages/DatabaseSchemaPage/SchemaTablesTab';
|
||||
import StoredProcedureTab from '../pages/StoredProcedure/StoredProcedureTab';
|
||||
import { DatabaseSchemaPageTabProps } from './DatabaseSchemaClassBase';
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
export const defaultFields = `${TabSpecificField.TAGS},${TabSpecificField.OWNERS},${TabSpecificField.USAGE_SUMMARY},${TabSpecificField.DOMAIN},${TabSpecificField.DATA_PRODUCTS}`;
|
||||
|
||||
export const getDataBaseSchemaPageBaseTabs = ({
|
||||
feedCount,
|
||||
tableData,
|
||||
activeTab,
|
||||
currentTablesPage,
|
||||
databaseSchema,
|
||||
description,
|
||||
editDescriptionPermission,
|
||||
isEdit,
|
||||
showDeletedTables,
|
||||
tableDataLoading,
|
||||
editCustomAttributePermission,
|
||||
editTagsPermission,
|
||||
decodedDatabaseSchemaFQN,
|
||||
tags,
|
||||
viewAllPermission,
|
||||
storedProcedureCount,
|
||||
onEditCancel,
|
||||
handleExtensionUpdate,
|
||||
handleTagSelection,
|
||||
onThreadLinkSelect,
|
||||
tablePaginationHandler,
|
||||
onDescriptionEdit,
|
||||
onDescriptionUpdate,
|
||||
handleShowDeletedTables,
|
||||
getEntityFeedCount,
|
||||
fetchDatabaseSchemaDetails,
|
||||
handleFeedCount,
|
||||
}: DatabaseSchemaPageTabProps) => {
|
||||
return [
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
count={tableData.paging.total}
|
||||
id={EntityTabs.TABLE}
|
||||
isActive={activeTab === EntityTabs.TABLE}
|
||||
name={t('label.table-plural')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.TABLE,
|
||||
children: (
|
||||
<Row gutter={[0, 16]} wrap={false}>
|
||||
<Col className="tab-content-height-with-resizable-panel" span={24}>
|
||||
<ResizablePanels
|
||||
firstPanel={{
|
||||
className: 'entity-resizable-panel-container',
|
||||
children: (
|
||||
<div className="p-t-sm m-x-lg">
|
||||
<SchemaTablesTab
|
||||
currentTablesPage={currentTablesPage}
|
||||
databaseSchemaDetails={databaseSchema}
|
||||
description={description}
|
||||
editDescriptionPermission={editDescriptionPermission}
|
||||
isEdit={isEdit}
|
||||
showDeletedTables={showDeletedTables}
|
||||
tableData={tableData}
|
||||
tableDataLoading={tableDataLoading}
|
||||
tablePaginationHandler={tablePaginationHandler}
|
||||
onCancel={onEditCancel}
|
||||
onDescriptionEdit={onDescriptionEdit}
|
||||
onDescriptionUpdate={onDescriptionUpdate}
|
||||
onShowDeletedTablesChange={handleShowDeletedTables}
|
||||
onThreadLinkSelect={onThreadLinkSelect}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL,
|
||||
}}
|
||||
secondPanel={{
|
||||
children: (
|
||||
<div data-testid="entity-right-panel">
|
||||
<EntityRightPanel<EntityType.DATABASE_SCHEMA>
|
||||
customProperties={databaseSchema}
|
||||
dataProducts={databaseSchema?.dataProducts ?? []}
|
||||
domain={databaseSchema?.domain}
|
||||
editCustomAttributePermission={
|
||||
editCustomAttributePermission
|
||||
}
|
||||
editTagPermission={editTagsPermission}
|
||||
entityFQN={decodedDatabaseSchemaFQN}
|
||||
entityId={databaseSchema?.id ?? ''}
|
||||
entityType={EntityType.DATABASE_SCHEMA}
|
||||
selectedTags={tags}
|
||||
viewAllPermission={viewAllPermission}
|
||||
onExtensionUpdate={handleExtensionUpdate}
|
||||
onTagSelectionChange={handleTagSelection}
|
||||
onThreadLinkSelect={onThreadLinkSelect}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL,
|
||||
className:
|
||||
'entity-resizable-right-panel-container entity-resizable-panel-container',
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
count={storedProcedureCount}
|
||||
id={EntityTabs.STORED_PROCEDURE}
|
||||
isActive={activeTab === EntityTabs.STORED_PROCEDURE}
|
||||
name={t('label.stored-procedure-plural')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.STORED_PROCEDURE,
|
||||
children: <StoredProcedureTab />,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
count={feedCount.totalCount}
|
||||
id={EntityTabs.ACTIVITY_FEED}
|
||||
isActive={activeTab === EntityTabs.ACTIVITY_FEED}
|
||||
name={t('label.activity-feed-plural')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.ACTIVITY_FEED,
|
||||
children: (
|
||||
<ActivityFeedProvider>
|
||||
<ActivityFeedTab
|
||||
refetchFeed
|
||||
entityFeedTotalCount={feedCount.totalCount}
|
||||
entityType={EntityType.DATABASE_SCHEMA}
|
||||
fqn={databaseSchema.fullyQualifiedName ?? ''}
|
||||
onFeedUpdate={getEntityFeedCount}
|
||||
onUpdateEntityDetails={fetchDatabaseSchemaDetails}
|
||||
onUpdateFeedCount={handleFeedCount}
|
||||
/>
|
||||
</ActivityFeedProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
id={EntityTabs.CUSTOM_PROPERTIES}
|
||||
name={t('label.custom-property-plural')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.CUSTOM_PROPERTIES,
|
||||
children: databaseSchema && (
|
||||
<div className="m-sm">
|
||||
<CustomPropertyTable<EntityType.DATABASE_SCHEMA>
|
||||
className=""
|
||||
entityDetails={databaseSchema}
|
||||
entityType={EntityType.DATABASE_SCHEMA}
|
||||
handleExtensionUpdate={handleExtensionUpdate}
|
||||
hasEditAccess={editCustomAttributePermission}
|
||||
hasPermission={viewAllPermission}
|
||||
isVersionView={false}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
@ -521,3 +521,11 @@ export const getServiceDisplayNameQueryFilter = (displayName: string) => ({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const getServiceNameQueryFilter = (serviceName: string) => ({
|
||||
query: {
|
||||
match: {
|
||||
'service.name.keyword': serviceName,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Licensed 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 { OperationPermission } from '../context/PermissionProvider/PermissionProvider.interface';
|
||||
import { EntityTabs } from '../enums/entity.enum';
|
||||
import { Table } from '../generated/entity/data/table';
|
||||
import { TestSummary } from '../generated/tests/testCase';
|
||||
import { FeedCounts } from '../interface/feed.interface';
|
||||
import { getTableDetailPageBaseTabs } from './TableUtils';
|
||||
|
||||
export interface TableDetailPageTabProps {
|
||||
queryCount: number;
|
||||
isTourOpen: boolean;
|
||||
activeTab: EntityTabs;
|
||||
totalFeedCount: number;
|
||||
schemaTab: JSX.Element;
|
||||
isViewTableType: boolean;
|
||||
viewAllPermission: boolean;
|
||||
viewQueriesPermission: boolean;
|
||||
editLineagePermission: boolean;
|
||||
viewProfilerPermission: boolean;
|
||||
viewSampleDataPermission: boolean;
|
||||
tablePermissions: OperationPermission;
|
||||
editCustomAttributePermission: boolean;
|
||||
deleted?: boolean;
|
||||
tableDetails?: Table;
|
||||
testCaseSummary?: TestSummary;
|
||||
getEntityFeedCount: () => void;
|
||||
fetchTableDetails: () => Promise<void>;
|
||||
onExtensionUpdate: (updatedData: Table) => Promise<void>;
|
||||
handleFeedCount: (data: FeedCounts) => void;
|
||||
}
|
||||
|
||||
class TableClassBase {
|
||||
public getTableDetailPageTabs(
|
||||
tableDetailsPageProps: TableDetailPageTabProps
|
||||
) {
|
||||
return getTableDetailPageBaseTabs(tableDetailsPageProps);
|
||||
}
|
||||
}
|
||||
|
||||
const tableClassBase = new TableClassBase();
|
||||
|
||||
export default tableClassBase;
|
||||
export { TableClassBase };
|
@ -12,11 +12,12 @@
|
||||
*/
|
||||
|
||||
import Icon, { SearchOutlined } from '@ant-design/icons';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Space, Tooltip, Typography } from 'antd';
|
||||
import { ExpandableConfig } from 'antd/lib/table/interface';
|
||||
import classNames from 'classnames';
|
||||
import { t } from 'i18next';
|
||||
import {
|
||||
get,
|
||||
isUndefined,
|
||||
lowerCase,
|
||||
omit,
|
||||
@ -79,12 +80,23 @@ import { ReactComponent as TagIcon } from '../assets/svg/tag.svg';
|
||||
import { ReactComponent as TaskIcon } from '../assets/svg/task-ic.svg';
|
||||
import { ReactComponent as TeamIcon } from '../assets/svg/teams.svg';
|
||||
import { ReactComponent as UserIcon } from '../assets/svg/user.svg';
|
||||
|
||||
import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component';
|
||||
import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable';
|
||||
import ErrorPlaceHolder from '../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
import QueryViewer from '../components/common/QueryViewer/QueryViewer.component';
|
||||
import TabsLabel from '../components/common/TabsLabel/TabsLabel.component';
|
||||
import TableProfiler from '../components/Database/Profiler/TableProfiler/TableProfiler';
|
||||
import SampleDataTableComponent from '../components/Database/SampleDataTable/SampleDataTable.component';
|
||||
import TableQueries from '../components/Database/TableQueries/TableQueries';
|
||||
import IncidentManager from '../components/IncidentManager/IncidentManager.component';
|
||||
import Lineage from '../components/Lineage/Lineage.component';
|
||||
import { SourceType } from '../components/SearchedData/SearchedData.interface';
|
||||
import { NON_SERVICE_TYPE_ASSETS } from '../constants/Assets.constants';
|
||||
import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
|
||||
import { DE_ACTIVE_COLOR, TEXT_BODY_COLOR } from '../constants/constants';
|
||||
import { EntityType, FqnPart } from '../enums/entity.enum';
|
||||
import LineageProvider from '../context/LineageProvider/LineageProvider';
|
||||
import { ERROR_PLACEHOLDER_TYPE } from '../enums/common.enum';
|
||||
import { EntityTabs, EntityType, FqnPart } from '../enums/entity.enum';
|
||||
import { SearchIndex } from '../enums/search.enum';
|
||||
import { ConstraintTypes, PrimaryTableDataTypes } from '../enums/table.enum';
|
||||
import { SearchIndexField } from '../generated/entity/data/searchIndex';
|
||||
@ -104,6 +116,7 @@ import EntityLink from './EntityLink';
|
||||
import searchClassBase from './SearchClassBase';
|
||||
import serviceUtilClassBase from './ServiceUtilClassBase';
|
||||
import { ordinalize } from './StringsUtils';
|
||||
import { TableDetailPageTabProps } from './TableClassBase';
|
||||
import { TableFieldsInfoCommonEntities } from './TableUtils.interface';
|
||||
|
||||
export const getUsagePercentile = (pctRank: number, isLiteral = false) => {
|
||||
@ -608,3 +621,212 @@ export const updateFieldTags = <T extends TableFieldsInfoCommonEntities>(
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getTableDetailPageBaseTabs = ({
|
||||
schemaTab,
|
||||
queryCount,
|
||||
isTourOpen,
|
||||
tablePermissions,
|
||||
activeTab,
|
||||
deleted,
|
||||
tableDetails,
|
||||
totalFeedCount,
|
||||
onExtensionUpdate,
|
||||
getEntityFeedCount,
|
||||
handleFeedCount,
|
||||
viewAllPermission,
|
||||
editCustomAttributePermission,
|
||||
viewSampleDataPermission,
|
||||
viewQueriesPermission,
|
||||
viewProfilerPermission,
|
||||
editLineagePermission,
|
||||
fetchTableDetails,
|
||||
testCaseSummary,
|
||||
isViewTableType,
|
||||
}: TableDetailPageTabProps) => {
|
||||
return [
|
||||
{
|
||||
label: <TabsLabel id={EntityTabs.SCHEMA} name={t('label.schema')} />,
|
||||
key: EntityTabs.SCHEMA,
|
||||
children: schemaTab,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
count={totalFeedCount}
|
||||
id={EntityTabs.ACTIVITY_FEED}
|
||||
isActive={activeTab === EntityTabs.ACTIVITY_FEED}
|
||||
name={t('label.activity-feed-and-task-plural')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.ACTIVITY_FEED,
|
||||
children: (
|
||||
<ActivityFeedTab
|
||||
refetchFeed
|
||||
columns={tableDetails?.columns}
|
||||
entityFeedTotalCount={totalFeedCount}
|
||||
entityType={EntityType.TABLE}
|
||||
fqn={tableDetails?.fullyQualifiedName ?? ''}
|
||||
owners={tableDetails?.owners}
|
||||
onFeedUpdate={getEntityFeedCount}
|
||||
onUpdateEntityDetails={fetchTableDetails}
|
||||
onUpdateFeedCount={handleFeedCount}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel id={EntityTabs.SAMPLE_DATA} name={t('label.sample-data')} />
|
||||
),
|
||||
|
||||
key: EntityTabs.SAMPLE_DATA,
|
||||
children:
|
||||
!isTourOpen && !viewSampleDataPermission ? (
|
||||
<ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />
|
||||
) : (
|
||||
<SampleDataTableComponent
|
||||
isTableDeleted={deleted}
|
||||
owners={tableDetails?.owners ?? []}
|
||||
permissions={tablePermissions}
|
||||
tableId={tableDetails?.id ?? ''}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
count={queryCount}
|
||||
id={EntityTabs.TABLE_QUERIES}
|
||||
isActive={activeTab === EntityTabs.TABLE_QUERIES}
|
||||
name={t('label.query-plural')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.TABLE_QUERIES,
|
||||
children: !viewQueriesPermission ? (
|
||||
<ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />
|
||||
) : (
|
||||
<TableQueries
|
||||
isTableDeleted={deleted}
|
||||
tableId={tableDetails?.id ?? ''}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
id={EntityTabs.PROFILER}
|
||||
name={t('label.profiler-amp-data-quality')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.PROFILER,
|
||||
children:
|
||||
!isTourOpen && !viewProfilerPermission ? (
|
||||
<ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />
|
||||
) : (
|
||||
<TableProfiler
|
||||
permissions={tablePermissions}
|
||||
table={tableDetails}
|
||||
testCaseSummary={testCaseSummary}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
id={EntityTabs.INCIDENTS}
|
||||
name={t('label.incident-plural')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.INCIDENTS,
|
||||
children:
|
||||
tablePermissions.ViewAll || tablePermissions.ViewTests ? (
|
||||
<div className="p-x-lg p-b-lg p-t-md">
|
||||
<IncidentManager
|
||||
isIncidentPage={false}
|
||||
tableDetails={tableDetails}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />
|
||||
),
|
||||
},
|
||||
{
|
||||
label: <TabsLabel id={EntityTabs.LINEAGE} name={t('label.lineage')} />,
|
||||
key: EntityTabs.LINEAGE,
|
||||
children: (
|
||||
<LineageProvider>
|
||||
<Lineage
|
||||
deleted={deleted}
|
||||
entity={tableDetails as SourceType}
|
||||
entityType={EntityType.TABLE}
|
||||
hasEditAccess={editLineagePermission}
|
||||
/>
|
||||
</LineageProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: <TabsLabel id={EntityTabs.DBT} name={t('label.dbt-lowercase')} />,
|
||||
isHidden: !(
|
||||
tableDetails?.dataModel?.sql || tableDetails?.dataModel?.rawSql
|
||||
),
|
||||
key: EntityTabs.DBT,
|
||||
children: (
|
||||
<QueryViewer
|
||||
sqlQuery={
|
||||
get(tableDetails, 'dataModel.sql', '') ||
|
||||
get(tableDetails, 'dataModel.rawSql', '')
|
||||
}
|
||||
title={
|
||||
<Space className="p-y-xss">
|
||||
<Typography.Text className="text-grey-muted">
|
||||
{`${t('label.path')}:`}
|
||||
</Typography.Text>
|
||||
<Typography.Text>{tableDetails?.dataModel?.path}</Typography.Text>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
id={
|
||||
isViewTableType
|
||||
? EntityTabs.VIEW_DEFINITION
|
||||
: EntityTabs.SCHEMA_DEFINITION
|
||||
}
|
||||
name={
|
||||
isViewTableType
|
||||
? t('label.view-definition')
|
||||
: t('label.schema-definition')
|
||||
}
|
||||
/>
|
||||
),
|
||||
isHidden: isUndefined(tableDetails?.schemaDefinition),
|
||||
key: isViewTableType
|
||||
? EntityTabs.VIEW_DEFINITION
|
||||
: EntityTabs.SCHEMA_DEFINITION,
|
||||
children: <QueryViewer sqlQuery={tableDetails?.schemaDefinition ?? ''} />,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<TabsLabel
|
||||
id={EntityTabs.CUSTOM_PROPERTIES}
|
||||
name={t('label.custom-property-plural')}
|
||||
/>
|
||||
),
|
||||
key: EntityTabs.CUSTOM_PROPERTIES,
|
||||
children: tableDetails && (
|
||||
<div className="m-sm">
|
||||
<CustomPropertyTable<EntityType.TABLE>
|
||||
entityDetails={tableDetails}
|
||||
entityType={EntityType.TABLE}
|
||||
handleExtensionUpdate={onExtensionUpdate}
|
||||
hasEditAccess={editCustomAttributePermission}
|
||||
hasPermission={viewAllPermission}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user