From fb826b2c6892d448ce874067ae02459ea320a3bd Mon Sep 17 00:00:00 2001 From: Teddy Date: Thu, 28 Nov 2024 08:09:56 +0100 Subject: [PATCH 01/10] MINOR -- propagate result to suites (#18823) * fix: update result summary for test suite * fix: stream pattern * fix: inverse null conditional logic --- .../jdbi3/TestCaseResultRepository.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseResultRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseResultRepository.java index 401095c63921..4e247fbab2bc 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseResultRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseResultRepository.java @@ -4,6 +4,7 @@ import static org.openmetadata.service.Entity.TEST_CASE; import static org.openmetadata.service.Entity.TEST_CASE_RESULT; import static org.openmetadata.service.Entity.TEST_DEFINITION; +import static org.openmetadata.service.Entity.TEST_SUITE; import java.io.IOException; import java.util.Arrays; @@ -17,7 +18,9 @@ import lombok.SneakyThrows; import org.openmetadata.common.utils.CommonUtil; import org.openmetadata.schema.api.tests.CreateTestCaseResult; +import org.openmetadata.schema.tests.ResultSummary; import org.openmetadata.schema.tests.TestCase; +import org.openmetadata.schema.tests.TestSuite; import org.openmetadata.schema.tests.type.TestCaseResult; import org.openmetadata.schema.tests.type.TestCaseStatus; import org.openmetadata.schema.type.EntityReference; @@ -222,6 +225,45 @@ private void updateTestCaseStatus(TestCaseResult testCaseResult, OperationType o EntityRepository.EntityUpdater entityUpdater = testCaseRepository.getUpdater(original, updated, EntityRepository.Operation.PATCH); entityUpdater.update(); + updateTestSuiteSummary(updated); + } + + private void updateTestSuiteSummary(TestCase testCase) { + List fqns = + testCase.getTestSuites() != null + ? testCase.getTestSuites().stream().map(TestSuite::getFullyQualifiedName).toList() + : null; + TestSuiteRepository testSuiteRepository = new TestSuiteRepository(); + if (fqns != null) { + for (String fqn : fqns) { + TestSuite testSuite = Entity.getEntityByName(TEST_SUITE, fqn, "*", Include.ALL); + if (testSuite != null) { + TestSuite original = JsonUtils.deepCopy(testSuite, TestSuite.class); + List resultSummaries = testSuite.getTestCaseResultSummary(); + + if (resultSummaries != null) { + resultSummaries.stream() + .filter(s -> s.getTestCaseName().equals(testCase.getFullyQualifiedName())) + .findFirst() + .ifPresent( + s -> { + s.setStatus(testCase.getTestCaseStatus()); + s.setTimestamp(testCase.getTestCaseResult().getTimestamp()); + }); + } else { + testSuite.setTestCaseResultSummary( + List.of( + new ResultSummary() + .withTestCaseName(testCase.getFullyQualifiedName()) + .withStatus(testCase.getTestCaseStatus()) + .withTimestamp(testCase.getTestCaseResult().getTimestamp()))); + } + EntityRepository.EntityUpdater entityUpdater = + testSuiteRepository.getUpdater(original, testSuite, EntityRepository.Operation.PATCH); + entityUpdater.update(); + } + } + } } @Override From d3af7ae60168522ce18143c3f38350bc0cf7863f Mon Sep 17 00:00:00 2001 From: RounakDhillon <162090200+RounakDhillon@users.noreply.github.com> Date: Thu, 28 Nov 2024 12:43:54 +0530 Subject: [PATCH 02/10] Docs: Airflow Version Update (#18828) * Docs: Airflow Version Update * Docs: Airflow Version Update --------- Co-authored-by: Rounak Dhillon --- openmetadata-docs/content/v1.5.x/deployment/bare-metal/index.md | 2 +- .../content/v1.6.x-SNAPSHOT/deployment/bare-metal/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openmetadata-docs/content/v1.5.x/deployment/bare-metal/index.md b/openmetadata-docs/content/v1.5.x/deployment/bare-metal/index.md index 180b1a29c9d5..5345831ce5a0 100644 --- a/openmetadata-docs/content/v1.5.x/deployment/bare-metal/index.md +++ b/openmetadata-docs/content/v1.5.x/deployment/bare-metal/index.md @@ -65,7 +65,7 @@ Please follow the instructions here to [install ElasticSearch](https://www.elast If you are using AWS OpenSearch Service, OpenMetadata Supports AWS OpenSearch Service engine version up to 2.7. For more information on AWS OpenSearch Service, please visit the official docs [here](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/what-is.html). -## Airflow (version 2.0.0 or higher) or other workflow schedulers +## Airflow (version 2.9.1) or other workflow schedulers OpenMetadata performs metadata ingestion using the Ingestion Framework. Learn more about how to deploy and manage the ingestion workflows [here](/deployment/ingestion). diff --git a/openmetadata-docs/content/v1.6.x-SNAPSHOT/deployment/bare-metal/index.md b/openmetadata-docs/content/v1.6.x-SNAPSHOT/deployment/bare-metal/index.md index 0299492340f6..f5a81bc9bc94 100644 --- a/openmetadata-docs/content/v1.6.x-SNAPSHOT/deployment/bare-metal/index.md +++ b/openmetadata-docs/content/v1.6.x-SNAPSHOT/deployment/bare-metal/index.md @@ -61,7 +61,7 @@ Please follow the instructions here to [install ElasticSearch](https://www.elast If you are using AWS OpenSearch Service, OpenMetadata Supports AWS OpenSearch Service engine version up to 2.7. For more information on AWS OpenSearch Service, please visit the official docs [here](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/what-is.html). -## Airflow (version 2.0.0 or higher) or other workflow schedulers +## Airflow (version 2.9.1) or other workflow schedulers OpenMetadata performs metadata ingestion using the Ingestion Framework. Learn more about how to deploy and manage the ingestion workflows [here](/deployment/ingestion). From 860b420d3d12d7b10aeecb237c7ebe3e1d631a31 Mon Sep 17 00:00:00 2001 From: Amir Shakeri <44241934+mramirsha@users.noreply.github.com> Date: Thu, 28 Nov 2024 08:21:23 +0100 Subject: [PATCH 03/10] Remove ORDER BY from the postgres DELETE statement (#18757) Co-authored-by: amir.shakeri Co-authored-by: Sriharsha Chintalapani --- .../main/java/org/openmetadata/service/jdbi3/CollectionDAO.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java index dc64fb56ba47..7515277c1183 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java @@ -1176,7 +1176,7 @@ void deleteLineageBySource( value = "DELETE FROM entity_relationship " + "WHERE json->'pipeline'->>'id' =:toId OR toId = :toId AND relation = :relation " - + "AND json->>'source' = :source ORDER BY toId", + + "AND json->>'source' = :source", connectionType = POSTGRES) void deleteLineageBySourcePipeline( @BindUUID("toId") UUID toId, From 609620e54f8086e82ba04dd8199a9627d9192bb7 Mon Sep 17 00:00:00 2001 From: Akash Verma <138790903+akashverma0786@users.noreply.github.com> Date: Thu, 28 Nov 2024 13:02:04 +0530 Subject: [PATCH 04/10] fixes : #12891: "Added more test connection cases for Airflow ingestion" (#18769) --- .../source/pipeline/airflow/connection.py | 47 ++++++++++++++++++- .../testConnections/pipeline/airflow.json | 13 +++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/ingestion/src/metadata/ingestion/source/pipeline/airflow/connection.py b/ingestion/src/metadata/ingestion/source/pipeline/airflow/connection.py index 70dacd993d54..8579fb739b37 100644 --- a/ingestion/src/metadata/ingestion/source/pipeline/airflow/connection.py +++ b/ingestion/src/metadata/ingestion/source/pipeline/airflow/connection.py @@ -16,7 +16,9 @@ from typing import Optional from airflow import settings +from airflow.models.serialized_dag import SerializedDagModel from sqlalchemy.engine import Engine +from sqlalchemy.orm import sessionmaker from metadata.generated.schema.entity.automations.workflow import ( Workflow as AutomationWorkflow, @@ -102,6 +104,18 @@ def get_connection(connection: AirflowConnection) -> Engine: raise SourceConnectionException(msg) from exc +class AirflowPipelineDetailsAccessError(Exception): + """ + Raise when Pipeline information is not retrieved + """ + + +class AirflowTaskDetailsAccessError(Exception): + """ + Raise when Task detail information is not retrieved + """ + + def test_connection( metadata: OpenMetadata, engine: Engine, @@ -114,8 +128,37 @@ def test_connection( of a metadata workflow or during an Automation Workflow """ - test_fn = {"CheckAccess": partial(test_connection_engine_step, engine)} - + session_maker = sessionmaker(bind=engine) + session = session_maker() + + def test_pipeline_details_access(session): + try: + result = session.query(SerializedDagModel).first() + return result + except Exception as e: + raise AirflowPipelineDetailsAccessError( + f"Pipeline details access error: {e}" + ) + + def test_task_detail_access(session): + try: + json_data_column = ( + SerializedDagModel._data # For 2.3.0 onwards # pylint: disable=protected-access + if hasattr(SerializedDagModel, "_data") + else SerializedDagModel.data # For 2.2.5 and 2.1.4 + ) + result = session.query(json_data_column).first() + + retrieved_tasks = result[0]["dag"]["tasks"] + return retrieved_tasks + except Exception as e: + raise AirflowTaskDetailsAccessError(f"Task details access error : {e}") + + test_fn = { + "CheckAccess": partial(test_connection_engine_step, engine), + "PipelineDetailsAccess": partial(test_pipeline_details_access, session), + "TaskDetailAccess": partial(test_task_detail_access, session), + } return test_connection_steps( metadata=metadata, test_fn=test_fn, diff --git a/openmetadata-service/src/main/resources/json/data/testConnections/pipeline/airflow.json b/openmetadata-service/src/main/resources/json/data/testConnections/pipeline/airflow.json index 27fb8ecba9ba..2a8cf423d40f 100644 --- a/openmetadata-service/src/main/resources/json/data/testConnections/pipeline/airflow.json +++ b/openmetadata-service/src/main/resources/json/data/testConnections/pipeline/airflow.json @@ -9,6 +9,19 @@ "errorMessage": "Failed to connect to airflow, please validate the credentials", "shortCircuit": true, "mandatory": true + }, + { + "name": "PipelineDetailsAccess", + "description": "Check if pipeline details can be fetched.", + "errorMessage": "Failed to fetch Pipeine details.", + "mandatory": true + }, + { + "name": "TaskDetailAccess", + "description": "Check if task details can be fetched.", + "errorMessage": "Failed to fetch Task details.", + "mandatory": true } + ] } \ No newline at end of file From b6145942526a75fc7ab8ab50ef52239d1f08c14f Mon Sep 17 00:00:00 2001 From: Karan Hotchandani <33024356+karanh37@users.noreply.github.com> Date: Thu, 28 Nov 2024 13:12:46 +0530 Subject: [PATCH 05/10] Json logic query builder fixes (#18819) * minor workflow changes * fix json logic fixes for extension field * fix tests * query builder ui changes * minor add entity type in global search * fix tests * update certification ui * update default icons * minor add test id --- .../json/data/tags/certification.json | 6 +-- .../CertificationTag/CertificationTag.tsx | 26 ++++++++--- .../QueryBuilderWidget/QueryBuilderWidget.tsx | 45 ++++++++++++++++++- .../query-builder-widget.less | 27 +++++++++++ .../pages/AppInstall/AppInstall.component.tsx | 2 +- .../src/utils/AdvancedSearchClassBase.test.ts | 1 + .../ui/src/utils/AdvancedSearchClassBase.ts | 13 ++++++ .../ui/src/utils/JSONLogicSearchClassBase.ts | 4 +- .../ui/src/utils/QueryBuilderUtils.tsx | 39 +++++++++++++++- 9 files changed, 149 insertions(+), 14 deletions(-) diff --git a/openmetadata-service/src/main/resources/json/data/tags/certification.json b/openmetadata-service/src/main/resources/json/data/tags/certification.json index dd02efd168ee..a2ca8c388591 100644 --- a/openmetadata-service/src/main/resources/json/data/tags/certification.json +++ b/openmetadata-service/src/main/resources/json/data/tags/certification.json @@ -11,7 +11,7 @@ "description": "Bronze certified Data Asset.", "style": { "color": "#C08329", - "iconURL": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAHqADAAQAAAABAAAAHgAAAADKQTcFAAAFZklEQVRIDZ1XTWwbVRAer3+ySZx4E9upEye109A0VSAxtE2qSpS4B1QJiSIqaA+AEokLggNcEFAaJ0WVgBOIKyLJCYpURE9IkcBpDwgFipyo+S1tbRIHO9j1xrHrjf+WN+vsy3qz+Skj2TtvZt58b+bNm7erg/9B/qudHiiAh07VFXlgmKD30kyAyvZgdHvoqdr/yZN9Fnujz8w1eCpr6jkTWw2MwSjpi/kcZFI8ZDPrfDoZm+Ajyze8gzOjdLIGsycwAlqdrSM2Z7vbyFZpuNguygmPIBZeDMaX7w/vtIAdgf1Dbs5c3+JztHneZc3cdu/7kOACwgu/fZF6GB72DgV55RRNYAS1uz1+e0uHR06nctLj8AWyDZH7U4FEeM6rBNernSBonfOov/GJpz06plyNTlIPo7BRrISCqQF03BHIghmAtUvyTGIJ1NlhiI9qi92REzJnL54Qr41N8AJiGtTAmN7GQ90etTyTWge9rRsOnvoQDGytWi2NBT4Mq9PXIRu9DdwBF7XRkyIkW+YR0ms+gOB7qChLtf9KZ/+R3rMj6iJK8mvQ0PMWmB0d1NluDC5g5dbnUMtZysxWQ7OwGlrwei/fmWCUGmvzIZ8WaNPp9zVB+dAkIIiaWM4JOAcXrCSr8zAYjfoRlNFNxGgPHu3tx7TIxEdC4DwzCOhITQh4++vzsPb379D0zAW1WtoOo6UV1kO/gtFUIelxv3NZgXv1uPEmjbjO0XxOHa3JcVwTFL3EFn+WnKUi85pRoxK3BusCi1KmWmsT2d+CjwJXWex9shKfGG1D18tKURkfnbpBx5HpHymvZhq6zkOa/5eKN6veI1W1/+phj0l1Dhizk0abF5KAzvPCOnWQis5RPr4ZvSzArXF0vSQNDWwN6IkvAFEa41Ya2WqudJyKBrcKF6ocnbIf4EN/wL3xT+lYzWC68ackW/sZeux0rJWoYko1lFItMpyyqNAiny9QQ851AjhXDx3vxbhOv01B0ZZhubIpJtLz6R6XaVQDTFf366OADncjTDHaufewy5IeToGVlYfOiwK/DQMdtj3/wTa5LOh85SvNzOjyadlEeuaE9CawPhvAgZJEIa4cUj6+8Avl1Qw2FC3a4JeoWCD3NpIUsffS3UAmlSgLsZAKkypO0gnIYNNQOseerezbWovCOcX0VndLr0lHi6epTieiASVKNWeXGr5SxpMuJZOz5w3ofWccjr15nR47XJR6sSuTo2UXRjz8F4gi3KQt82KvKcQdaKUtE9tbOjILxvp2MJltJTxyFJPhaWh/wQfNBJgxVEgRI18iHTQd22qfGO3a7Pf0qsSmxEeDxFT8rOx2mv7mxQeNbd3ukpPSPzZ6bPha/Vppp+YRVH1DLU7+BLmNdLDv47lWmmqcGA8vDsibLzvCqw0doKP9EjYTNSheiVjAYlEcRj801TgY88eCF3oNdRZby0lMtUwVLAurUz8AH56FSmtbWUHJNviUWuuf30Jy5juorbdTVTx8F1Yf3MGm+aV3cF5qgdveQDKJR0OxpfnnbOR9S9nNOIeLONqA5fGPAPt4BdcCoqFaco5nHo8fngQzKUqTrYmC8tEQRO5N4TgAhcyQrCjbY1noH/JwFWaT3/XUs+Ty2N8rrTxX+URAjJZQQCxkdn/ZQ6uxiYjw2in7tfg/85VkZSfJbQLK6NFmN8JrMDh9C1KJiJReEumA8g0T52pGrHTqv9LRrzeyvlpro7uGXOJ4vrUWgUW5Hl+BJPlJBaqDoCiKA97L8xNKfzK/J7BsiAvQ6ZlzUIQ+xmCQPmFQhz1+q93qeNCJAQI4vBOg7G/fwPIEfEofbcWiG8h1SuV6CDzOR9t/2kYjDNER/+0AAAAASUVORK5CYII=" + "iconURL": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAiIGhlaWdodD0iMzAiIHZpZXdCb3g9IjAgMCAzMCAzMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzLjI1NzIgMS4yODE3N0w2LjMyOTMyIDQuMTIyMDVDNC45MTY3IDQuNzAwNjIgNCA2LjA3NTY3IDQgNy42MDFWMTYuNzA3OUM0IDI0Ljg5ODEgMTEuMTQ1OCAyNy40ODI5IDEzLjc1MzEgMjguMTUxN0MxNC4zNjkzIDI4LjMwOTQgMTUuMDA3OSAyOC4zMDk0IDE1LjYyNDEgMjguMTUxN0MxOC4yMzE0IDI3LjQ4MjkgMjUuMzc3MiAyNC44OTgxIDI1LjM3NzIgMTYuNzA3OVY3LjYwMUMyNS4zNzcyIDYuMDc1NjcgMjQuNDUzIDQuNzAwNjIgMjMuMDQ3OSA0LjEyMjA1TDE2LjExMjUgMS4yODE3N0MxNS4xOTU4IDAuOTA2MDc2IDE0LjE3MzkgMC45MDYwNzYgMTMuMjU3MiAxLjI4MTc3WiIgZmlsbD0iI0VFRDZCMyIvPgo8cGF0aCBkPSJNMTQuNjg0NyAyNi4zODA2QzE0LjUyNjkgMjYuMzgwNiAxNC4zNjkxIDI2LjM1OCAxNC4yMTg4IDI2LjMyMDVDMTMuMDk5MyAyNi4wMzQ5IDEwLjkzNTMgMjUuMzIxMSA5LjA5NDM0IDIzLjc5NThDNi45NjAzNyAyMi4wMjI1IDUuODcwODUgMTkuNjMzMSA1Ljg3MDg1IDE2LjcwMjZWNy41OTU3MUM1Ljg3MDg1IDYuODI5MjkgNi4zMjkyIDYuMTQ1NTIgNy4wMzU1MSA1Ljg1OTk5TDEzLjk2MzQgMy4wMTk3MkMxNC4xODg4IDIuOTI5NTUgMTQuNDI5MiAyLjg3Njk1IDE0LjY3NzIgMi44NzY5NUMxNC45MjUyIDIuODc2OTUgMTUuMTY1NiAyLjkyMjA0IDE1LjM5MSAzLjAxOTcyTDIyLjMxODkgNS44NTk5OUMyMy4wMjUyIDYuMTUzMDQgMjMuNDgzNiA2LjgyOTI5IDIzLjQ4MzYgNy41OTU3MVYxNi43MDI2QzIzLjQ4MzYgMTkuNjQwNiAyMi40MDE1IDIyLjAyMjUgMjAuMjYwMSAyMy43OTU4QzE4LjQxOTEgMjUuMzIxMSAxNi4yNjI2IDI2LjAzNDkgMTUuMTM1NiAyNi4zMjA1QzE1LjAwMDMgMjYuMzY1NiAxNC44NDI1IDI2LjM4MDYgMTQuNjg0NyAyNi4zODA2WiIgZmlsbD0iI0QwQTI1RiIvPgo8cGF0aCBkPSJNMTQuNjg1NCAyMS43NzI2QzE4LjYyNzcgMjEuNzcyNiAyMS44MjM2IDE4LjU3NjcgMjEuODIzNiAxNC42MzQzQzIxLjgyMzYgMTAuNjkyIDE4LjYyNzcgNy40OTYwOSAxNC42ODU0IDcuNDk2MDlDMTAuNzQzIDcuNDk2MDkgNy41NDcxMiAxMC42OTIgNy41NDcxMiAxNC42MzQzQzcuNTQ3MTIgMTguNTc2NyAxMC43NDMgMjEuNzcyNiAxNC42ODU0IDIxLjc3MjZaIiBmaWxsPSIjRjZFQ0RGIi8+CjxwYXRoIGQ9Ik0xNS41ODcyIDEwLjk5MzlMMTYuNDEzNyAxMi41NzkzTDE4LjE3OTUgMTIuODc5OUMxOC45OTEgMTMuMDE1MiAxOS4zMTQxIDE0LjAwNyAxOC43MzU1IDE0LjYwMDZMMTcuNDg4MiAxNS44NzhMMTcuNzUxMiAxNy42NTEzQzE3Ljg3MTQgMTguNDYyOCAxNy4wMjk4IDE5LjA3ODkgMTYuMjkzNSAxOC43MTA3TDE0LjY4NTUgMTcuOTE0M0wxMy4wODUgMTguNzEwN0MxMi4zNDg2IDE5LjA3ODkgMTEuNTA3MSAxOC40NjI4IDExLjYyNzMgMTcuNjUxM0wxMS44OTAzIDE1Ljg3OEwxMC42NDMgMTQuNjAwNkMxMC4wNjQ0IDE0LjAxNDUgMTAuMzg3NSAxMy4wMjI3IDExLjE5OSAxMi44Nzk5TDEyLjk2NDggMTIuNTc5M0wxMy43OTEzIDEwLjk5MzlDMTQuMTY3IDEwLjI2NTEgMTUuMjAzOSAxMC4yNjUxIDE1LjU4NzIgMTAuOTkzOVoiIGZpbGw9IiNEMEEyNUYiLz4KPC9zdmc+Cg==" } }, { @@ -19,7 +19,7 @@ "description": "Silver certified Data Asset.", "style": { "color": "#ADADAD", - "iconURL": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAHqADAAQAAAABAAAAHgAAAADKQTcFAAAFFElEQVRIDcVWy0+bRxBfwDQ8hXEjgV1UDIQYzMtCQE0FKpVKQ1FRqnKpciHcOMIBhLgYpAalggP5C0LFKZxCOYUDLxEEBglTQAkHwEYCihSDTXg4DoXO74t3u99nE0gvHcn+Znd29rfz2Jll7H+iqP+C29XVZbu4uLBxXeJ9Op3O3dvb6+Jz131vDNzZ2VljsVgcWVlZNqPJpCdicXFxyv6BQIDt7++zA6/Xt729PbmysjLS19c3+DHwa4EBWFpa+vQru90MsJuQz+dj83Nz7oWFhZ6rDnAlcHd3t15vMDh+qKtrTU9Pvwle2BocYOT584GDg4Me2s8nL4iRB5wHaHFJyUR9ff1PN7WS68pfhCIvL8/u9/nqiouLn01OTga4PMxigObm5k782NAgkocvDrx7xzxuNzs5PmbR0dEsISGBnZ2dsRQKgffNGxYbG8vowHy5+CIH/hgZce3s7HzLLdcJaYiBe7+rrQ0DpaRhpycnjE4ukkqre3h4yFxLS8qh5APA8u/v3bP9PjjoIJ026Klc3d7e/rCpqelxUlKSak/KUmYyGuE2RtdGJZMH8fHxLDMzkyUnJ7O1tTWWlpYmxAAPBoP2L0ymqdnZWXe0kBBTXl7u0MYUoNb8fGYymeSlCr+5uckoccLmU1NTFR3oylRRUYHwPMWcsBjW/tzY+JDfTQj/XF5mBVYrw0ZaAuDAwAADuN1u14oZrNfFxLC/6H6npKQocnjr+O1bPXllSlhMsbuvtfbvi4uIoNgFrgTt7u4yxDYSwUunp6cMScnJQuGKiopyCOCMjIwaLsQX1hYXFclTKn5xcVGMZV5MhpiiwkK2TTeBUyjuNiVTyM22z2/fVpWl8/NzYS2uDDbHteC0t7fHWWE9n0BoysrKlCFc/p724oRQkmf1CjAVeXOapjoZDAa+lm1sbLDR0VEx1jJwN34yFRQUKHHG3PtgUBYpvOJqKgb6uFu3VMKjoyMxzsnJYdnZ2WJ8HVNbWytAsZZiqlJBsokYqySaAdzV0tLCsOHHCC6+yTq/3/8vsJx52JzcH4YB4IaGhrB5PkHFJ6JnPtN4E82DW+zya64E6m4k4tcokgx3OhKh1HLCvQYpwNQzUcBVbUtHwMhmmVA05M2RoXLBWV1dlZcrPHRkI0JXy8ctZm632yVroeZqS54MWlVVxegJxNra2sS1g1x7WHoMqDrWvNMJmClRMqkVekpsNlEyUd68dFrkI4o+J4/HwxobG1l1dbXSMJB44DlVVlZyVqnjSCR+VVGU8CP6TSwCMzw8vOU/OrqUfzMvX156vd7LTyXoQFfe69dHjy47Ojq2gCVcjYHT6WzGo02mIiqbr16/jtiF5HUyT/mi6ECX0/T0NKOXCCMDejAnXI0B+mT2nTup+VarXe67qK9IHBzAQHcV7o1EiO+Sy6W8UKzU1Tg55+fZ+Pg4hk8okR+DCevqnq2t7tmZmW++rqqyydWMvygQI9TxeHr2BENdh6xQMhfZazabVZm+TOvHxsaA5UpMTOwGA1LXsg9zrLW1VW80Gid+efDApg/10pDokz5jL14gfNABqHhvYULlakyA5ubmAoWFhc8WnM54qrP2FHKvfF8/rLr6H5k/NDTENqm5ED0h0Gb+yONaES3mQnzxMqGYOnLv3jXnWSzsS7hSUwKxDkm5Tjmwvr6u8HRgN5Xd5v7+/knItXQtMFfAAYi/T78ash4tVRGhR6P2hgiMC5l7FSBf+A/FoFXdoCYOlQAAAABJRU5ErkJggg==" + "iconURL": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAiIGhlaWdodD0iMzAiIHZpZXdCb3g9IjAgMCAzMCAzMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzLjI1NzIgMS4yODE3N0w2LjMyOTMyIDQuMTIyMDVDNC45MTY3IDQuNzAwNjIgNCA2LjA3NTY3IDQgNy42MDFWMTYuNzA3OUM0IDI0Ljg5ODEgMTEuMTQ1OCAyNy40ODI5IDEzLjc1MzEgMjguMTUxN0MxNC4zNjkzIDI4LjMwOTQgMTUuMDA3OSAyOC4zMDk0IDE1LjYyNDEgMjguMTUxN0MxOC4yMzE0IDI3LjQ4MjkgMjUuMzc3MiAyNC44OTgxIDI1LjM3NzIgMTYuNzA3OVY3LjYwMUMyNS4zNzcyIDYuMDc1NjcgMjQuNDUzIDQuNzAwNjIgMjMuMDQ3OSA0LjEyMjA1TDE2LjExMjUgMS4yODE3N0MxNS4xOTU4IDAuOTA2MDc2IDE0LjE3MzkgMC45MDYwNzYgMTMuMjU3MiAxLjI4MTc3WiIgZmlsbD0iI0Q5RDlEOSIvPgo8cGF0aCBkPSJNMTQuNjg0NyAyNi4zODA2QzE0LjUyNjkgMjYuMzgwNiAxNC4zNjkxIDI2LjM1OCAxNC4yMTg4IDI2LjMyMDVDMTMuMDk5MyAyNi4wMzQ5IDEwLjkzNTMgMjUuMzIxMSA5LjA5NDM0IDIzLjc5NThDNi45NjAzNyAyMi4wMjI1IDUuODcwODUgMTkuNjMzMSA1Ljg3MDg1IDE2LjcwMjZWNy41OTU3MUM1Ljg3MDg1IDYuODI5MjkgNi4zMjkyIDYuMTQ1NTIgNy4wMzU1MSA1Ljg1OTk5TDEzLjk2MzQgMy4wMTk3MkMxNC4xODg4IDIuOTI5NTUgMTQuNDI5MiAyLjg3Njk1IDE0LjY3NzIgMi44NzY5NUMxNC45MjUyIDIuODc2OTUgMTUuMTY1NiAyLjkyMjA0IDE1LjM5MSAzLjAxOTcyTDIyLjMxODkgNS44NTk5OUMyMy4wMjUyIDYuMTUzMDQgMjMuNDgzNiA2LjgyOTI5IDIzLjQ4MzYgNy41OTU3MVYxNi43MDI2QzIzLjQ4MzYgMTkuNjQwNiAyMi40MDE1IDIyLjAyMjUgMjAuMjYwMSAyMy43OTU4QzE4LjQxOTEgMjUuMzIxMSAxNi4yNjI2IDI2LjAzNDkgMTUuMTM1NiAyNi4zMjA1QzE1LjAwMDMgMjYuMzY1NiAxNC44NDI1IDI2LjM4MDYgMTQuNjg0NyAyNi4zODA2WiIgZmlsbD0iIzk3OTc5NyIvPgo8cGF0aCBkPSJNMTQuNjg1NCAyMS43NzI2QzE4LjYyNzcgMjEuNzcyNiAyMS44MjM2IDE4LjU3NjcgMjEuODIzNiAxNC42MzQzQzIxLjgyMzYgMTAuNjkyIDE4LjYyNzcgNy40OTYwOSAxNC42ODU0IDcuNDk2MDlDMTAuNzQzIDcuNDk2MDkgNy41NDcxMiAxMC42OTIgNy41NDcxMiAxNC42MzQzQzcuNTQ3MTIgMTguNTc2NyAxMC43NDMgMjEuNzcyNiAxNC42ODU0IDIxLjc3MjZaIiBmaWxsPSIjRTlFQ0VGIi8+CjxwYXRoIGQ9Ik0xNS41ODcyIDEwLjk5MzlMMTYuNDEzNyAxMi41NzkzTDE4LjE3OTUgMTIuODc5OUMxOC45OTEgMTMuMDE1MiAxOS4zMTQxIDE0LjAwNyAxOC43MzU1IDE0LjYwMDZMMTcuNDg4MiAxNS44NzhMMTcuNzUxMiAxNy42NTEzQzE3Ljg3MTQgMTguNDYyOCAxNy4wMjk4IDE5LjA3ODkgMTYuMjkzNSAxOC43MTA3TDE0LjY4NTUgMTcuOTE0M0wxMy4wODUgMTguNzEwN0MxMi4zNDg2IDE5LjA3ODkgMTEuNTA3MSAxOC40NjI4IDExLjYyNzMgMTcuNjUxM0wxMS44OTAzIDE1Ljg3OEwxMC42NDMgMTQuNjAwNkMxMC4wNjQ0IDE0LjAxNDUgMTAuMzg3NSAxMy4wMjI3IDExLjE5OSAxMi44Nzk5TDEyLjk2NDggMTIuNTc5M0wxMy43OTEzIDEwLjk5MzlDMTQuMTY3IDEwLjI2NTEgMTUuMjAzOSAxMC4yNjUxIDE1LjU4NzIgMTAuOTkzOVoiIGZpbGw9IiM5Nzk3OTciLz4KPC9zdmc+Cg==" } }, { @@ -27,7 +27,7 @@ "description": "Gold certified Data Asset.", "style": { "color": "#FFCE00", - "iconURL":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAHqADAAQAAAABAAAAHgAAAADKQTcFAAAETUlEQVRIDZ1WTW8cRRB9i8HGsUUmRrYiPscoJx9gcoEju5w4kQt38C/A/AJ7/4H9C2LuSEmk3D05wiWrHAgHS14MRA4OmzVJ7NjCLO91T+10z469Vkrq6equj9fVVd3TDbwCDW4io5maUZ9Mt7GMjk2M6xvjFExOsCYSrGKWgDPkpiiZKKSn7A/ZXqKPF8jxFHe4iM1CWtuNBXaA87iJBaQOzCz+o79B4VNzrxW85l6y7aPL1j5rAeamsCo7Aia4xAjfxwoj9BSClaoxJ4/aCS3giO03rHM3tAClY0i1wA50AVu4ym19g7raSotuaDqGkWfZyPYRc99DKwQfAXagcwT9kKCSytBI/HO2qSYwzdqa/QQ4YSATB0AvZ57Z5iivkux+J/hBCf56Vcdt73s1oEcJkHwHfLwCTJIfoVUuqgtsrwN/b8QL0Na/Q5+HTB3wvUyjiBntt1hiIU1T8q/EBb1IgaVbdJbZzPm9FtBpATPsQ9rj4LGLOrda9OJ5ruhNsuH2CjTbqgfdy32U3rr8zhY2sg1pnoMJBkYaRlwbbY8an+0wl6l0Y1JUPy76BX11P5bZqNcBfmXk032bAf4k+wStMuIruDES7dsr9aBys3vHO5NzLaKOlJqEPsIdvOwUV0vgGTRd/IPCg6K9xmI6i7Y3S8n2DyVf5eRDJ8FI9cPkuarmNmeMNonO6qVmGa2OjJyrN1KkRru3jfO9UnPtG8/rBEw3yed+rAqfRGLHKeUxii+JuaZX1HfvHvDzSjmuclpEuBDJP7hRHrupjL7zyMq2OhlecyY+TYwDrn7O1izH47hstQSV7mQaW0yWV3ssqI60XV9uMTN0eB5pi53e2nlaTFkIrMprBPon3WBQsNka8On66LzNfHGrfmdOu6bh+wC4g2POhcDHnVjZRtVCsnn1qoU6ehb40h+L5HLMv0aHP/B+BHyUx1UsbZ1X3VZGSoGaUd2iZCNfRv5o9a24dNY60XGapeb2hqn7PgRd4vn8egfQrWU3m+ThkZPVL+34h/HEubpXAvfRdi8HnTOR+r/W4mOim0hNuVSuFa1AtQAVnio/3AFF+2wTQ+qRY35Jt8OsYnAXO3gXaRT58xS4vlVG5ewu8BHo/Rbt2Bs9JHPiHoWLZcQS7mPZPVdMUb0M5UCOLkq6TKqgj2nso23LTQTMIsvxBzaiS11aAv9pkbfX8vkLUH6V0wfX40iVV/2LgQ1ibIpp6BOSe/rovbXA+9vyHSooT+4ez1gHqZfozJ8wysOcgJwK7Z5yvMvGpwHb8N01AiwNBz5F8I8IzuvtlekRLfeddQSqmWirnQo/7jV4jBYectvL3Jh4fK+zqkLyoDqTw0jNuDZiE6p3L5MJPokuI8VbnKhupSnrRvqH7YDN305dcsuubshUaSywGbgFNPhKGaDJHCbDFOiO99UqVd1+uojaZwGavwsDm4F6LiJjl7IlbEYdgimXF6L/Aeq1S9qHbqjuAAAAAElFTkSuQmCC" + "iconURL":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAiIGhlaWdodD0iMzAiIHZpZXdCb3g9IjAgMCAzMCAzMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzLjI1NzIgMS4yODE3N0w2LjMyOTMyIDQuMTIyMDVDNC45MTY3IDQuNzAwNjIgNCA2LjA3NTY3IDQgNy42MDFWMTYuNzA3OUM0IDI0Ljg5ODEgMTEuMTQ1OCAyNy40ODI5IDEzLjc1MzEgMjguMTUxN0MxNC4zNjkzIDI4LjMwOTQgMTUuMDA3OSAyOC4zMDk0IDE1LjYyNDEgMjguMTUxN0MxOC4yMzE0IDI3LjQ4MjkgMjUuMzc3MiAyNC44OTgxIDI1LjM3NzIgMTYuNzA3OVY3LjYwMUMyNS4zNzcyIDYuMDc1NjcgMjQuNDUzIDQuNzAwNjIgMjMuMDQ3OSA0LjEyMjA1TDE2LjExMjUgMS4yODE3N0MxNS4xOTU4IDAuOTA2MDc2IDE0LjE3MzkgMC45MDYwNzYgMTMuMjU3MiAxLjI4MTc3WiIgZmlsbD0iI0ZGREY0MyIvPgo8cGF0aCBkPSJNMTQuNjg0NyAyNi4zODA2QzE0LjUyNjkgMjYuMzgwNiAxNC4zNjkxIDI2LjM1OCAxNC4yMTg4IDI2LjMyMDVDMTMuMDk5MyAyNi4wMzQ5IDEwLjkzNTMgMjUuMzIxMSA5LjA5NDM0IDIzLjc5NThDNi45NjAzNyAyMi4wMjI1IDUuODcwODUgMTkuNjMzMSA1Ljg3MDg1IDE2LjcwMjZWNy41OTU3MUM1Ljg3MDg1IDYuODI5MjkgNi4zMjkyIDYuMTQ1NTIgNy4wMzU1MSA1Ljg1OTk5TDEzLjk2MzQgMy4wMTk3MkMxNC4xODg4IDIuOTI5NTUgMTQuNDI5MiAyLjg3Njk1IDE0LjY3NzIgMi44NzY5NUMxNC45MjUyIDIuODc2OTUgMTUuMTY1NiAyLjkyMjA0IDE1LjM5MSAzLjAxOTcyTDIyLjMxODkgNS44NTk5OUMyMy4wMjUyIDYuMTUzMDQgMjMuNDgzNiA2LjgyOTI5IDIzLjQ4MzYgNy41OTU3MVYxNi43MDI2QzIzLjQ4MzYgMTkuNjQwNiAyMi40MDE1IDIyLjAyMjUgMjAuMjYwMSAyMy43OTU4QzE4LjQxOTEgMjUuMzIxMSAxNi4yNjI2IDI2LjAzNDkgMTUuMTM1NiAyNi4zMjA1QzE1LjAwMDMgMjYuMzY1NiAxNC44NDI1IDI2LjM4MDYgMTQuNjg0NyAyNi4zODA2WiIgZmlsbD0iI0ZGOUYwMCIvPgo8cGF0aCBkPSJNMTQuNjg1NCAyMS43NzI2QzE4LjYyNzcgMjEuNzcyNiAyMS44MjM2IDE4LjU3NjcgMjEuODIzNiAxNC42MzQzQzIxLjgyMzYgMTAuNjkyIDE4LjYyNzcgNy40OTYwOSAxNC42ODU0IDcuNDk2MDlDMTAuNzQzIDcuNDk2MDkgNy41NDcxMiAxMC42OTIgNy41NDcxMiAxNC42MzQzQzcuNTQ3MTIgMTguNTc2NyAxMC43NDMgMjEuNzcyNiAxNC42ODU0IDIxLjc3MjZaIiBmaWxsPSIjRkZGMEM1Ii8+CjxwYXRoIGQ9Ik0xNS41ODcyIDEwLjk5MzlMMTYuNDEzNyAxMi41NzkzTDE4LjE3OTUgMTIuODc5OUMxOC45OTEgMTMuMDE1MiAxOS4zMTQxIDE0LjAwNyAxOC43MzU1IDE0LjYwMDZMMTcuNDg4MiAxNS44NzhMMTcuNzUxMiAxNy42NTEzQzE3Ljg3MTQgMTguNDYyOCAxNy4wMjk4IDE5LjA3ODkgMTYuMjkzNSAxOC43MTA3TDE0LjY4NTUgMTcuOTE0M0wxMy4wODUgMTguNzEwN0MxMi4zNDg2IDE5LjA3ODkgMTEuNTA3MSAxOC40NjI4IDExLjYyNzMgMTcuNjUxM0wxMS44OTAzIDE1Ljg3OEwxMC42NDMgMTQuNjAwNkMxMC4wNjQ0IDE0LjAxNDUgMTAuMzg3NSAxMy4wMjI3IDExLjE5OSAxMi44Nzk5TDEyLjk2NDggMTIuNTc5M0wxMy43OTEzIDEwLjk5MzlDMTQuMTY3IDEwLjI2NTEgMTUuMjAzOSAxMC4yNjUxIDE1LjU4NzIgMTAuOTkzOVoiIGZpbGw9IiNGRjlGMDAiLz4KPC9zdmc+Cg==" } } ] diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CertificationTag/CertificationTag.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CertificationTag/CertificationTag.tsx index 7320089bad58..7f6030aaadee 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CertificationTag/CertificationTag.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CertificationTag/CertificationTag.tsx @@ -10,10 +10,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Tag } from 'antd'; +import { Tag, Tooltip } from 'antd'; import React from 'react'; import { AssetCertification } from '../../../generated/entity/data/table'; import { getEntityName } from '../../../utils/EntityUtils'; +import { getTagTooltip } from '../../../utils/TagsUtils'; import './certification-tag.less'; const CertificationTag = ({ @@ -21,19 +22,34 @@ const CertificationTag = ({ }: { certification: AssetCertification; }) => { + if (certification.tagLabel.style?.iconURL) { + const name = getEntityName(certification.tagLabel); + + return ( + +
+ certification +
+
+ ); + } + return ( - {certification.tagLabel.style?.iconURL && ( - certification - )} - {getEntityName(certification.tagLabel)} ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget.tsx index 5a5b0390303c..c5c80caef1f9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget.tsx @@ -12,7 +12,16 @@ */ import { InfoCircleOutlined } from '@ant-design/icons'; import { WidgetProps } from '@rjsf/utils'; -import { Alert, Button, Card, Col, Row, Skeleton, Typography } from 'antd'; +import { + Alert, + Button, + Card, + Col, + Divider, + Row, + Skeleton, + Typography, +} from 'antd'; import classNames from 'classnames'; import { t } from 'i18next'; import { debounce, isEmpty, isUndefined } from 'lodash'; @@ -29,6 +38,10 @@ import { import { getExplorePath } from '../../../../../../constants/constants'; import { EntityType } from '../../../../../../enums/entity.enum'; import { SearchIndex } from '../../../../../../enums/search.enum'; +import { + EsBoolQuery, + QueryFieldInterface, +} from '../../../../../../pages/ExplorePage/ExplorePage.interface'; import { searchQuery } from '../../../../../../rest/searchAPI'; import { elasticSearchFormat, @@ -124,6 +137,22 @@ const QueryBuilderWidget: FC = ({ query: data, }; if (data) { + if (entityType !== EntityType.ALL) { + // Scope the search to the passed entity type + if ( + Array.isArray( + ((qFilter.query as QueryFieldInterface)?.bool as EsBoolQuery) + ?.must + ) + ) { + ( + (qFilter.query as QueryFieldInterface)?.bool + ?.must as QueryFieldInterface[] + )?.push({ + term: { entityType: entityType }, + }); + } + } debouncedFetchEntityCount(qFilter); } @@ -208,7 +237,19 @@ const QueryBuilderWidget: FC = ({ data-testid="query-builder-form-field"> - + + {outputType === SearchOutputType.JSONLogic && ( + <> + + {props.label} + + + + )} ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/query-builder-widget.less b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/query-builder-widget.less index dc74a8ff21f2..f5081f8baf1b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/query-builder-widget.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/query-builder-widget.less @@ -26,6 +26,29 @@ } } +.query-builder-form-field + .query-builder-card.jsonlogic + .query-builder-container { + .group-or-rule-container.group-container { + & > .group.group-or-rule { + & > .group--header { + order: 0; + + .action.action--ADD-RULE { + position: absolute !important; + margin-top: 0; + right: 0; + top: -48px; + } + } + } + } +} + +.query-filter-label { + text-transform: capitalize; +} + .query-builder-form-field { .hide--line.one--child { margin-top: 0; @@ -176,6 +199,10 @@ } } } + + .rule { + align-items: center; + } } .query-builder-card.elasticsearch { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx index 7a62d40fac64..0c0bc910b742 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx @@ -223,7 +223,7 @@ const AppInstall = () => { ); case 3: return ( -
+
{t('label.schedule')} { EntityFields.TIER, 'extension', 'descriptionStatus', + 'entityType', ]); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.ts index 3fc279330535..05da5daaffd5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.ts @@ -610,6 +610,19 @@ class AdvancedSearchClassBase { }, }, }, + [EntityFields.ENTITY_TYPE]: { + label: t('label.entity-type-plural', { entity: t('label.entity') }), + type: 'select', + mainWidgetProps: this.mainWidgetProps, + + fieldSettings: { + asyncFetch: this.autocomplete({ + searchIndex: entitySearchIndex, + entityField: EntityFields.ENTITY_TYPE, + }), + useAsyncSearch: true, + }, + }, }; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/JSONLogicSearchClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/JSONLogicSearchClassBase.ts index 25bc69ed366a..625dd967e5a1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/JSONLogicSearchClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/JSONLogicSearchClassBase.ts @@ -29,7 +29,7 @@ import { import { SearchIndex } from '../enums/search.enum'; import { searchData } from '../rest/miscAPI'; import advancedSearchClassBase from './AdvancedSearchClassBase'; -import { renderQueryBuilderFilterButtons } from './QueryBuilderUtils'; +import { renderJSONLogicQueryBuilderButtons } from './QueryBuilderUtils'; class JSONLogicSearchClassBase { baseConfig = AntdConfig as Config; @@ -370,7 +370,7 @@ class JSONLogicSearchClassBase { operatorLabel: t('label.condition') + ':', showNot: false, valueLabel: t('label.criteria') + ':', - renderButton: renderQueryBuilderFilterButtons, + renderButton: renderJSONLogicQueryBuilderButtons, }, }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/QueryBuilderUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/QueryBuilderUtils.tsx index de07b53e9149..e3952c1e4fa6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/QueryBuilderUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/QueryBuilderUtils.tsx @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { CloseOutlined } from '@ant-design/icons'; +import { CloseOutlined, PlusOutlined } from '@ant-design/icons'; import { Button } from 'antd'; import { t } from 'i18next'; import { isUndefined } from 'lodash'; @@ -404,6 +404,43 @@ export const renderQueryBuilderFilterButtons: RenderSettings['renderButton'] = ( return <>; }; +export const renderJSONLogicQueryBuilderButtons: RenderSettings['renderButton'] = + (props) => { + const type = props?.type; + + if (type === 'delRule') { + return ( + + {!isCertificationClassification && ( + + )} {manageButtonContent.length > 0 && ( { setAssetModalVisible(false)} onSave={handleAssetSave} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PermissionsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PermissionsUtils.ts index b9c3a7661fa7..463706988fe7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PermissionsUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PermissionsUtils.ts @@ -52,6 +52,31 @@ export const checkPermission = ( return hasPermission; }; +/** + * + * @param operation operation like Edit, Delete + * @param resourceType Resource type like "bot", "table" + * @param permissions UIPermission + * @param checkEditAllPermission boolean to check EditALL permission as well + * @returns boolean - true/false + */ +export const checkPermissionEntityResource = ( + operation: Operation, + resourceType: ResourceEntity, + permissions: UIPermission, + checkEditAllPermission = false +) => { + const entityResource = permissions?.[resourceType]; + let hasPermission = entityResource && entityResource[operation]; + + if (checkEditAllPermission) { + hasPermission = + hasPermission || (entityResource && entityResource[Operation.EditAll]); + } + + return hasPermission; +}; + /** * * @param permission ResourcePermission diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TagsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TagsUtils.tsx index 93dc8a9790ce..aaa80afaef1a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TagsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TagsUtils.tsx @@ -24,6 +24,10 @@ import Loader from '../components/common/Loader/Loader'; import RichTextEditorPreviewer from '../components/common/RichTextEditor/RichTextEditorPreviewer'; import { FQN_SEPARATOR_CHAR } from '../constants/char.constants'; import { getExplorePath } from '../constants/constants'; +import { + ResourceEntity, + UIPermission, +} from '../context/PermissionProvider/PermissionProvider.interface'; import { SettledStatus } from '../enums/Axios.enum'; import { EntityType } from '../enums/entity.enum'; import { ExplorePageTabs } from '../enums/Explore.enum'; @@ -32,6 +36,7 @@ import { Classification } from '../generated/entity/classification/classificatio import { Tag } from '../generated/entity/classification/tag'; import { GlossaryTerm } from '../generated/entity/data/glossaryTerm'; import { Column } from '../generated/entity/data/table'; +import { Operation } from '../generated/entity/policies/policy'; import { Paging } from '../generated/type/paging'; import { LabelType, State, TagLabel } from '../generated/type/tagLabel'; import { searchQuery } from '../rest/searchAPI'; @@ -41,6 +46,7 @@ import { getTags, } from '../rest/tagAPI'; import { getQueryFilterToIncludeApprovedTerm } from './GlossaryUtils'; +import { checkPermissionEntityResource } from './PermissionsUtils'; import { getTagsWithoutTier } from './TableUtils'; export const getClassifications = async ( @@ -318,7 +324,10 @@ export const createTagObject = (tags: EntityTags[]) => { ); }; -export const getQueryFilterToExcludeTerms = (fqn: string) => ({ +export const getQueryFilterToExcludeTermsAndEntities = ( + fqn: string, + excludeEntityIndex: string[] = [] +) => ({ query: { bool: { must: [ @@ -337,13 +346,17 @@ export const getQueryFilterToExcludeTerms = (fqn: string) => ({ bool: { must_not: [ { - term: { - entityType: EntityType.TAG, - }, - }, - { - term: { - entityType: EntityType.DATA_PRODUCT, + terms: { + entityType: [ + EntityType.CLASSIFICATION, + EntityType.TEST_SUITE, + EntityType.TEST_CASE, + EntityType.TEST_CASE_RESOLUTION_STATUS, + EntityType.TEST_CASE_RESULT, + EntityType.TAG, + EntityType.DATA_PRODUCT, + ...excludeEntityIndex, + ], }, }, ], @@ -354,6 +367,185 @@ export const getQueryFilterToExcludeTerms = (fqn: string) => ({ }, }); +export const getExcludedIndexesBasedOnEntityTypeEditTagPermission = ( + permissions: UIPermission +) => { + const entityPermission = { + [EntityType.TABLE]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.TABLE, + permissions, + true + ), + [EntityType.TOPIC]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.TOPIC, + permissions, + true + ), + [EntityType.DASHBOARD]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.DASHBOARD, + permissions, + true + ), + [EntityType.MLMODEL]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.ML_MODEL, + permissions, + true + ), + [EntityType.PIPELINE]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.PIPELINE, + permissions, + true + ), + [EntityType.CONTAINER]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.CONTAINER, + permissions, + true + ), + [EntityType.SEARCH_INDEX]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.SEARCH_INDEX, + permissions, + true + ), + [EntityType.API_SERVICE]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.API_SERVICE, + permissions, + true + ), + [EntityType.API_ENDPOINT]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.API_ENDPOINT, + permissions, + true + ), + [EntityType.API_COLLECTION]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.API_COLLECTION, + permissions, + true + ), + [EntityType.DASHBOARD_DATA_MODEL]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.DASHBOARD_DATA_MODEL, + permissions, + true + ), + [EntityType.STORED_PROCEDURE]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.STORED_PROCEDURE, + permissions, + true + ), + [EntityType.DATABASE]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.DATABASE, + permissions, + true + ), + [EntityType.DATABASE_SERVICE]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.DATABASE_SERVICE, + permissions, + true + ), + [EntityType.DATABASE_SCHEMA]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.DATABASE_SCHEMA, + permissions, + true + ), + [EntityType.MESSAGING_SERVICE]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.PIPELINE_SERVICE, + permissions, + true + ), + [EntityType.DASHBOARD_SERVICE]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.DASHBOARD_SERVICE, + permissions, + true + ), + [EntityType.MLMODEL_SERVICE]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.ML_MODEL_SERVICE, + permissions, + true + ), + [EntityType.PIPELINE_SERVICE]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.PIPELINE_SERVICE, + permissions, + true + ), + [EntityType.STORAGE_SERVICE]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.STORAGE_SERVICE, + permissions, + true + ), + [EntityType.SEARCH_SERVICE]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.SEARCH_SERVICE, + permissions, + true + ), + [EntityType.GLOSSARY]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.GLOSSARY, + permissions, + true + ), + [EntityType.GLOSSARY_TERM]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.GLOSSARY_TERM, + permissions, + true + ), + [EntityType.DOMAIN]: checkPermissionEntityResource( + Operation.EditTags, + ResourceEntity.DOMAIN, + permissions, + true + ), + }; + + return (Object.keys(entityPermission) as EntityType[]).reduce( + ( + acc: { + entitiesHavingPermission: EntityType[]; + entitiesNotHavingPermission: EntityType[]; + }, + cv: EntityType + ) => { + const currentEntityPermission = + entityPermission[cv as keyof typeof entityPermission]; + if (currentEntityPermission) { + return { + ...acc, + entitiesHavingPermission: [...acc.entitiesHavingPermission, cv], + }; + } + + return { + ...acc, + entitiesNotHavingPermission: [...acc.entitiesNotHavingPermission, cv], + }; + }, + { + entitiesHavingPermission: [], + entitiesNotHavingPermission: [], + } + ); +}; + export const getTagAssetsQueryFilter = (fqn: string) => { if (fqn.includes('Tier.')) { return `(tier.tagFQN:"${fqn}")`; From e22fc6ddeb2d57135a4b901a77c23f0f9b1e9867 Mon Sep 17 00:00:00 2001 From: Karan Hotchandani <33024356+karanh37@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:36:45 +0530 Subject: [PATCH 10/10] Fix Glossary tree hierarchy state (#18703) * fix the tree state on edit glossary term * add playwright tests * fix permission loading * fix tests * adding timeout for in review state * adding timeout for in review state * increasing timeout * fix tests * update tests * add a poll for checking tasks created * testing response * adding limit * fix tests * change user for testing * Catch Exceptions on the Consumer to avoid losing records --------- Co-authored-by: Pablo Takara --- .../workflows/WorkflowEventConsumer.java | 47 ++-- .../ui/playwright/e2e/Pages/Glossary.spec.ts | 263 +++++++++++------- .../resources/ui/playwright/utils/glossary.ts | 85 +++++- .../GlossaryDetails.component.tsx | 2 +- .../GlossaryTermTab.component.tsx | 14 +- .../Glossary/GlossaryV1.component.tsx | 52 +++- .../resources/ui/src/utils/GlossaryUtils.tsx | 24 ++ 7 files changed, 338 insertions(+), 149 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/governance/workflows/WorkflowEventConsumer.java b/openmetadata-service/src/main/java/org/openmetadata/service/governance/workflows/WorkflowEventConsumer.java index 8277275d7014..7cbc974a4d25 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/governance/workflows/WorkflowEventConsumer.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/governance/workflows/WorkflowEventConsumer.java @@ -1,9 +1,12 @@ package org.openmetadata.service.governance.workflows; +import static org.openmetadata.schema.entity.events.SubscriptionDestination.SubscriptionType.GOVERNANCE_WORKFLOW_CHANGE_EVENT; + import java.util.HashMap; import java.util.List; import java.util.Map; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; import org.openmetadata.schema.entity.events.EventSubscription; import org.openmetadata.schema.entity.events.SubscriptionDestination; import org.openmetadata.schema.type.ChangeEvent; @@ -13,6 +16,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.apps.bundles.changeEvent.Destination; import org.openmetadata.service.events.errors.EventPublisherException; +import org.openmetadata.service.exception.CatalogExceptionMessage; import org.openmetadata.service.resources.feeds.MessageParser; @Slf4j @@ -46,22 +50,33 @@ public WorkflowEventConsumer( @Override public void sendMessage(ChangeEvent event) throws EventPublisherException { // NOTE: We are only consuming ENTITY related events. - EventType eventType = event.getEventType(); - - if (validEventTypes.contains(eventType)) { - String signal = String.format("%s-%s", event.getEntityType(), eventType.toString()); - - EntityReference entityReference = - Entity.getEntityReferenceById(event.getEntityType(), event.getEntityId(), Include.ALL); - MessageParser.EntityLink entityLink = - new MessageParser.EntityLink( - event.getEntityType(), entityReference.getFullyQualifiedName()); - - Map variables = new HashMap<>(); - - variables.put("relatedEntity", entityLink.getLinkString()); - - WorkflowHandler.getInstance().triggerWithSignal(signal, variables); + try { + EventType eventType = event.getEventType(); + + if (validEventTypes.contains(eventType)) { + String signal = String.format("%s-%s", event.getEntityType(), eventType.toString()); + + EntityReference entityReference = + Entity.getEntityReferenceById(event.getEntityType(), event.getEntityId(), Include.ALL); + MessageParser.EntityLink entityLink = + new MessageParser.EntityLink( + event.getEntityType(), entityReference.getFullyQualifiedName()); + + Map variables = new HashMap<>(); + + variables.put("relatedEntity", entityLink.getLinkString()); + + WorkflowHandler.getInstance().triggerWithSignal(signal, variables); + } + } catch (Exception exc) { + String message = + CatalogExceptionMessage.eventPublisherFailedToPublish( + GOVERNANCE_WORKFLOW_CHANGE_EVENT, event, exc.getMessage()); + LOG.error(message); + throw new EventPublisherException( + CatalogExceptionMessage.eventPublisherFailedToPublish( + GOVERNANCE_WORKFLOW_CHANGE_EVENT, exc.getMessage()), + Pair.of(subscriptionDestination.getId(), event)); } } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index f5c5fe15000f..09ba8d8caac0 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -61,11 +61,13 @@ import { selectActiveGlossaryTerm, selectColumns, toggleAllColumnsSelection, + updateGlossaryTermDataFromTree, validateGlossaryTerm, verifyAllColumns, verifyColumnsVisibility, verifyGlossaryDetails, verifyGlossaryTermAssets, + verifyTaskCreated, } from '../../utils/glossary'; import { sidebarClick } from '../../utils/sidebar'; import { TaskDetails } from '../../utils/task'; @@ -74,131 +76,133 @@ import { performUserLogin } from '../../utils/user'; const user1 = new UserClass(); const user2 = new UserClass(); const team = new TeamClass(); +const user3 = new UserClass(); test.describe('Glossary tests', () => { test.beforeAll(async ({ browser }) => { const { afterAction, apiContext } = await performAdminLogin(browser); await user2.create(apiContext); await user1.create(apiContext); + await user3.create(apiContext); team.data.users = [user2.responseData.id]; await team.create(apiContext); await afterAction(); }); - test.fixme( - 'Glossary & terms creation for reviewer as user', - async ({ browser }) => { - test.slow(true); + test('Glossary & terms creation for reviewer as user', async ({ + browser, + }) => { + test.slow(true); - const { page, afterAction, apiContext } = await performAdminLogin( - browser - ); - const { page: page1, afterAction: afterActionUser1 } = - await performUserLogin(browser, user1); - const glossary1 = new Glossary(); - glossary1.data.owners = [{ name: 'admin', type: 'user' }]; - glossary1.data.mutuallyExclusive = true; - glossary1.data.reviewers = [ - { name: `${user1.data.firstName}${user1.data.lastName}`, type: 'user' }, - ]; - glossary1.data.terms = [new GlossaryTerm(glossary1)]; - - await test.step('Create Glossary', async () => { - await sidebarClick(page, SidebarItem.GLOSSARY); - await createGlossary(page, glossary1.data, false); - await verifyGlossaryDetails(page, glossary1.data); - }); + const { page, afterAction, apiContext } = await performAdminLogin(browser); + const { page: page1, afterAction: afterActionUser1 } = + await performUserLogin(browser, user3); + const glossary1 = new Glossary(); + glossary1.data.owners = [{ name: 'admin', type: 'user' }]; + glossary1.data.mutuallyExclusive = true; + glossary1.data.reviewers = [ + { name: `${user3.data.firstName}${user3.data.lastName}`, type: 'user' }, + ]; + glossary1.data.terms = [new GlossaryTerm(glossary1)]; - await test.step('Create Glossary Terms', async () => { - await sidebarClick(page, SidebarItem.GLOSSARY); - await createGlossaryTerms(page, glossary1.data); - }); + await test.step('Create Glossary', async () => { + await sidebarClick(page, SidebarItem.GLOSSARY); + await createGlossary(page, glossary1.data, false); + await verifyGlossaryDetails(page, glossary1.data); + }); - await test.step( - 'Approve Glossary Term from Glossary Listing for reviewer user', - async () => { - await redirectToHomePage(page1); - // wait for 15 seconds as the flowable which creates task is triggered every 10 seconds - await page1.waitForTimeout(15000); - await sidebarClick(page1, SidebarItem.GLOSSARY); - await selectActiveGlossary(page1, glossary1.data.name); - - await approveGlossaryTermTask(page1, glossary1.data.terms[0].data); - await redirectToHomePage(page1); - await sidebarClick(page1, SidebarItem.GLOSSARY); - await selectActiveGlossary(page1, glossary1.data.name); - await validateGlossaryTerm( - page1, - glossary1.data.terms[0].data, - 'Approved' - ); + await test.step('Create Glossary Terms', async () => { + await sidebarClick(page, SidebarItem.GLOSSARY); + await createGlossaryTerms(page, glossary1.data); + }); + + await test.step( + 'Approve Glossary Term from Glossary Listing for reviewer user', + async () => { + await redirectToHomePage(page1); + await sidebarClick(page1, SidebarItem.GLOSSARY); + await selectActiveGlossary(page1, glossary1.data.name); + await verifyTaskCreated( + page1, + glossary1.data.fullyQualifiedName, + glossary1.data.terms[0].data + ); - await afterActionUser1(); - } - ); + await approveGlossaryTermTask(page1, glossary1.data.terms[0].data); + await redirectToHomePage(page1); + await sidebarClick(page1, SidebarItem.GLOSSARY); + await selectActiveGlossary(page1, glossary1.data.name); + await validateGlossaryTerm( + page1, + glossary1.data.terms[0].data, + 'Approved' + ); - await glossary1.delete(apiContext); - await afterAction(); - } - ); + await afterActionUser1(); + } + ); - test.fixme( - 'Glossary & terms creation for reviewer as team', - async ({ browser }) => { - test.slow(true); + await glossary1.delete(apiContext); + await afterAction(); + }); - const { page, afterAction, apiContext } = await performAdminLogin( - browser - ); - const { page: page1, afterAction: afterActionUser1 } = - await performUserLogin(browser, user2); + test('Glossary & terms creation for reviewer as team', async ({ + browser, + }) => { + test.slow(true); - const glossary2 = new Glossary(); - glossary2.data.owners = [{ name: 'admin', type: 'user' }]; - glossary2.data.reviewers = [ - { name: team.data.displayName, type: 'team' }, - ]; - glossary2.data.terms = [new GlossaryTerm(glossary2)]; + const { page, afterAction, apiContext } = await performAdminLogin(browser); + const { page: page1, afterAction: afterActionUser1 } = + await performUserLogin(browser, user2); - await test.step('Create Glossary', async () => { - await sidebarClick(page, SidebarItem.GLOSSARY); - await createGlossary(page, glossary2.data, false); - await verifyGlossaryDetails(page, glossary2.data); - }); + const glossary2 = new Glossary(); + glossary2.data.owners = [{ name: 'admin', type: 'user' }]; + glossary2.data.reviewers = [{ name: team.data.displayName, type: 'team' }]; + glossary2.data.terms = [new GlossaryTerm(glossary2)]; - await test.step('Create Glossary Terms', async () => { - await redirectToHomePage(page); - await sidebarClick(page, SidebarItem.GLOSSARY); - await createGlossaryTerms(page, glossary2.data); - }); + await test.step('Create Glossary', async () => { + await sidebarClick(page, SidebarItem.GLOSSARY); + await createGlossary(page, glossary2.data, false); + await verifyGlossaryDetails(page, glossary2.data); + }); - await test.step( - 'Approve Glossary Term from Glossary Listing for reviewer team', - async () => { - await redirectToHomePage(page1); - // wait for 15 seconds as the flowable which creates task is triggered every 10 seconds - await page1.waitForTimeout(15000); - await sidebarClick(page1, SidebarItem.GLOSSARY); - await selectActiveGlossary(page1, glossary2.data.name); - await approveGlossaryTermTask(page1, glossary2.data.terms[0].data); - - await redirectToHomePage(page1); - await sidebarClick(page1, SidebarItem.GLOSSARY); - await selectActiveGlossary(page1, glossary2.data.name); - await validateGlossaryTerm( - page1, - glossary2.data.terms[0].data, - 'Approved' - ); + await test.step('Create Glossary Terms', async () => { + await redirectToHomePage(page); + await sidebarClick(page, SidebarItem.GLOSSARY); + await createGlossaryTerms(page, glossary2.data); + }); + + await test.step( + 'Approve Glossary Term from Glossary Listing for reviewer team', + async () => { + await redirectToHomePage(page1); + await sidebarClick(page1, SidebarItem.GLOSSARY); + await selectActiveGlossary(page1, glossary2.data.name); + + await verifyTaskCreated( + page1, + glossary2.data.fullyQualifiedName, + glossary2.data.terms[0].data + ); - await afterActionUser1(); - } - ); + await approveGlossaryTermTask(page1, glossary2.data.terms[0].data); - await glossary2.delete(apiContext); - await afterAction(); - } - ); + await redirectToHomePage(page1); + await sidebarClick(page1, SidebarItem.GLOSSARY); + await selectActiveGlossary(page1, glossary2.data.name); + await validateGlossaryTerm( + page1, + glossary2.data.terms[0].data, + 'Approved' + ); + + await afterActionUser1(); + } + ); + + await glossary2.delete(apiContext); + await afterAction(); + }); test('Update Glossary and Glossary Term', async ({ browser }) => { test.slow(true); @@ -789,9 +793,8 @@ test.describe('Glossary tests', () => { '[data-testid="viewer-container"]' ); - await expect(viewerContainerText).toContain('Updated description'); + expect(viewerContainerText).toContain('Updated description'); } finally { - await user1.delete(apiContext); await glossary1.delete(apiContext); await afterAction(); } @@ -836,9 +839,8 @@ test.describe('Glossary tests', () => { '[data-testid="viewer-container"]' ); - await expect(viewerContainerText).toContain('Updated description'); + expect(viewerContainerText).toContain('Updated description'); } finally { - await user1.delete(apiContext); await glossaryTerm1.delete(apiContext); await glossary1.delete(apiContext); await afterAction(); @@ -870,6 +872,7 @@ test.describe('Glossary tests', () => { } finally { await glossary1.delete(apiContext); await afterAction(); + await afterActionUser1(); } }); @@ -1063,10 +1066,60 @@ test.describe('Glossary tests', () => { } }); + test('Glossary Term Update in Glossary Page should persist tree', async ({ + browser, + }) => { + const { page, afterAction, apiContext } = await performAdminLogin(browser); + const glossary1 = new Glossary(); + const glossaryTerm1 = new GlossaryTerm(glossary1); + await glossary1.create(apiContext); + await glossaryTerm1.create(apiContext); + const glossaryTerm2 = new GlossaryTerm( + glossary1, + glossaryTerm1.responseData.fullyQualifiedName + ); + await glossaryTerm2.create(apiContext); + const glossaryTerm3 = new GlossaryTerm( + glossary1, + glossaryTerm2.responseData.fullyQualifiedName + ); + await glossaryTerm3.create(apiContext); + + try { + await sidebarClick(page, SidebarItem.GLOSSARY); + await selectActiveGlossary(page, glossary1.data.displayName); + await page.getByTestId('expand-collapse-all-button').click(); + + await expect( + page.getByRole('cell', { name: glossaryTerm1.data.displayName }) + ).toBeVisible(); + + await expect( + page.getByRole('cell', { name: glossaryTerm2.data.displayName }) + ).toBeVisible(); + + await expect( + page.getByRole('cell', { name: glossaryTerm3.data.displayName }) + ).toBeVisible(); + + await updateGlossaryTermDataFromTree( + page, + glossaryTerm2.responseData.fullyQualifiedName + ); + } finally { + await glossaryTerm3.delete(apiContext); + await glossaryTerm2.delete(apiContext); + await glossaryTerm1.delete(apiContext); + await glossary1.delete(apiContext); + await afterAction(); + } + }); + test.afterAll(async ({ browser }) => { const { afterAction, apiContext } = await performAdminLogin(browser); await user1.delete(apiContext); await user2.delete(apiContext); + await user3.create(apiContext); await team.delete(apiContext); await afterAction(); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts index 22cba32b692d..5505efe77efa 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts @@ -37,6 +37,12 @@ import { addMultiOwner } from './entity'; import { sidebarClick } from './sidebar'; import { TaskDetails, TASK_OPEN_FETCH_LINK } from './task'; +type TaskEntity = { + entityRef: { + name: string; + }; +}; + const GLOSSARY_NAME_VALIDATION_ERROR = 'Name size must be between 1 and 128'; export const descriptionBox = @@ -469,8 +475,47 @@ export const fillGlossaryTermDetails = async ( } }; -const validateGlossaryTermTask = async (page: Page, term: GlossaryTermData) => { +export const verifyTaskCreated = async ( + page: Page, + glossaryFqn: string, + glossaryTermData: GlossaryTermData +) => { + const { apiContext } = await getApiContext(page); + const entityLink = encodeURIComponent(`<#E::glossary::${glossaryFqn}>`); + + await expect + .poll( + async () => { + const response = await apiContext + .get( + `/api/v1/feed?entityLink=${entityLink}&type=Task&taskStatus=Open` + ) + .then((res) => res.json()); + + const arr = response.data.map( + (item: TaskEntity) => item.entityRef.name + ); + + return arr; + }, + { + // Custom expect message for reporting, optional. + message: 'To get the last run execution status as success', + timeout: 200_000, + intervals: [30_000], + } + ) + .toContain(glossaryTermData.name); +}; + +export const validateGlossaryTermTask = async ( + page: Page, + term: GlossaryTermData +) => { + const taskCountRes = page.waitForResponse('/api/v1/feed/count?*'); await page.click('[data-testid="activity_feed"]'); + await taskCountRes; + const taskFeeds = page.waitForResponse(TASK_OPEN_FETCH_LINK); await page .getByTestId('global-setting-left-panel') @@ -504,6 +549,37 @@ export const approveGlossaryTermTask = async ( await toastNotification(page, /Task resolved successfully/); }; +// Show the glossary term edit modal from glossary page tree. +// Update the description and verify the changes. +export const updateGlossaryTermDataFromTree = async ( + page: Page, + termFqn: string +) => { + // eslint-disable-next-line no-useless-escape + const escapedFqn = termFqn.replace(/\"/g, '\\"'); + const termRow = page.locator(`[data-row-key="${escapedFqn}"]`); + await termRow.getByTestId('edit-button').click(); + + await page.waitForSelector('[role="dialog"].edit-glossary-modal'); + + await expect( + page.locator('[role="dialog"].edit-glossary-modal') + ).toBeVisible(); + await expect(page.locator('.ant-modal-title')).toContainText( + 'Edit Glossary Term' + ); + + await page.locator(descriptionBox).fill('Updated description'); + + const glossaryTermResponse = page.waitForResponse('/api/v1/glossaryTerms/*'); + await page.getByTestId('save-glossary-term').click(); + await glossaryTermResponse; + + await expect( + termRow.getByRole('cell', { name: 'Updated description' }) + ).toBeVisible(); +}; + export const validateGlossaryTerm = async ( page: Page, term: GlossaryTermData, @@ -516,13 +592,6 @@ export const validateGlossaryTerm = async ( await expect(page.locator(termSelector)).toContainText(term.name); await expect(page.locator(statusSelector)).toContainText(status); - - if (status === 'Draft') { - // wait for 15 seconds as the flowable which creates task is triggered every 10 seconds - await page.waitForTimeout(15000); - await validateGlossaryTermTask(page, term); - await page.click('[data-testid="terms"]'); - } }; export const createGlossaryTerm = async ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx index 013b9e62d22a..470709b66484 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx @@ -270,7 +270,7 @@ const GlossaryDetails = ({ {getWidgetFromKeyInternal(widget)}
)); - }, [tagsWidget, layout]); + }, [tagsWidget, layout, permissions, termsLoading]); const detailsContent = useMemo(() => { return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx index 43865ecfd7d6..63954c347c6e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx @@ -114,6 +114,10 @@ const GlossaryTermTab = ({ const [isTableLoading, setIsTableLoading] = useState(false); const [isTableHovered, setIsTableHovered] = useState(false); const [expandedRowKeys, setExpandedRowKeys] = useState([]); + const listOfVisibleColumns = useMemo(() => { + return ['name', 'description', 'owners', 'status', 'new-term']; + }, []); + const [isStatusDropdownVisible, setIsStatusDropdownVisible] = useState(false); const statusOptions = useMemo( @@ -123,7 +127,7 @@ const GlossaryTermTab = ({ ); const [statusDropdownSelection, setStatusDropdownSelections] = useState< string[] - >(['Approved', 'Draft']); + >([Status.Approved, Status.Draft, Status.InReview]); const [selectedStatus, setSelectedStatus] = useState([ ...statusDropdownSelection, ]); @@ -296,11 +300,7 @@ const GlossaryTermTab = ({ } return data; - }, [glossaryTerms, permissions]); - - const listOfVisibleColumns = useMemo(() => { - return ['name', 'description', 'owners', 'status', 'new-term']; - }, []); + }, [permissions]); const defaultCheckedList = useMemo( () => @@ -353,7 +353,7 @@ const GlossaryTermTab = ({ return aIndex - bIndex; }), - [newColumns] + [options, newColumns] ); const handleColumnSelectionDropdownSave = useCallback(() => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx index fcbe01ad3926..87ac05380160 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx @@ -41,6 +41,7 @@ import { patchGlossaryTerm, } from '../../rest/glossaryAPI'; import { getEntityDeleteMessage } from '../../utils/CommonUtils'; +import { updateGlossaryTermByFqn } from '../../utils/GlossaryUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import { showErrorToast } from '../../utils/ToastUtils'; import { useActivityFeedProvider } from '../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; @@ -84,6 +85,7 @@ const GlossaryV1 = ({ const { getEntityPermission } = usePermissionProvider(); const [isLoading, setIsLoading] = useState(true); const [isTermsLoading, setIsTermsLoading] = useState(false); + const [isPermissionLoading, setIsPermissionLoading] = useState(false); const [isDelete, setIsDelete] = useState(false); @@ -97,7 +99,8 @@ const GlossaryV1 = ({ const [editMode, setEditMode] = useState(false); - const { activeGlossary, setGlossaryChildTerms } = useGlossaryStore(); + const { activeGlossary, glossaryChildTerms, setGlossaryChildTerms } = + useGlossaryStore(); const { id, fullyQualifiedName } = activeGlossary ?? {}; @@ -206,6 +209,17 @@ const GlossaryV1 = ({ setIsEditModalOpen(true); }; + const updateGlossaryTermInStore = (updatedTerm: GlossaryTerm) => { + const clonedTerms = cloneDeep(glossaryChildTerms); + const updatedGlossaryTerms = updateGlossaryTermByFqn( + clonedTerms, + updatedTerm.fullyQualifiedName ?? '', + updatedTerm as ModifiedGlossary + ); + + setGlossaryChildTerms(updatedGlossaryTerms); + }; + const updateGlossaryTerm = async ( currentData: GlossaryTerm, updatedData: GlossaryTerm @@ -217,8 +231,10 @@ const GlossaryV1 = ({ throw t('server.entity-updating-error', { entity: t('label.glossary-term'), }); + } else { + updateGlossaryTermInStore(response); + setIsEditModalOpen(false); } - onTermModalSuccess(); } catch (error) { if ( (error as AxiosError).response?.status === HTTP_STATUS_CODE.CONFLICT @@ -336,18 +352,29 @@ const GlossaryV1 = ({ shouldRefreshTerms && loadGlossaryTerms(true); }; - useEffect(() => { - if (id && !action) { - loadGlossaryTerms(); - if (isGlossaryActive) { - isVersionsView + const initPermissions = async () => { + setIsPermissionLoading(true); + const permissionFetch = isGlossaryActive + ? fetchGlossaryPermission + : fetchGlossaryTermPermission; + + try { + if (isVersionsView) { + isGlossaryActive ? setGlossaryPermission(VERSION_VIEW_GLOSSARY_PERMISSION) - : fetchGlossaryPermission(); + : setGlossaryTermPermission(VERSION_VIEW_GLOSSARY_PERMISSION); } else { - isVersionsView - ? setGlossaryTermPermission(VERSION_VIEW_GLOSSARY_PERMISSION) - : fetchGlossaryTermPermission(); + await permissionFetch(); } + } finally { + setIsPermissionLoading(false); + } + }; + + useEffect(() => { + if (id && !action) { + loadGlossaryTerms(); + initPermissions(); } }, [id, isGlossaryActive, isVersionsView, action]); @@ -355,8 +382,9 @@ const GlossaryV1 = ({ ) : ( <> - {isLoading && } + {(isLoading || isPermissionLoading) && } {!isLoading && + !isPermissionLoading && !isEmpty(selectedData) && (isGlossaryActive ? ( { return breadcrumbList; }; +export const updateGlossaryTermByFqn = ( + glossaryTerms: ModifiedGlossary[], + fqn: string, + newValue: ModifiedGlossary +): ModifiedGlossary[] => { + return glossaryTerms.map((term) => { + if (term.fullyQualifiedName === fqn) { + return newValue; + } + if (term.children) { + return { + ...term, + children: updateGlossaryTermByFqn( + term.children as ModifiedGlossary[], + fqn, + newValue + ), + }; + } + + return term; + }) as ModifiedGlossary[]; +}; + // This function finds and gives you the glossary term you're looking for. // You can then use this term or update its information in the Glossary or Term with it's reference created // Reference will only be created if withReference is true